From: Radek Czajka Date: Sun, 22 Mar 2020 17:22:20 +0000 (+0100) Subject: Rename catalogue to documents. X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/e977f7187b10b1bc0a30794cd585c6b840568996?hp=69d9738d6855e38869678a54991d30e5cddb8e67 Rename catalogue to documents. --- diff --git a/src/catalogue/__init__.py b/src/catalogue/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/catalogue/admin.py b/src/catalogue/admin.py deleted file mode 100644 index 8daad6e0..00000000 --- a/src/catalogue/admin.py +++ /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 index 4775d187..00000000 --- a/src/catalogue/constants.py +++ /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 index f9bd6d37..00000000 --- a/src/catalogue/ebook_utils.py +++ /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 index 99808bc6..00000000 --- a/src/catalogue/feeds.py +++ /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 index 5a46ec04..00000000 --- a/src/catalogue/fixtures/stages.json +++ /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 index e61a1b71..00000000 --- a/src/catalogue/forms.py +++ /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 index 59545031..00000000 --- a/src/catalogue/helpers.py +++ /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 index 395b34ef..00000000 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 index ac60fb22..00000000 --- a/src/catalogue/locale/pl/LC_MESSAGES/django.po +++ /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 , 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 \n" -"Language-Team: Fundacja Nowoczesna Polska \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 " -".xml will be ignored." -msgstr "" -"Proszę wskazać archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie " -"kończące się na .xml 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 .xml extension" -msgstr "Pliki pominięte z powodu braku rozszerzenia .xml." - -#: 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 index 6f6f6b6e..00000000 --- a/src/catalogue/management/__init__.py +++ /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 index 6e450663..00000000 --- a/src/catalogue/management/commands/__init__.py +++ /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 index 49dff284..00000000 --- a/src/catalogue/management/commands/add_parent.py +++ /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 = ''' - - -%(dc)s - - - - -''' - -DC_TEMPLATE = '%(value)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 index cd691723..00000000 --- a/src/catalogue/management/commands/fixdc.py +++ /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 index bb891edd..00000000 --- a/src/catalogue/management/commands/import_wl.py +++ /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 index 5bacb4bd..00000000 --- a/src/catalogue/management/commands/insert_isbn.py +++ /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'%(url)s' - r'', - r'ISBN-%(isbn)s', - r'ISBN', - r'%(content_type)s', -) - - -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 index 11d63a3c..00000000 --- a/src/catalogue/management/commands/mark_final.py +++ /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 index cebf73bc..00000000 --- a/src/catalogue/management/commands/merge_books.py +++ /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\d*)$'), None)) - res.append((re.compile(r'__?(rozdzialy__?)?(?P\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 index a271e329..00000000 --- a/src/catalogue/management/commands/prune_audience.py +++ /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 index 137baaf5..00000000 --- a/src/catalogue/managers.py +++ /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 index 86be6bc6..00000000 --- a/src/catalogue/models/__init__.py +++ /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 index fc0e18f6..00000000 --- a/src/catalogue/models/book.py +++ /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 index b3e6acaa..00000000 --- a/src/catalogue/models/chunk.py +++ /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 index becc308b..00000000 --- a/src/catalogue/models/image.py +++ /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 index 0c0663c9..00000000 --- a/src/catalogue/models/listeners.py +++ /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 index caea8b80..00000000 --- a/src/catalogue/models/project.py +++ /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 index eeec055e..00000000 --- a/src/catalogue/models/publish_log.py +++ /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 index 852dc964..00000000 --- a/src/catalogue/signals.py +++ /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 index 3741b412..00000000 --- a/src/catalogue/templates/catalogue/active_users_list.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "catalogue/base.html" %} -{% load i18n %} - - -{% block titleextra %}{% trans "Active users" %}{% endblock %} - - -{% block content %} - -

- {% trans "Users active in the year" %} {{ year }} -

- -
    -{% for email, names, count in users %} -
  • {% for name in names %}{{ name }}, {% endfor %}{{ email }} ({{ count }})
  • -{% endfor %} -
- -{% endblock content %} diff --git a/src/catalogue/templates/catalogue/activity.html b/src/catalogue/templates/catalogue/activity.html deleted file mode 100644 index 0ab5c0de..00000000 --- a/src/catalogue/templates/catalogue/activity.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "catalogue/base.html" %} -{% load i18n %} -{% load wall %} - - -{% block titleextra %}{% trans "Activity" %}{% endblock %} - - -{% block content %} - -
-
-

- < - {% trans "Activity" %}: {{ day }} - {% if next_day %} - > - {% endif %} -

-
-
- - {% day_wall day %} -
-{% endblock content %} diff --git a/src/catalogue/templates/catalogue/base.html b/src/catalogue/templates/catalogue/base.html deleted file mode 100644 index c35e5395..00000000 --- a/src/catalogue/templates/catalogue/base.html +++ /dev/null @@ -1,60 +0,0 @@ - -{% load pipeline i18n %} -{% load static %} -{% load catalogue %} - - - - - - - {% stylesheet 'catalogue' %} - {% block title %}{% block titleextra %}{% endblock %} :: - {% trans "Platforma Redakcyjna" %}{% endblock title %} - {% block add_css %}{% endblock %} - - - - - - - -
- {% block content %} -
-
- {% block leftcolumn %} - {% endblock leftcolumn %} -
-
- {% block rightcolumn %} - {% endblock rightcolumn %} -
-
- {% endblock content %} -
- - - - - -{% javascript 'catalogue' %} -{% block add_js %}{% endblock %} -{% block extrabody %} -{% endblock %} - - diff --git a/src/catalogue/templates/catalogue/book_append_to.html b/src/catalogue/templates/catalogue/book_append_to.html deleted file mode 100644 index 15a32430..00000000 --- a/src/catalogue/templates/catalogue/book_append_to.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "catalogue/base.html" %} -{% load i18n %} -{% load bootstrap4 %} - -{% block titleextra %}{% trans "Append book" %}{% endblock %} - -{% block content %} -
-
-
- {% csrf_token %} - {% bootstrap_form form %} - {% buttons %} - - {% endbuttons %} -
-
-{% endblock content %} - diff --git a/src/catalogue/templates/catalogue/book_detail.html b/src/catalogue/templates/catalogue/book_detail.html deleted file mode 100644 index b625b7e7..00000000 --- a/src/catalogue/templates/catalogue/book_detail.html +++ /dev/null @@ -1,119 +0,0 @@ -{% extends "catalogue/base.html" %} -{% load book_list i18n %} -{% load bootstrap4 %} - - -{% block titleextra %}{{ book.title }}{% endblock %} - - -{% block content %} - -
-
-

{{ book.title }}

-
-
- - - -{% if editable %}
{% csrf_token %}{% endif %} - {% bootstrap_form form %} - {% if editable %} - {% buttons %} - - {% endbuttons %} - {% endif %} -{% if editable %}
{% endif %} - -{% if editable %} - {% if book.gallery %} -

{% trans "Edit gallery" %}

- {% endif %} - -

{% trans "Append to other book" %}

-{% endif %} -
-
- -
-
-

{% trans "Chunks" %}

-
-
- - - {% for chunk in book %} - {% include 'catalogue/book_list/chunk.html' %} - {% endfor %} -
-
-
- - - - -
- -
-

{% trans "Publication" %}

-
-
-
-
- -{% if book.dc_cover_image %} - {{ book.dc_cover_image }} -{% endif %} -

-
- -Okładka w rozmiarze - x - -
-
-
-

{% trans "Last published" %}: - {% if book.last_published %} - {{ book.last_published }} - {% else %} - — - {% endif %} -

- -{% if publishable %} -

- {% trans "Full XML" %}
- {% trans "HTML version" %}
- {% trans "TXT version" %}
- {% trans "PDF version" %}
- {% trans "PDF version for mobiles" %}
- {% trans "EPUB version" %}
- {% trans "MOBI version" %}
-

- - {% if user.is_authenticated %} - -
{% csrf_token %} - {{ publish_options_form.as_p }} - - - -
- {% else %} - {% trans "Log in to publish." %} - {% endif %} -{% else %} -

{% trans "This book can't be published yet, because:" %}

-
  • {{ publishable_error }}
