nearly working version
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Mon, 3 Oct 2011 14:41:17 +0000 (16:41 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Mon, 3 Oct 2011 14:41:55 +0000 (16:41 +0200)
54 files changed:
apps/catalogue/forms.py
apps/catalogue/helpers.py
apps/catalogue/locale/pl/LC_MESSAGES/django.mo
apps/catalogue/locale/pl/LC_MESSAGES/django.po
apps/catalogue/management/commands/import_wl.py
apps/catalogue/management/commands/merge_books.py
apps/catalogue/managers.py [new file with mode: 0644]
apps/catalogue/migrations/0001_initial.py
apps/catalogue/migrations/0002_from_hg.py [deleted file]
apps/catalogue/migrations/0002_stages.py [new file with mode: 0644]
apps/catalogue/migrations/0003_from_hg.py [new file with mode: 0644]
apps/catalogue/models.py [deleted file]
apps/catalogue/models/__init__.py [new file with mode: 0755]
apps/catalogue/models/book.py [new file with mode: 0755]
apps/catalogue/models/chunk.py [new file with mode: 0755]
apps/catalogue/models/listeners.py [new file with mode: 0755]
apps/catalogue/models/publish_log.py [new file with mode: 0755]
apps/catalogue/signals.py [new file with mode: 0644]
apps/catalogue/tasks.py [new file with mode: 0644]
apps/catalogue/templates/catalogue/activity.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/base.html
apps/catalogue/templates/catalogue/book_list/book.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/book_list/book_list.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/book_list/chunk.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/document_list.html
apps/catalogue/templates/catalogue/my_page.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/user_page.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/wall.html
apps/catalogue/templatetags/book_list.py [new file with mode: 0755]
apps/catalogue/templatetags/catalogue.py
apps/catalogue/templatetags/wall.py [new file with mode: 0755]
apps/catalogue/tests.py [deleted file]
apps/catalogue/urls.py
apps/catalogue/views.py
apps/catalogue/xml_tools.py [changed mode: 0755->0644]
apps/dvcs/locale/pl/LC_MESSAGES/django.mo [new file with mode: 0644]
apps/dvcs/locale/pl/LC_MESSAGES/django.po [new file with mode: 0644]
apps/dvcs/models.py
apps/dvcs/settings.py [deleted file]
apps/dvcs/signals.py [new file with mode: 0755]
apps/dvcs/tests.py [deleted file]
apps/dvcs/tests/__init__.py [new file with mode: 0755]
apps/dvcs/urls.py [deleted file]
apps/dvcs/views.py [deleted file]
apps/wiki/tests.py [deleted file]
apps/wiki/views.py
redakcja.wsgi.template
redakcja/settings/common.py
redakcja/settings/compress.py
redakcja/settings/test.py
redakcja/static/js/catalogue/catalogue.js [new file with mode: 0755]
redakcja/templates/pagination/pagination.html [new file with mode: 0755]
requirements-test.txt
requirements.txt

index 6a56e76..f6b2dc9 100644 (file)
@@ -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']
index c9dc0bd..7bc2481 100644 (file)
@@ -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
index f841945..4eb7271 100644 (file)
Binary files a/apps/catalogue/locale/pl/LC_MESSAGES/django.mo and b/apps/catalogue/locale/pl/LC_MESSAGES/django.mo differ
index c760f3a..d7566e3 100644 (file)
@@ -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 <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
 "Language-Team: Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org.pl>\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 <code>.xml</code> will be ignored."
 msgstr "Proszę wskazać archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie kończące się na <code>.xml</code> zostaną zignorowane."
 
-#: templates/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 <code>.xml</code> extension"
 msgstr "Pliki pominięte z powodu braku rozszerzenia <code>.xml</code>."
 
-#: 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"
index 6836d36..4f15cef 100755 (executable)
@@ -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
index 4747591..00014df 100755 (executable)
@@ -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<n>\d*)$'), None))
-        res.append((re.compile(ur'__?(rozdzialy__?)?(?P<n>\d*)-'), None))
-    
-        for r, default in res:
-            m = r.search(slug)
-            if m:
-                start = m.start()
-                try:
-                    return int(m.group('n')), slug[:start]
-                except IndexError:
-                    return default, slug[:start]
-        return None, slug
-
-    def file_to_title(fname):
-        """ Returns a title-like version of a filename. """
-        parts = (p.replace('_', ' ').title() for p in fname.split('__'))
-        return ' / '.join(parts)
-
-    merges = defaultdict(list)
-    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<n>\d*)$'), None))
+            res.append((re.compile(ur'__?(rozdzialy__?)?(?P<n>\d*)-'), None))
+        
+            for r, default in res:
+                m = r.search(slug)
+                if m:
+                    start = m.start()
+                    try:
+                        return int(m.group('n')), slug[:start]
+                    except IndexError:
+                        return default, slug[:start]
+            return None, slug
+    
+        def file_to_title(fname):
+            """ Returns a title-like version of a filename. """
+            parts = (p.replace('_', ' ').title() for p in fname.split('__'))
+            return ' / '.join(parts)
+    
+        merges = defaultdict(list)
+        slugs = []
+        for b in Book.objects.all():
+            slugs.append(b.slug)
+            n, ns = read_slug(b.slug)
+            if n is not None:
+                merges[ns].append((n, b))
+    
+        conflicting_slugs = []
+        for slug in sorted(merges.keys()):
+            merge_list = sorted(merges[slug])
+            if len(merge_list) < 2:
+                continue
+    
+            merge_slugs = [b.slug for i, b in merge_list]
+            if slug in slugs and slug not in merge_slugs:
+                conflicting_slugs.append(slug)
+    
+            title = file_to_title(slug)
+            print "./manage.py merge_books %s%s--title=%s --slug=%s \\\n    %s\n" % (
+                '--dry-run ' if dry_run else '',
+                '--force ' if force else '',
+                quote(title), slug,
+                " \\\n    ".join(merge_slugs)
+                )
+    
+        if conflicting_slugs:
+            if force:
+                print self.style.NOTICE('# These books will be archived:')
+            else:
+                print self.style.ERROR('# ERROR: Conflicting slugs:')
+            for slug in conflicting_slugs:
+                print '#', slug
+
+
     def handle(self, *slugs, **options):
 
         self.style = color_style()
 
