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'))
         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']
 
     def clean(self):
         file = self.cleaned_data['file']
index c9dc0bd..7bc2481 100644 (file)
@@ -16,50 +16,15 @@ def active_tab(tab):
     return wrapper
 
 
     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"
 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"
 "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"
 "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"
 
 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: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"
 
 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"
 
 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ł"
 
 msgid "title"
 msgstr "tytuł"
 
-#: models.py:26
+#: models/book.py:21
+#: models/chunk.py:23
 msgid "slug"
 msgid "slug"
-msgstr ""
+msgstr "slug"
 
 
-#: models.py:27
+#: models/book.py:22
 msgid "scan gallery name"
 msgstr "nazwa galerii skanów"
 
 msgid "scan gallery name"
 msgstr "nazwa galerii skanów"
 
-#: models.py:29
+#: models/book.py:25
 msgid "parent"
 msgstr "rodzic"
 
 msgid "parent"
 msgstr "rodzic"
 
-#: models.py:30
+#: models/book.py:26
 msgid "parent number"
 msgstr "numeracja rodzica"
 
 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"
 
 msgid "book"
 msgstr "książka"
 
-#: models.py:41
+#: models/book.py:44
 msgid "books"
 msgstr "książki"
 
 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"
 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ę"
 
 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"
 
 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"
 
 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"
 
 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"
 
 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"
 
 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"
 
 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"
 
 msgid "unknown XML"
 msgstr "nieznany XML"
 
-#: templates/wiki/book_detail.html:42
+#: templates/catalogue/book_detail.html:42
 msgid "broken document"
 msgstr "uszkodzony dokument"
 
 msgid "broken document"
 msgstr "uszkodzony dokument"
 
-#: templates/wiki/book_detail.html:60
+#: templates/catalogue/book_detail.html:61
 msgid "Apply fixes"
 msgstr "Wykonaj zmiany"
 
 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"
 
 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"
 
 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"
 
 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"
 
 msgid "HTML version"
 msgstr "Wersja HTML"
 
-#: templates/wiki/book_detail.html:74
+#: templates/catalogue/book_detail.html:75
 msgid "TXT version"
 msgstr "Wersja TXT"
 
 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"
 
 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"
 
 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"
 
 msgid "Save"
 msgstr "Zapisz"
 
-#: templates/wiki/chunk_add.html:8
+#: templates/catalogue/chunk_add.html:9
 msgid "Add chunk"
 msgstr "Dodaj część"
 
 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"
 
 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"
 
 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."
 
 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"
 
 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."
 
 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"
 
 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"
 
 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."
 
 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"
 
 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"
 
 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>."
 
 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"
 
 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"
 
 #~ msgid "First correction"
 #~ msgstr "Autokorekta"
index 6836d36..4f15cef 100755 (executable)
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 
 # -*- coding: utf-8 -*-
 
+from collections import defaultdict
 import json
 from optparse import make_option
 import urllib2
 import json
 from optparse import make_option
 import urllib2
@@ -35,9 +36,9 @@ class Command(BaseCommand):
         transaction.managed(True)
 
         if verbose:
         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')
             if verbose:
                 print b.slug
             text = b.materialize().encode('utf-8')
@@ -46,28 +47,49 @@ class Command(BaseCommand):
             except (ParseError, ValidationError):
                 pass
             else:
             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',
 
         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'
         }
 
         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)
             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:
                 comm = previous_book.slug
             else:
+                previous_book = None
                 comm = '*'
             print book_count, info.slug , '-->', comm
             Book.import_xml_text(xml_text, title=info.title,
                 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
             book_count += 1
 
         # Print results
index 4747591..00014df 100755 (executable)
@@ -24,51 +24,6 @@ def common_prefix(texts):
     return "".join(common)
 
 
     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',
 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.'),
             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]...'
 
     )
     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()
 
     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')
         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 "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
 
                 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)
 
         # 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])
         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
 
         elif common_slug.startswith(new_slug):
             common_slug = new_slug
 
+        if slugs[0] != new_slug and Book.objects.filter(slug=new_slug).exists():
+            self.style.ERROR('Book already exists, skipping!')
+
+
         if dry_run and verbose:
             print self.style.NOTICE('DRY RUN: nothing will be changed.')
             print
         if 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:
                     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:
                 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
 
 from south.v2 import SchemaMigration
 from django.db import models
 
-
 class Migration(SchemaMigration):
 
     def forwards(self, orm):
 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)),
             ('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'])
 
         ))
         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)),
             ('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)),
         ))
             ('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)),
             ('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'])),
             ('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'])),
             ('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'])
 
         ))
         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)),
         # 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'])),
         ))
             ('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'])),
         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'])
 
         ))
         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):
         
 
     def backwards(self, orm):
         
