views.py 15.7 KB
Newer Older
1
import difflib
2
import os
3
import re
4
import shutil
5 6 7
import subprocess
import tempfile
from pathlib import Path
8
from xml.dom.minidom import parse
9

10
from django.conf import settings
11
from django.contrib import messages
Claude Paroz's avatar
Claude Paroz committed
12
from django.http import HttpResponseRedirect, Http404
13
from django.shortcuts import render, get_object_or_404
14
from django.urls import reverse
15 16
from django.utils.html import escape
from django.utils.safestring import mark_safe
17 18
from django.utils.translation import gettext as _
from django.views.generic import View
19

20 21 22
from stats.models import (
    Statistics, FakeLangStatistics, Module, ModuleLock, Branch, Domain, Language,
)
23
from stats.utils import DocFormat, UndetectableDocFormat, check_po_quality, is_po_reduced
24
from vertimus.models import State, Action, ActionArchived, SendMailFailed
25
from vertimus.forms import ActionForm
26

27

28
def vertimus_by_stats_id(request, stats_id, lang_id):
29 30
    """Access to Vertimus view by a Statistics ID"""
    stats = get_object_or_404(Statistics, pk=stats_id)
31 32
    lang = get_object_or_404(Language, pk=lang_id)
    return vertimus(request, stats.branch, stats.domain, lang, stats)
33

34

35 36 37 38 39 40 41
def vertimus_by_ids(request, branch_id, domain_id, language_id):
    """Access to Vertimus view by Branch, Domain and language IDs"""
    branch = get_object_or_404(Branch, pk=branch_id)
    domain = get_object_or_404(Domain, pk=domain_id)
    language = get_object_or_404(Language, pk=language_id)
    return vertimus(request, branch, domain, language)

42

43 44 45
def vertimus_by_names(request, module_name, branch_name,
                      domain_name, locale_name, level="0"):
    """Access to Vertimus view by Branch, Domain and Language names"""
46
    module = get_object_or_404(Module, name=module_name)
47 48 49
    branch = get_object_or_404(
        Branch.objects.select_related('module'), name=branch_name, module__id=module.id
    )
50 51 52 53
    try:
        domain = branch.get_domains()[domain_name]
    except KeyError:
        raise Http404
54
    language = get_object_or_404(Language, locale=locale_name)
55
    return vertimus(request, branch, domain, language, level=level)
56

57

58 59 60 61 62
def vertimus(request, branch, domain, language, stats=None, level="0"):
    """The Vertimus view and form management. Level argument is used to
       access to the previous action history, first level (1) is the
       grandparent, second (2) is the parent of the grandparent, etc."""
    level = int(level)
63

64
    pot_stats, stats, state = get_vertimus_state(branch, domain, language, stats=stats)
65 66 67
    # Filtering on domain.name instead of domain because we can have several domains
    # working on the same set of strings (e.g. when an extraction method changed,
    # each extraction is mapped to a different domain with branch_from/branch_to delimitations)
68
    other_branch_states = State.objects.filter(
69 70
        branch__module=branch.module, domain__name=domain.name, language=language
    ).exclude(branch=branch.pk).exclude(name='None')
71

72 73
    if level == 0:
        # Current actions
74
        action_history = Action.get_action_history(state=state)
75 76
    else:
        sequence = state.get_action_sequence_from_level(level)
77
        action_history = ActionArchived.get_action_history(sequence=sequence)
78

79 80 81 82 83
    # Get the sequence of the grandparent to know if exists a previous action
    # history
    sequence_grandparent = state.get_action_sequence_from_level(level + 1)
    grandparent_level = level + 1 if sequence_grandparent else None

84
    action_form = None
85
    if request.user.is_authenticated and level == 0:
86 87
        # Only authenticated user can act on the translation and it's not
        # possible to edit an archived workflow
88
        person = request.user.person
89

90
        available_actions = state.get_available_actions(person)
91
        has_ml = language.team and bool(language.team.mailing_list) or False
92
        if request.method == 'POST':
93
            action_form = ActionForm(
94 95
                request.user, state, available_actions, has_ml, request.POST, request.FILES
            )
