Obey length limits for wikidata import.
[redakcja.git] / src / documents / views.py
1 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
3 #
4 from collections import defaultdict
5 from copy import deepcopy
6 from datetime import datetime, date, timedelta
7 from itertools import zip_longest
8 import logging
9 import os
10 from urllib.parse import quote_plus, unquote, urlsplit, urlunsplit
11
12 from django.conf import settings
13 from django.contrib import auth
14 from django.contrib.auth.models import User
15 from django.contrib.auth.decorators import login_required, permission_required
16 from django.urls import reverse
17 from django.db.models import Count, Q
18 from django.db import transaction
19 from django import http
20 from django.http import Http404, HttpResponse, HttpResponseForbidden
21 from django.http.response import HttpResponseRedirect
22 from django.shortcuts import get_object_or_404, render
23 from django.utils.encoding import iri_to_uri
24 from django.utils.translation import gettext_lazy as _
25 from django.views.decorators.http import require_POST
26 from django_cas_ng.decorators import user_passes_test
27 import requests
28
29 from librarian import epubcheck
30 from librarian.html import raw_printable_text
31
32 from apiclient import api_call, NotAuthorizedError
33 from . import forms
34 from . import helpers
35 from .helpers import active_tab
36 from .models import (Book, Chunk, Image, BookPublishRecord, 
37         ChunkPublishRecord, ImagePublishRecord, Project)
38 import catalogue.models
39 from fileupload.views import UploadView
40
41 #
42 # Quick hack around caching problems, TODO: use ETags
43 #
44 from django.views.decorators.cache import never_cache
45
46 logger = logging.getLogger("fnp.documents")
47
48
49 @active_tab('all')
50 @never_cache
51 def document_list(request):
52     return render(request, 'documents/document_list.html')
53
54
55 @active_tab('images')
56 @never_cache
57 def image_list(request, user=None):
58     return render(request, 'documents/image_list.html')
59
60
61 @never_cache
62 def user(request, username):
63     user = get_object_or_404(User, username=username)
64     return render(request, 'documents/user_page.html', {"viewed_user": user})
65
66
67 @login_required
68 @active_tab('my')
69 @never_cache
70 def my(request):
71     last_books = sorted(request.session.get("wiki_last_books", {}).items(),
72         key=lambda x: x[1]['time'], reverse=True)
73     for k, v in last_books:
74         v['time'] = datetime.fromtimestamp(v['time'])
75     try:
76         resp = api_call(request.user, 'username/')
77     except NotAuthorizedError:
78         wllogin = None
79     else:
80         wllogin = resp['username']
81
82     return render(request, 'documents/my_page.html', {
83         'last_books': last_books,
84         "logout_to": '/',
85         "wllogin": wllogin,
86         })
87
88
89 @active_tab('users')
90 def users(request):
91     return render(request, 'documents/user_list.html', {
92         'users': User.objects.all().annotate(count=Count('chunk')).order_by(
93             '-count', 'last_name', 'first_name'),
94     })
95
96
97 @active_tab('activity')
98 def activity(request, isodate=None):
99     today = date.today()
100     try:
101         day = helpers.parse_isodate(isodate)
102     except ValueError:
103         day = today
104
105     if day > today:
106         raise Http404
107     if day != today:
108         next_day = day + timedelta(1)
109     prev_day = day - timedelta(1)
110
111     return render(request, 'documents/activity.html', locals())
112
113
114 @never_cache
115 def logout_then_redirect(request):
116     auth.logout(request)
117     return http.HttpResponseRedirect(quote_plus(request.GET.get('next', '/'), safe='/?='))
118
119
120 @permission_required('documents.add_book')
121 @active_tab('create')
122 def create_missing(request, slug=None):
123     if slug is None:
124         slug = ''
125     slug = slug.replace(' ', '-')
126
127     if request.method == "POST":
128         form = forms.DocumentCreateForm(request.POST, request.FILES)
129         if form.is_valid():
130             
131             if request.user.is_authenticated:
132                 creator = request.user
133             else:
134                 creator = None
135             book = Book.create(
136                 text=form.cleaned_data['text'],
137                 creator=creator,
138                 slug=form.cleaned_data['slug'],
139                 title=form.cleaned_data['title'],
140                 gallery=form.cleaned_data['gallery'],
141             )
142
143             return http.HttpResponseRedirect(reverse("documents_book", args=[book.slug]))
144     else:
145         form = forms.DocumentCreateForm(initial={
146                 "slug": slug,
147                 "title": slug.replace('-', ' ').title(),
148                 "gallery": slug,
149         })
150
151     return render(request, "documents/document_create_missing.html", {
152         "slug": slug,
153         "form": form,
154
155         "logout_to": '/',
156     })
157
158
159 @permission_required('documents.add_book')
160 @active_tab('upload')
161 def upload(request):
162     if request.method == "POST":
163         form = forms.DocumentsUploadForm(request.POST, request.FILES)
164         if form.is_valid():
165             from slugify import slugify
166
167             if request.user.is_authenticated:
168                 creator = request.user
169             else:
170                 creator = None
171
172             zip = form.cleaned_data['zip']
173             skipped_list = []
174             ok_list = []
175             error_list = []
176             slugs = {}
177             existing = [book.slug for book in Book.objects.all()]
178             for filename in zip.namelist():
179                 if filename[-1] == '/':
180                     continue
181                 title = os.path.basename(filename)[:-4]
182                 slug = slugify(title)
183                 if not (slug and filename.endswith('.xml')):
184                     skipped_list.append(filename)
185                 elif slug in slugs:
186                     error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
187                 elif slug in existing:
188                     error_list.append((filename, slug, _('Slug already used in repository.')))
189                 else:
190                     try:
191                         zip.read(filename).decode('utf-8') # test read
192                         ok_list.append((filename, slug, title))
193                     except UnicodeDecodeError:
194                         error_list.append((filename, title, _('File should be UTF-8 encoded.')))
195                     slugs[slug] = filename
196
197             if not error_list:
198                 for filename, slug, title in ok_list:
199                     book = Book.create(
200                         text=zip.read(filename).decode('utf-8'),
201                         creator=creator,
202                         slug=slug,
203                         title=title,
204                     )
205
206             return render(request, "documents/document_upload.html", {
207                 "form": form,
208                 "ok_list": ok_list,
209                 "skipped_list": skipped_list,
210                 "error_list": error_list,
211
212                 "logout_to": '/',
213             })
214     else:
215         form = forms.DocumentsUploadForm()
216
217     return render(request, "documents/document_upload.html", {
218         "form": form,
219
220         "logout_to": '/',
221     })
222
223
224 def serve_xml(request, book, slug):
225     if not book.accessible(request):
226         return HttpResponseForbidden("Not authorized.")
227     xml = book.materialize(publishable=True)
228     response = http.HttpResponse(xml, content_type='application/xml')
229     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
230     return response
231
232
233 @never_cache
234 def book_xml(request, slug):
235     book = get_object_or_404(Book, slug=slug)
236     return serve_xml(request, book, slug)
237
238
239 @never_cache
240 def book_xml_dc(request, slug):
241     book = get_object_or_404(Book, dc_slug=slug)
242     return serve_xml(request, book, slug)
243
244
245 @never_cache
246 def book_txt(request, slug):
247     book = get_object_or_404(Book, slug=slug)
248     if not book.accessible(request):
249         return HttpResponseForbidden("Not authorized.")
250
251     doc = book.wldocument()
252     text = doc.as_text().get_bytes()
253     response = http.HttpResponse(text, content_type='text/plain')
254     response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
255     return response
256
257
258 @never_cache
259 def book_html(request, slug):
260     book = get_object_or_404(Book, slug=slug)
261     if not book.accessible(request):
262         return HttpResponseForbidden("Not authorized.")
263
264     doc = book.wldocument(parse_dublincore=False)
265     html = doc.as_html(options={'gallery': "'%s'" % book.gallery_url()})
266
267     html = html.get_bytes().decode('utf-8') if html is not None else ''
268     # response = http.HttpResponse(html, content_type='text/html')
269     # return response
270     # book_themes = {}
271     # for fragment in book.fragments.all().iterator():
272     #     for theme in fragment.tags.filter(category='theme').iterator():
273     #         book_themes.setdefault(theme, []).append(fragment)
274
275     # book_themes = book_themes.items()
276     # book_themes.sort(key=lambda s: s[0].sort_key)
277     return render(request, 'documents/book_text.html', locals())
278
279
280 @login_required
281 @never_cache
282 def book_pdf(request, slug, mobile=False):
283     book = get_object_or_404(Book, slug=slug)
284     if not book.accessible(request):
285         return HttpResponseForbidden("Not authorized.")
286
287     # TODO: move to celery
288     doc = book.wldocument()
289     # TODO: error handling
290     customizations = ['26pt', 'nothemes', 'nomargins', 'notoc'] if mobile else None
291     pdf_file = doc.as_pdf(cover=True, base_url=request.build_absolute_uri(book.gallery_path()), customizations=customizations)
292     from .ebook_utils import serve_file
293     return serve_file(pdf_file.get_filename(),
294                 book.slug + '.pdf', 'application/pdf')
295
296
297 @login_required
298 @never_cache
299 def book_epub(request, slug):
300     book = get_object_or_404(Book, slug=slug)
301     if not book.accessible(request):
302         return HttpResponseForbidden("Not authorized.")
303
304     # TODO: move to celery
305     doc = book.wldocument(librarian2=True)
306     # TODO: error handling
307
308     from librarian.builders import EpubBuilder
309     epub = EpubBuilder(
310         base_url='file://' + book.gallery_path() + '/',
311         debug=True
312     ).build(doc).get_bytes()
313     response = HttpResponse(content_type='application/epub+zip')
314     response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.epub'
315     response.write(epub)
316     return response
317
318
319 @login_required
320 @never_cache
321 def book_epubcheck(request, slug):
322     book = get_object_or_404(Book, slug=slug)
323     if not book.accessible(request):
324         return HttpResponseForbidden("Not authorized.")
325
326     # TODO: move to celery
327     doc = book.wldocument(librarian2=True)
328     # TODO: error handling
329
330     from librarian.builders import EpubBuilder
331     epub = EpubBuilder(
332         base_url='file://' + book.gallery_path() + '/',
333         debug=True
334     ).build(doc)
335     fname = epub.get_filename()
336
337     messages = epubcheck.epubcheck(fname)
338     for message in messages:
339         for location in message.get('locations', []):
340             if 'wl_chunk' in location:
341                 location['wl_chunk'] = book[location['wl_chunk']]
342     return render(request, 'documents/book_epubcheck.html', {
343         'messages': messages,
344         'book': book,
345     })
346
347
348 @login_required
349 @never_cache
350 def book_mobi(request, slug):
351     book = get_object_or_404(Book, slug=slug)
352     if not book.accessible(request):
353         return HttpResponseForbidden("Not authorized.")
354
355     # TODO: move to celery
356     doc = book.wldocument(librarian2=True)
357     # TODO: error handling
358     from librarian.builders import MobiBuilder
359     mobi = MobiBuilder(
360         base_url='file://' + book.gallery_path() + '/'
361     ).build(doc).get_bytes()
362     response = HttpResponse(content_type='application/x-mobipocket-ebook')
363     response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.mobi'
364     response.write(mobi)
365     return response
366
367
368 @never_cache
369 def revision(request, slug, chunk=None):
370     try:
371         doc = Chunk.get(slug, chunk)
372     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
373         raise Http404
374     if not doc.book.accessible(request):
375         return HttpResponseForbidden("Not authorized.")
376     return http.HttpResponse(str(doc.revision()))
377
378
379 def book(request, slug):
380     book = get_object_or_404(Book, slug=slug)
381     if not book.accessible(request):
382         return HttpResponseForbidden("Not authorized.")
383
384     if request.user.has_perm('documents.change_book'):
385         if request.method == "POST":
386             form = forms.BookForm(request.POST, instance=book)
387             if form.is_valid():
388                 form.save()
389                 return http.HttpResponseRedirect(book.get_absolute_url())
390         else:
391             form = forms.BookForm(instance=book)
392         publish_options_form = forms.PublishOptionsForm()
393         editable = True
394     else:
395         form = forms.ReadonlyBookForm(instance=book)
396         publish_options_form = forms.PublishOptionsForm()
397         editable = False
398
399     publish_error = book.publishable_error()
400     publishable = publish_error is None
401
402     stats = None
403     try:
404         doc = book.wldocument(librarian2=True)
405     except:
406         doc = None
407     else:
408         try:
409             stats = doc.get_statistics()
410         except:
411             pass
412
413     cbook_by_slug = None
414     if book.dc_slug:
415         audio_items = requests.get(f'https://audio.wolnelektury.pl/archive/book/{book.dc_slug}.json').json()['items']
416         has_audio = bool(audio_items)
417         can_sell_audio = has_audio and all(x['project']['can_sell'] for x in audio_items)
418
419         if book.catalogue_book is None or book.dc_slug != book.catalogue_book.slug:
420             cbook_by_slug = catalogue.models.Book.objects.filter(slug=book.dc_slug).first()
421     else:
422         has_audio = None
423         can_sell_audio = None
424
425     return render(request, "documents/book_detail.html", {
426         "book": book,
427         "doc": doc,
428         "stats": stats,
429         "publishable": publishable,
430         "publishable_error": publish_error,
431         "form": form,
432         "publish_options_form": publish_options_form,
433         "editable": editable,
434         "has_audio": has_audio,
435         "can_sell_audio": can_sell_audio,
436         "cbook_by_slug": cbook_by_slug,
437     })
438
439
440 def image(request, slug):
441     image = get_object_or_404(Image, slug=slug)
442     if not image.accessible(request):
443         return HttpResponseForbidden("Not authorized.")
444
445     if request.user.has_perm('documents.change_image'):
446         if request.method == "POST":
447             form = forms.ImageForm(request.POST, instance=image)
448             if form.is_valid():
449                 form.save()
450                 return http.HttpResponseRedirect(image.get_absolute_url())
451         else:
452             form = forms.ImageForm(instance=image)
453         editable = True
454     else:
455         form = forms.ReadonlyImageForm(instance=image)
456         editable = False
457
458     publish_error = image.publishable_error()
459     publishable = publish_error is None
460
461     return render(request, "documents/image_detail.html", {
462         "object": image,
463         "publishable": publishable,
464         "publishable_error": publish_error,
465         "form": form,
466         "editable": editable,
467     })
468
469
470 @permission_required('documents.add_chunk')
471 def chunk_add(request, slug, chunk):
472     try:
473         doc = Chunk.get(slug, chunk)
474     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
475         raise Http404
476     if not doc.book.accessible(request):
477         return HttpResponseForbidden("Not authorized.")
478
479     if request.method == "POST":
480         form = forms.ChunkAddForm(request.POST, instance=doc)
481         if form.is_valid():
482             if request.user.is_authenticated:
483                 creator = request.user
484             else:
485                 creator = None
486             doc.split(creator=creator,
487                 slug=form.cleaned_data['slug'],
488                 title=form.cleaned_data['title'],
489                 gallery_start=form.cleaned_data['gallery_start'],
490                 user=form.cleaned_data['user'],
491                 stage=form.cleaned_data['stage']
492             )
493
494             return http.HttpResponseRedirect(doc.book.get_absolute_url())
495     else:
496         form = forms.ChunkAddForm(initial={
497                 "slug": str(doc.number + 1),
498                 "title": "cz. %d" % (doc.number + 1, ),
499         })
500
501     return render(request, "documents/chunk_add.html", {
502         "chunk": doc,
503         "form": form,
504     })
505
506
507 @login_required
508 def chunk_edit(request, slug, chunk):
509     try:
510         doc = Chunk.get(slug, chunk)
511     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
512         raise Http404
513     if not doc.book.accessible(request):
514         return HttpResponseForbidden("Not authorized.")
515
516     if request.method == "POST":
517         form = forms.ChunkForm(request.POST, instance=doc)
518         if form.is_valid():
519             form.save()
520             go_next = request.GET.get('next', None)
521             if go_next:
522                 go_next = quote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
523             else:
524                 go_next = doc.book.get_absolute_url()
525             return http.HttpResponseRedirect(go_next)
526     else:
527         form = forms.ChunkForm(instance=doc)
528
529     referer = request.META.get('HTTP_REFERER')
530     if referer:
531         parts = urlsplit(referer)
532         parts = ['', ''] + list(parts[2:])
533         go_next = quote_plus(urlunsplit(parts))
534     else:
535         go_next = ''
536
537     return render(request, "documents/chunk_edit.html", {
538         "chunk": doc,
539         "form": form,
540         "go_next": go_next,
541     })
542
543
544 @transaction.atomic
545 @login_required
546 @require_POST
547 def chunk_mass_edit(request):
548     ids = [int(i) for i in request.POST.get('ids').split(',') if i.strip()]
549     chunks = list(Chunk.objects.filter(id__in=ids))
550     
551     stage = request.POST.get('stage')
552     if stage:
553         try:
554             stage = Chunk.tag_model.objects.get(slug=stage)
555         except Chunk.DoesNotExist as e:
556             stage = None
557        
558         for c in chunks: c.stage = stage
559
560     username = request.POST.get('user')
561     logger.info("username: %s" % username)
562     logger.info(request.POST)
563     if username:
564         try:
565             user = User.objects.get(username=username)
566         except User.DoesNotExist as e:
567             user = None
568             
569         for c in chunks: c.user = user
570
571     project_id = request.POST.get('project')
572     if project_id:
573         try:
574             project = Project.objects.get(pk=int(project_id))
575         except (Project.DoesNotExist, ValueError) as e:
576             project = None
577         for c in chunks:
578             book = c.book
579             book.project = project
580             book.save()
581
582     for c in chunks: c.save()
583
584     return HttpResponse("", content_type="text/plain")
585
586
587 @transaction.atomic
588 @login_required
589 @require_POST
590 def image_mass_edit(request):
591     ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(',')))
592     images = map(lambda i: Image.objects.get(id=i), ids)
593     
594     stage = request.POST.get('stage')
595     if stage:
596         try:
597             stage = Image.tag_model.objects.get(slug=stage)
598         except Image.DoesNotExist as e:
599             stage = None
600        
601         for c in images: c.stage = stage
602
603     username = request.POST.get('user')
604     logger.info("username: %s" % username)
605     logger.info(request.POST)
606     if username:
607         try:
608             user = User.objects.get(username=username)
609         except User.DoesNotExist as e:
610             user = None
611             
612         for c in images: c.user = user
613
614     project_id = request.POST.get('project')
615     if project_id:
616         try:
617             project = Project.objects.get(pk=int(project_id))
618         except (Project.DoesNotExist, ValueError) as e:
619             project = None
620         for c in images:
621             c.project = project
622
623     for c in images: c.save()
624
625     return HttpResponse("", content_type="text/plain")
626
627
628 @permission_required('documents.change_book')
629 def book_append(request, slug):
630     book = get_object_or_404(Book, slug=slug)
631     if not book.accessible(request):
632         return HttpResponseForbidden("Not authorized.")
633
634     if request.method == "POST":
635         form = forms.BookAppendForm(book, request.POST)
636         if form.is_valid():
637             append_to = form.cleaned_data['append_to']
638             append_to.append(book)
639             return http.HttpResponseRedirect(append_to.get_absolute_url())
640     else:
641         form = forms.BookAppendForm(book)
642     return render(request, "documents/book_append_to.html", {
643         "book": book,
644         "form": form,
645
646         "logout_to": '/',
647     })
648
649
650 @require_POST
651 @login_required
652 def publish(request, slug):
653     form = forms.PublishOptionsForm(request.POST)
654     if form.is_valid():
655         days = form.cleaned_data['days']
656         beta = form.cleaned_data['beta']
657         hidden = form.cleaned_data['hidden']
658     else:
659         days = 0
660         beta = False
661         hidden = False
662     book = get_object_or_404(Book, slug=slug)
663     if not book.accessible(request):
664         return HttpResponseForbidden("Not authorized.")
665
666     try:
667         protocol = 'https://' if request.is_secure() else 'http://'
668         book.publish(request.user, host=protocol + request.get_host(), days=days, beta=beta, hidden=hidden)
669     except NotAuthorizedError:
670         return http.HttpResponseRedirect(reverse('apiclient_oauth' if not beta else 'apiclient_beta_oauth'))
671     except BaseException as e:
672         return http.HttpResponse(repr(e))
673     else:
674         return http.HttpResponseRedirect(book.get_absolute_url())
675
676
677 @require_POST
678 @login_required
679 def publish_image(request, slug):
680     image = get_object_or_404(Image, slug=slug)
681     if not image.accessible(request):
682         return HttpResponseForbidden("Not authorized.")
683
684     try:
685         image.publish(request.user)
686     except NotAuthorizedError:
687         return http.HttpResponseRedirect(reverse('apiclient_oauth'))
688     except BaseException as e:
689         return http.HttpResponse(e)
690     else:
691         return http.HttpResponseRedirect(image.get_absolute_url())
692
693
694 class GalleryView(UploadView):
695     def get_object(self, request, slug):
696         book = get_object_or_404(Book, slug=slug)
697         if not book.gallery:
698             raise Http404
699         return book
700
701     def breadcrumbs(self):
702         return [
703             (_('books'), reverse('documents_document_list')),
704             (self.object.title, self.object.get_absolute_url()),
705             (_('scan gallery'),),
706         ]
707
708     def get_directory(self):
709         return "%s%s/" % (settings.IMAGE_DIR, self.object.gallery)
710
711
712 def active_users_list(request, csv=False):
713     year = int(request.GET.get('y', date.today().year))
714     by_user = defaultdict(lambda: 0)
715     by_email = defaultdict(lambda: 0)
716     names_by_email = defaultdict(set)
717     for change_model in (Chunk.change_model, Image.change_model):
718         for c in change_model.objects.filter(
719                 created_at__year=year).order_by(
720                 'author', 'author_email', 'author_name').values(
721                 'author', 'author_name', 'author_email').annotate(
722                 c=Count('author'), ce=Count('author_email')).distinct():
723             if c['author']:
724                 by_user[c['author']] += c['c']
725             else:
726                 by_email[c['author_email']] += c['ce']
727                 if (c['author_name'] or '').strip():
728                     names_by_email[c['author_email']].add(c['author_name'])
729     for user in User.objects.filter(pk__in=by_user):
730         by_email[user.email] += by_user[user.pk]
731         names_by_email[user.email].add("%s %s" % (user.first_name, user.last_name))
732
733     active_users = []
734     for email, count in by_email.items():
735         active_users.append((email, names_by_email[email], count))
736     active_users.sort(key=lambda x: -x[2])
737     if csv:
738         return http.HttpResponse(
739             '\n'.join((
740                 ','.join(
741                     (str(x[2]), x[0], ','.join(x[1]))
742                 )
743                 for x in active_users
744             )),
745             content_type='text/csv',
746             headers={
747                 'Content-Disposition': f'attachment; filename=redakcja-{year}.csv',
748             }
749         )
750     else:
751         return render(request, 'documents/active_users_list.html', {
752             'users': active_users,
753             'year': year,
754         })
755
756
757 @user_passes_test(lambda u: u.is_superuser)
758 def mark_final(request):
759     if request.method == 'POST':
760         form = forms.MarkFinalForm(data=request.POST)
761         if form.is_valid():
762             form.save()
763             return HttpResponseRedirect(reverse('mark_final_completed'))
764     else:
765         form = forms.MarkFinalForm()
766     return render(request, 'documents/mark_final.html', {'form': form})
767
768
769 def mark_final_completed(request):
770     return render(request, 'documents/mark_final_completed.html')
771
772
773 def synchro(request, slug):
774     book = get_object_or_404(Book, slug=slug)
775     if not book.accessible(request):
776         return HttpResponseForbidden("Not authorized.")
777
778     document = book.wldocument(librarian2=True)
779     slug = document.meta.url.slug
780     print(f'https://audio.wolnelektury.pl/archive/book/{slug}.json')
781     error = None
782     try:
783         items = requests.get(f'https://audio.wolnelektury.pl/archive/book/{slug}.json').json()['items']
784     except:
785         error = 'Błąd połączenia z repozytorium audio.'
786         items = []
787     else:
788         mp3 = [
789             item['part'] for item in items
790         ]
791
792     split_on = (
793         'naglowek_rozdzial',
794         'naglowek_scena',
795         )
796     
797     if split_on:
798         documents = []
799         headers = [('Początek', 0, 0)]
800         present = True
801         n = 0
802         while present:
803             present = False
804             n += 1
805             newdoc = deepcopy(document)
806             newdoc.tree.getroot().document = newdoc
807             
808             master = newdoc.tree.getroot()[-1]
809             i = 0
810             for item in list(master):
811                 #chunkno, sourceline = 0, self.sourceline
812                 #if builder.splits:
813                 #    chunkno, sourceline = len(builder.splits), sourceline - builder.splits[-1]
814
815                 if 'forcesplit' in item.attrib or (item.tag in split_on and 'nosplit' not in item.attrib):
816                     # TODO: clear
817                     i += 1
818                     if n > 1 and i == n:
819                         headers.append((
820                             raw_printable_text(item),
821                             0,
822                             item.sourceline,
823                         ))
824                 if i != n and not (n == 1 and not i):
825                     master.remove(item)
826                 else:
827                     present = True
828                 if present:
829                     documents.append(newdoc)
830     else:
831         documents = [document]
832         headers = [(
833             document.meta.title, 0 ,0
834         )]
835
836     length_ok = len(headers) == len(mp3)
837     table = zip_longest(headers, mp3)
838
839     
840     return render(request, 'documents/synchro.html', {
841         'book': book,
842         'documents': documents,
843         'headers': headers,
844         'mp3': mp3,
845         'length_ok': length_ok,
846         'table': table,
847         'error': error,
848     })
849
850
851 @permission_required('documents.change_book')
852 def attach_book_to_catalogue(request, pk):
853     dbook = get_object_or_404(Book, pk=pk)
854     if dbook.dc_slug:
855         cbook = get_object_or_404(catalogue.models.Book, slug=dbook.dc_slug)
856         dbook.catalogue_book = cbook
857         dbook.save()
858     return http.HttpResponseRedirect(dbook.get_absolute_url())