[damned-lies] Add ability to build Mallard documentation with uploaded help po files



commit 881ee06fe7a191ddc2002ef23f67fb9f0746a0f9
Author: Claude Paroz <claude 2xlibre net>
Date:   Fri Sep 14 18:11:26 2018 +0200

    Add ability to build Mallard documentation with uploaded help po files

 damnedlies/urls.py                      |   5 ++
 stats/models.py                         |   3 +
 templates/vertimus/vertimus_detail.html |   7 +-
 vertimus/models.py                      |  27 ++++++-
 vertimus/tests/gnome-hello.help.fr.po   | 134 ++++++++++++++++++++++++++++++++
 vertimus/tests/tests.py                 |  21 +++++
 vertimus/urls.py                        |  11 ++-
 vertimus/views.py                       | 123 +++++++++++++++++++++++------
 8 files changed, 300 insertions(+), 31 deletions(-)
---
diff --git a/damnedlies/urls.py b/damnedlies/urls.py
index 5668db97..2bf30741 100644
--- a/damnedlies/urls.py
+++ b/damnedlies/urls.py
@@ -1,3 +1,5 @@
+import os
+
 from django.conf import settings
 from django.contrib import admin
 from django.contrib.auth import views as auth_views
@@ -121,4 +123,7 @@ if settings.STATIC_SERVE:
         path('POT/<path:path>',
              serve,
              kwargs={'document_root': settings.POTDIR}),
+        path('HTML/<path:path>',
+             serve,
+             kwargs={'document_root': os.path.join(settings.SCRATCHDIR, 'HTML')}),
     ]
diff --git a/stats/models.py b/stats/models.py
index 0b0987d5..9e79271d 100644
--- a/stats/models.py
+++ b/stats/models.py
@@ -785,6 +785,9 @@ class Domain(models.Model):
         return (self.dtype == 'ui' and self.layout == 'po/{lang}.po') or (
                 self.dtype == 'doc' and self.layout == 'help/{lang}/{lang}.po')
 
+    def can_build_docs(self, branch):
+        return self.dtype == 'doc' and self.doc_format(branch).format == 'mallard'
+
     def get_po_path(self, locale):
         """
         Return the relative filesystem path to the po file, existing or not.
diff --git a/templates/vertimus/vertimus_detail.html b/templates/vertimus/vertimus_detail.html
index e9032ba7..b82ff3db 100644
--- a/templates/vertimus/vertimus_detail.html
+++ b/templates/vertimus/vertimus_detail.html
@@ -224,7 +224,12 @@ $(document).ready(function() {
             </a>
                        <span style="margin-left: 5px;">{{ action.merged_file|num_stats:'short' }}</span><br/>
           {% endif %}
-          <div class="right">{% trans "diff with:" %}
+          {% if action.can_build %}
+            {% if action.build_url %}<a href="{{ action.build_url }}">{% trans "Help index" %}</a>
+            {% else %}<form method="post" action="{% url 'action-build-help' action.pk %}">{% csrf_token 
%}<button>{% trans "Build help" %}</button></form>
+            {% endif %}
+          {% endif %}
+          <div style="text-align: right">{% trans "diff with:" %}
             {% for f in files %}
                 <a href="{% url 'vertimus_diff' action.id f.action_id level %}" title="{{ f.title }}">[{{ 
forloop.revcounter }}]</a>
             {% endfor %}
diff --git a/vertimus/models.py b/vertimus/models.py
index 1724dd16..df12519b 100644
--- a/vertimus/models.py
+++ b/vertimus/models.py
@@ -1,6 +1,7 @@
 import os, sys
 import shutil
 from datetime import datetime, timedelta
+from pathlib import Path
 
 from django.conf import settings
 from django.db import models
@@ -64,7 +65,7 @@ class State(models.Model):
             self.branch.name, self.language.name, self.domain.name)
 
     def get_absolute_url(self):
-        return reverse('vertimus_by_ids', args=[self.branch.id, self.domain.id, self.language.id])
+        return reverse('vertimus_by_ids', args=[self.branch_id, self.domain_id, self.language_id])
 
     @property
     def stats(self):
@@ -368,6 +369,20 @@ class ActionAbstract(models.Model):
     def most_uptodate_file(self):
         return self.merged_file if self.merged_file else self.file
 
+    @property
+    def can_build(self):
+        return (
+            not isinstance(self, ActionArchived) and
+            self.state_db.domain.can_build_docs(self.state_db.branch)
+        )
+
+    @property
+    def build_url(self):
+        path = Path(
+            settings.SCRATCHDIR, 'HTML', str(self.pk), 'index.html'
+        )
+        return '/' + str(path.relative_to(settings.SCRATCHDIR)) if path.exists() else None
+
     def get_filename(self):
         if self.file:
             return os.path.basename(self.file.name)
@@ -792,8 +807,10 @@ def merge_uploaded_file(sender, instance, **kwargs):
 @receiver(pre_delete)
 def delete_action_files(sender, instance, **kwargs):
     """