96 97 98 99

            if action_form.is_valid():
                # Process the data in form.cleaned_data
                action = action_form.cleaned_data['action']
100

101
                action = Action.new_by_name(action, person=person,
102
                    file=request.FILES.get('file', None))
103
                try:
104
                    msg = action.apply_on(state, action_form.cleaned_data)
105
                except SendMailFailed:
Claude Paroz's avatar
Claude Paroz committed
106
                    messages.error(request,
107
                        _("A problem occurred while sending mail, no mail have been sent"))
Claude Paroz's avatar
Claude Paroz committed
108 109 110
                except Exception as e:
                    messages.error(request,
                        _("An error occurred during applying your action: %s") % e)
111 112 113
                else:
                    if msg:
                        messages.success(request, msg)
114 115

                return HttpResponseRedirect(
116
                    reverse('vertimus_by_names',
117 118
                        args=(branch.module.name, branch.name, domain.name,
                              language.locale)))
119
        elif available_actions:
120
            action_form = ActionForm(request.user, state, available_actions, has_ml)
121 122 123 124

    context = {
        'pageSection': 'module',
        'stats': stats,
125
        'pot_stats': pot_stats,
126
        'po_url': stats.po_url(),
127
        'po_url_reduced': stats.has_reducedstat() and stats.po_url(reduced=True) or '',
128
        'branch': branch,
129
        'other_states': other_branch_states,
130 131 132
        'domain': domain,
        'language': language,
        'module': branch.module,
133
        'non_standard_repo_msg': _(settings.VCS_HOME_WARNING),
134
        'state': state,
135
        'action_history': action_history,
136 137 138
        'action_form': action_form,
        'level': level,
        'grandparent_level': grandparent_level,
139
    }
140 141 142
    if stats.has_figures():
        context['fig_stats'] = stats.fig_stats()
        del context['fig_stats']['prc']
143
    return render(request, 'vertimus/vertimus_detail.html', context)
144

145

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
def get_vertimus_state(branch, domain, language, stats=None):
    pot_stats = get_object_or_404(Statistics, branch=branch, domain=domain, language=None)
    if not stats:
        try:
            stats = Statistics.objects.get(branch=branch, domain=domain, language=language)
        except Statistics.DoesNotExist:
            stats = FakeLangStatistics(pot_stats, language)

    # Get the state of the translation
    try:
        state = State.objects.get(branch=branch, domain=domain, language=language)
    except State.DoesNotExist:
        # No need to save the state at this stage
        state = State(branch=branch, domain=domain, language=language)
    return pot_stats, stats, state


163
def vertimus_diff(request, action_id_1, action_id_2, level):
164
    """Show a diff between current action po file and previous file"""
Claude Paroz's avatar
Claude Paroz committed
165
    if int(level) != 0:
166
        ActionReal = ActionArchived
Claude Paroz's avatar
Claude Paroz committed
167
    else:
168 169
        ActionReal = Action
    action_1 = get_object_or_404(ActionReal, pk=action_id_1)
170
    state = action_1.state_db
171

172
    file_path_1 = action_1.most_uptodate_file.path
173
    reduced = is_po_reduced(file_path_1)
174 175
    if not os.path.exists(file_path_1):
        raise Http404("File not found")
176

177 178 179 180 181 182
    descr_1 = _('<a href="%(url)s">Uploaded file</a> by %(name)s on %(date)s') % {
        'url': action_1.most_uptodate_file.url,
        'name': action_1.person.name,
        'date': action_1.created
    }

183
    if action_id_2 not in (None, 0):
184
        # 1) id_2 specified in URL
185
        action_2 = get_object_or_404(ActionReal, pk=action_id_2)
186 187 188 189 190 191
        file_path_2 = action_2.most_uptodate_file.path
        descr_2 = _('<a href="%(url)s">Uploaded file</a> by %(name)s on %(date)s') % {
            'url': action_2.most_uptodate_file.url,
            'name': action_2.person.name,
            'date': action_2.created,
        }
192
    else:
193 194
        action_2 = None
        if action_id_2 is None:
