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?ds=sidebyside;hp=577bb2b84d936d65bf072ef0a6b898db9d6e77ab 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 %} +
+ {% csrf_token %} + {{ form.as_p }} + +

+
+{% 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 %} + + + + + + + + + {% endfor %} + {% if need_fixing %} + + {% endif %} +
{{ 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 %}[+]
+
+ {% csrf_token %} + {% if choose_master %} + {{ form.master }} + {% 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 %} +

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

+{% 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 %} +
+ {% csrf_token %} + {{ form.as_p }} + +

+
+{% 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 %} +
+ {% csrf_token %} + {{ form.as_p }} + +

+
+{% 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 %} +
+ {% csrf_token %} + {{ form.as_p }} + +

+
+{% 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 %} +
+ {% csrf_token %} + {{ form.as_p }} + +

+
+{% 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 %} +
+ + + + + + + + + +
Filtr:
+
+ + + +
+ + + {% autopaginate books 100 %} + {% if not books %} + + {% endif %} + {% for item in books %} + {% with item.book as book %} + + {% ifequal item.book_length 1 %} + {% with item.chunks.0 as chunk %} + + + + + + + + {% endwith %} + {% else %} + + + + + + {% for chunk in item.chunks %} + + + + + + + + {% endfor %} + {% endifequal %} + {% endwith %} + {% endfor %} + + +
{% trans "No books found." %}
[B][c] + {{ book.title }}({{ chunk.stage }}){% if chunk.user %}{{ chunk.user.first_name }} {{ chunk.user.last_name }}{% endif %}
[B]{{ book.title }}
[c] + {{ chunk.number }}. + {{ chunk.comment }}({{ chunk.stage }}){% if chunk.user %}{{ chunk.user.first_name }} {{ chunk.user.last_name }}{% endif %}
{% paginate %}
+
+{% endblock leftcolumn %} + +{% block rightcolumn %} +
+

{% trans "Your last edited documents" %}

+
    + {% for slugs, item in last_books %} +
  1. {{ item.title }}
    ({{ item.time|date:"H:i:s, d/m/Y" }})
  2. + {% 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." %} +

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

+
+ +
+ +{% 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 %} + +
    +{% for item in wall %} +
  • +
    + {% if item.get_email %} + {% gravatar_img_for_email item.get_email 32 %} +
    + {% endif %} + + +
    + + {{ item.timestamp }} +
    + {% if item.user %} + + {{ item.user.first_name }} {{ item.user.last_name }} + <{{ item.user.email }}> + {% else %} + {{ item.user_name }} + {% if item.get_email %} + <{{ item.get_email }}> + {% endif %} + {% endif %} +
    {{ item.title }} +
    {{ item.summary }} +
  • +{% endfor %} +
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 %} -
- {% csrf_token %} - {{ form.as_p }} - -

-
-{% 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 %} - - - - - - - - - {% endfor %} - {% if need_fixing %} - - {% endif %} -
{{ 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 %}[+]
-
- {% csrf_token %} - {% if choose_master %} - {{ form.master }} - {% 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 %} -

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

-{% 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 %} -
- {% csrf_token %} - {{ form.as_p }} - -

-
-{% 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 %} -
- {% csrf_token %} - {{ form.as_p }} - -

-
-{% 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 %} -
- {% csrf_token %} - {{ form.as_p }} - -

-
-{% 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 %} -
- {% csrf_token %} - {{ form.as_p }} - -

-
-{% 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 @@