-{% endif %} - -
-
-{% endblock content %} diff --git a/src/catalogue/templates/catalogue/book_edit.html b/src/catalogue/templates/catalogue/book_edit.html deleted file mode 100644 index 43fe0ea9..00000000 --- a/src/catalogue/templates/catalogue/book_edit.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "catalogue/base.html" %} -{% load i18n %} - - -{% block titleextra %}{% trans "Edit book" %}{% endblock %} - - -{% block leftcolumn %} -
- {% csrf_token %} - {{ form.as_p }} - -

-
-{% 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 index 518811ee..00000000 --- a/src/catalogue/templates/catalogue/book_html.html +++ /dev/null @@ -1,29 +0,0 @@ -{% load i18n %} - - - - - {{ book.title }} - - - -
- {#% book_info book %#} -
- - - {{ html|safe }} - - - diff --git a/src/catalogue/templates/catalogue/book_list/book.html b/src/catalogue/templates/catalogue/book_list/book.html deleted file mode 100644 index 189c9322..00000000 --- a/src/catalogue/templates/catalogue/book_list/book.html +++ /dev/null @@ -1,50 +0,0 @@ -{% load i18n %} -{% load username from common_tags %} - -{% if book.single %} - {% with chunk as chunk %} - - - 📕 - 📜 - - {{ book.title }} - {% if chunk.stage %} - {{ chunk.stage }} - {% else %}– - {% endif %} - {% if chunk.user %}{{ chunk.user|username }}{% endif %} - - {% if book.published %} - opubl. - {% endif %} - {% if book.new_publishable %} - do publ. - {% endif %} - {% if chunk.changed %} - zmiany - {% endif %} - - {{ book.project.name }} - - {% endwith %} -{% else %} - - - 📕 - - {{ book.title }} - - - - {% if book.published %} - opubl. - {% endif %} - {% if book.new_publishable %} - do publ. - {% endif %} - - {{ book.project.name }} - -{% 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 index 4e213231..00000000 --- a/src/catalogue/templates/catalogue/book_list/book_list.html +++ /dev/null @@ -1,117 +0,0 @@ -{% load i18n %} -{% load pagination_tags %} -{% load username from common_tags %} - - -
- - -{% if not viewed_user %} - -{% endif %} - - - -
- - -
-
- - - - - - - - - - - {% if not viewed_user %} - - {% else %} - - {% endif %} - - - - - - - - {% autopaginate books 100 as books_page %} - - {% 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 %} - -
-
- -
-
-{% paginate %} - {% blocktrans count c=books|length %}{{c}} book{% plural %}{{c}} books{% endblocktrans %} - - -{% if not books %} -

{% trans "No books found." %}

-{% endif %} - -
-
- - - - diff --git a/src/catalogue/templates/catalogue/book_list/chunk.html b/src/catalogue/templates/catalogue/book_list/chunk.html deleted file mode 100644 index cdd3b739..00000000 --- a/src/catalogue/templates/catalogue/book_list/chunk.html +++ /dev/null @@ -1,30 +0,0 @@ -{% load i18n %} -{% load username from common_tags %} - - - - - 📜 - - {{ chunk.number }}. - {{ chunk.title }} - {% if chunk.stage %} - {{ chunk.stage }} - {% else %} - – - {% endif %} - {% if chunk.user %} - - {{ chunk.user|username }} - {% else %} - - {% endif %} - - - - {% if chunk.changed %} - zmiany - {% endif %} - - - diff --git a/src/catalogue/templates/catalogue/book_text.html b/src/catalogue/templates/catalogue/book_text.html deleted file mode 100644 index a88aad50..00000000 --- a/src/catalogue/templates/catalogue/book_text.html +++ /dev/null @@ -1,28 +0,0 @@ -{% load i18n pipeline %} - - - - - {% trans "Redakcja" %} :: {{ book.title }} - {% stylesheet 'book' %} - - {% javascript 'book' %} - - - - - {{ html|safe }} - - diff --git a/src/catalogue/templates/catalogue/chunk_add.html b/src/catalogue/templates/catalogue/chunk_add.html deleted file mode 100644 index f447ec60..00000000 --- a/src/catalogue/templates/catalogue/chunk_add.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends "catalogue/base.html" %} -{% load i18n %} -{% load bootstrap4 %} - - -{% block titleextra %}{% trans "Split chunk" %}{% endblock %} - - -{% block content %} -
-
-

{% trans "Split chunk" %}

-
-
- -
- {% csrf_token %} -
-

{% trans "Insert empty chunk after" %}: - {{ chunk.pretty_name }}

- {% bootstrap_form form %} - {% buttons %} - - {% endbuttons %} -
-
-
-
-{% endblock content %} diff --git a/src/catalogue/templates/catalogue/chunk_edit.html b/src/catalogue/templates/catalogue/chunk_edit.html deleted file mode 100644 index 971634dd..00000000 --- a/src/catalogue/templates/catalogue/chunk_edit.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends "catalogue/base.html" %} -{% load i18n %} -{% load bootstrap4 %} - - -{% block titleextra %}{% trans "Chunk settings" %}{% endblock %} - - -{% block content %} -
-
-

{% trans "Chunk settings" %}

-
-
- -
- {% csrf_token %} -
-

{% trans "Book" %}: {{ chunk.book }} ({{ chunk.number }}/{{ chunk.book|length }})

- {% bootstrap_form form %} - {% buttons %} - - {% endbuttons %} -
- -
- - -

{% trans "Split chunk" %}

-
-
- -{% 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 index 773dd9e2..00000000 --- a/src/catalogue/templates/catalogue/document_create_missing.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "catalogue/base.html" %} -{% load i18n %} -{% load bootstrap4 %} - - -{% block titleextra %}{% trans "Create a new book" %}{% endblock %} - - -{% block content %} -
-
-

{% trans "Create a new book" %}

-
-
- - -
- {% csrf_token %} - {% bootstrap_form form %} - {% buttons %} - - {% endbuttons %} -
-
-
-{% endblock content %} diff --git a/src/catalogue/templates/catalogue/document_list.html b/src/catalogue/templates/catalogue/document_list.html deleted file mode 100644 index 8a7e0fcc..00000000 --- a/src/catalogue/templates/catalogue/document_list.html +++ /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' %} - -{% endblock %} - -{% block add_css %} - {% stylesheet 'book_list' %} - -{% 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 index e42414e6..00000000 --- a/src/catalogue/templates/catalogue/document_upload.html +++ /dev/null @@ -1,78 +0,0 @@ -{% extends "catalogue/base.html" %} -{% load i18n %} -{% load bootstrap4 %} - - -{% block titleextra %}{% trans "Bulk document upload" %}{% endblock %} - - -{% block content %} - - -
-
-

{% trans "Bulk documents upload" %}

-
-
- -

-{% trans "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with .xml will be ignored." %} -

- -
-{% csrf_token %} -{% bootstrap_form form %} -{% buttons %} - -{% endbuttons %} -
- - -{% if error_list %} -
- -

{% trans "There have been some errors. No files have been added to the repository." %} -

{% trans "Offending files" %}

-
    - {% for filename, title, error in error_list %} -
  • {{ title }} ({{ filename }}): {{ error }}
  • - {% endfor %} -
- - {% if ok_list %} -

{% trans "Correct files" %}

-
    - {% for filename, slug, title in ok_list %} -
  • {{ title }} ({{ filename }})
  • - {% endfor %} -
- {% endif %} - -{% else %} - - {% if ok_list %} -

{% trans "Files have been successfully uploaded to the repository." %}

-

{% trans "Uploaded files" %}

-
    - {% for filename, slug, title in ok_list %} -
  • {{ title }} ({{ filename }})
  • - {% endfor %} -
- {% endif %} -{% endif %} - -{% if skipped_list %} -

{% trans "Skipped files" %}

-

{% trans "Files skipped due to no .xml extension" %}

-
    - {% for filename in skipped_list %} -
  • {{ filename }}
  • - {% endfor %} -
-{% endif %} - -
-
- -{% endblock content %} - diff --git a/src/catalogue/templates/catalogue/image_detail.html b/src/catalogue/templates/catalogue/image_detail.html deleted file mode 100644 index e3902b89..00000000 --- a/src/catalogue/templates/catalogue/image_detail.html +++ /dev/null @@ -1,85 +0,0 @@ -{% extends "catalogue/base.html" %} -{% load book_list i18n %} -{% load bootstrap4 %} - - -{% block titleextra %}{{ object.title }}{% endblock %} - - -{% block content %} -
- -
-

{{ object.title }}

-
-
- - -{% if editable %}
{% csrf_token %}{% endif %} - {% bootstrap_form form %} - {% if editable %} - {% buttons %} - - {% endbuttons %} - {% endif %} - -{% if editable %}
{% endif %} - -
-
- - -
-
-

{% trans "Editor" %}

-
- -
- - - -
-
- -

{% trans "Publication" %}

-
-
- -

{% trans "Last published" %}: - {% if object.last_published %} - {{ object.last_published }} - {% else %} - — - {% endif %} -

- -{% if publishable %} - {% if user.is_authenticated %} - -
{% csrf_token %} - - - -
- {% else %} - {% trans "Log in to publish." %} - {% endif %} -{% else %} -

{% trans "This book can't be published yet, because:" %}

-
  • {{ publishable_error }}
-{% endif %} - -
-
- - -{% endblock content %} diff --git a/src/catalogue/templates/catalogue/image_list.html b/src/catalogue/templates/catalogue/image_list.html deleted file mode 100644 index d1c0d951..00000000 --- a/src/catalogue/templates/catalogue/image_list.html +++ /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' %} - -{% endblock %} - -{% block add_css %} - {% stylesheet 'book_list' %} - -{% 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 index c00481ec..00000000 --- a/src/catalogue/templates/catalogue/image_short.html +++ /dev/null @@ -1,27 +0,0 @@ -{% load i18n %} -{% load username from common_tags %} - - - - 🖼 - - {{ image.title }} - {% if image.stage %} - {{ image.stage }} - {% else %}– - {% endif %} - {% if image.user %}{{ image.user|username }}{% endif %} - - {% if image.published %} - opubl. - {% endif %} - {% if image.new_publishable %} - do publ. - {% endif %} - {% if image.changed %} - zmiany - {% endif %} - - {{ image.project.name }} - diff --git a/src/catalogue/templates/catalogue/image_table.html b/src/catalogue/templates/catalogue/image_table.html deleted file mode 100644 index 4f0a4250..00000000 --- a/src/catalogue/templates/catalogue/image_table.html +++ /dev/null @@ -1,102 +0,0 @@ -{% load i18n %} -{% load pagination_tags %} -{% load username from common_tags %} - -
-
- - -
- - -{% if not viewed_user %} - -{% endif %} - - -
- - - - - - - - - {% if not viewed_user %} - - {% endif %} - - - - - - - - {% autopaginate objects 100 as objects_page %} - - {% for image in objects_page %} - {% include 'catalogue/image_short.html' %} - {% endfor %} - -
-
- -
-
- {% paginate %} - {% blocktrans count c=objects|length %}{{c}} image{% plural %}{{c}} images{% endblocktrans %} -{% if not objects %} -

{% trans "No images found." %}

-{% endif %} - - - - - -
-
diff --git a/src/catalogue/templates/catalogue/main_tabs.html b/src/catalogue/templates/catalogue/main_tabs.html deleted file mode 100644 index eb74bbe0..00000000 --- a/src/catalogue/templates/catalogue/main_tabs.html +++ /dev/null @@ -1,3 +0,0 @@ -{% for tab in tabs %} - -{% endfor %} diff --git a/src/catalogue/templates/catalogue/mark_final.html b/src/catalogue/templates/catalogue/mark_final.html deleted file mode 100644 index 9ed740c0..00000000 --- a/src/catalogue/templates/catalogue/mark_final.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "catalogue/base.html" %} - -{% block titleextra %}Oznacz książki{% endblock %} - - -{% block leftcolumn %} - -