195
            # 2) Search previous in action history
196 197 198
            action_2 = action_1.get_previous_action_with_po()

        if action_2:
199 200 201 202 203 204
            file_path_2 = action_2.most_uptodate_file.path
            descr_2 = _('<a href="%(url)s">Uploaded file</a> by %(name)s on %(date)s') % {
                'url': action_2.most_uptodate_file.url,
                'name': action_2.person.name,
                'date': action_2.created,
            }
205 206 207 208
        else:
             # 3) Lastly, the file should be the more recently committed file (merged)
            try:
                stats = Statistics.objects.get(branch=state.branch, domain=state.domain, language=state.language)
209 210 211 212
                descr_2 = _('<a href="%(url)s">Latest committed file</a> for %(lang)s') % {
                    'url': stats.po_url(),
                    'lang': state.language.get_name(),
                }
213 214
            except Statistics.DoesNotExist:
                stats = get_object_or_404(Statistics, branch=state.branch, domain=state.domain, language=None)
215 216 217 218
                descr_2 = '<a href="%(url)s">%(text)s</a>' % {
                    'url': stats.pot_url(),
                    'text': _("Latest POT file"),
                }
219
            file_path_2 = stats.po_path(reduced=reduced)
220 221
    if not os.path.exists(file_path_2):
        raise Http404("File not found")
222

223
    d = difflib.HtmlDiff(wrapcolumn=80)
Claude Paroz's avatar
Claude Paroz committed
224 225
    with open(file_path_1, encoding='utf-8', errors='replace') as fh1, \
            open(file_path_2, encoding='utf-8', errors='replace') as fh2:
226 227 228
        diff_content = d.make_table(
            fh2.readlines(), fh1.readlines(),
             descr_2, descr_1, context=True)
229 230 231 232 233

    context = {
        'diff_content': diff_content,
        'state': state,
    }
234
    return render(request, 'vertimus/vertimus_diff.html', context)
235

236

237 238 239 240 241
def latest_uploaded_po(request, module_name, branch_name, domain_name, locale_name):
    """ Redirect to the latest uploaded po for a module/branch/language """
    branch = get_object_or_404(Branch, module__name=module_name, name=branch_name)
    domain = get_object_or_404(Domain, module__name=module_name, name=domain_name)
    lang   = get_object_or_404(Language, locale=locale_name)
242 243 244 245
    latest_upload = Action.objects.filter(state_db__branch=branch,
                                          state_db__domain=domain,
                                          state_db__language=lang,
                                          file__endswith=".po").order_by('-created')[:1]
246 247
    if not latest_upload:
        raise Http404
248
    return HttpResponseRedirect(latest_upload[0].merged_file.url)
249

250

251 252
def activity_by_language(request, locale):
    language = get_object_or_404(Language, locale=locale)
253
    states = State.objects.filter(language=language).exclude(name='None')
254 255 256 257 258 259
    context = {
        'pageSection': "languages",
        'language':    language,
        'activities':  states,
    }
    return render(request, 'vertimus/activity_summary.html', context)
260 261


262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
class PoFileActionBase(View):
    def get(self, request, *args, **kwargs):
        self.pofile = self.get_po_file()
        context = self.get_context_data(**kwargs)
        return render(request, self.template_name, context)

    def get_po_file(self):
        pofile = None
        if self.kwargs.get('action_pk'):
            self.action = get_object_or_404(Action, pk=self.kwargs['action_pk'])
            if self.action.has_po_file():
                pofile = self.action.most_uptodate_file.path
        elif self.kwargs.get('stats_pk'):
            stats = get_object_or_404(Statistics, pk=self.kwargs['stats_pk'])
            pofile = stats.po_path()
277
        else:
278 279 280 281 282 283 284 285 286 287 288
            raise Http404("action_pk and stats_pk are both None")
        return pofile


class QualityCheckView(PoFileActionBase):
    template_name = 'vertimus/quality-check.html'

    def get_context_data(self, **kwargs):
        context = {'base': 'base_modal.html'}
        if self.pofile is None:
            context['results'] = _('No po file to check')