+        force = options.get('force')
         guess = options.get('guess')
         dry_run = options.get('dry_run')
         new_slug = options.get('new_slug')
@@ -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 (file)
index 0000000..4f804b8
--- /dev/null
@@ -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)
index 8bd220a..dccd9b7 100644 (file)
@@ -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 (file)
index 2a64819..0000000
+++ /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*<!--\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 (file)
index 0000000..7155457
--- /dev/null
@@ -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 (file)
index 0000000..1816af9
--- /dev/null
@@ -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*<!--\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 (file)
index 00e1d30..0000000
+++ /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 (executable)
index 0000000..d9a11dd
--- /dev/null
@@ -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 (executable)
index 0000000..9f809b8
--- /dev/null
@@ -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 (executable)
index 0000000..e68b1c1
--- /dev/null
@@ -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 (executable)
index 0000000..7848974
--- /dev/null
@@ -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 (executable)
index 0000000..f422e37
--- /dev/null
@@ -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 (file)
index 0000000..62ca514
--- /dev/null
@@ -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 (file)
index 0000000..e9b8cf9
--- /dev/null
@@ -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 (executable)
index 0000000..4354b33
--- /dev/null
@@ -0,0 +1,7 @@
+{% extends "catalogue/base.html" %}
+
+{% load wall %}
+
+{% block leftcolumn %}
+    {% wall %}
+{% endblock leftcolumn %}
index 4ba8a0e..9b32fe7 100644 (file)
@@ -4,7 +4,7 @@
 <html>
 <head>
     <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
-    {% compressed_css 'listing' %}
+    {% compressed_css 'catalogue' %}
     <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}style.css" />
     <title>{% block title %}{% trans "Platforma Redakcyjna" %}{% endblock title %}</title>
 </head>
@@ -42,7 +42,9 @@
 
 </div>
 
-{% compressed_js 'listing' %}
+
+<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
+{% compressed_js 'catalogue' %}
 {% block extrabody %}
 {% endblock %}
 </body>
