Merge with master.
authorRadek Czajka <radekczajka@nowoczesnapolska.org.pl>
Wed, 26 Mar 2014 15:32:58 +0000 (16:32 +0100)
committerRadek Czajka <radekczajka@nowoczesnapolska.org.pl>
Wed, 26 Mar 2014 15:32:58 +0000 (16:32 +0100)
39 files changed:
1  2 
apps/catalogue/admin.py
apps/catalogue/forms.py
apps/catalogue/locale/pl/LC_MESSAGES/django.mo
apps/catalogue/locale/pl/LC_MESSAGES/django.po
apps/catalogue/migrations/0012_auto__add_imagepublishrecord__add_imagechange__add_unique_imagechange_.py
apps/catalogue/models/__init__.py
apps/catalogue/models/image.py
apps/catalogue/templates/catalogue/activity.html
apps/catalogue/templates/catalogue/base.html
apps/catalogue/templates/catalogue/book_detail.html
apps/catalogue/templates/catalogue/book_list/book_list.html
apps/catalogue/templates/catalogue/chunk_edit.html
apps/catalogue/templates/catalogue/document_list.html
apps/catalogue/templates/catalogue/document_upload.html
apps/catalogue/templates/catalogue/image_detail.html
apps/catalogue/templates/catalogue/image_short.html
apps/catalogue/templates/catalogue/my_page.html
apps/catalogue/templates/catalogue/user_list.html
apps/catalogue/templates/catalogue/wall.html
apps/catalogue/templatetags/book_list.py
apps/catalogue/templatetags/catalogue.py
apps/catalogue/urls.py
apps/catalogue/views.py
apps/dvcs/models.py
apps/wiki/templates/wiki/document_details_base.html
apps/wiki/templates/wiki/tabs/gallery_view.html
apps/wiki/templates/wiki/tabs/summary_view.html
apps/wiki_img/templates/wiki_img/document_details_base.html
apps/wiki_img/templates/wiki_img/tabs/history_view.html
apps/wiki_img/urls.py
apps/wiki_img/views.py
lib/librarian
redakcja/settings/common.py
redakcja/settings/compress.py
redakcja/static/css/filelist.css
redakcja/static/css/master.css
redakcja/static/js/lib/jquery/jquery.imgareaselect.js
redakcja/static/js/wiki/loader.js
redakcja/urls.py

diff --combined apps/catalogue/admin.py
@@@ -3,13 -3,14 +3,16 @@@ from django.contrib import admi
  from catalogue import models
  
  class BookAdmin(admin.ModelAdmin):
+     list_display = ['title', 'public', '_published', '_new_publishable', 'project']
+     list_filter = ['public', '_published', '_new_publishable', 'project']
      prepopulated_fields = {'slug': ['title']}
      search_fields = ['title']
  
  
+ admin.site.register(models.Project)
  admin.site.register(models.Book, BookAdmin)
  admin.site.register(models.Chunk)
 -
  admin.site.register(models.Chunk.tag_model)
 +
 +admin.site.register(models.Image)
 +admin.site.register(models.Image.tag_model)
diff --combined apps/catalogue/forms.py
@@@ -9,7 -9,7 +9,7 @@@ from django import form
  from django.utils.translation import ugettext_lazy as _
  
  from catalogue.constants import MASTERS
 -from catalogue.models import Book, Chunk
 +from catalogue.models import Book, Chunk, Image
  
  class DocumentCreateForm(forms.ModelForm):
      """
@@@ -20,7 -20,7 +20,7 @@@
  
      class Meta:
          model = Book
-         exclude = ['parent', 'parent_number']
+         exclude = ['parent', 'parent_number', 'project']
  
      def __init__(self, *args, **kwargs):
          super(DocumentCreateForm, self).__init__(*args, **kwargs)
@@@ -72,7 -72,7 +72,7 @@@ class ChunkForm(forms.ModelForm)
      """
      user = forms.ModelChoiceField(queryset=
          User.objects.annotate(count=Count('chunk')).
-         order_by('-count', 'last_name', 'first_name'), required=False,
+         order_by('last_name', 'first_name'), required=False,
          label=_('Assigned to')) 
  
      class Meta:
@@@ -130,6 -130,7 +130,7 @@@ class BookForm(forms.ModelForm)
  
      class Meta:
          model = Book
+         exclude = ['project']
  
      def __init__(self, *args, **kwargs):
          ret = super(BookForm, self).__init__(*args, **kwargs)
@@@ -144,7 -145,7 +145,7 @@@ class ReadonlyBookForm(BookForm)
      def __init__(self, *args, **kwargs):
          ret = super(ReadonlyBookForm, self).__init__(*args, **kwargs)
          for field in self.fields.values():
 -            field.widget.attrs.update({"readonly": True})
 +            field.widget.attrs.update({"disabled": "disabled"})
          return ret
  
  