Oznacz książki

- -
- {% csrf_token %} - {{ form.as_p }} - -
- -{% 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 index 1b37c836..00000000 --- a/src/catalogue/templates/catalogue/mark_final_completed.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "catalogue/base.html" %} - -{% block titleextra %}Oznaczono książki{% endblock %} - - -{% block leftcolumn %} - -

Oznaczono książki

- -

Książki zostały oznaczone.

- -{% 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 index 2fec0a34..00000000 --- a/src/catalogue/templates/catalogue/my_page.html +++ /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' %} - -{% endblock %} - -{% block add_css %} - {% stylesheet 'book_list' %} - -{% endblock %} - -{% block titleextra %}{% trans "My page" %}{% endblock %} - - -{% block leftcolumn %} - {% book_list request.user %} -{% endblock leftcolumn %} - -{% block rightcolumn %} -
-
-

{% trans "Your last edited documents" %}

-
-
-
    - {% for edit_url, item in last_books %} -
  1. {{ item.title }}
    ({{ item.time|date:"H:i:s, d/m/Y" }})
  2. - {% endfor %} -
-
-
- -
-
-

{% trans "Recent activity for" %} {{ request.user|nice_name }}

-
-
- {% wall request.user 10 %} -
-{% endblock rightcolumn %} diff --git a/src/catalogue/templates/catalogue/upload_pdf.html b/src/catalogue/templates/catalogue/upload_pdf.html deleted file mode 100644 index 265b84ad..00000000 --- a/src/catalogue/templates/catalogue/upload_pdf.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "catalogue/base.html" %} -{% load i18n %} - - -{% block titleextra %}{% trans "PDF file upload" %}{% endblock %} - - -{% block content %} - - -

{% trans "PDF file upload" %}

- -
-{% csrf_token %} -{{ form.as_p }} -

-
- - -{% endblock content %} diff --git a/src/catalogue/templates/catalogue/user_list.html b/src/catalogue/templates/catalogue/user_list.html deleted file mode 100644 index b0607fa4..00000000 --- a/src/catalogue/templates/catalogue/user_list.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "catalogue/base.html" %} - -{% load i18n %} -{% load username from common_tags %} - - -{% block titleextra %}{% trans "Users" %}{% endblock %} - - -{% block content %} -
-
- -

{% trans "Users" %}

-
-
- - - -
-
- -{% endblock content %} diff --git a/src/catalogue/templates/catalogue/user_page.html b/src/catalogue/templates/catalogue/user_page.html deleted file mode 100644 index 85f30617..00000000 --- a/src/catalogue/templates/catalogue/user_page.html +++ /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 %} -

{{ viewed_user|nice_name }}

- {% book_list viewed_user %} -{% endblock leftcolumn %} - -{% block rightcolumn %} -
-
-

{% trans "Recent activity for" %} {{ viewed_user|nice_name }}

-
-
- - {% wall viewed_user 10 %} -
-
-{% endblock rightcolumn %} diff --git a/src/catalogue/templates/catalogue/wall.html b/src/catalogue/templates/catalogue/wall.html deleted file mode 100644 index a107dfa8..00000000 --- a/src/catalogue/templates/catalogue/wall.html +++ /dev/null @@ -1,37 +0,0 @@ -{% load i18n %} -{% load gravatar %} -{% load email %} -{% load username from common_tags %} - -
    -{% for item in wall %} -
  • -
    - {% if item.get_email %} - Avatar -
    - {% endif %} -
    - -
    {{ item.timestamp }}
    -

    {{ item.header }}

    - {{ item.title }} -
    {% trans "user" %}: - {% if item.user %} - - {{ item.user|username }} - <{{ item.user.email|email_link }}> - {% else %} - {{ item.user_name }} - {% if item.email %} - <{{ item.email|email_link }}> - {% endif %} - ({% trans "not logged in" %}) - {% endif %} -
    {{ item.summary|linebreaksbr }} -
  • -{% empty %} -
  • {% trans "No activity recorded." %}
  • -{% endfor %} -