diff --git a/apps/catalogue/templates/catalogue/book_list/book.html b/apps/catalogue/templates/catalogue/book_list/book.html
new file mode 100755 (executable)
index 0000000..a45a357
--- /dev/null
@@ -0,0 +1,34 @@
+{% load i18n %}
+
+{% if book.single %}
+    {% with book.0 as chunk %}
+    <tr>
+        <td><a target="_blank" href="{% url catalogue_book book.slug %}" title='{% trans "Book settings" %}'>[B]</a></td>
+        <td><a href="{% url catalogue_chunk_edit book.slug chunk.slug %}" title='{% trans "Chunk settings" %}'>[c]</a></td>
+        <td><a target="_blank"
+                    href="{% url wiki_editor book.slug %}">
+                    {{ book.title }}</a></td>
+        <td>{% if chunk.stage %}
+            {{ chunk.stage }}
+        {% else %}–
+        {% endif %}</td>
+        <td class='user-column'>{% if chunk.user %}<a href="{% url catalogue_user chunk.user.username %}">{{ chunk.user.first_name }} {{ chunk.user.last_name }}</a>{% endif %}</td>
+        <td>
+            {% if chunk.published %}P{% endif %}
+            {% if book.new_publishable %}p{% endif %}
+            {% if chunk.changed %}+{% endif %}
+        </td>
+    </tr>
+    {% endwith %}
+{% else %}
+    <tr>
+        <td><a target="_blank" href="{% url catalogue_book book.slug %}" title='{% trans "Book settings" %}'>[B]</a></td>
+        <td></td>
+        <td>{{ book.title }}</td>
+        <td></td><td></td>
+        <td>
+            {% if book.published %}P{% endif %}
+            {% if book.new_publishable %}p{% endif %}
+        </td>
+    </tr>
+{% 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 (executable)
index 0000000..73811ca
--- /dev/null
@@ -0,0 +1,81 @@
+{% load i18n %}
+{% load pagination_tags %}
+
+
+<form name='filter' action=''>
+<input type='hidden' name="title" value="{{ request.GET.title }}" />
+<input type='hidden' name="stage" value="{{ request.GET.stage }}" />
+{% if not viewed_user %}
+    <input type='hidden' name="user" value="{{ request.GET.user }}" />
+{% endif %}
+<input type='hidden' name="all" value="{{ request.GET.all }}" />
+<input type='hidden' name="status" value="{{ request.GET.status }}" />
+</form>
+
+<table id="file-list"{% if viewed_user %} class="book-list-user"{% endif %}>
+    <thead><tr>
+        <th></th>
+        <th>
+            <input class='check-filter' type='checkbox' name='all' title='{% trans "Show hidden books" %}'
+                {% if request.GET.all %}checked='checked'{% endif %} />
+            </th>
+        <th class='book-search-column'>
+            <form>
+            <input title='{% trans "Search in book titles" %}' name="title"
+                class='text-filter' value="{{ request.GET.title }}" />
+            </form>
+        </th>
+        <th><select name="stage" class="filter">
+            <option value=''>- {% trans "stage" %} -</option>
+            <option {% if request.GET.stage == '-' %}selected="selected"
+                    {% endif %}value="-">- {% trans "none" %} -</option>
+            {% for stage in stages %}
+                <option {% if request.GET.stage == stage.slug %}selected="selected"
+                    {% endif %}value="{{ stage.slug }}">{{ stage.name }}</option>
+            {% endfor %}
+        </select></th>
+
+        {% if not viewed_user %}
+            <th><select name="user" class="filter">
+                <option value=''>- {% trans "editor" %} -</option>
+                <option {% if request.GET.user == '-' %}selected="selected"
+                        {% endif %}value="-">- {% trans "none" %} -</option>
+                {% for user in users %}
+                    <option {% if request.GET.user == user.username %}selected="selected"
+                        {% endif %}value="{{ user.username }}">{{ user.first_name }} {{ user.last_name }} ({{ user.count }})</option>
+                {% endfor %}
+            </select></th>
+        {% endif %}
+
+        <th><select name="status" class="filter">
+            <option value=''>- {% trans "status" %} -</option>
+            {% for state, label in states %}
+                <option {% if request.GET.status == state %}selected="selected"
+                        {% endif %}value='{{ state }}'>{{ label }}</option>
+            {% endfor %}
+        </select></th>
+
+    </tr></thead>
+
+    {% with cnt=books|length %}
+    {% autopaginate books 100 %}
+    <tbody>
+    {% 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 %}
+    <tr><th class='paginator' colspan="5">
+        {% paginate %}
+        {% blocktrans count c=cnt %}{{c}} book{% plural %}{{c}} books{% endblocktrans %}</th></tr>
+    </tbody>
+    {% endwith %}
+</table>
+{% if not books %}
+    <p>{% trans "No books found." %}</p>
+{% 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 (executable)
index 0000000..3897d78
--- /dev/null
@@ -0,0 +1,25 @@
+{% load i18n %}
+
+<tr>
+    <td></td>
+    <td><a href="{% url catalogue_chunk_edit chunk.book.slug chunk.slug %}" title='{% trans "Chunk settings" %}'>[c]</a></td>
+    <td><a target="_blank" href="{{ chunk.get_absolute_url }}">
+            <span class='chunkno'>{{ chunk.number }}.</span>
+            {{ chunk.title }}</a></td>
+    <td>{% if chunk.stage %}
+            {{ chunk.stage }}
+        {% else %}
+            –
+        {% endif %}</td>
+        <td class='user-column'>{% if chunk.user %}
+            <a href="{% url catalogue_user chunk.user.username %}">
+                {{ chunk.user.first_name }} {{ chunk.user.last_name }}
+            </a>{% else %}
+            
+            {% endif %}</td>
+</td>
+<td>
+    {% if chunk.new_publishable %}p{% endif %}
+    {% if chunk.changed %}+{% endif %}
+</td>
+</tr>
index 01e5a24..d5343a7 100644 (file)
@@ -1,147 +1,9 @@
 {% extends "catalogue/base.html" %}
 
 {% load i18n %}
-{% load pagination_tags %}
-{% load catalogue %}
+{% load catalogue book_list %}
 
-{% block extrabody %}
-{{ block.super }}
-<script type="text/javascript" charset="utf-8">
-$(function() {
-    $("select.filter").change(function() {
-        document.filter[this.name].value = this.value;
-        document.filter.submit();
-    });
-
-    $('#book-search').keypress(function(e)
-    {
-         if (e.which == 13) 
-         {
-            document.filter[this.name] = this.value;
-            document.filter.submit();
-         }
-    });
-
-});
-</script>
-{% endblock %}
 
 {% block leftcolumn %}
-
-    <form name='filter' action=''>
-    <input type='hidden' name="title" value="{{ request.GET.title }}" />
-    <input type='hidden' name="stage" value="{{ request.GET.stage }}" />
-    <input type='hidden' name="user" value="{{ request.GET.user }}" />
-    </form>
-
-    <table id="file-list">
-        <thead><tr>
-            <th></th>
-            <th></th>
-            <th id='th-book-search' style='width:300px;'>
-                <form action='#'>
-                <input name="title" class='filter' style='width:300px;' value="{{ request.GET.title }}" />
-                </form>
-            </th>
-            <th><select name="stage" class="filter">
-                <option value=''>- {% trans "filter by stage" %} -</option>
-                <option {% if request.GET.stage == '-' %}selected="selected"
-                        {% endif %}value="-">- {% trans "none" %} -</option>
-                {% for stage in stages %}
-                    <option {% if request.GET.stage == stage.slug %}selected="selected"
-                        {% endif %}value="{{ stage.slug }}">{{ stage.name }}</option>
-                {% endfor %}
-            </select></th>
-
-            {% if not viewed_user %}
-                <th><select name="user" class="filter">
-                    <option value=''>- {% trans "filter by user" %} -</option>
-                    <option {% if request.GET.user == '-' %}selected="selected"
-                            {% endif %}value="-">- {% trans "none" %} -</option>
-                    {% for user in users %}
-                        <option {% if request.GET.user == user.username %}selected="selected"
-                            {% endif %}value="{{ user.username }}">{{ user.first_name }} {{ user.last_name }} ({{ user.count }})</option>
-                    {% endfor %}
-                </select></th>
-            {% endif %}
-
-        </tr></thead>
-
-        <tbody>
-        {% autopaginate books 100 %}
-        {% if not books %}
-            <tr><td>{% trans "No books found." %}</td></tr>
-        {% endif %}
-        {% for item in books %}
-            {% with item.book as book %}
-
-            {% ifequal item.book_length 1 %}
-                {% with item.chunks.0 as chunk %}
-                <tr>
-                    <td><a target="_blank" href="{% url catalogue_book book.slug %}">[B]</a></td>
-                    <td><a href="{% url catalogue_chunk_edit book.slug chunk.slug %}">[c]</a></td>
-                    <td><a target="_blank"
-                                href="{% url wiki_editor book.slug %}">
-                                {{ book.title }}</a></td>
-                    <td>{% if chunk.stage %}
-                        ({{ chunk.stage }})
-                    {% else %}–
-                    {% endif %}</td>
-                    <td>{% if chunk.user %}<a href="{% url catalogue_user chunk.user.username %}">{{ chunk.user.first_name }} {{ chunk.user.last_name }}</a>{% endif %}</td>
-                </tr>
-                {% endwith %}
-            {% else %}
-                <tr>
-                    <td><a target="_blank" href="{% url catalogue_book book.slug %}">[B]</a></td>
-                    <td></td>
-                    <td>{{ book.title }}</td>
-                </tr>
-                {% for chunk in item.chunks %}
-                    <tr>
-                        <td></td>
-                        <td><a href="{% url catalogue_chunk_edit book.slug chunk.slug %}">[c]</a></td>
-                        <td><a target="_blank" href="{{ chunk.get_absolute_url }}">
-                                <span class='chunkno'>{{ chunk.number }}.</span>
-                                {{ chunk.title }}</a></td>
-                        <td>{% if chunk.stage %}
-                                {{ chunk.stage }}
-                            {% else %}
-                                –
-                            {% endif %}</td>
-                        {% if not viewed_user %}
-                            <td>{% if chunk.user %}
-                                <a href="{% url catalogue_user chunk.user.username %}">
-                                    {{ chunk.user.first_name }} {{ chunk.user.last_name }}
-                                </a>{% else %}
-                                
-                                {% endif %}</td>
-                        {% endif %}
-                    </td></tr>
-                {% endfor %}
-            {% endifequal %}
-            {% endwith %}
-       {% endfor %}
-        <tr><td colspan="3">{% paginate %}</td></tr>
-               </tbody>
-    </table>
+    {% book_list %}
 {% endblock leftcolumn %}
-
-{% block rightcolumn %}
-       <div id="last-edited-list">
-               <h2>{% trans "Your last edited documents" %}</h2>
-           <ol>
-                       {% for slugs, item in last_books %}
-                       <li><a href="{% url wiki_editor slugs.0 slugs.1 %}"
-                               target="_blank">{{ item.title }}</a><br/><span class="date">({{ item.time|date:"H:i:s, d/m/Y" }})</span></li>
-                       {% endfor %}
-               </ol>
-       </div>
-
-    {% if viewed_user %}
-        <h2>{% trans "Recent activity for" %} {{ viewed_user }}</h2>
-        {% wall viewed_user %}
-    {% else %}
-        <h2>{% trans "Recent activity" %}</h2>
-        {% 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 (executable)
index 0000000..48a2179
--- /dev/null
@@ -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 %}
+    <div id="last-edited-list">
+        <h2>{% trans "Your last edited documents" %}</h2>
+        <ol>
+            {% for slugs, item in last_books %}
+            <li><a href="{% url wiki_editor slugs.0 slugs.1 %}"
+                target="_blank">{{ item.title }}</a><br/><span class="date">({{ item.time|date:"H:i:s, d/m/Y" }})</span></li>
+            {% endfor %}
+        </ol>
+    </div>
+
+    <h2>{% trans "Recent activity for" %} {{ request.user|nice_name }}</h2>
+    {% 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 (executable)
index 0000000..89b4ece
--- /dev/null
@@ -0,0 +1,15 @@
+{% extends "catalogue/base.html" %}
+
+{% load i18n %}
+{% load catalogue book_list wall %}
+
+
+{% block leftcolumn %}
+    <h1>{{ viewed_user|nice_name }}</h1>
+    {% book_list viewed_user %}
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+    <h2>{% trans "Recent activity for" %} {{ viewed_user|nice_name }}</h2>
+    {% wall viewed_user 10 %}
+{% endblock rightcolumn %}
index 0e4597e..2ec3c0a 100755 (executable)
@@ -13,8 +13,8 @@
             <!--img src='{{ STATIC_URL }}img/wall/{{ item.tag}}.png' alt='{% trans item.tag %}' /-->
         </div>
 
-        {{ item.timestamp }}
-        <br/>
+        <span style='float:right'>{{ item.timestamp }}</span>
+        <h3>{{ item.header }}</h3>
         {% if item.user %}
             <a href="{% url catalogue_user item.user.username %}">
             {{ item.user.first_name }} {{ item.user.last_name }}</a>
diff --git a/apps/catalogue/templatetags/book_list.py b/apps/catalogue/templatetags/book_list.py
new file mode 100755 (executable)
index 0000000..15654be
--- /dev/null
@@ -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
+
index 7d138ff..bfb900b 100644 (file)
@@ -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 (executable)
index 0000000..5236eed
--- /dev/null
@@ -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 (file)
index 6577737..0000000
+++ /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(), [])
index e9e5e89..73fd3ee 100644 (file)
@@ -10,6 +10,7 @@ urlpatterns = patterns('catalogue.views',
     url(r'^user/$', 'my', name='catalogue_user'),
     url(r'^user/(?P<username>[^/]+)/$', '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'),
index 81de5ac..aa214de 100644 (file)
@@ -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:
old mode 100755 (executable)
new mode 100644 (file)
index 522806b..d6a9333
@@ -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 (file)
index 0000000..4c3a1ff
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 (file)
index 0000000..64ddfd7
--- /dev/null
@@ -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 <EMAIL@ADDRESS>, 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 <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
+"Language-Team: LANGUAGE <LL@li.org>\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."
index 1668dee..ab5f77d 100644 (file)
@@ -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 (executable)
index d7863bf..0000000
+++ /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 (executable)
index 0000000..5da075b
--- /dev/null
@@ -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 (file)
index 0c71295..0000000
+++ /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 (executable)
index 0000000..de77d99
--- /dev/null
@@ -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 (file)
index d1e1e29..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-# -*- coding: utf-8
-from django.conf.urls.defaults import *
-
-urlpatterns = patterns('dvcs.views',
-    url(r'^data/(?P<document_id>[^/]+)/(?P<version>.*)$', 'document_data', name='storage_document_data'),
-)
diff --git a/apps/dvcs/views.py b/apps/dvcs/views.py
deleted file mode 100644 (file)
index 7918e96..0000000
+++ /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 (file)
index 6577737..0000000
+++ /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(), [])
index adffcb7..0455bdd 100644 (file)
@@ -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")})
index ef19f5a..2d10ac3 100644 (file)
@@ -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
index 9d6e175..5414246 100644 (file)
@@ -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
 
index db72c00..e4b5b08 100644 (file)
@@ -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',
      }
 }
 