@@@ -154,30 -155,3 +155,30 @@@ class ChooseMasterForm(forms.Form)
      """
  
      master = forms.ChoiceField(choices=((m, m) for m in MASTERS))
 +
 +
 +class ImageForm(forms.ModelForm):
 +    """Form used for editing an Image."""
 +    user = forms.ModelChoiceField(queryset=
 +        User.objects.annotate(count=Count('chunk')).
 +        order_by('-count', 'last_name', 'first_name'), required=False,
 +        label=_('Assigned to')) 
 +
 +    class Meta:
 +        model = Image
 +        fields = ['title', 'slug', 'user', 'stage']
 +
 +    def __init__(self, *args, **kwargs):
 +        super(ImageForm, self).__init__(*args, **kwargs)
 +        self.fields['slug'].widget.attrs={'class': 'autoslug'}
 +        self.fields['title'].widget.attrs={'class': 'autoslug-source'}
 +
 +
 +class ReadonlyImageForm(ImageForm):
 +    """Form used for not editing a Book."""
 +
 +    def __init__(self, *args, **kwargs):
 +        ret = super(ReadonlyImageForm, self).__init__(*args, **kwargs)
 +        for field in self.fields.values():
 +            field.widget.attrs.update({"disabled": "disabled"})
 +        return ret
index d933b5a,b6a12e3..64f78b4
Binary files differ
@@@ -7,15 -7,18 +7,18 @@@ msgid "
  msgstr ""
  "Project-Id-Version: Platforma Redakcyjna\n"
  "Report-Msgid-Bugs-To: \n"
- "POT-Creation-Date: 2011-12-21 12:44+0100\n"
- "PO-Revision-Date: 2011-12-21 12:45+0100\n"
 -"POT-Creation-Date: 2013-07-16 13:22+0200\n"
 -"PO-Revision-Date: 2013-07-16 13:22+0100\n"
++"POT-Creation-Date: 2014-03-26 16:13+0100\n"
++"PO-Revision-Date: 2014-03-26 16:14+0100\n"
  "Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
- "Language-Team: Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org.pl>\n"
+ "Language-Team: Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org."
+ "pl>\n"
  "Language: pl\n"
  "MIME-Version: 1.0\n"
  "Content-Type: text/plain; charset=UTF-8\n"
  "Content-Transfer-Encoding: 8bit\n"
- "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
+ "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+ "|| n%100>=20) ? 1 : 2);\n"
+ "X-Generator: Poedit 1.5.4\n"
  
  #: forms.py:39
  msgid "Text file must be UTF-8 encoded."
@@@ -33,13 -36,11 +36,11 @@@ msgstr "Plik ZIP
  msgid "Directories are documents in chunks"
  msgstr "Katalogi zawierają dokumenty w częściach"
  
--#: forms.py:76
- #: forms.py:164
++#: forms.py:76 forms.py:165
  msgid "Assigned to"
  msgstr "Przypisane do"
  
- #: forms.py:97
- #: forms.py:111
+ #: forms.py:97 forms.py:111
  msgid "Chunk with this slug already exists"
  msgstr "Część z tym slugiem już istnieje"
  
  msgid "Append to"
  msgstr "Dołącz do"
  
- #: views.py:165
 -#: views.py:160
++#: views.py:166
  #, python-format
  msgid "Slug already used for %s"
  msgstr "Slug taki sam jak dla pliku %s"
  
- #: views.py:167
 -#: views.py:162
++#: views.py:168
  msgid "Slug already used in repository."
  msgstr "Dokument o tym slugu już istnieje w repozytorium."
  
- #: views.py:173
 -#: views.py:168
++#: views.py:174
  msgid "File should be UTF-8 encoded."
  msgstr "Plik powinien mieć kodowanie UTF-8."
  
- #: models/book.py:21
- #: models/chunk.py:23
- #: models/image.py:21
 -#: views.py:498 models/book.py:56
++#: views.py:552 models/book.py:56
+ msgid "books"
+ msgstr "książki"
 -#: views.py:500
++#: views.py:554
+ msgid "scan gallery"
+ msgstr "galeria skanów"
 -#: models/book.py:28 models/chunk.py:23
++#: models/book.py:28 models/chunk.py:23 models/image.py:21
  msgid "title"
  msgstr "tytuł"
  
- #: models/book.py:22
- #: models/chunk.py:24
- #: models/image.py:22
 -#: models/book.py:29 models/chunk.py:24
++#: models/book.py:29 models/chunk.py:24 models/image.py:22
  msgid "slug"
  msgstr "slug"
  
- #: models/book.py:23
- #: models/image.py:23
 -#: models/book.py:30
++#: models/book.py:30 models/image.py:23
  msgid "public"
  msgstr "publiczna"
  
- #: models/book.py:24
+ #: models/book.py:31
  msgid "scan gallery name"
  msgstr "nazwa galerii skanów"
  
- #: models/book.py:27
+ #: models/book.py:35
  msgid "parent"
  msgstr "rodzic"
  
- #: models/book.py:28
+ #: models/book.py:36
  msgid "parent number"
  msgstr "numeracja rodzica"
  
- #: models/book.py:44
- #: models/chunk.py:21
- #: models/publish_log.py:17
+ #: models/book.py:55 models/chunk.py:21 models/publish_log.py:17
  msgid "book"
  msgstr "książka"
  
- #: models/book.py:45
- msgid "books"
- msgstr "książki"
- #: models/book.py:220
+ #: models/book.py:255
  msgid "No chunks in the book."
  msgstr "Książka nie ma części."
  
- #: models/book.py:224
+ #: models/book.py:259
  msgid "Not all chunks have publishable revisions."
  msgstr "Niektóre części nie są gotowe do publikacji."
  
- #: models/book.py:233
- #: models/image.py:80
 -#: models/book.py:266
++#: models/book.py:266 models/image.py:80
  msgid "Invalid XML"
  msgstr "Nieprawidłowy XML"
  
- #: models/book.py:235
- #: models/image.py:82
 -#: models/book.py:268
++#: models/book.py:268 models/image.py:82
  msgid "No Dublin Core found."
  msgstr "Brak sekcji Dublin Core."
  
- #: models/book.py:237
- #: models/image.py:84
 -#: models/book.py:270
++#: models/book.py:270 models/image.py:84
  msgid "Invalid Dublin Core"
  msgstr "Nieprawidłowy Dublin Core"
  
- #: models/book.py:240
- #: models/image.py:88
 -#: models/book.py:273
++#: models/book.py:273 models/image.py:88
  msgid "rdf:about is not"
  msgstr "rdf:about jest różny od"
  
@@@ -143,33 -137,31 +137,44 @@@ msgstr "część
  msgid "chunks"
  msgstr "części"
  
- #: models/image.py:20
- #: models/image.py:34
- #: models/publish_log.py:45
++#: models/image.py:20 models/image.py:34 models/publish_log.py:45
 +msgid "image"
 +msgstr "obraz"
 +
 +#: models/image.py:35
 +msgid "images"
 +msgstr "obrazy"
 +
 +#: models/image.py:73
 +msgid "There is no publishable revision"
 +msgstr "Żadna wersja nie została oznaczona do publikacji."
 +
- #: models/publish_log.py:18
- #: models/publish_log.py:46
+ #: models/project.py:13
+ msgid "name"
+ msgstr "nazwa"
+ #: models/project.py:14
+ msgid "notes"
+ msgstr "notatki"
 -#: models/project.py:19 templates/catalogue/book_list/book_list.html:62
++#: models/project.py:19 templates/catalogue/book_list/book_list.html:64
+ msgid "project"
+ msgstr "projekt"
+ #: models/project.py:20
+ msgid "projects"
+ msgstr "projekty"
 -#: models/publish_log.py:18
++#: models/publish_log.py:18 models/publish_log.py:46
  msgid "time"
  msgstr "czas"
  
- #: models/publish_log.py:19
- #: models/publish_log.py:47
- #: templates/catalogue/wall.html:18
 -#: models/publish_log.py:19 templates/catalogue/wall.html:18
++#: models/publish_log.py:19 models/publish_log.py:47
++#: templates/catalogue/wall.html:19
  msgid "user"
  msgstr "użytkownik"
  
- #: models/publish_log.py:24
- #: models/publish_log.py:33
+ #: models/publish_log.py:24 models/publish_log.py:33
  msgid "book publish record"
  msgstr "zapis publikacji książki"
  
  msgid "book publish records"
  msgstr "zapisy publikacji książek"
  
--#: models/publish_log.py:34
- #: models/publish_log.py:48
++#: models/publish_log.py:34 models/publish_log.py:48
  msgid "change"
  msgstr "zmiana"
  
@@@ -190,16 -181,7 +194,16 @@@ msgstr "zapis publikacji części
  msgid "chunk publish records"
  msgstr "zapisy publikacji części"
  
 -#: templates/catalogue/activity.html:9 templatetags/catalogue.py:29
 +#: models/publish_log.py:53
 +msgid "image publish record"
 +msgstr "zapis publikacji obrazu"
 +
 +#: models/publish_log.py:54
 +msgid "image publish records"
 +msgstr "zapisy publikacji obrazów"
 +
- #: templates/catalogue/activity.html:10
++#: templates/catalogue/activity.html:6 templates/catalogue/activity.html:12
 +#: templatetags/catalogue.py:29
  msgid "Activity"
  msgstr "Aktywność"
  
  msgid "Platforma Redakcyjna"
  msgstr "Platforma Redakcyjna"
  
--#: templates/catalogue/book_append_to.html:9
++#: templates/catalogue/book_append_to.html:4
++#: templates/catalogue/book_append_to.html:11
  msgid "Append book"
  msgstr "Dołącz książkę"
  
--#: templates/catalogue/book_detail.html:14
- #: templates/catalogue/book_edit.html:9
- #: templates/catalogue/chunk_edit.html:13
- #: templates/catalogue/image_detail.html:14
 -#: templates/catalogue/book_edit.html:9 templates/catalogue/chunk_edit.html:12
++#: templates/catalogue/book_detail.html:18
++#: templates/catalogue/book_edit.html:13
++#: templates/catalogue/chunk_edit.html:16
++#: templates/catalogue/image_detail.html:18
  msgid "Save"
  msgstr "Zapisz"
  
--#: templates/catalogue/book_detail.html:21
++#: templates/catalogue/book_detail.html:25
+ msgid "Edit gallery"
+ msgstr "Edytuj galerię"
 -#: templates/catalogue/book_detail.html:24
++#: templates/catalogue/book_detail.html:28
  msgid "Append to other book"
  msgstr "Dołącz do innej książki"
  
- #: templates/catalogue/book_detail.html:27
 -#: templates/catalogue/book_detail.html:30
++#: templates/catalogue/book_detail.html:34
  msgid "Chunks"
  msgstr "Części"
  
- #: templates/catalogue/book_detail.html:42
- #: templates/catalogue/image_detail.html:32
- #: templatetags/wall.py:78
 -#: templates/catalogue/book_detail.html:45 templatetags/wall.py:78
++#: templates/catalogue/book_detail.html:49
++#: templates/catalogue/image_detail.html:36 templatetags/wall.py:78
  msgid "Publication"
  msgstr "Publikacja"
  
- #: templates/catalogue/book_detail.html:44
- #: templates/catalogue/image_detail.html:34
 -#: templates/catalogue/book_detail.html:54
++#: templates/catalogue/book_detail.html:58
++#: templates/catalogue/image_detail.html:38
  msgid "Last published"
  msgstr "Ostatnio opublikowano"
  
- #: templates/catalogue/book_detail.html:54
 -#: templates/catalogue/book_detail.html:64
++#: templates/catalogue/book_detail.html:68
  msgid "Full XML"
  msgstr "Pełny XML"
  
- #: templates/catalogue/book_detail.html:55
 -#: templates/catalogue/book_detail.html:65
++#: templates/catalogue/book_detail.html:69
  msgid "HTML version"
  msgstr "Wersja HTML"
  
- #: templates/catalogue/book_detail.html:56
 -#: templates/catalogue/book_detail.html:66
++#: templates/catalogue/book_detail.html:70
  msgid "TXT version"
  msgstr "Wersja TXT"
  
- #: templates/catalogue/book_detail.html:57
 -#: templates/catalogue/book_detail.html:67
++#: templates/catalogue/book_detail.html:71
  msgid "PDF version"
  msgstr "Wersja PDF"
  
- #: templates/catalogue/book_detail.html:58
 -#: templates/catalogue/book_detail.html:68
++#: templates/catalogue/book_detail.html:72
  msgid "EPUB version"
  msgstr "Wersja EPUB"
  
- #: templates/catalogue/book_detail.html:71
- #: templates/catalogue/image_detail.html:53
 -#: templates/catalogue/book_detail.html:81
++#: templates/catalogue/book_detail.html:85
++#: templates/catalogue/image_detail.html:57
  msgid "Publish"
  msgstr "Opublikuj"
  
- #: templates/catalogue/book_detail.html:75
- #: templates/catalogue/image_detail.html:57
 -#: templates/catalogue/book_detail.html:85
++#: templates/catalogue/book_detail.html:89
++#: templates/catalogue/image_detail.html:61
  msgid "Log in to publish."
  msgstr "Zaloguj się, aby opublikować."
  
- #: templates/catalogue/book_detail.html:78
- #: templates/catalogue/image_detail.html:60
 -#: templates/catalogue/book_detail.html:88
++#: templates/catalogue/book_detail.html:92
++#: templates/catalogue/image_detail.html:64
  msgid "This book can't be published yet, because:"
  msgstr "Ta książka nie może jeszcze zostać opublikowana. Powód:"
  
- #: templates/catalogue/book_detail.html:87
- #: templates/catalogue/image_detail.html:68
 -#: templates/catalogue/book_detail.html:98
++#: templates/catalogue/book_detail.html:102
++#: templates/catalogue/image_detail.html:72
  msgid "Comments"
  msgstr "Komentarze"
  
- #: templates/catalogue/book_html.html:13
 -#: templates/catalogue/book_text.html:7
 -msgid "Redakcja"
 -msgstr ""
++#: templates/catalogue/book_edit.html:5
++msgid "Edit book"
++msgstr "Edytuj książkę"
 -#: templates/catalogue/book_text.html:15
++#: templates/catalogue/book_html.html:13 templates/catalogue/book_text.html:15
  msgid "Table of contents"
  msgstr "Spis treści"
  
- #: templates/catalogue/book_html.html:14
 -#: templates/catalogue/book_text.html:17
++#: templates/catalogue/book_html.html:14 templates/catalogue/book_text.html:17
  msgid "Edit. note"
  msgstr "Nota red."
  
 -#: templates/catalogue/chunk_add.html:5 templates/catalogue/chunk_edit.html:18
 +#: templates/catalogue/book_html.html:15
 +msgid "Infobox"
 +msgstr "Informacje"
 +
- #: templates/catalogue/chunk_add.html:5
- #: templates/catalogue/chunk_edit.html:19
++#: templates/catalogue/book_text.html:7
++msgid "Redakcja"
++msgstr ""
++
++#: templates/catalogue/chunk_add.html:5 templates/catalogue/chunk_add.html:9
++#: templates/catalogue/chunk_edit.html:22
  msgid "Split chunk"
  msgstr "Podziel część"
  
--#: templates/catalogue/chunk_add.html:10
++#: templates/catalogue/chunk_add.html:14
  msgid "Insert empty chunk after"
  msgstr "Wstaw pustą część po"
  
--#: templates/catalogue/chunk_add.html:13
++#: templates/catalogue/chunk_add.html:17
  msgid "Add chunk"
  msgstr "Dodaj część"
  
- #: templates/catalogue/chunk_edit.html:6
- #: templates/catalogue/book_list/book.html:7
- #: templates/catalogue/book_list/chunk.html:5
 -#: templates/catalogue/chunk_edit.html:5
++#: templates/catalogue/chunk_edit.html:5 templates/catalogue/chunk_edit.html:9
+ #: templates/catalogue/book_list/book.html:8
+ #: templates/catalogue/book_list/chunk.html:6
  msgid "Chunk settings"
  msgstr "Ustawienia części"
  
- #: templates/catalogue/chunk_edit.html:11
 -#: templates/catalogue/chunk_edit.html:10
++#: templates/catalogue/chunk_edit.html:14
  msgid "Book"
  msgstr "Książka"
  
  #: templates/catalogue/document_create_missing.html:5
++#: templates/catalogue/document_create_missing.html:9
  msgid "Create a new book"
  msgstr "Utwórz nową książkę"
  
--#: templates/catalogue/document_create_missing.html:11
++#: templates/catalogue/document_create_missing.html:15
  msgid "Create book"
  msgstr "Utwórz książkę"
  
--#: templates/catalogue/document_upload.html:8
--msgid "Bulk documents upload"
++#: templates/catalogue/document_list.html:7
++msgid "Book list"
++msgstr "Lista książek"
++
++#: templates/catalogue/document_upload.html:5
++msgid "Bulk document upload"
  msgstr "Hurtowe dodawanie dokumentów"
  
  #: 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 "Bulk documents upload"
++msgstr "Hurtowe dodawanie dokumentów"
++
++#: templates/catalogue/document_upload.html:14
+ 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/catalogue/document_upload.html:17
- #: templates/catalogue/upload_pdf.html:13
- #: templatetags/catalogue.py:36
 -#: templates/catalogue/document_upload.html:17 templatetags/catalogue.py:35
++#: templates/catalogue/document_upload.html:20
++#: templates/catalogue/upload_pdf.html:16 templatetags/catalogue.py:36
  msgid "Upload"
  msgstr "Załaduj"
  
--#: templates/catalogue/document_upload.html:24
- msgid "There have been some errors. No files have been added to the repository."
++#: templates/catalogue/document_upload.html:27
+ 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/catalogue/document_upload.html:25
++#: templates/catalogue/document_upload.html:28
  msgid "Offending files"
  msgstr "Błędne pliki"
  
--#: templates/catalogue/document_upload.html:33
++#: templates/catalogue/document_upload.html:36
  msgid "Correct files"
  msgstr "Poprawne pliki"
  
--#: templates/catalogue/document_upload.html:44
++#: templates/catalogue/document_upload.html:47
  msgid "Files have been successfully uploaded to the repository."
  msgstr "Pliki zostały dodane do repozytorium."
  
--#: templates/catalogue/document_upload.html:45
++#: templates/catalogue/document_upload.html:48
  msgid "Uploaded files"
  msgstr "Dodane pliki"
  
--#: templates/catalogue/document_upload.html:55
++#: templates/catalogue/document_upload.html:58
  msgid "Skipped files"
  msgstr "Pominięte pliki"
  
--#: templates/catalogue/document_upload.html:56
++#: templates/catalogue/document_upload.html:59
  msgid "Files skipped due to no <code>.xml</code> extension"
  msgstr "Pliki pominięte z powodu braku rozszerzenia <code>.xml</code>."
  
- #: templates/catalogue/image_detail.html:22
 -#: templates/catalogue/my_page.html:21
 -msgid "Your last edited documents"
 -msgstr "Twoje ostatnie edycje"
++#: templates/catalogue/image_detail.html:26
 +msgid "Editor"
 +msgstr "Edytor"
  
- #: templates/catalogue/image_detail.html:24
 -#: templates/catalogue/my_page.html:30 templates/catalogue/user_page.html:13
 -msgid "Recent activity for"
 -msgstr "Ostatnia aktywność dla:"
++#: templates/catalogue/image_detail.html:28
 +msgid "Proceed to the editor."
 +msgstr "Przejdź do edytora."
  
 -#: templates/catalogue/user_list.html:7 templatetags/catalogue.py:31
 -msgid "Users"
 -msgstr "Użytkownicy"
 -
 -#: templates/catalogue/wall.html:28
 -msgid "not logged in"
 -msgstr "nie zalogowany"
++#: templates/catalogue/image_list.html:7
++msgid "Image list"
++msgstr "Lista obrazów"
 -#: templates/catalogue/wall.html:33
 -msgid "No activity recorded."
 -msgstr "Nie zanotowano aktywności."
 -
 -#: templates/catalogue/book_list/book.html:7
 -#: templates/catalogue/book_list/book.html:28
 -msgid "Book settings"
 -msgstr "Ustawienia książki"
 -
 -#: templates/catalogue/book_list/book_list.html:22
 -msgid "Show hidden books"
 -msgstr "Pokaż ukryte książki"
 +#: templates/catalogue/image_short.html:4
 +msgid "Image settings"
 +msgstr "Ustawienia obrazu"
  
- #: templates/catalogue/book_list/book_list.html:24
 +#: templates/catalogue/image_table.html:19
+ #: templates/catalogue/book_list/book_list.html:27
  msgid "Search in book titles"
  msgstr "Szukaj w tytułach książek"
  
- #: templates/catalogue/book_list/book_list.html:29
 +#: templates/catalogue/image_table.html:24
+ #: templates/catalogue/book_list/book_list.html:32
  msgid "stage"
  msgstr "etap"
  
- #: templates/catalogue/book_list/book_list.html:31
- #: templates/catalogue/book_list/book_list.html:42
 +#: templates/catalogue/image_table.html:26
 +#: templates/catalogue/image_table.html:37
 -#: templates/catalogue/book_list/book_list.html:64
+ #: templates/catalogue/book_list/book_list.html:34
+ #: templates/catalogue/book_list/book_list.html:45
++#: templates/catalogue/book_list/book_list.html:66
  msgid "none"
  msgstr "brak"
  
- #: templates/catalogue/book_list/book_list.html:40
 +#: templates/catalogue/image_table.html:35
+ #: templates/catalogue/book_list/book_list.html:43
  msgid "editor"
  msgstr "redaktor"
  
 -#: templates/catalogue/book_list/book_list.html:54
 +#: templates/catalogue/image_table.html:46
- #: templates/catalogue/book_list/book_list.html:51
++#: templates/catalogue/book_list/book_list.html:56
  msgid "status"
  msgstr "status"
  
 -#: templates/catalogue/book_list/book_list.html:88
 +#: templates/catalogue/image_table.html:63
 +#, python-format
 +msgid "%(c)s image"
 +msgid_plural "%(c)s images"
 +msgstr[0] "%(c)s obraz"
 +msgstr[1] "%(c)s obrazy"
 +msgstr[2] "%(c)s obrazów"
 +
 +#: templates/catalogue/image_table.html:68
 +msgid "No images found."
 +msgstr "Nie znaleziono obrazów."
 +
- #: templates/catalogue/my_page.html:13
++#: templates/catalogue/my_page.html:15 templatetags/catalogue.py:27
++msgid "My page"
++msgstr "Moja strona"
++
++#: templates/catalogue/my_page.html:24
 +msgid "Your last edited documents"
 +msgstr "Twoje ostatnie edycje"
 +
- #: templates/catalogue/my_page.html:22
- #: templates/catalogue/user_page.html:13
++#: templates/catalogue/my_page.html:33 templates/catalogue/user_page.html:16
 +msgid "Recent activity for"
 +msgstr "Ostatnia aktywność dla:"
 +
- #: templates/catalogue/upload_pdf.html:8
++#: templates/catalogue/upload_pdf.html:5
++#: templates/catalogue/upload_pdf.html:11
 +msgid "PDF file upload"
- msgstr ""
++msgstr "Ładowanie pliku PDF"
 +
- #: templates/catalogue/user_list.html:7
++#: templates/catalogue/user_list.html:6 templates/catalogue/user_list.html:11
 +#: templatetags/catalogue.py:32
 +msgid "Users"
 +msgstr "Użytkownicy"
 +
- #: templates/catalogue/wall.html:28
++#: templates/catalogue/wall.html:29
 +msgid "not logged in"
 +msgstr "nie zalogowany"
 +
- #: templates/catalogue/wall.html:33
++#: templates/catalogue/wall.html:34
 +msgid "No activity recorded."
 +msgstr "Nie zanotowano aktywności."
 +
- #: templates/catalogue/book_list/book.html:6
- #: templates/catalogue/book_list/book.html:25
++#: templates/catalogue/book_list/book.html:7
++#: templates/catalogue/book_list/book.html:28
 +msgid "Book settings"
 +msgstr "Ustawienia książki"
 +
- #: templates/catalogue/book_list/book_list.html:19
++#: templates/catalogue/book_list/book_list.html:22
 +msgid "Show hidden books"
 +msgstr "Pokaż ukryte książki"
 +
- #: templates/catalogue/book_list/book_list.html:75
++#: templates/catalogue/book_list/book_list.html:90
  #, python-format
  msgid "%(c)s book"
  msgid_plural "%(c)s books"
@@@ -456,51 -400,70 +494,70 @@@ msgstr[0] "%(c)s książka
  msgstr[1] "%(c)s książki"
  msgstr[2] "%(c)s książek"
  
- #: templates/catalogue/book_list/book_list.html:80
 -#: templates/catalogue/book_list/book_list.html:93
++#: templates/catalogue/book_list/book_list.html:95
  msgid "No books found."
  msgstr "Nie znaleziono książek."
  
- #: templatetags/book_list.py:84
- #: templatetags/book_list.py:145
 -#: templates/catalogue/book_list/book_list.html:99
++#: templates/catalogue/book_list/book_list.html:101
+ msgid "Set stage"
+ msgstr "Ustaw etap"
 -#: templates/catalogue/book_list/book_list.html:100
++#: templates/catalogue/book_list/book_list.html:102
+ msgid "Set user"
+ msgstr "Przypisz redaktora"
 -#: templates/catalogue/book_list/book_list.html:102
++#: templates/catalogue/book_list/book_list.html:104
+ msgid "Project"
+ msgstr "Projekt"
 -#: templates/catalogue/book_list/book_list.html:103
++#: templates/catalogue/book_list/book_list.html:105
+ msgid "Mark publishable"
+ msgstr "Oznacz do publikacji"
 -#: templates/catalogue/book_list/book_list.html:104
++#: templates/catalogue/book_list/book_list.html:106
+ msgid "Mark not publishable"
+ msgstr "Odznacz do publikacji"
 -#: templates/catalogue/book_list/book_list.html:105
++#: templates/catalogue/book_list/book_list.html:107
+ msgid "Other user"
+ msgstr "Inny użytkownik"
 -#: templatetags/book_list.py:84
++#: templatetags/book_list.py:84 templatetags/book_list.py:152
  msgid "publishable"
  msgstr "do publikacji"
  
--#: templatetags/book_list.py:85
- #: templatetags/book_list.py:146
++#: templatetags/book_list.py:85 templatetags/book_list.py:153
  msgid "changed"
  msgstr "zmienione"
  
--#: templatetags/book_list.py:86
- #: templatetags/book_list.py:147
++#: templatetags/book_list.py:86 templatetags/book_list.py:154
  msgid "published"
  msgstr "opublikowane"
  
--#: templatetags/book_list.py:87
- #: templatetags/book_list.py:148
++#: templatetags/book_list.py:87 templatetags/book_list.py:155
  msgid "unpublished"
  msgstr "nie opublikowane"
  
--#: templatetags/book_list.py:88
- #: templatetags/book_list.py:149
++#: templatetags/book_list.py:88 templatetags/book_list.py:156
  msgid "empty"
  msgstr "puste"
  
--#: templatetags/catalogue.py:27
--msgid "My page"
--msgstr "Moja strona"
--
  #: templatetags/catalogue.py:30
  msgid "All"
  msgstr "Wszystkie"
  
 -#: templatetags/catalogue.py:34
 +#: templatetags/catalogue.py:31
 +msgid "Images"
 +msgstr "Obrazy"
 +
 +#: templatetags/catalogue.py:35
  msgid "Add"
  msgstr "Dodaj"
  
 -#: templatetags/catalogue.py:37
++#: templatetags/catalogue.py:38
+ msgid "Covers"
+ msgstr "Okładki"
  #: templatetags/wall.py:49
  msgid "Related edit"
  msgstr "Powiązana zmiana"
@@@ -513,6 -476,9 +570,6 @@@ msgstr "Zmiana
  msgid "Comment"
  msgstr "Komentarz"
  
 -#~ msgid "Infobox"
 -#~ msgstr "Informacje"
 -
  #~ msgid "Admin"
  #~ msgstr "Administracja"
  
  #~ msgid "Describe the reason for reverting."
  #~ msgstr "Opisz powód przywrócenia."
  
- #~ msgid "name"
- #~ msgstr "nazwa"
  #~ msgid "theme"
  #~ msgstr "motyw"
  
  #~ msgid "Last edited by"
  #~ msgstr "Ostatnio edytowane przez"
  
- #~ msgid "Link to gallery"
- #~ msgstr "Link do galerii"
  #~ msgid "Summary"
  #~ msgstr "Podsumowanie"
  
index 0000000,0000000..599e103
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,270 @@@
++# -*- coding: utf-8 -*-
++import datetime
++from south.db import db
++from south.v2 import SchemaMigration
++from django.db import models
++
++
++class Migration(SchemaMigration):
++
++    def forwards(self, orm):
++        # Adding model 'ImagePublishRecord'
++        db.create_table(u'catalogue_imagepublishrecord', (
++            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
++            ('image', self.gf('django.db.models.fields.related.ForeignKey')(related_name='publish_log', to=orm['catalogue.Image'])),
++            ('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'])),
++            ('change', self.gf('django.db.models.fields.related.ForeignKey')(related_name='publish_log', to=orm['catalogue.ImageChange'])),
++        ))
++        db.send_create_signal('catalogue', ['ImagePublishRecord'])
++
++        # Adding model 'ImageChange'
++        db.create_table(u'catalogue_imagechange', (
++            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
++            ('author', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
++            ('author_name', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
++            ('author_email', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
++            ('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.ImageChange'])),
++            ('merge_parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='merge_children', null=True, blank=True, to=orm['catalogue.ImageChange'])),
++            ('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
++            ('created_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, db_index=True)),
++            ('publishable', self.gf('django.db.models.fields.BooleanField')(default=False)),
++            ('tree', self.gf('django.db.models.fields.related.ForeignKey')(related_name='change_set', to=orm['catalogue.Image'])),
++            ('data', self.gf('django.db.models.fields.files.FileField')(max_length=100)),
++        ))
++        db.send_create_signal('catalogue', ['ImageChange'])
++
++        # Adding M2M table for field tags on 'ImageChange'
++        db.create_table(u'catalogue_imagechange_tags', (
++            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
++            ('imagechange', models.ForeignKey(orm['catalogue.imagechange'], null=False)),
++            ('imagetag', models.ForeignKey(orm['catalogue.imagetag'], null=False))
++        ))
++        db.create_unique(u'catalogue_imagechange_tags', ['imagechange_id', 'imagetag_id'])
++
++        # Adding unique constraint on 'ImageChange', fields ['tree', 'revision']
++        db.create_unique(u'catalogue_imagechange', ['tree_id', 'revision'])
++
++        # Adding model 'ImageTag'
++        db.create_table(u'catalogue_imagetag', (
++            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
++            ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
++            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=64, unique=True, null=True, blank=True)),
++            ('ordering', self.gf('django.db.models.fields.IntegerField')()),
++        ))
++        db.send_create_signal('catalogue', ['ImageTag'])
++
++        # Adding model 'Image'
++        db.create_table(u'catalogue_image', (
++            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
++            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
++            ('image', self.gf('django.db.models.fields.files.FileField')(max_length=100)),
++            ('title', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
++            ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=50)),
++            ('public', self.gf('django.db.models.fields.BooleanField')(default=True, db_index=True)),
++            ('_short_html', self.gf('django.db.models.fields.TextField')(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)),
++            ('_changed', self.gf('django.db.models.fields.NullBooleanField')(null=True, blank=True)),
++            ('stage', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.ImageTag'], null=True, blank=True)),
++            ('head', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['catalogue.ImageChange'], null=True, blank=True)),
++            ('creator', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='created_image', null=True, to=orm['auth.User'])),
++        ))
++        db.send_create_signal('catalogue', ['Image'])
++
++
++    def backwards(self, orm):
++        # Removing unique constraint on 'ImageChange', fields ['tree', 'revision']
++        db.delete_unique(u'catalogue_imagechange', ['tree_id', 'revision'])
++
++        # Deleting model 'ImagePublishRecord'
++        db.delete_table(u'catalogue_imagepublishrecord')
++
++        # Deleting model 'ImageChange'
++        db.delete_table(u'catalogue_imagechange')
++
++        # Removing M2M table for field tags on 'ImageChange'
++        db.delete_table('catalogue_imagechange_tags')
++
++        # Deleting model 'ImageTag'
++        db.delete_table(u'catalogue_imagetag')
++
++        # Deleting model 'Image'
++        db.delete_table(u'catalogue_image')
++
++
++    models = {
++        u'auth.group': {
++            'Meta': {'object_name': 'Group'},
++            u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
++        },
++        u'auth.permission': {
++            'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
++            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
++            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
++            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
++        },
++        u'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': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
++            u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
++            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
++        },
++        'catalogue.book': {
++            'Meta': {'ordering': "['title', 'slug']", 'object_name': 'Book'},
++            '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
++            '_on_track': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', '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'}),
++            'dc_cover_image': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['cover.Image']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
++            'dc_slug': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '128', 'null': 'True', 'blank': 'True'}),
++            'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
++            u'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'}),
++            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Project']", 'null': 'True', 'blank': 'True'}),
++            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
++            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128'}),
++            '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']"}),
++            u'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': u"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_chunk'", 'null': 'True', 'to': u"orm['auth.User']"}),
++            'gallery_start': ('django.db.models.fields.IntegerField', [], {'default': '1', 'null': 'True', 'blank': 'True'}),
++            'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ChunkChange']", 'null': 'True', 'blank': 'True'}),
++            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'number': ('django.db.models.fields.IntegerField', [], {}),
++            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
++            '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': u"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': u"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'}),
++            u'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']"}),
++            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
++        },
++        'catalogue.chunktag': {
++            'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
++            u'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', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
++        },
++        'catalogue.image': {
++            'Meta': {'ordering': "['title']", 'object_name': 'Image'},
++            '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
++            '_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'}),
++            'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_image'", 'null': 'True', 'to': u"orm['auth.User']"}),
++            'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ImageChange']", 'null': 'True', 'blank': 'True'}),
++            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'image': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
++            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
++            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}),
++            'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ImageTag']", 'null': 'True', 'blank': 'True'}),
++            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
++            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
++        },
++        'catalogue.imagechange': {
++            'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ImageChange'},
++            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"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'}),
++            u'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.ImageChange']"}),
++            'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ImageChange']"}),
++            '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.ImageTag']"}),
++            'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Image']"})
++        },
++        'catalogue.imagepublishrecord': {
++            'Meta': {'ordering': "['-timestamp']", 'object_name': 'ImagePublishRecord'},
++            'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ImageChange']"}),
++            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'image': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Image']"}),
++            'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
++            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
++        },
++        'catalogue.imagetag': {
++            'Meta': {'ordering': "['ordering']", 'object_name': 'ImageTag'},
++            u'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', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
++        },
++        'catalogue.project': {
++            'Meta': {'ordering': "['name']", 'object_name': 'Project'},
++            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
++            'notes': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
++        },
++        u'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'}),
++            u'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'})
++        },
++        u'cover.image': {
++            'Meta': {'object_name': 'Image'},
++            'author': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
++            'download_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
++            'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
++            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
++            'license_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
++            'license_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
++            'source_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
++            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
++        }
++    }
++
++    complete_apps = ['catalogue']
@@@ -3,10 -3,9 +3,11 @@@
  # 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.project import Project
  from catalogue.models.chunk import Chunk
 -from catalogue.models.publish_log import BookPublishRecord, ChunkPublishRecord
 +from catalogue.models.image import Image
 +from catalogue.models.publish_log import (BookPublishRecord,
 +    ChunkPublishRecord, ImagePublishRecord)
  from catalogue.models.book import Book
  from catalogue.models.listeners import *
  
index 558f4c1,0000000..7b5ce1f
mode 100755,000000..100755
--- /dev/null
@@@ -1,157 -1,0 +1,165 @@@
 +# -*- 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.contrib.sites.models import Site
 +from django.db import models
 +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.tasks import refresh_instance
 +from dvcs import models as dvcs_models
 +
 +
 +class Image(dvcs_models.Document):
 +    """ An editable chunk of text. Every Book text is divided into chunks. """
 +    REPO_PATH = settings.CATALOGUE_IMAGE_REPO_PATH
 +
 +    image = models.FileField(_('image'), upload_to='catalogue/images')
 +    title = models.CharField(_('title'), max_length=255, blank=True)
 +    slug = models.SlugField(_('slug'), unique=True)
 +    public = models.BooleanField(_('public'), default=True, db_index=True)
 +
 +    # cache
 +    _short_html = models.TextField(null=True, blank=True, editable=False)
 +    _new_publishable = models.NullBooleanField(editable=False)
 +    _published = models.NullBooleanField(editable=False)
 +    _changed = models.NullBooleanField(editable=False)
 +
 +    class Meta:
 +        app_label = 'catalogue'
 +        ordering = ['title']
 +        verbose_name = _('image')
 +        verbose_name_plural = _('images')
 +        permissions = [('can_pubmark_image', 'Can mark images for publishing')]
 +
 +    # Representing
 +    # ============
 +
 +    def __unicode__(self):
 +        return self.title
 +
 +    @models.permalink
 +    def get_absolute_url(self):
 +        return ("catalogue_image", [self.slug])
 +
 +    def correct_about(self):
 +        return "http://%s%s" % (
 +            Site.objects.get_current().domain,
 +            self.get_absolute_url()
 +        )
 +
 +    # State & cache
 +    # =============
 +
 +    def last_published(self):
 +        try:
 +            return self.publish_log.all()[0].timestamp
 +        except IndexError:
 +            return None
 +
 +    def assert_publishable(self):
 +        from librarian.picture import WLPicture
 +        from librarian import NoDublinCore, ParseError, ValidationError
 +
 +        class SelfImageStore(object):
 +            def path(self_, slug, mime_type):
 +                """Returns own file object. Ignores slug ad mime_type."""
 +                return open(self.image.path)
 +
 +        publishable = self.publishable()
 +        assert publishable, _("There is no publishable revision")
 +        picture_xml = publishable.materialize()
 +
 +        try:
 +            picture = WLPicture.from_string(picture_xml.encode('utf-8'),
 +                    image_store=SelfImageStore)
 +        except ParseError, e:
 +            raise AssertionError(_('Invalid XML') + ': ' + str(e))
 +        except NoDublinCore:
 +            raise AssertionError(_('No Dublin Core found.'))
 +        except ValidationError, e:
 +            raise AssertionError(_('Invalid Dublin Core') + ': ' + str(e))
 +
 +        valid_about = self.correct_about()
 +        assert picture.picture_info.about == valid_about, \
 +                _("rdf:about is not") + " " + valid_about
 +
++    def publishable_error(self):
++        try:
++            return self.assert_publishable()
++        except AssertionError, e:
++            return e
++        else:
++            return None
++
 +    def accessible(self, request):
 +        return self.public or request.user.is_authenticated()
 +
 +    def is_new_publishable(self):
 +        change = self.publishable()
 +        if not change:
 +            return False
 +        return not change.publish_log.exists()
 +    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_changed(self):
 +        if self.head is None:
 +            return False
 +        return not self.head.publishable
 +    changed = cached_in_field('_changed')(is_changed)
 +
 +    @cached_in_field('_short_html')
 +    def short_html(self):
 +        return render_to_string(
 +                    'catalogue/image_short.html', {'image': self})
 +
 +    def refresh(self):
 +        """This should be done offline."""
 +        self.short_html
 +        self.single
 +        self.new_publishable
 +        self.published
 +
 +    def touch(self):
 +        update = {
 +            "_changed": self.is_changed(),
 +            "_short_html": None,
 +            "_new_publishable": self.is_new_publishable(),
 +            "_published": self.is_published(),
 +        }
 +        Image.objects.filter(pk=self.pk).update(**update)
 +        refresh_instance(self)
 +
 +    def refresh(self):
 +        """This should be done offline."""
 +        self.changed
 +        self.short_html
 +
 +
 +    # Publishing
 +    # ==========
 +
 +    def publish(self, user):
 +        """Publishes the picture on behalf of a (local) user."""
 +        from base64 import b64encode
 +        import apiclient
 +        from catalogue.signals import post_publish
 +
 +        self.assert_publishable()
 +        change = self.publishable()
 +        picture_xml = change.materialize()
 +        picture_data = open(self.image.path).read()
 +        apiclient.api_call(user, "pictures/", {
 +                "picture_xml": picture_xml,
 +                "picture_image_data": b64encode(picture_data),
 +            })
 +        # record the publish
 +        log = self.publish_log.create(user=user, change=change)
 +        post_publish.send(sender=log)
@@@ -1,12 -1,8 +1,11 @@@
  {% extends "catalogue/base.html" %}
  {% load i18n %}
- {% load url from future %}
  {% load wall %}
  
  
 +{% block titleextra %}{% trans "Activity" %}{% endblock %}
 +
 +
  {% block content %}
  
  <h1><a href='{% url "catalogue_activity" prev_day.isoformat %}'>&lt;</a>
@@@ -2,18 -2,18 +2,19 @@@
  {% load catalogue %}
  <!DOCTYPE html>
  <html>
 -<head>
 +<head lang="{{ LANGUAGE_CODE }}">
      <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
      {% compressed_css 'catalogue' %}
 -    <title>{% block title %}{% trans "Platforma Redakcyjna" %}{% endblock title %}</title>
 +    <title>{% block title %}{% trans "Platforma Redakcyjna" %} ::
 +        {% block titleextra %}{% endblock %}{% endblock title %}</title>
+     {% block add_css %}{% endblock %}
  </head>
  <body>
  
  <div id="tabs-nav">
  
-     <a href="{% url catalogue_document_list %}">
+     <a href="{% url 'catalogue_document_list' %}">
 -        <img id="logo" src="{{ STATIC_URL }}img/wl-orange.png" />
 +        <img id="logo" src="{{ STATIC_URL }}img/wl-orange.png" alt="Platforma" />
      </a>
  
      <div id="tabs-nav-left">
@@@ -43,8 -43,9 +44,9 @@@
  </div>
  
  
- <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
+ <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
  {% compressed_js 'catalogue' %}
+ {% block add_js %}{% endblock %}
  {% block extrabody %}
  {% endblock %}
  </body>
@@@ -1,10 -1,6 +1,10 @@@
  {% extends "catalogue/base.html" %}
  {% load book_list comments i18n %}
  
 +
 +{% block titleextra %}{{ book.title }}{% endblock %}
 +
 +
  {% block content %}
  
  
  </tbody></table>
  {% if editable %}</form>{% endif %}
  
  {% if editable %}
-     <p><a href="{% url catalogue_book_append book.slug %}">{% trans "Append to other book" %}</a></p>
+     {% if book.gallery %}
+     <p><a href="{% url 'catalogue_book_gallery' book.slug %}">{% trans "Edit gallery" %}</a></p>
+     {% endif %}
+     <p><a href="{% url 'catalogue_book_append' book.slug %}">{% trans "Append to other book" %}</a></p>
  {% endif %}
  
  
  
  <h2>{% trans "Publication" %}</h2>
  
+ <div class="cover-preview">
+ <img class="cover-preview" src="{% url 'cover_preview' book.slug %}" />
+ {% if book.dc_cover_image %}
+     <a href="{{ book.dc_cover_image.get_absolute_url }}">{{ book.dc_cover_image }}</a>
+ {% endif %}
+ </div>
  <p>{% trans "Last published" %}: 
      {% if book.last_published %}
          {{ book.last_published }}
  
  {% if publishable %}
      <p>
-     <a href="{% url catalogue_book_xml book.slug %}">{% trans "Full XML" %}</a><br/>
-     <a target="_blank" href="{% url catalogue_book_html book.slug %}">{% trans "HTML version" %}</a><br/>
-     <a href="{% url catalogue_book_txt book.slug %}">{% trans "TXT version" %}</a><br/>
-     <a href="{% url catalogue_book_pdf book.slug %}">{% trans "PDF version" %}</a><br/>
-     <a href="{% url catalogue_book_epub book.slug %}">{% trans "EPUB version" %}</a><br/>
+     <a href="{% url 'catalogue_book_xml' book.slug %}" rel="nofollow">{% trans "Full XML" %}</a><br/>
+     <a target="_blank" href="{% url 'catalogue_book_html' book.slug %}" rel="nofollow">{% trans "HTML version" %}</a><br/>
+     <a href="{% url 'catalogue_book_txt' book.slug %}" rel="nofollow">{% trans "TXT version" %}</a><br/>
+     <a href="{% url 'catalogue_book_pdf' book.slug %}" rel="nofollow">{% trans "PDF version" %}</a><br/>
+     <a href="{% url 'catalogue_book_epub' book.slug %}" rel="nofollow">{% trans "EPUB version" %}</a><br/>
      </p>
  
      {% if user.is_authenticated %}
          mira66 (http://www.flickr.com/photos/21804434@N02/) /
          CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
          -->
-         <form method="POST" action="{% url catalogue_publish book.slug %}">{% csrf_token %}
+         <form method="POST" action="{% url 'catalogue_publish' book.slug %}">{% csrf_token %}
              <img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" />
              <button id="publish-button" type="submit">
                  <span>{% trans "Publish" %}</span></button>
              <img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" />
              </form>
      {% else %}
-         <a href="{% url login %}">{% trans "Log in to publish." %}</a>
+         <a href="{% url 'login' %}">{% trans "Log in to publish." %}</a>
      {% endif %}
  {% else %}
      <p>{% trans "This book can't be published yet, because:" %}</p>
      <ul><li>{{ publishable_error }}</li></ul>
  {% endif %}
  
+ <div style="clear: both;"></div>
  </div>
  
  
@@@ -2,7 -2,7 +2,7 @@@
  {% load pagination_tags %}
  
  
 -<form name='filter' action=''>
 +<form name='filter' action='{{ request.path }}'>
  <input type='hidden' name="title" value="{{ request.GET.title }}" />
  <input type='hidden' name="stage" value="{{ request.GET.stage }}" />
  {% if not viewed_user %}
  {% endif %}
  <input type='hidden' name="all" value="{{ request.GET.all }}" />
  <input type='hidden' name="status" value="{{ request.GET.status }}" />
+ <input type='hidden' name="project" value="{{ request.GET.project }}" />
  </form>
  
  <table id="file-list"{% if viewed_user %} class="book-list-user"{% endif %}>
      <thead><tr>
+       <th></th>
          <th></th>
          <th>
              <input class='check-filter' type='checkbox' name='all' title='{% trans "Show hidden books" %}'
@@@ -45,8 -48,6 +48,8 @@@
                          {% endif %}value="{{ user.username }}">{{ user.first_name }} {{ user.last_name }} ({{ user.count }})</option>
                  {% endfor %}
              </select></th>
 +        {% else %}
 +            <th style='display: none'></th>
          {% endif %}
  
          <th><select name="status" class="filter">
              {% endfor %}
          </select></th>
  
+         <th><select name="project" class="filter">
+             <option value=''>- {% trans "project" %} -</option>
+                 <option {% if request.GET.project == '-' %}selected="selected"
+                         {% endif %}value="-">- {% trans "none" %} -</option>
+             {% for project in projects %}
+                 <option {% if request.GET.project == project.pk|slugify %}selected="selected"
+                         {% endif %}value='{{ project.pk }}'>{{ project.name }}</option>
+             {% endfor %}
+         </select></th>
      </tr></thead>
  
      {% with cnt=books|length %}
@@@ -72,7 -83,7 +85,7 @@@
              {% endif %}
          {% endwith %}
      {% endfor %}
 -    <tr><th class='paginator' colspan="5">
 +    <tr><th class='paginator' colspan="6">
          {% paginate %}
          {% blocktrans count c=cnt %}{{c}} book{% plural %}{{c}} books{% endblocktrans %}</th></tr>
      </tbody>
  {% if not books %}
      <p>{% trans "No books found." %}</p>
  {% endif %}
+ <form id='chunk_mass_edit' action='{% url "catalogue_chunk_mass_edit" %}' style="display:none;">
+ {% csrf_token %}
+ <input type="hidden" name="ids" />
+ <label for="mass_edit_stage">{% trans "Set stage" %}</label><input type="hidden" name="stage" id="mass_edit_stage"/>
+ <label for="mass_edit_user">{% trans "Set user" %}</label><input type="hidden" name="user" id="mass_edit_stage" />
+ <input type="hidden" name="status" />
+ <label for="mass_edit_project">{% trans "Project" %}</label><input type="hidden" name="project" id="mass_edit_project" />
+ <label for="mass_edit_publish">{% trans "Mark publishable" %}</label>
+ <label for="mass_edit_unpublish">{% trans "Mark not publishable" %}</label>
+ <label for="mass_edit_other">{% trans "Other user" %}</label>
+ </form>
+ <select name="other-user" style="display:none;">
+   {% for user in other_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>
@@@ -1,11 -1,6 +1,10 @@@
  {% extends "catalogue/base.html" %}
- {% load url from future %}
  {% load i18n %}
  
 +
 +{% block titleextra %}{% trans "Chunk settings" %}{% endblock %}
 +
 +
  {% block content %}
      <h1>{% trans "Chunk settings" %}</h1>
  
@@@ -2,10 -2,17 +2,19 @@@
  
  {% load i18n %}
  {% load catalogue book_list %}
+ {% load compressed %}
  
 +{% block titleextra %}{% trans "Book list" %}{% endblock %}
 +
  
+ {% block add_js %}
+ {% compressed_js 'book_list' %}
+ {% endblock %}
+ {% block add_css %}
+ {% compressed_css 'book_list' %}
+ {% endblock %}
  {% block content %}
      {% book_list %}
  {% endblock content %}
@@@ -2,9 -2,6 +2,9 @@@
  {% load i18n %}
  
  
 +{% block titleextra %}{% trans "Bulk document upload" %}{% endblock %}
 +
 +
  {% block leftcolumn %}
  
  
@@@ -14,7 -11,7 +14,7 @@@
  {% trans "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with <code>.xml</code> will be ignored." %}
  </p>
  
 -<form enctype="multipart/form-data" method="POST" action="">
 +<form enctype="multipart/form-data" method="POST" action="{{ request.path }}">
  {% csrf_token %}
  {{ form.as_p }}
  <p><button type="submit">{% trans "Upload" %}</button></p>
@@@ -48,7 -45,7 +48,7 @@@
          <h3>{% trans "Uploaded files" %}</h3>
          <ul id='ok-list'>
          {% for filename, slug, title in ok_list %}
-             <li><a href='{% url wiki_editor slug %}'>{{ title }}</a> (<code>{{ filename }})</a></li>
+             <li><a href='{% url "wiki_editor" slug %}'>{{ title }}</a> (<code>{{ filename }})</a></li>
          {% endfor %}
          </ul>
      {% endif %}
index cd77654,0000000..d791bfd
mode 100755,000000..100755
--- /dev/null
@@@ -1,80 -1,0 +1,80 @@@
-     <p><a href="{% url wiki_img_editor object.slug %}">{% trans "Proceed to the editor." %}</a></p>
 +{% extends "catalogue/base.html" %}
 +{% load book_list comments i18n %}
 +
 +
 +{% block titleextra %}{{ object.title }}{% endblock %}
 +
 +
 +{% block content %}
 +
 +
 +<h1>{{ object.title }}</h1>
 +
 +
 +{% if editable %}<form method='POST'>{% csrf_token %}{% endif %}
 +<table class='editable'><tbody>
 +    {{ form.as_table }}
 +    {% if editable %}
 +        <tr><td></td><td><button type="submit">{% trans "Save" %}</button></td></tr>
 +    {% endif %}
 +</tbody></table>
 +{% if editable %}</form>{% endif %}
 +
 +
 +
 +<div class='section'>
 +    <h2>{% trans "Editor" %}</h2>
 +
-         <form method="POST" action="{% url catalogue_publish_image object.slug %}">{% csrf_token %}
++    <p><a href="{% url 'wiki_img_editor' object.slug %}">{% trans "Proceed to the editor." %}</a></p>
 +</div>
 +
 +
 +
 +<div class='section'>
 +
 +
 +<h2>{% trans "Publication" %}</h2>
 +
 +<p>{% trans "Last published" %}: 
 +    {% if object.last_published %}
 +        {{ object.last_published }}
 +    {% else %}
 +        &mdash;
 +    {% endif %}
 +</p>
 +
 +{% if publishable %}
 +    {% if user.is_authenticated %}
 +        <!--
 +        Angel photos:
 +        Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
 +        mira66 (http://www.flickr.com/photos/21804434@N02/) /
 +        CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
 +        -->
-         <a href="{% url login %}">{% trans "Log in to publish." %}</a>
++        <form method="POST" action="{% url 'catalogue_publish_image' object.slug %}">{% csrf_token %}
 +            <!--img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" /-->
 +            <button id="publish-button" type="submit">
 +                <span>{% trans "Publish" %}</span></button>
 +            <!--img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" /-->
 +            </form>
 +    {% else %}
++        <a href="{% url 'login' %}">{% trans "Log in to publish." %}</a>
 +    {% endif %}
 +{% else %}
 +    <p>{% trans "This book can't be published yet, because:" %}</p>
 +    <ul><li>{{ publishable_error }}</li></ul>
 +{% endif %}
 +
 +</div>
 +
 +
 +<div class='section'>
 +    <h2>{% trans "Comments" %}</h2>
 +
 +    {% render_comment_list for object %}
 +    {% with object.get_absolute_url as next %}
 +        {% render_comment_form for object %}
 +    {% endwith %}
 +</div>
 +
 +{% endblock content %}
index 2e2b386,0000000..e64733b
mode 100755,000000..100755
--- /dev/null
@@@ -1,18 -1,0 +1,18 @@@
-     <td><a href="{% url catalogue_image image.slug %}" title='{% trans "Image settings" %}'>[B]</a></td>
 +{% load i18n %}
 +
 +<tr>
-                 href="{% url wiki_img_editor image.slug %}">
++    <td><a href="{% url 'catalogue_image' image.slug %}" title='{% trans "Image settings" %}'>[B]</a></td>
 +    <td><a target="_blank"
-     <td class='user-column'>{% if image.user %}<a href="{% url catalogue_user image.user.username %}">{{ image.user.first_name }} {{ image.user.last_name }}</a>{% endif %}</td>
++                href="{% url 'wiki_img_editor' image.slug %}">
 +                {{ image.title }}</a></td>
 +    <td>{% if image.stage %}
 +        {{ image.stage }}
 +    {% else %}–
 +    {% endif %}</td>
++    <td class='user-column'>{% if image.user %}<a href="{% url 'catalogue_user' image.user.username %}">{{ image.user.first_name }} {{ image.user.last_name }}</a>{% endif %}</td>
 +    <td>
 +        {% if image.published %}P{% endif %}
 +        {% if image.new_publishable %}p{% endif %}
 +        {% if image.changed %}+{% endif %}
 +    </td>
 +</tr>
@@@ -2,11 -2,16 +2,19 @@@
  
  {% load i18n %}
  {% load catalogue book_list wall %}
+ {% load compressed %}
  
+ {% block add_js %}
+ {% compressed_js 'book_list' %}
+ {% endblock %}
+ {% block add_css %}
+ {% compressed_css 'book_list' %}
+ {% endblock %}
  
 +{% block titleextra %}{% trans "My page" %}{% endblock %}
 +
 +
  {% block leftcolumn %}
      {% book_list request.user %}
  {% endblock leftcolumn %}
@@@ -16,7 -21,7 +24,7 @@@
          <h2>{% trans "Your last edited documents" %}</h2>
          <ol>
              {% for slugs, item in last_books %}
-             <li><a href="{% url wiki_editor slugs.0 slugs.1 %}"
+             <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>
@@@ -2,17 -2,13 +2,17 @@@
  
  {% load i18n %}
  
 +
 +{% block titleextra %}{% trans "Users" %}{% endblock %}
 +
 +
  {% block leftcolumn %}
  
  <h1>{% trans "Users" %}</h1>
  
  <ul>
  {% for user in users %}
-     <li><a href="{% url catalogue_user user.username %}">
+     <li><a href="{% url 'catalogue_user' user.username %}">
          <span class="chunkno">{{ forloop.counter }}.</span>
          {{ user.first_name }} {{ user.last_name }}</a>
          ({{ user.count }})</li>
@@@ -7,8 -7,7 +7,8 @@@
      <li class="{{ item.tag }}{% if not item.user %} anonymous{% endif %}">
          <div class='gravatar'>
              {% if item.get_email %}
 -                {% gravatar_img_for_email item.get_email 32 %}
 +                <img src="{% gravatar_for_email item.get_email 32 %}"
 +                    height="32" width="32" alt='Avatar' />
                  <br/>
              {% endif %}
          </div>
@@@ -18,7 -17,7 +18,7 @@@
          <a target="_blank" href='{{ item.url }}'>{{ item.title }}</a>
          <br/><strong>{% trans "user" %}:</strong>
          {% if item.user %}
-             <a href="{% url catalogue_user item.user.username %}">
+             <a href="{% url 'catalogue_user' item.user.username %}">
              {{ item.user.first_name }} {{ item.user.last_name }}</a>
              &lt;{{ item.user.email|email_link }}>
          {% else %}
@@@ -5,7 -5,7 +5,7 @@@ from django.db.models import Q, Coun
  from django import template
  from django.utils.translation import ugettext_lazy as _
  from django.contrib.auth.models import User
- from catalogue.models import Chunk, Image
 -from catalogue.models import Chunk, Project
++from catalogue.models import Chunk, Image, Project
  
  register = template.Library()
  
@@@ -113,6 -113,7 +113,7 @@@ def document_list_filter(request, **kwa
      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', 'title'])
+     chunks = foreign_filter(chunks, arg_or_GET('project'), 'book__project', Project, 'pk')
      return chunks
  
  
@@@ -125,9 -126,14 +126,14 @@@ def book_list(context, user=None)
          new_context = {"viewed_user": user}
      else:
          filters = {}
-         new_context = {"users": User.objects.annotate(
+         new_context = {
+             "users": User.objects.annotate(
                  count=Count('chunk')).filter(count__gt=0).order_by(
-                 '-count', 'last_name', 'first_name')}
+                 '-count', 'last_name', 'first_name'),
+             "other_users": User.objects.annotate(
+                 count=Count('chunk')).filter(count=0).order_by(
+                 'last_name', 'first_name'),
+                 }
  
      new_context.update({
          "filters": True,
          "books": ChunksList(document_list_filter(request, **filters)),
          "stages": Chunk.tag_model.objects.all(),
          "states": _states_options,
+         "projects": Project.objects.all(),
      })
  
      return new_context
 +
 +
 +
 +_image_states = [
 +        ('publishable', _('publishable'), Q(_new_publishable=True)),
 +        ('changed', _('changed'), Q(_changed=True)),
 +        ('published', _('published'), Q(_published=True)),
 +        ('unpublished', _('unpublished'), Q(_published=False)),
 +        ('empty', _('empty'), Q(head=None)),
 +    ]
 +_image_states_options = [s[:2] for s in _image_states]
 +_image_states_dict = dict([(s[0], s[2]) for s in _image_states])
 +
 +def image_list_filter(request, **kwargs):
 +
 +    def arg_or_GET(field):
 +        return kwargs.get(field, request.GET.get(field))
 +
 +    images = Image.objects.all()
 +
 +    if not request.user.is_authenticated():
 +        images = images.filter(public=True)
 +
 +    state = arg_or_GET('status')
 +    if state in _image_states_dict:
 +        images = images.filter(_image_states_dict[state])
 +
 +    images = foreign_filter(images, arg_or_GET('user'), 'user', User, 'username')
 +    images = foreign_filter(images, arg_or_GET('stage'), 'stage', Image.tag_model, 'slug')
 +    images = search_filter(images, arg_or_GET('title'), ['title', 'title'])
 +    return images
 +
 +
 +@register.inclusion_tag('catalogue/image_table.html', takes_context=True)
 +def image_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('image')).filter(count__gt=0).order_by(
 +                '-count', 'last_name', 'first_name')}
 +
 +    new_context.update({
 +        "filters": True,
 +        "request": request,
 +        "objects": image_list_filter(request, **filters),
 +        "stages": Image.tag_model.objects.all(),
 +        "states": _image_states_options,
 +    })
 +
 +    return new_context
@@@ -28,13 -28,14 +28,15 @@@ def main_tabs(context)
  
      tabs.append(Tab('activity', _('Activity'), reverse("catalogue_activity")))
      tabs.append(Tab('all', _('All'), reverse("catalogue_document_list")))
 +    tabs.append(Tab('images', _('Images'), reverse("catalogue_image_list")))
      tabs.append(Tab('users', _('Users'), reverse("catalogue_users")))
  
      if user.has_perm('catalogue.add_book'):
          tabs.append(Tab('create', _('Add'), reverse("catalogue_create_missing")))
          tabs.append(Tab('upload', _('Upload'), reverse("catalogue_upload")))
  
+     tabs.append(Tab('cover', _('Covers'), reverse("cover_image_list")))
      return {"tabs": tabs, "active_tab": active}
  
  
diff --combined apps/catalogue/urls.py
@@@ -1,16 -1,14 +1,19 @@@
  # -*- coding: utf-8
- from django.conf.urls.defaults import *
- from django.views.generic.simple import redirect_to
+ from django.conf.urls import patterns, url
+ from django.contrib.auth.decorators import permission_required
+ from django.views.generic import RedirectView
+ from catalogue.feeds import PublishTrackFeed
+ from catalogue.views import GalleryView
  
  
  urlpatterns = patterns('catalogue.views',
-     url(r'^$', redirect_to, {'url': 'catalogue/'}),
+     url(r'^$', RedirectView.as_view(url='catalogue/')),
  
 +    url(r'^images/$', 'image_list', name='catalogue_image_list'),
 +    url(r'^image/(?P<slug>[^/]+)/$', 'image', name="catalogue_image"),
 +    url(r'^image/(?P<slug>[^/]+)/publish$', 'publish_image',
 +            name="catalogue_publish_image"),
 +
      url(r'^catalogue/$', 'document_list', name='catalogue_document_list'),
      url(r'^user/$', 'my', name='catalogue_user'),
      url(r'^user/(?P<username>[^/]+)/$', 'user', name='catalogue_user'),
          'create_missing', name='catalogue_create_missing'),
  
      url(r'^book/(?P<slug>[^/]+)/publish$', 'publish', name="catalogue_publish"),
 -    #url(r'^(?P<name>[^/]+)/publish/(?P<version>\d+)$', 'publish', name="catalogue_publish"),
  
      url(r'^book/(?P<slug>[^/]+)/$', 'book', name="catalogue_book"),
+     url(r'^book/(?P<slug>[^/]+)/gallery/$',
+             permission_required('catalogue.change_book')(GalleryView.as_view()),
+             name="catalogue_book_gallery"),
      url(r'^book/(?P<slug>[^/]+)/xml$', 'book_xml', name="catalogue_book_xml"),
      url(r'^book/(?P<slug>[^/]+)/txt$', 'book_txt', name="catalogue_book_txt"),
      url(r'^book/(?P<slug>[^/]+)/html$', 'book_html', name="catalogue_book_html"),
      url(r'^book/(?P<slug>[^/]+)/epub$', 'book_epub', name="catalogue_book_epub"),
      url(r'^book/(?P<slug>[^/]+)/pdf$', 'book_pdf', name="catalogue_book_pdf"),
      url(r'^chunk_add/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
          'chunk_add', name="catalogue_chunk_add"),
      url(r'^chunk_edit/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
          'chunk_edit', name="catalogue_chunk_edit"),
      url(r'^book_append/(?P<slug>[^/]+)/$',
          'book_append', name="catalogue_book_append"),
+     url(r'^chunk_mass_edit',
+         'chunk_mass_edit', name='catalogue_chunk_mass_edit'),
  
+     url(r'^track/(?P<slug>[^/]*)/$', PublishTrackFeed()),
  )
diff --combined apps/catalogue/views.py
@@@ -5,27 -5,28 +5,28 @@@ from StringIO import StringI
  from urllib import unquote
  from urlparse import urlsplit, urlunsplit
  
+ from django.conf import settings
  from django.contrib import auth
  from django.contrib.auth.models import User
  from django.contrib.auth.decorators import login_required, permission_required
  from django.core.urlresolvers import reverse
  from django.db.models import Count, Q
+ from django.db import transaction
  from django import http
  from django.http import Http404, HttpResponse, HttpResponseForbidden
 -from django.shortcuts import get_object_or_404, render, render_to_response
 +from django.shortcuts import get_object_or_404, render
  from django.utils.encoding import iri_to_uri
  from django.utils.http import urlquote_plus
  from django.utils.translation import ugettext_lazy as _
  from django.views.decorators.http import require_POST
- from django.views.generic.simple import direct_to_template
 -from django.template import RequestContext
  
  from apiclient import NotAuthorizedError
  from catalogue import forms
  from catalogue import helpers
  from catalogue.helpers import active_tab
 -from catalogue.models import Book, Chunk, BookPublishRecord, ChunkPublishRecord, Project
 +from catalogue.models import (Book, Chunk, Image, BookPublishRecord, 
-         ChunkPublishRecord, ImagePublishRecord)
- from catalogue.tasks import publishable_error
++        ChunkPublishRecord, ImagePublishRecord, Project)
+ from fileupload.views import UploadView
  
  #
  # Quick hack around caching problems, TODO: use ETags
@@@ -41,12 -42,6 +42,12 @@@ def document_list(request)
      return render(request, 'catalogue/document_list.html')
  
  
 +@active_tab('images')
 +@never_cache
 +def image_list(request, user=None):
 +    return render(request, 'catalogue/image_list.html')
 +
 +
  @never_cache
  def user(request, username):
      user = get_object_or_404(User, username=username)
@@@ -67,7 -62,7 +68,7 @@@ def my(request)
  
  @active_tab('users')
  def users(request):
-     return direct_to_template(request, 'catalogue/user_list.html', extra_context={
+     return render(request, 'catalogue/user_list.html', {
          'users': User.objects.all().annotate(count=Count('chunk')).order_by(
              '-count', 'last_name', 'first_name'),
      })
@@@ -127,7 -122,7 +128,7 @@@ def create_missing(request, slug=None)
                  "gallery": slug,
          })
  
-     return direct_to_template(request, "catalogue/document_create_missing.html", extra_context={
+     return render(request, "catalogue/document_create_missing.html", {
          "slug": slug,
          "form": form,
  
@@@ -182,7 -177,7 +183,7 @@@ def upload(request)
                          title=title,
                      )
  
-             return direct_to_template(request, "catalogue/document_upload.html", extra_context={
+             return render(request, "catalogue/document_upload.html", {
                  "form": form,
                  "ok_list": ok_list,
                  "skipped_list": skipped_list,
      else:
          form = forms.DocumentsUploadForm()
  
-     return direct_to_template(request, "catalogue/document_upload.html", extra_context={
+     return render(request, "catalogue/document_upload.html", {
          "form": form,
  
          "logout_to": '/',
@@@ -217,13 -212,9 +218,9 @@@ def book_txt(request, slug)
      book = get_object_or_404(Book, slug=slug)
      if not book.accessible(request):
          return HttpResponseForbidden("Not authorized.")
-     xml = book.materialize()
-     output = StringIO()
-     # errors?
  
-     import librarian.text
-     librarian.text.transform(StringIO(xml), output)
-     text = output.getvalue()
+     doc = book.wldocument()
+     text = doc.as_text().get_string()
      response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain')
      response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
      return response
@@@ -234,16 -225,22 +231,21 @@@ def book_html(request, slug)
      book = get_object_or_404(Book, slug=slug)
      if not book.accessible(request):
          return HttpResponseForbidden("Not authorized.")
-     xml = book.materialize()
-     output = StringIO()
-     # errors?
-     import librarian.html
-     librarian.html.transform(StringIO(xml), output, parse_dublincore=False,
-                              flags=['full-page'])
-     html = output.getvalue()
-     response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
-     return response
+     doc = book.wldocument(parse_dublincore=False)
+     html = doc.as_html()
+     html = html.get_string() if html is not None else ''
+     # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
+     # return response
+     # book_themes = {}
+     # for fragment in book.fragments.all().iterator():
+     #     for theme in fragment.tags.filter(category='theme').iterator():
+     #         book_themes.setdefault(theme, []).append(fragment)
+     # book_themes = book_themes.items()
+     # book_themes.sort(key=lambda s: s[0].sort_key)
 -    return render_to_response('catalogue/book_text.html', locals(),
 -        context_instance=RequestContext(request))
++    return render(request, 'catalogue/book_text.html', locals())
  
  
  @never_cache
@@@ -252,25 -249,13 +254,13 @@@ def book_pdf(request, slug)
      if not book.accessible(request):
          return HttpResponseForbidden("Not authorized.")
  
-     from tempfile import NamedTemporaryFile
-     from os import unlink
-     from librarian import pdf
-     from catalogue.ebook_utils import RedakcjaDocProvider, serve_file
-     xml = book.materialize()
-     xml_file = NamedTemporaryFile()
-     xml_file.write(xml.encode('utf-8'))
-     xml_file.flush()
-     try:
-         pdf_file = NamedTemporaryFile(delete=False)
-         pdf.transform(RedakcjaDocProvider(publishable=True),
-                   file_path=xml_file.name,
-                   output_file=pdf_file,
-                   )
-         return serve_file(pdf_file.name, book.slug + '.pdf', 'application/pdf')
-     finally:
-         unlink(pdf_file.name)
+     # TODO: move to celery
+     doc = book.wldocument()
+     # TODO: error handling
+     pdf_file = doc.as_pdf()
+     from catalogue.ebook_utils import serve_file
+     return serve_file(pdf_file.get_filename(),
+                 book.slug + '.pdf', 'application/pdf')
  
  
  @never_cache
@@@ -279,23 -264,13 +269,13 @@@ def book_epub(request, slug)
      if not book.accessible(request):
          return HttpResponseForbidden("Not authorized.")
  
-     from StringIO import StringIO
-     from tempfile import NamedTemporaryFile
-     from librarian import epub
-     from catalogue.ebook_utils import RedakcjaDocProvider
-     xml = book.materialize()
-     xml_file = NamedTemporaryFile()
-     xml_file.write(xml.encode('utf-8'))
-     xml_file.flush()
-     epub_file = StringIO()
-     epub.transform(RedakcjaDocProvider(publishable=True),
-             file_path=xml_file.name,
-             output_file=epub_file)
+     # TODO: move to celery
+     doc = book.wldocument()
+     # TODO: error handling
+     epub = doc.as_epub().get_string()
      response = HttpResponse(mimetype='application/epub+zip')
      response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.epub'
-     response.write(epub_file.getvalue())
+     response.write(epub)
      return response
  
  
@@@ -323,15 -298,15 +303,15 @@@ def book(request, slug)
                  return http.HttpResponseRedirect(book.get_absolute_url())
          else:
              form = forms.BookForm(instance=book)
-             editable = True
+         editable = True
      else:
          form = forms.ReadonlyBookForm(instance=book)
          editable = False
  
-     publish_error = publishable_error(book)
+     publish_error = book.publishable_error()
      publishable = publish_error is None
  
-     return direct_to_template(request, "catalogue/book_detail.html", extra_context={
+     return render(request, "catalogue/book_detail.html", {
          "book": book,
          "publishable": publishable,
          "publishable_error": publish_error,
      })
  
  
-     publish_error = publishable_error(image)
 +def image(request, slug):
 +    image = get_object_or_404(Image, slug=slug)
 +    if not image.accessible(request):
 +        return HttpResponseForbidden("Not authorized.")
 +
 +    if request.user.has_perm('catalogue.change_image'):
 +        if request.method == "POST":
 +            form = forms.ImageForm(request.POST, instance=image)
 +            if form.is_valid():
 +                form.save()
 +                return http.HttpResponseRedirect(image.get_absolute_url())
 +        else:
 +            form = forms.ImageForm(instance=image)
 +        editable = True
 +    else:
 +        form = forms.ReadonlyImageForm(instance=image)
 +        editable = False
 +
-     return direct_to_template(request, "catalogue/image_detail.html", extra_context={
++    publish_error = image.publishable_error()
 +    publishable = publish_error is None
 +
++    return render(request, "catalogue/image_detail.html", {
 +        "object": image,
 +        "publishable": publishable,
 +        "publishable_error": publish_error,
 +        "form": form,
 +        "editable": editable,
 +    })
 +
 +
  @permission_required('catalogue.add_chunk')
  def chunk_add(request, slug, chunk):
      try:
                  "title": "cz. %d" % (doc.number + 1, ),
          })
  
-     return direct_to_template(request, "catalogue/chunk_add.html", extra_context={
+     return render(request, "catalogue/chunk_add.html", {
          "chunk": doc,
          "form": form,
      })
  
  
+ @login_required
  def chunk_edit(request, slug, chunk):
      try:
          doc = Chunk.get(slug, chunk)
      else:
          go_next = ''
  
-     return direct_to_template(request, "catalogue/chunk_edit.html", extra_context={
+     return render(request, "catalogue/chunk_edit.html", {
          "chunk": doc,
          "form": form,
          "go_next": go_next,
      })
  
  
+ @transaction.commit_on_success
+ @login_required
+ def chunk_mass_edit(request):
+     if request.method == 'POST':
+         ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(',')))
+         chunks = map(lambda i: Chunk.objects.get(id=i), ids)
+         
+         stage = request.POST.get('stage')
+         if stage:
+             try:
+                 stage = Chunk.tag_model.objects.get(slug=stage)
+             except Chunk.DoesNotExist, e:
+                 stage = None
+            
+             for c in chunks: c.stage = stage
+         username = request.POST.get('user')
+         logger.info("username: %s" % username)
+         logger.info(request.POST)
+         if username:
+             try:
+                 user = User.objects.get(username=username)
+             except User.DoesNotExist, e:
+                 user = None
+                 
+             for c in chunks: c.user = user
+         status = request.POST.get('status')
+         if status:
+             books_affected = set()
+             for c in chunks:
+                 if status == 'publish':
+                     c.head.publishable = True
+                     c.head.save()
+                 elif status == 'unpublish':
+                     c.head.publishable = False
+                     c.head.save()
+                 c.touch()  # cache
+                 books_affected.add(c.book)
+             for b in books_affected:
+                 b.touch()  # cache
+         project_id = request.POST.get('project')
+         if project_id:
+             try:
+                 project = Project.objects.get(pk=int(project_id))
+             except (Project.DoesNotExist, ValueError), e:
+                 project = None
+             for c in chunks:
+                 book = c.book
+                 book.project = project
+                 book.save()
+         for c in chunks: c.save()
+         return HttpResponse("", content_type="text/plain")
+     else:
+         raise Http404
  @permission_required('catalogue.change_book')
  def book_append(request, slug):
      book = get_object_or_404(Book, slug=slug)
              return http.HttpResponseRedirect(append_to.get_absolute_url())
      else:
          form = forms.BookAppendForm(book)
-     return direct_to_template(request, "catalogue/book_append_to.html", extra_context={
+     return render(request, "catalogue/book_append_to.html", {
          "book": book,
          "form": form,
  
@@@ -482,18 -488,19 +523,36 @@@ def publish(request, slug)
          return http.HttpResponseRedirect(book.get_absolute_url())
  
  
 +@require_POST
 +@login_required
 +def publish_image(request, slug):
 +    image = get_object_or_404(Image, slug=slug)
 +    if not image.accessible(request):
 +        return HttpResponseForbidden("Not authorized.")
 +
 +    try:
 +        image.publish(request.user)
 +    except NotAuthorizedError:
 +        return http.HttpResponseRedirect(reverse('apiclient_oauth'))
 +    except BaseException, e:
 +        return http.HttpResponse(e)
 +    else:
 +        return http.HttpResponseRedirect(image.get_absolute_url())
++
++
+ class GalleryView(UploadView):
+     def get_object(self, request, slug):
+         book = get_object_or_404(Book, slug=slug)
+         if not book.gallery:
+             raise Http404
+         return book
+     def breadcrumbs(self):
+         return [
+             (_('books'), reverse('catalogue_document_list')),
+             (self.object.title, self.object.get_absolute_url()),
+             (_('scan gallery'),),
+         ]
+     def get_directory(self):
+         return "%s%s/" % (settings.IMAGE_DIR, self.object.gallery)
diff --combined apps/dvcs/models.py
@@@ -6,8 -6,8 +6,8 @@@ from django.core.files.base import Cont
  from django.core.files.storage import FileSystemStorage
  from django.db import models, transaction
  from django.db.models.base import ModelBase
 -from django.utils.translation import ugettext_lazy as _
 +from django.utils.translation import string_concat, ugettext_lazy as _
- from mercurial import mdiff, simplemerge
+ from mercurial import simplemerge
  
  from django.conf import settings
  from dvcs.signals import post_commit, post_publishable
@@@ -26,6 -26,8 +26,6 @@@ class Tag(models.Model)
      class Meta:
          abstract = True
          ordering = ['ordering']
 -        verbose_name = _("tag")
 -        verbose_name_plural = _("tags")
  
      def __unicode__(self):
          return self.name
@@@ -43,7 -45,7 +43,7 @@@
      def listener_changed(sender, instance, **kwargs):
          sender._object_cache = {}
  
-     def next(self):
+     def get_next(self):
          """
              Returns the next tag - stage to work on.
              Returns None for the last stage.