289
        else:
290 291 292 293 294 295 296 297 298 299 300 301
            context['checks'] = ['xmltags']
            results = check_po_quality(self.pofile, context['checks'])
            if results:
                context['results'] = mark_safe(re.sub(
                    r'^(# \(pofilter\) .*)', r'<span class="highlight">\1</span>', escape(results), flags=re.M
                ))
            else:
                context['results'] = _('The po file looks good!')
        return context


class BuildTranslatedDocsView(PoFileActionBase):
302 303
    http_method_names = ['post']

304
    def post(self, request, *args, **kwargs):
305 306
        pofile = self.get_po_file()
        if pofile is None:
307 308
            raise Http404('No target po file for this action')

309 310
        html_dir = Path(settings.SCRATCHDIR, 'HTML', str(self.kwargs['action_pk']))
        if html_dir.exists():
311 312 313 314
            # If the build already ran, redirect to the static results
            return HttpResponseRedirect(self.action.build_url)

        state = self.action.state_db
315 316
        with ModuleLock(state.branch.module):
            state.branch.checkout()
317
            error_message = self.build_docs(state, pofile, html_dir)
318 319 320 321 322 323

        if error_message:
            messages.error(request, error_message)
            return HttpResponseRedirect(state.get_absolute_url())
        return HttpResponseRedirect(self.action.build_url)

324
    def build_docs(self, state, po_file, html_dir):
325 326 327 328
        """
        Try building translated docs, return an error message or an empty string
        on success.
        """
329 330
        try:
            doc_format = DocFormat(state.domain, state.branch)
331
        except UndetectableDocFormat as err:
332 333
            return str(err)

334 335 336 337
        build_error = _('Build failed (%(program)s): %(err)s')
        with tempfile.NamedTemporaryFile(suffix='.gmo') as gmo, \
                tempfile.TemporaryDirectory() as build_dir:
            result = subprocess.run([
338
                'msgfmt', po_file, '-o', os.path.join(gmo.name)
339 340
            ], stderr=subprocess.PIPE)
            if result.returncode != 0:
341
                return build_error % {
342
                    'program': 'msgfmt', 'err': result.stderr.decode()
343
                }
344 345 346

            sources = doc_format.source_files()
            result = subprocess.run([
347 348
                'itstool', '-m', gmo.name, '-l', state.language.locale,
                '-o', str(build_dir), '--strict',
349 350 351
                *[str(s) for s in sources],
            ], cwd=str(doc_format.vcs_path), stderr=subprocess.PIPE)
            if result.returncode != 0:
352
                return build_error % {
353
                    'program': 'itstool', 'err': result.stderr.decode()
354
                }
355 356

            # Now build the html version
357 358
            if not html_dir.exists():
                html_dir.mkdir(parents=True)
359 360 361 362 363
            if doc_format.format == 'mallard':
                # With mallard, specifying the directory is enough.
                build_ref = [str(build_dir)]
            else:
                build_ref = [os.path.join(build_dir, s.name) for s in sources]
364
            cmd = [
365
                'yelp-build', 'html', '-o', str(html_dir),
366
                '-p', str(doc_format.vcs_path / 'C'),
367
                *build_ref
368 369
            ]
            result = subprocess.run(cmd, cwd=str(build_dir), stderr=subprocess.PIPE)
370 371
            index_html = html_dir / 'index.html'
            if result.returncode != 0 or (not index_html.exists() and len(result.stderr)):
372
                shutil.rmtree(str(html_dir))
373
                return build_error % {
374
                    'program': 'yelp-build', 'err': result.stderr.decode()
375
                }
376

377
            if not index_html.exists() and os.path.isfile(build_ref[0]):
378 379 380 381 382 383 384 385
                # Create an index.html symlink to the base html doc if needed
                try:
                    doc = parse(build_ref[0])
                    base_name = doc.getElementsByTagName("article")[0].attributes.get('id').value
                except (AttributeError, IndexError):
                    pass
                else:
                    html_name = '%s.html' % base_name
386
                    (html_dir / 'index.html').symlink_to(html_dir / html_name)
387
        return ''