Rename catalogue to documents.
authorRadek Czajka <rczajka@rczajka.pl>
Sun, 22 Mar 2020 17:22:20 +0000 (18:22 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Sun, 22 Mar 2020 19:15:29 +0000 (20:15 +0100)
167 files changed:
src/catalogue/__init__.py [deleted file]
src/catalogue/admin.py [deleted file]
src/catalogue/constants.py [deleted file]
src/catalogue/ebook_utils.py [deleted file]
src/catalogue/feeds.py [deleted file]
src/catalogue/fixtures/stages.json [deleted file]
src/catalogue/forms.py [deleted file]
src/catalogue/helpers.py [deleted file]
src/catalogue/locale/pl/LC_MESSAGES/django.mo [deleted file]
src/catalogue/locale/pl/LC_MESSAGES/django.po [deleted file]
src/catalogue/management/__init__.py [deleted file]
src/catalogue/management/commands/__init__.py [deleted file]
src/catalogue/management/commands/add_parent.py [deleted file]
src/catalogue/management/commands/fixdc.py [deleted file]
src/catalogue/management/commands/import_wl.py [deleted file]
src/catalogue/management/commands/insert_isbn.py [deleted file]
src/catalogue/management/commands/mark_final.py [deleted file]
src/catalogue/management/commands/merge_books.py [deleted file]
src/catalogue/management/commands/prune_audience.py [deleted file]
src/catalogue/managers.py [deleted file]
src/catalogue/models/__init__.py [deleted file]
src/catalogue/models/book.py [deleted file]
src/catalogue/models/chunk.py [deleted file]
src/catalogue/models/image.py [deleted file]
src/catalogue/models/listeners.py [deleted file]
src/catalogue/models/project.py [deleted file]
src/catalogue/models/publish_log.py [deleted file]
src/catalogue/signals.py [deleted file]
src/catalogue/templates/catalogue/active_users_list.html [deleted file]
src/catalogue/templates/catalogue/activity.html [deleted file]
src/catalogue/templates/catalogue/base.html [deleted file]
src/catalogue/templates/catalogue/book_append_to.html [deleted file]
src/catalogue/templates/catalogue/book_detail.html [deleted file]
src/catalogue/templates/catalogue/book_edit.html [deleted file]
src/catalogue/templates/catalogue/book_html.html [deleted file]
src/catalogue/templates/catalogue/book_list/book.html [deleted file]
src/catalogue/templates/catalogue/book_list/book_list.html [deleted file]
src/catalogue/templates/catalogue/book_list/chunk.html [deleted file]
src/catalogue/templates/catalogue/book_text.html [deleted file]
src/catalogue/templates/catalogue/chunk_add.html [deleted file]
src/catalogue/templates/catalogue/chunk_edit.html [deleted file]
src/catalogue/templates/catalogue/document_create_missing.html [deleted file]
src/catalogue/templates/catalogue/document_list.html [deleted file]
src/catalogue/templates/catalogue/document_upload.html [deleted file]
src/catalogue/templates/catalogue/image_detail.html [deleted file]
src/catalogue/templates/catalogue/image_list.html [deleted file]
src/catalogue/templates/catalogue/image_short.html [deleted file]
src/catalogue/templates/catalogue/image_table.html [deleted file]
src/catalogue/templates/catalogue/main_tabs.html [deleted file]
src/catalogue/templates/catalogue/mark_final.html [deleted file]
src/catalogue/templates/catalogue/mark_final_completed.html [deleted file]
src/catalogue/templates/catalogue/my_page.html [deleted file]
src/catalogue/templates/catalogue/upload_pdf.html [deleted file]
src/catalogue/templates/catalogue/user_list.html [deleted file]
src/catalogue/templates/catalogue/user_page.html [deleted file]
src/catalogue/templates/catalogue/wall.html [deleted file]
src/catalogue/templatetags/__init__.py [deleted file]
src/catalogue/templatetags/book_list.py [deleted file]
src/catalogue/templatetags/catalogue.py [deleted file]
src/catalogue/templatetags/common_tags.py [deleted file]
src/catalogue/templatetags/set_get_parameter.py [deleted file]
src/catalogue/templatetags/wall.py [deleted file]
src/catalogue/test_utils.py [deleted file]
src/catalogue/tests/__init__.py [deleted file]
src/catalogue/tests/files/chunk1.xml [deleted file]
src/catalogue/tests/files/chunk2.xml [deleted file]
src/catalogue/tests/files/expected.xml [deleted file]
src/catalogue/tests/test_book.py [deleted file]
src/catalogue/tests/test_gallery.py [deleted file]
src/catalogue/tests/test_publish.py [deleted file]
src/catalogue/tests/test_xml_updater.py [deleted file]
src/catalogue/urls.py [deleted file]
src/catalogue/views.py [deleted file]
src/catalogue/xml_tools.py [deleted file]
src/cover/templates/cover/add_image.html
src/cover/templates/cover/image_detail.html
src/cover/templates/cover/image_list.html
src/cover/views.py
src/documents/__init__.py [new file with mode: 0644]
src/documents/admin.py [new file with mode: 0644]
src/documents/constants.py [new file with mode: 0644]
src/documents/ebook_utils.py [new file with mode: 0644]
src/documents/feeds.py [new file with mode: 0644]
src/documents/fixtures/stages.json [new file with mode: 0644]
src/documents/forms.py [new file with mode: 0644]
src/documents/helpers.py [new file with mode: 0644]
src/documents/locale/pl/LC_MESSAGES/django.mo [new file with mode: 0644]
src/documents/locale/pl/LC_MESSAGES/django.po [new file with mode: 0644]
src/documents/management/__init__.py [new file with mode: 0644]
src/documents/management/commands/__init__.py [new file with mode: 0644]
src/documents/management/commands/add_parent.py [new file with mode: 0644]
src/documents/management/commands/fixdc.py [new file with mode: 0644]
src/documents/management/commands/import_wl.py [new file with mode: 0644]
src/documents/management/commands/insert_isbn.py [new file with mode: 0644]
src/documents/management/commands/mark_final.py [new file with mode: 0644]
src/documents/management/commands/merge_books.py [new file with mode: 0644]
src/documents/management/commands/prune_audience.py [new file with mode: 0644]
src/documents/managers.py [new file with mode: 0644]
src/documents/models/__init__.py [new file with mode: 0644]
src/documents/models/book.py [new file with mode: 0644]
src/documents/models/chunk.py [new file with mode: 0644]
src/documents/models/image.py [new file with mode: 0644]
src/documents/models/listeners.py [new file with mode: 0644]
src/documents/models/project.py [new file with mode: 0644]
src/documents/models/publish_log.py [new file with mode: 0644]
src/documents/signals.py [new file with mode: 0644]
src/documents/templates/documents/active_users_list.html [new file with mode: 0644]
src/documents/templates/documents/activity.html [new file with mode: 0644]
src/documents/templates/documents/base.html [new file with mode: 0644]
src/documents/templates/documents/book_append_to.html [new file with mode: 0644]
src/documents/templates/documents/book_detail.html [new file with mode: 0644]
src/documents/templates/documents/book_edit.html [new file with mode: 0644]
src/documents/templates/documents/book_html.html [new file with mode: 0644]
src/documents/templates/documents/book_list/book.html [new file with mode: 0644]
src/documents/templates/documents/book_list/book_list.html [new file with mode: 0644]
src/documents/templates/documents/book_list/chunk.html [new file with mode: 0644]
src/documents/templates/documents/book_text.html [new file with mode: 0644]
src/documents/templates/documents/chunk_add.html [new file with mode: 0644]
src/documents/templates/documents/chunk_edit.html [new file with mode: 0644]
src/documents/templates/documents/document_create_missing.html [new file with mode: 0644]
src/documents/templates/documents/document_list.html [new file with mode: 0644]
src/documents/templates/documents/document_upload.html [new file with mode: 0644]
src/documents/templates/documents/image_detail.html [new file with mode: 0644]
src/documents/templates/documents/image_list.html [new file with mode: 0644]
src/documents/templates/documents/image_short.html [new file with mode: 0644]
src/documents/templates/documents/image_table.html [new file with mode: 0644]
src/documents/templates/documents/main_tabs.html [new file with mode: 0644]
src/documents/templates/documents/mark_final.html [new file with mode: 0644]
src/documents/templates/documents/mark_final_completed.html [new file with mode: 0644]
src/documents/templates/documents/my_page.html [new file with mode: 0644]
src/documents/templates/documents/upload_pdf.html [new file with mode: 0644]
src/documents/templates/documents/user_list.html [new file with mode: 0644]
src/documents/templates/documents/user_page.html [new file with mode: 0644]
src/documents/templates/documents/wall.html [new file with mode: 0644]
src/documents/templatetags/__init__.py [new file with mode: 0644]
src/documents/templatetags/book_list.py [new file with mode: 0644]
src/documents/templatetags/common_tags.py [new file with mode: 0644]
src/documents/templatetags/documents.py [new file with mode: 0644]
src/documents/templatetags/set_get_parameter.py [new file with mode: 0644]
src/documents/templatetags/wall.py [new file with mode: 0644]
src/documents/test_utils.py [new file with mode: 0644]
src/documents/tests/__init__.py [new file with mode: 0644]
src/documents/tests/files/chunk1.xml [new file with mode: 0644]
src/documents/tests/files/chunk2.xml [new file with mode: 0644]
src/documents/tests/files/expected.xml [new file with mode: 0644]
src/documents/tests/test_book.py [new file with mode: 0644]
src/documents/tests/test_gallery.py [new file with mode: 0644]
src/documents/tests/test_publish.py [new file with mode: 0644]
src/documents/tests/test_xml_updater.py [new file with mode: 0644]
src/documents/urls.py [new file with mode: 0644]
src/documents/views.py [new file with mode: 0644]
src/documents/xml_tools.py [new file with mode: 0644]
src/fileupload/templates/fileupload/picture_form.html
src/redakcja/settings/__init__.py
src/redakcja/static/js/catalogue/book_list.js [deleted file]
src/redakcja/static/js/catalogue/catalogue.js [deleted file]
src/redakcja/static/js/documents/book_list.js [new file with mode: 0644]
src/redakcja/static/js/documents/documents.js [new file with mode: 0644]
src/redakcja/templates/404.html
src/redakcja/templates/registration/login.html
src/redakcja/urls.py
src/wiki/forms.py
src/wiki/templates/wiki/document_details_base.html
src/wiki/views.py
src/wiki_img/forms.py
src/wiki_img/templates/wiki_img/document_details_base.html
src/wiki_img/views.py

diff --git a/src/catalogue/__init__.py b/src/catalogue/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/catalogue/admin.py b/src/catalogue/admin.py
deleted file mode 100644 (file)
index 8daad6e..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.contrib import admin
-
-from catalogue import models
-
-class BookAdmin(admin.ModelAdmin):
-    list_display = ['title', 'public', '_published', '_new_publishable', 'project']
-    list_filter = ['public', '_published', '_new_publishable', 'project']
-    prepopulated_fields = {'slug': ['title']}
-    search_fields = ['title']
-
-
-admin.site.register(models.Project)
-admin.site.register(models.Book, BookAdmin)
-admin.site.register(models.Chunk)
-admin.site.register(models.Chunk.tag_model)
-
-admin.site.register(models.Image)
-admin.site.register(models.Image.tag_model)
diff --git a/src/catalogue/constants.py b/src/catalogue/constants.py
deleted file mode 100644 (file)
index 4775d18..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-TRIM_BEGIN = " TRIM_BEGIN "
-TRIM_END = " TRIM_END "
-
-MASTERS = ['powiesc',
-           'opowiadanie',
-           'liryka_l',
-           'liryka_lp',
-           'dramat_wierszowany_l',
-           'dramat_wierszowany_lp',
-           'dramat_wspolczesny',
-           ]
diff --git a/src/catalogue/ebook_utils.py b/src/catalogue/ebook_utils.py
deleted file mode 100644 (file)
index f9bd6d3..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from io import BytesIO
-from catalogue.models import Book
-from librarian import DocProvider
-from django.http import HttpResponse
-
-
-class RedakcjaDocProvider(DocProvider):
-    """Used for getting books' children."""
-
-    def __init__(self, publishable):
-        self.publishable = publishable
-
-    def by_slug(self, slug):
-        return BytesIO(Book.objects.get(dc_slug=slug
-                    ).materialize(publishable=self.publishable
-                    ).encode('utf-8'))
-
-
-def serve_file(file_path, name, mime_type):
-    def read_chunks(f, size=8192):
-        chunk = f.read(size)
-        while chunk:
-            yield chunk
-            chunk = f.read(size)
-
-    response = HttpResponse(content_type=mime_type)
-    response['Content-Disposition'] = 'attachment; filename=%s' % name
-    with open(file_path, 'rb') as f:
-        for chunk in read_chunks(f):
-            response.write(chunk)
-    return response
diff --git a/src/catalogue/feeds.py b/src/catalogue/feeds.py
deleted file mode 100644 (file)
index 99808bc..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.contrib.syndication.views import Feed
-from django.shortcuts import get_object_or_404
-from catalogue.models import Book, Chunk
-
-
-class PublishTrackFeed(Feed):
-    title = u"Planowane publikacje"
-    link = "/"
-
-    def description(self, obj):
-        tag, published = obj
-        return u"Publikacje, które dotarły co najmniej do etapu: %s" % tag.name
-
-    def get_object(self, request, slug):
-        published = request.GET.get('published')
-        if published is not None:
-            published = published == 'true'
-        return get_object_or_404(Chunk.tag_model, slug=slug), published
-
-    def item_title(self, item):
-        return item.title
-
-    def items(self, obj):
-        tag, published = obj
-        books = Book.objects.filter(public=True, _on_track__gte=tag.ordering
-                ).order_by('-_on_track', 'title')
-        if published is not None:
-            books = books.filter(_published=published)
-        return books
diff --git a/src/catalogue/fixtures/stages.json b/src/catalogue/fixtures/stages.json
deleted file mode 100644 (file)
index 5a46ec0..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-[
-    {
-        "pk": 1, 
-        "model": "catalogue.chunktag", 
-        "fields": {
-            "ordering": 1, 
-            "name": "Autokorekta", 
-            "slug": "first_correction"
-        }
-    }, 
-    {
-        "pk": 2, 
-        "model": "catalogue.chunktag", 
-        "fields": {
-            "ordering": 2, 
-            "name": "Tagowanie", 
-            "slug": "tagging"
-        }
-    }, 
-    {
-        "pk": 3, 
-        "model": "catalogue.chunktag", 
-        "fields": {
-            "ordering": 3, 
-            "name": "Korekta", 
-            "slug": "proofreading"
-        }
-    }, 
-    {
-        "pk": 4, 
-        "model": "catalogue.chunktag", 
-        "fields": {
-            "ordering": 4, 
-            "name": "Sprawdzenie przypis\u00f3w \u017ar\u00f3d\u0142a", 
-            "slug": "annotation-proofreading"
-        }
-    }, 
-    {
-        "pk": 5, 
-        "model": "catalogue.chunktag", 
-        "fields": {
-            "ordering": 5, 
-            "name": "Uwsp\u00f3\u0142cze\u015bnienie", 
-            "slug": "modernisation"
-        }
-    }, 
-    {
-        "pk": 6, 
-        "model": "catalogue.chunktag", 
-        "fields": {
-            "ordering": 6, 
-            "name": "Przypisy", 
-            "slug": "annotations"
-        }
-    }, 
-    {
-        "pk": 7, 
-        "model": "catalogue.chunktag", 
-        "fields": {
-            "ordering": 7, 
-            "name": "Motywy", 
-            "slug": "themes"
-        }
-    }, 
-    {
-        "pk": 8, 
-        "model": "catalogue.chunktag", 
-        "fields": {
-            "ordering": 8, 
-            "name": "Ostateczna redakcja literacka", 
-            "slug": "editor-proofreading"
-        }
-    }, 
-    {
-        "pk": 9, 
-        "model": "catalogue.chunktag", 
-        "fields": {
-            "ordering": 9, 
-            "name": "Ostateczna redakcja techniczna", 
-            "slug": "technical-editor-proofreading"
-        }
-    }
-]
diff --git a/src/catalogue/forms.py b/src/catalogue/forms.py
deleted file mode 100644 (file)
index e61a1b7..0000000
+++ /dev/null
@@ -1,232 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from catalogue.models import User
-from django.db.models import Count
-from django import forms
-from django.utils.translation import ugettext_lazy as _
-from django.conf import settings
-
-from catalogue.constants import MASTERS
-from catalogue.models import Book, Chunk, Image
-
-class DocumentCreateForm(forms.ModelForm):
-    """
-        Form used for creating new documents.
-    """
-    file = forms.FileField(required=False)
-    text = forms.CharField(required=False, widget=forms.Textarea)
-
-    class Meta:
-        model = Book
-        exclude = ['parent', 'parent_number', 'project']
-
-    def __init__(self, *args, **kwargs):
-        super(DocumentCreateForm, self).__init__(*args, **kwargs)
-        self.fields['slug'].widget.attrs={'class': 'autoslug'}
-        self.fields['gallery'].widget.attrs={'class': 'autoslug'}
-        self.fields['title'].widget.attrs={'class': 'autoslug-source'}
-
-    def clean(self):
-        super(DocumentCreateForm, self).clean()
-        file = self.cleaned_data['file']
-
-        if file is not None:
-            try:
-                self.cleaned_data['text'] = file.read().decode('utf-8')
-            except UnicodeDecodeError:
-                raise forms.ValidationError(_("Text file must be UTF-8 encoded."))
-
-        if not self.cleaned_data["text"]:
-            self._errors["file"] = self.error_class([_("You must either enter text or upload a file")])
-
-        return self.cleaned_data
-
-
-class DocumentsUploadForm(forms.Form):
-    """
-        Form used for uploading new documents.
-    """
-    file = forms.FileField(required=True, label=_('ZIP file'))
-    dirs = forms.BooleanField(label=_('Directories are documents in chunks'),
-            widget = forms.CheckboxInput(attrs={'disabled':'disabled'}))
-
-    def clean(self):
-        file = self.cleaned_data['file']
-
-        import zipfile
-        try:
-            z = self.cleaned_data['zip'] = zipfile.ZipFile(file)
-        except zipfile.BadZipfile:
-            raise forms.ValidationError("Should be a ZIP file.")
-        if z.testzip():
-            raise forms.ValidationError("ZIP file corrupt.")
-
-        return self.cleaned_data
-
-
-class ChunkForm(forms.ModelForm):
-    """
-        Form used for editing a chunk.
-    """
-    user = forms.ModelChoiceField(queryset=
-        User.objects.annotate(count=Count('chunk')).
-        order_by('last_name', 'first_name'), required=False,
-        label=_('Assigned to')) 
-
-    class Meta:
-        model = Chunk
-        fields = ['title', 'slug', 'gallery_start', 'user', 'stage']
-        exclude = ['number']
-
-    def __init__(self, *args, **kwargs):
-        super(ChunkForm, self).__init__(*args, **kwargs)
-        self.fields['gallery_start'].widget.attrs={'class': 'number-input'}
-        self.fields['slug'].widget.attrs={'class': 'autoslug'}
-        self.fields['title'].widget.attrs={'class': 'autoslug-source'}
-
-    def clean_slug(self):
-        slug = self.cleaned_data['slug']
-        try:
-            chunk = Chunk.objects.get(book=self.instance.book, slug=slug)
-        except Chunk.DoesNotExist:
-            return slug
-        if chunk == self.instance:
-            return slug
-        raise forms.ValidationError(_('Chunk with this slug already exists'))
-
-
-class ChunkAddForm(ChunkForm):
-    """
-        Form used for adding a chunk to a document.
-    """
-
-    def clean_slug(self):
-        slug = self.cleaned_data['slug']
-        try:
-            user = Chunk.objects.get(book=self.instance.book, slug=slug)
-        except Chunk.DoesNotExist:
-            return slug
-        raise forms.ValidationError(_('Chunk with this slug already exists'))
-
-
-class BookAppendForm(forms.Form):
-    """
-        Form for appending a book to another book.
-        It means moving all chunks from book A to book B and deleting A.
-    """
-    append_to = forms.ModelChoiceField(queryset=Book.objects.all(),
-            label=_("Append to"))
-
-    def __init__(self, book, *args, **kwargs):
-        ret =  super(BookAppendForm, self).__init__(*args, **kwargs)
-        self.fields['append_to'].queryset = Book.objects.exclude(pk=book.pk)
-        return ret
-
-
-class BookForm(forms.ModelForm):
-    """Form used for editing a Book."""
-
-    class Meta:
-        model = Book
-        exclude = ['project']
-
-    def __init__(self, *args, **kwargs):
-        ret = super(BookForm, self).__init__(*args, **kwargs)
-        self.fields['slug'].widget.attrs.update({"class": "autoslug"})
-        self.fields['title'].widget.attrs.update({"class": "autoslug-source"})
-        return ret
-
-    def save(self, **kwargs):
-        orig_instance = Book.objects.get(pk=self.instance.pk)
-        old_gallery = orig_instance.gallery
-        new_gallery = self.cleaned_data['gallery']
-        if new_gallery and old_gallery and new_gallery != old_gallery:
-            import shutil
-            import os.path
-            from django.conf import settings
-            shutil.move(orig_instance.gallery_path(),
-                        os.path.join(settings.MEDIA_ROOT, settings.IMAGE_DIR, new_gallery))
-        super(BookForm, self).save(**kwargs)
-
-
-class ReadonlyBookForm(BookForm):
-    """Form used for not editing a Book."""
-
-    def __init__(self, *args, **kwargs):
-        ret = super(ReadonlyBookForm, self).__init__(*args, **kwargs)
-        for field in self.fields.values():
-            field.widget.attrs.update({"disabled": "disabled"})
-        return ret
-
-
-class ChooseMasterForm(forms.Form):
-    """
-        Form used for fixing the chunks in a book.
-    """
-
-    master = forms.ChoiceField(choices=((m, m) for m in MASTERS))
-
-
-class ImageForm(forms.ModelForm):
-    """Form used for editing an Image."""
-    user = forms.ModelChoiceField(queryset=
-        User.objects.annotate(count=Count('chunk')).
-        order_by('-count', 'last_name', 'first_name'), required=False,
-        label=_('Assigned to')) 
-
-    class Meta:
-        model = Image
-        fields = ['title', 'slug', 'user', 'stage']
-
-    def __init__(self, *args, **kwargs):
-        super(ImageForm, self).__init__(*args, **kwargs)
-        self.fields['slug'].widget.attrs={'class': 'autoslug'}
-        self.fields['title'].widget.attrs={'class': 'autoslug-source'}
-
-
-class ReadonlyImageForm(ImageForm):
-    """Form used for not editing an Image."""
-
-    def __init__(self, *args, **kwargs):
-        super(ReadonlyImageForm, self).__init__(*args, **kwargs)
-        for field in self.fields.values():
-            field.widget.attrs.update({"disabled": "disabled"})
-
-
-class MarkFinalForm(forms.Form):
-    username = forms.CharField(initial=settings.LITERARY_DIRECTOR_USERNAME)
-    comment = forms.CharField(initial=u'Ostateczna akceptacja merytoryczna przez kierownika literackiego.')
-    books = forms.CharField(widget=forms.Textarea, help_text=u'linki do książek w redakcji, po jednym na wiersz')
-
-    def clean_books(self):
-        books_value = self.cleaned_data['books']
-        slugs = [line.strip().strip('/').split('/')[-1] for line in books_value.split('\n') if line.strip()]
-        books = Book.objects.filter(slug__in=slugs)
-        if len(books) != len(slugs):
-            raise forms.ValidationError(
-                'Incorrect slug(s): %s' % ' '.join(slug for slug in slugs if not Book.objects.filter(slug=slug)))
-        return books
-
-    def clean_username(self):
-        username = self.cleaned_data['username']
-        if not User.objects.filter(username=username):
-            raise forms.ValidationError('Invalid username')
-        return username
-
-    def save(self):
-        for book in self.cleaned_data['books']:
-            for chunk in book.chunk_set.all():
-                src = chunk.head.materialize()
-                chunk.commit(
-                    text=src,
-                    author=User.objects.get(username=self.cleaned_data['username']),
-                    description=self.cleaned_data['comment'],
-                    tags=[Chunk.tag_model.objects.get(slug='editor-proofreading')],
-                    publishable=True
-                )
-
-
-class PublishOptionsForm(forms.Form):
-    days = forms.IntegerField(label=u'po ilu dniach udostępnienić (0 = od razu)', min_value=0, initial=0)
-    beta = forms.BooleanField(label=u'Opublikuj na wersji testowej', required=False)
diff --git a/src/catalogue/helpers.py b/src/catalogue/helpers.py
deleted file mode 100644 (file)
index 5954503..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from datetime import date
-from functools import wraps
-from os.path import join
-from os import listdir, stat
-from shutil import move, rmtree
-from django.conf import settings
-import re
-import filecmp
-
-from django.db.models import Count
-
-
-def active_tab(tab):
-    """
-        View decorator, which puts tab info on a request.
-    """
-    def wrapper(f):
-        @wraps(f)
-        def wrapped(request, *args, **kwargs):
-            request.catalogue_active_tab = tab
-            return f(request, *args, **kwargs)
-        return wrapped
-    return wrapper
-
-
-def cached_in_field(field_name):
-    def decorator(f):
-        @property
-        @wraps(f)
-        def wrapped(self, *args, **kwargs):
-            value = getattr(self, field_name)
-            if value is None:
-                value = f(self, *args, **kwargs)
-                type(self)._default_manager.filter(pk=self.pk).update(**{field_name: value})
-            return value
-        return wrapped
-    return decorator
-
-
-def parse_isodate(isodate):
-    try:
-        return date(*[int(p) for p in isodate.split('-')])
-    except (AttributeError, TypeError, ValueError):
-        raise ValueError("Not a date in ISO format.")
-
-
-class GalleryMerger(object):
-    def __init__(self, dest_gallery, src_gallery):
-        self.dest = dest_gallery
-        self.src = src_gallery
-        self.dest_size = None
-        self.src_size = None
-        self.num_deleted = 0
-
-    @staticmethod
-    def path(gallery):
-        return join(settings.MEDIA_ROOT, settings.IMAGE_DIR, gallery)
-
-    @staticmethod
-    def get_prefix(name):
-        m = re.match(r"^([0-9])-", name)
-        if m:
-            return int(m.groups()[0])
-        return None
-
-    @staticmethod
-    def set_prefix(name, prefix, always=False):
-        m = not always and re.match(r"^([0-9])-", name)
-        return "%1d-%s" % (prefix, m and name[2:] or name)
-
-    @property
-    def was_merged(self):
-        "Check if we have gallery size recorded"
-        return self.dest_size is not None
-
-    def merge(self):
-        if not self.dest:
-            return self.src
-        if not self.src:
-            return self.dest
-
-        files = listdir(self.path(self.dest))
-        files.sort()
-        self.dest_size = len(files)
-        files_other = listdir(self.path(self.src))
-        files_other.sort()
-        self.src_size = len(files_other)
-
-        if files and files_other:
-            if filecmp.cmp(
-                    join(self.path(self.dest), files[-1]),
-                    join(self.path(self.src), files_other[0]),
-                    False
-                    ):
-                files_other.pop(0)
-                self.num_deleted = 1
-
-        prefixes = {}
-        renamed_files = {}
-        renamed_files_other = {}
-        last_pfx = -1
-
-        # check if all elements of my files have a prefix
-        files_prefixed = True
-        for f in files:
-            p = self.get_prefix(f)
-            if p:
-                if p > last_pfx: last_pfx = p
-            else:
-                files_prefixed = False
-                break
-
-        # if not, add a 0 prefix to them
-        if not files_prefixed:
-            prefixes[0] = 0
-            for f in files:
-                renamed_files[f] = self.set_prefix(f, 0, True)
-
-        # two cases here - either all are prefixed or not.
-        files_other_prefixed = True
-        for f in files_other:
-            pfx = self.get_prefix(f)
-            if pfx is not None:
-                if not pfx in prefixes:
-                    last_pfx += 1
-                    prefixes[pfx] = last_pfx
-                renamed_files_other[f] = self.set_prefix(f, prefixes[pfx])
-            else:
-                # ops, not all files here were prefixed.
-                files_other_prefixed = False
-                break
-
-        # just set a 1- prefix to all of them
-        if not files_other_prefixed:
-            for f in files_other:
-                renamed_files_other[f] = self.set_prefix(f, 1, True)
-
-        # finally, move / rename files.
-        for frm, to in renamed_files.items():
-            move(join(self.path(self.dest), frm),
-                        join(self.path(self.dest), to))
-        for frm, to in renamed_files_other.items():
-            move(join(self.path(self.src), frm),
-                        join(self.path(self.dest), to))            
-
-        rmtree(join(self.path(self.src)))
-        return self.dest
diff --git a/src/catalogue/locale/pl/LC_MESSAGES/django.mo b/src/catalogue/locale/pl/LC_MESSAGES/django.mo
deleted file mode 100644 (file)
index 395b34e..0000000
Binary files a/src/catalogue/locale/pl/LC_MESSAGES/django.mo and /dev/null differ
diff --git a/src/catalogue/locale/pl/LC_MESSAGES/django.po b/src/catalogue/locale/pl/LC_MESSAGES/django.po
deleted file mode 100644 (file)
index ac60fb2..0000000
+++ /dev/null
@@ -1,830 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: Platforma Redakcyjna\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-10-07 13:05+0200\n"
-"PO-Revision-Date: 2019-10-07 13:06+0200\n"
-"Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
-"Language-Team: Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org."
-"pl>\n"
-"Language: pl\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
-"|| n%100>=20) ? 1 : 2);\n"
-"X-Generator: Poedit 2.0.6\n"
-
-#: forms.py:38
-msgid "Text file must be UTF-8 encoded."
-msgstr "Plik powinien mieć kodowanie UTF-8."
-
-#: forms.py:41
-msgid "You must either enter text or upload a file"
-msgstr "Proszę wpisać tekst albo wybrać plik do załadowania"
-
-#: forms.py:50
-msgid "ZIP file"
-msgstr "Plik ZIP"
-
-#: forms.py:51
-msgid "Directories are documents in chunks"
-msgstr "Katalogi zawierają dokumenty w częściach"
-
-#: forms.py:75 forms.py:176
-msgid "Assigned to"
-msgstr "Przypisane do"
-
-#: forms.py:96 forms.py:110
-msgid "Chunk with this slug already exists"
-msgstr "Część z tym slugiem już istnieje"
-
-#: forms.py:119
-msgid "Append to"
-msgstr "Dołącz do"
-
-#: models/book.py:26 models/chunk.py:21 models/image.py:20
-msgid "title"
-msgstr "tytuł"
-
-#: models/book.py:27 models/chunk.py:22 models/image.py:21
-msgid "slug"
-msgstr "slug"
-
-#: models/book.py:28 models/image.py:22
-msgid "public"
-msgstr "publiczna"
-
-#: models/book.py:29
-msgid "scan gallery name"
-msgstr "nazwa galerii skanów"
-
-#: models/book.py:33
-msgid "parent"
-msgstr "rodzic"
-
-#: models/book.py:34
-msgid "parent number"
-msgstr "numeracja rodzica"
-
-#: models/book.py:52 models/chunk.py:19 models/publish_log.py:15
-msgid "book"
-msgstr "książka"
-
-#: models/book.py:53 views.py:619
-msgid "books"
-msgstr "książki"
-
-#: models/book.py:257
-msgid "No chunks in the book."
-msgstr "Książka nie ma części."
-
-#: models/book.py:261
-msgid "Not all chunks have publishable revisions."
-msgstr "Niektóre części nie są gotowe do publikacji."
-
-#: models/book.py:268 models/image.py:83
-msgid "Invalid XML"
-msgstr "Nieprawidłowy XML"
-
-#: models/book.py:270 models/image.py:85
-msgid "No Dublin Core found."
-msgstr "Brak sekcji Dublin Core."
-
-#: models/book.py:272 models/image.py:87
-msgid "Invalid Dublin Core"
-msgstr "Nieprawidłowy Dublin Core"
-
-#: models/book.py:275 models/image.py:91
-msgid "rdf:about is not"
-msgstr "rdf:about jest różny od"
-
-#: models/chunk.py:20
-msgid "number"
-msgstr "numer"
-
-#: models/chunk.py:23
-msgid "gallery start"
-msgstr "początek galerii"
-
-#: models/chunk.py:38
-msgid "chunk"
-msgstr "część"
-
-#: models/chunk.py:39
-msgid "chunks"
-msgstr "części"
-
-#: models/image.py:19 models/image.py:33 models/publish_log.py:43
-msgid "image"
-msgstr "obraz"
-
-#: models/image.py:34
-msgid "images"
-msgstr "obrazy"
-
-#: models/image.py:75
-msgid "There is no publishable revision"
-msgstr "Żadna wersja nie została oznaczona do publikacji."
-
-#: models/project.py:11
-msgid "name"
-msgstr "nazwa"
-
-#: models/project.py:12
-msgid "notes"
-msgstr "notatki"
-
-#: models/project.py:17 templates/catalogue/book_list/book_list.html:66
-#: templates/catalogue/image_table.html:60
-msgid "project"
-msgstr "projekt"
-
-#: models/project.py:18
-msgid "projects"
-msgstr "projekty"
-
-#: models/publish_log.py:16 models/publish_log.py:44
-msgid "time"
-msgstr "czas"
-
-#: models/publish_log.py:17 models/publish_log.py:45
-#: templates/catalogue/wall.html:20
-msgid "user"
-msgstr "użytkownik"
-
-#: models/publish_log.py:22 models/publish_log.py:31
-msgid "book publish record"
-msgstr "zapis publikacji książki"
-
-#: models/publish_log.py:23
-msgid "book publish records"
-msgstr "zapisy publikacji książek"
-
-#: models/publish_log.py:32 models/publish_log.py:46
-msgid "change"
-msgstr "zmiana"
-
-#: models/publish_log.py:36
-msgid "chunk publish record"
-msgstr "zapis publikacji części"
-
-#: models/publish_log.py:37
-msgid "chunk publish records"
-msgstr "zapisy publikacji części"
-
-#: models/publish_log.py:51
-msgid "image publish record"
-msgstr "zapis publikacji obrazu"
-
-#: models/publish_log.py:52
-msgid "image publish records"
-msgstr "zapisy publikacji obrazów"
-
-#: templates/catalogue/active_users_list.html:5
-msgid "Active users"
-msgstr "Aktywni użytkownicy"
-
-#: templates/catalogue/active_users_list.html:11
-msgid "Users active in the year"
-msgstr "Użytkownicy aktywni w roku"
-
-#: templates/catalogue/activity.html:6 templates/catalogue/activity.html:15
-#: templatetags/catalogue.py:30
-msgid "Activity"
-msgstr "Aktywność"
-
-#: templates/catalogue/base.html:13
-msgid "Platforma Redakcyjna"
-msgstr "Platforma Redakcyjna"
-
-#: templates/catalogue/book_append_to.html:5
-#: templates/catalogue/book_append_to.html:14
-msgid "Append book"
-msgstr "Dołącz książkę"
-
-#: templates/catalogue/book_detail.html:23
-#: templates/catalogue/book_edit.html:13 templates/catalogue/chunk_edit.html:22
-#: templates/catalogue/image_detail.html:22
-msgid "Save"
-msgstr "Zapisz"
-
-#: templates/catalogue/book_detail.html:30
-msgid "Edit gallery"
-msgstr "Edytuj galerię"
-
-#: templates/catalogue/book_detail.html:33
-msgid "Append to other book"
-msgstr "Dołącz do innej książki"
-
-#: templates/catalogue/book_detail.html:40
-msgid "Chunks"
-msgstr "Części"
-
-#: templates/catalogue/book_detail.html:58
-#: templates/catalogue/image_detail.html:47 templatetags/wall.py:108
-#: templatetags/wall.py:129
-msgid "Publication"
-msgstr "Publikacja"
-
-#: templates/catalogue/book_detail.html:69
-#: templates/catalogue/image_detail.html:51
-msgid "Last published"
-msgstr "Ostatnio opublikowano"
-
-#: templates/catalogue/book_detail.html:79
-msgid "Full XML"
-msgstr "Pełny XML"
-
-#: templates/catalogue/book_detail.html:80
-msgid "HTML version"
-msgstr "Wersja HTML"
-
-#: templates/catalogue/book_detail.html:81
-msgid "TXT version"
-msgstr "Wersja TXT"
-
-#: templates/catalogue/book_detail.html:82
-msgid "PDF version"
-msgstr "Wersja PDF"
-
-#: templates/catalogue/book_detail.html:83
-msgid "PDF version for mobiles"
-msgstr "Wersja PDF na telefony"
-
-#: templates/catalogue/book_detail.html:84
-msgid "EPUB version"
-msgstr "Wersja EPUB"
-
-#: templates/catalogue/book_detail.html:85
-msgid "MOBI version"
-msgstr "Wersja MOBI"
-
-#: templates/catalogue/book_detail.html:99
-#: templates/catalogue/image_detail.html:70
-msgid "Publish"
-msgstr "Opublikuj"
-
-#: templates/catalogue/book_detail.html:103
-#: templates/catalogue/image_detail.html:74
-msgid "Log in to publish."
-msgstr "Zaloguj się, aby opublikować."
-
-#: templates/catalogue/book_detail.html:106
-#: templates/catalogue/image_detail.html:77
-msgid "This book can't be published yet, because:"
-msgstr "Ta książka nie może jeszcze zostać opublikowana. Powód:"
-
-#: templates/catalogue/book_edit.html:5
-msgid "Edit book"
-msgstr "Edytuj książkę"
-
-#: templates/catalogue/book_html.html:12 templates/catalogue/book_text.html:15
-msgid "Table of contents"
-msgstr "Spis treści"
-
-#: templates/catalogue/book_html.html:13 templates/catalogue/book_text.html:17
-msgid "Edit. note"
-msgstr "Nota red."
-
-#: templates/catalogue/book_html.html:14
-msgid "Infobox"
-msgstr "Informacje"
-
-#: templates/catalogue/book_list/book.html:8
-#: templates/catalogue/book_list/book.html:36
-msgid "Book settings"
-msgstr "Ustawienia książki"
-
-#: templates/catalogue/book_list/book.html:9
-#: templates/catalogue/book_list/chunk.html:7
-#: templates/catalogue/chunk_edit.html:6 templates/catalogue/chunk_edit.html:12
-msgid "Chunk settings"
-msgstr "Ustawienia części"
-
-#: templates/catalogue/book_list/book.html:12
-#: templates/catalogue/book_list/chunk.html:9
-#: templates/catalogue/image_short.html:9
-msgid "Edit:"
-msgstr "Edytuj:"
-
-#: templates/catalogue/book_list/book.html:21
-#: templates/catalogue/book_list/book.html:43
-#: templates/catalogue/image_short.html:18 templatetags/book_list.py:82
-#: templatetags/book_list.py:150
-msgid "published"
-msgstr "opublikowane"
-
-#: templates/catalogue/book_list/book.html:24
-#: templates/catalogue/book_list/book.html:46
-#: templates/catalogue/book_list/chunk.html:28
-#: templates/catalogue/image_short.html:21 templatetags/book_list.py:80
-#: templatetags/book_list.py:148
-msgid "publishable"
-msgstr "do publikacji"
-
-#: templates/catalogue/book_list/book.html:27
-#: templates/catalogue/book_list/chunk.html:33
-#: templates/catalogue/image_short.html:24 templatetags/book_list.py:81
-#: templatetags/book_list.py:149
-msgid "changed"
-msgstr "zmienione"
-
-#: templates/catalogue/book_list/book_list.html:29
-#: templates/catalogue/image_table.html:25
-msgid "Search in book titles"
-msgstr "Szukaj w tytułach książek"
-
-#: templates/catalogue/book_list/book_list.html:34
-#: templates/catalogue/image_table.html:30
-msgid "stage"
-msgstr "etap"
-
-#: templates/catalogue/book_list/book_list.html:36
-#: templates/catalogue/book_list/book_list.html:47
-#: templates/catalogue/book_list/book_list.html:68
-#: templates/catalogue/image_table.html:32
-#: templates/catalogue/image_table.html:43
-#: templates/catalogue/image_table.html:62
-msgid "none"
-msgstr "brak"
-
-#: templates/catalogue/book_list/book_list.html:45
-#: templates/catalogue/image_table.html:41
-msgid "editor"
-msgstr "redaktor"
-
-#: templates/catalogue/book_list/book_list.html:58
-#: templates/catalogue/image_table.html:52
-msgid "status"
-msgstr "status"
-
-#: templates/catalogue/book_list/book_list.html:92
-#, python-format
-msgid "%(c)s book"
-msgid_plural "%(c)s books"
-msgstr[0] "%(c)s książka"
-msgstr[1] "%(c)s książki"
-msgstr[2] "%(c)s książek"
-
-#: templates/catalogue/book_list/book_list.html:96
-msgid "No books found."
-msgstr "Nie znaleziono książek."
-
-#: templates/catalogue/book_list/book_list.html:105
-#: templates/catalogue/image_table.html:87
-msgid "Set stage"
-msgstr "Ustaw etap"
-
-#: templates/catalogue/book_list/book_list.html:106
-#: templates/catalogue/image_table.html:88
-msgid "Set user"
-msgstr "Przypisz redaktora"
-
-#: templates/catalogue/book_list/book_list.html:108
-#: templates/catalogue/image_table.html:90
-msgid "Project"
-msgstr "Projekt"
-
-#: templates/catalogue/book_list/book_list.html:109
-#: templates/catalogue/image_table.html:91
-msgid "More users"
-msgstr "Więcej użytkowników"
-
-#: templates/catalogue/book_text.html:7
-msgid "Redakcja"
-msgstr ""
-
-#: templates/catalogue/chunk_add.html:6 templates/catalogue/chunk_add.html:12
-#: templates/catalogue/chunk_edit.html:29
-msgid "Split chunk"
-msgstr "Podziel część"
-
-#: templates/catalogue/chunk_add.html:19
-msgid "Insert empty chunk after"
-msgstr "Wstaw pustą część po"
-
-#: templates/catalogue/chunk_add.html:23
-msgid "Add chunk"
-msgstr "Dodaj część"
-
-#: templates/catalogue/chunk_edit.html:19
-msgid "Book"
-msgstr "Książka"
-
-#: templates/catalogue/document_create_missing.html:6
-#: templates/catalogue/document_create_missing.html:12
-msgid "Create a new book"
-msgstr "Utwórz nową książkę"
-
-#: templates/catalogue/document_create_missing.html:21
-msgid "Create book"
-msgstr "Utwórz książkę"
-
-#: templates/catalogue/document_list.html:7
-msgid "Book list"
-msgstr "Lista książek"
-
-#: templates/catalogue/document_upload.html:6
-msgid "Bulk document upload"
-msgstr "Hurtowe dodawanie dokumentów"
-
-#: templates/catalogue/document_upload.html:14
-msgid "Bulk documents upload"
-msgstr "Hurtowe dodawanie dokumentów"
-
-#: templates/catalogue/document_upload.html:19
-msgid ""
-"Please submit a ZIP with UTF-8 encoded XML files. Files not ending with "
-"<code>.xml</code> will be ignored."
-msgstr ""
-"Proszę wskazać archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie "
-"kończące się na <code>.xml</code> zostaną zignorowane."
-
-#: templates/catalogue/document_upload.html:26
-#: templates/catalogue/upload_pdf.html:16 templatetags/catalogue.py:37
-msgid "Upload"
-msgstr "Załaduj"
-
-#: templates/catalogue/document_upload.html:34
-msgid ""
-"There have been some errors. No files have been added to the repository."
-msgstr "Wystąpiły błędy. Żadne pliki nie zostały dodane do repozytorium."
-
-#: templates/catalogue/document_upload.html:35
-msgid "Offending files"
-msgstr "Błędne pliki"
-
-#: templates/catalogue/document_upload.html:43
-msgid "Correct files"
-msgstr "Poprawne pliki"
-
-#: templates/catalogue/document_upload.html:54
-msgid "Files have been successfully uploaded to the repository."
-msgstr "Pliki zostały dodane do repozytorium."
-
-#: templates/catalogue/document_upload.html:55
-msgid "Uploaded files"
-msgstr "Dodane pliki"
-
-#: templates/catalogue/document_upload.html:65
-msgid "Skipped files"
-msgstr "Pominięte pliki"
-
-#: templates/catalogue/document_upload.html:66
-msgid "Files skipped due to no <code>.xml</code> extension"
-msgstr "Pliki pominięte z powodu braku rozszerzenia <code>.xml</code>."
-
-#: templates/catalogue/head_login.html:10
-msgid "Admin"
-msgstr "Administracja"
-
-#: templates/catalogue/head_login.html:15
-msgid "Log Out"
-msgstr "Wyloguj"
-
-#: templates/catalogue/head_login.html:21
-msgid "Log In"
-msgstr "Zaloguj"
-
-#: templates/catalogue/image_detail.html:34
-msgid "Editor"
-msgstr "Edytor"
-
-#: templates/catalogue/image_detail.html:38
-msgid "Proceed to the editor."
-msgstr "Przejdź do edytora."
-
-#: templates/catalogue/image_list.html:8
-msgid "Image list"
-msgstr "Lista obrazów"
-
-#: templates/catalogue/image_short.html:6
-msgid "Image settings"
-msgstr "Ustawienia obrazu"
-
-#: templates/catalogue/image_table.html:79
-#, python-format
-msgid "%(c)s image"
-msgid_plural "%(c)s images"
-msgstr[0] "%(c)s obraz"
-msgstr[1] "%(c)s obrazy"
-msgstr[2] "%(c)s obrazów"
-
-#: templates/catalogue/image_table.html:81
-msgid "No images found."
-msgstr "Nie znaleziono obrazów."
-
-#: templates/catalogue/my_page.html:15 templatetags/catalogue.py:28
-msgid "My page"
-msgstr "Moja strona"
-
-#: templates/catalogue/my_page.html:25
-msgid "Your last edited documents"
-msgstr "Twoje ostatnie edycje"
-
-#: templates/catalogue/my_page.html:45 templates/catalogue/user_page.html:18
-msgid "Recent activity for"
-msgstr "Ostatnia aktywność dla:"
-
-#: templates/catalogue/upload_pdf.html:5 templates/catalogue/upload_pdf.html:11
-msgid "PDF file upload"
-msgstr "Ładowanie pliku PDF"
-
-#: templates/catalogue/user_list.html:7 templates/catalogue/user_list.html:14
-#: templatetags/catalogue.py:33
-msgid "Users"
-msgstr "Użytkownicy"
-
-#: templates/catalogue/wall.html:30
-msgid "not logged in"
-msgstr "nie zalogowany"
-
-#: templates/catalogue/wall.html:35
-msgid "No activity recorded."
-msgstr "Nie zanotowano aktywności."
-
-#: templatetags/book_list.py:83 templatetags/book_list.py:151
-msgid "unpublished"
-msgstr "nie opublikowane"
-
-#: templatetags/book_list.py:84 templatetags/book_list.py:152
-msgid "empty"
-msgstr "puste"
-
-#: templatetags/catalogue.py:31
-msgid "All"
-msgstr "Wszystkie"
-
-#: templatetags/catalogue.py:32
-msgid "Images"
-msgstr "Obrazy"
-
-#: templatetags/catalogue.py:36
-msgid "Add"
-msgstr "Dodaj"
-
-#: templatetags/catalogue.py:39
-msgid "Covers"
-msgstr "Okładki"
-
-#: templatetags/wall.py:49 templatetags/wall.py:78
-msgid "Related edit"
-msgstr "Powiązana zmiana"
-
-#: templatetags/wall.py:51 templatetags/wall.py:80
-msgid "Edit"
-msgstr "Zmiana"
-
-#: views.py:172
-#, python-format
-msgid "Slug already used for %s"
-msgstr "Slug taki sam jak dla pliku %s"
-
-#: views.py:174
-msgid "Slug already used in repository."
-msgstr "Dokument o tym slugu już istnieje w repozytorium."
-
-#: views.py:180
-msgid "File should be UTF-8 encoded."
-msgstr "Plik powinien mieć kodowanie UTF-8."
-
-#: views.py:621
-msgid "scan gallery"
-msgstr "galeria skanów"
-
-#~ msgid "Active users since"
-#~ msgstr "Użytkownicy aktywni od"
-
-#~ msgid "Show hidden books"
-#~ msgstr "Pokaż ukryte książki"
-
-#~ msgid "Comment"
-#~ msgstr "Komentarz"
-
-#~ msgid "Comments"
-#~ msgstr "Komentarze"
-
-#~ msgid "Mark publishable"
-#~ msgstr "Oznacz do publikacji"
-
-#~ msgid "Mark not publishable"
-#~ msgstr "Odznacz do publikacji"
-
-#~ msgid "Other user"
-#~ msgstr "Inny użytkownik"
-
-#~ msgid "edit"
-#~ msgstr "edytuj"
-
-#~ msgid "add basic document structure"
-#~ msgstr "dodaj podstawową strukturę dokumentu"
-
-#~ msgid "change master tag to"
-#~ msgstr "zmień tak master na"
-
-#~ msgid "add begin trimming tag"
-#~ msgstr "dodaj początkowy ogranicznik"
-
-#~ msgid "add end trimming tag"
-#~ msgstr "dodaj końcowy ogranicznik"
-
-#~ msgid "unstructured text"
-#~ msgstr "tekst bez struktury"
-
-#~ msgid "unknown XML"
-#~ msgstr "nieznany XML"
-
-#~ msgid "broken document"
-#~ msgstr "uszkodzony dokument"
-
-#~ msgid "Apply fixes"
-#~ msgstr "Wykonaj zmiany"
-
-#~ msgid "Can mark for publishing"
-#~ msgstr "Oznacza do publikacji"
-
-#~ msgid "Author"
-#~ msgstr "Autor"
-
-#~ msgid "Your name"
-#~ msgstr "Imię i nazwisko"
-
-#~ msgid "Author's email"
-#~ msgstr "E-mail autora"
-
-#~ msgid "Your email address, so we can show a gravatar :)"
-#~ msgstr "Adres e-mail, żebyśmy mogli pokazać gravatar :)"
-
-#~ msgid "Describe changes you made."
-#~ msgstr "Opisz swoje zmiany"
-
-#~ msgid "Completed"
-#~ msgstr "Ukończono"
-
-#~ msgid "If you completed a life cycle stage, select it."
-#~ msgstr "Jeśli został ukończony etap prac, wskaż go."
-
-#~ msgid "Describe the reason for reverting."
-#~ msgstr "Opisz powód przywrócenia."
-
-#~ msgid "theme"
-#~ msgstr "motyw"
-
-#~ msgid "themes"
-#~ msgstr "motywy"
-
-#~ msgid "Tag added"
-#~ msgstr "Dodano tag"
-
-#~ msgid "Revision marked"
-#~ msgstr "Wersja oznaczona"
-
-#~ msgid "New version"
-#~ msgstr "Nowa wersja"
-
-#~ msgid "Click to open/close gallery"
-#~ msgstr "Kliknij, aby (ro)zwinąć galerię"
-
-#~ msgid "Help"
-#~ msgstr "Pomoc"
-
-#~ msgid "Version"
-#~ msgstr "Wersja"
-
-#~ msgid "Unknown"
-#~ msgstr "nieznana"
-
-#~ msgid "Save attempt in progress"
-#~ msgstr "Trwa zapisywanie"
-
-#~ msgid "There is a newer version of this document!"
-#~ msgstr "Istnieje nowsza wersja tego dokumentu!"
-
-#~ msgid "Clear filter"
-#~ msgstr "Wyczyść filtr"
-
-#~ msgid "Cancel"
-#~ msgstr "Anuluj"
-
-#~ msgid "Revert"
-#~ msgstr "Przywróć"
-
-#~ msgid "all"
-#~ msgstr "wszystkie"
-
-#~ msgid "Annotations"
-#~ msgstr "Przypisy"
-
-#~ msgid "Previous"
-#~ msgstr "Poprzednie"
-
-#~ msgid "Next"
-#~ msgstr "Następne"
-
-#~ msgid "Zoom in"
-#~ msgstr "Powiększ"
-
-#~ msgid "Zoom out"
-#~ msgstr "Zmniejsz"
-
-#~ msgid "Gallery"
-#~ msgstr "Galeria"
-
-#~ msgid "Compare versions"
-#~ msgstr "Porównaj wersje"
-
-#~ msgid "Revert document"
-#~ msgstr "Przywróć wersję"
-
-#~ msgid "View version"
-#~ msgstr "Zobacz wersję"
-
-#~ msgid "History"
-#~ msgstr "Historia"
-
-#~ msgid "Search"
-#~ msgstr "Szukaj"
-
-#~ msgid "Replace with"
-#~ msgstr "Zamień na"
-
-#~ msgid "Replace"
-#~ msgstr "Zamień"
-
-#~ msgid "Options"
-#~ msgstr "Opcje"
-
-#~ msgid "Case sensitive"
-#~ msgstr "Rozróżniaj wielkość liter"
-
-#~ msgid "From cursor"
-#~ msgstr "Zacznij od kursora"
-
-#~ msgid "Search and replace"
-#~ msgstr "Znajdź i zamień"
-
-#~ msgid "Source code"
-#~ msgstr "Kod źródłowy"
-
-#~ msgid "Title"
-#~ msgstr "Tytuł"
-
-#~ msgid "Document ID"
-#~ msgstr "ID dokumentu"
-
-#~ msgid "Current version"
-#~ msgstr "Aktualna wersja"
-
-#~ msgid "Last edited by"
-#~ msgstr "Ostatnio edytowane przez"
-
-#~ msgid "Summary"
-#~ msgstr "Podsumowanie"
-
-#~ msgid "Insert theme"
-#~ msgstr "Wstaw motyw"
-
-#~ msgid "Insert annotation"
-#~ msgstr "Wstaw przypis"
-
-#~ msgid "Visual editor"
-#~ msgstr "Edytor wizualny"
-
-#~ msgid "Unassigned"
-#~ msgstr "Nie przypisane"
-
-#~ msgid "First correction"
-#~ msgstr "Autokorekta"
-
-#~ msgid "Tagging"
-#~ msgstr "Tagowanie"
-
-#~ msgid "Initial Proofreading"
-#~ msgstr "Korekta"
-
-#~ msgid "Annotation Proofreading"
-#~ msgstr "Sprawdzenie przypisów źródła"
-
-#~ msgid "Modernisation"
-#~ msgstr "Uwspółcześnienie"
-
-#~ msgid "Themes"
-#~ msgstr "Motywy"
-
-#~ msgid "Editor's Proofreading"
-#~ msgstr "Ostateczna redakcja literacka"
-
-#~ msgid "Technical Editor's Proofreading"
-#~ msgstr "Ostateczna redakcja techniczna"
-
-#~ msgid "Finished stage: %s"
-#~ msgstr "Ukończony etap: %s"
-
-#~ msgid "Refresh"
-#~ msgstr "Odśwież"
diff --git a/src/catalogue/management/__init__.py b/src/catalogue/management/__init__.py
deleted file mode 100644 (file)
index 6f6f6b6..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from collections import defaultdict
-from django.db import transaction
-from lxml import etree
-
-
-class XmlUpdater(object):
-    """A base class for massive XML updates.
-
-    In a subclass, override `fix_tree` and/or use `fixes_field` decorator.
-    Attributes:
-    * commit_desc: commits description
-    * retain_publishable: set publishable if head is (default: True)
-    * only_first_chunk: process only first chunks of books (default: False)
-    """
-    commit_desc = "auto-update"
-    retain_publishable = True
-    only_first_chunk = False
-
-    _element_fixers = defaultdict(list)
-
-    def __init__(self):
-        self.counters = defaultdict(lambda: 0)
-
-    @classmethod
-    def fixes_elements(cls, xpath):
-        """Decorator, registering a function as a fixer for given field type.
-
-        Any decorated function will be called like
-            f(element, change=..., verbose=...)
-        providing changeset as context.
-
-        :param xpath: element lookup, e.g. ".//{namespace-uri}tag-name"
-        :returns: True if anything changed
-        """
-        def wrapper(fixer):
-            cls._element_fixers[xpath].append(fixer)
-            return fixer
-        return wrapper
-
-    def fix_tree(self, tree, verbose):
-        """Override to provide general tree-fixing mechanism.
-
-        :param tree: the parsed XML tree
-        :param verbose: verbosity level
-        :returns: True if anythig changed
-        """
-        return False
-
-    def fix_chunk(self, chunk, user, verbose=0, dry_run=False):
-        """Runs the update for a single chunk."""
-        if verbose >= 2:
-            print(chunk.get_absolute_url())
-        old_head = chunk.head
-        src = old_head.materialize()
-        try:
-            tree = etree.fromstring(src)
-        except:
-            if verbose:
-                print("%s: invalid XML" % chunk.get_absolute_url())
-            self.counters['Bad XML'] += 1
-            return
-
-        dirty = False
-        # Call the general fixing function.
-        if self.fix_tree(tree, verbose=verbose):
-            dirty = True
-        # Call the registered fixers.
-        for xpath, fixers in self._element_fixers.items():
-            for elem in tree.findall(xpath):
-                for fixer in fixers:
-                    if fixer(elem, change=old_head, verbose=verbose):
-                        dirty = True
-
-        if not dirty:
-            self.counters['Clean'] += 1
-            return
-
-        if not dry_run:
-            new_head = chunk.commit(
-                etree.tostring(tree, encoding='unicode'),
-                author=user,
-                description=self.commit_desc
-            )
-            if self.retain_publishable:
-                if old_head.publishable:
-                    new_head.set_publishable(True)
-        if verbose >= 2:
-            print("done")
-        self.counters['Updated chunks'] += 1
-
-    def run(self, user, verbose=0, dry_run=False, books=None):
-        """Runs the actual update."""
-        if books is None:
-            from catalogue.models import Book
-            books = Book.objects.all()
-
-        # Start transaction management.
-        with transaction.atomic():
-            for book in books:
-                self.counters['All books'] += 1
-                chunks = book.chunk_set.all()
-                if self.only_first_chunk:
-                    chunks = chunks[:1]
-                for chunk in chunks:
-                    self.counters['All chunks'] += 1
-                    self.fix_chunk(chunk, user, verbose, dry_run)
-
-    def print_results(self):
-        """Prints the counters."""
-        for item in sorted(self.counters.items()):
-            print("%s: %d" % item)
diff --git a/src/catalogue/management/commands/__init__.py b/src/catalogue/management/commands/__init__.py
deleted file mode 100644 (file)
index 6e45066..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-import sys
-from django.contrib.auth.models import User
-from django.core.management.base import BaseCommand
-from catalogue.models import Book
-
-
-class XmlUpdaterCommand(BaseCommand):
-    """Base class for creating massive XML-updating commands.
-
-    In a subclass, provide an XmlUpdater class in the `updater' attribute.
-    """
-    args = "[slug]..."
-
-    def add_arguments(self, parser):
-        parser.add_argument(
-            '-q', '--quiet', action='store_false', dest='verbose',
-            default=True, help='Less output')
-        parser.add_argument(
-            '-d', '--dry-run', action='store_true', dest='dry_run',
-            default=False, help="Don't actually touch anything")
-        parser.add_argument(
-            '-u', '--username', dest='username', metavar='USER',
-            help='Assign commits to this user (required, preferably yourself).')
-
-    def handle(self, *args, **options):
-        verbose = options.get('verbose')
-        dry_run = options.get('dry_run')
-        username = options.get('username')
-
-        if username:
-            user = User.objects.get(username=username)
-        else:
-            print('Please provide a username.')
-            sys.exit(1)
-
-        books = Book.objects.filter(slug__in=args) if args else None
-
-        updater = self.updater()
-        updater.run(user, verbose=verbose, dry_run=dry_run, books=books)
-        updater.print_results()
diff --git a/src/catalogue/management/commands/add_parent.py b/src/catalogue/management/commands/add_parent.py
deleted file mode 100644 (file)
index 49dff28..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-import sys
-
-from datetime import date
-from lxml import etree
-
-from django.core.management import BaseCommand
-
-from catalogue.models import Book
-from librarian import RDFNS, DCNS
-
-TEMPLATE = '''<utwor>
-<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
-<rdf:Description rdf:about="http://redakcja.wolnelektury.pl/documents/book/%(slug)s/">
-%(dc)s
-</rdf:Description>
-</rdf:RDF>
-
-</utwor>
-'''
-
-DC_TEMPLATE = '<dc:%(tag)s xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">%(value)s</dc:%(tag)s>'
-
-DC_TAGS = (
-    'creator',
-    'title',
-    'relation.hasPart',
-    'contributor.translator',
-    'contributor.editor',
-    'contributor.technical_editor',
-    'contributor.funding',
-    'contributor.thanks',
-    'publisher',
-    'subject.period',
-    'subject.type',
-    'subject.genre',
-    'description',
-    'identifier.url',
-    'source',
-    'source.URL',
-    'rights.license',
-    'rights',
-    'date.pd',
-    'format',
-    'type',
-    'date',
-    'audience',
-    'language',
-)
-
-IDENTIFIER_PREFIX = 'http://wolnelektury.pl/katalog/lektura/'
-
-
-def dc_desc_element(book):
-    xml = book.materialize()
-    tree = etree.fromstring(xml)
-    return tree.find(".//" + RDFNS("Description"))
-
-
-def distinct_dc_values(tag, desc_elements):
-    values = set()
-    for desc in desc_elements:
-        values.update(elem.text for elem in desc.findall(DCNS(tag)))
-    return values
-
-
-class Command(BaseCommand):
-    args = 'slug'
-
-    def handle(self, slug, **options):
-        children_slugs = [line.strip() for line in sys.stdin]
-        children = Book.objects.filter(dc_slug__in=children_slugs)
-        desc_elements = [dc_desc_element(child) for child in children]
-        title = u'Utwory wybrane'
-        own_attributes = {
-            'title': title,
-            'relation.hasPart': [IDENTIFIER_PREFIX + child_slug for child_slug in children_slugs],
-            'identifier.url': IDENTIFIER_PREFIX + slug,
-            'date': date.today().isoformat(),
-        }
-        dc_tags = []
-        for tag in DC_TAGS:
-            if tag in own_attributes:
-                values = own_attributes[tag]
-                if not isinstance(values, list):
-                    values = [values]
-            else:
-                values = distinct_dc_values(tag, desc_elements)
-            for value in values:
-                dc_tags.append(DC_TEMPLATE % {'tag': tag, 'value': value})
-        xml = TEMPLATE % {'slug': slug, 'dc': '\n'.join(dc_tags)}
-        Book.create(
-            text=xml,
-            creator=None,
-            slug=slug,
-            title=title,
-            gallery=slug)
diff --git a/src/catalogue/management/commands/fixdc.py b/src/catalogue/management/commands/fixdc.py
deleted file mode 100644 (file)
index cd69172..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from librarian import RDFNS, WLURI, ValidationError
-from librarian.dcparser import BookInfo
-from catalogue.management import XmlUpdater
-from catalogue.management.commands import XmlUpdaterCommand
-
-
-class FixDC(XmlUpdater):
-    commit_desc = "auto-fixing DC"
-    retain_publishable = True
-    only_first_chunk = True
-
-    def fix_wluri(elem, change, verbose):
-        try:
-            WLURI.strict(elem.text)
-        except ValidationError:
-            correct_field = str(WLURI.from_slug(
-                                WLURI(elem.text.strip()).slug))
-            try:
-                WLURI.strict(correct_field)
-            except ValidationError:
-                # Can't make a valid WLURI out of it, leave as is.
-                return False
-            if verbose:
-                print("Changing %s from %s to %s" % (
-                        elem.tag, elem.text, correct_field
-                    ))
-            elem.text = correct_field
-            return True
-    for field in BookInfo.FIELDS:
-        if field.validator == WLURI:
-            XmlUpdater.fixes_elements('.//' + field.uri)(fix_wluri)
-
-    @XmlUpdater.fixes_elements(".//" + RDFNS("Description"))
-    def fix_rdfabout(elem, change, verbose):
-        correct_about = change.tree.book.correct_about()
-        attr_name = RDFNS("about")
-        current_about = elem.get(attr_name)
-        if current_about != correct_about:
-            if verbose:
-                print("Changing rdf:about from %s to %s" % (
-                        current_about, correct_about
-                    ))
-            elem.set(attr_name, correct_about)
-            return True
-
-
-class Command(XmlUpdaterCommand):
-    updater = FixDC
-    help = 'Fixes obvious errors in DC: rdf:about and WLURI format.'
diff --git a/src/catalogue/management/commands/import_wl.py b/src/catalogue/management/commands/import_wl.py
deleted file mode 100644 (file)
index bb891ed..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from collections import defaultdict
-import json
-from urllib.request import urlopen
-
-from django.core.management.base import BaseCommand
-from django.core.management.color import color_style
-from django.db import transaction
-from librarian.dcparser import BookInfo
-from librarian import ParseError, ValidationError
-
-from catalogue.models import Book
-
-
-WL_API = 'http://www.wolnelektury.pl/api/books/'
-
-
-class Command(BaseCommand):
-    help = 'Imports XML files from WL.'
-
-    def add_arguments(self, parser):
-        parser.add_argument('-q', '--quiet', action='store_false', dest='verbose', default=True,
-            help='Less output')
-
-    def handle(self, *args, **options):
-
-        self.style = color_style()
-
-        verbose = options.get('verbose')
-
-        # Start transaction management.
-        transaction.enter_transaction_management()
-
-        if verbose:
-            print('Reading currently managed files (skipping hidden ones).')
-        slugs = defaultdict(list)
-        for b in Book.objects.exclude(slug__startswith='.').all():
-            if verbose:
-                print(b.slug)
-            text = b.materialize().encode('utf-8')
-            try:
-                info = BookInfo.from_bytes(text)
-            except (ParseError, ValidationError):
-                pass
-            else:
-                slugs[info.slug].append(b)
-
-        book_count = 0
-        commit_args = {
-            "author_name": 'Platforma',
-            "description": 'Automatycznie zaimportowane z Wolnych Lektur',
-            "publishable": True,
-        }
-
-        if verbose:
-            print('Opening books list')
-        for book in json.load(urlopen(WL_API)):
-            book_detail = json.load(urlopen(book['href']))
-            xml_text = urlopen(book_detail['xml']).read()
-            info = BookInfo.from_bytes(xml_text)
-            previous_books = slugs.get(info.slug)
-            if previous_books:
-                if len(previous_books) > 1:
-                    print(self.style.ERROR("There is more than one book "
-                        "with slug %s:") % info.slug)
-                previous_book = previous_books[0]
-                comm = previous_book.slug
-            else:
-                previous_book = None
-                comm = '*'
-            print(book_count, info.slug , '-->', comm)
-            Book.import_xml_text(xml_text, title=info.title[:255],
-                slug=info.slug[:128], previous_book=previous_book,
-                commit_args=commit_args)
-            book_count += 1
-
-        # Print results
-        print()
-        print("Results:")
-        print("Imported %d books from WL:" % (
-                book_count, ))
-        print()
-
-
-        transaction.commit()
-        transaction.leave_transaction_management()
-
diff --git a/src/catalogue/management/commands/insert_isbn.py b/src/catalogue/management/commands/insert_isbn.py
deleted file mode 100644 (file)
index 5bacb4b..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-import csv
-
-import sys
-from django.contrib.auth.models import User
-from lxml import etree
-from collections import defaultdict
-from django.core.management import BaseCommand
-
-from catalogue.models import Book
-from librarian import RDFNS, DCNS
-
-CONTENT_TYPES = {
-    'pdf':  'application/pdf',
-    'epub': 'application/epub+zip',
-    'mobi': 'application/x-mobipocket-ebook',
-    'txt':  'text/plain',
-    'html': 'text/html',
-}
-
-
-ISBN_TEMPLATES = (
-    r'<dc:relation.hasFormat id="%(format)s" xmlns:dc="http://purl.org/dc/elements/1.1/">%(url)s'
-    r'</dc:relation.hasFormat>',
-    r'<meta refines="#%(format)s" id="%(format)s-id" property="dcterms:identifier">ISBN-%(isbn)s</meta>',
-    r'<meta refines="#%(format)s-id" property="identifier-type">ISBN</meta>',
-    r'<meta refines="#%(format)s" property="dcterms:format">%(content_type)s</meta>',
-)
-
-
-def url_for_format(slug, format):
-    if format == 'html':
-        return 'https://wolnelektury.pl/katalog/lektura/%s.html' % slug
-    else:
-        return 'http://wolnelektury.pl/media/book/%(format)s/%(slug)s.%(format)s' % {'slug': slug, 'format': format}
-
-
-class Command(BaseCommand):
-    args = 'csv_file'
-
-    def add_arguments(self, parser):
-        self.add_argument(
-            '-u', '--username', dest='username', metavar='USER',
-            help='Assign commits to this user (required, preferably yourself).')
-
-    def handle(self, csv_file, **options):
-        username = options.get('username')
-
-        if username:
-            user = User.objects.get(username=username)
-        else:
-            print('Please provide a username.')
-            sys.exit(1)
-
-        csvfile = open(csv_file, 'rb')
-        isbn_lists = defaultdict(list)
-        for slug, format, isbn in csv.reader(csvfile, delimiter=','):
-            isbn_lists[slug].append((format, isbn))
-        csvfile.close()
-
-        for slug, isbn_list in isbn_lists.iteritems():
-            print('processing %s' % slug)
-            book = Book.objects.get(dc_slug=slug)
-            chunk = book.chunk_set.first()
-            old_head = chunk.head
-            src = old_head.materialize()
-            tree = etree.fromstring(src)
-            isbn_node = tree.find('.//' + DCNS("relation.hasFormat"))
-            if isbn_node is not None:
-                print('%s already contains ISBN metadata, skipping' % slug)
-                continue
-            desc = tree.find(".//" + RDFNS("Description"))
-            for format, isbn in isbn_list:
-                for template in ISBN_TEMPLATES:
-                    isbn_xml = template % {
-                        'format': format,
-                        'isbn': isbn,
-                        'content_type': CONTENT_TYPES[format],
-                        'url': url_for_format(slug, format),
-                    }
-                    element = etree.XML(isbn_xml)
-                    element.tail = '\n'
-                    desc.append(element)
-            new_head = chunk.commit(
-                etree.tostring(tree, encoding='unicode'),
-                author=user,
-                description='automatyczne dodanie isbn'
-            )
-            print('committed %s' % slug)
-            if old_head.publishable:
-                new_head.set_publishable(True)
-            else:
-                print('Warning: %s not publishable' % slug)
diff --git a/src/catalogue/management/commands/mark_final.py b/src/catalogue/management/commands/mark_final.py
deleted file mode 100644 (file)
index 11d63a3..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-import sys
-from django.contrib.auth.models import User
-from django.core.management import BaseCommand
-
-from catalogue.models import Book, Chunk
-
-
-class Command(BaseCommand):
-    args = 'slug_file'
-
-    def add_arguments(self, parser):
-        self.add_argument(
-            '-u', '--username', dest='username', metavar='USER',
-            help='Assign commits to this user (required).')
-
-    def handle(self, slug_file, **options):
-        username = options.get('username')
-
-        if username:
-            user = User.objects.get(username=username)
-        else:
-            print('Please provide a username.')
-            sys.exit(1)
-
-        slugs = [line.strip() for line in open(slug_file)]
-        books = Book.objects.filter(slug__in=slugs)
-
-        for book in books:
-            print('processing %s' % book.slug)
-            for chunk in book.chunk_set.all():
-                src = chunk.head.materialize()
-                chunk.commit(
-                    text=src,
-                    author=user,
-                    description=u'Ostateczna akceptacja merytoryczna przez kierownika literackiego.',
-                    tags=[Chunk.tag_model.objects.get(slug='editor-proofreading')],
-                    publishable=True
-                )
-            print('committed %s' % book.slug)
diff --git a/src/catalogue/management/commands/merge_books.py b/src/catalogue/management/commands/merge_books.py
deleted file mode 100644 (file)
index cebf73b..0000000
+++ /dev/null
@@ -1,220 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-import sys
-
-from django.contrib.auth.models import User
-from django.core.management.base import BaseCommand
-from django.core.management.color import color_style
-from django.db import transaction
-
-from catalogue.models import Book
-
-
-def common_prefix(texts):
-    common = []
-
-    min_len = min(len(text) for text in texts)
-    for i in range(min_len):
-        chars = list(set([text[i] for text in texts]))
-        if len(chars) > 1:
-            break
-        common.append(chars[0])
-    return "".join(common)
-
-
-class Command(BaseCommand):
-    help = 'Merges multiple books into one.'
-    args = '[slug]...'
-
-    def add_arguments(self, parser):
-        self.add_argument(
-            '-s', '--slug', dest='new_slug', metavar='SLUG',
-            help='New slug of the merged book (defaults to common part of all slugs).')
-        self.add_argument(
-            '-t', '--title', dest='new_title', metavar='TITLE',
-            help='New title of the merged book (defaults to common part of all titles).')
-        self.add_argument(
-            '-q', '--quiet', action='store_false', dest='verbose', default=True,
-            help='Less output')
-        self.add_argument(
-            '-g', '--guess', action='store_true', dest='guess', default=False,
-            help='Try to guess what merges are needed (but do not apply them).')
-        self.add_argument(
-            '-d', '--dry-run', action='store_true', dest='dry_run', default=False,
-            help='Dry run: do not actually change anything.')
-        self.add_argument(
-            '-f', '--force', action='store_true', dest='force', default=False,
-            help='On slug conflict, hide the original book to archive.')
-
-    def print_guess(self, dry_run=True, force=False):
-        from collections import defaultdict
-        from pipes import quote
-        import re
-    
-        def read_slug(slug):
-            res = []
-            res.append((re.compile(r'__?(przedmowa)$'), -1))
-            res.append((re.compile(r'__?(cz(esc)?|ksiega|rozdzial)__?(?P<n>\d*)$'), None))
-            res.append((re.compile(r'__?(rozdzialy__?)?(?P<n>\d*)-'), None))
-        
-            for r, default in res:
-                m = r.search(slug)
-                if m:
-                    start = m.start()
-                    try:
-                        return int(m.group('n')), slug[:start]
-                    except IndexError:
-                        return default, slug[:start]
-            return None, slug
-    
-        def file_to_title(fname):
-            """ Returns a title-like version of a filename. """
-            parts = (p.replace('_', ' ').title() for p in fname.split('__'))
-            return ' / '.join(parts)
-    
-        merges = defaultdict(list)
-        slugs = []
-        for b in Book.objects.all():
-            slugs.append(b.slug)
-            n, ns = read_slug(b.slug)
-            if n is not None:
-                merges[ns].append((n, b))
-    
-        conflicting_slugs = []
-        for slug in sorted(merges.keys()):
-            merge_list = sorted(merges[slug])
-            if len(merge_list) < 2:
-                continue
-    
-            merge_slugs = [b.slug for i, b in merge_list]
-            if slug in slugs and slug not in merge_slugs:
-                conflicting_slugs.append(slug)
-    
-            title = file_to_title(slug)
-            print("./manage.py merge_books %s%s--title=%s --slug=%s \\\n    %s\n" % (
-                '--dry-run ' if dry_run else '',
-                '--force ' if force else '',
-                quote(title), slug,
-                " \\\n    ".join(merge_slugs)
-                ))
-    
-        if conflicting_slugs:
-            if force:
-                print(self.style.NOTICE('# These books will be archived:'))
-            else:
-                print(self.style.ERROR('# ERROR: Conflicting slugs:'))
-            for slug in conflicting_slugs:
-                print('#', slug)
-
-
-    def handle(self, *slugs, **options):
-
-        self.style = color_style()
-
-        force = options.get('force')
-        guess = options.get('guess')
-        dry_run = options.get('dry_run')
-        new_slug = options.get('new_slug').decode('utf-8')
-        new_title = options.get('new_title').decode('utf-8')
-        verbose = options.get('verbose')
-
-        if guess:
-            if slugs:
-                print("Please specify either slugs, or --guess.")
-                return
-            else:
-                self.print_guess(dry_run, force)
-                return
-        if not slugs:
-            print("Please specify some book slugs")
-            return
-
-        # Start transaction management.
-        transaction.enter_transaction_management()
-
-        books = [Book.objects.get(slug=slug) for slug in slugs]
-        common_slug = common_prefix(slugs)
-        common_title = common_prefix([b.title for b in books])
-
-        if not new_title:
-            new_title = common_title
-        elif common_title.startswith(new_title):
-            common_title = new_title
-
-        if not new_slug:
-            new_slug = common_slug
-        elif common_slug.startswith(new_slug):
-            common_slug = new_slug
-
-        if slugs[0] != new_slug and Book.objects.filter(slug=new_slug).exists():
-            self.style.ERROR('Book already exists, skipping!')
-
-
-        if dry_run and verbose:
-            print(self.style.NOTICE('DRY RUN: nothing will be changed.'))
-            print()
-
-        if verbose:
-            print("New title:", self.style.NOTICE(new_title))
-            print("New slug:", self.style.NOTICE(new_slug))
-            print()
-
-        for i, book in enumerate(books):
-            chunk_titles = []
-            chunk_slugs = []
-
-            book_title = book.title[len(common_title):].replace(' / ', ' ').lstrip()
-            book_slug = book.slug[len(common_slug):].replace('__', '_').lstrip('-_')
-            for j, chunk in enumerate(book):
-                if j:
-                    new_chunk_title = book_title + '_%d' % j
-                    new_chunk_slug = book_slug + '_%d' % j
-                else:
-                    new_chunk_title, new_chunk_slug = book_title, book_slug
-
-                chunk_titles.append(new_chunk_title)
-                chunk_slugs.append(new_chunk_slug)
-
-                if verbose:
-                    print("title: %s // %s  -->\n       %s // %s\nslug: %s / %s  -->\n      %s / %s" % (
-                        book.title, chunk.title,
-                        new_title, new_chunk_title,
-                        book.slug, chunk.slug,
-                        new_slug, new_chunk_slug))
-                    print()
-
-            if not dry_run:
-                try:
-                    conflict = Book.objects.get(slug=new_slug)
-                except Book.DoesNotExist:
-                    conflict = None
-                else:
-                    if conflict == books[0]:
-                        conflict = None
-
-                if conflict:
-                    if force:
-                        # FIXME: there still may be a conflict
-                        conflict.slug = '.' + conflict.slug
-                        conflict.save()
-                        print(self.style.NOTICE('Book with slug "%s" moved to "%s".' % (new_slug, conflict.slug)))
-                    else:
-                        print(self.style.ERROR('ERROR: Book with slug "%s" exists.' % new_slug))
-                        return
-
-                if i:
-                    books[0].append(books[i], slugs=chunk_slugs, titles=chunk_titles)
-                else:
-                    book.title = new_title
-                    book.slug = new_slug
-                    book.save()
-                    for j, chunk in enumerate(book):
-                        chunk.title = chunk_titles[j]
-                        chunk.slug = chunk_slugs[j]
-                        chunk.save()
-
-
-        transaction.commit()
-        transaction.leave_transaction_management()
-
diff --git a/src/catalogue/management/commands/prune_audience.py b/src/catalogue/management/commands/prune_audience.py
deleted file mode 100644 (file)
index a271e32..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-import sys
-from django.contrib.auth.models import User
-from lxml import etree
-
-from django.core.management import BaseCommand
-
-from catalogue.models import Book
-from librarian import DCNS
-
-
-class Command(BaseCommand):
-    args = 'exclude_file'
-
-    def add_arguments(self, parser):
-        parser.add_argument(
-            '-u', '--username', dest='username', metavar='USER',
-            help='Assign commits to this user (required, preferably yourself).')
-
-    def handle(self, exclude_file, **options):
-        username = options.get('username')
-
-        if username:
-            user = User.objects.get(username=username)
-        else:
-            print('Please provide a username.')
-            sys.exit(1)
-
-        excluded_slugs = [line.strip() for line in open(exclude_file, 'rb') if line.strip()]
-        books = Book.objects.exclude(slug__in=excluded_slugs)
-
-        for book in books:
-            if not book.is_published():
-                continue
-            print('processing %s' % book.slug)
-            chunk = book.chunk_set.first()
-            old_head = chunk.head
-            src = old_head.materialize()
-            tree = etree.fromstring(src)
-            audience_nodes = tree.findall('.//' + DCNS("audience"))
-            if not audience_nodes:
-                print('%s has no audience, skipping' % book.slug)
-                continue
-
-            for node in audience_nodes:
-                node.getparent().remove(node)
-
-            chunk.commit(
-                etree.tostring(tree, encoding='unicode'),
-                author=user,
-                description='automatyczne skasowanie audience',
-                publishable=old_head.publishable
-            )
-            print('committed %s' % book.slug)
-            if not old_head.publishable:
-                print('Warning: %s not publishable, last head: %s, %s' % (
-                    book.slug, old_head.author.username, old_head.description[:40].replace('\n', ' ')))
diff --git a/src/catalogue/managers.py b/src/catalogue/managers.py
deleted file mode 100644 (file)
index 137baaf..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.db import models
-
-class VisibleManager(models.Manager):
-    def get_queryset(self):
-        return super(VisibleManager, self).get_queryset().exclude(_hidden=True)
diff --git a/src/catalogue/models/__init__.py b/src/catalogue/models/__init__.py
deleted file mode 100644 (file)
index 86be6bc..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from catalogue.models.project import Project
-from catalogue.models.chunk import Chunk
-from catalogue.models.image import Image
-from catalogue.models.publish_log import (BookPublishRecord,
-    ChunkPublishRecord, ImagePublishRecord)
-from catalogue.models.book import Book
-from catalogue.models.listeners import *
-
-from django.contrib.auth.models import User as AuthUser
-
-class User(AuthUser):
-    class Meta:
-        proxy = True
-
-    def __str__(self):
-        return "%s %s" % (self.first_name, self.last_name)
diff --git a/src/catalogue/models/book.py b/src/catalogue/models/book.py
deleted file mode 100644 (file)
index fc0e18f..0000000
+++ /dev/null
@@ -1,433 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.contrib.sites.models import Site
-from django.db import models, transaction
-from django.template.loader import render_to_string
-from django.urls import reverse
-from django.utils.translation import ugettext_lazy as _
-from django.conf import settings
-from slugify import slugify
-
-
-import apiclient
-from catalogue.helpers import cached_in_field, GalleryMerger
-from catalogue.models import BookPublishRecord, ChunkPublishRecord, Project
-from catalogue.signals import post_publish
-from catalogue.xml_tools import compile_text, split_xml
-from cover.models import Image
-import os
-import shutil
-import re
-
-class Book(models.Model):
-    """ A document edited on the wiki """
-
-    title = models.CharField(_('title'), max_length=255, db_index=True)
-    slug = models.SlugField(_('slug'), max_length=128, unique=True, db_index=True)
-    public = models.BooleanField(_('public'), default=True, db_index=True)
-    gallery = models.CharField(_('scan gallery name'), max_length=255, blank=True)
-    project = models.ForeignKey(Project, models.SET_NULL, null=True, blank=True)
-
-    #wl_slug = models.CharField(_('title'), max_length=255, null=True, db_index=True, editable=False)
-    parent = models.ForeignKey('self', models.SET_NULL, null=True, blank=True, verbose_name=_('parent'), related_name="children", editable=False)
-    parent_number = models.IntegerField(_('parent number'), null=True, blank=True, db_index=True, editable=False)
-
-    # Cache
-    _single = models.NullBooleanField(editable=False, db_index=True)
-    _new_publishable = models.NullBooleanField(editable=False)
-    _published = models.NullBooleanField(editable=False)
-    _on_track = models.IntegerField(null=True, blank=True, db_index=True, editable=False)
-    dc_cover_image = models.ForeignKey(Image, blank=True, null=True,
-        db_index=True, on_delete=models.SET_NULL, editable=False)
-    dc_slug = models.CharField(max_length=128, null=True, blank=True,
-            editable=False, db_index=True)
-
-    class NoTextError(BaseException):
-        pass
-
-    class Meta:
-        app_label = 'catalogue'
-        ordering = ['title', 'slug']
-        verbose_name = _('book')
-        verbose_name_plural = _('books')
-
-
-    # Representing
-    # ============
-
-    def __iter__(self):
-        return iter(self.chunk_set.all())
-
-    def __getitem__(self, chunk):
-        return self.chunk_set.all()[chunk]
-
-    def __len__(self):
-        return self.chunk_set.count()
-
-    def __bool__(self):
-        """
-            Necessary so that __len__ isn't used for bool evaluation.
-        """
-        return True
-
-    def __str__(self):
-        return self.title
-
-    def get_absolute_url(self):
-        return reverse("catalogue_book", args=[self.slug])
-
-    def correct_about(self):
-        return "http://%s%s" % (
-            Site.objects.get_current().domain,
-            self.get_absolute_url()
-        )
-
-    def gallery_path(self):
-        return os.path.join(settings.MEDIA_ROOT, settings.IMAGE_DIR, self.gallery)
-
-    def gallery_url(self):
-        return '%s%s%s/' % (settings.MEDIA_URL, settings.IMAGE_DIR, self.gallery)
-
-    # Creating & manipulating
-    # =======================
-
-    def accessible(self, request):
-        return self.public or request.user.is_authenticated
-
-    @classmethod
-    @transaction.atomic
-    def create(cls, creator, text, *args, **kwargs):
-        b = cls.objects.create(*args, **kwargs)
-        b.chunk_set.all().update(creator=creator)
-        b[0].commit(text, author=creator)
-        return b
-
-    def add(self, *args, **kwargs):
-        """Add a new chunk at the end."""
-        return self.chunk_set.reverse()[0].split(*args, **kwargs)
-
-    @classmethod
-    @transaction.atomic
-    def import_xml_text(cls, text=u'', previous_book=None,
-                commit_args=None, **kwargs):
-        """Imports a book from XML, splitting it into chunks as necessary."""
-        texts = split_xml(text)
-        if previous_book:
-            instance = previous_book
-        else:
-            instance = cls(**kwargs)
-            instance.save()
-
-        # if there are more parts, set the rest to empty strings
-        book_len = len(instance)
-        for i in range(book_len - len(texts)):
-            texts.append((u'pusta część %d' % (i + 1), u''))
-
-        i = 0
-        for i, (title, text) in enumerate(texts):
-            if not title:
-                title = u'część %d' % (i + 1)
-
-            slug = slugify(title)
-
-            if i < book_len:
-                chunk = instance[i]
-                chunk.slug = slug[:50]
-                chunk.title = title[:255]
-                chunk.save()
-            else:
-                chunk = instance.add(slug, title)
-
-            chunk.commit(text, **commit_args)
-
-        return instance
-
-    def make_chunk_slug(self, proposed):
-        """ 
-            Finds a chunk slug not yet used in the book.
-        """
-        slugs = set(c.slug for c in self)
-        i = 1
-        new_slug = proposed[:50]
-        while new_slug in slugs:
-            new_slug = "%s_%d" % (proposed[:45], i)
-            i += 1
-        return new_slug
-
-    @transaction.atomic
-    def append(self, other, slugs=None, titles=None):
-        """Add all chunks of another book to self."""
-        assert self != other
-
-        number = self[len(self) - 1].number + 1
-        len_other = len(other)
-        single = len_other == 1
-
-        if slugs is not None:
-            assert len(slugs) == len_other
-        if titles is not None:
-            assert len(titles) == len_other
-            if slugs is None:
-                slugs = [slugify(t) for t in titles]
-
-        for i, chunk in enumerate(other):
-            # move chunk to new book
-            chunk.book = self
-            chunk.number = number
-
-            if titles is None:
-                # try some title guessing
-                if other.title.startswith(self.title):
-                    other_title_part = other.title[len(self.title):].lstrip(' /')
-                else:
-                    other_title_part = other.title
-
-                if single:
-                    # special treatment for appending one-parters:
-                    # just use the guessed title and original book slug
-                    chunk.title = other_title_part
-                    if other.slug.startswith(self.slug):
-                        chunk.slug = other.slug[len(self.slug):].lstrip('-_')
-                    else:
-                        chunk.slug = other.slug
-                else:
-                    chunk.title = ("%s, %s" % (other_title_part, chunk.title))[:255]
-            else:
-                chunk.slug = slugs[i]
-                chunk.title = titles[i]
-
-            chunk.slug = self.make_chunk_slug(chunk.slug)
-            chunk.save()
-            number += 1
-        assert not other.chunk_set.exists()
-
-        gm = GalleryMerger(self.gallery, other.gallery)
-        self.gallery = gm.merge()
-
-        # and move the gallery starts
-        if gm.was_merged:
-                for chunk in self[len(self) - len_other:]:
-                        old_start = chunk.gallery_start or 1
-                        chunk.gallery_start = old_start + gm.dest_size - gm.num_deleted
-                        chunk.save()
-
-        other.delete()
-
-
-    @transaction.atomic
-    def prepend_history(self, other):
-        """Prepend history from all the other book's chunks to own."""
-        assert self != other
-
-        for i in range(len(self), len(other)):
-            title = u"pusta część %d" % i
-            chunk = self.add(slugify(title), title)
-            chunk.commit('')
-
-        for i in range(len(other)):
-            self[i].prepend_history(other[0])
-
-        assert not other.chunk_set.exists()
-        other.delete()
-
-    def split(self):
-        """Splits all the chunks into separate books."""
-        self.title
-        for chunk in self:
-            book = Book.objects.create(title=chunk.title, slug=chunk.slug,
-                    public=self.public, gallery=self.gallery)
-            book[0].delete()
-            chunk.book = book
-            chunk.number = 1
-            chunk.save()
-        assert not self.chunk_set.exists()
-        self.delete()
-
-    # State & cache
-    # =============
-
-    def last_published(self):
-        try:
-            return self.publish_log.all()[0].timestamp
-        except IndexError:
-            return None
-
-    def assert_publishable(self):
-        assert self.chunk_set.exists(), _('No chunks in the book.')
-        try:
-            changes = self.get_current_changes(publishable=True)
-        except self.NoTextError:
-            raise AssertionError(_('Not all chunks have publishable revisions.'))
-
-        from librarian import NoDublinCore, ParseError, ValidationError
-
-        try:
-            bi = self.wldocument(changes=changes, strict=True).book_info
-        except ParseError as e:
-            raise AssertionError(_('Invalid XML') + ': ' + str(e))
-        except NoDublinCore:
-            raise AssertionError(_('No Dublin Core found.'))
-        except ValidationError as e:
-            raise AssertionError(_('Invalid Dublin Core') + ': ' + str(e))
-
-        valid_about = self.correct_about()
-        assert bi.about == valid_about, _("rdf:about is not") + " " + valid_about
-
-    def publishable_error(self):
-        try:
-            return self.assert_publishable()
-        except AssertionError as e:
-            return e
-        else:
-            return None
-
-    def hidden(self):
-        return self.slug.startswith('.')
-
-    def is_new_publishable(self):
-        """Checks if book is ready for publishing.
-
-        Returns True if there is a publishable version newer than the one
-        already published.
-
-        """
-        new_publishable = False
-        if not self.chunk_set.exists():
-            return False
-        for chunk in self:
-            change = chunk.publishable()
-            if not change:
-                return False
-            if not new_publishable and not change.publish_log.exists():
-                new_publishable = True
-        return new_publishable
-    new_publishable = cached_in_field('_new_publishable')(is_new_publishable)
-
-    def is_published(self):
-        return self.publish_log.exists()
-    published = cached_in_field('_published')(is_published)
-
-    def get_on_track(self):
-        if self.published:
-            return -1
-        stages = [ch.stage.ordering if ch.stage is not None else 0
-                    for ch in self]
-        if not len(stages):
-            return 0
-        return min(stages)
-    on_track = cached_in_field('_on_track')(get_on_track)
-
-    def is_single(self):
-        return len(self) == 1
-    single = cached_in_field('_single')(is_single)
-
-    def book_info(self, publishable=True):
-        try:
-            book_xml = self.materialize(publishable=publishable)
-        except self.NoTextError:
-            pass
-        else:
-            from librarian.dcparser import BookInfo
-            from librarian import NoDublinCore, ParseError, ValidationError
-            try:
-                return BookInfo.from_bytes(book_xml.encode('utf-8'))
-            except (self.NoTextError, ParseError, NoDublinCore, ValidationError):
-                return None
-
-    def refresh_dc_cache(self):
-        update = {
-            'dc_slug': None,
-            'dc_cover_image': None,
-        }
-
-        info = self.book_info()
-        if info is not None:
-            update['dc_slug'] = info.url.slug
-            if info.cover_source:
-                try:
-                    image = Image.objects.get(pk=int(info.cover_source.rstrip('/').rsplit('/', 1)[-1]))
-                except:
-                    pass
-                else:
-                    if info.cover_source == image.get_full_url():
-                        update['dc_cover_image'] = image
-        Book.objects.filter(pk=self.pk).update(**update)
-
-    def touch(self):
-        update = {
-            "_new_publishable": self.is_new_publishable(),
-            "_published": self.is_published(),
-            "_single": self.is_single(),
-            "_on_track": self.get_on_track(),
-        }
-        Book.objects.filter(pk=self.pk).update(**update)
-        self.refresh_dc_cache()
-
-    # Materializing & publishing
-    # ==========================
-
-    def get_current_changes(self, publishable=True):
-        """
-            Returns a list containing one Change for every Chunk in the Book.
-            Takes the most recent revision (publishable, if set).
-            Throws an error, if a proper revision is unavailable for a Chunk.
-        """
-        if publishable:
-            changes = [chunk.publishable() for chunk in self]
-        else:
-            changes = [chunk.head for chunk in self if chunk.head is not None]
-        if None in changes:
-            raise self.NoTextError('Some chunks have no available text.')
-        return changes
-
-    def materialize(self, publishable=False, changes=None):
-        """ 
-            Get full text of the document compiled from chunks.
-            Takes the current versions of all texts
-            or versions most recently tagged for publishing,
-            or a specified iterable changes.
-        """
-        if changes is None:
-            changes = self.get_current_changes(publishable)
-        return compile_text(change.materialize() for change in changes)
-
-    def wldocument(self, publishable=True, changes=None, 
-            parse_dublincore=True, strict=False):
-        from catalogue.ebook_utils import RedakcjaDocProvider
-        from librarian.parser import WLDocument
-
-        return WLDocument.from_bytes(
-                self.materialize(publishable=publishable, changes=changes).encode('utf-8'),
-                provider=RedakcjaDocProvider(publishable=publishable),
-                parse_dublincore=parse_dublincore,
-                strict=strict)
-
-    def publish(self, user, fake=False, host=None, days=0, beta=False):
-        """
-            Publishes a book on behalf of a (local) user.
-        """
-        self.assert_publishable()
-        changes = self.get_current_changes(publishable=True)
-        if not fake:
-            book_xml = self.materialize(changes=changes)
-            data = {"book_xml": book_xml, "days": days}
-            if host:
-                data['gallery_url'] = host + self.gallery_url()
-            apiclient.api_call(user, "books/", data, beta=beta)
-        if not beta:
-            # record the publish
-            br = BookPublishRecord.objects.create(book=self, user=user)
-            for c in changes:
-                ChunkPublishRecord.objects.create(book_record=br, change=c)
-            if not self.public and days == 0:
-                self.public = True
-                self.save()
-            if self.public and days > 0:
-                self.public = False
-                self.save()
-            post_publish.send(sender=br)
-
-    def latex_dir(self):
-        doc = self.wldocument()
-        return doc.latex_dir(cover=True, ilustr_path=self.gallery_path())
diff --git a/src/catalogue/models/chunk.py b/src/catalogue/models/chunk.py
deleted file mode 100644 (file)
index b3e6aca..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.conf import settings
-from django.db import models
-from django.db.utils import IntegrityError
-from django.template.loader import render_to_string
-from django.urls import reverse
-from django.utils.translation import ugettext_lazy as _
-from catalogue.helpers import cached_in_field
-from catalogue.managers import VisibleManager
-from dvcs import models as dvcs_models
-
-
-class Chunk(dvcs_models.Document):
-    """ An editable chunk of text. Every Book text is divided into chunks. """
-    REPO_PATH = settings.CATALOGUE_REPO_PATH
-
-    book = models.ForeignKey('Book', models.CASCADE, editable=False, verbose_name=_('book'))
-    number = models.IntegerField(_('number'))
-    title = models.CharField(_('title'), max_length=255, blank=True)
-    slug = models.SlugField(_('slug'))
-    gallery_start = models.IntegerField(_('gallery start'), null=True, blank=True, default=1)
-
-    # cache
-    _hidden = models.NullBooleanField(editable=False)
-    _changed = models.NullBooleanField(editable=False)
-    _new_publishable = models.NullBooleanField(editable=False)
-
-    # managers
-    objects = models.Manager()
-    visible_objects = VisibleManager()
-
-    class Meta:
-        app_label = 'catalogue'
-        unique_together = [['book', 'number'], ['book', 'slug']]
-        ordering = ['number']
-        verbose_name = _('chunk')
-        verbose_name_plural = _('chunks')
-        permissions = [('can_pubmark', 'Can mark for publishing')]
-
-    # Representing
-    # ============
-
-    def __str__(self):
-        return "%d:%d: %s" % (self.book_id, self.number, self.title)
-
-    def get_absolute_url(self):
-        return reverse("wiki_editor", args=[self.book.slug, self.slug])
-
-    def pretty_name(self, book_length=None):
-        title = self.book.title
-        if self.title:
-            title += ", %s" % self.title
-        if book_length and book_length > 1:
-            title += " (%d/%d)" % (self.number, book_length)
-        return title
-
-    # Creating and manipulation
-    # =========================
-
-    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)
-        new_chunk = None
-        while not new_chunk:
-            new_slug = self.book.make_chunk_slug(slug)
-            try:
-                new_chunk = self.book.chunk_set.create(
-                    number=self.number+1,
-                    slug=new_slug[:50], title=title[:255], **kwargs)
-            except IntegrityError:
-                pass
-        return new_chunk
-
-    @classmethod
-    def get(cls, book_slug, chunk_slug=None):
-        if chunk_slug is None:
-            return cls.objects.get(book__slug=book_slug, number=1)
-        else:
-            return cls.objects.get(book__slug=book_slug, slug=chunk_slug)
-
-    # State & cache
-    # =============
-
-    def is_new_publishable(self):
-        change = self.publishable()
-        if not change:
-            return False
-        return not change.publish_log.exists()
-    new_publishable = cached_in_field('_new_publishable')(is_new_publishable)
-
-    def is_changed(self):
-        if self.head is None:
-            return False
-        return not self.head.publishable
-    changed = cached_in_field('_changed')(is_changed)
-
-    def is_hidden(self):
-        return self.book.hidden()
-    hidden = cached_in_field('_hidden')(is_hidden)
-
-    def touch(self):
-        update = {
-            "_changed": self.is_changed(),
-            "_new_publishable": self.is_new_publishable(),
-            "_hidden": self.is_hidden(),
-        }
-        Chunk.objects.filter(pk=self.pk).update(**update)
diff --git a/src/catalogue/models/image.py b/src/catalogue/models/image.py
deleted file mode 100644 (file)
index becc308..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.conf import settings
-from django.contrib.sites.models import Site
-from django.db import models
-from django.template.loader import render_to_string
-from django.urls import reverse
-from django.utils.translation import ugettext_lazy as _
-from catalogue.helpers import cached_in_field
-from catalogue.models import Project
-from dvcs import models as dvcs_models
-
-
-class Image(dvcs_models.Document):
-    """ An editable chunk of text. Every Book text is divided into chunks. """
-    REPO_PATH = settings.CATALOGUE_IMAGE_REPO_PATH
-
-    image = models.FileField(_('image'), upload_to='catalogue/images')
-    title = models.CharField(_('title'), max_length=255, blank=True)
-    slug = models.SlugField(_('slug'), unique=True)
-    public = models.BooleanField(_('public'), default=True, db_index=True)
-    project = models.ForeignKey(Project, models.SET_NULL, null=True, blank=True)
-
-    # cache
-    _new_publishable = models.NullBooleanField(editable=False)
-    _published = models.NullBooleanField(editable=False)
-    _changed = models.NullBooleanField(editable=False)
-
-    class Meta:
-        app_label = 'catalogue'
-        ordering = ['title']
-        verbose_name = _('image')
-        verbose_name_plural = _('images')
-        permissions = [('can_pubmark_image', 'Can mark images for publishing')]
-
-    # Representing
-    # ============
-
-    def __str__(self):
-        return self.title
-
-    def get_absolute_url(self):
-        return reverse("catalogue_image", args=[self.slug])
-
-    def correct_about(self):
-        return ["http://%s%s" % (
-            Site.objects.get_current().domain,
-            self.get_absolute_url()
-            ),
-            "http://%s%s" % (
-                'obrazy.redakcja.wolnelektury.pl',
-                self.get_absolute_url()
-            )]
-
-    # State & cache
-    # =============
-
-    def last_published(self):
-        try:
-            return self.publish_log.all()[0].timestamp
-        except IndexError:
-            return None
-
-    def assert_publishable(self):
-        from librarian.picture import WLPicture
-        from librarian import NoDublinCore, ParseError, ValidationError
-
-        class SelfImageStore(object):
-            def path(self_, slug, mime_type):
-                """Returns own file object. Ignores slug ad mime_type."""
-                return open(self.image.path)
-
-        publishable = self.publishable()
-        assert publishable, _("There is no publishable revision")
-        picture_xml = publishable.materialize()
-
-        try:
-            picture = WLPicture.from_bytes(
-                    picture_xml.encode('utf-8'),
-                    image_store=SelfImageStore)
-        except ParseError as e:
-            raise AssertionError(_('Invalid XML') + ': ' + str(e))
-        except NoDublinCore:
-            raise AssertionError(_('No Dublin Core found.'))
-        except ValidationError as e:
-            raise AssertionError(_('Invalid Dublin Core') + ': ' + str(e))
-
-        valid_about = self.correct_about()
-        assert picture.picture_info.about in valid_about, \
-                _("rdf:about is not") + " " + valid_about[0]
-
-    def publishable_error(self):
-        try:
-            return self.assert_publishable()
-        except AssertionError as e:
-            return e
-        else:
-            return None
-
-    def accessible(self, request):
-        return self.public or request.user.is_authenticated
-
-    def is_new_publishable(self):
-        change = self.publishable()
-        if not change:
-            return False
-        return not change.publish_log.exists()
-    new_publishable = cached_in_field('_new_publishable')(is_new_publishable)
-
-    def is_published(self):
-        return self.publish_log.exists()
-    published = cached_in_field('_published')(is_published)
-
-    def is_changed(self):
-        if self.head is None:
-            return False
-        return not self.head.publishable
-    changed = cached_in_field('_changed')(is_changed)
-
-    def touch(self):
-        update = {
-            "_changed": self.is_changed(),
-            "_new_publishable": self.is_new_publishable(),
-            "_published": self.is_published(),
-        }
-        Image.objects.filter(pk=self.pk).update(**update)
-
-    # Publishing
-    # ==========
-
-    def publish(self, user):
-        """Publishes the picture on behalf of a (local) user."""
-        from base64 import b64encode
-        import apiclient
-        from catalogue.signals import post_publish
-
-        self.assert_publishable()
-        change = self.publishable()
-        picture_xml = change.materialize()
-        picture_data = open(self.image.path).read()
-        apiclient.api_call(user, "pictures/", {
-                "picture_xml": picture_xml,
-                "picture_image_data": b64encode(picture_data),
-            })
-        # record the publish
-        log = self.publish_log.create(user=user, change=change)
-        post_publish.send(sender=log)
diff --git a/src/catalogue/models/listeners.py b/src/catalogue/models/listeners.py
deleted file mode 100644 (file)
index 0c0663c..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.contrib.auth.models import User
-from django.db import models
-from catalogue.models import (Book, Chunk, Image, BookPublishRecord,
-        ImagePublishRecord)
-from catalogue.signals import post_publish
-from dvcs.signals import post_publishable
-
-
-def book_changed(sender, instance, created, **kwargs):
-    instance.touch()
-    for c in instance:
-        c.touch()
-models.signals.post_save.connect(book_changed, sender=Book)
-
-
-def chunk_changed(sender, instance, created, **kwargs):
-    instance.book.touch()
-    instance.touch()
-models.signals.post_save.connect(chunk_changed, sender=Chunk)
-
-
-def image_changed(sender, instance, created, **kwargs):
-    instance.touch()
-models.signals.post_save.connect(image_changed, sender=Image)
-
-
-def publish_listener(sender, *args, **kwargs):
-    if isinstance(sender, BookPublishRecord):
-        sender.book.touch()
-        for c in sender.book:
-            c.touch()
-    elif isinstance(sender, ImagePublishRecord):
-        sender.image.touch()
-post_publish.connect(publish_listener)
-
-
-def chunk_publishable_listener(sender, *args, **kwargs):
-    sender.tree.touch()
-    if isinstance(sender.tree, Chunk):
-        sender.tree.book.touch()
-post_publishable.connect(chunk_publishable_listener)
-
-def publishable_listener(sender, *args, **kwargs):
-    sender.tree.touch()
-post_publishable.connect(publishable_listener, sender=Image)
-
-
-def listener_create(sender, instance, created, **kwargs):
-    if created:
-        instance.chunk_set.create(number=1, slug='1')
-models.signals.post_save.connect(listener_create, sender=Book)
-
diff --git a/src/catalogue/models/project.py b/src/catalogue/models/project.py
deleted file mode 100644 (file)
index caea8b8..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.db import models
-from django.utils.translation import ugettext_lazy as _
-
-
-class Project(models.Model):
-    """ A project, tracked for funding purposes. """
-
-    name = models.CharField(_('name'), max_length=255, unique=True)
-    notes = models.TextField(_('notes'), blank=True, null=True)
-
-    class Meta:
-        app_label = 'catalogue'
-        ordering = ['name']
-        verbose_name = _('project')
-        verbose_name_plural = _('projects')
-
-    def __str__(self):
-        return self.name
diff --git a/src/catalogue/models/publish_log.py b/src/catalogue/models/publish_log.py
deleted file mode 100644 (file)
index eeec055..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.contrib.auth.models import User
-from django.db import models
-from django.utils.translation import ugettext_lazy as _
-from catalogue.models import Chunk, Image
-
-
-class BookPublishRecord(models.Model):
-    """
-        A record left after publishing a Book.
-    """
-
-    book = models.ForeignKey('Book', models.CASCADE, verbose_name=_('book'), related_name='publish_log')
-    timestamp = models.DateTimeField(_('time'), auto_now_add=True)
-    user = models.ForeignKey(User, models.CASCADE, verbose_name=_('user'))
-
-    class Meta:
-        app_label = 'catalogue'
-        ordering = ['-timestamp']
-        verbose_name = _('book publish record')
-        verbose_name_plural = _('book publish records')
-
-
-class ChunkPublishRecord(models.Model):
-    """
-        BookPublishRecord details for each Chunk.
-    """
-
-    book_record = models.ForeignKey(BookPublishRecord, models.CASCADE, verbose_name=_('book publish record'))
-    change = models.ForeignKey(Chunk.change_model, models.CASCADE, related_name='publish_log', verbose_name=_('change'))
-
-    class Meta:
-        app_label = 'catalogue'
-        verbose_name = _('chunk publish record')
-        verbose_name_plural = _('chunk publish records')
-
-
-class ImagePublishRecord(models.Model):
-    """A record left after publishing an Image."""
-
-    image = models.ForeignKey(Image, models.CASCADE, verbose_name=_('image'), related_name='publish_log')
-    timestamp = models.DateTimeField(_('time'), auto_now_add=True)
-    user = models.ForeignKey(User, models.CASCADE, verbose_name=_('user'))
-    change = models.ForeignKey(Image.change_model, models.CASCADE, related_name='publish_log', verbose_name=_('change'))
-
-    class Meta:
-        app_label = 'catalogue'
-        ordering = ['-timestamp']
-        verbose_name = _('image publish record')
-        verbose_name_plural = _('image publish records')
diff --git a/src/catalogue/signals.py b/src/catalogue/signals.py
deleted file mode 100644 (file)
index 852dc96..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.dispatch import Signal
-
-post_publish = Signal()
diff --git a/src/catalogue/templates/catalogue/active_users_list.html b/src/catalogue/templates/catalogue/active_users_list.html
deleted file mode 100644 (file)
index 3741b41..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-
-
-{% block titleextra %}{% trans "Active users" %}{% endblock %}
-
-
-{% block content %}
-
-<h1>
-    {% trans "Users active in the year" %} {{ year }}
-</h1>
-
-<ul>
-{% for email, names, count in users %}
-<li>{% for name in names %}{{ name }},  {% endfor %}<a href="mailto:{{ email }}">{{ email }}</a> ({{ count }})</li>
-{% endfor %}
-</ul>
-
-{% endblock content %}
diff --git a/src/catalogue/templates/catalogue/activity.html b/src/catalogue/templates/catalogue/activity.html
deleted file mode 100644 (file)
index 0ab5c0d..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-{% load wall %}
-
-
-{% block titleextra %}{% trans "Activity" %}{% endblock %}
-
-
-{% block content %}
-
-<div class="card">
-       <div class="card-header">
-<h1>
-       <a class="btn btn-light" href='{% url "catalogue_activity" prev_day.isoformat %}'>&lt;</a>
-    {% trans "Activity" %}: {{ day }}
-    {% if next_day %}
-        <a class="btn btn-light" href='{% url "catalogue_activity" next_day.isoformat %}'>&gt;</a>
-    {% endif %}
-</h1>
-       </div>
-       <div class="card-body">
-
-    {% day_wall day %}
-       </div>
-{% endblock content %}
diff --git a/src/catalogue/templates/catalogue/base.html b/src/catalogue/templates/catalogue/base.html
deleted file mode 100644 (file)
index c35e539..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-<!DOCTYPE html>
-{% load pipeline i18n %}
-{% load static %}
-{% load catalogue %}
-<!DOCTYPE html>
-<html>
-<head lang="{{ LANGUAGE_CODE }}">
-    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
-    <link rel="icon" href="{{ STATIC_URL }}img/pr-icon.png" type="image/png" />
-    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
-    {% stylesheet 'catalogue' %}
-    <title>{% block title %}{% block titleextra %}{% endblock %} ::
-        {% trans "Platforma Redakcyjna" %}{% endblock title %}</title>
-    {% block add_css %}{% endblock %}
-</head>
-<body>
-<!--#include file='/pozor.html'-->
-
-
-<nav class="navbar navbar-expand-md navbar-dark bg-dark">
-  <a class="navbar-brand" href="{% url 'catalogue_document_list' %}">
-         <img src="{% static "img/wl-orange.png" %}" alt="Platforma">
-  </a>
-  <ul class="navbar-nav mr-auto">
-        {% main_tabs %}
-    </ul>
-
-    <ul class="navbar-nav">
-        {% include "registration/head_login.html" %}
-    </ul>
-</nav>
-
-<div class="container mt-4 mb-4">
-  {% block content %}
-    <div class="row">
-      <div class="col-lg-8">
-        {% block leftcolumn %}
-        {% endblock leftcolumn %}
-      </div>
-      <div class="col-lg-4">
-        {% block rightcolumn %}
-        {% endblock rightcolumn %}
-      </div>
-    </div>
-  {% endblock content %}
-</div>
-
-
-<script
-    src="https://code.jquery.com/jquery-1.9.1.min.js"
-    integrity="sha256-wS9gmOZBqsqWxgIVgA8Y9WcQOa7PgSIX+rPA0VL2rbQ="
-    crossorigin="anonymous"></script>
-<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
-
-{% javascript 'catalogue' %}
-{% block add_js %}{% endblock %}
-{% block extrabody %}
-{% endblock %}
-</body>
-</html>
diff --git a/src/catalogue/templates/catalogue/book_append_to.html b/src/catalogue/templates/catalogue/book_append_to.html
deleted file mode 100644 (file)
index 15a3243..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-{% load bootstrap4 %}
-
-{% block titleextra %}{% trans "Append book" %}{% endblock %}
-
-{% block content %}
-<div class="card">
-       <div class="card-body">
-       <form enctype="multipart/form-data" method="POST" action="">
-    {% csrf_token %}
-       {% bootstrap_form form %}
-       {% buttons %}
-       <button class="btn btn-primary" type="submit">{% trans "Append book" %}</button>
-       {% endbuttons %}
-       </form>
-       </div></div>
-{% endblock content %}
-
diff --git a/src/catalogue/templates/catalogue/book_detail.html b/src/catalogue/templates/catalogue/book_detail.html
deleted file mode 100644 (file)
index b625b7e..0000000
+++ /dev/null
@@ -1,119 +0,0 @@
-{% extends "catalogue/base.html" %}
-{% load book_list i18n %}
-{% load bootstrap4 %}
-
-
-{% block titleextra %}{{ book.title }}{% endblock %}
-
-
-{% block content %}
-
-  <div class="card mt-4">
-    <div class="card-header">
-      <h1>{{ book.title }}</h1>
-    </div>
-    <div class="card-body">
-    
-
-
-{% if editable %}<form method='POST'>{% csrf_token %}{% endif %}
-    {% bootstrap_form form %}
-        {% if editable %}
-        {% buttons %}
-        <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
-        {% endbuttons %}
-    {% endif %}
-{% if editable %}</form>{% endif %}
-
-{% if editable %}
-    {% if book.gallery %}
-    <p><a href="{% url 'catalogue_book_gallery' book.slug %}">{% trans "Edit gallery" %}</a></p>
-    {% endif %}
-
-    <p style="text-align:right"><a class="btn btn-sm btn-danger" href="{% url 'catalogue_book_append' book.slug %}">{% trans "Append to other book" %}</a></p>
-{% endif %}
-    </div>
-    </div>
-
-  <div class="card mt-4">
-    <div class="card-header">
-      <h2>{% trans "Chunks" %}</h2>
-    </div>
-    <div class="card-body">
-
-    <table class='single-book-list table'><tbody>
-    {% for chunk in book %}
-        {% include 'catalogue/book_list/chunk.html' %}
-    {% endfor %}
-    </tbody></table>
-    </div>
-  </div>
-
-
-
-
-<div class='card mt-4'>
-
-<div class="card-header">
-  <h2>{% trans "Publication" %}</h2>
-  </div>
-<div class="card-body">
-  <div class="row">
-<div class="col-lg-3">
-<img class="cover-preview" src="{% url 'cover_preview' book.slug %}" />
-{% if book.dc_cover_image %}
-    <a href="{{ book.dc_cover_image.get_absolute_url }}">{{ book.dc_cover_image }}</a>
-{% endif %}
-<br><br>
-<form action="{% url 'cover_preview' book.slug %}">
-<input type="hidden" name="download" value="1">
-Okładka w rozmiarze
-<input name="width" type="number" required value="600"> x <input name="height" type="number" required value="833">
-<button type="submit" class="btn btn-sm btn-primary">Pobierz</button>
-</form>
-</div>
-<div class="col-lg-9">
-<p>{% trans "Last published" %}: 
-    {% if book.last_published %}
-        {{ book.last_published }}
-    {% else %}
-        &mdash;
-    {% endif %}
-</p>
-
-{% if publishable %}
-    <p>
-    <a href="{% url 'catalogue_book_xml' book.slug %}" rel="nofollow">{% trans "Full XML" %}</a><br/>
-    <a target="_blank" href="{% url 'catalogue_book_html' book.slug %}" rel="nofollow">{% trans "HTML version" %}</a><br/>
-    <a href="{% url 'catalogue_book_txt' book.slug %}" rel="nofollow">{% trans "TXT version" %}</a><br/>
-    <a href="{% url 'catalogue_book_pdf' book.slug %}" rel="nofollow">{% trans "PDF version" %}</a><br/>
-    <a href="{% url 'catalogue_book_pdf_mobile' book.slug %}" rel="nofollow">{% trans "PDF version for mobiles" %}</a><br/>
-    <a href="{% url 'catalogue_book_epub' book.slug %}" rel="nofollow">{% trans "EPUB version" %}</a><br/>
-    <a href="{% url 'catalogue_book_mobi' book.slug %}" rel="nofollow">{% trans "MOBI version" %}</a><br/>
-    </p>
-
-    {% if user.is_authenticated %}
-        <!--
-        Angel photos:
-        Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
-        mira66 (http://www.flickr.com/photos/21804434@N02/) /
-        CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
-        -->
-        <form method="POST" action="{% url 'catalogue_publish' book.slug %}">{% csrf_token %}
-            {{ publish_options_form.as_p }}
-            <img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" />
-            <button id="publish-button" type="submit">
-                <span>{% trans "Publish" %}</span></button>
-            <img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" />
-            </form>
-    {% else %}
-        <a href="{% url 'cas_ng_login' %}">{% trans "Log in to publish." %}</a>
-    {% endif %}
-{% else %}
-    <p>{% trans "This book can't be published yet, because:" %}</p>
-    <ul><li>{{ publishable_error }}</li></ul>
-{% endif %}
-
-</div>
-  </div>
-{% endblock content %}
diff --git a/src/catalogue/templates/catalogue/book_edit.html b/src/catalogue/templates/catalogue/book_edit.html
deleted file mode 100644 (file)
index 43fe0ea..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-
-
-{% block titleextra %}{% trans "Edit book" %}{% endblock %}
-
-
-{% block leftcolumn %}
-       <form enctype="multipart/form-data" method="POST" action="">
-    {% csrf_token %}
-       {{ form.as_p }}
-
-       <p><button type="submit">{% trans "Save" %}</button></p>
-       </form>
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-{% endblock rightcolumn %}
diff --git a/src/catalogue/templates/catalogue/book_html.html b/src/catalogue/templates/catalogue/book_html.html
deleted file mode 100644 (file)
index 518811e..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-{% load i18n %}
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
-    <head>
-        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
-        <title>{{ book.title }}</title>
-    </head>
-    <body>
-        <div id="menu">
-            <ul>
-                <li><a href="#toc">{% trans "Table of contents" %}</a></li>
-                <li><a href="#nota_red">{% trans "Edit. note" %}</a></li>
-                <li><a href="#info">{% trans "Infobox" %}</a></li>
-            </ul>
-        </div>
-        <div id="info">
-            {#% book_info book %#}
-        </div>
-        <div id="header">
-            <div id="logo">
-                <a href="/"><img src="http://static.wolnelektury.pl/img/logo.png" alt="WolneLektury.pl - logo" /></a>
-            </div>
-        </div>
-
-        {{ html|safe }}
-
-    </body>
-</html>
diff --git a/src/catalogue/templates/catalogue/book_list/book.html b/src/catalogue/templates/catalogue/book_list/book.html
deleted file mode 100644 (file)
index 189c932..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-{% load i18n %}
-{% load username from common_tags %}
-
-{% if book.single %}
-    {% with chunk as chunk %}
-    <tr class="table-sm">
-        <td><input type="checkbox" name="select_book" value="{{book.id}}" data-chunk-id="{{chunk.id}}"/></td>
-       <td><a class="btn btn-sm btn-outline-secondary" href="{% url 'catalogue_book' book.slug %}" title="{% trans "Book settings" %}">&#x1f4d5;</a></td>
-       <td><a class="btn btn-sm btn-outline-secondary" href="{% url 'catalogue_chunk_edit' book.slug chunk.slug %}" title="{% trans "Chunk settings" %}">&#x1f4dc;</a></td>
-        <td><a class="btn btn-primary" target="_blank"
-                    href="{% url 'wiki_editor' book.slug %}">
-                    {{ book.title }}</a></td>
-        <td>{% if chunk.stage %}
-            {{ chunk.stage }}
-        {% else %}–
-        {% endif %}</td>
-        <td class='user-column'>{% if chunk.user %}<a href="{% url 'catalogue_user' chunk.user.username %}">{{ chunk.user|username }}</a>{% endif %}</td>
-        <td>
-          {% if book.published %}
-            <span class="badge badge-info" title="{% trans "published" %}">opubl.</span>
-         {% endif %}
-          {% if book.new_publishable %}
-            <span class="badge badge-primary" title="{% trans "publishable" %}">do publ.</span>
-         {% endif %}
-          {% if chunk.changed %}
-            <span class="badge badge-warning title="{% trans "changed" %}">zmiany</span>
-         {% endif %}
-        </td>
-        <td>{{ book.project.name }}</td>
-    </tr>
-    {% endwith %}
-{% else %}
-    <tr class="table-sm">
-      <td><input type="checkbox" name="select_book" value="{{book.id}}"/></td>
-      <td><a class='btn btn-sm btn-outline-secondary' href="{% url 'catalogue_book' book.slug %}" title="{% trans "Book settings" %}">&#x1f4d5;</a></td>
-      <td></td>
-      <td>{{ book.title }}</td>
-      <td></td>
-      <td class='user-column'></td>
-      <td>
-        {% if book.published %}
-          <span class="badge badge-info" title="{% trans "published" %}">opubl.</span>
-       {% endif %}
-        {% if book.new_publishable %}
-          <span class="badge badge-info" title="{% trans "publishable" %}">do publ.</span>
-       {% endif %}
-      </td>
-      <td>{{ book.project.name }}</td>
-    </tr>
-{% endif %}
diff --git a/src/catalogue/templates/catalogue/book_list/book_list.html b/src/catalogue/templates/catalogue/book_list/book_list.html
deleted file mode 100644 (file)
index 4e21323..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-{% load i18n %}
-{% load pagination_tags %}
-{% load username from common_tags %}
-
-
-<form name='filter' action='{{ request.path }}'>
-<input type='hidden' name="title" value="{{ request.GET.title }}" />
-<input type='hidden' name="stage" value="{{ request.GET.stage }}" />
-{% if not viewed_user %}
-    <input type='hidden' name="user" value="{{ request.GET.user }}" />
-{% endif %}
-<input type='hidden' name="all" value="{{ request.GET.all }}" />
-<input type='hidden' name="status" value="{{ request.GET.status }}" />
-<input type='hidden' name="project" value="{{ request.GET.project }}" />
-</form>
-
-
-<div class="card">
-       <div class="card-body">
-
-
-<table id="file-list" class="table{% if viewed_user %} book-list-user{% endif %}">
-    <thead><tr>
-       <th></th>
-        <th></th>
-        <th></th>
-        <th class='book-search-column'>
-            <form>
-            <input title='{% trans "Search in book titles" %}' name="title"
-                class='form-control text-filter' value="{{ request.GET.title }}" />
-            </form>
-        </th>
-        <th><select name="stage" class="form-control filter">
-            <option value=''>- {% trans "stage" %} -</option>
-            <option {% if request.GET.stage == '-' %}selected="selected"
-                    {% endif %}value="-">- {% trans "none" %} -</option>
-            {% for stage in stages %}
-                <option {% if request.GET.stage == stage.slug %}selected="selected"
-                    {% endif %}value="{{ stage.slug }}">{{ stage.name }}</option>
-            {% endfor %}
-        </select></th>
-
-        {% if not viewed_user %}
-            <th><select name="user" class="form-control filter">
-                <option value=''>- {% trans "editor" %} -</option>
-                <option {% if request.GET.user == '-' %}selected="selected"
-                        {% endif %}value="-">- {% trans "none" %} -</option>
-                {% for user in users %}
-                    <option {% if request.GET.user == user.username %}selected="selected"
-                        {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
-                {% endfor %}
-            </select></th>
-        {% else %}
-            <th style='display: none'></th>
-        {% endif %}
-
-        <th><select name="status" class="form-control filter">
-            <option value=''>- {% trans "status" %} -</option>
-            {% for state, label in states %}
-                <option {% if request.GET.status == state %}selected="selected"
-                        {% endif %}value='{{ state }}'>{{ label }}</option>
-            {% endfor %}
-        </select></th>
-
-        <th><select name="project" class="form-control filter">
-            <option value=''>- {% trans "project" %} -</option>
-                <option {% if request.GET.project == '-' %}selected="selected"
-                        {% endif %}value="-">- {% trans "none" %} -</option>
-            {% for project in projects %}
-                <option {% if request.GET.project == project.pk|slugify %}selected="selected"
-                        {% endif %}value='{{ project.pk }}'>{{ project.name }}</option>
-            {% endfor %}
-        </select></th>
-
-    </tr></thead>
-
-    {% autopaginate books 100 as books_page %}
-    <tbody>
-    {% for item in books_page %}
-        {% with book=item.book chunk=item.chunks.0  %}
-           {% include 'catalogue/book_list/book.html' %}
-            {% if not book.single %}
-                {% for chunk in item.chunks %}
-                    {% include 'catalogue/book_list/chunk.html' %}
-                {% endfor %}
-            {% endif %}
-        {% endwith %}
-    {% endfor %}
-    </tbody>
-</table>
-{% paginate %}
-        {% blocktrans count c=books|length %}{{c}} book{% plural %}{{c}} books{% endblocktrans %}
-
-
-{% if not books %}
-    <p>{% trans "No books found." %}</p>
-{% endif %}
-
-       </div>
-</div>
-
-<form id='chunk_mass_edit' action='{% url "catalogue_chunk_mass_edit" %}' style="display:none;">
-{% csrf_token %}
-<input type="hidden" name="ids" />
-<label for="mass_edit_stage">{% trans "Set stage" %}</label><input type="hidden" name="stage" id="mass_edit_stage"/>
-<label for="mass_edit_user">{% trans "Set user" %}</label><input type="hidden" name="user" id="mass_edit_user" />
-<input type="hidden" name="status" />
-<label for="mass_edit_project">{% trans "Project" %}</label><input type="hidden" name="project" id="mass_edit_project" />
-<label for="mass_edit_more_users">{% trans "More users" %}</label>
-</form>
-
-<select name="other-user" style="display:none;">
-  {% for user in other_users %}
-  <option {% if request.GET.user == user.username %}selected="selected"
-          {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
-  {% endfor %}
-</select>
diff --git a/src/catalogue/templates/catalogue/book_list/chunk.html b/src/catalogue/templates/catalogue/book_list/chunk.html
deleted file mode 100644 (file)
index cdd3b73..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-{% load i18n %}
-{% load username from common_tags %}
-
-<tr class="table-secondary table-sm">
-    <td><input type="checkbox" name="select_chunk" value="{{chunk.id}}" data-book-id="{{chunk.book.id}}" /></td>
-    <td class='book-settings-column'></td>
-    <td><a class="btn btn-outline-secondary btn-sm" href="{% url 'catalogue_chunk_edit' chunk.book.slug chunk.slug %}" title="{% trans "Chunk settings" %}">&#x1f4dc;</a></td>
-    <td><a class="btn btn-primary" target="_blank" href="{{ chunk.get_absolute_url }}">
-      {{ chunk.number }}.
-      {{ chunk.title }}</a></td>
-    <td>{% if chunk.stage %}
-      {{ chunk.stage }}
-    {% else %}
-      –
-    {% endif %}</td>
-    <td class='user-column'>{% if chunk.user %}
-      <a href="{% url 'catalogue_user' chunk.user.username %}">
-        {{ chunk.user|username }}
-      </a>{% else %}
-
-    {% endif %}</td>
-
-    </td>
-    <td>
-      {% if chunk.changed %}
-        <span class="badge badge-warning title="{% trans "changed" %}">zmiany</span>
-      {% endif %}
-</td>
-<td></td>
-</tr>
diff --git a/src/catalogue/templates/catalogue/book_text.html b/src/catalogue/templates/catalogue/book_text.html
deleted file mode 100644 (file)
index a88aad5..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-{% load i18n pipeline %}
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
-    <head>
-        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
-        <title>{% trans "Redakcja" %} :: {{ book.title }}</title>
-       {% stylesheet 'book' %}
-        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
-       {% javascript 'book' %}
-    </head>
-    <body>
-        <div id="menu">
-            <ul>
-                <li><a class="menu" href="#toc">{% trans "Table of contents" %}</a></li>
-{#                <li><a class="menu" href="#themes">{% trans "Themes" %}</a></li>#}
-                <li><a class="menu" href="#nota_red">{% trans "Edit. note" %}</a></li>
-{#                <li><a class="menu" href="#info">{% trans "Infobox" %}</a></li>#}
-{#                <li><a href="{{ book.get_absolute_url }}">{% trans "Book's page" %}</a></li> #}
-{#                <li><a class="menu" href="#download">{% trans "Download" %}</a></li>#}
-            </ul>
-        </div>
-        <div id="header">
-            <a href="/"><img src="/media/static/img/logo-220.png" alt="Wolne Lektury" /></a>
-        </div>
-        {{ html|safe }}
-    </body>
-</html>
diff --git a/src/catalogue/templates/catalogue/chunk_add.html b/src/catalogue/templates/catalogue/chunk_add.html
deleted file mode 100644 (file)
index f447ec6..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-{% load bootstrap4 %}
-
-
-{% block titleextra %}{% trans "Split chunk" %}{% endblock %}
-
-
-{% block content %}
-<div class="card">
-       <div class="card-header">
-    <h1>{% trans "Split chunk" %}</h1>
-       </div>
-       <div class="card-body">
-
-       <form enctype="multipart/form-data" method="POST">
-    {% csrf_token %}
-    <div class='editable'>
-           <p>{% trans "Insert empty chunk after" %}:
-           <a href="{{ chunk.get_absolute_url }}">{{ chunk.pretty_name }}</a></p>
-        {% bootstrap_form form %}
-       {% buttons %}
-        <button class="btn btn-primary" type="submit">{% trans "Add chunk" %}</button>
-       {% endbuttons %}
-    </div>
-       </form>
-       </div>
-</div>
-{% endblock content %}
diff --git a/src/catalogue/templates/catalogue/chunk_edit.html b/src/catalogue/templates/catalogue/chunk_edit.html
deleted file mode 100644 (file)
index 971634d..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-{% load bootstrap4 %}
-
-
-{% block titleextra %}{% trans "Chunk settings" %}{% endblock %}
-
-
-{% block content %}
-<div class="card">
-       <div class="card-header">
-    <h1>{% trans "Chunk settings" %}</h1>
-       </div>
-       <div class="card-body">
-
-       <form enctype="multipart/form-data" method="POST" action="{% if go_next %}?next={{ go_next }}{% endif %}">
-    {% csrf_token %}
-    <div class='editable'>
-           <p>{% trans "Book" %}: {{ chunk.book }} ({{ chunk.number }}/{{ chunk.book|length }})</p>
-        {% bootstrap_form form %}
-       {% buttons %}
-               <button class="btn btn-primary" type="submit">{% trans "Save" %}</button>
-               {% endbuttons %}
-    </div>
-
-       </form>
-
-
-    <p style="text-align: right"><a class="btn btn-danger" href="{% url "catalogue_chunk_add" chunk.book.slug chunk.slug %}">{% trans "Split chunk" %}</a></p>
-   </div>
-</div>
-
-{% endblock content %}
diff --git a/src/catalogue/templates/catalogue/document_create_missing.html b/src/catalogue/templates/catalogue/document_create_missing.html
deleted file mode 100644 (file)
index 773dd9e..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-{% load bootstrap4 %}
-
-
-{% block titleextra %}{% trans "Create a new book" %}{% endblock %}
-
-
-{% block content %}
-<div class="card">
-       <div class="card-header">
-    <h1>{% trans "Create a new book" %}</h1>
-       </div>
-       <div class="card-body">
-
-
-    <form enctype="multipart/form-data" method="POST">
-    {% csrf_token %}
-        {% bootstrap_form form %}
-       {% buttons %}
-        <button class="btn btn-primary" type="submit">{% trans "Create book" %}</button>
-       {% endbuttons %}
-    </form>
-</div>
-</div>
-{% endblock content %}
diff --git a/src/catalogue/templates/catalogue/document_list.html b/src/catalogue/templates/catalogue/document_list.html
deleted file mode 100644 (file)
index 8a7e0fc..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-{% extends "catalogue/base.html" %}
-
-{% load i18n %}
-{% load catalogue book_list %}
-{% load pipeline %}
-
-{% block titleextra %}{% trans "Book list" %}{% endblock %}
-
-
-{% block add_js %}
-  {% javascript 'book_list' %}
-  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js"></script>
-{% endblock %}
-
-{% block add_css %}
-  {% stylesheet 'book_list' %}
-  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css">
-{% endblock %}
-
-{% block content %}
-    {% book_list %}
-{% endblock content %}
diff --git a/src/catalogue/templates/catalogue/document_upload.html b/src/catalogue/templates/catalogue/document_upload.html
deleted file mode 100644 (file)
index e42414e..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-{% load bootstrap4 %}
-
-
-{% block titleextra %}{% trans "Bulk document upload" %}{% endblock %}
-
-
-{% block content %}
-
-
-<div class="card">
-       <div class="card-header">
-<h2>{% trans "Bulk documents upload" %}</h2>
-       </div>
-       <div class="card-body">
-
-<p>
-{% trans "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with <code>.xml</code> will be ignored." %}
-</p>
-
-<form enctype="multipart/form-data" method="POST" action="{{ request.path }}">
-{% csrf_token %}
-{% bootstrap_form form %}
-{% buttons %}
-<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
-{% endbuttons %}
-</form>
-
-
-{% if error_list %}
-    <hr>
-
-    <p class='error'>{% trans "There have been some errors. No files have been added to the repository." %}
-    <h3>{% trans "Offending files" %}</h3>
-    <ul id='error-list'>
-        {% for filename, title, error in error_list %}
-            <li>{{ title }} (<code>{{ filename }}</code>): {{ error }}</li>
-        {% endfor %}
-    </ul>
-
-    {% if ok_list %}
-    <h3>{% trans "Correct files" %}</h3>
-        <ul>
-            {% for filename, slug, title in ok_list %}
-                <li>{{ title }} (<code>{{ filename }}</code>)</li>
-            {% endfor %}
-        </ul>
-    {% endif %}
-
-{% else %}
-
-    {% if ok_list %}
-        <p class='success'>{% trans "Files have been successfully uploaded to the repository." %}</p>
-        <h3>{% trans "Uploaded files" %}</h3>
-        <ul id='ok-list'>
-        {% for filename, slug, title in ok_list %}
-            <li><a href='{% url "wiki_editor" slug %}'>{{ title }}</a> (<code>{{ filename }})</a></li>
-        {% endfor %}
-        </ul>
-    {% endif %}
-{% endif %}
-
-{% if skipped_list %}
-    <h3>{% trans "Skipped files" %}</h3>
-    <p>{% trans "Files skipped due to no <code>.xml</code> extension" %}</p>
-    <ul id='skipped-list'>
-        {% for filename in skipped_list %}
-            <li>{{ filename }}</li>
-        {% endfor %}
-    </ul>
-{% endif %}
-
-       </div>
-</div>
-
-{% endblock content %}
-
diff --git a/src/catalogue/templates/catalogue/image_detail.html b/src/catalogue/templates/catalogue/image_detail.html
deleted file mode 100644 (file)
index e3902b8..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-{% extends "catalogue/base.html" %}
-{% load book_list i18n %}
-{% load bootstrap4 %}
-
-
-{% block titleextra %}{{ object.title }}{% endblock %}
-
-
-{% block content %}
-<div class="card mt-4">
-
-       <div class="card-header">
-<h1>{{ object.title }}</h1>
-       </div>
-       <div class="card-body">
-
-
-{% if editable %}<form method='POST'>{% csrf_token %}{% endif %}
-    {% bootstrap_form form %}
-    {% if editable %}
-    {% buttons %}
-        <button class="btn btn-primary" type="submit">{% trans "Save" %}</button>
-    {% endbuttons %}
-    {% endif %}
-</tbody></table>
-{% if editable %}</form>{% endif %}
-
-       </div>
-</div>
-
-
-<div class='card mt-4'>
-       <div class="card-header">
-    <h2>{% trans "Editor" %}</h2>
-       </div>
-       <div class="card-body">
-
-    <p><a class="btn btn-primary" href="{% url 'wiki_img_editor' object.slug %}">{% trans "Proceed to the editor." %}</a></p>
-</div>
-</div>
-
-
-
-<div class='card mt-4'>
-       <div class="card-header">
-
-<h2>{% trans "Publication" %}</h2>
-       </div>
-       <div class="card-body">
-
-<p>{% trans "Last published" %}: 
-    {% if object.last_published %}
-        {{ object.last_published }}
-    {% else %}
-        &mdash;
-    {% endif %}
-</p>
-
-{% if publishable %}
-    {% if user.is_authenticated %}
-        <!--
-        Angel photos:
-        Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
-        mira66 (http://www.flickr.com/photos/21804434@N02/) /
-        CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
-        -->
-        <form method="POST" action="{% url 'catalogue_publish_image' object.slug %}">{% csrf_token %}
-            <!--img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" /-->
-            <button id="publish-button" type="submit">
-                <span>{% trans "Publish" %}</span></button>
-            <!--img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" /-->
-            </form>
-    {% else %}
-        <a href="{% url 'cas_ng_login' %}">{% trans "Log in to publish." %}</a>
-    {% endif %}
-{% else %}
-    <p>{% trans "This book can't be published yet, because:" %}</p>
-    <ul><li>{{ publishable_error }}</li></ul>
-{% endif %}
-
-</div>
-</div>
-
-
-{% endblock content %}
diff --git a/src/catalogue/templates/catalogue/image_list.html b/src/catalogue/templates/catalogue/image_list.html
deleted file mode 100644 (file)
index d1c0d95..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-{% extends "catalogue/base.html" %}
-
-{% load i18n %}
-{% load catalogue book_list %}
-{% load pipeline %}
-
-
-{% block titleextra %}{% trans "Image list" %}{% endblock %}
-
-
-{% block add_js %}
-  {% javascript 'book_list' %}
-  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js"></script>
-{% endblock %}
-
-{% block add_css %}
-  {% stylesheet 'book_list' %}
-  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css">
-{% endblock %}
-
-
-{% block content %}
-    {% image_list %}
-{% endblock content %}
diff --git a/src/catalogue/templates/catalogue/image_short.html b/src/catalogue/templates/catalogue/image_short.html
deleted file mode 100644 (file)
index c00481e..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-{% load i18n %}
-{% load username from common_tags %}
-
-<tr class="table-sm">
-    <td><input type="checkbox" name="select_chunk" value="{{image.id}}"/></td>
-    <td><a href="{% url 'catalogue_image' image.slug %}" class="btn btn-sm btn-secondary" title="{% trans "Image settings" %}">&#x1f5bc;</a></td>
-    <td><a class="btn btn-primary" target="_blank"
-                href="{% url 'wiki_img_editor' image.slug %}">
-                {{ image.title }}</a></td>
-    <td>{% if image.stage %}
-        {{ image.stage }}
-    {% else %}–
-    {% endif %}</td>
-    <td class='user-column'>{% if image.user %}<a href="{% url 'catalogue_user' image.user.username %}">{{ image.user|username }}</a>{% endif %}</td>
-    <td>
-      {% if image.published %}
-        <span class="badge badge-info" title="{% trans "published" %}">opubl.</span>
-      {% endif %}
-      {% if image.new_publishable %}
-        <span class="badge badge-primary" title="{% trans "publishable" %}">do publ.</span>
-      {% endif %}
-      {% if image.changed %}
-        <span class="badge badge-warning title="{% trans "changed" %}">zmiany</span>
-      {% endif %}
-    </td>
-    <td>{{ image.project.name }}</td>
-</tr>
diff --git a/src/catalogue/templates/catalogue/image_table.html b/src/catalogue/templates/catalogue/image_table.html
deleted file mode 100644 (file)
index 4f0a425..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-{% load i18n %}
-{% load pagination_tags %}
-{% load username from common_tags %}
-
-<div class="card">
-       <div class="card-body">
-
-
-<form name='filter' action='{{ request.path }}'>
-<input type='hidden' name="title" value="{{ request.GET.title }}" />
-<input type='hidden' name="stage" value="{{ request.GET.stage }}" />
-{% if not viewed_user %}
-    <input type='hidden' name="user" value="{{ request.GET.user }}" />
-{% endif %}
-<input type='hidden' name="status" value="{{ request.GET.status }}" />
-<input type='hidden' name="project" value="{{ request.GET.project }}" />
-</form>
-
-<table id="file-list" class="table {% if viewed_user %}book-list-user{% endif %}">
-    <thead><tr>
-        <th></th>
-        <th></th>
-        <th class='book-search-column'>
-            <form>
-            <input title='{% trans "Search in book titles" %}' name="title"
-                class='form-control text-filter' value="{{ request.GET.title }}" />
-            </form>
-        </th>
-        <th><select name="stage" class="form-control filter">
-            <option value=''>- {% trans "stage" %} -</option>
-            <option {% if request.GET.stage == '-' %}selected="selected"
-                    {% endif %}value="-">- {% trans "none" %} -</option>
-            {% for stage in stages %}
-                <option {% if request.GET.stage == stage.slug %}selected="selected"
-                    {% endif %}value="{{ stage.slug }}">{{ stage.name }}</option>
-            {% endfor %}
-        </select></th>
-
-        {% if not viewed_user %}
-            <th><select name="user" class="form-control filter">
-                <option value=''>- {% trans "editor" %} -</option>
-                <option {% if request.GET.user == '-' %}selected="selected"
-                        {% endif %}value="-">- {% trans "none" %} -</option>
-                {% for user in users %}
-                    <option {% if request.GET.user == user.username %}selected="selected"
-                        {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
-                {% endfor %}
-            </select></th>
-        {% endif %}
-
-        <th><select name="status" class="form-control filter">
-            <option value=''>- {% trans "status" %} -</option>
-            {% for state, label in states %}
-                <option {% if request.GET.status == state %}selected="selected"
-                        {% endif %}value='{{ state }}'>{{ label }}</option>
-            {% endfor %}
-        </select></th>
-
-        <th><select name="project" class="form-control filter">
-            <option value=''>- {% trans "project" %} -</option>
-                <option {% if request.GET.project == '-' %}selected="selected"
-                        {% endif %}value="-">- {% trans "none" %} -</option>
-            {% for project in projects %}
-                <option {% if request.GET.project == project.pk|slugify %}selected="selected"
-                        {% endif %}value='{{ project.pk }}'>{{ project.name }}</option>
-            {% endfor %}
-        </select></th>
-
-    </tr></thead>
-
-    {% autopaginate objects 100 as objects_page %}
-    <tbody>
-    {% for image in objects_page %}
-        {% include 'catalogue/image_short.html' %}
-    {% endfor %}
-    </tbody>
-</table>
-    {% paginate %}
-    {% blocktrans count c=objects|length %}{{c}} image{% plural %}{{c}} images{% endblocktrans %}</th></tr>
-{% if not objects %}
-    <p>{% trans "No images found." %}</p>
-{% endif %}
-
-<form id='chunk_mass_edit' action='{% url "catalogue_image_mass_edit" %}' style="display:none;">
-{% csrf_token %}
-<input type="hidden" name="ids" />
-<label for="mass_edit_stage">{% trans "Set stage" %}</label><input type="hidden" name="stage" id="mass_edit_stage"/>
-<label for="mass_edit_user">{% trans "Set user" %}</label><input type="hidden" name="user" id="mass_edit_stage" />
-<input type="hidden" name="status" />
-<label for="mass_edit_project">{% trans "Project" %}</label><input type="hidden" name="project" id="mass_edit_project" />
-<label for="mass_edit_more_users">{% trans "More users" %}</label>
-</form>
-
-<select name="other-user" style="display:none;">
-  {% for user in other_users %}
-  <option {% if request.GET.user == user.username %}selected="selected"
-          {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
-  {% endfor %}
-</select>
-
-       </div>
-</div>
diff --git a/src/catalogue/templates/catalogue/main_tabs.html b/src/catalogue/templates/catalogue/main_tabs.html
deleted file mode 100644 (file)
index eb74bbe..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-{% for tab in tabs %}
-    <li class="nav-item"><a class="nav-link{% if active_tab == tab.slug %} active{% endif %}" href="{{ tab.url }}">{{ tab.caption }}</a></li>
-{% endfor %}
diff --git a/src/catalogue/templates/catalogue/mark_final.html b/src/catalogue/templates/catalogue/mark_final.html
deleted file mode 100644 (file)
index 9ed740c..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-{% extends "catalogue/base.html" %}
-
-{% block titleextra %}Oznacz książki{% endblock %}
-
-
-{% block leftcolumn %}
-
-<h1>Oznacz książki</h1>
-
-<form method="post" action="">
-    {% csrf_token %}
-    {{ form.as_p }}
-    <input type="submit" value="Oznacz">
-</form>
-
-{% endblock leftcolumn %}
\ No newline at end of file
diff --git a/src/catalogue/templates/catalogue/mark_final_completed.html b/src/catalogue/templates/catalogue/mark_final_completed.html
deleted file mode 100644 (file)
index 1b37c83..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-{% extends "catalogue/base.html" %}
-
-{% block titleextra %}Oznaczono książki{% endblock %}
-
-
-{% block leftcolumn %}
-
-<h1>Oznaczono książki</h1>
-
-<p>Książki zostały oznaczone.</p>
-
-{% endblock leftcolumn %}
\ No newline at end of file
diff --git a/src/catalogue/templates/catalogue/my_page.html b/src/catalogue/templates/catalogue/my_page.html
deleted file mode 100644 (file)
index 2fec0a3..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-{% extends "catalogue/base.html" %}
-
-{% load i18n %}
-{% load catalogue book_list wall %}
-{% load pipeline %}
-
-{% block add_js %}
-  {% javascript 'book_list' %}
-  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js"></script>
-{% endblock %}
-
-{% block add_css %}
-  {% stylesheet 'book_list' %}
-  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css">
-{% endblock %}
-
-{% block titleextra %}{% trans "My page" %}{% endblock %}
-
-
-{% block leftcolumn %}
-    {% book_list request.user %}
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-<div class="card">
-       <div class="card-header">
-        <h2>{% trans "Your last edited documents" %}</h2>
-       </div>
-       <div class="card-body">
-        <ol>
-            {% for edit_url, item in last_books %}
-                <li><a
-                {% if edit_url|length == 2 %}
-                    {# Temporary support for old-style last_books. #}
-                    href="{% url 'wiki_editor' edit_url.0 edit_url.1 %}"
-                {% else %}
-                    href="{{ edit_url }}"
-                {% endif %}
-                target="_blank">{{ item.title }}</a><br/><span class="date">({{ item.time|date:"H:i:s, d/m/Y" }})</span></li>
-            {% endfor %}
-        </ol>
-    </div>
-</div>
-
-<div class="card mt-4">
-       <div class="card-header">
-    <h2>{% trans "Recent activity for" %} {{ request.user|nice_name }}</h2>
-       </div>
-       <div class="card-body">
-    {% wall request.user 10 %}
-       </div></div>
-{% endblock rightcolumn %}
diff --git a/src/catalogue/templates/catalogue/upload_pdf.html b/src/catalogue/templates/catalogue/upload_pdf.html
deleted file mode 100644 (file)
index 265b84a..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-
-
-{% block titleextra %}{% trans "PDF file upload" %}{% endblock %}
-
-
-{% block content %}
-
-
-<h2>{% trans "PDF file upload" %}</h2>
-
-<form enctype="multipart/form-data" method="POST" action="">
-{% csrf_token %}
-{{ form.as_p }}
-<p><button type="submit">{% trans "Upload" %}</button></p>
-</form>
-
-
-{% endblock content %}
diff --git a/src/catalogue/templates/catalogue/user_list.html b/src/catalogue/templates/catalogue/user_list.html
deleted file mode 100644 (file)
index b0607fa..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-{% extends "catalogue/base.html" %}
-
-{% load i18n %}
-{% load username from common_tags %}
-
-
-{% block titleextra %}{% trans "Users" %}{% endblock %}
-
-
-{% block content %}
-<div class="card">
-       <div class="card-header">
-
-<h1>{% trans "Users" %}</h1>
-       </div>
-<div class="card-body">
-
-<ul>
-{% for user in users %}
-    <li><a href="{% url 'catalogue_user' user.username %}">
-        <span class="chunkno">{{ forloop.counter }}.</span>
-        {{ user|username }}</a>
-        ({{ user.count }})</li>
-{% endfor %}
-</ul>
-
-</div>
-</div>
-
-{% endblock content %}
diff --git a/src/catalogue/templates/catalogue/user_page.html b/src/catalogue/templates/catalogue/user_page.html
deleted file mode 100644 (file)
index 85f3061..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends "catalogue/base.html" %}
-
-{% load i18n %}
-{% load catalogue book_list wall %}
-
-
-{% block titleextra %}{{ viewed_user|nice_name }}{% endblock %}
-
-
-{% block leftcolumn %}
-    <h1 class="mb-4">{{ viewed_user|nice_name }}</h1>
-    {% book_list viewed_user %}
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-<div class="card">
-       <div class="card-header">
-    <h2>{% trans "Recent activity for" %} {{ viewed_user|nice_name }}</h2>
-       </div>
-       <div class="card-body">
-               
-    {% wall viewed_user 10 %}
-       </div>
-</div>
-{% endblock rightcolumn %}
diff --git a/src/catalogue/templates/catalogue/wall.html b/src/catalogue/templates/catalogue/wall.html
deleted file mode 100644 (file)
index a107dfa..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-{% load i18n %}
-{% load gravatar %}
-{% load email %}
-{% load username from common_tags %}
-
-<ul class='wall'>
-{% for item in wall %}
-    <li class="{{ item.tag }}{% if not item.user %} anonymous{% endif %}">
-        <div class='gravatar'>
-            {% if item.get_email %}
-                <img src="{% gravatar_url item.get_email 32 %}"
-                    height="32" width="32" alt='Avatar' />
-                <br/>
-            {% endif %}
-        </div>
-
-        <div class="time">{{ item.timestamp }}</div>
-        <h3>{{ item.header }}</h3>
-        <a target="_blank" href='{{ item.url }}'>{{ item.title }}</a>
-        <br/><strong>{% trans "user" %}:</strong>
-        {% if item.user %}
-            <a href="{% url 'catalogue_user' item.user.username %}">
-            {{ item.user|username }}</a>
-            &lt;{{ item.user.email|email_link }}>
-        {% else %}
-            {{ item.user_name }}
-            {% if item.email %}
-                &lt;{{ item.email|email_link }}>
-            {% endif %}
-            ({% trans "not logged in" %})
-        {% endif %}
-        <br/>{{ item.summary|linebreaksbr }}
-    </li>
-{% empty %}
-    <li>{% trans "No activity recorded." %}</li>
-{% endfor %}
-</ul>
diff --git a/src/catalogue/templatetags/__init__.py b/src/catalogue/templatetags/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/catalogue/templatetags/book_list.py b/src/catalogue/templatetags/book_list.py
deleted file mode 100644 (file)
index db24c69..0000000
+++ /dev/null
@@ -1,205 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from re import split
-from django.db.models import Q, Count
-from django import template
-from django.utils.translation import ugettext_lazy as _
-from django.contrib.auth.models import User
-from catalogue.models import Chunk, Image, Project
-
-register = template.Library()
-
-
-class ChunksList(object):
-    def __init__(self, chunk_qs):
-        self.chunk_qs = chunk_qs.select_related('book', 'book__project', 'stage', 'user')
-        self.book_qs = chunk_qs.values('book_id')
-
-    def __getitem__(self, key):
-        if isinstance(key, slice):
-            return self.get_slice(key)
-        elif isinstance(key, int):
-            return self.get_slice(slice(key, key+1))[0]
-        else:
-            raise TypeError('Unsupported list index. Must be a slice or an int.')
-
-    def __len__(self):
-        return self.book_qs.count()
-
-    def get_slice(self, slice_):
-        book_ids = [x['book_id'] for x in self.book_qs[slice_]]
-        chunk_qs = self.chunk_qs.filter(book__in=book_ids)
-
-        chunks_list = []
-        book = None
-        for chunk in chunk_qs:
-            if chunk.book != book:
-                book = chunk.book
-                chunks_list.append(ChoiceChunks(book, [chunk]))
-            else:
-                chunks_list[-1].chunks.append(chunk)
-        return chunks_list
-
-
-class ChoiceChunks(object):
-    """
-        Associates the given chunks iterable for a book.
-    """
-
-    chunks = None
-
-    def __init__(self, book, chunks):
-        self.book = book
-        self.chunks = chunks
-
-
-def foreign_filter(qs, value, filter_field, model, model_field='slug', unset='-'):
-    if value == unset:
-        return qs.filter(**{filter_field: None})
-    if not value:
-        return qs
-    try:
-        obj = model._default_manager.get(**{model_field: value})
-    except model.DoesNotExist:
-        return qs.none()
-    else:
-        return qs.filter(**{filter_field: obj})
-
-
-def search_filter(qs, value, filter_fields):
-    if not value:
-        return qs
-    q = Q(**{"%s__icontains" % filter_fields[0]: value})
-    for field in filter_fields[1:]:
-        q |= Q(**{"%s__icontains" % field: value})
-    return qs.filter(q)
-
-
-_states = [
-        ('publishable', _('publishable'), Q(book___new_publishable=True)),
-        ('changed', _('changed'), Q(_changed=True)),
-        ('published', _('published'), Q(book___published=True)),
-        ('unpublished', _('unpublished'), Q(book___published=False)),
-        ('empty', _('empty'), Q(head=None)),
-    ]
-_states_options = [s[:2] for s in _states]
-_states_dict = dict([(s[0], s[2]) for s in _states])
-
-
-def document_list_filter(request, **kwargs):
-
-    def arg_or_GET(field):
-        return kwargs.get(field, request.GET.get(field))
-
-    if arg_or_GET('all'):
-        chunks = Chunk.objects.all()
-    else:
-        chunks = Chunk.visible_objects.all()
-
-    chunks = chunks.order_by('book__title', 'book', 'number')
-
-    if not request.user.is_authenticated:
-        chunks = chunks.filter(book__public=True)
-
-    state = arg_or_GET('status')
-    if state in _states_dict:
-        chunks = chunks.filter(_states_dict[state])
-
-    chunks = foreign_filter(chunks, arg_or_GET('user'), 'user', User, 'username')
-    chunks = foreign_filter(chunks, arg_or_GET('stage'), 'stage', Chunk.tag_model, 'slug')
-    chunks = search_filter(chunks, arg_or_GET('title'), ['book__title', 'title'])
-    chunks = foreign_filter(chunks, arg_or_GET('project'), 'book__project', Project, 'pk')
-    return chunks
-
-
-@register.inclusion_tag('catalogue/book_list/book_list.html', takes_context=True)
-def book_list(context, user=None):
-    request = context['request']
-
-    if user:
-        filters = {"user": user}
-        new_context = {"viewed_user": user}
-    else:
-        filters = {}
-        new_context = {
-            "users": User.objects.annotate(
-                count=Count('chunk')).filter(count__gt=0).order_by(
-                '-count', 'last_name', 'first_name'),
-            "other_users": User.objects.annotate(
-                count=Count('chunk')).filter(count=0).order_by(
-                'last_name', 'first_name'),
-                }
-
-    new_context.update({
-        "filters": True,
-        "request": request,
-        "books": ChunksList(document_list_filter(request, **filters)),
-        "stages": Chunk.tag_model.objects.all(),
-        "states": _states_options,
-        "projects": Project.objects.all(),
-    })
-
-    return new_context
-
-
-
-_image_states = [
-        ('publishable', _('publishable'), Q(_new_publishable=True)),
-        ('changed', _('changed'), Q(_changed=True)),
-        ('published', _('published'), Q(_published=True)),
-        ('unpublished', _('unpublished'), Q(_published=False)),
-        ('empty', _('empty'), Q(head=None)),
-    ]
-_image_states_options = [s[:2] for s in _image_states]
-_image_states_dict = dict([(s[0], s[2]) for s in _image_states])
-
-def image_list_filter(request, **kwargs):
-
-    def arg_or_GET(field):
-        return kwargs.get(field, request.GET.get(field))
-
-    images = Image.objects.all().select_related('user', 'stage', 'project')
-
-    if not request.user.is_authenticated:
-        images = images.filter(public=True)
-
-    state = arg_or_GET('status')
-    if state in _image_states_dict:
-        images = images.filter(_image_states_dict[state])
-
-    images = foreign_filter(images, arg_or_GET('user'), 'user', User, 'username')
-    images = foreign_filter(images, arg_or_GET('stage'), 'stage', Image.tag_model, 'slug')
-    images = search_filter(images, arg_or_GET('title'), ['title', 'title'])
-    images = foreign_filter(images, arg_or_GET('project'), 'project', Project, 'pk')
-    return images
-
-
-@register.inclusion_tag('catalogue/image_table.html', takes_context=True)
-def image_list(context, user=None):
-    request = context['request']
-
-    if user:
-        filters = {"user": user}
-        new_context = {"viewed_user": user}
-    else:
-        filters = {}
-        new_context = {
-            "users": User.objects.annotate(
-                count=Count('image')).filter(count__gt=0).order_by(
-                '-count', 'last_name', 'first_name'),
-            "other_users": User.objects.annotate(
-                count=Count('image')).filter(count=0).order_by(
-                'last_name', 'first_name'),
-                }
-
-    new_context.update({
-        "filters": True,
-        "request": request,
-        "objects": image_list_filter(request, **filters),
-        "stages": Image.tag_model.objects.all(),
-        "states": _image_states_options,
-        "projects": Project.objects.all(),
-    })
-
-    return new_context
diff --git a/src/catalogue/templatetags/catalogue.py b/src/catalogue/templatetags/catalogue.py
deleted file mode 100644 (file)
index 0bfd95a..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.urls import reverse
-from django import template
-from django.utils.translation import ugettext as _
-
-register = template.Library()
-
-
-class Tab(object):
-    slug = None
-    caption = None
-    url = None
-
-    def __init__(self, slug, caption, url):
-        self.slug = slug
-        self.caption = caption
-        self.url = url
-
-
-@register.inclusion_tag("catalogue/main_tabs.html", takes_context=True)
-def main_tabs(context):
-    active = getattr(context['request'], 'catalogue_active_tab', None)
-
-    tabs = []
-    user = context['user']
-    tabs.append(Tab('my', _('My page'), reverse("catalogue_user")))
-
-    tabs.append(Tab('activity', _('Activity'), reverse("catalogue_activity")))
-    tabs.append(Tab('all', _('All'), reverse("catalogue_document_list")))
-    tabs.append(Tab('images', _('Images'), reverse("catalogue_image_list")))
-    tabs.append(Tab('users', _('Users'), reverse("catalogue_users")))
-
-    if user.has_perm('catalogue.add_book'):
-        tabs.append(Tab('create', _('Add'), reverse("catalogue_create_missing")))
-        tabs.append(Tab('upload', _('Upload'), reverse("catalogue_upload")))
-
-    tabs.append(Tab('cover', _('Covers'), reverse("cover_image_list")))
-
-    return {"tabs": tabs, "active_tab": active}
-
-
-@register.filter
-def nice_name(user):
-    return user.get_full_name() or user.username
-
diff --git a/src/catalogue/templatetags/common_tags.py b/src/catalogue/templatetags/common_tags.py
deleted file mode 100644 (file)
index 5544232..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django import template
-register = template.Library()
-
-@register.filter
-def username(user):
-    return ("%s %s" % (user.first_name, user.last_name)).lstrip() or user.username
diff --git a/src/catalogue/templatetags/set_get_parameter.py b/src/catalogue/templatetags/set_get_parameter.py
deleted file mode 100644 (file)
index 36e2245..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from re import split
-
-from django import template
-
-register = template.Library()
-
-
-"""
-In template:
-    {% set_get_paramater param1='const_value',param2=,param3=variable %}
-results with changes to query string:
-    param1 is set to `const_value' string
-    param2 is unset, if exists,
-    param3 is set to the value of variable in context
-
-Using 'django.core.context_processors.request' is required.
-
-"""
-
-
-class SetGetParameter(template.Node):
-    def __init__(self, values):
-        self.values = values
-        
-    def render(self, context):
-        request = template.Variable('request').resolve(context)
-        params = request.GET.copy()
-        for key, value in self.values.items():
-            if value == '':
-                if key in params:
-                    del(params[key])
-            else:
-                params[key] = template.Variable(value).resolve(context)
-        return '?%s' %  params.urlencode()
-
-
-@register.tag
-def set_get_parameter(parser, token):
-    parts = split(r'\s+', token.contents, 2)
-
-    values = {}
-    for pair in parts[1].split(','):
-        s = pair.split('=')
-        values[s[0]] = s[1]
-
-    return SetGetParameter(values)
diff --git a/src/catalogue/templatetags/wall.py b/src/catalogue/templatetags/wall.py
deleted file mode 100644 (file)
index 7543616..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from datetime import timedelta
-from django.db.models import Q
-from django.urls import reverse
-from django import template
-from django.utils.translation import ugettext as _
-
-from catalogue.models import Chunk, BookPublishRecord, Image, ImagePublishRecord
-
-register = template.Library()
-
-
-class WallItem(object):
-    title = ''
-    summary = ''
-    url = ''
-    timestamp = ''
-    user = None
-    user_name = ''
-    email = ''
-
-    def __init__(self, tag):
-        self.tag = tag
-
-    def get_email(self):
-        if self.user:
-            return self.user.email
-        else:
-            return self.email
-
-
-def changes_wall(user=None, max_len=None, day=None):
-    qs = Chunk.change_model.objects.order_by('-created_at')
-    qs = qs.select_related('author', 'tree', 'tree__book')
-    if user is not None:
-        qs = qs.filter(Q(author=user) | Q(tree__user=user))
-    if max_len is not None:
-        qs = qs[:max_len]
-    if day is not None:
-        next_day = day + timedelta(1)
-        qs = qs.filter(created_at__gte=day, created_at__lt=next_day)
-    for item in qs:
-        tag = 'stage' if item.tags.count() else 'change'
-        chunk = item.tree
-        w = WallItem(tag)
-        if user and item.author != user:
-            w.header = _('Related edit')
-        else:
-            w.header = _('Edit')
-        w.title = chunk.pretty_name()
-        w.summary = item.description
-        w.url = reverse('wiki_editor', 
-                args=[chunk.book.slug, chunk.slug]) + '?diff=%d' % item.revision
-        w.timestamp = item.created_at
-        w.user = item.author
-        w.user_name = item.author_name
-        w.email = item.author_email
-        yield w
-
-
-def image_changes_wall(user=None, max_len=None, day=None):
-    qs = Image.change_model.objects.order_by('-created_at')
-    qs = qs.select_related('author', 'tree')
-    if user is not None:
-        qs = qs.filter(Q(author=user) | Q(tree__user=user))
-    if max_len is not None:
-        qs = qs[:max_len]
-    if day is not None:
-        next_day = day + timedelta(1)
-        qs = qs.filter(created_at__gte=day, created_at__lt=next_day)
-    for item in qs:
-        tag = 'stage' if item.tags.count() else 'change'
-        image = item.tree
-        w  = WallItem(tag)
-        if user and item.author != user:
-            w.header = _('Related edit')
-        else:
-            w.header = _('Edit')
-        w.title = image.title
-        w.summary = item.description
-        w.url = reverse('wiki_img_editor', 
-                args=[image.slug]) + '?diff=%d' % item.revision
-        w.timestamp = item.created_at
-        w.user = item.author
-        w.user_name = item.author_name
-        w.email = item.author_email
-        yield w
-
-
-
-# TODO: marked for publishing
-
-
-def published_wall(user=None, max_len=None, day=None):
-    qs = BookPublishRecord.objects.select_related('book')
-    if user:
-        # TODO: published my book
-        qs = qs.filter(Q(user=user))
-    if max_len is not None:
-        qs = qs[:max_len]
-    if day is not None:
-        next_day = day + timedelta(1)
-        qs = qs.filter(timestamp__gte=day, timestamp__lt=next_day)
-    for item in qs:
-        w = WallItem('publish')
-        w.header = _('Publication')
-        w.title = item.book.title
-        w.timestamp = item.timestamp
-        w.url = item.book.get_absolute_url()
-        w.user = item.user
-        w.email = item.user.email
-        yield w
-
-
-def image_published_wall(user=None, max_len=None, day=None):
-    qs = ImagePublishRecord.objects.select_related('image')
-    if user:
-        # TODO: published my book
-        qs = qs.filter(Q(user=user))
-    if max_len is not None:
-        qs = qs[:max_len]
-    if day is not None:
-        next_day = day + timedelta(1)
-        qs = qs.filter(timestamp__gte=day, timestamp__lt=next_day)
-    for item in qs:
-        w = WallItem('publish')
-        w.header = _('Publication')
-        w.title = item.image.title
-        w.timestamp = item.timestamp
-        w.url = item.image.get_absolute_url()
-        w.user = item.user
-        w.email = item.user.email
-        yield w
-
-
-def big_wall(walls, max_len=None):
-    """
-        Takes some WallItem iterators and zips them into one big wall.
-        Input iterators must already be sorted by timestamp.
-    """
-    subwalls = []
-    for w in walls:
-        try:
-            subwalls.append([next(w), w])
-        except StopIteration:
-            pass
-
-    if max_len is None:
-        max_len = -1
-    while max_len and subwalls:
-        i, next_item = max(enumerate(subwalls), key=lambda x: x[1][0].timestamp)
-        yield next_item[0]
-        max_len -= 1
-        try:
-            next_item[0] = next(next_item[1])
-        except StopIteration:
-            del subwalls[i]
-
-
-@register.inclusion_tag("catalogue/wall.html", takes_context=True)
-def wall(context, user=None, max_len=100):
-    return {
-        "request": context['request'],
-        "STATIC_URL": context['STATIC_URL'],
-        "wall": big_wall([
-            changes_wall(user, max_len),
-            published_wall(user, max_len),
-            image_changes_wall(user, max_len),
-            image_published_wall(user, max_len),
-        ], max_len)}
-
-@register.inclusion_tag("catalogue/wall.html", takes_context=True)
-def day_wall(context, day):
-    return {
-        "request": context['request'],
-        "STATIC_URL": context['STATIC_URL'],
-        "wall": big_wall([
-            changes_wall(day=day),
-            published_wall(day=day),
-            image_changes_wall(day=day),
-            image_published_wall(day=day),
-        ])}
diff --git a/src/catalogue/test_utils.py b/src/catalogue/test_utils.py
deleted file mode 100644 (file)
index 7343d5c..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-"""Testing utilities."""
-
-from os.path import abspath, dirname, join
-
-
-def get_fixture(path):
-    f_path = join(dirname(abspath(__file__)), 'tests/files', path)
-    with open(f_path) as f:
-        return f.read()
diff --git a/src/catalogue/tests/__init__.py b/src/catalogue/tests/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/catalogue/tests/files/chunk1.xml b/src/catalogue/tests/files/chunk1.xml
deleted file mode 100644 (file)
index 6a75580..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-<utwor>
-  <liryka_l>
-
-<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
-<rdf:Description rdf:about="http://example.com/documents/book/test-book/">
-<dc:creator xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam</dc:creator>
-<dc:title xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Do M***</dc:title>
-<dc:publisher xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Fundacja Nowoczesna Polska</dc:publisher>
-<dc:subject.period xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Romantyzm</dc:subject.period>
-<dc:subject.type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Liryka</dc:subject.type>
-<dc:subject.genre xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Wiersz</dc:subject.genre>
-<!--dc:description xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Publikacja zrealizowana w ramach projektu Wolne Lektury (http://wolnelektury.pl). Reprodukcja cyfrowa wykonana przez Bibliotekę Narodową z egzemplarza pochodzącego ze zbiorów BN.</dc:description-->
-<dc:identifier.url xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m</dc:identifier.url>
-<dc:source.URL xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://www.polona.pl/Content/2222</dc:source.URL>
-<dc:source xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam (1798-1855), Poezje, tom 1 (Wiersze młodzieńcze - Ballady i romanse - Wiersze do r. 1824), Krakowska Spółdzielnia Wydawnicza, wyd. 2 zwiększone, Kraków, 1922</dc:source>
-
-<dc:rights xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Domena publiczna - Adam Mickiewicz zm. 1855</dc:rights>
-<dc:date.pd xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">1926</dc:date.pd>
-<dc:format xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">xml</dc:format>
-<dc:type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
-<dc:type xml:lang="en" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
-<dc:date xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">2007-09-06</dc:date>
-<dc:language xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">pol</dc:language>
-
-</rdf:Description>
-</rdf:RDF>
-
-<autor_utworu>Adam Mickiewicz</autor_utworu>
-<dzielo_nadrzedne>Sonety odeskie</dzielo_nadrzedne>
-<nazwa_utworu>Do M***</nazwa_utworu>
-
-<nota><akap>Wiérsz napisany w roku 1822</akap></nota>
-
-
-<strofa>Precz z moich oczu!... posłucham od razu,/
-Precz z mego serca!... i serce posłucha,/
-Precz z méj pamięci!... Nie! tego rozkazu/
-Moja i twoja pamięć nie posłucha.</strofa>
-
-<!-- TRIM_END -->
-</liryka_l>
-</utwor>
diff --git a/src/catalogue/tests/files/chunk2.xml b/src/catalogue/tests/files/chunk2.xml
deleted file mode 100644 (file)
index 63a243e..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<utwor><liryka_l>
-<!-- TRIM_BEGIN -->
-
-<strofa>Jak cień tém dłuższy, gdy padnie z daleka,/
-Tém szerzéj koło żałobne roztoczy,/
-Tak moja postać, im daléj ucieka,/
-Tém grubszym kirem twą pamięć pomroczy.</strofa>
-
-
-</liryka_l>
-</utwor>
diff --git a/src/catalogue/tests/files/expected.xml b/src/catalogue/tests/files/expected.xml
deleted file mode 100644 (file)
index ff225a0..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-<utwor>
-  <liryka_l>
-
-<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
-<rdf:Description rdf:about="http://example.com/documents/book/test-book/">
-<dc:creator xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam</dc:creator>
-<dc:title xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Do M***</dc:title>
-<dc:publisher xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Fundacja Nowoczesna Polska</dc:publisher>
-<dc:subject.period xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Romantyzm</dc:subject.period>
-<dc:subject.type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Liryka</dc:subject.type>
-<dc:subject.genre xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Wiersz</dc:subject.genre>
-<!--dc:description xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Publikacja zrealizowana w ramach projektu Wolne Lektury (http://wolnelektury.pl). Reprodukcja cyfrowa wykonana przez Bibliotekę Narodową z egzemplarza pochodzącego ze zbiorów BN.</dc:description-->
-<dc:identifier.url xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m</dc:identifier.url>
-<dc:source.URL xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://www.polona.pl/Content/2222</dc:source.URL>
-<dc:source xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam (1798-1855), Poezje, tom 1 (Wiersze młodzieńcze - Ballady i romanse - Wiersze do r. 1824), Krakowska Spółdzielnia Wydawnicza, wyd. 2 zwiększone, Kraków, 1922</dc:source>
-
-<dc:rights xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Domena publiczna - Adam Mickiewicz zm. 1855</dc:rights>
-<dc:date.pd xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">1926</dc:date.pd>
-<dc:format xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">xml</dc:format>
-<dc:type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
-<dc:type xml:lang="en" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
-<dc:date xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">2007-09-06</dc:date>
-<dc:language xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">pol</dc:language>
-
-</rdf:Description>
-</rdf:RDF>
-
-<autor_utworu>Adam Mickiewicz</autor_utworu>
-<dzielo_nadrzedne>Sonety odeskie</dzielo_nadrzedne>
-<nazwa_utworu>Do M***</nazwa_utworu>
-
-<nota><akap>Wiérsz napisany w roku 1822</akap></nota>
-
-
-<strofa>Precz z moich oczu!... posłucham od razu,/
-Precz z mego serca!... i serce posłucha,/
-Precz z méj pamięci!... Nie! tego rozkazu/
-Moja i twoja pamięć nie posłucha.</strofa>
-
-
-
-<strofa>Jak cień tém dłuższy, gdy padnie z daleka,/
-Tém szerzéj koło żałobne roztoczy,/
-Tak moja postać, im daléj ucieka,/
-Tém grubszym kirem twą pamięć pomroczy.</strofa>
-
-
-</liryka_l>
-</utwor>
diff --git a/src/catalogue/tests/test_book.py b/src/catalogue/tests/test_book.py
deleted file mode 100644 (file)
index 1d26e4d..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-"""Tests for manipulating books in the catalogue."""
-
-from django.test import TestCase
-from django.contrib.auth.models import User
-from catalogue.models import Book
-
-
-class ManipulationTests(TestCase):
-
-    def setUp(self):
-        self.user = User.objects.create(username='tester')
-        self.book1 = Book.create(self.user, 'book 1', slug='book1')
-        self.book2 = Book.create(self.user, 'book 2', slug='book2')
-
-    def test_append(self):
-        self.book1.append(self.book2)
-        self.assertEqual(Book.objects.all().count(), 1)
-        self.assertEqual(len(self.book1), 2)
-
-    def test_append_to_self(self):
-        with self.assertRaises(AssertionError):
-            self.book1.append(Book.objects.get(pk=self.book1.pk))
-        self.assertEqual(Book.objects.all().count(), 2)
-        self.assertEqual(len(self.book1), 1)
-
-    def test_prepend_history(self):
-        self.book1.prepend_history(self.book2)
-        self.assertEqual(Book.objects.all().count(), 1)
-        self.assertEqual(len(self.book1), 1)
-        self.assertEqual(self.book1.materialize(), 'book 1')
-
-    def test_prepend_history_to_self(self):
-        with self.assertRaises(AssertionError):
-            self.book1.prepend_history(self.book1)
-        self.assertEqual(Book.objects.all().count(), 2)
-        self.assertEqual(self.book1.materialize(), 'book 1')
-        self.assertEqual(self.book2.materialize(), 'book 2')
-
-    def test_split_book(self):
-        self.book1.chunk_set.create(number=2, title='Second chunk',
-                slug='book3')
-        self.book1[1].commit('I survived!')
-        self.assertEqual(len(self.book1), 2)
-        self.book1.split()
-        self.assertEqual(set([b.slug for b in Book.objects.all()]),
-                set(['book2', '1', 'book3']))
-        self.assertEqual(
-                Book.objects.get(slug='book3').materialize(),
-                'I survived!')
diff --git a/src/catalogue/tests/test_gallery.py b/src/catalogue/tests/test_gallery.py
deleted file mode 100644 (file)
index 32c0e0a..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-"""Tests for galleries of scans."""
-
-from os.path import join, basename, exists
-from os import makedirs, listdir
-from django.test import TestCase
-from django.contrib.auth.models import User
-from catalogue.models import Book
-from tempfile import mkdtemp
-from django.conf import settings
-
-
-class GalleryAppendTests(TestCase):
-    def setUp(self):
-        self.user = User.objects.create(username='tester')
-        self.book1 = Book.create(self.user, 'book 1', slug='book1')
-        self.book1.chunk_set.create(number=2, title='Second chunk',
-                slug='book1-2')
-        c=self.book1[1]
-        c.gallery_start=3
-        
-        self.scandir = join(settings.MEDIA_ROOT, settings.IMAGE_DIR)
-        if not exists(self.scandir):
-            makedirs(self.scandir)
-
-    def make_gallery(self, book, files):
-        d = mkdtemp('gallery', dir=self.scandir)
-        for named, cont in files.items():
-            f = open(join(d, named), 'w')
-            f.write(cont)
-            f.close()
-        book.gallery = basename(d)
-
-
-    def test_both_indexed(self):
-        self.book2 = Book.create(self.user, 'book 2', slug='book2')
-        self.book2.chunk_set.create(number=2, title='Second chunk of second book',
-                slug='book2-2')
-
-        c = self.book2[1]
-        c.gallery_start = 3
-        c.save()
-        
-        self.make_gallery(self.book1, {
-            '1-0001_1l' : 'aa',
-            '1-0001_2r' : 'bb',
-            '1-0002_1l' : 'cc',
-            '1-0002_2r' : 'dd',
-            })
-
-        self.make_gallery(self.book2, {
-            '1-0001_1l' : 'dd', # the same, should not be moved
-            '1-0001_2r' : 'ff',
-            '2-0002_1l' : 'gg',
-            '2-0002_2r' : 'hh',
-            })
-
-        self.book1.append(self.book2)
-
-        files = listdir(join(self.scandir, self.book1.gallery))
-        files.sort()
-        self.assertEqual(files, [
-            '1-0001_1l',
-            '1-0001_2r',
-            '1-0002_1l',
-            '1-0002_2r',
-            #            '2-0001_1l',
-            '2-0001_2r',
-            '3-0002_1l',
-            '3-0002_2r',
-            ])        
-
-        self.assertEqual((4, 6), (self.book1[2].gallery_start, self.book1[3].gallery_start))
-        
-        
-    def test_none_indexed(self):
-        self.book2 = Book.create(self.user, 'book 2', slug='book2')
-        self.make_gallery(self.book1, {
-            '0001_1l' : 'aa',
-            '0001_2r' : 'bb',
-            '0002_1l' : 'cc',
-            '0002_2r' : 'dd',
-            })
-
-        self.make_gallery(self.book2, {
-            '0001_1l' : 'ee',
-            '0001_2r' : 'ff',
-            '0002_1l' : 'gg',
-            '0002_2r' : 'hh',
-            })
-
-        self.book1.append(self.book2)
-
-        files = listdir(join(self.scandir, self.book1.gallery))
-        files.sort()
-        self.assertEqual(files, [
-            '0-0001_1l',
-            '0-0001_2r',
-            '0-0002_1l',
-            '0-0002_2r',
-            '1-0001_1l',
-            '1-0001_2r',
-            '1-0002_1l',
-            '1-0002_2r',
-            ])        
-
-
-    def test_none_indexed(self):
-        self.book2 = Book.create(self.user, 'book 2', slug='book2')
-        self.make_gallery(self.book1, {
-            '1-0001_1l' : 'aa',
-            '1-0001_2r' : 'bb',
-            '1002_1l' : 'cc',
-            '1002_2r' : 'dd',
-            })
-
-        self.make_gallery(self.book2, {
-            '0001_1l' : 'ee',
-            '0001_2r' : 'ff',
-            '0002_1l' : 'gg',
-            '0002_2r' : 'hh',
-            })
-
-        self.book1.append(self.book2)
-
-        files = listdir(join(self.scandir, self.book1.gallery))
-        files.sort()
-        self.assertEqual(files, [
-            '0-1-0001_1l',
-            '0-1-0001_2r',
-            '0-1002_1l',
-            '0-1002_2r',
-            '1-0001_1l',
-            '1-0001_2r',
-            '1-0002_1l',
-            '1-0002_2r',
-            ])        
-
diff --git a/src/catalogue/tests/test_publish.py b/src/catalogue/tests/test_publish.py
deleted file mode 100644 (file)
index 930d88a..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-"""Tests for the publishing process."""
-
-from catalogue.test_utils import get_fixture
-
-from mock import patch
-from django.test import TestCase
-from django.contrib.auth.models import User
-from catalogue.models import Book
-
-
-class PublishTests(TestCase):
-    def setUp(self):
-        self.user = User.objects.create(username='tester')
-        self.text1 = get_fixture('chunk1.xml')
-        self.book = Book.create(self.user, self.text1, slug='test-book')
-
-    @patch('apiclient.api_call')
-    def test_unpublishable(self, api_call):
-        with self.assertRaises(AssertionError):
-            self.book.publish(self.user)
-
-    @patch('apiclient.api_call')
-    def test_publish(self, api_call):
-        self.book[0].head.set_publishable(True)
-        self.book.publish(self.user)
-        api_call.assert_called_with(self.user, 'books/', {"book_xml": self.text1, "days": 0}, beta=False)
-
-    @patch('apiclient.api_call')
-    def test_publish_multiple(self, api_call):
-        self.book[0].head.set_publishable(True)
-        self.book[0].split(slug='part-2')
-        self.book[1].commit(get_fixture('chunk2.xml'))
-        self.book[1].head.set_publishable(True)
-        self.book.publish(self.user)
-        api_call.assert_called_with(self.user, 'books/', {"book_xml": get_fixture('expected.xml'), "days": 0}, beta=False)
diff --git a/src/catalogue/tests/test_xml_updater.py b/src/catalogue/tests/test_xml_updater.py
deleted file mode 100644 (file)
index a5ce567..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-"""XmlUpdater tests."""
-
-from catalogue.test_utils import get_fixture
-from django.test import TestCase
-from django.contrib.auth.models import User
-from catalogue.models import Book
-from catalogue.management import XmlUpdater
-from librarian import DCNS
-
-
-class XmlUpdaterTests(TestCase):
-    class SimpleUpdater(XmlUpdater):
-        @XmlUpdater.fixes_elements('.//' + DCNS('title'))
-        def fix_title(element, **kwargs):
-            element.text = element.text + " fixed"
-            return True
-
-    def setUp(self):
-        self.user = User.objects.create(username='tester')
-        text = get_fixture('chunk1.xml')
-        Book.create(self.user, text, slug='test-book')
-        self.title = "Do M***"
-
-    def test_xml_updater(self):
-        self.SimpleUpdater().run(self.user)
-        self.assertEqual(
-            Book.objects.get(slug='test-book').wldocument(
-                publishable=False).book_info.title,
-            self.title + " fixed"
-            )
diff --git a/src/catalogue/urls.py b/src/catalogue/urls.py
deleted file mode 100644 (file)
index 2cebacf..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.conf.urls import url
-from django.contrib.auth.decorators import permission_required
-from django.views.generic import RedirectView
-from catalogue.feeds import PublishTrackFeed
-from . import views
-
-
-urlpatterns = [
-    url(r'^$', RedirectView.as_view(url='catalogue/', permanent=False)),
-
-    url(r'^images/$', views.image_list, name='catalogue_image_list'),
-    url(r'^image/(?P<slug>[^/]+)/$', views.image, name="catalogue_image"),
-    url(r'^image/(?P<slug>[^/]+)/publish$', views.publish_image,
-            name="catalogue_publish_image"),
-
-    url(r'^catalogue/$', views.document_list, name='catalogue_document_list'),
-    url(r'^user/$', views.my, name='catalogue_user'),
-    url(r'^user/(?P<username>[^/]+)/$', views.user, name='catalogue_user'),
-    url(r'^users/$', views.users, name='catalogue_users'),
-    url(r'^activity/$', views.activity, name='catalogue_activity'),
-    url(r'^activity/(?P<isodate>\d{4}-\d{2}-\d{2})/$', 
-        views.activity, name='catalogue_activity'),
-
-    url(r'^upload/$',
-        views.upload, name='catalogue_upload'),
-
-    url(r'^create/(?P<slug>[^/]*)/',
-        views.create_missing, name='catalogue_create_missing'),
-    url(r'^create/',
-        views.create_missing, name='catalogue_create_missing'),
-
-    url(r'^book/(?P<slug>[^/]+)/publish$', views.publish, name="catalogue_publish"),
-
-    url(r'^book/(?P<slug>[^/]+)/$', views.book, name="catalogue_book"),
-    url(r'^book/(?P<slug>[^/]+)/gallery/$',
-            permission_required('catalogue.change_book')(views.GalleryView.as_view()),
-            name="catalogue_book_gallery"),
-    url(r'^book/(?P<slug>[^/]+)/xml$', views.book_xml, name="catalogue_book_xml"),
-    url(r'^book/dc/(?P<slug>[^/]+)/xml$', views.book_xml_dc, name="catalogue_book_xml_dc"),
-    url(r'^book/(?P<slug>[^/]+)/txt$', views.book_txt, name="catalogue_book_txt"),
-    url(r'^book/(?P<slug>[^/]+)/html$', views.book_html, name="catalogue_book_html"),
-    url(r'^book/(?P<slug>[^/]+)/epub$', views.book_epub, name="catalogue_book_epub"),
-    url(r'^book/(?P<slug>[^/]+)/mobi$', views.book_mobi, name="catalogue_book_mobi"),
-    url(r'^book/(?P<slug>[^/]+)/pdf$', views.book_pdf, name="catalogue_book_pdf"),
-    url(r'^book/(?P<slug>[^/]+)/pdf-mobile$', views.book_pdf, kwargs={'mobile': True}, name="catalogue_book_pdf_mobile"),
-
-    url(r'^chunk_add/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
-        views.chunk_add, name="catalogue_chunk_add"),
-    url(r'^chunk_edit/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
-        views.chunk_edit, name="catalogue_chunk_edit"),
-    url(r'^book_append/(?P<slug>[^/]+)/$',
-        views.book_append, name="catalogue_book_append"),
-    url(r'^chunk_mass_edit',
-        views.chunk_mass_edit, name='catalogue_chunk_mass_edit'),
-    url(r'^image_mass_edit',
-        views.image_mass_edit, name='catalogue_image_mass_edit'),
-
-    url(r'^track/(?P<slug>[^/]*)/$', PublishTrackFeed()),
-    url(r'^active/$', views.active_users_list, name='active_users_list'),
-
-    url(r'^mark-final/$', views.mark_final, name='mark_final'),
-    url(r'^mark-final-completed/$', views.mark_final_completed, name='mark_final_completed'),
-]
diff --git a/src/catalogue/views.py b/src/catalogue/views.py
deleted file mode 100644 (file)
index 019d25a..0000000
+++ /dev/null
@@ -1,672 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from collections import defaultdict
-from datetime import datetime, date, timedelta
-import logging
-import os
-from urllib.parse import unquote, urlsplit, urlunsplit
-
-from django.conf import settings
-from django.contrib import auth
-from django.contrib.auth.models import User
-from django.contrib.auth.decorators import login_required, permission_required
-from django.urls import reverse
-from django.db.models import Count, Q
-from django.db import transaction
-from django import http
-from django.http import Http404, HttpResponse, HttpResponseForbidden
-from django.http.response import HttpResponseRedirect
-from django.shortcuts import get_object_or_404, render
-from django.utils.encoding import iri_to_uri
-from django.utils.http import urlquote_plus
-from django.utils.translation import ugettext_lazy as _
-from django.views.decorators.http import require_POST
-from django_cas_ng.decorators import user_passes_test
-
-from apiclient import NotAuthorizedError
-from catalogue import forms
-from catalogue import helpers
-from catalogue.helpers import active_tab
-from catalogue.models import (Book, Chunk, Image, BookPublishRecord, 
-        ChunkPublishRecord, ImagePublishRecord, Project)
-from fileupload.views import UploadView
-
-#
-# Quick hack around caching problems, TODO: use ETags
-#
-from django.views.decorators.cache import never_cache
-
-logger = logging.getLogger("fnp.catalogue")
-
-
-@active_tab('all')
-@never_cache
-def document_list(request):
-    return render(request, 'catalogue/document_list.html')
-
-
-@active_tab('images')
-@never_cache
-def image_list(request, user=None):
-    return render(request, 'catalogue/image_list.html')
-
-
-@never_cache
-def user(request, username):
-    user = get_object_or_404(User, username=username)
-    return render(request, 'catalogue/user_page.html', {"viewed_user": user})
-
-
-@login_required
-@active_tab('my')
-@never_cache
-def my(request):
-    last_books = sorted(request.session.get("wiki_last_books", {}).items(),
-        key=lambda x: x[1]['time'], reverse=True)
-    for k, v in last_books:
-        v['time'] = datetime.fromtimestamp(v['time'])
-    return render(request, 'catalogue/my_page.html', {
-        'last_books': last_books,
-        "logout_to": '/',
-        })
-
-
-@active_tab('users')
-def users(request):
-    return render(request, 'catalogue/user_list.html', {
-        'users': User.objects.all().annotate(count=Count('chunk')).order_by(
-            '-count', 'last_name', 'first_name'),
-    })
-
-
-@active_tab('activity')
-def activity(request, isodate=None):
-    today = date.today()
-    try:
-        day = helpers.parse_isodate(isodate)
-    except ValueError:
-        day = today
-
-    if day > today:
-        raise Http404
-    if day != today:
-        next_day = day + timedelta(1)
-    prev_day = day - timedelta(1)
-
-    return render(request, 'catalogue/activity.html', locals())
-
-
-@never_cache
-def logout_then_redirect(request):
-    auth.logout(request)
-    return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
-
-
-@permission_required('catalogue.add_book')
-@active_tab('create')
-def create_missing(request, slug=None):
-    if slug is None:
-        slug = ''
-    slug = slug.replace(' ', '-')
-
-    if request.method == "POST":
-        form = forms.DocumentCreateForm(request.POST, request.FILES)
-        if form.is_valid():
-            
-            if request.user.is_authenticated:
-                creator = request.user
-            else:
-                creator = None
-            book = Book.create(
-                text=form.cleaned_data['text'],
-                creator=creator,
-                slug=form.cleaned_data['slug'],
-                title=form.cleaned_data['title'],
-                gallery=form.cleaned_data['gallery'],
-            )
-
-            return http.HttpResponseRedirect(reverse("catalogue_book", args=[book.slug]))
-    else:
-        form = forms.DocumentCreateForm(initial={
-                "slug": slug,
-                "title": slug.replace('-', ' ').title(),
-                "gallery": slug,
-        })
-
-    return render(request, "catalogue/document_create_missing.html", {
-        "slug": slug,
-        "form": form,
-
-        "logout_to": '/',
-    })
-
-
-@permission_required('catalogue.add_book')
-@active_tab('upload')
-def upload(request):
-    if request.method == "POST":
-        form = forms.DocumentsUploadForm(request.POST, request.FILES)
-        if form.is_valid():
-            from slugify import slugify
-
-            if request.user.is_authenticated:
-                creator = request.user
-            else:
-                creator = None
-
-            zip = form.cleaned_data['zip']
-            skipped_list = []
-            ok_list = []
-            error_list = []
-            slugs = {}
-            existing = [book.slug for book in Book.objects.all()]
-            for filename in zip.namelist():
-                if filename[-1] == '/':
-                    continue
-                title = os.path.basename(filename)[:-4]
-                slug = slugify(title)
-                if not (slug and filename.endswith('.xml')):
-                    skipped_list.append(filename)
-                elif slug in slugs:
-                    error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
-                elif slug in existing:
-                    error_list.append((filename, slug, _('Slug already used in repository.')))
-                else:
-                    try:
-                        zip.read(filename).decode('utf-8') # test read
-                        ok_list.append((filename, slug, title))
-                    except UnicodeDecodeError:
-                        error_list.append((filename, title, _('File should be UTF-8 encoded.')))
-                    slugs[slug] = filename
-
-            if not error_list:
-                for filename, slug, title in ok_list:
-                    book = Book.create(
-                        text=zip.read(filename).decode('utf-8'),
-                        creator=creator,
-                        slug=slug,
-                        title=title,
-                    )
-
-            return render(request, "catalogue/document_upload.html", {
-                "form": form,
-                "ok_list": ok_list,
-                "skipped_list": skipped_list,
-                "error_list": error_list,
-
-                "logout_to": '/',
-            })
-    else:
-        form = forms.DocumentsUploadForm()
-
-    return render(request, "catalogue/document_upload.html", {
-        "form": form,
-
-        "logout_to": '/',
-    })
-
-
-def serve_xml(request, book, slug):
-    if not book.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-    xml = book.materialize(publishable=True)
-    response = http.HttpResponse(xml, content_type='application/xml')
-    response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
-    return response
-
-
-@never_cache
-def book_xml(request, slug):
-    book = get_object_or_404(Book, slug=slug)
-    return serve_xml(request, book, slug)
-
-
-@never_cache
-def book_xml_dc(request, slug):
-    book = get_object_or_404(Book, dc_slug=slug)
-    return serve_xml(request, book, slug)
-
-
-@never_cache
-def book_txt(request, slug):
-    book = get_object_or_404(Book, slug=slug)
-    if not book.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-
-    doc = book.wldocument()
-    text = doc.as_text().get_bytes()
-    response = http.HttpResponse(text, content_type='text/plain')
-    response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
-    return response
-
-
-@never_cache
-def book_html(request, slug):
-    book = get_object_or_404(Book, slug=slug)
-    if not book.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-
-    doc = book.wldocument(parse_dublincore=False)
-    html = doc.as_html(options={'gallery': "'%s'" % book.gallery_url()})
-
-    html = html.get_bytes().decode('utf-8') if html is not None else ''
-    # response = http.HttpResponse(html, content_type='text/html')
-    # return response
-    # book_themes = {}
-    # for fragment in book.fragments.all().iterator():
-    #     for theme in fragment.tags.filter(category='theme').iterator():
-    #         book_themes.setdefault(theme, []).append(fragment)
-
-    # book_themes = book_themes.items()
-    # book_themes.sort(key=lambda s: s[0].sort_key)
-    return render(request, 'catalogue/book_text.html', locals())
-
-
-@never_cache
-def book_pdf(request, slug, mobile=False):
-    book = get_object_or_404(Book, slug=slug)
-    if not book.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-
-    # TODO: move to celery
-    doc = book.wldocument()
-    # TODO: error handling
-    customizations = ['26pt', 'nothemes', 'nomargins', 'notoc'] if mobile else None
-    pdf_file = doc.as_pdf(cover=True, ilustr_path=book.gallery_path(), customizations=customizations)
-    from catalogue.ebook_utils import serve_file
-    return serve_file(pdf_file.get_filename(),
-                book.slug + '.pdf', 'application/pdf')
-
-
-@never_cache
-def book_epub(request, slug):
-    book = get_object_or_404(Book, slug=slug)
-    if not book.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-
-    # TODO: move to celery
-    doc = book.wldocument()
-    # TODO: error handling
-    epub = doc.as_epub(ilustr_path=book.gallery_path()).get_bytes()
-    response = HttpResponse(content_type='application/epub+zip')
-    response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.epub'
-    response.write(epub)
-    return response
-
-
-@never_cache
-def book_mobi(request, slug):
-    book = get_object_or_404(Book, slug=slug)
-    if not book.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-
-    # TODO: move to celery
-    doc = book.wldocument()
-    # TODO: error handling
-    mobi = doc.as_mobi(ilustr_path=book.gallery_path()).get_bytes()
-    response = HttpResponse(content_type='application/x-mobipocket-ebook')
-    response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.mobi'
-    response.write(mobi)
-    return response
-
-
-@never_cache
-def revision(request, slug, chunk=None):
-    try:
-        doc = Chunk.get(slug, chunk)
-    except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
-        raise Http404
-    if not doc.book.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-    return http.HttpResponse(str(doc.revision()))
-
-
-def book(request, slug):
-    book = get_object_or_404(Book, slug=slug)
-    if not book.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-
-    if request.user.has_perm('catalogue.change_book'):
-        if request.method == "POST":
-            form = forms.BookForm(request.POST, instance=book)
-            if form.is_valid():
-                form.save()
-                return http.HttpResponseRedirect(book.get_absolute_url())
-        else:
-            form = forms.BookForm(instance=book)
-        publish_options_form = forms.PublishOptionsForm()
-        editable = True
-    else:
-        form = forms.ReadonlyBookForm(instance=book)
-        publish_options_form = forms.PublishOptionsForm()
-        editable = False
-
-    publish_error = book.publishable_error()
-    publishable = publish_error is None
-
-    return render(request, "catalogue/book_detail.html", {
-        "book": book,
-        "publishable": publishable,
-        "publishable_error": publish_error,
-        "form": form,
-        "publish_options_form": publish_options_form,
-        "editable": editable,
-    })
-
-
-def image(request, slug):
-    image = get_object_or_404(Image, slug=slug)
-    if not image.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-
-    if request.user.has_perm('catalogue.change_image'):
-        if request.method == "POST":
-            form = forms.ImageForm(request.POST, instance=image)
-            if form.is_valid():
-                form.save()
-                return http.HttpResponseRedirect(image.get_absolute_url())
-        else:
-            form = forms.ImageForm(instance=image)
-        editable = True
-    else:
-        form = forms.ReadonlyImageForm(instance=image)
-        editable = False
-
-    publish_error = image.publishable_error()
-    publishable = publish_error is None
-
-    return render(request, "catalogue/image_detail.html", {
-        "object": image,
-        "publishable": publishable,
-        "publishable_error": publish_error,
-        "form": form,
-        "editable": editable,
-    })
-
-
-@permission_required('catalogue.add_chunk')
-def chunk_add(request, slug, chunk):
-    try:
-        doc = Chunk.get(slug, chunk)
-    except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
-        raise Http404
-    if not doc.book.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-
-    if request.method == "POST":
-        form = forms.ChunkAddForm(request.POST, instance=doc)
-        if form.is_valid():
-            if request.user.is_authenticated:
-                creator = request.user
-            else:
-                creator = None
-            doc.split(creator=creator,
-                slug=form.cleaned_data['slug'],
-                title=form.cleaned_data['title'],
-                gallery_start=form.cleaned_data['gallery_start'],
-                user=form.cleaned_data['user'],
-                stage=form.cleaned_data['stage']
-            )
-
-            return http.HttpResponseRedirect(doc.book.get_absolute_url())
-    else:
-        form = forms.ChunkAddForm(initial={
-                "slug": str(doc.number + 1),
-                "title": "cz. %d" % (doc.number + 1, ),
-        })
-
-    return render(request, "catalogue/chunk_add.html", {
-        "chunk": doc,
-        "form": form,
-    })
-
-
-@login_required
-def chunk_edit(request, slug, chunk):
-    try:
-        doc = Chunk.get(slug, chunk)
-    except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
-        raise Http404
-    if not doc.book.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-
-    if request.method == "POST":
-        form = forms.ChunkForm(request.POST, instance=doc)
-        if form.is_valid():
-            form.save()
-            go_next = request.GET.get('next', None)
-            if go_next:
-                go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
-            else:
-                go_next = doc.book.get_absolute_url()
-            return http.HttpResponseRedirect(go_next)
-    else:
-        form = forms.ChunkForm(instance=doc)
-
-    referer = request.META.get('HTTP_REFERER')
-    if referer:
-        parts = urlsplit(referer)
-        parts = ['', ''] + list(parts[2:])
-        go_next = urlquote_plus(urlunsplit(parts))
-    else:
-        go_next = ''
-
-    return render(request, "catalogue/chunk_edit.html", {
-        "chunk": doc,
-        "form": form,
-        "go_next": go_next,
-    })
-
-
-@transaction.atomic
-@login_required
-@require_POST
-def chunk_mass_edit(request):
-    ids = [int(i) for i in request.POST.get('ids').split(',') if i.strip()]
-    chunks = list(Chunk.objects.filter(id__in=ids))
-    
-    stage = request.POST.get('stage')
-    if stage:
-        try:
-            stage = Chunk.tag_model.objects.get(slug=stage)
-        except Chunk.DoesNotExist as e:
-            stage = None
-       
-        for c in chunks: c.stage = stage
-
-    username = request.POST.get('user')
-    logger.info("username: %s" % username)
-    logger.info(request.POST)
-    if username:
-        try:
-            user = User.objects.get(username=username)
-        except User.DoesNotExist as e:
-            user = None
-            
-        for c in chunks: c.user = user
-
-    project_id = request.POST.get('project')
-    if project_id:
-        try:
-            project = Project.objects.get(pk=int(project_id))
-        except (Project.DoesNotExist, ValueError) as e:
-            project = None
-        for c in chunks:
-            book = c.book
-            book.project = project
-            book.save()
-
-    for c in chunks: c.save()
-
-    return HttpResponse("", content_type="text/plain")
-
-
-@transaction.atomic
-@login_required
-@require_POST
-def image_mass_edit(request):
-    ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(',')))
-    images = map(lambda i: Image.objects.get(id=i), ids)
-    
-    stage = request.POST.get('stage')
-    if stage:
-        try:
-            stage = Image.tag_model.objects.get(slug=stage)
-        except Image.DoesNotExist as e:
-            stage = None
-       
-        for c in images: c.stage = stage
-
-    username = request.POST.get('user')
-    logger.info("username: %s" % username)
-    logger.info(request.POST)
-    if username:
-        try:
-            user = User.objects.get(username=username)
-        except User.DoesNotExist as e:
-            user = None
-            
-        for c in images: c.user = user
-
-    project_id = request.POST.get('project')
-    if project_id:
-        try:
-            project = Project.objects.get(pk=int(project_id))
-        except (Project.DoesNotExist, ValueError) as e:
-            project = None
-        for c in images:
-            c.project = project
-
-    for c in images: c.save()
-
-    return HttpResponse("", content_type="text/plain")
-
-
-@permission_required('catalogue.change_book')
-def book_append(request, slug):
-    book = get_object_or_404(Book, slug=slug)
-    if not book.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-
-    if request.method == "POST":
-        form = forms.BookAppendForm(book, request.POST)
-        if form.is_valid():
-            append_to = form.cleaned_data['append_to']
-            append_to.append(book)
-            return http.HttpResponseRedirect(append_to.get_absolute_url())
-    else:
-        form = forms.BookAppendForm(book)
-    return render(request, "catalogue/book_append_to.html", {
-        "book": book,
-        "form": form,
-
-        "logout_to": '/',
-    })
-
-
-@require_POST
-@login_required
-def publish(request, slug):
-    form = forms.PublishOptionsForm(request.POST)
-    if form.is_valid():
-        days = form.cleaned_data['days']
-        beta = form.cleaned_data['beta']
-    else:
-        days = 0
-        beta = False
-    book = get_object_or_404(Book, slug=slug)
-    if not book.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-
-    try:
-        protocol = 'https://' if request.is_secure() else 'http://'
-        book.publish(request.user, host=protocol + request.get_host(), days=days, beta=beta)
-    except NotAuthorizedError:
-        return http.HttpResponseRedirect(reverse('apiclient_oauth' if not beta else 'apiclient_beta_oauth'))
-    except BaseException as e:
-        return http.HttpResponse(repr(e))
-    else:
-        return http.HttpResponseRedirect(book.get_absolute_url())
-
-
-@require_POST
-@login_required
-def publish_image(request, slug):
-    image = get_object_or_404(Image, slug=slug)
-    if not image.accessible(request):
-        return HttpResponseForbidden("Not authorized.")
-
-    try:
-        image.publish(request.user)
-    except NotAuthorizedError:
-        return http.HttpResponseRedirect(reverse('apiclient_oauth'))
-    except BaseException as e:
-        return http.HttpResponse(e)
-    else:
-        return http.HttpResponseRedirect(image.get_absolute_url())
-
-
-class GalleryView(UploadView):
-    def get_object(self, request, slug):
-        book = get_object_or_404(Book, slug=slug)
-        if not book.gallery:
-            raise Http404
-        return book
-
-    def breadcrumbs(self):
-        return [
-            (_('books'), reverse('catalogue_document_list')),
-            (self.object.title, self.object.get_absolute_url()),
-            (_('scan gallery'),),
-        ]
-
-    def get_directory(self):
-        return "%s%s/" % (settings.IMAGE_DIR, self.object.gallery)
-
-
-def active_users_list(request):
-    year = int(request.GET.get('y', date.today().year))
-    by_user = defaultdict(lambda: 0)
-    by_email = defaultdict(lambda: 0)
-    names_by_email = defaultdict(set)
-    for change_model in (Chunk.change_model, Image.change_model):
-        for c in change_model.objects.filter(
-                created_at__year=year).order_by(
-                'author', 'author_email', 'author_name').values(
-                'author', 'author_name', 'author_email').annotate(
-                c=Count('author'), ce=Count('author_email')).distinct():
-            if c['author']:
-                by_user[c['author']] += c['c']
-            else:
-                by_email[c['author_email']] += c['ce']
-                if (c['author_name'] or '').strip():
-                    names_by_email[c['author_email']].add(c['author_name'])
-    for user in User.objects.filter(pk__in=by_user):
-        by_email[user.email] += by_user[user.pk]
-        names_by_email[user.email].add("%s %s" % (user.first_name, user.last_name))
-
-    active_users = []
-    for email, count in by_email.items():
-        active_users.append((email, names_by_email[email], count))
-    active_users.sort(key=lambda x: -x[2])
-    return render(request, 'catalogue/active_users_list.html', {
-        'users': active_users,
-        'year': year,
-    })
-
-
-@user_passes_test(lambda u: u.is_superuser)
-def mark_final(request):
-    if request.method == 'POST':
-        form = forms.MarkFinalForm(data=request.POST)
-        if form.is_valid():
-            form.save()
-            return HttpResponseRedirect(reverse('mark_final_completed'))
-    else:
-        form = forms.MarkFinalForm()
-    return render(request, 'catalogue/mark_final.html', {'form': form})
-
-
-def mark_final_completed(request):
-    return render(request, 'catalogue/mark_final_completed.html')
diff --git a/src/catalogue/xml_tools.py b/src/catalogue/xml_tools.py
deleted file mode 100644 (file)
index 917e62a..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from copy import deepcopy
-import re
-
-from lxml import etree
-from catalogue.constants import TRIM_BEGIN, TRIM_END, MASTERS
-
-RE_TRIM_BEGIN = re.compile("^<!--%s-->$" % TRIM_BEGIN, re.M)
-RE_TRIM_END = re.compile("^<!--%s-->$" % TRIM_END, re.M)
-
-
-class ParseError(BaseException):
-    pass
-
-
-def _trim(text, trim_begin=True, trim_end=True):
-    """ 
-        Cut off everything before RE_TRIM_BEGIN and after RE_TRIM_END, so
-        that eg. one big XML file can be compiled from many small XML files.
-    """
-    if trim_begin:
-        text = RE_TRIM_BEGIN.split(text, maxsplit=1)[-1]
-    if trim_end:
-        text = RE_TRIM_END.split(text, maxsplit=1)[0]
-    return text
-
-
-def compile_text(parts):
-    """ 
-        Compiles full text from an iterable of parts,
-        trimming where applicable.
-    """
-    texts = []
-    trim_begin = False
-    text = ''
-    for next_text in parts:
-        if not next_text:
-            continue
-        if text:
-            # trim the end, because there's more non-empty text
-            # don't trim beginning, if `text' is the first non-empty part
-            texts.append(_trim(text, trim_begin=trim_begin))
-            trim_begin = True
-        text = next_text
-    # don't trim the end, because there's no more text coming after `text'
-    # only trim beginning if it's not still the first non-empty
-    texts.append(_trim(text, trim_begin=trim_begin, trim_end=False))
-    return "".join(texts)
-
-
-def add_trim_begin(text):
-    trim_tag = etree.Comment(TRIM_BEGIN)
-    e = etree.fromstring(text)
-    for master in e[::-1]:
-        if master.tag in MASTERS:
-            break
-    if master.tag not in MASTERS:
-        raise ParseError('No master tag found!')
-
-    master.insert(0, trim_tag)
-    trim_tag.tail = '\n\n\n' + (master.text or '')
-    master.text = '\n'
-    return str(etree.tostring(e, encoding="utf-8"), 'utf-8')
-
-
-def add_trim_end(text):
-    trim_tag = etree.Comment(TRIM_END)
-    e = etree.fromstring(text)
-    for master in e[::-1]:
-        if master.tag in MASTERS:
-            break
-    if master.tag not in MASTERS:
-        raise ParseError('No master tag found!')
-
-    master.append(trim_tag)
-    trim_tag.tail = '\n'
-    prev = trim_tag.getprevious()
-    if prev is not None:
-        prev.tail = (prev.tail or '') + '\n\n\n'
-    else:
-        master.text = (master.text or '') + '\n\n\n'
-    return str(etree.tostring(e, encoding="utf-8"), 'utf-8')
-
-
-def split_xml(text):
-    """Splits text into chapters.
-
-    All this stuff really must go somewhere else.
-
-    """
-    src = etree.fromstring(text)
-    chunks = []
-
-    splitter = u'naglowek_rozdzial'
-    parts = src.findall('.//naglowek_rozdzial')
-    while parts:
-        # copy the document
-        copied = deepcopy(src)
-
-        element = parts[-1]
-
-        # find the chapter's title
-        name_elem = deepcopy(element)
-        for tag in 'extra', 'motyw', 'pa', 'pe', 'pr', 'pt', 'uwaga':
-            for a in name_elem.findall('.//' + tag):
-                a.text=''
-                del a[:]
-        name = etree.tostring(name_elem, method='text', encoding='utf-8').strip()
-
-        # in the original, remove everything from the start of the last chapter
-        parent = element.getparent()
-        del parent[parent.index(element):]
-        element, parent = parent, parent.getparent()
-        while parent is not None:
-            del parent[parent.index(element) + 1:]
-            element, parent = parent, parent.getparent()
-
-        # in the copy, remove everything before the last chapter
-        element = copied.findall('.//naglowek_rozdzial')[-1]
-        parent = element.getparent()
-        while parent is not None:
-            parent.text = None
-            while parent[0] is not element:
-                del parent[0]
-            element, parent = parent, parent.getparent()
-        chunks[:0] = [[name,
-            str(etree.tostring(copied, encoding='utf-8'), 'utf-8')
-            ]]
-
-        parts = src.findall('.//naglowek_rozdzial')
-
-    chunks[:0] = [[u'początek',
-        str(etree.tostring(src, encoding='utf-8'), 'utf-8')
-        ]]
-
-    for ch in chunks[1:]:
-        ch[1] = add_trim_begin(ch[1])
-    for ch in chunks[:-1]:
-        ch[1] = add_trim_end(ch[1])
-
-    return chunks
index ba98184..56f5665 100644 (file)
@@ -1,4 +1,4 @@
-{% extends "catalogue/base.html" %}
+{% extends "documents/base.html" %}
 {% load i18n %}
 {% load bootstrap4 %}
 
 {% load i18n %}
 {% load bootstrap4 %}
 
index db9b176..670b3ce 100644 (file)
@@ -1,4 +1,4 @@
-{% extends "catalogue/base.html" %}
+{% extends "documents/base.html" %}
 {% load i18n %}
 {% load thumbnail %}
 {% load build_absolute_uri from fnp_common %}
 {% load i18n %}
 {% load thumbnail %}
 {% load build_absolute_uri from fnp_common %}
index 87971df..2d5608c 100644 (file)
@@ -1,4 +1,4 @@
-{% extends "catalogue/base.html" %}
+{% extends "documents/base.html" %}
 {% load i18n %}
 {% load thumbnail pagination_tags %}
 
 {% load i18n %}
 {% load thumbnail pagination_tags %}
 
index b1261e6..bfc3ee3 100644 (file)
@@ -8,8 +8,8 @@ from django.http import HttpResponse, HttpResponseRedirect, Http404
 from django.shortcuts import get_object_or_404, render
 from django.views.decorators.csrf import csrf_exempt
 from django.views.decorators.http import require_POST
 from django.shortcuts import get_object_or_404, render
 from django.views.decorators.csrf import csrf_exempt
 from django.views.decorators.http import require_POST
-from catalogue.helpers import active_tab
-from catalogue.models import Chunk
+from documents.helpers import active_tab
+from documents.models import Chunk
 from cover.models import Image
 from cover import forms
 
 from cover.models import Image
 from cover import forms
 
diff --git a/src/documents/__init__.py b/src/documents/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/documents/admin.py b/src/documents/admin.py
new file mode 100644 (file)
index 0000000..b793303
--- /dev/null
@@ -0,0 +1,20 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.contrib import admin
+from . import models
+
+class BookAdmin(admin.ModelAdmin):
+    list_display = ['title', 'public', '_published', '_new_publishable', 'project']
+    list_filter = ['public', '_published', '_new_publishable', 'project']
+    prepopulated_fields = {'slug': ['title']}
+    search_fields = ['title']
+
+
+admin.site.register(models.Project)
+admin.site.register(models.Book, BookAdmin)
+admin.site.register(models.Chunk)
+admin.site.register(models.Chunk.tag_model)
+
+admin.site.register(models.Image)
+admin.site.register(models.Image.tag_model)
diff --git a/src/documents/constants.py b/src/documents/constants.py
new file mode 100644 (file)
index 0000000..4775d18
--- /dev/null
@@ -0,0 +1,14 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+TRIM_BEGIN = " TRIM_BEGIN "
+TRIM_END = " TRIM_END "
+
+MASTERS = ['powiesc',
+           'opowiadanie',
+           'liryka_l',
+           'liryka_lp',
+           'dramat_wierszowany_l',
+           'dramat_wierszowany_lp',
+           'dramat_wspolczesny',
+           ]
diff --git a/src/documents/ebook_utils.py b/src/documents/ebook_utils.py
new file mode 100644 (file)
index 0000000..e75ed77
--- /dev/null
@@ -0,0 +1,34 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from io import BytesIO
+from .models import Book
+from librarian import DocProvider
+from django.http import HttpResponse
+
+
+class RedakcjaDocProvider(DocProvider):
+    """Used for getting books' children."""
+
+    def __init__(self, publishable):
+        self.publishable = publishable
+
+    def by_slug(self, slug):
+        return BytesIO(Book.objects.get(dc_slug=slug
+                    ).materialize(publishable=self.publishable
+                    ).encode('utf-8'))
+
+
+def serve_file(file_path, name, mime_type):
+    def read_chunks(f, size=8192):
+        chunk = f.read(size)
+        while chunk:
+            yield chunk
+            chunk = f.read(size)
+
+    response = HttpResponse(content_type=mime_type)
+    response['Content-Disposition'] = 'attachment; filename=%s' % name
+    with open(file_path, 'rb') as f:
+        for chunk in read_chunks(f):
+            response.write(chunk)
+    return response
diff --git a/src/documents/feeds.py b/src/documents/feeds.py
new file mode 100644 (file)
index 0000000..bf9f2db
--- /dev/null
@@ -0,0 +1,32 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.contrib.syndication.views import Feed
+from django.shortcuts import get_object_or_404
+from .models import Book, Chunk
+
+
+class PublishTrackFeed(Feed):
+    title = u"Planowane publikacje"
+    link = "/"
+
+    def description(self, obj):
+        tag, published = obj
+        return u"Publikacje, które dotarły co najmniej do etapu: %s" % tag.name
+
+    def get_object(self, request, slug):
+        published = request.GET.get('published')
+        if published is not None:
+            published = published == 'true'
+        return get_object_or_404(Chunk.tag_model, slug=slug), published
+
+    def item_title(self, item):
+        return item.title
+
+    def items(self, obj):
+        tag, published = obj
+        books = Book.objects.filter(public=True, _on_track__gte=tag.ordering
+                ).order_by('-_on_track', 'title')
+        if published is not None:
+            books = books.filter(_published=published)
+        return books
diff --git a/src/documents/fixtures/stages.json b/src/documents/fixtures/stages.json
new file mode 100644 (file)
index 0000000..1145669
--- /dev/null
@@ -0,0 +1,83 @@
+[
+    {
+        "pk": 1, 
+        "model": "documents.chunktag", 
+        "fields": {
+            "ordering": 1, 
+            "name": "Autokorekta", 
+            "slug": "first_correction"
+        }
+    }, 
+    {
+        "pk": 2, 
+        "model": "documents.chunktag", 
+        "fields": {
+            "ordering": 2, 
+            "name": "Tagowanie", 
+            "slug": "tagging"
+        }
+    }, 
+    {
+        "pk": 3, 
+        "model": "documents.chunktag", 
+        "fields": {
+            "ordering": 3, 
+            "name": "Korekta", 
+            "slug": "proofreading"
+        }
+    }, 
+    {
+        "pk": 4, 
+        "model": "documents.chunktag", 
+        "fields": {
+            "ordering": 4, 
+            "name": "Sprawdzenie przypis\u00f3w \u017ar\u00f3d\u0142a", 
+            "slug": "annotation-proofreading"
+        }
+    }, 
+    {
+        "pk": 5, 
+        "model": "documents.chunktag", 
+        "fields": {
+            "ordering": 5, 
+            "name": "Uwsp\u00f3\u0142cze\u015bnienie", 
+            "slug": "modernisation"
+        }
+    }, 
+    {
+        "pk": 6, 
+        "model": "documents.chunktag", 
+        "fields": {
+            "ordering": 6, 
+            "name": "Przypisy", 
+            "slug": "annotations"
+        }
+    }, 
+    {
+        "pk": 7, 
+        "model": "documents.chunktag", 
+        "fields": {
+            "ordering": 7, 
+            "name": "Motywy", 
+            "slug": "themes"
+        }
+    }, 
+    {
+        "pk": 8, 
+        "model": "documents.chunktag", 
+        "fields": {
+            "ordering": 8, 
+            "name": "Ostateczna redakcja literacka", 
+            "slug": "editor-proofreading"
+        }
+    }, 
+    {
+        "pk": 9, 
+        "model": "documents.chunktag", 
+        "fields": {
+            "ordering": 9, 
+            "name": "Ostateczna redakcja techniczna", 
+            "slug": "technical-editor-proofreading"
+        }
+    }
+]
diff --git a/src/documents/forms.py b/src/documents/forms.py
new file mode 100644 (file)
index 0000000..f5f2901
--- /dev/null
@@ -0,0 +1,231 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.db.models import Count
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+from django.conf import settings
+
+from .constants import MASTERS
+from .models import Book, Chunk, Image, User
+
+class DocumentCreateForm(forms.ModelForm):
+    """
+        Form used for creating new documents.
+    """
+    file = forms.FileField(required=False)
+    text = forms.CharField(required=False, widget=forms.Textarea)
+
+    class Meta:
+        model = Book
+        exclude = ['parent', 'parent_number', 'project']
+
+    def __init__(self, *args, **kwargs):
+        super(DocumentCreateForm, self).__init__(*args, **kwargs)
+        self.fields['slug'].widget.attrs={'class': 'autoslug'}
+        self.fields['gallery'].widget.attrs={'class': 'autoslug'}
+        self.fields['title'].widget.attrs={'class': 'autoslug-source'}
+
+    def clean(self):
+        super(DocumentCreateForm, self).clean()
+        file = self.cleaned_data['file']
+
+        if file is not None:
+            try:
+                self.cleaned_data['text'] = file.read().decode('utf-8')
+            except UnicodeDecodeError:
+                raise forms.ValidationError(_("Text file must be UTF-8 encoded."))
+
+        if not self.cleaned_data["text"]:
+            self._errors["file"] = self.error_class([_("You must either enter text or upload a file")])
+
+        return self.cleaned_data
+
+
+class DocumentsUploadForm(forms.Form):
+    """
+        Form used for uploading new documents.
+    """
+    file = forms.FileField(required=True, label=_('ZIP file'))
+    dirs = forms.BooleanField(label=_('Directories are documents in chunks'),
+            widget = forms.CheckboxInput(attrs={'disabled':'disabled'}))
+
+    def clean(self):
+        file = self.cleaned_data['file']
+
+        import zipfile
+        try:
+            z = self.cleaned_data['zip'] = zipfile.ZipFile(file)
+        except zipfile.BadZipfile:
+            raise forms.ValidationError("Should be a ZIP file.")
+        if z.testzip():
+            raise forms.ValidationError("ZIP file corrupt.")
+
+        return self.cleaned_data
+
+
+class ChunkForm(forms.ModelForm):
+    """
+        Form used for editing a chunk.
+    """
+    user = forms.ModelChoiceField(queryset=
+        User.objects.annotate(count=Count('chunk')).
+        order_by('last_name', 'first_name'), required=False,
+        label=_('Assigned to')) 
+
+    class Meta:
+        model = Chunk
+        fields = ['title', 'slug', 'gallery_start', 'user', 'stage']
+        exclude = ['number']
+
+    def __init__(self, *args, **kwargs):
+        super(ChunkForm, self).__init__(*args, **kwargs)
+        self.fields['gallery_start'].widget.attrs={'class': 'number-input'}
+        self.fields['slug'].widget.attrs={'class': 'autoslug'}
+        self.fields['title'].widget.attrs={'class': 'autoslug-source'}
+
+    def clean_slug(self):
+        slug = self.cleaned_data['slug']
+        try:
+            chunk = Chunk.objects.get(book=self.instance.book, slug=slug)
+        except Chunk.DoesNotExist:
+            return slug
+        if chunk == self.instance:
+            return slug
+        raise forms.ValidationError(_('Chunk with this slug already exists'))
+
+
+class ChunkAddForm(ChunkForm):
+    """
+        Form used for adding a chunk to a document.
+    """
+
+    def clean_slug(self):
+        slug = self.cleaned_data['slug']
+        try:
+            user = Chunk.objects.get(book=self.instance.book, slug=slug)
+        except Chunk.DoesNotExist:
+            return slug
+        raise forms.ValidationError(_('Chunk with this slug already exists'))
+
+
+class BookAppendForm(forms.Form):
+    """
+        Form for appending a book to another book.
+        It means moving all chunks from book A to book B and deleting A.
+    """
+    append_to = forms.ModelChoiceField(queryset=Book.objects.all(),
+            label=_("Append to"))
+
+    def __init__(self, book, *args, **kwargs):
+        ret =  super(BookAppendForm, self).__init__(*args, **kwargs)
+        self.fields['append_to'].queryset = Book.objects.exclude(pk=book.pk)
+        return ret
+
+
+class BookForm(forms.ModelForm):
+    """Form used for editing a Book."""
+
+    class Meta:
+        model = Book
+        exclude = ['project']
+
+    def __init__(self, *args, **kwargs):
+        ret = super(BookForm, self).__init__(*args, **kwargs)
+        self.fields['slug'].widget.attrs.update({"class": "autoslug"})
+        self.fields['title'].widget.attrs.update({"class": "autoslug-source"})
+        return ret
+
+    def save(self, **kwargs):
+        orig_instance = Book.objects.get(pk=self.instance.pk)
+        old_gallery = orig_instance.gallery
+        new_gallery = self.cleaned_data['gallery']
+        if new_gallery and old_gallery and new_gallery != old_gallery:
+            import shutil
+            import os.path
+            from django.conf import settings
+            shutil.move(orig_instance.gallery_path(),
+                        os.path.join(settings.MEDIA_ROOT, settings.IMAGE_DIR, new_gallery))
+        super(BookForm, self).save(**kwargs)
+
+
+class ReadonlyBookForm(BookForm):
+    """Form used for not editing a Book."""
+
+    def __init__(self, *args, **kwargs):
+        ret = super(ReadonlyBookForm, self).__init__(*args, **kwargs)
+        for field in self.fields.values():
+            field.widget.attrs.update({"disabled": "disabled"})
+        return ret
+
+
+class ChooseMasterForm(forms.Form):
+    """
+        Form used for fixing the chunks in a book.
+    """
+
+    master = forms.ChoiceField(choices=((m, m) for m in MASTERS))
+
+
+class ImageForm(forms.ModelForm):
+    """Form used for editing an Image."""
+    user = forms.ModelChoiceField(queryset=
+        User.objects.annotate(count=Count('chunk')).
+        order_by('-count', 'last_name', 'first_name'), required=False,
+        label=_('Assigned to')) 
+
+    class Meta:
+        model = Image
+        fields = ['title', 'slug', 'user', 'stage']
+
+    def __init__(self, *args, **kwargs):
+        super(ImageForm, self).__init__(*args, **kwargs)
+        self.fields['slug'].widget.attrs={'class': 'autoslug'}
+        self.fields['title'].widget.attrs={'class': 'autoslug-source'}
+
+
+class ReadonlyImageForm(ImageForm):
+    """Form used for not editing an Image."""
+
+    def __init__(self, *args, **kwargs):
+        super(ReadonlyImageForm, self).__init__(*args, **kwargs)
+        for field in self.fields.values():
+            field.widget.attrs.update({"disabled": "disabled"})
+
+
+class MarkFinalForm(forms.Form):
+    username = forms.CharField(initial=settings.LITERARY_DIRECTOR_USERNAME)
+    comment = forms.CharField(initial=u'Ostateczna akceptacja merytoryczna przez kierownika literackiego.')
+    books = forms.CharField(widget=forms.Textarea, help_text=u'linki do książek w redakcji, po jednym na wiersz')
+
+    def clean_books(self):
+        books_value = self.cleaned_data['books']
+        slugs = [line.strip().strip('/').split('/')[-1] for line in books_value.split('\n') if line.strip()]
+        books = Book.objects.filter(slug__in=slugs)
+        if len(books) != len(slugs):
+            raise forms.ValidationError(
+                'Incorrect slug(s): %s' % ' '.join(slug for slug in slugs if not Book.objects.filter(slug=slug)))
+        return books
+
+    def clean_username(self):
+        username = self.cleaned_data['username']
+        if not User.objects.filter(username=username):
+            raise forms.ValidationError('Invalid username')
+        return username
+
+    def save(self):
+        for book in self.cleaned_data['books']:
+            for chunk in book.chunk_set.all():
+                src = chunk.head.materialize()
+                chunk.commit(
+                    text=src,
+                    author=User.objects.get(username=self.cleaned_data['username']),
+                    description=self.cleaned_data['comment'],
+                    tags=[Chunk.tag_model.objects.get(slug='editor-proofreading')],
+                    publishable=True
+                )
+
+
+class PublishOptionsForm(forms.Form):
+    days = forms.IntegerField(label=u'po ilu dniach udostępnienić (0 = od razu)', min_value=0, initial=0)
+    beta = forms.BooleanField(label=u'Opublikuj na wersji testowej', required=False)
diff --git a/src/documents/helpers.py b/src/documents/helpers.py
new file mode 100644 (file)
index 0000000..df15b91
--- /dev/null
@@ -0,0 +1,150 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from datetime import date
+from functools import wraps
+from os.path import join
+from os import listdir, stat
+from shutil import move, rmtree
+from django.conf import settings
+import re
+import filecmp
+
+from django.db.models import Count
+
+
+def active_tab(tab):
+    """
+        View decorator, which puts tab info on a request.
+    """
+    def wrapper(f):
+        @wraps(f)
+        def wrapped(request, *args, **kwargs):
+            request.documents_active_tab = tab
+            return f(request, *args, **kwargs)
+        return wrapped
+    return wrapper
+
+
+def cached_in_field(field_name):
+    def decorator(f):
+        @property
+        @wraps(f)
+        def wrapped(self, *args, **kwargs):
+            value = getattr(self, field_name)
+            if value is None:
+                value = f(self, *args, **kwargs)
+                type(self)._default_manager.filter(pk=self.pk).update(**{field_name: value})
+            return value
+        return wrapped
+    return decorator
+
+
+def parse_isodate(isodate):
+    try:
+        return date(*[int(p) for p in isodate.split('-')])
+    except (AttributeError, TypeError, ValueError):
+        raise ValueError("Not a date in ISO format.")
+
+
+class GalleryMerger(object):
+    def __init__(self, dest_gallery, src_gallery):
+        self.dest = dest_gallery
+        self.src = src_gallery
+        self.dest_size = None
+        self.src_size = None
+        self.num_deleted = 0
+
+    @staticmethod
+    def path(gallery):
+        return join(settings.MEDIA_ROOT, settings.IMAGE_DIR, gallery)
+
+    @staticmethod
+    def get_prefix(name):
+        m = re.match(r"^([0-9])-", name)
+        if m:
+            return int(m.groups()[0])
+        return None
+
+    @staticmethod
+    def set_prefix(name, prefix, always=False):
+        m = not always and re.match(r"^([0-9])-", name)
+        return "%1d-%s" % (prefix, m and name[2:] or name)
+
+    @property
+    def was_merged(self):
+        "Check if we have gallery size recorded"
+        return self.dest_size is not None
+
+    def merge(self):
+        if not self.dest:
+            return self.src
+        if not self.src:
+            return self.dest
+
+        files = listdir(self.path(self.dest))
+        files.sort()
+        self.dest_size = len(files)
+        files_other = listdir(self.path(self.src))
+        files_other.sort()
+        self.src_size = len(files_other)
+
+        if files and files_other:
+            if filecmp.cmp(
+                    join(self.path(self.dest), files[-1]),
+                    join(self.path(self.src), files_other[0]),
+                    False
+                    ):
+                files_other.pop(0)
+                self.num_deleted = 1
+
+        prefixes = {}
+        renamed_files = {}
+        renamed_files_other = {}
+        last_pfx = -1
+
+        # check if all elements of my files have a prefix
+        files_prefixed = True
+        for f in files:
+            p = self.get_prefix(f)
+            if p:
+                if p > last_pfx: last_pfx = p
+            else:
+                files_prefixed = False
+                break
+
+        # if not, add a 0 prefix to them
+        if not files_prefixed:
+            prefixes[0] = 0
+            for f in files:
+                renamed_files[f] = self.set_prefix(f, 0, True)
+
+        # two cases here - either all are prefixed or not.
+        files_other_prefixed = True
+        for f in files_other:
+            pfx = self.get_prefix(f)
+            if pfx is not None:
+                if not pfx in prefixes:
+                    last_pfx += 1
+                    prefixes[pfx] = last_pfx
+                renamed_files_other[f] = self.set_prefix(f, prefixes[pfx])
+            else:
+                # ops, not all files here were prefixed.
+                files_other_prefixed = False
+                break
+
+        # just set a 1- prefix to all of them
+        if not files_other_prefixed:
+            for f in files_other:
+                renamed_files_other[f] = self.set_prefix(f, 1, True)
+
+        # finally, move / rename files.
+        for frm, to in renamed_files.items():
+            move(join(self.path(self.dest), frm),
+                        join(self.path(self.dest), to))
+        for frm, to in renamed_files_other.items():
+            move(join(self.path(self.src), frm),
+                        join(self.path(self.dest), to))            
+
+        rmtree(join(self.path(self.src)))
+        return self.dest
diff --git a/src/documents/locale/pl/LC_MESSAGES/django.mo b/src/documents/locale/pl/LC_MESSAGES/django.mo
new file mode 100644 (file)
index 0000000..395b34e
Binary files /dev/null and b/src/documents/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/src/documents/locale/pl/LC_MESSAGES/django.po b/src/documents/locale/pl/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..ac60fb2
--- /dev/null
@@ -0,0 +1,830 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Platforma Redakcyjna\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-10-07 13:05+0200\n"
+"PO-Revision-Date: 2019-10-07 13:06+0200\n"
+"Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
+"Language-Team: Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org."
+"pl>\n"
+"Language: pl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2);\n"
+"X-Generator: Poedit 2.0.6\n"
+
+#: forms.py:38
+msgid "Text file must be UTF-8 encoded."
+msgstr "Plik powinien mieć kodowanie UTF-8."
+
+#: forms.py:41
+msgid "You must either enter text or upload a file"
+msgstr "Proszę wpisać tekst albo wybrać plik do załadowania"
+
+#: forms.py:50
+msgid "ZIP file"
+msgstr "Plik ZIP"
+
+#: forms.py:51
+msgid "Directories are documents in chunks"
+msgstr "Katalogi zawierają dokumenty w częściach"
+
+#: forms.py:75 forms.py:176
+msgid "Assigned to"
+msgstr "Przypisane do"
+
+#: forms.py:96 forms.py:110
+msgid "Chunk with this slug already exists"
+msgstr "Część z tym slugiem już istnieje"
+
+#: forms.py:119
+msgid "Append to"
+msgstr "Dołącz do"
+
+#: models/book.py:26 models/chunk.py:21 models/image.py:20
+msgid "title"
+msgstr "tytuł"
+
+#: models/book.py:27 models/chunk.py:22 models/image.py:21
+msgid "slug"
+msgstr "slug"
+
+#: models/book.py:28 models/image.py:22
+msgid "public"
+msgstr "publiczna"
+
+#: models/book.py:29
+msgid "scan gallery name"
+msgstr "nazwa galerii skanów"
+
+#: models/book.py:33
+msgid "parent"
+msgstr "rodzic"
+
+#: models/book.py:34
+msgid "parent number"
+msgstr "numeracja rodzica"
+
+#: models/book.py:52 models/chunk.py:19 models/publish_log.py:15
+msgid "book"
+msgstr "książka"
+
+#: models/book.py:53 views.py:619
+msgid "books"
+msgstr "książki"
+
+#: models/book.py:257
+msgid "No chunks in the book."
+msgstr "Książka nie ma części."
+
+#: models/book.py:261
+msgid "Not all chunks have publishable revisions."
+msgstr "Niektóre części nie są gotowe do publikacji."
+
+#: models/book.py:268 models/image.py:83
+msgid "Invalid XML"
+msgstr "Nieprawidłowy XML"
+
+#: models/book.py:270 models/image.py:85
+msgid "No Dublin Core found."
+msgstr "Brak sekcji Dublin Core."
+
+#: models/book.py:272 models/image.py:87
+msgid "Invalid Dublin Core"
+msgstr "Nieprawidłowy Dublin Core"
+
+#: models/book.py:275 models/image.py:91
+msgid "rdf:about is not"
+msgstr "rdf:about jest różny od"
+
+#: models/chunk.py:20
+msgid "number"
+msgstr "numer"
+
+#: models/chunk.py:23
+msgid "gallery start"
+msgstr "początek galerii"
+
+#: models/chunk.py:38
+msgid "chunk"
+msgstr "część"
+
+#: models/chunk.py:39
+msgid "chunks"
+msgstr "części"
+
+#: models/image.py:19 models/image.py:33 models/publish_log.py:43
+msgid "image"
+msgstr "obraz"
+
+#: models/image.py:34
+msgid "images"
+msgstr "obrazy"
+
+#: models/image.py:75
+msgid "There is no publishable revision"
+msgstr "Żadna wersja nie została oznaczona do publikacji."
+
+#: models/project.py:11
+msgid "name"
+msgstr "nazwa"
+
+#: models/project.py:12
+msgid "notes"
+msgstr "notatki"
+
+#: models/project.py:17 templates/catalogue/book_list/book_list.html:66
+#: templates/catalogue/image_table.html:60
+msgid "project"
+msgstr "projekt"
+
+#: models/project.py:18
+msgid "projects"
+msgstr "projekty"
+
+#: models/publish_log.py:16 models/publish_log.py:44
+msgid "time"
+msgstr "czas"
+
+#: models/publish_log.py:17 models/publish_log.py:45
+#: templates/catalogue/wall.html:20
+msgid "user"
+msgstr "użytkownik"
+
+#: models/publish_log.py:22 models/publish_log.py:31
+msgid "book publish record"
+msgstr "zapis publikacji książki"
+
+#: models/publish_log.py:23
+msgid "book publish records"
+msgstr "zapisy publikacji książek"
+
+#: models/publish_log.py:32 models/publish_log.py:46
+msgid "change"
+msgstr "zmiana"
+
+#: models/publish_log.py:36
+msgid "chunk publish record"
+msgstr "zapis publikacji części"
+
+#: models/publish_log.py:37
+msgid "chunk publish records"
+msgstr "zapisy publikacji części"
+
+#: models/publish_log.py:51
+msgid "image publish record"
+msgstr "zapis publikacji obrazu"
+
+#: models/publish_log.py:52
+msgid "image publish records"
+msgstr "zapisy publikacji obrazów"
+
+#: templates/catalogue/active_users_list.html:5
+msgid "Active users"
+msgstr "Aktywni użytkownicy"
+
+#: templates/catalogue/active_users_list.html:11
+msgid "Users active in the year"
+msgstr "Użytkownicy aktywni w roku"
+
+#: templates/catalogue/activity.html:6 templates/catalogue/activity.html:15
+#: templatetags/catalogue.py:30
+msgid "Activity"
+msgstr "Aktywność"
+
+#: templates/catalogue/base.html:13
+msgid "Platforma Redakcyjna"
+msgstr "Platforma Redakcyjna"
+
+#: templates/catalogue/book_append_to.html:5
+#: templates/catalogue/book_append_to.html:14
+msgid "Append book"
+msgstr "Dołącz książkę"
+
+#: templates/catalogue/book_detail.html:23
+#: templates/catalogue/book_edit.html:13 templates/catalogue/chunk_edit.html:22
+#: templates/catalogue/image_detail.html:22
+msgid "Save"
+msgstr "Zapisz"
+
+#: templates/catalogue/book_detail.html:30
+msgid "Edit gallery"
+msgstr "Edytuj galerię"
+
+#: templates/catalogue/book_detail.html:33
+msgid "Append to other book"
+msgstr "Dołącz do innej książki"
+
+#: templates/catalogue/book_detail.html:40
+msgid "Chunks"
+msgstr "Części"
+
+#: templates/catalogue/book_detail.html:58
+#: templates/catalogue/image_detail.html:47 templatetags/wall.py:108
+#: templatetags/wall.py:129
+msgid "Publication"
+msgstr "Publikacja"
+
+#: templates/catalogue/book_detail.html:69
+#: templates/catalogue/image_detail.html:51
+msgid "Last published"
+msgstr "Ostatnio opublikowano"
+
+#: templates/catalogue/book_detail.html:79
+msgid "Full XML"
+msgstr "Pełny XML"
+
+#: templates/catalogue/book_detail.html:80
+msgid "HTML version"
+msgstr "Wersja HTML"
+
+#: templates/catalogue/book_detail.html:81
+msgid "TXT version"
+msgstr "Wersja TXT"
+
+#: templates/catalogue/book_detail.html:82
+msgid "PDF version"
+msgstr "Wersja PDF"
+
+#: templates/catalogue/book_detail.html:83
+msgid "PDF version for mobiles"
+msgstr "Wersja PDF na telefony"
+
+#: templates/catalogue/book_detail.html:84
+msgid "EPUB version"
+msgstr "Wersja EPUB"
+
+#: templates/catalogue/book_detail.html:85
+msgid "MOBI version"
+msgstr "Wersja MOBI"
+
+#: templates/catalogue/book_detail.html:99
+#: templates/catalogue/image_detail.html:70
+msgid "Publish"
+msgstr "Opublikuj"
+
+#: templates/catalogue/book_detail.html:103
+#: templates/catalogue/image_detail.html:74
+msgid "Log in to publish."
+msgstr "Zaloguj się, aby opublikować."
+
+#: templates/catalogue/book_detail.html:106
+#: templates/catalogue/image_detail.html:77
+msgid "This book can't be published yet, because:"
+msgstr "Ta książka nie może jeszcze zostać opublikowana. Powód:"
+
+#: templates/catalogue/book_edit.html:5
+msgid "Edit book"
+msgstr "Edytuj książkę"
+
+#: templates/catalogue/book_html.html:12 templates/catalogue/book_text.html:15
+msgid "Table of contents"
+msgstr "Spis treści"
+
+#: templates/catalogue/book_html.html:13 templates/catalogue/book_text.html:17
+msgid "Edit. note"
+msgstr "Nota red."
+
+#: templates/catalogue/book_html.html:14
+msgid "Infobox"
+msgstr "Informacje"
+
+#: templates/catalogue/book_list/book.html:8
+#: templates/catalogue/book_list/book.html:36
+msgid "Book settings"
+msgstr "Ustawienia książki"
+
+#: templates/catalogue/book_list/book.html:9
+#: templates/catalogue/book_list/chunk.html:7
+#: templates/catalogue/chunk_edit.html:6 templates/catalogue/chunk_edit.html:12
+msgid "Chunk settings"
+msgstr "Ustawienia części"
+
+#: templates/catalogue/book_list/book.html:12
+#: templates/catalogue/book_list/chunk.html:9
+#: templates/catalogue/image_short.html:9
+msgid "Edit:"
+msgstr "Edytuj:"
+
+#: templates/catalogue/book_list/book.html:21
+#: templates/catalogue/book_list/book.html:43
+#: templates/catalogue/image_short.html:18 templatetags/book_list.py:82
+#: templatetags/book_list.py:150
+msgid "published"
+msgstr "opublikowane"
+
+#: templates/catalogue/book_list/book.html:24
+#: templates/catalogue/book_list/book.html:46
+#: templates/catalogue/book_list/chunk.html:28
+#: templates/catalogue/image_short.html:21 templatetags/book_list.py:80
+#: templatetags/book_list.py:148
+msgid "publishable"
+msgstr "do publikacji"
+
+#: templates/catalogue/book_list/book.html:27
+#: templates/catalogue/book_list/chunk.html:33
+#: templates/catalogue/image_short.html:24 templatetags/book_list.py:81
+#: templatetags/book_list.py:149
+msgid "changed"
+msgstr "zmienione"
+
+#: templates/catalogue/book_list/book_list.html:29
+#: templates/catalogue/image_table.html:25
+msgid "Search in book titles"
+msgstr "Szukaj w tytułach książek"
+
+#: templates/catalogue/book_list/book_list.html:34
+#: templates/catalogue/image_table.html:30
+msgid "stage"
+msgstr "etap"
+
+#: templates/catalogue/book_list/book_list.html:36
+#: templates/catalogue/book_list/book_list.html:47
+#: templates/catalogue/book_list/book_list.html:68
+#: templates/catalogue/image_table.html:32
+#: templates/catalogue/image_table.html:43
+#: templates/catalogue/image_table.html:62
+msgid "none"
+msgstr "brak"
+
+#: templates/catalogue/book_list/book_list.html:45
+#: templates/catalogue/image_table.html:41
+msgid "editor"
+msgstr "redaktor"
+
+#: templates/catalogue/book_list/book_list.html:58
+#: templates/catalogue/image_table.html:52
+msgid "status"
+msgstr "status"
+
+#: templates/catalogue/book_list/book_list.html:92
+#, python-format
+msgid "%(c)s book"
+msgid_plural "%(c)s books"
+msgstr[0] "%(c)s książka"
+msgstr[1] "%(c)s książki"
+msgstr[2] "%(c)s książek"
+
+#: templates/catalogue/book_list/book_list.html:96
+msgid "No books found."
+msgstr "Nie znaleziono książek."
+
+#: templates/catalogue/book_list/book_list.html:105
+#: templates/catalogue/image_table.html:87
+msgid "Set stage"
+msgstr "Ustaw etap"
+
+#: templates/catalogue/book_list/book_list.html:106
+#: templates/catalogue/image_table.html:88
+msgid "Set user"
+msgstr "Przypisz redaktora"
+
+#: templates/catalogue/book_list/book_list.html:108
+#: templates/catalogue/image_table.html:90
+msgid "Project"
+msgstr "Projekt"
+
+#: templates/catalogue/book_list/book_list.html:109
+#: templates/catalogue/image_table.html:91
+msgid "More users"
+msgstr "Więcej użytkowników"
+
+#: templates/catalogue/book_text.html:7
+msgid "Redakcja"
+msgstr ""
+
+#: templates/catalogue/chunk_add.html:6 templates/catalogue/chunk_add.html:12
+#: templates/catalogue/chunk_edit.html:29
+msgid "Split chunk"
+msgstr "Podziel część"
+
+#: templates/catalogue/chunk_add.html:19
+msgid "Insert empty chunk after"
+msgstr "Wstaw pustą część po"
+
+#: templates/catalogue/chunk_add.html:23
+msgid "Add chunk"
+msgstr "Dodaj część"
+
+#: templates/catalogue/chunk_edit.html:19
+msgid "Book"
+msgstr "Książka"
+
+#: templates/catalogue/document_create_missing.html:6
+#: templates/catalogue/document_create_missing.html:12
+msgid "Create a new book"
+msgstr "Utwórz nową książkę"
+
+#: templates/catalogue/document_create_missing.html:21
+msgid "Create book"
+msgstr "Utwórz książkę"
+
+#: templates/catalogue/document_list.html:7
+msgid "Book list"
+msgstr "Lista książek"
+
+#: templates/catalogue/document_upload.html:6
+msgid "Bulk document upload"
+msgstr "Hurtowe dodawanie dokumentów"
+
+#: templates/catalogue/document_upload.html:14
+msgid "Bulk documents upload"
+msgstr "Hurtowe dodawanie dokumentów"
+
+#: templates/catalogue/document_upload.html:19
+msgid ""
+"Please submit a ZIP with UTF-8 encoded XML files. Files not ending with "
+"<code>.xml</code> will be ignored."
+msgstr ""
+"Proszę wskazać archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie "
+"kończące się na <code>.xml</code> zostaną zignorowane."
+
+#: templates/catalogue/document_upload.html:26
+#: templates/catalogue/upload_pdf.html:16 templatetags/catalogue.py:37
+msgid "Upload"
+msgstr "Załaduj"
+
+#: templates/catalogue/document_upload.html:34
+msgid ""
+"There have been some errors. No files have been added to the repository."
+msgstr "Wystąpiły błędy. Żadne pliki nie zostały dodane do repozytorium."
+
+#: templates/catalogue/document_upload.html:35
+msgid "Offending files"
+msgstr "Błędne pliki"
+
+#: templates/catalogue/document_upload.html:43
+msgid "Correct files"
+msgstr "Poprawne pliki"
+
+#: templates/catalogue/document_upload.html:54
+msgid "Files have been successfully uploaded to the repository."
+msgstr "Pliki zostały dodane do repozytorium."
+
+#: templates/catalogue/document_upload.html:55
+msgid "Uploaded files"
+msgstr "Dodane pliki"
+
+#: templates/catalogue/document_upload.html:65
+msgid "Skipped files"
+msgstr "Pominięte pliki"
+
+#: templates/catalogue/document_upload.html:66
+msgid "Files skipped due to no <code>.xml</code> extension"
+msgstr "Pliki pominięte z powodu braku rozszerzenia <code>.xml</code>."
+
+#: templates/catalogue/head_login.html:10
+msgid "Admin"
+msgstr "Administracja"
+
+#: templates/catalogue/head_login.html:15
+msgid "Log Out"
+msgstr "Wyloguj"
+
+#: templates/catalogue/head_login.html:21
+msgid "Log In"
+msgstr "Zaloguj"
+
+#: templates/catalogue/image_detail.html:34
+msgid "Editor"
+msgstr "Edytor"
+
+#: templates/catalogue/image_detail.html:38
+msgid "Proceed to the editor."
+msgstr "Przejdź do edytora."
+
+#: templates/catalogue/image_list.html:8
+msgid "Image list"
+msgstr "Lista obrazów"
+
+#: templates/catalogue/image_short.html:6
+msgid "Image settings"
+msgstr "Ustawienia obrazu"
+
+#: templates/catalogue/image_table.html:79
+#, python-format
+msgid "%(c)s image"
+msgid_plural "%(c)s images"
+msgstr[0] "%(c)s obraz"
+msgstr[1] "%(c)s obrazy"
+msgstr[2] "%(c)s obrazów"
+
+#: templates/catalogue/image_table.html:81
+msgid "No images found."
+msgstr "Nie znaleziono obrazów."
+
+#: templates/catalogue/my_page.html:15 templatetags/catalogue.py:28
+msgid "My page"
+msgstr "Moja strona"
+
+#: templates/catalogue/my_page.html:25
+msgid "Your last edited documents"
+msgstr "Twoje ostatnie edycje"
+
+#: templates/catalogue/my_page.html:45 templates/catalogue/user_page.html:18
+msgid "Recent activity for"
+msgstr "Ostatnia aktywność dla:"
+
+#: templates/catalogue/upload_pdf.html:5 templates/catalogue/upload_pdf.html:11
+msgid "PDF file upload"
+msgstr "Ładowanie pliku PDF"
+
+#: templates/catalogue/user_list.html:7 templates/catalogue/user_list.html:14
+#: templatetags/catalogue.py:33
+msgid "Users"
+msgstr "Użytkownicy"
+
+#: templates/catalogue/wall.html:30
+msgid "not logged in"
+msgstr "nie zalogowany"
+
+#: templates/catalogue/wall.html:35
+msgid "No activity recorded."
+msgstr "Nie zanotowano aktywności."
+
+#: templatetags/book_list.py:83 templatetags/book_list.py:151
+msgid "unpublished"
+msgstr "nie opublikowane"
+
+#: templatetags/book_list.py:84 templatetags/book_list.py:152
+msgid "empty"
+msgstr "puste"
+
+#: templatetags/catalogue.py:31
+msgid "All"
+msgstr "Wszystkie"
+
+#: templatetags/catalogue.py:32
+msgid "Images"
+msgstr "Obrazy"
+
+#: templatetags/catalogue.py:36
+msgid "Add"
+msgstr "Dodaj"
+
+#: templatetags/catalogue.py:39
+msgid "Covers"
+msgstr "Okładki"
+
+#: templatetags/wall.py:49 templatetags/wall.py:78
+msgid "Related edit"
+msgstr "Powiązana zmiana"
+
+#: templatetags/wall.py:51 templatetags/wall.py:80
+msgid "Edit"
+msgstr "Zmiana"
+
+#: views.py:172
+#, python-format
+msgid "Slug already used for %s"
+msgstr "Slug taki sam jak dla pliku %s"
+
+#: views.py:174
+msgid "Slug already used in repository."
+msgstr "Dokument o tym slugu już istnieje w repozytorium."
+
+#: views.py:180
+msgid "File should be UTF-8 encoded."
+msgstr "Plik powinien mieć kodowanie UTF-8."
+
+#: views.py:621
+msgid "scan gallery"
+msgstr "galeria skanów"
+
+#~ msgid "Active users since"
+#~ msgstr "Użytkownicy aktywni od"
+
+#~ msgid "Show hidden books"
+#~ msgstr "Pokaż ukryte książki"
+
+#~ msgid "Comment"
+#~ msgstr "Komentarz"
+
+#~ msgid "Comments"
+#~ msgstr "Komentarze"
+
+#~ msgid "Mark publishable"
+#~ msgstr "Oznacz do publikacji"
+
+#~ msgid "Mark not publishable"
+#~ msgstr "Odznacz do publikacji"
+
+#~ msgid "Other user"
+#~ msgstr "Inny użytkownik"
+
+#~ msgid "edit"
+#~ msgstr "edytuj"
+
+#~ msgid "add basic document structure"
+#~ msgstr "dodaj podstawową strukturę dokumentu"
+
+#~ msgid "change master tag to"
+#~ msgstr "zmień tak master na"
+
+#~ msgid "add begin trimming tag"
+#~ msgstr "dodaj początkowy ogranicznik"
+
+#~ msgid "add end trimming tag"
+#~ msgstr "dodaj końcowy ogranicznik"
+
+#~ msgid "unstructured text"
+#~ msgstr "tekst bez struktury"
+
+#~ msgid "unknown XML"
+#~ msgstr "nieznany XML"
+
+#~ msgid "broken document"
+#~ msgstr "uszkodzony dokument"
+
+#~ msgid "Apply fixes"
+#~ msgstr "Wykonaj zmiany"
+
+#~ msgid "Can mark for publishing"
+#~ msgstr "Oznacza do publikacji"
+
+#~ msgid "Author"
+#~ msgstr "Autor"
+
+#~ msgid "Your name"
+#~ msgstr "Imię i nazwisko"
+
+#~ msgid "Author's email"
+#~ msgstr "E-mail autora"
+
+#~ msgid "Your email address, so we can show a gravatar :)"
+#~ msgstr "Adres e-mail, żebyśmy mogli pokazać gravatar :)"
+
+#~ msgid "Describe changes you made."
+#~ msgstr "Opisz swoje zmiany"
+
+#~ msgid "Completed"
+#~ msgstr "Ukończono"
+
+#~ msgid "If you completed a life cycle stage, select it."
+#~ msgstr "Jeśli został ukończony etap prac, wskaż go."
+
+#~ msgid "Describe the reason for reverting."
+#~ msgstr "Opisz powód przywrócenia."
+
+#~ msgid "theme"
+#~ msgstr "motyw"
+
+#~ msgid "themes"
+#~ msgstr "motywy"
+
+#~ msgid "Tag added"
+#~ msgstr "Dodano tag"
+
+#~ msgid "Revision marked"
+#~ msgstr "Wersja oznaczona"
+
+#~ msgid "New version"
+#~ msgstr "Nowa wersja"
+
+#~ msgid "Click to open/close gallery"
+#~ msgstr "Kliknij, aby (ro)zwinąć galerię"
+
+#~ msgid "Help"
+#~ msgstr "Pomoc"
+
+#~ msgid "Version"
+#~ msgstr "Wersja"
+
+#~ msgid "Unknown"
+#~ msgstr "nieznana"
+
+#~ msgid "Save attempt in progress"
+#~ msgstr "Trwa zapisywanie"
+
+#~ msgid "There is a newer version of this document!"
+#~ msgstr "Istnieje nowsza wersja tego dokumentu!"
+
+#~ msgid "Clear filter"
+#~ msgstr "Wyczyść filtr"
+
+#~ msgid "Cancel"
+#~ msgstr "Anuluj"
+
+#~ msgid "Revert"
+#~ msgstr "Przywróć"
+
+#~ msgid "all"
+#~ msgstr "wszystkie"
+
+#~ msgid "Annotations"
+#~ msgstr "Przypisy"
+
+#~ msgid "Previous"
+#~ msgstr "Poprzednie"
+
+#~ msgid "Next"
+#~ msgstr "Następne"
+
+#~ msgid "Zoom in"
+#~ msgstr "Powiększ"
+
+#~ msgid "Zoom out"
+#~ msgstr "Zmniejsz"
+
+#~ msgid "Gallery"
+#~ msgstr "Galeria"
+
+#~ msgid "Compare versions"
+#~ msgstr "Porównaj wersje"
+
+#~ msgid "Revert document"
+#~ msgstr "Przywróć wersję"
+
+#~ msgid "View version"
+#~ msgstr "Zobacz wersję"
+
+#~ msgid "History"
+#~ msgstr "Historia"
+
+#~ msgid "Search"
+#~ msgstr "Szukaj"
+
+#~ msgid "Replace with"
+#~ msgstr "Zamień na"
+
+#~ msgid "Replace"
+#~ msgstr "Zamień"
+
+#~ msgid "Options"
+#~ msgstr "Opcje"
+
+#~ msgid "Case sensitive"
+#~ msgstr "Rozróżniaj wielkość liter"
+
+#~ msgid "From cursor"
+#~ msgstr "Zacznij od kursora"
+
+#~ msgid "Search and replace"
+#~ msgstr "Znajdź i zamień"
+
+#~ msgid "Source code"
+#~ msgstr "Kod źródłowy"
+
+#~ msgid "Title"
+#~ msgstr "Tytuł"
+
+#~ msgid "Document ID"
+#~ msgstr "ID dokumentu"
+
+#~ msgid "Current version"
+#~ msgstr "Aktualna wersja"
+
+#~ msgid "Last edited by"
+#~ msgstr "Ostatnio edytowane przez"
+
+#~ msgid "Summary"
+#~ msgstr "Podsumowanie"
+
+#~ msgid "Insert theme"
+#~ msgstr "Wstaw motyw"
+
+#~ msgid "Insert annotation"
+#~ msgstr "Wstaw przypis"
+
+#~ msgid "Visual editor"
+#~ msgstr "Edytor wizualny"
+
+#~ msgid "Unassigned"
+#~ msgstr "Nie przypisane"
+
+#~ msgid "First correction"
+#~ msgstr "Autokorekta"
+
+#~ msgid "Tagging"
+#~ msgstr "Tagowanie"
+
+#~ msgid "Initial Proofreading"
+#~ msgstr "Korekta"
+
+#~ msgid "Annotation Proofreading"
+#~ msgstr "Sprawdzenie przypisów źródła"
+
+#~ msgid "Modernisation"
+#~ msgstr "Uwspółcześnienie"
+
+#~ msgid "Themes"
+#~ msgstr "Motywy"
+
+#~ msgid "Editor's Proofreading"
+#~ msgstr "Ostateczna redakcja literacka"
+
+#~ msgid "Technical Editor's Proofreading"
+#~ msgstr "Ostateczna redakcja techniczna"
+
+#~ msgid "Finished stage: %s"
+#~ msgstr "Ukończony etap: %s"
+
+#~ msgid "Refresh"
+#~ msgstr "Odśwież"
diff --git a/src/documents/management/__init__.py b/src/documents/management/__init__.py
new file mode 100644 (file)
index 0000000..8c38828
--- /dev/null
@@ -0,0 +1,114 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from collections import defaultdict
+from django.db import transaction
+from lxml import etree
+
+
+class XmlUpdater(object):
+    """A base class for massive XML updates.
+
+    In a subclass, override `fix_tree` and/or use `fixes_field` decorator.
+    Attributes:
+    * commit_desc: commits description
+    * retain_publishable: set publishable if head is (default: True)
+    * only_first_chunk: process only first chunks of books (default: False)
+    """
+    commit_desc = "auto-update"
+    retain_publishable = True
+    only_first_chunk = False
+
+    _element_fixers = defaultdict(list)
+
+    def __init__(self):
+        self.counters = defaultdict(lambda: 0)
+
+    @classmethod
+    def fixes_elements(cls, xpath):
+        """Decorator, registering a function as a fixer for given field type.
+
+        Any decorated function will be called like
+            f(element, change=..., verbose=...)
+        providing changeset as context.
+
+        :param xpath: element lookup, e.g. ".//{namespace-uri}tag-name"
+        :returns: True if anything changed
+        """
+        def wrapper(fixer):
+            cls._element_fixers[xpath].append(fixer)
+            return fixer
+        return wrapper
+
+    def fix_tree(self, tree, verbose):
+        """Override to provide general tree-fixing mechanism.
+
+        :param tree: the parsed XML tree
+        :param verbose: verbosity level
+        :returns: True if anythig changed
+        """
+        return False
+
+    def fix_chunk(self, chunk, user, verbose=0, dry_run=False):
+        """Runs the update for a single chunk."""
+        if verbose >= 2:
+            print(chunk.get_absolute_url())
+        old_head = chunk.head
+        src = old_head.materialize()
+        try:
+            tree = etree.fromstring(src)
+        except:
+            if verbose:
+                print("%s: invalid XML" % chunk.get_absolute_url())
+            self.counters['Bad XML'] += 1
+            return
+
+        dirty = False
+        # Call the general fixing function.
+        if self.fix_tree(tree, verbose=verbose):
+            dirty = True
+        # Call the registered fixers.
+        for xpath, fixers in self._element_fixers.items():
+            for elem in tree.findall(xpath):
+                for fixer in fixers:
+                    if fixer(elem, change=old_head, verbose=verbose):
+                        dirty = True
+
+        if not dirty:
+            self.counters['Clean'] += 1
+            return
+
+        if not dry_run:
+            new_head = chunk.commit(
+                etree.tostring(tree, encoding='unicode'),
+                author=user,
+                description=self.commit_desc
+            )
+            if self.retain_publishable:
+                if old_head.publishable:
+                    new_head.set_publishable(True)
+        if verbose >= 2:
+            print("done")
+        self.counters['Updated chunks'] += 1
+
+    def run(self, user, verbose=0, dry_run=False, books=None):
+        """Runs the actual update."""
+        if books is None:
+            from documents.models import Book
+            books = Book.objects.all()
+
+        # Start transaction management.
+        with transaction.atomic():
+            for book in books:
+                self.counters['All books'] += 1
+                chunks = book.chunk_set.all()
+                if self.only_first_chunk:
+                    chunks = chunks[:1]
+                for chunk in chunks:
+                    self.counters['All chunks'] += 1
+                    self.fix_chunk(chunk, user, verbose, dry_run)
+
+    def print_results(self):
+        """Prints the counters."""
+        for item in sorted(self.counters.items()):
+            print("%s: %d" % item)
diff --git a/src/documents/management/commands/__init__.py b/src/documents/management/commands/__init__.py
new file mode 100644 (file)
index 0000000..22469b9
--- /dev/null
@@ -0,0 +1,43 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import sys
+from django.contrib.auth.models import User
+from django.core.management.base import BaseCommand
+from documents.models import Book
+
+
+class XmlUpdaterCommand(BaseCommand):
+    """Base class for creating massive XML-updating commands.
+
+    In a subclass, provide an XmlUpdater class in the `updater' attribute.
+    """
+    args = "[slug]..."
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            '-q', '--quiet', action='store_false', dest='verbose',
+            default=True, help='Less output')
+        parser.add_argument(
+            '-d', '--dry-run', action='store_true', dest='dry_run',
+            default=False, help="Don't actually touch anything")
+        parser.add_argument(
+            '-u', '--username', dest='username', metavar='USER',
+            help='Assign commits to this user (required, preferably yourself).')
+
+    def handle(self, *args, **options):
+        verbose = options.get('verbose')
+        dry_run = options.get('dry_run')
+        username = options.get('username')
+
+        if username:
+            user = User.objects.get(username=username)
+        else:
+            print('Please provide a username.')
+            sys.exit(1)
+
+        books = Book.objects.filter(slug__in=args) if args else None
+
+        updater = self.updater()
+        updater.run(user, verbose=verbose, dry_run=dry_run, books=books)
+        updater.print_results()
diff --git a/src/documents/management/commands/add_parent.py b/src/documents/management/commands/add_parent.py
new file mode 100644 (file)
index 0000000..59767bf
--- /dev/null
@@ -0,0 +1,99 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import sys
+
+from datetime import date
+from lxml import etree
+
+from django.core.management import BaseCommand
+
+from documents.models import Book
+from librarian import RDFNS, DCNS
+
+TEMPLATE = '''<utwor>
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description rdf:about="http://redakcja.wolnelektury.pl/documents/book/%(slug)s/">
+%(dc)s
+</rdf:Description>
+</rdf:RDF>
+
+</utwor>
+'''
+
+DC_TEMPLATE = '<dc:%(tag)s xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">%(value)s</dc:%(tag)s>'
+
+DC_TAGS = (
+    'creator',
+    'title',
+    'relation.hasPart',
+    'contributor.translator',
+    'contributor.editor',
+    'contributor.technical_editor',
+    'contributor.funding',
+    'contributor.thanks',
+    'publisher',
+    'subject.period',
+    'subject.type',
+    'subject.genre',
+    'description',
+    'identifier.url',
+    'source',
+    'source.URL',
+    'rights.license',
+    'rights',
+    'date.pd',
+    'format',
+    'type',
+    'date',
+    'audience',
+    'language',
+)
+
+IDENTIFIER_PREFIX = 'http://wolnelektury.pl/katalog/lektura/'
+
+
+def dc_desc_element(book):
+    xml = book.materialize()
+    tree = etree.fromstring(xml)
+    return tree.find(".//" + RDFNS("Description"))
+
+
+def distinct_dc_values(tag, desc_elements):
+    values = set()
+    for desc in desc_elements:
+        values.update(elem.text for elem in desc.findall(DCNS(tag)))
+    return values
+
+
+class Command(BaseCommand):
+    args = 'slug'
+
+    def handle(self, slug, **options):
+        children_slugs = [line.strip() for line in sys.stdin]
+        children = Book.objects.filter(dc_slug__in=children_slugs)
+        desc_elements = [dc_desc_element(child) for child in children]
+        title = u'Utwory wybrane'
+        own_attributes = {
+            'title': title,
+            'relation.hasPart': [IDENTIFIER_PREFIX + child_slug for child_slug in children_slugs],
+            'identifier.url': IDENTIFIER_PREFIX + slug,
+            'date': date.today().isoformat(),
+        }
+        dc_tags = []
+        for tag in DC_TAGS:
+            if tag in own_attributes:
+                values = own_attributes[tag]
+                if not isinstance(values, list):
+                    values = [values]
+            else:
+                values = distinct_dc_values(tag, desc_elements)
+            for value in values:
+                dc_tags.append(DC_TEMPLATE % {'tag': tag, 'value': value})
+        xml = TEMPLATE % {'slug': slug, 'dc': '\n'.join(dc_tags)}
+        Book.create(
+            text=xml,
+            creator=None,
+            slug=slug,
+            title=title,
+            gallery=slug)
diff --git a/src/documents/management/commands/fixdc.py b/src/documents/management/commands/fixdc.py
new file mode 100644 (file)
index 0000000..3f4a848
--- /dev/null
@@ -0,0 +1,52 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from librarian import RDFNS, WLURI, ValidationError
+from librarian.dcparser import BookInfo
+from documents.management import XmlUpdater
+from documents.management.commands import XmlUpdaterCommand
+
+
+class FixDC(XmlUpdater):
+    commit_desc = "auto-fixing DC"
+    retain_publishable = True
+    only_first_chunk = True
+
+    def fix_wluri(elem, change, verbose):
+        try:
+            WLURI.strict(elem.text)
+        except ValidationError:
+            correct_field = str(WLURI.from_slug(
+                                WLURI(elem.text.strip()).slug))
+            try:
+                WLURI.strict(correct_field)
+            except ValidationError:
+                # Can't make a valid WLURI out of it, leave as is.
+                return False
+            if verbose:
+                print("Changing %s from %s to %s" % (
+                        elem.tag, elem.text, correct_field
+                    ))
+            elem.text = correct_field
+            return True
+    for field in BookInfo.FIELDS:
+        if field.validator == WLURI:
+            XmlUpdater.fixes_elements('.//' + field.uri)(fix_wluri)
+
+    @XmlUpdater.fixes_elements(".//" + RDFNS("Description"))
+    def fix_rdfabout(elem, change, verbose):
+        correct_about = change.tree.book.correct_about()
+        attr_name = RDFNS("about")
+        current_about = elem.get(attr_name)
+        if current_about != correct_about:
+            if verbose:
+                print("Changing rdf:about from %s to %s" % (
+                        current_about, correct_about
+                    ))
+            elem.set(attr_name, correct_about)
+            return True
+
+
+class Command(XmlUpdaterCommand):
+    updater = FixDC
+    help = 'Fixes obvious errors in DC: rdf:about and WLURI format.'
diff --git a/src/documents/management/commands/import_wl.py b/src/documents/management/commands/import_wl.py
new file mode 100644 (file)
index 0000000..299d4c5
--- /dev/null
@@ -0,0 +1,89 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from collections import defaultdict
+import json
+from urllib.request import urlopen
+
+from django.core.management.base import BaseCommand
+from django.core.management.color import color_style
+from django.db import transaction
+from librarian.dcparser import BookInfo
+from librarian import ParseError, ValidationError
+
+from documents.models import Book
+
+
+WL_API = 'http://www.wolnelektury.pl/api/books/'
+
+
+class Command(BaseCommand):
+    help = 'Imports XML files from WL.'
+
+    def add_arguments(self, parser):
+        parser.add_argument('-q', '--quiet', action='store_false', dest='verbose', default=True,
+            help='Less output')
+
+    def handle(self, *args, **options):
+
+        self.style = color_style()
+
+        verbose = options.get('verbose')
+
+        # Start transaction management.
+        transaction.enter_transaction_management()
+
+        if verbose:
+            print('Reading currently managed files (skipping hidden ones).')
+        slugs = defaultdict(list)
+        for b in Book.objects.exclude(slug__startswith='.').all():
+            if verbose:
+                print(b.slug)
+            text = b.materialize().encode('utf-8')
+            try:
+                info = BookInfo.from_bytes(text)
+            except (ParseError, ValidationError):
+                pass
+            else:
+                slugs[info.slug].append(b)
+
+        book_count = 0
+        commit_args = {
+            "author_name": 'Platforma',
+            "description": 'Automatycznie zaimportowane z Wolnych Lektur',
+            "publishable": True,
+        }
+
+        if verbose:
+            print('Opening books list')
+        for book in json.load(urlopen(WL_API)):
+            book_detail = json.load(urlopen(book['href']))
+            xml_text = urlopen(book_detail['xml']).read()
+            info = BookInfo.from_bytes(xml_text)
+            previous_books = slugs.get(info.slug)
+            if previous_books:
+                if len(previous_books) > 1:
+                    print(self.style.ERROR("There is more than one book "
+                        "with slug %s:") % info.slug)
+                previous_book = previous_books[0]
+                comm = previous_book.slug
+            else:
+                previous_book = None
+                comm = '*'
+            print(book_count, info.slug , '-->', comm)
+            Book.import_xml_text(xml_text, title=info.title[:255],
+                slug=info.slug[:128], previous_book=previous_book,
+                commit_args=commit_args)
+            book_count += 1
+
+        # Print results
+        print()
+        print("Results:")
+        print("Imported %d books from WL:" % (
+                book_count, ))
+        print()
+
+
+        transaction.commit()
+        transaction.leave_transaction_management()
+
diff --git a/src/documents/management/commands/insert_isbn.py b/src/documents/management/commands/insert_isbn.py
new file mode 100644 (file)
index 0000000..536d30f
--- /dev/null
@@ -0,0 +1,95 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import csv
+
+import sys
+from django.contrib.auth.models import User
+from lxml import etree
+from collections import defaultdict
+from django.core.management import BaseCommand
+
+from documents.models import Book
+from librarian import RDFNS, DCNS
+
+CONTENT_TYPES = {
+    'pdf':  'application/pdf',
+    'epub': 'application/epub+zip',
+    'mobi': 'application/x-mobipocket-ebook',
+    'txt':  'text/plain',
+    'html': 'text/html',
+}
+
+
+ISBN_TEMPLATES = (
+    r'<dc:relation.hasFormat id="%(format)s" xmlns:dc="http://purl.org/dc/elements/1.1/">%(url)s'
+    r'</dc:relation.hasFormat>',
+    r'<meta refines="#%(format)s" id="%(format)s-id" property="dcterms:identifier">ISBN-%(isbn)s</meta>',
+    r'<meta refines="#%(format)s-id" property="identifier-type">ISBN</meta>',
+    r'<meta refines="#%(format)s" property="dcterms:format">%(content_type)s</meta>',
+)
+
+
+def url_for_format(slug, format):
+    if format == 'html':
+        return 'https://wolnelektury.pl/katalog/lektura/%s.html' % slug
+    else:
+        return 'http://wolnelektury.pl/media/book/%(format)s/%(slug)s.%(format)s' % {'slug': slug, 'format': format}
+
+
+class Command(BaseCommand):
+    args = 'csv_file'
+
+    def add_arguments(self, parser):
+        self.add_argument(
+            '-u', '--username', dest='username', metavar='USER',
+            help='Assign commits to this user (required, preferably yourself).')
+
+    def handle(self, csv_file, **options):
+        username = options.get('username')
+
+        if username:
+            user = User.objects.get(username=username)
+        else:
+            print('Please provide a username.')
+            sys.exit(1)
+
+        csvfile = open(csv_file, 'rb')
+        isbn_lists = defaultdict(list)
+        for slug, format, isbn in csv.reader(csvfile, delimiter=','):
+            isbn_lists[slug].append((format, isbn))
+        csvfile.close()
+
+        for slug, isbn_list in isbn_lists.iteritems():
+            print('processing %s' % slug)
+            book = Book.objects.get(dc_slug=slug)
+            chunk = book.chunk_set.first()
+            old_head = chunk.head
+            src = old_head.materialize()
+            tree = etree.fromstring(src)
+            isbn_node = tree.find('.//' + DCNS("relation.hasFormat"))
+            if isbn_node is not None:
+                print('%s already contains ISBN metadata, skipping' % slug)
+                continue
+            desc = tree.find(".//" + RDFNS("Description"))
+            for format, isbn in isbn_list:
+                for template in ISBN_TEMPLATES:
+                    isbn_xml = template % {
+                        'format': format,
+                        'isbn': isbn,
+                        'content_type': CONTENT_TYPES[format],
+                        'url': url_for_format(slug, format),
+                    }
+                    element = etree.XML(isbn_xml)
+                    element.tail = '\n'
+                    desc.append(element)
+            new_head = chunk.commit(
+                etree.tostring(tree, encoding='unicode'),
+                author=user,
+                description='automatyczne dodanie isbn'
+            )
+            print('committed %s' % slug)
+            if old_head.publishable:
+                new_head.set_publishable(True)
+            else:
+                print('Warning: %s not publishable' % slug)
diff --git a/src/documents/management/commands/mark_final.py b/src/documents/management/commands/mark_final.py
new file mode 100644 (file)
index 0000000..b2c316c
--- /dev/null
@@ -0,0 +1,42 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import sys
+from django.contrib.auth.models import User
+from django.core.management import BaseCommand
+
+from documents.models import Book, Chunk
+
+
+class Command(BaseCommand):
+    args = 'slug_file'
+
+    def add_arguments(self, parser):
+        self.add_argument(
+            '-u', '--username', dest='username', metavar='USER',
+            help='Assign commits to this user (required).')
+
+    def handle(self, slug_file, **options):
+        username = options.get('username')
+
+        if username:
+            user = User.objects.get(username=username)
+        else:
+            print('Please provide a username.')
+            sys.exit(1)
+
+        slugs = [line.strip() for line in open(slug_file)]
+        books = Book.objects.filter(slug__in=slugs)
+
+        for book in books:
+            print('processing %s' % book.slug)
+            for chunk in book.chunk_set.all():
+                src = chunk.head.materialize()
+                chunk.commit(
+                    text=src,
+                    author=user,
+                    description=u'Ostateczna akceptacja merytoryczna przez kierownika literackiego.',
+                    tags=[Chunk.tag_model.objects.get(slug='editor-proofreading')],
+                    publishable=True
+                )
+            print('committed %s' % book.slug)
diff --git a/src/documents/management/commands/merge_books.py b/src/documents/management/commands/merge_books.py
new file mode 100644 (file)
index 0000000..5dc6cb0
--- /dev/null
@@ -0,0 +1,220 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import sys
+
+from django.contrib.auth.models import User
+from django.core.management.base import BaseCommand
+from django.core.management.color import color_style
+from django.db import transaction
+
+from documents.models import Book
+
+
+def common_prefix(texts):
+    common = []
+
+    min_len = min(len(text) for text in texts)
+    for i in range(min_len):
+        chars = list(set([text[i] for text in texts]))
+        if len(chars) > 1:
+            break
+        common.append(chars[0])
+    return "".join(common)
+
+
+class Command(BaseCommand):
+    help = 'Merges multiple books into one.'
+    args = '[slug]...'
+
+    def add_arguments(self, parser):
+        self.add_argument(
+            '-s', '--slug', dest='new_slug', metavar='SLUG',
+            help='New slug of the merged book (defaults to common part of all slugs).')
+        self.add_argument(
+            '-t', '--title', dest='new_title', metavar='TITLE',
+            help='New title of the merged book (defaults to common part of all titles).')
+        self.add_argument(
+            '-q', '--quiet', action='store_false', dest='verbose', default=True,
+            help='Less output')
+        self.add_argument(
+            '-g', '--guess', action='store_true', dest='guess', default=False,
+            help='Try to guess what merges are needed (but do not apply them).')
+        self.add_argument(
+            '-d', '--dry-run', action='store_true', dest='dry_run', default=False,
+            help='Dry run: do not actually change anything.')
+        self.add_argument(
+            '-f', '--force', action='store_true', dest='force', default=False,
+            help='On slug conflict, hide the original book to archive.')
+
+    def print_guess(self, dry_run=True, force=False):
+        from collections import defaultdict
+        from pipes import quote
+        import re
+    
+        def read_slug(slug):
+            res = []
+            res.append((re.compile(r'__?(przedmowa)$'), -1))
+            res.append((re.compile(r'__?(cz(esc)?|ksiega|rozdzial)__?(?P<n>\d*)$'), None))
+            res.append((re.compile(r'__?(rozdzialy__?)?(?P<n>\d*)-'), None))
+        
+            for r, default in res:
+                m = r.search(slug)
+                if m:
+                    start = m.start()
+                    try:
+                        return int(m.group('n')), slug[:start]
+                    except IndexError:
+                        return default, slug[:start]
+            return None, slug
+    
+        def file_to_title(fname):
+            """ Returns a title-like version of a filename. """
+            parts = (p.replace('_', ' ').title() for p in fname.split('__'))
+            return ' / '.join(parts)
+    
+        merges = defaultdict(list)
+        slugs = []
+        for b in Book.objects.all():
+            slugs.append(b.slug)
+            n, ns = read_slug(b.slug)
+            if n is not None:
+                merges[ns].append((n, b))
+    
+        conflicting_slugs = []
+        for slug in sorted(merges.keys()):
+            merge_list = sorted(merges[slug])
+            if len(merge_list) < 2:
+                continue
+    
+            merge_slugs = [b.slug for i, b in merge_list]
+            if slug in slugs and slug not in merge_slugs:
+                conflicting_slugs.append(slug)
+    
+            title = file_to_title(slug)
+            print("./manage.py merge_books %s%s--title=%s --slug=%s \\\n    %s\n" % (
+                '--dry-run ' if dry_run else '',
+                '--force ' if force else '',
+                quote(title), slug,
+                " \\\n    ".join(merge_slugs)
+                ))
+    
+        if conflicting_slugs:
+            if force:
+                print(self.style.NOTICE('# These books will be archived:'))
+            else:
+                print(self.style.ERROR('# ERROR: Conflicting slugs:'))
+            for slug in conflicting_slugs:
+                print('#', slug)
+
+
+    def handle(self, *slugs, **options):
+
+        self.style = color_style()
+
+        force = options.get('force')
+        guess = options.get('guess')
+        dry_run = options.get('dry_run')
+        new_slug = options.get('new_slug').decode('utf-8')
+        new_title = options.get('new_title').decode('utf-8')
+        verbose = options.get('verbose')
+
+        if guess:
+            if slugs:
+                print("Please specify either slugs, or --guess.")
+                return
+            else:
+                self.print_guess(dry_run, force)
+                return
+        if not slugs:
+            print("Please specify some book slugs")
+            return
+
+        # Start transaction management.
+        transaction.enter_transaction_management()
+
+        books = [Book.objects.get(slug=slug) for slug in slugs]
+        common_slug = common_prefix(slugs)
+        common_title = common_prefix([b.title for b in books])
+
+        if not new_title:
+            new_title = common_title
+        elif common_title.startswith(new_title):
+            common_title = new_title
+
+        if not new_slug:
+            new_slug = common_slug
+        elif common_slug.startswith(new_slug):
+            common_slug = new_slug
+
+        if slugs[0] != new_slug and Book.objects.filter(slug=new_slug).exists():
+            self.style.ERROR('Book already exists, skipping!')
+
+
+        if dry_run and verbose:
+            print(self.style.NOTICE('DRY RUN: nothing will be changed.'))
+            print()
+
+        if verbose:
+            print("New title:", self.style.NOTICE(new_title))
+            print("New slug:", self.style.NOTICE(new_slug))
+            print()
+
+        for i, book in enumerate(books):
+            chunk_titles = []
+            chunk_slugs = []
+
+            book_title = book.title[len(common_title):].replace(' / ', ' ').lstrip()
+            book_slug = book.slug[len(common_slug):].replace('__', '_').lstrip('-_')
+            for j, chunk in enumerate(book):
+                if j:
+                    new_chunk_title = book_title + '_%d' % j
+                    new_chunk_slug = book_slug + '_%d' % j
+                else:
+                    new_chunk_title, new_chunk_slug = book_title, book_slug
+
+                chunk_titles.append(new_chunk_title)
+                chunk_slugs.append(new_chunk_slug)
+
+                if verbose:
+                    print("title: %s // %s  -->\n       %s // %s\nslug: %s / %s  -->\n      %s / %s" % (
+                        book.title, chunk.title,
+                        new_title, new_chunk_title,
+                        book.slug, chunk.slug,
+                        new_slug, new_chunk_slug))
+                    print()
+
+            if not dry_run:
+                try:
+                    conflict = Book.objects.get(slug=new_slug)
+                except Book.DoesNotExist:
+                    conflict = None
+                else:
+                    if conflict == books[0]:
+                        conflict = None
+
+                if conflict:
+                    if force:
+                        # FIXME: there still may be a conflict
+                        conflict.slug = '.' + conflict.slug
+                        conflict.save()
+                        print(self.style.NOTICE('Book with slug "%s" moved to "%s".' % (new_slug, conflict.slug)))
+                    else:
+                        print(self.style.ERROR('ERROR: Book with slug "%s" exists.' % new_slug))
+                        return
+
+                if i:
+                    books[0].append(books[i], slugs=chunk_slugs, titles=chunk_titles)
+                else:
+                    book.title = new_title
+                    book.slug = new_slug
+                    book.save()
+                    for j, chunk in enumerate(book):
+                        chunk.title = chunk_titles[j]
+                        chunk.slug = chunk_slugs[j]
+                        chunk.save()
+
+
+        transaction.commit()
+        transaction.leave_transaction_management()
+
diff --git a/src/documents/management/commands/prune_audience.py b/src/documents/management/commands/prune_audience.py
new file mode 100644 (file)
index 0000000..743a58a
--- /dev/null
@@ -0,0 +1,59 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import sys
+from django.contrib.auth.models import User
+from lxml import etree
+
+from django.core.management import BaseCommand
+
+from documents.models import Book
+from librarian import DCNS
+
+
+class Command(BaseCommand):
+    args = 'exclude_file'
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            '-u', '--username', dest='username', metavar='USER',
+            help='Assign commits to this user (required, preferably yourself).')
+
+    def handle(self, exclude_file, **options):
+        username = options.get('username')
+
+        if username:
+            user = User.objects.get(username=username)
+        else:
+            print('Please provide a username.')
+            sys.exit(1)
+
+        excluded_slugs = [line.strip() for line in open(exclude_file, 'rb') if line.strip()]
+        books = Book.objects.exclude(slug__in=excluded_slugs)
+
+        for book in books:
+            if not book.is_published():
+                continue
+            print('processing %s' % book.slug)
+            chunk = book.chunk_set.first()
+            old_head = chunk.head
+            src = old_head.materialize()
+            tree = etree.fromstring(src)
+            audience_nodes = tree.findall('.//' + DCNS("audience"))
+            if not audience_nodes:
+                print('%s has no audience, skipping' % book.slug)
+                continue
+
+            for node in audience_nodes:
+                node.getparent().remove(node)
+
+            chunk.commit(
+                etree.tostring(tree, encoding='unicode'),
+                author=user,
+                description='automatyczne skasowanie audience',
+                publishable=old_head.publishable
+            )
+            print('committed %s' % book.slug)
+            if not old_head.publishable:
+                print('Warning: %s not publishable, last head: %s, %s' % (
+                    book.slug, old_head.author.username, old_head.description[:40].replace('\n', ' ')))
diff --git a/src/documents/managers.py b/src/documents/managers.py
new file mode 100644 (file)
index 0000000..137baaf
--- /dev/null
@@ -0,0 +1,8 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.db import models
+
+class VisibleManager(models.Manager):
+    def get_queryset(self):
+        return super(VisibleManager, self).get_queryset().exclude(_hidden=True)
diff --git a/src/documents/models/__init__.py b/src/documents/models/__init__.py
new file mode 100644 (file)
index 0000000..48b6d07
--- /dev/null
@@ -0,0 +1,20 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from .project import Project
+from .chunk import Chunk
+from .image import Image
+from .publish_log import (BookPublishRecord,
+    ChunkPublishRecord, ImagePublishRecord)
+from .book import Book
+from .listeners import *
+
+from django.contrib.auth.models import User as AuthUser
+
+
+class User(AuthUser):
+    class Meta:
+        proxy = True
+
+    def __str__(self):
+        return "%s %s" % (self.first_name, self.last_name)
diff --git a/src/documents/models/book.py b/src/documents/models/book.py
new file mode 100644 (file)
index 0000000..e7c3481
--- /dev/null
@@ -0,0 +1,433 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.contrib.sites.models import Site
+from django.db import models, transaction
+from django.template.loader import render_to_string
+from django.urls import reverse
+from django.utils.translation import ugettext_lazy as _
+from django.conf import settings
+from slugify import slugify
+
+
+import apiclient
+from documents.helpers import cached_in_field, GalleryMerger
+from documents.models import BookPublishRecord, ChunkPublishRecord, Project
+from documents.signals import post_publish
+from documents.xml_tools import compile_text, split_xml
+from cover.models import Image
+import os
+import shutil
+import re
+
+class Book(models.Model):
+    """ A document edited on the wiki """
+
+    title = models.CharField(_('title'), max_length=255, db_index=True)
+    slug = models.SlugField(_('slug'), max_length=128, unique=True, db_index=True)
+    public = models.BooleanField(_('public'), default=True, db_index=True)
+    gallery = models.CharField(_('scan gallery name'), max_length=255, blank=True)
+    project = models.ForeignKey(Project, models.SET_NULL, null=True, blank=True)
+
+    #wl_slug = models.CharField(_('title'), max_length=255, null=True, db_index=True, editable=False)
+    parent = models.ForeignKey('self', models.SET_NULL, null=True, blank=True, verbose_name=_('parent'), related_name="children", editable=False)
+    parent_number = models.IntegerField(_('parent number'), null=True, blank=True, db_index=True, editable=False)
+
+    # Cache
+    _single = models.NullBooleanField(editable=False, db_index=True)
+    _new_publishable = models.NullBooleanField(editable=False)
+    _published = models.NullBooleanField(editable=False)
+    _on_track = models.IntegerField(null=True, blank=True, db_index=True, editable=False)
+    dc_cover_image = models.ForeignKey(Image, blank=True, null=True,
+        db_index=True, on_delete=models.SET_NULL, editable=False)
+    dc_slug = models.CharField(max_length=128, null=True, blank=True,
+            editable=False, db_index=True)
+
+    class NoTextError(BaseException):
+        pass
+
+    class Meta:
+        app_label = 'documents'
+        ordering = ['title', 'slug']
+        verbose_name = _('book')
+        verbose_name_plural = _('books')
+
+
+    # Representing
+    # ============
+
+    def __iter__(self):
+        return iter(self.chunk_set.all())
+
+    def __getitem__(self, chunk):
+        return self.chunk_set.all()[chunk]
+
+    def __len__(self):
+        return self.chunk_set.count()
+
+    def __bool__(self):
+        """
+            Necessary so that __len__ isn't used for bool evaluation.
+        """
+        return True
+
+    def __str__(self):
+        return self.title
+
+    def get_absolute_url(self):
+        return reverse("documents_book", args=[self.slug])
+
+    def correct_about(self):
+        return "http://%s%s" % (
+            Site.objects.get_current().domain,
+            self.get_absolute_url()
+        )
+
+    def gallery_path(self):
+        return os.path.join(settings.MEDIA_ROOT, settings.IMAGE_DIR, self.gallery)
+
+    def gallery_url(self):
+        return '%s%s%s/' % (settings.MEDIA_URL, settings.IMAGE_DIR, self.gallery)
+
+    # Creating & manipulating
+    # =======================
+
+    def accessible(self, request):
+        return self.public or request.user.is_authenticated
+
+    @classmethod
+    @transaction.atomic
+    def create(cls, creator, text, *args, **kwargs):
+        b = cls.objects.create(*args, **kwargs)
+        b.chunk_set.all().update(creator=creator)
+        b[0].commit(text, author=creator)
+        return b
+
+    def add(self, *args, **kwargs):
+        """Add a new chunk at the end."""
+        return self.chunk_set.reverse()[0].split(*args, **kwargs)
+
+    @classmethod
+    @transaction.atomic
+    def import_xml_text(cls, text=u'', previous_book=None,
+                commit_args=None, **kwargs):
+        """Imports a book from XML, splitting it into chunks as necessary."""
+        texts = split_xml(text)
+        if previous_book:
+            instance = previous_book
+        else:
+            instance = cls(**kwargs)
+            instance.save()
+
+        # if there are more parts, set the rest to empty strings
+        book_len = len(instance)
+        for i in range(book_len - len(texts)):
+            texts.append((u'pusta część %d' % (i + 1), u''))
+
+        i = 0
+        for i, (title, text) in enumerate(texts):
+            if not title:
+                title = u'część %d' % (i + 1)
+
+            slug = slugify(title)
+
+            if i < book_len:
+                chunk = instance[i]
+                chunk.slug = slug[:50]
+                chunk.title = title[:255]
+                chunk.save()
+            else:
+                chunk = instance.add(slug, title)
+
+            chunk.commit(text, **commit_args)
+
+        return instance
+
+    def make_chunk_slug(self, proposed):
+        """ 
+            Finds a chunk slug not yet used in the book.
+        """
+        slugs = set(c.slug for c in self)
+        i = 1
+        new_slug = proposed[:50]
+        while new_slug in slugs:
+            new_slug = "%s_%d" % (proposed[:45], i)
+            i += 1
+        return new_slug
+
+    @transaction.atomic
+    def append(self, other, slugs=None, titles=None):
+        """Add all chunks of another book to self."""
+        assert self != other
+
+        number = self[len(self) - 1].number + 1
+        len_other = len(other)
+        single = len_other == 1
+
+        if slugs is not None:
+            assert len(slugs) == len_other
+        if titles is not None:
+            assert len(titles) == len_other
+            if slugs is None:
+                slugs = [slugify(t) for t in titles]
+
+        for i, chunk in enumerate(other):
+            # move chunk to new book
+            chunk.book = self
+            chunk.number = number
+
+            if titles is None:
+                # try some title guessing
+                if other.title.startswith(self.title):
+                    other_title_part = other.title[len(self.title):].lstrip(' /')
+                else:
+                    other_title_part = other.title
+
+                if single:
+                    # special treatment for appending one-parters:
+                    # just use the guessed title and original book slug
+                    chunk.title = other_title_part
+                    if other.slug.startswith(self.slug):
+                        chunk.slug = other.slug[len(self.slug):].lstrip('-_')
+                    else:
+                        chunk.slug = other.slug
+                else:
+                    chunk.title = ("%s, %s" % (other_title_part, chunk.title))[:255]
+            else:
+                chunk.slug = slugs[i]
+                chunk.title = titles[i]
+
+            chunk.slug = self.make_chunk_slug(chunk.slug)
+            chunk.save()
+            number += 1
+        assert not other.chunk_set.exists()
+
+        gm = GalleryMerger(self.gallery, other.gallery)
+        self.gallery = gm.merge()
+
+        # and move the gallery starts
+        if gm.was_merged:
+                for chunk in self[len(self) - len_other:]:
+                        old_start = chunk.gallery_start or 1
+                        chunk.gallery_start = old_start + gm.dest_size - gm.num_deleted
+                        chunk.save()
+
+        other.delete()
+
+
+    @transaction.atomic
+    def prepend_history(self, other):
+        """Prepend history from all the other book's chunks to own."""
+        assert self != other
+
+        for i in range(len(self), len(other)):
+            title = u"pusta część %d" % i
+            chunk = self.add(slugify(title), title)
+            chunk.commit('')
+
+        for i in range(len(other)):
+            self[i].prepend_history(other[0])
+
+        assert not other.chunk_set.exists()
+        other.delete()
+
+    def split(self):
+        """Splits all the chunks into separate books."""
+        self.title
+        for chunk in self:
+            book = Book.objects.create(title=chunk.title, slug=chunk.slug,
+                    public=self.public, gallery=self.gallery)
+            book[0].delete()
+            chunk.book = book
+            chunk.number = 1
+            chunk.save()
+        assert not self.chunk_set.exists()
+        self.delete()
+
+    # State & cache
+    # =============
+
+    def last_published(self):
+        try:
+            return self.publish_log.all()[0].timestamp
+        except IndexError:
+            return None
+
+    def assert_publishable(self):
+        assert self.chunk_set.exists(), _('No chunks in the book.')
+        try:
+            changes = self.get_current_changes(publishable=True)
+        except self.NoTextError:
+            raise AssertionError(_('Not all chunks have publishable revisions.'))
+
+        from librarian import NoDublinCore, ParseError, ValidationError
+
+        try:
+            bi = self.wldocument(changes=changes, strict=True).book_info
+        except ParseError as e:
+            raise AssertionError(_('Invalid XML') + ': ' + str(e))
+        except NoDublinCore:
+            raise AssertionError(_('No Dublin Core found.'))
+        except ValidationError as e:
+            raise AssertionError(_('Invalid Dublin Core') + ': ' + str(e))
+
+        valid_about = self.correct_about()
+        assert bi.about == valid_about, _("rdf:about is not") + " " + valid_about
+
+    def publishable_error(self):
+        try:
+            return self.assert_publishable()
+        except AssertionError as e:
+            return e
+        else:
+            return None
+
+    def hidden(self):
+        return self.slug.startswith('.')
+
+    def is_new_publishable(self):
+        """Checks if book is ready for publishing.
+
+        Returns True if there is a publishable version newer than the one
+        already published.
+
+        """
+        new_publishable = False
+        if not self.chunk_set.exists():
+            return False
+        for chunk in self:
+            change = chunk.publishable()
+            if not change:
+                return False
+            if not new_publishable and not change.publish_log.exists():
+                new_publishable = True
+        return new_publishable
+    new_publishable = cached_in_field('_new_publishable')(is_new_publishable)
+
+    def is_published(self):
+        return self.publish_log.exists()
+    published = cached_in_field('_published')(is_published)
+
+    def get_on_track(self):
+        if self.published:
+            return -1
+        stages = [ch.stage.ordering if ch.stage is not None else 0
+                    for ch in self]
+        if not len(stages):
+            return 0
+        return min(stages)
+    on_track = cached_in_field('_on_track')(get_on_track)
+
+    def is_single(self):
+        return len(self) == 1
+    single = cached_in_field('_single')(is_single)
+
+    def book_info(self, publishable=True):
+        try:
+            book_xml = self.materialize(publishable=publishable)
+        except self.NoTextError:
+            pass
+        else:
+            from librarian.dcparser import BookInfo
+            from librarian import NoDublinCore, ParseError, ValidationError
+            try:
+                return BookInfo.from_bytes(book_xml.encode('utf-8'))
+            except (self.NoTextError, ParseError, NoDublinCore, ValidationError):
+                return None
+
+    def refresh_dc_cache(self):
+        update = {
+            'dc_slug': None,
+            'dc_cover_image': None,
+        }
+
+        info = self.book_info()
+        if info is not None:
+            update['dc_slug'] = info.url.slug
+            if info.cover_source:
+                try:
+                    image = Image.objects.get(pk=int(info.cover_source.rstrip('/').rsplit('/', 1)[-1]))
+                except:
+                    pass
+                else:
+                    if info.cover_source == image.get_full_url():
+                        update['dc_cover_image'] = image
+        Book.objects.filter(pk=self.pk).update(**update)
+
+    def touch(self):
+        update = {
+            "_new_publishable": self.is_new_publishable(),
+            "_published": self.is_published(),
+            "_single": self.is_single(),
+            "_on_track": self.get_on_track(),
+        }
+        Book.objects.filter(pk=self.pk).update(**update)
+        self.refresh_dc_cache()
+
+    # Materializing & publishing
+    # ==========================
+
+    def get_current_changes(self, publishable=True):
+        """
+            Returns a list containing one Change for every Chunk in the Book.
+            Takes the most recent revision (publishable, if set).
+            Throws an error, if a proper revision is unavailable for a Chunk.
+        """
+        if publishable:
+            changes = [chunk.publishable() for chunk in self]
+        else:
+            changes = [chunk.head for chunk in self if chunk.head is not None]
+        if None in changes:
+            raise self.NoTextError('Some chunks have no available text.')
+        return changes
+
+    def materialize(self, publishable=False, changes=None):
+        """ 
+            Get full text of the document compiled from chunks.
+            Takes the current versions of all texts
+            or versions most recently tagged for publishing,
+            or a specified iterable changes.
+        """
+        if changes is None:
+            changes = self.get_current_changes(publishable)
+        return compile_text(change.materialize() for change in changes)
+
+    def wldocument(self, publishable=True, changes=None, 
+            parse_dublincore=True, strict=False):
+        from documents.ebook_utils import RedakcjaDocProvider
+        from librarian.parser import WLDocument
+
+        return WLDocument.from_bytes(
+                self.materialize(publishable=publishable, changes=changes).encode('utf-8'),
+                provider=RedakcjaDocProvider(publishable=publishable),
+                parse_dublincore=parse_dublincore,
+                strict=strict)
+
+    def publish(self, user, fake=False, host=None, days=0, beta=False):
+        """
+            Publishes a book on behalf of a (local) user.
+        """
+        self.assert_publishable()
+        changes = self.get_current_changes(publishable=True)
+        if not fake:
+            book_xml = self.materialize(changes=changes)
+            data = {"book_xml": book_xml, "days": days}
+            if host:
+                data['gallery_url'] = host + self.gallery_url()
+            apiclient.api_call(user, "books/", data, beta=beta)
+        if not beta:
+            # record the publish
+            br = BookPublishRecord.objects.create(book=self, user=user)
+            for c in changes:
+                ChunkPublishRecord.objects.create(book_record=br, change=c)
+            if not self.public and days == 0:
+                self.public = True
+                self.save()
+            if self.public and days > 0:
+                self.public = False
+                self.save()
+            post_publish.send(sender=br)
+
+    def latex_dir(self):
+        doc = self.wldocument()
+        return doc.latex_dir(cover=True, ilustr_path=self.gallery_path())
diff --git a/src/documents/models/chunk.py b/src/documents/models/chunk.py
new file mode 100644 (file)
index 0000000..39a2087
--- /dev/null
@@ -0,0 +1,110 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf import settings
+from django.db import models
+from django.db.utils import IntegrityError
+from django.template.loader import render_to_string
+from django.urls import reverse
+from django.utils.translation import ugettext_lazy as _
+from documents.helpers import cached_in_field
+from documents.managers import VisibleManager
+from dvcs import models as dvcs_models
+
+
+class Chunk(dvcs_models.Document):
+    """ An editable chunk of text. Every Book text is divided into chunks. """
+    REPO_PATH = settings.CATALOGUE_REPO_PATH
+
+    book = models.ForeignKey('Book', models.CASCADE, editable=False, verbose_name=_('book'))
+    number = models.IntegerField(_('number'))
+    title = models.CharField(_('title'), max_length=255, blank=True)
+    slug = models.SlugField(_('slug'))
+    gallery_start = models.IntegerField(_('gallery start'), null=True, blank=True, default=1)
+
+    # cache
+    _hidden = models.NullBooleanField(editable=False)
+    _changed = models.NullBooleanField(editable=False)
+    _new_publishable = models.NullBooleanField(editable=False)
+
+    # managers
+    objects = models.Manager()
+    visible_objects = VisibleManager()
+
+    class Meta:
+        app_label = 'documents'
+        unique_together = [['book', 'number'], ['book', 'slug']]
+        ordering = ['number']
+        verbose_name = _('chunk')
+        verbose_name_plural = _('chunks')
+        permissions = [('can_pubmark', 'Can mark for publishing')]
+
+    # Representing
+    # ============
+
+    def __str__(self):
+        return "%d:%d: %s" % (self.book_id, self.number, self.title)
+
+    def get_absolute_url(self):
+        return reverse("wiki_editor", args=[self.book.slug, self.slug])
+
+    def pretty_name(self, book_length=None):
+        title = self.book.title
+        if self.title:
+            title += ", %s" % self.title
+        if book_length and book_length > 1:
+            title += " (%d/%d)" % (self.number, book_length)
+        return title
+
+    # Creating and manipulation
+    # =========================
+
+    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)
+        new_chunk = None
+        while not new_chunk:
+            new_slug = self.book.make_chunk_slug(slug)
+            try:
+                new_chunk = self.book.chunk_set.create(
+                    number=self.number+1,
+                    slug=new_slug[:50], title=title[:255], **kwargs)
+            except IntegrityError:
+                pass
+        return new_chunk
+
+    @classmethod
+    def get(cls, book_slug, chunk_slug=None):
+        if chunk_slug is None:
+            return cls.objects.get(book__slug=book_slug, number=1)
+        else:
+            return cls.objects.get(book__slug=book_slug, slug=chunk_slug)
+
+    # State & cache
+    # =============
+
+    def is_new_publishable(self):
+        change = self.publishable()
+        if not change:
+            return False
+        return not change.publish_log.exists()
+    new_publishable = cached_in_field('_new_publishable')(is_new_publishable)
+
+    def is_changed(self):
+        if self.head is None:
+            return False
+        return not self.head.publishable
+    changed = cached_in_field('_changed')(is_changed)
+
+    def is_hidden(self):
+        return self.book.hidden()
+    hidden = cached_in_field('_hidden')(is_hidden)
+
+    def touch(self):
+        update = {
+            "_changed": self.is_changed(),
+            "_new_publishable": self.is_new_publishable(),
+            "_hidden": self.is_hidden(),
+        }
+        Chunk.objects.filter(pk=self.pk).update(**update)
diff --git a/src/documents/models/image.py b/src/documents/models/image.py
new file mode 100644 (file)
index 0000000..10782dc
--- /dev/null
@@ -0,0 +1,148 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf import settings
+from django.contrib.sites.models import Site
+from django.db import models
+from django.template.loader import render_to_string
+from django.urls import reverse
+from django.utils.translation import ugettext_lazy as _
+from documents.helpers import cached_in_field
+from documents.models import Project
+from dvcs import models as dvcs_models
+
+
+class Image(dvcs_models.Document):
+    """ An editable chunk of text. Every Book text is divided into chunks. """
+    REPO_PATH = settings.CATALOGUE_IMAGE_REPO_PATH
+
+    image = models.FileField(_('image'), upload_to='catalogue/images')
+    title = models.CharField(_('title'), max_length=255, blank=True)
+    slug = models.SlugField(_('slug'), unique=True)
+    public = models.BooleanField(_('public'), default=True, db_index=True)
+    project = models.ForeignKey(Project, models.SET_NULL, null=True, blank=True)
+
+    # cache
+    _new_publishable = models.NullBooleanField(editable=False)
+    _published = models.NullBooleanField(editable=False)
+    _changed = models.NullBooleanField(editable=False)
+
+    class Meta:
+        app_label = 'documents'
+        ordering = ['title']
+        verbose_name = _('image')
+        verbose_name_plural = _('images')
+        permissions = [('can_pubmark_image', 'Can mark images for publishing')]
+
+    # Representing
+    # ============
+
+    def __str__(self):
+        return self.title
+
+    def get_absolute_url(self):
+        return reverse("documents_image", args=[self.slug])
+
+    def correct_about(self):
+        return ["http://%s%s" % (
+            Site.objects.get_current().domain,
+            self.get_absolute_url()
+            ),
+            "http://%s%s" % (
+                'obrazy.redakcja.wolnelektury.pl',
+                self.get_absolute_url()
+            )]
+
+    # State & cache
+    # =============
+
+    def last_published(self):
+        try:
+            return self.publish_log.all()[0].timestamp
+        except IndexError:
+            return None
+
+    def assert_publishable(self):
+        from librarian.picture import WLPicture
+        from librarian import NoDublinCore, ParseError, ValidationError
+
+        class SelfImageStore(object):
+            def path(self_, slug, mime_type):
+                """Returns own file object. Ignores slug ad mime_type."""
+                return open(self.image.path)
+
+        publishable = self.publishable()
+        assert publishable, _("There is no publishable revision")
+        picture_xml = publishable.materialize()
+
+        try:
+            picture = WLPicture.from_bytes(
+                    picture_xml.encode('utf-8'),
+                    image_store=SelfImageStore)
+        except ParseError as e:
+            raise AssertionError(_('Invalid XML') + ': ' + str(e))
+        except NoDublinCore:
+            raise AssertionError(_('No Dublin Core found.'))
+        except ValidationError as e:
+            raise AssertionError(_('Invalid Dublin Core') + ': ' + str(e))
+
+        valid_about = self.correct_about()
+        assert picture.picture_info.about in valid_about, \
+                _("rdf:about is not") + " " + valid_about[0]
+
+    def publishable_error(self):
+        try:
+            return self.assert_publishable()
+        except AssertionError as e:
+            return e
+        else:
+            return None
+
+    def accessible(self, request):
+        return self.public or request.user.is_authenticated
+
+    def is_new_publishable(self):
+        change = self.publishable()
+        if not change:
+            return False
+        return not change.publish_log.exists()
+    new_publishable = cached_in_field('_new_publishable')(is_new_publishable)
+
+    def is_published(self):
+        return self.publish_log.exists()
+    published = cached_in_field('_published')(is_published)
+
+    def is_changed(self):
+        if self.head is None:
+            return False
+        return not self.head.publishable
+    changed = cached_in_field('_changed')(is_changed)
+
+    def touch(self):
+        update = {
+            "_changed": self.is_changed(),
+            "_new_publishable": self.is_new_publishable(),
+            "_published": self.is_published(),
+        }
+        Image.objects.filter(pk=self.pk).update(**update)
+
+    # Publishing
+    # ==========
+
+    def publish(self, user):
+        """Publishes the picture on behalf of a (local) user."""
+        from base64 import b64encode
+        import apiclient
+        from documents.signals import post_publish
+
+        self.assert_publishable()
+        change = self.publishable()
+        picture_xml = change.materialize()
+        picture_data = open(self.image.path).read()
+        apiclient.api_call(user, "pictures/", {
+                "picture_xml": picture_xml,
+                "picture_image_data": b64encode(picture_data),
+            })
+        # record the publish
+        log = self.publish_log.create(user=user, change=change)
+        post_publish.send(sender=log)
diff --git a/src/documents/models/listeners.py b/src/documents/models/listeners.py
new file mode 100644 (file)
index 0000000..d306e4e
--- /dev/null
@@ -0,0 +1,55 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.contrib.auth.models import User
+from django.db import models
+from documents.models import (Book, Chunk, Image, BookPublishRecord,
+        ImagePublishRecord)
+from documents.signals import post_publish
+from dvcs.signals import post_publishable
+
+
+def book_changed(sender, instance, created, **kwargs):
+    instance.touch()
+    for c in instance:
+        c.touch()
+models.signals.post_save.connect(book_changed, sender=Book)
+
+
+def chunk_changed(sender, instance, created, **kwargs):
+    instance.book.touch()
+    instance.touch()
+models.signals.post_save.connect(chunk_changed, sender=Chunk)
+
+
+def image_changed(sender, instance, created, **kwargs):
+    instance.touch()
+models.signals.post_save.connect(image_changed, sender=Image)
+
+
+def publish_listener(sender, *args, **kwargs):
+    if isinstance(sender, BookPublishRecord):
+        sender.book.touch()
+        for c in sender.book:
+            c.touch()
+    elif isinstance(sender, ImagePublishRecord):
+        sender.image.touch()
+post_publish.connect(publish_listener)
+
+
+def chunk_publishable_listener(sender, *args, **kwargs):
+    sender.tree.touch()
+    if isinstance(sender.tree, Chunk):
+        sender.tree.book.touch()
+post_publishable.connect(chunk_publishable_listener)
+
+def publishable_listener(sender, *args, **kwargs):
+    sender.tree.touch()
+post_publishable.connect(publishable_listener, sender=Image)
+
+
+def listener_create(sender, instance, created, **kwargs):
+    if created:
+        instance.chunk_set.create(number=1, slug='1')
+models.signals.post_save.connect(listener_create, sender=Book)
+
diff --git a/src/documents/models/project.py b/src/documents/models/project.py
new file mode 100644 (file)
index 0000000..f0ac752
--- /dev/null
@@ -0,0 +1,21 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+
+class Project(models.Model):
+    """ A project, tracked for funding purposes. """
+
+    name = models.CharField(_('name'), max_length=255, unique=True)
+    notes = models.TextField(_('notes'), blank=True, null=True)
+
+    class Meta:
+        app_label = 'documents'
+        ordering = ['name']
+        verbose_name = _('project')
+        verbose_name_plural = _('projects')
+
+    def __str__(self):
+        return self.name
diff --git a/src/documents/models/publish_log.py b/src/documents/models/publish_log.py
new file mode 100644 (file)
index 0000000..5322bb6
--- /dev/null
@@ -0,0 +1,52 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.contrib.auth.models import User
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from documents.models import Chunk, Image
+
+
+class BookPublishRecord(models.Model):
+    """
+        A record left after publishing a Book.
+    """
+
+    book = models.ForeignKey('Book', models.CASCADE, verbose_name=_('book'), related_name='publish_log')
+    timestamp = models.DateTimeField(_('time'), auto_now_add=True)
+    user = models.ForeignKey(User, models.CASCADE, verbose_name=_('user'))
+
+    class Meta:
+        app_label = 'documents'
+        ordering = ['-timestamp']
+        verbose_name = _('book publish record')
+        verbose_name_plural = _('book publish records')
+
+
+class ChunkPublishRecord(models.Model):
+    """
+        BookPublishRecord details for each Chunk.
+    """
+
+    book_record = models.ForeignKey(BookPublishRecord, models.CASCADE, verbose_name=_('book publish record'))
+    change = models.ForeignKey(Chunk.change_model, models.CASCADE, related_name='publish_log', verbose_name=_('change'))
+
+    class Meta:
+        app_label = 'documents'
+        verbose_name = _('chunk publish record')
+        verbose_name_plural = _('chunk publish records')
+
+
+class ImagePublishRecord(models.Model):
+    """A record left after publishing an Image."""
+
+    image = models.ForeignKey(Image, models.CASCADE, verbose_name=_('image'), related_name='publish_log')
+    timestamp = models.DateTimeField(_('time'), auto_now_add=True)
+    user = models.ForeignKey(User, models.CASCADE, verbose_name=_('user'))
+    change = models.ForeignKey(Image.change_model, models.CASCADE, related_name='publish_log', verbose_name=_('change'))
+
+    class Meta:
+        app_label = 'documents'
+        ordering = ['-timestamp']
+        verbose_name = _('image publish record')
+        verbose_name_plural = _('image publish records')
diff --git a/src/documents/signals.py b/src/documents/signals.py
new file mode 100644 (file)
index 0000000..852dc96
--- /dev/null
@@ -0,0 +1,6 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.dispatch import Signal
+
+post_publish = Signal()
diff --git a/src/documents/templates/documents/active_users_list.html b/src/documents/templates/documents/active_users_list.html
new file mode 100644 (file)
index 0000000..ef9532f
--- /dev/null
@@ -0,0 +1,20 @@
+{% extends "documents/base.html" %}
+{% load i18n %}
+
+
+{% block titleextra %}{% trans "Active users" %}{% endblock %}
+
+
+{% block content %}
+
+<h1>
+    {% trans "Users active in the year" %} {{ year }}
+</h1>
+
+<ul>
+{% for email, names, count in users %}
+<li>{% for name in names %}{{ name }},  {% endfor %}<a href="mailto:{{ email }}">{{ email }}</a> ({{ count }})</li>
+{% endfor %}
+</ul>
+
+{% endblock content %}
diff --git a/src/documents/templates/documents/activity.html b/src/documents/templates/documents/activity.html
new file mode 100644 (file)
index 0000000..e99ee18
--- /dev/null
@@ -0,0 +1,25 @@
+{% extends "documents/base.html" %}
+{% load i18n %}
+{% load wall %}
+
+
+{% block titleextra %}{% trans "Activity" %}{% endblock %}
+
+
+{% block content %}
+
+<div class="card">
+       <div class="card-header">
+<h1>
+       <a class="btn btn-light" href='{% url "documents_activity" prev_day.isoformat %}'>&lt;</a>
+    {% trans "Activity" %}: {{ day }}
+    {% if next_day %}
+        <a class="btn btn-light" href='{% url "documents_activity" next_day.isoformat %}'>&gt;</a>
+    {% endif %}
+</h1>
+       </div>
+       <div class="card-body">
+
+    {% day_wall day %}
+       </div>
+{% endblock content %}
diff --git a/src/documents/templates/documents/base.html b/src/documents/templates/documents/base.html
new file mode 100644 (file)
index 0000000..81af893
--- /dev/null
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+{% load pipeline i18n %}
+{% load static %}
+{% load documents %}
+<!DOCTYPE html>
+<html>
+<head lang="{{ LANGUAGE_CODE }}">
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+    <link rel="icon" href="{{ STATIC_URL }}img/pr-icon.png" type="image/png" />
+    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
+    {% stylesheet 'documents' %}
+    <title>{% block title %}{% block titleextra %}{% endblock %} ::
+        {% trans "Platforma Redakcyjna" %}{% endblock title %}</title>
+    {% block add_css %}{% endblock %}
+</head>
+<body>
+<!--#include file='/pozor.html'-->
+
+
+<nav class="navbar navbar-expand-md navbar-dark bg-dark">
+  <a class="navbar-brand" href="{% url 'documents_document_list' %}">
+         <img src="{% static "img/wl-orange.png" %}" alt="Platforma">
+  </a>
+  <ul class="navbar-nav mr-auto">
+        {% main_tabs %}
+    </ul>
+
+    <ul class="navbar-nav">
+        {% include "registration/head_login.html" %}
+    </ul>
+</nav>
+
+<div class="container mt-4 mb-4">
+  {% block content %}
+    <div class="row">
+      <div class="col-lg-8">
+        {% block leftcolumn %}
+        {% endblock leftcolumn %}
+      </div>
+      <div class="col-lg-4">
+        {% block rightcolumn %}
+        {% endblock rightcolumn %}
+      </div>
+    </div>
+  {% endblock content %}
+</div>
+
+
+<script
+    src="https://code.jquery.com/jquery-1.9.1.min.js"
+    integrity="sha256-wS9gmOZBqsqWxgIVgA8Y9WcQOa7PgSIX+rPA0VL2rbQ="
+    crossorigin="anonymous"></script>
+<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
+
+{% javascript 'documents' %}
+{% block add_js %}{% endblock %}
+{% block extrabody %}
+{% endblock %}
+</body>
+</html>
diff --git a/src/documents/templates/documents/book_append_to.html b/src/documents/templates/documents/book_append_to.html
new file mode 100644 (file)
index 0000000..121cfab
--- /dev/null
@@ -0,0 +1,19 @@
+{% extends "documents/base.html" %}
+{% load i18n %}
+{% load bootstrap4 %}
+
+{% block titleextra %}{% trans "Append book" %}{% endblock %}
+
+{% block content %}
+<div class="card">
+       <div class="card-body">
+       <form enctype="multipart/form-data" method="POST" action="">
+    {% csrf_token %}
+       {% bootstrap_form form %}
+       {% buttons %}
+       <button class="btn btn-primary" type="submit">{% trans "Append book" %}</button>
+       {% endbuttons %}
+       </form>
+       </div></div>
+{% endblock content %}
+
diff --git a/src/documents/templates/documents/book_detail.html b/src/documents/templates/documents/book_detail.html
new file mode 100644 (file)
index 0000000..3dbaf21
--- /dev/null
@@ -0,0 +1,119 @@
+{% extends "documents/base.html" %}
+{% load book_list i18n %}
+{% load bootstrap4 %}
+
+
+{% block titleextra %}{{ book.title }}{% endblock %}
+
+
+{% block content %}
+
+  <div class="card mt-4">
+    <div class="card-header">
+      <h1>{{ book.title }}</h1>
+    </div>
+    <div class="card-body">
+    
+
+
+{% if editable %}<form method='POST'>{% csrf_token %}{% endif %}
+    {% bootstrap_form form %}
+        {% if editable %}
+        {% buttons %}
+        <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+        {% endbuttons %}
+    {% endif %}
+{% if editable %}</form>{% endif %}
+
+{% if editable %}
+    {% if book.gallery %}
+    <p><a href="{% url 'documents_book_gallery' book.slug %}">{% trans "Edit gallery" %}</a></p>
+    {% endif %}
+
+    <p style="text-align:right"><a class="btn btn-sm btn-danger" href="{% url 'documents_book_append' book.slug %}">{% trans "Append to other book" %}</a></p>
+{% endif %}
+    </div>
+    </div>
+
+  <div class="card mt-4">
+    <div class="card-header">
+      <h2>{% trans "Chunks" %}</h2>
+    </div>
+    <div class="card-body">
+
+    <table class='single-book-list table'><tbody>
+    {% for chunk in book %}
+        {% include 'documents/book_list/chunk.html' %}
+    {% endfor %}
+    </tbody></table>
+    </div>
+  </div>
+
+
+
+
+<div class='card mt-4'>
+
+<div class="card-header">
+  <h2>{% trans "Publication" %}</h2>
+  </div>
+<div class="card-body">
+  <div class="row">
+<div class="col-lg-3">
+<img class="cover-preview" src="{% url 'cover_preview' book.slug %}" />
+{% if book.dc_cover_image %}
+    <a href="{{ book.dc_cover_image.get_absolute_url }}">{{ book.dc_cover_image }}</a>
+{% endif %}
+<br><br>
+<form action="{% url 'cover_preview' book.slug %}">
+<input type="hidden" name="download" value="1">
+Okładka w rozmiarze
+<input name="width" type="number" required value="600"> x <input name="height" type="number" required value="833">
+<button type="submit" class="btn btn-sm btn-primary">Pobierz</button>
+</form>
+</div>
+<div class="col-lg-9">
+<p>{% trans "Last published" %}: 
+    {% if book.last_published %}
+        {{ book.last_published }}
+    {% else %}
+        &mdash;
+    {% endif %}
+</p>
+
+{% if publishable %}
+    <p>
+    <a href="{% url 'documents_book_xml' book.slug %}" rel="nofollow">{% trans "Full XML" %}</a><br/>
+    <a target="_blank" href="{% url 'documents_book_html' book.slug %}" rel="nofollow">{% trans "HTML version" %}</a><br/>
+    <a href="{% url 'documents_book_txt' book.slug %}" rel="nofollow">{% trans "TXT version" %}</a><br/>
+    <a href="{% url 'documents_book_pdf' book.slug %}" rel="nofollow">{% trans "PDF version" %}</a><br/>
+    <a href="{% url 'documents_book_pdf_mobile' book.slug %}" rel="nofollow">{% trans "PDF version for mobiles" %}</a><br/>
+    <a href="{% url 'documents_book_epub' book.slug %}" rel="nofollow">{% trans "EPUB version" %}</a><br/>
+    <a href="{% url 'documents_book_mobi' book.slug %}" rel="nofollow">{% trans "MOBI version" %}</a><br/>
+    </p>
+
+    {% if user.is_authenticated %}
+        <!--
+        Angel photos:
+        Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
+        mira66 (http://www.flickr.com/photos/21804434@N02/) /
+        CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
+        -->
+        <form method="POST" action="{% url 'documents_publish' book.slug %}">{% csrf_token %}
+            {{ publish_options_form.as_p }}
+            <img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" />
+            <button id="publish-button" type="submit">
+                <span>{% trans "Publish" %}</span></button>
+            <img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" />
+            </form>
+    {% else %}
+        <a href="{% url 'cas_ng_login' %}">{% trans "Log in to publish." %}</a>
+    {% endif %}
+{% else %}
+    <p>{% trans "This book can't be published yet, because:" %}</p>
+    <ul><li>{{ publishable_error }}</li></ul>
+{% endif %}
+
+</div>
+  </div>
+{% endblock content %}
diff --git a/src/documents/templates/documents/book_edit.html b/src/documents/templates/documents/book_edit.html
new file mode 100644 (file)
index 0000000..37cd835
--- /dev/null
@@ -0,0 +1,18 @@
+{% extends "documents/base.html" %}
+{% load i18n %}
+
+
+{% block titleextra %}{% trans "Edit book" %}{% endblock %}
+
+
+{% block leftcolumn %}
+       <form enctype="multipart/form-data" method="POST" action="">
+    {% csrf_token %}
+       {{ form.as_p }}
+
+       <p><button type="submit">{% trans "Save" %}</button></p>
+       </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
diff --git a/src/documents/templates/documents/book_html.html b/src/documents/templates/documents/book_html.html
new file mode 100644 (file)
index 0000000..518811e
--- /dev/null
@@ -0,0 +1,29 @@
+{% load i18n %}
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+        <title>{{ book.title }}</title>
+    </head>
+    <body>
+        <div id="menu">
+            <ul>
+                <li><a href="#toc">{% trans "Table of contents" %}</a></li>
+                <li><a href="#nota_red">{% trans "Edit. note" %}</a></li>
+                <li><a href="#info">{% trans "Infobox" %}</a></li>
+            </ul>
+        </div>
+        <div id="info">
+            {#% book_info book %#}
+        </div>
+        <div id="header">
+            <div id="logo">
+                <a href="/"><img src="http://static.wolnelektury.pl/img/logo.png" alt="WolneLektury.pl - logo" /></a>
+            </div>
+        </div>
+
+        {{ html|safe }}
+
+    </body>
+</html>
diff --git a/src/documents/templates/documents/book_list/book.html b/src/documents/templates/documents/book_list/book.html
new file mode 100644 (file)
index 0000000..9de8657
--- /dev/null
@@ -0,0 +1,50 @@
+{% load i18n %}
+{% load username from common_tags %}
+
+{% if book.single %}
+    {% with chunk as chunk %}
+    <tr class="table-sm">
+        <td><input type="checkbox" name="select_book" value="{{book.id}}" data-chunk-id="{{chunk.id}}"/></td>
+       <td><a class="btn btn-sm btn-outline-secondary" href="{% url 'documents_book' book.slug %}" title="{% trans "Book settings" %}">&#x1f4d5;</a></td>
+       <td><a class="btn btn-sm btn-outline-secondary" href="{% url 'documents_chunk_edit' book.slug chunk.slug %}" title="{% trans "Chunk settings" %}">&#x1f4dc;</a></td>
+        <td><a class="btn btn-primary" target="_blank"
+                    href="{% url 'wiki_editor' book.slug %}">
+                    {{ book.title }}</a></td>
+        <td>{% if chunk.stage %}
+            {{ chunk.stage }}
+        {% else %}–
+        {% endif %}</td>
+        <td class='user-column'>{% if chunk.user %}<a href="{% url 'documents_user' chunk.user.username %}">{{ chunk.user|username }}</a>{% endif %}</td>
+        <td>
+          {% if book.published %}
+            <span class="badge badge-info" title="{% trans "published" %}">opubl.</span>
+         {% endif %}
+          {% if book.new_publishable %}
+            <span class="badge badge-primary" title="{% trans "publishable" %}">do publ.</span>
+         {% endif %}
+          {% if chunk.changed %}
+            <span class="badge badge-warning title="{% trans "changed" %}">zmiany</span>
+         {% endif %}
+        </td>
+        <td>{{ book.project.name }}</td>
+    </tr>
+    {% endwith %}
+{% else %}
+    <tr class="table-sm">
+      <td><input type="checkbox" name="select_book" value="{{book.id}}"/></td>
+      <td><a class='btn btn-sm btn-outline-secondary' href="{% url 'documents_book' book.slug %}" title="{% trans "Book settings" %}">&#x1f4d5;</a></td>
+      <td></td>
+      <td>{{ book.title }}</td>
+      <td></td>
+      <td class='user-column'></td>
+      <td>
+        {% if book.published %}
+          <span class="badge badge-info" title="{% trans "published" %}">opubl.</span>
+       {% endif %}
+        {% if book.new_publishable %}
+          <span class="badge badge-info" title="{% trans "publishable" %}">do publ.</span>
+       {% endif %}
+      </td>
+      <td>{{ book.project.name }}</td>
+    </tr>
+{% endif %}
diff --git a/src/documents/templates/documents/book_list/book_list.html b/src/documents/templates/documents/book_list/book_list.html
new file mode 100644 (file)
index 0000000..eb4c8f5
--- /dev/null
@@ -0,0 +1,117 @@
+{% load i18n %}
+{% load pagination_tags %}
+{% load username from common_tags %}
+
+
+<form name='filter' action='{{ request.path }}'>
+<input type='hidden' name="title" value="{{ request.GET.title }}" />
+<input type='hidden' name="stage" value="{{ request.GET.stage }}" />
+{% if not viewed_user %}
+    <input type='hidden' name="user" value="{{ request.GET.user }}" />
+{% endif %}
+<input type='hidden' name="all" value="{{ request.GET.all }}" />
+<input type='hidden' name="status" value="{{ request.GET.status }}" />
+<input type='hidden' name="project" value="{{ request.GET.project }}" />
+</form>
+
+
+<div class="card">
+       <div class="card-body">
+
+
+<table id="file-list" class="table{% if viewed_user %} book-list-user{% endif %}">
+    <thead><tr>
+       <th></th>
+        <th></th>
+        <th></th>
+        <th class='book-search-column'>
+            <form>
+            <input title='{% trans "Search in book titles" %}' name="title"
+                class='form-control text-filter' value="{{ request.GET.title }}" />
+            </form>
+        </th>
+        <th><select name="stage" class="form-control filter">
+            <option value=''>- {% trans "stage" %} -</option>
+            <option {% if request.GET.stage == '-' %}selected="selected"
+                    {% endif %}value="-">- {% trans "none" %} -</option>
+            {% for stage in stages %}
+                <option {% if request.GET.stage == stage.slug %}selected="selected"
+                    {% endif %}value="{{ stage.slug }}">{{ stage.name }}</option>
+            {% endfor %}
+        </select></th>
+
+        {% if not viewed_user %}
+            <th><select name="user" class="form-control filter">
+                <option value=''>- {% trans "editor" %} -</option>
+                <option {% if request.GET.user == '-' %}selected="selected"
+                        {% endif %}value="-">- {% trans "none" %} -</option>
+                {% for user in users %}
+                    <option {% if request.GET.user == user.username %}selected="selected"
+                        {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
+                {% endfor %}
+            </select></th>
+        {% else %}
+            <th style='display: none'></th>
+        {% endif %}
+
+        <th><select name="status" class="form-control filter">
+            <option value=''>- {% trans "status" %} -</option>
+            {% for state, label in states %}
+                <option {% if request.GET.status == state %}selected="selected"
+                        {% endif %}value='{{ state }}'>{{ label }}</option>
+            {% endfor %}
+        </select></th>
+
+        <th><select name="project" class="form-control filter">
+            <option value=''>- {% trans "project" %} -</option>
+                <option {% if request.GET.project == '-' %}selected="selected"
+                        {% endif %}value="-">- {% trans "none" %} -</option>
+            {% for project in projects %}
+                <option {% if request.GET.project == project.pk|slugify %}selected="selected"
+                        {% endif %}value='{{ project.pk }}'>{{ project.name }}</option>
+            {% endfor %}
+        </select></th>
+
+    </tr></thead>
+
+    {% autopaginate books 100 as books_page %}
+    <tbody>
+    {% for item in books_page %}
+        {% with book=item.book chunk=item.chunks.0  %}
+           {% include 'documents/book_list/book.html' %}
+            {% if not book.single %}
+                {% for chunk in item.chunks %}
+                    {% include 'documents/book_list/chunk.html' %}
+                {% endfor %}
+            {% endif %}
+        {% endwith %}
+    {% endfor %}
+    </tbody>
+</table>
+{% paginate %}
+        {% blocktrans count c=books|length %}{{c}} book{% plural %}{{c}} books{% endblocktrans %}
+
+
+{% if not books %}
+    <p>{% trans "No books found." %}</p>
+{% endif %}
+
+       </div>
+</div>
+
+<form id='chunk_mass_edit' action='{% url "documents_chunk_mass_edit" %}' style="display:none;">
+{% csrf_token %}
+<input type="hidden" name="ids" />
+<label for="mass_edit_stage">{% trans "Set stage" %}</label><input type="hidden" name="stage" id="mass_edit_stage"/>
+<label for="mass_edit_user">{% trans "Set user" %}</label><input type="hidden" name="user" id="mass_edit_user" />
+<input type="hidden" name="status" />
+<label for="mass_edit_project">{% trans "Project" %}</label><input type="hidden" name="project" id="mass_edit_project" />
+<label for="mass_edit_more_users">{% trans "More users" %}</label>
+</form>
+
+<select name="other-user" style="display:none;">
+  {% for user in other_users %}
+  <option {% if request.GET.user == user.username %}selected="selected"
+          {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
+  {% endfor %}
+</select>
diff --git a/src/documents/templates/documents/book_list/chunk.html b/src/documents/templates/documents/book_list/chunk.html
new file mode 100644 (file)
index 0000000..8875bb6
--- /dev/null
@@ -0,0 +1,30 @@
+{% load i18n %}
+{% load username from common_tags %}
+
+<tr class="table-secondary table-sm">
+    <td><input type="checkbox" name="select_chunk" value="{{chunk.id}}" data-book-id="{{chunk.book.id}}" /></td>
+    <td class='book-settings-column'></td>
+    <td><a class="btn btn-outline-secondary btn-sm" href="{% url 'documents_chunk_edit' chunk.book.slug chunk.slug %}" title="{% trans "Chunk settings" %}">&#x1f4dc;</a></td>
+    <td><a class="btn btn-primary" target="_blank" href="{{ chunk.get_absolute_url }}">
+      {{ chunk.number }}.
+      {{ chunk.title }}</a></td>
+    <td>{% if chunk.stage %}
+      {{ chunk.stage }}
+    {% else %}
+      –
+    {% endif %}</td>
+    <td class='user-column'>{% if chunk.user %}
+      <a href="{% url 'documents_user' chunk.user.username %}">
+        {{ chunk.user|username }}
+      </a>{% else %}
+
+    {% endif %}</td>
+
+    </td>
+    <td>
+      {% if chunk.changed %}
+        <span class="badge badge-warning title="{% trans "changed" %}">zmiany</span>
+      {% endif %}
+</td>
+<td></td>
+</tr>
diff --git a/src/documents/templates/documents/book_text.html b/src/documents/templates/documents/book_text.html
new file mode 100644 (file)
index 0000000..a88aad5
--- /dev/null
@@ -0,0 +1,28 @@
+{% load i18n pipeline %}
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+        <title>{% trans "Redakcja" %} :: {{ book.title }}</title>
+       {% stylesheet 'book' %}
+        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
+       {% javascript 'book' %}
+    </head>
+    <body>
+        <div id="menu">
+            <ul>
+                <li><a class="menu" href="#toc">{% trans "Table of contents" %}</a></li>
+{#                <li><a class="menu" href="#themes">{% trans "Themes" %}</a></li>#}
+                <li><a class="menu" href="#nota_red">{% trans "Edit. note" %}</a></li>
+{#                <li><a class="menu" href="#info">{% trans "Infobox" %}</a></li>#}
+{#                <li><a href="{{ book.get_absolute_url }}">{% trans "Book's page" %}</a></li> #}
+{#                <li><a class="menu" href="#download">{% trans "Download" %}</a></li>#}
+            </ul>
+        </div>
+        <div id="header">
+            <a href="/"><img src="/media/static/img/logo-220.png" alt="Wolne Lektury" /></a>
+        </div>
+        {{ html|safe }}
+    </body>
+</html>
diff --git a/src/documents/templates/documents/chunk_add.html b/src/documents/templates/documents/chunk_add.html
new file mode 100644 (file)
index 0000000..b5944b2
--- /dev/null
@@ -0,0 +1,29 @@
+{% extends "documents/base.html" %}
+{% load i18n %}
+{% load bootstrap4 %}
+
+
+{% block titleextra %}{% trans "Split chunk" %}{% endblock %}
+
+
+{% block content %}
+<div class="card">
+       <div class="card-header">
+    <h1>{% trans "Split chunk" %}</h1>
+       </div>
+       <div class="card-body">
+
+       <form enctype="multipart/form-data" method="POST">
+    {% csrf_token %}
+    <div class='editable'>
+           <p>{% trans "Insert empty chunk after" %}:
+           <a href="{{ chunk.get_absolute_url }}">{{ chunk.pretty_name }}</a></p>
+        {% bootstrap_form form %}
+       {% buttons %}
+        <button class="btn btn-primary" type="submit">{% trans "Add chunk" %}</button>
+       {% endbuttons %}
+    </div>
+       </form>
+       </div>
+</div>
+{% endblock content %}
diff --git a/src/documents/templates/documents/chunk_edit.html b/src/documents/templates/documents/chunk_edit.html
new file mode 100644 (file)
index 0000000..f68787b
--- /dev/null
@@ -0,0 +1,33 @@
+{% extends "documents/base.html" %}
+{% load i18n %}
+{% load bootstrap4 %}
+
+
+{% block titleextra %}{% trans "Chunk settings" %}{% endblock %}
+
+
+{% block content %}
+<div class="card">
+       <div class="card-header">
+    <h1>{% trans "Chunk settings" %}</h1>
+       </div>
+       <div class="card-body">
+
+       <form enctype="multipart/form-data" method="POST" action="{% if go_next %}?next={{ go_next }}{% endif %}">
+    {% csrf_token %}
+    <div class='editable'>
+           <p>{% trans "Book" %}: {{ chunk.book }} ({{ chunk.number }}/{{ chunk.book|length }})</p>
+        {% bootstrap_form form %}
+       {% buttons %}
+               <button class="btn btn-primary" type="submit">{% trans "Save" %}</button>
+               {% endbuttons %}
+    </div>
+
+       </form>
+
+
+    <p style="text-align: right"><a class="btn btn-danger" href="{% url "documents_chunk_add" chunk.book.slug chunk.slug %}">{% trans "Split chunk" %}</a></p>
+   </div>
+</div>
+
+{% endblock content %}
diff --git a/src/documents/templates/documents/document_create_missing.html b/src/documents/templates/documents/document_create_missing.html
new file mode 100644 (file)
index 0000000..d003394
--- /dev/null
@@ -0,0 +1,26 @@
+{% extends "documents/base.html" %}
+{% load i18n %}
+{% load bootstrap4 %}
+
+
+{% block titleextra %}{% trans "Create a new book" %}{% endblock %}
+
+
+{% block content %}
+<div class="card">
+       <div class="card-header">
+    <h1>{% trans "Create a new book" %}</h1>
+       </div>
+       <div class="card-body">
+
+
+    <form enctype="multipart/form-data" method="POST">
+    {% csrf_token %}
+        {% bootstrap_form form %}
+       {% buttons %}
+        <button class="btn btn-primary" type="submit">{% trans "Create book" %}</button>
+       {% endbuttons %}
+    </form>
+</div>
+</div>
+{% endblock content %}
diff --git a/src/documents/templates/documents/document_list.html b/src/documents/templates/documents/document_list.html
new file mode 100644 (file)
index 0000000..633f8f4
--- /dev/null
@@ -0,0 +1,22 @@
+{% extends "documents/base.html" %}
+
+{% load i18n %}
+{% load documents book_list %}
+{% load pipeline %}
+
+{% block titleextra %}{% trans "Book list" %}{% endblock %}
+
+
+{% block add_js %}
+  {% javascript 'book_list' %}
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js"></script>
+{% endblock %}
+
+{% block add_css %}
+  {% stylesheet 'book_list' %}
+  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css">
+{% endblock %}
+
+{% block content %}
+    {% book_list %}
+{% endblock content %}
diff --git a/src/documents/templates/documents/document_upload.html b/src/documents/templates/documents/document_upload.html
new file mode 100644 (file)
index 0000000..60972b3
--- /dev/null
@@ -0,0 +1,78 @@
+{% extends "documents/base.html" %}
+{% load i18n %}
+{% load bootstrap4 %}
+
+
+{% block titleextra %}{% trans "Bulk document upload" %}{% endblock %}
+
+
+{% block content %}
+
+
+<div class="card">
+       <div class="card-header">
+<h2>{% trans "Bulk documents upload" %}</h2>
+       </div>
+       <div class="card-body">
+
+<p>
+{% trans "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with <code>.xml</code> will be ignored." %}
+</p>
+
+<form enctype="multipart/form-data" method="POST" action="{{ request.path }}">
+{% csrf_token %}
+{% bootstrap_form form %}
+{% buttons %}
+<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
+{% endbuttons %}
+</form>
+
+
+{% if error_list %}
+    <hr>
+
+    <p class='error'>{% trans "There have been some errors. No files have been added to the repository." %}
+    <h3>{% trans "Offending files" %}</h3>
+    <ul id='error-list'>
+        {% for filename, title, error in error_list %}
+            <li>{{ title }} (<code>{{ filename }}</code>): {{ error }}</li>
+        {% endfor %}
+    </ul>
+
+    {% if ok_list %}
+    <h3>{% trans "Correct files" %}</h3>
+        <ul>
+            {% for filename, slug, title in ok_list %}
+                <li>{{ title }} (<code>{{ filename }}</code>)</li>
+            {% endfor %}
+        </ul>
+    {% endif %}
+
+{% else %}
+
+    {% if ok_list %}
+        <p class='success'>{% trans "Files have been successfully uploaded to the repository." %}</p>
+        <h3>{% trans "Uploaded files" %}</h3>
+        <ul id='ok-list'>
+        {% for filename, slug, title in ok_list %}
+            <li><a href='{% url "wiki_editor" slug %}'>{{ title }}</a> (<code>{{ filename }})</a></li>
+        {% endfor %}
+        </ul>
+    {% endif %}
+{% endif %}
+
+{% if skipped_list %}
+    <h3>{% trans "Skipped files" %}</h3>
+    <p>{% trans "Files skipped due to no <code>.xml</code> extension" %}</p>
+    <ul id='skipped-list'>
+        {% for filename in skipped_list %}
+            <li>{{ filename }}</li>
+        {% endfor %}
+    </ul>
+{% endif %}
+
+       </div>
+</div>
+
+{% endblock content %}
+
diff --git a/src/documents/templates/documents/image_detail.html b/src/documents/templates/documents/image_detail.html
new file mode 100644 (file)
index 0000000..2ea83e0
--- /dev/null
@@ -0,0 +1,85 @@
+{% extends "documents/base.html" %}
+{% load book_list i18n %}
+{% load bootstrap4 %}
+
+
+{% block titleextra %}{{ object.title }}{% endblock %}
+
+
+{% block content %}
+<div class="card mt-4">
+
+       <div class="card-header">
+<h1>{{ object.title }}</h1>
+       </div>
+       <div class="card-body">
+
+
+{% if editable %}<form method='POST'>{% csrf_token %}{% endif %}
+    {% bootstrap_form form %}
+    {% if editable %}
+    {% buttons %}
+        <button class="btn btn-primary" type="submit">{% trans "Save" %}</button>
+    {% endbuttons %}
+    {% endif %}
+</tbody></table>
+{% if editable %}</form>{% endif %}
+
+       </div>
+</div>
+
+
+<div class='card mt-4'>
+       <div class="card-header">
+    <h2>{% trans "Editor" %}</h2>
+       </div>
+       <div class="card-body">
+
+    <p><a class="btn btn-primary" href="{% url 'wiki_img_editor' object.slug %}">{% trans "Proceed to the editor." %}</a></p>
+</div>
+</div>
+
+
+
+<div class='card mt-4'>
+       <div class="card-header">
+
+<h2>{% trans "Publication" %}</h2>
+       </div>
+       <div class="card-body">
+
+<p>{% trans "Last published" %}: 
+    {% if object.last_published %}
+        {{ object.last_published }}
+    {% else %}
+        &mdash;
+    {% endif %}
+</p>
+
+{% if publishable %}
+    {% if user.is_authenticated %}
+        <!--
+        Angel photos:
+        Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
+        mira66 (http://www.flickr.com/photos/21804434@N02/) /
+        CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
+        -->
+        <form method="POST" action="{% url 'documents_publish_image' object.slug %}">{% csrf_token %}
+            <!--img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" /-->
+            <button id="publish-button" type="submit">
+                <span>{% trans "Publish" %}</span></button>
+            <!--img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" /-->
+            </form>
+    {% else %}
+        <a href="{% url 'cas_ng_login' %}">{% trans "Log in to publish." %}</a>
+    {% endif %}
+{% else %}
+    <p>{% trans "This book can't be published yet, because:" %}</p>
+    <ul><li>{{ publishable_error }}</li></ul>
+{% endif %}
+
+</div>
+</div>
+
+
+{% endblock content %}
diff --git a/src/documents/templates/documents/image_list.html b/src/documents/templates/documents/image_list.html
new file mode 100644 (file)
index 0000000..a33921a
--- /dev/null
@@ -0,0 +1,24 @@
+{% extends "documents/base.html" %}
+
+{% load i18n %}
+{% load documents book_list %}
+{% load pipeline %}
+
+
+{% block titleextra %}{% trans "Image list" %}{% endblock %}
+
+
+{% block add_js %}
+  {% javascript 'book_list' %}
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js"></script>
+{% endblock %}
+
+{% block add_css %}
+  {% stylesheet 'book_list' %}
+  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css">
+{% endblock %}
+
+
+{% block content %}
+    {% image_list %}
+{% endblock content %}
diff --git a/src/documents/templates/documents/image_short.html b/src/documents/templates/documents/image_short.html
new file mode 100644 (file)
index 0000000..c10c7f0
--- /dev/null
@@ -0,0 +1,27 @@
+{% load i18n %}
+{% load username from common_tags %}
+
+<tr class="table-sm">
+    <td><input type="checkbox" name="select_chunk" value="{{image.id}}"/></td>
+    <td><a href="{% url 'documents_image' image.slug %}" class="btn btn-sm btn-secondary" title="{% trans "Image settings" %}">&#x1f5bc;</a></td>
+    <td><a class="btn btn-primary" target="_blank"
+                href="{% url 'wiki_img_editor' image.slug %}">
+                {{ image.title }}</a></td>
+    <td>{% if image.stage %}
+        {{ image.stage }}
+    {% else %}–
+    {% endif %}</td>
+    <td class='user-column'>{% if image.user %}<a href="{% url 'documents_user' image.user.username %}">{{ image.user|username }}</a>{% endif %}</td>
+    <td>
+      {% if image.published %}
+        <span class="badge badge-info" title="{% trans "published" %}">opubl.</span>
+      {% endif %}
+      {% if image.new_publishable %}
+        <span class="badge badge-primary" title="{% trans "publishable" %}">do publ.</span>
+      {% endif %}
+      {% if image.changed %}
+        <span class="badge badge-warning title="{% trans "changed" %}">zmiany</span>
+      {% endif %}
+    </td>
+    <td>{{ image.project.name }}</td>
+</tr>
diff --git a/src/documents/templates/documents/image_table.html b/src/documents/templates/documents/image_table.html
new file mode 100644 (file)
index 0000000..77a7188
--- /dev/null
@@ -0,0 +1,102 @@
+{% load i18n %}
+{% load pagination_tags %}
+{% load username from common_tags %}
+
+<div class="card">
+       <div class="card-body">
+
+
+<form name='filter' action='{{ request.path }}'>
+<input type='hidden' name="title" value="{{ request.GET.title }}" />
+<input type='hidden' name="stage" value="{{ request.GET.stage }}" />
+{% if not viewed_user %}
+    <input type='hidden' name="user" value="{{ request.GET.user }}" />
+{% endif %}
+<input type='hidden' name="status" value="{{ request.GET.status }}" />
+<input type='hidden' name="project" value="{{ request.GET.project }}" />
+</form>
+
+<table id="file-list" class="table {% if viewed_user %}book-list-user{% endif %}">
+    <thead><tr>
+        <th></th>
+        <th></th>
+        <th class='book-search-column'>
+            <form>
+            <input title='{% trans "Search in book titles" %}' name="title"
+                class='form-control text-filter' value="{{ request.GET.title }}" />
+            </form>
+        </th>
+        <th><select name="stage" class="form-control filter">
+            <option value=''>- {% trans "stage" %} -</option>
+            <option {% if request.GET.stage == '-' %}selected="selected"
+                    {% endif %}value="-">- {% trans "none" %} -</option>
+            {% for stage in stages %}
+                <option {% if request.GET.stage == stage.slug %}selected="selected"
+                    {% endif %}value="{{ stage.slug }}">{{ stage.name }}</option>
+            {% endfor %}
+        </select></th>
+
+        {% if not viewed_user %}
+            <th><select name="user" class="form-control filter">
+                <option value=''>- {% trans "editor" %} -</option>
+                <option {% if request.GET.user == '-' %}selected="selected"
+                        {% endif %}value="-">- {% trans "none" %} -</option>
+                {% for user in users %}
+                    <option {% if request.GET.user == user.username %}selected="selected"
+                        {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
+                {% endfor %}
+            </select></th>
+        {% endif %}
+
+        <th><select name="status" class="form-control filter">
+            <option value=''>- {% trans "status" %} -</option>
+            {% for state, label in states %}
+                <option {% if request.GET.status == state %}selected="selected"
+                        {% endif %}value='{{ state }}'>{{ label }}</option>
+            {% endfor %}
+        </select></th>
+
+        <th><select name="project" class="form-control filter">
+            <option value=''>- {% trans "project" %} -</option>
+                <option {% if request.GET.project == '-' %}selected="selected"
+                        {% endif %}value="-">- {% trans "none" %} -</option>
+            {% for project in projects %}
+                <option {% if request.GET.project == project.pk|slugify %}selected="selected"
+                        {% endif %}value='{{ project.pk }}'>{{ project.name }}</option>
+            {% endfor %}
+        </select></th>
+
+    </tr></thead>
+
+    {% autopaginate objects 100 as objects_page %}
+    <tbody>
+    {% for image in objects_page %}
+        {% include 'documents/image_short.html' %}
+    {% endfor %}
+    </tbody>
+</table>
+    {% paginate %}
+    {% blocktrans count c=objects|length %}{{c}} image{% plural %}{{c}} images{% endblocktrans %}</th></tr>
+{% if not objects %}
+    <p>{% trans "No images found." %}</p>
+{% endif %}
+
+<form id='chunk_mass_edit' action='{% url "documents_image_mass_edit" %}' style="display:none;">
+{% csrf_token %}
+<input type="hidden" name="ids" />
+<label for="mass_edit_stage">{% trans "Set stage" %}</label><input type="hidden" name="stage" id="mass_edit_stage"/>
+<label for="mass_edit_user">{% trans "Set user" %}</label><input type="hidden" name="user" id="mass_edit_stage" />
+<input type="hidden" name="status" />
+<label for="mass_edit_project">{% trans "Project" %}</label><input type="hidden" name="project" id="mass_edit_project" />
+<label for="mass_edit_more_users">{% trans "More users" %}</label>
+</form>
+
+<select name="other-user" style="display:none;">
+  {% for user in other_users %}
+  <option {% if request.GET.user == user.username %}selected="selected"
+          {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
+  {% endfor %}
+</select>
+
+       </div>
+</div>
diff --git a/src/documents/templates/documents/main_tabs.html b/src/documents/templates/documents/main_tabs.html
new file mode 100644 (file)
index 0000000..eb74bbe
--- /dev/null
@@ -0,0 +1,3 @@
+{% for tab in tabs %}
+    <li class="nav-item"><a class="nav-link{% if active_tab == tab.slug %} active{% endif %}" href="{{ tab.url }}">{{ tab.caption }}</a></li>
+{% endfor %}
diff --git a/src/documents/templates/documents/mark_final.html b/src/documents/templates/documents/mark_final.html
new file mode 100644 (file)
index 0000000..3e71978
--- /dev/null
@@ -0,0 +1,16 @@
+{% extends "documents/base.html" %}
+
+{% block titleextra %}Oznacz książki{% endblock %}
+
+
+{% block leftcolumn %}
+
+<h1>Oznacz książki</h1>
+
+<form method="post" action="">
+    {% csrf_token %}
+    {{ form.as_p }}
+    <input type="submit" value="Oznacz">
+</form>
+
+{% endblock leftcolumn %}
diff --git a/src/documents/templates/documents/mark_final_completed.html b/src/documents/templates/documents/mark_final_completed.html
new file mode 100644 (file)
index 0000000..b05637a
--- /dev/null
@@ -0,0 +1,12 @@
+{% extends "documents/base.html" %}
+
+{% block titleextra %}Oznaczono książki{% endblock %}
+
+
+{% block leftcolumn %}
+
+<h1>Oznaczono książki</h1>
+
+<p>Książki zostały oznaczone.</p>
+
+{% endblock leftcolumn %}
diff --git a/src/documents/templates/documents/my_page.html b/src/documents/templates/documents/my_page.html
new file mode 100644 (file)
index 0000000..6828706
--- /dev/null
@@ -0,0 +1,52 @@
+{% extends "documents/base.html" %}
+
+{% load i18n %}
+{% load documents book_list wall %}
+{% load pipeline %}
+
+{% block add_js %}
+  {% javascript 'book_list' %}
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js"></script>
+{% endblock %}
+
+{% block add_css %}
+  {% stylesheet 'book_list' %}
+  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css">
+{% endblock %}
+
+{% block titleextra %}{% trans "My page" %}{% endblock %}
+
+
+{% block leftcolumn %}
+    {% book_list request.user %}
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+<div class="card">
+       <div class="card-header">
+        <h2>{% trans "Your last edited documents" %}</h2>
+       </div>
+       <div class="card-body">
+        <ol>
+            {% for edit_url, item in last_books %}
+                <li><a
+                {% if edit_url|length == 2 %}
+                    {# Temporary support for old-style last_books. #}
+                    href="{% url 'wiki_editor' edit_url.0 edit_url.1 %}"
+                {% else %}
+                    href="{{ edit_url }}"
+                {% endif %}
+                target="_blank">{{ item.title }}</a><br/><span class="date">({{ item.time|date:"H:i:s, d/m/Y" }})</span></li>
+            {% endfor %}
+        </ol>
+    </div>
+</div>
+
+<div class="card mt-4">
+       <div class="card-header">
+    <h2>{% trans "Recent activity for" %} {{ request.user|nice_name }}</h2>
+       </div>
+       <div class="card-body">
+    {% wall request.user 10 %}
+       </div></div>
+{% endblock rightcolumn %}
diff --git a/src/documents/templates/documents/upload_pdf.html b/src/documents/templates/documents/upload_pdf.html
new file mode 100644 (file)
index 0000000..ea74e92
--- /dev/null
@@ -0,0 +1,20 @@
+{% extends "documents/base.html" %}
+{% load i18n %}
+
+
+{% block titleextra %}{% trans "PDF file upload" %}{% endblock %}
+
+
+{% block content %}
+
+
+<h2>{% trans "PDF file upload" %}</h2>
+
+<form enctype="multipart/form-data" method="POST" action="">
+{% csrf_token %}
+{{ form.as_p }}
+<p><button type="submit">{% trans "Upload" %}</button></p>
+</form>
+
+
+{% endblock content %}
diff --git a/src/documents/templates/documents/user_list.html b/src/documents/templates/documents/user_list.html
new file mode 100644 (file)
index 0000000..f0e9ab2
--- /dev/null
@@ -0,0 +1,30 @@
+{% extends "documents/base.html" %}
+
+{% load i18n %}
+{% load username from common_tags %}
+
+
+{% block titleextra %}{% trans "Users" %}{% endblock %}
+
+
+{% block content %}
+<div class="card">
+       <div class="card-header">
+
+<h1>{% trans "Users" %}</h1>
+       </div>
+<div class="card-body">
+
+<ul>
+{% for user in users %}
+    <li><a href="{% url 'documents_user' user.username %}">
+        <span class="chunkno">{{ forloop.counter }}.</span>
+        {{ user|username }}</a>
+        ({{ user.count }})</li>
+{% endfor %}
+</ul>
+
+</div>
+</div>
+
+{% endblock content %}
diff --git a/src/documents/templates/documents/user_page.html b/src/documents/templates/documents/user_page.html
new file mode 100644 (file)
index 0000000..3be2c57
--- /dev/null
@@ -0,0 +1,25 @@
+{% extends "documents/base.html" %}
+
+{% load i18n %}
+{% load documents book_list wall %}
+
+
+{% block titleextra %}{{ viewed_user|nice_name }}{% endblock %}
+
+
+{% block leftcolumn %}
+    <h1 class="mb-4">{{ viewed_user|nice_name }}</h1>
+    {% book_list viewed_user %}
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+<div class="card">
+       <div class="card-header">
+    <h2>{% trans "Recent activity for" %} {{ viewed_user|nice_name }}</h2>
+       </div>
+       <div class="card-body">
+               
+    {% wall viewed_user 10 %}
+       </div>
+</div>
+{% endblock rightcolumn %}
diff --git a/src/documents/templates/documents/wall.html b/src/documents/templates/documents/wall.html
new file mode 100644 (file)
index 0000000..5e2ed9a
--- /dev/null
@@ -0,0 +1,37 @@
+{% load i18n %}
+{% load gravatar %}
+{% load email %}
+{% load username from common_tags %}
+
+<ul class='wall'>
+{% for item in wall %}
+    <li class="{{ item.tag }}{% if not item.user %} anonymous{% endif %}">
+        <div class='gravatar'>
+            {% if item.get_email %}
+                <img src="{% gravatar_url item.get_email 32 %}"
+                    height="32" width="32" alt='Avatar' />
+                <br/>
+            {% endif %}
+        </div>
+
+        <div class="time">{{ item.timestamp }}</div>
+        <h3>{{ item.header }}</h3>
+        <a target="_blank" href='{{ item.url }}'>{{ item.title }}</a>
+        <br/><strong>{% trans "user" %}:</strong>
+        {% if item.user %}
+            <a href="{% url 'documents_user' item.user.username %}">
+            {{ item.user|username }}</a>
+            &lt;{{ item.user.email|email_link }}>
+        {% else %}
+            {{ item.user_name }}
+            {% if item.email %}
+                &lt;{{ item.email|email_link }}>
+            {% endif %}
+            ({% trans "not logged in" %})
+        {% endif %}
+        <br/>{{ item.summary|linebreaksbr }}
+    </li>
+{% empty %}
+    <li>{% trans "No activity recorded." %}</li>
+{% endfor %}
+</ul>
diff --git a/src/documents/templatetags/__init__.py b/src/documents/templatetags/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/documents/templatetags/book_list.py b/src/documents/templatetags/book_list.py
new file mode 100644 (file)
index 0000000..acd2208
--- /dev/null
@@ -0,0 +1,205 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from re import split
+from django.db.models import Q, Count
+from django import template
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth.models import User
+from documents.models import Chunk, Image, Project
+
+register = template.Library()
+
+
+class ChunksList(object):
+    def __init__(self, chunk_qs):
+        self.chunk_qs = chunk_qs.select_related('book', 'book__project', 'stage', 'user')
+        self.book_qs = chunk_qs.values('book_id')
+
+    def __getitem__(self, key):
+        if isinstance(key, slice):
+            return self.get_slice(key)
+        elif isinstance(key, int):
+            return self.get_slice(slice(key, key+1))[0]
+        else:
+            raise TypeError('Unsupported list index. Must be a slice or an int.')
+
+    def __len__(self):
+        return self.book_qs.count()
+
+    def get_slice(self, slice_):
+        book_ids = [x['book_id'] for x in self.book_qs[slice_]]
+        chunk_qs = self.chunk_qs.filter(book__in=book_ids)
+
+        chunks_list = []
+        book = None
+        for chunk in chunk_qs:
+            if chunk.book != book:
+                book = chunk.book
+                chunks_list.append(ChoiceChunks(book, [chunk]))
+            else:
+                chunks_list[-1].chunks.append(chunk)
+        return chunks_list
+
+
+class ChoiceChunks(object):
+    """
+        Associates the given chunks iterable for a book.
+    """
+
+    chunks = None
+
+    def __init__(self, book, chunks):
+        self.book = book
+        self.chunks = chunks
+
+
+def foreign_filter(qs, value, filter_field, model, model_field='slug', unset='-'):
+    if value == unset:
+        return qs.filter(**{filter_field: None})
+    if not value:
+        return qs
+    try:
+        obj = model._default_manager.get(**{model_field: value})
+    except model.DoesNotExist:
+        return qs.none()
+    else:
+        return qs.filter(**{filter_field: obj})
+
+
+def search_filter(qs, value, filter_fields):
+    if not value:
+        return qs
+    q = Q(**{"%s__icontains" % filter_fields[0]: value})
+    for field in filter_fields[1:]:
+        q |= Q(**{"%s__icontains" % field: value})
+    return qs.filter(q)
+
+
+_states = [
+        ('publishable', _('publishable'), Q(book___new_publishable=True)),
+        ('changed', _('changed'), Q(_changed=True)),
+        ('published', _('published'), Q(book___published=True)),
+        ('unpublished', _('unpublished'), Q(book___published=False)),
+        ('empty', _('empty'), Q(head=None)),
+    ]
+_states_options = [s[:2] for s in _states]
+_states_dict = dict([(s[0], s[2]) for s in _states])
+
+
+def document_list_filter(request, **kwargs):
+
+    def arg_or_GET(field):
+        return kwargs.get(field, request.GET.get(field))
+
+    if arg_or_GET('all'):
+        chunks = Chunk.objects.all()
+    else:
+        chunks = Chunk.visible_objects.all()
+
+    chunks = chunks.order_by('book__title', 'book', 'number')
+
+    if not request.user.is_authenticated:
+        chunks = chunks.filter(book__public=True)
+
+    state = arg_or_GET('status')
+    if state in _states_dict:
+        chunks = chunks.filter(_states_dict[state])
+
+    chunks = foreign_filter(chunks, arg_or_GET('user'), 'user', User, 'username')
+    chunks = foreign_filter(chunks, arg_or_GET('stage'), 'stage', Chunk.tag_model, 'slug')
+    chunks = search_filter(chunks, arg_or_GET('title'), ['book__title', 'title'])
+    chunks = foreign_filter(chunks, arg_or_GET('project'), 'book__project', Project, 'pk')
+    return chunks
+
+
+@register.inclusion_tag('documents/book_list/book_list.html', takes_context=True)
+def book_list(context, user=None):
+    request = context['request']
+
+    if user:
+        filters = {"user": user}
+        new_context = {"viewed_user": user}
+    else:
+        filters = {}
+        new_context = {
+            "users": User.objects.annotate(
+                count=Count('chunk')).filter(count__gt=0).order_by(
+                '-count', 'last_name', 'first_name'),
+            "other_users": User.objects.annotate(
+                count=Count('chunk')).filter(count=0).order_by(
+                'last_name', 'first_name'),
+                }
+
+    new_context.update({
+        "filters": True,
+        "request": request,
+        "books": ChunksList(document_list_filter(request, **filters)),
+        "stages": Chunk.tag_model.objects.all(),
+        "states": _states_options,
+        "projects": Project.objects.all(),
+    })
+
+    return new_context
+
+
+
+_image_states = [
+        ('publishable', _('publishable'), Q(_new_publishable=True)),
+        ('changed', _('changed'), Q(_changed=True)),
+        ('published', _('published'), Q(_published=True)),
+        ('unpublished', _('unpublished'), Q(_published=False)),
+        ('empty', _('empty'), Q(head=None)),
+    ]
+_image_states_options = [s[:2] for s in _image_states]
+_image_states_dict = dict([(s[0], s[2]) for s in _image_states])
+
+def image_list_filter(request, **kwargs):
+
+    def arg_or_GET(field):
+        return kwargs.get(field, request.GET.get(field))
+
+    images = Image.objects.all().select_related('user', 'stage', 'project')
+
+    if not request.user.is_authenticated:
+        images = images.filter(public=True)
+
+    state = arg_or_GET('status')
+    if state in _image_states_dict:
+        images = images.filter(_image_states_dict[state])
+
+    images = foreign_filter(images, arg_or_GET('user'), 'user', User, 'username')
+    images = foreign_filter(images, arg_or_GET('stage'), 'stage', Image.tag_model, 'slug')
+    images = search_filter(images, arg_or_GET('title'), ['title', 'title'])
+    images = foreign_filter(images, arg_or_GET('project'), 'project', Project, 'pk')
+    return images
+
+
+@register.inclusion_tag('documents/image_table.html', takes_context=True)
+def image_list(context, user=None):
+    request = context['request']
+
+    if user:
+        filters = {"user": user}
+        new_context = {"viewed_user": user}
+    else:
+        filters = {}
+        new_context = {
+            "users": User.objects.annotate(
+                count=Count('image')).filter(count__gt=0).order_by(
+                '-count', 'last_name', 'first_name'),
+            "other_users": User.objects.annotate(
+                count=Count('image')).filter(count=0).order_by(
+                'last_name', 'first_name'),
+                }
+
+    new_context.update({
+        "filters": True,
+        "request": request,
+        "objects": image_list_filter(request, **filters),
+        "stages": Image.tag_model.objects.all(),
+        "states": _image_states_options,
+        "projects": Project.objects.all(),
+    })
+
+    return new_context
diff --git a/src/documents/templatetags/common_tags.py b/src/documents/templatetags/common_tags.py
new file mode 100644 (file)
index 0000000..5544232
--- /dev/null
@@ -0,0 +1,9 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django import template
+register = template.Library()
+
+@register.filter
+def username(user):
+    return ("%s %s" % (user.first_name, user.last_name)).lstrip() or user.username
diff --git a/src/documents/templatetags/documents.py b/src/documents/templatetags/documents.py
new file mode 100644 (file)
index 0000000..5c3b1eb
--- /dev/null
@@ -0,0 +1,47 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.urls import reverse
+from django import template
+from django.utils.translation import ugettext as _
+
+register = template.Library()
+
+
+class Tab(object):
+    slug = None
+    caption = None
+    url = None
+
+    def __init__(self, slug, caption, url):
+        self.slug = slug
+        self.caption = caption
+        self.url = url
+
+
+@register.inclusion_tag("documents/main_tabs.html", takes_context=True)
+def main_tabs(context):
+    active = getattr(context['request'], 'documents_active_tab', None)
+
+    tabs = []
+    user = context['user']
+    tabs.append(Tab('my', _('My page'), reverse("documents_user")))
+
+    tabs.append(Tab('activity', _('Activity'), reverse("documents_activity")))
+    tabs.append(Tab('all', _('All'), reverse("documents_document_list")))
+    tabs.append(Tab('images', _('Images'), reverse("documents_image_list")))
+    tabs.append(Tab('users', _('Users'), reverse("documents_users")))
+
+    if user.has_perm('documents.add_book'):
+        tabs.append(Tab('create', _('Add'), reverse("documents_create_missing")))
+        tabs.append(Tab('upload', _('Upload'), reverse("documents_upload")))
+
+    tabs.append(Tab('cover', _('Covers'), reverse("cover_image_list")))
+
+    return {"tabs": tabs, "active_tab": active}
+
+
+@register.filter
+def nice_name(user):
+    return user.get_full_name() or user.username
+
diff --git a/src/documents/templatetags/set_get_parameter.py b/src/documents/templatetags/set_get_parameter.py
new file mode 100644 (file)
index 0000000..36e2245
--- /dev/null
@@ -0,0 +1,49 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from re import split
+
+from django import template
+
+register = template.Library()
+
+
+"""
+In template:
+    {% set_get_paramater param1='const_value',param2=,param3=variable %}
+results with changes to query string:
+    param1 is set to `const_value' string
+    param2 is unset, if exists,
+    param3 is set to the value of variable in context
+
+Using 'django.core.context_processors.request' is required.
+
+"""
+
+
+class SetGetParameter(template.Node):
+    def __init__(self, values):
+        self.values = values
+        
+    def render(self, context):
+        request = template.Variable('request').resolve(context)
+        params = request.GET.copy()
+        for key, value in self.values.items():
+            if value == '':
+                if key in params:
+                    del(params[key])
+            else:
+                params[key] = template.Variable(value).resolve(context)
+        return '?%s' %  params.urlencode()
+
+
+@register.tag
+def set_get_parameter(parser, token):
+    parts = split(r'\s+', token.contents, 2)
+
+    values = {}
+    for pair in parts[1].split(','):
+        s = pair.split('=')
+        values[s[0]] = s[1]
+
+    return SetGetParameter(values)
diff --git a/src/documents/templatetags/wall.py b/src/documents/templatetags/wall.py
new file mode 100644 (file)
index 0000000..4a6795b
--- /dev/null
@@ -0,0 +1,184 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from datetime import timedelta
+from django.db.models import Q
+from django.urls import reverse
+from django import template
+from django.utils.translation import ugettext as _
+
+from documents.models import Chunk, BookPublishRecord, Image, ImagePublishRecord
+
+register = template.Library()
+
+
+class WallItem(object):
+    title = ''
+    summary = ''
+    url = ''
+    timestamp = ''
+    user = None
+    user_name = ''
+    email = ''
+
+    def __init__(self, tag):
+        self.tag = tag
+
+    def get_email(self):
+        if self.user:
+            return self.user.email
+        else:
+            return self.email
+
+
+def changes_wall(user=None, max_len=None, day=None):
+    qs = Chunk.change_model.objects.order_by('-created_at')
+    qs = qs.select_related('author', 'tree', 'tree__book')
+    if user is not None:
+        qs = qs.filter(Q(author=user) | Q(tree__user=user))
+    if max_len is not None:
+        qs = qs[:max_len]
+    if day is not None:
+        next_day = day + timedelta(1)
+        qs = qs.filter(created_at__gte=day, created_at__lt=next_day)
+    for item in qs:
+        tag = 'stage' if item.tags.count() else 'change'
+        chunk = item.tree
+        w = WallItem(tag)
+        if user and item.author != user:
+            w.header = _('Related edit')
+        else:
+            w.header = _('Edit')
+        w.title = chunk.pretty_name()
+        w.summary = item.description
+        w.url = reverse('wiki_editor', 
+                args=[chunk.book.slug, chunk.slug]) + '?diff=%d' % item.revision
+        w.timestamp = item.created_at
+        w.user = item.author
+        w.user_name = item.author_name
+        w.email = item.author_email
+        yield w
+
+
+def image_changes_wall(user=None, max_len=None, day=None):
+    qs = Image.change_model.objects.order_by('-created_at')
+    qs = qs.select_related('author', 'tree')
+    if user is not None:
+        qs = qs.filter(Q(author=user) | Q(tree__user=user))
+    if max_len is not None:
+        qs = qs[:max_len]
+    if day is not None:
+        next_day = day + timedelta(1)
+        qs = qs.filter(created_at__gte=day, created_at__lt=next_day)
+    for item in qs:
+        tag = 'stage' if item.tags.count() else 'change'
+        image = item.tree
+        w  = WallItem(tag)
+        if user and item.author != user:
+            w.header = _('Related edit')
+        else:
+            w.header = _('Edit')
+        w.title = image.title
+        w.summary = item.description
+        w.url = reverse('wiki_img_editor', 
+                args=[image.slug]) + '?diff=%d' % item.revision
+        w.timestamp = item.created_at
+        w.user = item.author
+        w.user_name = item.author_name
+        w.email = item.author_email
+        yield w
+
+
+
+# TODO: marked for publishing
+
+
+def published_wall(user=None, max_len=None, day=None):
+    qs = BookPublishRecord.objects.select_related('book')
+    if user:
+        # TODO: published my book
+        qs = qs.filter(Q(user=user))
+    if max_len is not None:
+        qs = qs[:max_len]
+    if day is not None:
+        next_day = day + timedelta(1)
+        qs = qs.filter(timestamp__gte=day, timestamp__lt=next_day)
+    for item in qs:
+        w = WallItem('publish')
+        w.header = _('Publication')
+        w.title = item.book.title
+        w.timestamp = item.timestamp
+        w.url = item.book.get_absolute_url()
+        w.user = item.user
+        w.email = item.user.email
+        yield w
+
+
+def image_published_wall(user=None, max_len=None, day=None):
+    qs = ImagePublishRecord.objects.select_related('image')
+    if user:
+        # TODO: published my book
+        qs = qs.filter(Q(user=user))
+    if max_len is not None:
+        qs = qs[:max_len]
+    if day is not None:
+        next_day = day + timedelta(1)
+        qs = qs.filter(timestamp__gte=day, timestamp__lt=next_day)
+    for item in qs:
+        w = WallItem('publish')
+        w.header = _('Publication')
+        w.title = item.image.title
+        w.timestamp = item.timestamp
+        w.url = item.image.get_absolute_url()
+        w.user = item.user
+        w.email = item.user.email
+        yield w
+
+
+def big_wall(walls, max_len=None):
+    """
+        Takes some WallItem iterators and zips them into one big wall.
+        Input iterators must already be sorted by timestamp.
+    """
+    subwalls = []
+    for w in walls:
+        try:
+            subwalls.append([next(w), w])
+        except StopIteration:
+            pass
+
+    if max_len is None:
+        max_len = -1
+    while max_len and subwalls:
+        i, next_item = max(enumerate(subwalls), key=lambda x: x[1][0].timestamp)
+        yield next_item[0]
+        max_len -= 1
+        try:
+            next_item[0] = next(next_item[1])
+        except StopIteration:
+            del subwalls[i]
+
+
+@register.inclusion_tag("documents/wall.html", takes_context=True)
+def wall(context, user=None, max_len=100):
+    return {
+        "request": context['request'],
+        "STATIC_URL": context['STATIC_URL'],
+        "wall": big_wall([
+            changes_wall(user, max_len),
+            published_wall(user, max_len),
+            image_changes_wall(user, max_len),
+            image_published_wall(user, max_len),
+        ], max_len)}
+
+@register.inclusion_tag("documents/wall.html", takes_context=True)
+def day_wall(context, day):
+    return {
+        "request": context['request'],
+        "STATIC_URL": context['STATIC_URL'],
+        "wall": big_wall([
+            changes_wall(day=day),
+            published_wall(day=day),
+            image_changes_wall(day=day),
+            image_published_wall(day=day),
+        ])}
diff --git a/src/documents/test_utils.py b/src/documents/test_utils.py
new file mode 100644 (file)
index 0000000..7343d5c
--- /dev/null
@@ -0,0 +1,12 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+"""Testing utilities."""
+
+from os.path import abspath, dirname, join
+
+
+def get_fixture(path):
+    f_path = join(dirname(abspath(__file__)), 'tests/files', path)
+    with open(f_path) as f:
+        return f.read()
diff --git a/src/documents/tests/__init__.py b/src/documents/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/documents/tests/files/chunk1.xml b/src/documents/tests/files/chunk1.xml
new file mode 100644 (file)
index 0000000..6a75580
--- /dev/null
@@ -0,0 +1,42 @@
+<utwor>
+  <liryka_l>
+
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description rdf:about="http://example.com/documents/book/test-book/">
+<dc:creator xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam</dc:creator>
+<dc:title xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Do M***</dc:title>
+<dc:publisher xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Fundacja Nowoczesna Polska</dc:publisher>
+<dc:subject.period xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Romantyzm</dc:subject.period>
+<dc:subject.type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Liryka</dc:subject.type>
+<dc:subject.genre xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Wiersz</dc:subject.genre>
+<!--dc:description xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Publikacja zrealizowana w ramach projektu Wolne Lektury (http://wolnelektury.pl). Reprodukcja cyfrowa wykonana przez Bibliotekę Narodową z egzemplarza pochodzącego ze zbiorów BN.</dc:description-->
+<dc:identifier.url xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m</dc:identifier.url>
+<dc:source.URL xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://www.polona.pl/Content/2222</dc:source.URL>
+<dc:source xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam (1798-1855), Poezje, tom 1 (Wiersze młodzieńcze - Ballady i romanse - Wiersze do r. 1824), Krakowska Spółdzielnia Wydawnicza, wyd. 2 zwiększone, Kraków, 1922</dc:source>
+
+<dc:rights xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Domena publiczna - Adam Mickiewicz zm. 1855</dc:rights>
+<dc:date.pd xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">1926</dc:date.pd>
+<dc:format xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">xml</dc:format>
+<dc:type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
+<dc:type xml:lang="en" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
+<dc:date xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">2007-09-06</dc:date>
+<dc:language xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">pol</dc:language>
+
+</rdf:Description>
+</rdf:RDF>
+
+<autor_utworu>Adam Mickiewicz</autor_utworu>
+<dzielo_nadrzedne>Sonety odeskie</dzielo_nadrzedne>
+<nazwa_utworu>Do M***</nazwa_utworu>
+
+<nota><akap>Wiérsz napisany w roku 1822</akap></nota>
+
+
+<strofa>Precz z moich oczu!... posłucham od razu,/
+Precz z mego serca!... i serce posłucha,/
+Precz z méj pamięci!... Nie! tego rozkazu/
+Moja i twoja pamięć nie posłucha.</strofa>
+
+<!-- TRIM_END -->
+</liryka_l>
+</utwor>
diff --git a/src/documents/tests/files/chunk2.xml b/src/documents/tests/files/chunk2.xml
new file mode 100644 (file)
index 0000000..63a243e
--- /dev/null
@@ -0,0 +1,11 @@
+<utwor><liryka_l>
+<!-- TRIM_BEGIN -->
+
+<strofa>Jak cień tém dłuższy, gdy padnie z daleka,/
+Tém szerzéj koło żałobne roztoczy,/
+Tak moja postać, im daléj ucieka,/
+Tém grubszym kirem twą pamięć pomroczy.</strofa>
+
+
+</liryka_l>
+</utwor>
diff --git a/src/documents/tests/files/expected.xml b/src/documents/tests/files/expected.xml
new file mode 100644 (file)
index 0000000..ff225a0
--- /dev/null
@@ -0,0 +1,49 @@
+<utwor>
+  <liryka_l>
+
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description rdf:about="http://example.com/documents/book/test-book/">
+<dc:creator xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam</dc:creator>
+<dc:title xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Do M***</dc:title>
+<dc:publisher xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Fundacja Nowoczesna Polska</dc:publisher>
+<dc:subject.period xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Romantyzm</dc:subject.period>
+<dc:subject.type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Liryka</dc:subject.type>
+<dc:subject.genre xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Wiersz</dc:subject.genre>
+<!--dc:description xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Publikacja zrealizowana w ramach projektu Wolne Lektury (http://wolnelektury.pl). Reprodukcja cyfrowa wykonana przez Bibliotekę Narodową z egzemplarza pochodzącego ze zbiorów BN.</dc:description-->
+<dc:identifier.url xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m</dc:identifier.url>
+<dc:source.URL xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://www.polona.pl/Content/2222</dc:source.URL>
+<dc:source xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam (1798-1855), Poezje, tom 1 (Wiersze młodzieńcze - Ballady i romanse - Wiersze do r. 1824), Krakowska Spółdzielnia Wydawnicza, wyd. 2 zwiększone, Kraków, 1922</dc:source>
+
+<dc:rights xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Domena publiczna - Adam Mickiewicz zm. 1855</dc:rights>
+<dc:date.pd xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">1926</dc:date.pd>
+<dc:format xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">xml</dc:format>
+<dc:type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
+<dc:type xml:lang="en" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
+<dc:date xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">2007-09-06</dc:date>
+<dc:language xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">pol</dc:language>
+
+</rdf:Description>
+</rdf:RDF>
+
+<autor_utworu>Adam Mickiewicz</autor_utworu>
+<dzielo_nadrzedne>Sonety odeskie</dzielo_nadrzedne>
+<nazwa_utworu>Do M***</nazwa_utworu>
+
+<nota><akap>Wiérsz napisany w roku 1822</akap></nota>
+
+
+<strofa>Precz z moich oczu!... posłucham od razu,/
+Precz z mego serca!... i serce posłucha,/
+Precz z méj pamięci!... Nie! tego rozkazu/
+Moja i twoja pamięć nie posłucha.</strofa>
+
+
+
+<strofa>Jak cień tém dłuższy, gdy padnie z daleka,/
+Tém szerzéj koło żałobne roztoczy,/
+Tak moja postać, im daléj ucieka,/
+Tém grubszym kirem twą pamięć pomroczy.</strofa>
+
+
+</liryka_l>
+</utwor>
diff --git a/src/documents/tests/test_book.py b/src/documents/tests/test_book.py
new file mode 100644 (file)
index 0000000..c423d9e
--- /dev/null
@@ -0,0 +1,52 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+"""Tests for manipulating books in the catalogue."""
+
+from django.test import TestCase
+from django.contrib.auth.models import User
+from documents.models import Book
+
+
+class ManipulationTests(TestCase):
+
+    def setUp(self):
+        self.user = User.objects.create(username='tester')
+        self.book1 = Book.create(self.user, 'book 1', slug='book1')
+        self.book2 = Book.create(self.user, 'book 2', slug='book2')
+
+    def test_append(self):
+        self.book1.append(self.book2)
+        self.assertEqual(Book.objects.all().count(), 1)
+        self.assertEqual(len(self.book1), 2)
+
+    def test_append_to_self(self):
+        with self.assertRaises(AssertionError):
+            self.book1.append(Book.objects.get(pk=self.book1.pk))
+        self.assertEqual(Book.objects.all().count(), 2)
+        self.assertEqual(len(self.book1), 1)
+
+    def test_prepend_history(self):
+        self.book1.prepend_history(self.book2)
+        self.assertEqual(Book.objects.all().count(), 1)
+        self.assertEqual(len(self.book1), 1)
+        self.assertEqual(self.book1.materialize(), 'book 1')
+
+    def test_prepend_history_to_self(self):
+        with self.assertRaises(AssertionError):
+            self.book1.prepend_history(self.book1)
+        self.assertEqual(Book.objects.all().count(), 2)
+        self.assertEqual(self.book1.materialize(), 'book 1')
+        self.assertEqual(self.book2.materialize(), 'book 2')
+
+    def test_split_book(self):
+        self.book1.chunk_set.create(number=2, title='Second chunk',
+                slug='book3')
+        self.book1[1].commit('I survived!')
+        self.assertEqual(len(self.book1), 2)
+        self.book1.split()
+        self.assertEqual(set([b.slug for b in Book.objects.all()]),
+                set(['book2', '1', 'book3']))
+        self.assertEqual(
+                Book.objects.get(slug='book3').materialize(),
+                'I survived!')
diff --git a/src/documents/tests/test_gallery.py b/src/documents/tests/test_gallery.py
new file mode 100644 (file)
index 0000000..ba75b95
--- /dev/null
@@ -0,0 +1,140 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+"""Tests for galleries of scans."""
+
+from os.path import join, basename, exists
+from os import makedirs, listdir
+from django.test import TestCase
+from django.contrib.auth.models import User
+from documents.models import Book
+from tempfile import mkdtemp
+from django.conf import settings
+
+
+class GalleryAppendTests(TestCase):
+    def setUp(self):
+        self.user = User.objects.create(username='tester')
+        self.book1 = Book.create(self.user, 'book 1', slug='book1')
+        self.book1.chunk_set.create(number=2, title='Second chunk',
+                slug='book1-2')
+        c=self.book1[1]
+        c.gallery_start=3
+        
+        self.scandir = join(settings.MEDIA_ROOT, settings.IMAGE_DIR)
+        if not exists(self.scandir):
+            makedirs(self.scandir)
+
+    def make_gallery(self, book, files):
+        d = mkdtemp('gallery', dir=self.scandir)
+        for named, cont in files.items():
+            f = open(join(d, named), 'w')
+            f.write(cont)
+            f.close()
+        book.gallery = basename(d)
+
+
+    def test_both_indexed(self):
+        self.book2 = Book.create(self.user, 'book 2', slug='book2')
+        self.book2.chunk_set.create(number=2, title='Second chunk of second book',
+                slug='book2-2')
+
+        c = self.book2[1]
+        c.gallery_start = 3
+        c.save()
+        
+        self.make_gallery(self.book1, {
+            '1-0001_1l' : 'aa',
+            '1-0001_2r' : 'bb',
+            '1-0002_1l' : 'cc',
+            '1-0002_2r' : 'dd',
+            })
+
+        self.make_gallery(self.book2, {
+            '1-0001_1l' : 'dd', # the same, should not be moved
+            '1-0001_2r' : 'ff',
+            '2-0002_1l' : 'gg',
+            '2-0002_2r' : 'hh',
+            })
+
+        self.book1.append(self.book2)
+
+        files = listdir(join(self.scandir, self.book1.gallery))
+        files.sort()
+        self.assertEqual(files, [
+            '1-0001_1l',
+            '1-0001_2r',
+            '1-0002_1l',
+            '1-0002_2r',
+            #            '2-0001_1l',
+            '2-0001_2r',
+            '3-0002_1l',
+            '3-0002_2r',
+            ])        
+
+        self.assertEqual((4, 6), (self.book1[2].gallery_start, self.book1[3].gallery_start))
+        
+        
+    def test_none_indexed(self):
+        self.book2 = Book.create(self.user, 'book 2', slug='book2')
+        self.make_gallery(self.book1, {
+            '0001_1l' : 'aa',
+            '0001_2r' : 'bb',
+            '0002_1l' : 'cc',
+            '0002_2r' : 'dd',
+            })
+
+        self.make_gallery(self.book2, {
+            '0001_1l' : 'ee',
+            '0001_2r' : 'ff',
+            '0002_1l' : 'gg',
+            '0002_2r' : 'hh',
+            })
+
+        self.book1.append(self.book2)
+
+        files = listdir(join(self.scandir, self.book1.gallery))
+        files.sort()
+        self.assertEqual(files, [
+            '0-0001_1l',
+            '0-0001_2r',
+            '0-0002_1l',
+            '0-0002_2r',
+            '1-0001_1l',
+            '1-0001_2r',
+            '1-0002_1l',
+            '1-0002_2r',
+            ])        
+
+
+    def test_none_indexed(self):
+        self.book2 = Book.create(self.user, 'book 2', slug='book2')
+        self.make_gallery(self.book1, {
+            '1-0001_1l' : 'aa',
+            '1-0001_2r' : 'bb',
+            '1002_1l' : 'cc',
+            '1002_2r' : 'dd',
+            })
+
+        self.make_gallery(self.book2, {
+            '0001_1l' : 'ee',
+            '0001_2r' : 'ff',
+            '0002_1l' : 'gg',
+            '0002_2r' : 'hh',
+            })
+
+        self.book1.append(self.book2)
+
+        files = listdir(join(self.scandir, self.book1.gallery))
+        files.sort()
+        self.assertEqual(files, [
+            '0-1-0001_1l',
+            '0-1-0001_2r',
+            '0-1002_1l',
+            '0-1002_2r',
+            '1-0001_1l',
+            '1-0001_2r',
+            '1-0002_1l',
+            '1-0002_2r',
+            ])        
+
diff --git a/src/documents/tests/test_publish.py b/src/documents/tests/test_publish.py
new file mode 100644 (file)
index 0000000..b59136a
--- /dev/null
@@ -0,0 +1,38 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+"""Tests for the publishing process."""
+
+from documents.test_utils import get_fixture
+
+from mock import patch
+from django.test import TestCase
+from django.contrib.auth.models import User
+from documents.models import Book
+
+
+class PublishTests(TestCase):
+    def setUp(self):
+        self.user = User.objects.create(username='tester')
+        self.text1 = get_fixture('chunk1.xml')
+        self.book = Book.create(self.user, self.text1, slug='test-book')
+
+    @patch('apiclient.api_call')
+    def test_unpublishable(self, api_call):
+        with self.assertRaises(AssertionError):
+            self.book.publish(self.user)
+
+    @patch('apiclient.api_call')
+    def test_publish(self, api_call):
+        self.book[0].head.set_publishable(True)
+        self.book.publish(self.user)
+        api_call.assert_called_with(self.user, 'books/', {"book_xml": self.text1, "days": 0}, beta=False)
+
+    @patch('apiclient.api_call')
+    def test_publish_multiple(self, api_call):
+        self.book[0].head.set_publishable(True)
+        self.book[0].split(slug='part-2')
+        self.book[1].commit(get_fixture('chunk2.xml'))
+        self.book[1].head.set_publishable(True)
+        self.book.publish(self.user)
+        api_call.assert_called_with(self.user, 'books/', {"book_xml": get_fixture('expected.xml'), "days": 0}, beta=False)
diff --git a/src/documents/tests/test_xml_updater.py b/src/documents/tests/test_xml_updater.py
new file mode 100644 (file)
index 0000000..ef0c873
--- /dev/null
@@ -0,0 +1,33 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+"""XmlUpdater tests."""
+
+from documents.test_utils import get_fixture
+from django.test import TestCase
+from django.contrib.auth.models import User
+from documents.models import Book
+from documents.management import XmlUpdater
+from librarian import DCNS
+
+
+class XmlUpdaterTests(TestCase):
+    class SimpleUpdater(XmlUpdater):
+        @XmlUpdater.fixes_elements('.//' + DCNS('title'))
+        def fix_title(element, **kwargs):
+            element.text = element.text + " fixed"
+            return True
+
+    def setUp(self):
+        self.user = User.objects.create(username='tester')
+        text = get_fixture('chunk1.xml')
+        Book.create(self.user, text, slug='test-book')
+        self.title = "Do M***"
+
+    def test_xml_updater(self):
+        self.SimpleUpdater().run(self.user)
+        self.assertEqual(
+            Book.objects.get(slug='test-book').wldocument(
+                publishable=False).book_info.title,
+            self.title + " fixed"
+            )
diff --git a/src/documents/urls.py b/src/documents/urls.py
new file mode 100644 (file)
index 0000000..602d4f2
--- /dev/null
@@ -0,0 +1,66 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf.urls import url
+from django.contrib.auth.decorators import permission_required
+from django.views.generic import RedirectView
+from .feeds import PublishTrackFeed
+from . import views
+
+
+urlpatterns = [
+    url(r'^$', RedirectView.as_view(url='catalogue/', permanent=False)),
+
+    url(r'^images/$', views.image_list, name='documents_image_list'),
+    url(r'^image/(?P<slug>[^/]+)/$', views.image, name="documents_image"),
+    url(r'^image/(?P<slug>[^/]+)/publish$', views.publish_image,
+            name="documents_publish_image"),
+
+    url(r'^catalogue/$', views.document_list, name='documents_document_list'),
+    url(r'^user/$', views.my, name='documents_user'),
+    url(r'^user/(?P<username>[^/]+)/$', views.user, name='documents_user'),
+    url(r'^users/$', views.users, name='documents_users'),
+    url(r'^activity/$', views.activity, name='documents_activity'),
+    url(r'^activity/(?P<isodate>\d{4}-\d{2}-\d{2})/$', 
+        views.activity, name='documents_activity'),
+
+    url(r'^upload/$',
+        views.upload, name='documents_upload'),
+
+    url(r'^create/(?P<slug>[^/]*)/',
+        views.create_missing, name='documents_create_missing'),
+    url(r'^create/',
+        views.create_missing, name='documents_create_missing'),
+
+    url(r'^book/(?P<slug>[^/]+)/publish$', views.publish, name="documents_publish"),
+
+    url(r'^book/(?P<slug>[^/]+)/$', views.book, name="documents_book"),
+    url(r'^book/(?P<slug>[^/]+)/gallery/$',
+            permission_required('documents.change_book')(views.GalleryView.as_view()),
+            name="documents_book_gallery"),
+    url(r'^book/(?P<slug>[^/]+)/xml$', views.book_xml, name="documents_book_xml"),
+    url(r'^book/dc/(?P<slug>[^/]+)/xml$', views.book_xml_dc, name="documents_book_xml_dc"),
+    url(r'^book/(?P<slug>[^/]+)/txt$', views.book_txt, name="documents_book_txt"),
+    url(r'^book/(?P<slug>[^/]+)/html$', views.book_html, name="documents_book_html"),
+    url(r'^book/(?P<slug>[^/]+)/epub$', views.book_epub, name="documents_book_epub"),
+    url(r'^book/(?P<slug>[^/]+)/mobi$', views.book_mobi, name="documents_book_mobi"),
+    url(r'^book/(?P<slug>[^/]+)/pdf$', views.book_pdf, name="documents_book_pdf"),
+    url(r'^book/(?P<slug>[^/]+)/pdf-mobile$', views.book_pdf, kwargs={'mobile': True}, name="documents_book_pdf_mobile"),
+
+    url(r'^chunk_add/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
+        views.chunk_add, name="documents_chunk_add"),
+    url(r'^chunk_edit/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
+        views.chunk_edit, name="documents_chunk_edit"),
+    url(r'^book_append/(?P<slug>[^/]+)/$',
+        views.book_append, name="documents_book_append"),
+    url(r'^chunk_mass_edit',
+        views.chunk_mass_edit, name='documents_chunk_mass_edit'),
+    url(r'^image_mass_edit',
+        views.image_mass_edit, name='documents_image_mass_edit'),
+
+    url(r'^track/(?P<slug>[^/]*)/$', PublishTrackFeed()),
+    url(r'^active/$', views.active_users_list, name='active_users_list'),
+
+    url(r'^mark-final/$', views.mark_final, name='mark_final'),
+    url(r'^mark-final-completed/$', views.mark_final_completed, name='mark_final_completed'),
+]
diff --git a/src/documents/views.py b/src/documents/views.py
new file mode 100644 (file)
index 0000000..397d78b
--- /dev/null
@@ -0,0 +1,672 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from collections import defaultdict
+from datetime import datetime, date, timedelta
+import logging
+import os
+from urllib.parse import unquote, urlsplit, urlunsplit
+
+from django.conf import settings
+from django.contrib import auth
+from django.contrib.auth.models import User
+from django.contrib.auth.decorators import login_required, permission_required
+from django.urls import reverse
+from django.db.models import Count, Q
+from django.db import transaction
+from django import http
+from django.http import Http404, HttpResponse, HttpResponseForbidden
+from django.http.response import HttpResponseRedirect
+from django.shortcuts import get_object_or_404, render
+from django.utils.encoding import iri_to_uri
+from django.utils.http import urlquote_plus
+from django.utils.translation import ugettext_lazy as _
+from django.views.decorators.http import require_POST
+from django_cas_ng.decorators import user_passes_test
+
+from apiclient import NotAuthorizedError
+from . import forms
+from . import helpers
+from .helpers import active_tab
+from .models import (Book, Chunk, Image, BookPublishRecord, 
+        ChunkPublishRecord, ImagePublishRecord, Project)
+from fileupload.views import UploadView
+
+#
+# Quick hack around caching problems, TODO: use ETags
+#
+from django.views.decorators.cache import never_cache
+
+logger = logging.getLogger("fnp.documents")
+
+
+@active_tab('all')
+@never_cache
+def document_list(request):
+    return render(request, 'documents/document_list.html')
+
+
+@active_tab('images')
+@never_cache
+def image_list(request, user=None):
+    return render(request, 'documents/image_list.html')
+
+
+@never_cache
+def user(request, username):
+    user = get_object_or_404(User, username=username)
+    return render(request, 'documents/user_page.html', {"viewed_user": user})
+
+
+@login_required
+@active_tab('my')
+@never_cache
+def my(request):
+    last_books = sorted(request.session.get("wiki_last_books", {}).items(),
+        key=lambda x: x[1]['time'], reverse=True)
+    for k, v in last_books:
+        v['time'] = datetime.fromtimestamp(v['time'])
+    return render(request, 'documents/my_page.html', {
+        'last_books': last_books,
+        "logout_to": '/',
+        })
+
+
+@active_tab('users')
+def users(request):
+    return render(request, 'documents/user_list.html', {
+        'users': User.objects.all().annotate(count=Count('chunk')).order_by(
+            '-count', 'last_name', 'first_name'),
+    })
+
+
+@active_tab('activity')
+def activity(request, isodate=None):
+    today = date.today()
+    try:
+        day = helpers.parse_isodate(isodate)
+    except ValueError:
+        day = today
+
+    if day > today:
+        raise Http404
+    if day != today:
+        next_day = day + timedelta(1)
+    prev_day = day - timedelta(1)
+
+    return render(request, 'documents/activity.html', locals())
+
+
+@never_cache
+def logout_then_redirect(request):
+    auth.logout(request)
+    return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
+
+
+@permission_required('documents.add_book')
+@active_tab('create')
+def create_missing(request, slug=None):
+    if slug is None:
+        slug = ''
+    slug = slug.replace(' ', '-')
+
+    if request.method == "POST":
+        form = forms.DocumentCreateForm(request.POST, request.FILES)
+        if form.is_valid():
+            
+            if request.user.is_authenticated:
+                creator = request.user
+            else:
+                creator = None
+            book = Book.create(
+                text=form.cleaned_data['text'],
+                creator=creator,
+                slug=form.cleaned_data['slug'],
+                title=form.cleaned_data['title'],
+                gallery=form.cleaned_data['gallery'],
+            )
+
+            return http.HttpResponseRedirect(reverse("documents_book", args=[book.slug]))
+    else:
+        form = forms.DocumentCreateForm(initial={
+                "slug": slug,
+                "title": slug.replace('-', ' ').title(),
+                "gallery": slug,
+        })
+
+    return render(request, "documents/document_create_missing.html", {
+        "slug": slug,
+        "form": form,
+
+        "logout_to": '/',
+    })
+
+
+@permission_required('documents.add_book')
+@active_tab('upload')
+def upload(request):
+    if request.method == "POST":
+        form = forms.DocumentsUploadForm(request.POST, request.FILES)
+        if form.is_valid():
+            from slugify import slugify
+
+            if request.user.is_authenticated:
+                creator = request.user
+            else:
+                creator = None
+
+            zip = form.cleaned_data['zip']
+            skipped_list = []
+            ok_list = []
+            error_list = []
+            slugs = {}
+            existing = [book.slug for book in Book.objects.all()]
+            for filename in zip.namelist():
+                if filename[-1] == '/':
+                    continue
+                title = os.path.basename(filename)[:-4]
+                slug = slugify(title)
+                if not (slug and filename.endswith('.xml')):
+                    skipped_list.append(filename)
+                elif slug in slugs:
+                    error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
+                elif slug in existing:
+                    error_list.append((filename, slug, _('Slug already used in repository.')))
+                else:
+                    try:
+                        zip.read(filename).decode('utf-8') # test read
+                        ok_list.append((filename, slug, title))
+                    except UnicodeDecodeError:
+                        error_list.append((filename, title, _('File should be UTF-8 encoded.')))
+                    slugs[slug] = filename
+
+            if not error_list:
+                for filename, slug, title in ok_list:
+                    book = Book.create(
+                        text=zip.read(filename).decode('utf-8'),
+                        creator=creator,
+                        slug=slug,
+                        title=title,
+                    )
+
+            return render(request, "documents/document_upload.html", {
+                "form": form,
+                "ok_list": ok_list,
+                "skipped_list": skipped_list,
+                "error_list": error_list,
+
+                "logout_to": '/',
+            })
+    else:
+        form = forms.DocumentsUploadForm()
+
+    return render(request, "documents/document_upload.html", {
+        "form": form,
+
+        "logout_to": '/',
+    })
+
+
+def serve_xml(request, book, slug):
+    if not book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+    xml = book.materialize(publishable=True)
+    response = http.HttpResponse(xml, content_type='application/xml')
+    response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
+    return response
+
+
+@never_cache
+def book_xml(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    return serve_xml(request, book, slug)
+
+
+@never_cache
+def book_xml_dc(request, slug):
+    book = get_object_or_404(Book, dc_slug=slug)
+    return serve_xml(request, book, slug)
+
+
+@never_cache
+def book_txt(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    if not book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    doc = book.wldocument()
+    text = doc.as_text().get_bytes()
+    response = http.HttpResponse(text, content_type='text/plain')
+    response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
+    return response
+
+
+@never_cache
+def book_html(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    if not book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    doc = book.wldocument(parse_dublincore=False)
+    html = doc.as_html(options={'gallery': "'%s'" % book.gallery_url()})
+
+    html = html.get_bytes().decode('utf-8') if html is not None else ''
+    # response = http.HttpResponse(html, content_type='text/html')
+    # return response
+    # book_themes = {}
+    # for fragment in book.fragments.all().iterator():
+    #     for theme in fragment.tags.filter(category='theme').iterator():
+    #         book_themes.setdefault(theme, []).append(fragment)
+
+    # book_themes = book_themes.items()
+    # book_themes.sort(key=lambda s: s[0].sort_key)
+    return render(request, 'documents/book_text.html', locals())
+
+
+@never_cache
+def book_pdf(request, slug, mobile=False):
+    book = get_object_or_404(Book, slug=slug)
+    if not book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    # TODO: move to celery
+    doc = book.wldocument()
+    # TODO: error handling
+    customizations = ['26pt', 'nothemes', 'nomargins', 'notoc'] if mobile else None
+    pdf_file = doc.as_pdf(cover=True, ilustr_path=book.gallery_path(), customizations=customizations)
+    from .ebook_utils import serve_file
+    return serve_file(pdf_file.get_filename(),
+                book.slug + '.pdf', 'application/pdf')
+
+
+@never_cache
+def book_epub(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    if not book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    # TODO: move to celery
+    doc = book.wldocument()
+    # TODO: error handling
+    epub = doc.as_epub(ilustr_path=book.gallery_path()).get_bytes()
+    response = HttpResponse(content_type='application/epub+zip')
+    response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.epub'
+    response.write(epub)
+    return response
+
+
+@never_cache
+def book_mobi(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    if not book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    # TODO: move to celery
+    doc = book.wldocument()
+    # TODO: error handling
+    mobi = doc.as_mobi(ilustr_path=book.gallery_path()).get_bytes()
+    response = HttpResponse(content_type='application/x-mobipocket-ebook')
+    response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.mobi'
+    response.write(mobi)
+    return response
+
+
+@never_cache
+def revision(request, slug, chunk=None):
+    try:
+        doc = Chunk.get(slug, chunk)
+    except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+        raise Http404
+    if not doc.book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+    return http.HttpResponse(str(doc.revision()))
+
+
+def book(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    if not book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    if request.user.has_perm('documents.change_book'):
+        if request.method == "POST":
+            form = forms.BookForm(request.POST, instance=book)
+            if form.is_valid():
+                form.save()
+                return http.HttpResponseRedirect(book.get_absolute_url())
+        else:
+            form = forms.BookForm(instance=book)
+        publish_options_form = forms.PublishOptionsForm()
+        editable = True
+    else:
+        form = forms.ReadonlyBookForm(instance=book)
+        publish_options_form = forms.PublishOptionsForm()
+        editable = False
+
+    publish_error = book.publishable_error()
+    publishable = publish_error is None
+
+    return render(request, "documents/book_detail.html", {
+        "book": book,
+        "publishable": publishable,
+        "publishable_error": publish_error,
+        "form": form,
+        "publish_options_form": publish_options_form,
+        "editable": editable,
+    })
+
+
+def image(request, slug):
+    image = get_object_or_404(Image, slug=slug)
+    if not image.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    if request.user.has_perm('documents.change_image'):
+        if request.method == "POST":
+            form = forms.ImageForm(request.POST, instance=image)
+            if form.is_valid():
+                form.save()
+                return http.HttpResponseRedirect(image.get_absolute_url())
+        else:
+            form = forms.ImageForm(instance=image)
+        editable = True
+    else:
+        form = forms.ReadonlyImageForm(instance=image)
+        editable = False
+
+    publish_error = image.publishable_error()
+    publishable = publish_error is None
+
+    return render(request, "documents/image_detail.html", {
+        "object": image,
+        "publishable": publishable,
+        "publishable_error": publish_error,
+        "form": form,
+        "editable": editable,
+    })
+
+
+@permission_required('documents.add_chunk')
+def chunk_add(request, slug, chunk):
+    try:
+        doc = Chunk.get(slug, chunk)
+    except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+        raise Http404
+    if not doc.book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    if request.method == "POST":
+        form = forms.ChunkAddForm(request.POST, instance=doc)
+        if form.is_valid():
+            if request.user.is_authenticated:
+                creator = request.user
+            else:
+                creator = None
+            doc.split(creator=creator,
+                slug=form.cleaned_data['slug'],
+                title=form.cleaned_data['title'],
+                gallery_start=form.cleaned_data['gallery_start'],
+                user=form.cleaned_data['user'],
+                stage=form.cleaned_data['stage']
+            )
+
+            return http.HttpResponseRedirect(doc.book.get_absolute_url())
+    else:
+        form = forms.ChunkAddForm(initial={
+                "slug": str(doc.number + 1),
+                "title": "cz. %d" % (doc.number + 1, ),
+        })
+
+    return render(request, "documents/chunk_add.html", {
+        "chunk": doc,
+        "form": form,
+    })
+
+
+@login_required
+def chunk_edit(request, slug, chunk):
+    try:
+        doc = Chunk.get(slug, chunk)
+    except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+        raise Http404
+    if not doc.book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    if request.method == "POST":
+        form = forms.ChunkForm(request.POST, instance=doc)
+        if form.is_valid():
+            form.save()
+            go_next = request.GET.get('next', None)
+            if go_next:
+                go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
+            else:
+                go_next = doc.book.get_absolute_url()
+            return http.HttpResponseRedirect(go_next)
+    else:
+        form = forms.ChunkForm(instance=doc)
+
+    referer = request.META.get('HTTP_REFERER')
+    if referer:
+        parts = urlsplit(referer)
+        parts = ['', ''] + list(parts[2:])
+        go_next = urlquote_plus(urlunsplit(parts))
+    else:
+        go_next = ''
+
+    return render(request, "documents/chunk_edit.html", {
+        "chunk": doc,
+        "form": form,
+        "go_next": go_next,
+    })
+
+
+@transaction.atomic
+@login_required
+@require_POST
+def chunk_mass_edit(request):
+    ids = [int(i) for i in request.POST.get('ids').split(',') if i.strip()]
+    chunks = list(Chunk.objects.filter(id__in=ids))
+    
+    stage = request.POST.get('stage')
+    if stage:
+        try:
+            stage = Chunk.tag_model.objects.get(slug=stage)
+        except Chunk.DoesNotExist as e:
+            stage = None
+       
+        for c in chunks: c.stage = stage
+
+    username = request.POST.get('user')
+    logger.info("username: %s" % username)
+    logger.info(request.POST)
+    if username:
+        try:
+            user = User.objects.get(username=username)
+        except User.DoesNotExist as e:
+            user = None
+            
+        for c in chunks: c.user = user
+
+    project_id = request.POST.get('project')
+    if project_id:
+        try:
+            project = Project.objects.get(pk=int(project_id))
+        except (Project.DoesNotExist, ValueError) as e:
+            project = None
+        for c in chunks:
+            book = c.book
+            book.project = project
+            book.save()
+
+    for c in chunks: c.save()
+
+    return HttpResponse("", content_type="text/plain")
+
+
+@transaction.atomic
+@login_required
+@require_POST
+def image_mass_edit(request):
+    ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(',')))
+    images = map(lambda i: Image.objects.get(id=i), ids)
+    
+    stage = request.POST.get('stage')
+    if stage:
+        try:
+            stage = Image.tag_model.objects.get(slug=stage)
+        except Image.DoesNotExist as e:
+            stage = None
+       
+        for c in images: c.stage = stage
+
+    username = request.POST.get('user')
+    logger.info("username: %s" % username)
+    logger.info(request.POST)
+    if username:
+        try:
+            user = User.objects.get(username=username)
+        except User.DoesNotExist as e:
+            user = None
+            
+        for c in images: c.user = user
+
+    project_id = request.POST.get('project')
+    if project_id:
+        try:
+            project = Project.objects.get(pk=int(project_id))
+        except (Project.DoesNotExist, ValueError) as e:
+            project = None
+        for c in images:
+            c.project = project
+
+    for c in images: c.save()
+
+    return HttpResponse("", content_type="text/plain")
+
+
+@permission_required('documents.change_book')
+def book_append(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    if not book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    if request.method == "POST":
+        form = forms.BookAppendForm(book, request.POST)
+        if form.is_valid():
+            append_to = form.cleaned_data['append_to']
+            append_to.append(book)
+            return http.HttpResponseRedirect(append_to.get_absolute_url())
+    else:
+        form = forms.BookAppendForm(book)
+    return render(request, "documents/book_append_to.html", {
+        "book": book,
+        "form": form,
+
+        "logout_to": '/',
+    })
+
+
+@require_POST
+@login_required
+def publish(request, slug):
+    form = forms.PublishOptionsForm(request.POST)
+    if form.is_valid():
+        days = form.cleaned_data['days']
+        beta = form.cleaned_data['beta']
+    else:
+        days = 0
+        beta = False
+    book = get_object_or_404(Book, slug=slug)
+    if not book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    try:
+        protocol = 'https://' if request.is_secure() else 'http://'
+        book.publish(request.user, host=protocol + request.get_host(), days=days, beta=beta)
+    except NotAuthorizedError:
+        return http.HttpResponseRedirect(reverse('apiclient_oauth' if not beta else 'apiclient_beta_oauth'))
+    except BaseException as e:
+        return http.HttpResponse(repr(e))
+    else:
+        return http.HttpResponseRedirect(book.get_absolute_url())
+
+
+@require_POST
+@login_required
+def publish_image(request, slug):
+    image = get_object_or_404(Image, slug=slug)
+    if not image.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    try:
+        image.publish(request.user)
+    except NotAuthorizedError:
+        return http.HttpResponseRedirect(reverse('apiclient_oauth'))
+    except BaseException as e:
+        return http.HttpResponse(e)
+    else:
+        return http.HttpResponseRedirect(image.get_absolute_url())
+
+
+class GalleryView(UploadView):
+    def get_object(self, request, slug):
+        book = get_object_or_404(Book, slug=slug)
+        if not book.gallery:
+            raise Http404
+        return book
+
+    def breadcrumbs(self):
+        return [
+            (_('books'), reverse('documents_document_list')),
+            (self.object.title, self.object.get_absolute_url()),
+            (_('scan gallery'),),
+        ]
+
+    def get_directory(self):
+        return "%s%s/" % (settings.IMAGE_DIR, self.object.gallery)
+
+
+def active_users_list(request):
+    year = int(request.GET.get('y', date.today().year))
+    by_user = defaultdict(lambda: 0)
+    by_email = defaultdict(lambda: 0)
+    names_by_email = defaultdict(set)
+    for change_model in (Chunk.change_model, Image.change_model):
+        for c in change_model.objects.filter(
+                created_at__year=year).order_by(
+                'author', 'author_email', 'author_name').values(
+                'author', 'author_name', 'author_email').annotate(
+                c=Count('author'), ce=Count('author_email')).distinct():
+            if c['author']:
+                by_user[c['author']] += c['c']
+            else:
+                by_email[c['author_email']] += c['ce']
+                if (c['author_name'] or '').strip():
+                    names_by_email[c['author_email']].add(c['author_name'])
+    for user in User.objects.filter(pk__in=by_user):
+        by_email[user.email] += by_user[user.pk]
+        names_by_email[user.email].add("%s %s" % (user.first_name, user.last_name))
+
+    active_users = []
+    for email, count in by_email.items():
+        active_users.append((email, names_by_email[email], count))
+    active_users.sort(key=lambda x: -x[2])
+    return render(request, 'documents/active_users_list.html', {
+        'users': active_users,
+        'year': year,
+    })
+
+
+@user_passes_test(lambda u: u.is_superuser)
+def mark_final(request):
+    if request.method == 'POST':
+        form = forms.MarkFinalForm(data=request.POST)
+        if form.is_valid():
+            form.save()
+            return HttpResponseRedirect(reverse('mark_final_completed'))
+    else:
+        form = forms.MarkFinalForm()
+    return render(request, 'documents/mark_final.html', {'form': form})
+
+
+def mark_final_completed(request):
+    return render(request, 'documents/mark_final_completed.html')
diff --git a/src/documents/xml_tools.py b/src/documents/xml_tools.py
new file mode 100644 (file)
index 0000000..f2c885d
--- /dev/null
@@ -0,0 +1,143 @@
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from copy import deepcopy
+import re
+
+from lxml import etree
+from .constants import TRIM_BEGIN, TRIM_END, MASTERS
+
+RE_TRIM_BEGIN = re.compile("^<!--%s-->$" % TRIM_BEGIN, re.M)
+RE_TRIM_END = re.compile("^<!--%s-->$" % TRIM_END, re.M)
+
+
+class ParseError(BaseException):
+    pass
+
+
+def _trim(text, trim_begin=True, trim_end=True):
+    """ 
+        Cut off everything before RE_TRIM_BEGIN and after RE_TRIM_END, so
+        that eg. one big XML file can be compiled from many small XML files.
+    """
+    if trim_begin:
+        text = RE_TRIM_BEGIN.split(text, maxsplit=1)[-1]
+    if trim_end:
+        text = RE_TRIM_END.split(text, maxsplit=1)[0]
+    return text
+
+
+def compile_text(parts):
+    """ 
+        Compiles full text from an iterable of parts,
+        trimming where applicable.
+    """
+    texts = []
+    trim_begin = False
+    text = ''
+    for next_text in parts:
+        if not next_text:
+            continue
+        if text:
+            # trim the end, because there's more non-empty text
+            # don't trim beginning, if `text' is the first non-empty part
+            texts.append(_trim(text, trim_begin=trim_begin))
+            trim_begin = True
+        text = next_text
+    # don't trim the end, because there's no more text coming after `text'
+    # only trim beginning if it's not still the first non-empty
+    texts.append(_trim(text, trim_begin=trim_begin, trim_end=False))
+    return "".join(texts)
+
+
+def add_trim_begin(text):
+    trim_tag = etree.Comment(TRIM_BEGIN)
+    e = etree.fromstring(text)
+    for master in e[::-1]:
+        if master.tag in MASTERS:
+            break
+    if master.tag not in MASTERS:
+        raise ParseError('No master tag found!')
+
+    master.insert(0, trim_tag)
+    trim_tag.tail = '\n\n\n' + (master.text or '')
+    master.text = '\n'
+    return str(etree.tostring(e, encoding="utf-8"), 'utf-8')
+
+
+def add_trim_end(text):
+    trim_tag = etree.Comment(TRIM_END)
+    e = etree.fromstring(text)
+    for master in e[::-1]:
+        if master.tag in MASTERS:
+            break
+    if master.tag not in MASTERS:
+        raise ParseError('No master tag found!')
+
+    master.append(trim_tag)
+    trim_tag.tail = '\n'
+    prev = trim_tag.getprevious()
+    if prev is not None:
+        prev.tail = (prev.tail or '') + '\n\n\n'
+    else:
+        master.text = (master.text or '') + '\n\n\n'
+    return str(etree.tostring(e, encoding="utf-8"), 'utf-8')
+
+
+def split_xml(text):
+    """Splits text into chapters.
+
+    All this stuff really must go somewhere else.
+
+    """
+    src = etree.fromstring(text)
+    chunks = []
+
+    splitter = u'naglowek_rozdzial'
+    parts = src.findall('.//naglowek_rozdzial')
+    while parts:
+        # copy the document
+        copied = deepcopy(src)
+
+        element = parts[-1]
+
+        # find the chapter's title
+        name_elem = deepcopy(element)
+        for tag in 'extra', 'motyw', 'pa', 'pe', 'pr', 'pt', 'uwaga':
+            for a in name_elem.findall('.//' + tag):
+                a.text=''
+                del a[:]
+        name = etree.tostring(name_elem, method='text', encoding='utf-8').strip()
+
+        # in the original, remove everything from the start of the last chapter
+        parent = element.getparent()
+        del parent[parent.index(element):]
+        element, parent = parent, parent.getparent()
+        while parent is not None:
+            del parent[parent.index(element) + 1:]
+            element, parent = parent, parent.getparent()
+
+        # in the copy, remove everything before the last chapter
+        element = copied.findall('.//naglowek_rozdzial')[-1]
+        parent = element.getparent()
+        while parent is not None:
+            parent.text = None
+            while parent[0] is not element:
+                del parent[0]
+            element, parent = parent, parent.getparent()
+        chunks[:0] = [[name,
+            str(etree.tostring(copied, encoding='utf-8'), 'utf-8')
+            ]]
+
+        parts = src.findall('.//naglowek_rozdzial')
+
+    chunks[:0] = [[u'początek',
+        str(etree.tostring(src, encoding='utf-8'), 'utf-8')
+        ]]
+
+    for ch in chunks[1:]:
+        ch[1] = add_trim_begin(ch[1])
+    for ch in chunks[:-1]:
+        ch[1] = add_trim_end(ch[1])
+
+    return chunks
index 44c37db..dc49f9c 100644 (file)
@@ -1,4 +1,4 @@
-{% extends "catalogue/base.html" %}
+{% extends "documents/base.html" %}
 {% load i18n %}
 {% load upload_tags %}
 
 {% load i18n %}
 {% load upload_tags %}
 
index 51dec4d..8b7b9c4 100644 (file)
@@ -89,6 +89,7 @@ INSTALLED_APPS = (
     'bootstrap4',
 
     'catalogue',
     'bootstrap4',
 
     'catalogue',
+    'documents',
     'cover',
     'dvcs',
     'wiki',
     'cover',
     'dvcs',
     'wiki',
@@ -138,11 +139,11 @@ PIPELINE = {
             ),
             'output_filename': 'compressed/detail_styles.css',
         },
             ),
             'output_filename': 'compressed/detail_styles.css',
         },
-        'catalogue': {
+        'documents': {
             'source_filenames': (
                 'css/filelist.css',
             ),
             'source_filenames': (
                 'css/filelist.css',
             ),
-            'output_filename': 'compressed/catalogue_styles.css',
+            'output_filename': 'compressed/documents_styles.css',
         },
         'book': {
             'source_filenames': (
         },
         'book': {
             'source_filenames': (
@@ -226,13 +227,13 @@ PIPELINE = {
             ),
             'output_filename': 'compressed/detail_img_scripts.js',
         },
             ),
             'output_filename': 'compressed/detail_img_scripts.js',
         },
-        'catalogue': {
+        'documents': {
             'source_filenames': (
             'source_filenames': (
-                'js/catalogue/catalogue.js',
+                'js/documents/documents.js',
                 'js/slugify.js',
                 'email_mangler/email_mangler.js',
             ),
                 'js/slugify.js',
                 'email_mangler/email_mangler.js',
             ),
-            'output_filename': 'compressed/catalogue_scripts.js',
+            'output_filename': 'compressed/documents_scripts.js',
         },
         'book': {
             'source_filenames': (
         },
         'book': {
             'source_filenames': (
@@ -246,7 +247,7 @@ PIPELINE = {
         },
         'book_list': {
             'source_filenames': (
         },
         'book_list': {
             'source_filenames': (
-                'js/catalogue/book_list.js',
+                'js/documents/book_list.js',
             ),
             'output_filename': 'compressed/book_list.js',
         }
             ),
             'output_filename': 'compressed/book_list.js',
         }
diff --git a/src/redakcja/static/js/catalogue/book_list.js b/src/redakcja/static/js/catalogue/book_list.js
deleted file mode 100644 (file)
index 9d2511d..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-(function($) {
-    $(function() {
-
-    // clicking on book checks chunks, too
-    $("input[name=select_book]").change(function(ev) {
-        $book = $(this);
-        $book.closest("table").find("input[name=select_chunk][data-book-id=" + $book.val() + "]").attr("checked", $book.is(':checked'));
-    });
-
-    // initialize context menu
-
-   var get_ids = function() {
-       return $.map($("input[name=select_chunk]:checked"), function(ele, idx) {
-           return ele.value;
-           }).concat(
-               $.map($("input[name=select_book][data-chunk-id!=]:checked"), function(ele, idx) {
-                   return $(ele).attr("data-chunk-id");
-                   })).join();
-   };
-
-    var get_callback = function(form_field_name) {
-        var $form = $("#chunk_mass_edit");
-        var $field = $("[name=" + form_field_name + "]", $form);
-        var $ids_field = $("[name=ids]").val(get_ids());
-        var usable_callback = function(value) {
-            $field.val(value);
-            $ids_field.val(get_ids());
-            $.post($form.attr("action"),
-               $form.serialize(),
-               function(data, status) {
-                   location.reload(true);
-               }
-            );
-            return true;
-        };
-        return usable_callback;
-    };
-
-    var get_items = function(field, callback) {
-        var d = {};
-        $.each($("select[name="+field+"] option[value!=]"),
-            function(idx, ele) {
-                d[field + "_" + idx] = {
-                    name: $(ele).text(), 
-                    callback: function() {callback($(ele).attr('value'));}
-                };
-            });
-        return d;
-    };
-
-    var user_callback = get_callback('user');
-    var users = [
-        get_items("user", user_callback),
-        {sep: '----'},
-        get_items("other-user", user_callback)
-    ];
-    var current_user_items = user_items = {};
-    var i = 0;
-    var more_label = $("label[for=mass_edit_more_users]").text();
-    for (user_table in users) {
-        for (user in users[user_table]) {
-            if (i && i % 20 == 0) {
-                var more_items = {};
-                current_user_items['more'] = {
-                    name: more_label,
-                    items: more_items
-                };
-                current_user_items = more_items;
-            }
-            current_user_items[user] = users[user_table][user];
-            i += 1;
-        }
-    }
-    $.contextMenu({
-        selector: '#file-list',
-        items: {
-            stage: { 
-                name: $("label[for=mass_edit_stage]").text(),
-                items: get_items("stage", get_callback('stage')),
-                icon: "clock",
-            },
-            user: { 
-                name: $("label[for=mass_edit_user]").text(),
-                items: user_items,
-                icon: "user",
-            },
-            project: {
-                name: $("label[for=mass_edit_project]").text(),
-                items: get_items("project", get_callback('project')),
-            },
-        },
-    });
-
-
-    });
-})(jQuery);
diff --git a/src/redakcja/static/js/catalogue/catalogue.js b/src/redakcja/static/js/catalogue/catalogue.js
deleted file mode 100644 (file)
index 9d2bd95..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-(function($) {
-    $(function() {
-
-
-        $('.filter').change(function() {
-            document.filter[this.name].value = this.value;
-            document.filter.submit();
-        });
-
-        $('.check-filter').change(function() {
-            document.filter[this.name].value = this.checked ? '1' : '';
-            document.filter.submit();
-        });
-
-        $('.text-filter').each(function() {
-            var inp = this;
-            $(inp).parent().submit(function() {
-                document.filter[inp.name].value = inp.value;
-                document.filter.submit();
-                return false;
-            });
-        });
-
-
-        $('.autoslug-source').change(function() {
-            $('.autoslug').attr('value', slugify(this.value));
-        });
-
-
-    });
-})(jQuery);
-
diff --git a/src/redakcja/static/js/documents/book_list.js b/src/redakcja/static/js/documents/book_list.js
new file mode 100644 (file)
index 0000000..9d2511d
--- /dev/null
@@ -0,0 +1,96 @@
+(function($) {
+    $(function() {
+
+    // clicking on book checks chunks, too
+    $("input[name=select_book]").change(function(ev) {
+        $book = $(this);
+        $book.closest("table").find("input[name=select_chunk][data-book-id=" + $book.val() + "]").attr("checked", $book.is(':checked'));
+    });
+
+    // initialize context menu
+
+   var get_ids = function() {
+       return $.map($("input[name=select_chunk]:checked"), function(ele, idx) {
+           return ele.value;
+           }).concat(
+               $.map($("input[name=select_book][data-chunk-id!=]:checked"), function(ele, idx) {
+                   return $(ele).attr("data-chunk-id");
+                   })).join();
+   };
+
+    var get_callback = function(form_field_name) {
+        var $form = $("#chunk_mass_edit");
+        var $field = $("[name=" + form_field_name + "]", $form);
+        var $ids_field = $("[name=ids]").val(get_ids());
+        var usable_callback = function(value) {
+            $field.val(value);
+            $ids_field.val(get_ids());
+            $.post($form.attr("action"),
+               $form.serialize(),
+               function(data, status) {
+                   location.reload(true);
+               }
+            );
+            return true;
+        };
+        return usable_callback;
+    };
+
+    var get_items = function(field, callback) {
+        var d = {};
+        $.each($("select[name="+field+"] option[value!=]"),
+            function(idx, ele) {
+                d[field + "_" + idx] = {
+                    name: $(ele).text(), 
+                    callback: function() {callback($(ele).attr('value'));}
+                };
+            });
+        return d;
+    };
+
+    var user_callback = get_callback('user');
+    var users = [
+        get_items("user", user_callback),
+        {sep: '----'},
+        get_items("other-user", user_callback)
+    ];
+    var current_user_items = user_items = {};
+    var i = 0;
+    var more_label = $("label[for=mass_edit_more_users]").text();
+    for (user_table in users) {
+        for (user in users[user_table]) {
+            if (i && i % 20 == 0) {
+                var more_items = {};
+                current_user_items['more'] = {
+                    name: more_label,
+                    items: more_items
+                };
+                current_user_items = more_items;
+            }
+            current_user_items[user] = users[user_table][user];
+            i += 1;
+        }
+    }
+    $.contextMenu({
+        selector: '#file-list',
+        items: {
+            stage: { 
+                name: $("label[for=mass_edit_stage]").text(),
+                items: get_items("stage", get_callback('stage')),
+                icon: "clock",
+            },
+            user: { 
+                name: $("label[for=mass_edit_user]").text(),
+                items: user_items,
+                icon: "user",
+            },
+            project: {
+                name: $("label[for=mass_edit_project]").text(),
+                items: get_items("project", get_callback('project')),
+            },
+        },
+    });
+
+
+    });
+})(jQuery);
diff --git a/src/redakcja/static/js/documents/documents.js b/src/redakcja/static/js/documents/documents.js
new file mode 100644 (file)
index 0000000..9d2bd95
--- /dev/null
@@ -0,0 +1,32 @@
+(function($) {
+    $(function() {
+
+
+        $('.filter').change(function() {
+            document.filter[this.name].value = this.value;
+            document.filter.submit();
+        });
+
+        $('.check-filter').change(function() {
+            document.filter[this.name].value = this.checked ? '1' : '';
+            document.filter.submit();
+        });
+
+        $('.text-filter').each(function() {
+            var inp = this;
+            $(inp).parent().submit(function() {
+                document.filter[inp.name].value = inp.value;
+                document.filter.submit();
+                return false;
+            });
+        });
+
+
+        $('.autoslug-source').change(function() {
+            $('.autoslug').attr('value', slugify(this.value));
+        });
+
+
+    });
+})(jQuery);
+
index 11edc9d..a4fd20d 100644 (file)
@@ -1,4 +1,4 @@
-{% extends "catalogue/base.html" %}
+{% extends "documents/base.html" %}
 {% load i18n %}
 
 {% block titleextra %}{% trans "Page not found" %}{% endblock %}
 {% load i18n %}
 
 {% block titleextra %}{% trans "Page not found" %}{% endblock %}
@@ -17,7 +17,7 @@ still can't find what you're looking for, please
 <a href="{{m}}">contact the administrator</a>.{% endblocktrans %}
 </p>
 
 <a href="{{m}}">contact the administrator</a>.{% endblocktrans %}
 </p>
 
-{% url "catalogue_user" as m %}
+{% url "documents_user" as m %}
 <p>
 {% blocktrans %}If you're coming from Redmine, please note that
 work is no longer managed there. 
 <p>
 {% blocktrans %}If you're coming from Redmine, please note that
 work is no longer managed there. 
index 6ba9f75..7bef2eb 100644 (file)
@@ -1,4 +1,4 @@
-{% extends "catalogue/base.html" %}
+{% extends "documents/base.html" %}
 
 {% block titleextra %}Logowanie{% endblock %}
 {% block subtitle %} - Logowanie {% endblock subtitle %}
 
 {% block titleextra %}Logowanie{% endblock %}
 {% block subtitle %} - Logowanie {% endblock subtitle %}
index 9f5d6e0..af8b4f2 100644 (file)
@@ -20,7 +20,7 @@ urlpatterns = [
     url(r'^admin/', admin.site.urls),
 
     url(r'^$', RedirectView.as_view(url='/documents/', permanent=False)),
     url(r'^admin/', admin.site.urls),
 
     url(r'^$', RedirectView.as_view(url='/documents/', permanent=False)),
-    url(r'^documents/', include('catalogue.urls')),
+    url(r'^documents/', include('documents.urls')),
     url(r'^apiclient/', include('apiclient.urls')),
     url(r'^editor/', include('wiki.urls')),
     url(r'^images/', include('wiki_img.urls')),
     url(r'^apiclient/', include('apiclient.urls')),
     url(r'^editor/', include('wiki.urls')),
     url(r'^images/', include('wiki_img.urls')),
index 6f6defc..084ae46 100644 (file)
@@ -4,7 +4,7 @@
 from django import forms
 from django.utils.translation import ugettext_lazy as _
 
 from django import forms
 from django.utils.translation import ugettext_lazy as _
 
-from catalogue.models import Chunk
+from documents.models import Chunk
 
 
 class DocumentPubmarkForm(forms.Form):
 
 
 class DocumentPubmarkForm(forms.Form):
index 8b79d9a..6cf2543 100644 (file)
@@ -33,7 +33,7 @@
   </div>
 
   <nav class="navbar navbar-expand-sm navbar-dark bg-dark" id="header">
   </div>
 
   <nav class="navbar navbar-expand-sm navbar-dark bg-dark" id="header">
-    <a class="navbar-brand" href="{% url 'catalogue_document_list' %}">
+    <a class="navbar-brand" href="{% url 'documents_document_list' %}">
       <img src="{% static 'img/wl-orange.png' %}"  alt="Home" />
     </a>
 
       <img src="{% static 'img/wl-orange.png' %}"  alt="Home" />
     </a>
 
index 17f7b73..b664d30 100644 (file)
@@ -18,7 +18,7 @@ from django.utils.translation import ugettext as _
 from django.views.decorators.http import require_POST, require_GET
 from django.shortcuts import get_object_or_404, render
 
 from django.views.decorators.http import require_POST, require_GET
 from django.shortcuts import get_object_or_404, render
 
-from catalogue.models import Book, Chunk
+from documents.models import Book, Chunk
 from . import nice_diff
 from wiki import forms
 from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
 from . import nice_diff
 from wiki import forms
 from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
@@ -47,7 +47,7 @@ def editor(request, slug, chunk=None, template_name='wiki/document_details.html'
             try:
                 book = Book.objects.get(slug=slug)
             except Book.DoesNotExist:
             try:
                 book = Book.objects.get(slug=slug)
             except Book.DoesNotExist:
-                return http.HttpResponseRedirect(reverse("catalogue_create_missing", args=[slug]))
+                return http.HttpResponseRedirect(reverse("documents_create_missing", args=[slug]))
         else:
             raise Http404
     if not chunk.book.accessible(request):
         else:
             raise Http404
     if not chunk.book.accessible(request):
@@ -72,7 +72,7 @@ def editor(request, slug, chunk=None, template_name='wiki/document_details.html'
             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
             "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
         },
             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
             "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
         },
-        'can_pubmark': request.user.has_perm('catalogue.can_pubmark'),
+        'can_pubmark': request.user.has_perm('documents.can_pubmark'),
         'REDMINE_URL': settings.REDMINE_URL,
     })
 
         'REDMINE_URL': settings.REDMINE_URL,
     })
 
@@ -130,7 +130,7 @@ def text(request, chunk_id):
             stage = form.cleaned_data['stage_completed']
             tags = [stage] if stage else []
             publishable = (form.cleaned_data['publishable'] and
             stage = form.cleaned_data['stage_completed']
             tags = [stage] if stage else []
             publishable = (form.cleaned_data['publishable'] and
-                    request.user.has_perm('catalogue.can_pubmark'))
+                    request.user.has_perm('documents.can_pubmark'))
             doc.commit(author=author,
                        text=text,
                        parent=parent,
             doc.commit(author=author,
                        text=text,
                        parent=parent,
@@ -291,7 +291,7 @@ def history(request, chunk_id):
 
 
 @require_POST
 
 
 @require_POST
-@ajax_require_permission('catalogue.can_pubmark')
+@ajax_require_permission('documents.can_pubmark')
 def pubmark(request, chunk_id):
     form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
     if form.is_valid():
 def pubmark(request, chunk_id):
     form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
     if form.is_valid():
index ae68439..43b1a25 100644 (file)
@@ -4,7 +4,7 @@
 from django import forms
 from django.utils.translation import ugettext_lazy as _
 from wiki.forms import DocumentTextSaveForm
 from django import forms
 from django.utils.translation import ugettext_lazy as _
 from wiki.forms import DocumentTextSaveForm
-from catalogue.models import Image
+from documents.models import Image
 
 
 class ImageSaveForm(DocumentTextSaveForm):
 
 
 class ImageSaveForm(DocumentTextSaveForm):
index 1a8e684..6b779df 100644 (file)
@@ -31,7 +31,7 @@
   </div>
 
   <nav class="navbar navbar-expand-sm navbar-dark bg-dark" id="header">
   </div>
 
   <nav class="navbar navbar-expand-sm navbar-dark bg-dark" id="header">
-    <a class="navbar-brand" href="{% url 'catalogue_document_list' %}">
+    <a class="navbar-brand" href="{% url 'documents_document_list' %}">
       <img src="{% static 'img/wl-orange.png' %}"  alt="Home" />
     </a>
 
       <img src="{% static 'img/wl-orange.png' %}"  alt="Home" />
     </a>
 
index 280848a..7fc5c70 100644 (file)
@@ -17,7 +17,7 @@ from django.conf import settings
 from django.utils.formats import localize
 from django.utils.translation import ugettext as _
 
 from django.utils.formats import localize
 from django.utils.translation import ugettext as _
 
-from catalogue.models import Image
+from documents.models import Image
 from wiki import forms
 from wiki import nice_diff
 from wiki_img.forms import ImageSaveForm
 from wiki import forms
 from wiki import nice_diff
 from wiki_img.forms import ImageSaveForm
@@ -39,7 +39,7 @@ def editor(request, slug, template_name='wiki_img/document_details.html'):
             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
             "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
         },
             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
             "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
         },
-        'can_pubmark': request.user.has_perm('catalogue.can_pubmark_image'),
+        'can_pubmark': request.user.has_perm('documents.can_pubmark_image'),
         'REDMINE_URL': settings.REDMINE_URL,
     })
 
         'REDMINE_URL': settings.REDMINE_URL,
     })
 
@@ -79,7 +79,7 @@ def text(request, image_id):
             stage = form.cleaned_data['stage_completed']
             tags = [stage] if stage else []
             publishable = (form.cleaned_data['publishable'] and
             stage = form.cleaned_data['stage_completed']
             tags = [stage] if stage else []
             publishable = (form.cleaned_data['publishable'] and
-                    request.user.has_perm('catalogue.can_pubmark_image'))
+                    request.user.has_perm('documents.can_pubmark_image'))
             doc.commit(author=author,
                    text=text,
                    parent=parent,
             doc.commit(author=author,
                    text=text,
                    parent=parent,
@@ -196,7 +196,7 @@ def diff(request, object_id):
 
 
 @require_POST
 
 
 @require_POST
-@ajax_require_permission('catalogue.can_pubmark_image')
+@ajax_require_permission('documents.can_pubmark_image')
 def pubmark(request, object_id):
     form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
     if form.is_valid():
 def pubmark(request, object_id):
     form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
     if form.is_valid():