@@@ -97,6 -99,8 +97,6 @@@ class Change(models.Model)
          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)
          post_publishable.send(sender=self, publishable=publishable)
  
  
  def create_tag_model(model):
      name = model.__name__ + 'Tag'
  
      class Meta(Tag.Meta):
          app_label = model._meta.app_label
 +        verbose_name = string_concat(_("tag"), " ", _("for:"), " ", 
 +                model._meta.verbose_name)
 +        verbose_name_plural = string_concat(_("tags"), " ", _("for:"), " ",
 +                model._meta.verbose_name)
  
      attrs = {
          '__module__': model.__module__,
@@@ -191,10 -189,6 +189,10 @@@ def create_change_model(model)
  
      class Meta(Change.Meta):
          app_label = model._meta.app_label
 +        verbose_name = string_concat(_("change"), " ", _("for:"), " ",
 +                model._meta.verbose_name)
 +        verbose_name_plural = string_concat(_("changes"), " ", _("for:"), " ",
 +                model._meta.verbose_name)
  
      attrs = {
          '__module__': model.__module__,
@@@ -286,7 -280,7 +284,7 @@@ class Document(models.Model)
          tags = kwargs.get('tags', [])
          if tags:
              # set stage to next tag after the commited one
-             self.stage = max(tags, key=lambda t: t.ordering).next()
+             self.stage = max(tags, key=lambda t: t.ordering).get_next()
  
          change = self.change_set.create(author=author,
                      author_name=author_name,
@@@ -1,14 -1,14 +1,14 @@@
  {% extends "base.html" %}
  {% load toolbar_tags i18n %}
  
 -{% block title %}{{ book.title }} - {{ block.super }}{% endblock %}
 +{% block titleextra %}{{ chunk.pretty_title }}{% endblock %}
  {% block extrahead %}
  {% load compressed %}
  {% compressed_css 'detail' %}
  {% endblock %}
  
  {% block extrabody %}
 -<script type="text/javascript" charset="utf-8">
 +<script type="text/javascript">
      var STATIC_URL = '{{STATIC_URL}}';
  </script>
  {% compressed_js 'detail' %}
@@@ -27,7 -27,7 +27,7 @@@
  </div>
  
  <div id="header">
-     <h1><a href="{% url catalogue_document_list %}"><img src="{{STATIC_URL}}icons/go-home.png" alt="Home" /></a><a href="{% url catalogue_document_list %}">Strona<br/>główna</a></h1>
 -    <h1><a href="{% url 'catalogue_document_list' %}"><img src="{{STATIC_URL}}icons/go-home.png"/><a href="{% url 'catalogue_document_list' %}">Strona<br>główna</a></h1>
++    <h1><a href="{% url 'catalogue_document_list' %}"><img src="{{STATIC_URL}}icons/go-home.png"  alt="Home" /></a><a href="{% url 'catalogue_document_list' %}">Strona<br>główna</a></h1>
      <div id="tools">
          <a href="{{ REDMINE_URL }}projects/wl-publikacje/wiki/Pomoc" target="_blank">
          {% trans "Help" %}</a>
@@@ -2,15 -2,16 +2,17 @@@
  <div id="side-gallery">
      <!-- gallery toolbar -->
      <div class="toolbar">
-         <button class="previous-page">
-             <img src="{{STATIC_URL}}icons/go-previous.png"
-               alt="{% trans "Previous" %}" title="{% trans "Previous" %}"/>
+         <button class="start-page" alt="{% trans "Go to first image of this part" %}" title="{% trans "Go to first image of this part" %}">
+             <img src="{{STATIC_URL}}icons/revert.png"/>
+         </button>
+         <button class="previous-page" alt="{% trans "Previous" %}" title="{% trans "Previous" %}">
+             <img src="{{STATIC_URL}}icons/go-previous.png"/>
          </button>
          <input type="text" size="3" maxlength="3" value="0" class="page-number" />
 -        <span id="imagesCount" id="">/0</span>
 +        <span id="imagesCount">/0</span>
-         <button class="next-page">
+         <button class="next-page" alt="{% trans "Next" %}" title="{% trans "Next" %}">
 -            <img src="{{STATIC_URL}}icons/go-next.png"/>
 +            <img src="{{STATIC_URL}}icons/go-next.png"
-               alt="{% trans "Next" %}" title="{% trans "Next" %}"/>
++                alt="{% trans "Next" %}" title="{% trans "Next" %}"/>
          </button>
          <button class="zoom-in">{% trans "Zoom in" %}</button>
          <button class="zoom-out">{% trans "Zoom out" %}</button>
@@@ -20,6 -21,6 +22,6 @@@
      <div class="error_message">
      </div>
      <div class="gallery-image">
 -        <img src="{{MEDIA_URL}}images/empty.png" />
 +        <img src="{{MEDIA_URL}}images/empty.png" alt="no image" />
      </div>
  </div>
@@@ -3,10 -3,18 +3,18 @@@
      <!-- <div class="toolbar">
      </div> -->
      <div id="summary-view">
-               <img class="book-cover" src="{{ STATIC_URL }}img/sample_cover.png" alt="Sample cover" />
+       <div class="summary-cover-area">
+               <p><img id="summary-cover" class="book-cover"
+                       {% if revision %}
+                            src="{% url 'cover_preview' chunk.book.slug chunk.slug revision %}"
+                       {% else %}
+                            src="{% url 'cover_preview' chunk.book.slug chunk.slug %}"
+                       {% endif %}></p>
+               <p><button id="summary-cover-refresh">{% trans "Refresh from working copy" %}</button></p>
+               </div>
  
                <h2>
 -                      <label for="title">{% trans "Title" %}:</label>
 +                      <label>{% trans "Title" %}:</label>
                        <span data-ui-editable="true" data-edit-target="meta.displayTitle"
                        >{{ chunk.pretty_name }}</span>
                </h2>
                <p>
                        <label>{% trans "Current version" %}:</label>
                        {{ chunk.revision }} ({{ chunk.head.created_at }})
 +              </p>
                <p>
                        <label>{% trans "Last edited by" %}:</label>
                        {{ chunk.head.author }}
                </p>
                <p>
 -                      <label for="gallery">{% trans "Link to gallery" %}:</label>
 +                      <label>{% trans "Link to gallery" %}:</label>
                        <span data-ui-editable="true" data-edit-target="meta.galleryLink"
                        >{{ chunk.book.gallery }}</span>
                </p>
index d329c04,0000000..642c916
mode 100644,000000..100644
--- /dev/null
@@@ -1,52 -1,0 +1,52 @@@
-     <h1><a href="{% url catalogue_document_list %}"><img alt="Home" src="{{STATIC_URL}}icons/go-home.png"/></a><a href="{% url catalogue_document_list %}">Strona<br/>główna</a></h1>
 +{% extends "base.html" %}
 +{% load toolbar_tags i18n %}
 +
 +{% block title %}{{ document.title }} - {{ block.super }}{% endblock %}
 +{% block extrahead %}
 +{% load compressed %}
 +{% compressed_css 'detail' %}
 +{% endblock %}
 +
 +{% block extrabody %}
 +<script type="text/javascript">
 +    var STATIC_URL = '{{STATIC_URL}}';
 +</script>
 +{% compressed_js 'wiki_img' %}
 +{% endblock %}
 +
 +{% block maincontent %}
 +<div id="document-meta"
 +      data-object-id="{{ document.pk }}" style="display:none">
 +
 +      <span data-key="revision">{{ revision }}</span>
 +    <span data-key="diff">{{ request.GET.diff }}</span>
 +
 +      {% block meta-extra %} {% endblock %}
 +</div>
 +
 +<div id="header">
++    <h1><a href="{% url 'catalogue_document_list' %}"><img alt="Home" src="{{STATIC_URL}}icons/go-home.png"/></a><a href="{% url 'catalogue_document_list' %}">Strona<br/>główna</a></h1>
 +    <div id="tools">
 +        <a href="{{ REDMINE_URL }}projects/wl-publikacje/wiki/Pomoc" target="_blank">
 +        {% trans "Help" %}</a>
 +        | {% include "registration/head_login.html" %}
 +        | {% trans "Version" %}: <span id="document-revision">{% trans "Unknown" %}</span>
 +              {% if not readonly %}
 +            | <button style="margin-left: 6px" id="save-button">{% trans "Save" %}</button>
 +                      <span id='save-attempt-info'>{% trans "Save attempt in progress" %}</span>
 +            <span id='out-of-date-info'>{% trans "There is a newer version of this document!" %}</span>
 +              {% endif %}
 +    </div>
 +    <ol id="tabs" class="tabs">
 +      {% block tabs-menu %} {% endblock %}
 +    </ol>
 +</div>
 +<div id="splitter">
 +    <div id="editor" class="{% block editor-class %} {% endblock %}">
 +      {% block tabs-content %} {% endblock %}
 +    </div>
 +</div>
 +
 +{% block dialogs %} {% endblock %}
 +
 +{% endblock %}
index db49d64,0000000..dcb62ec
mode 100755,000000..100755
--- /dev/null
@@@ -1,40 -1,0 +1,40 @@@
-                       data-basehref="{% url wiki_img_editor_readonly document.slug %}">{% trans "View version" %}</button>
 +{% load i18n %}
 +<div id="history-view-editor" class="editor" style="display: none">
 +    <div class="toolbar">
 +      <button type="button" id="make-diff-button"
 +                      data-enabled-when="2" disabled="disabled">{% trans "Compare versions" %}</button>
 +        {% if can_pubmark %}
 +              <button type="button" id="pubmark-changeset-button"
 +                      data-enabled-when="1" disabled="disabled">{% trans "Mark for publishing" %}</button>
 +        {% endif %}
 +              <button type="button" id="doc-revert-button"
 +                      data-enabled-when="1" disabled="disabled">{% trans "Revert document" %}</button>
 +              <button id="open-preview-button" disabled="disabled"
 +                      data-enabled-when="1"
++                      data-basehref="{% url 'wiki_img_editor_readonly' document.slug %}">{% trans "View version" %}</button>
 +
 +      </div>
 +    <div id="history-view">
 +        <p class="message-box" style="display:none;"></p>
 +
 +              <table id="changes-list-container">
 +        <tbody id="changes-list">
 +        </tbody>
 +              <tbody style="display: none;">
 +                      <tr class="entry row-stub">
 +                      <td data-stub-value="version"></td>
 +                      <td>
 +                <span data-stub-value="date"></span>
 +              <br/><span data-stub-value="author"></span>
 +                              <br />
 +                              <span data-stub-value="description"></span>
 +                      </td>
 +                      <td>
 +                <div data-stub-value="publishable"></div>
 +                <div data-stub-value="tag"></div>
 +                      </td>
 +              </tr>
 +              </tbody>
 +              </table>
 +    </div>
 +</div>
diff --combined apps/wiki_img/urls.py
index 518da6b,0000000..6a516f3
mode 100644,000000..100644
--- /dev/null
@@@ -1,24 -1,0 +1,24 @@@
- from django.conf.urls.defaults import *
 +# -*- coding: utf-8
++from django.conf.urls import patterns, url
 +
 +
 +urlpatterns = patterns('wiki_img.views',
 +    url(r'^edit/(?P<slug>[^/]+)/$',
 +        'editor', name="wiki_img_editor"),
 +
 +    url(r'^readonly/(?P<slug>[^/]+)/$',
 +        'editor_readonly', name="wiki_img_editor_readonly"),
 +
 +    url(r'^text/(?P<image_id>\d+)/$',
 +        'text', name="wiki_img_text"),
 +
 +    url(r'^history/(?P<object_id>\d+)/$',
 +        'history', name="wiki_img_history"),
 +
 +    url(r'^revert/(?P<object_id>\d+)/$',
 +        'revert', name='wiki_img_revert'),
 +
 +    url(r'^diff/(?P<object_id>\d+)/$', 'diff', name="wiki_img_diff"),
 +    url(r'^pubmark/(?P<object_id>\d+)/$', 'pubmark', name="wiki_img_pubmark"),
 +
 +)
diff --combined apps/wiki_img/views.py
index 2b987dc,0000000..08a0a03
mode 100644,000000..100644
--- /dev/null
@@@ -1,214 -1,0 +1,213 @@@
- from django.views.generic.simple import direct_to_template
 +import os
 +import functools
 +import logging
 +logger = logging.getLogger("fnp.wiki_img")
 +
- from django.shortcuts import get_object_or_404
 +from django.core.urlresolvers import reverse
 +from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
 +                ajax_require_permission)
 +
 +from django import http
-     return direct_to_template(request, template_name, extra_context={
++from django.shortcuts import get_object_or_404, render
 +from django.views.decorators.http import require_GET, require_POST
 +from django.conf import settings
 +from django.utils.formats import localize
 +from django.utils.translation import ugettext as _
 +
 +from catalogue.models import Image
 +from wiki import forms
 +from wiki import nice_diff
 +from wiki_img.forms import ImageSaveForm
 +
 +#
 +# Quick hack around caching problems, TODO: use ETags
 +#
 +from django.views.decorators.cache import never_cache
 +
 +
 +@never_cache
 +def editor(request, slug, template_name='wiki_img/document_details.html'):
 +    doc = get_object_or_404(Image, slug=slug)
 +
-     return direct_to_template(request, template_name, extra_context={
++    return render(request, template_name, {
 +        'document': doc,
 +        'forms': {
 +            "text_save": ImageSaveForm(user=request.user, prefix="textsave"),
 +            "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
 +            "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
 +        },
 +        'can_pubmark': request.user.has_perm('catalogue.can_pubmark_image'),
 +        'REDMINE_URL': settings.REDMINE_URL,
 +    })
 +
 +
 +@require_GET
 +def editor_readonly(request, slug, template_name='wiki_img/document_details_readonly.html'):
 +    doc = get_object_or_404(Image, slug=slug)
 +    try:
 +        revision = request.GET['revision']
 +    except (KeyError):
 +        raise Http404
 +
++    return render(request, template_name, {
 +        'document': doc,
 +        'revision': revision,
 +        'readonly': True,
 +        'REDMINE_URL': settings.REDMINE_URL,
 +    })
 +
 +
 +@never_cache
 +def text(request, image_id):
 +    doc = get_object_or_404(Image, pk=image_id)
 +    if request.method == 'POST':
 +        form = ImageSaveForm(request.POST, user=request.user, prefix="textsave")
 +        if form.is_valid():
 +            if request.user.is_authenticated():
 +                author = request.user
 +            else:
 +                author = None
 +            text = form.cleaned_data['text']
 +            parent_revision = form.cleaned_data['parent_revision']
 +            if parent_revision is not None:
 +                parent = doc.at_revision(parent_revision)
 +            else:
 +                parent = None
 +            stage = form.cleaned_data['stage_completed']
 +            tags = [stage] if stage else []
 +            publishable = (form.cleaned_data['publishable'] and
 +                    request.user.has_perm('catalogue.can_pubmark_image'))
 +            doc.commit(author=author,
 +                   text=text,
 +                   parent=parent,
 +                   description=form.cleaned_data['comment'],
 +                   tags=tags,
 +                   author_name=form.cleaned_data['author_name'],
 +                   author_email=form.cleaned_data['author_email'],
 +                   publishable=publishable,
 +                )
 +            revision = doc.revision()
 +            return JSONResponse({
 +                'text': doc.materialize() if parent_revision != revision else None,
 +                'meta': {},
 +                'revision': revision,
 +            })
 +        else:
 +            return JSONFormInvalid(form)
 +    else:
 +        revision = request.GET.get("revision", None)
 +        
 +        try:
 +            revision = int(revision)
 +        except (ValueError, TypeError):
 +            revision = doc.revision()
 +
 +        if revision is not None:
 +            text = doc.at_revision(revision).materialize()
 +        else:
 +            text = ''
 +
 +        return JSONResponse({
 +            'text': text,
 +            'meta': {},
 +            'revision': revision,
 +        })
 +
 +
 +@never_cache
 +def history(request, object_id):
 +    # TODO: pagination
 +    doc = get_object_or_404(Image, pk=object_id)
 +    if not doc.accessible(request):
 +        return HttpResponseForbidden("Not authorized.")
 +
 +    changes = []
 +    for change in doc.history().reverse():
 +        changes.append({
 +                "version": change.revision,
 +                "description": change.description,
 +                "author": change.author_str(),
 +                "date": localize(change.created_at),
 +                "publishable": _("Publishable") + "\n" if change.publishable else "",
 +                "tag": ',\n'.join(unicode(tag) for tag in change.tags.all()),
 +            })
 +    return JSONResponse(changes)
 +
 +
 +@never_cache
 +@require_POST
 +def revert(request, object_id):
 +    form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
 +    if form.is_valid():
 +        doc = get_object_or_404(Image, pk=object_id)
 +        if not doc.accessible(request):
 +            return HttpResponseForbidden("Not authorized.")
 +
 +        revision = form.cleaned_data['revision']
 +
 +        comment = form.cleaned_data['comment']
 +        comment += "\n#revert to %s" % revision
 +
 +        if request.user.is_authenticated():
 +            author = request.user
 +        else:
 +            author = None
 +
 +        before = doc.revision()
 +        logger.info("Reverting %s to %s", object_id, revision)
 +        doc.at_revision(revision).revert(author=author, description=comment)
 +
 +        return JSONResponse({
 +            'text': doc.materialize() if before != doc.revision() else None,
 +            'meta': {},
 +            'revision': doc.revision(),
 +        })
 +    else:
 +        return JSONFormInvalid(form)
 +
 +
 +@never_cache
 +def diff(request, object_id):
 +    revA = int(request.GET.get('from', 0))
 +    revB = int(request.GET.get('to', 0))
 +
 +    if revA > revB:
 +        revA, revB = revB, revA
 +
 +    if revB == 0:
 +        revB = None
 +
 +    doc = get_object_or_404(Image, pk=object_id)
 +    if not doc.accessible(request):
 +        return HttpResponseForbidden("Not authorized.")
 +
 +    # allow diff from the beginning
 +    if revA:
 +        docA = doc.at_revision(revA).materialize()
 +    else:
 +        docA = ""
 +    docB = doc.at_revision(revB).materialize()
 +
 +    return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
 +                                         docB.splitlines(), context=3))
 +
 +
 +@require_POST
 +@ajax_require_permission('catalogue.can_pubmark_image')
 +def pubmark(request, object_id):
 +    form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
 +    if form.is_valid():
 +        doc = get_object_or_404(Image, pk=object_id)
 +        if not doc.accessible(request):
 +            return HttpResponseForbidden("Not authorized.")
 +
 +        revision = form.cleaned_data['revision']
 +        publishable = form.cleaned_data['publishable']
 +        change = doc.at_revision(revision)
 +        if publishable != change.publishable:
 +            change.set_publishable(publishable)
 +            return JSONResponse({"message": _("Revision marked")})
 +        else:
 +            return JSONResponse({"message": _("Nothing changed")})
 +    else:
 +        return JSONFormInvalid(form)
diff --combined lib/librarian
@@@ -1,1 -1,1 +1,1 @@@
- Subproject commit 859b209f96275dce7205f7c8011784d409a48de8
 -Subproject commit 6351b433475b8119c8cd0cf7a8c72c623308c5d3
++Subproject commit 75a1b6f79da05e694d2528769cad5e964d761f34
@@@ -40,7 -40,11 +40,11 @@@ USE_L10N = Tru
  # Absolute path to the directory that holds media.
  # Example: "/home/media/media.lawrence.com/"
  MEDIA_ROOT = PROJECT_ROOT + '/media/dynamic'
- STATIC_ROOT = PROJECT_ROOT + '/static/'
+ STATIC_ROOT = PROJECT_ROOT + '/../static/'
+ STATICFILES_DIRS = [
+     PROJECT_ROOT + '/static/'
+ ]
  
  # URL that handles the media served from MEDIA_ROOT. Make sure to use a
  # trailing slash if there is a path component (optional in other cases).
  MEDIA_URL = '/media/dynamic/'
  STATIC_URL = '/media/static/'
  
- # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
- # trailing slash.
- # Examples: "http://foo.com/media/", "/media/".
- ADMIN_MEDIA_PREFIX = '/media/admin-media/'
- # Make this unique, and don't share it with anybody.
- SECRET_KEY = 'ife@x^_lak+x84=lxtr!-ur$5g$+s6xt85gbbm@e_fk6q3r8=+'
  SESSION_COOKIE_NAME = "redakcja_sessionid"
  
  # List of callables that know how to import templates from various sources.
@@@ -77,6 -74,7 +74,7 @@@ MIDDLEWARE_CLASSES = 
      'django.middleware.common.CommonMiddleware',
      'django.middleware.csrf.CsrfViewMiddleware',
      'django.contrib.sessions.middleware.SessionMiddleware',
+     'django.contrib.messages.middleware.MessageMiddleware',
  
      'django.contrib.auth.middleware.AuthenticationMiddleware',
      'django_cas.middleware.CASMiddleware',
@@@ -103,24 -101,26 +101,27 @@@ INSTALLED_APPS = 
      'django.contrib.auth',
      'django.contrib.contenttypes',
      'django.contrib.sessions',
+     'django.contrib.messages',
+     'django.contrib.staticfiles',
      'django.contrib.sites',
      'django.contrib.admin',
      'django.contrib.admindocs',
      'django.contrib.comments',
  
-     'compress',
      'south',
      'sorl.thumbnail',
-     'filebrowser',
      'pagination',
      'gravatar',
      'djcelery',
      'djkombu',
+     'fileupload',
+     'pipeline',
  
      'catalogue',
+     'cover',
      'dvcs',
      'wiki',
 +    'wiki_img',
      'toolbar',
      'apiclient',
      'email_mangler',
@@@ -131,15 -131,9 +132,9 @@@ LOGIN_REDIRECT_URL = '/documents/user
  CAS_USER_ATTRS_MAP = {
      'email': 'email', 'firstname': 'first_name', 'lastname': 'last_name'}
  
- FILEBROWSER_URL_FILEBROWSER_MEDIA = STATIC_URL + 'filebrowser/'
- FILEBROWSER_DIRECTORY = 'images/'
- FILEBROWSER_ADMIN_VERSIONS = []
- FILEBROWSER_VERSIONS_BASEDIR = 'thumbnails/'
- FILEBROWSER_DEFAULT_ORDER = "path_relative"
  # REPOSITORY_PATH = '/Users/zuber/Projekty/platforma/files/books'
- IMAGE_DIR = 'images'
+ IMAGE_DIR = 'images/'
  
  
  import djcelery
@@@ -1,5 -1,18 +1,18 @@@
+ STATICFILES_FINDERS = (
+     'django.contrib.staticfiles.finders.FileSystemFinder',
+     'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+ #    'django.contrib.staticfiles.finders.DefaultStorageFinder',
+ )
+ STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
+ PIPELINE_CSS_COMPRESSOR = None
+ PIPELINE_JS_COMPRESSOR = None
+ PIPELINE_STORAGE = 'pipeline.storage.PipelineFinderStorage'
  # CSS and JS files to compress
COMPRESS_CSS = {
PIPELINE_CSS = {
      'detail': {
           'source_filenames': (
              'css/master.css',
              'css/summary.css',
              'css/html.css',
              'css/jquery.autocomplete.css',
 +            'css/imgareaselect-default.css', #img!
              'css/dialogs.css',
          ),
-         'output_filename': 'compressed/detail_styles_?.css',
+         'output_filename': 'compressed/detail_styles.css',
      },
      'catalogue': {
          'source_filenames': (
              'css/filelist.css',
          ),
-         'output_filename': 'compressed/catalogue_styles_?.css',
-      }
+         'output_filename': 'compressed/catalogue_styles.css',
+      },
+      'book': {
+         'source_filenames': (
+             'css/book.css',
+         ),
+         'output_filename': 'compressed/book.css',
+     },
+     'book_list': {
+         'source_filenames': (
+             'contextmenu/jquery.contextMenu.css',
+             'css/book_list.css',
+         ),
+         'output_filename': 'compressed/book_list.css',
+     },
  }
  
COMPRESS_JS = {
PIPELINE_JS = {
      # everything except codemirror
      'detail': {
          'source_filenames': (
                  'js/wiki/view_search.js',
                  'js/wiki/view_column_diff.js',
          ),
-         'output_filename': 'compressed/detail_scripts_?.js',
+         'output_filename': 'compressed/detail_scripts.js',
       },
-         'output_filename': 'compressed/detail_img_scripts_?.js',
 +    'wiki_img': {
 +        'source_filenames': (
 +                # libraries
 +                'js/lib/jquery-1.4.2.min.js',
 +                'js/lib/jquery/jquery.autocomplete.js',
 +                'js/lib/jquery/jquery.blockui.js',
 +                'js/lib/jquery/jquery.elastic.js',
 +                'js/lib/jquery/jquery.imgareaselect.js',
 +                'js/button_scripts.js',
 +                'js/slugify.js',
 +
 +                # wiki scripts
 +                'js/wiki_img/wikiapi.js',
 +
 +                # base UI
 +                'js/wiki_img/base.js',
 +                'js/wiki/toolbar.js',
 +
 +                # dialogs
 +                'js/wiki/dialog_save.js',
 +                'js/wiki/dialog_revert.js',
 +                'js/wiki/dialog_pubmark.js',
 +
 +                # views
 +                'js/wiki_img/view_editor_objects.js',
 +                'js/wiki_img/view_editor_motifs.js',
 +                'js/wiki/view_editor_source.js',
 +                'js/wiki/view_history.js',
 +                'js/wiki/view_column_diff.js',
 +        ),
++        'output_filename': 'compressed/detail_img_scripts.js',
 +     },
      'catalogue': {
          'source_filenames': (
                  'js/catalogue/catalogue.js',
                  'js/slugify.js',
                  'email_mangler/email_mangler.js',
          ),
-         'output_filename': 'compressed/catalogue_scripts_?.js',
-      }
+         'output_filename': 'compressed/catalogue_scripts.js',
+      },
+      'book': {
+         'source_filenames': (
+             'js/book_text/jquery.eventdelegation.js',
+             'js/book_text/jquery.scrollto.js',
+             'js/book_text/jquery.highlightfade.js',
+             'js/book_text/book.js',
+         ),
+         'output_filename': 'compressed/book.js',
+          },
+     'book_list': {
+         'source_filenames': (
+             'contextmenu/jquery.ui.position.js',
+             'contextmenu/jquery.contextMenu.js',
+             'js/catalogue/book_list.js',
+         ),
+         'output_filename': 'compressed/book_list.js',
+     }
  }
- COMPRESS = True
- COMPRESS_CSS_FILTERS = None
- COMPRESS_JS_FILTERS = None
- COMPRESS_AUTO = True
- COMPRESS_VERSION = True
- COMPRESS_VERSIONING = 'compress.versioning.hash.MD5Versioning'
@@@ -46,7 -46,7 +46,7 @@@ td 
      margin-bottom: -1px;
      border-width: 1px;
      border-style: solid;
 -    border-color: rgba(0,0,0,0);
 +    border-color: transparent;
  }
  
  #tabs-nav-left .active {
  
  #last-edited-list .date {
        font-size: 70%;
 -      color: grey;
 +      color: #808080;
  }
  
  a, a:visited, a:active {
 -      color: #bf6000;
 +      color: #a05000;
        text-decoration: none;
  }
  
@@@ -223,4 -223,16 +223,16 @@@ a:hover 
  
  .wall .publish {
      background-color: #fdc;
- }
+ }
+ div.cover-preview {
+       width: 216px;
+       min-height: 300px;
+       float: left;
+       margin-right: 2em;
+ }
+ img.cover-preview {
+       width: 216px;
+       height: 300px;
+ }
@@@ -28,13 -28,14 +28,14 @@@ body 
      border-right: 2px solid #999;
      cursor: pointer;  
      background: #C1C1C1; 
+     z-index:100;
+     cursor: col-resize;
  }
  
  .vsplitbar:hover {
        background-color: #E6E6E6;
  }
  
  .vsplitbar p {
      font: 12px Helvetica,Verdana,sans-serif;
  
    margin: 250px auto;
  }
  
