python-docx==0.8.11
Wikidata==0.7
-librarian==24.1
+librarian==24.5.8
## Django
Django==4.1.9
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)
{% load static %}
{% load documents %}
{% load alerts %}
-<!DOCTYPE html>
<html>
<head lang="{{ LANGUAGE_CODE }}">
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
{% 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 %}
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
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
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']
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,
})
# 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[:]
* 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;
}
}
}
+
+
+
+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;
+
+}
}
/* 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;
$.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();
<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">
</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>