index 118c7ff..f6e9080 100644 (file)
@@ -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 (executable)
index 0000000..e8ef5e9
--- /dev/null
@@ -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 (executable)
index 0000000..fe566a8
--- /dev/null
@@ -0,0 +1,26 @@
+{% if is_paginated %}
+{% load i18n %}
+<div class="pagination">
+    {% if page_obj.has_previous %}
+        <a href="?page={{ page_obj.previous_page_number }}{{ getvars }}{{ hashtag }}" class="prev">&lsaquo;&lsaquo; {% trans "previous" %}</a>
+    {% else %}
+        <span class="disabled prev">&lsaquo;&lsaquo; {% trans "previous" %}</span>
+    {% endif %}
+    {% for page in pages %}
+        {% if page %}
+            {% ifequal page page_obj.number %}
+                <span class="current page">{{ page }}</span>
+            {% else %}
+                <a href="?page={{ page }}{{ getvars }}{{ hashtag }}" class="page">{{ page }}</a>
+            {% endifequal %}
+        {% else %}
+            ...
+        {% endif %}
+    {% endfor %}
+    {% if page_obj.has_next %}
+        <a href="?page={{ page_obj.next_page_number }}{{ getvars }}{{ hashtag }}" class="next">{% trans "next" %} &rsaquo;&rsaquo;</a>
+    {% else %}
+        <span class="disabled next">{% trans "next" %} &rsaquo;&rsaquo;</span>
+    {% endif %}
+</div>
+{% endif %}
index fe7944c..2ec68c0 100644 (file)
@@ -1,3 +1,3 @@
-django-nose==0.0.3
+django-nose==0.1.3
 nose
 nosexcover
index c5d6b04..5e94254 100644 (file)
@@ -15,6 +15,8 @@ sorl-thumbnail>=3.2
 django-maintenancemode>=0.9
 django-pagination
 django-gravatar
+django-celery
+django-kombu
 
 # migrations
 south>=0.6