update master
authorRadek Czajka <rczajka@rczajka.pl>
Fri, 14 Mar 2025 08:04:29 +0000 (09:04 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Fri, 14 Mar 2025 08:04:29 +0000 (09:04 +0100)
requirements/requirements.txt
src/documents/models/chunk.py
src/documents/templates/documents/base.html
src/documents/templates/documents/synchro.html
src/documents/views.py
src/documents/xml_tools.py
src/redakcja/static/css/html.scss
src/redakcja/static/js/wiki/view_editor_wysiwyg.js
src/redakcja/static/js/wiki/wikiapi.js
src/wiki/templates/wiki/tabs/annotations_view.html
src/wlxml/templates/wlxml/wl2html.xsl

index 68dab26..f336baa 100644 (file)
@@ -10,7 +10,7 @@ python-slugify==8.0.1
 python-docx==0.8.11
 Wikidata==0.7
 
-librarian==24.1
+librarian==24.5.8
 
 ## Django
 Django==4.1.9
index 4d8f6d2..797fc17 100644 (file)
@@ -75,8 +75,10 @@ class Chunk(dvcs_models.Document):
 
     def split(self, slug, title='', **kwargs):
         """ Create an empty chunk after this one """
-        self.book.chunk_set.filter(number__gt=self.number).update(
-                number=models.F('number')+1)
+        # Single update makes unique constr choke on postgres.
+        for chunk in self.book.chunk_set.filter(number__gt=self.number).order_by('-number'):
+            chunk.number += 1
+            chunk.save()
         new_chunk = None
         while not new_chunk:
             new_slug = self.book.make_chunk_slug(slug)
index 53ed63a..9d7e3f7 100644 (file)
@@ -3,7 +3,6 @@
 {% load static %}
 {% load documents %}
 {% load alerts %}
-<!DOCTYPE html>
 <html>
 <head lang="{{ LANGUAGE_CODE }}">
     <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
index bc44673..346c33c 100644 (file)
 
 {% block content %}
 
-  <div class="row mb-4">
-    <div class="col-4">
-    </div>
-    <div class="col-8">
-      {% if not length_ok %}
-        <div class="alert alert-warning">
-          Liczba znalezionych części dokumentu nie zgadza się z liczbą części audiobooka.
-          Konieczna jest korekta za pomocą atrybutów <code>forcesplit</code> i <code>nosplit</code>.
-        </div>
-      {% else %}
-        <form method="post" action="">
-          {% csrf_token %}
-          <button class="btn btn-primary">
-            Zaplanuj synchronizację
-          </button>
-        </form>
-      {% endif %}
-
-    </div>
-  </div>
-
-  <table class="table">
+<h1>Wskazówki synchronizacji dla: <a href="{{ book.get_absolute_url }}">{{ book.title }}</a></h1>
+
+  
+  <table class="table" id="sync-table">
     <thead>
       <tr>
+        <th></th>
         <th>Nagłówek cięcia</th>
         <th>Audiobook</th>
       </tr>
     </thead>
-    {% for h, m in table %}
-      <tr>
-        <td>
-          {% if h %}
-            <a target="_blank" href="{% url 'wiki_editor' book.slug %}#CodeMirrorPerspective">
-              {{ h.0 }} (linia {{ h.2 }})
-            </a>
-          {% else %}
-            —
-          {% endif %}
-        </td>
-        <td>{{ m|default_if_none:'—' }}</td>
-      </tr>
-    {% endfor %}
+    <tbody>
+    </tbody>
   </table>
+
+
+<form method="post" action="">
+  {% csrf_token %}
+  <input type="hidden" name="hints" id="hints">
+  <button class="btn btn-primary">
+    Zapisz wskazówki
+  </button>
+</form>
+
+{% endblock %}
+
+
+{% block add_js %}
+<script type="text/javascript">
+  $(function() {
+  
+      let hints = {{ hints|safe }};
+      let mp3 = {{ mp3|safe }};
+      let headers = {{ headers|safe }};
+      let headers_other = {{ headers_other|safe }};
+
+      let table = $("#sync-table tbody");
+
+      function showTable() {
+         $("#hints").val(JSON.stringify(hints));
+         
+         let mI = 0;
+         let hI = 0;
+         let row = 0;
+         let hint;
+         table.empty();
+         while (mI < mp3.length || hI < headers.length) {
+             let tr = $("<tr>");
+
+             if (row < hints.length) {
+                 hint = hints[row];
+             } else {
+                 hint = [];
+             }
+             tr.data('hint', hint);
+
+             let td = $("<td>");
+             if (headers_other.length) {
+                 td.append(
+                     $('<button class="btn btn-primary mr-1">').text('+').click(function() {
+                         hintAdd(tr);
+                     })
+                 );
+             }
+             td.append(
+                 $('<button class="btn btn-primary">').text('-').click(function() {
+                     hintRm(tr);
+                 })
+             );
+             td.appendTo(tr);
+             if (hint[0] == '+') {
+                 tr.addClass('hit-add');
+                 // select?
+                 let sel = $("<select>");
+                 sel.append($("<option>").text('wybierz'));
+                 $.each(headers_other, (i, e) => {
+                     let opt = $("<option>").text(
+                         e[1] + ' (' + e[0] + ')'
+                     ).val(i);
+                     if (i == hint[1]) {
+                         opt.attr('selected', 'selected')
+                     }
+                     sel.append(opt)
+                 });
+                 sel.change(function() {
+                     tr.data('hint', ['+', $(this).val()]);
+                     refreshTable();
+                 });
+
+                 $("<td>").append(sel).appendTo(tr);
+             } else {
+                 let td = $("<td>").text(headers[hI][1]).appendTo(tr);
+                 if (hint[0] == '-') {
+                     tr.addClass('text-muted');
+                 }
+                 hI ++;
+             }
+
+             if (hint[0] == '-') {
+                 $("<td>").appendTo(tr);
+             } else {
+                 $("<td>").text(mp3[mI]).appendTo(tr);
+                 mI ++;
+             }
+             table.append(tr);
+             row ++;
+         }
+      }
+
+      showTable();
+
+      function refreshTable() {
+         hints = [];
+         $("tr", table).each((i, e) => {
+             hint = $(e).data('hint')
+             if (hint !== null) {
+                 hints.push(hint)
+             }
+         });
+         showTable();
+      }
+      
+      function hintAdd(tr) {
+         $("<tr>").data('hint', ['+']).insertBefore(tr);
+         refreshTable();
+      }
+      function hintRm(tr) {
+         let $tr = $(tr);
+         let hint = $tr.data('hint');
+         if (hint[0] == '+') {
+             $tr.data('hint', null)
+         } else if (hint[0] == '-') {
+             $tr.data('hint', [])
+         } else {
+             $tr.data('hint', ['-'])
+         }
+         refreshTable();
+      }
+  });
+</script>
 {% endblock %}
index 97d2ad6..e87dc6d 100644 (file)
@@ -5,6 +5,7 @@ from collections import defaultdict
 from copy import deepcopy
 from datetime import datetime, date, timedelta
 from itertools import zip_longest
+import json
 import logging
 import os
 from urllib.parse import quote_plus, unquote, urlsplit, urlunsplit
@@ -25,6 +26,7 @@ from django.utils.translation import gettext_lazy as _
 from django.views.decorators.http import require_POST
 from django_cas_ng.decorators import user_passes_test
 import requests
+from lxml import etree
 
 from librarian import epubcheck
 from librarian.html import raw_printable_text
@@ -770,14 +772,29 @@ def mark_final_completed(request):
     return render(request, 'documents/mark_final_completed.html')
 
 
+@login_required
 def synchro(request, slug):
     book = get_object_or_404(Book, slug=slug)
     if not book.accessible(request):
         return HttpResponseForbidden("Not authorized.")
 
-    document = book.wldocument(librarian2=True)
+    if request.method == 'POST':
+        #hints = json.loads(request.POST.get('hints'))
+        chunk = book[0]
+        tree = etree.fromstring(chunk.head.materialize())
+        m = tree.find('.//meta[@id="synchro"]')
+        if m is None:
+            rdf = tree.find('.//{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description')
+            m = etree.SubElement(rdf, 'meta', id="synchro")
+            m.tail = '\n'
+        m.text = request.POST.get('hints')
+        text = etree.tostring(tree, encoding='unicode')
+        chunk.commit(text, author=request.user, description='Synchronizacja')
+        return HttpResponseRedirect('')
+    
+    document = book.wldocument(librarian2=True, publishable=False)
+
     slug = document.meta.url.slug
-    print(f'https://audio.wolnelektury.pl/archive/book/{slug}.json')
     error = None
     try:
         items = requests.get(f'https://audio.wolnelektury.pl/archive/book/{slug}.json').json()['items']
@@ -792,59 +809,49 @@ def synchro(request, slug):
     split_on = (
         'naglowek_rozdzial',
         'naglowek_scena',
-        )
-    
-    if split_on:
-        documents = []
-        headers = [('Początek', 0, 0)]
-        present = True
-        n = 0
-        while present:
-            present = False
-            n += 1
-            newdoc = deepcopy(document)
-            newdoc.tree.getroot().document = newdoc
-            
-            master = newdoc.tree.getroot()[-1]
-            i = 0
-            for item in list(master):
-                #chunkno, sourceline = 0, self.sourceline
-                #if builder.splits:
-                #    chunkno, sourceline = len(builder.splits), sourceline - builder.splits[-1]
-
-                if 'forcesplit' in item.attrib or (item.tag in split_on and 'nosplit' not in item.attrib):
-                    # TODO: clear
-                    i += 1
-                    if n > 1 and i == n:
-                        headers.append((
-                            raw_printable_text(item),
-                            0,
-                            item.sourceline,
-                        ))
-                if i != n and not (n == 1 and not i):
-                    master.remove(item)
-                else:
-                    present = True
-                if present:
-                    documents.append(newdoc)
-    else:
-        documents = [document]
-        headers = [(
-            document.meta.title, 0 ,0
-        )]
-
-    length_ok = len(headers) == len(mp3)
-    table = zip_longest(headers, mp3)
-
+    )
+    split_other = (
+        'naglowek_czesc',
+        'naglowek_akt',
+        'naglowek_podrozdzial',
+        'srodtytul',
+    )
+
+    headers = []
+    headers_other = []
+    master = document.tree.getroot()[-1]
+    for item in master:
+        if item.tag in split_on:
+            headers.append([
+                item.tag,
+                raw_printable_text(item),
+                0,
+                item.sourceline,
+            ])
+        if item.tag in split_other:
+            headers_other.append([
+                item.tag,
+                raw_printable_text(item),
+                0,
+                item.sourceline,
+            ])
+
+    hints = []
+    m = document.tree.find('.//meta[@id="synchro"]')
+    if m is not None:
+        try:
+            hints = json.loads(m.text)
+        except:
+            raise
+            pass
     
     return render(request, 'documents/synchro.html', {
         'book': book,
-        'documents': documents,
         'headers': headers,
+        'headers_other': headers_other,
         'mp3': mp3,
-        'length_ok': length_ok,
-        'table': table,
         'error': error,
+        'hints': hints,
     })
 
 