@@ -164,6 +166,10 @@ class Migration(SchemaMigration):
         },
         'catalogue.book': {
             'Meta': {'ordering': "['parent_number', 'title']", 'object_name': 'Book'},
         },
         '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']"}),
             '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'},
         },
         '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'},
             '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']"}),
             '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'}),
             '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': {
             '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']"}),
         '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': {
             '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" />
 <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>
     <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>
 
 
 </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>
 {% 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 %}
 {% 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 %}
 
 {% 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 %}
 {% 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>
 
             <!--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>
         {% 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 __future__ import absolute_import
 
-from django.db.models import Count, Q
 from django.core.urlresolvers import reverse
 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 django import template
 from django.utils.translation import ugettext as _
 
-from catalogue.models import Book, Chunk, BookPublishRecord
-
 register = template.Library()
 
 
 register = template.Library()
 
 
@@ -32,6 +27,7 @@ def main_tabs(context):
     if user.is_authenticated():
         tabs.append(Tab('my', _('My page'), reverse("catalogue_user")))
 
     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")))
     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}
 
 
     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'^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'),
 
     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.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 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
 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")
 
 
 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
 @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
 
 
 @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),
         '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')
 
 
 @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)
 @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
                 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'],
                 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:
 
             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[:]
             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()
 
         # 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
 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.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 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):
 
 
 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)
     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']
     class Meta:
         abstract = True
         ordering = ['ordering']
+        verbose_name = _("tag")
+        verbose_name_plural = _("tags")
 
     def __unicode__(self):
         return self.name
 
     def __unicode__(self):
         return self.name
@@ -57,8 +58,6 @@ class Tag(models.Model):
 models.signals.pre_save.connect(Tag.listener_changed, sender=Tag)
 
 
 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)
 
 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.
     """
         
         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,
 
     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,
                         related_name="children")
 
     merge_parent = models.ForeignKey('self',
                         null=True, blank=True, default=None,
+                        verbose_name=_('merge parent'),
                         related_name="merge_children")
 
                         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)
     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']
 
     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)
 
     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)
 
         """ 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'
 
 def create_tag_model(model):
     name = model.__name__ + 'Tag'
+
+    class Meta(Tag.Meta):
+        app_label = model._meta.app_label
+
     attrs = {
         '__module__': model.__module__,
     attrs = {
         '__module__': model.__module__,
+        'Meta': Meta,
     }
     return type(name, (Tag,), attrs)
 
 
 def create_change_model(model):
     name = model.__name__ + 'Change'
     }
     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__,
 
     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)
 
 
     }
     return type(name, (Change,), attrs)
 
 
-
 class DocumentMeta(ModelBase):
     "Metaclass for Document models."
     def __new__(cls, name, bases, 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)
         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)
                 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,
             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')
 
                     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):
 
 
 class Document(models.Model):
-    """
-        File in repository.        
-    """
+    """File in repository. Subclass it to use version control in your app."""
+
     __metaclass__ = DocumentMeta
 
     __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
 
     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)
 
     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''
     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()
 
             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:
         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'])
 
             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
         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', ''),
                     author_name=author_name,
                     author_email=author_email,
                     description=kwargs.get('description', ''),
+                    publishable=publishable,
                     parent=parent)
 
         change.tags = tags
                     parent=parent)
 
         change.tags = tags
@@ -265,6 +301,9 @@ class Document(models.Model):
         else:
             self.head = change
         self.save()
         else:
             self.head = change
         self.save()
+
+        post_commit.send(sender=self.head)
+
         return self.head
 
     def history(self):
         return self.head
 
     def history(self):
@@ -280,8 +319,8 @@ class Document(models.Model):
         return self.change_set.get(revision=rev)
 
     def publishable(self):
         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
         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:
         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")})
             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
 ] + sys.path
 
 # Run Django
+os.environ["CELERY_LOADER"] = "django"
 os.environ['DJANGO_SETTINGS_MODULE'] = '$PROJECT_NAME.settings'
 
 from django.core.handlers.wsgi import WSGIHandler
 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 = (
 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'),
 )
 
     (u'Radek Czajka', 'radoslaw.czajka@nowoczesnapolska.org.pl'),
 )
 
@@ -116,6 +114,8 @@ INSTALLED_APPS = (
     'filebrowser',
     'pagination',
     'gravatar',
     'filebrowser',
     'pagination',
     'gravatar',
+    'djcelery',
+    'djkombu',
 
     'catalogue',
     'dvcs',
 
     'catalogue',
     'dvcs',
@@ -137,12 +137,10 @@ FILEBROWSER_DEFAULT_ORDER = "path_relative"
 IMAGE_DIR = 'images'
 
 
 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
 
 
 SHOW_APP_VERSION = False
 
index db72c00..e4b5b08 100644 (file)
@@ -13,11 +13,11 @@ COMPRESS_CSS = {
         ),
         'output_filename': 'compressed/detail_styles_?.css',
     },
         ),
         'output_filename': 'compressed/detail_styles_?.css',
     },
-    'listing': {
+    'catalogue': {
         'source_filenames': (
             'css/filelist.css',
         ),
         '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',
      },
         ),
         'output_filename': 'compressed/detail_scripts_?.js',
      },
-    'listing': {
+    'catalogue': {
         'source_filenames': (
         'source_filenames': (
-                'js/lib/jquery-1.4.2.min.js',
+                'js/catalogue.js',
                 'js/slugify.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
 
 
 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'
 
 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),
 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
 nose
 nosexcover
index c5d6b04..5e94254 100644 (file)
@@ -15,6 +15,8 @@ sorl-thumbnail>=3.2
 django-maintenancemode>=0.9
 django-pagination
 django-gravatar
 django-maintenancemode>=0.9
 django-pagination
 django-gravatar
+django-celery
+django-kombu
 
 # migrations
 south>=0.6
 
 # migrations
 south>=0.6