diff --git a/src/catalogue/templatetags/__init__.py b/src/catalogue/templatetags/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/catalogue/templatetags/book_list.py b/src/catalogue/templatetags/book_list.py deleted file mode 100644 index db24c695..00000000 --- a/src/catalogue/templatetags/book_list.py +++ /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 index 0bfd95ab..00000000 --- a/src/catalogue/templatetags/catalogue.py +++ /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 index 5544232e..00000000 --- a/src/catalogue/templatetags/common_tags.py +++ /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 index 36e22454..00000000 --- a/src/catalogue/templatetags/set_get_parameter.py +++ /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 index 7543616a..00000000 --- a/src/catalogue/templatetags/wall.py +++ /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 index 7343d5c1..00000000 --- a/src/catalogue/test_utils.py +++ /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 index e69de29b..00000000 diff --git a/src/catalogue/tests/files/chunk1.xml b/src/catalogue/tests/files/chunk1.xml deleted file mode 100644 index 6a75580a..00000000 --- a/src/catalogue/tests/files/chunk1.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - -Mickiewicz, Adam -Do M*** -Fundacja Nowoczesna Polska -Romantyzm -Liryka -Wiersz - -http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m -http://www.polona.pl/Content/2222 -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 - -Domena publiczna - Adam Mickiewicz zm. 1855 -1926 -xml -text -text -2007-09-06 -pol - - - - -Adam Mickiewicz -Sonety odeskie -Do M*** - -Wiérsz napisany w roku 1822 - - -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. - - - - diff --git a/src/catalogue/tests/files/chunk2.xml b/src/catalogue/tests/files/chunk2.xml deleted file mode 100644 index 63a243e1..00000000 --- a/src/catalogue/tests/files/chunk2.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - -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. - - - - diff --git a/src/catalogue/tests/files/expected.xml b/src/catalogue/tests/files/expected.xml deleted file mode 100644 index ff225a03..00000000 --- a/src/catalogue/tests/files/expected.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - -Mickiewicz, Adam -Do M*** -Fundacja Nowoczesna Polska -Romantyzm -Liryka -Wiersz - -http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m -http://www.polona.pl/Content/2222 -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 - -Domena publiczna - Adam Mickiewicz zm. 1855 -1926 -xml -text -text -2007-09-06 -pol - - - - -Adam Mickiewicz -Sonety odeskie -Do M*** - -Wiérsz napisany w roku 1822 - - -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. - - - -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. - - - - diff --git a/src/catalogue/tests/test_book.py b/src/catalogue/tests/test_book.py deleted file mode 100644 index 1d26e4d3..00000000 --- a/src/catalogue/tests/test_book.py +++ /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 index 32c0e0ac..00000000 --- a/src/catalogue/tests/test_gallery.py +++ /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 index 930d88a1..00000000 --- a/src/catalogue/tests/test_publish.py +++ /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 index a5ce5671..00000000 --- a/src/catalogue/tests/test_xml_updater.py +++ /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 index 2cebacf0..00000000 --- a/src/catalogue/urls.py +++ /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[^/]+)/$', views.image, name="catalogue_image"), - url(r'^image/(?P[^/]+)/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[^/]+)/$', 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\d{4}-\d{2}-\d{2})/$', - views.activity, name='catalogue_activity'), - - url(r'^upload/$', - views.upload, name='catalogue_upload'), - - url(r'^create/(?P[^/]*)/', - views.create_missing, name='catalogue_create_missing'), - url(r'^create/', - views.create_missing, name='catalogue_create_missing'), - - url(r'^book/(?P[^/]+)/publish$', views.publish, name="catalogue_publish"), - - url(r'^book/(?P[^/]+)/$', views.book, name="catalogue_book"), - url(r'^book/(?P[^/]+)/gallery/$', - permission_required('catalogue.change_book')(views.GalleryView.as_view()), - name="catalogue_book_gallery"), - url(r'^book/(?P[^/]+)/xml$', views.book_xml, name="catalogue_book_xml"), - url(r'^book/dc/(?P[^/]+)/xml$', views.book_xml_dc, name="catalogue_book_xml_dc"), - url(r'^book/(?P[^/]+)/txt$', views.book_txt, name="catalogue_book_txt"), - url(r'^book/(?P[^/]+)/html$', views.book_html, name="catalogue_book_html"), - url(r'^book/(?P[^/]+)/epub$', views.book_epub, name="catalogue_book_epub"), - url(r'^book/(?P[^/]+)/mobi$', views.book_mobi, name="catalogue_book_mobi"), - url(r'^book/(?P[^/]+)/pdf$', views.book_pdf, name="catalogue_book_pdf"), - url(r'^book/(?P[^/]+)/pdf-mobile$', views.book_pdf, kwargs={'mobile': True}, name="catalogue_book_pdf_mobile"), - - url(r'^chunk_add/(?P[^/]+)/(?P[^/]+)/$', - views.chunk_add, name="catalogue_chunk_add"), - url(r'^chunk_edit/(?P[^/]+)/(?P[^/]+)/$', - views.chunk_edit, name="catalogue_chunk_edit"), - url(r'^book_append/(?P[^/]+)/$', - 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[^/]*)/$', 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 index 019d25a7..00000000 --- a/src/catalogue/views.py +++ /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 index 917e62ac..00000000 --- a/src/catalogue/xml_tools.py +++ /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("^$" % TRIM_BEGIN, re.M) -RE_TRIM_END = re.compile("^$" % 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 diff --git a/src/cover/templates/cover/add_image.html b/src/cover/templates/cover/add_image.html index ba981844..56f5665e 100644 --- a/src/cover/templates/cover/add_image.html +++ b/src/cover/templates/cover/add_image.html @@ -1,4 +1,4 @@ -{% extends "catalogue/base.html" %} +{% extends "documents/base.html" %} {% load i18n %} {% load bootstrap4 %} diff --git a/src/cover/templates/cover/image_detail.html b/src/cover/templates/cover/image_detail.html index db9b176f..670b3ce5 100644 --- a/src/cover/templates/cover/image_detail.html +++ b/src/cover/templates/cover/image_detail.html @@ -1,4 +1,4 @@ -{% extends "catalogue/base.html" %} +{% extends "documents/base.html" %} {% load i18n %} {% load thumbnail %} {% load build_absolute_uri from fnp_common %} diff --git a/src/cover/templates/cover/image_list.html b/src/cover/templates/cover/image_list.html index 87971df2..2d5608c0 100644 --- a/src/cover/templates/cover/image_list.html +++ b/src/cover/templates/cover/image_list.html @@ -1,4 +1,4 @@ -{% extends "catalogue/base.html" %} +{% extends "documents/base.html" %} {% load i18n %} {% load thumbnail pagination_tags %} diff --git a/src/cover/views.py b/src/cover/views.py index b1261e6d..bfc3ee33 100644 --- a/src/cover/views.py +++ b/src/cover/views.py @@ -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 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 diff --git a/src/documents/__init__.py b/src/documents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/documents/admin.py b/src/documents/admin.py new file mode 100644 index 00000000..b793303d --- /dev/null +++ b/src/documents/admin.py @@ -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 index 00000000..4775d187 --- /dev/null +++ b/src/documents/constants.py @@ -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 index 00000000..e75ed777 --- /dev/null +++ b/src/documents/ebook_utils.py @@ -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 index 00000000..bf9f2dbf --- /dev/null +++ b/src/documents/feeds.py @@ -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 index 00000000..11456691 --- /dev/null +++ b/src/documents/fixtures/stages.json @@ -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 index 00000000..f5f2901d --- /dev/null +++ b/src/documents/forms.py @@ -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 index 00000000..df15b91d --- /dev/null +++ b/src/documents/helpers.py @@ -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 index 00000000..395b34ef 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 index 00000000..ac60fb22 --- /dev/null +++ b/src/documents/locale/pl/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: Fundacja Nowoczesna Polska \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 " +".xml will be ignored." +msgstr "" +"Proszę wskazać archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie " +"kończące się na .xml 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 .xml extension" +msgstr "Pliki pominięte z powodu braku rozszerzenia .xml." + +#: 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 index 00000000..8c38828d --- /dev/null +++ b/src/documents/management/__init__.py @@ -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 index 00000000..22469b98 --- /dev/null +++ b/src/documents/management/commands/__init__.py @@ -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 index 00000000..59767bf6 --- /dev/null +++ b/src/documents/management/commands/add_parent.py @@ -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 = ''' + + +%(dc)s + + + + +''' + +DC_TEMPLATE = '%(value)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 index 00000000..3f4a848e --- /dev/null +++ b/src/documents/management/commands/fixdc.py @@ -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 index 00000000..299d4c54 --- /dev/null +++ b/src/documents/management/commands/import_wl.py @@ -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 index 00000000..536d30ff --- /dev/null +++ b/src/documents/management/commands/insert_isbn.py @@ -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'%(url)s' + r'', + r'ISBN-%(isbn)s', + r'ISBN', + r'%(content_type)s', +) + + +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 index 00000000..b2c316c9 --- /dev/null +++ b/src/documents/management/commands/mark_final.py @@ -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 index 00000000..5dc6cb01 --- /dev/null +++ b/src/documents/management/commands/merge_books.py @@ -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\d*)$'), None)) + res.append((re.compile(r'__?(rozdzialy__?)?(?P\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 index 00000000..743a58a9 --- /dev/null +++ b/src/documents/management/commands/prune_audience.py @@ -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 index 00000000..137baaf5 --- /dev/null +++ b/src/documents/managers.py @@ -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 index 00000000..48b6d070 --- /dev/null +++ b/src/documents/models/__init__.py @@ -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 index 00000000..e7c34814 --- /dev/null +++ b/src/documents/models/book.py @@ -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 index 00000000..39a20871 --- /dev/null +++ b/src/documents/models/chunk.py @@ -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 index 00000000..10782dcf --- /dev/null +++ b/src/documents/models/image.py @@ -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 index 00000000..d306e4e9 --- /dev/null +++ b/src/documents/models/listeners.py @@ -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 index 00000000..f0ac752f --- /dev/null +++ b/src/documents/models/project.py @@ -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 index 00000000..5322bb62 --- /dev/null +++ b/src/documents/models/publish_log.py @@ -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 index 00000000..852dc964 --- /dev/null +++ b/src/documents/signals.py @@ -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 index 00000000..ef9532fc --- /dev/null +++ b/src/documents/templates/documents/active_users_list.html @@ -0,0 +1,20 @@ +{% extends "documents/base.html" %} +{% load i18n %} + + +{% block titleextra %}{% trans "Active users" %}{% endblock %} + + +{% block content %} + +