index ac145db..d7e3fcf 100644 (file)
@@ -107,7 +107,7 @@ def split_xml(text):
 
         # find the chapter's title
         name_elem = deepcopy(element)
-        for tag in 'extra', 'motyw', 'pa', 'pe', 'pr', 'pt', 'uwaga':
+        for tag in 'extra', 'motyw', 'pa', 'pe', 'pr', 'pt', 'ptrad', 'uwaga':
             for a in name_elem.findall('.//' + tag):
                 a.text=''
                 del a[:]
index d9886d4..3819c82 100644 (file)
@@ -300,12 +300,19 @@ div[x-node] > .uwaga {
  * Przypisy w tekście
  */
  
-.htmlview .annotation-inline-box {
-    &:hover > span[x-annotation-box] {
-        display: block;
+.htmlview .annotation-inline-box,
+.htmlview .reference-inline-box {
+    &:hover {
+       > span[x-annotation-box],
+       > span[x-preview]
+       {
+            display: block;
+       }
     }
 
-    > span[x-annotation-box] {
+    > span[x-annotation-box],
+    > span[x-preview]
+    {
         display: none;
         width: 300px;
         font-size: 10pt;
@@ -549,3 +556,27 @@ div[x-node] > .uwaga {
         }
     }
 }
+
+
+
+div[x-node="numeracja"] {
+    background: lightblue;
+    margin: 2em;
+    padding: 2em;
+    border-radius: 1em;
+    &::before {
+       content: "Reset numeracji";
+    }
+}
+
+*[x-number]::before {
+    display: block;
+    content: attr(x-number);
+    position: absolute;
+    text-align: right;
+    width: 40px;
+    left: -60px;
+    font-size: .9em;
+    opacity: .8;
+    
+}
index e43905c..4a3cd6b 100644 (file)
@@ -56,7 +56,7 @@
     }
 
     /* Convert HTML fragment to plaintext */
-    var ANNOT_FORBIDDEN = ['pt', 'pa', 'pr', 'pe', 'begin', 'end', 'motyw'];
+    var ANNOT_FORBIDDEN = ['pt', 'pa', 'pr', 'pe', 'ptrad', 'begin', 'end', 'motyw'];
 
     function html2plainText(fragment){
         var text = "";
             element: source,
             stripOuter: true,
             success: function(text){
-                $('textarea', $overlay).val($.trim(text));
+               let ttext = $.trim(text);
+                $('textarea', $overlay).val(ttext);
 
                 setTimeout(function(){
                     $('textarea', $overlay).elastic().focus();
                         $('*[x-annotation-box]', editable).css({
                         }).show();
                     }
+                    if (editable.is('.reference-inline-box')) {
+                       let preview = $('*[x-preview]', editable);
+                       preview.show();
+                       let link = $("a", preview);
+                       let href = link.attr('href');
+                       if (link.attr('title') == '?' && href.startsWith('https://www.wikidata.org/wiki/')) {
+                           link.attr('title', '…');
+                           let qid = href.split('/').reverse()[0];
+                           $.ajax({
+                               url: 'https://www.wikidata.org/w/rest.php/wikibase/v1/entities/items/' + qid + '?_fields=labels',
+                               dataType: "json",
+                               success: function(data) {
+                                   link.attr(
+                                       'title',
+                                       data['labels']['pl'] || data['labels']['en']
+                                   );
+                               },
+                           });
+                       }
+                    }
                 });
 
                 self.caret = new Caret(element);
                     callback();
             }
 
+           let self = this;
             xml2html({
                 xml: this.doc.text,
                 base: this.doc.getBase(),
 
                     var htmlView = $('#html-view');
                     htmlView.html(element);
+                   self.renumber();
                     if ('PropertiesPerspective' in $.wiki.perspectives)
                         $.wiki.perspectives.PropertiesPerspective.enable();
 
                 }
             })
         }
+
+       renumber() {
+           let number = 0;
+            $('#html-view *').each((i, e) => {
+               let $e = $(e);
+               if ($e.closest('[x-node="abstrakt"]').length) return;
+               if ($e.closest('[x-node="nota_red"]').length) return;
+               if ($e.closest('[x-annotation-box="true"]').length) return;
+               let node = $e.attr('x-node');
+               if (node == 'numeracja') {
+                   number = 0;
+               } else if (['werset', 'akap', 'wers'].includes(node)) {
+                   number ++;
+                   $e.attr('x-number', number);
+               }
+           })
+       }
     }
 
     $.wiki.VisualPerspective = VisualPerspective;
