[damned-lies] Add po file view with inlined diff for fuzzies



commit 66f2339d3810bf31949e6bf921aac1bfe4c78c91
Author: Claude Paroz <claude 2xlibre net>
Date:   Fri Aug 30 21:36:06 2019 +0200

    Add po file view with inlined diff for fuzzies

 common/static/img/diff.png              | Bin 0 -> 4858 bytes
 templates/vertimus/vertimus_detail.html |   7 ++-
 vertimus/urls.py                        |   3 +
 vertimus/views.py                       | 100 +++++++++++++++++++++++++++++++-
 4 files changed, 107 insertions(+), 3 deletions(-)
---
diff --git a/common/static/img/diff.png b/common/static/img/diff.png
new file mode 100644
index 00000000..1e5d1d7a
Binary files /dev/null and b/common/static/img/diff.png differ
diff --git a/templates/vertimus/vertimus_detail.html b/templates/vertimus/vertimus_detail.html
index ae3d3f80..c99050b9 100644
--- a/templates/vertimus/vertimus_detail.html
+++ b/templates/vertimus/vertimus_detail.html
@@ -8,7 +8,7 @@
 {% block extrahead %}
 <style type="text/css">
 tr.tr_author, tr.tr_sync_master { display: none; }
-img#qcheck-icon { width: 24px; height: 24px; margin: 0 0.7em; }
+img.icons { width: 24px; height: 24px; margin: 0 0.7em; }
 </style>
 <script type="text/javascript" src="{{ STATIC_URL }}js/autosize.min.js"></script>
 <script type="text/javascript">
@@ -83,10 +83,13 @@ $(document).ready(function() {
   </em></div>
   <div style="margin-top: 5px;">
     <a class="btn btn-action" href="{{ po_url }}" title="{% trans 'Download PO file' %}"><img src="{{ 
STATIC_URL }}img/download.png" alt="{% trans 'Download PO file' %}"></a>
+    {% if stats.fuzzy %}
+      <a href="{% url 'stats-msgiddiff' stats.pk %}" title="{% trans 'PO file with inline diffs for fuzzy 
strings' %}"><img class="icons" src="{{ STATIC_URL }}img/diff.png"></a>
+    {% endif %}
     {% if domain.dtype == 'doc' %}
       <a href="{% url 'stats-quality-check' stats.pk %}" data-target="#modal-container"
          role="button" data-toggle="modal" title="{% trans 'Quality checks' %}">
-         <img id="qcheck-icon" src="{{ STATIC_URL }}img/qa-icon.svg" alt="quality check icon"></a>
+         <img class="icons" src="{{ STATIC_URL }}img/qa-icon.svg" alt="quality check icon"></a>
     {% endif %}
     {% trans "PO file statistics:" %} <br>
        <div id="stats_po">