+ {% trans "Users active in the year" %} {{ year }} +

+ +
    +{% for email, names, count in users %} +
  • {% for name in names %}{{ name }}, {% endfor %}{{ email }} ({{ count }})
  • +{% endfor %} +
+ +{% endblock content %} diff --git a/src/documents/templates/documents/activity.html b/src/documents/templates/documents/activity.html new file mode 100644 index 00000000..e99ee189 --- /dev/null +++ b/src/documents/templates/documents/activity.html @@ -0,0 +1,25 @@ +{% extends "documents/base.html" %} +{% load i18n %} +{% load wall %} + + +{% block titleextra %}{% trans "Activity" %}{% endblock %} + + +{% block content %} + +
+
+

+ < + {% trans "Activity" %}: {{ day }} + {% if next_day %} + > + {% endif %} +

+
+
+ + {% day_wall day %} +
+{% endblock content %} diff --git a/src/documents/templates/documents/base.html b/src/documents/templates/documents/base.html new file mode 100644 index 00000000..81af8933 --- /dev/null +++ b/src/documents/templates/documents/base.html @@ -0,0 +1,60 @@ + +{% load pipeline i18n %} +{% load static %} +{% load documents %} + + + + + + + {% stylesheet 'documents' %} + {% block title %}{% block titleextra %}{% endblock %} :: + {% trans "Platforma Redakcyjna" %}{% endblock title %} + {% block add_css %}{% endblock %} + + + + + + + +
+ {% block content %} +
+
+ {% block leftcolumn %} + {% endblock leftcolumn %} +
+
+ {% block rightcolumn %} + {% endblock rightcolumn %} +
+
+ {% endblock content %} +
+ + + + + +{% javascript 'documents' %} +{% block add_js %}{% endblock %} +{% block extrabody %} +{% endblock %} + + diff --git a/src/documents/templates/documents/book_append_to.html b/src/documents/templates/documents/book_append_to.html new file mode 100644 index 00000000..121cfabc --- /dev/null +++ b/src/documents/templates/documents/book_append_to.html @@ -0,0 +1,19 @@ +{% extends "documents/base.html" %} +{% load i18n %} +{% load bootstrap4 %} + +{% block titleextra %}{% trans "Append book" %}{% endblock %} + +{% block content %} +
+
+
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
+
+{% endblock content %} + diff --git a/src/documents/templates/documents/book_detail.html b/src/documents/templates/documents/book_detail.html new file mode 100644 index 00000000..3dbaf213 --- /dev/null +++ b/src/documents/templates/documents/book_detail.html @@ -0,0 +1,119 @@ +{% extends "documents/base.html" %} +{% load book_list i18n %} +{% load bootstrap4 %} + + +{% block titleextra %}{{ book.title }}{% endblock %} + + +{% block content %} + +
+
+

{{ book.title }}

+
+
+ + + +{% if editable %}
{% csrf_token %}{% endif %} + {% bootstrap_form form %} + {% if editable %} + {% buttons %} + + {% endbuttons %} + {% endif %} +{% if editable %}
{% endif %} + +{% if editable %} + {% if book.gallery %} +

{% trans "Edit gallery" %}

+ {% endif %} + +

{% trans "Append to other book" %}

+{% endif %} +
+
+ +
+
+

{% trans "Chunks" %}

+
+
+ + + {% for chunk in book %} + {% include 'documents/book_list/chunk.html' %} + {% endfor %} +
+
+
+ + + + +
+ +
+

{% trans "Publication" %}

+
+
+
+
+ +{% if book.dc_cover_image %} + {{ book.dc_cover_image }} +{% endif %} +

+
+ +Okładka w rozmiarze + x + +
+
+
+

{% trans "Last published" %}: + {% if book.last_published %} + {{ book.last_published }} + {% else %} + — + {% endif %} +

+ +{% if publishable %} +

+ {% trans "Full XML" %}
+ {% trans "HTML version" %}
+ {% trans "TXT version" %}
+ {% trans "PDF version" %}
+ {% trans "PDF version for mobiles" %}
+ {% trans "EPUB version" %}
+ {% trans "MOBI version" %}
+

+ + {% if user.is_authenticated %} + +
{% csrf_token %} + {{ publish_options_form.as_p }} + + + +
+ {% else %} + {% trans "Log in to publish." %} + {% endif %} +{% else %} +

{% trans "This book can't be published yet, because:" %}

+
  • {{ publishable_error }}
+{% endif %} + +
+
+{% endblock content %} diff --git a/src/documents/templates/documents/book_edit.html b/src/documents/templates/documents/book_edit.html new file mode 100644 index 00000000..37cd8357 --- /dev/null +++ b/src/documents/templates/documents/book_edit.html @@ -0,0 +1,18 @@ +{% extends "documents/base.html" %} +{% load i18n %} + + +{% block titleextra %}{% trans "Edit book" %}{% endblock %} + + +{% block leftcolumn %} +
+ {% csrf_token %} + {{ form.as_p }} + +