index 73642ac..5675913 100644 (file)
             $.xmlns["rdf"] = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
             $('rdf|RDF', doc).remove();
             if (params.noFootnotes) {
-                $('pa, pe, pr, pt', doc).remove();
+                $('pa, pe, pr, pt, ptrad', doc).remove();
             }
            if (params.noThemes) {
                $('motyw', doc).remove();
index 6cfef19..1c6bcc0 100644 (file)
         <a href='#' class="nav-link refresh" title="Przypisy tłumacza" data-tag="pt">pt</a>
       </li>
       <li class="nav-item">
-        <a href='#' class="nav-link refresh" title="Wszystkie przypisy" data-tag="pa,pe,pr,pt">{% trans "all" %}</a>
+        <a href='#' class="nav-link refresh" title="Przypisy tradycyjne" data-tag="ptrad">ptrad</a>
+      </li>
+      <li class="nav-item">
+        <a href='#' class="nav-link refresh" title="Wszystkie przypisy" data-tag="pa,pe,pr,pt,ptrad">{% trans "all" %}</a>
       </li>
     </ul>
     <div id="annotations-container">
index c3af0ff..9716e19 100644 (file)
       </div>
     </xsl:template>
 
+    <xsl:template match="numeracja">
+      <div>
+        <xsl:call-template name="standard-attributes" />
+        <div>
+          <xsl:attribute name="data-start">
+            <xsl:value-of select="@start" />
+          </xsl:attribute>
+          <xsl:attribute name="data-link">
+            <xsl:value-of select="@link" />
+          </xsl:attribute>
+        </div>
+      </div>
+    </xsl:template>
+
     <!--
         ********
         STROFA
           <xsl:with-param name="extra-class" select="'reference-inline-box'" />
         </xsl:call-template>
         <a class="reference">📌</a>
+        <span x-preview="true" x-pass-thru="true">
+         <a target="wiki" title="?">
+           <xsl:attribute name="href">
+             <xsl:value-of select="@href" />
+           </xsl:attribute>
+           <xsl:value-of select="@href" />
+         </a>
+        </span>
       </span>
     </xsl:template>