diff --git a/vertimus/urls.py b/vertimus/urls.py
index 90c7c6ce..f35c3224 100644
--- a/vertimus/urls.py
+++ b/vertimus/urls.py
@@ -32,6 +32,9 @@ urlpatterns = [
     path('stats/<int:stats_pk>/qcheck/',
          views.QualityCheckView.as_view(),
          name='stats-quality-check'),
+    path('stats/<int:stats_pk>/msgiddiff/',
+         views.MsgiddiffView.as_view(),
+         name='stats-msgiddiff'),
     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 502b0776..956db3f2 100644
--- a/vertimus/views.py
+++ b/vertimus/views.py
@@ -1,4 +1,5 @@
 import difflib
+import html
 import os
 import re
 import shutil
@@ -9,7 +10,7 @@ from xml.dom.minidom import parse
 
 from django.conf import settings
 from django.contrib import messages
-from django.http import HttpResponseRedirect, Http404
+from django.http import HttpResponseRedirect, Http404, StreamingHttpResponse
 from django.shortcuts import render, get_object_or_404
 from django.urls import reverse
 from django.utils.html import escape
@@ -298,6 +299,74 @@ class QualityCheckView(PoFileActionBase):
         return context
 
 
+class MsgiddiffView(PoFileActionBase):
+    HEADER = '''
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <style>
+            body { font-family: monospace; }
+            div.warning { font-size: 200%; padding: 3em; background-color: #ddd; }
+            div.diff { color: #444; background-color: #eee; padding: 4px; width: 78ch; line-height: 1.4; 
margin-left: 2.5em; }
+            div.nowrap { white-space: pre; }
+            span.noline { color: #aaa; }
+            del { color: red; background-color: #fed4d4; }
+            ins { color: green; background-color: #c8f5c8; }
+        </style>
+    </head>
+<body>
+''' + '<div class="warning">{}</div>'.format(
+    _(
+        "WARNING: This file is <b>NOT</b> suitable as a base for completing this translation. "
+        "It contains HTML markup to highlight differential parts of changed strings."
+    ))
+    FOOTER = '</body</html>'
+
+    def streamed_file(self, po_file):
+        def strip(line):
+            if line.startswith('#| '):
+                line = line[3:]
+            if line.startswith('msgid '):
+                line = line[6:]
+            return line.rstrip('\n').strip('"')
+
+        yield self.HEADER
+        prev_id = curr_id = None
+        no_wrap = False
+        with open(po_file, 'r') as fh:
+            for noline, line in enumerate(fh.readlines(), start=1):
+                if prev_id is not None:
+                    if line.startswith('#|'):
+                        prev_id.append(line)
+                        continue
+                    elif line.startswith('msgstr "'):
+                        # Compute and display
+                        sep = '\n' if no_wrap else ''
+                        yield (
+                            '<div class="diff%s">' % (' nowrap' if no_wrap else '') + diff_strings(
+                                html.escape(sep.join(strip(l) for l in prev_id)),
+                                html.escape(sep.join(strip(l) for _, l in curr_id))
+                            ) + '</div>'
+                        )
+                        for _noline, _line in curr_id:
+                            yield '<span class="noline">%d</span> ' % _noline + html.escape(_line) + '<br>'
+                        prev_id = None
+                    else:
+                        curr_id.append((noline, line))
+                        continue
+                if line.startswith('#, fuzzy'):
+                    prev_id = []
+                    curr_id = []
+                    no_wrap = 'no-wrap' in line
+                yield '<span class="noline">%d</span> ' % noline + html.escape(line) + '<br>'
+        yield self.FOOTER
+
+    def get(self, request, *args, **kwargs):
+        stats = get_object_or_404(Statistics, pk=self.kwargs['stats_pk'])
+        pofile = stats.po_path()
+        return StreamingHttpResponse(self.streamed_file(pofile))
+
+
 class BuildTranslatedDocsView(PoFileActionBase):
     http_method_names = ['post']
 
@@ -388,3 +457,32 @@ class BuildTranslatedDocsView(PoFileActionBase):
                     html_name = '%s.html' % base_name
                     (html_dir / 'index.html').symlink_to(html_dir / html_name)
         return ''
+
+
+def diff_strings(previous, current):
+    """
+    Compute a diff between two strings, with inline differences.
+    http://stackoverflow.com/questions/774316/python-difflib-highlighting-differences-inline
+    >>> diff_strings(old string, new string)
+    'lorem<ins> foo</ins> ipsum dolor <del>sit </del>amet'
+    """
+    if not previous or not current:
+        return ''
+    seqm = difflib.SequenceMatcher(
+        a=previous.replace('\r\n', '\n'),
+        b=current.replace('\r\n', '\n'),
+    )
+    output= []
+    for opcode, a0, a1, b0, b1 in seqm.get_opcodes():
+        if opcode == 'equal':
+            output.append(seqm.a[a0:a1])
+        elif opcode == 'insert':
+            output.append('<ins title="New text">' + seqm.b[b0:b1] + "</ins>")
+        elif opcode == 'delete':
+            output.append('<del title="Deleted text">' + seqm.a[a0:a1] + "</del>")
+        elif opcode == 'replace':
+            output.append('<del title="Deleted text">' + seqm.a[a0:a1] + "</del>")
+            output.append('<ins title="New text">' + seqm.b[b0:b1] + "</ins>")
+        else:
+            raise RuntimeError("unexpected opcode")
+    return mark_safe(''.join(output))


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