+
+{% 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 index 00000000..518811ee --- /dev/null +++ b/src/documents/templates/documents/book_html.html @@ -0,0 +1,29 @@ +{% load i18n %} + + + + + {{ book.title }} + + + +
+ {#% book_info book %#} +
+ + + {{ html|safe }} + + + diff --git a/src/documents/templates/documents/book_list/book.html b/src/documents/templates/documents/book_list/book.html new file mode 100644 index 00000000..9de8657f --- /dev/null +++ b/src/documents/templates/documents/book_list/book.html @@ -0,0 +1,50 @@ +{% load i18n %} +{% load username from common_tags %} + +{% if book.single %} + {% with chunk as chunk %} + + + 📕 + 📜 + + {{ book.title }} + {% if chunk.stage %} + {{ chunk.stage }} + {% else %}– + {% endif %} + {% if chunk.user %}{{ chunk.user|username }}{% endif %} + + {% if book.published %} + opubl. + {% endif %} + {% if book.new_publishable %} + do publ. + {% endif %} + {% if chunk.changed %} + zmiany + {% endif %} + + {{ book.project.name }} + + {% endwith %} +{% else %} + + + 📕 + + {{ book.title }} + + + + {% if book.published %} + opubl. + {% endif %} + {% if book.new_publishable %} + do publ. + {% endif %} + + {{ book.project.name }} + +{% 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 index 00000000..eb4c8f5d --- /dev/null +++ b/src/documents/templates/documents/book_list/book_list.html @@ -0,0 +1,117 @@ +{% load i18n %} +{% load pagination_tags %} +{% load username from common_tags %} + + +
+ + +{% if not viewed_user %} + +{% endif %} + + + +
+ + +
+
+ + + + + + + + + + + {% if not viewed_user %} + + {% else %} + + {% endif %} + + + + + + + + {% autopaginate books 100 as books_page %} + + {% 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 %} + +
+
+ +
+
+{% paginate %} + {% blocktrans count c=books|length %}{{c}} book{% plural %}{{c}} books{% endblocktrans %} + + +{% if not books %} +

{% trans "No books found." %}

+{% endif %} + +
+
+ + + + diff --git a/src/documents/templates/documents/book_list/chunk.html b/src/documents/templates/documents/book_list/chunk.html new file mode 100644 index 00000000..8875bb60 --- /dev/null +++ b/src/documents/templates/documents/book_list/chunk.html @@ -0,0 +1,30 @@ +{% load i18n %} +{% load username from common_tags %} + + + + + 📜 + + {{ chunk.number }}. + {{ chunk.title }} + {% if chunk.stage %} + {{ chunk.stage }} + {% else %} + – + {% endif %} + {% if chunk.user %} + + {{ chunk.user|username }} + {% else %} + + {% endif %} + + + + {% if chunk.changed %} + zmiany + {% endif %} + + + diff --git a/src/documents/templates/documents/book_text.html b/src/documents/templates/documents/book_text.html new file mode 100644 index 00000000..a88aad50 --- /dev/null +++ b/src/documents/templates/documents/book_text.html @@ -0,0 +1,28 @@ +{% load i18n pipeline %} + + + + + {% trans "Redakcja" %} :: {{ book.title }} + {% stylesheet 'book' %} + + {% javascript 'book' %} + + + + + {{ html|safe }} + + diff --git a/src/documents/templates/documents/chunk_add.html b/src/documents/templates/documents/chunk_add.html new file mode 100644 index 00000000..b5944b29 --- /dev/null +++ b/src/documents/templates/documents/chunk_add.html @@ -0,0 +1,29 @@ +{% extends "documents/base.html" %} +{% load i18n %} +{% load bootstrap4 %} + + +{% block titleextra %}{% trans "Split chunk" %}{% endblock %} + + +{% block content %} +
+
+

{% trans "Split chunk" %}

+
+
+ +
+ {% csrf_token %} +
+

{% trans "Insert empty chunk after" %}: + {{ chunk.pretty_name }}

+ {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
+
+
+
+{% endblock content %} diff --git a/src/documents/templates/documents/chunk_edit.html b/src/documents/templates/documents/chunk_edit.html new file mode 100644 index 00000000..f68787b5 --- /dev/null +++ b/src/documents/templates/documents/chunk_edit.html @@ -0,0 +1,33 @@ +{% extends "documents/base.html" %} +{% load i18n %} +{% load bootstrap4 %} + + +{% block titleextra %}{% trans "Chunk settings" %}{% endblock %} + + +{% block content %} +
+
+

{% trans "Chunk settings" %}

+
+
+ +
+ {% csrf_token %} +
+

{% trans "Book" %}: {{ chunk.book }} ({{ chunk.number }}/{{ chunk.book|length }})

+ {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
+ +
+ + +

{% trans "Split chunk" %}

+
+
+ +{% 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 index 00000000..d003394c --- /dev/null +++ b/src/documents/templates/documents/document_create_missing.html @@ -0,0 +1,26 @@ +{% extends "documents/base.html" %} +{% load i18n %} +{% load bootstrap4 %} + + +{% block titleextra %}{% trans "Create a new book" %}{% endblock %} + + +{% block content %} +
+
+

{% trans "Create a new book" %}

+
+
+ + +
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
+
+
+{% endblock content %} diff --git a/src/documents/templates/documents/document_list.html b/src/documents/templates/documents/document_list.html new file mode 100644 index 00000000..633f8f47 --- /dev/null +++ b/src/documents/templates/documents/document_list.html @@ -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' %} + +{% endblock %} + +{% block add_css %} + {% stylesheet 'book_list' %} + +{% 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 index 00000000..60972b3a --- /dev/null +++ b/src/documents/templates/documents/document_upload.html @@ -0,0 +1,78 @@ +{% extends "documents/base.html" %} +{% load i18n %} +{% load bootstrap4 %} + + +{% block titleextra %}{% trans "Bulk document upload" %}{% endblock %} + + +{% block content %} + + +
+
+

{% trans "Bulk documents upload" %}

+
+
+ +

+{% trans "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with .xml will be ignored." %} +

+ +
+{% csrf_token %} +{% bootstrap_form form %} +{% buttons %} + +{% endbuttons %} +
+ + +{% if error_list %} +
+ +

{% trans "There have been some errors. No files have been added to the repository." %} +

{% trans "Offending files" %}

+
    + {% for filename, title, error in error_list %} +
  • {{ title }} ({{ filename }}): {{ error }}
  • + {% endfor %} +
+ + {% if ok_list %} +

{% trans "Correct files" %}

+
    + {% for filename, slug, title in ok_list %} +
  • {{ title }} ({{ filename }})
  • + {% endfor %} +
+ {% endif %} + +{% else %} + + {% if ok_list %} +

{% trans "Files have been successfully uploaded to the repository." %}

+

{% trans "Uploaded files" %}

+
    + {% for filename, slug, title in ok_list %} +
  • {{ title }} ({{ filename }})
  • + {% endfor %} +
+ {% endif %} +{% endif %} + +{% if skipped_list %} +

{% trans "Skipped files" %}

+

{% trans "Files skipped due to no .xml extension" %}

+
    + {% for filename in skipped_list %} +
  • {{ filename }}
  • + {% endfor %} +
+{% endif %} + +
+
+ +{% endblock content %} + diff --git a/src/documents/templates/documents/image_detail.html b/src/documents/templates/documents/image_detail.html new file mode 100644 index 00000000..2ea83e07 --- /dev/null +++ b/src/documents/templates/documents/image_detail.html @@ -0,0 +1,85 @@ +{% extends "documents/base.html" %} +{% load book_list i18n %} +{% load bootstrap4 %} + + +{% block titleextra %}{{ object.title }}{% endblock %} + + +{% block content %} +
+ +
+

{{ object.title }}

+
+
+ + +{% if editable %}
{% csrf_token %}{% endif %} + {% bootstrap_form form %} + {% if editable %} + {% buttons %} + + {% endbuttons %} + {% endif %} + +{% if editable %}
{% endif %} + +
+
+ + +
+
+

{% trans "Editor" %}

+
+ +
+ + + +
+
+ +

{% trans "Publication" %}

+
+
+ +

{% trans "Last published" %}: + {% if object.last_published %} + {{ object.last_published }} + {% else %} + — + {% endif %} +

+ +{% if publishable %} + {% if user.is_authenticated %} + +
{% csrf_token %} + + + +
+ {% else %} + {% trans "Log in to publish." %} + {% endif %} +{% else %} +

{% trans "This book can't be published yet, because:" %}

+
  • {{ publishable_error }}
+{% endif %} + +
+
+ + +{% endblock content %} diff --git a/src/documents/templates/documents/image_list.html b/src/documents/templates/documents/image_list.html new file mode 100644 index 00000000..a33921a7 --- /dev/null +++ b/src/documents/templates/documents/image_list.html @@ -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' %} + +{% endblock %} + +{% block add_css %} + {% stylesheet 'book_list' %} + +{% 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 index 00000000..c10c7f0a --- /dev/null +++ b/src/documents/templates/documents/image_short.html @@ -0,0 +1,27 @@ +{% load i18n %} +{% load username from common_tags %} + + + + 🖼 + + {{ image.title }} + {% if image.stage %} + {{ image.stage }} + {% else %}– + {% endif %} + {% if image.user %}{{ image.user|username }}{% endif %} + + {% if image.published %} + opubl. + {% endif %} + {% if image.new_publishable %} + do publ. + {% endif %} + {% if image.changed %} + zmiany + {% endif %} + + {{ image.project.name }} + diff --git a/src/documents/templates/documents/image_table.html b/src/documents/templates/documents/image_table.html new file mode 100644 index 00000000..77a71883 --- /dev/null +++ b/src/documents/templates/documents/image_table.html @@ -0,0 +1,102 @@ +{% load i18n %} +{% load pagination_tags %} +{% load username from common_tags %} + +
+
+ + +
+ + +{% if not viewed_user %} + +{% endif %} + + +
+ + + + + + + + + {% if not viewed_user %} + + {% endif %} + + + + + + + + {% autopaginate objects 100 as objects_page %} + + {% for image in objects_page %} + {% include 'documents/image_short.html' %} + {% endfor %} + +
+
+ +
+
+ {% paginate %} + {% blocktrans count c=objects|length %}{{c}} image{% plural %}{{c}} images{% endblocktrans %} +{% if not objects %} +

{% trans "No images found." %}

+{% endif %} + + + + + +
+
diff --git a/src/documents/templates/documents/main_tabs.html b/src/documents/templates/documents/main_tabs.html new file mode 100644 index 00000000..eb74bbe0 --- /dev/null +++ b/src/documents/templates/documents/main_tabs.html @@ -0,0 +1,3 @@ +{% for tab in tabs %} + +{% endfor %} diff --git a/src/documents/templates/documents/mark_final.html b/src/documents/templates/documents/mark_final.html new file mode 100644 index 00000000..3e71978c --- /dev/null +++ b/src/documents/templates/documents/mark_final.html @@ -0,0 +1,16 @@ +{% extends "documents/base.html" %} + +{% block titleextra %}Oznacz książki{% endblock %} + + +{% block leftcolumn %} + +

Oznacz książki

+ +
+ {% csrf_token %} + {{ form.as_p }} + +
+ +{% 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 index 00000000..b05637ab --- /dev/null +++ b/src/documents/templates/documents/mark_final_completed.html @@ -0,0 +1,12 @@ +{% extends "documents/base.html" %} + +{% block titleextra %}Oznaczono książki{% endblock %} + + +{% block leftcolumn %} + +

Oznaczono książki

+ +

Książki zostały oznaczone.

+ +{% endblock leftcolumn %} diff --git a/src/documents/templates/documents/my_page.html b/src/documents/templates/documents/my_page.html new file mode 100644 index 00000000..6828706a --- /dev/null +++ b/src/documents/templates/documents/my_page.html @@ -0,0 +1,52 @@ +{% extends "documents/base.html" %} + +{% load i18n %} +{% load documents book_list wall %} +{% load pipeline %} + +{% block add_js %} + {% javascript 'book_list' %} + +{% endblock %} + +{% block add_css %} + {% stylesheet 'book_list' %} + +{% endblock %} + +{% block titleextra %}{% trans "My page" %}{% endblock %} + + +{% block leftcolumn %} + {% book_list request.user %} +{% endblock leftcolumn %} + +{% block rightcolumn %} +
+
+

{% trans "Your last edited documents" %}

+
+
+
    + {% for edit_url, item in last_books %} +
  1. {{ item.title }}
    ({{ item.time|date:"H:i:s, d/m/Y" }})
  2. + {% endfor %} +
+
+
+ +
+
+

{% trans "Recent activity for" %} {{ request.user|nice_name }}

+
+
+ {% wall request.user 10 %} +
+{% endblock rightcolumn %} diff --git a/src/documents/templates/documents/upload_pdf.html b/src/documents/templates/documents/upload_pdf.html new file mode 100644 index 00000000..ea74e922 --- /dev/null +++ b/src/documents/templates/documents/upload_pdf.html @@ -0,0 +1,20 @@ +{% extends "documents/base.html" %} +{% load i18n %} + + +{% block titleextra %}{% trans "PDF file upload" %}{% endblock %} + + +{% block content %} + + +

{% trans "PDF file upload" %}

+ +
+{% csrf_token %} +{{ form.as_p }} +

+
+ + +{% endblock content %} diff --git a/src/documents/templates/documents/user_list.html b/src/documents/templates/documents/user_list.html new file mode 100644 index 00000000..f0e9ab25 --- /dev/null +++ b/src/documents/templates/documents/user_list.html @@ -0,0 +1,30 @@ +{% extends "documents/base.html" %} + +{% load i18n %} +{% load username from common_tags %} + + +{% block titleextra %}{% trans "Users" %}{% endblock %} + + +{% block content %} +
+
+ +

{% trans "Users" %}

+
+
+ + + +
+
+ +{% endblock content %} diff --git a/src/documents/templates/documents/user_page.html b/src/documents/templates/documents/user_page.html new file mode 100644 index 00000000..3be2c57c --- /dev/null +++ b/src/documents/templates/documents/user_page.html @@ -0,0 +1,25 @@ +{% extends "documents/base.html" %} + +{% load i18n %} +{% load documents book_list wall %} + + +{% block titleextra %}{{ viewed_user|nice_name }}{% endblock %} + + +{% block leftcolumn %} +

{{ viewed_user|nice_name }}

+ {% book_list viewed_user %} +{% endblock leftcolumn %} + +{% block rightcolumn %} +
+
+

{% trans "Recent activity for" %} {{ viewed_user|nice_name }}

+
+
+ + {% wall viewed_user 10 %} +
+
+{% endblock rightcolumn %} diff --git a/src/documents/templates/documents/wall.html b/src/documents/templates/documents/wall.html new file mode 100644 index 00000000..5e2ed9a0 --- /dev/null +++ b/src/documents/templates/documents/wall.html @@ -0,0 +1,37 @@ +{% load i18n %} +{% load gravatar %} +{% load email %} +{% load username from common_tags %} + +
    +{% for item in wall %} +
  • +
    + {% if item.get_email %} + Avatar +
    + {% endif %} +
    + +
    {{ item.timestamp }}
    +

    {{ item.header }}

    + {{ item.title }} +
    {% trans "user" %}: + {% if item.user %} + + {{ item.user|username }} + <{{ item.user.email|email_link }}> + {% else %} + {{ item.user_name }} + {% if item.email %} + <{{ item.email|email_link }}> + {% endif %} + ({% trans "not logged in" %}) + {% endif %} +
    {{ item.summary|linebreaksbr }} +
  • +{% empty %} +
  • {% trans "No activity recorded." %}
  • +{% endfor %} +
diff --git a/src/documents/templatetags/__init__.py b/src/documents/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/documents/templatetags/book_list.py b/src/documents/templatetags/book_list.py new file mode 100644 index 00000000..acd22083 --- /dev/null +++ b/src/documents/templatetags/book_list.py @@ -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 index 00000000..5544232e --- /dev/null +++ b/src/documents/templatetags/common_tags.py @@ -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 index 00000000..5c3b1eb8 --- /dev/null +++ b/src/documents/templatetags/documents.py @@ -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 index 00000000..36e22454 --- /dev/null +++ b/src/documents/templatetags/set_get_parameter.py @@ -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 index 00000000..4a6795be --- /dev/null +++ b/src/documents/templatetags/wall.py @@ -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 index 00000000..7343d5c1 --- /dev/null +++ b/src/documents/test_utils.py @@ -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 index 00000000..e69de29b diff --git a/src/documents/tests/files/chunk1.xml b/src/documents/tests/files/chunk1.xml new file mode 100644 index 00000000..6a75580a --- /dev/null +++ b/src/documents/tests/files/chunk1.xml @@ -0,0 +1,42 @@ + + + + + +Mickiewicz, Adam +Do M*** +Fundacja Nowoczesna Polska +Romantyzm +Liryka +Wiersz + +http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m +http://www.polona.pl/Content/2222 +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 + +Domena publiczna - Adam Mickiewicz zm. 1855 +1926 +xml +text +text +2007-09-06 +pol + + + + +Adam Mickiewicz +Sonety odeskie +Do M*** + +Wiérsz napisany w roku 1822 + + +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. + + + + diff --git a/src/documents/tests/files/chunk2.xml b/src/documents/tests/files/chunk2.xml new file mode 100644 index 00000000..63a243e1 --- /dev/null +++ b/src/documents/tests/files/chunk2.xml @@ -0,0 +1,11 @@ + + + +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. + + + + diff --git a/src/documents/tests/files/expected.xml b/src/documents/tests/files/expected.xml new file mode 100644 index 00000000..ff225a03 --- /dev/null +++ b/src/documents/tests/files/expected.xml @@ -0,0 +1,49 @@ + + + + + +Mickiewicz, Adam +Do M*** +Fundacja Nowoczesna Polska +Romantyzm +Liryka +Wiersz + +http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m +http://www.polona.pl/Content/2222 +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 + +Domena publiczna - Adam Mickiewicz zm. 1855 +1926 +xml +text +text +2007-09-06 +pol + + + + +Adam Mickiewicz +Sonety odeskie +Do M*** + +Wiérsz napisany w roku 1822 + + +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. + + + +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. + + + + diff --git a/src/documents/tests/test_book.py b/src/documents/tests/test_book.py new file mode 100644 index 00000000..c423d9e4 --- /dev/null +++ b/src/documents/tests/test_book.py @@ -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 index 00000000..ba75b95b --- /dev/null +++ b/src/documents/tests/test_gallery.py @@ -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 index 00000000..b59136a1 --- /dev/null +++ b/src/documents/tests/test_publish.py @@ -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 index 00000000..ef0c8734 --- /dev/null +++ b/src/documents/tests/test_xml_updater.py @@ -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 index 00000000..602d4f22 --- /dev/null +++ b/src/documents/urls.py @@ -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[^/]+)/$', views.image, name="documents_image"), + url(r'^image/(?P[^/]+)/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[^/]+)/$', 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\d{4}-\d{2}-\d{2})/$', + views.activity, name='documents_activity'), + + url(r'^upload/$', + views.upload, name='documents_upload'), + + url(r'^create/(?P[^/]*)/', + views.create_missing, name='documents_create_missing'), + url(r'^create/', + views.create_missing, name='documents_create_missing'), + + url(r'^book/(?P[^/]+)/publish$', views.publish, name="documents_publish"), + + url(r'^book/(?P[^/]+)/$', views.book, name="documents_book"), + url(r'^book/(?P[^/]+)/gallery/$', + permission_required('documents.change_book')(views.GalleryView.as_view()), + name="documents_book_gallery"), + url(r'^book/(?P[^/]+)/xml$', views.book_xml, name="documents_book_xml"), + url(r'^book/dc/(?P[^/]+)/xml$', views.book_xml_dc, name="documents_book_xml_dc"), + url(r'^book/(?P[^/]+)/txt$', views.book_txt, name="documents_book_txt"), + url(r'^book/(?P[^/]+)/html$', views.book_html, name="documents_book_html"), + url(r'^book/(?P[^/]+)/epub$', views.book_epub, name="documents_book_epub"), + url(r'^book/(?P[^/]+)/mobi$', views.book_mobi, name="documents_book_mobi"), + url(r'^book/(?P[^/]+)/pdf$', views.book_pdf, name="documents_book_pdf"), + url(r'^book/(?P[^/]+)/pdf-mobile$', views.book_pdf, kwargs={'mobile': True}, name="documents_book_pdf_mobile"), + + url(r'^chunk_add/(?P[^/]+)/(?P[^/]+)/$', + views.chunk_add, name="documents_chunk_add"), + url(r'^chunk_edit/(?P[^/]+)/(?P[^/]+)/$', + views.chunk_edit, name="documents_chunk_edit"), + url(r'^book_append/(?P[^/]+)/$', + 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[^/]*)/$', 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 index 00000000..397d78bd --- /dev/null +++ b/src/documents/views.py @@ -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 index 00000000..f2c885d9 --- /dev/null +++ b/src/documents/xml_tools.py @@ -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("^$" % TRIM_BEGIN, re.M) +RE_TRIM_END = re.compile("^$" % 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 diff --git a/src/fileupload/templates/fileupload/picture_form.html b/src/fileupload/templates/fileupload/picture_form.html index 44c37db7..dc49f9c9 100644 --- a/src/fileupload/templates/fileupload/picture_form.html +++ b/src/fileupload/templates/fileupload/picture_form.html @@ -1,4 +1,4 @@ -{% extends "catalogue/base.html" %} +{% extends "documents/base.html" %} {% load i18n %} {% load upload_tags %} diff --git a/src/redakcja/settings/__init__.py b/src/redakcja/settings/__init__.py index 51dec4da..8b7b9c4b 100644 --- a/src/redakcja/settings/__init__.py +++ b/src/redakcja/settings/__init__.py @@ -89,6 +89,7 @@ INSTALLED_APPS = ( 'bootstrap4', 'catalogue', + 'documents', 'cover', 'dvcs', 'wiki', @@ -138,11 +139,11 @@ PIPELINE = { ), 'output_filename': 'compressed/detail_styles.css', }, - 'catalogue': { + 'documents': { 'source_filenames': ( 'css/filelist.css', ), - 'output_filename': 'compressed/catalogue_styles.css', + 'output_filename': 'compressed/documents_styles.css', }, 'book': { 'source_filenames': ( @@ -226,13 +227,13 @@ PIPELINE = { ), 'output_filename': 'compressed/detail_img_scripts.js', }, - 'catalogue': { + 'documents': { 'source_filenames': ( - 'js/catalogue/catalogue.js', + 'js/documents/documents.js', 'js/slugify.js', 'email_mangler/email_mangler.js', ), - 'output_filename': 'compressed/catalogue_scripts.js', + 'output_filename': 'compressed/documents_scripts.js', }, 'book': { 'source_filenames': ( @@ -246,7 +247,7 @@ PIPELINE = { }, 'book_list': { 'source_filenames': ( - 'js/catalogue/book_list.js', + 'js/documents/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 index 9d2511d4..00000000 --- a/src/redakcja/static/js/catalogue/book_list.js +++ /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 index 9d2bd958..00000000 --- a/src/redakcja/static/js/catalogue/catalogue.js +++ /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 index 00000000..9d2511d4 --- /dev/null +++ b/src/redakcja/static/js/documents/book_list.js @@ -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 index 00000000..9d2bd958 --- /dev/null +++ b/src/redakcja/static/js/documents/documents.js @@ -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); + diff --git a/src/redakcja/templates/404.html b/src/redakcja/templates/404.html index 11edc9d5..a4fd20de 100644 --- a/src/redakcja/templates/404.html +++ b/src/redakcja/templates/404.html @@ -1,4 +1,4 @@ -{% extends "catalogue/base.html" %} +{% extends "documents/base.html" %} {% load i18n %} {% block titleextra %}{% trans "Page not found" %}{% endblock %} @@ -17,7 +17,7 @@ still can't find what you're looking for, please contact the administrator.{% endblocktrans %}

-{% url "catalogue_user" as m %} +{% url "documents_user" as m %}

{% blocktrans %}If you're coming from Redmine, please note that work is no longer managed there. diff --git a/src/redakcja/templates/registration/login.html b/src/redakcja/templates/registration/login.html index 6ba9f759..7bef2eb8 100644 --- a/src/redakcja/templates/registration/login.html +++ b/src/redakcja/templates/registration/login.html @@ -1,4 +1,4 @@ -{% extends "catalogue/base.html" %} +{% extends "documents/base.html" %} {% block titleextra %}Logowanie{% endblock %} {% block subtitle %} - Logowanie {% endblock subtitle %} diff --git a/src/redakcja/urls.py b/src/redakcja/urls.py index 9f5d6e03..af8b4f26 100644 --- a/src/redakcja/urls.py +++ b/src/redakcja/urls.py @@ -20,7 +20,7 @@ urlpatterns = [ 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')), diff --git a/src/wiki/forms.py b/src/wiki/forms.py index 6f6defc6..084ae46d 100644 --- a/src/wiki/forms.py +++ b/src/wiki/forms.py @@ -4,7 +4,7 @@ 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): diff --git a/src/wiki/templates/wiki/document_details_base.html b/src/wiki/templates/wiki/document_details_base.html index 8b79d9aa..6cf25432 100644 --- a/src/wiki/templates/wiki/document_details_base.html +++ b/src/wiki/templates/wiki/document_details_base.html @@ -33,7 +33,7 @@