From: Radek Czajka
Date: Mon, 4 Jul 2011 16:08:20 +0000 (+0200)
Subject: refactor catalogue to separate app,
X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/2f9c60b76f3ab4e69d794a6bb14388a81ff29eb7
refactor catalogue to separate app,
some minor fixes
---
diff --git a/apps/catalogue/__init__.py b/apps/catalogue/__init__.py
new file mode 100644
index 00000000..c53f0e73
--- /dev/null
+++ b/apps/catalogue/__init__.py
@@ -0,0 +1 @@
+ # pragma: no cover
diff --git a/apps/catalogue/admin.py b/apps/catalogue/admin.py
new file mode 100644
index 00000000..70e20d20
--- /dev/null
+++ b/apps/catalogue/admin.py
@@ -0,0 +1,12 @@
+from django.contrib import admin
+
+from catalogue import models
+
+class BookAdmin(admin.ModelAdmin):
+ prepopulated_fields = {'slug': ['title']}
+
+
+admin.site.register(models.Book, BookAdmin)
+admin.site.register(models.Chunk)
+
+admin.site.register(models.Chunk.tag_model)
diff --git a/apps/catalogue/constants.py b/apps/catalogue/constants.py
new file mode 100644
index 00000000..d75d6b4b
--- /dev/null
+++ b/apps/catalogue/constants.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+TRIM_BEGIN = " TRIM_BEGIN "
+TRIM_END = " TRIM_END "
+
+MASTERS = ['powiesc',
+ 'opowiadanie',
+ 'liryka_l',
+ 'liryka_lp',
+ 'dramat_wierszowany_l',
+ 'dramat_wierszowany_lp',
+ 'dramat_wspolczesny',
+ ]
diff --git a/apps/catalogue/fixtures/stages.json b/apps/catalogue/fixtures/stages.json
new file mode 100644
index 00000000..5a46ec04
--- /dev/null
+++ b/apps/catalogue/fixtures/stages.json
@@ -0,0 +1,83 @@
+[
+ {
+ "pk": 1,
+ "model": "catalogue.chunktag",
+ "fields": {
+ "ordering": 1,
+ "name": "Autokorekta",
+ "slug": "first_correction"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "catalogue.chunktag",
+ "fields": {
+ "ordering": 2,
+ "name": "Tagowanie",
+ "slug": "tagging"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "catalogue.chunktag",
+ "fields": {
+ "ordering": 3,
+ "name": "Korekta",
+ "slug": "proofreading"
+ }
+ },
+ {
+ "pk": 4,
+ "model": "catalogue.chunktag",
+ "fields": {
+ "ordering": 4,
+ "name": "Sprawdzenie przypis\u00f3w \u017ar\u00f3d\u0142a",
+ "slug": "annotation-proofreading"
+ }
+ },
+ {
+ "pk": 5,
+ "model": "catalogue.chunktag",
+ "fields": {
+ "ordering": 5,
+ "name": "Uwsp\u00f3\u0142cze\u015bnienie",
+ "slug": "modernisation"
+ }
+ },
+ {
+ "pk": 6,
+ "model": "catalogue.chunktag",
+ "fields": {
+ "ordering": 6,
+ "name": "Przypisy",
+ "slug": "annotations"
+ }
+ },
+ {
+ "pk": 7,
+ "model": "catalogue.chunktag",
+ "fields": {
+ "ordering": 7,
+ "name": "Motywy",
+ "slug": "themes"
+ }
+ },
+ {
+ "pk": 8,
+ "model": "catalogue.chunktag",
+ "fields": {
+ "ordering": 8,
+ "name": "Ostateczna redakcja literacka",
+ "slug": "editor-proofreading"
+ }
+ },
+ {
+ "pk": 9,
+ "model": "catalogue.chunktag",
+ "fields": {
+ "ordering": 9,
+ "name": "Ostateczna redakcja techniczna",
+ "slug": "technical-editor-proofreading"
+ }
+ }
+]
diff --git a/apps/catalogue/forms.py b/apps/catalogue/forms.py
new file mode 100644
index 00000000..33ccbe6f
--- /dev/null
+++ b/apps/catalogue/forms.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.contrib.auth.models import User
+from django.db.models import Count
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from catalogue.constants import MASTERS
+from catalogue.models import Book, Chunk
+
+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 = ['gallery', 'parent', 'parent_number']
+ prepopulated_fields = {'slug': ['title']}
+
+ 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"]:
+ raise forms.ValidationError("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'))
+
+ 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('-count', 'last_name', 'first_name'))
+
+
+ class Meta:
+ model = Chunk
+ exclude = ['number']
+
+ 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"))
+
+
+class BookForm(forms.ModelForm):
+ """
+ Form used for editing a Book.
+ """
+
+ class Meta:
+ model = Book
+
+
+class ChooseMasterForm(forms.Form):
+ """
+ Form used for fixing the chunks in a book.
+ """
+
+ master = forms.ChoiceField(choices=((m, m) for m in MASTERS))
diff --git a/apps/catalogue/helpers.py b/apps/catalogue/helpers.py
new file mode 100644
index 00000000..c9dc0bd9
--- /dev/null
+++ b/apps/catalogue/helpers.py
@@ -0,0 +1,65 @@
+from functools import wraps
+
+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
+
+
+class ChunksList(object):
+ def __init__(self, chunk_qs):
+ self.chunk_qs = chunk_qs.annotate(
+ book_length=Count('book__chunk')).select_related(
+ 'book', 'stage__name',
+ '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], chunk.book_length))
+ 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, book_length):
+ self.book = book
+ self.chunks = chunks
+ self.book_length = book_length
+
diff --git a/apps/catalogue/locale/pl/LC_MESSAGES/django.mo b/apps/catalogue/locale/pl/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..f841945b
Binary files /dev/null and b/apps/catalogue/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/apps/catalogue/locale/pl/LC_MESSAGES/django.po b/apps/catalogue/locale/pl/LC_MESSAGES/django.po
new file mode 100644
index 00000000..c760f3a7
--- /dev/null
+++ b/apps/catalogue/locale/pl/LC_MESSAGES/django.po
@@ -0,0 +1,508 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Platforma Redakcyjna\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-06-21 13:41+0200\n"
+"PO-Revision-Date: 2011-06-21 13:46+0100\n"
+"Last-Translator: Radek Czajka \n"
+"Language-Team: Fundacja Nowoczesna Polska \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:32
+msgid "Publishable"
+msgstr "Gotowe do publikacji"
+
+#: forms.py:68
+msgid "ZIP file"
+msgstr "Plik ZIP"
+
+#: forms.py:99
+#: forms.py:137
+msgid "Author"
+msgstr "Autor"
+
+#: forms.py:100
+#: forms.py:138
+msgid "Your name"
+msgstr "ImiÄ i nazwisko"
+
+#: forms.py:105
+#: forms.py:143
+msgid "Author's email"
+msgstr "E-mail autora"
+
+#: forms.py:106
+#: forms.py:144
+msgid "Your email address, so we can show a gravatar :)"
+msgstr "Adres e-mail, żebyÅmy mogli pokazaÄ gravatar :)"
+
+#: forms.py:112
+#: forms.py:150
+msgid "Your comments"
+msgstr "Twój komentarz"
+
+#: forms.py:113
+msgid "Describe changes you made."
+msgstr "Opisz swoje zmiany"
+
+#: forms.py:119
+msgid "Completed"
+msgstr "UkoÅczono"
+
+#: forms.py:120
+msgid "If you completed a life cycle stage, select it."
+msgstr "JeÅli zostaÅ ukoÅczony etap prac, wskaż go."
+
+#: forms.py:151
+msgid "Describe the reason for reverting."
+msgstr "Opisz powód przywrócenia."
+
+#: forms.py:176
+#: forms.py:190
+msgid "Chunk with this slug already exists"
+msgstr "CzÄÅÄ z tym slugiem już istnieje"
+
+#: forms.py:202
+msgid "Append to"
+msgstr "DoÅÄ
cz do"
+
+#: models.py:25
+msgid "title"
+msgstr "tytuÅ"
+
+#: models.py:26
+msgid "slug"
+msgstr ""
+
+#: models.py:27
+msgid "scan gallery name"
+msgstr "nazwa galerii skanów"
+
+#: models.py:29
+msgid "parent"
+msgstr "rodzic"
+
+#: models.py:30
+msgid "parent number"
+msgstr "numeracja rodzica"
+
+#: models.py:40
+msgid "book"
+msgstr "ksiÄ
żka"
+
+#: models.py:41
+msgid "books"
+msgstr "ksiÄ
żki"
+
+#: models.py:206
+msgid "name"
+msgstr "nazwa"
+
+#: models.py:210
+msgid "theme"
+msgstr "motyw"
+
+#: models.py:211
+msgid "themes"
+msgstr "motywy"
+
+#: views.py:241
+#, python-format
+msgid "Slug already used for %s"
+msgstr "Slug taki sam jak dla pliku %s"
+
+#: views.py:243
+msgid "Slug already used in repository."
+msgstr "Dokument o tym slugu już istnieje w repozytorium."
+
+#: views.py:249
+msgid "File should be UTF-8 encoded."
+msgstr "Plik powinien mieÄ kodowanie UTF-8."
+
+#: views.py:655
+msgid "Tag added"
+msgstr "Dodano tag"
+
+#: views.py:677
+msgid "Revision marked"
+msgstr "Wersja oznaczona"
+
+#: views.py:679
+msgid "Nothing changed"
+msgstr "Nic nie ulegÅo zmianie"
+
+#: templates/wiki/base.html:9
+msgid "Platforma Redakcyjna"
+msgstr ""
+
+#: templates/wiki/book_append_to.html:8
+msgid "Append book"
+msgstr "DoÅÄ
cz ksiÄ
żkÄ"
+
+#: templates/wiki/book_detail.html:6
+#: templates/wiki/book_detail.html.py:46
+msgid "edit"
+msgstr "edytuj"
+
+#: templates/wiki/book_detail.html:16
+msgid "add basic document structure"
+msgstr "dodaj podstawowÄ
strukturÄ dokumentu"
+
+#: templates/wiki/book_detail.html:20
+msgid "change master tag to"
+msgstr "zmieÅ tak master na"
+
+#: templates/wiki/book_detail.html:24
+msgid "add begin trimming tag"
+msgstr "dodaj poczÄ
tkowy ogranicznik"
+
+#: templates/wiki/book_detail.html:28
+msgid "add end trimming tag"
+msgstr "dodaj koÅcowy ogranicznik"
+
+#: templates/wiki/book_detail.html:34
+msgid "unstructured text"
+msgstr "tekst bez struktury"
+
+#: templates/wiki/book_detail.html:38
+msgid "unknown XML"
+msgstr "nieznany XML"
+
+#: templates/wiki/book_detail.html:42
+msgid "broken document"
+msgstr "uszkodzony dokument"
+
+#: templates/wiki/book_detail.html:60
+msgid "Apply fixes"
+msgstr "Wykonaj zmiany"
+
+#: templates/wiki/book_detail.html:66
+msgid "Append to other book"
+msgstr "DoÅÄ
cz do innej ksiÄ
żki"
+
+#: templates/wiki/book_detail.html:68
+msgid "Last published"
+msgstr "Ostatnio opublikowano"
+
+#: templates/wiki/book_detail.html:72
+msgid "Full XML"
+msgstr "PeÅny XML"
+
+#: templates/wiki/book_detail.html:73
+msgid "HTML version"
+msgstr "Wersja HTML"
+
+#: templates/wiki/book_detail.html:74
+msgid "TXT version"
+msgstr "Wersja TXT"
+
+#: templates/wiki/book_detail.html:76
+msgid "EPUB version"
+msgstr "Wersja EPUB"
+
+#: templates/wiki/book_detail.html:77
+msgid "PDF version"
+msgstr "Wersja PDF"
+
+#: templates/wiki/book_detail.html:90
+#: templates/wiki/tabs/summary_view.html:30
+msgid "Publish"
+msgstr "Opublikuj"
+
+#: templates/wiki/book_detail.html:94
+msgid "This book cannot be published yet"
+msgstr "Ta ksiÄ
żka nie może jeszcze zostaÄ opublikowana"
+
+#: templates/wiki/book_edit.html:8
+#: templates/wiki/chunk_edit.html:8
+#: templates/wiki/document_details_base.html:35
+#: templates/wiki/pubmark_dialog.html:15
+#: templates/wiki/tag_dialog.html:15
+msgid "Save"
+msgstr "Zapisz"
+
+#: templates/wiki/chunk_add.html:8
+msgid "Add chunk"
+msgstr "Dodaj czÄÅÄ"
+
+#: templates/wiki/diff_table.html:5
+msgid "Old version"
+msgstr "Stara wersja"
+
+#: templates/wiki/diff_table.html:6
+msgid "New version"
+msgstr "Nowa wersja"
+
+#: templates/wiki/document_create_missing.html:8
+msgid "Create document"
+msgstr "Utwórz dokument"
+
+#: templates/wiki/document_details.html:32
+msgid "Click to open/close gallery"
+msgstr "Kliknij, aby (ro)zwinÄ
Ä galeriÄ"
+
+#: templates/wiki/document_details_base.html:31
+msgid "Help"
+msgstr "Pomoc"
+
+#: templates/wiki/document_details_base.html:33
+msgid "Version"
+msgstr "Wersja"
+
+#: templates/wiki/document_details_base.html:33
+msgid "Unknown"
+msgstr "nieznana"
+
+#: templates/wiki/document_details_base.html:36
+msgid "Save attempt in progress"
+msgstr "Trwa zapisywanie"
+
+#: templates/wiki/document_details_base.html:37
+msgid "There is a newer version of this document!"
+msgstr "Istnieje nowsza wersja tego dokumentu!"
+
+#: templates/wiki/document_list.html:31
+msgid "Clear filter"
+msgstr "WyczyÅÄ filtr"
+
+#: templates/wiki/document_list.html:46
+msgid "No books found."
+msgstr "Nie znaleziono ksiÄ
żek."
+
+#: templates/wiki/document_list.html:89
+msgid "Your last edited documents"
+msgstr "Twoje ostatnie edycje"
+
+#: templates/wiki/document_upload.html:8
+msgid "Bulk documents upload"
+msgstr "Hurtowe dodawanie dokumentów"
+
+#: templates/wiki/document_upload.html:11
+msgid "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with .xml
will be ignored."
+msgstr "ProszÄ wskazaÄ archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie koÅczÄ
ce siÄ na .xml
zostanÄ
zignorowane."
+
+#: templates/wiki/document_upload.html:16
+#: templatetags/wiki.py:36
+msgid "Upload"
+msgstr "ZaÅaduj"
+
+#: templates/wiki/document_upload.html:23
+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/wiki/document_upload.html:24
+msgid "Offending files"
+msgstr "BÅÄdne pliki"
+
+#: templates/wiki/document_upload.html:32
+msgid "Correct files"
+msgstr "Poprawne pliki"
+
+#: templates/wiki/document_upload.html:43
+msgid "Files have been successfully uploaded to the repository."
+msgstr "Pliki zostaÅy dodane do repozytorium."
+
+#: templates/wiki/document_upload.html:44
+msgid "Uploaded files"
+msgstr "Dodane pliki"
+
+#: templates/wiki/document_upload.html:54
+msgid "Skipped files"
+msgstr "PominiÄte pliki"
+
+#: templates/wiki/document_upload.html:55
+msgid "Files skipped due to no .xml
extension"
+msgstr "Pliki pominiÄte z powodu braku rozszerzenia .xml
."
+
+#: templates/wiki/pubmark_dialog.html:16
+#: templates/wiki/revert_dialog.html:39
+#: templates/wiki/tag_dialog.html:16
+msgid "Cancel"
+msgstr "Anuluj"
+
+#: templates/wiki/revert_dialog.html:38
+msgid "Revert"
+msgstr "PrzywróÄ"
+
+#: templates/wiki/user_list.html:7
+#: templatetags/wiki.py:33
+msgid "Users"
+msgstr "Użytkownicy"
+
+#: templates/wiki/tabs/annotations_view.html:9
+msgid "all"
+msgstr "wszystkie"
+
+#: templates/wiki/tabs/annotations_view_item.html:3
+msgid "Annotations"
+msgstr "Przypisy"
+
+#: templates/wiki/tabs/gallery_view.html:7
+msgid "Previous"
+msgstr "Poprzednie"
+
+#: templates/wiki/tabs/gallery_view.html:13
+msgid "Next"
+msgstr "NastÄpne"
+
+#: templates/wiki/tabs/gallery_view.html:15
+msgid "Zoom in"
+msgstr "PowiÄksz"
+
+#: templates/wiki/tabs/gallery_view.html:16
+msgid "Zoom out"
+msgstr "Zmniejsz"
+
+#: templates/wiki/tabs/gallery_view_item.html:3
+msgid "Gallery"
+msgstr "Galeria"
+
+#: templates/wiki/tabs/history_view.html:5
+msgid "Compare versions"
+msgstr "Porównaj wersje"
+
+#: templates/wiki/tabs/history_view.html:7
+msgid "Mark for publishing"
+msgstr "Oznacz do publikacji"
+
+#: templates/wiki/tabs/history_view.html:9
+msgid "Revert document"
+msgstr "PrzywrÃ³Ä wersjÄ"
+
+#: templates/wiki/tabs/history_view.html:12
+msgid "View version"
+msgstr "Zobacz wersjÄ"
+
+#: templates/wiki/tabs/history_view_item.html:3
+msgid "History"
+msgstr "Historia"
+
+#: templates/wiki/tabs/search_view.html:3
+#: templates/wiki/tabs/search_view.html:5
+msgid "Search"
+msgstr "Szukaj"
+
+#: templates/wiki/tabs/search_view.html:8
+msgid "Replace with"
+msgstr "ZamieÅ na"
+
+#: templates/wiki/tabs/search_view.html:10
+msgid "Replace"
+msgstr "ZamieÅ"
+
+#: templates/wiki/tabs/search_view.html:13
+msgid "Options"
+msgstr "Opcje"
+
+#: templates/wiki/tabs/search_view.html:15
+msgid "Case sensitive"
+msgstr "Rozróżniaj wielkoÅÄ liter"
+
+#: templates/wiki/tabs/search_view.html:17
+msgid "From cursor"
+msgstr "Zacznij od kursora"
+
+#: templates/wiki/tabs/search_view_item.html:3
+msgid "Search and replace"
+msgstr "Znajdź i zamieÅ"
+
+#: templates/wiki/tabs/source_editor_item.html:5
+msgid "Source code"
+msgstr "Kod źródÅowy"
+
+#: templates/wiki/tabs/summary_view.html:9
+msgid "Title"
+msgstr "TytuÅ"
+
+#: templates/wiki/tabs/summary_view.html:14
+msgid "Document ID"
+msgstr "ID dokumentu"
+
+#: templates/wiki/tabs/summary_view.html:18
+msgid "Current version"
+msgstr "Aktualna wersja"
+
+#: templates/wiki/tabs/summary_view.html:21
+msgid "Last edited by"
+msgstr "Ostatnio edytowane przez"
+
+#: templates/wiki/tabs/summary_view.html:25
+msgid "Link to gallery"
+msgstr "Link do galerii"
+
+#: templates/wiki/tabs/summary_view_item.html:3
+msgid "Summary"
+msgstr "Podsumowanie"
+
+#: templates/wiki/tabs/wysiwyg_editor.html:9
+msgid "Insert theme"
+msgstr "Wstaw motyw"
+
+#: templates/wiki/tabs/wysiwyg_editor.html:12
+msgid "Insert annotation"
+msgstr "Wstaw przypis"
+
+#: templates/wiki/tabs/wysiwyg_editor_item.html:3
+msgid "Visual editor"
+msgstr "Edytor wizualny"
+
+#: templatetags/wiki.py:30
+msgid "Assigned to me"
+msgstr "Przypisane do mnie"
+
+#: templatetags/wiki.py:32
+msgid "Unassigned"
+msgstr "Nie przypisane"
+
+#: templatetags/wiki.py:34
+msgid "All"
+msgstr "Wszystkie"
+
+#: templatetags/wiki.py:35
+msgid "Add"
+msgstr "Dodaj"
+
+#: templatetags/wiki.py:39
+msgid "Admin"
+msgstr "Administracja"
+
+#~ 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ż"
+
+#~ msgid "Insert special character"
+#~ msgstr "Wstaw znak specjalny"
diff --git a/apps/catalogue/management/__init__.py b/apps/catalogue/management/__init__.py
new file mode 100755
index 00000000..e69de29b
diff --git a/apps/catalogue/management/commands/__init__.py b/apps/catalogue/management/commands/__init__.py
new file mode 100755
index 00000000..e69de29b
diff --git a/apps/catalogue/management/commands/assign_from_redmine.py b/apps/catalogue/management/commands/assign_from_redmine.py
new file mode 100755
index 00000000..9f7b12d4
--- /dev/null
+++ b/apps/catalogue/management/commands/assign_from_redmine.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+
+import csv
+from optparse import make_option
+import re
+import sys
+import urllib
+import urllib2
+
+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 slughifi import slughifi
+from catalogue.models import Chunk
+
+
+REDMINE_CSV = 'http://redmine.nowoczesnapolska.org.pl/projects/wl-publikacje/issues.csv'
+REDAKCJA_URL = 'http://redakcja.wolnelektury.pl/documents/'
+
+
+class Command(BaseCommand):
+ option_list = BaseCommand.option_list + (
+ make_option('-r', '--redakcja', dest='redakcja', metavar='URL',
+ help='Base URL of Redakcja documents',
+ default=REDAKCJA_URL),
+ make_option('-q', '--quiet', action='store_false', dest='verbose', default=True,
+ help='Less output'),
+ make_option('-f', '--force', action='store_true', dest='force', default=False,
+ help='Force assignment overwrite'),
+ )
+ help = 'Imports ticket assignments from Redmine.'
+ args = '[redmine-csv-url]'
+
+ def handle(self, *redmine_csv, **options):
+
+ self.style = color_style()
+
+ redakcja = options.get('redakcja')
+ verbose = options.get('verbose')
+ force = options.get('force')
+
+ if not redmine_csv:
+ if verbose:
+ print "Using default Redmine CSV URL:", REDMINE_CSV
+ redmine_csv = REDMINE_CSV
+
+ # Start transaction management.
+ transaction.commit_unless_managed()
+ transaction.enter_transaction_management()
+ transaction.managed(True)
+
+ redakcja_link = re.compile(re.escape(redakcja) + r'([-_.:?&%/a-zA-Z0-9]*)')
+
+ all_tickets = 0
+ all_chunks = 0
+ done_tickets = 0
+ done_chunks = 0
+ empty_users = 0
+ unknown_users = {}
+ unknown_books = []
+ forced = []
+
+ if verbose:
+ print 'Downloading CSV file'
+ for r in csv.reader(urllib2.urlopen(redmine_csv)):
+ if r[0] == '#':
+ continue
+ all_tickets += 1
+
+ username = r[6]
+ if not username:
+ if verbose:
+ print "Empty user, skipping"
+ empty_users += 1
+ continue
+
+ first_name, last_name = unicode(username, 'utf-8').rsplit(u' ', 1)
+ try:
+ user = User.objects.get(first_name=first_name, last_name=last_name)
+ except User.DoesNotExist:
+ print self.style.ERROR('Unknown user: ' + username)
+ unknown_users.setdefault(username, 0)
+ unknown_users[username] += 1
+ continue
+
+ ticket_done = False
+ for fname in redakcja_link.findall(r[-1]):
+ fname = unicode(urllib.unquote(fname), 'utf-8', 'ignore')
+ if fname.endswith('.xml'):
+ fname = fname[:-4]
+ fname = fname.replace(' ', '_')
+ fname = slughifi(fname)
+
+ chunks = Chunk.objects.filter(book__slug=fname)
+ if not chunks:
+ print self.style.ERROR('Unknown book: ' + fname)
+ unknown_books.append(fname)
+ continue
+ all_chunks += chunks.count()
+
+ for chunk in chunks:
+ if chunk.user:
+ if chunk.user == user:
+ continue
+ else:
+ forced.append((chunk, chunk.user, user))
+ if force:
+ print self.style.WARNING(
+ '%s assigned to %s, forcing change to %s.' %
+ (chunk.pretty_name(), chunk.user, user))
+ else:
+ print self.style.WARNING(
+ '%s assigned to %s not to %s, skipping.' %
+ (chunk.pretty_name(), chunk.user, user))
+ continue
+ chunk.user = user
+ chunk.save()
+ ticket_done = True
+ done_chunks += 1
+
+ if ticket_done:
+ done_tickets += 1
+
+
+ # Print results
+ print
+ print "Results:"
+ print "Assignments imported from %d/%d tickets to %d/%d relevalt chunks." % (
+ done_tickets, all_tickets, done_chunks, all_chunks)
+ if empty_users:
+ print "%d tickets were unassigned." % empty_users
+ if forced:
+ print "%d assignments conficts (%s):" % (
+ len(forced), "changed" if force else "left")
+ for chunk, orig, user in forced:
+ print " %s: \t%s \t-> %s" % (
+ chunk.pretty_name(), orig.username, user.username)
+ if unknown_books:
+ print "%d unknown books:" % len(unknown_books)
+ for fname in unknown_books:
+ print " %s" % fname
+ if unknown_users:
+ print "%d unknown users:" % len(unknown_users)
+ for name in unknown_users:
+ print " %s (%d tickets)" % (name, unknown_users[name])
+ print
+
+
+ transaction.commit()
+ transaction.leave_transaction_management()
+
diff --git a/apps/catalogue/migrations/0001_initial.py b/apps/catalogue/migrations/0001_initial.py
new file mode 100644
index 00000000..4ebde478
--- /dev/null
+++ b/apps/catalogue/migrations/0001_initial.py
@@ -0,0 +1,365 @@
+# encoding: utf-8
+import datetime
+import os.path
+import cPickle
+import re
+import urllib
+
+from django.conf import settings
+from django.db import models
+from mercurial import mdiff, hg, ui
+from south.db import db
+from south.v2 import SchemaMigration
+
+from slughifi import slughifi
+
+META_REGEX = re.compile(r'\s*', re.DOTALL | re.MULTILINE)
+STAGE_TAGS_RE = re.compile(r'^#stage-finished: (.*)$', re.MULTILINE)
+AUTHOR_RE = re.compile(r'\s*(.*?)\s*<(.*)>\s*')
+
+
+def urlunquote(url):
+ """Unqotes URL
+
+ # >>> urlunquote('Za%C5%BC%C3%B3%C5%82%C4%87_g%C4%99%C5%9Bl%C4%85_ja%C5%BA%C5%84')
+ # u'Za\u017c\xf3\u0142\u0107_g\u0119\u015bl\u0105 ja\u017a\u0144'
+ """
+ return unicode(urllib.unquote(url), 'utf-8', 'ignore')
+
+
+def split_name(name):
+ parts = name.split('__')
+ return parts
+
+
+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)
+
+
+def make_patch(src, dst):
+ if isinstance(src, unicode):
+ src = src.encode('utf-8')
+ if isinstance(dst, unicode):
+ dst = dst.encode('utf-8')
+ return cPickle.dumps(mdiff.textdiff(src, dst))
+
+
+def plain_text(text):
+ return re.sub(META_REGEX, '', text, 1)
+
+
+def gallery(slug, text):
+ result = {}
+
+ m = re.match(META_REGEX, text)
+ if m:
+ for line in m.group(1).split('\n'):
+ try:
+ k, v = line.split(':', 1)
+ result[k.strip()] = v.strip()
+ except ValueError:
+ continue
+
+ gallery = result.get('gallery', slughifi(slug))
+
+ if gallery.startswith('/'):
+ gallery = os.path.basename(gallery)
+
+ return gallery
+
+
+def migrate_file_from_hg(orm, fname, entry):
+ fname = urlunquote(fname)
+ print fname
+ if fname.endswith('.xml'):
+ fname = fname[:-4]
+ title = file_to_title(fname)
+ fname = slughifi(fname)
+ # create all the needed objects
+ # what if it already exists?
+ book = orm.Book.objects.create(
+ title=title,
+ slug=fname)
+ chunk = orm.Chunk.objects.create(
+ book=book,
+ number=1,
+ slug='1')
+ head = orm.ChunkChange.objects.create(
+ tree=chunk,
+ revision=-1,
+ patch=make_patch('', ''),
+ created_at=datetime.datetime.fromtimestamp(entry.filectx(0).date()[0]),
+ description=''
+ )
+ chunk.head = head
+ try:
+ chunk.stage = orm.ChunkTag.objects.order_by('ordering')[0]
+ except IndexError:
+ chunk.stage = None
+ old_data = ''
+
+ maxrev = entry.filerev()
+ gallery_link = None
+
+ for rev in xrange(maxrev + 1):
+ fctx = entry.filectx(rev)
+ data = fctx.data()
+ gallery_link = gallery(fname, data)
+ data = plain_text(data)
+
+ # get tags from description
+ description = fctx.description().decode("utf-8", 'replace')
+ tags = STAGE_TAGS_RE.findall(description)
+ tags = [orm.ChunkTag.objects.get(slug=slug.strip()) for slug in tags]
+
+ if tags:
+ max_ordering = max(tags, key=lambda x: x.ordering).ordering
+ try:
+ chunk.stage = orm.ChunkTag.objects.filter(ordering__gt=max_ordering).order_by('ordering')[0]
+ except IndexError:
+ chunk.stage = None
+
+ description = STAGE_TAGS_RE.sub('', description)
+
+ author = author_name = author_email = None
+ author_desc = fctx.user().decode("utf-8", 'replace')
+ m = AUTHOR_RE.match(author_desc)
+ if m:
+ try:
+ author = orm['auth.User'].objects.get(username=m.group(1), email=m.group(2))
+ except orm['auth.User'].DoesNotExist:
+ author_name = m.group(1)
+ author_email = m.group(2)
+ else:
+ author_name = author_desc
+
+ head = orm.ChunkChange.objects.create(
+ tree=chunk,
+ revision=rev + 1,
+ patch=make_patch(old_data, data),
+ created_at=datetime.datetime.fromtimestamp(fctx.date()[0]),
+ description=description,
+ author=author,
+ author_name=author_name,
+ author_email=author_email,
+ parent=chunk.head
+ )
+ head.tags = tags
+ chunk.head = head
+ old_data = data
+
+ chunk.save()
+ if gallery_link:
+ book.gallery = gallery_link
+ book.save()
+
+
+def migrate_from_hg(orm):
+ try:
+ hg_path = settings.WIKI_REPOSITORY_PATH
+ except:
+ pass
+
+ print 'migrate from', hg_path
+ repo = hg.repository(ui.ui(), hg_path)
+ tip = repo['tip']
+ for fname in tip:
+ if fname.startswith('.'):
+ continue
+ migrate_file_from_hg(orm, fname, tip[fname])
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'Book'
+ db.create_table('catalogue_book', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('title', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+ ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=128, db_index=True)),
+ ('gallery', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
+ ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='children', null=True, to=orm['catalogue.Book'])),
+ ('parent_number', self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True)),
+ ('last_published', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
+ ))
+ db.send_create_signal('catalogue', ['Book'])
+
+ # Adding model 'Chunk'
+ db.create_table('catalogue_chunk', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('creator', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='created_documents', null=True, to=orm['auth.User'])),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
+ ('book', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Book'])),
+ ('number', self.gf('django.db.models.fields.IntegerField')()),
+ ('slug', self.gf('django.db.models.fields.SlugField')(max_length=50, db_index=True)),
+ ('comment', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
+ ('stage', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.ChunkTag'], null=True, blank=True)),
+ ('head', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['catalogue.ChunkChange'], null=True, blank=True)),
+ ))
+ db.send_create_signal('catalogue', ['Chunk'])
+
+ # Adding unique constraint on 'Chunk', fields ['book', 'number']
+ db.create_unique('catalogue_chunk', ['book_id', 'number'])
+
+ # Adding unique constraint on 'Chunk', fields ['book', 'slug']
+ db.create_unique('catalogue_chunk', ['book_id', 'slug'])
+
+ # Adding model 'ChunkTag'
+ db.create_table('catalogue_chunktag', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
+ ('slug', self.gf('django.db.models.fields.SlugField')(db_index=True, max_length=64, unique=True, null=True, blank=True)),
+ ('ordering', self.gf('django.db.models.fields.IntegerField')()),
+ ))
+ db.send_create_signal('catalogue', ['ChunkTag'])
+
+ # Adding model 'ChunkChange'
+ db.create_table('catalogue_chunkchange', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('author', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
+ ('author_name', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
+ ('author_email', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
+ ('patch', self.gf('django.db.models.fields.TextField')(blank=True)),
+ ('revision', self.gf('django.db.models.fields.IntegerField')(db_index=True)),
+ ('parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='children', null=True, blank=True, to=orm['catalogue.ChunkChange'])),
+ ('merge_parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='merge_children', null=True, blank=True, to=orm['catalogue.ChunkChange'])),
+ ('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
+ ('created_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, db_index=True)),
+ ('publishable', self.gf('django.db.models.fields.BooleanField')(default=False)),
+ ('tree', self.gf('django.db.models.fields.related.ForeignKey')(related_name='change_set', to=orm['catalogue.Chunk'])),
+ ))
+ db.send_create_signal('catalogue', ['ChunkChange'])
+
+ # Adding unique constraint on 'ChunkChange', fields ['tree', 'revision']
+ db.create_unique('catalogue_chunkchange', ['tree_id', 'revision'])
+
+ # Adding M2M table for field tags on 'ChunkChange'
+ db.create_table('catalogue_chunkchange_tags', (
+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+ ('chunkchange', models.ForeignKey(orm['catalogue.chunkchange'], null=False)),
+ ('chunktag', models.ForeignKey(orm['catalogue.chunktag'], null=False))
+ ))
+ db.create_unique('catalogue_chunkchange_tags', ['chunkchange_id', 'chunktag_id'])
+
+ if not db.dry_run:
+ from django.core.management import call_command
+ call_command("loaddata", "stages.json")
+
+ migrate_from_hg(orm)
+
+
+ def backwards(self, orm):
+
+ # Removing unique constraint on 'ChunkChange', fields ['tree', 'revision']
+ db.delete_unique('catalogue_chunkchange', ['tree_id', 'revision'])
+
+ # Removing unique constraint on 'Chunk', fields ['book', 'slug']
+ db.delete_unique('catalogue_chunk', ['book_id', 'slug'])
+
+ # Removing unique constraint on 'Chunk', fields ['book', 'number']
+ db.delete_unique('catalogue_chunk', ['book_id', 'number'])
+
+ # Deleting model 'Book'
+ db.delete_table('catalogue_book')
+
+ # Deleting model 'Chunk'
+ db.delete_table('catalogue_chunk')
+
+ # Deleting model 'ChunkTag'
+ db.delete_table('catalogue_chunktag')
+
+ # Deleting model 'ChunkChange'
+ db.delete_table('catalogue_chunkchange')
+
+ # Removing M2M table for field tags on 'ChunkChange'
+ db.delete_table('catalogue_chunkchange_tags')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'catalogue.book': {
+ 'Meta': {'ordering': "['parent_number', 'title']", 'object_name': 'Book'},
+ 'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_published': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+ 'parent_number': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+ },
+ 'catalogue.chunk': {
+ 'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+ 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_documents'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ChunkChange']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'number': ('django.db.models.fields.IntegerField', [], {}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+ 'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'catalogue.chunkchange': {
+ 'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+ 'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+ 'patch': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}),
+ 'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"})
+ },
+ 'catalogue.chunktag': {
+ 'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'ordering': ('django.db.models.fields.IntegerField', [], {}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['catalogue']
diff --git a/apps/catalogue/migrations/__init__.py b/apps/catalogue/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/catalogue/models.py b/apps/catalogue/models.py
new file mode 100644
index 00000000..ff3d434e
--- /dev/null
+++ b/apps/catalogue/models.py
@@ -0,0 +1,178 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.core.urlresolvers import reverse
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+from dvcs import models as dvcs_models
+from catalogue.xml_tools import compile_text
+
+import logging
+logger = logging.getLogger("fnp.catalogue")
+
+
+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)
+ gallery = models.CharField(_('scan gallery name'), max_length=255, blank=True)
+
+ parent = models.ForeignKey('self', null=True, blank=True, verbose_name=_('parent'), related_name="children")
+ parent_number = models.IntegerField(_('parent number'), null=True, blank=True, db_index=True)
+ last_published = models.DateTimeField(null=True, editable=False, db_index=True)
+
+ class NoTextError(BaseException):
+ pass
+
+ class Meta:
+ ordering = ['parent_number', 'title']
+ verbose_name = _('book')
+ verbose_name_plural = _('books')
+
+ def __unicode__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ return reverse("catalogue_book", args=[self.slug])
+
+ @classmethod
+ def create(cls, creator=None, text=u'', *args, **kwargs):
+ """
+ >>> Book.create(slug='x', text='abc').materialize()
+ 'abc'
+ """
+ instance = cls(*args, **kwargs)
+ instance.save()
+ instance[0].commit(author=creator, text=text)
+ return instance
+
+ def __iter__(self):
+ return iter(self.chunk_set.all())
+
+ def __getitem__(self, chunk):
+ return self.chunk_set.all()[chunk]
+
+ def materialize(self, publishable=True):
+ """
+ Get full text of the document compiled from chunks.
+ Takes the current versions of all texts
+ or versions most recently tagged for publishing.
+ """
+ if publishable:
+ changes = [chunk.publishable() for chunk in self]
+ else:
+ changes = [chunk.head for chunk in self]
+ if None in changes:
+ raise self.NoTextError('Some chunks have no available text.')
+ return compile_text(change.materialize() for change in changes)
+
+ def publishable(self):
+ if not len(self):
+ return False
+ for chunk in self:
+ if not chunk.publishable():
+ return False
+ return True
+
+ 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
+ while new_slug in slugs:
+ new_slug = "%s-%d" % (proposed, i)
+ i += 1
+ return new_slug
+
+ def append(self, other):
+ number = self[len(self) - 1].number + 1
+ single = len(other) == 1
+ for chunk in other:
+ # move chunk to new book
+ chunk.book = self
+ chunk.number = number
+
+ # 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.comment = other_title_part
+ if other.slug.startswith(self.slug):
+ chunk_slug = other.slug[len(self.slug):].lstrip('-_')
+ else:
+ chunk_slug = other.slug
+ chunk.slug = self.make_chunk_slug(chunk_slug)
+ else:
+ chunk.comment = "%s, %s" % (other_title_part, chunk.comment)
+ chunk.slug = self.make_chunk_slug(chunk.slug)
+ chunk.save()
+ number += 1
+ other.delete()
+
+ @staticmethod
+ def listener_create(sender, instance, created, **kwargs):
+ if created:
+ instance.chunk_set.create(number=1, slug='1')
+
+models.signals.post_save.connect(Book.listener_create, sender=Book)
+
+
+class Chunk(dvcs_models.Document):
+ """ An editable chunk of text. Every Book text is divided into chunks. """
+
+ book = models.ForeignKey(Book, editable=False)
+ number = models.IntegerField()
+ slug = models.SlugField()
+ comment = models.CharField(max_length=255, blank=True)
+
+ class Meta:
+ unique_together = [['book', 'number'], ['book', 'slug']]
+ ordering = ['number']
+
+ def __unicode__(self):
+ return "%d-%d: %s" % (self.book_id, self.number, self.comment)
+
+ def get_absolute_url(self):
+ return reverse("wiki_editor", args=[self.book.slug, self.slug])
+
+ @classmethod
+ def get(cls, slug, chunk=None):
+ if chunk is None:
+ return cls.objects.get(book__slug=slug, number=1)
+ else:
+ return cls.objects.get(book__slug=slug, slug=chunk)
+
+ def pretty_name(self, book_length=None):
+ title = self.book.title
+ if self.comment:
+ title += ", %s" % self.comment
+ if book_length > 1:
+ title += " (%d/%d)" % (self.number, book_length)
+ return title
+
+ def split(self, slug, comment='', creator=None):
+ """ Create an empty chunk after this one """
+ self.book.chunk_set.filter(number__gt=self.number).update(
+ number=models.F('number')+1)
+ new_chunk = self.book.chunk_set.create(number=self.number+1,
+ creator=creator, slug=slug, comment=comment)
+ return new_chunk
+
+ @staticmethod
+ def listener_saved(sender, instance, created, **kwargs):
+ if instance.book:
+ # save book so that its _list_html is reset
+ instance.book.save()
+
+models.signals.post_save.connect(Chunk.listener_saved, sender=Chunk)
diff --git a/apps/catalogue/templates/catalogue/base.html b/apps/catalogue/templates/catalogue/base.html
new file mode 100644
index 00000000..4ba8a0ea
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/base.html
@@ -0,0 +1,49 @@
+{% load compressed i18n %}
+{% load catalogue %}
+
+
+
+
+ {% compressed_css 'listing' %}
+
+ {% block title %}{% trans "Platforma Redakcyjna" %}{% endblock title %}
+
+
+
+
+
+
+
+
+
+
+ {% main_tabs %}
+
+
+
+ {% include "registration/head_login.html" %}
+
+
+
+
+
+
+
+{% block content %}
+
+ {% block leftcolumn %}
+ {% endblock leftcolumn %}
+
+
+ {% block rightcolumn %}
+ {% endblock rightcolumn %}
+
+{% endblock content %}
+
+
+
+{% compressed_js 'listing' %}
+{% block extrabody %}
+{% endblock %}
+
+
diff --git a/apps/catalogue/templates/catalogue/book_append_to.html b/apps/catalogue/templates/catalogue/book_append_to.html
new file mode 100755
index 00000000..76a59621
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/book_append_to.html
@@ -0,0 +1,14 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/book_detail.html b/apps/catalogue/templates/catalogue/book_detail.html
new file mode 100755
index 00000000..1c0178c5
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/book_detail.html
@@ -0,0 +1,104 @@
+{% extends "catalogue/base.html" %}
+{% load comments i18n %}
+
+{% block leftcolumn %}
+
+{% trans "edit" %}
+ {{ book.title }}
+
+
+ {% for c in chunks %}
+
+ {{ c.chunk.comment }} |
+ {% for fix in c.fix %}
+
+ {% ifequal fix "wl" %}</>{% endifequal %}
+
+ {% ifequal fix "bad-master" %}master{% endifequal %}
+
+ {% ifequal fix "trim-begin" %}✁{% endifequal %}
+
+ {% ifequal fix "trim-end" %}✃{% endifequal %}
+
+ {% endfor %}
+
+ {% ifequal c.grade "plain" %}
+ {% trans "unstructured text" %}
+ {% endifequal %}
+
+ {% ifequal c.grade "xml" %}
+ {% trans "unknown XML" %}
+ {% endifequal %}
+
+ {% ifequal c.grade "wl-broken" %}
+ {% trans "broken document" %}
+ {% endifequal %}
+
+ |
+ [{% trans "edit" %}] |
+ {% if c.chunk.publishable %}P{% endif %} |
+ {% if c.chunk.user.is_authenticated %}
+ {{ c.chunk.user }}
+ {% endif %} |
+ [+] |
+
+ {% endfor %}
+ {% if need_fixing %}
+ |
+
+ |
+ {% endif %}
+
+
+{% trans "Append to other book" %}
+
+{% trans "Last published" %}: {{ book.last_published }}
+
+{% if book.publishable %}
+
+ {% trans "Full XML" %}
+ {% trans "HTML version" %}
+ {% trans "TXT version" %}
+ {% comment %}
+ {% trans "EPUB version" %}
+ {% trans "PDF version" %}
+ {% endcomment %}
+
+
+
+
+{% else %}
+ {% trans "This book cannot be published yet" %}
+{% endif %}
+
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% render_comment_list for book %}
+{% render_comment_form for book %}
+
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/book_edit.html b/apps/catalogue/templates/catalogue/book_edit.html
new file mode 100755
index 00000000..3fffa963
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/book_edit.html
@@ -0,0 +1,14 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/chunk_add.html b/apps/catalogue/templates/catalogue/chunk_add.html
new file mode 100755
index 00000000..800a7e45
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/chunk_add.html
@@ -0,0 +1,14 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/chunk_edit.html b/apps/catalogue/templates/catalogue/chunk_edit.html
new file mode 100755
index 00000000..3fffa963
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/chunk_edit.html
@@ -0,0 +1,14 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/document_create_missing.html b/apps/catalogue/templates/catalogue/document_create_missing.html
new file mode 100644
index 00000000..dcb61752
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/document_create_missing.html
@@ -0,0 +1,14 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/document_list.html b/apps/catalogue/templates/catalogue/document_list.html
new file mode 100644
index 00000000..9a40dfcc
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/document_list.html
@@ -0,0 +1,102 @@
+{% extends "catalogue/base.html" %}
+
+{% load i18n %}
+{% load pagination_tags %}
+{% load catalogue %}
+
+{% block extrabody %}
+{{ block.super }}
+
+{% endblock %}
+
+{% block leftcolumn %}
+
+
+
+
+
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+
+
{% trans "Your last edited documents" %}
+
+ {% for slugs, item in last_books %}
+ - {{ item.title }}
({{ item.time|date:"H:i:s, d/m/Y" }})
+ {% endfor %}
+
+
+
+ {% trans "Recent activity" %}
+ {% wall %}
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/document_upload.html b/apps/catalogue/templates/catalogue/document_upload.html
new file mode 100644
index 00000000..87e93e0a
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/document_upload.html
@@ -0,0 +1,69 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+
+{% block leftcolumn %}
+
+
+{% trans "Bulk documents upload" %}
+
+
+{% trans "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with .xml
will be ignored." %}
+
+
+
+
+
+
+{% if error_list %}
+
+ {% trans "There have been some errors. No files have been added to the repository." %}
+
{% trans "Offending files" %}
+
+ {% for filename, title, error in error_list %}
+ - {{ title }} (
{{ filename }}
): {{ error }}
+ {% endfor %}
+
+
+ {% if ok_list %}
+ {% trans "Correct files" %}
+
+ {% for filename, slug, title in ok_list %}
+ - {{ title }} (
{{ filename }}
)
+ {% endfor %}
+
+ {% endif %}
+
+{% else %}
+
+ {% if ok_list %}
+ {% trans "Files have been successfully uploaded to the repository." %}
+ {% trans "Uploaded files" %}
+
+ {% for filename, slug, title in ok_list %}
+ - {{ title }} (
{{ filename }})
+ {% endfor %}
+
+ {% endif %}
+{% endif %}
+
+{% if skipped_list %}
+ {% trans "Skipped files" %}
+ {% trans "Files skipped due to no .xml
extension" %}
+
+ {% for filename in skipped_list %}
+ - {{ filename }}
+ {% endfor %}
+
+{% endif %}
+
+
+{% endblock leftcolumn %}
+
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/main_tabs.html b/apps/catalogue/templates/catalogue/main_tabs.html
new file mode 100755
index 00000000..82321cc4
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/main_tabs.html
@@ -0,0 +1,3 @@
+{% for tab in tabs %}
+ {{ tab.caption }}
+{% endfor %}
diff --git a/apps/catalogue/templates/catalogue/user_list.html b/apps/catalogue/templates/catalogue/user_list.html
new file mode 100755
index 00000000..9e1e83e5
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/user_list.html
@@ -0,0 +1,18 @@
+{% extends "catalogue/base.html" %}
+
+{% load i18n %}
+
+{% block leftcolumn %}
+
+{% trans "Users" %}
+
+
+
+{% endblock leftcolumn %}
diff --git a/apps/catalogue/templates/catalogue/wall.html b/apps/catalogue/templates/catalogue/wall.html
new file mode 100755
index 00000000..0e4597e5
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/wall.html
@@ -0,0 +1,32 @@
+{% load i18n %}
+{% load gravatar %}
+
+
diff --git a/apps/catalogue/templatetags/__init__.py b/apps/catalogue/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/catalogue/templatetags/catalogue.py b/apps/catalogue/templatetags/catalogue.py
new file mode 100644
index 00000000..cdb19404
--- /dev/null
+++ b/apps/catalogue/templatetags/catalogue.py
@@ -0,0 +1,141 @@
+from __future__ import absolute_import
+
+from django.db.models import Count
+from django.core.urlresolvers import reverse
+from django.contrib.comments.models import Comment
+from django.template.defaultfilters import stringfilter
+from django import template
+from django.utils.translation import ugettext as _
+
+from catalogue.models import Book, Chunk
+
+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']
+ if user.is_authenticated():
+ tabs.append(Tab('my', _('My page'), reverse("catalogue_user")))
+
+ tabs.append(Tab('unassigned', _('Unassigned'), reverse("catalogue_unassigned")))
+ tabs.append(Tab('users', _('Users'), reverse("catalogue_users")))
+ tabs.append(Tab('create', _('Add'), reverse("catalogue_create_missing")))
+ tabs.append(Tab('upload', _('Upload'), reverse("catalogue_upload")))
+
+ if user.is_staff:
+ tabs.append(Tab('admin', _('Admin'), reverse("admin:index")))
+
+ return {"tabs": tabs, "active_tab": active}
+
+
+class WallItem(object):
+ title = ''
+ summary = ''
+ url = ''
+ timestamp = ''
+ user = None
+ 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(max_len):
+ qs = Chunk.change_model.objects.filter(revision__gt=-1).order_by('-created_at')
+ qs = qs.defer('patch')
+ qs = qs.select_related('author', 'tree', 'tree__book__title')
+ qs = qs[:max_len]
+ for item in qs:
+ tag = 'stage' if item.tags.count() else 'change'
+ chunk = item.tree
+ w = WallItem(tag)
+ 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.email = item.author_email
+ yield w
+
+
+def published_wall(max_len):
+ qs = Book.objects.exclude(last_published=None).order_by('-last_published')
+ qs = qs[:max_len]
+ for item in qs:
+ w = WallItem('publish')
+ w.title = item.title
+ w.summary = item.title
+ w.url = chunk.book.get_absolute_url()
+ w.timestamp = item.last_published
+ w.user = item.last_published_by
+ yield w
+
+
+def comments_wall(max_len):
+ qs = Comment.objects.filter(is_public=True).select_related().order_by('-submit_date')
+ qs = qs[:max_len]
+ for item in qs:
+ w = WallItem('comment')
+ w.title = item.content_object
+ w.summary = item.comment
+ w.url = item.content_object.get_absolute_url()
+ w.timestamp = item.submit_date
+ w.user = item.user
+ w.email = item.user_email
+ yield w
+
+
+def big_wall(max_len, *args):
+ """
+ Takes some WallItem iterators and zips them into one big wall.
+ Input iterators must already be sorted by timestamp.
+ """
+ subwalls = []
+ for w in args:
+ try:
+ subwalls.append([next(w), w])
+ except StopIteration:
+ pass
+
+ 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, max_len=10):
+ return {
+ "request": context['request'],
+ "STATIC_URL": context['STATIC_URL'],
+ "wall": big_wall(max_len,
+ changes_wall(max_len),
+ published_wall(max_len),
+ comments_wall(max_len),
+ )}
diff --git a/apps/catalogue/tests.py b/apps/catalogue/tests.py
new file mode 100644
index 00000000..65777379
--- /dev/null
+++ b/apps/catalogue/tests.py
@@ -0,0 +1,19 @@
+from nose.tools import *
+import wiki.models as models
+import shutil
+import tempfile
+
+
+class TestStorageBase:
+ def setUp(self):
+ self.dirpath = tempfile.mkdtemp(prefix='nosetest_')
+
+ def tearDown(self):
+ shutil.rmtree(self.dirpath)
+
+
+class TestDocumentStorage(TestStorageBase):
+
+ def test_storage_empty(self):
+ storage = models.DocumentStorage(self.dirpath)
+ eq_(storage.all(), [])
diff --git a/apps/catalogue/urls.py b/apps/catalogue/urls.py
new file mode 100644
index 00000000..93c02b13
--- /dev/null
+++ b/apps/catalogue/urls.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8
+from django.conf.urls.defaults import *
+from django.views.generic.simple import redirect_to
+
+
+urlpatterns = patterns('catalogue.views',
+ url(r'^$', redirect_to, {'url': 'catalogue/'}),
+
+ url(r'^catalogue/$', 'document_list', name='catalogue_document_list'),
+ url(r'^unassigned/$', 'unassigned', name='catalogue_unassigned'),
+ url(r'^user/$', 'my', name='catalogue_user'),
+ url(r'^user/(?P[^/]+)/$', 'user', name='catalogue_user'),
+ url(r'^users/$', 'users', name='catalogue_users'),
+
+ url(r'^upload/$',
+ 'upload', name='catalogue_upload'),
+
+ url(r'^create/(?P[^/]*)/',
+ 'create_missing', name='catalogue_create_missing'),
+ url(r'^create/',
+ 'create_missing', name='catalogue_create_missing'),
+
+ url(r'^book/(?P[^/]+)/publish$', 'publish', name="catalogue_publish"),
+ #url(r'^(?P[^/]+)/publish/(?P\d+)$', 'publish', name="catalogue_publish"),
+
+ url(r'^book/(?P[^/]+)/$', 'book', name="catalogue_book"),
+ url(r'^book/(?P[^/]+)/xml$', 'book_xml', name="catalogue_book_xml"),
+ url(r'^book/(?P[^/]+)/txt$', 'book_txt', name="catalogue_book_txt"),
+ url(r'^book/(?P[^/]+)/html$', 'book_html', name="catalogue_book_html"),
+ #url(r'^book/(?P[^/]+)/epub$', 'book_epub', name="catalogue_book_epub"),
+ #url(r'^book/(?P[^/]+)/pdf$', 'book_pdf', name="catalogue_book_pdf"),
+ url(r'^chunk_add/(?P[^/]+)/(?P[^/]+)/$',
+ 'chunk_add', name="catalogue_chunk_add"),
+ url(r'^chunk_edit/(?P[^/]+)/(?P[^/]+)/$',
+ 'chunk_edit', name="catalogue_chunk_edit"),
+ url(r'^book_append/(?P[^/]+)/$',
+ 'book_append', name="catalogue_book_append"),
+ url(r'^book_edit/(?P[^/]+)/$',
+ 'book_edit', name="catalogue_book_edit"),
+
+)
diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py
new file mode 100644
index 00000000..a44c7e81
--- /dev/null
+++ b/apps/catalogue/views.py
@@ -0,0 +1,418 @@
+from datetime import datetime
+import logging
+import os
+from StringIO import StringIO
+
+from django.contrib import auth
+from django.contrib.auth.models import User
+from django.contrib.auth.decorators import login_required
+from django.core.urlresolvers import reverse
+from django.db.models import Count
+from django import http
+from django.http import Http404
+from django.shortcuts import get_object_or_404
+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.views.generic.simple import direct_to_template
+
+import librarian.html
+import librarian.text
+
+from apiclient import api_call
+from catalogue import forms
+from catalogue import helpers
+from catalogue.helpers import active_tab
+from catalogue.models import Book, Chunk
+from catalogue import xml_tools
+
+#
+# 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):
+ chunks_list = helpers.ChunksList(Chunk.objects.order_by(
+ 'book__title', 'book', 'number'))
+
+ return direct_to_template(request, 'catalogue/document_list.html', extra_context={
+ 'books': chunks_list,
+ #'books': [helpers.BookChunks(b) for b in Book.objects.all().select_related()],
+ 'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
+ key=lambda x: x[1]['time'], reverse=True),
+ })
+
+
+@active_tab('unassigned')
+@never_cache
+def unassigned(request):
+ chunks_list = helpers.ChunksList(Chunk.objects.filter(
+ user=None).order_by('book__title', 'book__id', 'number'))
+
+ return direct_to_template(request, 'catalogue/document_list.html', extra_context={
+ 'books': chunks_list,
+ 'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
+ key=lambda x: x[1]['time'], reverse=True),
+ })
+
+
+@never_cache
+def user(request, username=None):
+ if username is None:
+ if request.user.is_authenticated():
+ user = request.user
+ else:
+ raise Http404
+ else:
+ user = get_object_or_404(User, username=username)
+
+ chunks_list = helpers.ChunksList(Chunk.objects.filter(
+ user=user).order_by('book__title', 'book', 'number'))
+
+ return direct_to_template(request, 'catalogue/document_list.html', extra_context={
+ 'books': chunks_list,
+ 'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
+ key=lambda x: x[1]['time'], reverse=True),
+ })
+my = login_required(active_tab('my')(user))
+
+
+@active_tab('users')
+def users(request):
+ return direct_to_template(request, 'catalogue/user_list.html', extra_context={
+ 'users': User.objects.all().annotate(count=Count('chunk')).order_by(
+ '-count', 'last_name', 'first_name'),
+ })
+
+
+@never_cache
+def logout_then_redirect(request):
+ auth.logout(request)
+ return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
+
+
+@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(creator=creator,
+ slug=form.cleaned_data['slug'],
+ title=form.cleaned_data['title'],
+ text=form.cleaned_data['text'],
+ )
+
+ return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug]))
+ else:
+ form = forms.DocumentCreateForm(initial={
+ "slug": slug,
+ "title": slug.replace('-', ' ').title(),
+ })
+
+ return direct_to_template(request, "catalogue/document_create_missing.html", extra_context={
+ "slug": slug,
+ "form": form,
+ })
+
+
+@active_tab('upload')
+def upload(request):
+ if request.method == "POST":
+ form = forms.DocumentsUploadForm(request.POST, request.FILES)
+ if form.is_valid():
+ import slughifi
+
+ 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 = slughifi(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.create(creator=creator,
+ slug=slug,
+ title=title,
+ text=zip.read(filename).decode('utf-8'),
+ )
+
+ return direct_to_template(request, "catalogue/document_upload.html", extra_context={
+ "form": form,
+ "ok_list": ok_list,
+ "skipped_list": skipped_list,
+ "error_list": error_list,
+ })
+ else:
+ form = forms.DocumentsUploadForm()
+
+ return direct_to_template(request, "catalogue/document_upload.html", extra_context={
+ "form": form,
+ })
+
+
+@never_cache
+def book_xml(request, slug):
+ xml = get_object_or_404(Book, slug=slug).materialize()
+
+ response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
+ response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
+ return response
+
+
+@never_cache
+def book_txt(request, slug):
+ xml = get_object_or_404(Book, slug=slug).materialize()
+ output = StringIO()
+ # errors?
+ librarian.text.transform(StringIO(xml), output)
+ text = output.getvalue()
+ response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain')
+ response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
+ return response
+
+
+@never_cache
+def book_html(request, slug):
+ xml = get_object_or_404(Book, slug=slug).materialize()
+ output = StringIO()
+ # errors?
+ librarian.html.transform(StringIO(xml), output, parse_dublincore=False,
+ flags=['full-page'])
+ html = output.getvalue()
+ response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
+ return response
+
+
+@never_cache
+def revision(request, slug, chunk=None):
+ try:
+ doc = Chunk.get(slug, chunk)
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
+ return http.HttpResponse(str(doc.revision()))
+
+
+def book(request, slug):
+ book = get_object_or_404(Book, slug=slug)
+
+ # TODO: most of this should go somewhere else
+
+ # do we need some automation?
+ first_master = None
+ chunks = []
+ need_fixing = False
+ choose_master = False
+
+ length = book.chunk_set.count()
+ for i, chunk in enumerate(book):
+ chunk_dict = {
+ "chunk": chunk,
+ "fix": [],
+ "grade": ""
+ }
+ graded = xml_tools.GradedText(chunk.materialize())
+ if graded.is_wl():
+ master = graded.master()
+ if first_master is None:
+ first_master = master
+ elif master != first_master:
+ chunk_dict['fix'].append('bad-master')
+
+ if i > 0 and not graded.has_trim_begin():
+ chunk_dict['fix'].append('trim-begin')
+ if i < length - 1 and not graded.has_trim_end():
+ chunk_dict['fix'].append('trim-end')
+
+ if chunk_dict['fix']:
+ chunk_dict['grade'] = 'wl-fix'
+ else:
+ chunk_dict['grade'] = 'wl'
+
+ elif graded.is_broken_wl():
+ chunk_dict['grade'] = 'wl-broken'
+ elif graded.is_xml():
+ chunk_dict['grade'] = 'xml'
+ else:
+ chunk_dict['grade'] = 'plain'
+ chunk_dict['fix'].append('wl')
+ choose_master = True
+
+ if chunk_dict['fix']:
+ need_fixing = True
+ chunks.append(chunk_dict)
+
+ if first_master or not need_fixing:
+ choose_master = False
+
+ if request.method == "POST":
+ form = forms.ChooseMasterForm(request.POST)
+ if not choose_master or form.is_valid():
+ if choose_master:
+ first_master = form.cleaned_data['master']
+
+ # do the actual fixing
+ for c in chunks:
+ if not c['fix']:
+ continue
+
+ text = c['chunk'].materialize()
+ for fix in c['fix']:
+ if fix == 'bad-master':
+ text = xml_tools.change_master(text, first_master)
+ elif fix == 'trim-begin':
+ text = xml_tools.add_trim_begin(text)
+ elif fix == 'trim-end':
+ text = xml_tools.add_trim_end(text)
+ elif fix == 'wl':
+ text = xml_tools.basic_structure(text, first_master)
+ author = request.user if request.user.is_authenticated() else None
+ description = "auto-fix: " + ", ".join(c['fix'])
+ c['chunk'].commit(text=text, author=author,
+ description=description)
+
+ return http.HttpResponseRedirect(book.get_absolute_url())
+ elif choose_master:
+ form = forms.ChooseMasterForm()
+ else:
+ form = None
+
+ return direct_to_template(request, "catalogue/book_detail.html", extra_context={
+ "book": book,
+ "chunks": chunks,
+ "need_fixing": need_fixing,
+ "choose_master": choose_master,
+ "first_master": first_master,
+ "form": form,
+ })
+
+
+def chunk_add(request, slug, chunk):
+ try:
+ doc = Chunk.get(slug, chunk)
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
+
+ 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'],
+ comment=form.cleaned_data['comment'],
+ )
+
+ return http.HttpResponseRedirect(doc.book.get_absolute_url())
+ else:
+ form = forms.ChunkAddForm(initial={
+ "slug": str(doc.number + 1),
+ "comment": "cz. %d" % (doc.number + 1, ),
+ })
+
+ return direct_to_template(request, "catalogue/chunk_add.html", extra_context={
+ "chunk": doc,
+ "form": form,
+ })
+
+
+def chunk_edit(request, slug, chunk):
+ try:
+ doc = Chunk.get(slug, chunk)
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
+ if request.method == "POST":
+ form = forms.ChunkForm(request.POST, instance=doc)
+ if form.is_valid():
+ form.save()
+ return http.HttpResponseRedirect(doc.book.get_absolute_url())
+ else:
+ form = forms.ChunkForm(instance=doc)
+ return direct_to_template(request, "catalogue/chunk_edit.html", extra_context={
+ "chunk": doc,
+ "form": form,
+ })
+
+
+def book_append(request, slug):
+ book = get_object_or_404(Book, slug=slug)
+ if request.method == "POST":
+ form = forms.BookAppendForm(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()
+ return direct_to_template(request, "catalogue/book_append_to.html", extra_context={
+ "book": book,
+ "form": form,
+ })
+
+
+def book_edit(request, slug):
+ book = get_object_or_404(Book, slug=slug)
+ 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)
+ return direct_to_template(request, "catalogue/book_edit.html", extra_context={
+ "book": book,
+ "form": form,
+ })
+
+
+@require_POST
+@login_required
+def publish(request, slug):
+ book = get_object_or_404(Book, slug=slug)
+ try:
+ ret = api_call(request.user, "books", {"book_xml": book.materialize()})
+ except BaseException, e:
+ return http.HttpResponse(e)
+ else:
+ book.last_published = datetime.now()
+ book.save()
+ return http.HttpResponseRedirect(book.get_absolute_url())
diff --git a/apps/catalogue/xml_tools.py b/apps/catalogue/xml_tools.py
new file mode 100755
index 00000000..928e57be
--- /dev/null
+++ b/apps/catalogue/xml_tools.py
@@ -0,0 +1,204 @@
+from functools import wraps
+import re
+
+from lxml import etree
+from catalogue.constants import TRIM_BEGIN, TRIM_END, MASTERS
+
+RE_TRIM_BEGIN = re.compile("^$" % TRIM_BEGIN, re.M)
+RE_TRIM_END = re.compile("^$" % TRIM_END, re.M)
+
+
+class ParseError(BaseException):
+ pass
+
+
+def obj_memoized(f):
+ """
+ A decorator that caches return value of object methods.
+ The cache is kept with the object, in a _obj_memoized property.
+ """
+ @wraps(f)
+ def wrapper(self, *args, **kwargs):
+ if not hasattr(self, '_obj_memoized'):
+ self._obj_memoized = {}
+ key = (f.__name__,) + args + tuple(sorted(kwargs.iteritems()))
+ try:
+ return self._obj_memoized[key]
+ except TypeError:
+ return f(self, *args, **kwargs)
+ except KeyError:
+ self._obj_memoized[key] = f(self, *args, **kwargs)
+ return self._obj_memoized[key]
+ return wrapper
+
+
+class GradedText(object):
+ _edoc = None
+
+ ROOT = 'utwor'
+ RDF = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}RDF'
+
+ def __init__(self, text):
+ self._text = text
+
+ @obj_memoized
+ def is_xml(self):
+ """
+ Determines if it's a well-formed XML.
+
+ >>> GradedText("").is_xml()
+ True
+ >>> GradedText("").is_xml()
+ False
+ """
+ try:
+ self._edoc = etree.fromstring(self._text)
+ except etree.XMLSyntaxError:
+ return False
+ return True
+
+ @obj_memoized
+ def is_wl(self):
+ """
+ Determines if it's an XML with a and a master tag.
+
+ >>> GradedText("").is_wl()
+ True
+ >>> GradedText("").is_wl()
+ False
+ """
+ if self.is_xml():
+ e = self._edoc
+ # FIXME: there could be comments
+ ret = e.tag == self.ROOT and (
+ len(e) == 1 and e[0].tag in MASTERS or
+ len(e) == 2 and e[0].tag == self.RDF
+ and e[1].tag in MASTERS)
+ if ret:
+ self._master = e[-1].tag
+ del self._edoc
+ return ret
+ else:
+ return False
+
+ @obj_memoized
+ def is_broken_wl(self):
+ """
+ Determines if it at least looks like broken WL file
+ and not just some untagged text.
+
+ >>> GradedText("<").is_broken_wl()
+ True
+ >>> GradedText("some text").is_broken_wl()
+ False
+ """
+ if self.is_wl():
+ return True
+ text = self._text.strip()
+ return text.startswith('') and text.endswith('')
+
+ def master(self):
+ """
+ Gets the master tag.
+
+ >>> GradedText("").master()
+ 'powiesc'
+ """
+ assert self.is_wl()
+ return self._master
+
+ @obj_memoized
+ def has_trim_begin(self):
+ return RE_TRIM_BEGIN.search(self._text)
+
+ @obj_memoized
+ def has_trim_end(self):
+ return RE_TRIM_END.search(self._text)
+
+
+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
+ # 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 change_master(text, master):
+ """
+ Changes the master tag in a WL document.
+ """
+ e = etree.fromstring(text)
+ e[-1].tag = master
+ return etree.tostring(e, encoding="utf-8")
+
+
+def basic_structure(text, master):
+ e = etree.fromstring('''
+
+
+
+''' % (TRIM_BEGIN, TRIM_END))
+ e[0].tag = master
+ e[0][0].tail = "\n"*3 + text + "\n"*3
+ return etree.tostring(e, encoding="utf-8")
+
+
+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 etree.tostring(e, encoding="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 etree.tostring(e, encoding="utf-8")
diff --git a/apps/wiki/admin.py b/apps/wiki/admin.py
index b742f70d..1a61b660 100644
--- a/apps/wiki/admin.py
+++ b/apps/wiki/admin.py
@@ -2,12 +2,4 @@ from django.contrib import admin
from wiki import models
-class BookAdmin(admin.ModelAdmin):
- prepopulated_fields = {'slug': ['title']}
-
-
-admin.site.register(models.Book, BookAdmin)
-admin.site.register(models.Chunk)
admin.site.register(models.Theme)
-
-admin.site.register(models.Chunk.tag_model)
diff --git a/apps/wiki/constants.py b/apps/wiki/constants.py
deleted file mode 100644
index d75d6b4b..00000000
--- a/apps/wiki/constants.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# -*- coding: utf-8 -*-
-
-TRIM_BEGIN = " TRIM_BEGIN "
-TRIM_END = " TRIM_END "
-
-MASTERS = ['powiesc',
- 'opowiadanie',
- 'liryka_l',
- 'liryka_lp',
- 'dramat_wierszowany_l',
- 'dramat_wierszowany_lp',
- 'dramat_wspolczesny',
- ]
diff --git a/apps/wiki/fixtures/stages.json b/apps/wiki/fixtures/stages.json
deleted file mode 100644
index 992ebc71..00000000
--- a/apps/wiki/fixtures/stages.json
+++ /dev/null
@@ -1,83 +0,0 @@
-[
- {
- "pk": 1,
- "model": "wiki.chunktag",
- "fields": {
- "ordering": 1,
- "name": "Autokorekta",
- "slug": "first_correction"
- }
- },
- {
- "pk": 2,
- "model": "wiki.chunktag",
- "fields": {
- "ordering": 2,
- "name": "Tagowanie",
- "slug": "tagging"
- }
- },
- {
- "pk": 3,
- "model": "wiki.chunktag",
- "fields": {
- "ordering": 3,
- "name": "Korekta",
- "slug": "proofreading"
- }
- },
- {
- "pk": 4,
- "model": "wiki.chunktag",
- "fields": {
- "ordering": 4,
- "name": "Sprawdzenie przypis\u00f3w \u017ar\u00f3d\u0142a",
- "slug": "annotation-proofreading"
- }
- },
- {
- "pk": 5,
- "model": "wiki.chunktag",
- "fields": {
- "ordering": 5,
- "name": "Uwsp\u00f3\u0142cze\u015bnienie",
- "slug": "modernisation"
- }
- },
- {
- "pk": 6,
- "model": "wiki.chunktag",
- "fields": {
- "ordering": 6,
- "name": "Przypisy",
- "slug": "annotations"
- }
- },
- {
- "pk": 7,
- "model": "wiki.chunktag",
- "fields": {
- "ordering": 7,
- "name": "Motywy",
- "slug": "themes"
- }
- },
- {
- "pk": 8,
- "model": "wiki.chunktag",
- "fields": {
- "ordering": 8,
- "name": "Ostateczna redakcja literacka",
- "slug": "editor-proofreading"
- }
- },
- {
- "pk": 9,
- "model": "wiki.chunktag",
- "fields": {
- "ordering": 9,
- "name": "Ostateczna redakcja techniczna",
- "slug": "technical-editor-proofreading"
- }
- }
-]
diff --git a/apps/wiki/forms.py b/apps/wiki/forms.py
index 6b10909b..a8a57c88 100644
--- a/apps/wiki/forms.py
+++ b/apps/wiki/forms.py
@@ -3,22 +3,10 @@
# 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.models import Count
from django import forms
from django.utils.translation import ugettext_lazy as _
-from wiki.constants import MASTERS
-from wiki.models import Book, Chunk
-
-class DocumentTagForm(forms.Form):
- """
- Form for tagging revisions.
- """
-
- id = forms.CharField(widget=forms.HiddenInput)
- tag = forms.ModelChoiceField(queryset=Chunk.tag_model.objects.all())
- revision = forms.IntegerField(widget=forms.HiddenInput)
+from catalogue.models import Chunk
class DocumentPubmarkForm(forms.Form):
@@ -32,54 +20,6 @@ class DocumentPubmarkForm(forms.Form):
revision = forms.IntegerField(widget=forms.HiddenInput)
-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 = ['gallery', 'parent', 'parent_number']
- prepopulated_fields = {'slug': ['title']}
-
- 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"]:
- raise forms.ValidationError("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'))
-
- 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 DocumentTextSaveForm(forms.Form):
"""
Form for saving document's text:
@@ -149,70 +89,3 @@ class DocumentTextRevertForm(forms.Form):
label=_(u"Your comments"),
help_text=_(u"Describe the reason for reverting."),
)
-
-
-class ChunkForm(forms.ModelForm):
- """
- Form used for editing a chunk.
- """
- user = forms.ModelChoiceField(queryset=
- User.objects.annotate(count=Count('chunk')).
- order_by('-count', 'last_name', 'first_name'))
-
-
- class Meta:
- model = Chunk
- exclude = ['number']
-
- 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"))
-
-
-class BookForm(forms.ModelForm):
- """
- Form used for editing a Book.
- """
-
- class Meta:
- model = Book
-
-
-class ChooseMasterForm(forms.Form):
- """
- Form used for fixing the chunks in a book.
- """
-
- master = forms.ChoiceField(choices=((m, m) for m in MASTERS))
diff --git a/apps/wiki/helpers.py b/apps/wiki/helpers.py
index 3e0267ed..dace3d00 100644
--- a/apps/wiki/helpers.py
+++ b/apps/wiki/helpers.py
@@ -1,9 +1,9 @@
+from datetime import datetime
+from functools import wraps
+
from django import http
-from django.db.models import Count
from django.utils import simplejson as json
from django.utils.functional import Promise
-from datetime import datetime
-from functools import wraps
class ExtendedEncoder(json.JSONEncoder):
@@ -59,136 +59,3 @@ def ajax_require_permission(permission):
return view(request, *args, **kwargs)
return authorized_view
return decorator
-
-import collections
-
-def recursive_groupby(iterable):
- """
-# >>> recursive_groupby([1,2,3,4,5])
-# [1, 2, 3, 4, 5]
-
- >>> recursive_groupby([[1]])
- [1]
-
- >>> recursive_groupby([('a', 1),('a', 2), 3, ('b', 4), 5])
- ['a', [1, 2], 3, 'b', [4], 5]
-
- >>> recursive_groupby([('a', 'x', 1),('a', 'x', 2), ('a', 'x', 3)])
- ['a', ['x', [1, 2, 3]]]
-
- """
-
- def _generator(iterator):
- group = None
- grouper = None
-
- for item in iterator:
- if not isinstance(item, collections.Sequence):
- if grouper is not None:
- yield grouper
- if len(group):
- yield recursive_groupby(group)
- group = None
- grouper = None
- yield item
- continue
- elif len(item) == 1:
- if grouper is not None:
- yield grouper
- if len(group):
- yield recursive_groupby(group)
- group = None
- grouper = None
- yield item[0]
- continue
- elif not len(item):
- continue
-
- if grouper is None:
- group = [item[1:]]
- grouper = item[0]
- continue
-
- if grouper != item[0]:
- if grouper is not None:
- yield grouper
- if len(group):
- yield recursive_groupby(group)
- group = None
- grouper = None
- group = [item[1:]]
- grouper = item[0]
- continue
-
- group.append(item[1:])
-
- if grouper is not None:
- yield grouper
- if len(group):
- yield recursive_groupby(group)
- group = None
- grouper = None
-
- return list(_generator(iterable))
-
-
-def active_tab(tab):
- """
- View decorator, which puts tab info on a request.
- """
- def wrapper(f):
- @wraps(f)
- def wrapped(request, *args, **kwargs):
- request.wiki_active_tab = tab
- return f(request, *args, **kwargs)
- return wrapped
- return wrapper
-
-
-class ChunksList(object):
- def __init__(self, chunk_qs):
- self.chunk_qs = chunk_qs.annotate(
- book_length=Count('book__chunk')).select_related(
- 'book', 'stage__name',
- '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], chunk.book_length))
- 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, book_length):
- self.book = book
- self.chunks = chunks
- self.book_length = book_length
-
diff --git a/apps/wiki/management/__init__.py b/apps/wiki/management/__init__.py
deleted file mode 100755
index e69de29b..00000000
diff --git a/apps/wiki/management/commands/__init__.py b/apps/wiki/management/commands/__init__.py
deleted file mode 100755
index e69de29b..00000000
diff --git a/apps/wiki/management/commands/assign_from_redmine.py b/apps/wiki/management/commands/assign_from_redmine.py
deleted file mode 100755
index 45177497..00000000
--- a/apps/wiki/management/commands/assign_from_redmine.py
+++ /dev/null
@@ -1,142 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import csv
-from optparse import make_option
-import re
-import sys
-import urllib
-import urllib2
-
-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 slughifi import slughifi
-from wiki.models import Chunk
-
-
-REDMINE_CSV = 'http://redmine.nowoczesnapolska.org.pl/projects/wl-publikacje/issues.csv'
-REDAKCJA_URL = 'http://redakcja.wolnelektury.pl/documents/'
-
-
-class Command(BaseCommand):
- option_list = BaseCommand.option_list + (
- make_option('-r', '--redakcja', dest='redakcja', metavar='URL',
- help='Base URL of Redakcja documents',
- default=REDAKCJA_URL),
- make_option('-q', '--quiet', action='store_false', dest='verbose', default=True,
- help='Less output'),
- make_option('-f', '--force', action='store_true', dest='force', default=False,
- help='Force assignment overwrite'),
- )
- help = 'Imports ticket assignments from Redmine.'
- args = '[redmine-csv-url]'
-
- def handle(self, *redmine_csv, **options):
-
- self.style = color_style()
-
- redakcja = options.get('redakcja')
- verbose = options.get('verbose')
- force = options.get('force')
-
- if not redmine_csv:
- if verbose:
- print "Using default Redmine CSV URL:", REDMINE_CSV
- redmine_csv = REDMINE_CSV
-
- # Start transaction management.
- transaction.commit_unless_managed()
- transaction.enter_transaction_management()
- transaction.managed(True)
-
- redakcja_link = re.compile(re.escape(redakcja) + r'([-_.:?&%/a-zA-Z0-9]*)')
-
- all_tickets = 0
- all_chunks = 0
- done_tickets = 0
- done_chunks = 0
- empty_users = 0
- unknown_users = 0
- unknown_books = 0
- forced = 0
-
- if verbose:
- print 'Downloading CSV file'
- for r in csv.reader(urllib2.urlopen(redmine_csv)):
- if r[0] == '#':
- continue
- all_tickets += 1
-
- username = r[6]
- if not username:
- if verbose:
- print "Empty user, skipping"
- empty_users += 1
- continue
-
- first_name, last_name = unicode(username, 'utf-8').rsplit(u' ', 1)
- try:
- user = User.objects.get(first_name=first_name, last_name=last_name)
- except User.DoesNotExist:
- print self.style.ERROR('Unknown user: ' + username)
- print "'%s' '%s'" % (first_name, last_name)
- print type(last_name)
- unknown_users += 1
- continue
-
- ticket_done = False
- for fname in redakcja_link.findall(r[-1]):
- fname = unicode(urllib.unquote(fname), 'utf-8', 'ignore')
- if fname.endswith('.xml'):
- fname = fname[:-4]
- fname = fname.replace(' ', '_')
- fname = slughifi(fname)
-
- chunks = Chunk.objects.filter(book__slug=fname)
- if not chunks:
- print self.style.ERROR('Unknown book: ' + fname)
- unknown_books += 1
- continue
- all_chunks += chunks.count()
-
- for chunk in chunks:
- if chunk.user:
- if chunk.user == user:
- continue
- else:
- forced += 1
- if force:
- print self.style.WARNING(
- '%s assigned to %s, forcing change to %s.' %
- (chunk.pretty_name(), chunk.user, user))
- else:
- print self.style.WARNING(
- '%s assigned to %s not to %s, skipping.' %
- (chunk.pretty_name(), chunk.user, user))
- continue
- chunk.user = user
- chunk.save()
- ticket_done = True
- done_chunks += 1
-
- if ticket_done:
- done_tickets += 1
-
-
- # Print results
- print
- print "Results:"
- print "Done %d/%d tickets, assigned %d/%d book chunks." % (
- done_tickets, all_tickets, done_chunks, all_chunks)
- print "%d tickets unassigned, for %d chunks assignment differed." % (
- empty_users, forced)
- print "Unrecognized: %d books, %d users." % (
- unknown_books, unknown_users)
- print
-
-
- transaction.commit()
- transaction.leave_transaction_management()
-
diff --git a/apps/wiki/migrations/0003_add_dvcs.py b/apps/wiki/migrations/0003_add_dvcs.py
deleted file mode 100644
index 87ac321e..00000000
--- a/apps/wiki/migrations/0003_add_dvcs.py
+++ /dev/null
@@ -1,369 +0,0 @@
-# encoding: utf-8
-import datetime
-import os.path
-import cPickle
-import re
-import urllib
-
-from django.conf import settings
-from django.db import models
-from mercurial import mdiff, hg, ui
-from south.db import db
-from south.v2 import SchemaMigration
-
-from slughifi import slughifi
-
-META_REGEX = re.compile(r'\s*', re.DOTALL | re.MULTILINE)
-STAGE_TAGS_RE = re.compile(r'^#stage-finished: (.*)$', re.MULTILINE)
-AUTHOR_RE = re.compile(r'\s*(.*?)\s*<(.*)>\s*')
-
-
-def urlunquote(url):
- """Unqotes URL
-
- # >>> urlunquote('Za%C5%BC%C3%B3%C5%82%C4%87_g%C4%99%C5%9Bl%C4%85_ja%C5%BA%C5%84')
- # u'Za\u017c\xf3\u0142\u0107_g\u0119\u015bl\u0105 ja\u017a\u0144'
- """
- return unicode(urllib.unquote(url), 'utf-8', 'ignore')
-
-
-def split_name(name):
- parts = name.split('__')
- return parts
-
-
-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)
-
-
-def make_patch(src, dst):
- if isinstance(src, unicode):
- src = src.encode('utf-8')
- if isinstance(dst, unicode):
- dst = dst.encode('utf-8')
- return cPickle.dumps(mdiff.textdiff(src, dst))
-
-
-def plain_text(text):
- return re.sub(META_REGEX, '', text, 1)
-
-
-def gallery(slug, text):
- result = {}
-
- m = re.match(META_REGEX, text)
- if m:
- for line in m.group(1).split('\n'):
- try:
- k, v = line.split(':', 1)
- result[k.strip()] = v.strip()
- except ValueError:
- continue
-
- gallery = result.get('gallery', slughifi(slug))
-
- if gallery.startswith('/'):
- gallery = os.path.basename(gallery)
-
- return gallery
-
-
-def migrate_file_from_hg(orm, fname, entry):
- fname = urlunquote(fname)
- print fname
- if fname.endswith('.xml'):
- fname = fname[:-4]
- title = file_to_title(fname)
- fname = slughifi(fname)
- # create all the needed objects
- # what if it already exists?
- book = orm.Book.objects.create(
- title=title,
- slug=fname)
- chunk = orm.Chunk.objects.create(
- book=book,
- number=1,
- slug='1')
- head = orm['wiki.ChunkChange'].objects.create(
- tree=chunk,
- revision=-1,
- patch=make_patch('', ''),
- created_at=datetime.datetime.fromtimestamp(entry.filectx(0).date()[0]),
- description=''
- )
- chunk.head = head
- try:
- chunk.stage = orm['wiki.ChunkTag'].objects.order_by('ordering')[0]
- except IndexError:
- chunk.stage = None
- old_data = ''
-
- maxrev = entry.filerev()
- gallery_link = None
-
- for rev in xrange(maxrev + 1):
- fctx = entry.filectx(rev)
- data = fctx.data()
- gallery_link = gallery(fname, data)
- data = plain_text(data)
-
- # get tags from description
- description = fctx.description().decode("utf-8", 'replace')
- tags = STAGE_TAGS_RE.findall(description)
- tags = [orm['wiki.ChunkTag'].objects.get(slug=slug.strip()) for slug in tags]
-
- if tags:
- max_ordering = max(tags, key=lambda x: x.ordering).ordering
- try:
- chunk.stage = orm['wiki.ChunkTag'].objects.filter(ordering__gt=max_ordering).order_by('ordering')[0]
- except IndexError:
- chunk.stage = None
-
- description = STAGE_TAGS_RE.sub('', description)
-
- author = author_name = author_email = None
- author_desc = fctx.user().decode("utf-8", 'replace')
- m = AUTHOR_RE.match(author_desc)
- if m:
- try:
- author = orm['auth.User'].objects.get(username=m.group(1), email=m.group(2))
- except orm['auth.User'].DoesNotExist:
- author_name = m.group(1)
- author_email = m.group(2)
- else:
- author_name = author_desc
-
- head = orm['wiki.ChunkChange'].objects.create(
- tree=chunk,
- revision=rev + 1,
- patch=make_patch(old_data, data),
- created_at=datetime.datetime.fromtimestamp(fctx.date()[0]),
- description=description,
- author=author,
- author_name=author_name,
- author_email=author_email,
- parent=chunk.head
- )
- head.tags = tags
- chunk.head = head
- old_data = data
-
- chunk.save()
- if gallery_link:
- book.gallery = gallery_link
- book.save()
-
-
-def migrate_from_hg(orm):
- try:
- hg_path = settings.WIKI_REPOSITORY_PATH
- except:
- pass
-
- print 'migrate from', hg_path
- repo = hg.repository(ui.ui(), hg_path)
- tip = repo['tip']
- for fname in tip:
- if fname.startswith('.'):
- continue
- migrate_file_from_hg(orm, fname, tip[fname])
-
-
-class Migration(SchemaMigration):
-
- def forwards(self, orm):
-
- # Adding model 'Book'
- db.create_table('wiki_book', (
- ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
- ('title', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
- ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=128, db_index=True)),
- ('gallery', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
- ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='children', null=True, to=orm['wiki.Book'])),
- ('parent_number', self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True)),
- ('last_published', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)),
- ))
- db.send_create_signal('wiki', ['Book'])
-
- # Adding model 'Chunk'
- db.create_table('wiki_chunk', (
- ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
- ('creator', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='created_documents', null=True, to=orm['auth.User'])),
- ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
- ('book', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['wiki.Book'])),
- ('number', self.gf('django.db.models.fields.IntegerField')()),
- ('slug', self.gf('django.db.models.fields.SlugField')(max_length=50, db_index=True)),
- ('comment', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
- ('stage', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['wiki.ChunkTag'], null=True, blank=True)),
- ('head', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['wiki.ChunkChange'], null=True, blank=True)),
- ))
- db.send_create_signal('wiki', ['Chunk'])
-
- # Adding unique constraint on 'Chunk', fields ['book', 'number']
- db.create_unique('wiki_chunk', ['book_id', 'number'])
-
- # Adding unique constraint on 'Chunk', fields ['book', 'slug']
- db.create_unique('wiki_chunk', ['book_id', 'slug'])
-
- # Adding model 'ChunkTag'
- db.create_table('wiki_chunktag', (
- ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
- ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
- ('slug', self.gf('django.db.models.fields.SlugField')(db_index=True, max_length=64, unique=True, null=True, blank=True)),
- ('ordering', self.gf('django.db.models.fields.IntegerField')()),
- ))
- db.send_create_signal('wiki', ['ChunkTag'])
-
- # Adding model 'ChunkChange'
- db.create_table('wiki_chunkchange', (
- ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
- ('author', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
- ('author_name', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
- ('author_email', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
- ('patch', self.gf('django.db.models.fields.TextField')(blank=True)),
- ('revision', self.gf('django.db.models.fields.IntegerField')(db_index=True)),
- ('parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='children', null=True, blank=True, to=orm['wiki.ChunkChange'])),
- ('merge_parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='merge_children', null=True, blank=True, to=orm['wiki.ChunkChange'])),
- ('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
- ('created_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, db_index=True)),
- ('publishable', self.gf('django.db.models.fields.BooleanField')(default=False)),
- ('tree', self.gf('django.db.models.fields.related.ForeignKey')(related_name='change_set', to=orm['wiki.Chunk'])),
- ))
- db.send_create_signal('wiki', ['ChunkChange'])
-
- # Adding unique constraint on 'ChunkChange', fields ['tree', 'revision']
- db.create_unique('wiki_chunkchange', ['tree_id', 'revision'])
-
- # Adding M2M table for field tags on 'ChunkChange'
- db.create_table('wiki_chunkchange_tags', (
- ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
- ('chunkchange', models.ForeignKey(orm['wiki.chunkchange'], null=False)),
- ('chunktag', models.ForeignKey(orm['wiki.chunktag'], null=False))
- ))
- db.create_unique('wiki_chunkchange_tags', ['chunkchange_id', 'chunktag_id'])
-
- if not db.dry_run:
- from django.core.management import call_command
- call_command("loaddata", "stages.json")
-
- migrate_from_hg(orm)
-
- def backwards(self, orm):
-
- # Removing unique constraint on 'ChunkChange', fields ['tree', 'revision']
- db.delete_unique('wiki_chunkchange', ['tree_id', 'revision'])
-
- # Removing unique constraint on 'Chunk', fields ['book', 'slug']
- db.delete_unique('wiki_chunk', ['book_id', 'slug'])
-
- # Removing unique constraint on 'Chunk', fields ['book', 'number']
- db.delete_unique('wiki_chunk', ['book_id', 'number'])
-
- # Deleting model 'Book'
- db.delete_table('wiki_book')
-
- # Deleting model 'Chunk'
- db.delete_table('wiki_chunk')
-
- # Deleting model 'ChunkTag'
- db.delete_table('wiki_chunktag')
-
- # Deleting model 'ChunkChange'
- db.delete_table('wiki_chunkchange')
-
- # Removing M2M table for field tags on 'ChunkChange'
- db.delete_table('wiki_chunkchange_tags')
-
-
- models = {
- 'auth.group': {
- 'Meta': {'object_name': 'Group'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
- 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
- },
- 'auth.permission': {
- 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
- 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
- },
- 'auth.user': {
- 'Meta': {'object_name': 'User'},
- 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
- 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
- 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
- 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
- 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
- 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
- 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
- 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
- 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
- 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
- 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
- },
- 'contenttypes.contenttype': {
- 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
- 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
- },
- 'wiki.book': {
- 'Meta': {'ordering': "['parent_number', 'title']", 'object_name': 'Book'},
- 'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'last_published': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
- 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['wiki.Book']"}),
- 'parent_number': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
- 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
- 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
- },
- 'wiki.chunk': {
- 'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
- 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['wiki.Book']"}),
- 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
- 'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_documents'", 'null': 'True', 'to': "orm['auth.User']"}),
- 'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['wiki.ChunkChange']", 'null': 'True', 'blank': 'True'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'number': ('django.db.models.fields.IntegerField', [], {}),
- 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
- 'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['wiki.ChunkTag']", 'null': 'True', 'blank': 'True'}),
- 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
- },
- 'wiki.chunkchange': {
- 'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
- 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
- 'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
- 'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
- 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
- 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['wiki.ChunkChange']"}),
- 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['wiki.ChunkChange']"}),
- 'patch': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
- 'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
- 'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
- 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['wiki.ChunkTag']"}),
- 'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['wiki.Chunk']"})
- },
- 'wiki.chunktag': {
- 'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
- 'ordering': ('django.db.models.fields.IntegerField', [], {}),
- 'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
- },
- 'wiki.theme': {
- 'Meta': {'ordering': "('name',)", 'object_name': 'Theme'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'})
- }
- }
-
- complete_apps = ['wiki']
diff --git a/apps/wiki/models.py b/apps/wiki/models.py
index 20c8b869..c539908d 100644
--- a/apps/wiki/models.py
+++ b/apps/wiki/models.py
@@ -3,191 +3,13 @@
# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
-import itertools
-import re
-
-from django.core.urlresolvers import reverse
from django.db import models
-from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
-from django.template.loader import render_to_string
-
-from dvcs import models as dvcs_models
-from wiki.xml_tools import compile_text
import logging
logger = logging.getLogger("fnp.wiki")
-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)
- gallery = models.CharField(_('scan gallery name'), max_length=255, blank=True)
-
- parent = models.ForeignKey('self', null=True, blank=True, verbose_name=_('parent'), related_name="children")
- parent_number = models.IntegerField(_('parent number'), null=True, blank=True, db_index=True)
- last_published = models.DateTimeField(null=True, editable=False, db_index=True)
-
- class NoTextError(BaseException):
- pass
-
- class Meta:
- ordering = ['parent_number', 'title']
- verbose_name = _('book')
- verbose_name_plural = _('books')
-
- def __unicode__(self):
- return self.title
-
- def get_absolute_url(self):
- return reverse("wiki_book", args=[self.slug])
-
- @classmethod
- def create(cls, creator=None, text=u'', *args, **kwargs):
- """
- >>> Book.create(slug='x', text='abc').materialize()
- 'abc'
- """
- instance = cls(*args, **kwargs)
- instance.save()
- instance[0].commit(author=creator, text=text)
- return instance
-
- def __iter__(self):
- return iter(self.chunk_set.all())
-
- def __getitem__(self, chunk):
- return self.chunk_set.all()[chunk]
-
- def materialize(self, publishable=True):
- """
- Get full text of the document compiled from chunks.
- Takes the current versions of all texts
- or versions most recently tagged for publishing.
- """
- if publishable:
- changes = [chunk.publishable() for chunk in self]
- else:
- changes = [chunk.head for chunk in self]
- if None in changes:
- raise self.NoTextError('Some chunks have no available text.')
- return compile_text(change.materialize() for change in changes)
-
- def publishable(self):
- if not len(self):
- return False
- for chunk in self:
- if not chunk.publishable():
- return False
- return True
-
- 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
- while new_slug in slugs:
- new_slug = "%s-%d" % (proposed, i)
- i += 1
- return new_slug
-
- def append(self, other):
- number = self[len(self) - 1].number + 1
- single = len(other) == 1
- for chunk in other:
- # move chunk to new book
- chunk.book = self
- chunk.number = number
-
- # 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.comment = other_title_part
- if other.slug.startswith(self.slug):
- chunk_slug = other.slug[len(self.slug):].lstrip('-_')
- else:
- chunk_slug = other.slug
- chunk.slug = self.make_chunk_slug(chunk_slug)
- else:
- chunk.comment = "%s, %s" % (other_title_part, chunk.comment)
- chunk.slug = self.make_chunk_slug(chunk.slug)
- chunk.save()
- number += 1
- other.delete()
-
- @staticmethod
- def listener_create(sender, instance, created, **kwargs):
- if created:
- instance.chunk_set.create(number=1, slug='1')
-
-models.signals.post_save.connect(Book.listener_create, sender=Book)
-
-
-class Chunk(dvcs_models.Document):
- """ An editable chunk of text. Every Book text is divided into chunks. """
-
- book = models.ForeignKey(Book, editable=False)
- number = models.IntegerField()
- slug = models.SlugField()
- comment = models.CharField(max_length=255, blank=True)
-
- class Meta:
- unique_together = [['book', 'number'], ['book', 'slug']]
- ordering = ['number']
-
- def __unicode__(self):
- return "%d-%d: %s" % (self.book_id, self.number, self.comment)
-
- def get_absolute_url(self):
- return reverse("wiki_editor", args=[self.book.slug, self.slug])
-
- @classmethod
- def get(cls, slug, chunk=None):
- if chunk is None:
- return cls.objects.get(book__slug=slug, number=1)
- else:
- return cls.objects.get(book__slug=slug, slug=chunk)
-
- def pretty_name(self, book_length=None):
- title = self.book.title
- if self.comment:
- title += ", %s" % self.comment
- if book_length > 1:
- title += " (%d/%d)" % (self.number, book_length)
- return title
-
- def split(self, slug, comment='', creator=None):
- """ Create an empty chunk after this one """
- self.book.chunk_set.filter(number__gt=self.number).update(
- number=models.F('number')+1)
- new_chunk = self.book.chunk_set.create(number=self.number+1,
- creator=creator, slug=slug, comment=comment)
- return new_chunk
-
- def list_html(self):
- _list_html = render_to_string('wiki/chunk_list_item.html',
- {'chunk': self})
- return mark_safe(_list_html)
-
- @staticmethod
- def listener_saved(sender, instance, created, **kwargs):
- if instance.book:
- # save book so that its _list_html is reset
- instance.book.save()
-
-models.signals.post_save.connect(Chunk.listener_saved, sender=Chunk)
-
-
class Theme(models.Model):
name = models.CharField(_('name'), max_length=50, unique=True)
diff --git a/apps/wiki/templates/wiki/base.html b/apps/wiki/templates/wiki/base.html
deleted file mode 100644
index 83cfb7c5..00000000
--- a/apps/wiki/templates/wiki/base.html
+++ /dev/null
@@ -1,49 +0,0 @@
-{% load compressed i18n %}
-{% load wiki %}
-
-
-
-
- {% compressed_css 'listing' %}
-
- {% block title %}{% trans "Platforma Redakcyjna" %}{% endblock title %}
-
-
-
-
-
-
-
-
-
-
- {% main_tabs %}
-
-
-
- {% include "registration/head_login.html" %}
-
-
-
-
-
-
-
-{% block content %}
-
- {% block leftcolumn %}
- {% endblock leftcolumn %}
-
-
- {% block rightcolumn %}
- {% endblock rightcolumn %}
-
-{% endblock content %}
-
-
-
-{% compressed_js 'listing' %}
-{% block extrabody %}
-{% endblock %}
-
-
diff --git a/apps/wiki/templates/wiki/book_append_to.html b/apps/wiki/templates/wiki/book_append_to.html
deleted file mode 100755
index da6594da..00000000
--- a/apps/wiki/templates/wiki/book_append_to.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% extends "wiki/base.html" %}
-{% load i18n %}
-
-{% block leftcolumn %}
-
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-{% endblock rightcolumn %}
diff --git a/apps/wiki/templates/wiki/book_detail.html b/apps/wiki/templates/wiki/book_detail.html
deleted file mode 100755
index 5ea0e3df..00000000
--- a/apps/wiki/templates/wiki/book_detail.html
+++ /dev/null
@@ -1,104 +0,0 @@
-{% extends "wiki/base.html" %}
-{% load comments i18n %}
-
-{% block leftcolumn %}
-
-{% trans "edit" %}
- {{ book.title }}
-
-
- {% for c in chunks %}
-
- {{ c.chunk.comment }} |
- {% for fix in c.fix %}
-
- {% ifequal fix "wl" %}</>{% endifequal %}
-
- {% ifequal fix "bad-master" %}master{% endifequal %}
-
- {% ifequal fix "trim-begin" %}✁{% endifequal %}
-
- {% ifequal fix "trim-end" %}✃{% endifequal %}
-
- {% endfor %}
-
- {% ifequal c.grade "plain" %}
- {% trans "unstructured text" %}
- {% endifequal %}
-
- {% ifequal c.grade "xml" %}
- {% trans "unknown XML" %}
- {% endifequal %}
-
- {% ifequal c.grade "wl-broken" %}
- {% trans "broken document" %}
- {% endifequal %}
-
- |
- [{% trans "edit" %}] |
- {% if c.chunk.publishable %}P{% endif %} |
- {% if c.chunk.user.is_authenticated %}
- {{ c.chunk.user }}
- {% endif %} |
- [+] |
-
- {% endfor %}
- {% if need_fixing %}
- |
-
- |
- {% endif %}
-
-
-{% trans "Append to other book" %}
-
-{% trans "Last published" %}: {{ book.last_published }}
-
-{% if book.publishable %}
-
- {% trans "Full XML" %}
- {% trans "HTML version" %}
- {% trans "TXT version" %}
- {% comment %}
- {% trans "EPUB version" %}
- {% trans "PDF version" %}
- {% endcomment %}
-
-
-
-
-{% else %}
- {% trans "This book cannot be published yet" %}
-{% endif %}
-
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-{% render_comment_list for book %}
-{% render_comment_form for book %}
-
-{% endblock rightcolumn %}
diff --git a/apps/wiki/templates/wiki/book_edit.html b/apps/wiki/templates/wiki/book_edit.html
deleted file mode 100755
index 8bc7ea7f..00000000
--- a/apps/wiki/templates/wiki/book_edit.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% extends "wiki/base.html" %}
-{% load i18n %}
-
-{% block leftcolumn %}
-
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-{% endblock rightcolumn %}
diff --git a/apps/wiki/templates/wiki/chunk_add.html b/apps/wiki/templates/wiki/chunk_add.html
deleted file mode 100755
index 836c34d6..00000000
--- a/apps/wiki/templates/wiki/chunk_add.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% extends "wiki/base.html" %}
-{% load i18n %}
-
-{% block leftcolumn %}
-
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-{% endblock rightcolumn %}
diff --git a/apps/wiki/templates/wiki/chunk_edit.html b/apps/wiki/templates/wiki/chunk_edit.html
deleted file mode 100755
index 8bc7ea7f..00000000
--- a/apps/wiki/templates/wiki/chunk_edit.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% extends "wiki/base.html" %}
-{% load i18n %}
-
-{% block leftcolumn %}
-
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-{% endblock rightcolumn %}
diff --git a/apps/wiki/templates/wiki/document_create_missing.html b/apps/wiki/templates/wiki/document_create_missing.html
deleted file mode 100644
index 9414f7cb..00000000
--- a/apps/wiki/templates/wiki/document_create_missing.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% extends "wiki/base.html" %}
-{% load i18n %}
-
-{% block leftcolumn %}
-
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-{% endblock rightcolumn %}
diff --git a/apps/wiki/templates/wiki/document_details_base.html b/apps/wiki/templates/wiki/document_details_base.html
index 5f98b733..76711dd9 100644
--- a/apps/wiki/templates/wiki/document_details_base.html
+++ b/apps/wiki/templates/wiki/document_details_base.html
@@ -26,7 +26,7 @@