-    pre_delete callback for Action that deletes the file + the merged file from upload
-    directory.
+    pre_delete callback for Action that deletes:
+    - the uploaded file
+    - the merged file
+    - the html dir where docs are built
     """
     if not isinstance(instance, ActionAbstract) or not getattr(instance, 'file'):
         return
@@ -803,6 +820,10 @@ def delete_action_files(sender, instance, **kwargs):
                  os.remove(instance.merged_file.path)
     if os.access(instance.file.path, os.W_OK):
          os.remove(instance.file.path)
+    html_dir = Path(settings.SCRATCHDIR, 'HTML', str(instance.pk))
+    if html_dir.exists():
+        shutil.rmtree(str(html_dir))
+
 
 @receiver(pre_delete, sender=Statistics)
 def clean_dangling_states(sender, instance, **kwargs):
diff --git a/vertimus/tests/gnome-hello.help.fr.po b/vertimus/tests/gnome-hello.help.fr.po
new file mode 100644
index 00000000..07e03ca4
--- /dev/null
+++ b/vertimus/tests/gnome-hello.help.fr.po
@@ -0,0 +1,134 @@
+# French translation of gnome-hello documentation.
+# Copyright (C) 2008-2012 Free Software Foundation, Inc.
+# This file is distributed under the same license as the gnome-hello documentation package.
+msgid ""
+msgstr ""
+"Project-Id-Version: GNOME Hello 2.0\n"
+"POT-Creation-Date: 2012-12-27 21:29+0000\n"
+"PO-Revision-Date: 2013-02-26 16:33+0100\n"
+"Last-Translator:\n"
+"Language-Team: GNOME French Team <gnomefr traduc org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#. Put one translator per line, in the form NAME <EMAIL>, YEAR1, YEAR2
+msgctxt "_"
+msgid "translator-credits"
+msgstr ""
+
+#. This is a reference to an external file such as an image or video. When
+#. the file changes, the md5 hash will change to let you know you need to
+#. update your localized copy. The msgstr is not used at all. Set it to
+#. whatever you like once you have updated your copy of the file.
+#: C/index.page:7(media) C/index.page:26(media)
+msgctxt "_"
+msgid ""
+"external ref='figures/gnome-hello-logo.png' "
+"md5='1ae47b7a7c4fbeb1f6bb72c0cf18d389'"
+msgstr ""
+"external ref='figures/gnome-hello-logo.png' "
+"md5='1ae47b7a7c4fbeb1f6bb72c0cf18d389'"
+
+#: C/index.page:6(info/desc)
+msgid "Help for GNOME Hello."
+msgstr "Aide de GNOME Hello."
+
+#: C/index.page:20(license/p) C/compiling.page:15(license/p)
+#: C/what-is.page:15(license/p) C/what-is-like.page:16(license/p)
+#: C/what-can-do.page:15(license/p)
+msgid "Creative Commons Share Alike 3.0"
+msgstr "Creative Commons Partage des conditions initiales à l'identique 3.0"
+
+#: C/index.page:26(page/title)
+msgid ""
+"<media type=\"image\" src=\"figures/gnome-hello-logo.png\">GNOME Hello logo</"
+"media>GNOME Hello"
+msgstr ""
+"<media type=\"image\" src=\"figures/gnome-hello-logo.png\">Logo de GNOME "
+"Hello</media>GNOME Hello"
+
+#: C/index.page:28(section/title)
+msgid "Introduction"
+msgstr "Introduction"
+
+#: C/index.page:32(section/title)
+msgid "It's a small world"
+msgstr "C'est un petit monde"
+
+#: C/index.page:36(section/title)
+msgid "Advanced"
+msgstr "Avancé"
+
+#: C/compiling.page:7(info/desc)
+msgid ""
+"<link xref=\"get#compile\">Compile</link> and <link xref=\"get#run\">run</"
+"link> and <link xref=\"get#help\">more</link>."
+msgstr ""
+"<link xref=\"get#compile\">Compiler</link>, <link xref=\"get#run\">lancer</"
+"link> et <link xref=\"get#help\">plus encore</link>."
+
+#: C/compiling.page:21(page/title)
+msgid "Get <app>GNOME Hello</app>"
+msgstr "Obtenir <app>GNOME Hello</app>"
+
+#: C/compiling.page:24(section/title)
+msgid "Compile"
+msgstr "Compiler"
+
+#: C/compiling.page:25(section/p)
+msgid "To compile the app from source:"
+msgstr "Pour compiler l'application à partir des sources :"
+
+#: C/compiling.page:29(item/p)
+msgid "In a terminal, type:"
+msgstr "Dans un terminal, saisissez :"
+
+#: C/compiling.page:30(item/p)
+msgid "<cmd>git clone git://git.gnome.org/gnome-hello</cmd>"
+msgstr "<cmd>git clone git://git.gnome.org/gnome-hello</cmd>"
+
+#: C/compiling.page:31(item/p)
+msgid "<cmd>cd gnome-hello</cmd>"
+msgstr "<cmd>cd gnome-hello</cmd>"
+
+#: C/compiling.page:32(item/p)
+msgid "<cmd>./autogen.sh --prefix=`pwd`/install</cmd>"
+msgstr "<cmd>./autogen.sh --prefix=`pwd`/install</cmd>"
+
+#: C/compiling.page:33(item/p)
+msgid "<cmd>make</cmd>"
+msgstr "<cmd>make</cmd>"
+
+#: C/compiling.page:34(item/p)
+msgid "<cmd>make install</cmd>"
+msgstr "<cmd>make install</cmd>"
+
+#: C/compiling.page:39(section/title)
+msgid "Run"
+msgstr "Lancer"
+
+#: C/compiling.page:40(section/p)
+msgid "To run gnome-hello type:"
+msgstr "Pour lancer gnome-hello, saisissez :"
+
+#: C/compiling.page:43(section/p)
+msgid "<cmd>./src/gnome-hello</cmd>"
+msgstr "<cmd>./src/gnome-hello</cmd>"
+
+#: C/compiling.page:49(section/title)
+msgid "View most up-to-date help pages"
+msgstr "Afficher les pages d'aide les plus à jour"
+
+#: C/compiling.page:50(section/p)
+msgid ""
+"To run most recent help pages (and view your changes if you made any) type:"
+msgstr ""
+"Pour afficher les pages d'aide les plus récentes (et voir vos modifications "
+"si vous en avez faites), saisissez :"
+
+#: C/compiling.page:53(section/p)
+msgid "<cmd>yelp help/C/</cmd>"
+msgstr "<cmd>yelp help/C/</cmd>"
diff --git a/vertimus/tests/tests.py b/vertimus/tests/tests.py
index 9f8cfbb4..2587c684 100644
--- a/vertimus/tests/tests.py
+++ b/vertimus/tests/tests.py
@@ -740,6 +740,27 @@ class VertimusTest(TeamsAndRolesTests):
         response = self.client.get(reverse('stats-quality-check', args=[po_stat.pk]))
         self.assertContains(response, "The po file looks good!")
 
+    @test_scratchdir
+    def test_doc_building(self):
+        dom = Domain.objects.create(
+            module=self.m, name='help', description='User Guide', dtype='doc',
+            layout='help/{lang}/{lang}.po'
+        )
+        state = StateTranslating(branch=self.b, domain=dom, language=self.l, person=self.pt)
+        state.save()
+
+        file_path = Path(__file__).parent / "gnome-hello.help.fr.po"
+        with file_path.open() as test_file:
+            action = Action.new_by_name('UT', person=self.pt, file=File(test_file))
+            action.apply_on(state, {'send_to_ml': action.send_mail_to_ml, 'comment': "Done by translator."})
+        self.assertTrue(action.can_build)
+        self.assertIsNone(action.build_url)
+        response = self.client.post(reverse('action-build-help', args=[action.pk]))
+        self.assertRedirects(
+            response, '/HTML/%d/index.html' % action.pk, fetch_redirect_response=False
+        )
+        self.assertEqual(action.build_url, '/HTML/%d/index.html' % action.pk)
+
     def test_mysql(self):
         # Copied from test_action_undo() with minor changes
         state = StateNone(branch=self.b, domain=self.d, language=self.l)
diff --git a/vertimus/urls.py b/vertimus/urls.py
index f730e821..90c7c6ce 100644
--- a/vertimus/urls.py
+++ b/vertimus/urls.py
@@ -26,6 +26,13 @@ urlpatterns = [
     path('<locale:locale>/activity_summary/',
          views.activity_by_language,
          name='activity_by_language'),
-    path('action/<int:action_pk>/qcheck/', views.quality_check, name='action-quality-check'),
-    path('stats/<int:stats_pk>/qcheck/', views.quality_check, name='stats-quality-check'),
+    path('action/<int:action_pk>/qcheck/',
+         views.QualityCheckView.as_view(),
+         name='action-quality-check'),
+    path('stats/<int:stats_pk>/qcheck/',
+         views.QualityCheckView.as_view(),
+         name='stats-quality-check'),
+    path('action/<int:action_pk>/build_help/',
+         views.BuildTranslatedDocsView.as_view(),
+         name='action-build-help'),
 ]
diff --git a/vertimus/views.py b/vertimus/views.py
index 321ae304..ba2fe9e2 100644
--- a/vertimus/views.py
+++ b/vertimus/views.py
@@ -1,6 +1,9 @@
 import difflib
 import os
 import re
+import subprocess
+import tempfile
+from pathlib import Path
 
 from django.conf import settings
 from django.contrib import messages
@@ -9,10 +12,11 @@ from django.shortcuts import render, get_object_or_404
 from django.urls import reverse
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
-from django.utils.translation import ugettext as _
+from django.utils.translation import gettext as _
+from django.views.generic import View
 
 from stats.models import Statistics, FakeLangStatistics, Module, Branch, Domain, Language
-from stats.utils import check_po_quality, is_po_reduced
+from stats.utils import DocFormat, check_po_quality, is_po_reduced
 from vertimus.models import State, Action, ActionArchived, SendMailFailed
 from vertimus.forms import ActionForm
 
@@ -246,28 +250,97 @@ def activity_by_language(request, locale):
     return render(request, 'vertimus/activity_summary.html', context)
 
 
-def quality_check(request, action_pk=None, stats_pk=None):
-    context = {'base': 'base_modal.html'}
-    pofile = None
-    if action_pk:
-        action = get_object_or_404(Action, pk=action_pk)
-        if not action.has_po_file():
-            context['results'] = _('No po file to check')
+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()
         else:
-            pofile = action.most_uptodate_file.path
-    elif stats_pk:
-        stats = get_object_or_404(Statistics, pk=stats_pk)
-        pofile = stats.po_path()
-    else:
-        raise Http404("action_pk and stats_pk are both None")
-
-    if pofile:
-        context['checks'] = ['xmltags']
-        results = check_po_quality(pofile, context['checks'])
-        if results:
-            context['results'] = mark_safe(re.sub(
-                r'^(# \(pofilter\) .*)', r'<span class="highlight">\1</span>', escape(results), flags=re.M
-            ))
+            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')
         else:
-            context['results'] = _('The po file looks good!')
-    return render(request, 'vertimus/quality-check.html', context)
+            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):
+    def post(self, request, *args, **kwargs):
+        self.pofile = self.get_po_file()
+        if self.pofile is None:
+            raise Http404('No target po file for this action')
+
+        html_dir = Path(settings.SCRATCHDIR, 'HTML', str(self.kwargs['action_pk']))
+        if (html_dir / 'index.html').exists():
+            # If the build already ran, redirect to the static results
+            return HttpResponseRedirect(self.action.build_url)
+
+        state = self.action.state_db
+        try:
+            doc_format = DocFormat(state.domain, state.branch)
+        except Exception as err:
+            messages.error(request, err)
+            return HttpResponseRedirect(state.get_absolute_url())
+        build_error = _('Build failed (%(program)s): %(err)s')
+        with tempfile.NamedTemporaryFile(suffix='.gmo') as gmo, \
+                tempfile.TemporaryDirectory() as build_dir:
+            result = subprocess.run([
+                'msgfmt', self.pofile, '-o', os.path.join(gmo.name)
+            ], stderr=subprocess.PIPE)
+            if result.returncode != 0:
+                messages.error(request, build_error % {
+                    'program': 'msgfmt', 'err': result.stderr.decode()
+                })
+                return HttpResponseRedirect(state.get_absolute_url())
+
+            sources = doc_format.source_files()
+            result = subprocess.run([
+                'itstool', '-m', gmo.name, '-o', str(build_dir), '--strict',
+                *[str(s) for s in sources],
+            ], cwd=str(doc_format.vcs_path), stderr=subprocess.PIPE)
+            if result.returncode != 0:
+                messages.error(request, build_error % {
+                    'program': 'itstool', 'err': result.stderr.decode()
+                })
+                return HttpResponseRedirect(state.get_absolute_url())
+
+            # Now build the html version
+            if not html_dir.exists():
+                html_dir.mkdir(parents=True)
+            cmd = [
+                'yelp-build', 'html', '-o', str(html_dir),
+                '-p', str(doc_format.vcs_path / 'C'),
+                str(build_dir)
+            ]
+            result = subprocess.run(cmd, cwd=str(build_dir), stderr=subprocess.PIPE)
+            if result.returncode != 0:
+                messages.error(request, build_error % {
+                    'program': 'yelp-build', 'err': result.stderr.decode()
+                })
+                return HttpResponseRedirect(state.get_absolute_url())
+        return HttpResponseRedirect(self.action.build_url)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]