+ #drag-layer {
+     position:absolute;
+     top:0;
+     bottom:0;
+     left:0;
+     right:0;
+     z-index:1000;
+     display: none;
+     cursor: col-resize;
+ }
  .editor {
      position: absolute;
      top: 0px;
      overflow: hidden;
  }
  
 +.sideless .editor {
 +    right: 0;
 +}
 +.image-object {
 +    padding-left: 1em;
 +    font: 12px Sans, Helvetica, Verdana, sans-serif;
 +}
 +.image-object:hover {
 +    cursor: pointer;
 +}
 +#objects-list .delete {
 +    padding-left: 3px;
 +    font: 10px Sans, Helvetica, Verdana, sans-serif;
 +}
 +#objects-list .delete:hover {
 +    cursor: pointer;
 +}
 +
 +#objects-list .active {
 +    color: #800;
 +}
 +
 +
  #editor.readonly .editor {
        right: 0px;
  }
  
      font: 11px Helvetica, Verdana, sans-serif;
      font-weight: bold;
 +
 +    z-index: 100;
  }
  
  
      float: left;
  }
  
 +.tabs a {
 +    color: black;
 +}
 +
  #tabs-right {
      float: right;
      padding-right: 1em;
@@@ -365,7 -348,7 +377,7 @@@ img.tabclose 
  
  .saveNotify {
      position:absolute; 
-     top:22px; 
+     bottom:22px; 
      right:7px; 
      z-index:800;
      background-color: #FFFF69; 
  .saveNotify span {
      font-weight: bold;
  }
 +
 +
 +
 +.scrolled {
 +    position: absolute;
 +    top: 29px;
 +    left: 0;
 +    right: 0;
 +    bottom: 0;
 +    overflow: auto;
 +}
index cb31eef,0000000..7e1b790
mode 100644,000000..100644
--- /dev/null
@@@ -1,716 -1,0 +1,730 @@@
-  * version 0.9.3
 +/*
 + * imgAreaSelect jQuery plugin
-  * Copyright (c) 2008-2010 Michal Wojciechowski (odyniec.net)
++ * version 0.9.10
 + *
-         resizeMargin = 10,
++ * Copyright (c) 2008-2013 Michal Wojciechowski (odyniec.net)
 + *
 + * Dual licensed under the MIT (MIT-LICENSE.txt)
 + * and GPL (GPL-LICENSE.txt) licenses.
 + *
 + * http://odyniec.net/projects/imgareaselect/
 + *
 + */
 +
 +(function($) {
 +
 +var abs = Math.abs,
 +    max = Math.max,
 +    min = Math.min,
 +    round = Math.round;
 +
 +function div() {
 +    return $('<div/>');
 +}
 +
 +$.imgAreaSelect = function (img, options) {
 +    var
 +
 +        $img = $(img),
 +
 +        imgLoaded,
 +
 +        $box = div(),
 +        $area = div(),
 +        $border = div().add(div()).add(div()).add(div()),
 +        $outer = div().add(div()).add(div()).add(div()),
 +        $handles = $([]),
 +
 +        $areaOpera,
 +
 +        left, top,
 +
 +        imgOfs = { left: 0, top: 0 },
 +
 +        imgWidth, imgHeight,
 +
 +        $parent,
 +
 +        parOfs = { left: 0, top: 0 },
 +
 +        zIndex = 0,
 +
 +        position = 'absolute',
 +
 +        startX, startY,
 +
 +        scaleX, scaleY,
 +
-             x1: round(x1 / sx),
-             y1: round(y1 / sy),
-             x2: round(x2 / sx),
-             y2: round(y2 / sy)
 +        resize,
 +
 +        minWidth, minHeight, maxWidth, maxHeight,
 +
 +        aspectRatio,
 +
 +        shown,
 +
 +        x1, y1, x2, y2,
 +
 +        selection = { x1: 0, y1: 0, x2: 0, y2: 0, width: 0, height: 0 },
 +
 +        docElem = document.documentElement,
 +
++        ua = navigator.userAgent,
++
 +        $p, d, i, o, w, h, adjusted;
 +
 +    function viewX(x) {
 +        return x + imgOfs.left - parOfs.left;
 +    }
 +
 +    function viewY(y) {
 +        return y + imgOfs.top - parOfs.top;
 +    }
 +
 +    function selX(x) {
 +        return x - imgOfs.left + parOfs.left;
 +    }
 +
 +    function selY(y) {
 +        return y - imgOfs.top + parOfs.top;
 +    }
 +
 +    function evX(event) {
 +        return event.pageX - parOfs.left;
 +    }
 +
 +    function evY(event) {
 +        return event.pageY - parOfs.top;
 +    }
 +
 +    function getSelection(noScale) {
 +        var sx = noScale || scaleX, sy = noScale || scaleY;
 +
 +        return { x1: round(selection.x1 * sx),
 +            y1: round(selection.y1 * sy),
 +            x2: round(selection.x2 * sx),
 +            y2: round(selection.y2 * sy),
 +            width: round(selection.x2 * sx) - round(selection.x1 * sx),
 +            height: round(selection.y2 * sy) - round(selection.y1 * sy) };
 +    }
 +
 +    function setSelection(x1, y1, x2, y2, noScale) {
 +        var sx = noScale || scaleX, sy = noScale || scaleY;
 +
 +        selection = {
-         if (!$img.width())
++            x1: round(x1 / sx || 0),
++            y1: round(y1 / sy || 0),
++            x2: round(x2 / sx || 0),
++            y2: round(y2 / sy || 0)
 +        };
 +
 +        selection.width = selection.x2 - selection.x1;
 +        selection.height = selection.y2 - selection.y1;
 +    }
 +
 +    function adjust() {
-         imgWidth = $img.width();
-         imgHeight = $img.height();
++        if (!imgLoaded || !$img.width())
 +            return;
 +
 +        imgOfs = { left: round($img.offset().left), top: round($img.offset().top) };
 +
-         minWidth = options.minWidth || 0;
-         minHeight = options.minHeight || 0;
-         maxWidth = min(options.maxWidth || 1<<24, imgWidth);
-         maxHeight = min(options.maxHeight || 1<<24, imgHeight);
++        imgWidth = $img.innerWidth();
++        imgHeight = $img.innerHeight();
++
++        imgOfs.top += ($img.outerHeight() - imgHeight) >> 1;
++        imgOfs.left += ($img.outerWidth() - imgWidth) >> 1;
 +
-         parOfs = $.inArray($parent.css('position'), ['absolute', 'relative']) + 1 ?
++        minWidth = round(options.minWidth / scaleX) || 0;
++        minHeight = round(options.minHeight / scaleY) || 0;
++        maxWidth = round(min(options.maxWidth / scaleX || 1<<24, imgWidth));
++        maxHeight = round(min(options.maxHeight / scaleY || 1<<24, imgHeight));
 +
 +        if ($().jquery == '1.3.2' && position == 'fixed' &&
 +            !docElem['getBoundingClientRect'])
 +        {
 +            imgOfs.top += max(document.body.scrollTop, docElem.scrollTop);
 +            imgOfs.left += max(document.body.scrollLeft, docElem.scrollLeft);
 +        }
 +
-             $($handles[4]).css({ left: w / 2 });
-             $($handles[5]).css({ left: w, top: h / 2 });
-             $($handles[6]).css({ left: w / 2, top: h });
-             $($handles[7]).css({ top: h / 2 });
++        parOfs = /absolute|relative/.test($parent.css('position')) ?
 +            { left: round($parent.offset().left) - $parent.scrollLeft(),
 +                top: round($parent.offset().top) - $parent.scrollTop() } :
 +            position == 'fixed' ?
 +                { left: $(document).scrollLeft(), top: $(document).scrollTop() } :
 +                { left: 0, top: 0 };
 +
 +        left = viewX(0);
 +        top = viewY(0);
 +
 +        if (selection.x2 > imgWidth || selection.y2 > imgHeight)
 +            doResize();
 +    }
 +
 +    function update(resetKeyPress) {
 +        if (!shown) return;
 +
 +        $box.css({ left: viewX(selection.x1), top: viewY(selection.y1) })
 +            .add($area).width(w = selection.width).height(h = selection.height);
 +
 +        $area.add($border).add($handles).css({ left: 0, top: 0 });
 +
 +        $border
 +            .width(max(w - $border.outerWidth() + $border.innerWidth(), 0))
 +            .height(max(h - $border.outerHeight() + $border.innerHeight(), 0));
 +
 +        $($outer[0]).css({ left: left, top: top,
 +            width: selection.x1, height: imgHeight });
 +        $($outer[1]).css({ left: left + selection.x1, top: top,
 +            width: w, height: selection.y1 });
 +        $($outer[2]).css({ left: left + selection.x2, top: top,
 +            width: imgWidth - selection.x2, height: imgHeight });
 +        $($outer[3]).css({ left: left + selection.x1, top: top + selection.y2,
 +            width: w, height: imgHeight - selection.y2 });
 +
 +        w -= $handles.outerWidth();
 +        h -= $handles.outerHeight();
 +
 +        switch ($handles.length) {
 +        case 8:
-             if ($.imgAreaSelect.keyPress != docKeyPress)
++            $($handles[4]).css({ left: w >> 1 });
++            $($handles[5]).css({ left: w, top: h >> 1 });
++            $($handles[6]).css({ left: w >> 1, top: h });
++            $($handles[7]).css({ top: h >> 1 });
 +        case 4:
 +            $handles.slice(1,3).css({ left: w });
 +            $handles.slice(2,4).css({ top: h });
 +        }
 +
 +        if (resetKeyPress !== false) {
-         if ($.browser.msie && $border.outerWidth() - $border.innerWidth() == 2) {
++            if ($.imgAreaSelect.onKeyPress != docKeyPress)
 +                $(document).unbind($.imgAreaSelect.keyPress,
 +                    $.imgAreaSelect.onKeyPress);
 +
 +            if (options.keys)
 +                $(document)[$.imgAreaSelect.keyPress](
 +                    $.imgAreaSelect.onKeyPress = docKeyPress);
 +        }
 +
-             if (y <= resizeMargin)
++        if (msie && $border.outerWidth() - $border.innerWidth() == 2) {
 +            $border.css('margin', 0);
 +            setTimeout(function () { $border.css('margin', 'auto'); }, 0);
 +        }
 +    }
 +
 +    function doUpdate(resetKeyPress) {
 +        adjust();
 +        update(resetKeyPress);
 +        x1 = viewX(selection.x1); y1 = viewY(selection.y1);
 +        x2 = viewX(selection.x2); y2 = viewY(selection.y2);
 +    }
 +
 +    function hide($elem, fn) {
 +        options.fadeSpeed ? $elem.fadeOut(options.fadeSpeed, fn) : $elem.hide();
 +
 +    }
 +
 +    function areaMouseMove(event) {
 +        var x = selX(evX(event)) - selection.x1,
 +            y = selY(evY(event)) - selection.y1;
 +
 +        if (!adjusted) {
 +            adjust();
 +            adjusted = true;
 +
 +            $box.one('mouseout', function () { adjusted = false; });
 +        }
 +
 +        resize = '';
 +
 +        if (options.resizable) {
-             else if (y >= selection.height - resizeMargin)
++            if (y <= options.resizeMargin)
 +                resize = 'n';
-             if (x <= resizeMargin)
++            else if (y >= selection.height - options.resizeMargin)
 +                resize = 's';
-             else if (x >= selection.width - resizeMargin)
++            if (x <= options.resizeMargin)
 +                resize += 'w';
++            else if (x >= selection.width - options.resizeMargin)
 +                resize += 'e';
 +        }
 +
 +        $box.css('cursor', resize ? resize + '-resize' :
 +            options.movable ? 'move' : '');
 +        if ($areaOpera)
 +            $areaOpera.toggle();
 +    }
 +
 +    function docMouseUp(event) {
 +        $('body').css('cursor', '');
-         options.onSelectEnd(img, getSelection());
 +        if (options.autoHide || selection.width * selection.height == 0)
 +            hide($box.add($outer), function () { $(this).hide(); });
 +
-         x2 = resize == '' || /w|e/.test(resize) || aspectRatio ? evX(event) : viewX(selection.x2);
-         y2 = resize == '' || /n|s/.test(resize) || aspectRatio ? evY(event) : viewY(selection.y2);
 +        $(document).unbind('mousemove', selectingMouseMove);
 +        $box.mousemove(areaMouseMove);
++
++        options.onSelectEnd(img, getSelection());
 +    }
 +
 +    function areaMouseDown(event) {
 +        if (event.which != 1) return false;
 +
 +        adjust();
 +
 +        if (resize) {
 +            $('body').css('cursor', resize + '-resize');
 +
 +            x1 = viewX(selection[/w/.test(resize) ? 'x2' : 'x1']);
 +            y1 = viewY(selection[/n/.test(resize) ? 'y2' : 'y1']);
 +
 +            $(document).mousemove(selectingMouseMove)
 +                .one('mouseup', docMouseUp);
 +            $box.unbind('mousemove', areaMouseMove);
 +        }
 +        else if (options.movable) {
 +            startX = left + selection.x1 - evX(event);
 +            startY = top + selection.y1 - evY(event);
 +
 +            $box.unbind('mousemove', areaMouseMove);
 +
 +            $(document).mousemove(movingMouseMove)
 +                .one('mouseup', function () {
 +                    options.onSelectEnd(img, getSelection());
 +
 +                    $(document).unbind('mousemove', movingMouseMove);
 +                    $box.mousemove(areaMouseMove);
 +                });
 +        }
 +        else
 +            $img.mousedown(event);
 +
 +        return false;
 +    }
 +
 +    function fixAspectRatio(xFirst) {
 +        if (aspectRatio)
 +            if (xFirst) {
 +                x2 = max(left, min(left + imgWidth,
 +                    x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1)));
 +
 +                y2 = round(max(top, min(top + imgHeight,
 +                    y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1))));
 +                x2 = round(x2);
 +            }
 +            else {
 +                y2 = max(top, min(top + imgHeight,
 +                    y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1)));
 +                x2 = round(max(left, min(left + imgWidth,
 +                    x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1))));
 +                y2 = round(y2);
 +            }
 +    }
 +
 +    function doResize() {
 +        x1 = min(x1, left + imgWidth);
 +        y1 = min(y1, top + imgHeight);
 +
 +        if (abs(x2 - x1) < minWidth) {
 +            x2 = x1 - minWidth * (x2 < x1 || -1);
 +
 +            if (x2 < left)
 +                x1 = left + minWidth;
 +            else if (x2 > left + imgWidth)
 +                x1 = left + imgWidth - minWidth;
 +        }
 +
 +        if (abs(y2 - y1) < minHeight) {
 +            y2 = y1 - minHeight * (y2 < y1 || -1);
 +
 +            if (y2 < top)
 +                y1 = top + minHeight;
 +            else if (y2 > top + imgHeight)
 +                y1 = top + imgHeight - minHeight;
 +        }
 +
 +        x2 = max(left, min(x2, left + imgWidth));
 +        y2 = max(top, min(y2, top + imgHeight));
 +
 +        fixAspectRatio(abs(x2 - x1) < abs(y2 - y1) * aspectRatio);
 +
 +        if (abs(x2 - x1) > maxWidth) {
 +            x2 = x1 - maxWidth * (x2 < x1 || -1);
 +            fixAspectRatio();
 +        }
 +
 +        if (abs(y2 - y1) > maxHeight) {
 +            y2 = y1 - maxHeight * (y2 < y1 || -1);
 +            fixAspectRatio(true);
 +        }
 +
 +        selection = { x1: selX(min(x1, x2)), x2: selX(max(x1, x2)),
 +            y1: selY(min(y1, y2)), y2: selY(max(y1, y2)),
 +            width: abs(x2 - x1), height: abs(y2 - y1) };
 +
 +        update();
 +
 +        options.onSelectChange(img, getSelection());
 +    }
 +
 +    function selectingMouseMove(event) {
-         if ($outer.is(':not(:visible)'))
++        x2 = /w|e|^$/.test(resize) || aspectRatio ? evX(event) : viewX(selection.x2);
++        y2 = /n|s|^$/.test(resize) || aspectRatio ? evY(event) : viewY(selection.y2);
 +
 +        doResize();
 +
 +        return false;
 +
 +    }
 +
 +    function doMove(newX1, newY1) {
 +        x2 = (x1 = newX1) + selection.width;
 +        y2 = (y1 = newY1) + selection.height;
 +
 +        $.extend(selection, { x1: selX(x1), y1: selY(y1), x2: selX(x2),
 +            y2: selY(y2) });
 +
 +        update();
 +
 +        options.onSelectChange(img, getSelection());
 +    }
 +
 +    function movingMouseMove(event) {
 +        x1 = max(left, min(startX + evX(event), left + imgWidth - selection.width));
 +        y1 = max(top, min(startY + evY(event), top + imgHeight - selection.height));
 +
 +        doMove(x1, y1);
 +
 +        event.preventDefault();
 +
 +        return false;
 +    }
 +
 +    function startSelection() {
++        $(document).unbind('mousemove', startSelection);
 +        adjust();
 +
 +        x2 = x1;
 +        y2 = y1;
 +
 +        doResize();
 +
 +        resize = '';
 +
-         $(document).unbind('mousemove', startSelection);
++        if (!$outer.is(':visible'))
 +            $box.add($outer).hide().fadeIn(options.fadeSpeed||0);
 +
 +        shown = true;
 +
 +        $(document).unbind('mouseup', cancelSelection)
 +            .mousemove(selectingMouseMove).one('mouseup', docMouseUp);
 +        $box.unbind('mousemove', areaMouseMove);
 +
 +        options.onSelectStart(img, getSelection());
 +    }
 +
 +    function cancelSelection() {
-         selection = { x1: selX(x1), y1: selY(y1), x2: selX(x1), y2: selY(y1),
-                 width: 0, height: 0 };
++        $(document).unbind('mousemove', startSelection)
++            .unbind('mouseup', cancelSelection);
 +        hide($box.add($outer));
 +
-         options.onSelectChange(img, getSelection());
-         options.onSelectEnd(img, getSelection());
++        setSelection(selX(x1), selY(y1), selX(x1), selY(y1));
 +
-         $(document).one('mousemove', startSelection)
-             .one('mouseup', cancelSelection);
++        if (!(this instanceof $.imgAreaSelect)) {
++            options.onSelectChange(img, getSelection());
++            options.onSelectEnd(img, getSelection());
++        }
 +    }
 +
 +    function imgMouseDown(event) {
 +        if (event.which != 1 || $outer.is(':animated')) return false;
 +
 +        adjust();
 +        startX = x1 = evX(event);
 +        startY = y1 = evY(event);
 +
-             resizable: true,
++        $(document).mousemove(startSelection).mouseup(cancelSelection);
 +
 +        return false;
 +    }
 +
 +    function windowResize() {
 +        doUpdate(false);
 +    }
 +
 +    function imgLoad() {
 +        imgLoaded = true;
 +
 +        setOptions(options = $.extend({
 +            classPrefix: 'imgareaselect',
 +            movable: true,
-         for (option in props)
 +            parent: 'body',
++            resizable: true,
++            resizeMargin: 10,
 +            onInit: function () {},
 +            onSelectStart: function () {},
 +            onSelectChange: function () {},
 +            onSelectEnd: function () {}
 +        }, options));
 +
 +        $box.add($outer).css({ visibility: '' });
 +
 +        if (options.show) {
 +            shown = true;
 +            adjust();
 +            update();
 +            $box.add($outer).hide().fadeIn(options.fadeSpeed||0);
 +        }
 +
 +        setTimeout(function () { options.onInit(img, getSelection()); }, 0);
 +    }
 +
 +    var docKeyPress = function(event) {
 +        var k = options.keys, d, t, key = event.keyCode;
 +
 +        d = !isNaN(k.alt) && (event.altKey || event.originalEvent.altKey) ? k.alt :
 +            !isNaN(k.ctrl) && event.ctrlKey ? k.ctrl :
 +            !isNaN(k.shift) && event.shiftKey ? k.shift :
 +            !isNaN(k.arrows) ? k.arrows : 10;
 +
 +        if (k.arrows == 'resize' || (k.shift == 'resize' && event.shiftKey) ||
 +            (k.ctrl == 'resize' && event.ctrlKey) ||
 +            (k.alt == 'resize' && (event.altKey || event.originalEvent.altKey)))
 +        {
 +            switch (key) {
 +            case 37:
 +                d = -d;
 +            case 39:
 +                t = max(x1, x2);
 +                x1 = min(x1, x2);
 +                x2 = max(t + d, x1);
 +                fixAspectRatio();
 +                break;
 +            case 38:
 +                d = -d;
 +            case 40:
 +                t = max(y1, y2);
 +                y1 = min(y1, y2);
 +                y2 = max(t + d, y1);
 +                fixAspectRatio(true);
 +                break;
 +            default:
 +                return;
 +            }
 +
 +            doResize();
 +        }
 +        else {
 +            x1 = min(x1, x2);
 +            y1 = min(y1, y2);
 +
 +            switch (key) {
 +            case 37:
 +                doMove(max(x1 - d, left), y1);
 +                break;
 +            case 38:
 +                doMove(x1, max(y1 - d, top));
 +                break;
 +            case 39:
 +                doMove(x1 + min(d, imgWidth - selX(x2)), y1);
 +                break;
 +            case 40:
 +                doMove(x1, y1 + min(d, imgHeight - selY(y2)));
 +                break;
 +            default:
 +                return;
 +            }
 +        }
 +
 +        return false;
 +    };
 +
 +    function styleOptions($elem, props) {
-         $box.append($area.add($border).add($handles).add($areaOpera));
++        for (var option in props)
 +            if (options[option] !== undefined)
 +                $elem.css(props[option], options[option]);
 +    }
 +
 +    function setOptions(newOptions) {
 +        if (newOptions.parent)
 +            ($parent = $(newOptions.parent)).append($box.add($outer));
 +
 +        $.extend(options, newOptions);
 +
 +        adjust();
 +
 +        if (newOptions.handles != null) {
 +            $handles.remove();
 +            $handles = $([]);
 +
 +            i = newOptions.handles ? newOptions.handles == 'corners' ? 4 : 8 : 0;
 +
 +            while (i--)
 +                $handles = $handles.add(div());
 +
 +            $handles.addClass(options.classPrefix + '-handle').css({
 +                position: 'absolute',
 +                fontSize: 0,
 +                zIndex: zIndex + 1 || 1
 +            });
 +
 +            if (!parseInt($handles.css('width')) >= 0)
 +                $handles.width(5).height(5);
 +
 +            if (o = options.borderWidth)
 +                $handles.css({ borderWidth: o, borderStyle: 'solid' });
 +
 +            styleOptions($handles, { borderColor1: 'border-color',
 +                borderColor2: 'background-color',
 +                borderOpacity: 'opacity' });
 +        }
 +
 +        scaleX = options.imageWidth / imgWidth || 1;
 +        scaleY = options.imageHeight / imgHeight || 1;
 +
 +        if (newOptions.x1 != null) {
 +            setSelection(newOptions.x1, newOptions.y1, newOptions.x2,
 +                newOptions.y2);
 +            newOptions.show = !newOptions.hide;
 +        }
 +
 +        if (newOptions.keys)
 +            options.keys = $.extend({ shift: 1, ctrl: 'resize' },
 +                newOptions.keys);
 +
 +        $outer.addClass(options.classPrefix + '-outer');
 +        $area.addClass(options.classPrefix + '-selection');
 +        for (i = 0; i++ < 4;)
 +            $($border[i-1]).addClass(options.classPrefix + '-border' + i);
 +
 +        styleOptions($area, { selectionColor: 'background-color',
 +            selectionOpacity: 'opacity' });
 +        styleOptions($border, { borderOpacity: 'opacity',
 +            borderWidth: 'border-width' });
 +        styleOptions($outer, { outerColor: 'background-color',
 +            outerOpacity: 'opacity' });
 +        if (o = options.borderColor1)
 +            $($border[0]).css({ borderStyle: 'solid', borderColor: o });
 +        if (o = options.borderColor2)
 +            $($border[1]).css({ borderStyle: 'dashed', borderColor: o });
 +
-         if ($.browser.msie) {
-             if (o = $outer.css('filter').match(/opacity=([0-9]+)/))
++        $box.append($area.add($border).add($areaOpera)).append($handles);
 +
-             if (o = $border.css('filter').match(/opacity=([0-9]+)/))
++        if (msie) {
++            if (o = ($outer.css('filter')||'').match(/opacity=(\d+)/))
 +                $outer.css('opacity', o[1]/100);
-         $img.unbind('mousedown', imgMouseDown);
++            if (o = ($border.css('filter')||'').match(/opacity=(\d+)/))
 +                $border.css('opacity', o[1]/100);
 +        }
 +
 +        if (newOptions.hide)
 +            hide($box.add($outer));
 +        else if (newOptions.show && imgLoaded) {
 +            shown = true;
 +            $box.add($outer).fadeIn(options.fadeSpeed||0);
 +            doUpdate();
 +        }
 +
 +        aspectRatio = (d = (options.aspectRatio || '').split(/:/))[0] / d[1];
 +
 +        $img.add($outer).unbind('mousedown', imgMouseDown);
 +
 +        if (options.disable || options.enable === false) {
 +            $box.unbind('mousemove', areaMouseMove).unbind('mousedown', areaMouseDown);
 +            $(window).unbind('resize', windowResize);
 +        }
 +        else {
 +            if (options.enable || options.disable === false) {
 +                if (options.resizable || options.movable)
 +                    $box.mousemove(areaMouseMove).mousedown(areaMouseDown);
 +
 +                $(window).resize(windowResize);
 +            }
 +
 +            if (!options.persistent)
 +                $img.add($outer).mousedown(imgMouseDown);
 +        }
 +
 +        options.enable = options.disable = undefined;
 +    }
 +
 +    this.remove = function () {
-     if ($.browser.msie)
++        setOptions({ disable: true });
 +        $box.add($outer).remove();
 +    };
 +
 +    this.getOptions = function () { return options; };
 +
 +    this.setOptions = setOptions;
 +
 +    this.getSelection = getSelection;
 +
 +    this.setSelection = setSelection;
 +
++    this.cancelSelection = cancelSelection;
++
 +    this.update = doUpdate;
 +
++    var msie = (/msie ([\w.]+)/i.exec(ua)||[])[1],
++        opera = /opera/i.test(ua),
++        safari = /webkit/i.test(ua) && !/chrome/i.test(ua);
++
 +    $p = $img;
 +
 +    while ($p.length) {
 +        zIndex = max(zIndex,
 +            !isNaN($p.css('z-index')) ? $p.css('z-index') : zIndex);
 +        if ($p.css('position') == 'fixed')
 +            position = 'fixed';
 +
 +        $p = $p.parent(':not(body)');
 +    }
 +
 +    zIndex = options.zIndex || zIndex;
 +
-     $.imgAreaSelect.keyPress = $.browser.msie ||
-         $.browser.safari ? 'keydown' : 'keypress';
++    if (msie)
 +        $img.attr('unselectable', 'on');
 +
-     if ($.browser.opera)
++    $.imgAreaSelect.keyPress = msie || safari ? 'keydown' : 'keypress';
++
++    if (opera)
 +
 +        $areaOpera = div().css({ width: '100%', height: '100%',
 +            position: 'absolute', zIndex: zIndex + 2 || 2 });
 +
 +    $box.add($outer).css({ visibility: 'hidden', position: position,
 +        overflow: 'hidden', zIndex: zIndex || '0' });
 +    $box.css({ zIndex: zIndex + 2 || 2 });
 +    $area.add($border).css({ position: 'absolute', fontSize: 0 });
 +
 +    img.complete || img.readyState == 'complete' || !$img.is('img') ?
 +        imgLoad() : $img.one('load', imgLoad);
++
++    if (!imgLoaded && msie && msie >= 7)
++        img.src = img.src;
 +};
 +
 +$.fn.imgAreaSelect = function (options) {
 +    options = options || {};
 +
 +    this.each(function () {
 +        if ($(this).data('imgAreaSelect')) {
 +            if (options.remove) {
 +                $(this).data('imgAreaSelect').remove();
 +                $(this).removeData('imgAreaSelect');
 +            }
 +            else
 +                $(this).data('imgAreaSelect').setOptions(options);
 +        }
 +        else if (!options.remove) {
 +            if (options.enable === undefined && options.disable === undefined)
 +                options.enable = true;
 +
 +            $(this).data('imgAreaSelect', new $.imgAreaSelect(this, options));
 +        }
 +    });
 +
 +    if (options.instance)
 +        return $(this).data('imgAreaSelect');
 +
 +    return this;
 +};
 +
 +})(jQuery);
