+++ /dev/null
-# 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)
+++ /dev/null
-# 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',
- ]
+++ /dev/null
-# 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
+++ /dev/null
-# 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
+++ /dev/null
-[
- {
- "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"
- }
- }
-]
+++ /dev/null
-# 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)
+++ /dev/null
-# 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
+++ /dev/null
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: Platforma Redakcyjna\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-10-07 13:05+0200\n"
-"PO-Revision-Date: 2019-10-07 13:06+0200\n"
-"Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
-"Language-Team: Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org."
-"pl>\n"
-"Language: pl\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
-"|| n%100>=20) ? 1 : 2);\n"
-"X-Generator: Poedit 2.0.6\n"
-
-#: forms.py:38
-msgid "Text file must be UTF-8 encoded."
-msgstr "Plik powinien mieć kodowanie UTF-8."
-
-#: forms.py:41
-msgid "You must either enter text or upload a file"
-msgstr "Proszę wpisać tekst albo wybrać plik do załadowania"
-
-#: forms.py:50
-msgid "ZIP file"
-msgstr "Plik ZIP"
-
-#: forms.py:51
-msgid "Directories are documents in chunks"
-msgstr "Katalogi zawierają dokumenty w częściach"
-
-#: forms.py:75 forms.py:176
-msgid "Assigned to"
-msgstr "Przypisane do"
-
-#: forms.py:96 forms.py:110
-msgid "Chunk with this slug already exists"
-msgstr "Część z tym slugiem już istnieje"
-
-#: forms.py:119
-msgid "Append to"
-msgstr "Dołącz do"
-
-#: models/book.py:26 models/chunk.py:21 models/image.py:20
-msgid "title"
-msgstr "tytuł"
-
-#: models/book.py:27 models/chunk.py:22 models/image.py:21
-msgid "slug"
-msgstr "slug"
-
-#: models/book.py:28 models/image.py:22
-msgid "public"
-msgstr "publiczna"
-
-#: models/book.py:29
-msgid "scan gallery name"
-msgstr "nazwa galerii skanów"
-
-#: models/book.py:33
-msgid "parent"
-msgstr "rodzic"
-
-#: models/book.py:34
-msgid "parent number"
-msgstr "numeracja rodzica"
-
-#: models/book.py:52 models/chunk.py:19 models/publish_log.py:15
-msgid "book"
-msgstr "książka"
-
-#: models/book.py:53 views.py:619
-msgid "books"
-msgstr "książki"
-
-#: models/book.py:257
-msgid "No chunks in the book."
-msgstr "Książka nie ma części."
-
-#: models/book.py:261
-msgid "Not all chunks have publishable revisions."
-msgstr "Niektóre części nie są gotowe do publikacji."
-
-#: models/book.py:268 models/image.py:83
-msgid "Invalid XML"
-msgstr "Nieprawidłowy XML"
-
-#: models/book.py:270 models/image.py:85
-msgid "No Dublin Core found."
-msgstr "Brak sekcji Dublin Core."
-
-#: models/book.py:272 models/image.py:87
-msgid "Invalid Dublin Core"
-msgstr "Nieprawidłowy Dublin Core"
-
-#: models/book.py:275 models/image.py:91
-msgid "rdf:about is not"
-msgstr "rdf:about jest różny od"
-
-#: models/chunk.py:20
-msgid "number"
-msgstr "numer"
-
-#: models/chunk.py:23
-msgid "gallery start"
-msgstr "początek galerii"
-
-#: models/chunk.py:38
-msgid "chunk"
-msgstr "część"
-
-#: models/chunk.py:39
-msgid "chunks"
-msgstr "części"
-
-#: models/image.py:19 models/image.py:33 models/publish_log.py:43
-msgid "image"
-msgstr "obraz"
-
-#: models/image.py:34
-msgid "images"
-msgstr "obrazy"
-
-#: models/image.py:75
-msgid "There is no publishable revision"
-msgstr "Żadna wersja nie została oznaczona do publikacji."
-
-#: models/project.py:11
-msgid "name"
-msgstr "nazwa"
-
-#: models/project.py:12
-msgid "notes"
-msgstr "notatki"
-
-#: models/project.py:17 templates/catalogue/book_list/book_list.html:66
-#: templates/catalogue/image_table.html:60
-msgid "project"
-msgstr "projekt"
-
-#: models/project.py:18
-msgid "projects"
-msgstr "projekty"
-
-#: models/publish_log.py:16 models/publish_log.py:44
-msgid "time"
-msgstr "czas"
-
-#: models/publish_log.py:17 models/publish_log.py:45
-#: templates/catalogue/wall.html:20
-msgid "user"
-msgstr "użytkownik"
-
-#: models/publish_log.py:22 models/publish_log.py:31
-msgid "book publish record"
-msgstr "zapis publikacji książki"
-
-#: models/publish_log.py:23
-msgid "book publish records"
-msgstr "zapisy publikacji książek"
-
-#: models/publish_log.py:32 models/publish_log.py:46
-msgid "change"
-msgstr "zmiana"
-
-#: models/publish_log.py:36
-msgid "chunk publish record"
-msgstr "zapis publikacji części"
-
-#: models/publish_log.py:37
-msgid "chunk publish records"
-msgstr "zapisy publikacji części"
-
-#: models/publish_log.py:51
-msgid "image publish record"
-msgstr "zapis publikacji obrazu"
-
-#: models/publish_log.py:52
-msgid "image publish records"
-msgstr "zapisy publikacji obrazów"
-
-#: templates/catalogue/active_users_list.html:5
-msgid "Active users"
-msgstr "Aktywni użytkownicy"
-
-#: templates/catalogue/active_users_list.html:11
-msgid "Users active in the year"
-msgstr "Użytkownicy aktywni w roku"
-
-#: templates/catalogue/activity.html:6 templates/catalogue/activity.html:15
-#: templatetags/catalogue.py:30
-msgid "Activity"
-msgstr "Aktywność"
-
-#: templates/catalogue/base.html:13
-msgid "Platforma Redakcyjna"
-msgstr "Platforma Redakcyjna"
-
-#: templates/catalogue/book_append_to.html:5
-#: templates/catalogue/book_append_to.html:14
-msgid "Append book"
-msgstr "Dołącz książkę"
-
-#: templates/catalogue/book_detail.html:23
-#: templates/catalogue/book_edit.html:13 templates/catalogue/chunk_edit.html:22
-#: templates/catalogue/image_detail.html:22
-msgid "Save"
-msgstr "Zapisz"
-
-#: templates/catalogue/book_detail.html:30
-msgid "Edit gallery"
-msgstr "Edytuj galerię"
-
-#: templates/catalogue/book_detail.html:33
-msgid "Append to other book"
-msgstr "Dołącz do innej książki"
-
-#: templates/catalogue/book_detail.html:40
-msgid "Chunks"
-msgstr "Części"
-
-#: templates/catalogue/book_detail.html:58
-#: templates/catalogue/image_detail.html:47 templatetags/wall.py:108
-#: templatetags/wall.py:129
-msgid "Publication"
-msgstr "Publikacja"
-
-#: templates/catalogue/book_detail.html:69
-#: templates/catalogue/image_detail.html:51
-msgid "Last published"
-msgstr "Ostatnio opublikowano"
-
-#: templates/catalogue/book_detail.html:79
-msgid "Full XML"
-msgstr "Pełny XML"
-
-#: templates/catalogue/book_detail.html:80
-msgid "HTML version"
-msgstr "Wersja HTML"
-
-#: templates/catalogue/book_detail.html:81
-msgid "TXT version"
-msgstr "Wersja TXT"
-
-#: templates/catalogue/book_detail.html:82
-msgid "PDF version"
-msgstr "Wersja PDF"
-
-#: templates/catalogue/book_detail.html:83
-msgid "PDF version for mobiles"
-msgstr "Wersja PDF na telefony"
-
-#: templates/catalogue/book_detail.html:84
-msgid "EPUB version"
-msgstr "Wersja EPUB"
-
-#: templates/catalogue/book_detail.html:85
-msgid "MOBI version"
-msgstr "Wersja MOBI"
-
-#: templates/catalogue/book_detail.html:99
-#: templates/catalogue/image_detail.html:70
-msgid "Publish"
-msgstr "Opublikuj"
-
-#: templates/catalogue/book_detail.html:103
-#: templates/catalogue/image_detail.html:74
-msgid "Log in to publish."
-msgstr "Zaloguj się, aby opublikować."
-
-#: templates/catalogue/book_detail.html:106
-#: templates/catalogue/image_detail.html:77
-msgid "This book can't be published yet, because:"
-msgstr "Ta książka nie może jeszcze zostać opublikowana. Powód:"
-
-#: templates/catalogue/book_edit.html:5
-msgid "Edit book"
-msgstr "Edytuj książkę"
-
-#: templates/catalogue/book_html.html:12 templates/catalogue/book_text.html:15
-msgid "Table of contents"
-msgstr "Spis treści"
-
-#: templates/catalogue/book_html.html:13 templates/catalogue/book_text.html:17
-msgid "Edit. note"
-msgstr "Nota red."
-
-#: templates/catalogue/book_html.html:14
-msgid "Infobox"
-msgstr "Informacje"
-
-#: templates/catalogue/book_list/book.html:8
-#: templates/catalogue/book_list/book.html:36
-msgid "Book settings"
-msgstr "Ustawienia książki"
-
-#: templates/catalogue/book_list/book.html:9
-#: templates/catalogue/book_list/chunk.html:7
-#: templates/catalogue/chunk_edit.html:6 templates/catalogue/chunk_edit.html:12
-msgid "Chunk settings"
-msgstr "Ustawienia części"
-
-#: templates/catalogue/book_list/book.html:12
-#: templates/catalogue/book_list/chunk.html:9
-#: templates/catalogue/image_short.html:9
-msgid "Edit:"
-msgstr "Edytuj:"
-
-#: templates/catalogue/book_list/book.html:21
-#: templates/catalogue/book_list/book.html:43
-#: templates/catalogue/image_short.html:18 templatetags/book_list.py:82
-#: templatetags/book_list.py:150
-msgid "published"
-msgstr "opublikowane"
-
-#: templates/catalogue/book_list/book.html:24
-#: templates/catalogue/book_list/book.html:46
-#: templates/catalogue/book_list/chunk.html:28
-#: templates/catalogue/image_short.html:21 templatetags/book_list.py:80
-#: templatetags/book_list.py:148
-msgid "publishable"
-msgstr "do publikacji"
-
-#: templates/catalogue/book_list/book.html:27
-#: templates/catalogue/book_list/chunk.html:33
-#: templates/catalogue/image_short.html:24 templatetags/book_list.py:81
-#: templatetags/book_list.py:149
-msgid "changed"
-msgstr "zmienione"
-
-#: templates/catalogue/book_list/book_list.html:29
-#: templates/catalogue/image_table.html:25
-msgid "Search in book titles"
-msgstr "Szukaj w tytułach książek"
-
-#: templates/catalogue/book_list/book_list.html:34
-#: templates/catalogue/image_table.html:30
-msgid "stage"
-msgstr "etap"
-
-#: templates/catalogue/book_list/book_list.html:36
-#: templates/catalogue/book_list/book_list.html:47
-#: templates/catalogue/book_list/book_list.html:68
-#: templates/catalogue/image_table.html:32
-#: templates/catalogue/image_table.html:43
-#: templates/catalogue/image_table.html:62
-msgid "none"
-msgstr "brak"
-
-#: templates/catalogue/book_list/book_list.html:45
-#: templates/catalogue/image_table.html:41
-msgid "editor"
-msgstr "redaktor"
-
-#: templates/catalogue/book_list/book_list.html:58
-#: templates/catalogue/image_table.html:52
-msgid "status"
-msgstr "status"
-
-#: templates/catalogue/book_list/book_list.html:92
-#, python-format
-msgid "%(c)s book"
-msgid_plural "%(c)s books"
-msgstr[0] "%(c)s książka"
-msgstr[1] "%(c)s książki"
-msgstr[2] "%(c)s książek"
-
-#: templates/catalogue/book_list/book_list.html:96
-msgid "No books found."
-msgstr "Nie znaleziono książek."
-
-#: templates/catalogue/book_list/book_list.html:105
-#: templates/catalogue/image_table.html:87
-msgid "Set stage"
-msgstr "Ustaw etap"
-
-#: templates/catalogue/book_list/book_list.html:106
-#: templates/catalogue/image_table.html:88
-msgid "Set user"
-msgstr "Przypisz redaktora"
-
-#: templates/catalogue/book_list/book_list.html:108
-#: templates/catalogue/image_table.html:90
-msgid "Project"
-msgstr "Projekt"
-
-#: templates/catalogue/book_list/book_list.html:109
-#: templates/catalogue/image_table.html:91
-msgid "More users"
-msgstr "Więcej użytkowników"
-
-#: templates/catalogue/book_text.html:7
-msgid "Redakcja"
-msgstr ""
-
-#: templates/catalogue/chunk_add.html:6 templates/catalogue/chunk_add.html:12
-#: templates/catalogue/chunk_edit.html:29
-msgid "Split chunk"
-msgstr "Podziel część"
-
-#: templates/catalogue/chunk_add.html:19
-msgid "Insert empty chunk after"
-msgstr "Wstaw pustą część po"
-
-#: templates/catalogue/chunk_add.html:23
-msgid "Add chunk"
-msgstr "Dodaj część"
-
-#: templates/catalogue/chunk_edit.html:19
-msgid "Book"
-msgstr "Książka"
-
-#: templates/catalogue/document_create_missing.html:6
-#: templates/catalogue/document_create_missing.html:12
-msgid "Create a new book"
-msgstr "Utwórz nową książkę"
-
-#: templates/catalogue/document_create_missing.html:21
-msgid "Create book"
-msgstr "Utwórz książkę"
-
-#: templates/catalogue/document_list.html:7
-msgid "Book list"
-msgstr "Lista książek"
-
-#: templates/catalogue/document_upload.html:6
-msgid "Bulk document upload"
-msgstr "Hurtowe dodawanie dokumentów"
-
-#: templates/catalogue/document_upload.html:14
-msgid "Bulk documents upload"
-msgstr "Hurtowe dodawanie dokumentów"
-
-#: templates/catalogue/document_upload.html:19
-msgid ""
-"Please submit a ZIP with UTF-8 encoded XML files. Files not ending with "
-"<code>.xml</code> will be ignored."
-msgstr ""
-"Proszę wskazać archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie "
-"kończące się na <code>.xml</code> zostaną zignorowane."
-
-#: templates/catalogue/document_upload.html:26
-#: templates/catalogue/upload_pdf.html:16 templatetags/catalogue.py:37
-msgid "Upload"
-msgstr "Załaduj"
-
-#: templates/catalogue/document_upload.html:34
-msgid ""
-"There have been some errors. No files have been added to the repository."
-msgstr "Wystąpiły błędy. Żadne pliki nie zostały dodane do repozytorium."
-
-#: templates/catalogue/document_upload.html:35
-msgid "Offending files"
-msgstr "Błędne pliki"
-
-#: templates/catalogue/document_upload.html:43
-msgid "Correct files"
-msgstr "Poprawne pliki"
-
-#: templates/catalogue/document_upload.html:54
-msgid "Files have been successfully uploaded to the repository."
-msgstr "Pliki zostały dodane do repozytorium."
-
-#: templates/catalogue/document_upload.html:55
-msgid "Uploaded files"
-msgstr "Dodane pliki"
-
-#: templates/catalogue/document_upload.html:65
-msgid "Skipped files"
-msgstr "Pominięte pliki"
-
-#: templates/catalogue/document_upload.html:66
-msgid "Files skipped due to no <code>.xml</code> extension"
-msgstr "Pliki pominięte z powodu braku rozszerzenia <code>.xml</code>."
-
-#: templates/catalogue/head_login.html:10
-msgid "Admin"
-msgstr "Administracja"
-
-#: templates/catalogue/head_login.html:15
-msgid "Log Out"
-msgstr "Wyloguj"
-
-#: templates/catalogue/head_login.html:21
-msgid "Log In"
-msgstr "Zaloguj"
-
-#: templates/catalogue/image_detail.html:34
-msgid "Editor"
-msgstr "Edytor"
-
-#: templates/catalogue/image_detail.html:38
-msgid "Proceed to the editor."
-msgstr "Przejdź do edytora."
-
-#: templates/catalogue/image_list.html:8
-msgid "Image list"
-msgstr "Lista obrazów"
-
-#: templates/catalogue/image_short.html:6
-msgid "Image settings"
-msgstr "Ustawienia obrazu"
-
-#: templates/catalogue/image_table.html:79
-#, python-format
-msgid "%(c)s image"
-msgid_plural "%(c)s images"
-msgstr[0] "%(c)s obraz"
-msgstr[1] "%(c)s obrazy"
-msgstr[2] "%(c)s obrazów"
-
-#: templates/catalogue/image_table.html:81
-msgid "No images found."
-msgstr "Nie znaleziono obrazów."
-
-#: templates/catalogue/my_page.html:15 templatetags/catalogue.py:28
-msgid "My page"
-msgstr "Moja strona"
-
-#: templates/catalogue/my_page.html:25
-msgid "Your last edited documents"
-msgstr "Twoje ostatnie edycje"
-
-#: templates/catalogue/my_page.html:45 templates/catalogue/user_page.html:18
-msgid "Recent activity for"
-msgstr "Ostatnia aktywność dla:"
-
-#: templates/catalogue/upload_pdf.html:5 templates/catalogue/upload_pdf.html:11
-msgid "PDF file upload"
-msgstr "Ładowanie pliku PDF"
-
-#: templates/catalogue/user_list.html:7 templates/catalogue/user_list.html:14
-#: templatetags/catalogue.py:33
-msgid "Users"
-msgstr "Użytkownicy"
-
-#: templates/catalogue/wall.html:30
-msgid "not logged in"
-msgstr "nie zalogowany"
-
-#: templates/catalogue/wall.html:35
-msgid "No activity recorded."
-msgstr "Nie zanotowano aktywności."
-
-#: templatetags/book_list.py:83 templatetags/book_list.py:151
-msgid "unpublished"
-msgstr "nie opublikowane"
-
-#: templatetags/book_list.py:84 templatetags/book_list.py:152
-msgid "empty"
-msgstr "puste"
-
-#: templatetags/catalogue.py:31
-msgid "All"
-msgstr "Wszystkie"
-
-#: templatetags/catalogue.py:32
-msgid "Images"
-msgstr "Obrazy"
-
-#: templatetags/catalogue.py:36
-msgid "Add"
-msgstr "Dodaj"
-
-#: templatetags/catalogue.py:39
-msgid "Covers"
-msgstr "Okładki"
-
-#: templatetags/wall.py:49 templatetags/wall.py:78
-msgid "Related edit"
-msgstr "Powiązana zmiana"
-
-#: templatetags/wall.py:51 templatetags/wall.py:80
-msgid "Edit"
-msgstr "Zmiana"
-
-#: views.py:172
-#, python-format
-msgid "Slug already used for %s"
-msgstr "Slug taki sam jak dla pliku %s"
-
-#: views.py:174
-msgid "Slug already used in repository."
-msgstr "Dokument o tym slugu już istnieje w repozytorium."
-
-#: views.py:180
-msgid "File should be UTF-8 encoded."
-msgstr "Plik powinien mieć kodowanie UTF-8."
-
-#: views.py:621
-msgid "scan gallery"
-msgstr "galeria skanów"
-
-#~ msgid "Active users since"
-#~ msgstr "Użytkownicy aktywni od"
-
-#~ msgid "Show hidden books"
-#~ msgstr "Pokaż ukryte książki"
-
-#~ msgid "Comment"
-#~ msgstr "Komentarz"
-
-#~ msgid "Comments"
-#~ msgstr "Komentarze"
-
-#~ msgid "Mark publishable"
-#~ msgstr "Oznacz do publikacji"
-
-#~ msgid "Mark not publishable"
-#~ msgstr "Odznacz do publikacji"
-
-#~ msgid "Other user"
-#~ msgstr "Inny użytkownik"
-
-#~ msgid "edit"
-#~ msgstr "edytuj"
-
-#~ msgid "add basic document structure"
-#~ msgstr "dodaj podstawową strukturę dokumentu"
-
-#~ msgid "change master tag to"
-#~ msgstr "zmień tak master na"
-
-#~ msgid "add begin trimming tag"
-#~ msgstr "dodaj początkowy ogranicznik"
-
-#~ msgid "add end trimming tag"
-#~ msgstr "dodaj końcowy ogranicznik"
-
-#~ msgid "unstructured text"
-#~ msgstr "tekst bez struktury"
-
-#~ msgid "unknown XML"
-#~ msgstr "nieznany XML"
-
-#~ msgid "broken document"
-#~ msgstr "uszkodzony dokument"
-
-#~ msgid "Apply fixes"
-#~ msgstr "Wykonaj zmiany"
-
-#~ msgid "Can mark for publishing"
-#~ msgstr "Oznacza do publikacji"
-
-#~ msgid "Author"
-#~ msgstr "Autor"
-
-#~ msgid "Your name"
-#~ msgstr "Imię i nazwisko"
-
-#~ msgid "Author's email"
-#~ msgstr "E-mail autora"
-
-#~ msgid "Your email address, so we can show a gravatar :)"
-#~ msgstr "Adres e-mail, żebyśmy mogli pokazać gravatar :)"
-
-#~ msgid "Describe changes you made."
-#~ msgstr "Opisz swoje zmiany"
-
-#~ msgid "Completed"
-#~ msgstr "Ukończono"
-
-#~ msgid "If you completed a life cycle stage, select it."
-#~ msgstr "Jeśli został ukończony etap prac, wskaż go."
-
-#~ msgid "Describe the reason for reverting."
-#~ msgstr "Opisz powód przywrócenia."
-
-#~ msgid "theme"
-#~ msgstr "motyw"
-
-#~ msgid "themes"
-#~ msgstr "motywy"
-
-#~ msgid "Tag added"
-#~ msgstr "Dodano tag"
-
-#~ msgid "Revision marked"
-#~ msgstr "Wersja oznaczona"
-
-#~ msgid "New version"
-#~ msgstr "Nowa wersja"
-
-#~ msgid "Click to open/close gallery"
-#~ msgstr "Kliknij, aby (ro)zwinąć galerię"
-
-#~ msgid "Help"
-#~ msgstr "Pomoc"
-
-#~ msgid "Version"
-#~ msgstr "Wersja"
-
-#~ msgid "Unknown"
-#~ msgstr "nieznana"
-
-#~ msgid "Save attempt in progress"
-#~ msgstr "Trwa zapisywanie"
-
-#~ msgid "There is a newer version of this document!"
-#~ msgstr "Istnieje nowsza wersja tego dokumentu!"
-
-#~ msgid "Clear filter"
-#~ msgstr "Wyczyść filtr"
-
-#~ msgid "Cancel"
-#~ msgstr "Anuluj"
-
-#~ msgid "Revert"
-#~ msgstr "Przywróć"
-
-#~ msgid "all"
-#~ msgstr "wszystkie"
-
-#~ msgid "Annotations"
-#~ msgstr "Przypisy"
-
-#~ msgid "Previous"
-#~ msgstr "Poprzednie"
-
-#~ msgid "Next"
-#~ msgstr "Następne"
-
-#~ msgid "Zoom in"
-#~ msgstr "Powiększ"
-
-#~ msgid "Zoom out"
-#~ msgstr "Zmniejsz"
-
-#~ msgid "Gallery"
-#~ msgstr "Galeria"
-
-#~ msgid "Compare versions"
-#~ msgstr "Porównaj wersje"
-
-#~ msgid "Revert document"
-#~ msgstr "Przywróć wersję"
-
-#~ msgid "View version"
-#~ msgstr "Zobacz wersję"
-
-#~ msgid "History"
-#~ msgstr "Historia"
-
-#~ msgid "Search"
-#~ msgstr "Szukaj"
-
-#~ msgid "Replace with"
-#~ msgstr "Zamień na"
-
-#~ msgid "Replace"
-#~ msgstr "Zamień"
-
-#~ msgid "Options"
-#~ msgstr "Opcje"
-
-#~ msgid "Case sensitive"
-#~ msgstr "Rozróżniaj wielkość liter"
-
-#~ msgid "From cursor"
-#~ msgstr "Zacznij od kursora"
-
-#~ msgid "Search and replace"
-#~ msgstr "Znajdź i zamień"
-
-#~ msgid "Source code"
-#~ msgstr "Kod źródłowy"
-
-#~ msgid "Title"
-#~ msgstr "Tytuł"
-
-#~ msgid "Document ID"
-#~ msgstr "ID dokumentu"
-
-#~ msgid "Current version"
-#~ msgstr "Aktualna wersja"
-
-#~ msgid "Last edited by"
-#~ msgstr "Ostatnio edytowane przez"
-
-#~ msgid "Summary"
-#~ msgstr "Podsumowanie"
-
-#~ msgid "Insert theme"
-#~ msgstr "Wstaw motyw"
-
-#~ msgid "Insert annotation"
-#~ msgstr "Wstaw przypis"
-
-#~ msgid "Visual editor"
-#~ msgstr "Edytor wizualny"
-
-#~ msgid "Unassigned"
-#~ msgstr "Nie przypisane"
-
-#~ msgid "First correction"
-#~ msgstr "Autokorekta"
-
-#~ msgid "Tagging"
-#~ msgstr "Tagowanie"
-
-#~ msgid "Initial Proofreading"
-#~ msgstr "Korekta"
-
-#~ msgid "Annotation Proofreading"
-#~ msgstr "Sprawdzenie przypisów źródła"
-
-#~ msgid "Modernisation"
-#~ msgstr "Uwspółcześnienie"
-
-#~ msgid "Themes"
-#~ msgstr "Motywy"
-
-#~ msgid "Editor's Proofreading"
-#~ msgstr "Ostateczna redakcja literacka"
-
-#~ msgid "Technical Editor's Proofreading"
-#~ msgstr "Ostateczna redakcja techniczna"
-
-#~ msgid "Finished stage: %s"
-#~ msgstr "Ukończony etap: %s"
-
-#~ msgid "Refresh"
-#~ msgstr "Odśwież"
+++ /dev/null
-# 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)
+++ /dev/null
-# 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()
+++ /dev/null
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-import sys
-
-from datetime import date
-from lxml import etree
-
-from django.core.management import BaseCommand
-
-from catalogue.models import Book
-from librarian import RDFNS, DCNS
-
-TEMPLATE = '''<utwor>
-<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
-<rdf:Description rdf:about="http://redakcja.wolnelektury.pl/documents/book/%(slug)s/">
-%(dc)s
-</rdf:Description>
-</rdf:RDF>
-
-</utwor>
-'''
-
-DC_TEMPLATE = '<dc:%(tag)s xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">%(value)s</dc:%(tag)s>'
-
-DC_TAGS = (
- 'creator',
- 'title',
- 'relation.hasPart',
- 'contributor.translator',
- 'contributor.editor',
- 'contributor.technical_editor',
- 'contributor.funding',
- 'contributor.thanks',
- 'publisher',
- 'subject.period',
- 'subject.type',
- 'subject.genre',
- 'description',
- 'identifier.url',
- 'source',
- 'source.URL',
- 'rights.license',
- 'rights',
- 'date.pd',
- 'format',
- 'type',
- 'date',
- 'audience',
- 'language',
-)
-
-IDENTIFIER_PREFIX = 'http://wolnelektury.pl/katalog/lektura/'
-
-
-def dc_desc_element(book):
- xml = book.materialize()
- tree = etree.fromstring(xml)
- return tree.find(".//" + RDFNS("Description"))
-
-
-def distinct_dc_values(tag, desc_elements):
- values = set()
- for desc in desc_elements:
- values.update(elem.text for elem in desc.findall(DCNS(tag)))
- return values
-
-
-class Command(BaseCommand):
- args = 'slug'
-
- def handle(self, slug, **options):
- children_slugs = [line.strip() for line in sys.stdin]
- children = Book.objects.filter(dc_slug__in=children_slugs)
- desc_elements = [dc_desc_element(child) for child in children]
- title = u'Utwory wybrane'
- own_attributes = {
- 'title': title,
- 'relation.hasPart': [IDENTIFIER_PREFIX + child_slug for child_slug in children_slugs],
- 'identifier.url': IDENTIFIER_PREFIX + slug,
- 'date': date.today().isoformat(),
- }
- dc_tags = []
- for tag in DC_TAGS:
- if tag in own_attributes:
- values = own_attributes[tag]
- if not isinstance(values, list):
- values = [values]
- else:
- values = distinct_dc_values(tag, desc_elements)
- for value in values:
- dc_tags.append(DC_TEMPLATE % {'tag': tag, 'value': value})
- xml = TEMPLATE % {'slug': slug, 'dc': '\n'.join(dc_tags)}
- Book.create(
- text=xml,
- creator=None,
- slug=slug,
- title=title,
- gallery=slug)
+++ /dev/null
-# 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.'
+++ /dev/null
-# 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()
-
+++ /dev/null
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-import csv
-
-import sys
-from django.contrib.auth.models import User
-from lxml import etree
-from collections import defaultdict
-from django.core.management import BaseCommand
-
-from catalogue.models import Book
-from librarian import RDFNS, DCNS
-
-CONTENT_TYPES = {
- 'pdf': 'application/pdf',
- 'epub': 'application/epub+zip',
- 'mobi': 'application/x-mobipocket-ebook',
- 'txt': 'text/plain',
- 'html': 'text/html',
-}
-
-
-ISBN_TEMPLATES = (
- r'<dc:relation.hasFormat id="%(format)s" xmlns:dc="http://purl.org/dc/elements/1.1/">%(url)s'
- r'</dc:relation.hasFormat>',
- r'<meta refines="#%(format)s" id="%(format)s-id" property="dcterms:identifier">ISBN-%(isbn)s</meta>',
- r'<meta refines="#%(format)s-id" property="identifier-type">ISBN</meta>',
- r'<meta refines="#%(format)s" property="dcterms:format">%(content_type)s</meta>',
-)
-
-
-def url_for_format(slug, format):
- if format == 'html':
- return 'https://wolnelektury.pl/katalog/lektura/%s.html' % slug
- else:
- return 'http://wolnelektury.pl/media/book/%(format)s/%(slug)s.%(format)s' % {'slug': slug, 'format': format}
-
-
-class Command(BaseCommand):
- args = 'csv_file'
-
- def add_arguments(self, parser):
- self.add_argument(
- '-u', '--username', dest='username', metavar='USER',
- help='Assign commits to this user (required, preferably yourself).')
-
- def handle(self, csv_file, **options):
- username = options.get('username')
-
- if username:
- user = User.objects.get(username=username)
- else:
- print('Please provide a username.')
- sys.exit(1)
-
- csvfile = open(csv_file, 'rb')
- isbn_lists = defaultdict(list)
- for slug, format, isbn in csv.reader(csvfile, delimiter=','):
- isbn_lists[slug].append((format, isbn))
- csvfile.close()
-
- for slug, isbn_list in isbn_lists.iteritems():
- print('processing %s' % slug)
- book = Book.objects.get(dc_slug=slug)
- chunk = book.chunk_set.first()
- old_head = chunk.head
- src = old_head.materialize()
- tree = etree.fromstring(src)
- isbn_node = tree.find('.//' + DCNS("relation.hasFormat"))
- if isbn_node is not None:
- print('%s already contains ISBN metadata, skipping' % slug)
- continue
- desc = tree.find(".//" + RDFNS("Description"))
- for format, isbn in isbn_list:
- for template in ISBN_TEMPLATES:
- isbn_xml = template % {
- 'format': format,
- 'isbn': isbn,
- 'content_type': CONTENT_TYPES[format],
- 'url': url_for_format(slug, format),
- }
- element = etree.XML(isbn_xml)
- element.tail = '\n'
- desc.append(element)
- new_head = chunk.commit(
- etree.tostring(tree, encoding='unicode'),
- author=user,
- description='automatyczne dodanie isbn'
- )
- print('committed %s' % slug)
- if old_head.publishable:
- new_head.set_publishable(True)
- else:
- print('Warning: %s not publishable' % slug)
+++ /dev/null
-# 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)
+++ /dev/null
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-import sys
-
-from django.contrib.auth.models import User
-from django.core.management.base import BaseCommand
-from django.core.management.color import color_style
-from django.db import transaction
-
-from catalogue.models import Book
-
-
-def common_prefix(texts):
- common = []
-
- min_len = min(len(text) for text in texts)
- for i in range(min_len):
- chars = list(set([text[i] for text in texts]))
- if len(chars) > 1:
- break
- common.append(chars[0])
- return "".join(common)
-
-
-class Command(BaseCommand):
- help = 'Merges multiple books into one.'
- args = '[slug]...'
-
- def add_arguments(self, parser):
- self.add_argument(
- '-s', '--slug', dest='new_slug', metavar='SLUG',
- help='New slug of the merged book (defaults to common part of all slugs).')
- self.add_argument(
- '-t', '--title', dest='new_title', metavar='TITLE',
- help='New title of the merged book (defaults to common part of all titles).')
- self.add_argument(
- '-q', '--quiet', action='store_false', dest='verbose', default=True,
- help='Less output')
- self.add_argument(
- '-g', '--guess', action='store_true', dest='guess', default=False,
- help='Try to guess what merges are needed (but do not apply them).')
- self.add_argument(
- '-d', '--dry-run', action='store_true', dest='dry_run', default=False,
- help='Dry run: do not actually change anything.')
- self.add_argument(
- '-f', '--force', action='store_true', dest='force', default=False,
- help='On slug conflict, hide the original book to archive.')
-
- def print_guess(self, dry_run=True, force=False):
- from collections import defaultdict
- from pipes import quote
- import re
-
- def read_slug(slug):
- res = []
- res.append((re.compile(r'__?(przedmowa)$'), -1))
- res.append((re.compile(r'__?(cz(esc)?|ksiega|rozdzial)__?(?P<n>\d*)$'), None))
- res.append((re.compile(r'__?(rozdzialy__?)?(?P<n>\d*)-'), None))
-
- for r, default in res:
- m = r.search(slug)
- if m:
- start = m.start()
- try:
- return int(m.group('n')), slug[:start]
- except IndexError:
- return default, slug[:start]
- return None, slug
-
- def file_to_title(fname):
- """ Returns a title-like version of a filename. """
- parts = (p.replace('_', ' ').title() for p in fname.split('__'))
- return ' / '.join(parts)
-
- merges = defaultdict(list)
- slugs = []
- for b in Book.objects.all():
- slugs.append(b.slug)
- n, ns = read_slug(b.slug)
- if n is not None:
- merges[ns].append((n, b))
-
- conflicting_slugs = []
- for slug in sorted(merges.keys()):
- merge_list = sorted(merges[slug])
- if len(merge_list) < 2:
- continue
-
- merge_slugs = [b.slug for i, b in merge_list]
- if slug in slugs and slug not in merge_slugs:
- conflicting_slugs.append(slug)
-
- title = file_to_title(slug)
- print("./manage.py merge_books %s%s--title=%s --slug=%s \\\n %s\n" % (
- '--dry-run ' if dry_run else '',
- '--force ' if force else '',
- quote(title), slug,
- " \\\n ".join(merge_slugs)
- ))
-
- if conflicting_slugs:
- if force:
- print(self.style.NOTICE('# These books will be archived:'))
- else:
- print(self.style.ERROR('# ERROR: Conflicting slugs:'))
- for slug in conflicting_slugs:
- print('#', slug)
-
-
- def handle(self, *slugs, **options):
-
- self.style = color_style()
-
- force = options.get('force')
- guess = options.get('guess')
- dry_run = options.get('dry_run')
- new_slug = options.get('new_slug').decode('utf-8')
- new_title = options.get('new_title').decode('utf-8')
- verbose = options.get('verbose')
-
- if guess:
- if slugs:
- print("Please specify either slugs, or --guess.")
- return
- else:
- self.print_guess(dry_run, force)
- return
- if not slugs:
- print("Please specify some book slugs")
- return
-
- # Start transaction management.
- transaction.enter_transaction_management()
-
- books = [Book.objects.get(slug=slug) for slug in slugs]
- common_slug = common_prefix(slugs)
- common_title = common_prefix([b.title for b in books])
-
- if not new_title:
- new_title = common_title
- elif common_title.startswith(new_title):
- common_title = new_title
-
- if not new_slug:
- new_slug = common_slug
- elif common_slug.startswith(new_slug):
- common_slug = new_slug
-
- if slugs[0] != new_slug and Book.objects.filter(slug=new_slug).exists():
- self.style.ERROR('Book already exists, skipping!')
-
-
- if dry_run and verbose:
- print(self.style.NOTICE('DRY RUN: nothing will be changed.'))
- print()
-
- if verbose:
- print("New title:", self.style.NOTICE(new_title))
- print("New slug:", self.style.NOTICE(new_slug))
- print()
-
- for i, book in enumerate(books):
- chunk_titles = []
- chunk_slugs = []
-
- book_title = book.title[len(common_title):].replace(' / ', ' ').lstrip()
- book_slug = book.slug[len(common_slug):].replace('__', '_').lstrip('-_')
- for j, chunk in enumerate(book):
- if j:
- new_chunk_title = book_title + '_%d' % j
- new_chunk_slug = book_slug + '_%d' % j
- else:
- new_chunk_title, new_chunk_slug = book_title, book_slug
-
- chunk_titles.append(new_chunk_title)
- chunk_slugs.append(new_chunk_slug)
-
- if verbose:
- print("title: %s // %s -->\n %s // %s\nslug: %s / %s -->\n %s / %s" % (
- book.title, chunk.title,
- new_title, new_chunk_title,
- book.slug, chunk.slug,
- new_slug, new_chunk_slug))
- print()
-
- if not dry_run:
- try:
- conflict = Book.objects.get(slug=new_slug)
- except Book.DoesNotExist:
- conflict = None
- else:
- if conflict == books[0]:
- conflict = None
-
- if conflict:
- if force:
- # FIXME: there still may be a conflict
- conflict.slug = '.' + conflict.slug
- conflict.save()
- print(self.style.NOTICE('Book with slug "%s" moved to "%s".' % (new_slug, conflict.slug)))
- else:
- print(self.style.ERROR('ERROR: Book with slug "%s" exists.' % new_slug))
- return
-
- if i:
- books[0].append(books[i], slugs=chunk_slugs, titles=chunk_titles)
- else:
- book.title = new_title
- book.slug = new_slug
- book.save()
- for j, chunk in enumerate(book):
- chunk.title = chunk_titles[j]
- chunk.slug = chunk_slugs[j]
- chunk.save()
-
-
- transaction.commit()
- transaction.leave_transaction_management()
-
+++ /dev/null
-# 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', ' ')))
+++ /dev/null
-# 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)
+++ /dev/null
-# 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)
+++ /dev/null
-# 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())
+++ /dev/null
-# 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)
+++ /dev/null
-# 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)
+++ /dev/null
-# 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)
-
+++ /dev/null
-# 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
+++ /dev/null
-# 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')
+++ /dev/null
-# 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()
+++ /dev/null
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-
-
-{% block titleextra %}{% trans "Active users" %}{% endblock %}
-
-
-{% block content %}
-
-<h1>
- {% trans "Users active in the year" %} {{ year }}
-</h1>
-
-<ul>
-{% for email, names, count in users %}
-<li>{% for name in names %}{{ name }}, {% endfor %}<a href="mailto:{{ email }}">{{ email }}</a> ({{ count }})</li>
-{% endfor %}
-</ul>
-
-{% endblock content %}
+++ /dev/null
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-{% load wall %}
-
-
-{% block titleextra %}{% trans "Activity" %}{% endblock %}
-
-
-{% block content %}
-
-<div class="card">
- <div class="card-header">
-<h1>
- <a class="btn btn-light" href='{% url "catalogue_activity" prev_day.isoformat %}'><</a>
- {% trans "Activity" %}: {{ day }}
- {% if next_day %}
- <a class="btn btn-light" href='{% url "catalogue_activity" next_day.isoformat %}'>></a>
- {% endif %}
-</h1>
- </div>
- <div class="card-body">
-
- {% day_wall day %}
- </div>
-{% endblock content %}
+++ /dev/null
-<!DOCTYPE html>
-{% load pipeline i18n %}
-{% load static %}
-{% load catalogue %}
-<!DOCTYPE html>
-<html>
-<head lang="{{ LANGUAGE_CODE }}">
- <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
- <link rel="icon" href="{{ STATIC_URL }}img/pr-icon.png" type="image/png" />
- <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
- {% stylesheet 'catalogue' %}
- <title>{% block title %}{% block titleextra %}{% endblock %} ::
- {% trans "Platforma Redakcyjna" %}{% endblock title %}</title>
- {% block add_css %}{% endblock %}
-</head>
-<body>
-<!--#include file='/pozor.html'-->
-
-
-<nav class="navbar navbar-expand-md navbar-dark bg-dark">
- <a class="navbar-brand" href="{% url 'catalogue_document_list' %}">
- <img src="{% static "img/wl-orange.png" %}" alt="Platforma">
- </a>
- <ul class="navbar-nav mr-auto">
- {% main_tabs %}
- </ul>
-
- <ul class="navbar-nav">
- {% include "registration/head_login.html" %}
- </ul>
-</nav>
-
-<div class="container mt-4 mb-4">
- {% block content %}
- <div class="row">
- <div class="col-lg-8">
- {% block leftcolumn %}
- {% endblock leftcolumn %}
- </div>
- <div class="col-lg-4">
- {% block rightcolumn %}
- {% endblock rightcolumn %}
- </div>
- </div>
- {% endblock content %}
-</div>
-
-
-<script
- src="https://code.jquery.com/jquery-1.9.1.min.js"
- integrity="sha256-wS9gmOZBqsqWxgIVgA8Y9WcQOa7PgSIX+rPA0VL2rbQ="
- crossorigin="anonymous"></script>
-<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
-
-{% javascript 'catalogue' %}
-{% block add_js %}{% endblock %}
-{% block extrabody %}
-{% endblock %}
-</body>
-</html>
+++ /dev/null
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-{% load bootstrap4 %}
-
-{% block titleextra %}{% trans "Append book" %}{% endblock %}
-
-{% block content %}
-<div class="card">
- <div class="card-body">
- <form enctype="multipart/form-data" method="POST" action="">
- {% csrf_token %}
- {% bootstrap_form form %}
- {% buttons %}
- <button class="btn btn-primary" type="submit">{% trans "Append book" %}</button>
- {% endbuttons %}
- </form>
- </div></div>
-{% endblock content %}
-
+++ /dev/null
-{% extends "catalogue/base.html" %}
-{% load book_list i18n %}
-{% load bootstrap4 %}
-
-
-{% block titleextra %}{{ book.title }}{% endblock %}
-
-
-{% block content %}
-
- <div class="card mt-4">
- <div class="card-header">
- <h1>{{ book.title }}</h1>
- </div>
- <div class="card-body">
-
-
-
-{% if editable %}<form method='POST'>{% csrf_token %}{% endif %}
- {% bootstrap_form form %}
- {% if editable %}
- {% buttons %}
- <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
- {% endbuttons %}
- {% endif %}
-{% if editable %}</form>{% endif %}
-
-{% if editable %}
- {% if book.gallery %}
- <p><a href="{% url 'catalogue_book_gallery' book.slug %}">{% trans "Edit gallery" %}</a></p>
- {% endif %}
-
- <p style="text-align:right"><a class="btn btn-sm btn-danger" href="{% url 'catalogue_book_append' book.slug %}">{% trans "Append to other book" %}</a></p>
-{% endif %}
- </div>
- </div>
-
- <div class="card mt-4">
- <div class="card-header">
- <h2>{% trans "Chunks" %}</h2>
- </div>
- <div class="card-body">
-
- <table class='single-book-list table'><tbody>
- {% for chunk in book %}
- {% include 'catalogue/book_list/chunk.html' %}
- {% endfor %}
- </tbody></table>
- </div>
- </div>
-
-
-
-
-<div class='card mt-4'>
-
-<div class="card-header">
- <h2>{% trans "Publication" %}</h2>
- </div>
-<div class="card-body">
- <div class="row">
-<div class="col-lg-3">
-<img class="cover-preview" src="{% url 'cover_preview' book.slug %}" />
-{% if book.dc_cover_image %}
- <a href="{{ book.dc_cover_image.get_absolute_url }}">{{ book.dc_cover_image }}</a>
-{% endif %}
-<br><br>
-<form action="{% url 'cover_preview' book.slug %}">
-<input type="hidden" name="download" value="1">
-Okładka w rozmiarze
-<input name="width" type="number" required value="600"> x <input name="height" type="number" required value="833">
-<button type="submit" class="btn btn-sm btn-primary">Pobierz</button>
-</form>
-</div>
-<div class="col-lg-9">
-<p>{% trans "Last published" %}:
- {% if book.last_published %}
- {{ book.last_published }}
- {% else %}
- —
- {% endif %}
-</p>
-
-{% if publishable %}
- <p>
- <a href="{% url 'catalogue_book_xml' book.slug %}" rel="nofollow">{% trans "Full XML" %}</a><br/>
- <a target="_blank" href="{% url 'catalogue_book_html' book.slug %}" rel="nofollow">{% trans "HTML version" %}</a><br/>
- <a href="{% url 'catalogue_book_txt' book.slug %}" rel="nofollow">{% trans "TXT version" %}</a><br/>
- <a href="{% url 'catalogue_book_pdf' book.slug %}" rel="nofollow">{% trans "PDF version" %}</a><br/>
- <a href="{% url 'catalogue_book_pdf_mobile' book.slug %}" rel="nofollow">{% trans "PDF version for mobiles" %}</a><br/>
- <a href="{% url 'catalogue_book_epub' book.slug %}" rel="nofollow">{% trans "EPUB version" %}</a><br/>
- <a href="{% url 'catalogue_book_mobi' book.slug %}" rel="nofollow">{% trans "MOBI version" %}</a><br/>
- </p>
-
- {% if user.is_authenticated %}
- <!--
- Angel photos:
- Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
- mira66 (http://www.flickr.com/photos/21804434@N02/) /
- CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
- -->
- <form method="POST" action="{% url 'catalogue_publish' book.slug %}">{% csrf_token %}
- {{ publish_options_form.as_p }}
- <img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" />
- <button id="publish-button" type="submit">
- <span>{% trans "Publish" %}</span></button>
- <img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" />
- </form>
- {% else %}
- <a href="{% url 'cas_ng_login' %}">{% trans "Log in to publish." %}</a>
- {% endif %}
-{% else %}
- <p>{% trans "This book can't be published yet, because:" %}</p>
- <ul><li>{{ publishable_error }}</li></ul>
-{% endif %}
-
-</div>
- </div>
-{% endblock content %}
+++ /dev/null
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-
-
-{% block titleextra %}{% trans "Edit book" %}{% endblock %}
-
-
-{% block leftcolumn %}
- <form enctype="multipart/form-data" method="POST" action="">
- {% csrf_token %}
- {{ form.as_p }}
-
- <p><button type="submit">{% trans "Save" %}</button></p>
- </form>
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-{% endblock rightcolumn %}
+++ /dev/null
-{% load i18n %}
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
- <title>{{ book.title }}</title>
- </head>
- <body>
- <div id="menu">
- <ul>
- <li><a href="#toc">{% trans "Table of contents" %}</a></li>
- <li><a href="#nota_red">{% trans "Edit. note" %}</a></li>
- <li><a href="#info">{% trans "Infobox" %}</a></li>
- </ul>
- </div>
- <div id="info">
- {#% book_info book %#}
- </div>
- <div id="header">
- <div id="logo">
- <a href="/"><img src="http://static.wolnelektury.pl/img/logo.png" alt="WolneLektury.pl - logo" /></a>
- </div>
- </div>
-
- {{ html|safe }}
-
- </body>
-</html>
+++ /dev/null
-{% load i18n %}
-{% load username from common_tags %}
-
-{% if book.single %}
- {% with chunk as chunk %}
- <tr class="table-sm">
- <td><input type="checkbox" name="select_book" value="{{book.id}}" data-chunk-id="{{chunk.id}}"/></td>
- <td><a class="btn btn-sm btn-outline-secondary" href="{% url 'catalogue_book' book.slug %}" title="{% trans "Book settings" %}">📕</a></td>
- <td><a class="btn btn-sm btn-outline-secondary" href="{% url 'catalogue_chunk_edit' book.slug chunk.slug %}" title="{% trans "Chunk settings" %}">📜</a></td>
- <td><a class="btn btn-primary" target="_blank"
- href="{% url 'wiki_editor' book.slug %}">
- {{ book.title }}</a></td>
- <td>{% if chunk.stage %}
- {{ chunk.stage }}
- {% else %}–
- {% endif %}</td>
- <td class='user-column'>{% if chunk.user %}<a href="{% url 'catalogue_user' chunk.user.username %}">{{ chunk.user|username }}</a>{% endif %}</td>
- <td>
- {% if book.published %}
- <span class="badge badge-info" title="{% trans "published" %}">opubl.</span>
- {% endif %}
- {% if book.new_publishable %}
- <span class="badge badge-primary" title="{% trans "publishable" %}">do publ.</span>
- {% endif %}
- {% if chunk.changed %}
- <span class="badge badge-warning title="{% trans "changed" %}">zmiany</span>
- {% endif %}
- </td>
- <td>{{ book.project.name }}</td>
- </tr>
- {% endwith %}
-{% else %}
- <tr class="table-sm">
- <td><input type="checkbox" name="select_book" value="{{book.id}}"/></td>
- <td><a class='btn btn-sm btn-outline-secondary' href="{% url 'catalogue_book' book.slug %}" title="{% trans "Book settings" %}">📕</a></td>
- <td></td>
- <td>{{ book.title }}</td>
- <td></td>
- <td class='user-column'></td>
- <td>
- {% if book.published %}
- <span class="badge badge-info" title="{% trans "published" %}">opubl.</span>
- {% endif %}
- {% if book.new_publishable %}
- <span class="badge badge-info" title="{% trans "publishable" %}">do publ.</span>
- {% endif %}
- </td>
- <td>{{ book.project.name }}</td>
- </tr>
-{% endif %}
+++ /dev/null
-{% load i18n %}
-{% load pagination_tags %}
-{% load username from common_tags %}
-
-
-<form name='filter' action='{{ request.path }}'>
-<input type='hidden' name="title" value="{{ request.GET.title }}" />
-<input type='hidden' name="stage" value="{{ request.GET.stage }}" />
-{% if not viewed_user %}
- <input type='hidden' name="user" value="{{ request.GET.user }}" />
-{% endif %}
-<input type='hidden' name="all" value="{{ request.GET.all }}" />
-<input type='hidden' name="status" value="{{ request.GET.status }}" />
-<input type='hidden' name="project" value="{{ request.GET.project }}" />
-</form>
-
-
-<div class="card">
- <div class="card-body">
-
-
-<table id="file-list" class="table{% if viewed_user %} book-list-user{% endif %}">
- <thead><tr>
- <th></th>
- <th></th>
- <th></th>
- <th class='book-search-column'>
- <form>
- <input title='{% trans "Search in book titles" %}' name="title"
- class='form-control text-filter' value="{{ request.GET.title }}" />
- </form>
- </th>
- <th><select name="stage" class="form-control filter">
- <option value=''>- {% trans "stage" %} -</option>
- <option {% if request.GET.stage == '-' %}selected="selected"
- {% endif %}value="-">- {% trans "none" %} -</option>
- {% for stage in stages %}
- <option {% if request.GET.stage == stage.slug %}selected="selected"
- {% endif %}value="{{ stage.slug }}">{{ stage.name }}</option>
- {% endfor %}
- </select></th>
-
- {% if not viewed_user %}
- <th><select name="user" class="form-control filter">
- <option value=''>- {% trans "editor" %} -</option>
- <option {% if request.GET.user == '-' %}selected="selected"
- {% endif %}value="-">- {% trans "none" %} -</option>
- {% for user in users %}
- <option {% if request.GET.user == user.username %}selected="selected"
- {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
- {% endfor %}
- </select></th>
- {% else %}
- <th style='display: none'></th>
- {% endif %}
-
- <th><select name="status" class="form-control filter">
- <option value=''>- {% trans "status" %} -</option>
- {% for state, label in states %}
- <option {% if request.GET.status == state %}selected="selected"
- {% endif %}value='{{ state }}'>{{ label }}</option>
- {% endfor %}
- </select></th>
-
- <th><select name="project" class="form-control filter">
- <option value=''>- {% trans "project" %} -</option>
- <option {% if request.GET.project == '-' %}selected="selected"
- {% endif %}value="-">- {% trans "none" %} -</option>
- {% for project in projects %}
- <option {% if request.GET.project == project.pk|slugify %}selected="selected"
- {% endif %}value='{{ project.pk }}'>{{ project.name }}</option>
- {% endfor %}
- </select></th>
-
- </tr></thead>
-
- {% autopaginate books 100 as books_page %}
- <tbody>
- {% for item in books_page %}
- {% with book=item.book chunk=item.chunks.0 %}
- {% include 'catalogue/book_list/book.html' %}
- {% if not book.single %}
- {% for chunk in item.chunks %}
- {% include 'catalogue/book_list/chunk.html' %}
- {% endfor %}
- {% endif %}
- {% endwith %}
- {% endfor %}
- </tbody>
-</table>
-{% paginate %}
- {% blocktrans count c=books|length %}{{c}} book{% plural %}{{c}} books{% endblocktrans %}
-
-
-{% if not books %}
- <p>{% trans "No books found." %}</p>
-{% endif %}
-
- </div>
-</div>
-
-<form id='chunk_mass_edit' action='{% url "catalogue_chunk_mass_edit" %}' style="display:none;">
-{% csrf_token %}
-<input type="hidden" name="ids" />
-<label for="mass_edit_stage">{% trans "Set stage" %}</label><input type="hidden" name="stage" id="mass_edit_stage"/>
-<label for="mass_edit_user">{% trans "Set user" %}</label><input type="hidden" name="user" id="mass_edit_user" />
-<input type="hidden" name="status" />
-<label for="mass_edit_project">{% trans "Project" %}</label><input type="hidden" name="project" id="mass_edit_project" />
-<label for="mass_edit_more_users">{% trans "More users" %}</label>
-</form>
-
-<select name="other-user" style="display:none;">
- {% for user in other_users %}
- <option {% if request.GET.user == user.username %}selected="selected"
- {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
- {% endfor %}
-</select>
+++ /dev/null
-{% load i18n %}
-{% load username from common_tags %}
-
-<tr class="table-secondary table-sm">
- <td><input type="checkbox" name="select_chunk" value="{{chunk.id}}" data-book-id="{{chunk.book.id}}" /></td>
- <td class='book-settings-column'></td>
- <td><a class="btn btn-outline-secondary btn-sm" href="{% url 'catalogue_chunk_edit' chunk.book.slug chunk.slug %}" title="{% trans "Chunk settings" %}">📜</a></td>
- <td><a class="btn btn-primary" target="_blank" href="{{ chunk.get_absolute_url }}">
- {{ chunk.number }}.
- {{ chunk.title }}</a></td>
- <td>{% if chunk.stage %}
- {{ chunk.stage }}
- {% else %}
- –
- {% endif %}</td>
- <td class='user-column'>{% if chunk.user %}
- <a href="{% url 'catalogue_user' chunk.user.username %}">
- {{ chunk.user|username }}
- </a>{% else %}
-
- {% endif %}</td>
-
- </td>
- <td>
- {% if chunk.changed %}
- <span class="badge badge-warning title="{% trans "changed" %}">zmiany</span>
- {% endif %}
-</td>
-<td></td>
-</tr>
+++ /dev/null
-{% load i18n pipeline %}
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
- <title>{% trans "Redakcja" %} :: {{ book.title }}</title>
- {% stylesheet 'book' %}
- <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
- {% javascript 'book' %}
- </head>
- <body>
- <div id="menu">
- <ul>
- <li><a class="menu" href="#toc">{% trans "Table of contents" %}</a></li>
-{# <li><a class="menu" href="#themes">{% trans "Themes" %}</a></li>#}
- <li><a class="menu" href="#nota_red">{% trans "Edit. note" %}</a></li>
-{# <li><a class="menu" href="#info">{% trans "Infobox" %}</a></li>#}
-{# <li><a href="{{ book.get_absolute_url }}">{% trans "Book's page" %}</a></li> #}
-{# <li><a class="menu" href="#download">{% trans "Download" %}</a></li>#}
- </ul>
- </div>
- <div id="header">
- <a href="/"><img src="/media/static/img/logo-220.png" alt="Wolne Lektury" /></a>
- </div>
- {{ html|safe }}
- </body>
-</html>
+++ /dev/null
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-{% load bootstrap4 %}
-
-
-{% block titleextra %}{% trans "Split chunk" %}{% endblock %}
-
-
-{% block content %}
-<div class="card">
- <div class="card-header">
- <h1>{% trans "Split chunk" %}</h1>
- </div>
- <div class="card-body">
-
- <form enctype="multipart/form-data" method="POST">
- {% csrf_token %}
- <div class='editable'>
- <p>{% trans "Insert empty chunk after" %}:
- <a href="{{ chunk.get_absolute_url }}">{{ chunk.pretty_name }}</a></p>
- {% bootstrap_form form %}
- {% buttons %}
- <button class="btn btn-primary" type="submit">{% trans "Add chunk" %}</button>
- {% endbuttons %}
- </div>
- </form>
- </div>
-</div>
-{% endblock content %}
+++ /dev/null
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-{% load bootstrap4 %}
-
-
-{% block titleextra %}{% trans "Chunk settings" %}{% endblock %}
-
-
-{% block content %}
-<div class="card">
- <div class="card-header">
- <h1>{% trans "Chunk settings" %}</h1>
- </div>
- <div class="card-body">
-
- <form enctype="multipart/form-data" method="POST" action="{% if go_next %}?next={{ go_next }}{% endif %}">
- {% csrf_token %}
- <div class='editable'>
- <p>{% trans "Book" %}: {{ chunk.book }} ({{ chunk.number }}/{{ chunk.book|length }})</p>
- {% bootstrap_form form %}
- {% buttons %}
- <button class="btn btn-primary" type="submit">{% trans "Save" %}</button>
- {% endbuttons %}
- </div>
-
- </form>
-
-
- <p style="text-align: right"><a class="btn btn-danger" href="{% url "catalogue_chunk_add" chunk.book.slug chunk.slug %}">{% trans "Split chunk" %}</a></p>
- </div>
-</div>
-
-{% endblock content %}
+++ /dev/null
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-{% load bootstrap4 %}
-
-
-{% block titleextra %}{% trans "Create a new book" %}{% endblock %}
-
-
-{% block content %}
-<div class="card">
- <div class="card-header">
- <h1>{% trans "Create a new book" %}</h1>
- </div>
- <div class="card-body">
-
-
- <form enctype="multipart/form-data" method="POST">
- {% csrf_token %}
- {% bootstrap_form form %}
- {% buttons %}
- <button class="btn btn-primary" type="submit">{% trans "Create book" %}</button>
- {% endbuttons %}
- </form>
-</div>
-</div>
-{% endblock content %}
+++ /dev/null
-{% extends "catalogue/base.html" %}
-
-{% load i18n %}
-{% load catalogue book_list %}
-{% load pipeline %}
-
-{% block titleextra %}{% trans "Book list" %}{% endblock %}
-
-
-{% block add_js %}
- {% javascript 'book_list' %}
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js"></script>
-{% endblock %}
-
-{% block add_css %}
- {% stylesheet 'book_list' %}
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css">
-{% endblock %}
-
-{% block content %}
- {% book_list %}
-{% endblock content %}
+++ /dev/null
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-{% load bootstrap4 %}
-
-
-{% block titleextra %}{% trans "Bulk document upload" %}{% endblock %}
-
-
-{% block content %}
-
-
-<div class="card">
- <div class="card-header">
-<h2>{% trans "Bulk documents upload" %}</h2>
- </div>
- <div class="card-body">
-
-<p>
-{% trans "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with <code>.xml</code> will be ignored." %}
-</p>
-
-<form enctype="multipart/form-data" method="POST" action="{{ request.path }}">
-{% csrf_token %}
-{% bootstrap_form form %}
-{% buttons %}
-<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
-{% endbuttons %}
-</form>
-
-
-{% if error_list %}
- <hr>
-
- <p class='error'>{% trans "There have been some errors. No files have been added to the repository." %}
- <h3>{% trans "Offending files" %}</h3>
- <ul id='error-list'>
- {% for filename, title, error in error_list %}
- <li>{{ title }} (<code>{{ filename }}</code>): {{ error }}</li>
- {% endfor %}
- </ul>
-
- {% if ok_list %}
- <h3>{% trans "Correct files" %}</h3>
- <ul>
- {% for filename, slug, title in ok_list %}
- <li>{{ title }} (<code>{{ filename }}</code>)</li>
- {% endfor %}
- </ul>
- {% endif %}
-
-{% else %}
-
- {% if ok_list %}
- <p class='success'>{% trans "Files have been successfully uploaded to the repository." %}</p>
- <h3>{% trans "Uploaded files" %}</h3>
- <ul id='ok-list'>
- {% for filename, slug, title in ok_list %}
- <li><a href='{% url "wiki_editor" slug %}'>{{ title }}</a> (<code>{{ filename }})</a></li>
- {% endfor %}
- </ul>
- {% endif %}
-{% endif %}
-
-{% if skipped_list %}
- <h3>{% trans "Skipped files" %}</h3>
- <p>{% trans "Files skipped due to no <code>.xml</code> extension" %}</p>
- <ul id='skipped-list'>
- {% for filename in skipped_list %}
- <li>{{ filename }}</li>
- {% endfor %}
- </ul>
-{% endif %}
-
- </div>
-</div>
-
-{% endblock content %}
-
+++ /dev/null
-{% extends "catalogue/base.html" %}
-{% load book_list i18n %}
-{% load bootstrap4 %}
-
-
-{% block titleextra %}{{ object.title }}{% endblock %}
-
-
-{% block content %}
-<div class="card mt-4">
-
- <div class="card-header">
-<h1>{{ object.title }}</h1>
- </div>
- <div class="card-body">
-
-
-{% if editable %}<form method='POST'>{% csrf_token %}{% endif %}
- {% bootstrap_form form %}
- {% if editable %}
- {% buttons %}
- <button class="btn btn-primary" type="submit">{% trans "Save" %}</button>
- {% endbuttons %}
- {% endif %}
-</tbody></table>
-{% if editable %}</form>{% endif %}
-
- </div>
-</div>
-
-
-<div class='card mt-4'>
- <div class="card-header">
- <h2>{% trans "Editor" %}</h2>
- </div>
- <div class="card-body">
-
- <p><a class="btn btn-primary" href="{% url 'wiki_img_editor' object.slug %}">{% trans "Proceed to the editor." %}</a></p>
-</div>
-</div>
-
-
-
-<div class='card mt-4'>
- <div class="card-header">
-
-<h2>{% trans "Publication" %}</h2>
- </div>
- <div class="card-body">
-
-<p>{% trans "Last published" %}:
- {% if object.last_published %}
- {{ object.last_published }}
- {% else %}
- —
- {% endif %}
-</p>
-
-{% if publishable %}
- {% if user.is_authenticated %}
- <!--
- Angel photos:
- Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
- mira66 (http://www.flickr.com/photos/21804434@N02/) /
- CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
- -->
- <form method="POST" action="{% url 'catalogue_publish_image' object.slug %}">{% csrf_token %}
- <!--img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" /-->
- <button id="publish-button" type="submit">
- <span>{% trans "Publish" %}</span></button>
- <!--img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" /-->
- </form>
- {% else %}
- <a href="{% url 'cas_ng_login' %}">{% trans "Log in to publish." %}</a>
- {% endif %}
-{% else %}
- <p>{% trans "This book can't be published yet, because:" %}</p>
- <ul><li>{{ publishable_error }}</li></ul>
-{% endif %}
-
-</div>
-</div>
-
-
-{% endblock content %}
+++ /dev/null
-{% extends "catalogue/base.html" %}
-
-{% load i18n %}
-{% load catalogue book_list %}
-{% load pipeline %}
-
-
-{% block titleextra %}{% trans "Image list" %}{% endblock %}
-
-
-{% block add_js %}
- {% javascript 'book_list' %}
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js"></script>
-{% endblock %}
-
-{% block add_css %}
- {% stylesheet 'book_list' %}
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css">
-{% endblock %}
-
-
-{% block content %}
- {% image_list %}
-{% endblock content %}
+++ /dev/null
-{% load i18n %}
-{% load username from common_tags %}
-
-<tr class="table-sm">
- <td><input type="checkbox" name="select_chunk" value="{{image.id}}"/></td>
- <td><a href="{% url 'catalogue_image' image.slug %}" class="btn btn-sm btn-secondary" title="{% trans "Image settings" %}">🖼</a></td>
- <td><a class="btn btn-primary" target="_blank"
- href="{% url 'wiki_img_editor' image.slug %}">
- {{ image.title }}</a></td>
- <td>{% if image.stage %}
- {{ image.stage }}
- {% else %}–
- {% endif %}</td>
- <td class='user-column'>{% if image.user %}<a href="{% url 'catalogue_user' image.user.username %}">{{ image.user|username }}</a>{% endif %}</td>
- <td>
- {% if image.published %}
- <span class="badge badge-info" title="{% trans "published" %}">opubl.</span>
- {% endif %}
- {% if image.new_publishable %}
- <span class="badge badge-primary" title="{% trans "publishable" %}">do publ.</span>
- {% endif %}
- {% if image.changed %}
- <span class="badge badge-warning title="{% trans "changed" %}">zmiany</span>
- {% endif %}
- </td>
- <td>{{ image.project.name }}</td>
-</tr>
+++ /dev/null
-{% load i18n %}
-{% load pagination_tags %}
-{% load username from common_tags %}
-
-<div class="card">
- <div class="card-body">
-
-
-<form name='filter' action='{{ request.path }}'>
-<input type='hidden' name="title" value="{{ request.GET.title }}" />
-<input type='hidden' name="stage" value="{{ request.GET.stage }}" />
-{% if not viewed_user %}
- <input type='hidden' name="user" value="{{ request.GET.user }}" />
-{% endif %}
-<input type='hidden' name="status" value="{{ request.GET.status }}" />
-<input type='hidden' name="project" value="{{ request.GET.project }}" />
-</form>
-
-<table id="file-list" class="table {% if viewed_user %}book-list-user{% endif %}">
- <thead><tr>
- <th></th>
- <th></th>
- <th class='book-search-column'>
- <form>
- <input title='{% trans "Search in book titles" %}' name="title"
- class='form-control text-filter' value="{{ request.GET.title }}" />
- </form>
- </th>
- <th><select name="stage" class="form-control filter">
- <option value=''>- {% trans "stage" %} -</option>
- <option {% if request.GET.stage == '-' %}selected="selected"
- {% endif %}value="-">- {% trans "none" %} -</option>
- {% for stage in stages %}
- <option {% if request.GET.stage == stage.slug %}selected="selected"
- {% endif %}value="{{ stage.slug }}">{{ stage.name }}</option>
- {% endfor %}
- </select></th>
-
- {% if not viewed_user %}
- <th><select name="user" class="form-control filter">
- <option value=''>- {% trans "editor" %} -</option>
- <option {% if request.GET.user == '-' %}selected="selected"
- {% endif %}value="-">- {% trans "none" %} -</option>
- {% for user in users %}
- <option {% if request.GET.user == user.username %}selected="selected"
- {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
- {% endfor %}
- </select></th>
- {% endif %}
-
- <th><select name="status" class="form-control filter">
- <option value=''>- {% trans "status" %} -</option>
- {% for state, label in states %}
- <option {% if request.GET.status == state %}selected="selected"
- {% endif %}value='{{ state }}'>{{ label }}</option>
- {% endfor %}
- </select></th>
-
- <th><select name="project" class="form-control filter">
- <option value=''>- {% trans "project" %} -</option>
- <option {% if request.GET.project == '-' %}selected="selected"
- {% endif %}value="-">- {% trans "none" %} -</option>
- {% for project in projects %}
- <option {% if request.GET.project == project.pk|slugify %}selected="selected"
- {% endif %}value='{{ project.pk }}'>{{ project.name }}</option>
- {% endfor %}
- </select></th>
-
- </tr></thead>
-
- {% autopaginate objects 100 as objects_page %}
- <tbody>
- {% for image in objects_page %}
- {% include 'catalogue/image_short.html' %}
- {% endfor %}
- </tbody>
-</table>
- {% paginate %}
- {% blocktrans count c=objects|length %}{{c}} image{% plural %}{{c}} images{% endblocktrans %}</th></tr>
-{% if not objects %}
- <p>{% trans "No images found." %}</p>
-{% endif %}
-
-<form id='chunk_mass_edit' action='{% url "catalogue_image_mass_edit" %}' style="display:none;">
-{% csrf_token %}
-<input type="hidden" name="ids" />
-<label for="mass_edit_stage">{% trans "Set stage" %}</label><input type="hidden" name="stage" id="mass_edit_stage"/>
-<label for="mass_edit_user">{% trans "Set user" %}</label><input type="hidden" name="user" id="mass_edit_stage" />
-<input type="hidden" name="status" />
-<label for="mass_edit_project">{% trans "Project" %}</label><input type="hidden" name="project" id="mass_edit_project" />
-<label for="mass_edit_more_users">{% trans "More users" %}</label>
-</form>
-
-<select name="other-user" style="display:none;">
- {% for user in other_users %}
- <option {% if request.GET.user == user.username %}selected="selected"
- {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
- {% endfor %}
-</select>
-
- </div>
-</div>
+++ /dev/null
-{% for tab in tabs %}
- <li class="nav-item"><a class="nav-link{% if active_tab == tab.slug %} active{% endif %}" href="{{ tab.url }}">{{ tab.caption }}</a></li>
-{% endfor %}
+++ /dev/null
-{% extends "catalogue/base.html" %}
-
-{% block titleextra %}Oznacz książki{% endblock %}
-
-
-{% block leftcolumn %}
-
-<h1>Oznacz książki</h1>
-
-<form method="post" action="">
- {% csrf_token %}
- {{ form.as_p }}
- <input type="submit" value="Oznacz">
-</form>
-
-{% endblock leftcolumn %}
\ No newline at end of file
+++ /dev/null
-{% extends "catalogue/base.html" %}
-
-{% block titleextra %}Oznaczono książki{% endblock %}
-
-
-{% block leftcolumn %}
-
-<h1>Oznaczono książki</h1>
-
-<p>Książki zostały oznaczone.</p>
-
-{% endblock leftcolumn %}
\ No newline at end of file
+++ /dev/null
-{% extends "catalogue/base.html" %}
-
-{% load i18n %}
-{% load catalogue book_list wall %}
-{% load pipeline %}
-
-{% block add_js %}
- {% javascript 'book_list' %}
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js"></script>
-{% endblock %}
-
-{% block add_css %}
- {% stylesheet 'book_list' %}
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css">
-{% endblock %}
-
-{% block titleextra %}{% trans "My page" %}{% endblock %}
-
-
-{% block leftcolumn %}
- {% book_list request.user %}
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-<div class="card">
- <div class="card-header">
- <h2>{% trans "Your last edited documents" %}</h2>
- </div>
- <div class="card-body">
- <ol>
- {% for edit_url, item in last_books %}
- <li><a
- {% if edit_url|length == 2 %}
- {# Temporary support for old-style last_books. #}
- href="{% url 'wiki_editor' edit_url.0 edit_url.1 %}"
- {% else %}
- href="{{ edit_url }}"
- {% endif %}
- target="_blank">{{ item.title }}</a><br/><span class="date">({{ item.time|date:"H:i:s, d/m/Y" }})</span></li>
- {% endfor %}
- </ol>
- </div>
-</div>
-
-<div class="card mt-4">
- <div class="card-header">
- <h2>{% trans "Recent activity for" %} {{ request.user|nice_name }}</h2>
- </div>
- <div class="card-body">
- {% wall request.user 10 %}
- </div></div>
-{% endblock rightcolumn %}
+++ /dev/null
-{% extends "catalogue/base.html" %}
-{% load i18n %}
-
-
-{% block titleextra %}{% trans "PDF file upload" %}{% endblock %}
-
-
-{% block content %}
-
-
-<h2>{% trans "PDF file upload" %}</h2>
-
-<form enctype="multipart/form-data" method="POST" action="">
-{% csrf_token %}
-{{ form.as_p }}
-<p><button type="submit">{% trans "Upload" %}</button></p>
-</form>
-
-
-{% endblock content %}
+++ /dev/null
-{% extends "catalogue/base.html" %}
-
-{% load i18n %}
-{% load username from common_tags %}
-
-
-{% block titleextra %}{% trans "Users" %}{% endblock %}
-
-
-{% block content %}
-<div class="card">
- <div class="card-header">
-
-<h1>{% trans "Users" %}</h1>
- </div>
-<div class="card-body">
-
-<ul>
-{% for user in users %}
- <li><a href="{% url 'catalogue_user' user.username %}">
- <span class="chunkno">{{ forloop.counter }}.</span>
- {{ user|username }}</a>
- ({{ user.count }})</li>
-{% endfor %}
-</ul>
-
-</div>
-</div>
-
-{% endblock content %}
+++ /dev/null
-{% extends "catalogue/base.html" %}
-
-{% load i18n %}
-{% load catalogue book_list wall %}
-
-
-{% block titleextra %}{{ viewed_user|nice_name }}{% endblock %}
-
-
-{% block leftcolumn %}
- <h1 class="mb-4">{{ viewed_user|nice_name }}</h1>
- {% book_list viewed_user %}
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-<div class="card">
- <div class="card-header">
- <h2>{% trans "Recent activity for" %} {{ viewed_user|nice_name }}</h2>
- </div>
- <div class="card-body">
-
- {% wall viewed_user 10 %}
- </div>
-</div>
-{% endblock rightcolumn %}
+++ /dev/null
-{% load i18n %}
-{% load gravatar %}
-{% load email %}
-{% load username from common_tags %}
-
-<ul class='wall'>
-{% for item in wall %}
- <li class="{{ item.tag }}{% if not item.user %} anonymous{% endif %}">
- <div class='gravatar'>
- {% if item.get_email %}
- <img src="{% gravatar_url item.get_email 32 %}"
- height="32" width="32" alt='Avatar' />
- <br/>
- {% endif %}
- </div>
-
- <div class="time">{{ item.timestamp }}</div>
- <h3>{{ item.header }}</h3>
- <a target="_blank" href='{{ item.url }}'>{{ item.title }}</a>
- <br/><strong>{% trans "user" %}:</strong>
- {% if item.user %}
- <a href="{% url 'catalogue_user' item.user.username %}">
- {{ item.user|username }}</a>
- <{{ item.user.email|email_link }}>
- {% else %}
- {{ item.user_name }}
- {% if item.email %}
- <{{ item.email|email_link }}>
- {% endif %}
- ({% trans "not logged in" %})
- {% endif %}
- <br/>{{ item.summary|linebreaksbr }}
- </li>
-{% empty %}
- <li>{% trans "No activity recorded." %}</li>
-{% endfor %}
-</ul>
+++ /dev/null
-# 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
+++ /dev/null
-# 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
-
+++ /dev/null
-# 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
+++ /dev/null
-# 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)
+++ /dev/null
-# 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),
- ])}
+++ /dev/null
-# 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()
+++ /dev/null
-<utwor>
- <liryka_l>
-
-<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
-<rdf:Description rdf:about="http://example.com/documents/book/test-book/">
-<dc:creator xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam</dc:creator>
-<dc:title xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Do M***</dc:title>
-<dc:publisher xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Fundacja Nowoczesna Polska</dc:publisher>
-<dc:subject.period xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Romantyzm</dc:subject.period>
-<dc:subject.type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Liryka</dc:subject.type>
-<dc:subject.genre xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Wiersz</dc:subject.genre>
-<!--dc:description xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Publikacja zrealizowana w ramach projektu Wolne Lektury (http://wolnelektury.pl). Reprodukcja cyfrowa wykonana przez Bibliotekę Narodową z egzemplarza pochodzącego ze zbiorów BN.</dc:description-->
-<dc:identifier.url xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m</dc:identifier.url>
-<dc:source.URL xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://www.polona.pl/Content/2222</dc:source.URL>
-<dc:source xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam (1798-1855), Poezje, tom 1 (Wiersze młodzieńcze - Ballady i romanse - Wiersze do r. 1824), Krakowska Spółdzielnia Wydawnicza, wyd. 2 zwiększone, Kraków, 1922</dc:source>
-
-<dc:rights xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Domena publiczna - Adam Mickiewicz zm. 1855</dc:rights>
-<dc:date.pd xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">1926</dc:date.pd>
-<dc:format xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">xml</dc:format>
-<dc:type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
-<dc:type xml:lang="en" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
-<dc:date xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">2007-09-06</dc:date>
-<dc:language xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">pol</dc:language>
-
-</rdf:Description>
-</rdf:RDF>
-
-<autor_utworu>Adam Mickiewicz</autor_utworu>
-<dzielo_nadrzedne>Sonety odeskie</dzielo_nadrzedne>
-<nazwa_utworu>Do M***</nazwa_utworu>
-
-<nota><akap>Wiérsz napisany w roku 1822</akap></nota>
-
-
-<strofa>Precz z moich oczu!... posłucham od razu,/
-Precz z mego serca!... i serce posłucha,/
-Precz z méj pamięci!... Nie! tego rozkazu/
-Moja i twoja pamięć nie posłucha.</strofa>
-
-<!-- TRIM_END -->
-</liryka_l>
-</utwor>
+++ /dev/null
-<utwor><liryka_l>
-<!-- TRIM_BEGIN -->
-
-<strofa>Jak cień tém dłuższy, gdy padnie z daleka,/
-Tém szerzéj koło żałobne roztoczy,/
-Tak moja postać, im daléj ucieka,/
-Tém grubszym kirem twą pamięć pomroczy.</strofa>
-
-
-</liryka_l>
-</utwor>
+++ /dev/null
-<utwor>
- <liryka_l>
-
-<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
-<rdf:Description rdf:about="http://example.com/documents/book/test-book/">
-<dc:creator xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam</dc:creator>
-<dc:title xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Do M***</dc:title>
-<dc:publisher xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Fundacja Nowoczesna Polska</dc:publisher>
-<dc:subject.period xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Romantyzm</dc:subject.period>
-<dc:subject.type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Liryka</dc:subject.type>
-<dc:subject.genre xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Wiersz</dc:subject.genre>
-<!--dc:description xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Publikacja zrealizowana w ramach projektu Wolne Lektury (http://wolnelektury.pl). Reprodukcja cyfrowa wykonana przez Bibliotekę Narodową z egzemplarza pochodzącego ze zbiorów BN.</dc:description-->
-<dc:identifier.url xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m</dc:identifier.url>
-<dc:source.URL xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://www.polona.pl/Content/2222</dc:source.URL>
-<dc:source xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam (1798-1855), Poezje, tom 1 (Wiersze młodzieńcze - Ballady i romanse - Wiersze do r. 1824), Krakowska Spółdzielnia Wydawnicza, wyd. 2 zwiększone, Kraków, 1922</dc:source>
-
-<dc:rights xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Domena publiczna - Adam Mickiewicz zm. 1855</dc:rights>
-<dc:date.pd xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">1926</dc:date.pd>
-<dc:format xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">xml</dc:format>
-<dc:type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
-<dc:type xml:lang="en" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
-<dc:date xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">2007-09-06</dc:date>
-<dc:language xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">pol</dc:language>
-
-</rdf:Description>
-</rdf:RDF>
-
-<autor_utworu>Adam Mickiewicz</autor_utworu>
-<dzielo_nadrzedne>Sonety odeskie</dzielo_nadrzedne>
-<nazwa_utworu>Do M***</nazwa_utworu>
-
-<nota><akap>Wiérsz napisany w roku 1822</akap></nota>
-
-
-<strofa>Precz z moich oczu!... posłucham od razu,/
-Precz z mego serca!... i serce posłucha,/
-Precz z méj pamięci!... Nie! tego rozkazu/
-Moja i twoja pamięć nie posłucha.</strofa>
-
-
-
-<strofa>Jak cień tém dłuższy, gdy padnie z daleka,/
-Tém szerzéj koło żałobne roztoczy,/
-Tak moja postać, im daléj ucieka,/
-Tém grubszym kirem twą pamięć pomroczy.</strofa>
-
-
-</liryka_l>
-</utwor>
+++ /dev/null
-# 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!')
+++ /dev/null
-# 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',
- ])
-
+++ /dev/null
-# 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)
+++ /dev/null
-# 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"
- )
+++ /dev/null
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from django.conf.urls import url
-from django.contrib.auth.decorators import permission_required
-from django.views.generic import RedirectView
-from catalogue.feeds import PublishTrackFeed
-from . import views
-
-
-urlpatterns = [
- url(r'^$', RedirectView.as_view(url='catalogue/', permanent=False)),
-
- url(r'^images/$', views.image_list, name='catalogue_image_list'),
- url(r'^image/(?P<slug>[^/]+)/$', views.image, name="catalogue_image"),
- url(r'^image/(?P<slug>[^/]+)/publish$', views.publish_image,
- name="catalogue_publish_image"),
-
- url(r'^catalogue/$', views.document_list, name='catalogue_document_list'),
- url(r'^user/$', views.my, name='catalogue_user'),
- url(r'^user/(?P<username>[^/]+)/$', views.user, name='catalogue_user'),
- url(r'^users/$', views.users, name='catalogue_users'),
- url(r'^activity/$', views.activity, name='catalogue_activity'),
- url(r'^activity/(?P<isodate>\d{4}-\d{2}-\d{2})/$',
- views.activity, name='catalogue_activity'),
-
- url(r'^upload/$',
- views.upload, name='catalogue_upload'),
-
- url(r'^create/(?P<slug>[^/]*)/',
- views.create_missing, name='catalogue_create_missing'),
- url(r'^create/',
- views.create_missing, name='catalogue_create_missing'),
-
- url(r'^book/(?P<slug>[^/]+)/publish$', views.publish, name="catalogue_publish"),
-
- url(r'^book/(?P<slug>[^/]+)/$', views.book, name="catalogue_book"),
- url(r'^book/(?P<slug>[^/]+)/gallery/$',
- permission_required('catalogue.change_book')(views.GalleryView.as_view()),
- name="catalogue_book_gallery"),
- url(r'^book/(?P<slug>[^/]+)/xml$', views.book_xml, name="catalogue_book_xml"),
- url(r'^book/dc/(?P<slug>[^/]+)/xml$', views.book_xml_dc, name="catalogue_book_xml_dc"),
- url(r'^book/(?P<slug>[^/]+)/txt$', views.book_txt, name="catalogue_book_txt"),
- url(r'^book/(?P<slug>[^/]+)/html$', views.book_html, name="catalogue_book_html"),
- url(r'^book/(?P<slug>[^/]+)/epub$', views.book_epub, name="catalogue_book_epub"),
- url(r'^book/(?P<slug>[^/]+)/mobi$', views.book_mobi, name="catalogue_book_mobi"),
- url(r'^book/(?P<slug>[^/]+)/pdf$', views.book_pdf, name="catalogue_book_pdf"),
- url(r'^book/(?P<slug>[^/]+)/pdf-mobile$', views.book_pdf, kwargs={'mobile': True}, name="catalogue_book_pdf_mobile"),
-
- url(r'^chunk_add/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
- views.chunk_add, name="catalogue_chunk_add"),
- url(r'^chunk_edit/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
- views.chunk_edit, name="catalogue_chunk_edit"),
- url(r'^book_append/(?P<slug>[^/]+)/$',
- views.book_append, name="catalogue_book_append"),
- url(r'^chunk_mass_edit',
- views.chunk_mass_edit, name='catalogue_chunk_mass_edit'),
- url(r'^image_mass_edit',
- views.image_mass_edit, name='catalogue_image_mass_edit'),
-
- url(r'^track/(?P<slug>[^/]*)/$', PublishTrackFeed()),
- url(r'^active/$', views.active_users_list, name='active_users_list'),
-
- url(r'^mark-final/$', views.mark_final, name='mark_final'),
- url(r'^mark-final-completed/$', views.mark_final_completed, name='mark_final_completed'),
-]
+++ /dev/null
-# 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')
+++ /dev/null
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-from copy import deepcopy
-import re
-
-from lxml import etree
-from catalogue.constants import TRIM_BEGIN, TRIM_END, MASTERS
-
-RE_TRIM_BEGIN = re.compile("^<!--%s-->$" % TRIM_BEGIN, re.M)
-RE_TRIM_END = re.compile("^<!--%s-->$" % TRIM_END, re.M)
-
-
-class ParseError(BaseException):
- pass
-
-
-def _trim(text, trim_begin=True, trim_end=True):
- """
- Cut off everything before RE_TRIM_BEGIN and after RE_TRIM_END, so
- that eg. one big XML file can be compiled from many small XML files.
- """
- if trim_begin:
- text = RE_TRIM_BEGIN.split(text, maxsplit=1)[-1]
- if trim_end:
- text = RE_TRIM_END.split(text, maxsplit=1)[0]
- return text
-
-
-def compile_text(parts):
- """
- Compiles full text from an iterable of parts,
- trimming where applicable.
- """
- texts = []
- trim_begin = False
- text = ''
- for next_text in parts:
- if not next_text:
- continue
- if text:
- # trim the end, because there's more non-empty text
- # don't trim beginning, if `text' is the first non-empty part
- texts.append(_trim(text, trim_begin=trim_begin))
- trim_begin = True
- text = next_text
- # don't trim the end, because there's no more text coming after `text'
- # only trim beginning if it's not still the first non-empty
- texts.append(_trim(text, trim_begin=trim_begin, trim_end=False))
- return "".join(texts)
-
-
-def add_trim_begin(text):
- trim_tag = etree.Comment(TRIM_BEGIN)
- e = etree.fromstring(text)
- for master in e[::-1]:
- if master.tag in MASTERS:
- break
- if master.tag not in MASTERS:
- raise ParseError('No master tag found!')
-
- master.insert(0, trim_tag)
- trim_tag.tail = '\n\n\n' + (master.text or '')
- master.text = '\n'
- return str(etree.tostring(e, encoding="utf-8"), 'utf-8')
-
-
-def add_trim_end(text):
- trim_tag = etree.Comment(TRIM_END)
- e = etree.fromstring(text)
- for master in e[::-1]:
- if master.tag in MASTERS:
- break
- if master.tag not in MASTERS:
- raise ParseError('No master tag found!')
-
- master.append(trim_tag)
- trim_tag.tail = '\n'
- prev = trim_tag.getprevious()
- if prev is not None:
- prev.tail = (prev.tail or '') + '\n\n\n'
- else:
- master.text = (master.text or '') + '\n\n\n'
- return str(etree.tostring(e, encoding="utf-8"), 'utf-8')
-
-
-def split_xml(text):
- """Splits text into chapters.
-
- All this stuff really must go somewhere else.
-
- """
- src = etree.fromstring(text)
- chunks = []
-
- splitter = u'naglowek_rozdzial'
- parts = src.findall('.//naglowek_rozdzial')
- while parts:
- # copy the document
- copied = deepcopy(src)
-
- element = parts[-1]
-
- # find the chapter's title
- name_elem = deepcopy(element)
- for tag in 'extra', 'motyw', 'pa', 'pe', 'pr', 'pt', 'uwaga':
- for a in name_elem.findall('.//' + tag):
- a.text=''
- del a[:]
- name = etree.tostring(name_elem, method='text', encoding='utf-8').strip()
-
- # in the original, remove everything from the start of the last chapter
- parent = element.getparent()
- del parent[parent.index(element):]
- element, parent = parent, parent.getparent()
- while parent is not None:
- del parent[parent.index(element) + 1:]
- element, parent = parent, parent.getparent()
-
- # in the copy, remove everything before the last chapter
- element = copied.findall('.//naglowek_rozdzial')[-1]
- parent = element.getparent()
- while parent is not None:
- parent.text = None
- while parent[0] is not element:
- del parent[0]
- element, parent = parent, parent.getparent()
- chunks[:0] = [[name,
- str(etree.tostring(copied, encoding='utf-8'), 'utf-8')
- ]]
-
- parts = src.findall('.//naglowek_rozdzial')
-
- chunks[:0] = [[u'początek',
- str(etree.tostring(src, encoding='utf-8'), 'utf-8')
- ]]
-
- for ch in chunks[1:]:
- ch[1] = add_trim_begin(ch[1])
- for ch in chunks[:-1]:
- ch[1] = add_trim_end(ch[1])
-
- return chunks
-{% extends "catalogue/base.html" %}
+{% extends "documents/base.html" %}
{% load i18n %}
{% load bootstrap4 %}
-{% extends "catalogue/base.html" %}
+{% extends "documents/base.html" %}
{% load i18n %}
{% load thumbnail %}
{% load build_absolute_uri from fnp_common %}
-{% extends "catalogue/base.html" %}
+{% extends "documents/base.html" %}
{% load i18n %}
{% load thumbnail pagination_tags %}
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
--- /dev/null
+# 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)
--- /dev/null
+# 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',
+ ]
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+[
+ {
+ "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"
+ }
+ }
+]
--- /dev/null
+# 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)
--- /dev/null
+# 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
--- /dev/null
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Platforma Redakcyjna\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-10-07 13:05+0200\n"
+"PO-Revision-Date: 2019-10-07 13:06+0200\n"
+"Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
+"Language-Team: Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org."
+"pl>\n"
+"Language: pl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2);\n"
+"X-Generator: Poedit 2.0.6\n"
+
+#: forms.py:38
+msgid "Text file must be UTF-8 encoded."
+msgstr "Plik powinien mieć kodowanie UTF-8."
+
+#: forms.py:41
+msgid "You must either enter text or upload a file"
+msgstr "Proszę wpisać tekst albo wybrać plik do załadowania"
+
+#: forms.py:50
+msgid "ZIP file"
+msgstr "Plik ZIP"
+
+#: forms.py:51
+msgid "Directories are documents in chunks"
+msgstr "Katalogi zawierają dokumenty w częściach"
+
+#: forms.py:75 forms.py:176
+msgid "Assigned to"
+msgstr "Przypisane do"
+
+#: forms.py:96 forms.py:110
+msgid "Chunk with this slug already exists"
+msgstr "Część z tym slugiem już istnieje"
+
+#: forms.py:119
+msgid "Append to"
+msgstr "Dołącz do"
+
+#: models/book.py:26 models/chunk.py:21 models/image.py:20
+msgid "title"
+msgstr "tytuł"
+
+#: models/book.py:27 models/chunk.py:22 models/image.py:21
+msgid "slug"
+msgstr "slug"
+
+#: models/book.py:28 models/image.py:22
+msgid "public"
+msgstr "publiczna"
+
+#: models/book.py:29
+msgid "scan gallery name"
+msgstr "nazwa galerii skanów"
+
+#: models/book.py:33
+msgid "parent"
+msgstr "rodzic"
+
+#: models/book.py:34
+msgid "parent number"
+msgstr "numeracja rodzica"
+
+#: models/book.py:52 models/chunk.py:19 models/publish_log.py:15
+msgid "book"
+msgstr "książka"
+
+#: models/book.py:53 views.py:619
+msgid "books"
+msgstr "książki"
+
+#: models/book.py:257
+msgid "No chunks in the book."
+msgstr "Książka nie ma części."
+
+#: models/book.py:261
+msgid "Not all chunks have publishable revisions."
+msgstr "Niektóre części nie są gotowe do publikacji."
+
+#: models/book.py:268 models/image.py:83
+msgid "Invalid XML"
+msgstr "Nieprawidłowy XML"
+
+#: models/book.py:270 models/image.py:85
+msgid "No Dublin Core found."
+msgstr "Brak sekcji Dublin Core."
+
+#: models/book.py:272 models/image.py:87
+msgid "Invalid Dublin Core"
+msgstr "Nieprawidłowy Dublin Core"
+
+#: models/book.py:275 models/image.py:91
+msgid "rdf:about is not"
+msgstr "rdf:about jest różny od"
+
+#: models/chunk.py:20
+msgid "number"
+msgstr "numer"
+
+#: models/chunk.py:23
+msgid "gallery start"
+msgstr "początek galerii"
+
+#: models/chunk.py:38
+msgid "chunk"
+msgstr "część"
+
+#: models/chunk.py:39
+msgid "chunks"
+msgstr "części"
+
+#: models/image.py:19 models/image.py:33 models/publish_log.py:43
+msgid "image"
+msgstr "obraz"
+
+#: models/image.py:34
+msgid "images"
+msgstr "obrazy"
+
+#: models/image.py:75
+msgid "There is no publishable revision"
+msgstr "Żadna wersja nie została oznaczona do publikacji."
+
+#: models/project.py:11
+msgid "name"
+msgstr "nazwa"
+
+#: models/project.py:12
+msgid "notes"
+msgstr "notatki"
+
+#: models/project.py:17 templates/catalogue/book_list/book_list.html:66
+#: templates/catalogue/image_table.html:60
+msgid "project"
+msgstr "projekt"
+
+#: models/project.py:18
+msgid "projects"
+msgstr "projekty"
+
+#: models/publish_log.py:16 models/publish_log.py:44
+msgid "time"
+msgstr "czas"
+
+#: models/publish_log.py:17 models/publish_log.py:45
+#: templates/catalogue/wall.html:20
+msgid "user"
+msgstr "użytkownik"
+
+#: models/publish_log.py:22 models/publish_log.py:31
+msgid "book publish record"
+msgstr "zapis publikacji książki"
+
+#: models/publish_log.py:23
+msgid "book publish records"
+msgstr "zapisy publikacji książek"
+
+#: models/publish_log.py:32 models/publish_log.py:46
+msgid "change"
+msgstr "zmiana"
+
+#: models/publish_log.py:36
+msgid "chunk publish record"
+msgstr "zapis publikacji części"
+
+#: models/publish_log.py:37
+msgid "chunk publish records"
+msgstr "zapisy publikacji części"
+
+#: models/publish_log.py:51
+msgid "image publish record"
+msgstr "zapis publikacji obrazu"
+
+#: models/publish_log.py:52
+msgid "image publish records"
+msgstr "zapisy publikacji obrazów"
+
+#: templates/catalogue/active_users_list.html:5
+msgid "Active users"
+msgstr "Aktywni użytkownicy"
+
+#: templates/catalogue/active_users_list.html:11
+msgid "Users active in the year"
+msgstr "Użytkownicy aktywni w roku"
+
+#: templates/catalogue/activity.html:6 templates/catalogue/activity.html:15
+#: templatetags/catalogue.py:30
+msgid "Activity"
+msgstr "Aktywność"
+
+#: templates/catalogue/base.html:13
+msgid "Platforma Redakcyjna"
+msgstr "Platforma Redakcyjna"
+
+#: templates/catalogue/book_append_to.html:5
+#: templates/catalogue/book_append_to.html:14
+msgid "Append book"
+msgstr "Dołącz książkę"
+
+#: templates/catalogue/book_detail.html:23
+#: templates/catalogue/book_edit.html:13 templates/catalogue/chunk_edit.html:22
+#: templates/catalogue/image_detail.html:22
+msgid "Save"
+msgstr "Zapisz"
+
+#: templates/catalogue/book_detail.html:30
+msgid "Edit gallery"
+msgstr "Edytuj galerię"
+
+#: templates/catalogue/book_detail.html:33
+msgid "Append to other book"
+msgstr "Dołącz do innej książki"
+
+#: templates/catalogue/book_detail.html:40
+msgid "Chunks"
+msgstr "Części"
+
+#: templates/catalogue/book_detail.html:58
+#: templates/catalogue/image_detail.html:47 templatetags/wall.py:108
+#: templatetags/wall.py:129
+msgid "Publication"
+msgstr "Publikacja"
+
+#: templates/catalogue/book_detail.html:69
+#: templates/catalogue/image_detail.html:51
+msgid "Last published"
+msgstr "Ostatnio opublikowano"
+
+#: templates/catalogue/book_detail.html:79
+msgid "Full XML"
+msgstr "Pełny XML"
+
+#: templates/catalogue/book_detail.html:80
+msgid "HTML version"
+msgstr "Wersja HTML"
+
+#: templates/catalogue/book_detail.html:81
+msgid "TXT version"
+msgstr "Wersja TXT"
+
+#: templates/catalogue/book_detail.html:82
+msgid "PDF version"
+msgstr "Wersja PDF"
+
+#: templates/catalogue/book_detail.html:83
+msgid "PDF version for mobiles"
+msgstr "Wersja PDF na telefony"
+
+#: templates/catalogue/book_detail.html:84
+msgid "EPUB version"
+msgstr "Wersja EPUB"
+
+#: templates/catalogue/book_detail.html:85
+msgid "MOBI version"
+msgstr "Wersja MOBI"
+
+#: templates/catalogue/book_detail.html:99
+#: templates/catalogue/image_detail.html:70
+msgid "Publish"
+msgstr "Opublikuj"
+
+#: templates/catalogue/book_detail.html:103
+#: templates/catalogue/image_detail.html:74
+msgid "Log in to publish."
+msgstr "Zaloguj się, aby opublikować."
+
+#: templates/catalogue/book_detail.html:106
+#: templates/catalogue/image_detail.html:77
+msgid "This book can't be published yet, because:"
+msgstr "Ta książka nie może jeszcze zostać opublikowana. Powód:"
+
+#: templates/catalogue/book_edit.html:5
+msgid "Edit book"
+msgstr "Edytuj książkę"
+
+#: templates/catalogue/book_html.html:12 templates/catalogue/book_text.html:15
+msgid "Table of contents"
+msgstr "Spis treści"
+
+#: templates/catalogue/book_html.html:13 templates/catalogue/book_text.html:17
+msgid "Edit. note"
+msgstr "Nota red."
+
+#: templates/catalogue/book_html.html:14
+msgid "Infobox"
+msgstr "Informacje"
+
+#: templates/catalogue/book_list/book.html:8
+#: templates/catalogue/book_list/book.html:36
+msgid "Book settings"
+msgstr "Ustawienia książki"
+
+#: templates/catalogue/book_list/book.html:9
+#: templates/catalogue/book_list/chunk.html:7
+#: templates/catalogue/chunk_edit.html:6 templates/catalogue/chunk_edit.html:12
+msgid "Chunk settings"
+msgstr "Ustawienia części"
+
+#: templates/catalogue/book_list/book.html:12
+#: templates/catalogue/book_list/chunk.html:9
+#: templates/catalogue/image_short.html:9
+msgid "Edit:"
+msgstr "Edytuj:"
+
+#: templates/catalogue/book_list/book.html:21
+#: templates/catalogue/book_list/book.html:43
+#: templates/catalogue/image_short.html:18 templatetags/book_list.py:82
+#: templatetags/book_list.py:150
+msgid "published"
+msgstr "opublikowane"
+
+#: templates/catalogue/book_list/book.html:24
+#: templates/catalogue/book_list/book.html:46
+#: templates/catalogue/book_list/chunk.html:28
+#: templates/catalogue/image_short.html:21 templatetags/book_list.py:80
+#: templatetags/book_list.py:148
+msgid "publishable"
+msgstr "do publikacji"
+
+#: templates/catalogue/book_list/book.html:27
+#: templates/catalogue/book_list/chunk.html:33
+#: templates/catalogue/image_short.html:24 templatetags/book_list.py:81
+#: templatetags/book_list.py:149
+msgid "changed"
+msgstr "zmienione"
+
+#: templates/catalogue/book_list/book_list.html:29
+#: templates/catalogue/image_table.html:25
+msgid "Search in book titles"
+msgstr "Szukaj w tytułach książek"
+
+#: templates/catalogue/book_list/book_list.html:34
+#: templates/catalogue/image_table.html:30
+msgid "stage"
+msgstr "etap"
+
+#: templates/catalogue/book_list/book_list.html:36
+#: templates/catalogue/book_list/book_list.html:47
+#: templates/catalogue/book_list/book_list.html:68
+#: templates/catalogue/image_table.html:32
+#: templates/catalogue/image_table.html:43
+#: templates/catalogue/image_table.html:62
+msgid "none"
+msgstr "brak"
+
+#: templates/catalogue/book_list/book_list.html:45
+#: templates/catalogue/image_table.html:41
+msgid "editor"
+msgstr "redaktor"
+
+#: templates/catalogue/book_list/book_list.html:58
+#: templates/catalogue/image_table.html:52
+msgid "status"
+msgstr "status"
+
+#: templates/catalogue/book_list/book_list.html:92
+#, python-format
+msgid "%(c)s book"
+msgid_plural "%(c)s books"
+msgstr[0] "%(c)s książka"
+msgstr[1] "%(c)s książki"
+msgstr[2] "%(c)s książek"
+
+#: templates/catalogue/book_list/book_list.html:96
+msgid "No books found."
+msgstr "Nie znaleziono książek."
+
+#: templates/catalogue/book_list/book_list.html:105
+#: templates/catalogue/image_table.html:87
+msgid "Set stage"
+msgstr "Ustaw etap"
+
+#: templates/catalogue/book_list/book_list.html:106
+#: templates/catalogue/image_table.html:88
+msgid "Set user"
+msgstr "Przypisz redaktora"
+
+#: templates/catalogue/book_list/book_list.html:108
+#: templates/catalogue/image_table.html:90
+msgid "Project"
+msgstr "Projekt"
+
+#: templates/catalogue/book_list/book_list.html:109
+#: templates/catalogue/image_table.html:91
+msgid "More users"
+msgstr "Więcej użytkowników"
+
+#: templates/catalogue/book_text.html:7
+msgid "Redakcja"
+msgstr ""
+
+#: templates/catalogue/chunk_add.html:6 templates/catalogue/chunk_add.html:12
+#: templates/catalogue/chunk_edit.html:29
+msgid "Split chunk"
+msgstr "Podziel część"
+
+#: templates/catalogue/chunk_add.html:19
+msgid "Insert empty chunk after"
+msgstr "Wstaw pustą część po"
+
+#: templates/catalogue/chunk_add.html:23
+msgid "Add chunk"
+msgstr "Dodaj część"
+
+#: templates/catalogue/chunk_edit.html:19
+msgid "Book"
+msgstr "Książka"
+
+#: templates/catalogue/document_create_missing.html:6
+#: templates/catalogue/document_create_missing.html:12
+msgid "Create a new book"
+msgstr "Utwórz nową książkę"
+
+#: templates/catalogue/document_create_missing.html:21
+msgid "Create book"
+msgstr "Utwórz książkę"
+
+#: templates/catalogue/document_list.html:7
+msgid "Book list"
+msgstr "Lista książek"
+
+#: templates/catalogue/document_upload.html:6
+msgid "Bulk document upload"
+msgstr "Hurtowe dodawanie dokumentów"
+
+#: templates/catalogue/document_upload.html:14
+msgid "Bulk documents upload"
+msgstr "Hurtowe dodawanie dokumentów"
+
+#: templates/catalogue/document_upload.html:19
+msgid ""
+"Please submit a ZIP with UTF-8 encoded XML files. Files not ending with "
+"<code>.xml</code> will be ignored."
+msgstr ""
+"Proszę wskazać archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie "
+"kończące się na <code>.xml</code> zostaną zignorowane."
+
+#: templates/catalogue/document_upload.html:26
+#: templates/catalogue/upload_pdf.html:16 templatetags/catalogue.py:37
+msgid "Upload"
+msgstr "Załaduj"
+
+#: templates/catalogue/document_upload.html:34
+msgid ""
+"There have been some errors. No files have been added to the repository."
+msgstr "Wystąpiły błędy. Żadne pliki nie zostały dodane do repozytorium."
+
+#: templates/catalogue/document_upload.html:35
+msgid "Offending files"
+msgstr "Błędne pliki"
+
+#: templates/catalogue/document_upload.html:43
+msgid "Correct files"
+msgstr "Poprawne pliki"
+
+#: templates/catalogue/document_upload.html:54
+msgid "Files have been successfully uploaded to the repository."
+msgstr "Pliki zostały dodane do repozytorium."
+
+#: templates/catalogue/document_upload.html:55
+msgid "Uploaded files"
+msgstr "Dodane pliki"
+
+#: templates/catalogue/document_upload.html:65
+msgid "Skipped files"
+msgstr "Pominięte pliki"
+
+#: templates/catalogue/document_upload.html:66
+msgid "Files skipped due to no <code>.xml</code> extension"
+msgstr "Pliki pominięte z powodu braku rozszerzenia <code>.xml</code>."
+
+#: templates/catalogue/head_login.html:10
+msgid "Admin"
+msgstr "Administracja"
+
+#: templates/catalogue/head_login.html:15
+msgid "Log Out"
+msgstr "Wyloguj"
+
+#: templates/catalogue/head_login.html:21
+msgid "Log In"
+msgstr "Zaloguj"
+
+#: templates/catalogue/image_detail.html:34
+msgid "Editor"
+msgstr "Edytor"
+
+#: templates/catalogue/image_detail.html:38
+msgid "Proceed to the editor."
+msgstr "Przejdź do edytora."
+
+#: templates/catalogue/image_list.html:8
+msgid "Image list"
+msgstr "Lista obrazów"
+
+#: templates/catalogue/image_short.html:6
+msgid "Image settings"
+msgstr "Ustawienia obrazu"
+
+#: templates/catalogue/image_table.html:79
+#, python-format
+msgid "%(c)s image"
+msgid_plural "%(c)s images"
+msgstr[0] "%(c)s obraz"
+msgstr[1] "%(c)s obrazy"
+msgstr[2] "%(c)s obrazów"
+
+#: templates/catalogue/image_table.html:81
+msgid "No images found."
+msgstr "Nie znaleziono obrazów."
+
+#: templates/catalogue/my_page.html:15 templatetags/catalogue.py:28
+msgid "My page"
+msgstr "Moja strona"
+
+#: templates/catalogue/my_page.html:25
+msgid "Your last edited documents"
+msgstr "Twoje ostatnie edycje"
+
+#: templates/catalogue/my_page.html:45 templates/catalogue/user_page.html:18
+msgid "Recent activity for"
+msgstr "Ostatnia aktywność dla:"
+
+#: templates/catalogue/upload_pdf.html:5 templates/catalogue/upload_pdf.html:11
+msgid "PDF file upload"
+msgstr "Ładowanie pliku PDF"
+
+#: templates/catalogue/user_list.html:7 templates/catalogue/user_list.html:14
+#: templatetags/catalogue.py:33
+msgid "Users"
+msgstr "Użytkownicy"
+
+#: templates/catalogue/wall.html:30
+msgid "not logged in"
+msgstr "nie zalogowany"
+
+#: templates/catalogue/wall.html:35
+msgid "No activity recorded."
+msgstr "Nie zanotowano aktywności."
+
+#: templatetags/book_list.py:83 templatetags/book_list.py:151
+msgid "unpublished"
+msgstr "nie opublikowane"
+
+#: templatetags/book_list.py:84 templatetags/book_list.py:152
+msgid "empty"
+msgstr "puste"
+
+#: templatetags/catalogue.py:31
+msgid "All"
+msgstr "Wszystkie"
+
+#: templatetags/catalogue.py:32
+msgid "Images"
+msgstr "Obrazy"
+
+#: templatetags/catalogue.py:36
+msgid "Add"
+msgstr "Dodaj"
+
+#: templatetags/catalogue.py:39
+msgid "Covers"
+msgstr "Okładki"
+
+#: templatetags/wall.py:49 templatetags/wall.py:78
+msgid "Related edit"
+msgstr "Powiązana zmiana"
+
+#: templatetags/wall.py:51 templatetags/wall.py:80
+msgid "Edit"
+msgstr "Zmiana"
+
+#: views.py:172
+#, python-format
+msgid "Slug already used for %s"
+msgstr "Slug taki sam jak dla pliku %s"
+
+#: views.py:174
+msgid "Slug already used in repository."
+msgstr "Dokument o tym slugu już istnieje w repozytorium."
+
+#: views.py:180
+msgid "File should be UTF-8 encoded."
+msgstr "Plik powinien mieć kodowanie UTF-8."
+
+#: views.py:621
+msgid "scan gallery"
+msgstr "galeria skanów"
+
+#~ msgid "Active users since"
+#~ msgstr "Użytkownicy aktywni od"
+
+#~ msgid "Show hidden books"
+#~ msgstr "Pokaż ukryte książki"
+
+#~ msgid "Comment"
+#~ msgstr "Komentarz"
+
+#~ msgid "Comments"
+#~ msgstr "Komentarze"
+
+#~ msgid "Mark publishable"
+#~ msgstr "Oznacz do publikacji"
+
+#~ msgid "Mark not publishable"
+#~ msgstr "Odznacz do publikacji"
+
+#~ msgid "Other user"
+#~ msgstr "Inny użytkownik"
+
+#~ msgid "edit"
+#~ msgstr "edytuj"
+
+#~ msgid "add basic document structure"
+#~ msgstr "dodaj podstawową strukturę dokumentu"
+
+#~ msgid "change master tag to"
+#~ msgstr "zmień tak master na"
+
+#~ msgid "add begin trimming tag"
+#~ msgstr "dodaj początkowy ogranicznik"
+
+#~ msgid "add end trimming tag"
+#~ msgstr "dodaj końcowy ogranicznik"
+
+#~ msgid "unstructured text"
+#~ msgstr "tekst bez struktury"
+
+#~ msgid "unknown XML"
+#~ msgstr "nieznany XML"
+
+#~ msgid "broken document"
+#~ msgstr "uszkodzony dokument"
+
+#~ msgid "Apply fixes"
+#~ msgstr "Wykonaj zmiany"
+
+#~ msgid "Can mark for publishing"
+#~ msgstr "Oznacza do publikacji"
+
+#~ msgid "Author"
+#~ msgstr "Autor"
+
+#~ msgid "Your name"
+#~ msgstr "Imię i nazwisko"
+
+#~ msgid "Author's email"
+#~ msgstr "E-mail autora"
+
+#~ msgid "Your email address, so we can show a gravatar :)"
+#~ msgstr "Adres e-mail, żebyśmy mogli pokazać gravatar :)"
+
+#~ msgid "Describe changes you made."
+#~ msgstr "Opisz swoje zmiany"
+
+#~ msgid "Completed"
+#~ msgstr "Ukończono"
+
+#~ msgid "If you completed a life cycle stage, select it."
+#~ msgstr "Jeśli został ukończony etap prac, wskaż go."
+
+#~ msgid "Describe the reason for reverting."
+#~ msgstr "Opisz powód przywrócenia."
+
+#~ msgid "theme"
+#~ msgstr "motyw"
+
+#~ msgid "themes"
+#~ msgstr "motywy"
+
+#~ msgid "Tag added"
+#~ msgstr "Dodano tag"
+
+#~ msgid "Revision marked"
+#~ msgstr "Wersja oznaczona"
+
+#~ msgid "New version"
+#~ msgstr "Nowa wersja"
+
+#~ msgid "Click to open/close gallery"
+#~ msgstr "Kliknij, aby (ro)zwinąć galerię"
+
+#~ msgid "Help"
+#~ msgstr "Pomoc"
+
+#~ msgid "Version"
+#~ msgstr "Wersja"
+
+#~ msgid "Unknown"
+#~ msgstr "nieznana"
+
+#~ msgid "Save attempt in progress"
+#~ msgstr "Trwa zapisywanie"
+
+#~ msgid "There is a newer version of this document!"
+#~ msgstr "Istnieje nowsza wersja tego dokumentu!"
+
+#~ msgid "Clear filter"
+#~ msgstr "Wyczyść filtr"
+
+#~ msgid "Cancel"
+#~ msgstr "Anuluj"
+
+#~ msgid "Revert"
+#~ msgstr "Przywróć"
+
+#~ msgid "all"
+#~ msgstr "wszystkie"
+
+#~ msgid "Annotations"
+#~ msgstr "Przypisy"
+
+#~ msgid "Previous"
+#~ msgstr "Poprzednie"
+
+#~ msgid "Next"
+#~ msgstr "Następne"
+
+#~ msgid "Zoom in"
+#~ msgstr "Powiększ"
+
+#~ msgid "Zoom out"
+#~ msgstr "Zmniejsz"
+
+#~ msgid "Gallery"
+#~ msgstr "Galeria"
+
+#~ msgid "Compare versions"
+#~ msgstr "Porównaj wersje"
+
+#~ msgid "Revert document"
+#~ msgstr "Przywróć wersję"
+
+#~ msgid "View version"
+#~ msgstr "Zobacz wersję"
+
+#~ msgid "History"
+#~ msgstr "Historia"
+
+#~ msgid "Search"
+#~ msgstr "Szukaj"
+
+#~ msgid "Replace with"
+#~ msgstr "Zamień na"
+
+#~ msgid "Replace"
+#~ msgstr "Zamień"
+
+#~ msgid "Options"
+#~ msgstr "Opcje"
+
+#~ msgid "Case sensitive"
+#~ msgstr "Rozróżniaj wielkość liter"
+
+#~ msgid "From cursor"
+#~ msgstr "Zacznij od kursora"
+
+#~ msgid "Search and replace"
+#~ msgstr "Znajdź i zamień"
+
+#~ msgid "Source code"
+#~ msgstr "Kod źródłowy"
+
+#~ msgid "Title"
+#~ msgstr "Tytuł"
+
+#~ msgid "Document ID"
+#~ msgstr "ID dokumentu"
+
+#~ msgid "Current version"
+#~ msgstr "Aktualna wersja"
+
+#~ msgid "Last edited by"
+#~ msgstr "Ostatnio edytowane przez"
+
+#~ msgid "Summary"
+#~ msgstr "Podsumowanie"
+
+#~ msgid "Insert theme"
+#~ msgstr "Wstaw motyw"
+
+#~ msgid "Insert annotation"
+#~ msgstr "Wstaw przypis"
+
+#~ msgid "Visual editor"
+#~ msgstr "Edytor wizualny"
+
+#~ msgid "Unassigned"
+#~ msgstr "Nie przypisane"
+
+#~ msgid "First correction"
+#~ msgstr "Autokorekta"
+
+#~ msgid "Tagging"
+#~ msgstr "Tagowanie"
+
+#~ msgid "Initial Proofreading"
+#~ msgstr "Korekta"
+
+#~ msgid "Annotation Proofreading"
+#~ msgstr "Sprawdzenie przypisów źródła"
+
+#~ msgid "Modernisation"
+#~ msgstr "Uwspółcześnienie"
+
+#~ msgid "Themes"
+#~ msgstr "Motywy"
+
+#~ msgid "Editor's Proofreading"
+#~ msgstr "Ostateczna redakcja literacka"
+
+#~ msgid "Technical Editor's Proofreading"
+#~ msgstr "Ostateczna redakcja techniczna"
+
+#~ msgid "Finished stage: %s"
+#~ msgstr "Ukończony etap: %s"
+
+#~ msgid "Refresh"
+#~ msgstr "Odśwież"
--- /dev/null
+# 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)
--- /dev/null
+# 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()
--- /dev/null
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import sys
+
+from datetime import date
+from lxml import etree
+
+from django.core.management import BaseCommand
+
+from documents.models import Book
+from librarian import RDFNS, DCNS
+
+TEMPLATE = '''<utwor>
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description rdf:about="http://redakcja.wolnelektury.pl/documents/book/%(slug)s/">
+%(dc)s
+</rdf:Description>
+</rdf:RDF>
+
+</utwor>
+'''
+
+DC_TEMPLATE = '<dc:%(tag)s xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">%(value)s</dc:%(tag)s>'
+
+DC_TAGS = (
+ 'creator',
+ 'title',
+ 'relation.hasPart',
+ 'contributor.translator',
+ 'contributor.editor',
+ 'contributor.technical_editor',
+ 'contributor.funding',
+ 'contributor.thanks',
+ 'publisher',
+ 'subject.period',
+ 'subject.type',
+ 'subject.genre',
+ 'description',
+ 'identifier.url',
+ 'source',
+ 'source.URL',
+ 'rights.license',
+ 'rights',
+ 'date.pd',
+ 'format',
+ 'type',
+ 'date',
+ 'audience',
+ 'language',
+)
+
+IDENTIFIER_PREFIX = 'http://wolnelektury.pl/katalog/lektura/'
+
+
+def dc_desc_element(book):
+ xml = book.materialize()
+ tree = etree.fromstring(xml)
+ return tree.find(".//" + RDFNS("Description"))
+
+
+def distinct_dc_values(tag, desc_elements):
+ values = set()
+ for desc in desc_elements:
+ values.update(elem.text for elem in desc.findall(DCNS(tag)))
+ return values
+
+
+class Command(BaseCommand):
+ args = 'slug'
+
+ def handle(self, slug, **options):
+ children_slugs = [line.strip() for line in sys.stdin]
+ children = Book.objects.filter(dc_slug__in=children_slugs)
+ desc_elements = [dc_desc_element(child) for child in children]
+ title = u'Utwory wybrane'
+ own_attributes = {
+ 'title': title,
+ 'relation.hasPart': [IDENTIFIER_PREFIX + child_slug for child_slug in children_slugs],
+ 'identifier.url': IDENTIFIER_PREFIX + slug,
+ 'date': date.today().isoformat(),
+ }
+ dc_tags = []
+ for tag in DC_TAGS:
+ if tag in own_attributes:
+ values = own_attributes[tag]
+ if not isinstance(values, list):
+ values = [values]
+ else:
+ values = distinct_dc_values(tag, desc_elements)
+ for value in values:
+ dc_tags.append(DC_TEMPLATE % {'tag': tag, 'value': value})
+ xml = TEMPLATE % {'slug': slug, 'dc': '\n'.join(dc_tags)}
+ Book.create(
+ text=xml,
+ creator=None,
+ slug=slug,
+ title=title,
+ gallery=slug)
--- /dev/null
+# 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.'
--- /dev/null
+# 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()
+
--- /dev/null
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import csv
+
+import sys
+from django.contrib.auth.models import User
+from lxml import etree
+from collections import defaultdict
+from django.core.management import BaseCommand
+
+from documents.models import Book
+from librarian import RDFNS, DCNS
+
+CONTENT_TYPES = {
+ 'pdf': 'application/pdf',
+ 'epub': 'application/epub+zip',
+ 'mobi': 'application/x-mobipocket-ebook',
+ 'txt': 'text/plain',
+ 'html': 'text/html',
+}
+
+
+ISBN_TEMPLATES = (
+ r'<dc:relation.hasFormat id="%(format)s" xmlns:dc="http://purl.org/dc/elements/1.1/">%(url)s'
+ r'</dc:relation.hasFormat>',
+ r'<meta refines="#%(format)s" id="%(format)s-id" property="dcterms:identifier">ISBN-%(isbn)s</meta>',
+ r'<meta refines="#%(format)s-id" property="identifier-type">ISBN</meta>',
+ r'<meta refines="#%(format)s" property="dcterms:format">%(content_type)s</meta>',
+)
+
+
+def url_for_format(slug, format):
+ if format == 'html':
+ return 'https://wolnelektury.pl/katalog/lektura/%s.html' % slug
+ else:
+ return 'http://wolnelektury.pl/media/book/%(format)s/%(slug)s.%(format)s' % {'slug': slug, 'format': format}
+
+
+class Command(BaseCommand):
+ args = 'csv_file'
+
+ def add_arguments(self, parser):
+ self.add_argument(
+ '-u', '--username', dest='username', metavar='USER',
+ help='Assign commits to this user (required, preferably yourself).')
+
+ def handle(self, csv_file, **options):
+ username = options.get('username')
+
+ if username:
+ user = User.objects.get(username=username)
+ else:
+ print('Please provide a username.')
+ sys.exit(1)
+
+ csvfile = open(csv_file, 'rb')
+ isbn_lists = defaultdict(list)
+ for slug, format, isbn in csv.reader(csvfile, delimiter=','):
+ isbn_lists[slug].append((format, isbn))
+ csvfile.close()
+
+ for slug, isbn_list in isbn_lists.iteritems():
+ print('processing %s' % slug)
+ book = Book.objects.get(dc_slug=slug)
+ chunk = book.chunk_set.first()
+ old_head = chunk.head
+ src = old_head.materialize()
+ tree = etree.fromstring(src)
+ isbn_node = tree.find('.//' + DCNS("relation.hasFormat"))
+ if isbn_node is not None:
+ print('%s already contains ISBN metadata, skipping' % slug)
+ continue
+ desc = tree.find(".//" + RDFNS("Description"))
+ for format, isbn in isbn_list:
+ for template in ISBN_TEMPLATES:
+ isbn_xml = template % {
+ 'format': format,
+ 'isbn': isbn,
+ 'content_type': CONTENT_TYPES[format],
+ 'url': url_for_format(slug, format),
+ }
+ element = etree.XML(isbn_xml)
+ element.tail = '\n'
+ desc.append(element)
+ new_head = chunk.commit(
+ etree.tostring(tree, encoding='unicode'),
+ author=user,
+ description='automatyczne dodanie isbn'
+ )
+ print('committed %s' % slug)
+ if old_head.publishable:
+ new_head.set_publishable(True)
+ else:
+ print('Warning: %s not publishable' % slug)
--- /dev/null
+# 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)
--- /dev/null
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+import sys
+
+from django.contrib.auth.models import User
+from django.core.management.base import BaseCommand
+from django.core.management.color import color_style
+from django.db import transaction
+
+from documents.models import Book
+
+
+def common_prefix(texts):
+ common = []
+
+ min_len = min(len(text) for text in texts)
+ for i in range(min_len):
+ chars = list(set([text[i] for text in texts]))
+ if len(chars) > 1:
+ break
+ common.append(chars[0])
+ return "".join(common)
+
+
+class Command(BaseCommand):
+ help = 'Merges multiple books into one.'
+ args = '[slug]...'
+
+ def add_arguments(self, parser):
+ self.add_argument(
+ '-s', '--slug', dest='new_slug', metavar='SLUG',
+ help='New slug of the merged book (defaults to common part of all slugs).')
+ self.add_argument(
+ '-t', '--title', dest='new_title', metavar='TITLE',
+ help='New title of the merged book (defaults to common part of all titles).')
+ self.add_argument(
+ '-q', '--quiet', action='store_false', dest='verbose', default=True,
+ help='Less output')
+ self.add_argument(
+ '-g', '--guess', action='store_true', dest='guess', default=False,
+ help='Try to guess what merges are needed (but do not apply them).')
+ self.add_argument(
+ '-d', '--dry-run', action='store_true', dest='dry_run', default=False,
+ help='Dry run: do not actually change anything.')
+ self.add_argument(
+ '-f', '--force', action='store_true', dest='force', default=False,
+ help='On slug conflict, hide the original book to archive.')
+
+ def print_guess(self, dry_run=True, force=False):
+ from collections import defaultdict
+ from pipes import quote
+ import re
+
+ def read_slug(slug):
+ res = []
+ res.append((re.compile(r'__?(przedmowa)$'), -1))
+ res.append((re.compile(r'__?(cz(esc)?|ksiega|rozdzial)__?(?P<n>\d*)$'), None))
+ res.append((re.compile(r'__?(rozdzialy__?)?(?P<n>\d*)-'), None))
+
+ for r, default in res:
+ m = r.search(slug)
+ if m:
+ start = m.start()
+ try:
+ return int(m.group('n')), slug[:start]
+ except IndexError:
+ return default, slug[:start]
+ return None, slug
+
+ def file_to_title(fname):
+ """ Returns a title-like version of a filename. """
+ parts = (p.replace('_', ' ').title() for p in fname.split('__'))
+ return ' / '.join(parts)
+
+ merges = defaultdict(list)
+ slugs = []
+ for b in Book.objects.all():
+ slugs.append(b.slug)
+ n, ns = read_slug(b.slug)
+ if n is not None:
+ merges[ns].append((n, b))
+
+ conflicting_slugs = []
+ for slug in sorted(merges.keys()):
+ merge_list = sorted(merges[slug])
+ if len(merge_list) < 2:
+ continue
+
+ merge_slugs = [b.slug for i, b in merge_list]
+ if slug in slugs and slug not in merge_slugs:
+ conflicting_slugs.append(slug)
+
+ title = file_to_title(slug)
+ print("./manage.py merge_books %s%s--title=%s --slug=%s \\\n %s\n" % (
+ '--dry-run ' if dry_run else '',
+ '--force ' if force else '',
+ quote(title), slug,
+ " \\\n ".join(merge_slugs)
+ ))
+
+ if conflicting_slugs:
+ if force:
+ print(self.style.NOTICE('# These books will be archived:'))
+ else:
+ print(self.style.ERROR('# ERROR: Conflicting slugs:'))
+ for slug in conflicting_slugs:
+ print('#', slug)
+
+
+ def handle(self, *slugs, **options):
+
+ self.style = color_style()
+
+ force = options.get('force')
+ guess = options.get('guess')
+ dry_run = options.get('dry_run')
+ new_slug = options.get('new_slug').decode('utf-8')
+ new_title = options.get('new_title').decode('utf-8')
+ verbose = options.get('verbose')
+
+ if guess:
+ if slugs:
+ print("Please specify either slugs, or --guess.")
+ return
+ else:
+ self.print_guess(dry_run, force)
+ return
+ if not slugs:
+ print("Please specify some book slugs")
+ return
+
+ # Start transaction management.
+ transaction.enter_transaction_management()
+
+ books = [Book.objects.get(slug=slug) for slug in slugs]
+ common_slug = common_prefix(slugs)
+ common_title = common_prefix([b.title for b in books])
+
+ if not new_title:
+ new_title = common_title
+ elif common_title.startswith(new_title):
+ common_title = new_title
+
+ if not new_slug:
+ new_slug = common_slug
+ elif common_slug.startswith(new_slug):
+ common_slug = new_slug
+
+ if slugs[0] != new_slug and Book.objects.filter(slug=new_slug).exists():
+ self.style.ERROR('Book already exists, skipping!')
+
+
+ if dry_run and verbose:
+ print(self.style.NOTICE('DRY RUN: nothing will be changed.'))
+ print()
+
+ if verbose:
+ print("New title:", self.style.NOTICE(new_title))
+ print("New slug:", self.style.NOTICE(new_slug))
+ print()
+
+ for i, book in enumerate(books):
+ chunk_titles = []
+ chunk_slugs = []
+
+ book_title = book.title[len(common_title):].replace(' / ', ' ').lstrip()
+ book_slug = book.slug[len(common_slug):].replace('__', '_').lstrip('-_')
+ for j, chunk in enumerate(book):
+ if j:
+ new_chunk_title = book_title + '_%d' % j
+ new_chunk_slug = book_slug + '_%d' % j
+ else:
+ new_chunk_title, new_chunk_slug = book_title, book_slug
+
+ chunk_titles.append(new_chunk_title)
+ chunk_slugs.append(new_chunk_slug)
+
+ if verbose:
+ print("title: %s // %s -->\n %s // %s\nslug: %s / %s -->\n %s / %s" % (
+ book.title, chunk.title,
+ new_title, new_chunk_title,
+ book.slug, chunk.slug,
+ new_slug, new_chunk_slug))
+ print()
+
+ if not dry_run:
+ try:
+ conflict = Book.objects.get(slug=new_slug)
+ except Book.DoesNotExist:
+ conflict = None
+ else:
+ if conflict == books[0]:
+ conflict = None
+
+ if conflict:
+ if force:
+ # FIXME: there still may be a conflict
+ conflict.slug = '.' + conflict.slug
+ conflict.save()
+ print(self.style.NOTICE('Book with slug "%s" moved to "%s".' % (new_slug, conflict.slug)))
+ else:
+ print(self.style.ERROR('ERROR: Book with slug "%s" exists.' % new_slug))
+ return
+
+ if i:
+ books[0].append(books[i], slugs=chunk_slugs, titles=chunk_titles)
+ else:
+ book.title = new_title
+ book.slug = new_slug
+ book.save()
+ for j, chunk in enumerate(book):
+ chunk.title = chunk_titles[j]
+ chunk.slug = chunk_slugs[j]
+ chunk.save()
+
+
+ transaction.commit()
+ transaction.leave_transaction_management()
+
--- /dev/null
+# 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', ' ')))
--- /dev/null
+# 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)
--- /dev/null
+# 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)
--- /dev/null
+# 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())
--- /dev/null
+# 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)
--- /dev/null
+# 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)
--- /dev/null
+# 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)
+
--- /dev/null
+# 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
--- /dev/null
+# 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')
--- /dev/null
+# 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()
--- /dev/null
+{% extends "documents/base.html" %}
+{% load i18n %}
+
+
+{% block titleextra %}{% trans "Active users" %}{% endblock %}
+
+
+{% block content %}
+
+<h1>
+ {% trans "Users active in the year" %} {{ year }}
+</h1>
+
+<ul>
+{% for email, names, count in users %}
+<li>{% for name in names %}{{ name }}, {% endfor %}<a href="mailto:{{ email }}">{{ email }}</a> ({{ count }})</li>
+{% endfor %}
+</ul>
+
+{% endblock content %}
--- /dev/null
+{% extends "documents/base.html" %}
+{% load i18n %}
+{% load wall %}
+
+
+{% block titleextra %}{% trans "Activity" %}{% endblock %}
+
+
+{% block content %}
+
+<div class="card">
+ <div class="card-header">
+<h1>
+ <a class="btn btn-light" href='{% url "documents_activity" prev_day.isoformat %}'><</a>
+ {% trans "Activity" %}: {{ day }}
+ {% if next_day %}
+ <a class="btn btn-light" href='{% url "documents_activity" next_day.isoformat %}'>></a>
+ {% endif %}
+</h1>
+ </div>
+ <div class="card-body">
+
+ {% day_wall day %}
+ </div>
+{% endblock content %}
--- /dev/null
+<!DOCTYPE html>
+{% load pipeline i18n %}
+{% load static %}
+{% load documents %}
+<!DOCTYPE html>
+<html>
+<head lang="{{ LANGUAGE_CODE }}">
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <link rel="icon" href="{{ STATIC_URL }}img/pr-icon.png" type="image/png" />
+ <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
+ {% stylesheet 'documents' %}
+ <title>{% block title %}{% block titleextra %}{% endblock %} ::
+ {% trans "Platforma Redakcyjna" %}{% endblock title %}</title>
+ {% block add_css %}{% endblock %}
+</head>
+<body>
+<!--#include file='/pozor.html'-->
+
+
+<nav class="navbar navbar-expand-md navbar-dark bg-dark">
+ <a class="navbar-brand" href="{% url 'documents_document_list' %}">
+ <img src="{% static "img/wl-orange.png" %}" alt="Platforma">
+ </a>
+ <ul class="navbar-nav mr-auto">
+ {% main_tabs %}
+ </ul>
+
+ <ul class="navbar-nav">
+ {% include "registration/head_login.html" %}
+ </ul>
+</nav>
+
+<div class="container mt-4 mb-4">
+ {% block content %}
+ <div class="row">
+ <div class="col-lg-8">
+ {% block leftcolumn %}
+ {% endblock leftcolumn %}
+ </div>
+ <div class="col-lg-4">
+ {% block rightcolumn %}
+ {% endblock rightcolumn %}
+ </div>
+ </div>
+ {% endblock content %}
+</div>
+
+
+<script
+ src="https://code.jquery.com/jquery-1.9.1.min.js"
+ integrity="sha256-wS9gmOZBqsqWxgIVgA8Y9WcQOa7PgSIX+rPA0VL2rbQ="
+ crossorigin="anonymous"></script>
+<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
+
+{% javascript 'documents' %}
+{% block add_js %}{% endblock %}
+{% block extrabody %}
+{% endblock %}
+</body>
+</html>
--- /dev/null
+{% extends "documents/base.html" %}
+{% load i18n %}
+{% load bootstrap4 %}
+
+{% block titleextra %}{% trans "Append book" %}{% endblock %}
+
+{% block content %}
+<div class="card">
+ <div class="card-body">
+ <form enctype="multipart/form-data" method="POST" action="">
+ {% csrf_token %}
+ {% bootstrap_form form %}
+ {% buttons %}
+ <button class="btn btn-primary" type="submit">{% trans "Append book" %}</button>
+ {% endbuttons %}
+ </form>
+ </div></div>
+{% endblock content %}
+
--- /dev/null
+{% extends "documents/base.html" %}
+{% load book_list i18n %}
+{% load bootstrap4 %}
+
+
+{% block titleextra %}{{ book.title }}{% endblock %}
+
+
+{% block content %}
+
+ <div class="card mt-4">
+ <div class="card-header">
+ <h1>{{ book.title }}</h1>
+ </div>
+ <div class="card-body">
+
+
+
+{% if editable %}<form method='POST'>{% csrf_token %}{% endif %}
+ {% bootstrap_form form %}
+ {% if editable %}
+ {% buttons %}
+ <button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
+ {% endbuttons %}
+ {% endif %}
+{% if editable %}</form>{% endif %}
+
+{% if editable %}
+ {% if book.gallery %}
+ <p><a href="{% url 'documents_book_gallery' book.slug %}">{% trans "Edit gallery" %}</a></p>
+ {% endif %}
+
+ <p style="text-align:right"><a class="btn btn-sm btn-danger" href="{% url 'documents_book_append' book.slug %}">{% trans "Append to other book" %}</a></p>
+{% endif %}
+ </div>
+ </div>
+
+ <div class="card mt-4">
+ <div class="card-header">
+ <h2>{% trans "Chunks" %}</h2>
+ </div>
+ <div class="card-body">
+
+ <table class='single-book-list table'><tbody>
+ {% for chunk in book %}
+ {% include 'documents/book_list/chunk.html' %}
+ {% endfor %}
+ </tbody></table>
+ </div>
+ </div>
+
+
+
+
+<div class='card mt-4'>
+
+<div class="card-header">
+ <h2>{% trans "Publication" %}</h2>
+ </div>
+<div class="card-body">
+ <div class="row">
+<div class="col-lg-3">
+<img class="cover-preview" src="{% url 'cover_preview' book.slug %}" />
+{% if book.dc_cover_image %}
+ <a href="{{ book.dc_cover_image.get_absolute_url }}">{{ book.dc_cover_image }}</a>
+{% endif %}
+<br><br>
+<form action="{% url 'cover_preview' book.slug %}">
+<input type="hidden" name="download" value="1">
+Okładka w rozmiarze
+<input name="width" type="number" required value="600"> x <input name="height" type="number" required value="833">
+<button type="submit" class="btn btn-sm btn-primary">Pobierz</button>
+</form>
+</div>
+<div class="col-lg-9">
+<p>{% trans "Last published" %}:
+ {% if book.last_published %}
+ {{ book.last_published }}
+ {% else %}
+ —
+ {% endif %}
+</p>
+
+{% if publishable %}
+ <p>
+ <a href="{% url 'documents_book_xml' book.slug %}" rel="nofollow">{% trans "Full XML" %}</a><br/>
+ <a target="_blank" href="{% url 'documents_book_html' book.slug %}" rel="nofollow">{% trans "HTML version" %}</a><br/>
+ <a href="{% url 'documents_book_txt' book.slug %}" rel="nofollow">{% trans "TXT version" %}</a><br/>
+ <a href="{% url 'documents_book_pdf' book.slug %}" rel="nofollow">{% trans "PDF version" %}</a><br/>
+ <a href="{% url 'documents_book_pdf_mobile' book.slug %}" rel="nofollow">{% trans "PDF version for mobiles" %}</a><br/>
+ <a href="{% url 'documents_book_epub' book.slug %}" rel="nofollow">{% trans "EPUB version" %}</a><br/>
+ <a href="{% url 'documents_book_mobi' book.slug %}" rel="nofollow">{% trans "MOBI version" %}</a><br/>
+ </p>
+
+ {% if user.is_authenticated %}
+ <!--
+ Angel photos:
+ Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
+ mira66 (http://www.flickr.com/photos/21804434@N02/) /
+ CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
+ -->
+ <form method="POST" action="{% url 'documents_publish' book.slug %}">{% csrf_token %}
+ {{ publish_options_form.as_p }}
+ <img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" />
+ <button id="publish-button" type="submit">
+ <span>{% trans "Publish" %}</span></button>
+ <img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" />
+ </form>
+ {% else %}
+ <a href="{% url 'cas_ng_login' %}">{% trans "Log in to publish." %}</a>
+ {% endif %}
+{% else %}
+ <p>{% trans "This book can't be published yet, because:" %}</p>
+ <ul><li>{{ publishable_error }}</li></ul>
+{% endif %}
+
+</div>
+ </div>
+{% endblock content %}
--- /dev/null
+{% extends "documents/base.html" %}
+{% load i18n %}
+
+
+{% block titleextra %}{% trans "Edit book" %}{% endblock %}
+
+
+{% block leftcolumn %}
+ <form enctype="multipart/form-data" method="POST" action="">
+ {% csrf_token %}
+ {{ form.as_p }}
+
+ <p><button type="submit">{% trans "Save" %}</button></p>
+ </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
--- /dev/null
+{% load i18n %}
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <title>{{ book.title }}</title>
+ </head>
+ <body>
+ <div id="menu">
+ <ul>
+ <li><a href="#toc">{% trans "Table of contents" %}</a></li>
+ <li><a href="#nota_red">{% trans "Edit. note" %}</a></li>
+ <li><a href="#info">{% trans "Infobox" %}</a></li>
+ </ul>
+ </div>
+ <div id="info">
+ {#% book_info book %#}
+ </div>
+ <div id="header">
+ <div id="logo">
+ <a href="/"><img src="http://static.wolnelektury.pl/img/logo.png" alt="WolneLektury.pl - logo" /></a>
+ </div>
+ </div>
+
+ {{ html|safe }}
+
+ </body>
+</html>
--- /dev/null
+{% load i18n %}
+{% load username from common_tags %}
+
+{% if book.single %}
+ {% with chunk as chunk %}
+ <tr class="table-sm">
+ <td><input type="checkbox" name="select_book" value="{{book.id}}" data-chunk-id="{{chunk.id}}"/></td>
+ <td><a class="btn btn-sm btn-outline-secondary" href="{% url 'documents_book' book.slug %}" title="{% trans "Book settings" %}">📕</a></td>
+ <td><a class="btn btn-sm btn-outline-secondary" href="{% url 'documents_chunk_edit' book.slug chunk.slug %}" title="{% trans "Chunk settings" %}">📜</a></td>
+ <td><a class="btn btn-primary" target="_blank"
+ href="{% url 'wiki_editor' book.slug %}">
+ {{ book.title }}</a></td>
+ <td>{% if chunk.stage %}
+ {{ chunk.stage }}
+ {% else %}–
+ {% endif %}</td>
+ <td class='user-column'>{% if chunk.user %}<a href="{% url 'documents_user' chunk.user.username %}">{{ chunk.user|username }}</a>{% endif %}</td>
+ <td>
+ {% if book.published %}
+ <span class="badge badge-info" title="{% trans "published" %}">opubl.</span>
+ {% endif %}
+ {% if book.new_publishable %}
+ <span class="badge badge-primary" title="{% trans "publishable" %}">do publ.</span>
+ {% endif %}
+ {% if chunk.changed %}
+ <span class="badge badge-warning title="{% trans "changed" %}">zmiany</span>
+ {% endif %}
+ </td>
+ <td>{{ book.project.name }}</td>
+ </tr>
+ {% endwith %}
+{% else %}
+ <tr class="table-sm">
+ <td><input type="checkbox" name="select_book" value="{{book.id}}"/></td>
+ <td><a class='btn btn-sm btn-outline-secondary' href="{% url 'documents_book' book.slug %}" title="{% trans "Book settings" %}">📕</a></td>
+ <td></td>
+ <td>{{ book.title }}</td>
+ <td></td>
+ <td class='user-column'></td>
+ <td>
+ {% if book.published %}
+ <span class="badge badge-info" title="{% trans "published" %}">opubl.</span>
+ {% endif %}
+ {% if book.new_publishable %}
+ <span class="badge badge-info" title="{% trans "publishable" %}">do publ.</span>
+ {% endif %}
+ </td>
+ <td>{{ book.project.name }}</td>
+ </tr>
+{% endif %}
--- /dev/null
+{% load i18n %}
+{% load pagination_tags %}
+{% load username from common_tags %}
+
+
+<form name='filter' action='{{ request.path }}'>
+<input type='hidden' name="title" value="{{ request.GET.title }}" />
+<input type='hidden' name="stage" value="{{ request.GET.stage }}" />
+{% if not viewed_user %}
+ <input type='hidden' name="user" value="{{ request.GET.user }}" />
+{% endif %}
+<input type='hidden' name="all" value="{{ request.GET.all }}" />
+<input type='hidden' name="status" value="{{ request.GET.status }}" />
+<input type='hidden' name="project" value="{{ request.GET.project }}" />
+</form>
+
+
+<div class="card">
+ <div class="card-body">
+
+
+<table id="file-list" class="table{% if viewed_user %} book-list-user{% endif %}">
+ <thead><tr>
+ <th></th>
+ <th></th>
+ <th></th>
+ <th class='book-search-column'>
+ <form>
+ <input title='{% trans "Search in book titles" %}' name="title"
+ class='form-control text-filter' value="{{ request.GET.title }}" />
+ </form>
+ </th>
+ <th><select name="stage" class="form-control filter">
+ <option value=''>- {% trans "stage" %} -</option>
+ <option {% if request.GET.stage == '-' %}selected="selected"
+ {% endif %}value="-">- {% trans "none" %} -</option>
+ {% for stage in stages %}
+ <option {% if request.GET.stage == stage.slug %}selected="selected"
+ {% endif %}value="{{ stage.slug }}">{{ stage.name }}</option>
+ {% endfor %}
+ </select></th>
+
+ {% if not viewed_user %}
+ <th><select name="user" class="form-control filter">
+ <option value=''>- {% trans "editor" %} -</option>
+ <option {% if request.GET.user == '-' %}selected="selected"
+ {% endif %}value="-">- {% trans "none" %} -</option>
+ {% for user in users %}
+ <option {% if request.GET.user == user.username %}selected="selected"
+ {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
+ {% endfor %}
+ </select></th>
+ {% else %}
+ <th style='display: none'></th>
+ {% endif %}
+
+ <th><select name="status" class="form-control filter">
+ <option value=''>- {% trans "status" %} -</option>
+ {% for state, label in states %}
+ <option {% if request.GET.status == state %}selected="selected"
+ {% endif %}value='{{ state }}'>{{ label }}</option>
+ {% endfor %}
+ </select></th>
+
+ <th><select name="project" class="form-control filter">
+ <option value=''>- {% trans "project" %} -</option>
+ <option {% if request.GET.project == '-' %}selected="selected"
+ {% endif %}value="-">- {% trans "none" %} -</option>
+ {% for project in projects %}
+ <option {% if request.GET.project == project.pk|slugify %}selected="selected"
+ {% endif %}value='{{ project.pk }}'>{{ project.name }}</option>
+ {% endfor %}
+ </select></th>
+
+ </tr></thead>
+
+ {% autopaginate books 100 as books_page %}
+ <tbody>
+ {% for item in books_page %}
+ {% with book=item.book chunk=item.chunks.0 %}
+ {% include 'documents/book_list/book.html' %}
+ {% if not book.single %}
+ {% for chunk in item.chunks %}
+ {% include 'documents/book_list/chunk.html' %}
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+ {% endfor %}
+ </tbody>
+</table>
+{% paginate %}
+ {% blocktrans count c=books|length %}{{c}} book{% plural %}{{c}} books{% endblocktrans %}
+
+
+{% if not books %}
+ <p>{% trans "No books found." %}</p>
+{% endif %}
+
+ </div>
+</div>
+
+<form id='chunk_mass_edit' action='{% url "documents_chunk_mass_edit" %}' style="display:none;">
+{% csrf_token %}
+<input type="hidden" name="ids" />
+<label for="mass_edit_stage">{% trans "Set stage" %}</label><input type="hidden" name="stage" id="mass_edit_stage"/>
+<label for="mass_edit_user">{% trans "Set user" %}</label><input type="hidden" name="user" id="mass_edit_user" />
+<input type="hidden" name="status" />
+<label for="mass_edit_project">{% trans "Project" %}</label><input type="hidden" name="project" id="mass_edit_project" />
+<label for="mass_edit_more_users">{% trans "More users" %}</label>
+</form>
+
+<select name="other-user" style="display:none;">
+ {% for user in other_users %}
+ <option {% if request.GET.user == user.username %}selected="selected"
+ {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
+ {% endfor %}
+</select>
--- /dev/null
+{% load i18n %}
+{% load username from common_tags %}
+
+<tr class="table-secondary table-sm">
+ <td><input type="checkbox" name="select_chunk" value="{{chunk.id}}" data-book-id="{{chunk.book.id}}" /></td>
+ <td class='book-settings-column'></td>
+ <td><a class="btn btn-outline-secondary btn-sm" href="{% url 'documents_chunk_edit' chunk.book.slug chunk.slug %}" title="{% trans "Chunk settings" %}">📜</a></td>
+ <td><a class="btn btn-primary" target="_blank" href="{{ chunk.get_absolute_url }}">
+ {{ chunk.number }}.
+ {{ chunk.title }}</a></td>
+ <td>{% if chunk.stage %}
+ {{ chunk.stage }}
+ {% else %}
+ –
+ {% endif %}</td>
+ <td class='user-column'>{% if chunk.user %}
+ <a href="{% url 'documents_user' chunk.user.username %}">
+ {{ chunk.user|username }}
+ </a>{% else %}
+
+ {% endif %}</td>
+
+ </td>
+ <td>
+ {% if chunk.changed %}
+ <span class="badge badge-warning title="{% trans "changed" %}">zmiany</span>
+ {% endif %}
+</td>
+<td></td>
+</tr>
--- /dev/null
+{% load i18n pipeline %}
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ <title>{% trans "Redakcja" %} :: {{ book.title }}</title>
+ {% stylesheet 'book' %}
+ <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
+ {% javascript 'book' %}
+ </head>
+ <body>
+ <div id="menu">
+ <ul>
+ <li><a class="menu" href="#toc">{% trans "Table of contents" %}</a></li>
+{# <li><a class="menu" href="#themes">{% trans "Themes" %}</a></li>#}
+ <li><a class="menu" href="#nota_red">{% trans "Edit. note" %}</a></li>
+{# <li><a class="menu" href="#info">{% trans "Infobox" %}</a></li>#}
+{# <li><a href="{{ book.get_absolute_url }}">{% trans "Book's page" %}</a></li> #}
+{# <li><a class="menu" href="#download">{% trans "Download" %}</a></li>#}
+ </ul>
+ </div>
+ <div id="header">
+ <a href="/"><img src="/media/static/img/logo-220.png" alt="Wolne Lektury" /></a>
+ </div>
+ {{ html|safe }}
+ </body>
+</html>
--- /dev/null
+{% extends "documents/base.html" %}
+{% load i18n %}
+{% load bootstrap4 %}
+
+
+{% block titleextra %}{% trans "Split chunk" %}{% endblock %}
+
+
+{% block content %}
+<div class="card">
+ <div class="card-header">
+ <h1>{% trans "Split chunk" %}</h1>
+ </div>
+ <div class="card-body">
+
+ <form enctype="multipart/form-data" method="POST">
+ {% csrf_token %}
+ <div class='editable'>
+ <p>{% trans "Insert empty chunk after" %}:
+ <a href="{{ chunk.get_absolute_url }}">{{ chunk.pretty_name }}</a></p>
+ {% bootstrap_form form %}
+ {% buttons %}
+ <button class="btn btn-primary" type="submit">{% trans "Add chunk" %}</button>
+ {% endbuttons %}
+ </div>
+ </form>
+ </div>
+</div>
+{% endblock content %}
--- /dev/null
+{% extends "documents/base.html" %}
+{% load i18n %}
+{% load bootstrap4 %}
+
+
+{% block titleextra %}{% trans "Chunk settings" %}{% endblock %}
+
+
+{% block content %}
+<div class="card">
+ <div class="card-header">
+ <h1>{% trans "Chunk settings" %}</h1>
+ </div>
+ <div class="card-body">
+
+ <form enctype="multipart/form-data" method="POST" action="{% if go_next %}?next={{ go_next }}{% endif %}">
+ {% csrf_token %}
+ <div class='editable'>
+ <p>{% trans "Book" %}: {{ chunk.book }} ({{ chunk.number }}/{{ chunk.book|length }})</p>
+ {% bootstrap_form form %}
+ {% buttons %}
+ <button class="btn btn-primary" type="submit">{% trans "Save" %}</button>
+ {% endbuttons %}
+ </div>
+
+ </form>
+
+
+ <p style="text-align: right"><a class="btn btn-danger" href="{% url "documents_chunk_add" chunk.book.slug chunk.slug %}">{% trans "Split chunk" %}</a></p>
+ </div>
+</div>
+
+{% endblock content %}
--- /dev/null
+{% extends "documents/base.html" %}
+{% load i18n %}
+{% load bootstrap4 %}
+
+
+{% block titleextra %}{% trans "Create a new book" %}{% endblock %}
+
+
+{% block content %}
+<div class="card">
+ <div class="card-header">
+ <h1>{% trans "Create a new book" %}</h1>
+ </div>
+ <div class="card-body">
+
+
+ <form enctype="multipart/form-data" method="POST">
+ {% csrf_token %}
+ {% bootstrap_form form %}
+ {% buttons %}
+ <button class="btn btn-primary" type="submit">{% trans "Create book" %}</button>
+ {% endbuttons %}
+ </form>
+</div>
+</div>
+{% endblock content %}
--- /dev/null
+{% extends "documents/base.html" %}
+
+{% load i18n %}
+{% load documents book_list %}
+{% load pipeline %}
+
+{% block titleextra %}{% trans "Book list" %}{% endblock %}
+
+
+{% block add_js %}
+ {% javascript 'book_list' %}
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js"></script>
+{% endblock %}
+
+{% block add_css %}
+ {% stylesheet 'book_list' %}
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css">
+{% endblock %}
+
+{% block content %}
+ {% book_list %}
+{% endblock content %}
--- /dev/null
+{% extends "documents/base.html" %}
+{% load i18n %}
+{% load bootstrap4 %}
+
+
+{% block titleextra %}{% trans "Bulk document upload" %}{% endblock %}
+
+
+{% block content %}
+
+
+<div class="card">
+ <div class="card-header">
+<h2>{% trans "Bulk documents upload" %}</h2>
+ </div>
+ <div class="card-body">
+
+<p>
+{% trans "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with <code>.xml</code> will be ignored." %}
+</p>
+
+<form enctype="multipart/form-data" method="POST" action="{{ request.path }}">
+{% csrf_token %}
+{% bootstrap_form form %}
+{% buttons %}
+<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
+{% endbuttons %}
+</form>
+
+
+{% if error_list %}
+ <hr>
+
+ <p class='error'>{% trans "There have been some errors. No files have been added to the repository." %}
+ <h3>{% trans "Offending files" %}</h3>
+ <ul id='error-list'>
+ {% for filename, title, error in error_list %}
+ <li>{{ title }} (<code>{{ filename }}</code>): {{ error }}</li>
+ {% endfor %}
+ </ul>
+
+ {% if ok_list %}
+ <h3>{% trans "Correct files" %}</h3>
+ <ul>
+ {% for filename, slug, title in ok_list %}
+ <li>{{ title }} (<code>{{ filename }}</code>)</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+
+{% else %}
+
+ {% if ok_list %}
+ <p class='success'>{% trans "Files have been successfully uploaded to the repository." %}</p>
+ <h3>{% trans "Uploaded files" %}</h3>
+ <ul id='ok-list'>
+ {% for filename, slug, title in ok_list %}
+ <li><a href='{% url "wiki_editor" slug %}'>{{ title }}</a> (<code>{{ filename }})</a></li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+{% endif %}
+
+{% if skipped_list %}
+ <h3>{% trans "Skipped files" %}</h3>
+ <p>{% trans "Files skipped due to no <code>.xml</code> extension" %}</p>
+ <ul id='skipped-list'>
+ {% for filename in skipped_list %}
+ <li>{{ filename }}</li>
+ {% endfor %}
+ </ul>
+{% endif %}
+
+ </div>
+</div>
+
+{% endblock content %}
+
--- /dev/null
+{% extends "documents/base.html" %}
+{% load book_list i18n %}
+{% load bootstrap4 %}
+
+
+{% block titleextra %}{{ object.title }}{% endblock %}
+
+
+{% block content %}
+<div class="card mt-4">
+
+ <div class="card-header">
+<h1>{{ object.title }}</h1>
+ </div>
+ <div class="card-body">
+
+
+{% if editable %}<form method='POST'>{% csrf_token %}{% endif %}
+ {% bootstrap_form form %}
+ {% if editable %}
+ {% buttons %}
+ <button class="btn btn-primary" type="submit">{% trans "Save" %}</button>
+ {% endbuttons %}
+ {% endif %}
+</tbody></table>
+{% if editable %}</form>{% endif %}
+
+ </div>
+</div>
+
+
+<div class='card mt-4'>
+ <div class="card-header">
+ <h2>{% trans "Editor" %}</h2>
+ </div>
+ <div class="card-body">
+
+ <p><a class="btn btn-primary" href="{% url 'wiki_img_editor' object.slug %}">{% trans "Proceed to the editor." %}</a></p>
+</div>
+</div>
+
+
+
+<div class='card mt-4'>
+ <div class="card-header">
+
+<h2>{% trans "Publication" %}</h2>
+ </div>
+ <div class="card-body">
+
+<p>{% trans "Last published" %}:
+ {% if object.last_published %}
+ {{ object.last_published }}
+ {% else %}
+ —
+ {% endif %}
+</p>
+
+{% if publishable %}
+ {% if user.is_authenticated %}
+ <!--
+ Angel photos:
+ Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
+ mira66 (http://www.flickr.com/photos/21804434@N02/) /
+ CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
+ -->
+ <form method="POST" action="{% url 'documents_publish_image' object.slug %}">{% csrf_token %}
+ <!--img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" /-->
+ <button id="publish-button" type="submit">
+ <span>{% trans "Publish" %}</span></button>
+ <!--img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" /-->
+ </form>
+ {% else %}
+ <a href="{% url 'cas_ng_login' %}">{% trans "Log in to publish." %}</a>
+ {% endif %}
+{% else %}
+ <p>{% trans "This book can't be published yet, because:" %}</p>
+ <ul><li>{{ publishable_error }}</li></ul>
+{% endif %}
+
+</div>
+</div>
+
+
+{% endblock content %}
--- /dev/null
+{% extends "documents/base.html" %}
+
+{% load i18n %}
+{% load documents book_list %}
+{% load pipeline %}
+
+
+{% block titleextra %}{% trans "Image list" %}{% endblock %}
+
+
+{% block add_js %}
+ {% javascript 'book_list' %}
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js"></script>
+{% endblock %}
+
+{% block add_css %}
+ {% stylesheet 'book_list' %}
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css">
+{% endblock %}
+
+
+{% block content %}
+ {% image_list %}
+{% endblock content %}
--- /dev/null
+{% load i18n %}
+{% load username from common_tags %}
+
+<tr class="table-sm">
+ <td><input type="checkbox" name="select_chunk" value="{{image.id}}"/></td>
+ <td><a href="{% url 'documents_image' image.slug %}" class="btn btn-sm btn-secondary" title="{% trans "Image settings" %}">🖼</a></td>
+ <td><a class="btn btn-primary" target="_blank"
+ href="{% url 'wiki_img_editor' image.slug %}">
+ {{ image.title }}</a></td>
+ <td>{% if image.stage %}
+ {{ image.stage }}
+ {% else %}–
+ {% endif %}</td>
+ <td class='user-column'>{% if image.user %}<a href="{% url 'documents_user' image.user.username %}">{{ image.user|username }}</a>{% endif %}</td>
+ <td>
+ {% if image.published %}
+ <span class="badge badge-info" title="{% trans "published" %}">opubl.</span>
+ {% endif %}
+ {% if image.new_publishable %}
+ <span class="badge badge-primary" title="{% trans "publishable" %}">do publ.</span>
+ {% endif %}
+ {% if image.changed %}
+ <span class="badge badge-warning title="{% trans "changed" %}">zmiany</span>
+ {% endif %}
+ </td>
+ <td>{{ image.project.name }}</td>
+</tr>
--- /dev/null
+{% load i18n %}
+{% load pagination_tags %}
+{% load username from common_tags %}
+
+<div class="card">
+ <div class="card-body">
+
+
+<form name='filter' action='{{ request.path }}'>
+<input type='hidden' name="title" value="{{ request.GET.title }}" />
+<input type='hidden' name="stage" value="{{ request.GET.stage }}" />
+{% if not viewed_user %}
+ <input type='hidden' name="user" value="{{ request.GET.user }}" />
+{% endif %}
+<input type='hidden' name="status" value="{{ request.GET.status }}" />
+<input type='hidden' name="project" value="{{ request.GET.project }}" />
+</form>
+
+<table id="file-list" class="table {% if viewed_user %}book-list-user{% endif %}">
+ <thead><tr>
+ <th></th>
+ <th></th>
+ <th class='book-search-column'>
+ <form>
+ <input title='{% trans "Search in book titles" %}' name="title"
+ class='form-control text-filter' value="{{ request.GET.title }}" />
+ </form>
+ </th>
+ <th><select name="stage" class="form-control filter">
+ <option value=''>- {% trans "stage" %} -</option>
+ <option {% if request.GET.stage == '-' %}selected="selected"
+ {% endif %}value="-">- {% trans "none" %} -</option>
+ {% for stage in stages %}
+ <option {% if request.GET.stage == stage.slug %}selected="selected"
+ {% endif %}value="{{ stage.slug }}">{{ stage.name }}</option>
+ {% endfor %}
+ </select></th>
+
+ {% if not viewed_user %}
+ <th><select name="user" class="form-control filter">
+ <option value=''>- {% trans "editor" %} -</option>
+ <option {% if request.GET.user == '-' %}selected="selected"
+ {% endif %}value="-">- {% trans "none" %} -</option>
+ {% for user in users %}
+ <option {% if request.GET.user == user.username %}selected="selected"
+ {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
+ {% endfor %}
+ </select></th>
+ {% endif %}
+
+ <th><select name="status" class="form-control filter">
+ <option value=''>- {% trans "status" %} -</option>
+ {% for state, label in states %}
+ <option {% if request.GET.status == state %}selected="selected"
+ {% endif %}value='{{ state }}'>{{ label }}</option>
+ {% endfor %}
+ </select></th>
+
+ <th><select name="project" class="form-control filter">
+ <option value=''>- {% trans "project" %} -</option>
+ <option {% if request.GET.project == '-' %}selected="selected"
+ {% endif %}value="-">- {% trans "none" %} -</option>
+ {% for project in projects %}
+ <option {% if request.GET.project == project.pk|slugify %}selected="selected"
+ {% endif %}value='{{ project.pk }}'>{{ project.name }}</option>
+ {% endfor %}
+ </select></th>
+
+ </tr></thead>
+
+ {% autopaginate objects 100 as objects_page %}
+ <tbody>
+ {% for image in objects_page %}
+ {% include 'documents/image_short.html' %}
+ {% endfor %}
+ </tbody>
+</table>
+ {% paginate %}
+ {% blocktrans count c=objects|length %}{{c}} image{% plural %}{{c}} images{% endblocktrans %}</th></tr>
+{% if not objects %}
+ <p>{% trans "No images found." %}</p>
+{% endif %}
+
+<form id='chunk_mass_edit' action='{% url "documents_image_mass_edit" %}' style="display:none;">
+{% csrf_token %}
+<input type="hidden" name="ids" />
+<label for="mass_edit_stage">{% trans "Set stage" %}</label><input type="hidden" name="stage" id="mass_edit_stage"/>
+<label for="mass_edit_user">{% trans "Set user" %}</label><input type="hidden" name="user" id="mass_edit_stage" />
+<input type="hidden" name="status" />
+<label for="mass_edit_project">{% trans "Project" %}</label><input type="hidden" name="project" id="mass_edit_project" />
+<label for="mass_edit_more_users">{% trans "More users" %}</label>
+</form>
+
+<select name="other-user" style="display:none;">
+ {% for user in other_users %}
+ <option {% if request.GET.user == user.username %}selected="selected"
+ {% endif %}value="{{ user.username }}">{{ user|username }} ({{ user.count }})</option>
+ {% endfor %}
+</select>
+
+ </div>
+</div>
--- /dev/null
+{% for tab in tabs %}
+ <li class="nav-item"><a class="nav-link{% if active_tab == tab.slug %} active{% endif %}" href="{{ tab.url }}">{{ tab.caption }}</a></li>
+{% endfor %}
--- /dev/null
+{% extends "documents/base.html" %}
+
+{% block titleextra %}Oznacz książki{% endblock %}
+
+
+{% block leftcolumn %}
+
+<h1>Oznacz książki</h1>
+
+<form method="post" action="">
+ {% csrf_token %}
+ {{ form.as_p }}
+ <input type="submit" value="Oznacz">
+</form>
+
+{% endblock leftcolumn %}
--- /dev/null
+{% extends "documents/base.html" %}
+
+{% block titleextra %}Oznaczono książki{% endblock %}
+
+
+{% block leftcolumn %}
+
+<h1>Oznaczono książki</h1>
+
+<p>Książki zostały oznaczone.</p>
+
+{% endblock leftcolumn %}
--- /dev/null
+{% extends "documents/base.html" %}
+
+{% load i18n %}
+{% load documents book_list wall %}
+{% load pipeline %}
+
+{% block add_js %}
+ {% javascript 'book_list' %}
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js"></script>
+{% endblock %}
+
+{% block add_css %}
+ {% stylesheet 'book_list' %}
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css">
+{% endblock %}
+
+{% block titleextra %}{% trans "My page" %}{% endblock %}
+
+
+{% block leftcolumn %}
+ {% book_list request.user %}
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+<div class="card">
+ <div class="card-header">
+ <h2>{% trans "Your last edited documents" %}</h2>
+ </div>
+ <div class="card-body">
+ <ol>
+ {% for edit_url, item in last_books %}
+ <li><a
+ {% if edit_url|length == 2 %}
+ {# Temporary support for old-style last_books. #}
+ href="{% url 'wiki_editor' edit_url.0 edit_url.1 %}"
+ {% else %}
+ href="{{ edit_url }}"
+ {% endif %}
+ target="_blank">{{ item.title }}</a><br/><span class="date">({{ item.time|date:"H:i:s, d/m/Y" }})</span></li>
+ {% endfor %}
+ </ol>
+ </div>
+</div>
+
+<div class="card mt-4">
+ <div class="card-header">
+ <h2>{% trans "Recent activity for" %} {{ request.user|nice_name }}</h2>
+ </div>
+ <div class="card-body">
+ {% wall request.user 10 %}
+ </div></div>
+{% endblock rightcolumn %}
--- /dev/null
+{% extends "documents/base.html" %}
+{% load i18n %}
+
+
+{% block titleextra %}{% trans "PDF file upload" %}{% endblock %}
+
+
+{% block content %}
+
+
+<h2>{% trans "PDF file upload" %}</h2>
+
+<form enctype="multipart/form-data" method="POST" action="">
+{% csrf_token %}
+{{ form.as_p }}
+<p><button type="submit">{% trans "Upload" %}</button></p>
+</form>
+
+
+{% endblock content %}
--- /dev/null
+{% extends "documents/base.html" %}
+
+{% load i18n %}
+{% load username from common_tags %}
+
+
+{% block titleextra %}{% trans "Users" %}{% endblock %}
+
+
+{% block content %}
+<div class="card">
+ <div class="card-header">
+
+<h1>{% trans "Users" %}</h1>
+ </div>
+<div class="card-body">
+
+<ul>
+{% for user in users %}
+ <li><a href="{% url 'documents_user' user.username %}">
+ <span class="chunkno">{{ forloop.counter }}.</span>
+ {{ user|username }}</a>
+ ({{ user.count }})</li>
+{% endfor %}
+</ul>
+
+</div>
+</div>
+
+{% endblock content %}
--- /dev/null
+{% extends "documents/base.html" %}
+
+{% load i18n %}
+{% load documents book_list wall %}
+
+
+{% block titleextra %}{{ viewed_user|nice_name }}{% endblock %}
+
+
+{% block leftcolumn %}
+ <h1 class="mb-4">{{ viewed_user|nice_name }}</h1>
+ {% book_list viewed_user %}
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+<div class="card">
+ <div class="card-header">
+ <h2>{% trans "Recent activity for" %} {{ viewed_user|nice_name }}</h2>
+ </div>
+ <div class="card-body">
+
+ {% wall viewed_user 10 %}
+ </div>
+</div>
+{% endblock rightcolumn %}
--- /dev/null
+{% load i18n %}
+{% load gravatar %}
+{% load email %}
+{% load username from common_tags %}
+
+<ul class='wall'>
+{% for item in wall %}
+ <li class="{{ item.tag }}{% if not item.user %} anonymous{% endif %}">
+ <div class='gravatar'>
+ {% if item.get_email %}
+ <img src="{% gravatar_url item.get_email 32 %}"
+ height="32" width="32" alt='Avatar' />
+ <br/>
+ {% endif %}
+ </div>
+
+ <div class="time">{{ item.timestamp }}</div>
+ <h3>{{ item.header }}</h3>
+ <a target="_blank" href='{{ item.url }}'>{{ item.title }}</a>
+ <br/><strong>{% trans "user" %}:</strong>
+ {% if item.user %}
+ <a href="{% url 'documents_user' item.user.username %}">
+ {{ item.user|username }}</a>
+ <{{ item.user.email|email_link }}>
+ {% else %}
+ {{ item.user_name }}
+ {% if item.email %}
+ <{{ item.email|email_link }}>
+ {% endif %}
+ ({% trans "not logged in" %})
+ {% endif %}
+ <br/>{{ item.summary|linebreaksbr }}
+ </li>
+{% empty %}
+ <li>{% trans "No activity recorded." %}</li>
+{% endfor %}
+</ul>
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
+
--- /dev/null
+# 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)
--- /dev/null
+# 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),
+ ])}
--- /dev/null
+# 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()
--- /dev/null
+<utwor>
+ <liryka_l>
+
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description rdf:about="http://example.com/documents/book/test-book/">
+<dc:creator xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam</dc:creator>
+<dc:title xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Do M***</dc:title>
+<dc:publisher xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Fundacja Nowoczesna Polska</dc:publisher>
+<dc:subject.period xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Romantyzm</dc:subject.period>
+<dc:subject.type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Liryka</dc:subject.type>
+<dc:subject.genre xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Wiersz</dc:subject.genre>
+<!--dc:description xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Publikacja zrealizowana w ramach projektu Wolne Lektury (http://wolnelektury.pl). Reprodukcja cyfrowa wykonana przez Bibliotekę Narodową z egzemplarza pochodzącego ze zbiorów BN.</dc:description-->
+<dc:identifier.url xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m</dc:identifier.url>
+<dc:source.URL xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://www.polona.pl/Content/2222</dc:source.URL>
+<dc:source xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam (1798-1855), Poezje, tom 1 (Wiersze młodzieńcze - Ballady i romanse - Wiersze do r. 1824), Krakowska Spółdzielnia Wydawnicza, wyd. 2 zwiększone, Kraków, 1922</dc:source>
+
+<dc:rights xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Domena publiczna - Adam Mickiewicz zm. 1855</dc:rights>
+<dc:date.pd xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">1926</dc:date.pd>
+<dc:format xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">xml</dc:format>
+<dc:type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
+<dc:type xml:lang="en" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
+<dc:date xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">2007-09-06</dc:date>
+<dc:language xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">pol</dc:language>
+
+</rdf:Description>
+</rdf:RDF>
+
+<autor_utworu>Adam Mickiewicz</autor_utworu>
+<dzielo_nadrzedne>Sonety odeskie</dzielo_nadrzedne>
+<nazwa_utworu>Do M***</nazwa_utworu>
+
+<nota><akap>Wiérsz napisany w roku 1822</akap></nota>
+
+
+<strofa>Precz z moich oczu!... posłucham od razu,/
+Precz z mego serca!... i serce posłucha,/
+Precz z méj pamięci!... Nie! tego rozkazu/
+Moja i twoja pamięć nie posłucha.</strofa>
+
+<!-- TRIM_END -->
+</liryka_l>
+</utwor>
--- /dev/null
+<utwor><liryka_l>
+<!-- TRIM_BEGIN -->
+
+<strofa>Jak cień tém dłuższy, gdy padnie z daleka,/
+Tém szerzéj koło żałobne roztoczy,/
+Tak moja postać, im daléj ucieka,/
+Tém grubszym kirem twą pamięć pomroczy.</strofa>
+
+
+</liryka_l>
+</utwor>
--- /dev/null
+<utwor>
+ <liryka_l>
+
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description rdf:about="http://example.com/documents/book/test-book/">
+<dc:creator xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam</dc:creator>
+<dc:title xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Do M***</dc:title>
+<dc:publisher xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Fundacja Nowoczesna Polska</dc:publisher>
+<dc:subject.period xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Romantyzm</dc:subject.period>
+<dc:subject.type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Liryka</dc:subject.type>
+<dc:subject.genre xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Wiersz</dc:subject.genre>
+<!--dc:description xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Publikacja zrealizowana w ramach projektu Wolne Lektury (http://wolnelektury.pl). Reprodukcja cyfrowa wykonana przez Bibliotekę Narodową z egzemplarza pochodzącego ze zbiorów BN.</dc:description-->
+<dc:identifier.url xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m</dc:identifier.url>
+<dc:source.URL xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://www.polona.pl/Content/2222</dc:source.URL>
+<dc:source xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam (1798-1855), Poezje, tom 1 (Wiersze młodzieńcze - Ballady i romanse - Wiersze do r. 1824), Krakowska Spółdzielnia Wydawnicza, wyd. 2 zwiększone, Kraków, 1922</dc:source>
+
+<dc:rights xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Domena publiczna - Adam Mickiewicz zm. 1855</dc:rights>
+<dc:date.pd xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">1926</dc:date.pd>
+<dc:format xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">xml</dc:format>
+<dc:type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
+<dc:type xml:lang="en" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
+<dc:date xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">2007-09-06</dc:date>
+<dc:language xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">pol</dc:language>
+
+</rdf:Description>
+</rdf:RDF>
+
+<autor_utworu>Adam Mickiewicz</autor_utworu>
+<dzielo_nadrzedne>Sonety odeskie</dzielo_nadrzedne>
+<nazwa_utworu>Do M***</nazwa_utworu>
+
+<nota><akap>Wiérsz napisany w roku 1822</akap></nota>
+
+
+<strofa>Precz z moich oczu!... posłucham od razu,/
+Precz z mego serca!... i serce posłucha,/
+Precz z méj pamięci!... Nie! tego rozkazu/
+Moja i twoja pamięć nie posłucha.</strofa>
+
+
+
+<strofa>Jak cień tém dłuższy, gdy padnie z daleka,/
+Tém szerzéj koło żałobne roztoczy,/
+Tak moja postać, im daléj ucieka,/
+Tém grubszym kirem twą pamięć pomroczy.</strofa>
+
+
+</liryka_l>
+</utwor>
--- /dev/null
+# 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!')
--- /dev/null
+# 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',
+ ])
+
--- /dev/null
+# 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)
--- /dev/null
+# 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"
+ )
--- /dev/null
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf.urls import url
+from django.contrib.auth.decorators import permission_required
+from django.views.generic import RedirectView
+from .feeds import PublishTrackFeed
+from . import views
+
+
+urlpatterns = [
+ url(r'^$', RedirectView.as_view(url='catalogue/', permanent=False)),
+
+ url(r'^images/$', views.image_list, name='documents_image_list'),
+ url(r'^image/(?P<slug>[^/]+)/$', views.image, name="documents_image"),
+ url(r'^image/(?P<slug>[^/]+)/publish$', views.publish_image,
+ name="documents_publish_image"),
+
+ url(r'^catalogue/$', views.document_list, name='documents_document_list'),
+ url(r'^user/$', views.my, name='documents_user'),
+ url(r'^user/(?P<username>[^/]+)/$', views.user, name='documents_user'),
+ url(r'^users/$', views.users, name='documents_users'),
+ url(r'^activity/$', views.activity, name='documents_activity'),
+ url(r'^activity/(?P<isodate>\d{4}-\d{2}-\d{2})/$',
+ views.activity, name='documents_activity'),
+
+ url(r'^upload/$',
+ views.upload, name='documents_upload'),
+
+ url(r'^create/(?P<slug>[^/]*)/',
+ views.create_missing, name='documents_create_missing'),
+ url(r'^create/',
+ views.create_missing, name='documents_create_missing'),
+
+ url(r'^book/(?P<slug>[^/]+)/publish$', views.publish, name="documents_publish"),
+
+ url(r'^book/(?P<slug>[^/]+)/$', views.book, name="documents_book"),
+ url(r'^book/(?P<slug>[^/]+)/gallery/$',
+ permission_required('documents.change_book')(views.GalleryView.as_view()),
+ name="documents_book_gallery"),
+ url(r'^book/(?P<slug>[^/]+)/xml$', views.book_xml, name="documents_book_xml"),
+ url(r'^book/dc/(?P<slug>[^/]+)/xml$', views.book_xml_dc, name="documents_book_xml_dc"),
+ url(r'^book/(?P<slug>[^/]+)/txt$', views.book_txt, name="documents_book_txt"),
+ url(r'^book/(?P<slug>[^/]+)/html$', views.book_html, name="documents_book_html"),
+ url(r'^book/(?P<slug>[^/]+)/epub$', views.book_epub, name="documents_book_epub"),
+ url(r'^book/(?P<slug>[^/]+)/mobi$', views.book_mobi, name="documents_book_mobi"),
+ url(r'^book/(?P<slug>[^/]+)/pdf$', views.book_pdf, name="documents_book_pdf"),
+ url(r'^book/(?P<slug>[^/]+)/pdf-mobile$', views.book_pdf, kwargs={'mobile': True}, name="documents_book_pdf_mobile"),
+
+ url(r'^chunk_add/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
+ views.chunk_add, name="documents_chunk_add"),
+ url(r'^chunk_edit/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
+ views.chunk_edit, name="documents_chunk_edit"),
+ url(r'^book_append/(?P<slug>[^/]+)/$',
+ views.book_append, name="documents_book_append"),
+ url(r'^chunk_mass_edit',
+ views.chunk_mass_edit, name='documents_chunk_mass_edit'),
+ url(r'^image_mass_edit',
+ views.image_mass_edit, name='documents_image_mass_edit'),
+
+ url(r'^track/(?P<slug>[^/]*)/$', PublishTrackFeed()),
+ url(r'^active/$', views.active_users_list, name='active_users_list'),
+
+ url(r'^mark-final/$', views.mark_final, name='mark_final'),
+ url(r'^mark-final-completed/$', views.mark_final_completed, name='mark_final_completed'),
+]
--- /dev/null
+# 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')
--- /dev/null
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from copy import deepcopy
+import re
+
+from lxml import etree
+from .constants import TRIM_BEGIN, TRIM_END, MASTERS
+
+RE_TRIM_BEGIN = re.compile("^<!--%s-->$" % TRIM_BEGIN, re.M)
+RE_TRIM_END = re.compile("^<!--%s-->$" % TRIM_END, re.M)
+
+
+class ParseError(BaseException):
+ pass
+
+
+def _trim(text, trim_begin=True, trim_end=True):
+ """
+ Cut off everything before RE_TRIM_BEGIN and after RE_TRIM_END, so
+ that eg. one big XML file can be compiled from many small XML files.
+ """
+ if trim_begin:
+ text = RE_TRIM_BEGIN.split(text, maxsplit=1)[-1]
+ if trim_end:
+ text = RE_TRIM_END.split(text, maxsplit=1)[0]
+ return text
+
+
+def compile_text(parts):
+ """
+ Compiles full text from an iterable of parts,
+ trimming where applicable.
+ """
+ texts = []
+ trim_begin = False
+ text = ''
+ for next_text in parts:
+ if not next_text:
+ continue
+ if text:
+ # trim the end, because there's more non-empty text
+ # don't trim beginning, if `text' is the first non-empty part
+ texts.append(_trim(text, trim_begin=trim_begin))
+ trim_begin = True
+ text = next_text
+ # don't trim the end, because there's no more text coming after `text'
+ # only trim beginning if it's not still the first non-empty
+ texts.append(_trim(text, trim_begin=trim_begin, trim_end=False))
+ return "".join(texts)
+
+
+def add_trim_begin(text):
+ trim_tag = etree.Comment(TRIM_BEGIN)
+ e = etree.fromstring(text)
+ for master in e[::-1]:
+ if master.tag in MASTERS:
+ break
+ if master.tag not in MASTERS:
+ raise ParseError('No master tag found!')
+
+ master.insert(0, trim_tag)
+ trim_tag.tail = '\n\n\n' + (master.text or '')
+ master.text = '\n'
+ return str(etree.tostring(e, encoding="utf-8"), 'utf-8')
+
+
+def add_trim_end(text):
+ trim_tag = etree.Comment(TRIM_END)
+ e = etree.fromstring(text)
+ for master in e[::-1]:
+ if master.tag in MASTERS:
+ break
+ if master.tag not in MASTERS:
+ raise ParseError('No master tag found!')
+
+ master.append(trim_tag)
+ trim_tag.tail = '\n'
+ prev = trim_tag.getprevious()
+ if prev is not None:
+ prev.tail = (prev.tail or '') + '\n\n\n'
+ else:
+ master.text = (master.text or '') + '\n\n\n'
+ return str(etree.tostring(e, encoding="utf-8"), 'utf-8')
+
+
+def split_xml(text):
+ """Splits text into chapters.
+
+ All this stuff really must go somewhere else.
+
+ """
+ src = etree.fromstring(text)
+ chunks = []
+
+ splitter = u'naglowek_rozdzial'
+ parts = src.findall('.//naglowek_rozdzial')
+ while parts:
+ # copy the document
+ copied = deepcopy(src)
+
+ element = parts[-1]
+
+ # find the chapter's title
+ name_elem = deepcopy(element)
+ for tag in 'extra', 'motyw', 'pa', 'pe', 'pr', 'pt', 'uwaga':
+ for a in name_elem.findall('.//' + tag):
+ a.text=''
+ del a[:]
+ name = etree.tostring(name_elem, method='text', encoding='utf-8').strip()
+
+ # in the original, remove everything from the start of the last chapter
+ parent = element.getparent()
+ del parent[parent.index(element):]
+ element, parent = parent, parent.getparent()
+ while parent is not None:
+ del parent[parent.index(element) + 1:]
+ element, parent = parent, parent.getparent()
+
+ # in the copy, remove everything before the last chapter
+ element = copied.findall('.//naglowek_rozdzial')[-1]
+ parent = element.getparent()
+ while parent is not None:
+ parent.text = None
+ while parent[0] is not element:
+ del parent[0]
+ element, parent = parent, parent.getparent()
+ chunks[:0] = [[name,
+ str(etree.tostring(copied, encoding='utf-8'), 'utf-8')
+ ]]
+
+ parts = src.findall('.//naglowek_rozdzial')
+
+ chunks[:0] = [[u'początek',
+ str(etree.tostring(src, encoding='utf-8'), 'utf-8')
+ ]]
+
+ for ch in chunks[1:]:
+ ch[1] = add_trim_begin(ch[1])
+ for ch in chunks[:-1]:
+ ch[1] = add_trim_end(ch[1])
+
+ return chunks
-{% extends "catalogue/base.html" %}
+{% extends "documents/base.html" %}
{% load i18n %}
{% load upload_tags %}
'bootstrap4',
'catalogue',
+ 'documents',
'cover',
'dvcs',
'wiki',
),
'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': (
),
'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': (
},
'book_list': {
'source_filenames': (
- 'js/catalogue/book_list.js',
+ 'js/documents/book_list.js',
),
'output_filename': 'compressed/book_list.js',
}
+++ /dev/null
-(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);
+++ /dev/null
-(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);
-
--- /dev/null
+(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);
--- /dev/null
+(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);
+
-{% extends "catalogue/base.html" %}
+{% extends "documents/base.html" %}
{% load i18n %}
{% block titleextra %}{% trans "Page not found" %}{% endblock %}
<a href="{{m}}">contact the administrator</a>.{% endblocktrans %}
</p>
-{% url "catalogue_user" as m %}
+{% url "documents_user" as m %}
<p>
{% blocktrans %}If you're coming from Redmine, please note that
work is no longer managed there.
-{% extends "catalogue/base.html" %}
+{% extends "documents/base.html" %}
{% block titleextra %}Logowanie{% endblock %}
{% block subtitle %} - Logowanie {% endblock subtitle %}
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')),
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):
</div>
<nav class="navbar navbar-expand-sm navbar-dark bg-dark" id="header">
- <a class="navbar-brand" href="{% url 'catalogue_document_list' %}">
+ <a class="navbar-brand" href="{% url 'documents_document_list' %}">
<img src="{% static 'img/wl-orange.png' %}" alt="Home" />
</a>
from django.views.decorators.http import require_POST, require_GET
from django.shortcuts import get_object_or_404, render
-from catalogue.models import Book, Chunk
+from documents.models import Book, Chunk
from . import nice_diff
from wiki import forms
from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
try:
book = Book.objects.get(slug=slug)
except Book.DoesNotExist:
- return http.HttpResponseRedirect(reverse("catalogue_create_missing", args=[slug]))
+ return http.HttpResponseRedirect(reverse("documents_create_missing", args=[slug]))
else:
raise Http404
if not chunk.book.accessible(request):
"text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
"pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
},
- 'can_pubmark': request.user.has_perm('catalogue.can_pubmark'),
+ 'can_pubmark': request.user.has_perm('documents.can_pubmark'),
'REDMINE_URL': settings.REDMINE_URL,
})
stage = form.cleaned_data['stage_completed']
tags = [stage] if stage else []
publishable = (form.cleaned_data['publishable'] and
- request.user.has_perm('catalogue.can_pubmark'))
+ request.user.has_perm('documents.can_pubmark'))
doc.commit(author=author,
text=text,
parent=parent,
@require_POST
-@ajax_require_permission('catalogue.can_pubmark')
+@ajax_require_permission('documents.can_pubmark')
def pubmark(request, chunk_id):
form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
if form.is_valid():
from django import forms
from django.utils.translation import ugettext_lazy as _
from wiki.forms import DocumentTextSaveForm
-from catalogue.models import Image
+from documents.models import Image
class ImageSaveForm(DocumentTextSaveForm):
</div>
<nav class="navbar navbar-expand-sm navbar-dark bg-dark" id="header">
- <a class="navbar-brand" href="{% url 'catalogue_document_list' %}">
+ <a class="navbar-brand" href="{% url 'documents_document_list' %}">
<img src="{% static 'img/wl-orange.png' %}" alt="Home" />
</a>
from django.utils.formats import localize
from django.utils.translation import ugettext as _
-from catalogue.models import Image
+from documents.models import Image
from wiki import forms
from wiki import nice_diff
from wiki_img.forms import ImageSaveForm
"text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
"pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
},
- 'can_pubmark': request.user.has_perm('catalogue.can_pubmark_image'),
+ 'can_pubmark': request.user.has_perm('documents.can_pubmark_image'),
'REDMINE_URL': settings.REDMINE_URL,
})
stage = form.cleaned_data['stage_completed']
tags = [stage] if stage else []
publishable = (form.cleaned_data['publishable'] and
- request.user.has_perm('catalogue.can_pubmark_image'))
+ request.user.has_perm('documents.can_pubmark_image'))
doc.commit(author=author,
text=text,
parent=parent,
@require_POST
-@ajax_require_permission('catalogue.can_pubmark_image')
+@ajax_require_permission('documents.can_pubmark_image')
def pubmark(request, object_id):
form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
if form.is_valid():