From: Radek Czajka Date: Mon, 3 Oct 2011 14:41:17 +0000 (+0200) Subject: nearly working version X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/6a259b96cff47e1fd64cbfd6f3d1e1d8e8b6486c nearly working version --- diff --git a/apps/catalogue/forms.py b/apps/catalogue/forms.py index 6a56e764..f6b2dc98 100644 --- a/apps/catalogue/forms.py +++ b/apps/catalogue/forms.py @@ -44,6 +44,8 @@ class DocumentsUploadForm(forms.Form): Form used for uploading new documents. """ file = forms.FileField(required=True, label=_('ZIP file')) + dirs = forms.BooleanField(label=_('Directories are documents in chunks'), + widget = forms.CheckboxInput(attrs={'disabled':'disabled'})) def clean(self): file = self.cleaned_data['file'] diff --git a/apps/catalogue/helpers.py b/apps/catalogue/helpers.py index c9dc0bd9..7bc24819 100644 --- a/apps/catalogue/helpers.py +++ b/apps/catalogue/helpers.py @@ -16,50 +16,15 @@ def active_tab(tab): 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 - +def cached_in_field(field_name): + def decorator(f): + @property + @wraps(f) + def wrapped(self, *args, **kwargs): + value = getattr(self, field_name) + if value is None: + value = f(self, *args, **kwargs) + type(self)._default_manager.filter(pk=self.pk).update(**{field_name: value}) + return value + return wrapped + return decorator diff --git a/apps/catalogue/locale/pl/LC_MESSAGES/django.mo b/apps/catalogue/locale/pl/LC_MESSAGES/django.mo index f841945b..4eb72719 100644 Binary files a/apps/catalogue/locale/pl/LC_MESSAGES/django.mo 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 index c760f3a7..d7566e38 100644 --- a/apps/catalogue/locale/pl/LC_MESSAGES/django.po +++ b/apps/catalogue/locale/pl/LC_MESSAGES/django.po @@ -7,472 +7,534 @@ 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" +"POT-Creation-Date: 2011-10-03 15:56+0200\n" +"PO-Revision-Date: 2011-10-03 15:56+0100\n" "Last-Translator: Radek Czajka \n" "Language-Team: Fundacja Nowoczesna Polska \n" -"Language: \n" +"Language: pl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" -#: forms.py:32 -msgid "Publishable" -msgstr "Gotowe do publikacji" - -#: forms.py:68 +#: forms.py:46 msgid "ZIP file" msgstr "Plik ZIP" +#: forms.py:47 +msgid "Directories are documents in chunks" +msgstr "Katalogi zawierają dokumenty w częściach" + +#: forms.py:85 #: 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 +#: forms.py:109 msgid "Append to" msgstr "Dołącz do" -#: models.py:25 +#: views.py:137 +#, python-format +msgid "Slug already used for %s" +msgstr "Slug taki sam jak dla pliku %s" + +#: views.py:139 +msgid "Slug already used in repository." +msgstr "Dokument o tym slugu już istnieje w repozytorium." + +#: views.py:145 +msgid "File should be UTF-8 encoded." +msgstr "Plik powinien mieć kodowanie UTF-8." + +#: models/book.py:20 +#: models/chunk.py:24 msgid "title" msgstr "tytuł" -#: models.py:26 +#: models/book.py:21 +#: models/chunk.py:23 msgid "slug" -msgstr "" +msgstr "slug" -#: models.py:27 +#: models/book.py:22 msgid "scan gallery name" msgstr "nazwa galerii skanów" -#: models.py:29 +#: models/book.py:25 msgid "parent" msgstr "rodzic" -#: models.py:30 +#: models/book.py:26 msgid "parent number" msgstr "numeracja rodzica" -#: models.py:40 +#: models/book.py:43 +#: models/chunk.py:21 +#: models/publish_log.py:17 msgid "book" msgstr "książka" -#: models.py:41 +#: models/book.py:44 msgid "books" msgstr "książki" -#: models.py:206 -msgid "name" -msgstr "nazwa" +#: models/book.py:45 +msgid "Can mark for publishing" +msgstr "Oznacza do publikacji" -#: models.py:210 -msgid "theme" -msgstr "motyw" +#: models/chunk.py:22 +msgid "number" +msgstr "numer" -#: models.py:211 -msgid "themes" -msgstr "motywy" +#: models/chunk.py:39 +msgid "chunk" +msgstr "część" -#: views.py:241 -#, python-format -msgid "Slug already used for %s" -msgstr "Slug taki sam jak dla pliku %s" +#: models/chunk.py:40 +msgid "chunks" +msgstr "części" -#: views.py:243 -msgid "Slug already used in repository." -msgstr "Dokument o tym slugu już istnieje w repozytorium." +#: models/publish_log.py:18 +msgid "time" +msgstr "czas" -#: views.py:249 -msgid "File should be UTF-8 encoded." -msgstr "Plik powinien mieć kodowanie UTF-8." +#: models/publish_log.py:19 +msgid "user" +msgstr "użytkownik" -#: views.py:655 -msgid "Tag added" -msgstr "Dodano tag" +#: models/publish_log.py:24 +#: models/publish_log.py:33 +msgid "book publish record" +msgstr "zapis publikacji książki" -#: views.py:677 -msgid "Revision marked" -msgstr "Wersja oznaczona" +#: models/publish_log.py:25 +msgid "book publish records" +msgstr "zapisy publikacji książek" -#: views.py:679 -msgid "Nothing changed" -msgstr "Nic nie uległo zmianie" +#: models/publish_log.py:34 +msgid "change" +msgstr "zmiana" -#: templates/wiki/base.html:9 +#: models/publish_log.py:38 +msgid "chunk publish record" +msgstr "zapis publikacji części" + +#: models/publish_log.py:39 +msgid "chunk publish records" +msgstr "zapisy publikacji części" + +#: templates/catalogue/base.html:9 msgid "Platforma Redakcyjna" -msgstr "" +msgstr "Platforma Redakcyjna" -#: templates/wiki/book_append_to.html:8 +#: templates/catalogue/book_append_to.html:9 msgid "Append book" msgstr "Dołącz książkę" -#: templates/wiki/book_detail.html:6 -#: templates/wiki/book_detail.html.py:46 +#: templates/catalogue/book_detail.html:6 +#: templates/catalogue/book_detail.html:46 msgid "edit" msgstr "edytuj" -#: templates/wiki/book_detail.html:16 +#: templates/catalogue/book_detail.html:16 msgid "add basic document structure" msgstr "dodaj podstawową strukturę dokumentu" -#: templates/wiki/book_detail.html:20 +#: templates/catalogue/book_detail.html:20 msgid "change master tag to" msgstr "zmień tak master na" -#: templates/wiki/book_detail.html:24 +#: templates/catalogue/book_detail.html:24 msgid "add begin trimming tag" msgstr "dodaj początkowy ogranicznik" -#: templates/wiki/book_detail.html:28 +#: templates/catalogue/book_detail.html:28 msgid "add end trimming tag" msgstr "dodaj końcowy ogranicznik" -#: templates/wiki/book_detail.html:34 +#: templates/catalogue/book_detail.html:34 msgid "unstructured text" msgstr "tekst bez struktury" -#: templates/wiki/book_detail.html:38 +#: templates/catalogue/book_detail.html:38 msgid "unknown XML" msgstr "nieznany XML" -#: templates/wiki/book_detail.html:42 +#: templates/catalogue/book_detail.html:42 msgid "broken document" msgstr "uszkodzony dokument" -#: templates/wiki/book_detail.html:60 +#: templates/catalogue/book_detail.html:61 msgid "Apply fixes" msgstr "Wykonaj zmiany" -#: templates/wiki/book_detail.html:66 +#: templates/catalogue/book_detail.html:67 msgid "Append to other book" msgstr "Dołącz do innej książki" -#: templates/wiki/book_detail.html:68 +#: templates/catalogue/book_detail.html:69 msgid "Last published" msgstr "Ostatnio opublikowano" -#: templates/wiki/book_detail.html:72 +#: templates/catalogue/book_detail.html:73 msgid "Full XML" msgstr "Pełny XML" -#: templates/wiki/book_detail.html:73 +#: templates/catalogue/book_detail.html:74 msgid "HTML version" msgstr "Wersja HTML" -#: templates/wiki/book_detail.html:74 +#: templates/catalogue/book_detail.html:75 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 +#: templates/catalogue/book_detail.html:92 msgid "Publish" msgstr "Opublikuj" -#: templates/wiki/book_detail.html:94 +#: templates/catalogue/book_detail.html:96 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 +#: templates/catalogue/book_edit.html:9 +#: templates/catalogue/chunk_edit.html:9 msgid "Save" msgstr "Zapisz" -#: templates/wiki/chunk_add.html:8 +#: templates/catalogue/chunk_add.html:9 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 +#: templates/catalogue/document_create_missing.html:9 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 +#: templates/catalogue/document_upload.html:8 msgid "Bulk documents upload" msgstr "Hurtowe dodawanie dokumentów" -#: templates/wiki/document_upload.html:11 +#: templates/catalogue/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 +#: templates/catalogue/document_upload.html:17 +#: templatetags/catalogue.py:34 msgid "Upload" msgstr "Załaduj" -#: templates/wiki/document_upload.html:23 +#: templates/catalogue/document_upload.html:24 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 +#: templates/catalogue/document_upload.html:25 msgid "Offending files" msgstr "Błędne pliki" -#: templates/wiki/document_upload.html:32 +#: templates/catalogue/document_upload.html:33 msgid "Correct files" msgstr "Poprawne pliki" -#: templates/wiki/document_upload.html:43 +#: templates/catalogue/document_upload.html:44 msgid "Files have been successfully uploaded to the repository." msgstr "Pliki zostały dodane do repozytorium." -#: templates/wiki/document_upload.html:44 +#: templates/catalogue/document_upload.html:45 msgid "Uploaded files" msgstr "Dodane pliki" -#: templates/wiki/document_upload.html:54 +#: templates/catalogue/document_upload.html:55 msgid "Skipped files" msgstr "Pominięte pliki" -#: templates/wiki/document_upload.html:55 +#: templates/catalogue/document_upload.html:56 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/catalogue/my_page.html:13 +msgid "Your last edited documents" +msgstr "Twoje ostatnie edycje" -#: templates/wiki/revert_dialog.html:38 -msgid "Revert" -msgstr "Przywróć" +#: templates/catalogue/my_page.html:22 +#: templates/catalogue/user_page.html:13 +msgid "Recent activity for" +msgstr "Ostatnia aktywność dla:" -#: templates/wiki/user_list.html:7 -#: templatetags/wiki.py:33 +#: templates/catalogue/user_list.html:7 +#: templatetags/catalogue.py:32 msgid "Users" msgstr "Użytkownicy" -#: templates/wiki/tabs/annotations_view.html:9 -msgid "all" -msgstr "wszystkie" +#: templates/catalogue/book_list/book.html:6 +#: templates/catalogue/book_list/book.html:25 +msgid "Book settings" +msgstr "Ustawienia książki" -#: templates/wiki/tabs/annotations_view_item.html:3 -msgid "Annotations" -msgstr "Przypisy" +#: templates/catalogue/book_list/book.html:7 +#: templates/catalogue/book_list/chunk.html:5 +msgid "Chunk settings" +msgstr "Ustawienia części" -#: templates/wiki/tabs/gallery_view.html:7 -msgid "Previous" -msgstr "Poprzednie" +#: templates/catalogue/book_list/book_list.html:19 +msgid "Show hidden books" +msgstr "Pokaż ukryte książki" -#: templates/wiki/tabs/gallery_view.html:13 -msgid "Next" -msgstr "Następne" +#: templates/catalogue/book_list/book_list.html:24 +msgid "Search in book titles" +msgstr "Szukaj w tytułach książek" -#: templates/wiki/tabs/gallery_view.html:15 -msgid "Zoom in" -msgstr "Powiększ" +#: templates/catalogue/book_list/book_list.html:29 +msgid "stage" +msgstr "etap" -#: templates/wiki/tabs/gallery_view.html:16 -msgid "Zoom out" -msgstr "Zmniejsz" +#: templates/catalogue/book_list/book_list.html:31 +#: templates/catalogue/book_list/book_list.html:42 +msgid "none" +msgstr "brak" -#: templates/wiki/tabs/gallery_view_item.html:3 -msgid "Gallery" -msgstr "Galeria" +#: templates/catalogue/book_list/book_list.html:40 +msgid "editor" +msgstr "redaktor" -#: templates/wiki/tabs/history_view.html:5 -msgid "Compare versions" -msgstr "Porównaj wersje" +#: templates/catalogue/book_list/book_list.html:51 +msgid "status" +msgstr "status" -#: templates/wiki/tabs/history_view.html:7 -msgid "Mark for publishing" -msgstr "Oznacz do publikacji" +#: templates/catalogue/book_list/book_list.html:75 +#, python-format +msgid "%(c)s book" +msgid_plural "%(c)s books" +msgstr[0] "%(c)s książka" +msgstr[1] "%(c)s książki" +msgstr[2] "%(c)s książek" -#: templates/wiki/tabs/history_view.html:9 -msgid "Revert document" -msgstr "Przywróć wersję" +#: templates/catalogue/book_list/book_list.html:80 +msgid "No books found." +msgstr "Nie znaleziono książek." -#: templates/wiki/tabs/history_view.html:12 -msgid "View version" -msgstr "Zobacz wersję" +#: templatetags/book_list.py:81 +msgid "publishable" +msgstr "do publikacji" -#: templates/wiki/tabs/history_view_item.html:3 -msgid "History" -msgstr "Historia" +#: templatetags/book_list.py:82 +msgid "changed" +msgstr "zmienione" -#: templates/wiki/tabs/search_view.html:3 -#: templates/wiki/tabs/search_view.html:5 -msgid "Search" -msgstr "Szukaj" +#: templatetags/book_list.py:83 +msgid "published" +msgstr "opublikowane" -#: templates/wiki/tabs/search_view.html:8 -msgid "Replace with" -msgstr "Zamień na" +#: templatetags/book_list.py:84 +msgid "unpublished" +msgstr "nie opublikowane" -#: templates/wiki/tabs/search_view.html:10 -msgid "Replace" -msgstr "Zamień" +#: templatetags/book_list.py:85 +msgid "empty" +msgstr "puste" -#: templates/wiki/tabs/search_view.html:13 -msgid "Options" -msgstr "Opcje" +#: templatetags/catalogue.py:28 +msgid "My page" +msgstr "Moja strona" -#: templates/wiki/tabs/search_view.html:15 -msgid "Case sensitive" -msgstr "Rozróżniaj wielkość liter" +#: templatetags/catalogue.py:30 +msgid "Activity" +msgstr "Aktywność" -#: templates/wiki/tabs/search_view.html:17 -msgid "From cursor" -msgstr "Zacznij od kursora" +#: templatetags/catalogue.py:31 +msgid "All" +msgstr "Wszystkie" -#: templates/wiki/tabs/search_view_item.html:3 -msgid "Search and replace" -msgstr "Znajdź i zamień" +#: templatetags/catalogue.py:33 +msgid "Add" +msgstr "Dodaj" -#: templates/wiki/tabs/source_editor_item.html:5 -msgid "Source code" -msgstr "Kod źródłowy" +#: templatetags/catalogue.py:37 +msgid "Admin" +msgstr "Administracja" -#: templates/wiki/tabs/summary_view.html:9 -msgid "Title" -msgstr "Tytuł" +#: templatetags/wall.py:43 +msgid "Related edit" +msgstr "Powiązana zmiana" -#: templates/wiki/tabs/summary_view.html:14 -msgid "Document ID" -msgstr "ID dokumentu" +#: templatetags/wall.py:45 +msgid "Edit" +msgstr "Zmiana" -#: templates/wiki/tabs/summary_view.html:18 -msgid "Current version" -msgstr "Aktualna wersja" +#: templatetags/wall.py:67 +msgid "Publication" +msgstr "Publikacja" -#: templates/wiki/tabs/summary_view.html:21 -msgid "Last edited by" -msgstr "Ostatnio edytowane przez" +#: templatetags/wall.py:84 +msgid "Comment" +msgstr "Komentarz" -#: templates/wiki/tabs/summary_view.html:25 -msgid "Link to gallery" -msgstr "Link do galerii" +#~ msgid "Author" +#~ msgstr "Autor" -#: templates/wiki/tabs/summary_view_item.html:3 -msgid "Summary" -msgstr "Podsumowanie" +#~ msgid "Your name" +#~ msgstr "Imię i nazwisko" -#: templates/wiki/tabs/wysiwyg_editor.html:9 -msgid "Insert theme" -msgstr "Wstaw motyw" +#~ msgid "Author's email" +#~ msgstr "E-mail autora" -#: templates/wiki/tabs/wysiwyg_editor.html:12 -msgid "Insert annotation" -msgstr "Wstaw przypis" +#~ msgid "Your email address, so we can show a gravatar :)" +#~ msgstr "Adres e-mail, żebyśmy mogli pokazać gravatar :)" -#: templates/wiki/tabs/wysiwyg_editor_item.html:3 -msgid "Visual editor" -msgstr "Edytor wizualny" +#~ msgid "Describe changes you made." +#~ msgstr "Opisz swoje zmiany" -#: templatetags/wiki.py:30 -msgid "Assigned to me" -msgstr "Przypisane do mnie" +#~ msgid "Completed" +#~ msgstr "Ukończono" -#: templatetags/wiki.py:32 -msgid "Unassigned" -msgstr "Nie przypisane" +#~ msgid "If you completed a life cycle stage, select it." +#~ msgstr "Jeśli został ukończony etap prac, wskaż go." -#: templatetags/wiki.py:34 -msgid "All" -msgstr "Wszystkie" +#~ msgid "Describe the reason for reverting." +#~ msgstr "Opisz powód przywrócenia." -#: templatetags/wiki.py:35 -msgid "Add" -msgstr "Dodaj" +#~ msgid "name" +#~ msgstr "nazwa" -#: templatetags/wiki.py:39 -msgid "Admin" -msgstr "Administracja" +#~ msgid "theme" +#~ msgstr "motyw" + +#~ msgid "themes" +#~ msgstr "motywy" + +#~ msgid "Tag added" +#~ msgstr "Dodano tag" + +#~ msgid "Revision marked" +#~ msgstr "Wersja oznaczona" + +#~ msgid "EPUB version" +#~ msgstr "Wersja EPUB" + +#~ msgid "PDF version" +#~ msgstr "Wersja PDF" + +#~ msgid "Old version" +#~ msgstr "Stara wersja" + +#~ msgid "New version" +#~ msgstr "Nowa wersja" + +#~ msgid "Click to open/close gallery" +#~ msgstr "Kliknij, aby (ro)zwinąć galerię" + +#~ msgid "Help" +#~ msgstr "Pomoc" + +#~ msgid "Version" +#~ msgstr "Wersja" + +#~ msgid "Unknown" +#~ msgstr "nieznana" + +#~ msgid "Save attempt in progress" +#~ msgstr "Trwa zapisywanie" + +#~ msgid "There is a newer version of this document!" +#~ msgstr "Istnieje nowsza wersja tego dokumentu!" + +#~ msgid "Clear filter" +#~ msgstr "Wyczyść filtr" + +#~ msgid "Cancel" +#~ msgstr "Anuluj" + +#~ msgid "Revert" +#~ msgstr "Przywróć" + +#~ msgid "all" +#~ msgstr "wszystkie" + +#~ msgid "Annotations" +#~ msgstr "Przypisy" + +#~ msgid "Previous" +#~ msgstr "Poprzednie" + +#~ msgid "Next" +#~ msgstr "Następne" + +#~ msgid "Zoom in" +#~ msgstr "Powiększ" + +#~ msgid "Zoom out" +#~ msgstr "Zmniejsz" + +#~ msgid "Gallery" +#~ msgstr "Galeria" + +#~ msgid "Compare versions" +#~ msgstr "Porównaj wersje" + +#~ msgid "Revert document" +#~ msgstr "Przywróć wersję" + +#~ msgid "View version" +#~ msgstr "Zobacz wersję" + +#~ msgid "History" +#~ msgstr "Historia" + +#~ msgid "Search" +#~ msgstr "Szukaj" + +#~ msgid "Replace with" +#~ msgstr "Zamień na" + +#~ msgid "Replace" +#~ msgstr "Zamień" + +#~ msgid "Options" +#~ msgstr "Opcje" + +#~ msgid "Case sensitive" +#~ msgstr "Rozróżniaj wielkość liter" + +#~ msgid "From cursor" +#~ msgstr "Zacznij od kursora" + +#~ msgid "Search and replace" +#~ msgstr "Znajdź i zamień" + +#~ msgid "Source code" +#~ msgstr "Kod źródłowy" + +#~ msgid "Title" +#~ msgstr "Tytuł" + +#~ msgid "Document ID" +#~ msgstr "ID dokumentu" + +#~ msgid "Current version" +#~ msgstr "Aktualna wersja" + +#~ msgid "Last edited by" +#~ msgstr "Ostatnio edytowane przez" + +#~ msgid "Link to gallery" +#~ msgstr "Link do galerii" + +#~ msgid "Summary" +#~ msgstr "Podsumowanie" + +#~ msgid "Insert theme" +#~ msgstr "Wstaw motyw" + +#~ msgid "Insert annotation" +#~ msgstr "Wstaw przypis" + +#~ msgid "Visual editor" +#~ msgstr "Edytor wizualny" + +#~ msgid "Assigned to me" +#~ msgstr "Przypisane do mnie" + +#~ msgid "Unassigned" +#~ msgstr "Nie przypisane" #~ msgid "First correction" #~ msgstr "Autokorekta" diff --git a/apps/catalogue/management/commands/import_wl.py b/apps/catalogue/management/commands/import_wl.py index 6836d366..4f15cef8 100755 --- a/apps/catalogue/management/commands/import_wl.py +++ b/apps/catalogue/management/commands/import_wl.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from collections import defaultdict import json from optparse import make_option import urllib2 @@ -35,9 +36,9 @@ class Command(BaseCommand): transaction.managed(True) if verbose: - print 'Reading currently managed files.' - slugs = {} - for b in Book.objects.all(): + print 'Reading currently managed files (skipping hidden ones).' + slugs = defaultdict(list) + for b in Book.objects.exclude(slug__startswith='.').all(): if verbose: print b.slug text = b.materialize().encode('utf-8') @@ -46,28 +47,49 @@ class Command(BaseCommand): except (ParseError, ValidationError): pass else: - slugs[info.slug] = b + slugs[info.slug].append(b) + + #~ conflicts = [] + #~ for slug, book_list in slugs.items(): + #~ if len(book_list) > 1: + #~ conflicts.append((slug, book_list)) + #~ if conflicts: + #~ print self.style.ERROR("There is more than one book " + #~ "with the same slug in dc:url. " + #~ "Merge or hide them before proceeding.") + #~ for slug, book_list in sorted(conflicts): + #~ print slug + #~ print "\n".join(b.slug for b in book_list) + #~ print + #~ return book_count = 0 commit_args = { "author_name": 'Platforma', - "description": 'Import from WL', + "description": 'Automatycznie zaimportowane z Wolnych Lektur', + "publishable": True, } if verbose: print 'Opening books list' - for book in json.load(urllib2.urlopen(WL_API)): + for book in json.load(urllib2.urlopen(WL_API))[:10]: book_detail = json.load(urllib2.urlopen(book['href'])) xml_text = urllib2.urlopen(book_detail['xml']).read() info = BookInfo.from_string(xml_text) - previous_book = slugs.get(info.slug, None) - if previous_book: + previous_books = slugs.get(info.slug) + if previous_books: + if len(previous_books) > 1: + print self.style.ERROR("There is more than one book " + "with slug %s:"), + previous_book = previous_books[0] comm = previous_book.slug else: + previous_book = None comm = '*' print book_count, info.slug , '-->', comm Book.import_xml_text(xml_text, title=info.title, - slug=info.slug, previous_book=slugs.get(info.slug, None)) + slug=info.slug, previous_book=previous_book, + commit_args=commit_args) book_count += 1 # Print results diff --git a/apps/catalogue/management/commands/merge_books.py b/apps/catalogue/management/commands/merge_books.py index 4747591c..00014dfd 100755 --- a/apps/catalogue/management/commands/merge_books.py +++ b/apps/catalogue/management/commands/merge_books.py @@ -24,51 +24,6 @@ def common_prefix(texts): return "".join(common) -def print_guess(dry_run=True): - from collections import defaultdict - from pipes import quote - import re - - def read_slug(slug): - res = [] - res.append((re.compile(ur'__?(przedmowa)$'), -1)) - res.append((re.compile(ur'__?(cz(esc)?|ksiega|rozdzial)__?(?P\d*)$'), None)) - res.append((re.compile(ur'__?(rozdzialy__?)?(?P\d*)-'), None)) - - for r, default in res: - m = r.search(slug) - if m: - start = m.start() - try: - return int(m.group('n')), slug[:start] - except IndexError: - return default, slug[:start] - return None, slug - - def file_to_title(fname): - """ Returns a title-like version of a filename. """ - parts = (p.replace('_', ' ').title() for p in fname.split('__')) - return ' / '.join(parts) - - merges = defaultdict(list) - for b in Book.objects.all(): - n, ns = read_slug(b.slug) - if n is not None: - merges[ns].append((n, b)) - - for slug in sorted(merges.keys()): - merge_list = sorted(merges[slug]) - if len(merge_list) < 2: - continue - - title = file_to_title(slug) - print "./manage.py merge_books %s--title=%s --slug=%s \\\n %s\n" % ( - '--dry-run ' if dry_run else '', - quote(title), slug, - " \\\n ".join(b.slug for i, b in merge_list) - ) - - class Command(BaseCommand): option_list = BaseCommand.option_list + ( make_option('-s', '--slug', dest='new_slug', metavar='SLUG', @@ -81,14 +36,79 @@ class Command(BaseCommand): help='Try to guess what merges are needed (but do not apply them).'), make_option('-d', '--dry-run', action='store_true', dest='dry_run', default=False, help='Dry run: do not actually change anything.'), + make_option('-f', '--force', action='store_true', dest='force', default=False, + help='On slug conflict, hide the original book to archive.'), ) help = 'Merges multiple books into one.' args = '[slug]...' + + def print_guess(self, dry_run=True, force=False): + from collections import defaultdict + from pipes import quote + import re + + def read_slug(slug): + res = [] + res.append((re.compile(ur'__?(przedmowa)$'), -1)) + res.append((re.compile(ur'__?(cz(esc)?|ksiega|rozdzial)__?(?P\d*)$'), None)) + res.append((re.compile(ur'__?(rozdzialy__?)?(?P\d*)-'), None)) + + for r, default in res: + m = r.search(slug) + if m: + start = m.start() + try: + return int(m.group('n')), slug[:start] + except IndexError: + return default, slug[:start] + return None, slug + + def file_to_title(fname): + """ Returns a title-like version of a filename. """ + parts = (p.replace('_', ' ').title() for p in fname.split('__')) + return ' / '.join(parts) + + merges = defaultdict(list) + slugs = [] + for b in Book.objects.all(): + slugs.append(b.slug) + n, ns = read_slug(b.slug) + if n is not None: + merges[ns].append((n, b)) + + conflicting_slugs = [] + for slug in sorted(merges.keys()): + merge_list = sorted(merges[slug]) + if len(merge_list) < 2: + continue + + merge_slugs = [b.slug for i, b in merge_list] + if slug in slugs and slug not in merge_slugs: + conflicting_slugs.append(slug) + + title = file_to_title(slug) + print "./manage.py merge_books %s%s--title=%s --slug=%s \\\n %s\n" % ( + '--dry-run ' if dry_run else '', + '--force ' if force else '', + quote(title), slug, + " \\\n ".join(merge_slugs) + ) + + if conflicting_slugs: + if force: + print self.style.NOTICE('# These books will be archived:') + else: + print self.style.ERROR('# ERROR: Conflicting slugs:') + for slug in conflicting_slugs: + print '#', slug + + def handle(self, *slugs, **options): self.style = color_style() + force = options.get('force') guess = options.get('guess') dry_run = options.get('dry_run') new_slug = options.get('new_slug') @@ -100,19 +120,17 @@ class Command(BaseCommand): print "Please specify either slugs, or --guess." return else: - print_guess(dry_run) + self.print_guess(dry_run, force) return if not slugs: print "Please specify some book slugs" return - # Start transaction management. transaction.commit_unless_managed() transaction.enter_transaction_management() transaction.managed(True) - books = [Book.objects.get(slug=slug) for slug in slugs] common_slug = common_prefix(slugs) common_title = common_prefix([b.title for b in books]) @@ -127,6 +145,10 @@ class Command(BaseCommand): elif common_slug.startswith(new_slug): common_slug = new_slug + if slugs[0] != new_slug and Book.objects.filter(slug=new_slug).exists(): + self.style.ERROR('Book already exists, skipping!') + + if dry_run and verbose: print self.style.NOTICE('DRY RUN: nothing will be changed.') print @@ -161,6 +183,24 @@ class Command(BaseCommand): print if not dry_run: + try: + conflict = Book.objects.get(slug=new_slug) + except Book.DoesNotExist: + conflict = None + else: + if conflict == books[0]: + conflict = None + + if conflict: + if force: + # FIXME: there still may be a conflict + conflict.slug = '.' + conflict.slug + conflict.save() + print self.style.NOTICE('Book with slug "%s" moved to "%s".' % (new_slug, conflict.slug)) + else: + print self.style.ERROR('ERROR: Book with slug "%s" exists.' % new_slug) + return + if i: books[0].append(books[i], slugs=chunk_slugs, titles=chunk_titles) else: diff --git a/apps/catalogue/managers.py b/apps/catalogue/managers.py new file mode 100644 index 00000000..4f804b84 --- /dev/null +++ b/apps/catalogue/managers.py @@ -0,0 +1,5 @@ +from django.db import models + +class VisibleManager(models.Manager): + def get_query_set(self): + return super(VisibleManager, self).get_query_set().exclude(_hidden=True) diff --git a/apps/catalogue/migrations/0001_initial.py b/apps/catalogue/migrations/0001_initial.py index 8bd220ad..dccd9b7b 100644 --- a/apps/catalogue/migrations/0001_initial.py +++ b/apps/catalogue/migrations/0001_initial.py @@ -4,7 +4,6 @@ from south.db import db from south.v2 import SchemaMigration from django.db import models - class Migration(SchemaMigration): def forwards(self, orm): @@ -17,6 +16,10 @@ class Migration(SchemaMigration): ('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)), + ('_short_html', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('_single', self.gf('django.db.models.fields.NullBooleanField')(db_index=True, null=True, blank=True)), + ('_new_publishable', self.gf('django.db.models.fields.NullBooleanField')(null=True, blank=True)), + ('_published', self.gf('django.db.models.fields.NullBooleanField')(null=True, blank=True)), )) db.send_create_signal('catalogue', ['Book']) @@ -29,6 +32,9 @@ class Migration(SchemaMigration): ('number', self.gf('django.db.models.fields.IntegerField')()), ('slug', self.gf('django.db.models.fields.SlugField')(max_length=50, db_index=True)), ('title', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('_short_html', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('_hidden', self.gf('django.db.models.fields.NullBooleanField')(null=True, blank=True)), + ('_changed', self.gf('django.db.models.fields.NullBooleanField')(null=True, 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)), )) @@ -55,7 +61,6 @@ class Migration(SchemaMigration): ('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)), - ('data', self.gf('django.db.models.fields.files.FileField')(max_length=100)), ('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'])), @@ -63,6 +68,7 @@ class Migration(SchemaMigration): ('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'])), + ('data', self.gf('django.db.models.fields.files.FileField')(max_length=100)), )) db.send_create_signal('catalogue', ['ChunkChange']) @@ -80,7 +86,7 @@ class Migration(SchemaMigration): # Adding model 'BookPublishRecord' db.create_table('catalogue_bookpublishrecord', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('book', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Book'])), + ('book', self.gf('django.db.models.fields.related.ForeignKey')(related_name='publish_log', to=orm['catalogue.Book'])), ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), )) @@ -90,14 +96,10 @@ class Migration(SchemaMigration): db.create_table('catalogue_chunkpublishrecord', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('book_record', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.BookPublishRecord'])), - ('change', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.ChunkChange'])), + ('change', self.gf('django.db.models.fields.related.ForeignKey')(related_name='publish_log', to=orm['catalogue.ChunkChange'])), )) db.send_create_signal('catalogue', ['ChunkPublishRecord']) - if not db.dry_run: - from django.core.management import call_command - call_command("loaddata", "stages.json") - def backwards(self, orm): @@ -164,6 +166,10 @@ class Migration(SchemaMigration): }, 'catalogue.book': { 'Meta': {'ordering': "['parent_number', 'title']", 'object_name': 'Book'}, + '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), 'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}), @@ -173,21 +179,24 @@ class Migration(SchemaMigration): }, 'catalogue.bookpublishrecord': { 'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'}, - 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}), + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) }, 'catalogue.chunk': { 'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'}, + '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}), - 'title': ('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'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}) }, 'catalogue.chunkchange': { @@ -209,7 +218,7 @@ class Migration(SchemaMigration): 'catalogue.chunkpublishrecord': { 'Meta': {'object_name': 'ChunkPublishRecord'}, 'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}), - 'change': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkChange']"}), + 'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) }, 'catalogue.chunktag': { diff --git a/apps/catalogue/migrations/0002_from_hg.py b/apps/catalogue/migrations/0002_from_hg.py deleted file mode 100644 index 2a64819e..00000000 --- a/apps/catalogue/migrations/0002_from_hg.py +++ /dev/null @@ -1,273 +0,0 @@ -# encoding: utf-8 -import datetime -from zlib import compress -import os -import os.path -import re -import urllib - -from django.db import models -from mercurial import hg, ui -from south.db import db -from south.v2 import DataMigration - -from django.conf import settings -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 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') - try: - chunk.stage = orm.ChunkTag.objects.order_by('ordering')[0] - except IndexError: - chunk.stage = None - - maxrev = entry.filerev() - gallery_link = None - - # this will fail if directory exists - os.makedirs(os.path.join(settings.DVCS_REPO_PATH, str(chunk.pk))) - - 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, - created_at=datetime.datetime.fromtimestamp(fctx.date()[0]), - description=description, - author=author, - author_name=author_name, - author_email=author_email, - parent=chunk.head - ) - - path = "%d/%d" % (chunk.pk, head.pk) - abs_path = os.path.join(settings.DVCS_REPO_PATH, path) - f = open(abs_path, 'wb') - f.write(compress(data)) - f.close() - head.data = path - - head.tags = tags - head.save() - - chunk.head = head - - chunk.save() - if gallery_link: - book.gallery = gallery_link - book.save() - - -class Migration(DataMigration): - - def forwards(self, orm): - try: - hg_path = settings.WIKI_REPOSITORY_PATH - except: - print 'repository not configured, skipping' - else: - print 'migrate from', hg_path - repo = hg.repository(ui.ui(), hg_path) - tip = repo['tip'] - for fname in tip: - if fname.startswith('.') or not fname.startswith('a'): - continue - migrate_file_from_hg(orm, fname, tip[fname]) - - - def backwards(self, orm): - "Write your backwards methods here." - pass - - - 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'}), - '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.bookpublishrecord': { - 'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'}, - 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) - }, - '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']"}), - 'title': ('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'}), - 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), - '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']"}), - '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.chunkpublishrecord': { - 'Meta': {'object_name': 'ChunkPublishRecord'}, - 'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}), - 'change': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkChange']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) - }, - '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/0002_stages.py b/apps/catalogue/migrations/0002_stages.py new file mode 100644 index 00000000..71554570 --- /dev/null +++ b/apps/catalogue/migrations/0002_stages.py @@ -0,0 +1,122 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + + from django.core.management import call_command + call_command("loaddata", "stages.json") + + + def backwards(self, orm): + "Write your backwards methods here." + + + 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'}, + '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': '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.bookpublishrecord': { + 'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'}, + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'catalogue.chunk': { + 'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'}, + '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}), + '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'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', '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'}), + 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + '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']"}), + '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.chunkpublishrecord': { + 'Meta': {'object_name': 'ChunkPublishRecord'}, + 'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}), + 'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + '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/0003_from_hg.py b/apps/catalogue/migrations/0003_from_hg.py new file mode 100644 index 00000000..1816af90 --- /dev/null +++ b/apps/catalogue/migrations/0003_from_hg.py @@ -0,0 +1,280 @@ +# encoding: utf-8 +import datetime +from zlib import compress +import os +import os.path +import re +import urllib + +from django.db import models +from mercurial import hg, ui +from south.db import db +from south.v2 import DataMigration + +from django.conf import settings +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 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') + try: + chunk.stage = orm.ChunkTag.objects.order_by('ordering')[0] + except IndexError: + chunk.stage = None + + maxrev = entry.filerev() + gallery_link = None + + # this will fail if directory exists + os.makedirs(os.path.join(settings.CATALOGUE_REPO_PATH, str(chunk.pk))) + + 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, + created_at=datetime.datetime.fromtimestamp(fctx.date()[0]), + description=description, + author=author, + author_name=author_name, + author_email=author_email, + parent=chunk.head + ) + + path = "%d/%d" % (chunk.pk, head.pk) + abs_path = os.path.join(settings.CATALOGUE_REPO_PATH, path) + f = open(abs_path, 'wb') + f.write(compress(data)) + f.close() + head.data = path + + head.tags = tags + head.save() + + chunk.head = head + + chunk.save() + if gallery_link: + book.gallery = gallery_link + book.save() + + +class Migration(DataMigration): + + def forwards(self, orm): + try: + hg_path = settings.WIKI_REPOSITORY_PATH + except: + print 'repository not configured, skipping' + else: + 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]) + + + def backwards(self, orm): + "Write your backwards methods here." + pass + + + 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'}, + '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': '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.bookpublishrecord': { + 'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'}, + 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'catalogue.chunk': { + 'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'}, + '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}), + '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'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', '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'}), + 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + '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']"}), + '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.chunkpublishrecord': { + 'Meta': {'object_name': 'ChunkPublishRecord'}, + 'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}), + 'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + '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/models.py b/apps/catalogue/models.py deleted file mode 100644 index 00e1d300..00000000 --- a/apps/catalogue/models.py +++ /dev/null @@ -1,298 +0,0 @@ -# -*- 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.core.urlresolvers import reverse -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.db.utils import IntegrityError - -from slughifi import slughifi - -from dvcs import models as dvcs_models -from catalogue.xml_tools import compile_text, split_xml - -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) - - class NoTextError(BaseException): - pass - - class Meta: - ordering = ['parent_number', 'title'] - verbose_name = _('book') - verbose_name_plural = _('books') - permissions = [('can_pubmark', 'Can mark for publishing')] - - def __unicode__(self): - return self.title - - def get_absolute_url(self): - return reverse("catalogue_book", args=[self.slug]) - - @classmethod - def import_xml_text(cls, text=u'', creator=None, previous_book=None, - *args, **kwargs): - - texts = split_xml(text) - if previous_book: - instance = previous_book - else: - instance = cls(*args, **kwargs) - instance.save() - - # if there are more parts, set the rest to empty strings - book_len = len(instance) - for i in range(book_len - len(texts)): - texts.append(u'pusta część %d' % (i + 1), u'') - - i = 0 - for i, (title, text) in enumerate(texts): - if not title: - title = u'część %d' % (i + 1) - - slug = slughifi(title) - - if i < book_len: - chunk = instance[i] - chunk.slug = slug - chunk.title = title - chunk.save() - else: - chunk = instance.add(slug, title, creator, adjust_slug=True) - - chunk.commit(text, author=creator) - - return instance - - @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(text, author=creator) - return instance - - def __iter__(self): - return iter(self.chunk_set.all()) - - def __getitem__(self, chunk): - return self.chunk_set.all()[chunk] - - def __len__(self): - return self.chunk_set.count() - - def __nonzero__(self): - """ - Necessary so that __len__ isn't used for bool evaluation. - """ - return True - - def get_current_changes(self, publishable=True): - """ - Returns a list containing one Change for every Chunk in the Book. - Takes the most recent revision (publishable, if set). - Throws an error, if a proper revision is unavailable for a Chunk. - """ - if publishable: - changes = [chunk.publishable() for chunk in self] - else: - changes = [chunk.head for chunk in self if chunk.head is not None] - if None in changes: - raise self.NoTextError('Some chunks have no available text.') - return changes - - def materialize(self, publishable=False, changes=None): - """ - Get full text of the document compiled from chunks. - Takes the current versions of all texts - or versions most recently tagged for publishing, - or a specified iterable changes. - """ - if changes is None: - changes = self.get_current_changes(publishable) - return compile_text(change.materialize() for change in changes) - - def publishable(self): - if not self.chunk_set.exists(): - return False - for chunk in self: - if not chunk.publishable(): - return False - return True - - def publish(self, user): - """ - Publishes a book on behalf of a (local) user. - """ - from apiclient import api_call - - changes = self.get_current_changes(publishable=True) - book_xml = book.materialize(changes=changes) - #api_call(user, "books", {"book_xml": book_xml}) - # record the publish - br = BookPublishRecord.objects.create(book=self, user=user) - for c in changes: - ChunkPublishRecord.objects.create(book_record=br, change=c) - - 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, slugs=None, titles=None): - """Add all chunks of another book to self.""" - number = self[len(self) - 1].number + 1 - len_other = len(other) - single = len_other == 1 - - if slugs is not None: - assert len(slugs) == len_other - if titles is not None: - assert len(titles) == len_other - if slugs is None: - slugs = [slughifi(t) for t in titles] - - for i, chunk in enumerate(other): - # move chunk to new book - chunk.book = self - chunk.number = number - - if titles is None: - # try some title guessing - if other.title.startswith(self.title): - other_title_part = other.title[len(self.title):].lstrip(' /') - else: - other_title_part = other.title - - if single: - # special treatment for appending one-parters: - # just use the guessed title and original book slug - chunk.title = other_title_part - if other.slug.startswith(self.slug): - chunk_slug = other.slug[len(self.slug):].lstrip('-_') - else: - chunk_slug = other.slug - chunk.slug = self.make_chunk_slug(chunk_slug) - else: - chunk.title = "%s, %s" % (other_title_part, chunk.title) - else: - chunk.slug = slugs[i] - chunk.title = titles[i] - - chunk.slug = self.make_chunk_slug(chunk.slug) - chunk.save() - number += 1 - other.delete() - - def add(self, *args, **kwargs): - """Add a new chunk at the end.""" - return self.chunk_set.reverse()[0].split(*args, **kwargs) - - @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() - title = 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.title) - - 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.title: - title += ", %s" % self.title - if book_length > 1: - title += " (%d/%d)" % (self.number, book_length) - return title - - def split(self, slug, title='', creator=None, adjust_slug=False): - """ Create an empty chunk after this one """ - self.book.chunk_set.filter(number__gt=self.number).update( - number=models.F('number')+1) - new_chunk = None - while not new_chunk: - new_slug = self.book.make_chunk_slug(slug) - try: - new_chunk = self.book.chunk_set.create(number=self.number+1, - creator=creator, slug=new_slug, title=title) - except IntegrityError: - pass - 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) - - -class BookPublishRecord(models.Model): - """ - A record left after publishing a Book. - """ - - book = models.ForeignKey(Book) - timestamp = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey(User) - - class Meta: - ordering = ['-timestamp'] - - -class ChunkPublishRecord(models.Model): - """ - BookPublishRecord details for each Chunk. - """ - - book_record = models.ForeignKey(BookPublishRecord) - change = models.ForeignKey(Chunk.change_model) diff --git a/apps/catalogue/models/__init__.py b/apps/catalogue/models/__init__.py new file mode 100755 index 00000000..d9a11dde --- /dev/null +++ b/apps/catalogue/models/__init__.py @@ -0,0 +1,9 @@ +# -*- 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 catalogue.models.chunk import Chunk +from catalogue.models.publish_log import BookPublishRecord, ChunkPublishRecord +from catalogue.models.book import Book +from catalogue.models.listeners import * diff --git a/apps/catalogue/models/book.py b/apps/catalogue/models/book.py new file mode 100755 index 00000000..9f809b86 --- /dev/null +++ b/apps/catalogue/models/book.py @@ -0,0 +1,290 @@ +# -*- 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.db import models +from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ +from slughifi import slughifi +from catalogue.helpers import cached_in_field +from catalogue.models import BookPublishRecord, ChunkPublishRecord +from catalogue.signals import post_publish +from catalogue.tasks import refresh_instance +from catalogue.xml_tools import compile_text, split_xml + + +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) + + #wl_slug = models.CharField(_('title'), max_length=255, null=True, db_index=True, editable=False) + 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) + + # Cache + _short_html = models.TextField(null=True, blank=True, editable=False) + _single = models.NullBooleanField(editable=False, db_index=True) + _new_publishable = models.NullBooleanField(editable=False) + _published = models.NullBooleanField(editable=False) + + # Managers + objects = models.Manager() + + class NoTextError(BaseException): + pass + + class Meta: + app_label = 'catalogue' + ordering = ['parent_number', 'title'] + verbose_name = _('book') + verbose_name_plural = _('books') + permissions = [('can_pubmark', 'Can mark for publishing')] + + + # Representing + # ============ + + def __iter__(self): + return iter(self.chunk_set.all()) + + def __getitem__(self, chunk): + return self.chunk_set.all()[chunk] + + def __len__(self): + return self.chunk_set.count() + + def __nonzero__(self): + """ + Necessary so that __len__ isn't used for bool evaluation. + """ + return True + + def __unicode__(self): + return self.title + + @models.permalink + def get_absolute_url(self): + return ("catalogue_book", [self.slug]) + + + # Creating & manipulating + # ======================= + + @classmethod + def create(cls, creator, text, *args, **kwargs): + b = cls.objects.create(*args, **kwargs) + b.chunk_set.all().update(creator=creator) + b[0].commit(text, author=creator) + return b + + def add(self, *args, **kwargs): + """Add a new chunk at the end.""" + return self.chunk_set.reverse()[0].split(*args, **kwargs) + + @classmethod + def import_xml_text(cls, text=u'', previous_book=None, + commit_args=None, **kwargs): + """Imports a book from XML, splitting it into chunks as necessary.""" + texts = split_xml(text) + if previous_book: + instance = previous_book + else: + instance = cls(**kwargs) + instance.save() + + # if there are more parts, set the rest to empty strings + book_len = len(instance) + for i in range(book_len - len(texts)): + texts.append(u'pusta część %d' % (i + 1), u'') + + i = 0 + for i, (title, text) in enumerate(texts): + if not title: + title = u'część %d' % (i + 1) + + slug = slughifi(title) + + if i < book_len: + chunk = instance[i] + chunk.slug = slug + chunk.title = title + chunk.save() + else: + chunk = instance.add(slug, title, adjust_slug=True) + + chunk.commit(text, **commit_args) + + return instance + + def make_chunk_slug(self, proposed): + """ + Finds a chunk slug not yet used in the book. + """ + slugs = set(c.slug for c in self) + i = 1 + new_slug = proposed + while new_slug in slugs: + new_slug = "%s_%d" % (proposed, i) + i += 1 + return new_slug + + def append(self, other, slugs=None, titles=None): + """Add all chunks of another book to self.""" + number = self[len(self) - 1].number + 1 + len_other = len(other) + single = len_other == 1 + + if slugs is not None: + assert len(slugs) == len_other + if titles is not None: + assert len(titles) == len_other + if slugs is None: + slugs = [slughifi(t) for t in titles] + + for i, chunk in enumerate(other): + # move chunk to new book + chunk.book = self + chunk.number = number + + if titles is None: + # try some title guessing + if other.title.startswith(self.title): + other_title_part = other.title[len(self.title):].lstrip(' /') + else: + other_title_part = other.title + + if single: + # special treatment for appending one-parters: + # just use the guessed title and original book slug + chunk.title = other_title_part + if other.slug.startswith(self.slug): + chunk_slug = other.slug[len(self.slug):].lstrip('-_') + else: + chunk_slug = other.slug + chunk.slug = self.make_chunk_slug(chunk_slug) + else: + chunk.title = "%s, %s" % (other_title_part, chunk.title) + else: + chunk.slug = slugs[i] + chunk.title = titles[i] + + chunk.slug = self.make_chunk_slug(chunk.slug) + chunk.save() + number += 1 + other.delete() + + + # State & cache + # ============= + + def last_published(self): + try: + return self.publish_log.all()[0].timestamp + except IndexError: + return None + + def publishable(self): + if not self.chunk_set.exists(): + return False + for chunk in self: + if not chunk.publishable(): + return False + return True + + def hidden(self): + return self.slug.startswith('.') + + def is_new_publishable(self): + """Checks if book is ready for publishing. + + Returns True if there is a publishable version newer than the one + already published. + + """ + new_publishable = False + if not self.chunk_set.exists(): + return False + for chunk in self: + change = chunk.publishable() + if not change: + return False + if not new_publishable and not change.publish_log.exists(): + new_publishable = True + return new_publishable + new_publishable = cached_in_field('_new_publishable')(is_new_publishable) + + def is_published(self): + return self.publish_log.exists() + published = cached_in_field('_published')(is_published) + + def is_single(self): + return len(self) == 1 + single = cached_in_field('_single')(is_single) + + @cached_in_field('_short_html') + def short_html(self): + return render_to_string('catalogue/book_list/book.html', {'book': self}) + + def touch(self): + update = { + "_new_publishable": self.is_new_publishable(), + "_published": self.is_published(), + "_single": self.is_single(), + "_short_html": None, + } + Book.objects.filter(pk=self.pk).update(**update) + refresh_instance(self) + + def refresh(self): + """This should be done offline.""" + self.short_html + self.single + self.new_publishable + self.published + + # Materializing & publishing + # ========================== + + def get_current_changes(self, publishable=True): + """ + Returns a list containing one Change for every Chunk in the Book. + Takes the most recent revision (publishable, if set). + Throws an error, if a proper revision is unavailable for a Chunk. + """ + if publishable: + changes = [chunk.publishable() for chunk in self] + else: + changes = [chunk.head for chunk in self if chunk.head is not None] + if None in changes: + raise self.NoTextError('Some chunks have no available text.') + return changes + + def materialize(self, publishable=False, changes=None): + """ + Get full text of the document compiled from chunks. + Takes the current versions of all texts + or versions most recently tagged for publishing, + or a specified iterable changes. + """ + if changes is None: + changes = self.get_current_changes(publishable) + return compile_text(change.materialize() for change in changes) + + def publish(self, user): + """ + Publishes a book on behalf of a (local) user. + """ + from apiclient import api_call + + changes = self.get_current_changes(publishable=True) + book_xml = self.materialize(changes=changes) + #api_call(user, "books", {"book_xml": book_xml}) + # record the publish + br = BookPublishRecord.objects.create(book=self, user=user) + for c in changes: + ChunkPublishRecord.objects.create(book_record=br, change=c) + post_publish.send(sender=br) diff --git a/apps/catalogue/models/chunk.py b/apps/catalogue/models/chunk.py new file mode 100755 index 00000000..e68b1c17 --- /dev/null +++ b/apps/catalogue/models/chunk.py @@ -0,0 +1,123 @@ +# -*- 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.conf import settings +from django.db import models +from django.db.utils import IntegrityError +from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ +from catalogue.helpers import cached_in_field +from catalogue.managers import VisibleManager +from catalogue.tasks import refresh_instance +from dvcs import models as dvcs_models + + +class Chunk(dvcs_models.Document): + """ An editable chunk of text. Every Book text is divided into chunks. """ + REPO_PATH = settings.CATALOGUE_REPO_PATH + + book = models.ForeignKey('Book', editable=False, verbose_name=_('book')) + number = models.IntegerField(_('number')) + slug = models.SlugField(_('slug')) + title = models.CharField(_('title'), max_length=255, blank=True) + + # cache + _short_html = models.TextField(null=True, blank=True, editable=False) + _hidden = models.NullBooleanField(editable=False) + _changed = models.NullBooleanField(editable=False) + + # managers + objects = models.Manager() + visible_objects = VisibleManager() + + class Meta: + app_label = 'catalogue' + unique_together = [['book', 'number'], ['book', 'slug']] + ordering = ['number'] + verbose_name = _('chunk') + verbose_name_plural = _('chunks') + + # Representing + # ============ + + def __unicode__(self): + return "%d:%d: %s" % (self.book_id, self.number, self.title) + + @models.permalink + def get_absolute_url(self): + return ("wiki_editor", [self.book.slug, self.slug]) + + def pretty_name(self, book_length=None): + title = self.book.title + if self.title: + title += ", %s" % self.title + if book_length > 1: + title += " (%d/%d)" % (self.number, book_length) + return title + + + # Creating and manipulation + # ========================= + + def split(self, slug, title='', adjust_slug=False, **kwargs): + """ Create an empty chunk after this one """ + self.book.chunk_set.filter(number__gt=self.number).update( + number=models.F('number')+1) + new_chunk = None + while not new_chunk: + new_slug = self.book.make_chunk_slug(slug) + try: + new_chunk = self.book.chunk_set.create(number=self.number+1, + slug=new_slug, title=title, **kwargs) + except IntegrityError: + pass + return new_chunk + + @classmethod + def get(cls, book_slug, chunk_slug=None): + if chunk_slug is None: + return cls.objects.get(book__slug=book_slug, number=1) + else: + return cls.objects.get(book__slug=book_slug, slug=chunk_slug) + + + # State & cache + # ============= + + def new_publishable(self): + change = self.publishable() + if not change: + return False + return change.publish_log.exists() + + def is_changed(self): + if self.head is None: + return False + return not self.head.publishable + changed = cached_in_field('_changed')(is_changed) + + def is_hidden(self): + return self.book.hidden() + hidden = cached_in_field('_hidden')(is_hidden) + + @cached_in_field('_short_html') + def short_html(self): + return render_to_string( + 'catalogue/book_list/chunk.html', {'chunk': self}) + + def touch(self): + update = { + "_changed": self.is_changed(), + "_hidden": self.is_hidden(), + "_short_html": None, + } + Chunk.objects.filter(pk=self.pk).update(**update) + refresh_instance(self) + + def refresh(self): + """This should be done offline.""" + self.changed + self.hidden + self.short_html diff --git a/apps/catalogue/models/listeners.py b/apps/catalogue/models/listeners.py new file mode 100755 index 00000000..7848974f --- /dev/null +++ b/apps/catalogue/models/listeners.py @@ -0,0 +1,48 @@ +# -*- 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 import models +from catalogue.models import Book, Chunk +from catalogue.signals import post_publish +from dvcs.signals import post_publishable + + +def book_changed(sender, instance, created, **kwargs): + instance.touch() + for c in instance: + c.touch() +models.signals.post_save.connect(book_changed, sender=Book) + + +def chunk_changed(sender, instance, created, **kwargs): + instance.book.touch() + instance.touch() +models.signals.post_save.connect(chunk_changed, sender=Chunk) + + +def user_changed(sender, instance, *args, **kwargs): + books = set() + for c in instance.chunk_set.all(): + books.add(c.book) + c.touch() + for b in books: + b.touch() +models.signals.post_save.connect(user_changed, sender=User) + + +def publish_listener(sender, *args, **kwargs): + sender.book.touch() + for c in sender.book: + c.touch() +post_publish.connect(publish_listener) + + +def listener_create(sender, instance, created, **kwargs): + if created: + instance.chunk_set.create(number=1, slug='1') +models.signals.post_save.connect(listener_create, sender=Book) + + diff --git a/apps/catalogue/models/publish_log.py b/apps/catalogue/models/publish_log.py new file mode 100755 index 00000000..f422e377 --- /dev/null +++ b/apps/catalogue/models/publish_log.py @@ -0,0 +1,39 @@ +# -*- 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 import models +from django.utils.translation import ugettext_lazy as _ +from catalogue.models import Chunk + + +class BookPublishRecord(models.Model): + """ + A record left after publishing a Book. + """ + + book = models.ForeignKey('Book', verbose_name=_('book'), related_name='publish_log') + timestamp = models.DateTimeField(_('time'), auto_now_add=True) + user = models.ForeignKey(User, verbose_name=_('user')) + + class Meta: + app_label = 'catalogue' + ordering = ['-timestamp'] + verbose_name = _('book publish record') + verbose_name = _('book publish records') + + +class ChunkPublishRecord(models.Model): + """ + BookPublishRecord details for each Chunk. + """ + + book_record = models.ForeignKey(BookPublishRecord, verbose_name=_('book publish record')) + change = models.ForeignKey(Chunk.change_model, related_name='publish_log', verbose_name=_('change')) + + class Meta: + app_label = 'catalogue' + verbose_name = _('chunk publish record') + verbose_name = _('chunk publish records') diff --git a/apps/catalogue/signals.py b/apps/catalogue/signals.py new file mode 100644 index 00000000..62ca5145 --- /dev/null +++ b/apps/catalogue/signals.py @@ -0,0 +1,3 @@ +from django.dispatch import Signal + +post_publish = Signal() diff --git a/apps/catalogue/tasks.py b/apps/catalogue/tasks.py new file mode 100644 index 00000000..e9b8cf9b --- /dev/null +++ b/apps/catalogue/tasks.py @@ -0,0 +1,11 @@ +from celery.task import task + + +@task +def refresh_by_pk(cls, pk): + cls._default_manager.get(pk=pk).refresh() + + +def refresh_instance(instance): + refresh_by_pk.delay(type(instance), instance.pk) + diff --git a/apps/catalogue/templates/catalogue/activity.html b/apps/catalogue/templates/catalogue/activity.html new file mode 100755 index 00000000..4354b337 --- /dev/null +++ b/apps/catalogue/templates/catalogue/activity.html @@ -0,0 +1,7 @@ +{% extends "catalogue/base.html" %} + +{% load wall %} + +{% block leftcolumn %} + {% wall %} +{% endblock leftcolumn %} diff --git a/apps/catalogue/templates/catalogue/base.html b/apps/catalogue/templates/catalogue/base.html index 4ba8a0ea..9b32fe7e 100644 --- a/apps/catalogue/templates/catalogue/base.html +++ b/apps/catalogue/templates/catalogue/base.html @@ -4,7 +4,7 @@ - {% compressed_css 'listing' %} + {% compressed_css 'catalogue' %} {% block title %}{% trans "Platforma Redakcyjna" %}{% endblock title %} @@ -42,7 +42,9 @@ -{% compressed_js 'listing' %} + + +{% compressed_js 'catalogue' %} {% block extrabody %} {% endblock %} diff --git a/apps/catalogue/templates/catalogue/book_list/book.html b/apps/catalogue/templates/catalogue/book_list/book.html new file mode 100755 index 00000000..a45a3578 --- /dev/null +++ b/apps/catalogue/templates/catalogue/book_list/book.html @@ -0,0 +1,34 @@ +{% load i18n %} + +{% if book.single %} + {% with book.0 as chunk %} + + [B] + [c] + + {{ book.title }} + {% if chunk.stage %} + {{ chunk.stage }} + {% else %}– + {% endif %} + {% if chunk.user %}{{ chunk.user.first_name }} {{ chunk.user.last_name }}{% endif %} + + {% if chunk.published %}P{% endif %} + {% if book.new_publishable %}p{% endif %} + {% if chunk.changed %}+{% endif %} + + + {% endwith %} +{% else %} + + [B] + + {{ book.title }} + + + {% if book.published %}P{% endif %} + {% if book.new_publishable %}p{% endif %} + + +{% endif %} diff --git a/apps/catalogue/templates/catalogue/book_list/book_list.html b/apps/catalogue/templates/catalogue/book_list/book_list.html new file mode 100755 index 00000000..73811cab --- /dev/null +++ b/apps/catalogue/templates/catalogue/book_list/book_list.html @@ -0,0 +1,81 @@ +{% load i18n %} +{% load pagination_tags %} + + +
+ + +{% if not viewed_user %} + +{% endif %} + + +
+ + + + + + + + + {% if not viewed_user %} + + {% endif %} + + + + + + {% with cnt=books|length %} + {% autopaginate books 100 %} + + {% for item in books %} + {% with item.book as book %} + {{ book.short_html|safe }} + {% if not book.single %} + {% for chunk in item.chunks %} + {{ chunk.short_html|safe }} + {% endfor %} + {% endif %} + {% endwith %} + {% endfor %} + + + {% endwith %} +
+ + +
+ +
+
+ {% paginate %} + {% blocktrans count c=cnt %}{{c}} book{% plural %}{{c}} books{% endblocktrans %}
+{% if not books %} +

{% trans "No books found." %}

+{% endif %} diff --git a/apps/catalogue/templates/catalogue/book_list/chunk.html b/apps/catalogue/templates/catalogue/book_list/chunk.html new file mode 100755 index 00000000..3897d78b --- /dev/null +++ b/apps/catalogue/templates/catalogue/book_list/chunk.html @@ -0,0 +1,25 @@ +{% load i18n %} + + + + [c] + + {{ chunk.number }}. + {{ chunk.title }} + {% if chunk.stage %} + {{ chunk.stage }} + {% else %} + – + {% endif %} + {% if chunk.user %} + + {{ chunk.user.first_name }} {{ chunk.user.last_name }} + {% else %} + + {% endif %} + + + {% if chunk.new_publishable %}p{% endif %} + {% if chunk.changed %}+{% endif %} + + diff --git a/apps/catalogue/templates/catalogue/document_list.html b/apps/catalogue/templates/catalogue/document_list.html index 01e5a24c..d5343a7d 100644 --- a/apps/catalogue/templates/catalogue/document_list.html +++ b/apps/catalogue/templates/catalogue/document_list.html @@ -1,147 +1,9 @@ {% extends "catalogue/base.html" %} {% load i18n %} -{% load pagination_tags %} -{% load catalogue %} +{% load catalogue book_list %} -{% block extrabody %} -{{ block.super }} - -{% endblock %} {% block leftcolumn %} - -
- - - -
- - - - - - - - - {% if not viewed_user %} - - {% endif %} - - - - - {% 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 %} - - - - - - {% if not viewed_user %} - - {% endif %} - - {% endfor %} - {% endifequal %} - {% endwith %} - {% endfor %} - - -
-
- -
-
{% trans "No books found." %}
[B][c] - {{ book.title }}{% if chunk.stage %} - ({{ chunk.stage }}) - {% else %}– - {% endif %}{% if chunk.user %}{{ chunk.user.first_name }} {{ chunk.user.last_name }}{% endif %}
[B]{{ book.title }}
[c] - {{ chunk.number }}. - {{ chunk.title }}{% if chunk.stage %} - {{ chunk.stage }} - {% else %} - – - {% endif %}{% if chunk.user %} - - {{ chunk.user.first_name }} {{ chunk.user.last_name }} - {% else %} - - {% endif %}
{% paginate %}
+ {% book_list %} {% 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 %} -
-
- - {% if viewed_user %} -

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

- {% wall viewed_user %} - {% else %} -

{% trans "Recent activity" %}

- {% wall %} - {% endif %} -{% endblock rightcolumn %} diff --git a/apps/catalogue/templates/catalogue/my_page.html b/apps/catalogue/templates/catalogue/my_page.html new file mode 100755 index 00000000..48a21796 --- /dev/null +++ b/apps/catalogue/templates/catalogue/my_page.html @@ -0,0 +1,24 @@ +{% extends "catalogue/base.html" %} + +{% load i18n %} +{% load catalogue book_list wall %} + + +{% block leftcolumn %} + {% book_list request.user %} +{% 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 for" %} {{ request.user|nice_name }}

+ {% wall request.user 10 %} +{% endblock rightcolumn %} diff --git a/apps/catalogue/templates/catalogue/user_page.html b/apps/catalogue/templates/catalogue/user_page.html new file mode 100755 index 00000000..89b4ecef --- /dev/null +++ b/apps/catalogue/templates/catalogue/user_page.html @@ -0,0 +1,15 @@ +{% extends "catalogue/base.html" %} + +{% load i18n %} +{% load catalogue book_list wall %} + + +{% block leftcolumn %} +

{{ viewed_user|nice_name }}

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

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

+ {% wall viewed_user 10 %} +{% endblock rightcolumn %} diff --git a/apps/catalogue/templates/catalogue/wall.html b/apps/catalogue/templates/catalogue/wall.html index 0e4597e5..2ec3c0ac 100755 --- a/apps/catalogue/templates/catalogue/wall.html +++ b/apps/catalogue/templates/catalogue/wall.html @@ -13,8 +13,8 @@ - {{ item.timestamp }} -
+ {{ item.timestamp }} +

{{ item.header }}

{% if item.user %} {{ item.user.first_name }} {{ item.user.last_name }} diff --git a/apps/catalogue/templatetags/book_list.py b/apps/catalogue/templatetags/book_list.py new file mode 100755 index 00000000..15654be2 --- /dev/null +++ b/apps/catalogue/templatetags/book_list.py @@ -0,0 +1,134 @@ +from __future__ import absolute_import + +from re import split +from django.db.models import Q, Count +from django import template +from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.models import User +from catalogue.models import Chunk + +register = template.Library() + + +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.chunk_qs = chunk_qs.select_related('book__hidden') + + self.book_qs = chunk_qs.values('book_id') + + def __getitem__(self, key): + if isinstance(key, slice): + return self.get_slice(key) + elif isinstance(key, int): + return self.get_slice(slice(key, key+1))[0] + else: + raise TypeError('Unsupported list index. Must be a slice or an int.') + + def __len__(self): + return self.book_qs.count() + + def get_slice(self, slice_): + book_ids = [x['book_id'] for x in self.book_qs[slice_]] + chunk_qs = self.chunk_qs.filter(book__in=book_ids) + + chunks_list = [] + book = None + for chunk in chunk_qs: + if chunk.book != book: + book = chunk.book + chunks_list.append(ChoiceChunks(book, [chunk])) + else: + chunks_list[-1].chunks.append(chunk) + return chunks_list + + +class ChoiceChunks(object): + """ + Associates the given chunks iterable for a book. + """ + + chunks = None + + def __init__(self, book, chunks): + self.book = book + self.chunks = chunks + + +def foreign_filter(qs, value, filter_field, model, model_field='slug', unset='-'): + if value == unset: + return qs.filter(**{filter_field: None}) + if not value: + return qs + try: + obj = model._default_manager.get(**{model_field: value}) + except model.DoesNotExist: + return qs.none() + else: + return qs.filter(**{filter_field: obj}) + + +def search_filter(qs, value, filter_field): + if not value: + return qs + return qs.filter(**{"%s__icontains" % filter_field: value}) + + +_states = [ + ('publishable', _('publishable'), Q(book___new_publishable=True)), + ('changed', _('changed'), Q(_changed=True)), + ('published', _('published'), Q(book___published=True)), + ('unpublished', _('unpublished'), Q(book___published=False)), + ('empty', _('empty'), Q(head=None)), + ] +_states_options = [s[:2] for s in _states] +_states_dict = dict([(s[0], s[2]) for s in _states]) + + +def document_list_filter(request, **kwargs): + + def arg_or_GET(field): + return kwargs.get(field, request.GET.get(field)) + + if arg_or_GET('all'): + chunks = Chunk.objects.all() + else: + chunks = Chunk.visible_objects.all() + + chunks = chunks.order_by('book__title', 'book', 'number') + + state = arg_or_GET('status') + if state in _states_dict: + chunks = chunks.filter(_states_dict[state]) + + chunks = foreign_filter(chunks, arg_or_GET('user'), 'user', User, 'username') + chunks = foreign_filter(chunks, arg_or_GET('stage'), 'stage', Chunk.tag_model, 'slug') + chunks = search_filter(chunks, arg_or_GET('title'), 'book__title') + return chunks + + +@register.inclusion_tag('catalogue/book_list/book_list.html', takes_context=True) +def book_list(context, user=None): + request = context['request'] + + if user: + filters = {"user": user} + new_context = {"viewed_user": user} + else: + filters = {} + new_context = {"users": User.objects.annotate( + count=Count('chunk')).filter(count__gt=0).order_by( + '-count', 'last_name', 'first_name')} + + new_context.update({ + "request": request, + "books": ChunksList(document_list_filter(request, **filters)), + "stages": Chunk.tag_model.objects.all(), + "states": _states_options, + }) + + return new_context + diff --git a/apps/catalogue/templatetags/catalogue.py b/apps/catalogue/templatetags/catalogue.py index 7d138ffa..bfb900bf 100644 --- a/apps/catalogue/templatetags/catalogue.py +++ b/apps/catalogue/templatetags/catalogue.py @@ -1,14 +1,9 @@ from __future__ import absolute_import -from django.db.models import Count, Q 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, BookPublishRecord - register = template.Library() @@ -32,6 +27,7 @@ def main_tabs(context): if user.is_authenticated(): tabs.append(Tab('my', _('My page'), reverse("catalogue_user"))) + tabs.append(Tab('activity', _('Activity'), reverse("catalogue_activity"))) tabs.append(Tab('all', _('All'), reverse("catalogue_document_list"))) tabs.append(Tab('users', _('Users'), reverse("catalogue_users"))) tabs.append(Tab('create', _('Add'), reverse("catalogue_create_missing"))) @@ -43,108 +39,7 @@ def main_tabs(context): 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(user, max_len): - qs = Chunk.change_model.objects.filter(revision__gt=-1).order_by('-created_at') - qs = qs.select_related('author', 'tree', 'tree__book__title') - if user: - qs = qs.filter(Q(author=user) | Q(tree__user=user)) - 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 - - -# TODO: marked for publishing - - -def published_wall(user, max_len): - qs = BookPublishRecord.objects.select_related('book__title') - if user: - # TODO: published my book - qs = qs.filter(Q(user=user)) - qs = qs[:max_len] - for item in qs: - w = WallItem('publish') - w.title = item.book.title - #w.summary = - w.url = chunk.book.get_absolute_url() - yield w - - -def comments_wall(user, max_len): - qs = Comment.objects.filter(is_public=True).select_related().order_by('-submit_date') - if user: - # TODO: comments concerning my books - qs = qs.filter(Q(user=user)) - 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.filter +def nice_name(user): + return user.get_full_name() or user.username -@register.inclusion_tag("catalogue/wall.html", takes_context=True) -def wall(context, user=None, max_len=10): - print user - return { - "request": context['request'], - "STATIC_URL": context['STATIC_URL'], - "wall": big_wall(max_len, - changes_wall(user, max_len), - published_wall(user, max_len), - comments_wall(user, max_len), - )} diff --git a/apps/catalogue/templatetags/wall.py b/apps/catalogue/templatetags/wall.py new file mode 100755 index 00000000..5236eedf --- /dev/null +++ b/apps/catalogue/templatetags/wall.py @@ -0,0 +1,125 @@ +from __future__ import absolute_import + +from django.db.models import Q +from django.core.urlresolvers import reverse +from django.contrib.comments.models import Comment +from django import template +from django.utils.translation import ugettext as _ + +from catalogue.models import Chunk, BookPublishRecord + +register = template.Library() + + +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(user, max_len): + qs = Chunk.change_model.objects.filter(revision__gt=-1).order_by('-created_at') + qs = qs.select_related('author', 'tree', 'tree__book__title') + if user: + qs = qs.filter(Q(author=user) | Q(tree__user=user)) + qs = qs[:max_len] + for item in qs: + tag = 'stage' if item.tags.count() else 'change' + chunk = item.tree + w = WallItem(tag) + if user and item.author != user: + w.header = _('Related edit') + else: + w.header = _('Edit') + w.title = chunk.pretty_name() + w.summary = item.description + w.url = reverse('wiki_editor', + args=[chunk.book.slug, chunk.slug]) + '?diff=%d' % item.revision + w.timestamp = item.created_at + w.user = item.author + w.email = item.author_email + yield w + + +# TODO: marked for publishing + + +def published_wall(user, max_len): + qs = BookPublishRecord.objects.select_related('book__title') + if user: + # TODO: published my book + qs = qs.filter(Q(user=user)) + qs = qs[:max_len] + for item in qs: + w = WallItem('publish') + w.header = _('Publication') + w.title = item.book.title + w.timestamp = item.timestamp + w.url = item.book.get_absolute_url() + w.user = item.user + w.email = item.user.email + yield w + + +def comments_wall(user, max_len): + qs = Comment.objects.filter(is_public=True).select_related().order_by('-submit_date') + if user: + # TODO: comments concerning my books + qs = qs.filter(Q(user=user)) + qs = qs[:max_len] + for item in qs: + w = WallItem('comment') + w.header = _('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, user=None, max_len=100): + return { + "request": context['request'], + "STATIC_URL": context['STATIC_URL'], + "wall": big_wall(max_len, + changes_wall(user, max_len), + published_wall(user, max_len), + comments_wall(user, max_len), + )} diff --git a/apps/catalogue/tests.py b/apps/catalogue/tests.py deleted file mode 100644 index 65777379..00000000 --- a/apps/catalogue/tests.py +++ /dev/null @@ -1,19 +0,0 @@ -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 index e9e5e893..73fd3ee6 100644 --- a/apps/catalogue/urls.py +++ b/apps/catalogue/urls.py @@ -10,6 +10,7 @@ urlpatterns = patterns('catalogue.views', url(r'^user/$', 'my', name='catalogue_user'), url(r'^user/(?P[^/]+)/$', 'user', name='catalogue_user'), url(r'^users/$', 'users', name='catalogue_users'), + url(r'^activity/$', 'activity', name='catalogue_activity'), url(r'^upload/$', 'upload', name='catalogue_upload'), diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py index 81de5ac7..aa214de7 100644 --- a/apps/catalogue/views.py +++ b/apps/catalogue/views.py @@ -7,10 +7,10 @@ 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.db.models import Count, Q from django import http from django.http import Http404 -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, render from django.utils.http import urlquote_plus from django.utils.translation import ugettext_lazy as _ from django.views.decorators.http import require_POST @@ -33,70 +33,26 @@ from django.views.decorators.cache import never_cache logger = logging.getLogger("fnp.catalogue") -def foreign_filter(qs, value, filter_field, model, model_field='slug', unset='-'): - if value == unset: - return qs.filter(**{filter_field: None}) - if not value: - return qs - try: - obj = model._default_manager.get(**{model_field: value}) - except model.DoesNotExist: - return qs.none() - else: - return qs.filter(**{filter_field: obj}) - - -def search_filter(qs, value, filter_field): - if not value: - return qs - return qs.filter(**{"%s__icontains" % filter_field: value}) - - @active_tab('all') @never_cache -def document_list(request, filters=None): - chunks = Chunk.objects.order_by('book__title', 'book', 'number') - - chunks = foreign_filter(chunks, request.GET.get('user', None), 'user', User, 'username') - chunks = foreign_filter(chunks, request.GET.get('stage', None), 'stage', Chunk.tag_model, 'slug') - chunks = search_filter(chunks, request.GET.get('title', None), 'book__title') - - chunks_list = helpers.ChunksList(chunks) - - users = User.objects.annotate(count=Count('chunk')).filter(count__gt=0).order_by('-count', 'last_name', 'first_name') - #users = User.objects.annotate(count=Count('chunk')).order_by('-count', 'last_name', 'first_name') - - - 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), - 'stages': Chunk.tag_model.objects.all(), - 'users': users, - }) +def document_list(request): + return render(request, 'catalogue/document_list.html') @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) +def user(request, username): + user = get_object_or_404(User, username=username) + return render(request, 'catalogue/user_page.html', {"viewed_user": user}) - 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, +@login_required +@active_tab('my') +@never_cache +def my(request): + return render(request, 'catalogue/my_page.html', { 'last_books': sorted(request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True), - 'viewed_user': user, - 'stages': Chunk.tag_model.objects.all(), - }) -my = login_required(active_tab('my')(user)) + }) @active_tab('users') @@ -107,6 +63,11 @@ def users(request): }) +@active_tab('activity') +def activity(request): + return render(request, 'catalogue/activity.html') + + @never_cache def logout_then_redirect(request): auth.logout(request) @@ -127,11 +88,12 @@ def create_missing(request, slug=None): creator = request.user else: creator = None - book = Book.create(creator=creator, + book = Book.objects.create( slug=form.cleaned_data['slug'], title=form.cleaned_data['title'], - text=form.cleaned_data['text'], ) + book.chunk_set.all().update(creator=creator) + book[0].commit(text=form.cleaned_data['text'], author=creator) return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug])) else: diff --git a/apps/catalogue/xml_tools.py b/apps/catalogue/xml_tools.py old mode 100755 new mode 100644 index 522806b6..d6a9333b --- a/apps/catalogue/xml_tools.py +++ b/apps/catalogue/xml_tools.py @@ -230,7 +230,7 @@ def split_xml(text): for a in name_elem.findall('.//' + tag): a.text='' del a[:] - name = etree.tostring(name_elem, method='text', encoding='utf-8') + name = etree.tostring(name_elem, method='text', encoding='utf-8').strip() # in the original, remove everything from the start of the last chapter parent = element.getparent() diff --git a/apps/dvcs/locale/pl/LC_MESSAGES/django.mo b/apps/dvcs/locale/pl/LC_MESSAGES/django.mo new file mode 100644 index 00000000..4c3a1ffc Binary files /dev/null and b/apps/dvcs/locale/pl/LC_MESSAGES/django.mo differ diff --git a/apps/dvcs/locale/pl/LC_MESSAGES/django.po b/apps/dvcs/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 00000000..64ddfd78 --- /dev/null +++ b/apps/dvcs/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,115 @@ +# 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: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-10-03 15:35+0200\n" +"PO-Revision-Date: 2011-10-03 15:35+0100\n" +"Last-Translator: Radek Czajka \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2)\n" + +#: models.py:19 +msgid "name" +msgstr "nazwa" + +#: models.py:20 +msgid "slug" +msgstr "slug" + +#: models.py:22 +msgid "ordering" +msgstr "kolejność" + +#: models.py:29 +msgid "tag" +msgstr "tag" + +#: models.py:30 models.py:196 +msgid "tags" +msgstr "tagi" + +#: models.py:72 +msgid "author" +msgstr "autor" + +#: models.py:73 +msgid "author name" +msgstr "imię i nazwisko autora" + +#: models.py:75 models.py:79 +msgid "Used if author is not set." +msgstr "Używane, gdy nie jest ustawiony autor." + +#: models.py:77 +msgid "author email" +msgstr "e-mail autora" + +#: models.py:81 +msgid "revision" +msgstr "rewizja" + +#: models.py:85 +msgid "parent" +msgstr "rodzic" + +#: models.py:90 +msgid "merge parent" +msgstr "drugi rodzic" + +#: models.py:93 +msgid "description" +msgstr "opis" + +#: models.py:96 +msgid "publishable" +msgstr "do publikacji" + +#: models.py:102 +msgid "change" +msgstr "zmiana" + +#: models.py:103 +msgid "changes" +msgstr "zmiany" + +#: models.py:195 +msgid "document" +msgstr "dokument" + +#: models.py:197 +msgid "data" +msgstr "dane" + +#: models.py:211 +msgid "stage" +msgstr "etap" + +#: models.py:219 +msgid "head" +msgstr "głowica" + +#: models.py:220 +msgid "This document's current head." +msgstr "Aktualna wersja dokumentu." + +#: models.py:224 +msgid "creator" +msgstr "utworzył" + +#: models.py:239 +msgid "user" +msgstr "użytkownik" + +#: models.py:239 +msgid "Work assignment." +msgstr "Przypisanie pracy użytkownikowi." diff --git a/apps/dvcs/models.py b/apps/dvcs/models.py index 1668dee8..ab5f77d9 100644 --- a/apps/dvcs/models.py +++ b/apps/dvcs/models.py @@ -1,22 +1,21 @@ from datetime import datetime +import os.path +from django.contrib.auth.models import User from django.core.files.base import ContentFile from django.core.files.storage import FileSystemStorage from django.db import models from django.db.models.base import ModelBase -from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ from mercurial import mdiff, simplemerge -from dvcs.fields import GzipFileSystemStorage -from dvcs.settings import REPO_PATH +from django.conf import settings +from dvcs.signals import post_commit +from dvcs.storage import GzipFileSystemStorage class Tag(models.Model): - """ - a tag (e.g. document stage) which can be applied to a change - """ - + """A tag (e.g. document stage) which can be applied to a Change.""" name = models.CharField(_('name'), max_length=64) slug = models.SlugField(_('slug'), unique=True, max_length=64, null=True, blank=True) @@ -27,6 +26,8 @@ class Tag(models.Model): class Meta: abstract = True ordering = ['ordering'] + verbose_name = _("tag") + verbose_name_plural = _("tags") def __unicode__(self): return self.name @@ -57,8 +58,6 @@ class Tag(models.Model): models.signals.pre_save.connect(Tag.listener_changed, sender=Tag) -repo = GzipFileSystemStorage(location=REPO_PATH) - def data_upload_to(instance, filename): return "%d/%d" % (instance.tree.pk, instance.pk) @@ -70,29 +69,38 @@ class Change(models.Model): Data file contains a gzipped text of the document. """ - author = models.ForeignKey(User, null=True, blank=True) - author_name = models.CharField(max_length=128, null=True, blank=True) - author_email = models.CharField(max_length=128, null=True, blank=True) - data = models.FileField(upload_to=data_upload_to, storage=repo) - revision = models.IntegerField(db_index=True) + author = models.ForeignKey(User, null=True, blank=True, verbose_name=_('author')) + author_name = models.CharField(_('author name'), max_length=128, + null=True, blank=True, + help_text=_("Used if author is not set.") + ) + author_email = models.CharField(_('author email'), max_length=128, + null=True, blank=True, + help_text=_("Used if author is not set.") + ) + revision = models.IntegerField(_('revision'), db_index=True) parent = models.ForeignKey('self', null=True, blank=True, default=None, + verbose_name=_('parent'), related_name="children") merge_parent = models.ForeignKey('self', null=True, blank=True, default=None, + verbose_name=_('merge parent'), related_name="merge_children") - description = models.TextField(blank=True, default='') + description = models.TextField(_('description'), blank=True, default='') created_at = models.DateTimeField(editable=False, db_index=True, default=datetime.now) - publishable = models.BooleanField(default=False) + publishable = models.BooleanField(_('publishable'), default=False) class Meta: abstract = True ordering = ('created_at',) unique_together = ['tree', 'revision'] + verbose_name = _("change") + verbose_name_plural = _("changes") def __unicode__(self): return u"Id: %r, Tree %r, Parent %r, Data: %s" % (self.id, self.tree_id, self.parent_id, self.data) @@ -156,58 +164,79 @@ class Change(models.Model): """ commit this version of a doc as new head """ self.tree.commit(text=self.materialize(), **kwargs) + def set_publishable(self, publishable): + self.publishable = publishable + self.save() + post_publishable(sender=self, publishable=publishable).send() + def create_tag_model(model): name = model.__name__ + 'Tag' + + class Meta(Tag.Meta): + app_label = model._meta.app_label + attrs = { '__module__': model.__module__, + 'Meta': Meta, } return type(name, (Tag,), attrs) def create_change_model(model): name = model.__name__ + 'Change' + repo = GzipFileSystemStorage(location=model.REPO_PATH) + + class Meta(Change.Meta): + app_label = model._meta.app_label attrs = { '__module__': model.__module__, - 'tree': models.ForeignKey(model, related_name='change_set'), - 'tags': models.ManyToManyField(model.tag_model, related_name='change_set'), + 'tree': models.ForeignKey(model, related_name='change_set', verbose_name=_('document')), + 'tags': models.ManyToManyField(model.tag_model, verbose_name=_('tags'), related_name='change_set'), + 'data': models.FileField(_('data'), upload_to=data_upload_to, storage=repo), + 'Meta': Meta, } return type(name, (Change,), attrs) - class DocumentMeta(ModelBase): "Metaclass for Document models." def __new__(cls, name, bases, attrs): + model = super(DocumentMeta, cls).__new__(cls, name, bases, attrs) if not model._meta.abstract: # create a real Tag object and `stage' fk model.tag_model = create_tag_model(model) - models.ForeignKey(model.tag_model, + models.ForeignKey(model.tag_model, verbose_name=_('stage'), null=True, blank=True).contribute_to_class(model, 'stage') # create real Change model and `head' fk model.change_model = create_change_model(model) + models.ForeignKey(model.change_model, null=True, blank=True, default=None, + verbose_name=_('head'), help_text=_("This document's current head."), editable=False).contribute_to_class(model, 'head') - return model + models.ForeignKey(User, null=True, blank=True, editable=False, + verbose_name=_('creator'), related_name="created_%s" % name.lower() + ).contribute_to_class(model, 'creator') + return model class Document(models.Model): - """ - File in repository. - """ + """File in repository. Subclass it to use version control in your app.""" + __metaclass__ = DocumentMeta - creator = models.ForeignKey(User, null=True, blank=True, editable=False, - related_name="created_documents") + # default repository path + REPO_PATH = os.path.join(settings.MEDIA_ROOT, 'dvcs') - user = models.ForeignKey(User, null=True, blank=True) + user = models.ForeignKey(User, null=True, blank=True, + verbose_name=_('user'), help_text=_('Work assignment.')) class Meta: abstract = True @@ -215,13 +244,6 @@ class Document(models.Model): def __unicode__(self): return u"{0}, HEAD: {1}".format(self.id, self.head_id) - @models.permalink - def get_absolute_url(self): - return ('dvcs.views.document_data', (), { - 'document_id': self.id, - 'version': self.head_id, - }) - def materialize(self, change=None): if self.head is None: return u'' @@ -231,7 +253,23 @@ class Document(models.Model): change = self.change_set.get(pk=change) return change.materialize() - def commit(self, text, **kwargs): + def commit(self, text, author=None, author_name=None, author_email=None, + publishable=False, **kwargs): + """Commits a new revision. + + This will automatically merge the commit into the main branch, + if parent is not document's head. + + :param unicode text: new version of the document + :param parent: parent revision (head, if not specified) + :type parent: Change or None + :param User author: the commiter + :param unicode author_name: commiter name (if ``author`` not specified) + :param unicode author_email: commiter e-mail (if ``author`` not specified) + :param Tag[] tags: list of tags to apply to the new commit + :param bool publishable: set new commit as ready to publish + :returns: new head + """ if 'parent' not in kwargs: parent = self.head else: @@ -239,9 +277,6 @@ class Document(models.Model): if parent is not None and not isinstance(parent, Change): parent = self.change_set.objects.get(pk=kwargs['parent']) - author = kwargs.get('author', None) - author_name = kwargs.get('author_name', None) - author_email = kwargs.get('author_email', None) tags = kwargs.get('tags', []) if tags: # set stage to next tag after the commited one @@ -251,6 +286,7 @@ class Document(models.Model): author_name=author_name, author_email=author_email, description=kwargs.get('description', ''), + publishable=publishable, parent=parent) change.tags = tags @@ -265,6 +301,9 @@ class Document(models.Model): else: self.head = change self.save() + + post_commit.send(sender=self.head) + return self.head def history(self): @@ -280,8 +319,8 @@ class Document(models.Model): return self.change_set.get(revision=rev) def publishable(self): - changes = self.change_set.filter(publishable=True).order_by('-created_at')[:1] - if changes.count(): - return changes[0] + changes = self.change_set.filter(publishable=True) + if changes.exists(): + return changes.order_by('-created_at')[0] else: return None diff --git a/apps/dvcs/settings.py b/apps/dvcs/settings.py deleted file mode 100755 index d7863bfa..00000000 --- a/apps/dvcs/settings.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.conf import settings - -REPO_PATH = settings.DVCS_REPO_PATH diff --git a/apps/dvcs/signals.py b/apps/dvcs/signals.py new file mode 100755 index 00000000..5da075be --- /dev/null +++ b/apps/dvcs/signals.py @@ -0,0 +1,4 @@ +from django.dispatch import Signal + +post_commit = Signal() +post_publishable = Signal(providing_args=['publishable']) diff --git a/apps/dvcs/tests.py b/apps/dvcs/tests.py deleted file mode 100644 index 0c712957..00000000 --- a/apps/dvcs/tests.py +++ /dev/null @@ -1,164 +0,0 @@ -from django.test import TestCase -from dvcs.models import Change, Document -from django.contrib.auth.models import User - -class DocumentModelTests(TestCase): - - def setUp(self): - self.user = User.objects.create_user("tester", "tester@localhost.local") - - def assertTextEqual(self, given, expected): - return self.assertEqual(given, expected, - "Expected '''%s'''\n differs from text: '''%s'''" % (expected, given) - ) - - def test_empty_file(self): - doc = Document.objects.create(name=u"Sample Document", creator=self.user) - self.assert_(doc.head is not None) - self.assertEqual(doc.materialize(), u"") - - def test_single_commit(self): - doc = Document.objects.create(name=u"Sample Document", creator=self.user) - doc.commit(text=u"Ala ma kota", description="Commit #1", author=self.user) - self.assert_(doc.head is not None) - self.assertEqual(doc.change_set.count(), 2) - self.assertEqual(doc.materialize(), u"Ala ma kota") - - def test_chained_commits(self): - doc = Document.objects.create(name=u"Sample Document", creator=self.user) - c1 = doc.commit(description="Commit #1", text=u""" - Line #1 - Line #2 is cool - """, author=self.user) - c2 = doc.commit(description="Commit #2", text=u""" - Line #1 - Line #2 is hot - """, author=self.user) - c3 = doc.commit(description="Commit #3", text=u""" - Line #1 - ... is hot - Line #3 ate Line #2 - """, author=self.user) - self.assert_(doc.head is not None) - self.assertEqual(doc.change_set.count(), 4) - - self.assertEqual(doc.materialize(), u""" - Line #1 - ... is hot - Line #3 ate Line #2 - """) - self.assertEqual(doc.materialize(version=c3), u""" - Line #1 - ... is hot - Line #3 ate Line #2 - """) - self.assertEqual(doc.materialize(version=c2), u""" - Line #1 - Line #2 is hot - """) - self.assertEqual(doc.materialize(version=c1), """ - Line #1 - Line #2 is cool - """) - - - def test_parallel_commit_noconflict(self): - doc = Document.objects.create(name=u"Sample Document", creator=self.user) - self.assert_(doc.head is not None) - base = doc.head - base = doc.commit(description="Commit #1", text=u""" - Line #1 - Line #2 -""", author=self.user) - - c1 = doc.commit(description="Commit #2", text=u""" - Line #1 is hot - Line #2 -""", parent=base, author=self.user) - self.assertTextEqual(c1.materialize(), u""" - Line #1 is hot - Line #2 -""") - c2 = doc.commit(description="Commit #3", text=u""" - Line #1 - Line #2 - Line #3 -""", parent=base, author=self.user) - self.assertEqual(doc.change_set.count(), 5) - self.assertTextEqual(doc.materialize(), u""" - Line #1 is hot - Line #2 - Line #3 -""") - - def test_parallel_commit_conflict(self): - doc = Document.objects.create(name=u"Sample Document", creator=self.user) - self.assert_(doc.head is not None) - base = doc.head - base = doc.commit(description="Commit #1", text=u""" -Line #1 -Line #2 -Line #3 -""", author=self.user) - - c1 = doc.commit(description="Commit #2", text=u""" -Line #1 -Line #2 is hot -Line #3 -""", parent=base, author=self.user) - c2 = doc.commit(description="Commit #3", text=u""" -Line #1 -Line #2 is cool -Line #3 -""", parent=base, author=self.user) - self.assertEqual(doc.change_set.count(), 5) - self.assertTextEqual(doc.materialize(), u""" -Line #1 -<<<<<<< -Line #2 is hot -======= -Line #2 is cool ->>>>>>> -Line #3 -""") - - def test_multiply_parallel_commits(self): - doc = Document.objects.create(name=u"Sample Document", creator=self.user) - self.assert_(doc.head is not None) - c1 = doc.commit(description="Commit A1", text=u""" -Line #1 - -Line #2 - -Line #3 -""", author=self.user) - c2 = doc.commit(description="Commit A2", text=u""" -Line #1 * - -Line #2 - -Line #3 -""", author=self.user) - c3 = doc.commit(description="Commit B1", text=u""" -Line #1 - -Line #2 ** - -Line #3 -""", parent=c1, author=self.user) - c4 = doc.commit(description="Commit C1", text=u""" -Line #1 * - -Line #2 - -Line #3 *** -""", parent=c2, author=self.user) - self.assertEqual(doc.change_set.count(), 7) - self.assertTextEqual(doc.materialize(), u""" -Line #1 * - -Line #2 ** - -Line #3 *** -""") - diff --git a/apps/dvcs/tests/__init__.py b/apps/dvcs/tests/__init__.py new file mode 100755 index 00000000..de77d991 --- /dev/null +++ b/apps/dvcs/tests/__init__.py @@ -0,0 +1,158 @@ +from nose.tools import * +from django.test import TestCase +from dvcs.models import Document + + +class ADocument(Document): + pass + + +class DocumentModelTests(TestCase): + + def assertTextEqual(self, given, expected): + return self.assertEqual(given, expected, + "Expected '''%s'''\n differs from text: '''%s'''" % (expected, given) + ) + + def test_empty_file(self): + doc = ADocument.objects.create() + self.assertTextEqual(doc.materialize(), u"") + + def test_single_commit(self): + doc = ADocument.objects.create() + doc.commit(text=u"Ala ma kota", description="Commit #1") + self.assertTextEqual(doc.materialize(), u"Ala ma kota") + + def test_chained_commits(self): + doc = ADocument.objects.create() + text1 = u""" + Line #1 + Line #2 is cool + """ + text2 = u""" + Line #1 + Line #2 is hot + """ + text3 = u""" + Line #1 + ... is hot + Line #3 ate Line #2 + """ + + c1 = doc.commit(description="Commit #1", text=text1) + c2 = doc.commit(description="Commit #2", text=text2) + c3 = doc.commit(description="Commit #3", text=text3) + + self.assertTextEqual(doc.materialize(), text3) + self.assertTextEqual(doc.materialize(change=c3), text3) + self.assertTextEqual(doc.materialize(change=c2), text2) + self.assertTextEqual(doc.materialize(change=c1), text1) + + def test_parallel_commit_noconflict(self): + doc = ADocument.objects.create() + text1 = u""" + Line #1 + Line #2 + """ + text2 = u""" + Line #1 is hot + Line #2 + """ + text3 = u""" + Line #1 + Line #2 + Line #3 + """ + text_merged = u""" + Line #1 is hot + Line #2 + Line #3 + """ + + base = doc.commit(description="Commit #1", text=text1) + c1 = doc.commit(description="Commit #2", text=text2) + commits = doc.change_set.count() + c2 = doc.commit(description="Commit #3", text=text3, parent=base) + self.assertEqual(doc.change_set.count(), commits + 2, + u"Parallel commits should create an additional merge commit") + self.assertTextEqual(doc.materialize(), text_merged) + + def test_parallel_commit_conflict(self): + doc = ADocument.objects.create() + text1 = u""" + Line #1 + Line #2 + Line #3 + """ + text2 = u""" + Line #1 + Line #2 is hot + Line #3 + """ + text3 = u""" + Line #1 + Line #2 is cool + Line #3 + """ + text_merged = u""" + Line #1 +<<<<<<< + Line #2 is hot +======= + Line #2 is cool +>>>>>>> + Line #3 + """ + base = doc.commit(description="Commit #1", text=text1) + c1 = doc.commit(description="Commit #2", text=text2) + commits = doc.change_set.count() + c2 = doc.commit(description="Commit #3", text=text3, parent=base) + self.assertEqual(doc.change_set.count(), commits + 2, + u"Parallel commits should create an additional merge commit") + self.assertTextEqual(doc.materialize(), text_merged) + + + def test_multiple_parallel_commits(self): + text_a1 = u""" + Line #1 + + Line #2 + + Line #3 + """ + text_a2 = u""" + Line #1 * + + Line #2 + + Line #3 + """ + text_b1 = u""" + Line #1 + + Line #2 ** + + Line #3 + """ + text_c1 = u""" + Line #1 + + Line #2 + + Line #3 *** + """ + text_merged = u""" + Line #1 * + + Line #2 ** + + Line #3 *** + """ + + + doc = ADocument.objects.create() + c1 = doc.commit(description="Commit A1", text=text_a1) + c2 = doc.commit(description="Commit A2", text=text_a2, parent=c1) + c3 = doc.commit(description="Commit B1", text=text_b1, parent=c1) + c4 = doc.commit(description="Commit C1", text=text_c1, parent=c1) + self.assertTextEqual(doc.materialize(), text_merged) diff --git a/apps/dvcs/urls.py b/apps/dvcs/urls.py deleted file mode 100644 index d1e1e296..00000000 --- a/apps/dvcs/urls.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -from django.conf.urls.defaults import * - -urlpatterns = patterns('dvcs.views', - url(r'^data/(?P[^/]+)/(?P.*)$', 'document_data', name='storage_document_data'), -) diff --git a/apps/dvcs/views.py b/apps/dvcs/views.py deleted file mode 100644 index 7918e96c..00000000 --- a/apps/dvcs/views.py +++ /dev/null @@ -1,21 +0,0 @@ -# Create your views here. -from django.views.generic.simple import direct_to_template -from django import http -from dvcs.models import Document - -def document_list(request, template_name="dvcs/document_list.html"): - return direct_to_template(request, template_name, { - "documents": Document.objects.all(), - }) - -def document_data(request, document_id, version=None): - doc = Document.objects.get(pk=document_id) - return http.HttpResponse(doc.materialize(version or None), content_type="text/plain") - -def document_history(request, docid, template_name="dvcs/document_history.html"): - document = Document.objects.get(pk=docid) - return direct_to_template(request, template_name, { - "document": document, - "changes": document.history(), - }) - diff --git a/apps/wiki/tests.py b/apps/wiki/tests.py deleted file mode 100644 index 65777379..00000000 --- a/apps/wiki/tests.py +++ /dev/null @@ -1,19 +0,0 @@ -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/wiki/views.py b/apps/wiki/views.py index adffcb76..0455bdde 100644 --- a/apps/wiki/views.py +++ b/apps/wiki/views.py @@ -280,8 +280,7 @@ def pubmark(request, slug, chunk=None): publishable = form.cleaned_data['publishable'] change = doc.at_revision(revision) if publishable != change.publishable: - change.publishable = publishable - change.save() + change.set_publishable(publishable) return JSONResponse({"message": _("Revision marked")}) else: return JSONResponse({"message": _("Nothing changed")}) diff --git a/redakcja.wsgi.template b/redakcja.wsgi.template index ef19f5a7..2d10ac38 100644 --- a/redakcja.wsgi.template +++ b/redakcja.wsgi.template @@ -19,6 +19,7 @@ sys.path = [ ] + sys.path # Run Django +os.environ["CELERY_LOADER"] = "django" os.environ['DJANGO_SETTINGS_MODULE'] = '$PROJECT_NAME.settings' from django.core.handlers.wsgi import WSGIHandler diff --git a/redakcja/settings/common.py b/redakcja/settings/common.py index 9d6e175e..5414246d 100644 --- a/redakcja/settings/common.py +++ b/redakcja/settings/common.py @@ -10,8 +10,6 @@ TEMPLATE_DEBUG = DEBUG MAINTENANCE_MODE = False ADMINS = ( - # (u'Marek Stępniowski', 'marek@stepniowski.com'), - # (u'Łukasz Rekucki', 'lrekucki@gmail.com'), (u'Radek Czajka', 'radoslaw.czajka@nowoczesnapolska.org.pl'), ) @@ -116,6 +114,8 @@ INSTALLED_APPS = ( 'filebrowser', 'pagination', 'gravatar', + 'djcelery', + 'djkombu', 'catalogue', 'dvcs', @@ -137,12 +137,10 @@ FILEBROWSER_DEFAULT_ORDER = "path_relative" IMAGE_DIR = 'images' -WL_API_CONFIG = { - "URL": "http://localhost:7000/api/", - "AUTH_REALM": "WL API", - "AUTH_USER": "platforma", - "AUTH_PASSWD": "platforma", -} +import djcelery +djcelery.setup_loader() + + SHOW_APP_VERSION = False diff --git a/redakcja/settings/compress.py b/redakcja/settings/compress.py index db72c00f..e4b5b085 100644 --- a/redakcja/settings/compress.py +++ b/redakcja/settings/compress.py @@ -13,11 +13,11 @@ COMPRESS_CSS = { ), 'output_filename': 'compressed/detail_styles_?.css', }, - 'listing': { + 'catalogue': { 'source_filenames': ( 'css/filelist.css', ), - 'output_filename': 'compressed/listing_styles_?.css', + 'output_filename': 'compressed/catalogue_styles_?.css', } } @@ -59,12 +59,12 @@ COMPRESS_JS = { ), 'output_filename': 'compressed/detail_scripts_?.js', }, - 'listing': { + 'catalogue': { 'source_filenames': ( - 'js/lib/jquery-1.4.2.min.js', + 'js/catalogue.js', 'js/slugify.js', ), - 'output_filename': 'compressed/listing_scripts_?.js', + 'output_filename': 'compressed/catalogue_scripts_?.js', } } diff --git a/redakcja/settings/test.py b/redakcja/settings/test.py index 118c7ffb..f6e90803 100644 --- a/redakcja/settings/test.py +++ b/redakcja/settings/test.py @@ -11,12 +11,12 @@ DATABASE_NAME = ':memory:' import tempfile -WIKI_REPOSITORY_PATH = tempfile.mkdtemp(prefix='wikirepo') +CATALOGUE_REPO_PATH = tempfile.mkdtemp(prefix='wikirepo') INSTALLED_APPS += ('django_nose',) TEST_RUNNER = 'django_nose.run_tests' -TEST_MODULES = ('wiki', 'toolbar', 'vstorage') +TEST_MODULES = ('catalogue', 'dvcs.tests', 'wiki', 'toolbar') NOSE_ARGS = ( '--tests=' + ','.join(TEST_MODULES), '--cover-package=' + ','.join(TEST_MODULES), diff --git a/redakcja/static/js/catalogue/catalogue.js b/redakcja/static/js/catalogue/catalogue.js new file mode 100755 index 00000000..e8ef5e95 --- /dev/null +++ b/redakcja/static/js/catalogue/catalogue.js @@ -0,0 +1,29 @@ +(function($) { + $(function() { + + + $(function() { + $('.filter').change(function() { + document.filter[this.name].value = this.value; + document.filter.submit(); + }); + + $('.check-filter').change(function() { + document.filter[this.name].value = this.checked ? '1' : ''; + document.filter.submit(); + }); + + $('.text-filter').each(function() { + var inp = this; + $(inp).parent().submit(function() { + document.filter[inp.name].value = inp.value; + document.filter.submit(); + return false; + }); + }); + }); + + + }); +})(jQuery) + diff --git a/redakcja/templates/pagination/pagination.html b/redakcja/templates/pagination/pagination.html new file mode 100755 index 00000000..fe566a86 --- /dev/null +++ b/redakcja/templates/pagination/pagination.html @@ -0,0 +1,26 @@ +{% if is_paginated %} +{% load i18n %} + +{% endif %} diff --git a/requirements-test.txt b/requirements-test.txt index fe7944cf..2ec68c0d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,3 @@ -django-nose==0.0.3 +django-nose==0.1.3 nose nosexcover diff --git a/requirements.txt b/requirements.txt index c5d6b044..5e942547 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,8 @@ sorl-thumbnail>=3.2 django-maintenancemode>=0.9 django-pagination django-gravatar +django-celery +django-kombu # migrations south>=0.6