@@@ -17,6 -17,23 +17,23 @@@ $(function(
  
      function initialize()
        {
+         var splitter = $('#splitter'),
+             editors = $('#editor .editor'),
+             vsplitbar = $('.vsplitbar'),
+             sidebar = $('#sidebar'),
+             dragLayer = $('#drag-layer'),
+             vsplitbarWidth = vsplitbar.outerWidth(),
+             isHolding = false;
+         // Moves panes so that left border of the vsplitbar lands x pixels from the left border of the splitter
+         function setSplitbarAt(x) {
+             var right = splitterWidth - x;
+             editors.each(function() {
+                 this.style.right = right + 'px';
+             });
+             vsplitbar[0].style.right = sidebar[0].style.width = (right - vsplitbarWidth) + 'px';
+         };
                $(document).keydown(function(event) {
                        console.log("Received key:", event);
                });
@@@ -33,7 -50,6 +50,7 @@@
                 * TABS
                 */
          $('.tabs li').live('click', function(event, callback) {
 +            event.preventDefault();
                        $.wiki.switchToTab(this);
          });
  
  
          $(window).resize(function(){
              $('iframe').height($(window).height() - $('#tabs').outerHeight() - $('#source-editor .toolbar').outerHeight());
+             splitterWidth = splitter.width();
          });
  
          $(window).resize();
  
-         $('.vsplitbar').toggle(
+         vsplitbar.toggle(
                        function() {
                                $.wiki.state.perspectives.ScanGalleryPerspective.show = true;
-                               $('#sidebar').show();
-                               $('.vsplitbar').css('right', 480).addClass('active');
-                               $('#editor .editor').css('right', 510);
+                               setSplitbarAt(splitterWidth - (480 + vsplitbarWidth));
+                               $('.vsplitbar').addClass('active');
                                $(window).resize();
                                $.wiki.perspectiveForTab('#tabs-right .active').onEnter();
                        },
                        function() {
                            var active_right = $.wiki.perspectiveForTab('#tabs-right .active');
                                $.wiki.state.perspectives.ScanGalleryPerspective.show = false;
-                               $('#sidebar').hide();
-                               $('.vsplitbar').css('right', 0).removeClass('active');
                                $(".vsplitbar-title").html("&uarr;&nbsp;" + active_right.vsplitbar + "&nbsp;&uarr;");
-                               $('#editor .editor').css('right', 30);
+                               setSplitbarAt(splitterWidth - vsplitbarWidth);
+                               $('.vsplitbar').removeClass('active');
                                $(window).resize();
                                active_right.onExit();
                        }
                );
  
+         /* Splitbar dragging support */
+         vsplitbar
+             .mousedown(function(e) {
+                 e.preventDefault();
+                 isHolding = true;
+             })
+             .mousemove(function(e) {
+                 if(isHolding) {
+                     dragLayer.show(); // We don't show it up until now so that we don't lose single click events on vsplitbar
+                 }
+             });
+         dragLayer.mousemove(function(e) {
+             setSplitbarAt(e.clientX - vsplitbarWidth/2);
+         });
+         $('body').mouseup(function(e) {
+             dragLayer.hide();
+             isHolding = false;
+         });
                if($.wiki.state.perspectives.ScanGalleryPerspective.show){
              $('.vsplitbar').trigger('click');
              $(".vsplitbar-title").html("&darr;&nbsp;GALERIA&nbsp;&darr;");
diff --combined redakcja/urls.py
@@@ -1,41 -1,35 +1,36 @@@
  # -*- coding: utf-8 -*-
  
- from django.conf.urls.defaults import *
+ from django.conf.urls import include, patterns, url
  from django.contrib import admin
  from django.conf import settings
+ from django.conf.urls.static import static
+ from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+ from django.views.generic import RedirectView
  
  
  admin.autodiscover()
  
  urlpatterns = patterns('',
      # Auth
-     #url(r'^accounts/login/$', 'django.contrib.auth.views.login', name='login'),
-     #url(r'^accounts/logout/$', 'catalogue.views.logout_then_redirect', name='logout'),
      url(r'^accounts/login/$', 'django_cas.views.login', name='login'),
      url(r'^accounts/logout/$', 'django_cas.views.logout', name='logout'),
+     url(r'^admin/login/$', 'django_cas.views.login', name='login'),
+     url(r'^admin/logout/$', 'django_cas.views.logout', name='logout'),
  
      # Admin panel
-     (r'^admin/filebrowser/', include('filebrowser.urls')),
      url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
      (r'^admin/', include(admin.site.urls)),
  
      (r'^comments/', include('django.contrib.comments.urls')),
  
-     url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/documents/'}),
+     url(r'^$', RedirectView.as_view(url= '/documents/')),
      url(r'^documents/', include('catalogue.urls')),
      url(r'^apiclient/', include('apiclient.urls')),
      url(r'^editor/', include('wiki.urls')),
-     # Static files (should be served by Apache)
-     url(r'^%s(?P<path>.+)$' % settings.MEDIA_URL[1:], 'django.views.static.serve',
-         {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}),
-     url(r'^%s(?P<path>.+)$' % settings.ADMIN_MEDIA_PREFIX[1:], 'django.views.static.serve',
-         {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}),
-     url(r'^%s(?P<path>.+)$' % settings.STATIC_URL[1:], 'django.views.static.serve',
-         {'document_root': settings.STATIC_ROOT, 'show_indexes': True}),
-     url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/documents/'}),
 +    url(r'^images/', include('wiki_img.urls')),
+     url(r'^cover/', include('cover.urls')),
  )
+ if settings.DEBUG:
+     urlpatterns += staticfiles_urlpatterns()
+     urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)