Merge master into img-playground. Image support with new management features. Missing...
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 14 Dec 2011 13:44:29 +0000 (14:44 +0100)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 14 Dec 2011 13:44:29 +0000 (14:44 +0100)
198 files changed:
.gitmodules [new file with mode: 0644]
apps/apiclient/__init__.py [new file with mode: 0644]
apps/apiclient/migrations/0001_initial.py [new file with mode: 0644]
apps/apiclient/migrations/__init__.py [new file with mode: 0644]
apps/apiclient/models.py [new file with mode: 0644]
apps/apiclient/settings.py [new file with mode: 0755]
apps/apiclient/tests.py [new file with mode: 0644]
apps/apiclient/urls.py [new file with mode: 0755]
apps/apiclient/views.py [new file with mode: 0644]
apps/catalogue/__init__.py [new file with mode: 0644]
apps/catalogue/admin.py [new file with mode: 0644]
apps/catalogue/constants.py [new file with mode: 0644]
apps/catalogue/ebook_utils.py [new file with mode: 0644]
apps/catalogue/fixtures/stages.json [new file with mode: 0644]
apps/catalogue/forms.py [new file with mode: 0644]
apps/catalogue/helpers.py [new file with mode: 0644]
apps/catalogue/locale/pl/LC_MESSAGES/django.mo [new file with mode: 0644]
apps/catalogue/locale/pl/LC_MESSAGES/django.po [new file with mode: 0644]
apps/catalogue/management/__init__.py [new file with mode: 0755]
apps/catalogue/management/commands/__init__.py [new file with mode: 0755]
apps/catalogue/management/commands/assign_from_redmine.py [new file with mode: 0755]
apps/catalogue/management/commands/fix_rdf_about.py [new file with mode: 0755]
apps/catalogue/management/commands/import_wl.py [new file with mode: 0755]
apps/catalogue/management/commands/merge_books.py [new file with mode: 0755]
apps/catalogue/managers.py [new file with mode: 0644]
apps/catalogue/migrations/0001_initial.py [new file with mode: 0644]
apps/catalogue/migrations/0002_stages.py [new file with mode: 0644]
apps/catalogue/migrations/0003_from_hg.py [new file with mode: 0644]
apps/catalogue/migrations/0004_fix_revisions.py [new file with mode: 0644]
apps/catalogue/migrations/0005_auto__add_field_chunk_gallery_start.py [new file with mode: 0644]
apps/catalogue/migrations/0006_auto__add_field_book_public.py [new file with mode: 0644]
apps/catalogue/migrations/0007_auto__add_field_book_dc_slug.py [new file with mode: 0644]
apps/catalogue/migrations/0008_auto.py [new file with mode: 0644]
apps/catalogue/migrations/0009_auto__add_imagechange__add_unique_imagechange_tree_revision__add_image.py [new file with mode: 0644]
apps/catalogue/migrations/__init__.py [new file with mode: 0644]
apps/catalogue/models/__init__.py [new file with mode: 0755]
apps/catalogue/models/book.py [new file with mode: 0755]
apps/catalogue/models/chunk.py [new file with mode: 0755]
apps/catalogue/models/image.py [new file with mode: 0755]
apps/catalogue/models/listeners.py [new file with mode: 0755]
apps/catalogue/models/publish_log.py [new file with mode: 0755]
apps/catalogue/signals.py [new file with mode: 0644]
apps/catalogue/tasks.py [new file with mode: 0644]
apps/catalogue/templates/catalogue/activity.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/base.html [new file with mode: 0644]
apps/catalogue/templates/catalogue/book_append_to.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/book_detail.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/book_edit.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/book_html.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/book_list/book.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/book_list/book_list.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/book_list/chunk.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/chunk_add.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/chunk_edit.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/document_create_missing.html [new file with mode: 0644]
apps/catalogue/templates/catalogue/document_list.html [new file with mode: 0644]
apps/catalogue/templates/catalogue/document_upload.html [new file with mode: 0644]
apps/catalogue/templates/catalogue/image_list.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/image_short.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/image_table.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/main_tabs.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/my_page.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/upload_pdf.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/user_list.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/user_page.html [new file with mode: 0755]
apps/catalogue/templates/catalogue/wall.html [new file with mode: 0755]
apps/catalogue/templatetags/__init__.py [new file with mode: 0644]
apps/catalogue/templatetags/book_list.py [new file with mode: 0755]
apps/catalogue/templatetags/catalogue.py [new file with mode: 0644]
apps/catalogue/templatetags/set_get_parameter.py [new file with mode: 0755]
apps/catalogue/templatetags/wall.py [new file with mode: 0755]
apps/catalogue/tests/__init__.py [new file with mode: 0755]
apps/catalogue/tests/files/chunk1.xml [new file with mode: 0755]
apps/catalogue/tests/files/chunk2.xml [new file with mode: 0755]
apps/catalogue/tests/files/expected.xml [new file with mode: 0755]
apps/catalogue/urls.py [new file with mode: 0644]
apps/catalogue/views.py [new file with mode: 0644]
apps/catalogue/xml_tools.py [new file with mode: 0644]
apps/django_cas/backends.py
apps/dvcs/admin.py [deleted file]
apps/dvcs/locale/pl/LC_MESSAGES/django.mo [new file with mode: 0644]
apps/dvcs/locale/pl/LC_MESSAGES/django.po [new file with mode: 0644]
apps/dvcs/models.py
apps/dvcs/signals.py [new file with mode: 0755]
apps/dvcs/storage.py [new file with mode: 0755]
apps/dvcs/tests/__init__.py [new file with mode: 0755]
apps/email_mangler/__init__.py [new file with mode: 0644]
apps/email_mangler/locale/pl/LC_MESSAGES/django.mo [new file with mode: 0644]
apps/email_mangler/locale/pl/LC_MESSAGES/django.po [new file with mode: 0644]
apps/email_mangler/models.py [new file with mode: 0644]
apps/email_mangler/templatetags/__init__.py [new file with mode: 0755]
apps/email_mangler/templatetags/email.py [new file with mode: 0755]
apps/filebrowser/templates/filebrowser/makedir.html
apps/filebrowser/templates/filebrowser/rename.html
apps/filebrowser/views.py
apps/toolbar/fixtures/initial_data.yaml [deleted file]
apps/toolbar/fixtures/initial_toolbar.yaml [new file with mode: 0644]
apps/toolbar/migrations/0005_initial_data.py [new file with mode: 0644]
apps/wiki/admin.py
apps/wiki/constants.py [deleted file]
apps/wiki/forms.py
apps/wiki/helpers.py
apps/wiki/locale/pl/LC_MESSAGES/django.mo
apps/wiki/locale/pl/LC_MESSAGES/django.po
apps/wiki/models.py
apps/wiki/settings.py
apps/wiki/templates/admin/wiki/theme/change_list.html [new file with mode: 0755]
apps/wiki/templates/wiki/base.html [deleted file]
apps/wiki/templates/wiki/document_create_missing.html [deleted file]
apps/wiki/templates/wiki/document_details.html
apps/wiki/templates/wiki/document_details_base.html
apps/wiki/templates/wiki/document_list.html [deleted file]
apps/wiki/templates/wiki/document_upload.html [deleted file]
apps/wiki/templates/wiki/pubmark_dialog.html [new file with mode: 0755]
apps/wiki/templates/wiki/revert_dialog.html [new file with mode: 0644]
apps/wiki/templates/wiki/save_dialog.html
apps/wiki/templates/wiki/tabs/annotations_view.html
apps/wiki/templates/wiki/tabs/gallery_view.html
apps/wiki/templates/wiki/tabs/history_view.html
apps/wiki/templates/wiki/tabs/source_editor.html
apps/wiki/templates/wiki/tabs/summary_view.html
apps/wiki/templates/wiki/tabs/summary_view_item.html
apps/wiki/templates/wiki/tabs/wysiwyg_editor.html
apps/wiki/templates/wiki/tag_dialog.html [deleted file]
apps/wiki/templatetags/__init__.py [deleted file]
apps/wiki/templatetags/wiki.py [deleted file]
apps/wiki/tests.py [deleted file]
apps/wiki/urls.py
apps/wiki/views.py
apps/wiki_img/admin.py [deleted file]
apps/wiki_img/constants.py [deleted file]
apps/wiki_img/forms.py
apps/wiki_img/helpers.py [deleted file]
apps/wiki_img/models.py
apps/wiki_img/nice_diff.py [deleted file]
apps/wiki_img/templates/wiki_img/document_create_missing.html [deleted file]
apps/wiki_img/templates/wiki_img/document_details.html
apps/wiki_img/templates/wiki_img/document_details_base.html
apps/wiki_img/templates/wiki_img/document_details_readonly.html
apps/wiki_img/templates/wiki_img/document_list.html [deleted file]
apps/wiki_img/templates/wiki_img/save_dialog.html
apps/wiki_img/templates/wiki_img/tabs/history_view.html [new file with mode: 0755]
apps/wiki_img/urls.py
apps/wiki_img/views.py
lib/librarian [new submodule]
lib/vstorage/__init__.py
redakcja-celery.conf [new file with mode: 0644]
redakcja.wsgi.template
redakcja/locale/pl/LC_MESSAGES/django.mo
redakcja/locale/pl/LC_MESSAGES/django.po
redakcja/localsettings.sample
redakcja/manage.py
redakcja/settings/__init__.py
redakcja/settings/common.py
redakcja/settings/compress.py
redakcja/settings/test.py
redakcja/static/css/dialogs.css
redakcja/static/css/filelist.css
redakcja/static/css/history.css
redakcja/static/css/html.css
redakcja/static/css/master.css
redakcja/static/email_mangler/email_mangler.js [new file with mode: 0755]
redakcja/static/img/angel-left.png [new file with mode: 0644]
redakcja/static/img/angel-right.png [new file with mode: 0644]
redakcja/static/img/wl-orange.png [new file with mode: 0644]
redakcja/static/js/button_scripts.js
redakcja/static/js/catalogue/catalogue.js [new file with mode: 0755]
redakcja/static/js/lib/jquery/jquery.xmlns.js [new file with mode: 0644]
redakcja/static/js/wiki/base.js
redakcja/static/js/wiki/dialog_addtag.js [deleted file]
redakcja/static/js/wiki/dialog_pubmark.js [new file with mode: 0755]
redakcja/static/js/wiki/dialog_revert.js [new file with mode: 0644]
redakcja/static/js/wiki/dialog_save.js
redakcja/static/js/wiki/view_annotations.js
redakcja/static/js/wiki/view_editor_wysiwyg.js
redakcja/static/js/wiki/view_gallery.js
redakcja/static/js/wiki/view_history.js
redakcja/static/js/wiki/view_summary.js
redakcja/static/js/wiki/wikiapi.js
redakcja/static/js/wiki/xslt.js
redakcja/static/js/wiki_img/loader.js
redakcja/static/js/wiki_img/loader_readonly.js [new file with mode: 0755]
redakcja/static/js/wiki_img/wikiapi.js
redakcja/static/xsl/wl2html_client.xsl
redakcja/templates/404.html
redakcja/templates/500.html
redakcja/templates/503.html
redakcja/templates/base.html
redakcja/templates/error_base.html [new file with mode: 0755]
redakcja/templates/pagination/pagination.html [new file with mode: 0755]
redakcja/templates/registration/head_login.html
redakcja/templates/registration/login.html
redakcja/urls.py
requirements-test.txt
requirements.txt
scripts/merge.sh [new file with mode: 0644]
scripts/once_delete_unneeded.py [new file with mode: 0644]
scripts/tiff2png

diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..56b9c42
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "lib/librarian"]
+       path = lib/librarian
+       url = git://github.com/fnp/librarian.git
diff --git a/apps/apiclient/__init__.py b/apps/apiclient/__init__.py
new file mode 100644 (file)
index 0000000..d44e016
--- /dev/null
@@ -0,0 +1,50 @@
+import urllib
+
+from django.utils import simplejson
+import oauth2
+
+from apiclient.models import OAuthConnection
+from apiclient.settings import WL_CONSUMER_KEY, WL_CONSUMER_SECRET, WL_API_URL
+
+
+if WL_CONSUMER_KEY and WL_CONSUMER_SECRET:
+    wl_consumer = oauth2.Consumer(WL_CONSUMER_KEY, WL_CONSUMER_SECRET)
+else:
+    wl_consumer = None
+
+
+class ApiError(BaseException):
+    pass
+
+
+class NotAuthorizedError(BaseException):
+    pass
+
+
+def api_call(user, path, data=None):
+    conn = OAuthConnection.get(user)
+    if not conn.access:
+        raise NotAuthorizedError("No WL authorization for user %s." % user)
+    token = oauth2.Token(conn.token, conn.token_secret)
+    client = oauth2.Client(wl_consumer, token)
+    if data is not None:
+        data = simplejson.dumps(data)
+        data = urllib.urlencode({"data": data})
+        resp, content = client.request(
+                "%s%s" % (WL_API_URL, path),
+                method="POST",
+                body=data)
+    else:
+        resp, content = client.request(
+                "%s%s" % (WL_API_URL, path))
+    status = resp['status']
+
+    if status == '200':
+        return simplejson.loads(content)
+    elif status.startswith('2'):
+        return
+    elif status == '401':
+        raise ApiError('User not authorized for publishing.')
+    else:
+        raise ApiError("WL API call error")
+
diff --git a/apps/apiclient/migrations/0001_initial.py b/apps/apiclient/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..4af28a5
--- /dev/null
@@ -0,0 +1,75 @@
+# encoding: 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 'OAuthConnection'
+        db.create_table('apiclient_oauthconnection', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('user', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['auth.User'], unique=True)),
+            ('access', self.gf('django.db.models.fields.BooleanField')(default=False)),
+            ('token', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)),
+            ('token_secret', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)),
+        ))
+        db.send_create_signal('apiclient', ['OAuthConnection'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'OAuthConnection'
+        db.delete_table('apiclient_oauthconnection')
+
+
+    models = {
+        'apiclient.oauthconnection': {
+            'Meta': {'object_name': 'OAuthConnection'},
+            'access': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+            'token_secret': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
+        },
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['apiclient']
diff --git a/apps/apiclient/migrations/__init__.py b/apps/apiclient/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/apiclient/models.py b/apps/apiclient/models.py
new file mode 100644 (file)
index 0000000..d3c8f62
--- /dev/null
@@ -0,0 +1,20 @@
+from django.db import models
+from django.contrib.auth.models import User
+
+
+class OAuthConnection(models.Model):
+    user = models.OneToOneField(User)
+    access = models.BooleanField(default=False)
+    token = models.CharField(max_length=64, null=True, blank=True)
+    token_secret = models.CharField(max_length=64, null=True, blank=True)
+
+    @classmethod
+    def get(cls, user):
+        try:
+            return cls.objects.get(user=user)
+        except cls.DoesNotExist:
+            o = cls(user=user)
+            o.save()
+            return o
+
+
diff --git a/apps/apiclient/settings.py b/apps/apiclient/settings.py
new file mode 100755 (executable)
index 0000000..5fbf18e
--- /dev/null
@@ -0,0 +1,15 @@
+from django.conf import settings
+
+
+WL_CONSUMER_KEY = getattr(settings, 'APICLIENT_WL_CONSUMER_KEY', None)
+WL_CONSUMER_SECRET = getattr(settings, 'APICLIENT_WL_CONSUMER_SECRET', None)
+
+WL_API_URL = getattr(settings, 'APICLIENT_WL_API_URL', 
+        'http://www.wolnelektury.pl/api/')
+
+WL_REQUEST_TOKEN_URL = getattr(settings, 'APICLIENT_WL_REQUEST_TOKEN_URL', 
+        WL_API_URL + 'oauth/request_token/')
+WL_ACCESS_TOKEN_URL = getattr(settings, 'APICLIENT_WL_ACCESS_TOKEN_URL', 
+        WL_API_URL + 'oauth/access_token/')
+WL_AUTHORIZE_URL = getattr(settings, 'APICLIENT_WL_AUTHORIZE_URL', 
+        WL_API_URL + 'oauth/authorize/')
diff --git a/apps/apiclient/tests.py b/apps/apiclient/tests.py
new file mode 100644 (file)
index 0000000..2247054
--- /dev/null
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+    def test_basic_addition(self):
+        """
+        Tests that 1 + 1 always equals 2.
+        """
+        self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
diff --git a/apps/apiclient/urls.py b/apps/apiclient/urls.py
new file mode 100755 (executable)
index 0000000..87d9997
--- /dev/null
@@ -0,0 +1,6 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('apiclient.views',
+    url(r'^oauth/$', 'oauth', name='apiclient_oauth'),
+    url(r'^oauth_callback/$', 'oauth_callback', name='apiclient_oauth_callback'),
+)
diff --git a/apps/apiclient/views.py b/apps/apiclient/views.py
new file mode 100644 (file)
index 0000000..d496014
--- /dev/null
@@ -0,0 +1,60 @@
+import cgi
+
+from django.contrib.auth.decorators import login_required
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect, HttpResponse
+import oauth2
+
+from apiclient.models import OAuthConnection
+from apiclient import wl_consumer
+from apiclient.settings import (WL_REQUEST_TOKEN_URL, WL_ACCESS_TOKEN_URL, 
+        WL_AUTHORIZE_URL)
+
+
+@login_required
+def oauth(request):
+    if wl_consumer is None:
+        return HttpResponse("OAuth consumer not configured.")
+
+    client = oauth2.Client(wl_consumer)
+    resp, content = client.request(WL_REQUEST_TOKEN_URL)
+    if resp['status'] != '200':
+        raise Exception("Invalid response %s." % resp['status'])
+
+    request_token = dict(cgi.parse_qsl(content))
+    
+    conn = OAuthConnection.get(request.user)
+    # this might reset existing auth!
+    conn.access = False
+    conn.token = request_token['oauth_token']
+    conn.token_secret = request_token['oauth_token_secret']
+    conn.save()
+
+    url = "%s?oauth_token=%s&oauth_callback=%s" % (
+            WL_AUTHORIZE_URL, 
+            request_token['oauth_token'],
+            request.build_absolute_uri(reverse("apiclient_oauth_callback")),
+            )
+
+    return HttpResponseRedirect(url)
+
+
+@login_required
+def oauth_callback(request):
+    if wl_consumer is None:
+        return HttpResponse("OAuth consumer not configured.")
+
+    oauth_verifier = request.GET.get('oauth_verifier')
+    conn = OAuthConnection.get(request.user)
+    token = oauth2.Token(conn.token, conn.token_secret)
+    token.set_verifier(oauth_verifier)
+    client = oauth2.Client(wl_consumer, token)
+    resp, content = client.request(WL_ACCESS_TOKEN_URL, method="POST")
+    access_token = dict(cgi.parse_qsl(content))
+
+    conn.access = True
+    conn.token = access_token['oauth_token']
+    conn.token_secret = access_token['oauth_token_secret']
+    conn.save()
+
+    return HttpResponseRedirect('/')
diff --git a/apps/catalogue/__init__.py b/apps/catalogue/__init__.py
new file mode 100644 (file)
index 0000000..c53f0e7
--- /dev/null
@@ -0,0 +1 @@
+  # pragma: no cover
diff --git a/apps/catalogue/admin.py b/apps/catalogue/admin.py
new file mode 100644 (file)
index 0000000..8ba803e
--- /dev/null
@@ -0,0 +1,15 @@
+from django.contrib import admin
+
+from catalogue import models
+
+class BookAdmin(admin.ModelAdmin):
+    prepopulated_fields = {'slug': ['title']}
+    search_fields = ['title']
+
+
+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 --git a/apps/catalogue/constants.py b/apps/catalogue/constants.py
new file mode 100644 (file)
index 0000000..d75d6b4
--- /dev/null
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+TRIM_BEGIN = " TRIM_BEGIN "
+TRIM_END = " TRIM_END "
+
+MASTERS = ['powiesc',
+           'opowiadanie',
+           'liryka_l',
+           'liryka_lp',
+           'dramat_wierszowany_l',
+           'dramat_wierszowany_lp',
+           'dramat_wspolczesny',
+           ]
diff --git a/apps/catalogue/ebook_utils.py b/apps/catalogue/ebook_utils.py
new file mode 100644 (file)
index 0000000..1fcf8d3
--- /dev/null
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+from StringIO import StringIO
+from catalogue.models import Book
+from librarian import DocProvider
+from django.http import HttpResponse
+
+
+class RedakcjaDocProvider(DocProvider):
+    """Used for getting books' children."""
+
+    def __init__(self, publishable):
+        self.publishable = publishable
+
+    def by_slug(self, slug):
+        return StringIO(Book.objects.get(dc_slug=slug
+                    ).materialize(publishable=self.publishable))
+
+
+def serve_file(file_path, name, mime_type):
+    def read_chunks(f, size=8192):
+        chunk = f.read(size)
+        while chunk:
+            yield chunk
+            chunk = f.read(size)
+
+    response = HttpResponse(mimetype=mime_type)
+    response['Content-Disposition'] = 'attachment; filename=%s' % name
+    with open(file_path) as f:
+        for chunk in read_chunks(f):
+            response.write(chunk)
+    return response
diff --git a/apps/catalogue/fixtures/stages.json b/apps/catalogue/fixtures/stages.json
new file mode 100644 (file)
index 0000000..5a46ec0
--- /dev/null
@@ -0,0 +1,83 @@
+[
+    {
+        "pk": 1, 
+        "model": "catalogue.chunktag", 
+        "fields": {
+            "ordering": 1, 
+            "name": "Autokorekta", 
+            "slug": "first_correction"
+        }
+    }, 
+    {
+        "pk": 2, 
+        "model": "catalogue.chunktag", 
+        "fields": {
+            "ordering": 2, 
+            "name": "Tagowanie", 
+            "slug": "tagging"
+        }
+    }, 
+    {
+        "pk": 3, 
+        "model": "catalogue.chunktag", 
+        "fields": {
+            "ordering": 3, 
+            "name": "Korekta", 
+            "slug": "proofreading"
+        }
+    }, 
+    {
+        "pk": 4, 
+        "model": "catalogue.chunktag", 
+        "fields": {
+            "ordering": 4, 
+            "name": "Sprawdzenie przypis\u00f3w \u017ar\u00f3d\u0142a", 
+            "slug": "annotation-proofreading"
+        }
+    }, 
+    {
+        "pk": 5, 
+        "model": "catalogue.chunktag", 
+        "fields": {
+            "ordering": 5, 
+            "name": "Uwsp\u00f3\u0142cze\u015bnienie", 
+            "slug": "modernisation"
+        }
+    }, 
+    {
+        "pk": 6, 
+        "model": "catalogue.chunktag", 
+        "fields": {
+            "ordering": 6, 
+            "name": "Przypisy", 
+            "slug": "annotations"
+        }
+    }, 
+    {
+        "pk": 7, 
+        "model": "catalogue.chunktag", 
+        "fields": {
+            "ordering": 7, 
+            "name": "Motywy", 
+            "slug": "themes"
+        }
+    }, 
+    {
+        "pk": 8, 
+        "model": "catalogue.chunktag", 
+        "fields": {
+            "ordering": 8, 
+            "name": "Ostateczna redakcja literacka", 
+            "slug": "editor-proofreading"
+        }
+    }, 
+    {
+        "pk": 9, 
+        "model": "catalogue.chunktag", 
+        "fields": {
+            "ordering": 9, 
+            "name": "Ostateczna redakcja techniczna", 
+            "slug": "technical-editor-proofreading"
+        }
+    }
+]
diff --git a/apps/catalogue/forms.py b/apps/catalogue/forms.py
new file mode 100644 (file)
index 0000000..4e5b2cb
--- /dev/null
@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from catalogue.models import User
+from django.db.models import Count
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from catalogue.constants import MASTERS
+from catalogue.models import Book, Chunk
+
+class DocumentCreateForm(forms.ModelForm):
+    """
+        Form used for creating new documents.
+    """
+    file = forms.FileField(required=False)
+    text = forms.CharField(required=False, widget=forms.Textarea)
+
+    class Meta:
+        model = Book
+        exclude = ['parent', 'parent_number']
+
+    def __init__(self, *args, **kwargs):
+        super(DocumentCreateForm, self).__init__(*args, **kwargs)
+        self.fields['slug'].widget.attrs={'class': 'autoslug'}
+        self.fields['gallery'].widget.attrs={'class': 'autoslug'}
+        self.fields['title'].widget.attrs={'class': 'autoslug-source'}
+
+    def clean(self):
+        super(DocumentCreateForm, self).clean()
+        file = self.cleaned_data['file']
+
+        if file is not None:
+            try:
+                self.cleaned_data['text'] = file.read().decode('utf-8')
+            except UnicodeDecodeError:
+                raise forms.ValidationError(_("Text file must be UTF-8 encoded."))
+
+        if not self.cleaned_data["text"]:
+            self._errors["file"] = self.error_class([_("You must either enter text or upload a file")])
+
+        return self.cleaned_data
+
+
+class DocumentsUploadForm(forms.Form):
+    """
+        Form used for uploading new documents.
+    """
+    file = forms.FileField(required=True, label=_('ZIP file'))
+    dirs = forms.BooleanField(label=_('Directories are documents in chunks'),
+            widget = forms.CheckboxInput(attrs={'disabled':'disabled'}))
+
+    def clean(self):
+        file = self.cleaned_data['file']
+
+        import zipfile
+        try:
+            z = self.cleaned_data['zip'] = zipfile.ZipFile(file)
+        except zipfile.BadZipfile:
+            raise forms.ValidationError("Should be a ZIP file.")
+        if z.testzip():
+            raise forms.ValidationError("ZIP file corrupt.")
+
+        return self.cleaned_data
+
+
+class ChunkForm(forms.ModelForm):
+    """
+        Form used for editing a chunk.
+    """
+    user = forms.ModelChoiceField(queryset=
+        User.objects.annotate(count=Count('chunk')).
+        order_by('-count', 'last_name', 'first_name'), required=False,
+        label=_('Assigned to')) 
+
+    class Meta:
+        model = Chunk
+        fields = ['title', 'slug', 'gallery_start', 'user', 'stage']
+        exclude = ['number']
+
+    def __init__(self, *args, **kwargs):
+        super(ChunkForm, self).__init__(*args, **kwargs)
+        self.fields['gallery_start'].widget.attrs={'class': 'number-input'}
+        self.fields['slug'].widget.attrs={'class': 'autoslug'}
+        self.fields['title'].widget.attrs={'class': 'autoslug-source'}
+
+    def clean_slug(self):
+        slug = self.cleaned_data['slug']
+        try:
+            chunk = Chunk.objects.get(book=self.instance.book, slug=slug)
+        except Chunk.DoesNotExist:
+            return slug
+        if chunk == self.instance:
+            return slug
+        raise forms.ValidationError(_('Chunk with this slug already exists'))
+
+
+class ChunkAddForm(ChunkForm):
+    """
+        Form used for adding a chunk to a document.
+    """
+
+    def clean_slug(self):
+        slug = self.cleaned_data['slug']
+        try:
+            user = Chunk.objects.get(book=self.instance.book, slug=slug)
+        except Chunk.DoesNotExist:
+            return slug
+        raise forms.ValidationError(_('Chunk with this slug already exists'))
+
+
+class BookAppendForm(forms.Form):
+    """
+        Form for appending a book to another book.
+        It means moving all chunks from book A to book B and deleting A.
+    """
+    append_to = forms.ModelChoiceField(queryset=Book.objects.all(),
+            label=_("Append to"))
+
+    def __init__(self, book, *args, **kwargs):
+        ret =  super(BookAppendForm, self).__init__(*args, **kwargs)
+        self.fields['append_to'].queryset = Book.objects.exclude(pk=book.pk)
+        return ret
+
+
+class BookForm(forms.ModelForm):
+    """Form used for editing a Book."""
+
+    class Meta:
+        model = Book
+
+    def __init__(self, *args, **kwargs):
+        ret = super(BookForm, self).__init__(*args, **kwargs)
+        self.fields['slug'].widget.attrs.update({"class": "autoslug"})
+        self.fields['title'].widget.attrs.update({"class": "autoslug-source"})
+        return ret
+
+
+class ReadonlyBookForm(BookForm):
+    """Form used for not editing a Book."""
+
+    def __init__(self, *args, **kwargs):
+        ret = super(ReadonlyBookForm, self).__init__(*args, **kwargs)
+        for field in self.fields.values():
+            field.widget.attrs.update({"readonly": True})
+        return ret
+
+
+class ChooseMasterForm(forms.Form):
+    """
+        Form used for fixing the chunks in a book.
+    """
+
+    master = forms.ChoiceField(choices=((m, m) for m in MASTERS))
diff --git a/apps/catalogue/helpers.py b/apps/catalogue/helpers.py
new file mode 100644 (file)
index 0000000..df64ade
--- /dev/null
@@ -0,0 +1,38 @@
+from datetime import date
+from functools import wraps
+
+from django.db.models import Count
+
+
+def active_tab(tab):
+    """
+        View decorator, which puts tab info on a request.
+    """
+    def wrapper(f):
+        @wraps(f)
+        def wrapped(request, *args, **kwargs):
+            request.catalogue_active_tab = tab
+            return f(request, *args, **kwargs)
+        return wrapped
+    return wrapper
+
+
+def cached_in_field(field_name):
+    def decorator(f):
+        @property
+        @wraps(f)
+        def wrapped(self, *args, **kwargs):
+            value = getattr(self, field_name)
+            if value is None:
+                value = f(self, *args, **kwargs)
+                type(self)._default_manager.filter(pk=self.pk).update(**{field_name: value})
+            return value
+        return wrapped
+    return decorator
+
+
+def parse_isodate(isodate):
+    try:
+        return date(*[int(p) for p in isodate.split('-')])
+    except (AttributeError, TypeError, ValueError):
+        raise ValueError("Not a date in ISO format.")
diff --git a/apps/catalogue/locale/pl/LC_MESSAGES/django.mo b/apps/catalogue/locale/pl/LC_MESSAGES/django.mo
new file mode 100644 (file)
index 0000000..e6f7d3b
Binary files /dev/null and b/apps/catalogue/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/apps/catalogue/locale/pl/LC_MESSAGES/django.po b/apps/catalogue/locale/pl/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..65d9ba3
--- /dev/null
@@ -0,0 +1,657 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Platforma Redakcyjna\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-12-01 16:21+0100\n"
+"PO-Revision-Date: 2011-12-01 16:23+0100\n"
+"Last-Translator: Radek Czajka <radoslaw.czajka@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"
+
+#: forms.py:39
+msgid "Text file must be UTF-8 encoded."
+msgstr "Plik powinien mieć kodowanie UTF-8."
+
+#: forms.py:42
+msgid "You must either enter text or upload a file"
+msgstr "Proszę wpisać tekst albo wybrać plik do załadowania"
+
+#: forms.py:51
+msgid "ZIP file"
+msgstr "Plik ZIP"
+
+#: forms.py:52
+msgid "Directories are documents in chunks"
+msgstr "Katalogi zawierają dokumenty w częściach"
+
+#: forms.py:76
+msgid "Assigned to"
+msgstr "Przypisane do"
+
+#: forms.py:97
+#: forms.py:111
+msgid "Chunk with this slug already exists"
+msgstr "Część z tym slugiem już istnieje"
+
+#: forms.py:120
+msgid "Append to"
+msgstr "Dołącz do"
+
+#: views.py:158
+#, python-format
+msgid "Slug already used for %s"
+msgstr "Slug taki sam jak dla pliku %s"
+
+#: views.py:160
+msgid "Slug already used in repository."
+msgstr "Dokument o tym slugu już istnieje w repozytorium."
+
+#: views.py:166
+msgid "File should be UTF-8 encoded."
+msgstr "Plik powinien mieć kodowanie UTF-8."
+
+#: models/book.py:23
+#: models/chunk.py:23
+msgid "title"
+msgstr "tytuł"
+
+#: models/book.py:24
+#: models/chunk.py:24
+msgid "slug"
+msgstr "slug"
+
+#: models/book.py:25
+msgid "public"
+msgstr "publiczna"
+
+#: models/book.py:26
+msgid "scan gallery name"
+msgstr "nazwa galerii skanów"
+
+#: models/book.py:29
+msgid "parent"
+msgstr "rodzic"
+
+#: models/book.py:30
+msgid "parent number"
+msgstr "numeracja rodzica"
+
+#: models/book.py:45
+#: models/chunk.py:21
+#: models/publish_log.py:17
+msgid "book"
+msgstr "książka"
+
+#: models/book.py:46
+msgid "books"
+msgstr "książki"
+
+#: models/book.py:221
+msgid "No chunks in the book."
+msgstr "Książka nie ma części."
+
+#: models/book.py:225
+msgid "Not all chunks have publishable revisions."
+msgstr "Niektóre części nie są gotowe do publikacji."
+
+#: models/book.py:234
+msgid "Invalid XML"
+msgstr "Nieprawidłowy XML"
+
+#: models/book.py:236
+msgid "No Dublin Core found."
+msgstr "Brak sekcji Dublin Core."
+
+#: models/book.py:238
+msgid "Invalid Dublin Core"
+msgstr "Nieprawidłowy Dublin Core"
+
+#: models/book.py:241
+msgid "rdf:about is not"
+msgstr "rdf:about jest różny od"
+
+#: models/chunk.py:22
+msgid "number"
+msgstr "numer"
+
+#: models/chunk.py:25
+msgid "gallery start"
+msgstr "początek galerii"
+
+#: models/chunk.py:40
+msgid "chunk"
+msgstr "część"
+
+#: models/chunk.py:41
+msgid "chunks"
+msgstr "części"
+
+#: models/publish_log.py:18
+msgid "time"
+msgstr "czas"
+
+#: models/publish_log.py:19
+#: templates/catalogue/wall.html:18
+msgid "user"
+msgstr "użytkownik"
+
+#: models/publish_log.py:24
+#: models/publish_log.py:33
+msgid "book publish record"
+msgstr "zapis publikacji książki"
+
+#: models/publish_log.py:25
+msgid "book publish records"
+msgstr "zapisy publikacji książek"
+
+#: models/publish_log.py:34
+msgid "change"
+msgstr "zmiana"
+
+#: models/publish_log.py:38
+msgid "chunk publish record"
+msgstr "zapis publikacji części"
+
+#: models/publish_log.py:39
+msgid "chunk publish records"
+msgstr "zapisy publikacji części"
+
+#: templates/catalogue/activity.html:10
+#: templatetags/catalogue.py:29
+msgid "Activity"
+msgstr "Aktywność"
+
+#: templates/catalogue/base.html:8
+msgid "Platforma Redakcyjna"
+msgstr "Platforma Redakcyjna"
+
+#: templates/catalogue/book_append_to.html:9
+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
+msgid "Save"
+msgstr "Zapisz"
+
+#: templates/catalogue/book_detail.html:21
+msgid "Append to other book"
+msgstr "Dołącz do innej książki"
+
+#: templates/catalogue/book_detail.html:27
+msgid "Chunks"
+msgstr "Części"
+
+#: templates/catalogue/book_detail.html:42
+#: templatetags/wall.py:78
+msgid "Publication"
+msgstr "Publikacja"
+
+#: templates/catalogue/book_detail.html:44
+msgid "Last published"
+msgstr "Ostatnio opublikowano"
+
+#: templates/catalogue/book_detail.html:54
+msgid "Full XML"
+msgstr "Pełny XML"
+
+#: templates/catalogue/book_detail.html:55
+msgid "HTML version"
+msgstr "Wersja HTML"
+
+#: templates/catalogue/book_detail.html:56
+msgid "TXT version"
+msgstr "Wersja TXT"
+
+#: templates/catalogue/book_detail.html:57
+msgid "PDF version"
+msgstr "Wersja PDF"
+
+#: templates/catalogue/book_detail.html:58
+msgid "EPUB version"
+msgstr "Wersja EPUB"
+
+#: templates/catalogue/book_detail.html:71
+msgid "Publish"
+msgstr "Opublikuj"
+
+#: templates/catalogue/book_detail.html:75
+msgid "Log in to publish."
+msgstr "Zaloguj się, aby opublikować."
+
+#: templates/catalogue/book_detail.html:78
+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
+msgid "Comments"
+msgstr "Komentarze"
+
+#: templates/catalogue/book_html.html:13
+msgid "Table of contents"
+msgstr "Spis treści"
+
+#: templates/catalogue/book_html.html:14
+msgid "Edit. note"
+msgstr "Nota red."
+
+#: templates/catalogue/book_html.html:15
+msgid "Infobox"
+msgstr "Informacje"
+
+#: templates/catalogue/chunk_add.html:5
+#: templates/catalogue/chunk_edit.html:19
+msgid "Split chunk"
+msgstr "Podziel część"
+
+#: templates/catalogue/chunk_add.html:10
+msgid "Insert empty chunk after"
+msgstr "Wstaw pustą część po"
+
+#: templates/catalogue/chunk_add.html:13
+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
+msgid "Chunk settings"
+msgstr "Ustawienia części"
+
+#: templates/catalogue/chunk_edit.html:11
+msgid "Book"
+msgstr "Książka"
+
+#: templates/catalogue/document_create_missing.html:5
+msgid "Create a new book"
+msgstr "Utwórz nową książkę"
+
+#: templates/catalogue/document_create_missing.html:11
+msgid "Create book"
+msgstr "Utwórz książkę"
+
+#: templates/catalogue/document_upload.html:8
+msgid "Bulk documents 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."
+
+#: templates/catalogue/document_upload.html:17
+#: templates/catalogue/upload_pdf.html:13
+#: templatetags/catalogue.py:35
+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."
+msgstr "Wystąpiły błędy. Żadne pliki nie zostały dodane do repozytorium."
+
+#: templates/catalogue/document_upload.html:25
+msgid "Offending files"
+msgstr "Błędne pliki"
+
+#: templates/catalogue/document_upload.html:33
+msgid "Correct files"
+msgstr "Poprawne pliki"
+
+#: templates/catalogue/document_upload.html:44
+msgid "Files have been successfully uploaded to the repository."
+msgstr "Pliki zostały dodane do repozytorium."
+
+#: templates/catalogue/document_upload.html:45
+msgid "Uploaded files"
+msgstr "Dodane pliki"
+
+#: templates/catalogue/document_upload.html:55
+msgid "Skipped files"
+msgstr "Pominięte pliki"
+
+#: templates/catalogue/document_upload.html:56
+msgid "Files skipped due to no <code>.xml</code> extension"
+msgstr "Pliki pominięte z powodu braku rozszerzenia <code>.xml</code>."
+
+#: templates/catalogue/my_page.html:13
+msgid "Your last edited documents"
+msgstr "Twoje ostatnie edycje"
+
+#: templates/catalogue/my_page.html:22
+#: templates/catalogue/user_page.html:13
+msgid "Recent activity for"
+msgstr "Ostatnia aktywność dla:"
+
+#: templates/catalogue/upload_pdf.html:8
+msgid "PDF file upload"
+msgstr ""
+
+#: 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/wall.html:33
+msgid "No activity recorded."
+msgstr "Nie zanotowano aktywności."
+
+#: templates/catalogue/book_list/book.html:6
+#: templates/catalogue/book_list/book.html:25
+msgid "Book settings"
+msgstr "Ustawienia książki"
+
+#: templates/catalogue/book_list/book_list.html:19
+msgid "Show hidden books"
+msgstr "Pokaż ukryte książki"
+
+#: templates/catalogue/book_list/book_list.html:24
+msgid "Search in book titles"
+msgstr "Szukaj w tytułach książek"
+
+#: templates/catalogue/book_list/book_list.html:29
+msgid "stage"
+msgstr "etap"
+
+#: templates/catalogue/book_list/book_list.html:31
+#: templates/catalogue/book_list/book_list.html:42
+msgid "none"
+msgstr "brak"
+
+#: templates/catalogue/book_list/book_list.html:40
+msgid "editor"
+msgstr "redaktor"
+
+#: templates/catalogue/book_list/book_list.html:51
+msgid "status"
+msgstr "status"
+
+#: templates/catalogue/book_list/book_list.html:75
+#, python-format
+msgid "%(c)s book"
+msgid_plural "%(c)s books"
+msgstr[0] "%(c)s książka"
+msgstr[1] "%(c)s książki"
+msgstr[2] "%(c)s książek"
+
+#: templates/catalogue/book_list/book_list.html:80
+msgid "No books found."
+msgstr "Nie znaleziono książek."
+
+#: templatetags/book_list.py:84
+msgid "publishable"
+msgstr "do publikacji"
+
+#: templatetags/book_list.py:85
+msgid "changed"
+msgstr "zmienione"
+
+#: templatetags/book_list.py:86
+msgid "published"
+msgstr "opublikowane"
+
+#: templatetags/book_list.py:87
+msgid "unpublished"
+msgstr "nie opublikowane"
+
+#: templatetags/book_list.py:88
+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
+msgid "Add"
+msgstr "Dodaj"
+
+#: templatetags/wall.py:49
+msgid "Related edit"
+msgstr "Powiązana zmiana"
+
+#: templatetags/wall.py:51
+msgid "Edit"
+msgstr "Zmiana"
+
+#: templatetags/wall.py:99
+msgid "Comment"
+msgstr "Komentarz"
+
+#~ msgid "Admin"
+#~ msgstr "Administracja"
+
+#~ msgid "edit"
+#~ msgstr "edytuj"
+
+#~ msgid "add basic document structure"
+#~ msgstr "dodaj podstawową strukturę dokumentu"
+
+#~ msgid "change master tag to"
+#~ msgstr "zmień tak master na"
+
+#~ msgid "add begin trimming tag"
+#~ msgstr "dodaj początkowy ogranicznik"
+
+#~ msgid "add end trimming tag"
+#~ msgstr "dodaj końcowy ogranicznik"
+
+#~ msgid "unstructured text"
+#~ msgstr "tekst bez struktury"
+
+#~ msgid "unknown XML"
+#~ msgstr "nieznany XML"
+
+#~ msgid "broken document"
+#~ msgstr "uszkodzony dokument"
+
+#~ msgid "Apply fixes"
+#~ msgstr "Wykonaj zmiany"
+
+#~ msgid "Can mark for publishing"
+#~ msgstr "Oznacza do publikacji"
+
+#~ msgid "Author"
+#~ msgstr "Autor"
+
+#~ msgid "Your name"
+#~ msgstr "Imię i nazwisko"
+
+#~ msgid "Author's email"
+#~ msgstr "E-mail autora"
+
+#~ msgid "Your email address, so we can show a gravatar :)"
+#~ msgstr "Adres e-mail, żebyśmy mogli pokazać gravatar :)"
+
+#~ msgid "Describe changes you made."
+#~ msgstr "Opisz swoje zmiany"
+
+#~ msgid "Completed"
+#~ msgstr "Ukończono"
+
+#~ msgid "If you completed a life cycle stage, select it."
+#~ msgstr "Jeśli został ukończony etap prac, wskaż go."
+
+#~ msgid "Describe the reason for reverting."
+#~ msgstr "Opisz powód przywrócenia."
+
+#~ msgid "name"
+#~ msgstr "nazwa"
+
+#~ msgid "theme"
+#~ msgstr "motyw"
+
+#~ msgid "themes"
+#~ msgstr "motywy"
+
+#~ msgid "Tag added"
+#~ msgstr "Dodano tag"
+
+#~ msgid "Revision marked"
+#~ msgstr "Wersja oznaczona"
+
+#~ msgid "Old version"
+#~ msgstr "Stara wersja"
+
+#~ msgid "New version"
+#~ msgstr "Nowa wersja"
+
+#~ msgid "Click to open/close gallery"
+#~ msgstr "Kliknij, aby (ro)zwinąć galerię"
+
+#~ msgid "Help"
+#~ msgstr "Pomoc"
+
+#~ msgid "Version"
+#~ msgstr "Wersja"
+
+#~ msgid "Unknown"
+#~ msgstr "nieznana"
+
+#~ msgid "Save attempt in progress"
+#~ msgstr "Trwa zapisywanie"
+
+#~ msgid "There is a newer version of this document!"
+#~ msgstr "Istnieje nowsza wersja tego dokumentu!"
+
+#~ msgid "Clear filter"
+#~ msgstr "Wyczyść filtr"
+
+#~ msgid "Cancel"
+#~ msgstr "Anuluj"
+
+#~ msgid "Revert"
+#~ msgstr "Przywróć"
+
+#~ msgid "all"
+#~ msgstr "wszystkie"
+
+#~ msgid "Annotations"
+#~ msgstr "Przypisy"
+
+#~ msgid "Previous"
+#~ msgstr "Poprzednie"
+
+#~ msgid "Next"
+#~ msgstr "Następne"
+
+#~ msgid "Zoom in"
+#~ msgstr "Powiększ"
+
+#~ msgid "Zoom out"
+#~ msgstr "Zmniejsz"
+
+#~ msgid "Gallery"
+#~ msgstr "Galeria"
+
+#~ msgid "Compare versions"
+#~ msgstr "Porównaj wersje"
+
+#~ msgid "Revert document"
+#~ msgstr "Przywróć wersję"
+
+#~ msgid "View version"
+#~ msgstr "Zobacz wersję"
+
+#~ msgid "History"
+#~ msgstr "Historia"
+
+#~ msgid "Search"
+#~ msgstr "Szukaj"
+
+#~ msgid "Replace with"
+#~ msgstr "Zamień na"
+
+#~ msgid "Replace"
+#~ msgstr "Zamień"
+
+#~ msgid "Options"
+#~ msgstr "Opcje"
+
+#~ msgid "Case sensitive"
+#~ msgstr "Rozróżniaj wielkość liter"
+
+#~ msgid "From cursor"
+#~ msgstr "Zacznij od kursora"
+
+#~ msgid "Search and replace"
+#~ msgstr "Znajdź i zamień"
+
+#~ msgid "Source code"
+#~ msgstr "Kod źródłowy"
+
+#~ msgid "Title"
+#~ msgstr "Tytuł"
+
+#~ msgid "Document ID"
+#~ msgstr "ID dokumentu"
+
+#~ msgid "Current version"
+#~ msgstr "Aktualna wersja"
+
+#~ msgid "Last edited by"
+#~ msgstr "Ostatnio edytowane przez"
+
+#~ msgid "Link to gallery"
+#~ msgstr "Link do galerii"
+
+#~ msgid "Summary"
+#~ msgstr "Podsumowanie"
+
+#~ msgid "Insert theme"
+#~ msgstr "Wstaw motyw"
+
+#~ msgid "Insert annotation"
+#~ msgstr "Wstaw przypis"
+
+#~ msgid "Visual editor"
+#~ msgstr "Edytor wizualny"
+
+#~ msgid "Unassigned"
+#~ msgstr "Nie przypisane"
+
+#~ msgid "First correction"
+#~ msgstr "Autokorekta"
+
+#~ msgid "Tagging"
+#~ msgstr "Tagowanie"
+
+#~ msgid "Initial Proofreading"
+#~ msgstr "Korekta"
+
+#~ msgid "Annotation Proofreading"
+#~ msgstr "Sprawdzenie przypisów źródła"
+
+#~ msgid "Modernisation"
+#~ msgstr "Uwspółcześnienie"
+
+#~ msgid "Themes"
+#~ msgstr "Motywy"
+
+#~ msgid "Editor's Proofreading"
+#~ msgstr "Ostateczna redakcja literacka"
+
+#~ msgid "Technical Editor's Proofreading"
+#~ msgstr "Ostateczna redakcja techniczna"
+
+#~ msgid "Finished stage: %s"
+#~ msgstr "Ukończony etap: %s"
+
+#~ msgid "Refresh"
+#~ msgstr "Odśwież"
diff --git a/apps/catalogue/management/__init__.py b/apps/catalogue/management/__init__.py
new file mode 100755 (executable)
index 0000000..e69de29
diff --git a/apps/catalogue/management/commands/__init__.py b/apps/catalogue/management/commands/__init__.py
new file mode 100755 (executable)
index 0000000..e69de29
diff --git a/apps/catalogue/management/commands/assign_from_redmine.py b/apps/catalogue/management/commands/assign_from_redmine.py
new file mode 100755 (executable)
index 0000000..9f7b12d
--- /dev/null
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+
+import csv
+from optparse import make_option
+import re
+import sys
+import urllib
+import urllib2
+
+from django.contrib.auth.models import User
+from django.core.management.base import BaseCommand
+from django.core.management.color import color_style
+from django.db import transaction
+
+from slughifi import slughifi
+from catalogue.models import Chunk
+
+
+REDMINE_CSV = 'http://redmine.nowoczesnapolska.org.pl/projects/wl-publikacje/issues.csv'
+REDAKCJA_URL = 'http://redakcja.wolnelektury.pl/documents/'
+
+
+class Command(BaseCommand):
+    option_list = BaseCommand.option_list + (
+        make_option('-r', '--redakcja', dest='redakcja', metavar='URL',
+            help='Base URL of Redakcja documents',
+            default=REDAKCJA_URL),
+        make_option('-q', '--quiet', action='store_false', dest='verbose', default=True,
+            help='Less output'),
+        make_option('-f', '--force', action='store_true', dest='force', default=False,
+            help='Force assignment overwrite'),
+    )
+    help = 'Imports ticket assignments from Redmine.'
+    args = '[redmine-csv-url]'
+
+    def handle(self, *redmine_csv, **options):
+
+        self.style = color_style()
+
+        redakcja = options.get('redakcja')
+        verbose = options.get('verbose')
+        force = options.get('force')
+
+        if not redmine_csv:
+            if verbose:
+                print "Using default Redmine CSV URL:", REDMINE_CSV
+            redmine_csv = REDMINE_CSV
+
+        # Start transaction management.
+        transaction.commit_unless_managed()
+        transaction.enter_transaction_management()
+        transaction.managed(True)
+
+        redakcja_link = re.compile(re.escape(redakcja) + r'([-_.:?&%/a-zA-Z0-9]*)')
+
+        all_tickets = 0
+        all_chunks = 0
+        done_tickets = 0
+        done_chunks = 0
+        empty_users = 0
+        unknown_users = {}
+        unknown_books = []
+        forced = []
+
+        if verbose:
+            print 'Downloading CSV file'
+        for r in csv.reader(urllib2.urlopen(redmine_csv)):
+            if r[0] == '#':
+                continue
+            all_tickets += 1
+
+            username = r[6]
+            if not username:
+                if verbose:
+                    print "Empty user, skipping"
+                empty_users += 1
+                continue
+
+            first_name, last_name = unicode(username, 'utf-8').rsplit(u' ', 1)
+            try:
+                user = User.objects.get(first_name=first_name, last_name=last_name)
+            except User.DoesNotExist:
+                print self.style.ERROR('Unknown user: ' + username)
+                unknown_users.setdefault(username, 0)
+                unknown_users[username] += 1
+                continue
+
+            ticket_done = False
+            for fname in redakcja_link.findall(r[-1]):
+                fname = unicode(urllib.unquote(fname), 'utf-8', 'ignore')
+                if fname.endswith('.xml'):
+                    fname = fname[:-4]
+                fname = fname.replace(' ', '_')
+                fname = slughifi(fname)
+
+                chunks = Chunk.objects.filter(book__slug=fname)
+                if not chunks:
+                    print self.style.ERROR('Unknown book: ' + fname)
+                    unknown_books.append(fname)
+                    continue
+                all_chunks += chunks.count()
+
+                for chunk in chunks:
+                    if chunk.user:
+                        if chunk.user == user:
+                            continue
+                        else:
+                            forced.append((chunk, chunk.user, user))
+                            if force:
+                                print self.style.WARNING(
+                                    '%s assigned to %s, forcing change to %s.' %
+                                    (chunk.pretty_name(), chunk.user, user))
+                            else:
+                                print self.style.WARNING(
+                                    '%s assigned to %s not to %s, skipping.' %
+                                    (chunk.pretty_name(), chunk.user, user))
+                                continue
+                    chunk.user = user
+                    chunk.save()
+                    ticket_done = True
+                    done_chunks += 1
+
+            if ticket_done:
+                done_tickets += 1
+
+
+        # Print results
+        print
+        print "Results:"
+        print "Assignments imported from %d/%d tickets to %d/%d relevalt chunks." % (
+                done_tickets, all_tickets, done_chunks, all_chunks)
+        if empty_users:
+            print "%d tickets were unassigned." % empty_users
+        if forced:
+            print "%d assignments conficts (%s):" % (
+                len(forced), "changed" if force else "left")
+            for chunk, orig, user in forced:
+                print "  %s: \t%s \t->  %s" % (
+                    chunk.pretty_name(), orig.username, user.username)
+        if unknown_books:
+            print "%d unknown books:" % len(unknown_books)
+            for fname in unknown_books:
+                print "  %s" % fname
+        if unknown_users:
+            print "%d unknown users:" % len(unknown_users)
+            for name in unknown_users:
+                print "  %s (%d tickets)" % (name, unknown_users[name])
+        print
+
+
+        transaction.commit()
+        transaction.leave_transaction_management()
+
diff --git a/apps/catalogue/management/commands/fix_rdf_about.py b/apps/catalogue/management/commands/fix_rdf_about.py
new file mode 100755 (executable)
index 0000000..c252c20
--- /dev/null
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+
+from optparse import make_option
+
+from django.contrib.auth.models import User
+from django.core.management.base import BaseCommand
+from django.db import transaction
+
+from catalogue.models import Book
+
+
+class Command(BaseCommand):
+    option_list = BaseCommand.option_list + (
+        make_option('-q', '--quiet', action='store_false', dest='verbose',
+            default=True, help='Less output'),
+        make_option('-d', '--dry-run', action='store_true', dest='dry_run',
+            default=False, help="Don't actually touch anything"),
+    )
+    help = 'Updates the rdf:about metadata field.'
+
+    def handle(self, *args, **options):
+        from lxml import etree
+
+        verbose = options.get('verbose')
+        dry_run = options.get('dry_run')
+
+        # Start transaction management.
+        transaction.commit_unless_managed()
+        transaction.enter_transaction_management()
+        transaction.managed(True)
+
+        all_books = 0
+        nonxml = 0
+        nordf = 0
+        already = 0
+        done = 0
+
+        for b in Book.objects.all():
+            all_books += 1
+            if verbose:
+                print "%s: " % b.title,
+            chunk = b[0]
+            old_head = chunk.head
+            src = old_head.materialize()
+
+            try:
+                t = etree.fromstring(src)
+            except:
+                nonxml += 1
+                if verbose:
+                    print "invalid XML"
+                continue
+            desc = t.find(".//{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description")
+            if desc is None:
+                nordf += 1
+                if verbose:
+                    print "no RDF found"
+                continue
+
+            correct_about = b.correct_about()
+            attr_name = "{http://www.w3.org/1999/02/22-rdf-syntax-ns#}about"
+            if desc.get(attr_name) == correct_about:
+                already += 1
+                if verbose:
+                    print "already correct"
+                continue
+            desc.set(attr_name, correct_about)
+            if not dry_run:
+                new_head = chunk.commit(etree.tostring(t, encoding=unicode),
+                    author_name='platforma redakcyjna',
+                    description='auto-update rdf:about'
+                    )
+                # retain the publishable status
+                if old_head.publishable:
+                    new_head.set_publishable(True)
+            if verbose:
+                print "done"
+            done += 1
+
+        # Print results
+        print "All books: ", all_books
+        print "Invalid XML: ", nonxml
+        print "No RDF found: ", nordf
+        print "Already correct: ", already
+        print "Books updated: ", done
+
+        transaction.commit()
+        transaction.leave_transaction_management()
+
diff --git a/apps/catalogue/management/commands/import_wl.py b/apps/catalogue/management/commands/import_wl.py
new file mode 100755 (executable)
index 0000000..5f60388
--- /dev/null
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+
+from collections import defaultdict
+import json
+from optparse import make_option
+import urllib2
+
+from django.core.management.base import BaseCommand
+from django.core.management.color import color_style
+from django.db import transaction
+from librarian.dcparser import BookInfo
+from librarian import ParseError, ValidationError
+
+from catalogue.models import Book
+
+
+WL_API = 'http://www.wolnelektury.pl/api/books/'
+
+
+class Command(BaseCommand):
+    option_list = BaseCommand.option_list + (
+        make_option('-q', '--quiet', action='store_false', dest='verbose', default=True,
+            help='Less output'),
+    )
+    help = 'Imports XML files from WL.'
+
+    def handle(self, *args, **options):
+
+        self.style = color_style()
+
+        verbose = options.get('verbose')
+
+        # Start transaction management.
+        transaction.commit_unless_managed()
+        transaction.enter_transaction_management()
+        transaction.managed(True)
+
+        if verbose:
+            print 'Reading currently managed files (skipping hidden ones).'
+        slugs = defaultdict(list)
+        for b in Book.objects.exclude(slug__startswith='.').all():
+            if verbose:
+                print b.slug
+            text = b.materialize().encode('utf-8')
+            try:
+                info = BookInfo.from_string(text)
+            except (ParseError, ValidationError):
+                pass
+            else:
+                slugs[info.slug].append(b)
+
+        book_count = 0
+        commit_args = {
+            "author_name": 'Platforma',
+            "description": 'Automatycznie zaimportowane z Wolnych Lektur',
+            "publishable": True,
+        }
+
+        if verbose:
+            print 'Opening books list'
+        for book in json.load(urllib2.urlopen(WL_API)):
+            book_detail = json.load(urllib2.urlopen(book['href']))
+            xml_text = urllib2.urlopen(book_detail['xml']).read()
+            info = BookInfo.from_string(xml_text)
+            previous_books = slugs.get(info.slug)
+            if previous_books:
+                if len(previous_books) > 1:
+                    print self.style.ERROR("There is more than one book "
+                        "with slug %s:"), 
+                previous_book = previous_books[0]
+                comm = previous_book.slug
+            else:
+                previous_book = None
+                comm = '*'
+            print book_count, info.slug , '-->', comm
+            Book.import_xml_text(xml_text, title=info.title[:255],
+                slug=info.slug[:128], previous_book=previous_book,
+                commit_args=commit_args)
+            book_count += 1
+
+        # Print results
+        print
+        print "Results:"
+        print "Imported %d books from WL:" % (
+                book_count, )
+        print
+
+
+        transaction.commit()
+        transaction.leave_transaction_management()
+
diff --git a/apps/catalogue/management/commands/merge_books.py b/apps/catalogue/management/commands/merge_books.py
new file mode 100755 (executable)
index 0000000..aec113e
--- /dev/null
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+
+from optparse import make_option
+import sys
+
+from django.contrib.auth.models import User
+from django.core.management.base import BaseCommand
+from django.core.management.color import color_style
+from django.db import transaction
+
+from slughifi import slughifi
+from catalogue.models import Book
+
+
+def common_prefix(texts):
+    common = []
+
+    min_len = min(len(text) for text in texts)
+    for i in range(min_len):
+        chars = list(set([text[i] for text in texts]))
+        if len(chars) > 1:
+            break
+        common.append(chars[0])
+    return "".join(common)
+
+
+class Command(BaseCommand):
+    option_list = BaseCommand.option_list + (
+        make_option('-s', '--slug', dest='new_slug', metavar='SLUG',
+            help='New slug of the merged book (defaults to common part of all slugs).'),
+        make_option('-t', '--title', dest='new_title', metavar='TITLE',
+            help='New title of the merged book (defaults to common part of all titles).'),
+        make_option('-q', '--quiet', action='store_false', dest='verbose', default=True,
+            help='Less output'),
+        make_option('-g', '--guess', action='store_true', dest='guess', default=False,
+            help='Try to guess what merges are needed (but do not apply them).'),
+        make_option('-d', '--dry-run', action='store_true', dest='dry_run', default=False,
+            help='Dry run: do not actually change anything.'),
+        make_option('-f', '--force', action='store_true', dest='force', default=False,
+            help='On slug conflict, hide the original book to archive.'),
+    )
+    help = 'Merges multiple books into one.'
+    args = '[slug]...'
+
+
+    def print_guess(self, dry_run=True, force=False):
+        from collections import defaultdict
+        from pipes import quote
+        import re
+    
+        def read_slug(slug):
+            res = []
+            res.append((re.compile(ur'__?(przedmowa)$'), -1))
+            res.append((re.compile(ur'__?(cz(esc)?|ksiega|rozdzial)__?(?P<n>\d*)$'), None))
+            res.append((re.compile(ur'__?(rozdzialy__?)?(?P<n>\d*)-'), None))
+        
+            for r, default in res:
+                m = r.search(slug)
+                if m:
+                    start = m.start()
+                    try:
+                        return int(m.group('n')), slug[:start]
+                    except IndexError:
+                        return default, slug[:start]
+            return None, slug
+    
+        def file_to_title(fname):
+            """ Returns a title-like version of a filename. """
+            parts = (p.replace('_', ' ').title() for p in fname.split('__'))
+            return ' / '.join(parts)
+    
+        merges = defaultdict(list)
+        slugs = []
+        for b in Book.objects.all():
+            slugs.append(b.slug)
+            n, ns = read_slug(b.slug)
+            if n is not None:
+                merges[ns].append((n, b))
+    
+        conflicting_slugs = []
+        for slug in sorted(merges.keys()):
+            merge_list = sorted(merges[slug])
+            if len(merge_list) < 2:
+                continue
+    
+            merge_slugs = [b.slug for i, b in merge_list]
+            if slug in slugs and slug not in merge_slugs:
+                conflicting_slugs.append(slug)
+    
+            title = file_to_title(slug)
+            print "./manage.py merge_books %s%s--title=%s --slug=%s \\\n    %s\n" % (
+                '--dry-run ' if dry_run else '',
+                '--force ' if force else '',
+                quote(title), slug,
+                " \\\n    ".join(merge_slugs)
+                )
+    
+        if conflicting_slugs:
+            if force:
+                print self.style.NOTICE('# These books will be archived:')
+            else:
+                print self.style.ERROR('# ERROR: Conflicting slugs:')
+            for slug in conflicting_slugs:
+                print '#', slug
+
+
+    def handle(self, *slugs, **options):
+
+        self.style = color_style()
+
+        force = options.get('force')
+        guess = options.get('guess')
+        dry_run = options.get('dry_run')
+        new_slug = options.get('new_slug').decode('utf-8')
+        new_title = options.get('new_title').decode('utf-8')
+        verbose = options.get('verbose')
+
+        if guess:
+            if slugs:
+                print "Please specify either slugs, or --guess."
+                return
+            else:
+                self.print_guess(dry_run, force)
+                return
+        if not slugs:
+            print "Please specify some book slugs"
+            return
+
+        # Start transaction management.
+        transaction.commit_unless_managed()
+        transaction.enter_transaction_management()
+        transaction.managed(True)
+
+        books = [Book.objects.get(slug=slug) for slug in slugs]
+        common_slug = common_prefix(slugs)
+        common_title = common_prefix([b.title for b in books])
+
+        if not new_title:
+            new_title = common_title
+        elif common_title.startswith(new_title):
+            common_title = new_title
+
+        if not new_slug:
+            new_slug = common_slug
+        elif common_slug.startswith(new_slug):
+            common_slug = new_slug
+
+        if slugs[0] != new_slug and Book.objects.filter(slug=new_slug).exists():
+            self.style.ERROR('Book already exists, skipping!')
+
+
+        if dry_run and verbose:
+            print self.style.NOTICE('DRY RUN: nothing will be changed.')
+            print
+
+        if verbose:
+            print "New title:", self.style.NOTICE(new_title)
+            print "New slug:", self.style.NOTICE(new_slug)
+            print
+
+        for i, book in enumerate(books):
+            chunk_titles = []
+            chunk_slugs = []
+
+            book_title = book.title[len(common_title):].replace(' / ', ' ').lstrip()
+            book_slug = book.slug[len(common_slug):].replace('__', '_').lstrip('-_')
+            for j, chunk in enumerate(book):
+                if j:
+                    new_chunk_title = book_title + '_%d' % j
+                    new_chunk_slug = book_slug + '_%d' % j
+                else:
+                    new_chunk_title, new_chunk_slug = book_title, book_slug
+
+                chunk_titles.append(new_chunk_title)
+                chunk_slugs.append(new_chunk_slug)
+
+                if verbose:
+                    print "title: %s // %s  -->\n       %s // %s\nslug: %s / %s  -->\n      %s / %s" % (
+                        book.title, chunk.title,
+                        new_title, new_chunk_title,
+                        book.slug, chunk.slug,
+                        new_slug, new_chunk_slug)
+                    print
+
+            if not dry_run:
+                try:
+                    conflict = Book.objects.get(slug=new_slug)
+                except Book.DoesNotExist:
+                    conflict = None
+                else:
+                    if conflict == books[0]:
+                        conflict = None
+
+                if conflict:
+                    if force:
+                        # FIXME: there still may be a conflict
+                        conflict.slug = '.' + conflict.slug
+                        conflict.save()
+                        print self.style.NOTICE('Book with slug "%s" moved to "%s".' % (new_slug, conflict.slug))
+                    else:
+                        print self.style.ERROR('ERROR: Book with slug "%s" exists.' % new_slug)
+                        return
+
+                if i:
+                    books[0].append(books[i], slugs=chunk_slugs, titles=chunk_titles)
+                else:
+                    book.title = new_title
+                    book.slug = new_slug
+                    book.save()
+                    for j, chunk in enumerate(book):
+                        chunk.title = chunk_titles[j]
+                        chunk.slug = chunk_slugs[j]
+                        chunk.save()
+
+
+        transaction.commit()
+        transaction.leave_transaction_management()
+
diff --git a/apps/catalogue/managers.py b/apps/catalogue/managers.py
new file mode 100644 (file)
index 0000000..4f804b8
--- /dev/null
@@ -0,0 +1,5 @@
+from django.db import models
+
+class VisibleManager(models.Manager):
+    def get_query_set(self):
+        return super(VisibleManager, self).get_query_set().exclude(_hidden=True)
diff --git a/apps/catalogue/migrations/0001_initial.py b/apps/catalogue/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..dccd9b7
--- /dev/null
@@ -0,0 +1,240 @@
+# encoding: 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 'Book'
+        db.create_table('catalogue_book', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+            ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=128, db_index=True)),
+            ('gallery', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
+            ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='children', null=True, to=orm['catalogue.Book'])),
+            ('parent_number', self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True)),
+            ('_short_html', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+            ('_single', self.gf('django.db.models.fields.NullBooleanField')(db_index=True, null=True, blank=True)),
+            ('_new_publishable', self.gf('django.db.models.fields.NullBooleanField')(null=True, blank=True)),
+            ('_published', self.gf('django.db.models.fields.NullBooleanField')(null=True, blank=True)),
+        ))
+        db.send_create_signal('catalogue', ['Book'])
+
+        # Adding model 'Chunk'
+        db.create_table('catalogue_chunk', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('creator', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='created_documents', null=True, to=orm['auth.User'])),
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
+            ('book', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Book'])),
+            ('number', self.gf('django.db.models.fields.IntegerField')()),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=50, db_index=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
+            ('_short_html', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+            ('_hidden', self.gf('django.db.models.fields.NullBooleanField')(null=True, blank=True)),
+            ('_changed', self.gf('django.db.models.fields.NullBooleanField')(null=True, blank=True)),
+            ('stage', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.ChunkTag'], null=True, blank=True)),
+            ('head', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['catalogue.ChunkChange'], null=True, blank=True)),
+        ))
+        db.send_create_signal('catalogue', ['Chunk'])
+
+        # Adding unique constraint on 'Chunk', fields ['book', 'number']
+        db.create_unique('catalogue_chunk', ['book_id', 'number'])
+
+        # Adding unique constraint on 'Chunk', fields ['book', 'slug']
+        db.create_unique('catalogue_chunk', ['book_id', 'slug'])
+
+        # Adding model 'ChunkTag'
+        db.create_table('catalogue_chunktag', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
+            ('slug', self.gf('django.db.models.fields.SlugField')(db_index=True, max_length=64, unique=True, null=True, blank=True)),
+            ('ordering', self.gf('django.db.models.fields.IntegerField')()),
+        ))
+        db.send_create_signal('catalogue', ['ChunkTag'])
+
+        # Adding model 'ChunkChange'
+        db.create_table('catalogue_chunkchange', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('author', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
+            ('author_name', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
+            ('author_email', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
+            ('revision', self.gf('django.db.models.fields.IntegerField')(db_index=True)),
+            ('parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='children', null=True, blank=True, to=orm['catalogue.ChunkChange'])),
+            ('merge_parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='merge_children', null=True, blank=True, to=orm['catalogue.ChunkChange'])),
+            ('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
+            ('created_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, db_index=True)),
+            ('publishable', self.gf('django.db.models.fields.BooleanField')(default=False)),
+            ('tree', self.gf('django.db.models.fields.related.ForeignKey')(related_name='change_set', to=orm['catalogue.Chunk'])),
+            ('data', self.gf('django.db.models.fields.files.FileField')(max_length=100)),
+        ))
+        db.send_create_signal('catalogue', ['ChunkChange'])
+
+        # Adding unique constraint on 'ChunkChange', fields ['tree', 'revision']
+        db.create_unique('catalogue_chunkchange', ['tree_id', 'revision'])
+
+        # Adding M2M table for field tags on 'ChunkChange'
+        db.create_table('catalogue_chunkchange_tags', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('chunkchange', models.ForeignKey(orm['catalogue.chunkchange'], null=False)),
+            ('chunktag', models.ForeignKey(orm['catalogue.chunktag'], null=False))
+        ))
+        db.create_unique('catalogue_chunkchange_tags', ['chunkchange_id', 'chunktag_id'])
+
+        # Adding model 'BookPublishRecord'
+        db.create_table('catalogue_bookpublishrecord', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('book', self.gf('django.db.models.fields.related.ForeignKey')(related_name='publish_log', to=orm['catalogue.Book'])),
+            ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+        ))
+        db.send_create_signal('catalogue', ['BookPublishRecord'])
+
+        # Adding model 'ChunkPublishRecord'
+        db.create_table('catalogue_chunkpublishrecord', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('book_record', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.BookPublishRecord'])),
+            ('change', self.gf('django.db.models.fields.related.ForeignKey')(related_name='publish_log', to=orm['catalogue.ChunkChange'])),
+        ))
+        db.send_create_signal('catalogue', ['ChunkPublishRecord'])
+
+
+    def backwards(self, orm):
+        
+        # Removing unique constraint on 'ChunkChange', fields ['tree', 'revision']
+        db.delete_unique('catalogue_chunkchange', ['tree_id', 'revision'])
+
+        # Removing unique constraint on 'Chunk', fields ['book', 'slug']
+        db.delete_unique('catalogue_chunk', ['book_id', 'slug'])
+
+        # Removing unique constraint on 'Chunk', fields ['book', 'number']
+        db.delete_unique('catalogue_chunk', ['book_id', 'number'])
+
+        # Deleting model 'Book'
+        db.delete_table('catalogue_book')
+
+        # Deleting model 'Chunk'
+        db.delete_table('catalogue_chunk')
+
+        # Deleting model 'ChunkTag'
+        db.delete_table('catalogue_chunktag')
+
+        # Deleting model 'ChunkChange'
+        db.delete_table('catalogue_chunkchange')
+
+        # Removing M2M table for field tags on 'ChunkChange'
+        db.delete_table('catalogue_chunkchange_tags')
+
+        # Deleting model 'BookPublishRecord'
+        db.delete_table('catalogue_bookpublishrecord')
+
+        # Deleting model 'ChunkPublishRecord'
+        db.delete_table('catalogue_chunkpublishrecord')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'catalogue.book': {
+            'Meta': {'ordering': "['parent_number', 'title']", 'object_name': 'Book'},
+            '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+            'parent_number': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+        },
+        'catalogue.bookpublishrecord': {
+            'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'catalogue.chunk': {
+            'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+            '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+            'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_documents'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ChunkChange']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'number': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'catalogue.chunkchange': {
+            'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
+            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}),
+            'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"})
+        },
+        'catalogue.chunkpublishrecord': {
+            'Meta': {'object_name': 'ChunkPublishRecord'},
+            'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}),
+            'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'catalogue.chunktag': {
+            'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'ordering': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['catalogue']
diff --git a/apps/catalogue/migrations/0002_stages.py b/apps/catalogue/migrations/0002_stages.py
new file mode 100644 (file)
index 0000000..7155457
--- /dev/null
@@ -0,0 +1,122 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+    def forwards(self, orm):
+
+        from django.core.management import call_command
+        call_command("loaddata", "stages.json")
+
+
+    def backwards(self, orm):
+        "Write your backwards methods here."
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'catalogue.book': {
+            'Meta': {'ordering': "['parent_number', 'title']", 'object_name': 'Book'},
+            '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+            'parent_number': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+        },
+        'catalogue.bookpublishrecord': {
+            'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'catalogue.chunk': {
+            'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+            '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+            'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_documents'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ChunkChange']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'number': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'catalogue.chunkchange': {
+            'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
+            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}),
+            'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"})
+        },
+        'catalogue.chunkpublishrecord': {
+            'Meta': {'object_name': 'ChunkPublishRecord'},
+            'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}),
+            'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'catalogue.chunktag': {
+            'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'ordering': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['catalogue']
diff --git a/apps/catalogue/migrations/0003_from_hg.py b/apps/catalogue/migrations/0003_from_hg.py
new file mode 100644 (file)
index 0000000..1816af9
--- /dev/null
@@ -0,0 +1,280 @@
+# encoding: utf-8
+import datetime
+from zlib import compress
+import os
+import os.path
+import re
+import urllib
+
+from django.db import models
+from mercurial import hg, ui
+from south.db import db
+from south.v2 import DataMigration
+
+from django.conf import settings
+from slughifi import slughifi
+
+META_REGEX = re.compile(r'\s*<!--\s(.*?)-->', re.DOTALL | re.MULTILINE)
+STAGE_TAGS_RE = re.compile(r'^#stage-finished: (.*)$', re.MULTILINE)
+AUTHOR_RE = re.compile(r'\s*(.*?)\s*<(.*)>\s*')
+
+
+def urlunquote(url):
+    """Unqotes URL
+
+    # >>> urlunquote('Za%C5%BC%C3%B3%C5%82%C4%87_g%C4%99%C5%9Bl%C4%85_ja%C5%BA%C5%84')
+    # u'Za\u017c\xf3\u0142\u0107_g\u0119\u015bl\u0105 ja\u017a\u0144'
+    """
+    return unicode(urllib.unquote(url), 'utf-8', 'ignore')
+
+
+def split_name(name):
+    parts = name.split('__')
+    return parts
+
+
+def file_to_title(fname):
+    """ Returns a title-like version of a filename. """
+    parts = (p.replace('_', ' ').title() for p in fname.split('__'))
+    return ' / '.join(parts)
+
+
+def plain_text(text):
+    return re.sub(META_REGEX, '', text, 1)
+
+
+def gallery(slug, text):
+    result = {}
+
+    m = re.match(META_REGEX, text)
+    if m:
+        for line in m.group(1).split('\n'):
+            try:
+                k, v = line.split(':', 1)
+                result[k.strip()] = v.strip()
+            except ValueError:
+                continue
+
+    gallery = result.get('gallery', slughifi(slug))
+
+    if gallery.startswith('/'):
+        gallery = os.path.basename(gallery)
+
+    return gallery
+
+
+def migrate_file_from_hg(orm, fname, entry):
+    fname = urlunquote(fname)
+    print fname
+    if fname.endswith('.xml'):
+        fname = fname[:-4]
+    title = file_to_title(fname)
+    fname = slughifi(fname)
+
+    # create all the needed objects
+    # what if it already exists?
+    book = orm.Book.objects.create(
+        title=title,
+        slug=fname)
+    chunk = orm.Chunk.objects.create(
+        book=book,
+        number=1,
+        slug='1')
+    try:
+        chunk.stage = orm.ChunkTag.objects.order_by('ordering')[0]
+    except IndexError:
+        chunk.stage = None
+
+    maxrev = entry.filerev()
+    gallery_link = None
+
+    # this will fail if directory exists
+    os.makedirs(os.path.join(settings.CATALOGUE_REPO_PATH, str(chunk.pk)))
+
+    for rev in xrange(maxrev + 1):
+        fctx = entry.filectx(rev)
+        data = fctx.data()
+        gallery_link = gallery(fname, data)
+        data = plain_text(data)
+
+        # get tags from description
+        description = fctx.description().decode("utf-8", 'replace')
+        tags = STAGE_TAGS_RE.findall(description)
+        tags = [orm.ChunkTag.objects.get(slug=slug.strip()) for slug in tags]
+
+        if tags:
+            max_ordering = max(tags, key=lambda x: x.ordering).ordering
+            try:
+                chunk.stage = orm.ChunkTag.objects.filter(ordering__gt=max_ordering).order_by('ordering')[0]
+            except IndexError:
+                chunk.stage = None
+
+        description = STAGE_TAGS_RE.sub('', description)
+
+        author = author_name = author_email = None
+        author_desc = fctx.user().decode("utf-8", 'replace')
+        m = AUTHOR_RE.match(author_desc)
+        if m:
+            try:
+                author = orm['auth.User'].objects.get(username=m.group(1), email=m.group(2))
+            except orm['auth.User'].DoesNotExist:
+                author_name = m.group(1)
+                author_email = m.group(2)
+        else:
+            author_name = author_desc
+
+        head = orm.ChunkChange.objects.create(
+            tree=chunk,
+            revision=rev + 1,
+            created_at=datetime.datetime.fromtimestamp(fctx.date()[0]),
+            description=description,
+            author=author,
+            author_name=author_name,
+            author_email=author_email,
+            parent=chunk.head
+            )
+
+        path = "%d/%d" % (chunk.pk, head.pk)
+        abs_path = os.path.join(settings.CATALOGUE_REPO_PATH, path)
+        f = open(abs_path, 'wb')
+        f.write(compress(data))
+        f.close()
+        head.data = path
+
+        head.tags = tags
+        head.save()
+
+        chunk.head = head
+
+    chunk.save()
+    if gallery_link:
+        book.gallery = gallery_link
+        book.save()
+
+
+class Migration(DataMigration):
+
+    def forwards(self, orm):
+        try:
+            hg_path = settings.WIKI_REPOSITORY_PATH
+        except:
+            print 'repository not configured, skipping'
+        else:
+            print 'migrate from', hg_path
+            repo = hg.repository(ui.ui(), hg_path)
+            tip = repo['tip']
+            for fname in tip:
+                if fname.startswith('.'):
+                    continue
+                migrate_file_from_hg(orm, fname, tip[fname])
+
+
+    def backwards(self, orm):
+        "Write your backwards methods here."
+        pass
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'catalogue.book': {
+            'Meta': {'ordering': "['parent_number', 'title']", 'object_name': 'Book'},
+            '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+            'parent_number': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+        },
+        'catalogue.bookpublishrecord': {
+            'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'catalogue.chunk': {
+            'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+            '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+            'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_documents'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ChunkChange']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'number': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'catalogue.chunkchange': {
+            'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
+            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}),
+            'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"})
+        },
+        'catalogue.chunkpublishrecord': {
+            'Meta': {'object_name': 'ChunkPublishRecord'},
+            'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}),
+            'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'catalogue.chunktag': {
+            'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'ordering': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['catalogue']
diff --git a/apps/catalogue/migrations/0004_fix_revisions.py b/apps/catalogue/migrations/0004_fix_revisions.py
new file mode 100644 (file)
index 0000000..fe5c86b
--- /dev/null
@@ -0,0 +1,125 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+    def forwards(self, orm):
+        "Make sure all revisions start with 1, not 0."
+        for zero_commit in orm.ChunkChange.objects.filter(revision=0):
+            for change in zero_commit.tree.change_set.all().order_by('-revision'):
+                change.revision=models.F('revision') + 1
+                change.save()
+
+
+    def backwards(self, orm):
+        "Write your backwards methods here."
+        pass
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'catalogue.book': {
+            'Meta': {'ordering': "['parent_number', 'title']", 'object_name': 'Book'},
+            '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+            'parent_number': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+        },
+        'catalogue.bookpublishrecord': {
+            'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'catalogue.chunk': {
+            'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+            '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+            'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_chunk'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ChunkChange']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'number': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'catalogue.chunkchange': {
+            'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
+            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}),
+            'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"})
+        },
+        'catalogue.chunkpublishrecord': {
+            'Meta': {'object_name': 'ChunkPublishRecord'},
+            'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}),
+            'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'catalogue.chunktag': {
+            'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'ordering': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['catalogue']
diff --git a/apps/catalogue/migrations/0005_auto__add_field_chunk_gallery_start.py b/apps/catalogue/migrations/0005_auto__add_field_chunk_gallery_start.py
new file mode 100644 (file)
index 0000000..71af5f6
--- /dev/null
@@ -0,0 +1,125 @@
+# encoding: 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 field 'Chunk.gallery_start'
+        db.add_column('catalogue_chunk', 'gallery_start', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'Chunk.gallery_start'
+        db.delete_column('catalogue_chunk', 'gallery_start')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'catalogue.book': {
+            'Meta': {'ordering': "['parent_number', 'title']", 'object_name': 'Book'},
+            '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+            'parent_number': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+        },
+        'catalogue.bookpublishrecord': {
+            'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'catalogue.chunk': {
+            'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+            '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+            'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_chunk'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'gallery_start': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ChunkChange']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'number': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'catalogue.chunkchange': {
+            'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
+            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}),
+            'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"})
+        },
+        'catalogue.chunkpublishrecord': {
+            'Meta': {'object_name': 'ChunkPublishRecord'},
+            'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}),
+            'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'catalogue.chunktag': {
+            'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'ordering': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['catalogue']
diff --git a/apps/catalogue/migrations/0006_auto__add_field_book_public.py b/apps/catalogue/migrations/0006_auto__add_field_book_public.py
new file mode 100644 (file)
index 0000000..fd1cea5
--- /dev/null
@@ -0,0 +1,126 @@
+# encoding: 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 field 'Book.public'
+        db.add_column('catalogue_book', 'public', self.gf('django.db.models.fields.BooleanField')(default=True, db_index=True), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'Book.public'
+        db.delete_column('catalogue_book', 'public')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'catalogue.book': {
+            'Meta': {'ordering': "['title', 'slug']", 'object_name': 'Book'},
+            '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+            'parent_number': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+        },
+        'catalogue.bookpublishrecord': {
+            'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'catalogue.chunk': {
+            'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+            '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+            'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_chunk'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'gallery_start': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ChunkChange']", 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'number': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'catalogue.chunkchange': {
+            'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
+            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}),
+            'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"})
+        },
+        'catalogue.chunkpublishrecord': {
+            'Meta': {'object_name': 'ChunkPublishRecord'},
+            'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}),
+            'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'catalogue.chunktag': {
+            'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'ordering': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['catalogue']
diff --git a/apps/catalogue/migrations/0007_auto__add_field_book_dc_slug.py b/apps/catalogue/migrations/0007_auto__add_field_book_dc_slug.py
new file mode 100644 (file)
index 0000000..5ae20ea
--- /dev/null
@@ -0,0 +1,127 @@
+# encoding: 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 field 'Book.dc_slug'
+        db.add_column('catalogue_book', 'dc_slug', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'Book.dc_slug'
+        db.delete_column('catalogue_book', 'dc_slug')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'catalogue.book': {
+            'Meta': {'ordering': "['title', 'slug']", 'object_name': 'Book'},
+            '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'dc_slug': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+            'parent_number': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+        },
+        'catalogue.bookpublishrecord': {
+            'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'catalogue.chunk': {
+            'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+            '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+            'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_chunk'", 'null': 'True', 'to': "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'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'number': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'catalogue.chunkchange': {
+            'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
+            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}),
+            'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"})
+        },
+        'catalogue.chunkpublishrecord': {
+            'Meta': {'object_name': 'ChunkPublishRecord'},
+            'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}),
+            'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'catalogue.chunktag': {
+            'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'ordering': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['catalogue']
diff --git a/apps/catalogue/migrations/0008_auto.py b/apps/catalogue/migrations/0008_auto.py
new file mode 100644 (file)
index 0000000..5276b27
--- /dev/null
@@ -0,0 +1,127 @@
+# encoding: 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 index on 'Book', fields ['dc_slug']
+        db.create_index('catalogue_book', ['dc_slug'])
+
+
+    def backwards(self, orm):
+        
+        # Removing index on 'Book', fields ['dc_slug']
+        db.delete_index('catalogue_book', ['dc_slug'])
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'catalogue.book': {
+            'Meta': {'ordering': "['title', 'slug']", 'object_name': 'Book'},
+            '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            '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'}),
+            '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'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+        },
+        'catalogue.bookpublishrecord': {
+            'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'catalogue.chunk': {
+            'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+            '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+            'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_chunk'", 'null': 'True', 'to': "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'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'number': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'catalogue.chunkchange': {
+            'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
+            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}),
+            'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"})
+        },
+        'catalogue.chunkpublishrecord': {
+            'Meta': {'object_name': 'ChunkPublishRecord'},
+            'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}),
+            'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'catalogue.chunktag': {
+            'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'ordering': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['catalogue']
diff --git a/apps/catalogue/migrations/0009_auto__add_imagechange__add_unique_imagechange_tree_revision__add_image.py b/apps/catalogue/migrations/0009_auto__add_imagechange__add_unique_imagechange_tree_revision__add_image.py
new file mode 100644 (file)
index 0000000..4de5212
--- /dev/null
@@ -0,0 +1,251 @@
+# encoding: 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 'ImageChange'
+        db.create_table('catalogue_imagechange', (
+            ('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('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('catalogue_imagechange_tags', ['imagechange_id', 'imagetag_id'])
+
+        # Adding unique constraint on 'ImageChange', fields ['tree', 'revision']
+        db.create_unique('catalogue_imagechange', ['tree_id', 'revision'])
+
+        # Adding model 'ImagePublishRecord'
+        db.create_table('catalogue_imagepublishrecord', (
+            ('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 'ImageTag'
+        db.create_table('catalogue_imagetag', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
+            ('slug', self.gf('django.db.models.fields.SlugField')(db_index=True, max_length=64, unique=True, null=True, blank=True)),
+            ('ordering', self.gf('django.db.models.fields.IntegerField')()),
+        ))
+        db.send_create_signal('catalogue', ['ImageTag'])
+
+        # Adding model 'Image'
+        db.create_table('catalogue_image', (
+            ('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, db_index=True)),
+            ('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('catalogue_imagechange', ['tree_id', 'revision'])
+
+        # Deleting model 'ImageChange'
+        db.delete_table('catalogue_imagechange')
+
+        # Removing M2M table for field tags on 'ImageChange'
+        db.delete_table('catalogue_imagechange_tags')
+
+        # Deleting model 'ImagePublishRecord'
+        db.delete_table('catalogue_imagepublishrecord')
+
+        # Deleting model 'ImageTag'
+        db.delete_table('catalogue_imagetag')
+
+        # Deleting model 'Image'
+        db.delete_table('catalogue_image')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'catalogue.book': {
+            'Meta': {'ordering': "['title', 'slug']", 'object_name': 'Book'},
+            '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            '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'}),
+            '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'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+        },
+        'catalogue.bookpublishrecord': {
+            'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'catalogue.chunk': {
+            'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+            '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+            'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_chunk'", 'null': 'True', 'to': "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'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'number': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        'catalogue.chunkchange': {
+            'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
+            'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}),
+            'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"})
+        },
+        'catalogue.chunkpublishrecord': {
+            'Meta': {'object_name': 'ChunkPublishRecord'},
+            'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}),
+            'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'catalogue.chunktag': {
+            'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'ordering': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        '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': "orm['auth.User']"}),
+            'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ImageChange']", 'null': 'True', 'blank': 'True'}),
+            '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', 'db_index': 'True'}),
+            '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': "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': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.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']"}),
+            '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': "orm['auth.User']"})
+        },
+        'catalogue.imagetag': {
+            'Meta': {'ordering': "['ordering']", 'object_name': 'ImageTag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'ordering': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['catalogue']
diff --git a/apps/catalogue/migrations/__init__.py b/apps/catalogue/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/catalogue/models/__init__.py b/apps/catalogue/models/__init__.py
new file mode 100755 (executable)
index 0000000..82e1c11
--- /dev/null
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from catalogue.models.chunk import Chunk
+from catalogue.models.image import Image
+from catalogue.models.publish_log import BookPublishRecord, ChunkPublishRecord
+from catalogue.models.book import Book
+from catalogue.models.listeners import *
+
+from django.contrib.auth.models import User as AuthUser
+
+class User(AuthUser):
+    class Meta:
+        proxy = True
+
+    def __unicode__(self):
+        return "%s %s" % (self.first_name, self.last_name)
diff --git a/apps/catalogue/models/book.py b/apps/catalogue/models/book.py
new file mode 100755 (executable)
index 0000000..46d11e8
--- /dev/null
@@ -0,0 +1,361 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.contrib.sites.models import Site
+from django.db import models, transaction
+from django.template.loader import render_to_string
+from django.utils.translation import ugettext_lazy as _
+from slughifi import slughifi
+
+import apiclient
+from catalogue.helpers import cached_in_field
+from catalogue.models import BookPublishRecord, ChunkPublishRecord
+from catalogue.signals import post_publish
+from catalogue.tasks import refresh_instance, book_content_updated
+from catalogue.xml_tools import compile_text, split_xml
+
+
+class Book(models.Model):
+    """ A document edited on the wiki """
+
+    title = models.CharField(_('title'), max_length=255, db_index=True)
+    slug = models.SlugField(_('slug'), max_length=128, unique=True, db_index=True)
+    public = models.BooleanField(_('public'), default=True, db_index=True)
+    gallery = models.CharField(_('scan gallery name'), max_length=255, blank=True)
+
+    #wl_slug = models.CharField(_('title'), max_length=255, null=True, db_index=True, editable=False)
+    parent = models.ForeignKey('self', null=True, blank=True, verbose_name=_('parent'), related_name="children", editable=False)
+    parent_number = models.IntegerField(_('parent number'), null=True, blank=True, db_index=True, editable=False)
+
+    # Cache
+    _short_html = models.TextField(null=True, blank=True, editable=False)
+    _single = models.NullBooleanField(editable=False, db_index=True)
+    _new_publishable = models.NullBooleanField(editable=False)
+    _published = models.NullBooleanField(editable=False)
+    dc_slug = models.CharField(max_length=128, null=True, blank=True,
+            editable=False, db_index=True)
+
+    class NoTextError(BaseException):
+        pass
+
+    class Meta:
+        app_label = 'catalogue'
+        ordering = ['title', 'slug']
+        verbose_name = _('book')
+        verbose_name_plural = _('books')
+
+
+    # Representing
+    # ============
+
+    def __iter__(self):
+        return iter(self.chunk_set.all())
+
+    def __getitem__(self, chunk):
+        return self.chunk_set.all()[chunk]
+
+    def __len__(self):
+        return self.chunk_set.count()
+
+    def __nonzero__(self):
+        """
+            Necessary so that __len__ isn't used for bool evaluation.
+        """
+        return True
+
+    def __unicode__(self):
+        return self.title
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ("catalogue_book", [self.slug])
+
+    def correct_about(self):
+        return "http://%s%s" % (
+            Site.objects.get_current().domain,
+            self.get_absolute_url()
+        )
+
+    # Creating & manipulating
+    # =======================
+
+    def accessible(self, request):
+        return self.public or request.user.is_authenticated()
+
+    @classmethod
+    @transaction.commit_on_success
+    def create(cls, creator, text, *args, **kwargs):
+        b = cls.objects.create(*args, **kwargs)
+        b.chunk_set.all().update(creator=creator)
+        b[0].commit(text, author=creator)
+        return b
+
+    def add(self, *args, **kwargs):
+        """Add a new chunk at the end."""
+        return self.chunk_set.reverse()[0].split(*args, **kwargs)
+
+    @classmethod
+    @transaction.commit_on_success
+    def import_xml_text(cls, text=u'', previous_book=None,
+                commit_args=None, **kwargs):
+        """Imports a book from XML, splitting it into chunks as necessary."""
+        texts = split_xml(text)
+        if previous_book:
+            instance = previous_book
+        else:
+            instance = cls(**kwargs)
+            instance.save()
+
+        # if there are more parts, set the rest to empty strings
+        book_len = len(instance)
+        for i in range(book_len - len(texts)):
+            texts.append((u'pusta część %d' % (i + 1), u''))
+
+        i = 0
+        for i, (title, text) in enumerate(texts):
+            if not title:
+                title = u'część %d' % (i + 1)
+
+            slug = slughifi(title)
+
+            if i < book_len:
+                chunk = instance[i]
+                chunk.slug = slug[:50]
+                chunk.title = title[:255]
+                chunk.save()
+            else:
+                chunk = instance.add(slug, title)
+
+            chunk.commit(text, **commit_args)
+
+        return instance
+
+    def make_chunk_slug(self, proposed):
+        """ 
+            Finds a chunk slug not yet used in the book.
+        """
+        slugs = set(c.slug for c in self)
+        i = 1
+        new_slug = proposed[:50]
+        while new_slug in slugs:
+            new_slug = "%s_%d" % (proposed[:45], i)
+            i += 1
+        return new_slug
+
+    @transaction.commit_on_success
+    def append(self, other, slugs=None, titles=None):
+        """Add all chunks of another book to self."""
+        assert self != other
+
+        number = self[len(self) - 1].number + 1
+        len_other = len(other)
+        single = len_other == 1
+
+        if slugs is not None:
+            assert len(slugs) == len_other
+        if titles is not None:
+            assert len(titles) == len_other
+            if slugs is None:
+                slugs = [slughifi(t) for t in titles]
+
+        for i, chunk in enumerate(other):
+            # move chunk to new book
+            chunk.book = self
+            chunk.number = number
+
+            if titles is None:
+                # try some title guessing
+                if other.title.startswith(self.title):
+                    other_title_part = other.title[len(self.title):].lstrip(' /')
+                else:
+                    other_title_part = other.title
+
+                if single:
+                    # special treatment for appending one-parters:
+                    # just use the guessed title and original book slug
+                    chunk.title = other_title_part
+                    if other.slug.startswith(self.slug):
+                        chunk.slug = other.slug[len(self.slug):].lstrip('-_')
+                    else:
+                        chunk.slug = other.slug
+                else:
+                    chunk.title = ("%s, %s" % (other_title_part, chunk.title))[:255]
+            else:
+                chunk.slug = slugs[i]
+                chunk.title = titles[i]
+
+            chunk.slug = self.make_chunk_slug(chunk.slug)
+            chunk.save()
+            number += 1
+        assert not other.chunk_set.exists()
+        other.delete()
+
+    @transaction.commit_on_success
+    def prepend_history(self, other):
+        """Prepend history from all the other book's chunks to own."""
+        assert self != other
+
+        for i in range(len(self), len(other)):
+            title = u"pusta część %d" % i
+            chunk = self.add(slughifi(title), title)
+            chunk.commit('')
+
+        for i in range(len(other)):
+            self[i].prepend_history(other[0])
+
+        assert not other.chunk_set.exists()
+        other.delete()
+
+
+    # State & cache
+    # =============
+
+    def last_published(self):
+        try:
+            return self.publish_log.all()[0].timestamp
+        except IndexError:
+            return None
+
+    def assert_publishable(self):
+        assert self.chunk_set.exists(), _('No chunks in the book.')
+        try:
+            changes = self.get_current_changes(publishable=True)
+        except self.NoTextError:
+            raise AssertionError(_('Not all chunks have publishable revisions.'))
+        book_xml = self.materialize(changes=changes)
+
+        from librarian.dcparser import BookInfo
+        from librarian import NoDublinCore, ParseError, ValidationError
+
+        try:
+            bi = BookInfo.from_string(book_xml.encode('utf-8'))
+        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 bi.about == valid_about, _("rdf:about is not") + " " + valid_about
+
+    def hidden(self):
+        return self.slug.startswith('.')
+
+    def is_new_publishable(self):
+        """Checks if book is ready for publishing.
+
+        Returns True if there is a publishable version newer than the one
+        already published.
+
+        """
+        new_publishable = False
+        if not self.chunk_set.exists():
+            return False
+        for chunk in self:
+            change = chunk.publishable()
+            if not change:
+                return False
+            if not new_publishable and not change.publish_log.exists():
+                new_publishable = True
+        return new_publishable
+    new_publishable = cached_in_field('_new_publishable')(is_new_publishable)
+
+    def is_published(self):
+        return self.publish_log.exists()
+    published = cached_in_field('_published')(is_published)
+
+    def is_single(self):
+        return len(self) == 1
+    single = cached_in_field('_single')(is_single)
+
+    @cached_in_field('_short_html')
+    def short_html(self):
+        return render_to_string('catalogue/book_list/book.html', {'book': self})
+
+    def book_info(self, publishable=True):
+        try:
+            book_xml = self.materialize(publishable=publishable)
+        except self.NoTextError:
+            pass
+        else:
+            from librarian.dcparser import BookInfo
+            from librarian import NoDublinCore, ParseError, ValidationError
+            try:
+                return BookInfo.from_string(book_xml.encode('utf-8'))
+            except (self.NoTextError, ParseError, NoDublinCore, ValidationError):
+                return None
+
+    def refresh_dc_cache(self):
+        update = {
+            'dc_slug': None,
+        }
+
+        info = self.book_info()
+        if info is not None:
+            update['dc_slug'] = info.slug
+        Book.objects.filter(pk=self.pk).update(**update)
+
+    def touch(self):
+        # this should only really be done when text or publishable status changes
+        book_content_updated.delay(self)
+
+        update = {
+            "_new_publishable": self.is_new_publishable(),
+            "_published": self.is_published(),
+            "_single": self.is_single(),
+            "_short_html": None,
+        }
+        Book.objects.filter(pk=self.pk).update(**update)
+        refresh_instance(self)
+
+    def refresh(self):
+        """This should be done offline."""
+        self.short_html
+        self.single
+        self.new_publishable
+        self.published
+
+    # Materializing & publishing
+    # ==========================
+
+    def get_current_changes(self, publishable=True):
+        """
+            Returns a list containing one Change for every Chunk in the Book.
+            Takes the most recent revision (publishable, if set).
+            Throws an error, if a proper revision is unavailable for a Chunk.
+        """
+        if publishable:
+            changes = [chunk.publishable() for chunk in self]
+        else:
+            changes = [chunk.head for chunk in self if chunk.head is not None]
+        if None in changes:
+            raise self.NoTextError('Some chunks have no available text.')
+        return changes
+
+    def materialize(self, publishable=False, changes=None):
+        """ 
+            Get full text of the document compiled from chunks.
+            Takes the current versions of all texts
+            or versions most recently tagged for publishing,
+            or a specified iterable changes.
+        """
+        if changes is None:
+            changes = self.get_current_changes(publishable)
+        return compile_text(change.materialize() for change in changes)
+
+    def publish(self, user):
+        """
+            Publishes a book on behalf of a (local) user.
+        """
+        self.assert_publishable()
+        changes = self.get_current_changes(publishable=True)
+        book_xml = self.materialize(changes=changes)
+        apiclient.api_call(user, "books/", {"book_xml": book_xml})
+        # record the publish
+        br = BookPublishRecord.objects.create(book=self, user=user)
+        for c in changes:
+            ChunkPublishRecord.objects.create(book_record=br, change=c)
+        post_publish.send(sender=br)
diff --git a/apps/catalogue/models/chunk.py b/apps/catalogue/models/chunk.py
new file mode 100755 (executable)
index 0000000..171ba53
--- /dev/null
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf import settings
+from django.db import models
+from django.db.utils import IntegrityError
+from django.template.loader import render_to_string
+from django.utils.translation import ugettext_lazy as _
+from catalogue.helpers import cached_in_field
+from catalogue.managers import VisibleManager
+from catalogue.tasks import refresh_instance
+from dvcs import models as dvcs_models
+
+
+class Chunk(dvcs_models.Document):
+    """ An editable chunk of text. Every Book text is divided into chunks. """
+    REPO_PATH = settings.CATALOGUE_REPO_PATH
+
+    book = models.ForeignKey('Book', editable=False, verbose_name=_('book'))
+    number = models.IntegerField(_('number'))
+    title = models.CharField(_('title'), max_length=255, blank=True)
+    slug = models.SlugField(_('slug'))
+    gallery_start = models.IntegerField(_('gallery start'), null=True, blank=True, default=1)
+
+    # cache
+    _short_html = models.TextField(null=True, blank=True, editable=False)
+    _hidden = models.NullBooleanField(editable=False)
+    _changed = models.NullBooleanField(editable=False)
+
+    # managers
+    objects = models.Manager()
+    visible_objects = VisibleManager()
+
+    class Meta:
+        app_label = 'catalogue'
+        unique_together = [['book', 'number'], ['book', 'slug']]
+        ordering = ['number']
+        verbose_name = _('chunk')
+        verbose_name_plural = _('chunks')
+        permissions = [('can_pubmark', 'Can mark for publishing')]
+
+    # Representing
+    # ============
+
+    def __unicode__(self):
+        return "%d:%d: %s" % (self.book_id, self.number, self.title)
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ("wiki_editor", [self.book.slug, self.slug])
+
+    def pretty_name(self, book_length=None):
+        title = self.book.title
+        if self.title:
+            title += ", %s" % self.title
+        if book_length > 1:
+            title += " (%d/%d)" % (self.number, book_length)
+        return title
+
+
+    # Creating and manipulation
+    # =========================
+
+    def split(self, slug, title='', **kwargs):
+        """ Create an empty chunk after this one """
+        self.book.chunk_set.filter(number__gt=self.number).update(
+                number=models.F('number')+1)
+        new_chunk = None
+        while not new_chunk:
+            new_slug = self.book.make_chunk_slug(slug)
+            try:
+                new_chunk = self.book.chunk_set.create(number=self.number+1,
+                    slug=new_slug[:50], title=title[:255], **kwargs)
+            except IntegrityError:
+                pass
+        return new_chunk
+
+    @classmethod
+    def get(cls, book_slug, chunk_slug=None):
+        if chunk_slug is None:
+            return cls.objects.get(book__slug=book_slug, number=1)
+        else:
+            return cls.objects.get(book__slug=book_slug, slug=chunk_slug)
+
+
+    # State & cache
+    # =============
+
+    def new_publishable(self):
+        change = self.publishable()
+        if not change:
+            return False
+        return change.publish_log.exists()
+
+    def is_changed(self):
+        if self.head is None:
+            return False
+        return not self.head.publishable
+    changed = cached_in_field('_changed')(is_changed)
+
+    def is_hidden(self):
+        return self.book.hidden()
+    hidden = cached_in_field('_hidden')(is_hidden)
+
+    @cached_in_field('_short_html')
+    def short_html(self):
+        return render_to_string(
+                    'catalogue/book_list/chunk.html', {'chunk': self})
+
+    def touch(self):
+        update = {
+            "_changed": self.is_changed(),
+            "_hidden": self.is_hidden(),
+            "_short_html": None,
+        }
+        Chunk.objects.filter(pk=self.pk).update(**update)
+        refresh_instance(self)
+
+    def refresh(self):
+        """This should be done offline."""
+        self.changed
+        self.hidden
+        self.short_html
diff --git a/apps/catalogue/models/image.py b/apps/catalogue/models/image.py
new file mode 100755 (executable)
index 0000000..53f8830
--- /dev/null
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.conf import settings
+from django.db import models
+from django.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 ("wiki_img_editor", [self.slug])
+
+    # State & cache
+    # =============
+
+    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 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
diff --git a/apps/catalogue/models/listeners.py b/apps/catalogue/models/listeners.py
new file mode 100755 (executable)
index 0000000..de1387e
--- /dev/null
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.contrib.auth.models import User
+from django.db import models
+from catalogue.models import Book, Chunk, Image
+from catalogue.signals import post_publish
+from dvcs.signals import post_publishable
+
+
+def book_changed(sender, instance, created, **kwargs):
+    instance.touch()
+    for c in instance:
+        c.touch()
+models.signals.post_save.connect(book_changed, sender=Book)
+
+
+def chunk_changed(sender, instance, created, **kwargs):
+    instance.book.touch()
+    instance.touch()
+models.signals.post_save.connect(chunk_changed, sender=Chunk)
+
+
+def image_changed(sender, instance, created, **kwargs):
+    instance.touch()
+models.signals.post_save.connect(image_changed, sender=Image)
+
+
+def user_changed(sender, instance, *args, **kwargs):
+    books = set()
+    for c in instance.chunk_set.all():
+        books.add(c.book)
+        c.touch()
+    for b in books:
+        b.touch()
+models.signals.post_save.connect(user_changed, sender=User)
+
+
+def publish_listener(sender, *args, **kwargs):
+    sender.book.touch()
+    for c in sender.book:
+        c.touch()
+post_publish.connect(publish_listener)
+
+
+def publishable_listener(sender, *args, **kwargs):
+    sender.tree.touch()
+    sender.tree.book.touch()
+post_publishable.connect(publishable_listener)
+
+
+def listener_create(sender, instance, created, **kwargs):
+    if created:
+        instance.chunk_set.create(number=1, slug='1')
+models.signals.post_save.connect(listener_create, sender=Book)
+
diff --git a/apps/catalogue/models/publish_log.py b/apps/catalogue/models/publish_log.py
new file mode 100755 (executable)
index 0000000..6cc86d0
--- /dev/null
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from django.contrib.auth.models import User
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from catalogue.models import Chunk, Image
+
+
+class BookPublishRecord(models.Model):
+    """
+        A record left after publishing a Book.
+    """
+
+    book = models.ForeignKey('Book', verbose_name=_('book'), related_name='publish_log')
+    timestamp = models.DateTimeField(_('time'), auto_now_add=True)
+    user = models.ForeignKey(User, verbose_name=_('user'))
+
+    class Meta:
+        app_label = 'catalogue'
+        ordering = ['-timestamp']
+        verbose_name = _('book publish record')
+        verbose_name = _('book publish records')
+
+
+class ChunkPublishRecord(models.Model):
+    """
+        BookPublishRecord details for each Chunk.
+    """
+
+    book_record = models.ForeignKey(BookPublishRecord, verbose_name=_('book publish record'))
+    change = models.ForeignKey(Chunk.change_model, related_name='publish_log', verbose_name=_('change'))
+
+    class Meta:
+        app_label = 'catalogue'
+        verbose_name = _('chunk publish record')
+        verbose_name = _('chunk publish records')
+
+
+class ImagePublishRecord(models.Model):
+    """A record left after publishing an Image."""
+
+    image = models.ForeignKey(Image, verbose_name=_('image'), related_name='publish_log')
+    timestamp = models.DateTimeField(_('time'), auto_now_add=True)
+    user = models.ForeignKey(User, verbose_name=_('user'))
+    change = models.ForeignKey(Image.change_model, related_name='publish_log', verbose_name=_('change'))
+
+    class Meta:
+        app_label = 'catalogue'
+        ordering = ['-timestamp']
+        verbose_name = _('image publish record')
+        verbose_name = _('image publish records')
diff --git a/apps/catalogue/signals.py b/apps/catalogue/signals.py
new file mode 100644 (file)
index 0000000..62ca514
--- /dev/null
@@ -0,0 +1,3 @@
+from django.dispatch import Signal
+
+post_publish = Signal()
diff --git a/apps/catalogue/tasks.py b/apps/catalogue/tasks.py
new file mode 100644 (file)
index 0000000..547f36b
--- /dev/null
@@ -0,0 +1,39 @@
+from celery.task import task
+from django.utils import translation
+from django.conf import settings
+
+
+@task
+def _refresh_by_pk(cls, pk, language=None):
+    prev_language = translation.get_language()
+    language and translation.activate(language)
+    try:
+        cls._default_manager.get(pk=pk).refresh()
+    finally:
+        translation.activate(prev_language)
+
+def refresh_instance(instance):
+    _refresh_by_pk.delay(type(instance), instance.pk, translation.get_language())
+
+
+@task
+def _publishable_error(book, language=None):
+    prev_language = translation.get_language()
+    language and translation.activate(language)
+    try:
+        return book.assert_publishable()
+    except AssertionError, e:
+        return e
+    else:
+       return None
+    finally:
+        translation.activate(prev_language)
+
+def publishable_error(book):
+    return _publishable_error.delay(book, 
+        translation.get_language()).wait()
+
+
+@task
+def book_content_updated(book):
+    book.refresh_dc_cache()
diff --git a/apps/catalogue/templates/catalogue/activity.html b/apps/catalogue/templates/catalogue/activity.html
new file mode 100755 (executable)
index 0000000..9c2eac5
--- /dev/null
@@ -0,0 +1,17 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+{% load url from future %}
+{% load wall %}
+
+
+{% block content %}
+
+<h1><a href='{% url "catalogue_activity" prev_day.isoformat %}'>&lt;</a>
+    {% trans "Activity" %}: {{ day }}
+    {% if next_day %}
+        <a href='{% url "catalogue_activity" next_day.isoformat %}'>&gt;</a>
+    {% endif %}
+</h1>
+
+    {% day_wall day %}
+{% endblock content %}
diff --git a/apps/catalogue/templates/catalogue/base.html b/apps/catalogue/templates/catalogue/base.html
new file mode 100644 (file)
index 0000000..f3ebcd9
--- /dev/null
@@ -0,0 +1,50 @@
+{% load compressed i18n %}
+{% load catalogue %}
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+    {% compressed_css 'catalogue' %}
+    <title>{% block title %}{% trans "Platforma Redakcyjna" %}{% endblock title %}</title>
+</head>
+<body>
+
+<div id="tabs-nav">
+
+    <a href="{% url catalogue_document_list %}">
+        <img id="logo" src="{{ STATIC_URL }}img/wl-orange.png" />
+    </a>
+
+    <div id="tabs-nav-left">
+        {% main_tabs %}
+    </div>
+
+    <span id="login-box">
+        {% include "registration/head_login.html" %}
+    </span>
+
+    <div class='clr' ></div>
+</div>
+
+<div id="content">
+
+{% block content %}
+<div id="catalogue_layout_left_column">
+       {% block leftcolumn %}
+       {% endblock leftcolumn %}
+</div>
+<div id="catalogue_layout_right_column">
+       {% block rightcolumn %}
+       {% endblock rightcolumn %}
+</div>
+{% endblock content %}
+
+</div>
+
+
+<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
+{% compressed_js 'catalogue' %}
+{% block extrabody %}
+{% endblock %}
+</body>
+</html>
diff --git a/apps/catalogue/templates/catalogue/book_append_to.html b/apps/catalogue/templates/catalogue/book_append_to.html
new file mode 100755 (executable)
index 0000000..76a5962
--- /dev/null
@@ -0,0 +1,14 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+       <form enctype="multipart/form-data" method="POST" action="">
+    {% csrf_token %}
+       {{ form.as_p }}
+
+       <p><button type="submit">{% trans "Append book" %}</button></p>
+       </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/book_detail.html b/apps/catalogue/templates/catalogue/book_detail.html
new file mode 100755 (executable)
index 0000000..bfd4ef5
--- /dev/null
@@ -0,0 +1,95 @@
+{% extends "catalogue/base.html" %}
+{% load book_list comments i18n %}
+
+{% block content %}
+
+
+<h1>{{ book.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 %}
+
+
+{% if editable %}
+    <p><a href="{% url catalogue_book_append book.slug %}">{% trans "Append to other book" %}</a></p>
+{% endif %}
+
+
+<div class='section'>
+
+    <h2>{% trans "Chunks" %}</h2>
+
+    <table class='single-book-list'><tbody>
+    {% for chunk in book %}
+        {{ chunk.short_html|safe }}
+    {% endfor %}
+    </tbody></table>
+
+</div>
+
+
+
+<div class='section'>
+
+
+<h2>{% trans "Publication" %}</h2>
+
+<p>{% trans "Last published" %}: 
+    {% if book.last_published %}
+        {{ book.last_published }}
+    {% else %}
+        &mdash;
+    {% endif %}
+</p>
+
+{% 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/>
+    </p>
+
+    {% 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/)
+        -->
+        <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>
+    {% 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 book %}
+    {% with book.get_absolute_url as next %}
+        {% render_comment_form for book %}
+    {% endwith %}
+</div>
+
+{% endblock content %}
diff --git a/apps/catalogue/templates/catalogue/book_edit.html b/apps/catalogue/templates/catalogue/book_edit.html
new file mode 100755 (executable)
index 0000000..3fffa96
--- /dev/null
@@ -0,0 +1,14 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+       <form enctype="multipart/form-data" method="POST" action="">
+    {% csrf_token %}
+       {{ form.as_p }}
+
+       <p><button type="submit">{% trans "Save" %}</button></p>
+       </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/book_html.html b/apps/catalogue/templates/catalogue/book_html.html
new file mode 100755 (executable)
index 0000000..af4cfa7
--- /dev/null
@@ -0,0 +1,30 @@
+{% load i18n %}
+{% load compressed %}
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+        <title>{{ book.title }}</title>
+    </head>
+    <body>
+        <div id="menu">
+            <ul>
+                <li><a href="#toc">{% trans "Table of contents" %}</a></li>
+                <li><a href="#nota_red">{% trans "Edit. note" %}</a></li>
+                <li><a href="#info">{% trans "Infobox" %}</a></li>
+            </ul>
+        </div>
+        <div id="info">
+            {#% book_info book %#}
+        </div>
+        <div id="header">
+            <div id="logo">
+                <a href="/"><img src="http://static.wolnelektury.pl/img/logo.png" alt="WolneLektury.pl - logo" /></a>
+            </div>
+        </div>
+
+        {{ html|safe }}
+
+    </body>
+</html>
diff --git a/apps/catalogue/templates/catalogue/book_list/book.html b/apps/catalogue/templates/catalogue/book_list/book.html
new file mode 100755 (executable)
index 0000000..46d5ae1
--- /dev/null
@@ -0,0 +1,35 @@
+{% load i18n %}
+
+{% if book.single %}
+    {% with book.0 as chunk %}
+    <tr>
+        <td><a href="{% url catalogue_book book.slug %}" title='{% trans "Book settings" %}'>[B]</a></td>
+        <td><a href="{% url catalogue_chunk_edit book.slug chunk.slug %}" title='{% trans "Chunk settings" %}'>[c]</a></td>
+        <td><a target="_blank"
+                    href="{% url wiki_editor book.slug %}">
+                    {{ book.title }}</a></td>
+        <td>{% if chunk.stage %}
+            {{ chunk.stage }}
+        {% else %}–
+        {% endif %}</td>
+        <td class='user-column'>{% if chunk.user %}<a href="{% url catalogue_user chunk.user.username %}">{{ chunk.user.first_name }} {{ chunk.user.last_name }}</a>{% endif %}</td>
+        <td>
+            {% if chunk.published %}P{% endif %}
+            {% if book.new_publishable %}p{% endif %}
+            {% if chunk.changed %}+{% endif %}
+        </td>
+    </tr>
+    {% endwith %}
+{% else %}
+    <tr>
+        <td class='book-settings-link'><a href="{% url catalogue_book book.slug %}" title='{% trans "Book settings" %}'>[B]</a></td>
+        <td></td>
+        <td>{{ book.title }}</td>
+        <td></td>
+        <td class='user-column'></td>
+        <td>
+            {% if book.published %}P{% endif %}
+            {% if book.new_publishable %}p{% endif %}
+        </td>
+    </tr>
+{% endif %}
diff --git a/apps/catalogue/templates/catalogue/book_list/book_list.html b/apps/catalogue/templates/catalogue/book_list/book_list.html
new file mode 100755 (executable)
index 0000000..73811ca
--- /dev/null
@@ -0,0 +1,81 @@
+{% load i18n %}
+{% load pagination_tags %}
+
+
+<form name='filter' action=''>
+<input type='hidden' name="title" value="{{ request.GET.title }}" />
+<input type='hidden' name="stage" value="{{ request.GET.stage }}" />
+{% if not viewed_user %}
+    <input type='hidden' name="user" value="{{ request.GET.user }}" />
+{% endif %}
+<input type='hidden' name="all" value="{{ request.GET.all }}" />
+<input type='hidden' name="status" value="{{ request.GET.status }}" />
+</form>
+
+<table id="file-list"{% if viewed_user %} class="book-list-user"{% endif %}>
+    <thead><tr>
+        <th></th>
+        <th>
+            <input class='check-filter' type='checkbox' name='all' title='{% trans "Show hidden books" %}'
+                {% if request.GET.all %}checked='checked'{% endif %} />
+            </th>
+        <th class='book-search-column'>
+            <form>
+            <input title='{% trans "Search in book titles" %}' name="title"
+                class='text-filter' value="{{ request.GET.title }}" />
+            </form>
+        </th>
+        <th><select name="stage" class="filter">
+            <option value=''>- {% trans "stage" %} -</option>
+            <option {% if request.GET.stage == '-' %}selected="selected"
+                    {% endif %}value="-">- {% trans "none" %} -</option>
+            {% for stage in stages %}
+                <option {% if request.GET.stage == stage.slug %}selected="selected"
+                    {% endif %}value="{{ stage.slug }}">{{ stage.name }}</option>
+            {% endfor %}
+        </select></th>
+
+        {% if not viewed_user %}
+            <th><select name="user" class="filter">
+                <option value=''>- {% trans "editor" %} -</option>
+                <option {% if request.GET.user == '-' %}selected="selected"
+                        {% endif %}value="-">- {% trans "none" %} -</option>
+                {% for user in users %}
+                    <option {% if request.GET.user == user.username %}selected="selected"
+                        {% endif %}value="{{ user.username }}">{{ user.first_name }} {{ user.last_name }} ({{ user.count }})</option>
+                {% endfor %}
+            </select></th>
+        {% endif %}
+
+        <th><select name="status" class="filter">
+            <option value=''>- {% trans "status" %} -</option>
+            {% for state, label in states %}
+                <option {% if request.GET.status == state %}selected="selected"
+                        {% endif %}value='{{ state }}'>{{ label }}</option>
+            {% endfor %}
+        </select></th>
+
+    </tr></thead>
+
+    {% with cnt=books|length %}
+    {% autopaginate books 100 %}
+    <tbody>
+    {% for item in books %}
+        {% with item.book as book %}
+            {{ book.short_html|safe }}
+            {% if not book.single %}
+                {% for chunk in item.chunks %}
+                    {{ chunk.short_html|safe }}
+                {% endfor %}
+            {% endif %}
+        {% endwith %}
+    {% endfor %}
+    <tr><th class='paginator' colspan="5">
+        {% paginate %}
+        {% blocktrans count c=cnt %}{{c}} book{% plural %}{{c}} books{% endblocktrans %}</th></tr>
+    </tbody>
+    {% endwith %}
+</table>
+{% if not books %}
+    <p>{% trans "No books found." %}</p>
+{% endif %}
diff --git a/apps/catalogue/templates/catalogue/book_list/chunk.html b/apps/catalogue/templates/catalogue/book_list/chunk.html
new file mode 100755 (executable)
index 0000000..1459942
--- /dev/null
@@ -0,0 +1,25 @@
+{% load i18n %}
+
+<tr>
+    <td class='book-settings-column'></td>
+    <td><a href="{% url catalogue_chunk_edit chunk.book.slug chunk.slug %}" title='{% trans "Chunk settings" %}'>[c]</a></td>
+    <td><a target="_blank" href="{{ chunk.get_absolute_url }}">
+            <span class='chunkno'>{{ chunk.number }}.</span>
+            {{ chunk.title }}</a></td>
+    <td>{% if chunk.stage %}
+            {{ chunk.stage }}
+        {% else %}
+            –
+        {% endif %}</td>
+        <td class='user-column'>{% if chunk.user %}
+            <a href="{% url catalogue_user chunk.user.username %}">
+                {{ chunk.user.first_name }} {{ chunk.user.last_name }}
+            </a>{% else %}
+            
+            {% endif %}</td>
+</td>
+<td>
+    {% if chunk.new_publishable %}p{% endif %}
+    {% if chunk.changed %}+{% endif %}
+</td>
+</tr>
diff --git a/apps/catalogue/templates/catalogue/chunk_add.html b/apps/catalogue/templates/catalogue/chunk_add.html
new file mode 100755 (executable)
index 0000000..b287479
--- /dev/null
@@ -0,0 +1,16 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block content %}
+    <h1>{% trans "Split chunk" %}</h1>
+
+       <form enctype="multipart/form-data" method="POST">
+    {% csrf_token %}
+    <table class='editable'>
+        <tr><th>{% trans "Insert empty chunk after" %}:</th>
+            <td><a href="{{ chunk.get_absolute_url }}">{{ chunk.pretty_name }}</a></td></tr>
+        {{ form.as_table }}
+        <tr><td></td><td><button type="submit">{% trans "Add chunk" %}</button></td></tr>
+    </table>
+       </form>
+{% endblock content %}
diff --git a/apps/catalogue/templates/catalogue/chunk_edit.html b/apps/catalogue/templates/catalogue/chunk_edit.html
new file mode 100755 (executable)
index 0000000..bdacd02
--- /dev/null
@@ -0,0 +1,21 @@
+{% extends "catalogue/base.html" %}
+{% load url from future %}
+{% load i18n %}
+
+{% block content %}
+    <h1>{% trans "Chunk settings" %}</h1>
+
+       <form enctype="multipart/form-data" method="POST" action="{% if go_next %}?next={{ go_next }}{% endif %}">
+    {% csrf_token %}
+    <table class='editable'>
+        <tr><th>{% trans "Book" %}:</th><td>{{ chunk.book }} ({{ chunk.number }}/{{ chunk.book|length }})</td></tr>
+        {{ form.as_table}}
+        <tr><td></td><td><button type="submit">{% trans "Save" %}</button></td></tr>
+    </table>
+
+       </form>
+
+
+    <p><a href="{% url "catalogue_chunk_add" chunk.book.slug chunk.slug %}">{% trans "Split chunk" %}</a></p>
+
+{% endblock content %}
diff --git a/apps/catalogue/templates/catalogue/document_create_missing.html b/apps/catalogue/templates/catalogue/document_create_missing.html
new file mode 100644 (file)
index 0000000..47c99f9
--- /dev/null
@@ -0,0 +1,14 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block content %}
+    <h1>{% trans "Create a new book" %}</h1>
+
+    <form enctype="multipart/form-data" method="POST">
+    {% csrf_token %}
+    <table class='editable'>
+        {{ form.as_table}}
+        <tr><td></td><td><button type="submit">{% trans "Create book" %}</button></td></tr>
+    </table>
+    </form>
+{% endblock content %}
diff --git a/apps/catalogue/templates/catalogue/document_list.html b/apps/catalogue/templates/catalogue/document_list.html
new file mode 100644 (file)
index 0000000..920f25a
--- /dev/null
@@ -0,0 +1,9 @@
+{% extends "catalogue/base.html" %}
+
+{% load i18n %}
+{% load catalogue book_list %}
+
+
+{% block content %}
+    {% book_list %}
+{% endblock content %}
diff --git a/apps/catalogue/templates/catalogue/document_upload.html b/apps/catalogue/templates/catalogue/document_upload.html
new file mode 100644 (file)
index 0000000..87e93e0
--- /dev/null
@@ -0,0 +1,69 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+
+{% block leftcolumn %}
+
+
+<h2>{% trans "Bulk documents upload" %}</h2>
+
+<p>
+{% 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="">
+{% csrf_token %}
+{{ form.as_p }}
+<p><button type="submit">{% trans "Upload" %}</button></p>
+</form>
+
+<hr/>
+
+{% if error_list %}
+
+    <p class='error'>{% trans "There have been some errors. No files have been added to the repository." %}
+    <h3>{% trans "Offending files" %}</h3>
+    <ul id='error-list'>
+        {% for filename, title, error in error_list %}
+            <li>{{ title }} (<code>{{ filename }}</code>): {{ error }}</li>
+        {% endfor %}
+    </ul>
+
+    {% if ok_list %}
+    <h3>{% trans "Correct files" %}</h3>
+        <ul>
+            {% for filename, slug, title in ok_list %}
+                <li>{{ title }} (<code>{{ filename }}</code>)</li>
+            {% endfor %}
+        </ul>
+    {% endif %}
+
+{% else %}
+
+    {% if ok_list %}
+        <p class='success'>{% trans "Files have been successfully uploaded to the repository." %}</p>
+        <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>
+        {% endfor %}
+        </ul>
+    {% endif %}
+{% endif %}
+
+{% if skipped_list %}
+    <h3>{% trans "Skipped files" %}</h3>
+    <p>{% trans "Files skipped due to no <code>.xml</code> extension" %}</p>
+    <ul id='skipped-list'>
+        {% for filename in skipped_list %}
+            <li>{{ filename }}</li>
+        {% endfor %}
+    </ul>
+{% endif %}
+
+
+{% endblock leftcolumn %}
+
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/image_list.html b/apps/catalogue/templates/catalogue/image_list.html
new file mode 100755 (executable)
index 0000000..3ff75bc
--- /dev/null
@@ -0,0 +1,9 @@
+{% extends "catalogue/base.html" %}
+
+{% load i18n %}
+{% load catalogue book_list %}
+
+
+{% block content %}
+    {% image_list %}
+{% endblock content %}
diff --git a/apps/catalogue/templates/catalogue/image_short.html b/apps/catalogue/templates/catalogue/image_short.html
new file mode 100755 (executable)
index 0000000..2e2b386
--- /dev/null
@@ -0,0 +1,18 @@
+{% load i18n %}
+
+<tr>
+    <td><a href="{% url catalogue_image image.slug %}" title='{% trans "Image settings" %}'>[B]</a></td>
+    <td><a target="_blank"
+                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>
diff --git a/apps/catalogue/templates/catalogue/image_table.html b/apps/catalogue/templates/catalogue/image_table.html
new file mode 100755 (executable)
index 0000000..68293e7
--- /dev/null
@@ -0,0 +1,69 @@
+{% load i18n %}
+{% load pagination_tags %}
+
+
+<form name='filter' action=''>
+<input type='hidden' name="title" value="{{ request.GET.title }}" />
+<input type='hidden' name="stage" value="{{ request.GET.stage }}" />
+{% if not viewed_user %}
+    <input type='hidden' name="user" value="{{ request.GET.user }}" />
+{% endif %}
+<input type='hidden' name="status" value="{{ request.GET.status }}" />
+</form>
+
+<table id="file-list"{% if viewed_user %} class="book-list-user"{% endif %}>
+    <thead><tr>
+        <th></th>
+        <th class='book-search-column'>
+            <form>
+            <input title='{% trans "Search in book titles" %}' name="title"
+                class='text-filter' value="{{ request.GET.title }}" />
+            </form>
+        </th>
+        <th><select name="stage" class="filter">
+            <option value=''>- {% trans "stage" %} -</option>
+            <option {% if request.GET.stage == '-' %}selected="selected"
+                    {% endif %}value="-">- {% trans "none" %} -</option>
+            {% for stage in stages %}
+                <option {% if request.GET.stage == stage.slug %}selected="selected"
+                    {% endif %}value="{{ stage.slug }}">{{ stage.name }}</option>
+            {% endfor %}
+        </select></th>
+
+        {% if not viewed_user %}
+            <th><select name="user" class="filter">
+                <option value=''>- {% trans "editor" %} -</option>
+                <option {% if request.GET.user == '-' %}selected="selected"
+                        {% endif %}value="-">- {% trans "none" %} -</option>
+                {% for user in users %}
+                    <option {% if request.GET.user == user.username %}selected="selected"
+                        {% endif %}value="{{ user.username }}">{{ user.first_name }} {{ user.last_name }} ({{ user.count }})</option>
+                {% endfor %}
+            </select></th>
+        {% endif %}
+
+        <th><select name="status" class="filter">
+            <option value=''>- {% trans "status" %} -</option>
+            {% for state, label in states %}
+                <option {% if request.GET.status == state %}selected="selected"
+                        {% endif %}value='{{ state }}'>{{ label }}</option>
+            {% endfor %}
+        </select></th>
+
+    </tr></thead>
+
+    {% with cnt=objects|length %}
+    {% autopaginate objects 100 %}
+    <tbody>
+    {% for item in objects %}
+        {{ item.short_html|safe }}
+    {% endfor %}
+    <tr><th class='paginator' colspan="5">
+        {% paginate %}
+        {% blocktrans count c=cnt %}{{c}} image{% plural %}{{c}} images{% endblocktrans %}</th></tr>
+    </tbody>
+    {% endwith %}
+</table>
+{% if not objects %}
+    <p>{% trans "No images found." %}</p>
+{% endif %}
diff --git a/apps/catalogue/templates/catalogue/main_tabs.html b/apps/catalogue/templates/catalogue/main_tabs.html
new file mode 100755 (executable)
index 0000000..82321cc
--- /dev/null
@@ -0,0 +1,3 @@
+{% for tab in tabs %}
+    <a {% ifequal active_tab tab.slug %}class="active" {% endifequal %}href="{{ tab.url }}">{{ tab.caption }}</a>
+{% endfor %}
diff --git a/apps/catalogue/templates/catalogue/my_page.html b/apps/catalogue/templates/catalogue/my_page.html
new file mode 100755 (executable)
index 0000000..48a2179
--- /dev/null
@@ -0,0 +1,24 @@
+{% extends "catalogue/base.html" %}
+
+{% load i18n %}
+{% load catalogue book_list wall %}
+
+
+{% block leftcolumn %}
+    {% book_list request.user %}
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+    <div id="last-edited-list">
+        <h2>{% trans "Your last edited documents" %}</h2>
+        <ol>
+            {% for slugs, item in last_books %}
+            <li><a href="{% url wiki_editor slugs.0 slugs.1 %}"
+                target="_blank">{{ item.title }}</a><br/><span class="date">({{ item.time|date:"H:i:s, d/m/Y" }})</span></li>
+            {% endfor %}
+        </ol>
+    </div>
+
+    <h2>{% trans "Recent activity for" %} {{ request.user|nice_name }}</h2>
+    {% wall request.user 10 %}
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/upload_pdf.html b/apps/catalogue/templates/catalogue/upload_pdf.html
new file mode 100755 (executable)
index 0000000..a9670e4
--- /dev/null
@@ -0,0 +1,17 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+
+{% block content %}
+
+
+<h2>{% trans "PDF file upload" %}</h2>
+
+<form enctype="multipart/form-data" method="POST" action="">
+{% csrf_token %}
+{{ form.as_p }}
+<p><button type="submit">{% trans "Upload" %}</button></p>
+</form>
+
+
+{% endblock content %}
diff --git a/apps/catalogue/templates/catalogue/user_list.html b/apps/catalogue/templates/catalogue/user_list.html
new file mode 100755 (executable)
index 0000000..9e1e83e
--- /dev/null
@@ -0,0 +1,18 @@
+{% extends "catalogue/base.html" %}
+
+{% load i18n %}
+
+{% block leftcolumn %}
+
+<h1>{% trans "Users" %}</h1>
+
+<ul>
+{% for user in users %}
+    <li><a href="{% url catalogue_user user.username %}">
+        <span class="chunkno">{{ forloop.counter }}.</span>
+        {{ user.first_name }} {{ user.last_name }}</a>
+        ({{ user.count }})</li>
+{% endfor %}
+</ul>
+
+{% endblock leftcolumn %}
diff --git a/apps/catalogue/templates/catalogue/user_page.html b/apps/catalogue/templates/catalogue/user_page.html
new file mode 100755 (executable)
index 0000000..89b4ece
--- /dev/null
@@ -0,0 +1,15 @@
+{% extends "catalogue/base.html" %}
+
+{% load i18n %}
+{% load catalogue book_list wall %}
+
+
+{% block leftcolumn %}
+    <h1>{{ viewed_user|nice_name }}</h1>
+    {% book_list viewed_user %}
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+    <h2>{% trans "Recent activity for" %} {{ viewed_user|nice_name }}</h2>
+    {% wall viewed_user 10 %}
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/wall.html b/apps/catalogue/templates/catalogue/wall.html
new file mode 100755 (executable)
index 0000000..9227ba1
--- /dev/null
@@ -0,0 +1,35 @@
+{% load i18n %}
+{% load gravatar %}
+{% load email %}
+
+<ul class='wall'>
+{% for item in wall %}
+    <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 %}
+                <br/>
+            {% endif %}
+        </div>
+
+        <div class="time">{{ item.timestamp }}</div>
+        <h3>{{ item.header }}</h3>
+        <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 %}">
+            {{ item.user.first_name }} {{ item.user.last_name }}</a>
+            &lt;{{ item.user.email|email_link }}>
+        {% else %}
+            {{ item.user_name }}
+            {% if item.email %}
+                &lt;{{ item.email|email_link }}>
+            {% endif %}
+            ({% trans "not logged in" %})
+        {% endif %}
+        <br/>{{ item.summary|linebreaksbr }}
+    </li>
+{% empty %}
+    <li>{% trans "No activity recorded." %}</li>
+{% endfor %}
+</ul>
diff --git a/apps/catalogue/templatetags/__init__.py b/apps/catalogue/templatetags/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/catalogue/templatetags/book_list.py b/apps/catalogue/templatetags/book_list.py
new file mode 100755 (executable)
index 0000000..5e18b7e
--- /dev/null
@@ -0,0 +1,195 @@
+from __future__ import absolute_import
+
+from re import split
+from django.db.models import Q, Count
+from django import template
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth.models import User
+from catalogue.models import Chunk, Image
+
+register = template.Library()
+
+
+class ChunksList(object):
+    def __init__(self, chunk_qs):
+        #self.chunk_qs = chunk_qs#.annotate(
+            #book_length=Count('book__chunk')).select_related(
+            #'book')#, 'stage__name',
+            #'user')
+        self.chunk_qs = chunk_qs.select_related('book__hidden')
+
+        self.book_qs = chunk_qs.values('book_id')
+
+    def __getitem__(self, key):
+        if isinstance(key, slice):
+            return self.get_slice(key)
+        elif isinstance(key, int):
+            return self.get_slice(slice(key, key+1))[0]
+        else:
+            raise TypeError('Unsupported list index. Must be a slice or an int.')
+
+    def __len__(self):
+        return self.book_qs.count()
+
+    def get_slice(self, slice_):
+        book_ids = [x['book_id'] for x in self.book_qs[slice_]]
+        chunk_qs = self.chunk_qs.filter(book__in=book_ids)
+
+        chunks_list = []
+        book = None
+        for chunk in chunk_qs:
+            if chunk.book != book:
+                book = chunk.book
+                chunks_list.append(ChoiceChunks(book, [chunk]))
+            else:
+                chunks_list[-1].chunks.append(chunk)
+        return chunks_list
+
+
+class ChoiceChunks(object):
+    """
+        Associates the given chunks iterable for a book.
+    """
+
+    chunks = None
+
+    def __init__(self, book, chunks):
+        self.book = book
+        self.chunks = chunks
+
+
+def foreign_filter(qs, value, filter_field, model, model_field='slug', unset='-'):
+    if value == unset:
+        return qs.filter(**{filter_field: None})
+    if not value:
+        return qs
+    try:
+        obj = model._default_manager.get(**{model_field: value})
+    except model.DoesNotExist:
+        return qs.none()
+    else:
+        return qs.filter(**{filter_field: obj})
+
+
+def search_filter(qs, value, filter_fields):
+    if not value:
+        return qs
+    q = Q(**{"%s__icontains" % filter_fields[0]: value})
+    for field in filter_fields[1:]:
+        q |= Q(**{"%s__icontains" % field: value})
+    return qs.filter(q)
+
+
+_states = [
+        ('publishable', _('publishable'), Q(book___new_publishable=True)),
+        ('changed', _('changed'), Q(_changed=True)),
+        ('published', _('published'), Q(book___published=True)),
+        ('unpublished', _('unpublished'), Q(book___published=False)),
+        ('empty', _('empty'), Q(head=None)),
+    ]
+_states_options = [s[:2] for s in _states]
+_states_dict = dict([(s[0], s[2]) for s in _states])
+
+
+def document_list_filter(request, **kwargs):
+
+    def arg_or_GET(field):
+        return kwargs.get(field, request.GET.get(field))
+
+    if arg_or_GET('all'):
+        chunks = Chunk.objects.all()
+    else:
+        chunks = Chunk.visible_objects.all()
+
+    chunks = chunks.order_by('book__title', 'book', 'number')
+
+    if not request.user.is_authenticated():
+        chunks = chunks.filter(book__public=True)
+
+    state = arg_or_GET('status')
+    if state in _states_dict:
+        chunks = chunks.filter(_states_dict[state])
+
+    chunks = foreign_filter(chunks, arg_or_GET('user'), 'user', User, 'username')
+    chunks = foreign_filter(chunks, arg_or_GET('stage'), 'stage', Chunk.tag_model, 'slug')
+    chunks = search_filter(chunks, arg_or_GET('title'), ['book__title', 'title'])
+    return chunks
+
+
+@register.inclusion_tag('catalogue/book_list/book_list.html', takes_context=True)
+def book_list(context, user=None):
+    request = context['request']
+
+    if user:
+        filters = {"user": user}
+        new_context = {"viewed_user": user}
+    else:
+        filters = {}
+        new_context = {"users": User.objects.annotate(
+                count=Count('chunk')).filter(count__gt=0).order_by(
+                '-count', 'last_name', 'first_name')}
+
+    new_context.update({
+        "filters": True,
+        "request": request,
+        "books": ChunksList(document_list_filter(request, **filters)),
+        "stages": Chunk.tag_model.objects.all(),
+        "states": _states_options,
+    })
+
+    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 _states]
+_image_states_dict = dict([(s[0], s[2]) for s in _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('chunk')).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
diff --git a/apps/catalogue/templatetags/catalogue.py b/apps/catalogue/templatetags/catalogue.py
new file mode 100644 (file)
index 0000000..0b57b49
--- /dev/null
@@ -0,0 +1,44 @@
+from __future__ import absolute_import
+
+from django.core.urlresolvers import reverse
+from django import template
+from django.utils.translation import ugettext as _
+
+register = template.Library()
+
+
+class Tab(object):
+    slug = None
+    caption = None
+    url = None
+
+    def __init__(self, slug, caption, url):
+        self.slug = slug
+        self.caption = caption
+        self.url = url
+
+
+@register.inclusion_tag("catalogue/main_tabs.html", takes_context=True)
+def main_tabs(context):
+    active = getattr(context['request'], 'catalogue_active_tab', None)
+
+    tabs = []
+    user = context['user']
+    tabs.append(Tab('my', _('My page'), reverse("catalogue_user")))
+
+    tabs.append(Tab('activity', _('Activity'), reverse("catalogue_activity")))
+    tabs.append(Tab('all', _('All'), reverse("catalogue_document_list")))
+    tabs.append(Tab('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")))
+
+    return {"tabs": tabs, "active_tab": active}
+
+
+@register.filter
+def nice_name(user):
+    return user.get_full_name() or user.username
+
diff --git a/apps/catalogue/templatetags/set_get_parameter.py b/apps/catalogue/templatetags/set_get_parameter.py
new file mode 100755 (executable)
index 0000000..b3d44d7
--- /dev/null
@@ -0,0 +1,46 @@
+from re import split
+
+from django import template
+
+register = template.Library()
+
+
+"""
+In template:
+    {% set_get_paramater param1='const_value',param2=,param3=variable %}
+results with changes to query string:
+    param1 is set to `const_value' string
+    param2 is unset, if exists,
+    param3 is set to the value of variable in context
+
+Using 'django.core.context_processors.request' is required.
+
+"""
+
+
+class SetGetParameter(template.Node):
+    def __init__(self, values):
+        self.values = values
+        
+    def render(self, context):
+        request = template.Variable('request').resolve(context)
+        params = request.GET.copy()
+        for key, value in self.values.items():
+            if value == '':
+                if key in params:
+                    del(params[key])
+            else:
+                params[key] = template.Variable(value).resolve(context)
+        return '?%s' %  params.urlencode()
+
+
+@register.tag
+def set_get_parameter(parser, token):
+    parts = split(r'\s+', token.contents, 2)
+
+    values = {}
+    for pair in parts[1].split(','):
+        s = pair.split('=')
+        values[s[0]] = s[1]
+
+    return SetGetParameter(values)
diff --git a/apps/catalogue/templatetags/wall.py b/apps/catalogue/templatetags/wall.py
new file mode 100755 (executable)
index 0000000..28671fb
--- /dev/null
@@ -0,0 +1,155 @@
+from __future__ import absolute_import
+
+from datetime import timedelta
+from django.db.models import Q
+from django.core.urlresolvers import reverse
+from django.contrib.comments.models import Comment
+from django import template
+from django.utils.translation import ugettext as _
+
+from catalogue.models import Chunk, BookPublishRecord
+
+register = template.Library()
+
+
+class WallItem(object):
+    title = ''
+    summary = ''
+    url = ''
+    timestamp = ''
+    user = None
+    user_name = ''
+    email = ''
+
+    def __init__(self, tag):
+        self.tag = tag
+
+    def get_email(self):
+        if self.user:
+            return self.user.email
+        else:
+            return self.email
+
+
+def changes_wall(user=None, max_len=None, day=None):
+    qs = Chunk.change_model.objects.order_by('-created_at')
+    qs = qs.select_related('author', 'tree', 'tree__book__title')
+    if user is not None:
+        qs = qs.filter(Q(author=user) | Q(tree__user=user))
+    if max_len is not None:
+        qs = qs[:max_len]
+    if day is not None:
+        next_day = day + timedelta(1)
+        qs = qs.filter(created_at__gte=day, created_at__lt=next_day)
+    for item in qs:
+        tag = 'stage' if item.tags.count() else 'change'
+        chunk = item.tree
+        w  = WallItem(tag)
+        if user and item.author != user:
+            w.header = _('Related edit')
+        else:
+            w.header = _('Edit')
+        w.title = chunk.pretty_name()
+        w.summary = item.description
+        w.url = reverse('wiki_editor', 
+                args=[chunk.book.slug, chunk.slug]) + '?diff=%d' % item.revision
+        w.timestamp = item.created_at
+        w.user = item.author
+        w.user_name = item.author_name
+        w.email = item.author_email
+        yield w
+
+
+# TODO: marked for publishing
+
+
+def published_wall(user=None, max_len=None, day=None):
+    qs = BookPublishRecord.objects.select_related('book__title')
+    if user:
+        # TODO: published my book
+        qs = qs.filter(Q(user=user))
+    if max_len is not None:
+        qs = qs[:max_len]
+    if day is not None:
+        next_day = day + timedelta(1)
+        qs = qs.filter(timestamp__gte=day, timestamp__lt=next_day)
+    for item in qs:
+        w = WallItem('publish')
+        w.header = _('Publication')
+        w.title = item.book.title
+        w.timestamp = item.timestamp
+        w.url = item.book.get_absolute_url()
+        w.user = item.user
+        w.email = item.user.email
+        yield w
+
+
+def comments_wall(user=None, max_len=None, day=None):
+    qs = Comment.objects.filter(is_public=True).select_related().order_by('-submit_date')
+    if user:
+        # TODO: comments concerning my books
+        qs = qs.filter(Q(user=user))
+    if max_len is not None:
+        qs = qs[:max_len]
+    if day is not None:
+        next_day = day + timedelta(1)
+        qs = qs.filter(submit_date__gte=day, submit_date__lt=next_day)
+    for item in qs:
+        w  = WallItem('comment')
+        w.header = _('Comment')
+        w.title = item.content_object
+        w.summary = item.comment
+        w.url = item.content_object.get_absolute_url()
+        w.timestamp = item.submit_date
+        w.user = item.user
+        ui = item.userinfo
+        w.email = item.email
+        w.user_name = item.name
+        yield w
+
+
+def big_wall(walls, max_len=None):
+    """
+        Takes some WallItem iterators and zips them into one big wall.
+        Input iterators must already be sorted by timestamp.
+    """
+    subwalls = []
+    for w in walls:
+        try:
+            subwalls.append([next(w), w])
+        except StopIteration:
+            pass
+
+    if max_len is None:
+        max_len = -1
+    while max_len and subwalls:
+        i, next_item = max(enumerate(subwalls), key=lambda x: x[1][0].timestamp)
+        yield next_item[0]
+        max_len -= 1
+        try:
+            next_item[0] = next(next_item[1])
+        except StopIteration:
+            del subwalls[i]
+
+
+@register.inclusion_tag("catalogue/wall.html", takes_context=True)
+def wall(context, user=None, max_len=100):
+    return {
+        "request": context['request'],
+        "STATIC_URL": context['STATIC_URL'],
+        "wall": big_wall([
+            changes_wall(user, max_len),
+            published_wall(user, max_len),
+            comments_wall(user, max_len),
+        ], max_len)}
+
+@register.inclusion_tag("catalogue/wall.html", takes_context=True)
+def day_wall(context, day):
+    return {
+        "request": context['request'],
+        "STATIC_URL": context['STATIC_URL'],
+        "wall": big_wall([
+            changes_wall(day=day),
+            published_wall(day=day),
+            comments_wall(day=day),
+        ])}
diff --git a/apps/catalogue/tests/__init__.py b/apps/catalogue/tests/__init__.py
new file mode 100755 (executable)
index 0000000..b03701f
--- /dev/null
@@ -0,0 +1,72 @@
+from os.path import abspath, dirname, join
+from nose.tools import *
+from mock import patch
+from django.test import TestCase
+from django.contrib.auth.models import User
+from catalogue.models import Book, BookPublishRecord
+
+
+def get_fixture(path):
+    f_path = join(dirname(abspath(__file__)), 'files', path)
+    with open(f_path) as f:
+        return unicode(f.read(), 'utf-8')
+
+
+class PublishTests(TestCase):
+
+    def setUp(self):
+        self.user = User.objects.create(username='tester')
+        self.text1 = get_fixture('chunk1.xml')
+        self.book = Book.create(self.user, self.text1, slug='test-book')
+
+    @patch('apiclient.api_call')
+    def test_unpublishable(self, api_call):
+        with self.assertRaises(AssertionError):
+            self.book.publish(self.user)
+
+    @patch('apiclient.api_call')
+    def test_publish(self, api_call):
+        self.book[0].head.set_publishable(True)
+        self.book.publish(self.user)
+        api_call.assert_called_with(self.user, 'books/', {"book_xml": self.text1})
+
+    @patch('apiclient.api_call')
+    def test_publish_multiple(self, api_call):
+        self.book[0].head.set_publishable(True)
+        self.book[0].split(slug='part-2')
+        self.book[1].commit(get_fixture('chunk2.xml'))
+        self.book[1].head.set_publishable(True)
+        self.book.publish(self.user)
+        api_call.assert_called_with(self.user, 'books/', {"book_xml": get_fixture('expected.xml')})
+
+
+class ManipulationTests(TestCase):
+
+    def setUp(self):
+        self.user = User.objects.create(username='tester')
+        self.book1 = Book.create(self.user, 'book 1', slug='book1')
+        self.book2 = Book.create(self.user, 'book 2', slug='book2')
+
+    def test_append(self):
+        self.book1.append(self.book2)
+        self.assertEqual(Book.objects.all().count(), 1)
+        self.assertEqual(len(self.book1), 2)
+
+    def test_append_to_self(self):
+        with self.assertRaises(AssertionError):
+            self.book1.append(Book.objects.get(pk=self.book1.pk))
+        self.assertEqual(Book.objects.all().count(), 2)
+        self.assertEqual(len(self.book1), 1)
+
+    def test_prepend_history(self):
+        self.book1.prepend_history(self.book2)
+        self.assertEqual(Book.objects.all().count(), 1)
+        self.assertEqual(len(self.book1), 1)
+        self.assertEqual(self.book1.materialize(), 'book 1')
+
+    def test_prepend_history_to_self(self):
+        with self.assertRaises(AssertionError):
+            self.book1.prepend_history(self.book1)
+        self.assertEqual(Book.objects.all().count(), 2)
+        self.assertEqual(self.book1.materialize(), 'book 1')
+        self.assertEqual(self.book2.materialize(), 'book 2')
diff --git a/apps/catalogue/tests/files/chunk1.xml b/apps/catalogue/tests/files/chunk1.xml
new file mode 100755 (executable)
index 0000000..8497f60
--- /dev/null
@@ -0,0 +1,41 @@
+<utwor>
+  <liryka_l>
+
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description rdf:about="http://example.com/documents/book/test-book/">
+<dc:creator xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam</dc:creator>
+<dc:title xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Do M***</dc:title>
+<dc:publisher xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Fundacja Nowoczesna Polska</dc:publisher>
+<dc:subject.period xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Romantyzm</dc:subject.period>
+<dc:subject.type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Liryka</dc:subject.type>
+<dc:subject.genre xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Wiersz</dc:subject.genre>
+<!--dc:description xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Publikacja zrealizowana w ramach projektu Wolne Lektury (http://wolnelektury.pl). Reprodukcja cyfrowa wykonana przez Bibliotekę Narodową z egzemplarza pochodzącego ze zbiorów BN.</dc:description-->
+<dc:identifier.url xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m</dc:identifier.url>
+<dc:source.URL xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://www.polona.pl/Content/2222</dc:source.URL>
+<dc:source xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam (1798-1855), Poezje, tom 1 (Wiersze młodzieńcze - Ballady i romanse - Wiersze do r. 1824), Krakowska Spółdzielnia Wydawnicza, wyd. 2 zwiększone, Kraków, 1922</dc:source>
+
+<dc:rights xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Domena publiczna - Adam Mickiewicz zm. 1855</dc:rights>
+<dc:date.pd xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">1926</dc:date.pd>
+<dc:format xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">xml</dc:format>
+<dc:type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
+<dc:type xml:lang="en" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
+<dc:date xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">2007-09-06</dc:date>
+
+</rdf:Description>
+</rdf:RDF>
+
+<autor_utworu>Adam Mickiewicz</autor_utworu>
+<dzielo_nadrzedne>Sonety odeskie</dzielo_nadrzedne>
+<nazwa_utworu>Do M***</nazwa_utworu>
+
+<nota><akap>Wiérsz napisany w roku 1822</akap></nota>
+
+
+<strofa>Precz z moich oczu!... posłucham od razu,/
+Precz z mego serca!... i serce posłucha,/
+Precz z méj pamięci!... Nie! tego rozkazu/
+Moja i twoja pamięć nie posłucha.</strofa>
+
+<!-- TRIM_END -->
+</liryka_l>
+</utwor>
diff --git a/apps/catalogue/tests/files/chunk2.xml b/apps/catalogue/tests/files/chunk2.xml
new file mode 100755 (executable)
index 0000000..63a243e
--- /dev/null
@@ -0,0 +1,11 @@
+<utwor><liryka_l>
+<!-- TRIM_BEGIN -->
+
+<strofa>Jak cień tém dłuższy, gdy padnie z daleka,/
+Tém szerzéj koło żałobne roztoczy,/
+Tak moja postać, im daléj ucieka,/
+Tém grubszym kirem twą pamięć pomroczy.</strofa>
+
+
+</liryka_l>
+</utwor>
diff --git a/apps/catalogue/tests/files/expected.xml b/apps/catalogue/tests/files/expected.xml
new file mode 100755 (executable)
index 0000000..ccbeefb
--- /dev/null
@@ -0,0 +1,48 @@
+<utwor>
+  <liryka_l>
+
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<rdf:Description rdf:about="http://example.com/documents/book/test-book/">
+<dc:creator xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam</dc:creator>
+<dc:title xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Do M***</dc:title>
+<dc:publisher xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Fundacja Nowoczesna Polska</dc:publisher>
+<dc:subject.period xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Romantyzm</dc:subject.period>
+<dc:subject.type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Liryka</dc:subject.type>
+<dc:subject.genre xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Wiersz</dc:subject.genre>
+<!--dc:description xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Publikacja zrealizowana w ramach projektu Wolne Lektury (http://wolnelektury.pl). Reprodukcja cyfrowa wykonana przez Bibliotekę Narodową z egzemplarza pochodzącego ze zbiorów BN.</dc:description-->
+<dc:identifier.url xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m</dc:identifier.url>
+<dc:source.URL xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">http://www.polona.pl/Content/2222</dc:source.URL>
+<dc:source xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Mickiewicz, Adam (1798-1855), Poezje, tom 1 (Wiersze młodzieńcze - Ballady i romanse - Wiersze do r. 1824), Krakowska Spółdzielnia Wydawnicza, wyd. 2 zwiększone, Kraków, 1922</dc:source>
+
+<dc:rights xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">Domena publiczna - Adam Mickiewicz zm. 1855</dc:rights>
+<dc:date.pd xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">1926</dc:date.pd>
+<dc:format xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">xml</dc:format>
+<dc:type xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
+<dc:type xml:lang="en" xmlns:dc="http://purl.org/dc/elements/1.1/">text</dc:type>
+<dc:date xml:lang="pl" xmlns:dc="http://purl.org/dc/elements/1.1/">2007-09-06</dc:date>
+
+</rdf:Description>
+</rdf:RDF>
+
+<autor_utworu>Adam Mickiewicz</autor_utworu>
+<dzielo_nadrzedne>Sonety odeskie</dzielo_nadrzedne>
+<nazwa_utworu>Do M***</nazwa_utworu>
+
+<nota><akap>Wiérsz napisany w roku 1822</akap></nota>
+
+
+<strofa>Precz z moich oczu!... posłucham od razu,/
+Precz z mego serca!... i serce posłucha,/
+Precz z méj pamięci!... Nie! tego rozkazu/
+Moja i twoja pamięć nie posłucha.</strofa>
+
+
+
+<strofa>Jak cień tém dłuższy, gdy padnie z daleka,/
+Tém szerzéj koło żałobne roztoczy,/
+Tak moja postać, im daléj ucieka,/
+Tém grubszym kirem twą pamięć pomroczy.</strofa>
+
+
+</liryka_l>
+</utwor>
diff --git a/apps/catalogue/urls.py b/apps/catalogue/urls.py
new file mode 100644 (file)
index 0000000..621eb12
--- /dev/null
@@ -0,0 +1,44 @@
+# -*- coding: utf-8
+from django.conf.urls.defaults import *
+from django.views.generic.simple import redirect_to
+
+
+urlpatterns = patterns('catalogue.views',
+    url(r'^$', redirect_to, {'url': 'catalogue/'}),
+
+    url(r'^images/$', 'image_list', name='catalogue_image_list'),
+    url(r'^image/(?P<slug>[^/]+)/$', 'image', name="catalogue_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'),
+    url(r'^users/$', 'users', name='catalogue_users'),
+    url(r'^activity/$', 'activity', name='catalogue_activity'),
+    url(r'^activity/(?P<isodate>\d{4}-\d{2}-\d{2})/$', 
+        'activity', name='catalogue_activity'),
+
+    url(r'^upload/$',
+        'upload', name='catalogue_upload'),
+
+    url(r'^create/(?P<slug>[^/]*)/',
+        'create_missing', name='catalogue_create_missing'),
+    url(r'^create/',
+        '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>[^/]+)/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"),
+
+)
diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py
new file mode 100644 (file)
index 0000000..3c37ee6
--- /dev/null
@@ -0,0 +1,482 @@
+from datetime import datetime, date, timedelta
+import logging
+import os
+from StringIO import StringIO
+from urllib import unquote
+from urlparse import urlsplit, urlunsplit
+
+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 import http
+from django.http import Http404, HttpResponse, HttpResponseForbidden
+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 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
+from catalogue.tasks import publishable_error
+
+#
+# Quick hack around caching problems, TODO: use ETags
+#
+from django.views.decorators.cache import never_cache
+
+logger = logging.getLogger("fnp.catalogue")
+
+
+@active_tab('all')
+@never_cache
+def document_list(request):
+    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)
+    return render(request, 'catalogue/user_page.html', {"viewed_user": user})
+
+
+@login_required
+@active_tab('my')
+@never_cache
+def my(request):
+    return render(request, 'catalogue/my_page.html', {
+        'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
+                        key=lambda x: x[1]['time'], reverse=True),
+
+        "logout_to": '/',
+        })
+
+
+@active_tab('users')
+def users(request):
+    return direct_to_template(request, 'catalogue/user_list.html', extra_context={
+        'users': User.objects.all().annotate(count=Count('chunk')).order_by(
+            '-count', 'last_name', 'first_name'),
+    })
+
+
+@active_tab('activity')
+def activity(request, isodate=None):
+    today = date.today()
+    try:
+        day = helpers.parse_isodate(isodate)
+    except ValueError:
+        day = today
+
+    if day > today:
+        raise Http404
+    if day != today:
+        next_day = day + timedelta(1)
+    prev_day = day - timedelta(1)
+
+    return render(request, 'catalogue/activity.html', locals())
+
+
+@never_cache
+def logout_then_redirect(request):
+    auth.logout(request)
+    return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
+
+
+@permission_required('catalogue.add_book')
+@active_tab('create')
+def create_missing(request, slug=None):
+    if slug is None:
+        slug = ''
+    slug = slug.replace(' ', '-')
+
+    if request.method == "POST":
+        form = forms.DocumentCreateForm(request.POST, request.FILES)
+        if form.is_valid():
+            
+            if request.user.is_authenticated():
+                creator = request.user
+            else:
+                creator = None
+            book = Book.create(
+                text=form.cleaned_data['text'],
+                creator=creator,
+                slug=form.cleaned_data['slug'],
+                title=form.cleaned_data['title'],
+                gallery=form.cleaned_data['gallery'],
+            )
+
+            return http.HttpResponseRedirect(reverse("catalogue_book", args=[book.slug]))
+    else:
+        form = forms.DocumentCreateForm(initial={
+                "slug": slug,
+                "title": slug.replace('-', ' ').title(),
+                "gallery": slug,
+        })
+
+    return direct_to_template(request, "catalogue/document_create_missing.html", extra_context={
+        "slug": slug,
+        "form": form,
+
+        "logout_to": '/',
+    })
+
+
+@permission_required('catalogue.add_book')
+@active_tab('upload')
+def upload(request):
+    if request.method == "POST":
+        form = forms.DocumentsUploadForm(request.POST, request.FILES)
+        if form.is_valid():
+            import slughifi
+
+            if request.user.is_authenticated():
+                creator = request.user
+            else:
+                creator = None
+
+            zip = form.cleaned_data['zip']
+            skipped_list = []
+            ok_list = []
+            error_list = []
+            slugs = {}
+            existing = [book.slug for book in Book.objects.all()]
+            for filename in zip.namelist():
+                if filename[-1] == '/':
+                    continue
+                title = os.path.basename(filename)[:-4]
+                slug = slughifi(title)
+                if not (slug and filename.endswith('.xml')):
+                    skipped_list.append(filename)
+                elif slug in slugs:
+                    error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
+                elif slug in existing:
+                    error_list.append((filename, slug, _('Slug already used in repository.')))
+                else:
+                    try:
+                        zip.read(filename).decode('utf-8') # test read
+                        ok_list.append((filename, slug, title))
+                    except UnicodeDecodeError:
+                        error_list.append((filename, title, _('File should be UTF-8 encoded.')))
+                    slugs[slug] = filename
+
+            if not error_list:
+                for filename, slug, title in ok_list:
+                    book = Book.create(
+                        text=zip.read(filename).decode('utf-8'),
+                        creator=creator,
+                        slug=slug,
+                        title=title,
+                    )
+
+            return direct_to_template(request, "catalogue/document_upload.html", extra_context={
+                "form": form,
+                "ok_list": ok_list,
+                "skipped_list": skipped_list,
+                "error_list": error_list,
+
+                "logout_to": '/',
+            })
+    else:
+        form = forms.DocumentsUploadForm()
+
+    return direct_to_template(request, "catalogue/document_upload.html", extra_context={
+        "form": form,
+
+        "logout_to": '/',
+    })
+
+
+@never_cache
+def book_xml(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    if not book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+    xml = book.materialize()
+
+    response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
+    response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
+    return response
+
+
+@never_cache
+def book_txt(request, slug):
+    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()
+    response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain')
+    response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
+    return response
+
+
+@never_cache
+def book_html(request, slug):
+    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
+
+
+@never_cache
+def book_pdf(request, slug):
+    book = get_object_or_404(Book, slug=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)
+
+
+@never_cache
+def book_epub(request, slug):
+    book = get_object_or_404(Book, slug=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)
+    response = HttpResponse(mimetype='application/epub+zip')
+    response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.epub'
+    response.write(epub_file.getvalue())
+    return response
+
+
+@never_cache
+def revision(request, slug, chunk=None):
+    try:
+        doc = Chunk.get(slug, chunk)
+    except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+        raise Http404
+    if not doc.book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+    return http.HttpResponse(str(doc.revision()))
+
+
+def book(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    if not book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    if request.user.has_perm('catalogue.change_book'):
+        if request.method == "POST":
+            form = forms.BookForm(request.POST, instance=book)
+            if form.is_valid():
+                form.save()
+                return http.HttpResponseRedirect(book.get_absolute_url())
+        else:
+            form = forms.BookForm(instance=book)
+            editable = True
+    else:
+        form = forms.ReadonlyBookForm(instance=book)
+        editable = False
+
+    publish_error = publishable_error(book)
+    publishable = publish_error is None
+
+    return direct_to_template(request, "catalogue/book_detail.html", extra_context={
+        "book": book,
+        "publishable": publishable,
+        "publishable_error": publish_error,
+        "form": form,
+        "editable": editable,
+    })
+
+
+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
+
+    #publish_error = publishable_error(book)
+    publish_error = 'Publishing not implemented yet.'
+    publishable = publish_error is None
+
+    return direct_to_template(request, "catalogue/image_detail.html", extra_context={
+        "object": image,
+        "publishable": publishable,
+        "publishable_error": publish_error,
+        "form": form,
+        "editable": editable,
+    })
+
+
+@permission_required('catalogue.add_chunk')
+def chunk_add(request, slug, chunk):
+    try:
+        doc = Chunk.get(slug, chunk)
+    except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+        raise Http404
+    if not doc.book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    if request.method == "POST":
+        form = forms.ChunkAddForm(request.POST, instance=doc)
+        if form.is_valid():
+            if request.user.is_authenticated():
+                creator = request.user
+            else:
+                creator = None
+            doc.split(creator=creator,
+                slug=form.cleaned_data['slug'],
+                title=form.cleaned_data['title'],
+                gallery_start=form.cleaned_data['gallery_start'],
+                user=form.cleaned_data['user'],
+                stage=form.cleaned_data['stage']
+            )
+
+            return http.HttpResponseRedirect(doc.book.get_absolute_url())
+    else:
+        form = forms.ChunkAddForm(initial={
+                "slug": str(doc.number + 1),
+                "title": "cz. %d" % (doc.number + 1, ),
+        })
+
+    return direct_to_template(request, "catalogue/chunk_add.html", extra_context={
+        "chunk": doc,
+        "form": form,
+    })
+
+
+def chunk_edit(request, slug, chunk):
+    try:
+        doc = Chunk.get(slug, chunk)
+    except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+        raise Http404
+    if not doc.book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    if request.method == "POST":
+        form = forms.ChunkForm(request.POST, instance=doc)
+        if form.is_valid():
+            form.save()
+            go_next = request.GET.get('next', None)
+            if go_next:
+                go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
+            else:
+                go_next = doc.book.get_absolute_url()
+            return http.HttpResponseRedirect(go_next)
+    else:
+        form = forms.ChunkForm(instance=doc)
+
+    referer = request.META.get('HTTP_REFERER')
+    if referer:
+        parts = urlsplit(referer)
+        parts = ['', ''] + list(parts[2:])
+        go_next = urlquote_plus(urlunsplit(parts))
+    else:
+        go_next = ''
+
+    return direct_to_template(request, "catalogue/chunk_edit.html", extra_context={
+        "chunk": doc,
+        "form": form,
+        "go_next": go_next,
+    })
+
+
+@permission_required('catalogue.change_book')
+def book_append(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    if not book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    if request.method == "POST":
+        form = forms.BookAppendForm(book, request.POST)
+        if form.is_valid():
+            append_to = form.cleaned_data['append_to']
+            append_to.append(book)
+            return http.HttpResponseRedirect(append_to.get_absolute_url())
+    else:
+        form = forms.BookAppendForm(book)
+    return direct_to_template(request, "catalogue/book_append_to.html", extra_context={
+        "book": book,
+        "form": form,
+
+        "logout_to": '/',
+    })
+
+
+@require_POST
+@login_required
+def publish(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    if not book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+
+    try:
+        book.publish(request.user)
+    except NotAuthorizedError:
+        return http.HttpResponseRedirect(reverse('apiclient_oauth'))
+    except BaseException, e:
+        return http.HttpResponse(e)
+    else:
+        return http.HttpResponseRedirect(book.get_absolute_url())
diff --git a/apps/catalogue/xml_tools.py b/apps/catalogue/xml_tools.py
new file mode 100644 (file)
index 0000000..242714b
--- /dev/null
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+from copy import deepcopy
+import re
+
+from lxml import etree
+from catalogue.constants import TRIM_BEGIN, TRIM_END, MASTERS
+
+RE_TRIM_BEGIN = re.compile("^<!--%s-->$" % TRIM_BEGIN, re.M)
+RE_TRIM_END = re.compile("^<!--%s-->$" % TRIM_END, re.M)
+
+
+class ParseError(BaseException):
+    pass
+
+
+def _trim(text, trim_begin=True, trim_end=True):
+    """ 
+        Cut off everything before RE_TRIM_BEGIN and after RE_TRIM_END, so
+        that eg. one big XML file can be compiled from many small XML files.
+    """
+    if trim_begin:
+        text = RE_TRIM_BEGIN.split(text, maxsplit=1)[-1]
+    if trim_end:
+        text = RE_TRIM_END.split(text, maxsplit=1)[0]
+    return text
+
+
+def compile_text(parts):
+    """ 
+        Compiles full text from an iterable of parts,
+        trimming where applicable.
+    """
+    texts = []
+    trim_begin = False
+    text = ''
+    for next_text in parts:
+        if not next_text:
+            continue
+        if text:
+            # trim the end, because there's more non-empty text
+            # don't trim beginning, if `text' is the first non-empty part
+            texts.append(_trim(text, trim_begin=trim_begin))
+            trim_begin = True
+        text = next_text
+    # don't trim the end, because there's no more text coming after `text'
+    # only trim beginning if it's not still the first non-empty
+    texts.append(_trim(text, trim_begin=trim_begin, trim_end=False))
+    return "".join(texts)
+
+
+def add_trim_begin(text):
+    trim_tag = etree.Comment(TRIM_BEGIN)
+    e = etree.fromstring(text)
+    for master in e[::-1]:
+        if master.tag in MASTERS:
+            break
+    if master.tag not in MASTERS:
+        raise ParseError('No master tag found!')
+
+    master.insert(0, trim_tag)
+    trim_tag.tail = '\n\n\n' + (master.text or '')
+    master.text = '\n'
+    return unicode(etree.tostring(e, encoding="utf-8"), 'utf-8')
+
+
+def add_trim_end(text):
+    trim_tag = etree.Comment(TRIM_END)
+    e = etree.fromstring(text)
+    for master in e[::-1]:
+        if master.tag in MASTERS:
+            break
+    if master.tag not in MASTERS:
+        raise ParseError('No master tag found!')
+
+    master.append(trim_tag)
+    trim_tag.tail = '\n'
+    prev = trim_tag.getprevious()
+    if prev is not None:
+        prev.tail = (prev.tail or '') + '\n\n\n'
+    else:
+        master.text = (master.text or '') + '\n\n\n'
+    return unicode(etree.tostring(e, encoding="utf-8"), 'utf-8')
+
+
+def split_xml(text):
+    """Splits text into chapters.
+
+    All this stuff really must go somewhere else.
+
+    """
+    src = etree.fromstring(text)
+    chunks = []
+
+    splitter = u'naglowek_rozdzial'
+    parts = src.findall('.//naglowek_rozdzial')
+    while parts:
+        # copy the document
+        copied = deepcopy(src)
+
+        element = parts[-1]
+
+        # find the chapter's title
+        name_elem = deepcopy(element)
+        for tag in 'extra', 'motyw', 'pa', 'pe', 'pr', 'pt', 'uwaga':
+            for a in name_elem.findall('.//' + tag):
+                a.text=''
+                del a[:]
+        name = etree.tostring(name_elem, method='text', encoding='utf-8').strip()
+
+        # in the original, remove everything from the start of the last chapter
+        parent = element.getparent()
+        del parent[parent.index(element):]
+        element, parent = parent, parent.getparent()
+        while parent is not None:
+            del parent[parent.index(element) + 1:]
+            element, parent = parent, parent.getparent()
+
+        # in the copy, remove everything before the last chapter
+        element = copied.findall('.//naglowek_rozdzial')[-1]
+        parent = element.getparent()
+        while parent is not None:
+            parent.text = None
+            while parent[0] is not element:
+                del parent[0]
+            element, parent = parent, parent.getparent()
+        chunks[:0] = [[name,
+            unicode(etree.tostring(copied, encoding='utf-8'), 'utf-8')
+            ]]
+
+        parts = src.findall('.//naglowek_rozdzial')
+
+    chunks[:0] = [[u'początek',
+        unicode(etree.tostring(src, encoding='utf-8'), 'utf-8')
+        ]]
+
+    for ch in chunks[1:]:
+        ch[1] = add_trim_begin(ch[1])
+    for ch in chunks[:-1]:
+        ch[1] = add_trim_end(ch[1])
+
+    return chunks
index d55c9db..4cf9f62 100644 (file)
@@ -11,7 +11,7 @@ __all__ = ['CASBackend']
 def _verify_cas1(ticket, service):
     """Verifies CAS 1.0 authentication ticket.
 
-    Returns username on success and None on failure.
+    Returns (username, None) on success and (None, None) on failure.
     """
 
     params = {'ticket': ticket, 'service': service}
@@ -21,9 +21,9 @@ def _verify_cas1(ticket, service):
     try:
         verified = page.readline().strip()
         if verified == 'yes':
-            return page.readline().strip()
+            return page.readline().strip(), None
         else:
-            return None
+            return None, None
     finally:
         page.close()
 
@@ -31,7 +31,7 @@ def _verify_cas1(ticket, service):
 def _verify_cas2(ticket, service):
     """Verifies CAS 2.0+ XML-based authentication ticket.
 
-    Returns username on success and None on failure.
+    Returns (username, attr_dict) on success and (None, None) on failure.
     """
 
     try:
@@ -47,9 +47,12 @@ def _verify_cas2(ticket, service):
         response = page.read()
         tree = ElementTree.fromstring(response)
         if tree[0].tag.endswith('authenticationSuccess'):
-            return tree[0][0].text
+            attrs = {}
+            for tag in tree[0][1:]:
+                attrs[tag.tag] = tag.text
+            return tree[0][0].text, attrs
         else:
-            return None
+            return None, None
     except:
         import traceback
         traceback.print_exc()
@@ -74,14 +77,34 @@ class CASBackend(object):
     def authenticate(self, ticket, service):
         """Verifies CAS ticket and gets or creates User object"""
 
-        username = _verify(ticket, service)
+        username, attrs = _verify(ticket, service)
         if not username:
             return None
+
+        user_attrs = {}
+        if hasattr(settings, 'CAS_USER_ATTRS_MAP'):
+            attr_map = settings.CAS_USER_ATTRS_MAP
+            for k, v in attrs.items():
+                if k in attr_map:
+                    user_attrs[attr_map[k]] = v # unicode(v, 'utf-8')
+
         try:
             user = User.objects.get(username__iexact=username)
+            # update user info
+            changed = False
+            for k, v in user_attrs.items():
+                if getattr(user, k) != v:
+                    setattr(user, k, v)
+                    changed = True
+            if changed:
+                user.save()
         except User.DoesNotExist:
             # user will have an "unusable" password
             user = User.objects.create_user(username, '')
+            for k, v in user_attrs.items():
+                setattr(user, k, v)
+            user.first_name = attrs.get('firstname', '')
+            user.last_name = attrs.get('lastname', '')
             user.save()
         return user
 
diff --git a/apps/dvcs/admin.py b/apps/dvcs/admin.py
deleted file mode 100644 (file)
index c81d3b7..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-from django.contrib.admin import site
-from dvcs.models import Document, Change
-
-site.register(Document)
-site.register(Change)
diff --git a/apps/dvcs/locale/pl/LC_MESSAGES/django.mo b/apps/dvcs/locale/pl/LC_MESSAGES/django.mo
new file mode 100644 (file)
index 0000000..4c3a1ff
Binary files /dev/null and b/apps/dvcs/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/apps/dvcs/locale/pl/LC_MESSAGES/django.po b/apps/dvcs/locale/pl/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..64ddfd7
--- /dev/null
@@ -0,0 +1,115 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-10-03 15:35+0200\n"
+"PO-Revision-Date: 2011-10-03 15:35+0100\n"
+"Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2)\n"
+
+#: models.py:19
+msgid "name"
+msgstr "nazwa"
+
+#: models.py:20
+msgid "slug"
+msgstr "slug"
+
+#: models.py:22
+msgid "ordering"
+msgstr "kolejność"
+
+#: models.py:29
+msgid "tag"
+msgstr "tag"
+
+#: models.py:30 models.py:196
+msgid "tags"
+msgstr "tagi"
+
+#: models.py:72
+msgid "author"
+msgstr "autor"
+
+#: models.py:73
+msgid "author name"
+msgstr "imię i nazwisko autora"
+
+#: models.py:75 models.py:79
+msgid "Used if author is not set."
+msgstr "Używane, gdy nie jest ustawiony autor."
+
+#: models.py:77
+msgid "author email"
+msgstr "e-mail autora"
+
+#: models.py:81
+msgid "revision"
+msgstr "rewizja"
+
+#: models.py:85
+msgid "parent"
+msgstr "rodzic"
+
+#: models.py:90
+msgid "merge parent"
+msgstr "drugi rodzic"
+
+#: models.py:93
+msgid "description"
+msgstr "opis"
+
+#: models.py:96
+msgid "publishable"
+msgstr "do publikacji"
+
+#: models.py:102
+msgid "change"
+msgstr "zmiana"
+
+#: models.py:103
+msgid "changes"
+msgstr "zmiany"
+
+#: models.py:195
+msgid "document"
+msgstr "dokument"
+
+#: models.py:197
+msgid "data"
+msgstr "dane"
+
+#: models.py:211
+msgid "stage"
+msgstr "etap"
+
+#: models.py:219
+msgid "head"
+msgstr "głowica"
+
+#: models.py:220
+msgid "This document's current head."
+msgstr "Aktualna wersja dokumentu."
+
+#: models.py:224
+msgid "creator"
+msgstr "utworzył"
+
+#: models.py:239
+msgid "user"
+msgstr "użytkownik"
+
+#: models.py:239
+msgid "Work assignment."
+msgstr "Przypisanie pracy użytkownikowi."
index ea83ff0..d7816fa 100644 (file)
@@ -1,8 +1,65 @@
-from django.db import models
+from datetime import datetime
+import os.path
+
 from django.contrib.auth.models import User
+from django.core.files.base import ContentFile
+from django.core.files.storage import FileSystemStorage
+from django.db import models, transaction
+from django.db.models.base import ModelBase
 from django.utils.translation import ugettext_lazy as _
 from mercurial import mdiff, simplemerge
-import pickle
+
+from django.conf import settings
+from dvcs.signals import post_commit, post_publishable
+from dvcs.storage import GzipFileSystemStorage
+
+
+class Tag(models.Model):
+    """A tag (e.g. document stage) which can be applied to a Change."""
+    name = models.CharField(_('name'), max_length=64)
+    slug = models.SlugField(_('slug'), unique=True, max_length=64, 
+            null=True, blank=True)
+    ordering = models.IntegerField(_('ordering'))
+
+    _object_cache = {}
+
+    class Meta:
+        abstract = True
+        ordering = ['ordering']
+        verbose_name = _("tag")
+        verbose_name_plural = _("tags")
+
+    def __unicode__(self):
+        return self.name
+
+    @classmethod
+    def get(cls, slug):
+        if slug in cls._object_cache:
+            return cls._object_cache[slug]
+        else:
+            obj = cls.objects.get(slug=slug)
+            cls._object_cache[slug] = obj
+            return obj
+
+    @staticmethod
+    def listener_changed(sender, instance, **kwargs):
+        sender._object_cache = {}
+
+    def next(self):
+        """
+            Returns the next tag - stage to work on.
+            Returns None for the last stage.
+        """
+        try:
+            return type(self).objects.filter(ordering__gt=self.ordering)[0]
+        except IndexError:
+            return None
+
+models.signals.pre_save.connect(Tag.listener_changed, sender=Tag)
+
+
+def data_upload_to(instance, filename):
+    return "%d/%d" % (instance.tree.pk, instance.pk)
 
 class Change(models.Model):
     """
@@ -10,142 +67,273 @@ class Change(models.Model):
         argument points to the version against which this change has been 
         recorded. Initial text will have a null parent.
         
-        Data contains a pickled diff needed to reproduce the initial document.
+        Data file contains a gzipped text of the document.
     """
-    author = models.ForeignKey(User, null=True, blank=True) 
-    patch = models.TextField(blank=True)
-    tree = models.ForeignKey('Document')
+    author = models.ForeignKey(User, null=True, blank=True, verbose_name=_('author'))
+    author_name = models.CharField(_('author name'), max_length=128,
+                        null=True, blank=True,
+                        help_text=_("Used if author is not set.")
+                        )
+    author_email = models.CharField(_('author email'), max_length=128,
+                        null=True, blank=True,
+                        help_text=_("Used if author is not set.")
+                        )
+    revision = models.IntegerField(_('revision'), db_index=True)
 
     parent = models.ForeignKey('self',
                         null=True, blank=True, default=None,
+                        verbose_name=_('parent'),
                         related_name="children")
 
     merge_parent = models.ForeignKey('self',
                         null=True, blank=True, default=None,
+                        verbose_name=_('merge parent'),
                         related_name="merge_children")
 
-    description = models.TextField(blank=True, default='')
-    created_at = models.DateTimeField(auto_now_add=True)
+    description = models.TextField(_('description'), blank=True, default='')
+    created_at = models.DateTimeField(editable=False, db_index=True, 
+                        default=datetime.now)
+    publishable = models.BooleanField(_('publishable'), default=False)
 
     class Meta:
+        abstract = True
         ordering = ('created_at',)
+        unique_together = ['tree', 'revision']
+        verbose_name = _("change")
+        verbose_name_plural = _("changes")
 
     def __unicode__(self):
-        return u"Id: %r, Tree %r, Parent %r, Patch '''\n%s'''" % (self.id, self.tree_id, self.parent_id, self.patch)
+        return u"Id: %r, Tree %r, Parent %r, Data: %s" % (self.id, self.tree_id, self.parent_id, self.data)
+
+    def author_str(self):
+        if self.author:
+            return "%s %s <%s>" % (
+                self.author.first_name,
+                self.author.last_name, 
+                self.author.email)
+        else:
+            return "%s <%s>" % (
+                self.author_name,
+                self.author_email
+                )
 
-    @staticmethod
-    def make_patch(src, dst):
-        if isinstance(src, unicode):
-            src = src.encode('utf-8')
-        if isinstance(dst, unicode):
-            dst = dst.encode('utf-8')
-        return pickle.dumps(mdiff.textdiff(src, dst))
+
+    def save(self, *args, **kwargs):
+        """
+            take the next available revision number if none yet
+        """
+        if self.revision is None:
+            tree_rev = self.tree.revision()
+            if tree_rev is None:
+                self.revision = 1
+            else:
+                self.revision = tree_rev + 1
+        return super(Change, self).save(*args, **kwargs)
 
     def materialize(self):
-        changes = Change.objects.exclude(parent=None).filter(
-                        tree=self.tree,
-                        created_at__lte=self.created_at).order_by('created_at')
-        text = u''
-        for change in changes:
-            text = change.apply_to(text)
-        return text
-
-    def make_child(self, patch, author, description):
-        return self.children.create(patch=patch,
-                        tree=self.tree, author=author,
-                        description=description)
-
-    def make_merge_child(self, patch, author, description):
-        return self.merge_children.create(patch=patch,
-                        tree=self.tree, author=author,
-                        description=description)
-
-    def apply_to(self, text):
-        return mdiff.patch(text, pickle.loads(self.patch.encode('ascii')))
-
-    def merge_with(self, other, author, description=u"Automatic merge."):
+        f = self.data.storage.open(self.data)
+        text = f.read()
+        f.close()
+        return unicode(text, 'utf-8')
+
+    def merge_with(self, other, author=None, 
+            author_name=None, author_email=None, 
+            description=u"Automatic merge."):
+        """Performs an automatic merge after straying commits."""
         assert self.tree_id == other.tree_id  # same tree
         if other.parent_id == self.pk:
-            # immediate child 
+            # immediate child - fast forward
             return other
 
-        local = self.materialize()
-        base = other.merge_parent.materialize()
-        remote = other.apply_to(base)
+        local = self.materialize().encode('utf-8')
+        base = other.parent.materialize().encode('utf-8')
+        remote = other.materialize().encode('utf-8')
 
         merge = simplemerge.Merge3Text(base, local, remote)
         result = ''.join(merge.merge_lines())
-        patch = self.make_patch(local, result)
-        return self.children.create(
-                    patch=patch, merge_parent=other, tree=self.tree,
-                    author=author, description=description)
+        merge_node = self.children.create(
+                    merge_parent=other, tree=self.tree,
+                    author=author,
+                    author_name=author_name,
+                    author_email=author_email,
+                    description=description)
+        merge_node.data.save('', ContentFile(result))
+        return merge_node
 
+    def revert(self, **kwargs):
+        """ commit this version of a doc as new head """
+        self.tree.commit(text=self.materialize(), **kwargs)
 
-class Document(models.Model):
-    """
-        File in repository.        
-    """
-    creator = models.ForeignKey(User, null=True, blank=True)
-    head = models.ForeignKey(Change,
+    def set_publishable(self, publishable):
+        self.publishable = publishable
+        self.save()
+        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
+
+    attrs = {
+        '__module__': model.__module__,
+        'Meta': Meta,
+    }
+    return type(name, (Tag,), attrs)
+
+
+def create_change_model(model):
+    name = model.__name__ + 'Change'
+    repo = GzipFileSystemStorage(location=model.REPO_PATH)
+
+    class Meta(Change.Meta):
+        app_label = model._meta.app_label
+
+    attrs = {
+        '__module__': model.__module__,
+        'tree': models.ForeignKey(model, related_name='change_set', verbose_name=_('document')),
+        'tags': models.ManyToManyField(model.tag_model, verbose_name=_('tags'), related_name='change_set'),
+        'data': models.FileField(_('data'), upload_to=data_upload_to, storage=repo),
+        'Meta': Meta,
+    }
+    return type(name, (Change,), attrs)
+
+
+class DocumentMeta(ModelBase):
+    "Metaclass for Document models."
+    def __new__(cls, name, bases, attrs):
+
+        model = super(DocumentMeta, cls).__new__(cls, name, bases, attrs)
+        if not model._meta.abstract:
+            # create a real Tag object and `stage' fk
+            model.tag_model = create_tag_model(model)
+            models.ForeignKey(model.tag_model, verbose_name=_('stage'),
+                null=True, blank=True).contribute_to_class(model, 'stage')
+
+            # create real Change model and `head' fk
+            model.change_model = create_change_model(model)
+
+            models.ForeignKey(model.change_model,
                     null=True, blank=True, default=None,
-                    help_text=_("This document's current head."))
+                    verbose_name=_('head'), 
+                    help_text=_("This document's current head."),
+                    editable=False).contribute_to_class(model, 'head')
+
+            models.ForeignKey(User, null=True, blank=True, editable=False,
+                verbose_name=_('creator'), related_name="created_%s" % name.lower()
+                ).contribute_to_class(model, 'creator')
+
+        return model
+
+
+class Document(models.Model):
+    """File in repository. Subclass it to use version control in your app."""
+
+    __metaclass__ = DocumentMeta
+
+    # default repository path
+    REPO_PATH = os.path.join(settings.MEDIA_ROOT, 'dvcs')
+
+    user = models.ForeignKey(User, null=True, blank=True,
+        verbose_name=_('user'), help_text=_('Work assignment.'))
+
+    class Meta:
+        abstract = True
 
     def __unicode__(self):
         return u"{0}, HEAD: {1}".format(self.id, self.head_id)
 
-    @models.permalink
-    def get_absolute_url(self):
-        return ('dvcs.views.document_data', (), {
-                        'document_id': self.id,
-                        'version': self.head_id,
-        })
-
-    def materialize(self, version=None):
+    def materialize(self, change=None):
         if self.head is None:
             return u''
-        if version is None:
-            version = self.head
-        elif not isinstance(version, Change):
-            version = self.change_set.get(pk=version)
-        return version.materialize()
+        if change is None:
+            change = self.head
+        elif not isinstance(change, Change):
+            change = self.change_set.get(pk=change)
+        return change.materialize()
+
+    def commit(self, text, author=None, author_name=None, author_email=None,
+            publishable=False, **kwargs):
+        """Commits a new revision.
+
+        This will automatically merge the commit into the main branch,
+        if parent is not document's head.
 
-    def commit(self, **kwargs):
+        :param unicode text: new version of the document
+        :param parent: parent revision (head, if not specified)
+        :type parent: Change or None
+        :param User author: the commiter
+        :param unicode author_name: commiter name (if ``author`` not specified)
+        :param unicode author_email: commiter e-mail (if ``author`` not specified)
+        :param Tag[] tags: list of tags to apply to the new commit
+        :param bool publishable: set new commit as ready to publish
+        :returns: new head
+        """
         if 'parent' not in kwargs:
             parent = self.head
         else:
             parent = kwargs['parent']
-            if not isinstance(parent, Change):
-                parent = Change.objects.get(pk=kwargs['parent'])
+            if parent is not None and not isinstance(parent, Change):
+                parent = self.change_set.objects.get(pk=kwargs['parent'])
 
-        if 'patch' not in kwargs:
-            if 'text' not in kwargs:
-                raise ValueError("You must provide either patch or target document.")
-            patch = Change.make_patch(self.materialize(version=parent), kwargs['text'])
-        else:
-            if 'text' in kwargs:
-                raise ValueError("You can provide only text or patch - not both")
-            patch = kwargs['patch']
-
-        old_head = self.head
-        if parent != old_head:
-            change = parent.make_merge_child(patch, kwargs['author'], kwargs.get('description', ''))
-            # not Fast-Forward - perform a merge
-            self.head = old_head.merge_with(change, author=kwargs['author'])
+        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()
+
+        change = self.change_set.create(author=author,
+                    author_name=author_name,
+                    author_email=author_email,
+                    description=kwargs.get('description', ''),
+                    publishable=publishable,
+                    parent=parent)
+
+        change.tags = tags
+        change.data.save('', ContentFile(text.encode('utf-8')))
+        change.save()
+
+        if self.head:
+            # merge new change as new head
+            self.head = self.head.merge_with(change, author=author,
+                    author_name=author_name,
+                    author_email=author_email)
         else:
-            self.head = parent.make_child(patch, kwargs['author'], kwargs.get('description', ''))
+            self.head = change
         self.save()
+
+        post_commit.send(sender=self.head)
+
         return self.head
 
     def history(self):
-        return self.changes.all()
+        return self.change_set.all().order_by('revision')
 
-    @staticmethod
-    def listener_initial_commit(sender, instance, created, **kwargs):
-        if created:
-            instance.head = Change.objects.create(
-                    author=instance.creator,
-                    patch=pickle.dumps(mdiff.textdiff('', '')),
-                    tree=instance)
-            instance.save()
-
-models.signals.post_save.connect(Document.listener_initial_commit, sender=Document)
+    def revision(self):
+        rev = self.change_set.aggregate(
+                models.Max('revision'))['revision__max']
+        return rev
+
+    def at_revision(self, rev):
+        """Returns a Change with given revision number."""
+        return self.change_set.get(revision=rev)
+
+    def publishable(self):
+        changes = self.history().filter(publishable=True)
+        if changes.exists():
+            return changes.order_by('-revision')[0]
+        else:
+            return None
+
+    @transaction.commit_on_success
+    def prepend_history(self, other):
+        """Takes over the the other document's history and prepends to own."""
+
+        assert self != other
+        other_revs = other.change_set.all().count()
+        # workaround for a non-atomic UPDATE in SQLITE
+        self.change_set.all().update(revision=0-models.F('revision'))
+        self.change_set.all().update(revision=other_revs - models.F('revision'))
+        other.change_set.all().update(tree=self)
+        assert not other.change_set.exists()
+        other.delete()
diff --git a/apps/dvcs/signals.py b/apps/dvcs/signals.py
new file mode 100755 (executable)
index 0000000..5da075b
--- /dev/null
@@ -0,0 +1,4 @@
+from django.dispatch import Signal
+
+post_commit = Signal()
+post_publishable = Signal(providing_args=['publishable'])
diff --git a/apps/dvcs/storage.py b/apps/dvcs/storage.py
new file mode 100755 (executable)
index 0000000..6bb5b59
--- /dev/null
@@ -0,0 +1,18 @@
+from zlib import compress, decompress
+
+from django.core.files.base import ContentFile, File
+from django.core.files.storage import FileSystemStorage
+
+
+class GzipFileSystemStorage(FileSystemStorage):
+    def _open(self, name, mode='rb'):
+        """TODO: This is good for reading; what about writing?"""
+        f = open(self.path(name), 'rb')
+        text = f.read()
+        f.close()
+        return ContentFile(decompress(text))
+
+    def _save(self, name, content):
+        content = ContentFile(compress(content.read()))
+
+        return super(GzipFileSystemStorage, self)._save(name, content)
diff --git a/apps/dvcs/tests/__init__.py b/apps/dvcs/tests/__init__.py
new file mode 100755 (executable)
index 0000000..868f00a
--- /dev/null
@@ -0,0 +1,178 @@
+from nose.tools import *
+from django.test import TestCase
+from dvcs.models import Document
+
+
+class ADocument(Document):
+    class Meta:
+        app_label = 'dvcs'
+
+
+class DocumentModelTests(TestCase):
+
+    def assertTextEqual(self, given, expected):
+        return self.assertEqual(given, expected,
+            "Expected '''%s'''\n differs from text: '''%s'''" % (expected, given)
+        )
+
+    def test_empty_file(self):
+        doc = ADocument.objects.create()
+        self.assertTextEqual(doc.materialize(), u"")
+
+    def test_single_commit(self):
+        doc = ADocument.objects.create()
+        doc.commit(text=u"Ala ma kota", description="Commit #1")
+        self.assertTextEqual(doc.materialize(), u"Ala ma kota")
+
+    def test_chained_commits(self):
+        doc = ADocument.objects.create()
+        text1 = u"""
+            Line #1
+            Line #2 is cool
+        """
+        text2 = u"""
+            Line #1
+            Line #2 is hot
+        """
+        text3 = u"""
+            Line #1
+            ... is hot
+            Line #3 ate Line #2
+        """
+
+        c1 = doc.commit(description="Commit #1", text=text1)
+        c2 = doc.commit(description="Commit #2", text=text2)
+        c3 = doc.commit(description="Commit #3", text=text3)
+
+        self.assertTextEqual(doc.materialize(), text3)
+        self.assertTextEqual(doc.materialize(change=c3), text3)
+        self.assertTextEqual(doc.materialize(change=c2), text2)
+        self.assertTextEqual(doc.materialize(change=c1), text1)
+
+    def test_parallel_commit_noconflict(self):
+        doc = ADocument.objects.create()
+        text1 = u"""
+            Line #1
+            Line #2
+        """
+        text2 = u"""
+            Line #1 is hot
+            Line #2
+        """
+        text3 = u"""
+            Line #1
+            Line #2
+            Line #3
+        """
+        text_merged = u"""
+            Line #1 is hot
+            Line #2
+            Line #3
+        """
+
+        base = doc.commit(description="Commit #1", text=text1)
+        c1 = doc.commit(description="Commit #2", text=text2)
+        commits = doc.change_set.count()
+        c2 = doc.commit(description="Commit #3", text=text3, parent=base)
+        self.assertEqual(doc.change_set.count(), commits + 2,
+            u"Parallel commits should create an additional merge commit")
+        self.assertTextEqual(doc.materialize(), text_merged)
+
+    def test_parallel_commit_conflict(self):
+        doc = ADocument.objects.create()
+        text1 = u"""
+            Line #1
+            Line #2
+            Line #3
+        """
+        text2 = u"""
+            Line #1
+            Line #2 is hot
+            Line #3
+        """
+        text3 = u"""
+            Line #1
+            Line #2 is cool
+            Line #3
+        """
+        text_merged = u"""
+            Line #1
+<<<<<<<
+            Line #2 is hot
+=======
+            Line #2 is cool
+>>>>>>>
+            Line #3
+        """
+        base = doc.commit(description="Commit #1", text=text1)
+        c1 = doc.commit(description="Commit #2", text=text2)
+        commits = doc.change_set.count()
+        c2 = doc.commit(description="Commit #3", text=text3, parent=base)
+        self.assertEqual(doc.change_set.count(), commits + 2,
+            u"Parallel commits should create an additional merge commit")
+        self.assertTextEqual(doc.materialize(), text_merged)
+
+
+    def test_multiple_parallel_commits(self):
+        text_a1 = u"""
+            Line #1
+
+            Line #2
+
+            Line #3
+            """
+        text_a2 = u"""
+            Line #1 *
+
+            Line #2
+
+            Line #3
+            """
+        text_b1 = u"""
+            Line #1
+
+            Line #2 **
+
+            Line #3
+            """
+        text_c1 = u"""
+            Line #1
+
+            Line #2
+
+            Line #3 ***
+            """
+        text_merged = u"""
+            Line #1 *
+
+            Line #2 **
+
+            Line #3 ***
+            """
+
+
+        doc = ADocument.objects.create()
+        c1 = doc.commit(description="Commit A1", text=text_a1)
+        c2 = doc.commit(description="Commit A2", text=text_a2, parent=c1)
+        c3 = doc.commit(description="Commit B1", text=text_b1, parent=c1)
+        c4 = doc.commit(description="Commit C1", text=text_c1, parent=c1)
+        self.assertTextEqual(doc.materialize(), text_merged)
+
+
+    def test_prepend_history(self):
+        doc1 = ADocument.objects.create()
+        doc2 = ADocument.objects.create()
+        doc1.commit(text='Commit 1')
+        doc2.commit(text='Commit 2')
+        doc2.prepend_history(doc1)
+        self.assertEqual(ADocument.objects.all().count(), 1)
+        self.assertTextEqual(doc2.at_revision(1).materialize(), 'Commit 1')
+        self.assertTextEqual(doc2.materialize(), 'Commit 2')
+
+    def test_prepend_to_self(self):
+        doc = ADocument.objects.create()
+        doc.commit(text='Commit 1')
+        with self.assertRaises(AssertionError):
+            doc.prepend_history(doc)
+        self.assertTextEqual(doc.materialize(), 'Commit 1')
+
diff --git a/apps/email_mangler/__init__.py b/apps/email_mangler/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/email_mangler/locale/pl/LC_MESSAGES/django.mo b/apps/email_mangler/locale/pl/LC_MESSAGES/django.mo
new file mode 100644 (file)
index 0000000..ed20bfb
Binary files /dev/null and b/apps/email_mangler/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/apps/email_mangler/locale/pl/LC_MESSAGES/django.po b/apps/email_mangler/locale/pl/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..046b883
--- /dev/null
@@ -0,0 +1,27 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-11-30 14:27+0100\n"
+"PO-Revision-Date: 2011-11-30 14:27+0100\n"
+"Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
+
+#: templatetags/email.py:17
+msgid "at"
+msgstr "na"
+
+#: templatetags/email.py:18
+msgid "dot"
+msgstr "kropka"
+
diff --git a/apps/email_mangler/models.py b/apps/email_mangler/models.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/email_mangler/templatetags/__init__.py b/apps/email_mangler/templatetags/__init__.py
new file mode 100755 (executable)
index 0000000..e69de29
diff --git a/apps/email_mangler/templatetags/email.py b/apps/email_mangler/templatetags/email.py
new file mode 100755 (executable)
index 0000000..376117a
--- /dev/null
@@ -0,0 +1,25 @@
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext as _
+from django import template
+
+register = template.Library()
+
+
+@register.filter
+def email_link(email):
+    email_safe = escape(email)
+    try:
+        name, domain = email_safe.split('@', 1)
+    except ValueError:
+        return email
+
+    at = escape(_('at'))
+    dot = escape(_('dot'))
+    mangled = "%s %s %s" % (name, at, (' %s ' % dot).join(domain.split('.')))
+    return mark_safe("<a class='mangled' data-addr1='%(name)s' "
+        "data-addr2='%(domain)s'>%(mangled)s</a>" % {
+            'name': name.encode('rot13'),
+            'domain': domain.encode('rot13'),
+            'mangled': mangled,
+        })
index 2a4466f..c0320df 100644 (file)
@@ -34,6 +34,7 @@
 {% block content %}
 <div id="content-main">
     <form action="{% query_string %}" method="post">
+    {% csrf_token %}
     <div>
         {% if form.errors %}<p class="errornote">{% trans 'Please correct the following errors.' %}</p>{% endif %}
         <fieldset class="module aligned">
@@ -59,4 +60,4 @@
     </div>
     </form>
 </div>
-{% endblock %}
\ No newline at end of file
+{% endblock %}
index 4c12830..19e63f9 100644 (file)
@@ -34,6 +34,7 @@
 {% block content %}
 <div id="content-main">
     <form action="{% query_string "" "filter_date,filter_type,q" %}" method="post">
+    {% csrf_token %}
     <div>
         {% if form.errors %}<p class="errornote">{% trans 'Please correct the following errors.' %}</p>{% endif %}
         <fieldset class="module aligned">
@@ -60,4 +61,4 @@
     </div>
     </form>
 </div>
-{% endblock %}
\ No newline at end of file
+{% endblock %}
index 7c2967a..6c1c92d 100644 (file)
@@ -15,6 +15,7 @@ from django import forms
 from django.core.urlresolvers import reverse
 from django.core.exceptions import ImproperlyConfigured
 from django.dispatch import Signal
+from django.views.decorators.csrf import csrf_exempt
 
 from django.utils.encoding import smart_unicode, smart_str
 
@@ -186,6 +187,7 @@ def mkdir(request):
 mkdir = staff_member_required(never_cache(mkdir))
 
 
+@csrf_exempt
 def upload(request):
     """
     Multipe File Upload.
@@ -217,6 +219,7 @@ def upload(request):
 upload = staff_member_required(never_cache(upload))
 
 
+@csrf_exempt
 def _check_file(request):
     """
     Check if file already exists on the server.
@@ -272,7 +275,7 @@ def _upload_file(request):
             # POST UPLOAD SIGNAL
             filebrowser_post_upload.send(sender=request, path=request.POST.get('folder'), file=FileObject(os.path.join(DIRECTORY, folder, filedata.name)))
     return HttpResponse('True')
-_upload_file = flash_login_required(_upload_file)
+_upload_file = csrf_exempt(flash_login_required(_upload_file))
 
 
 # delete signals
diff --git a/apps/toolbar/fixtures/initial_data.yaml b/apps/toolbar/fixtures/initial_data.yaml
deleted file mode 100644 (file)
index 21feb1f..0000000
+++ /dev/null
@@ -1,983 +0,0 @@
--   fields: {name: Akapity, position: 0, slug: akapity}
-    model: toolbar.buttongroup
-    pk: 14
--   fields: {name: Autokorekta, position: 0, slug: autokorekta}
-    model: toolbar.buttongroup
-    pk: 2
--   fields: {name: Autotagowanie, position: 0, slug: autotagowanie}
-    model: toolbar.buttongroup
-    pk: 28
--   fields: {name: Bloki, position: 0, slug: bloki}
-    model: toolbar.buttongroup
-    pk: 21
--   fields: {name: 'Dramat ', position: 0, slug: dramat}
-    model: toolbar.buttongroup
-    pk: 12
--   fields: {name: "Elementy pocz\u0105tkowe", position: 0, slug: elementy-poczatkowe}
-    model: toolbar.buttongroup
-    pk: 13
--   fields: {name: Mastery, position: 0, slug: mastery}
-    model: toolbar.buttongroup
-    pk: 11
--   fields: {name: "Nag\u0142\xF3wki", position: 0, slug: naglowki}
-    model: toolbar.buttongroup
-    pk: 1
--   fields: {name: "Pocz\u0105tek dramatu", position: 0, slug: poczatek-dramatu}
-    model: toolbar.buttongroup
-    pk: 22
--   fields: {name: Przypisy, position: 0, slug: przypisy}
-    model: toolbar.buttongroup
-    pk: 26
--   fields: {name: Separatory, position: 0, slug: separatory}
-    model: toolbar.buttongroup
-    pk: 16
--   fields: {name: Style znakowe, position: 0, slug: style-znakowe}
-    model: toolbar.buttongroup
-    pk: 15
--   fields: {name: Uwaga, position: 0, slug: uwaga}
-    model: toolbar.buttongroup
-    pk: 29
--   fields: {name: Wersy, position: 0, slug: wersy}
-    model: toolbar.buttongroup
-    pk: 17
--   fields:
-        accesskey: a
-        group: [14, 12]
-        label: akapit
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "akap"}'
-        scriptlet: insert_tag
-        slug: akapit
-        tooltip: wstawia akapit
-    model: toolbar.button
-    pk: 39
--   fields:
-        accesskey: ''
-        group: [14]
-        label: akapit cd.
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "akap_cd"}'
-        scriptlet: insert_tag
-        slug: akapit-cd
-        tooltip: "ci\u0105g dalszy akapitu po wewn\u0105trzakapitowym wtr\u0105ceniu"
-    model: toolbar.button
-    pk: 40
--   fields:
-        accesskey: d
-        group: [14]
-        label: akapit dialogowy
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "akap_dialog"}'
-        scriptlet: insert_tag
-        slug: akapit-dialogowy
-        tooltip: wstawia akapit dialogowy
-    model: toolbar.button
-    pk: 41
--   fields:
-        accesskey: ''
-        group: [28]
-        label: akapity
-        link: ''
-        params: '{"tag": "akap"}'
-        scriptlet: autotag
-        slug: akapity
-        tooltip: "autotagowanie akapit\xF3w"
-    model: toolbar.button
-    pk: 97
--   fields:
-        accesskey: ''
-        group: [1]
-        label: akt
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "naglowek_akt"}'
-        scriptlet: insert_tag
-        slug: akt
-        tooltip: ''
-    model: toolbar.button
-    pk: 14
--   fields:
-        accesskey: ''
-        group: [13]
-        label: autor
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 2, "tag": "autor_utworu"}'
-        scriptlet: insert_tag
-        slug: autor
-        tooltip: ''
-    model: toolbar.button
-    pk: 32
--   fields:
-        accesskey: ''
-        group: [2]
-        label: Podstawowa
-        link: ''
-        params: '[["fulltextregexp", {"exprs": [["\ufeff", ""], ["$[\\s]*\\d+[\\s]*^",
-            ""], ["-\\s*^", ""], ["\\,\\.\\.|\\.\\,\\.|\\.\\.\\,", "..."], ["<(/?)P([aert])",
-            "<$1p$2"], ["[\u2014\u2013\u2010-]{2,}|[\u2014\u2013\u2010]+", "---"],
-            ["(\\s)-([^-])", "$1---$2"], ["([^-])-(\\s)", "$1---$2"], ["(\\d)-+(\\d)",
-            "$1--$2"], ["---(\\S)", "--- $1"], ["(\\S)---", "$1 ---"], ["<akap_dialog>\\s*-+\\s*",
-            "<akap_dialog>--- "]]}], ["lineregexp", {"exprs": [["^\\s+|\\s+$", ""],
-            ["\\s+", " "], ["(,,)\\s+", "$1"], ["\\s+(\")", "$1"], ["([^\\.])(\\s*)</p",
-            "$1.$2</p"], ["([\\.:;!\\?])([^\\s\\\\])", "$1 $2"], ["([^\\s])\\s+([\\.:;!\\?])",
-            "$1$2"], ["\\s+,([^,])", ",$1"], ["([^,]),([^\\s\\\\,])", "$1, $2"]]}]]'
-        scriptlet: macro
-        slug: basic_correction
-        tooltip: "Wykonuj\u0119 podstawow\u0105 korekt\u0119 tekstu."
-    model: toolbar.button
-    pk: 4
--   fields:
-        accesskey: ''
-        group: [2]
-        label: "zamiana cudzys\u0142ow\xF3w 1"
-        link: ''
-        params: '{"exprs": [["\u00ab|\u201e", ",,"], ["\u00bb", "\""], ["([^=])\"([\u0104\u0118\u00d3\u0141\u017b\u0179\u0106\u0143\u0105\u017c\u017a\u015b\u0144\u00f3\u0142\u0107\\w])",
-            "$1,,$2"], ["^\"([\u0104\u0118\u00d3\u0141\u017b\u0179\u0106\u0143\u0105\u017c\u017a\u015b\u0144\u00f3\u0142\u0107\\w])",
-            ",,$1"], ["(,,)\\s+|\\s+(\")", "$1"]]}'
-        scriptlet: lineregexp
-        slug: cudzyslow-francuski
-        tooltip: "zamiana \" na ,, oraz  \xABa\xBB na ,,a\""
-    model: toolbar.button
-    pk: 89
--   fields:
-        accesskey: ''
-        group: [2]
-        label: "zamiana cudzys\u0142ow\xF3w 2"
-        link: ''
-        params: '{"exprs": [["\u00bb|\u201e", ",,"], ["\u00ab", "\""], ["([^=])\"([\u0104\u0118\u00d3\u0141\u017b\u0179\u0106\u0143\u0105\u017c\u017a\u015b\u0144\u00f3\u0142\u0107\\w])",
-            "$1,,$2"], ["^\"([\u0104\u0118\u00d3\u0141\u017b\u0179\u0106\u0143\u0105\u017c\u017a\u015b\u0144\u00f3\u0142\u0107\\w])",
-            ",,$1"], ["(,,)\\s+|\\s+(\")", "$1"]]}'
-        scriptlet: lineregexp
-        slug: cudzyslow-niemiecki
-        tooltip: "zamienia \" na ,, oraz \xBBa\xAB na ,,a\""
-    model: toolbar.button
-    pk: 77
--   fields:
-        accesskey: ''
-        group: [1]
-        label: "cz\u0119\u015B\u0107/ksi\u0119ga"
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "naglowek_czesc"}'
-        scriptlet: insert_tag
-        slug: czesc
-        tooltip: ''
-    model: toolbar.button
-    pk: 10
--   fields:
-        accesskey: ''
-        group: [13, 22]
-        label: dedykacja
-        link: ''
-        params: '{"tag": "dedykacja"}'
-        scriptlet: insert_tag
-        slug: dedykacja
-        tooltip: ''
-    model: toolbar.button
-    pk: 74
--   fields:
-        accesskey: ''
-        group: [12]
-        label: didaskalia
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 2, "tag": "didaskalia"}'
-        scriptlet: insert_tag
-        slug: didaskalia
-        tooltip: ''
-    model: toolbar.button
-    pk: 62
--   fields:
-        accesskey: ''
-        group: [22]
-        label: "didaskalia pocz\u0105tkowe"
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "miejsce_czas"}'
-        scriptlet: insert_tag
-        slug: didaskalia-poczatkowe
-        tooltip: "komentarze wprowadzaj\u0105ce przed tekstem dramatu"
-    model: toolbar.button
-    pk: 79
--   fields:
-        accesskey: ''
-        group: [12]
-        label: didaskalia tekstowe
-        link: ''
-        params: '{"tag": "didask_tekst"}'
-        scriptlet: insert_tag
-        slug: didaskalia-tekstowe
-        tooltip: "didaskalia umieszczone w obr\u0119bie innego tekstu"
-    model: toolbar.button
-    pk: 63
--   fields:
-        accesskey: ''
-        group: [21]
-        label: "d\u0142ugi cyt. poet."
-        link: ''
-        params: '{"tag": "poezja_cyt"}'
-        scriptlet: insert_tag
-        slug: dlugi-cyt-poet
-        tooltip: "d\u0142ugi cytat wierszowany wyr\xF3\u017Cniony sk\u0142adem"
-    model: toolbar.button
-    pk: 67
--   fields:
-        accesskey: ''
-        group: [21]
-        label: "d\u0142ugi cytat"
-        link: ''
-        params: '{"tag": "dlugi_cyt"}'
-        scriptlet: insert_tag
-        slug: dlugi-cytat
-        tooltip: "d\u0142ugi cytat wyr\xF3\u017Cniony sk\u0142adem"
-    model: toolbar.button
-    pk: 42
--   fields:
-        accesskey: ''
-        group: [11]
-        label: dramat wiersz.
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "dramat_wierszowany_l"}'
-        scriptlet: insert_tag
-        slug: dramat-wiersz
-        tooltip: ''
-    model: toolbar.button
-    pk: 20
--   fields:
-        accesskey: ''
-        group: [11]
-        label: "dramat wiersz./w. \u0142am"
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "dramat_wierszowany_lp"}'
-        scriptlet: insert_tag
-        slug: dramat-wiersz-w-lam
-        tooltip: "dramat wierszowany o zw\u0119\u017Conej szeroko\u015Bci \u0142amu"
-    model: toolbar.button
-    pk: 22
--   fields:
-        accesskey: ''
-        group: [11]
-        label: "dramat wsp\xF3\u0142czesny"
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "dramat_wspolczesny"}'
-        scriptlet: insert_tag
-        slug: dramat-wspolczesny
-        tooltip: "dramat wsp\xF3\u0142czesny (proz\u0105)"
-    model: toolbar.button
-    pk: 21
--   fields:
-        accesskey: ''
-        group: [13]
-        label: "dzie\u0142o nadrz\u0119dne"
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 2, "tag": "dzielo_nadrzedne"}'
-        scriptlet: insert_tag
-        slug: dzielo-nadrzedne
-        tooltip: ''
-    model: toolbar.button
-    pk: 38
--   fields:
-        accesskey: ''
-        group: []
-        label: extra
-        link: ''
-        params: '{"tag": "extra"}'
-        scriptlet: insert_tag
-        slug: extra
-        tooltip: "uwagi dotycz\u0105ce sk\u0142adu"
-    model: toolbar.button
-    pk: 96
--   fields:
-        accesskey: ''
-        group: []
-        label: Wydrukuj
-        link: print/html
-        params: '[]'
-        scriptlet: insert_tag
-        slug: htmleditor-print
-        tooltip: ''
-    model: toolbar.button
-    pk: 87
--   fields:
-        accesskey: k
-        group: [12]
-        label: kwestia
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 1, "tag": "kwestia"}'
-        scriptlet: insert_tag
-        slug: kwestia
-        tooltip: "wstawia kwesti\u0119"
-    model: toolbar.button
-    pk: 82
--   fields:
-        accesskey: ''
-        group: [12]
-        label: kwestioakapit
-        link: ''
-        params: '[["insert_tag", {"tag": "akap"}], ["insert_tag", {"padding_top":
-            1, "padding_bottom": 1, "tag": "kwestia"}]]'
-        scriptlet: macro
-        slug: kwestioakapit
-        tooltip: ''
-    model: toolbar.button
-    pk: 101
--   fields:
-        accesskey: ''
-        group: [12]
-        label: kwestiostrofa
-        link: ''
-        params: '[["insert_stanza", {"tag": "strofa"}], ["insert_tag", {"padding_top":
-            1, "padding_bottom": 1, "tag": "kwestia"}]]'
-        scriptlet: macro
-        slug: kwestiostrofa
-        tooltip: ''
-    model: toolbar.button
-    pk: 102
--   fields:
-        accesskey: ''
-        group: [11]
-        label: liryka
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "liryka_l"}'
-        scriptlet: insert_tag
-        slug: liryka
-        tooltip: ''
-    model: toolbar.button
-    pk: 23
--   fields:
-        accesskey: ''
-        group: [11]
-        label: "liryka/w. \u0142am"
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "liryka_lp"}'
-        scriptlet: insert_tag
-        slug: liryka-w-lam
-        tooltip: "utw\xF3r liryczny o zw\u0119\u017Conej szeroko\u015Bci \u0142amu"
-    model: toolbar.button
-    pk: 24
--   fields:
-        accesskey: ''
-        group: [22]
-        label: "lista os\xF3b"
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "lista_osob"}'
-        scriptlet: insert_tag
-        slug: lista-osob
-        tooltip: "lista os\xF3b poprzedzaj\u0105ca tekst dramatu"
-    model: toolbar.button
-    pk: 93
--   fields:
-        accesskey: ''
-        group: [22]
-        label: 'typ osoby '
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 1, "tag": "lista_osoba", "attrs":
-            {"typ": ""}}'
-        scriptlet: insert_tag
-        slug: lista-osob-pole
-        tooltip: osoby z takim samym opisem
-    model: toolbar.button
-    pk: 78
--   fields:
-        accesskey: ''
-        group: [15]
-        label: matemat.
-        link: ''
-        params: '{"tag": "mat"}'
-        scriptlet: insert_tag
-        slug: matemat
-        tooltip: "wyra\u017Cenia matematyczne lub zmienne symboliczne"
-    model: toolbar.button
-    pk: 47
--   fields:
-        accesskey: ''
-        group: [13, 22]
-        label: motto
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "motto"}'
-        scriptlet: insert_tag
-        slug: motto
-        tooltip: ''
-    model: toolbar.button
-    pk: 75
--   fields:
-        accesskey: ''
-        group: [13, 22]
-        label: motto podpis
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 2, "tag": "motto_podpis"}'
-        scriptlet: insert_tag
-        slug: motto-podpis
-        tooltip: ''
-    model: toolbar.button
-    pk: 37
--   fields:
-        accesskey: ''
-        group: [2]
-        label: ",,\u2026\" na \xAB\u2026\xBB"
-        link: ''
-        params: '{"exprs": [[",,", "\u00ab"], ["\"", "\u00bb"]]}'
-        scriptlet: fulltextregexp
-        slug: na-francuskie
-        tooltip: "Zamienia cudzys\u0142owy podw\xF3jne na francuskie"
-    model: toolbar.button
-    pk: 2
--   fields:
-        accesskey: ''
-        group: [2]
-        label: ",,\u2026\" na \xBB\u2026\xAB"
-        link: ''
-        params: '{"exprs": [[",,", "\u00bb"], ["\"", "\u00ab"]]}'
-        scriptlet: fulltextregexp
-        slug: na-niemieckie
-        tooltip: "Zamienia cudzys\u0142owy podw\xF3jne na niemieckie"
-    model: toolbar.button
-    pk: 3
--   fields:
-        accesskey: ''
-        group: [28]
-        label: "nag\u0142. dramatu"
-        link: ''
-        params: '{"exprs": [["^AKT(\\s\\w*)$", "<naglowek_akt>AKT$1</naglowek_akt>"],
-            ["^SCENA(\\s\\w*)$", "<naglowek_scena>SCENA$1</naglowek_scena>"], ["([A-Z\u0104\u0106\u0118\u0141\u0143\u00d3\u015a\u017b\u0179]{2}[A-Z\u0104\u0106\u0118\u0141\u0143\u00d3\u015a\u017b\u0179\\s]+)$",
-            "<naglowek_osoba>$1</naglowek_osoba>"]]}'
-        scriptlet: lineregexp
-        slug: nagl-dramatu
-        tooltip: "autotagowanie akt\xF3w, scen, nag\u0142\xF3wk\xF3w os\xF3b"
-    model: toolbar.button
-    pk: 103
--   fields:
-        accesskey: ''
-        group: [12]
-        label: "nag\u0142\xF3wek kwestii"
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "naglowek_osoba"}'
-        scriptlet: insert_tag
-        slug: naglowek-kwestii
-        tooltip: "nag\u0142\xF3wek kwestii - nazwa osoby"
-    model: toolbar.button
-    pk: 16
--   fields:
-        accesskey: ''
-        group: [22]
-        label: "nag\u0142\xF3wek listy"
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 2, "tag": "naglowek_listy"}'
-        scriptlet: insert_tag
-        slug: naglowek-listy
-        tooltip: "nag\u0142\xF3wek listy os\xF3b"
-    model: toolbar.button
-    pk: 94
--   fields:
-        accesskey: ''
-        group: [13]
-        label: nazwa utworu
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 2, "tag": "nazwa_utworu"}'
-        scriptlet: insert_tag
-        slug: nazwa-utworu
-        tooltip: ''
-    model: toolbar.button
-    pk: 33
--   fields:
-        accesskey: ''
-        group: [13]
-        label: nota
-        link: ''
-        params: '{"tag": "nota"}'
-        scriptlet: insert_tag
-        slug: nota
-        tooltip: ''
-    model: toolbar.button
-    pk: 35
--   fields:
-        accesskey: ''
-        group: [13]
-        label: nota red.
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "nota_red"}'
-        scriptlet: insert_tag
-        slug: nota-red
-        tooltip: nota redakcyjna
-    model: toolbar.button
-    pk: 104
--   fields:
-        accesskey: ''
-        group: [11]
-        label: opowiadanie
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "opowiadanie"}'
-        scriptlet: insert_tag
-        slug: opowiadanie
-        tooltip: ''
-    model: toolbar.button
-    pk: 18
--   fields:
-        accesskey: b
-        group: [12]
-        label: osoba
-        link: ''
-        params: '{"tag": "osoba"}'
-        scriptlet: insert_tag
-        slug: osoba
-        tooltip: "wstawia nazw\u0119 osoby w didaskaliach"
-    model: toolbar.button
-    pk: 64
--   fields:
-        accesskey: ''
-        group: [22]
-        label: osoba na liscie
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 1, "tag": "lista_osoba"}'
-        scriptlet: insert_tag
-        slug: osoba-na-liscie
-        tooltip: "nazwa osoby na liscie os\xF3b"
-    model: toolbar.button
-    pk: 95
--   fields:
-        accesskey: ''
-        group: [1]
-        label: "podrozdzia\u0142"
-        link: ''
-        params: '{"tag": "naglowek_podrozdzial"}'
-        scriptlet: insert_tag
-        slug: podrozdzial
-        tooltip: ''
-    model: toolbar.button
-    pk: 12
--   fields:
-        accesskey: ''
-        group: [1]
-        label: "podtytu\u0142"
-        link: ''
-        params: '{"tag": "podtytul"}'
-        scriptlet: insert_tag
-        slug: podtytul
-        tooltip: ''
-    model: toolbar.button
-    pk: 34
--   fields:
-        accesskey: ''
-        group: [11]
-        label: "powie\u015B\u0107"
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "powiesc"}'
-        scriptlet: insert_tag
-        slug: powiesc
-        tooltip: ''
-    model: toolbar.button
-    pk: 19
--   fields:
-        accesskey: ''
-        group: []
-        label: Wydrukuj
-        link: print/xml
-        params: '[]'
-        scriptlet: insert_tag
-        slug: print-xml
-        tooltip: ''
-    model: toolbar.button
-    pk: 86
--   fields:
-        accesskey: ''
-        group: [26]
-        label: przypis autorski
-        link: ''
-        params: '{"tag": "pa"}'
-        scriptlet: insert_tag
-        slug: przypis-autorski
-        tooltip: ''
-    model: toolbar.button
-    pk: 68
--   fields:
-        accesskey: ''
-        group: [26]
-        label: przypis edytorski
-        link: ''
-        params: '{"tag": "pe"}'
-        scriptlet: insert_tag
-        slug: przypis-edytorski
-        tooltip: ''
-    model: toolbar.button
-    pk: 71
--   fields:
-        accesskey: ''
-        group: [26]
-        label: przypis redaktorski
-        link: ''
-        params: '{"tag": "pr"}'
-        scriptlet: insert_tag
-        slug: przypis-redaktorski
-        tooltip: ''
-    model: toolbar.button
-    pk: 70
--   fields:
-        accesskey: ''
-        group: [26]
-        label: "przypis t\u0142umacza"
-        link: ''
-        params: '{"tag": "pt"}'
-        scriptlet: insert_tag
-        slug: przypis-tlumacza
-        tooltip: ''
-    model: toolbar.button
-    pk: 69
--   fields:
-        accesskey: ''
-        group: [1]
-        label: "rozdzia\u0142"
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "naglowek_rozdzial"}'
-        scriptlet: insert_tag
-        slug: rozdzial
-        tooltip: ''
-    model: toolbar.button
-    pk: 11
--   fields:
-        accesskey: ''
-        group: [1]
-        label: scena
-        link: ''
-        params: '{"tag": "naglowek_scena"}'
-        scriptlet: insert_tag
-        slug: scena
-        tooltip: ''
-    model: toolbar.button
-    pk: 15
--   fields:
-        accesskey: ''
-        group: [16]
-        label: asterysk
-        link: ''
-        params: '{"nocontent": "true", "tag": "sekcja_asterysk"}'
-        scriptlet: insert_tag
-        slug: sep-asterysk
-        tooltip: rozdzielenie partii tekstu asteryskiem
-    model: toolbar.button
-    pk: 54
--   fields:
-        accesskey: ''
-        group: [16]
-        label: linia
-        link: ''
-        params: '{"nocontent": "true", "tag": "separator_linia"}'
-        scriptlet: insert_tag
-        slug: sep-linia
-        tooltip: "rozdzielenie partii tekstu pozioma lini\u0105"
-    model: toolbar.button
-    pk: 55
--   fields:
-        accesskey: ''
-        group: [16]
-        label: "\u015Bwiat\u0142o"
-        link: ''
-        params: '{"nocontent": "true", "tag": "sekcja_swiatlo"}'
-        scriptlet: insert_tag
-        slug: sep-swiatlo
-        tooltip: "\u015Bwiat\u0142o rozdzielaj\u0105ce sekcje tekstu"
-    model: toolbar.button
-    pk: 53
--   fields:
-        accesskey: ''
-        group: [15]
-        label: "s\u0142owo obce"
-        link: ''
-        params: '{"tag": "slowo_obce"}'
-        scriptlet: insert_tag
-        slug: slowo-obce
-        tooltip: "frazy w j\u0119zykach innych ni\u017C polski/definiendum w przypisie"
-    model: toolbar.button
-    pk: 46
--   fields:
-        accesskey: ''
-        group: [1]
-        label: "\u015Br\xF3dtytu\u0142"
-        link: ''
-        params: '{"tag": "srodtytul"}'
-        scriptlet: insert_tag
-        slug: srodtytul
-        tooltip: ''
-    model: toolbar.button
-    pk: 13
--   fields:
-        accesskey: s
-        group: [12, 17]
-        label: strofa
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "strofa"}'
-        scriptlet: insert_stanza
-        slug: strofa
-        tooltip: "wstawia strof\u0119"
-    model: toolbar.button
-    pk: 81
--   fields:
-        accesskey: ''
-        group: [28]
-        label: strofy
-        link: ''
-        params: '{"tag": "strofa"}'
-        scriptlet: autotag
-        slug: strofy
-        tooltip: autotagowanie strof
-    model: toolbar.button
-    pk: 99
--   fields:
-        accesskey: ''
-        group: [11]
-        label: "tag g\u0142\xF3wny"
-        link: ''
-        params: '{"tag": "utwor"}'
-        scriptlet: insert_tag
-        slug: tag-glowny
-        tooltip: ''
-    model: toolbar.button
-    pk: 17
--   fields:
-        accesskey: u
-        group: [2]
-        label: "A<sup>\u2193</sup>"
-        link: ''
-        params: '[]'
-        scriptlet: lowercase
-        slug: tolowercase
-        tooltip: "Zamie\u0144 wielkie litery na ma\u0142e"
-    model: toolbar.button
-    pk: 76
--   fields:
-        accesskey: ''
-        group: [15]
-        label: "tytu\u0142 dzie\u0142a"
-        link: ''
-        params: '{"tag": "tytul_dziela"}'
-        scriptlet: insert_tag
-        slug: tytul-dziela
-        tooltip: ''
-    model: toolbar.button
-    pk: 92
--   fields:
-        accesskey: ''
-        group: [15]
-        label: "tytu\u0142 dzie\u0142a typ 1"
-        link: ''
-        params: '{"tag": "tytul_dziela", "attrs": {"typ": "1"}}'
-        scriptlet: insert_tag
-        slug: tytul-dziela-typ
-        tooltip: "tytu\u0142 dzie\u0142a w cytowanym tytule dzie\u0142a"
-    model: toolbar.button
-    pk: 45
--   fields:
-        accesskey: ''
-        group: [29]
-        label: uwaga
-        link: ''
-        params: '{"tag": "uwaga"}'
-        scriptlet: insert_tag
-        slug: uwaga
-        tooltip: 'uwagi redaktorsko-korektorskie '
-    model: toolbar.button
-    pk: 51
--   fields:
-        accesskey: ''
-        group: [14, 17]
-        label: wers akap.
-        link: ''
-        params: '{"tag": "wers_akap"}'
-        scriptlet: insert_tag
-        slug: wers-akap
-        tooltip: "wers rozpoczynaj\u0105cy si\u0119 wci\u0119ciem akapitowym"
-    model: toolbar.button
-    pk: 83
--   fields:
-        accesskey: ''
-        group: [12, 17]
-        label: wers cd.
-        link: ''
-        params: '{"tag": "wers_cd"}'
-        scriptlet: insert_tag
-        slug: wers-cd
-        tooltip: "cz\u0119\u015B\u0107 wersu przeniesiona do innego wiersza"
-    model: toolbar.button
-    pk: 85
--   fields:
-        accesskey: w
-        group: [12, 17]
-        label: "wers mocno wci\u0119ty"
-        link: ''
-        params: '{"tag": "wers_wciety", "attrs": {"typ": ""}}'
-        scriptlet: insert_tag
-        slug: wers-mocno-wciety
-        tooltip: "argumenty wersu wci\u0119tego: od 2 do 6"
-    model: toolbar.button
-    pk: 84
--   fields:
-        accesskey: q
-        group: [12, 17]
-        label: "wers wci\u0119ty"
-        link: ''
-        params: '{"tag": "wers_wciety", "attrs": {"typ": "1"}}'
-        scriptlet: insert_tag
-        slug: wers-wciety
-        tooltip: "wstawia wers wci\u0119ty"
-    model: toolbar.button
-    pk: 91
--   fields:
-        accesskey: ''
-        group: [28]
-        label: "wersy wci\u0119te"
-        link: ''
-        params: '{"padding": 1, "tag": "wers_wciety", "split": 1}'
-        scriptlet: autotag
-        slug: wersy-wciete
-        tooltip: "autotagowanie wers\xF3w wci\u0119tych"
-    model: toolbar.button
-    pk: 100
--   fields:
-        accesskey: ''
-        group: [15]
-        label: www
-        link: ''
-        params: '{"tag": "www"}'
-        scriptlet: insert_tag
-        slug: www
-        tooltip: ''
-    model: toolbar.button
-    pk: 48
--   fields:
-        accesskey: ''
-        group: [12, 15]
-        label: "wyr\xF3\u017Cnienie"
-        link: ''
-        params: '{"tag": "wyroznienie"}'
-        scriptlet: insert_tag
-        slug: wyroznienie
-        tooltip: "wyr\xF3\u017Cnienie autorskie"
-    model: toolbar.button
-    pk: 44
--   fields:
-        accesskey: ''
-        group: [11]
-        label: wywiad
-        link: ''
-        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "wywiad"}'
-        scriptlet: insert_tag
-        slug: wywiad
-        tooltip: ''
-    model: toolbar.button
-    pk: 25
--   fields:
-        accesskey: ''
-        group: [21]
-        label: "wywiad odpowied\u017A"
-        link: ''
-        params: '{"tag": "wywiad_odp"}'
-        scriptlet: insert_tag
-        slug: wywiad-odpowiedz
-        tooltip: ''
-    model: toolbar.button
-    pk: 73
--   fields:
-        accesskey: ''
-        group: [21]
-        label: wywiad pytanie
-        link: ''
-        params: '{"tag": "wywiad_pyt"}'
-        scriptlet: insert_tag
-        slug: wywiad-pytanie
-        tooltip: ''
-    model: toolbar.button
-    pk: 72
--   fields:
-        accesskey: ''
-        group: [16]
-        label: "zast\u0119pnik wersu"
-        link: ''
-        params: '{"tag": "zastepnik_wersu"}'
-        scriptlet: insert_tag
-        slug: zastepnik-wersu
-        tooltip: wykropkowanie wersu
-    model: toolbar.button
-    pk: 56
--   fields: {code: "$(params).each(function() {\n    $.log(this[0], this[1]);\n  \
-            \  editor.callScriptlet(this[0], panel, this[1]);\n\n});"}
-    model: toolbar.scriptlet
-    pk: macro
--   fields: {code: "var texteditor = panel.texteditor;\nvar text = texteditor.selection();\n\
-            var start_tag = '<'+params.tag;\nfor (var attr in params.attrs) {\n  \
-            \  start_tag += ' '+attr+'=\"' + params.attrs[attr] + '\"';\n};\nstart_tag\
-            \ += '>';\nvar end_tag = '</'+params.tag+'>';\n\nif(text.length > 0) {\n\
-            // tokenize\nvar output = ''\nvar token = ''\nfor(var index=0; index <\
-            \ text.length; index++)\n{\n    if (text[index].match(/\\s/)) { // whitespace\n\
-            \        token += text[index];\n    }\n    else { // character\n     \
-            \   output += token;\n        if(output == token) output += start_tag;\n\
-            \        token = ''\n        output += text[index];\n    }\n}\n\nif( output[output.length-1]\
-            \ == '\\\\' ) {\n    output = output.substr(0, output.length-1) + end_tag\
-            \ + '\\\\';\n} else {\n    output += end_tag;\n}\noutput += token;\n}\n\
-            else {\n output = start_tag + end_tag;\n}\n\ntexteditor.replaceSelection(output);\n\
-            \nif (text.length == 0) {\n    var pos = texteditor.cursorPosition();\n\
-            \    texteditor.selectLines(pos.line, pos.character + params.tag.length\
-            \ + 2);\n}\n\npanel.fireEvent('contentChanged');"}
-    model: toolbar.scriptlet
-    pk: insert_tag
--   fields: {code: "editor.showPopup('generic-info', 'Przetwarzanie zaznaczonego tekstu...',\
-            \ '', -1);\n\nvar cm = panel.texteditor;\nvar exprs = $.map(params.exprs,\
-            \ function(expr) {\n\n    var opts = \"g\";\n\n    if(expr.length > 2)\n\
-            \n        opts = expr[2];\n\n    return {rx: new RegExp(expr[0], opts),\
-            \ repl: expr[1]};\n\n});\n\n\n\nvar partial = true;\n\nvar text = cm.selection();\n\
-            \n\n\nif(!text) {\n\n    var cpos = cm.cursorPosition();\n\n    cpos.line\
-            \ = cm.lineNumber(cpos.line)\n\n    cm.selectLines(cm.firstLine(), 0,\
-            \ cm.lastLine(), 0);\n\n    text = cm.selection();\n\n    partial = false;\n\
-            \n}\n\n\n\nvar changed = 0;\nvar lines = text.split('\\n');\nvar lines\
-            \ = $.map(lines, function(line) { \n    var old_line = line;\n    $(exprs).each(function()\
-            \ { \n        var expr = this;\n        line = line.replace(expr.rx, expr.repl);\n\
-            \    });\n\n    if(old_line != line) changed += 1;\n    return line;\n\
-            });\n\nif(changed > 0) \n{\n    cm.replaceSelection( lines.join('\\n')\
-            \ );\n    panel.fireEvent('contentChanged');\n    editor.showPopup('generic-yes',\
-            \ 'Zmieniono ' + changed + ' linii.', 1500);\n    editor.advancePopupQueue();\n\
-            }\nelse {\n    editor.showPopup('generic-info',  'Brak zmian w tek\u015B\
-            cie', 1500);\n    editor.advancePopupQueue();\n}\n\nif(!partial)\n   \
-            \ cm.selectLines( cm.nthLine(cpos.line), cpos.character )"}
-    model: toolbar.scriptlet
-    pk: lineregexp
--   fields: {code: '-'}
-    model: toolbar.scriptlet
-    pk: autotag
--   fields: {code: "editor.showPopup('generic-info', 'Przetwarzanie zaznaczonego tekstu...',\
-            \ '', -1);\n$.log(editor, panel, params);\nvar cm = panel.texteditor;\n\
-            var exprs = $.map(params.exprs, function(expr) {\n    var opts = \"mg\"\
-            ;\n    if(expr.length > 2)\n        opts = expr[2];\n\n    return {rx:\
-            \ new RegExp(expr[0], opts), repl: expr[1]};\n});\n\nvar partial = true;\n\
-            var text = cm.selection();\n\nif(!text) {\n    var cpos = cm.cursorPosition();\n\
-            \    cpos.line = cm.lineNumber(cpos.line)\n    cm.selectLines(cm.firstLine(),\
-            \ 0, cm.lastLine(), 0);\n\n    text = cm.selection();\n    partial = false;\n\
-            }\n\nvar original = text;\n$(exprs).each(function() { \n    text = text.replace(this.rx,\
-            \ this.repl);\n});\n\nif( original != text) \n{    \n    cm.replaceSelection(text);\n\
-            \    panel.fireEvent('contentChanged');\n    editor.showPopup('generic-yes',\
-            \ 'Zmieniono tekst' );\n    editor.advancePopupQueue();\n}\nelse {\n \
-            \   editor.showPopup('generic-info', 'Brak zmian w tek\u015Bcie.');\n\
-            \    editor.advancePopupQueue();\n}\n\nif(!partial) {\n    cm.selectLines(\
-            \ cm.nthLine(cpos.line), cpos.character );\n}"}
-    model: toolbar.scriptlet
-    pk: fulltextregexp
--   fields: {code: "var cm = panel.texteditor;\r\nvar text = cm.selection();\r\n\r\
-            \nif(!text) return;\r\nvar repl = '';\r\nvar lcase = text.toLowerCase();\r\
-            \nvar ucase = text.toUpperCase();\r\n\r\nif(lcase == text) repl = ucase;\
-            \ /* was lowercase */\r\nelse if(ucase != text) repl = lcase; /* neither\
-            \ lower- or upper-case */\r\nelse { /* upper case -> title-case */\r\n\
-            \   var words = $(lcase.split(/\\s/)).map(function() { \r\n        if(this.length\
-            \ > 0) { return this[0].toUpperCase() + this.slice(1); } else { return\
-            \ ''}\r\n   }); \r\n   repl = words.join(' ');\r\n} \r\n\r\nif(repl !=\
-            \ text) {\r\n    cm.replaceSelection(repl);\r\n    panel.fireEvent('contentChanged');\r\
-            \n};"}
-    model: toolbar.scriptlet
-    pk: lowercase
--   fields: {code: "var texteditor = panel.texteditor;\r\nvar text = texteditor.selection();\r\
-            \n\r\nif(text) {\r\n  var verses = text.split('\\n');\r\n  var text =\
-            \ ''; var buf = ''; var ebuf = '';\r\n  var first = true;\r\n\r\n  for(var\
-            \ i=0;  i < verses.length; i++) {\r\n    verse = verses[i].replace(/^\\\
-            s+/, \"\").replace(/\\s+$/, \"\");   \r\n    if(verse) {\r\n      text\
-            \ += (buf ? buf + '/\\n' : '') + ebuf;\r\n      buf = (first ? '<strofa>\\\
-            n' : '') + verses[i];\r\n      ebuf = '';\r\n      first = false;\r\n\
-            \    } else {    \r\n      ebuf += '\\n' + verses[i];\r\n    }\r\n  };\r\
-            \n  text = text + buf + '\\n</strofa>' + ebuf; \r\n  texteditor.replaceSelection(text);\r\
-            \n}\r\n\r\nif (!text) {\r\n    var pos = texteditor.cursorPosition();\r\
-            \n    texteditor.selectLines(pos.line, pos.character + 6 + 2);\r\n}\r\n\
-            \r\n\r\n\r\n\r\n\r\n\r\n\r\npanel.fireEvent('contentChanged');"}
-    model: toolbar.scriptlet
-    pk: insert_stanza
-
diff --git a/apps/toolbar/fixtures/initial_toolbar.yaml b/apps/toolbar/fixtures/initial_toolbar.yaml
new file mode 100644 (file)
index 0000000..c2fb84d
--- /dev/null
@@ -0,0 +1,1021 @@
+-   fields: {name: Akapity, position: 0, slug: akapity}
+    model: toolbar.buttongroup
+    pk: 14
+-   fields: {name: Autokorekta, position: 0, slug: autokorekta}
+    model: toolbar.buttongroup
+    pk: 2
+-   fields: {name: Autotagowanie, position: 0, slug: autotagowanie}
+    model: toolbar.buttongroup
+    pk: 28
+-   fields: {name: Bloki, position: 0, slug: bloki}
+    model: toolbar.buttongroup
+    pk: 21
+-   fields: {name: 'Dramat ', position: 0, slug: dramat}
+    model: toolbar.buttongroup
+    pk: 12
+-   fields: {name: "Elementy pocz\u0105tkowe", position: 0, slug: elementy-poczatkowe}
+    model: toolbar.buttongroup
+    pk: 13
+-   fields: {name: Mastery, position: 0, slug: mastery}
+    model: toolbar.buttongroup
+    pk: 11
+-   fields: {name: "Nag\u0142\xF3wki", position: 0, slug: naglowki}
+    model: toolbar.buttongroup
+    pk: 1
+-   fields: {name: "Pocz\u0105tek dramatu", position: 0, slug: poczatek-dramatu}
+    model: toolbar.buttongroup
+    pk: 22
+-   fields: {name: Przypisy, position: 0, slug: przypisy}
+    model: toolbar.buttongroup
+    pk: 26
+-   fields: {name: Separatory, position: 0, slug: separatory}
+    model: toolbar.buttongroup
+    pk: 16
+-   fields: {name: Style znakowe, position: 0, slug: style-znakowe}
+    model: toolbar.buttongroup
+    pk: 15
+-   fields: {name: Uwaga, position: 0, slug: uwaga}
+    model: toolbar.buttongroup
+    pk: 29
+-   fields: {name: Wersy, position: 0, slug: wersy}
+    model: toolbar.buttongroup
+    pk: 17
+-   fields:
+        accesskey: a
+        group: [14, 12]
+        label: akapit
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "akap"}'
+        scriptlet: insert_tag
+        slug: akapit
+        tooltip: wstawia akapit
+    model: toolbar.button
+    pk: 39
+-   fields:
+        accesskey: ''
+        group: [14]
+        label: akapit cd.
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "akap_cd"}'
+        scriptlet: insert_tag
+        slug: akapit-cd
+        tooltip: "ci\u0105g dalszy akapitu po wewn\u0105trzakapitowym wtr\u0105ceniu"
+    model: toolbar.button
+    pk: 40
+-   fields:
+        accesskey: d
+        group: [14]
+        label: akapit dialogowy
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "akap_dialog"}'
+        scriptlet: insert_tag
+        slug: akapit-dialogowy
+        tooltip: wstawia akapit dialogowy
+    model: toolbar.button
+    pk: 41
+-   fields:
+        accesskey: ''
+        group: [28]
+        label: akapity
+        link: ''
+        params: '{"tag": "akap"}'
+        scriptlet: autotag
+        slug: akapity
+        tooltip: "autotagowanie akapit\xF3w"
+    model: toolbar.button
+    pk: 97
+-   fields:
+        accesskey: ''
+        group: [1]
+        label: akt
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "naglowek_akt"}'
+        scriptlet: insert_tag
+        slug: akt
+        tooltip: ''
+    model: toolbar.button
+    pk: 14
+-   fields:
+        accesskey: ''
+        group: [13]
+        label: autor
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 2, "tag": "autor_utworu"}'
+        scriptlet: insert_tag
+        slug: autor
+        tooltip: ''
+    model: toolbar.button
+    pk: 32
+-   fields:
+        accesskey: ''
+        group: [2]
+        label: Podstawowa
+        link: ''
+        params: '[["fulltextregexp", {"exprs": [["\ufeff", ""], ["$[\\s]*\\d+[\\s]*^",
+            ""], ["-\\s*^", ""], ["\\,\\.\\.|\\.\\,\\.|\\.\\.\\,", "..."], ["<(/?)P([aert])",
+            "<$1p$2"], ["[\u2014\u2013\u2010-]{2,}|[\u2014\u2013\u2010]+", "---"],
+            ["(\\s)-([^-])", "$1---$2"], ["([^-])-(\\s)", "$1---$2"], ["(\\d)-+(\\d)",
+            "$1--$2"], ["---(\\S)", "--- $1"], ["(\\S)---", "$1 ---"], ["<akap_dialog>\\s*-+\\s*",
+            "<akap_dialog>--- "]]}], ["lineregexp", {"exprs": [["^\\s+|\\s+$", ""],
+            ["\\s+", " "], ["(,,)\\s+", "$1"], ["\\s+(\")", "$1"], ["([^\\.])(\\s*)</p",
+            "$1.$2</p"], ["([\\.:;!\\?])([^\\s\\\\])", "$1 $2"], ["([^\\s])\\s+([\\.:;!\\?])",
+            "$1$2"], ["\\s+,([^,])", ",$1"], ["([^,]),([^\\s\\\\,])", "$1, $2"]]}]]'
+        scriptlet: macro
+        slug: basic_correction
+        tooltip: "Wykonuj\u0119 podstawow\u0105 korekt\u0119 tekstu."
+    model: toolbar.button
+    pk: 4
+-   fields:
+        accesskey: ''
+        group: [2]
+        label: "zamiana cudzys\u0142ow\xF3w 1"
+        link: ''
+        params: '{"exprs": [["\u00ab|\u201e", ",,"], ["\u00bb", "\""], ["([^=])\"([\u0104\u0118\u00d3\u0141\u017b\u0179\u0106\u0143\u0105\u017c\u017a\u015b\u0144\u00f3\u0142\u0107\\w])",
+            "$1,,$2"], ["^\"([\u0104\u0118\u00d3\u0141\u017b\u0179\u0106\u0143\u0105\u017c\u017a\u015b\u0144\u00f3\u0142\u0107\\w])",
+            ",,$1"], ["(,,)\\s+|\\s+(\")", "$1"]]}'
+        scriptlet: lineregexp
+        slug: cudzyslow-francuski
+        tooltip: "zamiana \" na ,, oraz  \xABa\xBB na ,,a\""
+    model: toolbar.button
+    pk: 89
+-   fields:
+        accesskey: ''
+        group: [2]
+        label: "zamiana cudzys\u0142ow\xF3w 2"
+        link: ''
+        params: '{"exprs": [["\u00bb|\u201e", ",,"], ["\u00ab", "\""], ["([^=])\"([\u0104\u0118\u00d3\u0141\u017b\u0179\u0106\u0143\u0105\u017c\u017a\u015b\u0144\u00f3\u0142\u0107\\w])",
+            "$1,,$2"], ["^\"([\u0104\u0118\u00d3\u0141\u017b\u0179\u0106\u0143\u0105\u017c\u017a\u015b\u0144\u00f3\u0142\u0107\\w])",
+            ",,$1"], ["(,,)\\s+|\\s+(\")", "$1"]]}'
+        scriptlet: lineregexp
+        slug: cudzyslow-niemiecki
+        tooltip: "zamienia \" na ,, oraz \xBBa\xAB na ,,a\""
+    model: toolbar.button
+    pk: 77
+-   fields:
+        accesskey: ''
+        group: [1]
+        label: "cz\u0119\u015B\u0107/ksi\u0119ga"
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "naglowek_czesc"}'
+        scriptlet: insert_tag
+        slug: czesc
+        tooltip: ''
+    model: toolbar.button
+    pk: 10
+-   fields:
+        accesskey: ''
+        group: [13, 22]
+        label: dedykacja
+        link: ''
+        params: '{"tag": "dedykacja"}'
+        scriptlet: insert_tag
+        slug: dedykacja
+        tooltip: ''
+    model: toolbar.button
+    pk: 74
+-   fields:
+        accesskey: ''
+        group: [12]
+        label: didaskalia
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 2, "tag": "didaskalia"}'
+        scriptlet: insert_tag
+        slug: didaskalia
+        tooltip: ''
+    model: toolbar.button
+    pk: 62
+-   fields:
+        accesskey: ''
+        group: [22]
+        label: "didaskalia pocz\u0105tkowe"
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "miejsce_czas"}'
+        scriptlet: insert_tag
+        slug: didaskalia-poczatkowe
+        tooltip: "komentarze wprowadzaj\u0105ce przed tekstem dramatu"
+    model: toolbar.button
+    pk: 79
+-   fields:
+        accesskey: ''
+        group: [12]
+        label: didaskalia tekstowe
+        link: ''
+        params: '{"tag": "didask_tekst"}'
+        scriptlet: insert_tag
+        slug: didaskalia-tekstowe
+        tooltip: "didaskalia umieszczone w obr\u0119bie innego tekstu"
+    model: toolbar.button
+    pk: 63
+-   fields:
+        accesskey: ''
+        group: [21]
+        label: "d\u0142ugi cytat"
+        link: ''
+        params: '{"tag": "dlugi_cytat"}'
+        scriptlet: insert_tag
+        slug: dlugi-cytat
+        tooltip: "d\u0142ugi cytat wyr\xF3\u017Cniony sk\u0142adem"
+    model: toolbar.button
+    pk: 42
+-   fields:
+        accesskey: ''
+        group: [21]
+        label: "d\u0142ugi cyt. poet."
+        link: ''
+        params: '{"tag": "poezja_cyt"}'
+        scriptlet: insert_tag
+        slug: dlugi-cyt-poet
+        tooltip: "d\u0142ugi cytat wierszowany wyr\xF3\u017Cniony sk\u0142adem"
+    model: toolbar.button
+    pk: 67
+-   fields:
+        accesskey: ''
+        group: [11]
+        label: dramat wiersz.
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "dramat_wierszowany_l"}'
+        scriptlet: insert_tag
+        slug: dramat-wiersz
+        tooltip: ''
+    model: toolbar.button
+    pk: 20
+-   fields:
+        accesskey: ''
+        group: [11]
+        label: "dramat wiersz./w. \u0142am"
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "dramat_wierszowany_lp"}'
+        scriptlet: insert_tag
+        slug: dramat-wiersz-w-lam
+        tooltip: "dramat wierszowany o zw\u0119\u017Conej szeroko\u015Bci \u0142amu"
+    model: toolbar.button
+    pk: 22
+-   fields:
+        accesskey: ''
+        group: [11]
+        label: "dramat wsp\xF3\u0142czesny"
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "dramat_wspolczesny"}'
+        scriptlet: insert_tag
+        slug: dramat-wspolczesny
+        tooltip: "dramat wsp\xF3\u0142czesny (proz\u0105)"
+    model: toolbar.button
+    pk: 21
+-   fields:
+        accesskey: ''
+        group: [13]
+        label: "dzie\u0142o nadrz\u0119dne"
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 2, "tag": "dzielo_nadrzedne"}'
+        scriptlet: insert_tag
+        slug: dzielo-nadrzedne
+        tooltip: ''
+    model: toolbar.button
+    pk: 38
+-   fields:
+        accesskey: ''
+        group: []
+        label: extra
+        link: ''
+        params: '{"tag": "extra"}'
+        scriptlet: insert_tag
+        slug: extra
+        tooltip: "uwagi dotycz\u0105ce sk\u0142adu"
+    model: toolbar.button
+    pk: 96
+-   fields:
+        accesskey: ''
+        group: []
+        label: Wydrukuj
+        link: print/html
+        params: '[]'
+        scriptlet: insert_tag
+        slug: htmleditor-print
+        tooltip: ''
+    model: toolbar.button
+    pk: 87
+-   fields:
+        accesskey: k
+        group: [12]
+        label: kwestia
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 1, "tag": "kwestia"}'
+        scriptlet: insert_tag
+        slug: kwestia
+        tooltip: "wstawia kwesti\u0119"
+    model: toolbar.button
+    pk: 82
+-   fields:
+        accesskey: ''
+        group: [12]
+        label: kwestioakapit
+        link: ''
+        params: '[["insert_tag", {"tag": "akap"}], ["insert_tag", {"padding_top":
+            1, "padding_bottom": 1, "tag": "kwestia"}]]'
+        scriptlet: macro
+        slug: kwestioakapit
+        tooltip: ''
+    model: toolbar.button
+    pk: 101
+-   fields:
+        accesskey: ''
+        group: [12]
+        label: kwestiostrofa
+        link: ''
+        params: '[["insert_stanza", {"tag": "strofa"}], ["insert_tag", {"padding_top":
+            1, "padding_bottom": 1, "tag": "kwestia"}]]'
+        scriptlet: macro
+        slug: kwestiostrofa
+        tooltip: ''
+    model: toolbar.button
+    pk: 102
+-   fields:
+        accesskey: ''
+        group: [11]
+        label: liryka
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "liryka_l"}'
+        scriptlet: insert_tag
+        slug: liryka
+        tooltip: ''
+    model: toolbar.button
+    pk: 23
+-   fields:
+        accesskey: ''
+        group: [11]
+        label: "liryka/w. \u0142am"
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "liryka_lp"}'
+        scriptlet: insert_tag
+        slug: liryka-w-lam
+        tooltip: "utw\xF3r liryczny o zw\u0119\u017Conej szeroko\u015Bci \u0142amu"
+    model: toolbar.button
+    pk: 24
+-   fields:
+        accesskey: ''
+        group: [22]
+        label: "lista os\xF3b"
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "lista_osob"}'
+        scriptlet: insert_tag
+        slug: lista-osob
+        tooltip: "lista os\xF3b poprzedzaj\u0105ca tekst dramatu"
+    model: toolbar.button
+    pk: 93
+-   fields:
+        accesskey: ''
+        group: [22]
+        label: 'typ osoby '
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 1, "tag": "lista_osoba", "attrs":
+            {"typ": ""}}'
+        scriptlet: insert_tag
+        slug: lista-osob-pole
+        tooltip: osoby z takim samym opisem
+    model: toolbar.button
+    pk: 78
+-   fields:
+        accesskey: ''
+        group: [15]
+        label: matemat.
+        link: ''
+        params: '{"tag": "mat"}'
+        scriptlet: insert_tag
+        slug: matemat
+        tooltip: "wyra\u017Cenia matematyczne lub zmienne symboliczne"
+    model: toolbar.button
+    pk: 47
+-   fields:
+        accesskey: ''
+        group: [13, 22]
+        label: motto
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "motto"}'
+        scriptlet: insert_tag
+        slug: motto
+        tooltip: ''
+    model: toolbar.button
+    pk: 75
+-   fields:
+        accesskey: ''
+        group: [13, 22]
+        label: motto podpis
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 2, "tag": "motto_podpis"}'
+        scriptlet: insert_tag
+        slug: motto-podpis
+        tooltip: ''
+    model: toolbar.button
+    pk: 37
+-   fields:
+        accesskey: ''
+        group: [2]
+        label: ",,\u2026\" na \xAB\u2026\xBB"
+        link: ''
+        params: '{"exprs": [[",,", "\u00ab"], ["\"", "\u00bb"]]}'
+        scriptlet: fulltextregexp
+        slug: na-francuskie
+        tooltip: "Zamienia cudzys\u0142owy podw\xF3jne na francuskie"
+    model: toolbar.button
+    pk: 2
+-   fields:
+        accesskey: ''
+        group: [28]
+        label: "nag\u0142. dramatu"
+        link: ''
+        params: '{"exprs": [["^AKT(\\s\\w*)$", "<naglowek_akt>AKT$1</naglowek_akt>"],
+            ["^SCENA(\\s\\w*)$", "<naglowek_scena>SCENA$1</naglowek_scena>"], ["([A-Z\u0104\u0106\u0118\u0141\u0143\u00d3\u015a\u017b\u0179]{2}[A-Z\u0104\u0106\u0118\u0141\u0143\u00d3\u015a\u017b\u0179\\s]+)$",
+            "<naglowek_osoba>$1</naglowek_osoba>"]]}'
+        scriptlet: lineregexp
+        slug: nagl-dramatu
+        tooltip: "autotagowanie akt\xF3w, scen, nag\u0142\xF3wk\xF3w os\xF3b"
+    model: toolbar.button
+    pk: 103
+-   fields:
+        accesskey: ''
+        group: [12]
+        label: "nag\u0142\xF3wek kwestii"
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "naglowek_osoba"}'
+        scriptlet: insert_tag
+        slug: naglowek-kwestii
+        tooltip: "nag\u0142\xF3wek kwestii - nazwa osoby"
+    model: toolbar.button
+    pk: 16
+-   fields:
+        accesskey: ''
+        group: [22]
+        label: "nag\u0142\xF3wek listy"
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 2, "tag": "naglowek_listy"}'
+        scriptlet: insert_tag
+        slug: naglowek-listy
+        tooltip: "nag\u0142\xF3wek listy os\xF3b"
+    model: toolbar.button
+    pk: 94
+-   fields:
+        accesskey: ''
+        group: [2]
+        label: ",,\u2026\" na \xBB\u2026\xAB"
+        link: ''
+        params: '{"exprs": [[",,", "\u00bb"], ["\"", "\u00ab"]]}'
+        scriptlet: fulltextregexp
+        slug: na-niemieckie
+        tooltip: "Zamienia cudzys\u0142owy podw\xF3jne na niemieckie"
+    model: toolbar.button
+    pk: 3
+-   fields:
+        accesskey: ''
+        group: [13]
+        label: nazwa utworu
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 2, "tag": "nazwa_utworu"}'
+        scriptlet: insert_tag
+        slug: nazwa-utworu
+        tooltip: ''
+    model: toolbar.button
+    pk: 33
+-   fields:
+        accesskey: ''
+        group: [13]
+        label: nota
+        link: ''
+        params: '{"tag": "nota"}'
+        scriptlet: insert_tag
+        slug: nota
+        tooltip: ''
+    model: toolbar.button
+    pk: 35
+-   fields:
+        accesskey: ''
+        group: [13]
+        label: nota red.
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "nota_red"}'
+        scriptlet: insert_tag
+        slug: nota-red
+        tooltip: nota redakcyjna
+    model: toolbar.button
+    pk: 104
+-   fields:
+        accesskey: ''
+        group: [11]
+        label: opowiadanie
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "opowiadanie"}'
+        scriptlet: insert_tag
+        slug: opowiadanie
+        tooltip: ''
+    model: toolbar.button
+    pk: 18
+-   fields:
+        accesskey: b
+        group: [12]
+        label: osoba
+        link: ''
+        params: '{"tag": "osoba"}'
+        scriptlet: insert_tag
+        slug: osoba
+        tooltip: "wstawia nazw\u0119 osoby w didaskaliach"
+    model: toolbar.button
+    pk: 64
+-   fields:
+        accesskey: ''
+        group: [22]
+        label: osoba na liscie
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 1, "tag": "lista_osoba"}'
+        scriptlet: insert_tag
+        slug: osoba-na-liscie
+        tooltip: "nazwa osoby na liscie os\xF3b"
+    model: toolbar.button
+    pk: 95
+-   fields:
+        accesskey: ''
+        group: [1]
+        label: "podrozdzia\u0142"
+        link: ''
+        params: '{"tag": "naglowek_podrozdzial"}'
+        scriptlet: insert_tag
+        slug: podrozdzial
+        tooltip: ''
+    model: toolbar.button
+    pk: 12
+-   fields:
+        accesskey: ''
+        group: [1]
+        label: "podtytu\u0142"
+        link: ''
+        params: '{"tag": "podtytul"}'
+        scriptlet: insert_tag
+        slug: podtytul
+        tooltip: ''
+    model: toolbar.button
+    pk: 34
+-   fields:
+        accesskey: ''
+        group: [11]
+        label: "powie\u015B\u0107"
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "powiesc"}'
+        scriptlet: insert_tag
+        slug: powiesc
+        tooltip: ''
+    model: toolbar.button
+    pk: 19
+-   fields:
+        accesskey: ''
+        group: []
+        label: Wydrukuj
+        link: print/xml
+        params: '[]'
+        scriptlet: insert_tag
+        slug: print-xml
+        tooltip: ''
+    model: toolbar.button
+    pk: 86
+-   fields:
+        accesskey: ''
+        group: [26]
+        label: przypis autorski
+        link: ''
+        params: '{"tag": "pa"}'
+        scriptlet: insert_tag
+        slug: przypis-autorski
+        tooltip: ''
+    model: toolbar.button
+    pk: 68
+-   fields:
+        accesskey: ''
+        group: [26]
+        label: przypis edytorski
+        link: ''
+        params: '{"tag": "pe"}'
+        scriptlet: insert_tag
+        slug: przypis-edytorski
+        tooltip: ''
+    model: toolbar.button
+    pk: 71
+-   fields:
+        accesskey: ''
+        group: [26]
+        label: przypis redaktorski
+        link: ''
+        params: '{"tag": "pr"}'
+        scriptlet: insert_tag
+        slug: przypis-redaktorski
+        tooltip: ''
+    model: toolbar.button
+    pk: 70
+-   fields:
+        accesskey: ''
+        group: [26]
+        label: "przypis t\u0142umacza"
+        link: ''
+        params: '{"tag": "pt"}'
+        scriptlet: insert_tag
+        slug: przypis-tlumacza
+        tooltip: ''
+    model: toolbar.button
+    pk: 69
+-   fields:
+        accesskey: ''
+        group: [1]
+        label: "rozdzia\u0142"
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "naglowek_rozdzial"}'
+        scriptlet: insert_tag
+        slug: rozdzial
+        tooltip: ''
+    model: toolbar.button
+    pk: 11
+-   fields:
+        accesskey: ''
+        group: [1]
+        label: scena
+        link: ''
+        params: '{"tag": "naglowek_scena"}'
+        scriptlet: insert_tag
+        slug: scena
+        tooltip: ''
+    model: toolbar.button
+    pk: 15
+-   fields:
+        accesskey: ''
+        group: [16]
+        label: asterysk
+        link: ''
+        params: '{"nocontent": "true", "tag": "sekcja_asterysk"}'
+        scriptlet: insert_tag
+        slug: sep-asterysk
+        tooltip: rozdzielenie partii tekstu asteryskiem
+    model: toolbar.button
+    pk: 54
+-   fields:
+        accesskey: ''
+        group: [16]
+        label: linia
+        link: ''
+        params: '{"nocontent": "true", "tag": "separator_linia"}'
+        scriptlet: insert_tag
+        slug: sep-linia
+        tooltip: "rozdzielenie partii tekstu pozioma lini\u0105"
+    model: toolbar.button
+    pk: 55
+-   fields:
+        accesskey: ''
+        group: [16]
+        label: "\u015Bwiat\u0142o"
+        link: ''
+        params: '{"nocontent": "true", "tag": "sekcja_swiatlo"}'
+        scriptlet: insert_tag
+        slug: sep-swiatlo
+        tooltip: "\u015Bwiat\u0142o rozdzielaj\u0105ce sekcje tekstu"
+    model: toolbar.button
+    pk: 53
+-   fields:
+        accesskey: e
+        group: [15]
+        label: "s\u0142owo obce"
+        link: ''
+        params: '{"tag": "slowo_obce"}'
+        scriptlet: insert_tag
+        slug: slowo-obce
+        tooltip: "frazy w j\u0119zykach innych ni\u017C polski/definiendum w przypisie"
+    model: toolbar.button
+    pk: 46
+-   fields:
+        accesskey: ''
+        group: [2]
+        label: slug
+        link: ''
+        params: '[]'
+        scriptlet: slugify
+        slug: slug
+        tooltip: slugifikacja
+    model: toolbar.button
+    pk: 105
+-   fields:
+        accesskey: ''
+        group: [1]
+        label: "\u015Br\xF3dtytu\u0142"
+        link: ''
+        params: '{"tag": "srodtytul"}'
+        scriptlet: insert_tag
+        slug: srodtytul
+        tooltip: ''
+    model: toolbar.button
+    pk: 13
+-   fields:
+        accesskey: s
+        group: [12, 17]
+        label: strofa
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 3, "tag": "strofa"}'
+        scriptlet: insert_stanza
+        slug: strofa
+        tooltip: "wstawia strof\u0119"
+    model: toolbar.button
+    pk: 81
+-   fields:
+        accesskey: ''
+        group: [28]
+        label: strofy
+        link: ''
+        params: '{"tag": "strofa"}'
+        scriptlet: autotag
+        slug: strofy
+        tooltip: autotagowanie strof
+    model: toolbar.button
+    pk: 99
+-   fields:
+        accesskey: ''
+        group: [11]
+        label: "tag g\u0142\xF3wny"
+        link: ''
+        params: '{"tag": "utwor"}'
+        scriptlet: insert_tag
+        slug: tag-glowny
+        tooltip: ''
+    model: toolbar.button
+    pk: 17
+-   fields:
+        accesskey: u
+        group: [2]
+        label: "A<sup>\u2193</sup>"
+        link: ''
+        params: '[]'
+        scriptlet: lowercase
+        slug: tolowercase
+        tooltip: "Zamie\u0144 wielkie litery na ma\u0142e"
+    model: toolbar.button
+    pk: 76
+-   fields:
+        accesskey: ''
+        group: [28]
+        label: trim begin
+        link: ''
+        params: '{"text": "\n<!-- TRIM_BEGIN -->\n"}'
+        scriptlet: insert_text
+        slug: trim-begin
+        tooltip: "Wstawia pocz\u0105tkowy znacznik ci\u0119cia cz\u0119\u015Bci"
+    model: toolbar.button
+    pk: 106
+-   fields:
+        accesskey: ''
+        group: [28]
+        label: trim end
+        link: ''
+        params: '{"text": "\n<!-- TRIM_END -->\n"}'
+        scriptlet: insert_text
+        slug: trim-end
+        tooltip: "Wstawia ko\u0144cowy znacznik ci\u0119cia cz\u0119\u015Bci"
+    model: toolbar.button
+    pk: 107
+-   fields:
+        accesskey: r
+        group: [15]
+        label: "tytu\u0142 dzie\u0142a"
+        link: ''
+        params: '{"tag": "tytul_dziela"}'
+        scriptlet: insert_tag
+        slug: tytul-dziela
+        tooltip: ''
+    model: toolbar.button
+    pk: 92
+-   fields:
+        accesskey: ''
+        group: [15]
+        label: "tytu\u0142 dzie\u0142a typ 1"
+        link: ''
+        params: '{"tag": "tytul_dziela", "attrs": {"typ": "1"}}'
+        scriptlet: insert_tag
+        slug: tytul-dziela-typ
+        tooltip: "tytu\u0142 dzie\u0142a w cytowanym tytule dzie\u0142a"
+    model: toolbar.button
+    pk: 45
+-   fields:
+        accesskey: ''
+        group: [29]
+        label: uwaga
+        link: ''
+        params: '{"tag": "uwaga"}'
+        scriptlet: insert_tag
+        slug: uwaga
+        tooltip: 'uwagi redaktorsko-korektorskie '
+    model: toolbar.button
+    pk: 51
+-   fields:
+        accesskey: ''
+        group: [14, 17]
+        label: wers akap.
+        link: ''
+        params: '{"tag": "wers_akap"}'
+        scriptlet: insert_tag
+        slug: wers-akap
+        tooltip: "wers rozpoczynaj\u0105cy si\u0119 wci\u0119ciem akapitowym"
+    model: toolbar.button
+    pk: 83
+-   fields:
+        accesskey: ''
+        group: [12, 17]
+        label: wers cd.
+        link: ''
+        params: '{"tag": "wers_cd"}'
+        scriptlet: insert_tag
+        slug: wers-cd
+        tooltip: "cz\u0119\u015B\u0107 wersu przeniesiona do innego wiersza"
+    model: toolbar.button
+    pk: 85
+-   fields:
+        accesskey: w
+        group: [12, 17]
+        label: "wers mocno wci\u0119ty"
+        link: ''
+        params: '{"tag": "wers_wciety", "attrs": {"typ": ""}}'
+        scriptlet: insert_tag
+        slug: wers-mocno-wciety
+        tooltip: "argumenty wersu wci\u0119tego: od 2 do 6"
+    model: toolbar.button
+    pk: 84
+-   fields:
+        accesskey: q
+        group: [12, 17]
+        label: "wers wci\u0119ty"
+        link: ''
+        params: '{"tag": "wers_wciety", "attrs": {"typ": "1"}}'
+        scriptlet: insert_tag
+        slug: wers-wciety
+        tooltip: "wstawia wers wci\u0119ty"
+    model: toolbar.button
+    pk: 91
+-   fields:
+        accesskey: ''
+        group: [28]
+        label: "wersy wci\u0119te"
+        link: ''
+        params: '{"padding": 1, "tag": "wers_wciety", "split": 1}'
+        scriptlet: autotag
+        slug: wersy-wciete
+        tooltip: "autotagowanie wers\xF3w wci\u0119tych"
+    model: toolbar.button
+    pk: 100
+-   fields:
+        accesskey: ''
+        group: [15]
+        label: www
+        link: ''
+        params: '{"tag": "www"}'
+        scriptlet: insert_tag
+        slug: www
+        tooltip: ''
+    model: toolbar.button
+    pk: 48
+-   fields:
+        accesskey: f
+        group: [12, 15]
+        label: "wyr\xF3\u017Cnienie"
+        link: ''
+        params: '{"tag": "wyroznienie"}'
+        scriptlet: insert_tag
+        slug: wyroznienie
+        tooltip: "wyr\xF3\u017Cnienie autorskie"
+    model: toolbar.button
+    pk: 44
+-   fields:
+        accesskey: ''
+        group: [11]
+        label: wywiad
+        link: ''
+        params: '{"padding_top": 1, "padding_bottom": 4, "tag": "wywiad"}'
+        scriptlet: insert_tag
+        slug: wywiad
+        tooltip: ''
+    model: toolbar.button
+    pk: 25
+-   fields:
+        accesskey: ''
+        group: [21]
+        label: "wywiad odpowied\u017A"
+        link: ''
+        params: '{"tag": "wywiad_odp"}'
+        scriptlet: insert_tag
+        slug: wywiad-odpowiedz
+        tooltip: ''
+    model: toolbar.button
+    pk: 73
+-   fields:
+        accesskey: ''
+        group: [21]
+        label: wywiad pytanie
+        link: ''
+        params: '{"tag": "wywiad_pyt"}'
+        scriptlet: insert_tag
+        slug: wywiad-pytanie
+        tooltip: ''
+    model: toolbar.button
+    pk: 72
+-   fields:
+        accesskey: ''
+        group: [16]
+        label: "zast\u0119pnik wersu"
+        link: ''
+        params: '{"tag": "zastepnik_wersu"}'
+        scriptlet: insert_tag
+        slug: zastepnik-wersu
+        tooltip: wykropkowanie wersu
+    model: toolbar.button
+    pk: 56
+-   fields: {code: "$(params).each(function() {\n    $.log(this[0], this[1]);\n  \
+            \  editor.callScriptlet(this[0], panel, this[1]);\n\n});"}
+    model: toolbar.scriptlet
+    pk: macro
+-   fields: {code: "var texteditor = panel.texteditor;\nvar text = texteditor.selection();\n\
+            var start_tag = '<'+params.tag;\nfor (var attr in params.attrs) {\n  \
+            \  start_tag += ' '+attr+'=\"' + params.attrs[attr] + '\"';\n};\nstart_tag\
+            \ += '>';\nvar end_tag = '</'+params.tag+'>';\n\nif(text.length > 0) {\n\
+            // tokenize\nvar output = ''\nvar token = ''\nfor(var index=0; index <\
+            \ text.length; index++)\n{\n    if (text[index].match(/\\s/)) { // whitespace\n\
+            \        token += text[index];\n    }\n    else { // character\n     \
+            \   output += token;\n        if(output == token) output += start_tag;\n\
+            \        token = ''\n        output += text[index];\n    }\n}\n\nif( output[output.length-1]\
+            \ == '\\\\' ) {\n    output = output.substr(0, output.length-1) + end_tag\
+            \ + '\\\\';\n} else {\n    output += end_tag;\n}\noutput += token;\n}\n\
+            else {\n output = start_tag + end_tag;\n}\n\ntexteditor.replaceSelection(output);\n\
+            \nif (text.length == 0) {\n    var pos = texteditor.cursorPosition();\n\
+            \    texteditor.selectLines(pos.line, pos.character + params.tag.length\
+            \ + 2);\n}\n\npanel.fireEvent('contentChanged');"}
+    model: toolbar.scriptlet
+    pk: insert_tag
+-   fields: {code: "editor.showPopup('generic-info', 'Przetwarzanie zaznaczonego tekstu...',\
+            \ '', -1);\n\nvar cm = panel.texteditor;\nvar exprs = $.map(params.exprs,\
+            \ function(expr) {\n\n    var opts = \"g\";\n\n    if(expr.length > 2)\n\
+            \n        opts = expr[2];\n\n    return {rx: new RegExp(expr[0], opts),\
+            \ repl: expr[1]};\n\n});\n\n\n\nvar partial = true;\n\nvar text = cm.selection();\n\
+            \n\n\nif(!text) {\n\n    var cpos = cm.cursorPosition();\n\n    cpos.line\
+            \ = cm.lineNumber(cpos.line)\n\n    cm.selectLines(cm.firstLine(), 0,\
+            \ cm.lastLine(), 0);\n\n    text = cm.selection();\n\n    partial = false;\n\
+            \n}\n\n\n\nvar changed = 0;\nvar lines = text.split('\\n');\nvar lines\
+            \ = $.map(lines, function(line) { \n    var old_line = line;\n    $(exprs).each(function()\
+            \ { \n        var expr = this;\n        line = line.replace(expr.rx, expr.repl);\n\
+            \    });\n\n    if(old_line != line) changed += 1;\n    return line;\n\
+            });\n\nif(changed > 0) \n{\n    cm.replaceSelection( lines.join('\\n')\
+            \ );\n    panel.fireEvent('contentChanged');\n    editor.showPopup('generic-yes',\
+            \ 'Zmieniono ' + changed + ' linii.', 1500);\n    editor.advancePopupQueue();\n\
+            }\nelse {\n    editor.showPopup('generic-info',  'Brak zmian w tek\u015B\
+            cie', 1500);\n    editor.advancePopupQueue();\n}\n\nif(!partial)\n   \
+            \ cm.selectLines( cm.nthLine(cpos.line), cpos.character )"}
+    model: toolbar.scriptlet
+    pk: lineregexp
+-   fields: {code: '-'}
+    model: toolbar.scriptlet
+    pk: autotag
+-   fields: {code: "editor.showPopup('generic-info', 'Przetwarzanie zaznaczonego tekstu...',\
+            \ '', -1);\n$.log(editor, panel, params);\nvar cm = panel.texteditor;\n\
+            var exprs = $.map(params.exprs, function(expr) {\n    var opts = \"mg\"\
+            ;\n    if(expr.length > 2)\n        opts = expr[2];\n\n    return {rx:\
+            \ new RegExp(expr[0], opts), repl: expr[1]};\n});\n\nvar partial = true;\n\
+            var text = cm.selection();\n\nif(!text) {\n    var cpos = cm.cursorPosition();\n\
+            \    cpos.line = cm.lineNumber(cpos.line)\n    cm.selectLines(cm.firstLine(),\
+            \ 0, cm.lastLine(), 0);\n\n    text = cm.selection();\n    partial = false;\n\
+            }\n\nvar original = text;\n$(exprs).each(function() { \n    text = text.replace(this.rx,\
+            \ this.repl);\n});\n\nif( original != text) \n{    \n    cm.replaceSelection(text);\n\
+            \    panel.fireEvent('contentChanged');\n    editor.showPopup('generic-yes',\
+            \ 'Zmieniono tekst' );\n    editor.advancePopupQueue();\n}\nelse {\n \
+            \   editor.showPopup('generic-info', 'Brak zmian w tek\u015Bcie.');\n\
+            \    editor.advancePopupQueue();\n}\n\nif(!partial) {\n    cm.selectLines(\
+            \ cm.nthLine(cpos.line), cpos.character );\n}"}
+    model: toolbar.scriptlet
+    pk: fulltextregexp
+-   fields: {code: '-'}
+    model: toolbar.scriptlet
+    pk: insert_text
+-   fields: {code: "var cm = panel.texteditor;\r\nvar text = cm.selection();\r\n\r\
+            \nif(!text) return;\r\nvar repl = '';\r\nvar lcase = text.toLowerCase();\r\
+            \nvar ucase = text.toUpperCase();\r\n\r\nif(lcase == text) repl = ucase;\
+            \ /* was lowercase */\r\nelse if(ucase != text) repl = lcase; /* neither\
+            \ lower- or upper-case */\r\nelse { /* upper case -> title-case */\r\n\
+            \   var words = $(lcase.split(/\\s/)).map(function() { \r\n        if(this.length\
+            \ > 0) { return this[0].toUpperCase() + this.slice(1); } else { return\
+            \ ''}\r\n   }); \r\n   repl = words.join(' ');\r\n} \r\n\r\nif(repl !=\
+            \ text) {\r\n    cm.replaceSelection(repl);\r\n    panel.fireEvent('contentChanged');\r\
+            \n};"}
+    model: toolbar.scriptlet
+    pk: lowercase
+-   fields: {code: "var texteditor = panel.texteditor;\r\nvar text = texteditor.selection();\r\
+            \n\r\nif(text) {\r\n  var verses = text.split('\\n');\r\n  var text =\
+            \ ''; var buf = ''; var ebuf = '';\r\n  var first = true;\r\n\r\n  for(var\
+            \ i=0;  i < verses.length; i++) {\r\n    verse = verses[i].replace(/^\\\
+            s+/, \"\").replace(/\\s+$/, \"\");   \r\n    if(verse) {\r\n      text\
+            \ += (buf ? buf + '/\\n' : '') + ebuf;\r\n      buf = (first ? '<strofa>\\\
+            n' : '') + verses[i];\r\n      ebuf = '';\r\n      first = false;\r\n\
+            \    } else {    \r\n      ebuf += '\\n' + verses[i];\r\n    }\r\n  };\r\
+            \n  text = text + buf + '\\n</strofa>' + ebuf; \r\n  texteditor.replaceSelection(text);\r\
+            \n}\r\n\r\nif (!text) {\r\n    var pos = texteditor.cursorPosition();\r\
+            \n    texteditor.selectLines(pos.line, pos.character + 6 + 2);\r\n}\r\n\
+            \r\n\r\n\r\n\r\n\r\n\r\n\r\npanel.fireEvent('contentChanged');"}
+    model: toolbar.scriptlet
+    pk: insert_stanza
+-   fields: {code: '-'}
+    model: toolbar.scriptlet
+    pk: slugify
diff --git a/apps/toolbar/migrations/0005_initial_data.py b/apps/toolbar/migrations/0005_initial_data.py
new file mode 100644 (file)
index 0000000..b31f380
--- /dev/null
@@ -0,0 +1,46 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+    def forwards(self, orm):
+        from django.core.management import call_command
+        call_command("loaddata", "initial_toolbar.yaml")
+
+
+    def backwards(self, orm):
+        "Write your backwards methods here."
+        pass
+
+
+    models = {
+        'toolbar.button': {
+            'Meta': {'ordering': "('slug',)", 'object_name': 'Button'},
+            'accesskey': ('django.db.models.fields.CharField', [], {'max_length': '1', 'blank': 'True'}),
+            'group': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['toolbar.ButtonGroup']", 'symmetrical': 'False'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'label': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'link': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}),
+            'params': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'scriptlet': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['toolbar.Scriptlet']", 'null': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}),
+            'tooltip': ('django.db.models.fields.CharField', [], {'max_length': '120', 'blank': 'True'})
+        },
+        'toolbar.buttongroup': {
+            'Meta': {'ordering': "('position', 'name')", 'object_name': 'ButtonGroup'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'})
+        },
+        'toolbar.scriptlet': {
+            'Meta': {'object_name': 'Scriptlet'},
+            'code': ('django.db.models.fields.TextField', [], {}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'primary_key': 'True'})
+        }
+    }
+
+    complete_apps = ['toolbar']
index 9c32b43..90da85e 100644 (file)
@@ -2,4 +2,7 @@ from django.contrib import admin
 
 from wiki import models
 
-#admin.site.register(models.Theme)
+class ThemeAdmin(admin.ModelAdmin):
+    search_fields = ['name']
+
+admin.site.register(models.Theme, ThemeAdmin)
diff --git a/apps/wiki/constants.py b/apps/wiki/constants.py
deleted file mode 100644 (file)
index 6781a48..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-from django.utils.translation import ugettext_lazy as _
-
-DOCUMENT_STAGES = (
-    ("", u"-----"),
-    ("first_correction", _(u"First correction")),
-    ("tagging", _(u"Tagging")),
-    ("proofreading", _(u"Initial Proofreading")),
-    ("annotation-proofreading", _(u"Annotation Proofreading")),
-    ("modernisation", _(u"Modernisation")),
-    ("annotations", _(u"Annotations")),
-    ("themes", _(u"Themes")),
-    ("editor-proofreading", _(u"Editor's Proofreading")),
-    ("technical-editor-proofreading", _(u"Technical Editor's Proofreading")),
-)
-
-DOCUMENT_TAGS = DOCUMENT_STAGES + \
-    (("ready-to-publish", _(u"Ready to publish")),)
-
-DOCUMENT_TAGS_DICT = dict(DOCUMENT_TAGS)
-DOCUMENT_STAGES_DICT = dict(DOCUMENT_STAGES)
index d5c0ed5..3ef3ed1 100644 (file)
@@ -4,78 +4,85 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django import forms
-from wiki.constants import DOCUMENT_TAGS, DOCUMENT_STAGES
 from django.utils.translation import ugettext_lazy as _
 
+from catalogue.models import Chunk
 
-class DocumentTagForm(forms.Form):
+
+class DocumentPubmarkForm(forms.Form):
     """
-        Form for tagging revisions.
+        Form for marking revisions for publishing.
     """
 
     id = forms.CharField(widget=forms.HiddenInput)
-    tag = forms.ChoiceField(choices=DOCUMENT_TAGS)
+    publishable = forms.BooleanField(required=False, initial=True,
+            label=_('Publishable'))
     revision = forms.IntegerField(widget=forms.HiddenInput)
 
 
-class DocumentCreateForm(forms.Form):
-    """
-        Form used for creating new documents.
+class DocumentTextSaveForm(forms.Form):
     """
-    title = forms.CharField()
-    id = forms.RegexField(regex=ur"^[-\wąćęłńóśźżĄĆĘŁŃÓŚŹŻ]+$")
-    file = forms.FileField(required=False)
-    text = forms.CharField(required=False, widget=forms.Textarea)
+    Form for saving document's text:
 
-    def clean(self):
-        file = self.cleaned_data['file']
+        * parent_revision - revision which the modified text originated from.
+        * comment - user's verbose comment; will be used in commit.
+        * stage_completed - mark this change as end of given stage.
 
-        if file is not None:
-            try:
-                self.cleaned_data['text'] = file.read().decode('utf-8')
-            except UnicodeDecodeError:
-                raise forms.ValidationError("Text file must be UTF-8 encoded.")
+    """
 
-        if not self.cleaned_data["text"]:
-            raise forms.ValidationError("You must either enter text or upload a file")
+    parent_revision = forms.IntegerField(widget=forms.HiddenInput, required=False)
+    text = forms.CharField(widget=forms.HiddenInput)
 
-        return self.cleaned_data
+    author_name = forms.CharField(
+        required=True,
+        label=_(u"Author"),
+        help_text=_(u"Your name"),
+    )
 
+    author_email = forms.EmailField(
+        required=True,
+        label=_(u"Author's email"),
+        help_text=_(u"Your email address, so we can show a gravatar :)"),
+    )
 
-class DocumentsUploadForm(forms.Form):
-    """
-        Form used for uploading new documents.
-    """
-    file = forms.FileField(required=True, label=_('ZIP file'))
+    comment = forms.CharField(
+        required=True,
+        widget=forms.Textarea,
+        label=_(u"Your comments"),
+        help_text=_(u"Describe changes you made."),
+    )
 
-    def clean(self):
-        file = self.cleaned_data['file']
+    stage_completed = forms.ModelChoiceField(
+        queryset=Chunk.tag_model.objects.all(),
+        required=False,
+        label=_(u"Completed"),
+        help_text=_(u"If you completed a life cycle stage, select it."),
+    )
 
-        import zipfile
-        try:
-            z = self.cleaned_data['zip'] = zipfile.ZipFile(file)
-        except zipfile.BadZipfile:
-            raise forms.ValidationError("Should be a ZIP file.")
-        if z.testzip():
-            raise forms.ValidationError("ZIP file corrupt.")
+    publishable = forms.BooleanField(required=False, initial=False,
+        label=_('Publishable'),
+        help_text=_(u"Mark this revision as publishable.")
+    )
 
-        return self.cleaned_data
+    def __init__(self, *args, **kwargs):
+        user = kwargs.pop('user')
+        r = super(DocumentTextSaveForm, self).__init__(*args, **kwargs)
+        if user and user.is_authenticated():
+            self.fields['author_name'].required = False
+            self.fields['author_email'].required = False
+        return r
 
 
-class DocumentTextSaveForm(forms.Form):
+class DocumentTextRevertForm(forms.Form):
     """
-    Form for saving document's text:
+    Form for reverting document's text:
 
-        * name - document's storage identifier.
-        * parent_revision - revision which the modified text originated from.
+        * revision - revision to revert to.
         * comment - user's verbose comment; will be used in commit.
-        * stage_completed - mark this change as end of given stage.
 
     """
 
-    id = forms.CharField(widget=forms.HiddenInput)
-    parent_revision = forms.IntegerField(widget=forms.HiddenInput)
-    text = forms.CharField(widget=forms.HiddenInput)
+    revision = forms.IntegerField(widget=forms.HiddenInput)
 
     author_name = forms.CharField(
         required=False,
@@ -93,12 +100,5 @@ class DocumentTextSaveForm(forms.Form):
         required=True,
         widget=forms.Textarea,
         label=_(u"Your comments"),
-        help_text=_(u"Describe changes you made."),
-    )
-
-    stage_completed = forms.ChoiceField(
-        choices=DOCUMENT_STAGES,
-        required=False,
-        label=_(u"Completed"),
-        help_text=_(u"If you completed a life cycle stage, select it."),
+        help_text=_(u"Describe the reason for reverting."),
     )
index f072ef9..dace3d0 100644 (file)
@@ -1,8 +1,9 @@
+from datetime import datetime
+from functools import wraps
+
 from django import http
 from django.utils import simplejson as json
 from django.utils.functional import Promise
-from datetime import datetime
-from functools import wraps
 
 
 class ExtendedEncoder(json.JSONEncoder):
@@ -58,74 +59,3 @@ def ajax_require_permission(permission):
             return view(request, *args, **kwargs)
         return authorized_view
     return decorator
-
-import collections
-
-def recursive_groupby(iterable):
-    """
-#    >>> recursive_groupby([1,2,3,4,5])
-#    [1, 2, 3, 4, 5]
-
-    >>> recursive_groupby([[1]])
-    [1]
-
-    >>> recursive_groupby([('a', 1),('a', 2), 3, ('b', 4), 5])
-    ['a', [1, 2], 3, 'b', [4], 5]
-
-    >>> recursive_groupby([('a', 'x', 1),('a', 'x', 2), ('a', 'x', 3)])
-    ['a', ['x', [1, 2, 3]]]
-
-    """
-
-    def _generator(iterator):
-        group = None
-        grouper = None
-
-        for item in iterator:
-            if not isinstance(item, collections.Sequence):
-                if grouper is not None:
-                    yield grouper
-                    if len(group):
-                        yield recursive_groupby(group)
-                    group = None
-                    grouper = None
-                yield item
-                continue
-            elif len(item) == 1:
-                if grouper is not None:
-                    yield grouper
-                    if len(group):
-                        yield recursive_groupby(group)
-                    group = None
-                    grouper = None
-                yield item[0]
-                continue
-            elif not len(item):
-                continue
-
-            if grouper is None:
-                group = [item[1:]]
-                grouper = item[0]
-                continue
-
-            if grouper != item[0]:
-                if grouper is not None:
-                    yield grouper
-                    if len(group):
-                        yield recursive_groupby(group)
-                    group = None
-                    grouper = None
-                group = [item[1:]]
-                grouper = item[0]
-                continue
-
-            group.append(item[1:])
-
-        if grouper is not None:
-            yield grouper
-            if len(group):
-                yield recursive_groupby(group)
-            group = None
-            grouper = None
-
-    return list(_generator(iterable))
index c334ead..d886dce 100644 (file)
Binary files a/apps/wiki/locale/pl/LC_MESSAGES/django.mo and b/apps/wiki/locale/pl/LC_MESSAGES/django.mo differ
index 568e844..0182abe 100644 (file)
@@ -7,128 +7,89 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Platforma Redakcyjna\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2010-09-29 15:34+0200\n"
-"PO-Revision-Date: 2010-09-29 15:36+0100\n"
+"POT-Creation-Date: 2011-11-30 16:07+0100\n"
+"PO-Revision-Date: 2011-11-30 16:08+0100\n"
 "Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
 "Language-Team: Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org.pl>\n"
+"Language: \n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: constants.py:6
-msgid "First correction"
-msgstr "Autokorekta"
-
-#: constants.py:7
-msgid "Tagging"
-msgstr "Tagowanie"
-
-#: constants.py:8
-msgid "Initial Proofreading"
-msgstr "Korekta"
-
-#: constants.py:9
-msgid "Annotation Proofreading"
-msgstr "Sprawdzenie przypisów źródła"
-
-#: constants.py:10
-msgid "Modernisation"
-msgstr "Uwspółcześnienie"
-
-#: constants.py:11
-#: templates/wiki/tabs/annotations_view_item.html:3
-msgid "Annotations"
-msgstr "Przypisy"
-
-#: constants.py:12
-msgid "Themes"
-msgstr "Motywy"
-
-#: constants.py:13
-msgid "Editor's Proofreading"
-msgstr "Ostateczna redakcja literacka"
-
-#: constants.py:14
-msgid "Technical Editor's Proofreading"
-msgstr "Ostateczna redakcja techniczna"
-
-#: constants.py:18
-msgid "Ready to publish"
+#: forms.py:19
+#: forms.py:63
+#: views.py:279
+msgid "Publishable"
 msgstr "Gotowe do publikacji"
 
-#: forms.py:49
-msgid "ZIP file"
-msgstr "Plik ZIP"
-
-#: forms.py:82
+#: forms.py:38
+#: forms.py:89
 msgid "Author"
 msgstr "Autor"
 
-#: forms.py:83
+#: forms.py:39
+#: forms.py:90
 msgid "Your name"
 msgstr "Imię i nazwisko"
 
-#: forms.py:88
+#: forms.py:44
+#: forms.py:95
 msgid "Author's email"
 msgstr "E-mail autora"
 
-#: forms.py:89
+#: forms.py:45
+#: forms.py:96
 msgid "Your email address, so we can show a gravatar :)"
 msgstr "Adres e-mail, żebyśmy mogli pokazać gravatar :)"
 
-#: forms.py:95
+#: forms.py:51
+#: forms.py:102
 msgid "Your comments"
 msgstr "Twój komentarz"
 
-#: forms.py:96
+#: forms.py:52
 msgid "Describe changes you made."
 msgstr "Opisz swoje zmiany"
 
-#: forms.py:102
+#: forms.py:58
 msgid "Completed"
 msgstr "Ukończono"
 
-#: forms.py:103
+#: forms.py:59
 msgid "If you completed a life cycle stage, select it."
 msgstr "Jeśli został ukończony etap prac, wskaż go."
 
-#: models.py:93
-#, python-format
-msgid "Finished stage: %s"
-msgstr "Ukończony etap: %s"
+#: forms.py:64
+msgid "Mark this revision as publishable."
+msgstr "Oznacz tę wersję jako gotową do publikacji."
 
-#: models.py:152
+#: forms.py:103
+msgid "Describe the reason for reverting."
+msgstr "Opisz powód przywrócenia."
+
+#: models.py:14
 msgid "name"
 msgstr "nazwa"
 
-#: models.py:156
+#: models.py:18
 msgid "theme"
 msgstr "motyw"
 
-#: models.py:157
+#: models.py:19
 msgid "themes"
 msgstr "motywy"
 
-#: views.py:167
-#, python-format
-msgid "Title already used for %s"
-msgstr "Nazwa taka sama jak dla pliku %s"
+#: views.py:299
+msgid "Revision marked"
+msgstr "Wersja oznaczona"
 
-#: views.py:169
-msgid "Title already used in repository."
-msgstr "Plik o tej nazwie już istnieje w repozytorium."
+#: views.py:301
+msgid "Nothing changed"
+msgstr "Nic nie uległo zmianie"
 
-#: views.py:175
-msgid "File should be UTF-8 encoded."
-msgstr "Plik powinien mieć kodowanie UTF-8."
-
-#: views.py:358
-msgid "Tag added"
-msgstr "Dodano tag"
-
-#: templates/wiki/base.html:15
-msgid "Platforma Redakcyjna"
-msgstr ""
+#: templates/admin/wiki/theme/change_list.html:21
+msgid "Table for Redmine wiki"
+msgstr "Tabela do wiki na Redmine"
 
 #: templates/wiki/diff_table.html:5
 msgid "Old version"
@@ -138,94 +99,51 @@ msgstr "Stara wersja"
 msgid "New version"
 msgstr "Nowa wersja"
 
-#: templates/wiki/document_create_missing.html:8
-msgid "Create document"
-msgstr "Utwórz dokument"
-
 #: templates/wiki/document_details.html:32
 msgid "Click to open/close gallery"
 msgstr "Kliknij, aby (ro)zwinąć galerię"
 
-#: templates/wiki/document_details_base.html:36
+#: templates/wiki/document_details_base.html:33
 msgid "Help"
 msgstr "Pomoc"
 
-#: templates/wiki/document_details_base.html:38
+#: templates/wiki/document_details_base.html:35
 msgid "Version"
 msgstr "Wersja"
 
-#: templates/wiki/document_details_base.html:38
+#: templates/wiki/document_details_base.html:35
 msgid "Unknown"
 msgstr "nieznana"
 
-#: templates/wiki/document_details_base.html:40
-#: templates/wiki/tag_dialog.html:15
+#: templates/wiki/document_details_base.html:37
+#: templates/wiki/pubmark_dialog.html:16
 msgid "Save"
 msgstr "Zapisz"
 
-#: templates/wiki/document_details_base.html:41
+#: templates/wiki/document_details_base.html:38
 msgid "Save attempt in progress"
 msgstr "Trwa zapisywanie"
 
-#: templates/wiki/document_details_base.html:42
+#: templates/wiki/document_details_base.html:39
 msgid "There is a newer version of this document!"
 msgstr "Istnieje nowsza wersja tego dokumentu!"
 
-#: templates/wiki/document_list.html:30
-msgid "Clear filter"
-msgstr "Wyczyść filtr"
-
-#: templates/wiki/document_list.html:48
-msgid "Your last edited documents"
-msgstr "Twoje ostatnie edycje"
-
-#: templates/wiki/document_upload.html:9
-msgid "Bulk documents upload"
-msgstr "Hurtowe dodawanie dokumentów"
-
-#: templates/wiki/document_upload.html:12
-msgid "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with <code>.xml</code> will be ignored."
-msgstr "Proszę wskazać archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie kończące się na <code>.xml</code> zostaną zignorowane."
-
-#: templates/wiki/document_upload.html:17
-msgid "Upload"
-msgstr "Dodaj"
-
-#: templates/wiki/document_upload.html:24
-msgid "There have been some errors. No files have been added to the repository."
-msgstr "Wystąpiły błędy. Żadne pliki nie zostały dodane do repozytorium."
-
-#: templates/wiki/document_upload.html:25
-msgid "Offending files"
-msgstr "Błędne pliki"
-
-#: templates/wiki/document_upload.html:33
-msgid "Correct files"
-msgstr "Poprawne pliki"
-
-#: templates/wiki/document_upload.html:44
-msgid "Files have been successfully uploaded to the repository."
-msgstr "Pliki zostały dodane do repozytorium."
-
-#: templates/wiki/document_upload.html:45
-msgid "Uploaded files"
-msgstr "Dodane pliki"
-
-#: templates/wiki/document_upload.html:55
-msgid "Skipped files"
-msgstr "Pominięte pliki"
-
-#: templates/wiki/document_upload.html:56
-msgid "Files skipped due to no <code>.xml</code> extension"
-msgstr "Pliki pominięte z powodu braku rozszerzenia <code>.xml</code>."
-
-#: templates/wiki/tag_dialog.html:16
+#: templates/wiki/pubmark_dialog.html:17
+#: templates/wiki/revert_dialog.html:40
 msgid "Cancel"
 msgstr "Anuluj"
 
-#: templates/wiki/tabs/annotations_view.html:5
-msgid "Refresh"
-msgstr "Odśwież"
+#: templates/wiki/revert_dialog.html:39
+msgid "Revert"
+msgstr "Przywróć"
+
+#: templates/wiki/tabs/annotations_view.html:9
+msgid "all"
+msgstr "wszystkie"
+
+#: templates/wiki/tabs/annotations_view_item.html:3
+msgid "Annotations"
+msgstr "Przypisy"
 
 #: templates/wiki/tabs/gallery_view.html:7
 msgid "Previous"
@@ -251,15 +169,15 @@ msgstr "Galeria"
 msgid "Compare versions"
 msgstr "Porównaj wersje"
 
-#: templates/wiki/tabs/history_view.html:7
-msgid "Mark version"
-msgstr "Oznacz wersję"
+#: templates/wiki/tabs/history_view.html:8
+msgid "Mark for publishing"
+msgstr "Oznacz do publikacji"
 
-#: templates/wiki/tabs/history_view.html:9
+#: templates/wiki/tabs/history_view.html:11
 msgid "Revert document"
 msgstr "Przywróć wersję"
 
-#: templates/wiki/tabs/history_view.html:12
+#: templates/wiki/tabs/history_view.html:14
 msgid "View version"
 msgstr "Zobacz wersję"
 
@@ -300,31 +218,43 @@ msgstr "Znajdź i zamień"
 msgid "Source code"
 msgstr "Kod źródłowy"
 
-#: templates/wiki/tabs/summary_view.html:10
+#: templates/wiki/tabs/summary_view.html:9
 msgid "Title"
 msgstr "Tytuł"
 
-#: templates/wiki/tabs/summary_view.html:15
+#: templates/wiki/tabs/summary_view.html:13
+msgid "Go to the book's page"
+msgstr "Przejdź do strony książki"
+
+#: templates/wiki/tabs/summary_view.html:16
 msgid "Document ID"
 msgstr "ID dokumentu"
 
-#: templates/wiki/tabs/summary_view.html:19
+#: templates/wiki/tabs/summary_view.html:20
 msgid "Current version"
 msgstr "Aktualna wersja"
 
-#: templates/wiki/tabs/summary_view.html:22
+#: templates/wiki/tabs/summary_view.html:23
 msgid "Last edited by"
 msgstr "Ostatnio edytowane przez"
 
-#: templates/wiki/tabs/summary_view.html:26
+#: templates/wiki/tabs/summary_view.html:27
 msgid "Link to gallery"
 msgstr "Link do galerii"
 
-#: templates/wiki/tabs/summary_view.html:31
-msgid "Publish"
-msgstr "Opublikuj"
+#: templates/wiki/tabs/summary_view.html:32
+msgid "Characters in document"
+msgstr "Znaków w dokumencie"
+
+#: templates/wiki/tabs/summary_view.html:33
+msgid "pages"
+msgstr "stron maszynopisu"
 
-#: templates/wiki/tabs/summary_view_item.html:4
+#: templates/wiki/tabs/summary_view.html:33
+msgid "untagged"
+msgstr "nieotagowane"
+
+#: templates/wiki/tabs/summary_view_item.html:3
 msgid "Summary"
 msgstr "Podsumowanie"
 
@@ -336,11 +266,200 @@ msgstr "Wstaw motyw"
 msgid "Insert annotation"
 msgstr "Wstaw przypis"
 
-#: templates/wiki/tabs/wysiwyg_editor.html:15
-msgid "Insert special character"
-msgstr "Wstaw znak specjalny"
-
 #: templates/wiki/tabs/wysiwyg_editor_item.html:3
 msgid "Visual editor"
 msgstr "Edytor wizualny"
 
+#~ msgid "Publish"
+#~ msgstr "Opublikuj"
+
+#~ msgid "ZIP file"
+#~ msgstr "Plik ZIP"
+
+#~ msgid "Chunk with this slug already exists"
+#~ msgstr "Część z tym slugiem już istnieje"
+
+#~ msgid "Append to"
+#~ msgstr "Dołącz do"
+
+#~ msgid "title"
+#~ msgstr "tytuł"
+
+#~ msgid "scan gallery name"
+#~ msgstr "nazwa galerii skanów"
+
+#~ msgid "parent"
+#~ msgstr "rodzic"
+
+#~ msgid "parent number"
+#~ msgstr "numeracja rodzica"
+
+#~ msgid "book"
+#~ msgstr "książka"
+
+#~ msgid "books"
+#~ msgstr "książki"
+
+#~ msgid "Slug already used for %s"
+#~ msgstr "Slug taki sam jak dla pliku %s"
+
+#~ msgid "Slug already used in repository."
+#~ msgstr "Dokument o tym slugu już istnieje w repozytorium."
+
+#~ msgid "File should be UTF-8 encoded."
+#~ msgstr "Plik powinien mieć kodowanie UTF-8."
+
+#~ msgid "Tag added"
+#~ msgstr "Dodano tag"
+
+#~ msgid "Append book"
+#~ msgstr "Dołącz książkę"
+
+#~ msgid "edit"
+#~ msgstr "edytuj"
+
+#~ msgid "add basic document structure"
+#~ msgstr "dodaj podstawową strukturę dokumentu"
+
+#~ msgid "change master tag to"
+#~ msgstr "zmień tak master na"
+
+#~ msgid "add begin trimming tag"
+#~ msgstr "dodaj początkowy ogranicznik"
+
+#~ msgid "add end trimming tag"
+#~ msgstr "dodaj końcowy ogranicznik"
+
+#~ msgid "unstructured text"
+#~ msgstr "tekst bez struktury"
+
+#~ msgid "unknown XML"
+#~ msgstr "nieznany XML"
+
+#~ msgid "broken document"
+#~ msgstr "uszkodzony dokument"
+
+#~ msgid "Apply fixes"
+#~ msgstr "Wykonaj zmiany"
+
+#~ msgid "Append to other book"
+#~ msgstr "Dołącz do innej książki"
+
+#~ msgid "Last published"
+#~ msgstr "Ostatnio opublikowano"
+
+#~ msgid "Full XML"
+#~ msgstr "Pełny XML"
+
+#~ msgid "HTML version"
+#~ msgstr "Wersja HTML"
+
+#~ msgid "TXT version"
+#~ msgstr "Wersja TXT"
+
+#~ msgid "EPUB version"
+#~ msgstr "Wersja EPUB"
+
+#~ msgid "PDF version"
+#~ msgstr "Wersja PDF"
+
+#~ msgid "This book cannot be published yet"
+#~ msgstr "Ta książka nie może jeszcze zostać opublikowana"
+
+#~ msgid "Add chunk"
+#~ msgstr "Dodaj część"
+
+#~ msgid "Clear filter"
+#~ msgstr "Wyczyść filtr"
+
+#~ msgid "No books found."
+#~ msgstr "Nie znaleziono książek."
+
+#~ msgid "Your last edited documents"
+#~ msgstr "Twoje ostatnie edycje"
+
+#~ msgid "Bulk documents upload"
+#~ msgstr "Hurtowe dodawanie dokumentów"
+
+#~ 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 "Upload"
+#~ msgstr "Załaduj"
+
+#~ msgid ""
+#~ "There have been some errors. No files have been added to the repository."
+#~ msgstr "Wystąpiły błędy. Żadne pliki nie zostały dodane do repozytorium."
+
+#~ msgid "Offending files"
+#~ msgstr "Błędne pliki"
+
+#~ msgid "Correct files"
+#~ msgstr "Poprawne pliki"
+
+#~ msgid "Files have been successfully uploaded to the repository."
+#~ msgstr "Pliki zostały dodane do repozytorium."
+
+#~ msgid "Uploaded files"
+#~ msgstr "Dodane pliki"
+
+#~ msgid "Skipped files"
+#~ msgstr "Pominięte pliki"
+
+#~ msgid "Files skipped due to no <code>.xml</code> extension"
+#~ msgstr "Pliki pominięte z powodu braku rozszerzenia <code>.xml</code>."
+
+#~ msgid "Users"
+#~ msgstr "Użytkownicy"
+
+#~ msgid "Assigned to me"
+#~ msgstr "Przypisane do mnie"
+
+#~ msgid "Unassigned"
+#~ msgstr "Nie przypisane"
+
+#~ msgid "All"
+#~ msgstr "Wszystkie"
+
+#~ msgid "Add"
+#~ msgstr "Dodaj"
+
+#~ msgid "Admin"
+#~ msgstr "Administracja"
+
+#~ msgid "First correction"
+#~ msgstr "Autokorekta"
+
+#~ msgid "Tagging"
+#~ msgstr "Tagowanie"
+
+#~ msgid "Initial Proofreading"
+#~ msgstr "Korekta"
+
+#~ msgid "Annotation Proofreading"
+#~ msgstr "Sprawdzenie przypisów źródła"
+
+#~ msgid "Modernisation"
+#~ msgstr "Uwspółcześnienie"
+
+#~ msgid "Themes"
+#~ msgstr "Motywy"
+
+#~ msgid "Editor's Proofreading"
+#~ msgstr "Ostateczna redakcja literacka"
+
+#~ msgid "Technical Editor's Proofreading"
+#~ msgstr "Ostateczna redakcja techniczna"
+
+#~ msgid "Finished stage: %s"
+#~ msgstr "Ukończony etap: %s"
+
+#~ msgid "Refresh"
+#~ msgstr "Odśwież"
+
+#~ msgid "Insert special character"
+#~ msgstr "Wstaw znak specjalny"
index ec9ded5..c539908 100644 (file)
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django.db import models
-import re
-import os
-import vstorage
-from vstorage import DocumentNotFound
-from wiki import settings, constants
-from slughifi import slughifi
 from django.utils.translation import ugettext_lazy as _
 
-from django.http import Http404
-
 import logging
 logger = logging.getLogger("fnp.wiki")
 
 
-# _PCHARS_DICT = dict(zip((ord(x) for x in u"ĄĆĘŁŃÓŚŻŹąćęłńóśżź "), u"ACELNOSZZacelnoszz_"))
-_PCHARS_DICT = dict(zip((ord(x) for x in u" "), u"_"))
-
-# I know this is barbaric, but I didn't find a better solution ;(
-def split_name(name):
-    parts = name.translate(_PCHARS_DICT).split('__')
-    return parts
-
-def join_name(*parts, **kwargs):
-    name = u'__'.join(p.translate(_PCHARS_DICT) for p in parts)
-    logger.info("JOIN %r -> %r", parts, name)
-    return name
-
-def normalize_name(name):
-    """
-    >>> normalize_name("gąska".decode('utf-8'))
-    u'g\u0105ska'
-    """
-    return unicode(name).translate(_PCHARS_DICT)
-
-STAGE_TAGS_RE = re.compile(r'^#stage-finished: (.*)$', re.MULTILINE)
-
-
-class DocumentStorage(object):
-    def __init__(self, path):
-        self.vstorage = vstorage.VersionedStorage(path)
-
-    def get(self, name, revision=None):
-        text, rev = self.vstorage.page_text(name, revision)
-        return Document(self, name=name, text=text, revision=rev)
-
-    def get_by_tag(self, name, tag):
-        text, rev = self.vstorage.page_text_by_tag(name, tag)
-        return Document(self, name=name, text=text, revision=rev)
-
-    def revert(self, name, revision):
-        text, rev = self.vstorage.revert(name, revision)
-        return Document(self, name=name, text=text, revision=rev)
-
-    def get_or_404(self, *args, **kwargs):
-        try:
-            return self.get(*args, **kwargs)
-        except DocumentNotFound:
-            raise Http404
-
-    def put(self, document, author, comment, parent=None):
-        self.vstorage.save_text(
-                title=document.name,
-                text=document.text,
-                author=author,
-                comment=comment,
-                parent=parent)
-
-        return document
-
-    def create_document(self, text, name):
-        title = u', '.join(p.title() for p in split_name(name))
-
-        if text is None:
-            text = u''
-
-        document = Document(self, name=name, text=text, title=title)
-        return self.put(document, u"<wiki>", u"Document created.")
-
-    def delete(self, name, author, comment):
-        self.vstorage.delete_page(name, author, comment)
-
-    def all(self):
-        return list(self.vstorage.all_pages())
-
-    def history(self, title):
-        def stage_desc(match):
-            stage = match.group(1)
-            return _("Finished stage: %s") % constants.DOCUMENT_STAGES_DICT[stage]
-
-        for changeset in self.vstorage.page_history(title):
-            changeset['description'] = STAGE_TAGS_RE.sub(stage_desc, changeset['description'])
-            yield changeset
-
-    def doc_meta(self, title, revision=None):
-        return self.vstorage.page_meta(title, revision)
-
-
-
-class Document(object):
-    META_REGEX = re.compile(r'\s*<!--\s(.*?)-->', re.DOTALL | re.MULTILINE)
-
-    def __init__(self, storage, **kwargs):
-        self.storage = storage
-        for attr, value in kwargs.iteritems():
-            setattr(self, attr, value)
-
-    def add_tag(self, tag, revision, author):
-        """ Add document specific tag """
-        logger.debug("Adding tag %s to doc %s version %d", tag, self.name, revision)
-        self.storage.vstorage.add_page_tag(self.name, revision, tag, user=author)
-
-    @property
-    def plain_text(self):
-        return re.sub(self.META_REGEX, '', self.text, 1)
-
-    def meta(self):
-        result = {}
-
-        m = re.match(self.META_REGEX, self.text)
-        if m:
-            for line in m.group(1).split('\n'):
-                try:
-                    k, v = line.split(':', 1)
-                    result[k.strip()] = v.strip()
-                except ValueError:
-                    continue
-
-        gallery = result.get('gallery', slughifi(self.name.replace(' ', '_')))
-
-        if gallery.startswith('/'):
-            gallery = os.path.basename(gallery)
-
-        result['gallery'] = gallery
-        return result
-
-    def info(self):
-        return self.storage.vstorage.page_meta(self.name, self.revision)
-
-def getstorage():
-    return DocumentStorage(settings.REPOSITORY_PATH)
-
-#
-# Django models
-#
-
 class Theme(models.Model):
     name = models.CharField(_('name'), max_length=50, unique=True)
 
index 0a227e4..50f49d8 100644 (file)
@@ -1,7 +1,3 @@
 from django.conf import settings
 
-if not hasattr(settings, 'WIKI_REPOSITORY_PATH'):
-    raise Exception('You must set WIKI_REPOSITORY_PATH in your settings file.')
-
-REPOSITORY_PATH = settings.WIKI_REPOSITORY_PATH
 GALLERY_URL = settings.MEDIA_URL + 'images/'
diff --git a/apps/wiki/templates/admin/wiki/theme/change_list.html b/apps/wiki/templates/admin/wiki/theme/change_list.html
new file mode 100755 (executable)
index 0000000..1a74c7b
--- /dev/null
@@ -0,0 +1,28 @@
+{% extends "admin/change_list.html" %}
+
+{% block extrahead %}
+{{ block.super }}
+<script type="text/javascript">
+(function($) {
+    $(document).ready(function($) {
+        $("#redmine-table-switch").click(function() {
+            $('#redmine-table').toggle()
+        });
+    });
+})(django.jQuery);
+</script>
+{% endblock %}
+
+
+
+{% block pretitle %}
+
+
+<a id="redmine-table-switch">↓ {% trans "Table for Redmine wiki" %} ↓</a>
+<div id="redmine-table" style="display:none; padding:1em; border: 1px solid #aaa;">
+    |{% for theme in cl.get_query_set %}[[{{ theme }}]]|{% if forloop.counter|divisibleby:7 %}<br/>
+        |{% endif %}{% endfor %}
+</div>
+
+{{ block.super }}
+{% endblock %}
diff --git a/apps/wiki/templates/wiki/base.html b/apps/wiki/templates/wiki/base.html
deleted file mode 100644 (file)
index f88fac3..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-{% extends "base.html" %}
-{% load compressed i18n %}
-
-{% block title %}{{ document_name }} - {{ block.super }}{% endblock %}
-
-{% block extrahead %}
-{% compressed_css 'listing' %}
-{% endblock %}
-
-{% block extrabody %}
-{% compressed_js 'listing' %}
-{% endblock %}
-
-{% block maincontent %}
-<h1><img src="{{ STATIC_URL }}img/logo.png">{% trans "Platforma Redakcyjna" %}</h1>
-<div id="wiki_layout_left_column">
-       {% block leftcolumn %}
-       {% endblock leftcolumn %}
-</div>
-<div id="wiki_layout_right_column">
-       {% block rightcolumn %}
-       {% endblock rightcolumn %}
-</div>
-{% endblock maincontent %}
\ No newline at end of file
diff --git a/apps/wiki/templates/wiki/document_create_missing.html b/apps/wiki/templates/wiki/document_create_missing.html
deleted file mode 100644 (file)
index 351e87a..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "wiki/base.html" %}
-{% load i18n %}
-
-{% block leftcolumn %}
-       <form enctype="multipart/form-data" method="POST" action="">
-       {{ form.as_p }}
-
-       <p><button type="submit">{% trans "Create document" %}</button></p>
-       </form>
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-{% endblock rightcolumn %}
\ No newline at end of file
index bdb2200..db003d2 100644 (file)
 {% block tabs-menu %}
     {% include "wiki/tabs/summary_view_item.html" %}
     {% include "wiki/tabs/wysiwyg_editor_item.html" %}
-       {% include "wiki/tabs/source_editor_item.html" %}
+    {% include "wiki/tabs/source_editor_item.html" %}
     {% include "wiki/tabs/history_view_item.html" %}
 {% endblock %}
 
 {% block tabs-content %}
     {% include "wiki/tabs/summary_view.html" %}
     {% include "wiki/tabs/wysiwyg_editor.html" %}
-       {% include "wiki/tabs/source_editor.html" %}
+    {% include "wiki/tabs/source_editor.html" %}
     {% include "wiki/tabs/history_view.html" %}
 {% endblock %}
 
 {% endblock %}
 
 {% block dialogs %}
-       {% include "wiki/save_dialog.html" %}
-       {% include "wiki/tag_dialog.html" %}
+    {% include "wiki/save_dialog.html" %}
+    {% include "wiki/revert_dialog.html" %}
+    {% include "wiki/tag_dialog.html" %}
+    {% if can_pubmark %}
+        {% include "wiki/pubmark_dialog.html" %}
+    {% endif %}
 {% endblock %}
index 0323133..dbbe7a1 100644 (file)
@@ -1,7 +1,7 @@
 {% extends "base.html" %}
 {% load toolbar_tags i18n %}
 
-{% block title %}{{ document.name }} - {{ block.super }}{% endblock %}
+{% block title %}{{ book.title }} - {{ block.super }}{% endblock %}
 {% block extrahead %}
 {% load compressed %}
 {% compressed_css 'detail' %}
 
 {% block maincontent %}
 <div id="document-meta"
-       data-document-name="{{ document.name }}" style="display:none">
+       data-chunk-id="{{ chunk.pk }}" style="display:none">
 
-       {% for k, v in document_meta.items %}
-               <span data-key="{{ k }}">{{ v }}</span>
-       {% endfor %}
-
-       {% for k, v in document_info.items %}
-               <span data-key="{{ k }}">{{ v }}</span>
-       {% endfor %}
+       <span data-key="gallery">{{ chunk.book.gallery }}</span>
+       <span data-key="gallery-start">{% if chunk.gallery_start %}{{ chunk.gallery_start }}{% endif %}</span>
+       <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 wiki_document_list %}"><img src="{{STATIC_URL}}icons/go-home.png"/><a href="{% url wiki_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>
     <div id="tools">
         <a href="{{ REDMINE_URL }}projects/wl-publikacje/wiki/Pomoc" target="_blank">
         {% trans "Help" %}</a>
diff --git a/apps/wiki/templates/wiki/document_list.html b/apps/wiki/templates/wiki/document_list.html
deleted file mode 100644 (file)
index 6853801..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-{% extends "wiki/base.html" %}
-
-{% load i18n %}
-{% load wiki %}
-
-{% block extrabody %}
-{{ block.super }}
-<script type="text/javascript" charset="utf-8">
-$(function() {
-       function search(event) {
-        event.preventDefault();
-        var expr = new RegExp(slugify($('#file-list-filter').val()), 'i');
-        $('#file-list tbody tr').hide().filter(function(index) {
-            return expr.test(slugify( $('a', this).attr('data-id') ));
-        }).show();
-    }
-
-    $('#file-list-find-button').click(search).hide();
-       $('#file-list-filter').bind('keyup change DOMAttrModified', search);
-});
-</script>
-{% endblock %}
-
-{% block leftcolumn %}
-       <form method="get" action="#">
-    <table  id="file-list">
-       <thead>
-               <tr><th>Filtr:</th>
-                       <th><input autocomplete="off" name="filter" id="file-list-filter" type="text" size="40" /></th>
-                       <th><input type="reset" value="{% trans "Clear filter" %}" id="file-list-reset-button"/></th>
-                       </tr>
-               </thead>
-               <tbody>
-       {% for doc in docs %}
-            <tr>
-               <td colspan="3"><a target="_blank" data-id="{{doc}}"
-                                       href="{% url wiki_editor doc %}">{{ doc|wiki_title }}</a></td>
-                               <!-- placeholder </td> -->
-                       </tr>
-       {% endfor %}
-               </tbody>
-    </table>
-       </form>
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-       <div id="last-edited-list">
-               <h2>{% trans "Your last edited documents" %}</h2>
-           <ol>
-                       {% for name, date in last_docs %}
-                       <li><a href="{% url wiki_editor name %}"
-                               target="_blank">{{ name|wiki_title }}</a><br/><span class="date">({{ date|date:"H:i:s, d/m/Y" }})</span></li>
-                       {% endfor %}
-               </ol>
-       </div>
-{% endblock rightcolumn %}
diff --git a/apps/wiki/templates/wiki/document_upload.html b/apps/wiki/templates/wiki/document_upload.html
deleted file mode 100644 (file)
index d4c89d3..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-{% extends "wiki/base.html" %}
-{% load i18n %}
-{% load wiki %}
-
-
-{% block leftcolumn %}
-
-
-<h2>{% trans "Bulk documents upload" %}</h2>
-
-<p>
-{% 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.as_p }}
-<p><button type="submit">{% trans "Upload" %}</button></p>
-</form>
-
-<hr/>
-
-{% if error_list %}
-
-    <p class='error'>{% trans "There have been some errors. No files have been added to the repository." %}
-    <h3>{% trans "Offending files" %}</h3>
-    <ul id='error-list'>
-        {% for filename, title, error in error_list %}
-            <li>{{title|wiki_title}} (<code>{{ filename }}</code>): {{ error }}</li>
-        {% endfor %}
-    </ul>
-
-    {% if ok_list %}
-    <h3>{% trans "Correct files" %}</h3>
-        <ul>
-            {% for filename, title in ok_list %}
-                <li>{{title|wiki_title}} (<code>{{ filename }}</code>)</li>
-            {% endfor %}
-        </ul>
-    {% endif %}
-
-{% else %}
-
-    {% if ok_list %}
-        <p class='success'>{% trans "Files have been successfully uploaded to the repository." %}</p>
-        <h3>{% trans "Uploaded files" %}</h3>
-        <ul id='ok-list'>
-        {% for filename, title in ok_list %}
-            <li><a href='{% url wiki_editor title %}'>{{ title|wiki_title }}</a> (<code>{{ filename }})</a></li>
-        {% endfor %}
-        </ul>
-    {% endif %}
-{% endif %}
-
-{% if skipped_list %}
-    <h3>{% trans "Skipped files" %}</h3>
-    <p>{% trans "Files skipped due to no <code>.xml</code> extension" %}</p>
-    <ul id='skipped-list'>
-        {% for filename in skipped_list %}
-            <li>{{ filename }}</li>
-        {% endfor %}
-    </ul>
-{% endif %}
-
-
-{% endblock leftcolumn %}
-
-
-{% block rightcolumn %}
-{% endblock rightcolumn %}
diff --git a/apps/wiki/templates/wiki/pubmark_dialog.html b/apps/wiki/templates/wiki/pubmark_dialog.html
new file mode 100755 (executable)
index 0000000..a70a0c3
--- /dev/null
@@ -0,0 +1,20 @@
+{% load i18n %}
+<div id="pubmark_dialog" class="dialog" data-ui-jsclass="PubmarkDialog">
+       <form method="POST" action="#">
+    {% csrf_token %}
+               {% for field in forms.pubmark.visible_fields %}
+               <p>{{ field.label_tag }} {{ field }} <span data-ui-error-for="{{ field.name }}"> </span></p>
+               <p>{{ field.help_text }}</p>
+               {% endfor %}
+
+               {% for f in forms.pubmark.hidden_fields %}
+                       {{ f }}
+               {% endfor %}
+               <p data-ui-error-for="__all__"> </p>
+
+               <p class="action_area">
+                       <button type="submit" class="ok" data-ui-action="save">{% trans "Save" %}</button>
+                       <button type="button" class="cancel" data-ui-action="cancel">{% trans "Cancel" %}</button>
+               </p>
+       </form>
+</div>
diff --git a/apps/wiki/templates/wiki/revert_dialog.html b/apps/wiki/templates/wiki/revert_dialog.html
new file mode 100644 (file)
index 0000000..c2fc155
--- /dev/null
@@ -0,0 +1,43 @@
+{% load i18n %}
+<div id="revert_dialog" class="dialog" data-ui-jsclass="RevertDialog">
+       <form method="POST" action="">
+    {% csrf_token %}
+       <p>{{ forms.text_revert.comment.label }}</p>
+       <p class="help_text">
+               {{ forms.text_revert.comment.help_text}}
+               <span data-ui-error-for="{{ forms.text_revert.comment.name }}"> </span>
+       </p>
+       {{forms.text_revert.comment }}
+
+
+
+       {% if request.user.is_anonymous %}
+    <table style='margin:0 4%;'>
+    <tr>
+        <td>{{ forms.text_revert.author_name.label }}:</td>
+        <td>{{ forms.text_revert.author_name }}
+        <span class="help_text">{{ forms.text_revert.author_name.help_text }}</span>
+        <span data-ui-error-for="{{ forms.text_revert.author_name.name }}"> </span></td>
+    </tr>
+    <tr>
+        <td>{{ forms.text_revert.author_email.label }}:</td>
+        <td>{{ forms.text_revert.author_email }}
+        <span class="help_text">{{ forms.text_revert.author_email.help_text }}</span>
+        <span data-ui-error-for="{{ forms.text_revert.author_email.name }}"> </span></td>
+    </tr>
+    </table>
+       {% endif %}
+
+
+       {% for f in forms.text_revert.hidden_fields %}
+               {{ f }}
+       {% endfor %}
+
+       <p data-ui-error-for="__all__"> </p>
+
+       <p class="action_area">
+               <button type="submit" class"ok" data-ui-action="revert">{% trans "Revert" %}</button>
+               <button type="button" class="cancel" data-ui-action="cancel">{% trans "Cancel" %}</button>
+       </p>
+       </form>
+</div>
index b2f53ba..31c5b01 100644 (file)
@@ -1,6 +1,7 @@
 {% load i18n %}
 <div id="save_dialog" class="dialog" data-ui-jsclass="SaveDialog">
        <form method="POST" action="">
+    {% csrf_token %}
        <p>{{ forms.text_save.comment.label }}</p>
        <p class="help_text">
                {{ forms.text_save.comment.help_text}}
                <span class="help_text">{{ forms.text_save.stage_completed.help_text }}</span>
                <span data-ui-error-for="{{ forms.text_save.stage_completed.name }}"> </span>
        </p>
+    {% if can_pubmark %}
+       <p>
+               {{ forms.text_save.publishable.label_tag }}:
+               {{ forms.text_save.publishable }}
+               <span class="help_text">{{ forms.text_save.publishable.help_text }}</span>
+               <span data-ui-error-for="{{ forms.text_save.publishable.name }}"> </span>
+       </p>
+    {% endif %}
+
        {% endif %}
 
 
index c118d0f..f7a0851 100644 (file)
@@ -2,10 +2,11 @@
 <div id="side-annotations">
     <!-- annotations toolbar -->
     <div class="toolbar">
-        <button class="refresh" title="Przypisy autorskie">pa</button>
-        <button class="refresh active" title="Przypisy edytorskie">pe</button>
-        <button class="refresh" title="Przypisy redakcyjne">pr</button>
-        <button class="refresh" title="Przypisy tłumacza">pt</button>
+        <button class="refresh" title="Przypisy autorskie" data-tag="pa">pa</button>
+        <button class="refresh active" title="Przypisy edytorskie" data-tag="pe">pe</button>
+        <button class="refresh" title="Przypisy redakcyjne" data-tag="pr">pr</button>
+        <button class="refresh" title="Przypisy tłumacza" data-tag="pt">pt</button>
+        <button class="refresh" title="Wszystkie przypisy" data-tag="pa,pe,pr,pt">{% trans "all" %}</button>
         <div class="toolbar-end">
         </div>
     </div>
index b4be533..1176797 100644 (file)
@@ -7,7 +7,7 @@
                alt="{% trans "Previous" %}" title="{% trans "Previous" %}"/>
         </button>
         <input type="text" size="3" maxlength="3" value="0" class="page-number" />
-        <span id="imagesCount" id="">/1</span>
+        <span id="imagesCount" id="">/0</span>
         <button class="next-page">
             <img src="{{STATIC_URL}}icons/go-next.png"
                alt="{% trans "Next" %}" title="{% trans "Next" %}"/>
index d9b74dc..0d662a7 100644 (file)
@@ -3,13 +3,15 @@
     <div class="toolbar">
        <button type="button" id="make-diff-button"
                        data-enabled-when="2" disabled="disabled">{% trans "Compare versions" %}</button>
-               <button type="button" id="tag-changeset-button"
-                       data-enabled-when="1" disabled="disabled">{% trans "Mark version" %}</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_editor_readonly document_name %}">{% trans "View version" %}</button>
+                       data-basehref="{% url wiki_editor_readonly chunk.book.slug chunk.slug %}">{% trans "View version" %}</button>
 
        </div>
     <div id="history-view">
                        <tr class="entry row-stub">
                        <td data-stub-value="version"></td>
                        <td>
-                               <span data-stub-value="description"></span>
+                <span data-stub-value="date"></span>
+               <br/><span data-stub-value="author"></span>
                                <br />
-               <span data-stub-value="author"></span>, <span data-stub-value="date"></span>
+                               <span data-stub-value="description"></span>
                        </td>
-                       <td data-stub-value="tag">
+                       <td>
+                <div data-stub-value="publishable"></div>
+                <div data-stub-value="tag"></div>
                        </td>
                </tr>
                </tbody>
index 72d881c..9b22825 100644 (file)
@@ -1,10 +1,5 @@
 {% load toolbar_tags i18n %}
 <div id="source-editor" class="editor">
-    {% if not document_info.readonly %}{% toolbar %}{% endif %}
+    {% if not readonly %}{% toolbar %}{% endif %}
     <textarea id="codemirror_placeholder">&lt;br/&gt;</textarea>
-    <!-- <input type="hidden" name="name" value="{{ document.name }}" />
-       <input type="hidden" name="author" value="annonymous" />
-       <input type="hidden" name="comment" value="no comment" />
-       <input type="hidden" name="revision" value="{{ document_info.revision }}" />
-       -->
-</div>
\ No newline at end of file
+</div>
index c33baec..bcea34d 100644 (file)
@@ -1,5 +1,4 @@
 {% load i18n %}
-{% load wiki %}
 <div id="summary-view-editor" class="editor" style="display: none">
     <!-- <div class="toolbar">
     </div> -->
@@ -9,25 +8,29 @@
                <h2>
                        <label for="title">{% trans "Title" %}:</label>
                        <span data-ui-editable="true" data-edit-target="meta.displayTitle"
-                       >{{ document.name|wiki_title }}</span>
+                       >{{ chunk.pretty_name }}</span>
                </h2>
+        <p><a href="{{ chunk.book.get_absolute_url }}">{% trans "Go to the book's page" %}</a>
+        </p>
                <p>
                        <label>{% trans "Document ID" %}:</label>
-                       <span>{{ document.name }}</span>
+                       <span>{{ chunk.book.slug }}/{{ chunk.slug }}</span>
                </p>
                <p>
                        <label>{% trans "Current version" %}:</label>
-                       {{ document_info.revision }} ({{document_info.date}})
+                       {{ chunk.revision }} ({{ chunk.head.created_at }})
                <p>
                        <label>{% trans "Last edited by" %}:</label>
-                       {{document_info.author}}
+                       {{ chunk.head.author }}
                </p>
                <p>
                        <label for="gallery">{% trans "Link to gallery" %}:</label>
                        <span data-ui-editable="true" data-edit-target="meta.galleryLink"
-                       >{{ document_meta.gallery}}</span>
+                       >{{ chunk.book.gallery }}</span>
+               </p>
+               <p>
+                   <label>{% trans "Characters in document" %}:</label>
+                   <span id="charcount"></span> (<span id="charcount_pages"></span> {% trans "pages" %}<span id="charcount_untagged">, {% trans "untagged" %}</span>)
                </p>
-
-               <p><button type="button" id="publish_button">{% trans "Publish" %}</button></p>
        </div>
 </div>
index 2b4daeb..856b3d7 100644 (file)
@@ -1,5 +1,4 @@
 {% load i18n %}
-{% load wiki %}
 <li id="SummaryPerspective" data-ui-related="summary-view-editor" data-ui-jsclass="SummaryPerspective">
     <span>{% trans "Summary" %}</span>
 </li>
index 9f6232c..f54f3fb 100644 (file)
@@ -4,15 +4,12 @@
     </div>
 
        <div class="toolbar">
-       {% if not document_info.readonly %}
+       {% if not readonly %}
         <button id="insert-theme-button">
             {% trans "Insert theme" %}
         </button>
         <button id="insert-annotation-button">
             {% trans "Insert annotation" %}
-        </button>
-        <button id="insert-symbol-button">
-            {% trans "Insert special character" %}
         </button>
                {% endif %}
         <div class="toolbar-end">
diff --git a/apps/wiki/templates/wiki/tag_dialog.html b/apps/wiki/templates/wiki/tag_dialog.html
deleted file mode 100644 (file)
index bc601cb..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-{% load i18n %}
-<div id="add_tag_dialog" class="dialog" data-ui-jsclass="AddTagDialog">
-       <form method="POST" action="#">
-               {% for field in forms.add_tag.visible_fields %}
-               <p>{{ field.label_tag }} {{ field }} <span data-ui-error-for="{{ field.name }}"> </span></p>
-               <p>{{ field.help_text }}</p>
-               {% endfor %}
-
-               {% for f in forms.add_tag.hidden_fields %}
-                       {{ f }}
-               {% endfor %}
-               <p data-ui-error-for="__all__"> </p>
-
-               <p class="action_area">
-                       <button type="submit" class"ok" data-ui-action="save">{% trans "Save" %}</button>
-                       <button type="button" class="cancel" data-ui-action="cancel">{% trans "Cancel" %}</button>
-               </p>
-       </form>
-</div>
diff --git a/apps/wiki/templatetags/__init__.py b/apps/wiki/templatetags/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/apps/wiki/templatetags/wiki.py b/apps/wiki/templatetags/wiki.py
deleted file mode 100644 (file)
index cb5bf20..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-from __future__ import absolute_import
-
-from django.template.defaultfilters import stringfilter
-from django import template
-
-register = template.Library()
-
-from wiki.models import split_name
-
-@register.filter
-@stringfilter
-def wiki_title(value):
-    parts = (p.replace('_', ' ').title() for p in split_name(value))
-    return ' / '.join(parts)
diff --git a/apps/wiki/tests.py b/apps/wiki/tests.py
deleted file mode 100644 (file)
index 6577737..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-from nose.tools import *
-import wiki.models as models
-import shutil
-import tempfile
-
-
-class TestStorageBase:
-    def setUp(self):
-        self.dirpath = tempfile.mkdtemp(prefix='nosetest_')
-
-    def tearDown(self):
-        shutil.rmtree(self.dirpath)
-
-
-class TestDocumentStorage(TestStorageBase):
-
-    def test_storage_empty(self):
-        storage = models.DocumentStorage(self.dirpath)
-        eq_(storage.all(), [])
index a84330a..211bb3a 100644 (file)
@@ -1,49 +1,31 @@
 # -*- coding: utf-8
 from django.conf.urls.defaults import *
-from django.views.generic.simple import redirect_to
-from django.conf import settings
 
 
-PART = ur"""[ ĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9\w_.-]+"""
-
 urlpatterns = patterns('wiki.views',
-    url(r'^$', redirect_to, {'url': 'catalogue/'}),
-
-    url(r'^catalogue/$', 'document_list', name='wiki_document_list'),
-    url(r'^catalogue/([^/]+)/$', 'document_list'),
-    url(r'^catalogue/([^/]+)/([^/]+)/$', 'document_list'),
-    url(r'^catalogue/([^/]+)/([^/]+)/([^/]+)$', 'document_list'),
-
-    url(r'^(?P<name>%s)$' % PART,
+    url(r'^edit/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
         'editor', name="wiki_editor"),
 
-    url(r'^(?P<name>[^/]+)/readonly$',
+    url(r'^readonly/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
         'editor_readonly', name="wiki_editor_readonly"),
 
-    url(r'^upload/$',
-        'upload', name='wiki_upload'),
-
-    url(r'^create/(?P<name>[^/]+)',
-        'create_missing', name='wiki_create_missing'),
-
-    url(r'^(?P<directory>[^/]+)/gallery$',
+    url(r'^gallery/(?P<directory>[^/]+)/$',
         'gallery', name="wiki_gallery"),
 
-    url(r'^(?P<name>[^/]+)/history$',
+    url(r'^history/(?P<chunk_id>\d+)/$',
         'history', name="wiki_history"),
 
-    url(r'^(?P<name>[^/]+)/rev$',
+    url(r'^rev/(?P<chunk_id>\d+)/$',
         'revision', name="wiki_revision"),
 
-    url(r'^(?P<name>[^/]+)/text$',
+    url(r'^text/(?P<chunk_id>\d+)/$',
         'text', name="wiki_text"),
 
-    url(r'^(?P<name>[^/]+)/publish$', 'publish', name="wiki_publish"),
-    url(r'^(?P<name>[^/]+)/publish/(?P<version>\d+)$', 'publish', name="wiki_publish"),
-
-    url(r'^(?P<name>[^/]+)/diff$', 'diff', name="wiki_diff"),
-    url(r'^(?P<name>[^/]+)/tags$', 'add_tag', name="wiki_add_tag"),
-
+    url(r'^revert/(?P<chunk_id>\d+)/$',
+        'revert', name='wiki_revert'),
 
+    url(r'^diff/(?P<chunk_id>\d+)/$', 'diff', name="wiki_diff"),
+    url(r'^pubmark/(?P<chunk_id>\d+)/$', 'pubmark', name="wiki_pubmark"),
 
+    url(r'^themes$', 'themes', name="themes"),
 )
index 7d60341..0356d50 100644 (file)
+from datetime import datetime
 import os
-import functools
 import logging
-logger = logging.getLogger("fnp.wiki")
 
 from django.conf import settings
-
-from django.views.generic.simple import direct_to_template
-from django.views.decorators.http import require_POST, require_GET
 from django.core.urlresolvers import reverse
-from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
-                ajax_require_permission, recursive_groupby)
 from django import http
-
-from wiki.models import getstorage, DocumentNotFound, normalize_name, split_name, join_name, Theme
-from wiki.forms import DocumentTextSaveForm, DocumentTagForm, DocumentCreateForm, DocumentsUploadForm
-from datetime import datetime
-from django.utils.encoding import smart_unicode
-from django.utils.translation import ugettext_lazy as _
-from django.utils.decorators import decorator_from_middleware
+from django.http import Http404, HttpResponseForbidden
 from django.middleware.gzip import GZipMiddleware
+from django.utils.decorators import decorator_from_middleware
+from django.utils.encoding import smart_unicode
+from django.utils.formats import localize
+from django.utils.translation import ugettext as _
+from django.views.decorators.http import require_POST, require_GET
+from django.views.generic.simple import direct_to_template
+from django.shortcuts import get_object_or_404
 
+from catalogue.models import Book, Chunk
+import nice_diff
+from wiki import forms
+from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
+                ajax_require_permission)
+from wiki.models import Theme
 
 #
 # Quick hack around caching problems, TODO: use ETags
 #
 from django.views.decorators.cache import never_cache
 
-import wlapi
-import nice_diff
-import operator
+logger = logging.getLogger("fnp.wiki")
 
 MAX_LAST_DOCS = 10
 
 
-def normalized_name(view):
-
-    @functools.wraps(view)
-    def decorated(request, name, *args):
-        normalized = normalize_name(name)
-        logger.debug('View check %r -> %r', name, normalized)
-
-        if normalized != name:
-            return http.HttpResponseRedirect(
-                        reverse('wiki_' + view.__name__, kwargs={'name': normalized}))
-
-        return view(request, name, *args)
-
-    return decorated
-
-
-@never_cache
-def document_list(request):
-    return direct_to_template(request, 'wiki/document_list.html', extra_context={
-        'docs': getstorage().all(),
-        'last_docs': sorted(request.session.get("wiki_last_docs", {}).items(),
-                        key=operator.itemgetter(1), reverse=True),
-    })
-
-
 @never_cache
-@normalized_name
-def editor(request, name, template_name='wiki/document_details.html'):
-    storage = getstorage()
-
+def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
     try:
-        document = storage.get(name)
-    except DocumentNotFound:
-        return http.HttpResponseRedirect(reverse("wiki_create_missing", args=[name]))
+        chunk = Chunk.get(slug, chunk)
+    except Chunk.MultipleObjectsReturned:
+        # TODO: choice page
+        raise Http404
+    except Chunk.DoesNotExist:
+        if chunk is None:
+            try:
+                book = Book.objects.get(slug=slug)
+            except Book.DoesNotExist:
+                return http.HttpResponseRedirect(reverse("catalogue_create_missing", args=[slug]))
+        else:
+            raise Http404
+    if not chunk.book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
 
     access_time = datetime.now()
-    last_documents = request.session.get("wiki_last_docs", {})
-    last_documents[name] = access_time
+    last_books = request.session.get("wiki_last_books", {})
+    last_books[slug, chunk.slug] = {
+        'time': access_time,
+        'title': chunk.pretty_name(),
+        }
 
-    if len(last_documents) > MAX_LAST_DOCS:
-        oldest_key = min(last_documents, key=last_documents.__getitem__)
-        del last_documents[oldest_key]
-    request.session['wiki_last_docs'] = last_documents
+    if len(last_books) > MAX_LAST_DOCS:
+        oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
+        del last_books[oldest_key]
+    request.session['wiki_last_books'] = last_books
 
     return direct_to_template(request, template_name, extra_context={
-        'document': document,
-        'document_name': document.name,
-        'document_info': document.info,
-        'document_meta': document.meta,
+        'chunk': chunk,
         'forms': {
-            "text_save": DocumentTextSaveForm(prefix="textsave"),
-            "add_tag": DocumentTagForm(prefix="addtag"),
+            "text_save": forms.DocumentTextSaveForm(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'),
         'REDMINE_URL': settings.REDMINE_URL,
     })
 
 
 @require_GET
-@normalized_name
-def editor_readonly(request, name, template_name='wiki/document_details_readonly.html'):
-    name = normalize_name(name)
-    storage = getstorage()
-
+def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
     try:
+        chunk = Chunk.get(slug, chunk)
         revision = request.GET['revision']
-        document = storage.get(name, revision)
-    except (KeyError, DocumentNotFound):
-        raise http.Http404
+    except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
+        raise Http404
+    if not chunk.book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
 
     access_time = datetime.now()
-    last_documents = request.session.get("wiki_last_docs", {})
-    last_documents[name] = access_time
+    last_books = request.session.get("wiki_last_books", {})
+    last_books[slug, chunk.slug] = {
+        'time': access_time,
+        'title': chunk.book.title,
+        }
 
-    if len(last_documents) > MAX_LAST_DOCS:
-        oldest_key = min(last_documents, key=last_documents.__getitem__)
-        del last_documents[oldest_key]
-    request.session['wiki_last_docs'] = last_documents
+    if len(last_books) > MAX_LAST_DOCS:
+        oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
+        del last_books[oldest_key]
+    request.session['wiki_last_books'] = last_books
 
     return direct_to_template(request, template_name, extra_context={
-        'document': document,
-        'document_name': document.name,
-        'document_info': dict(document.info(), readonly=True),
-        'document_meta': document.meta,
+        'chunk': chunk,
+        'revision': revision,
+        'readonly': True,
         'REDMINE_URL': settings.REDMINE_URL,
     })
 
 
-@normalized_name
-def create_missing(request, name):
-    storage = getstorage()
-
-    if request.method == "POST":
-        form = DocumentCreateForm(request.POST, request.FILES)
-        if form.is_valid():
-            doc = storage.create_document(
-                name=form.cleaned_data['id'],
-                text=form.cleaned_data['text'],
-            )
-
-            return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.name]))
-    else:
-        form = DocumentCreateForm(initial={
-                "id": name.replace(" ", "_"),
-                "title": name.title(),
-        })
-
-    return direct_to_template(request, "wiki/document_create_missing.html", extra_context={
-        "document_name": name,
-        "form": form,
-    })
-
-
-def upload(request):
-    storage = getstorage()
-
-    if request.method == "POST":
-        form = DocumentsUploadForm(request.POST, request.FILES)
-        if form.is_valid():
-            zip = form.cleaned_data['zip']
-            skipped_list = []
-            ok_list = []
-            error_list = []
-            titles = {}
-            existing = storage.all()
-            for filename in zip.namelist():
-                if filename[-1] == '/':
-                    continue
-                title = normalize_name(os.path.basename(filename)[:-4])
-                if not (title and filename.endswith('.xml')):
-                    skipped_list.append(filename)
-                elif title in titles:
-                    error_list.append((filename, title, _('Title already used for %s' % titles[title])))
-                elif title in existing:
-                    error_list.append((filename, title, _('Title already used in repository.')))
-                else:
-                    try:
-                        zip.read(filename).decode('utf-8') # test read
-                        ok_list.append((filename, title))
-                    except UnicodeDecodeError:
-                        error_list.append((filename, title, _('File should be UTF-8 encoded.')))
-                    titles[title] = filename
-
-            if not error_list:
-                for filename, title in ok_list:
-                    storage.create_document(
-                        name=title,
-                        text=zip.read(filename).decode('utf-8')
-                    )
-
-            return direct_to_template(request, "wiki/document_upload.html", extra_context={
-                "form": form,
-                "ok_list": ok_list,
-                "skipped_list": skipped_list,
-                "error_list": error_list,
-            })
-                #doc = storage.create_document(
-                #    name=base,
-                #    text=form.cleaned_data['text'],
-
-            
-            return http.HttpResponse('\n'.join(yeslist) + '\n\n' + '\n'.join(nolist))
-    else:
-        form = DocumentsUploadForm()
-
-    return direct_to_template(request, "wiki/document_upload.html", extra_context={
-        "form": form,
-    })
-
-
 @never_cache
-@normalized_name
 @decorator_from_middleware(GZipMiddleware)
-def text(request, name):
-    storage = getstorage()
+def text(request, chunk_id):
+    doc = get_object_or_404(Chunk, pk=chunk_id)
+    if not doc.book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
 
     if request.method == 'POST':
-        form = DocumentTextSaveForm(request.POST, prefix="textsave")
+        form = forms.DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave")
         if form.is_valid():
-            revision = form.cleaned_data['parent_revision']
-            document = storage.get_or_404(name, revision)          
-            document.text = form.cleaned_data['text']
-            comment = form.cleaned_data['comment']
-            if form.cleaned_data['stage_completed']:        
-                comment += '\n#stage-finished: %s\n' % form.cleaned_data['stage_completed']         
             if request.user.is_authenticated():
-                author_name = request.user
-                author_email = request.user.email
+                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:
-                author_name = form.cleaned_data['author_name']
-                author_email = form.cleaned_data['author_email']
-            author = "%s <%s>" % (author_name, author_email)
-            storage.put(document, author=author, comment=comment, parent=revision)           
-            document = storage.get(name)          
+                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'))
+            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': document.plain_text if revision != document.revision else None,
-                'meta': document.meta(),
-                'revision': document.revision,
+                'text': doc.materialize() if parent_revision != revision else None,
+                'meta': {},
+                'revision': revision,
             })
         else:
             return JSONFormInvalid(form)
     else:
         revision = request.GET.get("revision", None)
-
+        
         try:
-            try:
-                revision = revision and int(revision)
-                logger.info("Fetching %s", revision)
-                document = storage.get(name, revision)
-            except ValueError:
-                # treat as a tag
-                logger.info("Fetching tag %s", revision)
-                document = storage.get_by_tag(name, revision)
-        except DocumentNotFound:
-            raise http.Http404
+            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': document.plain_text,
-            'meta': document.meta(),
-            'revision': document.revision,
+            'text': text,
+            'meta': {},
+            'revision': revision,
         })
 
 
 @never_cache
-@normalized_name
 @require_POST
-def revert(request, name):
-    storage = getstorage()
-    revision = request.POST['target_revision']
+def revert(request, chunk_id):
+    form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
+    if form.is_valid():
+        doc = get_object_or_404(Chunk, pk=chunk_id)
+        if not doc.book.accessible(request):
+            return HttpResponseForbidden("Not authorized.")
 
-    try:
-        document = storage.revert(name, revision)
+        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", chunk_id, revision)
+        doc.at_revision(revision).revert(author=author, description=comment)
 
         return JSONResponse({
-            'text': document.plain_text if revision != document.revision else None,
-            'meta': document.meta(),
-            'revision': document.revision,
+            'text': doc.materialize() if before != doc.revision() else None,
+            'meta': {},
+            'revision': doc.revision(),
         })
-    except DocumentNotFound:
-        raise http.Http404
+    else:
+        return JSONFormInvalid(form)
+
 
 @never_cache
 def gallery(request, directory):
@@ -294,6 +218,10 @@ def gallery(request, directory):
 
         images = [map_to_url(f) for f in map(smart_unicode, os.listdir(base_dir)) if is_image(f)]
         images.sort()
+
+        if not request.user.is_authenticated():
+            return HttpResponseForbidden("Not authorized.")
+
         return JSONResponse(images)
     except (IndexError, OSError):
         logger.exception("Unable to fetch gallery")
@@ -301,10 +229,7 @@ def gallery(request, directory):
 
 
 @never_cache
-@normalized_name
-def diff(request, name):
-    storage = getstorage()
-
+def diff(request, chunk_id):
     revA = int(request.GET.get('from', 0))
     revB = int(request.GET.get('to', 0))
 
@@ -314,68 +239,70 @@ def diff(request, name):
     if revB == 0:
         revB = None
 
-    docA = storage.get_or_404(name, int(revA))
-    docB = storage.get_or_404(name, int(revB))
+    doc = get_object_or_404(Chunk, pk=chunk_id)
+    if not doc.book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
 
-    return http.HttpResponse(nice_diff.html_diff_table(docA.plain_text.splitlines(),
-                                         docB.plain_text.splitlines(), context=3))
+    # 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))
 
-@never_cache
-@normalized_name
-def revision(request, name):
-    storage = getstorage()
 
-    try:
-        return http.HttpResponse(str(storage.doc_meta(name)['revision']))
-    except DocumentNotFound:
-        raise http.Http404
+@never_cache
+def revision(request, chunk_id):
+    doc = get_object_or_404(Chunk, pk=chunk_id)
+    if not doc.book.accessible(request):
+        return HttpResponseForbidden("Not authorized.")
+    return http.HttpResponse(str(doc.revision()))
 
 
 @never_cache
-@normalized_name
-def history(request, name):
-    storage = getstorage()
-
+def history(request, chunk_id):
     # TODO: pagination
-    changesets = list(storage.history(name))
-
-    return JSONResponse(changesets)
+    doc = get_object_or_404(Chunk, pk=chunk_id)
+    if not doc.book.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)
 
 
 @require_POST
-@ajax_require_permission('wiki.can_change_tags')
-def add_tag(request, name):
-    name = normalize_name(name)
-    storage = getstorage()
-
-    form = DocumentTagForm(request.POST, prefix="addtag")
+@ajax_require_permission('catalogue.can_pubmark')
+def pubmark(request, chunk_id):
+    form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
     if form.is_valid():
-        doc = storage.get_or_404(form.cleaned_data['id'])
-        doc.add_tag(tag=form.cleaned_data['tag'],
-                    revision=form.cleaned_data['revision'],
-                    author=request.user.username)
-        return JSONResponse({"message": _("Tag added")})
+        doc = get_object_or_404(Chunk, pk=chunk_id)
+        if not doc.book.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)
 
 
-@require_POST
-@ajax_require_permission('wiki.can_publish')
-def publish(request, name):
-    name = normalize_name(name)
-
-    storage = getstorage()
-    document = storage.get_by_tag(name, "ready_to_publish")
-
-    api = wlapi.WLAPI(**settings.WL_API_CONFIG)
-
-    try:
-        return JSONResponse({"result": api.publish_book(document)})
-    except wlapi.APICallException, e:
-        return JSONServerError({"message": str(e)})
-
-
 def themes(request):
     prefix = request.GET.get('q', '')
     return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))
diff --git a/apps/wiki_img/admin.py b/apps/wiki_img/admin.py
deleted file mode 100644 (file)
index 80f7b36..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-from django.contrib.admin import site
-from wiki_img.models import ImageDocument
-
-site.register(ImageDocument)
diff --git a/apps/wiki_img/constants.py b/apps/wiki_img/constants.py
deleted file mode 100644 (file)
index 6781a48..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-from django.utils.translation import ugettext_lazy as _
-
-DOCUMENT_STAGES = (
-    ("", u"-----"),
-    ("first_correction", _(u"First correction")),
-    ("tagging", _(u"Tagging")),
-    ("proofreading", _(u"Initial Proofreading")),
-    ("annotation-proofreading", _(u"Annotation Proofreading")),
-    ("modernisation", _(u"Modernisation")),
-    ("annotations", _(u"Annotations")),
-    ("themes", _(u"Themes")),
-    ("editor-proofreading", _(u"Editor's Proofreading")),
-    ("technical-editor-proofreading", _(u"Technical Editor's Proofreading")),
-)
-
-DOCUMENT_TAGS = DOCUMENT_STAGES + \
-    (("ready-to-publish", _(u"Ready to publish")),)
-
-DOCUMENT_TAGS_DICT = dict(DOCUMENT_TAGS)
-DOCUMENT_STAGES_DICT = dict(DOCUMENT_STAGES)
index bc9e2d6..555f264 100644 (file)
@@ -4,94 +4,17 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django import forms
-from wiki.constants import DOCUMENT_TAGS, DOCUMENT_STAGES
 from django.utils.translation import ugettext_lazy as _
+from wiki.forms import DocumentTextSaveForm
+from catalogue.models import Image
 
 
-class DocumentTagForm(forms.Form):
-    """
-        Form for tagging revisions.
-    """
+class ImageSaveForm(DocumentTextSaveForm):
+    """Form for saving document's text."""
 
-    id = forms.CharField(widget=forms.HiddenInput)
-    tag = forms.ChoiceField(choices=DOCUMENT_TAGS)
-    revision = forms.IntegerField(widget=forms.HiddenInput)
-
-
-class DocumentCreateForm(forms.Form):
-    """
-        Form used for creating new documents.
-    """
-    title = forms.CharField()
-    id = forms.RegexField(regex=ur"^[-\wąćęłńóśźżĄĆĘŁŃÓŚŹŻ]+$")
-    file = forms.FileField(required=False)
-    text = forms.CharField(required=False, widget=forms.Textarea)
-
-    def clean(self):
-        file = self.cleaned_data['file']
-
-        if file is not None:
-            try:
-                self.cleaned_data['text'] = file.read().decode('utf-8')
-            except UnicodeDecodeError:
-                raise forms.ValidationError("Text file must be UTF-8 encoded.")
-
-        if not self.cleaned_data["text"]:
-            raise forms.ValidationError("You must either enter text or upload a file")
-
-        return self.cleaned_data
-
-
-class DocumentsUploadForm(forms.Form):
-    """
-        Form used for uploading new documents.
-    """
-    file = forms.FileField(required=True, label=_('ZIP file'))
-
-    def clean(self):
-        file = self.cleaned_data['file']
-
-        import zipfile
-        try:
-            z = self.cleaned_data['zip'] = zipfile.ZipFile(file)
-        except zipfile.BadZipfile:
-            raise forms.ValidationError("Should be a ZIP file.")
-        if z.testzip():
-            raise forms.ValidationError("ZIP file corrupt.")
-
-        return self.cleaned_data
-
-
-class DocumentTextSaveForm(forms.Form):
-    """
-    Form for saving document's text:
-
-        * name - document's storage identifier.
-        * parent_revision - revision which the modified text originated from.
-        * comment - user's verbose comment; will be used in commit.
-        * stage_completed - mark this change as end of given stage.
-
-    """
-
-    id = forms.CharField(widget=forms.HiddenInput)
-    parent_commit = forms.IntegerField(widget=forms.HiddenInput)
-    text = forms.CharField(widget=forms.HiddenInput)
-
-    author_name = forms.CharField(
+    stage_completed = forms.ModelChoiceField(
+        queryset=Image.tag_model.objects.all(),
         required=False,
-        label=_(u"Author"),
-        help_text=_(u"Your name"),
-    )
-
-    author_email = forms.EmailField(
-        required=False,
-        label=_(u"Author's email"),
-        help_text=_(u"Your email address, so we can show a gravatar :)"),
-    )
-
-    comment = forms.CharField(
-        required=True,
-        widget=forms.Textarea,
-        label=_(u"Your comments"),
-        help_text=_(u"Describe changes you made."),
+        label=_(u"Completed"),
+        help_text=_(u"If you completed a life cycle stage, select it."),
     )
diff --git a/apps/wiki_img/helpers.py b/apps/wiki_img/helpers.py
deleted file mode 100644 (file)
index f072ef9..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-from django import http
-from django.utils import simplejson as json
-from django.utils.functional import Promise
-from datetime import datetime
-from functools import wraps
-
-
-class ExtendedEncoder(json.JSONEncoder):
-
-    def default(self, obj):
-        if isinstance(obj, Promise):
-            return unicode(obj)
-
-        if isinstance(obj, datetime):
-            return datetime.ctime(obj) + " " + (datetime.tzname(obj) or 'GMT')
-
-        return json.JSONEncoder.default(self, obj)
-
-
-# shortcut for JSON reponses
-class JSONResponse(http.HttpResponse):
-
-    def __init__(self, data={}, **kwargs):
-        # get rid of mimetype
-        kwargs.pop('mimetype', None)
-
-        data = json.dumps(data, cls=ExtendedEncoder)
-        super(JSONResponse, self).__init__(data, mimetype="application/json", **kwargs)
-
-
-# return errors
-class JSONFormInvalid(JSONResponse):
-    def __init__(self, form):
-        super(JSONFormInvalid, self).__init__(form.errors, status=400)
-
-
-class JSONServerError(JSONResponse):
-    def __init__(self, *args, **kwargs):
-        kwargs['status'] = 500
-        super(JSONServerError, self).__init__(*args, **kwargs)
-
-
-def ajax_login_required(view):
-    @wraps(view)
-    def authenticated_view(request, *args, **kwargs):
-        if not request.user.is_authenticated():
-            return http.HttpResponse("Login required.", status=401, mimetype="text/plain")
-        return view(request, *args, **kwargs)
-    return authenticated_view
-
-
-def ajax_require_permission(permission):
-    def decorator(view):
-        @wraps(view)
-        def authorized_view(request, *args, **kwargs):
-            if not request.user.has_perm(permission):
-                return http.HttpResponse("Access Forbidden.", status=403, mimetype="text/plain")
-            return view(request, *args, **kwargs)
-        return authorized_view
-    return decorator
-
-import collections
-
-def recursive_groupby(iterable):
-    """
-#    >>> recursive_groupby([1,2,3,4,5])
-#    [1, 2, 3, 4, 5]
-
-    >>> recursive_groupby([[1]])
-    [1]
-
-    >>> recursive_groupby([('a', 1),('a', 2), 3, ('b', 4), 5])
-    ['a', [1, 2], 3, 'b', [4], 5]
-
-    >>> recursive_groupby([('a', 'x', 1),('a', 'x', 2), ('a', 'x', 3)])
-    ['a', ['x', [1, 2, 3]]]
-
-    """
-
-    def _generator(iterator):
-        group = None
-        grouper = None
-
-        for item in iterator:
-            if not isinstance(item, collections.Sequence):
-                if grouper is not None:
-                    yield grouper
-                    if len(group):
-                        yield recursive_groupby(group)
-                    group = None
-                    grouper = None
-                yield item
-                continue
-            elif len(item) == 1:
-                if grouper is not None:
-                    yield grouper
-                    if len(group):
-                        yield recursive_groupby(group)
-                    group = None
-                    grouper = None
-                yield item[0]
-                continue
-            elif not len(item):
-                continue
-
-            if grouper is None:
-                group = [item[1:]]
-                grouper = item[0]
-                continue
-
-            if grouper != item[0]:
-                if grouper is not None:
-                    yield grouper
-                    if len(group):
-                        yield recursive_groupby(group)
-                    group = None
-                    grouper = None
-                group = [item[1:]]
-                grouper = item[0]
-                continue
-
-            group.append(item[1:])
-
-        if grouper is not None:
-            yield grouper
-            if len(group):
-                yield recursive_groupby(group)
-            group = None
-            grouper = None
-
-    return list(_generator(iterable))
index dd16a87..b685324 100644 (file)
@@ -3,27 +3,3 @@
 # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
-from django.db import models
-from django.contrib.auth.models import User
-from django.utils.translation import ugettext_lazy as _
-from dvcs.models import Document
-
-
-class ImageDocument(models.Model):
-    slug = models.SlugField(_('slug'), max_length=120)
-    name = models.CharField(_('name'), max_length=120)
-    image = models.ImageField(_('image'), upload_to='wiki_img')
-    doc = models.OneToOneField(Document, null=True, blank=True)
-    creator = models.ForeignKey(User, null=True, blank=True)
-
-    @staticmethod
-    def listener_initial_commit(sender, instance, created, **kwargs):
-        if created:
-            instance.doc = Document.objects.create(creator=instance.creator)
-            instance.save()
-
-    def __unicode__(self):
-        return self.name
-
-
-models.signals.post_save.connect(ImageDocument.listener_initial_commit, sender=ImageDocument)
diff --git a/apps/wiki_img/nice_diff.py b/apps/wiki_img/nice_diff.py
deleted file mode 100644 (file)
index b228fad..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
-#
-import difflib
-import re
-from collections import deque
-
-from django.template.loader import render_to_string
-from django.utils.html import escape as html_escape
-
-DIFF_RE = re.compile(r"""\x00([+^-])""", re.UNICODE)
-NAMES = {'+': 'added', '-': 'removed', '^': 'changed'}
-
-
-def diff_replace(match):
-    return """<span class="diff_mark diff_mark_%s">""" % NAMES[match.group(1)]
-
-
-def filter_line(line):
-    return  DIFF_RE.sub(diff_replace, html_escape(line)).replace('\x01', '</span>')
-
-
-def format_changeset(a, b, change):
-    return (a[0], filter_line(a[1]), b[0], filter_line(b[1]), change)
-
-
-def html_diff_table(la, lb, context=None):
-    all_changes = difflib._mdiff(la, lb)
-
-    if context is None:
-        changes = (format_changeset(*c) for c in all_changes)
-    else:
-        changes = []
-        q = deque()
-        after_change = False
-
-        for changeset in all_changes:
-            q.append(changeset)
-
-            if changeset[2]:
-                after_change = True
-                if not after_change:
-                    changes.append((0, '-----', 0, '-----', False))
-                changes.extend(format_changeset(*c) for c in q)
-                q.clear()
-            else:
-                if len(q) == context and after_change:
-                    changes.extend(format_changeset(*c) for c in q)
-                    q.clear()
-                    after_change = False
-                elif len(q) > context:
-                    q.popleft()
-
-    return render_to_string("wiki/diff_table.html", {
-        "changes": changes,
-    })
-
-
-__all__ = ['html_diff_table']
diff --git a/apps/wiki_img/templates/wiki_img/document_create_missing.html b/apps/wiki_img/templates/wiki_img/document_create_missing.html
deleted file mode 100644 (file)
index 351e87a..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "wiki/base.html" %}
-{% load i18n %}
-
-{% block leftcolumn %}
-       <form enctype="multipart/form-data" method="POST" action="">
-       {{ form.as_p }}
-
-       <p><button type="submit">{% trans "Create document" %}</button></p>
-       </form>
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-{% endblock rightcolumn %}
\ No newline at end of file
index d03a0bf..fc2e207 100644 (file)
@@ -13,6 +13,7 @@
     {% include "wiki_img/tabs/motifs_editor_item.html" %}
     {% include "wiki_img/tabs/objects_editor_item.html" %}
     {% include "wiki_img/tabs/source_editor_item.html" %}
+    {% include "wiki/tabs/history_view_item.html" %}
 {% endblock %}
 
 {% block tabs-content %}
@@ -20,6 +21,7 @@
     {% include "wiki_img/tabs/motifs_editor.html" %}
     {% include "wiki_img/tabs/objects_editor.html" %}
     {% include "wiki_img/tabs/source_editor.html" %}
+    {% include "wiki_img/tabs/history_view.html" %}
 {% endblock %}
 
 {% block dialogs %}
index 30accf2..8cba7bf 100644 (file)
 
 {% block maincontent %}
 <div id="document-meta"
-       data-document-name="{{ document.slug }}" style="display:none">
+       data-object-id="{{ document.pk }}" style="display:none">
 
-       {% for k, v in document_meta.items %}
-               <span data-key="{{ k }}">{{ v }}</span>
-       {% endfor %}
-
-       {% for k, v in document_info.items %}
-               <span data-key="{{ k }}">{{ v }}</span>
-       {% endfor %}
+       <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 wiki_document_list %}"><img src="{{STATIC_URL}}icons/go-home.png"/><a href="{% url wiki_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>
     <div id="tools">
         <a href="{{ REDMINE_URL }}projects/wl-publikacje/wiki/Pomoc" target="_blank">
         {% trans "Help" %}</a>
index 71556a1..ca38838 100644 (file)
@@ -1,27 +1,26 @@
-{% extends "wiki/document_details_base.html" %}
+{% extends "wiki_img/document_details_base.html" %}
 {% load i18n %}
 
-{% block editor-class %}readonly{% endblock %}
-
 {% block extrabody %}
 {{ block.super }}
-<script src="{{STATIC_URL}}js/lib/codemirror-0.8/codemirror.js" type="text/javascript" charset="utf-8">
+<script src="{{ STATIC_URL }}js/lib/codemirror-0.8/codemirror.js" type="text/javascript" charset="utf-8">
 </script>
-<script src="{{STATIC_URL}}js/wiki/loader_readonly.js" type="text/javascript" charset="utf-8"> </script>
+<script src="{{ STATIC_URL }}js/wiki_img/loader_readonly.js" type="text/javascript" charset="utf-8"> </script>
 {% endblock %}
 
 {% block tabs-menu %}
-    {% include "wiki/tabs/wysiwyg_editor_item.html" %}
-       {% include "wiki/tabs/source_editor_item.html" %}
+    {% include "wiki_img/tabs/motifs_editor_item.html" %}
+    {% include "wiki_img/tabs/objects_editor_item.html" %}
+    {% include "wiki_img/tabs/source_editor_item.html" %}
 {% endblock %}
 
 {% block tabs-content %}
-    {% include "wiki/tabs/wysiwyg_editor.html" %}
-       {% include "wiki/tabs/source_editor.html" %}
+    {% include "wiki_img/tabs/motifs_editor.html" %}
+    {% include "wiki_img/tabs/objects_editor.html" %}
+    {% include "wiki_img/tabs/source_editor.html" %}
 {% endblock %}
 
-{% block splitter-extra %}
+{% block editor-class %}
+    sideless
 {% endblock %}
 
-{% block dialogs %}
-{% endblock %}
\ No newline at end of file
diff --git a/apps/wiki_img/templates/wiki_img/document_list.html b/apps/wiki_img/templates/wiki_img/document_list.html
deleted file mode 100644 (file)
index cf10cde..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-{% extends "wiki/base.html" %}
-
-{% load i18n %}
-{% load wiki %}
-
-{% block extrabody %}
-{{ block.super }}
-<script type="text/javascript" charset="utf-8">
-$(function() {
-       function search(event) {
-        event.preventDefault();
-        var expr = new RegExp(slugify($('#file-list-filter').val()), 'i');
-        $('#file-list tbody tr').hide().filter(function(index) {
-            return expr.test(slugify( $('a', this).attr('data-id') ));
-        }).show();
-    }
-
-    $('#file-list-find-button').click(search).hide();
-       $('#file-list-filter').bind('keyup change DOMAttrModified', search);
-});
-</script>
-{% endblock %}
-
-{% block leftcolumn %}
-       <form method="get" action="#">
-    <table  id="file-list">
-       <thead>
-               <tr><th>Filtr:</th>
-                       <th><input autocomplete="off" name="filter" id="file-list-filter" type="text" size="40" /></th>
-                       <th><input type="reset" value="{% trans "Clear filter" %}" id="file-list-reset-button"/></th>
-                       </tr>
-               </thead>
-               <tbody>
-       {% for doc in object_list %}
-            <tr>
-               <td colspan="3"><a target="_blank" data-id="{{doc.name}}"
-                                       href="{% url wiki_img_editor doc.slug %}">{{ doc.name }}</a></td>
-                               <!-- placeholder </td> -->
-                       </tr>
-       {% endfor %}
-               </tbody>
-    </table>
-       </form>
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-       <div id="last-edited-list">
-               <h2>{% trans "Your last edited documents" %}</h2>
-           <ol>
-                       {% for name, date in last_docs %}
-                       <li><a href="{% url wiki_editor name %}"
-                               target="_blank">{{ name|wiki_title }}</a><br/><span class="date">({{ date|date:"H:i:s, d/m/Y" }})</span></li>
-                       {% endfor %}
-               </ol>
-       </div>
-{% endblock rightcolumn %}
index fc239d2..e8f89e6 100644 (file)
@@ -1,6 +1,7 @@
 {% load i18n %}
 <div id="save_dialog" class="dialog" data-ui-jsclass="SaveDialog">
        <form method="POST" action="">
+    {% csrf_token %}
        <p>{{ forms.text_save.comment.label }}</p>
        <p class="help_text">
                {{ forms.text_save.comment.help_text}}
diff --git a/apps/wiki_img/templates/wiki_img/tabs/history_view.html b/apps/wiki_img/templates/wiki_img/tabs/history_view.html
new file mode 100755 (executable)
index 0000000..db49d64
--- /dev/null
@@ -0,0 +1,40 @@
+{% 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>
index 075e5ad..b4396c6 100644 (file)
@@ -1,20 +1,18 @@
 # -*- coding: utf-8
 from django.conf.urls.defaults import *
-from django.conf import settings
-from django.views.generic.list_detail import object_list
 
-from wiki_img.models import ImageDocument
-
-
-PART = ur"""[ ĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9\w_.-]+"""
 
 urlpatterns = patterns('wiki_img.views',
-    url(r'^$', object_list, {'queryset': ImageDocument.objects.all(), "template_name": "wiki_img/document_list.html"}),
-
-    url(r'^edit/(?P<slug>%s)$' % PART,
+    url(r'^edit/(?P<slug>[^/]+)/$',
         'editor', name="wiki_img_editor"),
 
-    url(r'^(?P<slug>[^/]+)/text$',
+    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<chunk_id>\d+)/$',
+        'history', name="wiki_history"),
+
 )
index c4d32b7..9e87f66 100644 (file)
@@ -1,16 +1,18 @@
 import os
 import functools
 import logging
-logger = logging.getLogger("fnp.wiki")
+logger = logging.getLogger("fnp.wiki_img")
 
 from django.views.generic.simple import direct_to_template
 from django.core.urlresolvers import reverse
 from wiki.helpers import JSONResponse
 from django import http
 from django.shortcuts import get_object_or_404
+from django.views.decorators.http import require_GET
 from django.conf import settings
+from django.utils.formats import localize
 
-from wiki_img.models import ImageDocument
+from catalogue.models import Image
 from wiki_img.forms import DocumentTextSaveForm
 
 #
@@ -21,50 +23,107 @@ 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(ImageDocument, slug=slug)
+    doc = get_object_or_404(Image, slug=slug)
 
     return direct_to_template(request, template_name, extra_context={
         'document': doc,
         'forms': {
-            "text_save": DocumentTextSaveForm(prefix="textsave"),
+            "text_save": DocumentTextSaveForm(user=request.user, prefix="textsave"),
         },
         '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 direct_to_template(request, template_name, extra_context={
+        'document': doc,
+        'revision': revision,
+        'readonly': True,
+        'REDMINE_URL': settings.REDMINE_URL,
+    })
+
+
 @never_cache
-def text(request, slug):
+def text(request, image_id):
+    doc = get_object_or_404(Image, pk=image_id)
     if request.method == 'POST':
-        form = DocumentTextSaveForm(request.POST, prefix="textsave")
+        form = DocumentTextSaveForm(request.POST, user=request.user, prefix="textsave")
         if form.is_valid():
-            document = get_object_or_404(ImageDocument, slug=slug)
-            commit = form.cleaned_data['parent_commit']
-
-            comment = form.cleaned_data['comment']
-
             if request.user.is_authenticated():
-                user = request.user
+                author = request.user
             else:
-                user = None
-
-            document.doc.commit(
-                parent=commit,
-                text=form.cleaned_data['text'],
-                author=user,
-                description=comment
-            )
-
+                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'))
+            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': document.doc.materialize(),
-                'revision': document.doc.change_set.count(),
+                'text': doc.materialize() if parent_revision != revision else None,
+                'meta': {},
+                'revision': revision,
             })
         else:
             return JSONFormInvalid(form)
     else:
-        doc = get_object_or_404(ImageDocument, slug=slug).doc
+        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': doc.materialize(),
-            'revision': doc.change_set.count(),
-            'commit': doc.head.id,
+            'text': text,
+            'meta': {},
+            'revision': revision,
         })
 
+
+@never_cache
+def history(request, chunk_id):
+    # TODO: pagination
+    doc = get_object_or_404(Image, pk=chunk_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)
diff --git a/lib/librarian b/lib/librarian
new file mode 160000 (submodule)
index 0000000..2887829
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit 28878296bacad453735a520f350ba5a971f8ffc8
index a98f8de..2708ed7 100644 (file)
@@ -162,7 +162,7 @@ class VersionedStorage(object):
 
     def _file_to_title(self, filename):
         assert filename.startswith(self.repo_prefix)
-        name = filename[len(self.repo_prefix):].strip('/').split('.', 1)[0]
+        name = filename[len(self.repo_prefix):].strip('/').rsplit('.', 1)[0]
         return urlunquote(name)
 
     def __contains__(self, title):
diff --git a/redakcja-celery.conf b/redakcja-celery.conf
new file mode 100644 (file)
index 0000000..9168db8
--- /dev/null
@@ -0,0 +1,22 @@
+; =======================================
+; celeryd supervisor example for Django
+; =======================================
+
+[program:celery]
+command=$APP_DIR/redakcja/manage.py celeryd --loglevel=INFO
+directory=$APP_DIR/redakcja
+user=nobody
+numprocs=2
+stdout_logfile=$APP_DIR/log/celeryd.log
+stderr_logfile=$APP_DIR/log/celeryd.log
+autostart=true
+autorestart=true
+startsecs=10
+
+; Need to wait for currently executing tasks to finish at shutdown.
+; Increase this if you have very long running tasks.
+stopwaitsecs = 600
+
+; if rabbitmq is supervised, set its priority higher
+; so it starts first
+priority=998
index d271fa1..2d10ac3 100644 (file)
@@ -14,10 +14,12 @@ sys.stdout = sys.stderr
 sys.path = [
     '$APP_DIR',
        '$APP_DIR/lib',
+       '$APP_DIR/lib/librarian',
        '$APP_DIR/apps',
 ] + sys.path
 
 # Run Django
+os.environ["CELERY_LOADER"] = "django"
 os.environ['DJANGO_SETTINGS_MODULE'] = '$PROJECT_NAME.settings'
 
 from django.core.handlers.wsgi import WSGIHandler
index a0cefb7..7f35966 100644 (file)
Binary files a/redakcja/locale/pl/LC_MESSAGES/django.mo and b/redakcja/locale/pl/LC_MESSAGES/django.mo differ
index 98523f5..916f4db 100644 (file)
@@ -3,19 +3,58 @@
 # This file is distributed under the same license as the 'platforma' package.
 # lrekucki@gmail.com, 2009.
 #
-#, fuzzy
 msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2010-05-18 10:57+0200\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"POT-Creation-Date: 2011-10-18 11:18+0200\n"
+"PO-Revision-Date: 2011-10-18 11:19+0100\n"
+"Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
+#: templates/404.html:8
+msgid "Page not found"
+msgstr "Strona nie została znaleziona"
+
+#: templates/404.html:10
+#, python-format
+msgid ""
+"The page you're trying\n"
+"to reach (<tt>%(p)s</tt>) could not be found. If it's a document, try\n"
+"looking for it on the <a href=\"/\">document list</a>."
+msgstr "Strona, do której próbujesz dotrzeć (<tt>%(p)s</tt>), nie istnieje. Spróbuj poszukać jej na <a href=\"/\">liście dokumentów</a>."
+
+#: templates/404.html:15
+#, python-format
+msgid ""
+"If you\n"
+"still can't find what you're looking for, please\n"
+"<a href=\"%(m)s\">contact the administrator</a>."
+msgstr "Jeśli nadal nie możesz znaleźć tego, czego szukasz, <a href=\"%(m)s\">skontaktuj się z administratorem</a>."
+
+#: templates/404.html:22
+#, python-format
+msgid ""
+"If you're coming from Redmine, please note that\n"
+"work is no longer managed there. \n"
+"Go to the <a href=\"/\">document list</a>\n"
+"or to <a href=\"%(m)s\">your page</a> instead."
+msgstr ""
+"Jeśli skierował Cię tu Redmine, zwróć uwagę, że nie służy on już do koordynowania prac redakcyjnych. Możesz za to przejść do <a href=\"/\">listy dokumentów</a>\n"
+"albo do <a href=\"%(m)s\">swojej strony</a>."
+
+#: templates/base.html:7
+msgid "Platforma Redakcyjna"
+msgstr ""
+
+#: templates/base.html:16
+msgid "Loading"
+msgstr "Ładowanie"
+
 #: templates/admin/index.html:21
 #, python-format
 msgid "Models available in the %(name)s application."
@@ -54,6 +93,16 @@ msgstr ""
 msgid "Unknown content"
 msgstr ""
 
+#: templates/pagination/pagination.html:5
+#: templates/pagination/pagination.html:7
+msgid "previous"
+msgstr "poprzednie"
+
+#: templates/pagination/pagination.html:21
+#: templates/pagination/pagination.html:23
+msgid "next"
+msgstr "następne"
+
 #: templates/registration/head_login.html:5
 msgid "Log Out"
 msgstr "Wyloguj"
index c1b1a19..509022e 100644 (file)
@@ -12,7 +12,7 @@
 from redakcja.settings import *
 
 # Path to repository with managed documents
-WIKI_REPOSITORY_PATH = '/srv/redakcja/books'
+CATALOGUE_REPO_PATH = '/srv/redakcja/books'
 
 LOGGING_CONFIG_FILE = "/srv/redakcja/logging.cfg.dev"
 
@@ -22,7 +22,13 @@ MEDIA_ROOT = '/srv/redakcja/media/'
 # Subdirectory of MEDIA_ROOT containing images
 IMAGE_DIR = 'images'
 
-CAS_SERVER_URL = 'http://logowanie.wolnelektury.pl/cas/'
+CAS_SERVER_URL = 'http://logowanie.nowoczesnapolska.org.pl/cas/'
 REDMINE_URL = 'http://redmine.nowoczesnapolska.org.pl/'
 DEBUG = True
-COMPRESS = False
\ No newline at end of file
+MAINTENANCE_MODE = False
+COMPRESS = False
+
+APICLIENT_WL_CONSUMER_KEY = None
+APICLIENT_WL_CONSUMER_SECRET = None
+
+CELERY_ALWAYS_EAGER = False
index 28b571d..7647675 100755 (executable)
@@ -11,6 +11,7 @@ PROJECT_ROOT = os.path.realpath(os.path.dirname(__file__))
 sys.path += [os.path.realpath(os.path.join(*x)) for x in (
         (PROJECT_ROOT, '..'),
         (PROJECT_ROOT, '..', 'apps'),
+        (PROJECT_ROOT, '..', 'lib/librarian'),
         (PROJECT_ROOT, '..', 'lib')
 )]
 
index 6f1c094..600533b 100644 (file)
@@ -1,12 +1,17 @@
 from __future__ import absolute_import
+from os import path
 from redakcja.settings.common import *
 
-DATABASE_ENGINE = 'sqlite3'    # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
-DATABASE_NAME = PROJECT_ROOT + '/dev.sqlite'             # Or path to database file if using sqlite3.
-DATABASE_USER = ''             # Not used with sqlite3.
-DATABASE_PASSWORD = ''         # Not used with sqlite3.
-DATABASE_HOST = ''             # Set to empty string for localhost. Not used with sqlite3.
-DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+        'NAME': path.join(PROJECT_ROOT, 'dev.sqlite'), # Or path to database file if using sqlite3.
+        'USER': '',                      # Not used with sqlite3.
+        'PASSWORD': '',                  # Not used with sqlite3.
+        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
+        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
+    }
+}
 
 try:
     LOGGING_CONFIG_FILE
index 5acf265..cc7765e 100644 (file)
@@ -10,8 +10,6 @@ TEMPLATE_DEBUG = DEBUG
 MAINTENANCE_MODE = False
 
 ADMINS = (
-    # (u'Marek Stępniowski', 'marek@stepniowski.com'),
-    # (u'Łukasz Rekucki', 'lrekucki@gmail.com'),
     (u'Radek Czajka', 'radoslaw.czajka@nowoczesnapolska.org.pl'),
 )
 
@@ -36,6 +34,8 @@ SITE_ID = 1
 # If you set this to False, Django will make some optimizations so as not
 # to load the internationalization machinery.
 USE_I18N = True
+USE_L10N = True
+
 
 # Absolute path to the directory that holds media.
 # Example: "/home/media/media.lawrence.com/"
@@ -59,28 +59,30 @@ SESSION_COOKIE_NAME = "redakcja_sessionid"
 
 # List of callables that know how to import templates from various sources.
 TEMPLATE_LOADERS = (
-    'django.template.loaders.filesystem.load_template_source',
-    'django.template.loaders.app_directories.load_template_source',
-#     'django.template.loaders.eggs.load_template_source',
+    'django.template.loaders.filesystem.Loader',
+    'django.template.loaders.app_directories.Loader',
 )
 
 TEMPLATE_CONTEXT_PROCESSORS = (
-    "django.core.context_processors.auth",
+    "django.contrib.auth.context_processors.auth",
     "django.core.context_processors.debug",
     "django.core.context_processors.i18n",
     "redakcja.context_processors.settings", # this is instead of media
+    'django.core.context_processors.csrf',
     "django.core.context_processors.request",
 )
 
 
 MIDDLEWARE_CLASSES = (
     'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
 
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django_cas.middleware.CASMiddleware',
 
     'django.middleware.doc.XViewMiddleware',
+    'pagination.middleware.PaginationMiddleware',
     'maintenancemode.middleware.MaintenanceModeMiddleware',
 )
 
@@ -97,13 +99,6 @@ TEMPLATE_DIRS = (
 
 FIREPYTHON_LOGGER_NAME = "fnp"
 
-#
-# Central Auth System
-#
-## Set this to where the CAS server lives
-# CAS_SERVER_URL = "http://cas.fnp.pl/
-CAS_LOGOUT_COMPLETELY = True
-
 INSTALLED_APPS = (
     'django.contrib.auth',
     'django.contrib.contenttypes',
@@ -111,19 +106,32 @@ INSTALLED_APPS = (
     'django.contrib.sites',
     'django.contrib.admin',
     'django.contrib.admindocs',
+    'django.contrib.comments',
 
-    'django_cas',
     'compress',
     'south',
     'sorl.thumbnail',
     'filebrowser',
+    'pagination',
+    'gravatar',
+    'djcelery',
+    'djkombu',
 
+    'catalogue',
     'dvcs',
     'wiki',
     'wiki_img',
     'toolbar',
+    'apiclient',
+    'email_mangler',
 )
 
+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 = []
@@ -134,12 +142,15 @@ FILEBROWSER_DEFAULT_ORDER = "path_relative"
 IMAGE_DIR = 'images'
 
 
-WL_API_CONFIG = {
-    "URL": "http://localhost:7000/api/",
-    "AUTH_REALM": "WL API",
-    "AUTH_USER": "platforma",
-    "AUTH_PASSWD": "platforma",
-}
+import djcelery
+djcelery.setup_loader()
+
+BROKER_BACKEND = "djkombu.transport.DatabaseTransport"
+BROKER_HOST = "localhost"
+BROKER_PORT = 5672
+BROKER_USER = "guest"
+BROKER_PASSWORD = "guest"
+BROKER_VHOST = "/"
 
 SHOW_APP_VERSION = False
 
@@ -147,3 +158,4 @@ try:
     from redakcja.settings.compress import *
 except ImportError:
     pass
+
index 714e4c2..34cfa2f 100644 (file)
@@ -14,11 +14,11 @@ COMPRESS_CSS = {
         ),
         'output_filename': 'compressed/detail_styles_?.css',
     },
-    'listing': {
+    'catalogue': {
         'source_filenames': (
             'css/filelist.css',
         ),
-        'output_filename': 'compressed/listing_styles_?.css',
+        'output_filename': 'compressed/catalogue_styles_?.css',
      }
 }
 
@@ -27,10 +27,10 @@ COMPRESS_JS = {
     'detail': {
         '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.xmlns.js',
                 'js/button_scripts.js',
                 'js/slugify.js',
 
@@ -44,7 +44,8 @@ COMPRESS_JS = {
 
                 # dialogs
                 'js/wiki/dialog_save.js',
-                'js/wiki/dialog_addtag.js',
+                'js/wiki/dialog_revert.js',
+                'js/wiki/dialog_pubmark.js',
 
                 # views
                 'js/wiki/view_history.js',
@@ -85,15 +86,17 @@ COMPRESS_JS = {
                 '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',
         ),
         'output_filename': 'compressed/detail_img_scripts_?.js',
      },
-    'listing': {
+    'catalogue': {
         'source_filenames': (
-                'js/lib/jquery-1.4.2.min.js',
+                'js/catalogue/catalogue.js',
                 'js/slugify.js',
+                'email_mangler/email_mangler.js',
         ),
-        'output_filename': 'compressed/listing_scripts_?.js',
+        'output_filename': 'compressed/catalogue_scripts_?.js',
      }
 }
 
index 118c7ff..5fbd59f 100644 (file)
@@ -6,22 +6,31 @@ from redakcja.settings.common import *
 
 # ROOT_URLCONF = 'yourapp.settings.test.urls'
 
-DATABASE_ENGINE = 'sqlite3'
-DATABASE_NAME = ':memory:'
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+        'NAME': '', # Or path to database file if using sqlite3.
+        'USER': '',                      # Not used with sqlite3.
+        'PASSWORD': '',                  # Not used with sqlite3.
+        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
+        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
+    }
+}
 
 import tempfile
 
-WIKI_REPOSITORY_PATH = tempfile.mkdtemp(prefix='wikirepo')
+CATALOGUE_REPO_PATH = tempfile.mkdtemp(prefix='redakcja-repo')
+USE_CELERY = False
 
-INSTALLED_APPS += ('django_nose',)
+INSTALLED_APPS += ('django_nose', 'dvcs.tests')
 
-TEST_RUNNER = 'django_nose.run_tests'
-TEST_MODULES = ('wiki', 'toolbar', 'vstorage')
+TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
+TEST_MODULES = ('catalogue', 'dvcs.tests', 'wiki', 'toolbar')
+COVER_APPS = ('catalogue', 'dvcs', 'wiki', 'toolbar')
 NOSE_ARGS = (
     '--tests=' + ','.join(TEST_MODULES),
-    '--cover-package=' + ','.join(TEST_MODULES),
+    '--cover-package=' + ','.join(COVER_APPS),
     '-d',
-    '--with-coverage',
     '--with-doctest',
     '--with-xunit',
     '--with-xcoverage',
index 871d723..1032b13 100644 (file)
@@ -26,7 +26,7 @@
        font-weight: bold;
 }
 
-#save_dialog textarea {
+#save_dialog textarea, #revert_dialog textarea {
        width: 90%;
        margin: 0.2em 4%;
 }
\ No newline at end of file
index 2ccf33e..166def0 100644 (file)
@@ -7,40 +7,97 @@
 */
 
 body {
-       background-color: #84BF2A;
+    margin: 0;
+    font-family: verdana, sans-serif;
+    font-size: 10px;
 }
 
-#content {
-    background: #EFEFEF;
-    border: 1px solid black;
-    padding: 0.5em 2em;
-    margin: 1em;
-       overflow: hidden;
+img {
+    border: 0;
+}
+
+th {
+    text-align: left;
+}
+
+td {
+    vertical-align: top;
+    padding: 0 3px;
+}
+.clr {
+    clear: both;
+}
+
+#tabs-nav {
+    padding: 5px 5px 0 10px;
+    background: #ffdfbf;
+    border-bottom: 1px solid #ff8000;
+    position: relative;
 }
 
-#content h1 img {
-       vertical-align: middle;
+#tabs-nav-left {
+    margin-left: 60px;
 }
 
-#content h1 {
-       border-bottom: 2px solid black;
-       padding: 0.5em;
-       font-size: 2opt;
-       font-family: sans-serif;
+#tabs-nav-left a {
+    display: block;
+    float: left;
+    padding: 5px 20px 5px 20px;
+    margin-bottom: -1px;
+    border-width: 1px;
+    border-style: solid;
+    border-color: rgba(0,0,0,0);
 }
 
-#file-list {
+#tabs-nav-left .active {
+    background: white;
+    border-color: #ff8000 #ff8000 white #ff8000;
+}
+
+.section {
+    border-top: 1px solid #ffdfbf;
+    margin-top: 2em;
+    padding-top: 1em;
+}
+
+.editable td {
+    padding: 1px;
+}
+.editable input, .editable select, .editable textarea {
+    width: 400px;
+}
+.editable .number-input {
+    width: 100px;
+}
+
+
+#login-box {
+    float: right;
+}
+
+#logo {
+    position: absolute;
+    bottom: 0;
+}
+
+#content {
+    padding: 10px;
+}
+
+
+
+#catalogue_layout_left_column {
        overflow: visible;
        float: left;
-       max-width: 50%;
+       max-width: 60%;
        padding-right: 2%;
        border-right: 1px dashed black;
 
 }
 
-#last-edited-list {
+#catalogue_layout_right_column {
        float: left;
-       max-width: 35%;
+       max-width: 30%;
        margin-left: 5%;
 }
 
@@ -58,7 +115,7 @@ body {
 }
 
 a, a:visited, a:active {
-       color: blue;
+       color: #bf6000;
        text-decoration: none;
 }
 
@@ -67,9 +124,6 @@ a:hover {
 }
 
 
-#loading-overlay {
-       display: none;
-}
 
 .error {
     color: red;
@@ -86,4 +140,87 @@ a:hover {
 
 #skipped-list {
     color: #666;
+}
+
+.chunkno {
+    font-size: .7em;
+    padding-left: 2em;
+}
+
+
+/* Big cheesy publish button */
+#publish-button {
+        color: black;
+        border: 2px solid black; 
+        border-radius: 20px;
+        box-shadow: 0px 0px 15px #88f;
+        /*moz-border-radius: 20px;
+        -moz-box-shadow: 10px 10px 5px #888;*/
+        font-size:1.5em; 
+        padding: 1em; 
+        background: -moz-linear-gradient(top,  #fff,  #44f);
+        -moz-transition: all 0.5s ease-in-out;        
+        margin: 20px;
+}
+
+#publish-button:hover {
+    -moz-transition: all 0.5s ease-in-out;        
+    -moz-transform: scale(1.1);
+    background: -moz-linear-gradient(top,  #fff,  #88f);
+    -moz-box-shadow: 0px 0px 30px #ff8;
+}
+
+#publish-button:active {
+    background: -moz-linear-gradient(top,  #88f,  #fff);
+}
+
+
+/* book list */
+
+.book-search-column input {
+    width: 96%;
+}
+
+.book-list-user .user-column {
+    display: none;
+}
+
+
+/* wall */
+.wall {
+    padding-left: 0;
+    list-style: none;
+}
+
+.wall li {
+    clear: left;
+    border-top: 1px dotted gray;
+    padding: 0 1em 2em 1em;
+    margin-bottom: 0;
+}
+
+.wall .gravatar {
+    float: left;
+    margin-right: 1em;
+    margin-left: -1em;
+}
+.wall h3 {
+    font-size: 1.25em;
+    margin-top: 0;
+}
+
+.wall .time {
+    /* float:right; */
+}
+
+.wall .anonymous {
+    background-color: #efa;
+}
+
+.wall .comment {
+    background-color: #dfc;
+}
+
+.wall .publish {
+    background-color: #fdc;
 }
\ No newline at end of file
index b319116..acd9f93 100644 (file)
@@ -79,15 +79,14 @@ table#changes-list-container {
     font-weight: bold;
 }
 
-#changes-list td[data-stub-value =
-'version'] {
+#changes-list td {
     vertical-align: text-top;
 }
 
-#changes-list *[data-stub-value =
-'date'], #changes-list *[data-stub-value = 'author'] {
-    font-size: 11px;
+#changes-list *[data-stub-value = 'description'] {
+    font-size: .8em;
     color: gray;
+    white-space: pre-line;
 }
 
 /*
index cc53e22..0d43611 100644 (file)
@@ -37,7 +37,7 @@
  -webkit-border-radius: 4px;
  }
  */
-.htmlview *[x-node = 'RDF'] {
+.htmlview *[x-node = 'RDF'][x-ns = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'] {
     display: none;
 }
 
 }
 
 /* wersy */
+.htmlview *[x-verse]:after {
+    content: "\feff";
+}
+
 .htmlview .strofa .wers_wciety, .htmlview .strofa .wers_wciety[data-wlf-typ='1'] {
     margin-left: 1em;
 }
@@ -253,9 +257,9 @@ not(.strofa) > *[x-verse]::after {
     padding: 0 1em 1em 1em;
 }
 
-.htmlview br.sekcja_swiatlo {
-    height: 3em;
-    /* visibility: hidden; */
+.htmlview hr.sekcja_swiatlo {
+    margin: 2em 0;
+    visibility: hidden;
 }
 
 .htmlview hr.separator_linia {
@@ -270,6 +274,10 @@ not(.strofa) > *[x-verse]::after {
     text-align: center;
 }
 
+.htmlview p.sekcja_asterysk:after {
+    content: "*";
+}
+
 .htmlview div.lista_osob ol {
     list-style: none;
     padding: 0 0 0 1.5em;
@@ -488,7 +496,7 @@ div[x-node] > .uwaga {
     visibility: hidden;
 }
 
-.edit-button, .delete-button, .accept-button, .tytul-button, .wyroznienie-button, .slowo-button {
+.edit-button, .delete-button, .accept-button, .tytul-button, .wyroznienie-button, .slowo-button, .znak-button {
     position: absolute;
     top: -21px;
     left: -1px;
@@ -512,16 +520,20 @@ div[x-node] > .uwaga {
 }
 
 .tytul-button {
-    left:200px;
+    left:150px;
     width:100px;
 }
 
 .wyroznienie-button {
-    left:300px;
+    left:250px;
     width:100px;
 }
 .slowo-button {
-    left: 400px;
+    left:350px;
+    width:100px;
+}
+.znak-button {
+    left:450px;
     width:100px;
 }
 
@@ -530,7 +542,8 @@ div[x-node] > .uwaga {
 .accept-button:hover, .accept-button:active,
 .tytul-button:hover, .tytul-button:active,
 .wyroznienie-button:hover, .wyroznienie-button:active,
-.slowo-button:hover, .slowo-button:active {
+.slowo-button:hover, .slowo-button:active,
+.znak-button:hover, .znak-button:active {
     /*    color: #FFF;*/
     background-color: #999;
     color: #FFF;
@@ -629,23 +642,36 @@ div[x-node] > .uwaga {
     border: 1px solid orange;
 }
 
+.alien {
+    color: red;
+}
+
 /* specialChars */
 #specialCharsContainer {
     text-align: center; 
     width: 600px; 
+    height: 400px;
     padding:20px; 
     background-color: gray; 
     position: absolute; 
     top: 20px; 
     left: 20px; 
     z-index:1000;
+    overflow:auto;
 }
 #specialCharsContainer a {
     color: white;
     font-weight: bold;
 } 
+
 #tableSpecialChars td input {
     background-color: transparent;
     border:0;
     color: white;
 } 
+
+#tableSpecialChars td input.recentSymbol {
+    background-color: white;
+    border:0;
+    color: black;
+} 
index 1c56e54..c1060bc 100644 (file)
@@ -360,7 +360,15 @@ img.tabclose {
 }
 
 .saveNotify {
-    position:absolute; bottom:7px; left:30px; z-index:800; background-color: #E6E6E6; padding:20px; border: 1px solid black;
+    position:absolute; 
+    top:22px; 
+    right:7px; 
+    z-index:800;
+    background-color: #FFFF69; 
+    padding:10px; 
+    border: 1px solid black;
+    border-radius: 5px;
+    -moz-border-radius: 15px;
 }
 
 .notifyTip {
diff --git a/redakcja/static/email_mangler/email_mangler.js b/redakcja/static/email_mangler/email_mangler.js
new file mode 100755 (executable)
index 0000000..03c1a91
--- /dev/null
@@ -0,0 +1,21 @@
+var rot13 = function(s){
+    return s.replace(/[a-zA-Z]/g, function(c){
+        return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
+    });
+};
+
+(function($) {
+    $(function() {
+
+        $(".mangled").each(function() {
+            $this = $(this);
+            var email = rot13($this.attr('data-addr1')) + '@' +
+                rot13($this.attr('data-addr2'));
+            $this.attr('href', "mailto:" + email);
+            $this.html(email);
+        });
+
+
+    });
+})(jQuery);
+
diff --git a/redakcja/static/img/angel-left.png b/redakcja/static/img/angel-left.png
new file mode 100644 (file)
index 0000000..7744103
Binary files /dev/null and b/redakcja/static/img/angel-left.png differ
diff --git a/redakcja/static/img/angel-right.png b/redakcja/static/img/angel-right.png
new file mode 100644 (file)
index 0000000..df85d33
Binary files /dev/null and b/redakcja/static/img/angel-right.png differ
diff --git a/redakcja/static/img/wl-orange.png b/redakcja/static/img/wl-orange.png
new file mode 100644 (file)
index 0000000..d5c56d0
Binary files /dev/null and b/redakcja/static/img/wl-orange.png differ
index c97a231..6e72915 100644 (file)
@@ -48,6 +48,11 @@ function ScriptletCenter()
 {
     this.scriptlets = {};
 
+    this.scriptlets['insert_text'] = function(context, params, text, move_forward, move_up, done)
+    {
+        done(params.text, move_forward, move_up);
+    }.bind(this);
+
     this.scriptlets['insert_tag'] = function(context, params, text, move_forward, move_up, done)
     {
         var padding_top = '';
@@ -241,38 +246,51 @@ function ScriptletCenter()
     {
         if(!text.match(/^\n+$/)) done(text, move_forward, move_up);
 
-        function insert_done(output, mf) {
-            text += output;
+        var output = '';
+
+        function insert_done(text, mf, mu) {
+            output += text;
         }
 
         if (!params.split) params.split = 2;
         if (!params.padding) params.padding = 3;
 
-        chunks = text.replace(/^\n+|\n+$/, '').split(new RegExp("\\n{"+params.split+",}"));
-        text = text.match(/^\n+/);
-        if (!text)
-            text = '';
-        padding = '';
-        for(; params.padding; params.padding--) {
-            padding += "\n";
-        }
-
         if (params.tag == 'strofa')
             tagger = this.scriptlets['insert_stanza'];
         else
             tagger = this.scriptlets['insert_tag'];
 
-        for (i in chunks) {
-            if (chunks[i]) {
-                if (params.tag == 'akap' && chunks[i].match(/^---/))
-                    tag = 'akap_dialog';
-                else tag = params.tag;
-                tagger(context, {tag: tag}, chunks[i], 0, 0, insert_done);
-                text += padding;
-            }
+        var padding_top = text.match(/^\n+/)
+        output = padding_top ? padding_top[0] : '';
+
+        padding = '';
+        for(var i=params.padding; i; --i) {
+            padding += "\n";
         }
 
-        done(text, move_forward, move_up);
+        text = text.substr(output.length);
+        var chunk_reg = new RegExp("^([\\s\\S]+?)(\\n{"+params.split+",}|$)");
+        while (match = text.match(chunk_reg)) {
+            if (params.tag == 'akap' && match[1].match(/^---/))
+                tag = 'akap_dialog';
+            else tag = params.tag;
+            tagger(context, {tag: tag}, match[1], 0, 0, insert_done);
+            if (match[2].length > params.padding)
+                output += match[2];
+            else
+                output += padding;
+            text = text.substr(match[0].length)
+        }
+
+        output += text;
+
+        done(output, move_forward, move_up);
+    }.bind(this);
+
+
+    this.scriptlets['slugify'] = function(context, params, text, move_forward, move_up, done)
+    {
+        done(slugify(text.replace(/_/g, '-')), move_forward, move_up);
     }.bind(this);
 
 }
@@ -342,4 +360,4 @@ var scriptletCenter;
 
 $(function() {
     scriptletCenter = new ScriptletCenter();
-});
\ No newline at end of file
+});
diff --git a/redakcja/static/js/catalogue/catalogue.js b/redakcja/static/js/catalogue/catalogue.js
new file mode 100755 (executable)
index 0000000..9d2bd95
--- /dev/null
@@ -0,0 +1,32 @@
+(function($) {
+    $(function() {
+
+
+        $('.filter').change(function() {
+            document.filter[this.name].value = this.value;
+            document.filter.submit();
+        });
+
+        $('.check-filter').change(function() {
+            document.filter[this.name].value = this.checked ? '1' : '';
+            document.filter.submit();
+        });
+
+        $('.text-filter').each(function() {
+            var inp = this;
+            $(inp).parent().submit(function() {
+                document.filter[inp.name].value = inp.value;
+                document.filter.submit();
+                return false;
+            });
+        });
+
+
+        $('.autoslug-source').change(function() {
+            $('.autoslug').attr('value', slugify(this.value));
+        });
+
+
+    });
+})(jQuery);
+
diff --git a/redakcja/static/js/lib/jquery/jquery.xmlns.js b/redakcja/static/js/lib/jquery/jquery.xmlns.js
new file mode 100644 (file)
index 0000000..96c5a55
--- /dev/null
@@ -0,0 +1,411 @@
+//
+//  jquery.xmlns.js:  xml-namespace selector support for jQuery
+//
+//  This plugin modifies the jQuery tag and attribute selectors to
+//  support optional namespace specifiers as defined in CSS 3:
+//
+//    $("elem")      - matches 'elem' nodes in the default namespace
+//    $("|elem")     - matches 'elem' nodes that don't have a namespace
+//    $("NS|elem")   - matches 'elem' nodes in declared namespace 'NS'
+//    $("*|elem")    - matches 'elem' nodes in any namespace
+//    $("NS|*")      - matches any nodes in declared namespace 'NS'
+//
+//  A similar synax is also supported for attribute selectors, but note
+//  that the default namespace does *not* apply to attributes - a missing
+//  or empty namespace selector selects only attributes with no namespace.
+//
+//  In a slight break from the W3C standards, and with a nod to ease of
+//  implementation, the empty namespace URI is treated as equivalent to
+//  an unspecified namespace.  Plenty of browsers seem to make the same
+//  assumption...
+//
+//  Namespace declarations live in the $.xmlns object, which is a simple
+//  mapping from namespace ids to namespace URIs.  The default namespace
+//  is determined by the value associated with the empty string.
+//
+//    $.xmlns.D = "DAV:"
+//    $.xmlns.FOO = "http://www.foo.com/xmlns/foobar"
+//    $.xmlns[""] = "http://www.example.com/new/default/namespace/"
+//
+//  Unfortunately this is a global setting - I can't find a way to do
+//  query-object-specific namespaces since the jQuery selector machinery
+//  is stateless.  However, you can use the 'xmlns' function to push and
+//  pop namespace delcarations with ease:
+//
+//    $().xmlns({D:"DAV:"})     // pushes the DAV: namespace
+//    $().xmlns("DAV:")         // makes DAV: the default namespace
+//    $().xmlns(false)          // pops the namespace we just pushed
+//    $().xmlns(false)          // pops again, returning to defaults
+//
+//  To execute this as a kind of "transaction", pass a function as the
+//  second argument.  It will be executed in the context of the current
+//  jQuery object:
+//
+//    $().xmlns("DAV:",function() {
+//      //  The default namespace is DAV: within this function,
+//      //  but it is reset to the previous value on exit.
+//      return this.find("response").each(...);
+//    }).find("div")
+//
+//  If you pass a string as a function, it will be executed against the
+//  current jQuery object using find(); i.e. the following will find all
+//  "href" elements in the "DAV:" namespace:
+//
+//    $().xmlns("DAV:","href")
+//
+// 
+//  And finally, the legal stuff:
+//
+//    Copyright (c) 2009, Ryan Kelly.
+//    TAG and ATTR functions derived from jQuery's selector.js.
+//    Dual licensed under the MIT and GPL licenses.
+//    http://docs.jquery.com/License
+//
+
+(function($) {
+
+//  Some common default namespaces, that are treated specially by browsers.
+//
+var default_xmlns = {
+    "xml": "http://www.w3.org/XML/1998/namespace",
+    "xmlns": "http://www.w3.org/2000/xmlns/",
+    "html": "http://www.w3.org/1999/xhtml/"
+};
+
+//  A reverse mapping for common namespace prefixes.
+//
+var default_xmlns_rev = {}
+for(var k in default_xmlns) {
+    default_xmlns_rev[default_xmlns[k]] = k;
+}
+
+
+//  $.xmlns is a mapping from namespace identifiers to namespace URIs.
+//  The default default-namespace is "*", and we provide some additional
+//  defaults that are specified in the XML Namespaces standard.
+//
+$.extend({xmlns: $.extend({},default_xmlns,{"":"*"})});
+
+
+//  jQuery method to push/pop namespace declarations.
+//
+//  If a single argument is specified:
+//    * if it's a mapping, push those namespaces onto the stack
+//    * if it's a string, push that as the default namespace
+//    * if it evaluates to false, pop the latest namespace
+//
+//  If two arguments are specified, the second is executed "transactionally"
+//  using the namespace declarations found in the first.  It can be either a
+//  a selector string (in which case it is passed to this.find()) or a function
+//  (in which case it is called in the context of the current jQuery object).
+//  The given namespace mapping is automatically pushed before executing and
+//  popped afterwards.
+//
+var xmlns_stack = [];
+$.fn.extend({xmlns: function(nsmap,func) {
+    if(typeof nsmap == "string") {
+        nsmap = {"":nsmap};
+    }
+    if(nsmap) {
+        xmlns_stack.push($.xmlns);
+        $.xmlns = $.extend({},$.xmlns,nsmap);
+        if(func !== undefined) {
+            if(typeof func == "string") {
+                return this.find(func).xmlns(undefined)
+            } else {
+                var self = this;
+                try {
+                    self = func.call(this);
+                    if(!self) {
+                        self = this;
+                    }
+                } finally {
+                    self.xmlns(undefined);
+                }
+                return self
+            }
+        } else {
+            return this;
+        }
+    } else {
+        $.xmlns = (xmlns_stack ? xmlns_stack.pop() : {});
+        return this;
+    }
+}});
+
+
+//  Convert a namespace prefix into a namespace URI, based
+//  on the delcarations made in $.xmlns.
+//
+var getNamespaceURI = function(id) {
+    // No namespace id, use the default.
+    if(!id) {
+        return $.xmlns[""];
+    }
+    // Strip the pipe character from the specifier
+    id = id.substr(0,id.length-1);
+    // Certain special namespaces aren't mapped to a URI
+    if(id == "" || id == "*") {
+        return id;
+    }
+    var ns = $.xmlns[id];
+    if(typeof(ns) == "undefined") {
+        throw "Syntax error, undefined namespace prefix '" + id + "'";
+    }
+    return ns;
+};
+
+
+//  Update the regex used by $.expr to parse selector components for a
+//  particular type of selector (e.g. "TAG" or "ATTR").
+//
+//  This logic is taken straight from the jQuery/Sizzle sources.
+//
+var setExprMatchRegex = function(type,regex) {
+  $.expr.match[type] = new RegExp(regex.source + /(?![^\[]*\])(?![^\(]*\))/.source);
+  if($.expr.leftMatch) {
+      $.expr.leftMatch[type] = new RegExp(/(^(?:.|\r|\n)*?)/.source + $.expr.match[type].source.replace(/\\(\d+)/g, function(all, num){
+          return "\\" + (num - 0 + 1);
+      }));
+  }
+}
+
+
+
+//  Modify the TAG match regexp to include optional namespace selector.
+//  This is basically (namespace|)?(tagname).
+//
+setExprMatchRegex("TAG",/^((?:((?:[\w\u00c0-\uFFFF\*_-]*\|)?)((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)))/);
+
+
+//  Perform some capability-testing.
+//
+var div = document.createElement("div");
+
+//  Sometimes getElementsByTagName("*") will return comment nodes,
+//  which we will have to remove from the results.
+//
+var gebtn_yields_comments = false;
+div.appendChild(document.createComment(""));
+if(div.getElementsByTagName("*").length > 0) {
+    gebtn_yields_comments = true;
+}
+
+//  Some browsers return node.localName in upper case, some in lower case.
+//
+var localname_is_uppercase = true;
+if(div.localName && div.localName == "div") {
+    localname_is_uppercase = false;
+}
+
+//  Allow the testing div to be garbage-collected.
+//
+div = null;
+
+
+//  Modify the TAG find function to account for a namespace selector.
+//
+$.expr.find.TAG = function(match,context,isXML) {
+    var ns = getNamespaceURI(match[2]);
+    var ln = match[3];
+    var res;
+    if(typeof context.getElementsByTagNameNS != "undefined") {
+        //  Easy case - we have getElementsByTagNameNS
+        res = context.getElementsByTagNameNS(ns,ln);
+    } else if(typeof context.selectNodes != "undefined") {
+        //  Use xpath if possible (not available on HTML DOM nodes in IE)
+        if(context.ownerDocument) {
+            context.ownerDocument.setProperty("SelectionLanguage","XPath");
+        } else {
+            context.setProperty("SelectionLanguage","XPath");
+        }
+        var predicate = "";
+        if(ns != "*") {
+            if(ln != "*") {
+                predicate="namespace-uri()='"+ns+"' and local-name()='"+ln+"'";
+            } else {
+                predicate="namespace-uri()='"+ns+"'";
+            }
+        } else {
+            if(ln != "*") {
+                predicate="local-name()='"+ln+"'";
+            }
+        }
+        if(predicate) {
+            res = context.selectNodes("descendant-or-self::*["+predicate+"]");
+        } else {
+            res = context.selectNodes("descendant-or-self::*");
+        }
+    } else {
+        //  Otherwise, we need to simulate using getElementsByTagName
+        res = context.getElementsByTagName(ln); 
+        if(gebtn_yields_comments && ln == "*") {
+            var tmp = [];
+            for(var i=0; res[i]; i++) {
+                if(res[i].nodeType == 1) {
+                    tmp.push(res[i]);
+                }
+            }
+            res = tmp;
+        }
+        if(res && ns != "*") {
+            var tmp = [];
+            for(var i=0; res[i]; i++) {
+               if(res[i].namespaceURI == ns || res[i].tagUrn == ns) {
+                   tmp.push(res[i]);
+               }
+            }
+            res = tmp;
+        }
+    }
+    return res;
+};
+
+
+//  Check whether a node is part of an XML document.
+//  Copied verbatim from jQuery sources, needed in TAG preFilter below.
+//
+var isXML = function(elem){
+    return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" ||
+            !!elem.ownerDocument && elem.ownerDocument.documentElement.nodeName !== "HTML";
+};
+
+
+//  Modify the TAG preFilter function to work with modified match regexp.
+//  This normalises case of the tag name if we're in a HTML document.
+//
+$.expr.preFilter.TAG = function(match, curLoop, inplace, result, not, isXML) {
+  var ln = match[3];
+  if(!isXML) {
+      if(localname_is_uppercase) {
+          ln = ln.toUpperCase();
+      } else {
+          ln = ln.toLowerCase();
+      }
+  }
+  return [match[0],getNamespaceURI(match[2]),ln];
+};
+
+
+//  Modify the TAG filter function to account for a namespace selector.
+//
+$.expr.filter.TAG = function(elem,match) {
+    var ns = match[1];
+    var ln = match[2];
+    var e_ns = elem.namespaceURI ? elem.namespaceURI : elem.tagUrn;
+    var e_ln = elem.localName ? elem.localName : elem.tagName;
+    if(ns == "*" || e_ns == ns || (ns == "" && !e_ns)) {
+        return ((ln == "*" && elem.nodeType == 1)  || e_ln == ln);
+    }
+    return false;
+};
+
+
+//  Modify the ATTR match regexp to extract a namespace selector.
+//  This is basically ([namespace|])(attrname)(op)(quote)(pattern)(quote)
+//
+setExprMatchRegex("ATTR",/\[\s*((?:((?:[\w\u00c0-\uFFFF\*_-]*\|)?)((?:[\w\u00c0-\uFFFF_-]|\\.)+)))\s*(?:(\S?=)\s*(['"]*)(.*?)\5|)\s*\]/);
+
+
+//  Modify the ATTR preFilter function to account for new regexp match groups,
+//  and normalise the namespace URI.
+//
+$.expr.preFilter.ATTR = function(match, curLoop, inplace, result, not, isXML) {
+    var name = match[3].replace(/\\/g, "");
+    if(!isXML && $.expr.attrMap[name]) {
+        match[3] = $.expr.attrMap[name];
+    }
+    if( match[4] == "~=" ) {
+        match[6] = " " + match[6] + " ";
+    }
+    if(!match[2] || match[2] == "|") {
+        match[2] = "";
+    } else {
+        match[2] = getNamespaceURI(match[2]);
+    }
+    return match;
+};
+
+
+//  Modify the ATTR filter function to account for namespace selector.
+//  Unfortunately this means factoring out the attribute-checking code
+//  into a separate function, since it might be called multiple times.
+//
+var filter_attr = function(result,type,check) {
+    var value = result + "";
+    return result == null ?
+                type === "!=" :
+                type === "=" ?
+                value === check :
+                type === "*=" ?
+                value.indexOf(check) >= 0 :
+                type === "~=" ?
+                (" " + value + " ").indexOf(check) >= 0 :
+                !check ?
+                value && result !== false :
+                type === "!=" ?
+                value != check :
+                type === "^=" ?
+                value.indexOf(check) === 0 :
+                type === "$=" ?
+                value.substr(value.length - check.length) === check :
+                type === "|=" ?
+                value === check || value.substr(0,check.length+1)===check+"-" :
+                false;
+}
+
+
+$.expr.filter.ATTR = function(elem, match) {
+    var ns = match[2];
+    var name = match[3];
+    var type = match[4];
+    var check = match[6];
+    var result;
+    //  No namespace, just use ordinary attribute lookup.
+    if(ns == "") {
+        result = $.expr.attrHandle[name] ?
+                     $.expr.attrHandle[name](elem) :
+                     elem[name] != null ?
+                         elem[name] :
+                         elem.getAttribute(name);
+        return filter_attr(result,type,check);
+    }
+    //  Directly use getAttributeNS if applicable and available
+    if(ns != "*" && typeof elem.getAttributeNS != "undefined") {
+        return filter_attr(elem.getAttributeNS(ns,name),type,check);
+    }
+    //  Need to iterate over all attributes, either because we couldn't
+    //  look it up or because we need to match all namespaces.
+    var attrs = elem.attributes;
+    for(var i=0; attrs[i]; i++) {
+        var ln = attrs[i].localName;
+        if(!ln) {
+            ln = attrs[i].nodeName
+            var idx = ln.indexOf(":");
+            if(idx >= 0) {
+                ln = ln.substr(idx+1);
+            }
+        }
+        if(ln == name) {
+            result = attrs[i].nodeValue;
+            if(ns == "*" || attrs[i].namespaceURI == ns) {
+                if(filter_attr(result,type,check)) {
+                    return true;
+                }
+            }
+            if(attrs[i].namespaceURI === "" && attrs[i].prefix) {
+                if(attrs[i].prefix == default_xmlns_rev[ns]) {
+                    if(filter_attr(result,type,check)) {
+                        return true;
+                    }
+                }
+            }
+        }
+    }
+    return false;
+};
+
+
+})(jQuery);
+
index de1d8e5..8625b11 100644 (file)
                        var global = $("*[data-ui-error-for='__all__']", this.$elem);
                        var unassigned = [];
 
+            $("*[data-ui-error-for]", this.$elem).text('');
                        for (var field_name in errors)
                        {
                                var span = $("*[data-ui-error-for='"+field_name+"']", this.$elem);
diff --git a/redakcja/static/js/wiki/dialog_addtag.js b/redakcja/static/js/wiki/dialog_addtag.js
deleted file mode 100644 (file)
index 1a90ccf..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Dialog for saving document to the server
- *
- */
-(function($){
-
-    function AddTagDialog(element, options){
-        if (!options.revision  && options.revision != 0)
-            throw "AddTagDialog needs a revision number.";
-
-        this.ctx = $.wiki.exitContext();
-        this.clearForm();
-
-        /* fill out hidden fields */
-        this.$form = $('form', element);
-
-        $("input[name='addtag-id']", this.$form).val(CurrentDocument.id);
-        $("input[name='addtag-revision']", this.$form).val(options.revision);
-
-        $.wiki.cls.GenericDialog.call(this, element);
-    };
-
-    AddTagDialog.prototype = $.extend(new $.wiki.cls.GenericDialog(), {
-        cancelAction: function(){
-            $.wiki.enterContext(this.ctx);
-            this.hide();
-        },
-
-        saveAction: function(){
-            var self = this;
-
-            self.$elem.block({
-                message: "Dodawanie tagu",
-                fadeIn: 0,
-            });
-
-            CurrentDocument.setTag({
-                form: self.$form,
-                success: function(doc, changed, info){
-                    self.$elem.block({
-                        message: info,
-                        timeout: 2000,
-                        fadeOut: 0,
-                        onUnblock: function(){
-                            self.hide();
-                            $.wiki.enterContext(self.ctx);
-                        }
-                    });
-                },
-                failure: function(doc, info){
-                    console.log("Failure", info);
-                    self.reportErrors(info);
-                    self.$elem.unblock();
-                }
-            });
-        }
-    });
-
-    /* make it global */
-    $.wiki.cls.AddTagDialog = AddTagDialog;
-})(jQuery);
diff --git a/redakcja/static/js/wiki/dialog_pubmark.js b/redakcja/static/js/wiki/dialog_pubmark.js
new file mode 100755 (executable)
index 0000000..902a737
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Dialog for marking document for publishing
+ *
+ */
+(function($){
+
+    function PubmarkDialog(element, options){
+        if (!options.revision  && options.revision != 0)
+            throw "PubmarkDialog needs a revision number.";
+
+        this.ctx = $.wiki.exitContext();
+        this.clearForm();
+
+        /* fill out hidden fields */
+        this.$form = $('form', element);
+
+        $("input[name='pubmark-id']", this.$form).val(CurrentDocument.id);
+        $("input[name='pubmark-revision']", this.$form).val(options.revision);
+
+        $.wiki.cls.GenericDialog.call(this, element);
+    };
+
+    PubmarkDialog.prototype = $.extend(new $.wiki.cls.GenericDialog(), {
+        cancelAction: function(){
+            $.wiki.enterContext(this.ctx);
+            this.hide();
+        },
+
+        saveAction: function(){
+            var self = this;
+
+            self.$elem.block({
+                message: "Oznaczanie wersji",
+                fadeIn: 0,
+            });
+
+            CurrentDocument.pubmark({
+                form: self.$form,
+                success: function(doc, changed, info){
+                    self.$elem.block({
+                        message: info,
+                        timeout: 2000,
+                        fadeOut: 0,
+                        onUnblock: function(){
+                            self.hide();
+                            $.wiki.enterContext(self.ctx);
+                        }
+                    });
+                },
+                failure: function(doc, info){
+                    console.log("Failure", info);
+                    self.reportErrors(info);
+                    self.$elem.unblock();
+                }
+            });
+        }
+    });
+
+    /* make it global */
+    $.wiki.cls.PubmarkDialog = PubmarkDialog;
+})(jQuery);
diff --git a/redakcja/static/js/wiki/dialog_revert.js b/redakcja/static/js/wiki/dialog_revert.js
new file mode 100644 (file)
index 0000000..4d550f9
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Dialog for reverting document on the server
+ *
+ */
+(function($) {
+
+    function RevertDialog(element, options) {
+        this.ctx = $.wiki.exitContext();
+        this.clearForm();
+
+        /* fill out hidden fields */
+        this.$form = $('form', element);
+
+        $("input[name='textrevert-revision']", this.$form).val(options.revision);
+
+        $.wiki.cls.GenericDialog.call(this, element);
+    };
+
+    RevertDialog.prototype = new $.wiki.cls.GenericDialog();
+
+    RevertDialog.prototype.cancelAction = function() {
+        $.wiki.enterContext(this.ctx);
+        this.hide();
+    };
+
+    RevertDialog.prototype.revertAction = function() {
+            var self = this;
+
+            self.$elem.block({
+                message: "Przywracanie...",
+                fadeIn: 0,
+            });
+            $.wiki.blocking = self.$elem;
+
+            try {
+
+                CurrentDocument.revertToVersion({
+                    form: self.$form,
+                    success: function(e, msg) {
+                        self.$elem.block({
+                            message: msg,
+                            timeout: 2000,
+                            fadeOut: 0,
+                            onUnblock: function() {
+                                self.hide();
+                                $.wiki.enterContext(self.ctx);
+                            }
+                        });
+                    },
+                    'failure': function(e, info) {
+                        console.log("Failure", info);
+                        self.reportErrors(info);
+                        self.$elem.unblock();
+                    }
+                });
+
+            } catch(e) {
+                console.log('Exception:', e)
+                self.$elem.unblock();
+            }
+    }; /* end of revert dialog */
+
+    /* make it global */
+    $.wiki.cls.RevertDialog = RevertDialog;
+})(jQuery);
index aa9258d..903c0e1 100644 (file)
@@ -11,7 +11,6 @@
                /* fill out hidden fields */
                this.$form = $('form', element);
 
-               $("input[name='textsave-id']", this.$form).val(CurrentDocument.id);
                $("input[name='textsave-parent_revision']", this.$form).val(CurrentDocument.revision);
 
                $.wiki.cls.GenericDialog.call(this, element);
index f62cd65..5bac593 100644 (file)
@@ -23,7 +23,7 @@
 
                 self.$refresh.removeClass('active');
                 $this.addClass('active');
-                atype = $this.text();
+                atype = $this.attr('data-tag');
 
                 self.$annos.hide();
                 self.$error.hide();
         else {
             self.$annos.html('');
             var anno_list = new Array();
-            var annos = doc.getElementsByTagName(atype);
+            var annos = $(atype, doc);
             var counter = annos.length;
+            var atype_rx = atype.replace(/,/g, '|');
+            var ann_expr = new RegExp("^<("+atype_rx+")[^>]*>|</("+atype_rx+")>$", "g")
 
             if (annos.length == 0)
             {
                 self.$spinner.hide();
                 self.$annos.show();
             }
-            for (var i=0; i<annos.length; i++)
-            {
-                ann_expr = new RegExp("^<"+atype+"[^>]*>|</"+atype+">$", "g")
-                xml_text = serializer.serializeToString(annos[i]).replace(ann_expr, "");
+            annos.each(function (i, elem) {
+                xml_text = serializer.serializeToString(elem).replace(ann_expr, "");
                 xml2html({
                     xml: "<akap>" + xml_text + "</akap>",
                     success: function(xml_text){
 
                             if (!counter) {
                                 anno_list.sort(function(a, b){return a.sortby.localeCompare(b.sortby);});
-                                self.$annos.append(anno_list);
+                                for (i in anno_list)
+                                    self.$annos.append(anno_list[i]);
                                 self.$spinner.hide();
                                 self.$annos.show();
                             }
                         self.$error.show();
                     }
                 });
-            }
+            });
         }
     }
 
index 4e0bdee..3ccaea7 100644 (file)
     function addSymbol() {
         if($('div.html-editarea textarea')[0]) {
             var specialCharsContainer = $("<div id='specialCharsContainer'><a href='#' id='specialCharsClose'>Zamknij</a><table id='tableSpecialChars' style='width: 600px;'></table></div>");
+                        
             var specialChars = ['Ą','ą','Ć','ć','Ę','ę','Ł','ł','Ń','ń','Ó','ó','Ś','ś','Ż','ż','Ź','ź','Á','á','À','à',
             'Â','â','Ä','ä','Å','å','Ā','ā','Ă','ă','Ã','ã',
             'Æ','æ','Ç','ç','Č','č','Ċ','ċ','Ď','ď','É','é','È','è',
             'Ê','ê','Ë','ë','Ē','ē','Ě','ě','Ġ','ġ','Ħ','ħ','Í','í','Î','î',
             'Ī','ī','Ĭ','ĭ','Ľ','ľ','Ñ','ñ','Ň','ň','Ó','ó','Ö','ö',
             'Ô','ô','Ō','ō','Ǒ','ǒ','Œ','œ','Ø','ø','Ř','ř','Š',
-            'š','Ş','ş','Ť','ť','Ţ','ţ','Ű','ű','Ú','ú',
+            'š','Ş','ş','Ť','ť','Ţ','ţ','Ű','ű','Ú','ú','Ù','ù',
             'Ü','ü','Ů','ů','Ū','ū','Û','û','Ŭ','ŭ',
             'Ý','ý','Ž','ž','ß','Ð','ð','Þ','þ','А','а','Б',
             'б','В','в','Г','г','Д','д','Е','е','Ё','ё','Ж',
             'Τ','τ','Υ','υ','Φ','φ','Χ','χ','Ψ','ψ','Ω','ω','–',
             '—','¡','¿','$','¢','£','€','©','®','°','¹','²','³',
             '¼','½','¾','†','§','‰','•','←','↑','→','↓',
-            '„”','«»','’','[',']','[','~','|','−','·',
+            '„','”','„”','«','»','«»','»«','’','[',']','~','|','−','·',
             '×','÷','≈','≠','±','≤','≥','∈'];
             var tableContent = "<tr>";
             
             
             tableContent += "</tr>";                                   
             $("#content").append(specialCharsContainer);
+            
+            
+             // localStorage for recently used characters - reading
+             if (typeof(localStorage) != 'undefined') {
+                 if (localStorage.getItem("recentSymbols")) {
+                     var recent = localStorage.getItem("recentSymbols");
+                     var recentArray = recent.split(";");
+                     var recentRow = "";
+                     for(var i in recentArray.reverse()) {
+                        recentRow += "<td><input type='button' class='specialBtn recentSymbol' value='"+recentArray[i]+"'/></td>";              
+                     }
+                     recentRow = "<tr>" + recentRow + "</tr>";                              
+                 }
+             }            
+            $("#tableSpecialChars").append(recentRow);
             $("#tableSpecialChars").append(tableContent);
             
             /* events */
             
             $('.specialBtn').click(function(){
-                insertAtCaret($('div.html-editarea textarea')[0], $(this).val());
+                var editArea = $('div.html-editarea textarea')[0];
+                var insertVal = $(this).val();
+                
+                // if we want to surround text with quotes
+                // not sure if just check if value has length == 2
+                
+                if (insertVal.length == 2) {
+                    var startTag = insertVal[0];
+                    var endTag = insertVal[1];
+                               var textAreaOpened = editArea;                                                  
+                               //IE support
+                               if (document.selection) {
+                                   textAreaOpened.focus();
+                                   sel = document.selection.createRange();
+                                   sel.text = startTag + sel.text + endTag;
+                               }
+                               //MOZILLA/NETSCAPE support
+                               else if (textAreaOpened.selectionStart || textAreaOpened.selectionStart == '0') {
+                                   var startPos = textAreaOpened.selectionStart;
+                                   var endPos = textAreaOpened.selectionEnd;
+                                   textAreaOpened.value = textAreaOpened.value.substring(0, startPos)
+                                         + startTag + textAreaOpened.value.substring(startPos, endPos) + endTag + textAreaOpened.value.substring(endPos, textAreaOpened.value.length);
+                               }                
+                } else {
+                    // if we just want to insert single symbol
+                    insertAtCaret(editArea, insertVal);
+                }
+                
+                // localStorage for recently used characters - saving
+                if (typeof(localStorage) != 'undefined') {
+                    if (localStorage.getItem("recentSymbols")) {
+                        var recent = localStorage.getItem("recentSymbols");
+                        var recentArray = recent.split(";");
+                        var valIndex = $.inArray(insertVal, recentArray);
+                        //alert(valIndex);
+                        if(valIndex == -1) {
+                            // value not present in array yet
+                            if(recentArray.length > 13){
+                                recentArray.shift();
+                                recentArray.push(insertVal);
+                            } else {
+                                recentArray.push(insertVal);
+                            }
+                        } else  {
+                            // value already in the array
+                            for(var i = valIndex; i < recentArray.length; i++){
+                                recentArray[i] = recentArray[i+1];
+                            }
+                            recentArray[recentArray.length-1] = insertVal;
+                        }
+                        localStorage.setItem("recentSymbols", recentArray.join(";"));
+                    } else {
+                        localStorage.setItem("recentSymbols", insertVal);
+                    }
+                }
+                
                 $(specialCharsContainer).remove();
             });         
             $('#specialCharsClose').click(function(){
         }
 
         // start edition on this node
-        var $overlay = $('<div class="html-editarea"><button class="accept-button">Zapisz</button><button class="delete-button">Usuń</button><button class="tytul-button akap-edit-button">tytuł dzieła</button><button class="wyroznienie-button akap-edit-button">wyróżnienie</button><button class="slowo-button akap-edit-button">słowo obce</button><textarea></textarea></div>').css({
+        var $overlay = $('<div class="html-editarea"><button class="accept-button">Zapisz</button><button class="delete-button">Usuń</button><button class="tytul-button akap-edit-button">tytuł dzieła</button><button class="wyroznienie-button akap-edit-button">wyróżnienie</button><button class="slowo-button akap-edit-button">słowo obce</button><button class="znak-button akap-edit-button">znak spec.</button><textarea></textarea></div>').css({
             position: 'absolute',
             height: h,
             left: x,
                         }
                     })
                     
-                    var msg = $("<div class='saveNotify'><p>Twoje zmiany zostały naniesione na tekst źródłowy. Pamiętaj, że aby zmiany zostały utrwalone <span>należy je zapisać</span>!</p><p class='notifyTip'>Ta wiadomość zostanie automatycznie zamknięta za 6 sekund.</p></div>");
+                    var msg = $("<div class='saveNotify'><p>Pamiętaj, żeby zapisać swoje zmiany.</p></div>");
                     $("#base").prepend(msg);
-                    $("#save-button").css({border: '2px solid #801000', backgroundColor: '#E1C1C1'});
-                    $('#base .saveNotify').fadeOut(7000, function(){
+                    $('#base .saveNotify').fadeOut(3000, function(){
                         $(this).remove(); 
-                        $("#save-button").css({border: '1px solid black'});
                     });
                 }
 
                        } else if (buttonName == "tytuł dzieła") {
                                startTag = "<tytul_dziela>";
                                endTag = "</tytul_dziela>";
+                       } else if(buttonName == "znak spec."){
+                           addSymbol();
+                           return false;
                        }
+                       
                        var myField = textAreaOpened;                   
                         
                        //IE support
                 $('#insert-theme-button').click(function(){
                     addTheme();
                     return false;
-                });
-                
-                $('#insert-symbol-button').click(function(){
-                    addSymbol();
-                    return false;
-                });                
+                });            
 
                 $('.edit-button').live('click', function(event){
                     event.preventDefault();
             return _finalize(failure);
 
         html2text({
-            element: $('#html-view div').get(0),
+            element: $('#html-view').get(0),
+            stripOuter: true,
             success: function(text){
                 self.doc.setText(text);
                 _finalize(success);
index 56faaa1..4b74b3e 100644 (file)
@@ -1,14 +1,17 @@
 (function($){
 
     function normalizeNumber(pageNumber, pageCount){
-        // Numer strony musi być pomiędzy 1 a najwyższym numerem
+        // Page number should be >= 1, <= pageCount; 0 if pageCount = 0
         var pageNumber = parseInt(pageNumber, 10);
 
+        if (!pageCount)
+            return 0;
+
         if (!pageNumber ||
-        pageNumber == NaN ||
-        pageNumber == Infinity ||
-        pageNumber == -Infinity ||
-        pageNumber < 1)
+                isNaN(pageNumber) ||
+                pageNumber == Infinity ||
+                pageNumber == -Infinity ||
+                pageNumber < 1)
             return 1;
 
         if (pageNumber > pageCount)
@@ -52,6 +55,7 @@
 
             this.dimensions = {};
             this.zoomFactor = 1;
+            this.config().page = CurrentDocument.galleryStart;
             this.$element = $("#side-gallery");
             this.$numberInput = $('.page-number', this.$element);
 
@@ -88,7 +92,7 @@
                 self.dimensions.galleryHeight = self.$image.parent().height();
             });
 
-            $('.gallery-image img', this.$element).load(function(){
+            this.$image.load(function(){
                 console.log("Image loaded.")
                 self._resizeImage();
             }).bind('mousedown', function() {
         var $img = this.$image;
 
         $img.css({
-            width: null,
-            height: null
+            width: '',
+            height: ''
         });
 
         this.dimensions = {
 
     ScanGalleryPerspective.prototype.setPage = function(newPage){
         newPage = normalizeNumber(newPage, this.doc.galleryImages.length);
-        $('#imagesCount').html("/"+this.doc.galleryImages.length);
         this.$numberInput.val(newPage);
                this.config().page = newPage;
         $('.gallery-image img', this.$element).attr('src', this.doc.galleryImages[newPage - 1]);
         // var position = normalizePosition(this.$image.position().left, this.$image.position().top, this.dimensions.galleryWidth, this.dimensions.galleryHeight, this.dimensions.width, this.dimensions.height);
 
                this._resizeImage();
-        /* this.$image.css({
-            width: this.dimensions.width,
-            height: this.dimensions.height,
-            left: position.x,
-            top: position.y
-        });*/
     };
 
        /*
                 self.$image.show();
                                console.log("gconfig:", self.config().page );
                                self.setPage( self.config().page );
+                $('#imagesCount').html("/" + doc.galleryImages.length);
 
                 $('.error_message', self.$element).hide();
                 if(success) success();
index d35a8da..85adca0 100644 (file)
@@ -5,18 +5,32 @@
 
                options.callback = function() {
                        var self = this;
+            if (CurrentDocument.diff) {
+                rev_from = CurrentDocument.diff[0];
+                rev_to = CurrentDocument.diff[1];
+                this.doc.fetchDiff({
+                    from: rev_from,
+                    to: rev_to,
+                    success: function(doc, data){
+                        var result = $.wiki.newTab(doc, ''+rev_from +' -> ' + rev_to, 'DiffPerspective');
+
+                        $(result.view).html(data);
+                        $.wiki.switchToTab(result.tab);
+                    }
+                });
+            }
 
                        // first time page is rendered
                $('#make-diff-button').click(function() {
                                self.makeDiff();
                        });
 
-                       $('#tag-changeset-button').click(function() {
-                               self.showTagForm();
+                       $('#pubmark-changeset-button').click(function() {
+                               self.showPubmarkForm();
                        });
 
                $('#doc-revert-button').click(function() {
-                   self.revertDocumentToVersion();
+                   self.revertDialog();
                });
 
                        $('#open-preview-button').click(function(event) {
                                        stub: $stub,
                                        data: this,
                                        filters: {
-                                               tag: function(value) {
-                                                       return tags.filter("*[value='"+value+"']").text();
-                                               }
+//                                             tag: function(value) {
+//                                                     return tags.filter("*[value='"+value+"']").text();
+//                                             }
 //                        description: function(value) {
 //                                                 return value.replace('\n', ');
 //                                             }
         });
     };
 
-       HistoryPerspective.prototype.showTagForm = function(){
+       HistoryPerspective.prototype.showPubmarkForm = function(){
                var selected = $('#changes-list .entry.selected');
 
                if (selected.length != 1) {
         }
 
                var version = parseInt($("*[data-stub-value='version']", selected[0]).text());
-               $.wiki.showDialog('#add_tag_dialog', {'revision': version});
+               $.wiki.showDialog('#pubmark_dialog', {'revision': version});
        };
 
        HistoryPerspective.prototype.makeDiff = function() {
         });
     };
 
-    HistoryPerspective.prototype.revertDocumentToVersion = function(){
+    HistoryPerspective.prototype.revertDialog = function(){
+        var self = this;
         var selected = $('#changes-list .entry.selected');
 
         if (selected.length != 1) {
         }
 
         var version = parseInt($("*[data-stub-value='version']", selected[0]).text());
-        this.doc.revertToVersion({'revision': version});
+        $.wiki.showDialog('#revert_dialog', {revision: version});
     };
 
     $.wiki.HistoryPerspective = HistoryPerspective;
index 811096d..de6fcf1 100644 (file)
@@ -1,40 +1,33 @@
 (function($){
 
        function SummaryPerspective(options) {
-               var old_callback = options.callback;
-               var self = this;
-
-        options.callback = function(){
-                       $('#publish_button').click(function() {
-                               $.blockUI({message: "Oczekiwanie na odpowiedź serwera..."});
-                               self.doc.publish({
-                                       success: function(doc, data) {
-                                               $.blockUI({message: "Udało się.", timeout: 2000});
-                                       },
-                                       failure: function(doc, message) {
-                                               $.blockUI({
-                                                       message: message,
-                                                       timeout: 5000
-                                               });
-                                       }
-
-                               });
-                       });
-
-                       old_callback.call(this);
-               };
-
                $.wiki.Perspective.call(this, options);
     };
 
     SummaryPerspective.prototype = new $.wiki.Perspective();
 
+    SummaryPerspective.prototype.showCharCount = function() {
+        var cc;
+        try {
+            cc = this.doc.getLength();
+            $('#charcount_untagged').hide();
+        }
+        catch (e) {
+            $('#charcount_untagged').show();
+            cc = this.doc.text.replace(/\s{2,}/g, ' ').length;
+        }
+        $('#charcount').html(cc);
+        $('#charcount_pages').html((Math.round(cc/18)/100).toLocaleString());
+    }
+
     SummaryPerspective.prototype.freezeState = function(){
         // must
     };
 
        SummaryPerspective.prototype.onEnter = function(success, failure){
                $.wiki.Perspective.prototype.onEnter.call(this);
+               
+               this.showCharCount();
 
                console.log("Entered summery view");
        };
index 97d1886..4a4da5a 100644 (file)
         */
        function reverse() {
                var vname = arguments[0];
-               var base_path = "/documents";
+               var base_path = "/editor";
 
                if (vname == "ajax_document_text") {
-                       var path = "/" + arguments[1] + "/text";
+                       var path = "/text/" + arguments[1] + '/';
 
                if (arguments[2] !== undefined)
-                               path += "/" + arguments[2];
+                               path += arguments[2] + '/';
 
                        return base_path + path;
                }
 
+        if (vname == "ajax_document_revert") {
+            return base_path + "/revert/" + arguments[1] + '/';
+        }
+
+
                if (vname == "ajax_document_history") {
 
-                       return base_path + "/" + arguments[1] + "/history";
+                       return base_path + "/history/" + arguments[1] + '/';
                }
 
                if (vname == "ajax_document_gallery") {
 
-                       return base_path + "/" + arguments[1] + "/gallery";
+                       return base_path + "/gallery/" + arguments[1] + '/';
                }
 
                if (vname == "ajax_document_diff")
-                       return base_path + "/" + arguments[1] + "/diff";
+                       return base_path + "/diff/" + arguments[1] + '/';
 
         if (vname == "ajax_document_rev")
-            return base_path + "/" + arguments[1] + "/rev";
-
-               if (vname == "ajax_document_addtag")
-                       return base_path + "/" + arguments[1] + "/tags";
+            return base_path + "/rev/" + arguments[1] + '/';
 
-               if (vname == "ajax_publish")
-                       return base_path + "/" + arguments[1] + "/publish";
+               if (vname == "ajax_document_pubmark")
+                       return base_path + "/pubmark/" + arguments[1] + '/';
 
                console.log("Couldn't reverse match:", vname);
                return "/404.html";
         */
        function WikiDocument(element_id) {
                var meta = $('#' + element_id);
-               this.id = meta.attr('data-document-name');
+               this.id = meta.attr('data-chunk-id');
 
                this.revision = $("*[data-key='revision']", meta).text();
                this.readonly = !!$("*[data-key='readonly']", meta).text();
 
                this.galleryLink = $("*[data-key='gallery']", meta).text();
+        this.galleryStart = parseInt($("*[data-key='gallery-start']", meta).text());
+
+        var diff = $("*[data-key='diff']", meta).text();
+        if (diff) {
+            diff = diff.split(',');
+            if (diff.length == 2 && diff[0] < diff[1])
+                this.diff = diff;
+            else if (diff.length == 1) {
+                diff = parseInt(diff);
+                if (diff != NaN)
+                    this.diff = [diff - 1, diff];
+            }
+        }
+
                this.galleryImages = [];
                this.text = null;
                this.has_local_changes = false;
                                self.galleryImages = data;
                                params['success'](self, data);
                        },
-                       error: function() {
+                       error: function(xhr) {
+                switch (xhr.status) {
+                    case 403:
+                        var msg = 'Galerie dostępne tylko dla zalogowanych użytkowników.';
+                        break;
+                    case 404:
+                        var msg = "Nie znaleziono galerii o nazwie: '" + self.galleryLink + "'.";
+                    default:
+                        var msg = "Nie udało się wczytać galerii o nazwie: '" + self.galleryLink + "'.";
+                }
                                self.galleryImages = [];
-                               params['failure'](self, "<p>Nie udało się wczytać galerii pod nazwą: '" + self.galleryLink + "'.</p>");
+                               params['failure'](self, "<p>" + msg + "</p>");
                        }
                });
        };
                        data[this.name] = this.value;
                });
 
-               var metaComment = '<!--';
-               metaComment += '\n\tgallery:' + self.galleryLink;
-               metaComment += '\n-->\n'
-
-               data['textsave-text'] = metaComment + self.text;
+               data['textsave-text'] = self.text;
 
                $.ajax({
                        url: reverse("ajax_document_text", self.id),
         });
        }; /* end of save() */
 
-       WikiDocument.prototype.publish = function(params) {
-               params = $.extend({}, noops, params);
-               var self = this;
-               $.ajax({
-                       url: reverse("ajax_publish", self.id),
-                       type: "POST",
-                       dataType: "json",
-                       success: function(data) {
-                               params.success(self, data);
-                       },
-                       error: function(xhr) {
-                               if (xhr.status == 403 || xhr.status == 401) {
-                                       params.failure(self, "Nie masz uprawnień lub nie jesteś zalogowany.");
-                               }
-                               else {
-                                       try {
-                                               params.failure(self, xhr.responseText);
-                                       }
-                                       catch (e) {
-                                               params.failure(self, "Nie udało się - błąd serwera.");
-                                       };
-                               };
+    WikiDocument.prototype.revertToVersion = function(params) {
+        var self = this;
+        params = $.extend({}, noops, params);
 
-                       }
-               });
-       };
-       WikiDocument.prototype.setTag = function(params) {
+        if (params.revision >= this.revision) {
+            params.failure(self, 'Proszę wybrać rewizję starszą niż aktualna.');
+            return;
+        }
+
+        // Serialize form to dictionary
+        var data = {};
+        $.each(params['form'].serializeArray(), function() {
+            data[this.name] = this.value;
+        });
+
+        $.ajax({
+            url: reverse("ajax_document_revert", self.id),
+            type: "POST",
+            dataType: "json",
+            data: data,
+            success: function(data) {
+                if (data.text) {
+                    self.text = data.text;
+                    self.revision = data.revision;
+                    self.gallery = data.gallery;
+                    self.triggerDocumentChanged();
+
+                    params.success(self, "Udało się przywrócić wersję :)");
+                }
+                else {
+                    params.failure(self, "Przywracana wersja identyczna z aktualną. Anulowano przywracanie.");
+                }
+            },
+            error: function(xhr) {
+                params.failure(self, "Nie udało się przywrócić wersji - błąd serwera.");
+            }
+        });
+    };
+
+       WikiDocument.prototype.pubmark = function(params) {
                params = $.extend({}, noops, params);
                var self = this;
                var data = {
-                       "addtag-id": self.id,
+                       "pubmark-id": self.id,
                };
 
                /* unpack form */
                });
 
                $.ajax({
-                       url: reverse("ajax_document_addtag", self.id),
+                       url: reverse("ajax_document_pubmark", self.id),
                        type: "POST",
                        dataType: "json",
                        data: data,
                });
        };
 
+    WikiDocument.prototype.getLength = function(params) {
+        var xml = this.text.replace(/\/(\s+)/g, '<br />$1');
+        var parser = new DOMParser();
+        var doc = parser.parseFromString(xml, 'text/xml');
+        var error = $('parsererror', doc);
+
+        if (error.length > 0) {
+            throw "Not an XML document.";
+        }
+        $.xmlns["rdf"] = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; 
+        $('rdf|RDF, motyw, pa, pe, pr, pt', doc).remove();
+        var text = $(doc).text();
+        text = $.trim(text.replace(/\s{2,}/g, ' '));
+        return text.length;
+    }
+
+
        $.wikiapi.WikiDocument = WikiDocument;
 })(jQuery);
index 9926673..1327fc6 100644 (file)
@@ -17,7 +17,7 @@ function withStylesheets(code_block, onError)
     if (!xml2htmlStylesheet) {
        $.blockUI({message: 'Ładowanie arkuszy stylów...'});
        $.ajax({
-               url: STATIC_URL + 'xsl/wl2html_client.xsl?20101123',
+               url: STATIC_URL + 'xsl/wl2html_client.xsl?20110520',
                dataType: 'xml',
                timeout: 10000,
                success: function(data) {
@@ -40,7 +40,7 @@ function withThemes(code_block, onError)
 {
     if (typeof withThemes.canon == 'undefined') {
         $.ajax({
-            url: '/themes',
+            url: '/editor/themes',
             dataType: 'text',
             success: function(data) {
                 withThemes.canon = data.split('\n');
@@ -61,6 +61,7 @@ function withThemes(code_block, onError)
 function xml2html(options) {
     withStylesheets(function() {
         var xml = options.xml.replace(/\/(\s+)/g, '<br />$1');
+        xml = xml.replace(/([^a-zA-Z0-9ąćęłńóśźżĄĆĘŁŃÓŚŹŻ\s<>«»\\*_!,:;?&%."'=#()\/-]+)/g, '<alien>$1</alien>');
         var parser = new DOMParser();
         var serializer = new XMLSerializer();
         var doc = parser.parseFromString(xml, 'text/xml');
@@ -84,7 +85,7 @@ function xml2html(options) {
             source.text('');
             options.error(error.text(), source_text);
         } else {
-            options.success(doc.firstChild);
+            options.success(doc.childNodes);
 
             withThemes(function(canonThemes) {
                 if (canonThemes != null) {
@@ -252,8 +253,13 @@ HTMLSerializer.prototype.serialize = function(rootElement, stripOuter)
                                break;
                        case TEXT_NODE:
                                self.result += text_buffer;
-                               text_buffer = token.node.nodeValue;
+                               text_buffer = token.node.nodeValue.replace(/&/g, '&amp;').replace(/</g, '&lt;');
                                break;
+            case COMMENT_NODE:
+                self.result += text_buffer;
+                text_buffer = '';
+                self.result += '<!--' + token.node.nodeValue + '-->';
+                break;
                };
        };
     self.result += text_buffer;
@@ -384,4 +390,4 @@ function html2text(params) {
        } catch(e) {
                params.error("Nie udało się zserializować tekstu:" + e)
        }
-}
\ No newline at end of file
+}
index 41a029d..9cfc640 100644 (file)
@@ -5,7 +5,7 @@ if (!window.console) {
     }
 }
 
-DEFAULT_PERSPECTIVE = "#SummaryPerspective";
+DEFAULT_PERSPECTIVE = "#MotifsPerspective";
 
 $(function()
 {
@@ -134,26 +134,3 @@ $(function()
 });
 
 
-// Wykonuje block z załadowanymi kanonicznymi motywami
-function withThemes(code_block, onError)
-{
-    if (typeof withThemes.canon == 'undefined') {
-        $.ajax({
-            url: '/themes',
-            dataType: 'text',
-            success: function(data) {
-                withThemes.canon = data.split('\n');
-                code_block(withThemes.canon);
-            },
-            error: function() {
-                withThemes.canon = null;
-                code_block(withThemes.canon);
-            }
-        })
-    }
-    else {
-        code_block(withThemes.canon);
-    }
-}
-
-
diff --git a/redakcja/static/js/wiki_img/loader_readonly.js b/redakcja/static/js/wiki_img/loader_readonly.js
new file mode 100755 (executable)
index 0000000..1ce15b7
--- /dev/null
@@ -0,0 +1,92 @@
+if (!window.console) {
+    window.console = {
+        log: function(){
+        }
+    }
+}
+
+
+DEFAULT_PERSPECTIVE = "#MotifsPerspective";
+
+$(function()
+{
+       var tabs = $('ol#tabs li');
+       var gallery = null;
+
+       CurrentDocument = new $.wikiapi.WikiDocument("document-meta");
+       $.blockUI.defaults.baseZ = 10000;
+
+       function initialize()
+       {
+               $('.editor').hide();
+
+               /*
+                * TABS
+                */
+        $('#tabs li').live('click', function(event, callback) {
+                       $.wiki.switchToTab(this);
+        });
+
+               $('#tabs li > .tabclose').live('click', function(event, callback) {
+                       var $tab = $(this).parent();
+
+                       if($tab.is('.active'))
+                               $.wiki.switchToTab(DEFAULT_PERSPECTIVE);
+
+                       var p = $.wiki.perspectiveForTab($tab);
+                       p.destroy();
+                       return false;
+        });
+
+        $(window).resize(function(){
+            $('iframe').height($(window).height() - $('#tabs').outerHeight() - $('#source-editor .toolbar').outerHeight());
+        });
+
+               $(document).bind('wlapi_document_changed', function(event, doc) {
+                       try {
+                               $('#document-revision').text(doc.revision);
+                       } catch(e) {
+                               console.log("Failed handler", e);
+                       }
+               });
+
+               CurrentDocument.fetch({
+                       success: function(){
+                               console.log("Fetch success");
+                               $('#loading-overlay').fadeOut();
+                               var active_tab = document.location.hash || DEFAULT_PERSPECTIVE;
+
+                               $(window).resize();
+
+                               console.log("Initial tab is:", active_tab)
+                               $.wiki.switchToTab(active_tab);
+                       },
+                       failure: function() {
+                               $('#loading-overlay').fadeOut();
+                               alert("FAILURE");
+                       }
+               });
+    }; /* end of initialize() */
+
+       /* Load configuration */
+       $.wiki.loadConfig();
+
+       var initAll = function(a, f) {
+               if (a.length == 0) return f();
+
+               $.wiki.initTab({
+                       tab: a.pop(),
+                       doc: CurrentDocument,
+                       callback: function(){
+                               initAll(a, f);
+                       }
+               });
+       };
+
+
+       /*
+        * Initialize all perspectives
+        */
+       initAll( $.makeArray($('ol#tabs li')), initialize);
+       console.log(location.hash);
+});
index 0990e60..0f56ffe 100644 (file)
                var base_path = "/images";
 
                if (vname == "ajax_document_text") {
-                       var path = "/" + arguments[1] + "/text";
-
-                   if (arguments[2] !== undefined)
-                               path += "/" + arguments[2];
-
-                       return base_path + path;
+                       return base_path + "/text/" + arguments[1] + "/";
                }
 
-               /*if (vname == "ajax_document_history") {
+               if (vname == "ajax_document_history") {
 
-                       return base_path + "/" + arguments[1] + "/history";
+                       return base_path + "/history/" + arguments[1] + "/";
                }
 */
-               if (vname == "ajax_document_gallery") {
-
-                       return base_path + "/" + arguments[1] + "/gallery";
-               }
 /*
                if (vname == "ajax_document_diff")
                        return base_path + "/" + arguments[1] + "/diff";
         */
        function WikiDocument(element_id) {
                var meta = $('#' + element_id);
-               this.id = meta.attr('data-document-name');
+               this.id = meta.attr('data-object-id');
 
                this.revision = $("*[data-key='revision']", meta).text();
-        this.commit = $("*[data-key='commit']", meta).text();
                this.readonly = !!$("*[data-key='readonly']", meta).text();
 
-               this.galleryLink = $("*[data-key='gallery']", meta).text();
-               this.galleryImages = [];
                this.text = null;
                this.has_local_changes = false;
                this._lock = -1;
                        }
                });
        };
+       /*
+        * Fetch history of this document.
+        *
+        * from - First revision to fetch (default = 0) upto - Last revision to
+        * fetch (default = tip)
+        *
+        */
+       WikiDocument.prototype.fetchHistory = function(params) {
+               /* this doesn't modify anything, so no locks */
+               params = $.extend({}, noops, params);
+               var self = this;
+               $.ajax({
+                       method: "GET",
+                       url: reverse("ajax_document_history", self.id),
+                       dataType: 'json',
+                       data: {
+                               "from": params['from'],
+                               "upto": params['upto']
+                       },
+                       success: function(data) {
+                               params['success'](self, data);
+                       },
+                       error: function() {
+                               params['failure'](self, "Nie udało się wczytać historii dokumentu.");
+                       }
+               });
+       };
 
        /*
         * Set document's text
 
        $.wikiapi.WikiDocument = WikiDocument;
 })(jQuery);
+
+
+
+// Wykonuje block z załadowanymi kanonicznymi motywami
+function withThemes(code_block, onError)
+{
+    if (typeof withThemes.canon == 'undefined') {
+        $.ajax({
+            url: '/editor/themes',
+            dataType: 'text',
+            success: function(data) {
+                withThemes.canon = data.split('\n');
+                code_block(withThemes.canon);
+            },
+            error: function() {
+                withThemes.canon = null;
+                code_block(withThemes.canon);
+            }
+        })
+    }
+    else {
+        code_block(withThemes.canon);
+    }
+}
+
index a3346d1..4f64291 100644 (file)
     -->
     <xsl:template match="sekcja_swiatlo">
         <xsl:param name="mixed" />
-        <br><xsl:call-template name="standard-attributes" /></br>
+        <hr><xsl:call-template name="standard-attributes" /></hr>
     </xsl:template>
 
     <xsl:template match="sekcja_asterysk">
         <xsl:param name="mixed" />
-        <hr><xsl:call-template name="standard-attributes" /></hr>
+        <p><xsl:call-template name="standard-attributes" /></p>
     </xsl:template>
 
     <xsl:template match="separator_linia">
 
     <xsl:template match="zastepnik_wersu">
         <xsl:param name="mixed" />
-        <hr><xsl:call-template name="standard-attributes" /></hr>
+        <span>
+            <xsl:call-template name="standard-attributes" />
+            <xsl:apply-templates select="child::node()">
+                <xsl:with-param name="mixed" select="true()" />
+            </xsl:apply-templates>
+        </span>
     </xsl:template>
 
     <!--
                        </xsl:choose>               
         </xsl:for-each>
     </xsl:template>
-    
-</xsl:stylesheet>
\ No newline at end of file
+
+    <xsl:template match="alien">
+            <span class="alien" x-pass-thru="true">
+                <xsl:apply-templates select="node()">
+                    <xsl:with-param name="mixed" select="true()" />
+                </xsl:apply-templates>
+            </span>
+    </xsl:template>
+
+    <xsl:template match="comment()">
+        <xsl:comment><xsl:value-of select="."/></xsl:comment>
+    </xsl:template>
+</xsl:stylesheet>
index d2621ed..49562a8 100644 (file)
@@ -1,71 +1,29 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns:p="http://platforma.wolnelektury.pl/">
-    <head>
-        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
-        <title>Platforma Redakcyjna</title>
-               <style type="text/css">
-                       body {
-                               background-color: #84BF2A;
-                       }
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+{% load url from future  %}
 
-                       #main {
-                               position: absolute;
-                               top: 20%;
-                               left: 20%;
-                               right: 20%;
-                               border-width: 3px;
-                               border-color: black;
-                               border-style: ridge;
-                               padding: 1em;
-                               background: white;
-                       }
 
-      #logo-box {
-        float:right;
-        padding:0 0 1em 3em;
-      }
+{% block content %}
 
-                       p {
-                               text-indent: 1em;
-                               text-align: justify;
-                       }
+<h1>{% trans "Page not found" %}</h1>
 
-                       #main a {
-                               text-decoration:none;
-                               color: #325f70;
-                       }
+{% blocktrans with p=request.build_absolute_uri %}The page you're trying
+to reach (<tt>{{p}}</tt>) could not be found. If it's a document, try
+looking for it on the <a href="/">document list</a>.{% endblocktrans %}
 
-                       #main a:hover {
-                               color: #f9c325;
-                       }
+<p>
+{% blocktrans with m="mailto:radoslaw.czajka@nowoczesnapolska.org.pl" %}If you
+still can't find what you're looking for, please
+<a href="{{m}}">contact the administrator</a>.{% endblocktrans %}
+</p>
 
-               </style>
-    </head>
-    <body>
-       <div id="main">
-        <div id="logo-box">
-          <img id="logo" src="data:image/png;base64,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">
-          <br/><a href="http://redakcja.wolnelektury.pl/">Platforma Redakcyjna</a>
-        </div>
-      <h1>Nie odnaleziono strony</h1>
-               <p>Strona o podanym przez ciebie adresie:</p>
-               <pre style="margin-left: 2em;">{{request.build_absolute_uri}}</pre>
-                       <p>nie instnieje.</p>
-                       <ul>
-                               <li>Sprawdź, czy adres nie zawiera literówek, np:
-                               <em>bog_mnie_oposcil</em>, zamiast <em>bog_mnie_opuscil</em>.
-                               </li>
-                               <li>
-                                       Upewnij się, że dokument do którego chcesz się dostać jest na
-                                       <a href="/documents/">liście utworów</a>.
-                               </li>
-                               </ul>
-                       <p>Jeśli nadal nie jesteś w stanie odszukać dokumentu, skontaktuj się
-                               z <a href="mailto:radoslaw.czajka@nowoczesnapolska.org.pl">administratorem</a>.</p>
+{% url "catalogue_user" as m %}
+<p>
+{% blocktrans %}If you're coming from Redmine, please note that
+work is no longer managed there. 
+Go to the <a href="/">document list</a>
+or to <a href="{{m}}">your page</a> instead.{% endblocktrans %}
+</p>
 
 
-               </div>
-               </div>
-    </body>
-</html>
+{% endblock content %}
index 5d86d60..bd7404f 100644 (file)
@@ -1,58 +1,13 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns:p="http://platforma.wolnelektury.pl/">
-    <head>
-        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
-        <title>Platforma Redakcyjna</title>
-               <style type="text/css">
-                       body {
-                               background-color: #84BF2A;
-                       }
+{% extends "error_base.html" %}
 
-                       #main {
-                               position: absolute;
-                               top: 20%;
-                               left: 20%;
-                               right: 20%;
-                               border-width: 3px;
-                               border-color: black;
-                               border-style: ridge;
-                               padding: 1em;
-                               background: white;
-                       }
+{% block "content" %}
 
-      #logo-box {
-        float:right;
-        padding:0 0 1em 3em;
-      }
+<h1>Błąd po stronie serwera.</h1>
 
-                       p {
-                               text-indent: 1em;
-                               text-align: justify;
-                       }
+<p>Niestety nasz serwer WWW nie był w stanie dostarczyć Ci strony o którą prosisz.</p>
+<p><b>Serdecznie przepraszamy.</b></p>
+<p>Administrator został już powiadomiony o błędzie, ale jeśli chciałbyś
+przekazać nam więcej informacji na temat błędu, napisz na <a href="mailto:radoslaw.czajka@nowoczesnapolska.org.pl">nasz adres</a>.</p>
+</div>
 
-                       #main a {
-                               text-decoration:none;
-                               color: #325f70;
-                       }
-
-                       #main a:hover {
-                               color: #f9c325;
-                       }
-               </style>
-    </head>
-    <body>
-       <div id="main">
-        <div id="logo-box">
-          <img id="logo" src="data:image/png;base64,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">
-          <br/><a href="http://redakcja.wolnelektury.pl/">Platforma Redakcyjna</a>
-        </div>
-        <h1>Błąd po stronie serwera.</h1>
-          <p>Niestety nasz serwer WWW nie był w stanie dostarczyć Ci strony o którą prosiłeś.</p>
-                       <p><b>Serdecznie przepraszamy.</b></p>
-                       <p>Administrator został już powiadomiony o błędzie, ale jeśli chciałbyś
-                       przekazać nam więcej informacji na temat błędu, napisz na <a href="mailto:radoslaw.czajka@nowoczesnapolska.org.pl">nasz adres</a>.</p>
-               </div>
-               </div>
-    </body>
-</html>
+{% endblock %}
index e1593dc..1c1d39a 100644 (file)
@@ -1,58 +1,14 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xmlns:p="http://platforma.wolnelektury.pl/">
-    <head>
-        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
-        <title>Platforma Redakcyjna</title>
-               <style type="text/css">
-                       body {
-                               background-color: #84BF2A;
-                       }
+{% extends "error_base.html" %}
 
-                       #main {
-                               position: absolute;
-                               top: 20%;
-                               left: 20%;
-                               right: 20%;
-                               border-width: 3px;
-                               border-color: black;
-                               border-style: ridge;
-                               padding: 1em;
-                               background: white;
-                       }
+{% block "content" %}
 
-      #logo-box {
-        float:right;
-        padding:0 0 1em 3em;
-      }
+<h1>Serwis tymczasowo niedostępny</h1>
 
-                       p {
-                               text-indent: 1em;
-                               text-align: justify;
-                       }
+<p>
+Platfroma redakcyjna serwisu <a href="http://wolnelektury.pl/">Wolne Lektury</a> jest
+tymczasowo niedostępna z powodu prac administracyjnych.
+</p>
 
-                       #main a {
-                               text-decoration:none;
-                               color: #325f70;
-                       }
+<p>Prosimy o wyrozumiałość i ponowne odwiedziny.</p>
 
-                       #main a:hover {
-                               color: #f9c325;
-                       }
-               </style>
-    </head>
-    <body>
-      <div id="main">
-        <div id="logo-box">
-          <img id="logo" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABKCAYAAAD+IBtNAAAACXBIWXMAAAMTAAADEwE9ZoPHAAAgAElEQVR4nO2dd5iU1fX4P+e+78zs7MIqIIigsLusikFBWERRUazYNSr4jVKjASMqIGCJbdAkVLGiwZ+NYgnExGiMGkSwIrJUgwpIF1B63d0p7z2/P2Zm2Q4LKyXO53n22SnnnnvOfee8t7y3CDXAY5/mnOClmc+MZal15DOJ6We73ILPQ23XbqwJ/XuLKrJzWq1f+aweF7hg1wci6IHMP8X/HlITSobl5zRx1axU5QfQo0QkLfHVd9YyTZQPNvy09aMRV2/cURP5JVFFdn3oP9lVOd8xpiPQAaEegEvhsXIBa2oyvxS/PGokQABGzmq2wqj8Y0dG4J7MbdFTY0bbi6Nng14gSB0gqsoMa3nf8XuvDWqzfCXAqFm53WJil9/bdtlne5OPKhKb4p4jrr+bWr0SoQGqEYWZKuZjUXuuiJzsnl9YL1WDpNhf3JpSpPCxwrmhFgsjwFeJvydDismccUJb6/cuRuUix2GIRk3/0MtZTUO9VhSB9nOsWQtUGSDbPwycEDBOj8hU29U40sSqXYSVN4xrP3AKwtPlSgoAIlMDX6PyWSo4UtQENRcgnpmuju06dEGTOve1XLUl+XlIsLA4GTB/HDH7hA4G+0ntk92LgbctLBToUJne8BROEkkbKyIdsLpZRF61nnnJf/GueeVsmEhmTOVXGMbXlF8pftmYmlJkC51pghh/xH9hVXIFbRZ/rqo/euh1AGrtLBHNejz/pGMqTOCkPYxQX6x3vbu58Bj3gsI7KwoOgHDDtHaImKgX+3S/HUqRghoMkPvO+26FKsvV04urkgsJVlX+IXDl2Pw8X6ETfkORaNSLdSsrqxPJNMpVWEa4F0XelC5EqtIt1pyDakGwTnT2/vqTIgXUYIAktL2H0GlPYmLM3wWps0W2nR9qu3ajKu8YY7uWlYs2Srsexfq2FE3em+zF6CUY/UTaEt0X81OkKEuNBoiV2AREjhv9xQntqpJrunzxNLDrfJbOAMboOyCnDMvPaVJSTixdrZo3pQs795S3fkg9QfM85YP98yJFit3UaIDck7fiSxVdYl29uSq5Ll3wgAlWuSYU6ujuNJEpAG6MK5Iy+kHwOISO1rOv7E3eERu8GBEjsfD7++NDihQlqdkmFiAwHvT/QvmN0quSi4h9WYR6mZetuDDUZvVaVBdax7k8+X3YZ6+3sCqtU9H0vcrYlU4WXRnoxHf750GKFLup8QCJoeMRrZ0RC1xbldwf2q74DsuXFuf3AIJMEbXnj83P8wEYlevxzPg9Pc/QSfiLpmR0MlYvdaymmlcpapQaD5B72y5bpapTxXUGDc9vep5q5U/rrcOLKlwxdFrzLM9Gp4hI2k7deLL+M72RKGdorGBcRen0ZdJiU/zXhaemvRqrG1zvGPs+4BShz9S0Pyl+2dR4gADYSPQ+taijvo8em52zbPjM3NCjM0/MLitXEAyMF9FVbnr4tvXBoo9R9Tzx5YVredcqfJ52GUtLykc+Ss+LTQmOiTUJrlXj/A2hgwjjY55e6H5W2LDWRZGvfw5/UvxyqbG5WBXxp1nNTg6g3VTNjSLaGJhurX2lwAn/LdR2bQHAiJnZ3R1jnosaudbx9C0xZkC/rWuuVWPf8J9f9JI+RSDcPP0WY7S3CC2BHxQmx2xsUvDC6MzUlJIUPyc/a4AkCSkmMKdZR8dqF1G5GiRDlMkxx76cs2zZjFVNc9/CcAWqhSYWbnnzpg2SuTG8Otw4vYfj6AOgR1vh7yYae9a9OPZpMih0Ko1j1t8LkTPFSHM8/cC9KPz7A+FTil8GByRAShJSjP/LZu0dV6911FzjmegthXkrPw7Mzr6QHYHFiSfyEpmW9rqxXGPVDPMHCsbKOawDCH9Ac3UDFzrWXILoJQhWkQXGyvyota8GOxV9dKB9SvG/ywEPEIBpWVlpzWOxlkE1tQs0Nr/R2tILq/RdmsbSgiuwDN4Z079lOF4HFd+FRvQChMaoWoWZ1upfA2H/q3LljgO6MCvFL4cDEiAKZn3j3NN9fr0cy6UoLTGJmcQqmwuNd2bj5csXFcuHMNGz074VkRNKKNlkRaeK5/3LF057LxUUKQ4EP1uAKJjNTZpcIPi6IVyWWOkXQfnCWpkS1aIP/a5rRM0XotKrzqqlpYZ0dTwZXuPAOQjiRM3X0qlw9c9la4oUlVGjAbIlO7upetoHIyermjwRGmH5XsR7Rz1nSuSHnZ805Kddmpfn27Jp0/Wo8yiAGys6PXPNmk15vXunazjtJN/6TUt21orYQFqjfIx8LMibtVce8dH06aFYTdqbIsWeqNEA2dgku6uIjBXkO1HmYL2JdX5Y8XHy+zWNGh3ndwK/cx3prUgdPJ6P+Hk0LeZmXJP9m9XbmmydIUJbsN/FdhRc5Dsi0MB6/l6C/Y0VEdT+Uzwz+Yg1R05JBUuKA8HP3gdZddRxjWqn+6+3jnYRy5kI21HzWlhiI45ZsWLF6nonNE6vFVvy23bt265Or79Q4DaFQWr5fO6EJ7oDtOgc8gfStl6hoj0EuUxhsxheR+2EOeOeSq39SPGz8bMEyJbs7KZW5VqxXItwFqAiZqoSe3k+/OO8FSuKkrKbs3LuVZVzLz73ij4qsjJG7Gq/9cc8Y9/xbFGrBRP+8t+Supt1G9SgthO7UTx6iqGVqn5rxYTmj3t80s/hS4pfNjUaIGsbNUoPumn/VCMXoqqKzLTGvhkJh//aeO3aCjvZW7JyFlnVEfVWLn+xdfcBm8Xo+DmvPDGgTfd+S0Bmzxn/xA2V5df6t3e1Ek/vV7WXRgrXHrNw8uQ9rhtJkaI61OhcLNfxX6lGLgS9T2PhJvVWLm1ff/nyUZUFx09Nm56pVnJ8schbAKI6V1UuAlSU5xSuad/55rqV5Tf3pdHzd4Wj/RFJD6Qf07kmfUmRAmo4QIxxO6mypu6KZcPqrVnzw57kXXV7Wkc/zlyzZhOAODJF0F+d0PnWxhEv8hKiGg7UurEqHYveeGYt2P+oSs8aciNFimJqNEAE7WTiU8/3yGoIYrjBIMV9h6hX+C+A9Iy0i75+7bktKB/gmJ57VBbTF4EOp/bsn7VvlqdIUTE1FiCbc3JOEaGR9dirAAk2yb5OIGC86OQWN/U7rWXX35+4YMJf/quWlXj2ooRxfwXNa9nt1pOr0hWJ1HtbkE0OttzOKClS7A81V4OoXIKqt93oh3sl70hPq/rukatWbfE7Ms2YwHgAMfzLiLk4r3dvX1Hh2rfBFgn+m6pStXByKCIi/7Qq+xwgj+efdMyw/Jyz9zV9itLk9ejd7MT/u73RwbZjf6mxAFHlAkVmZq9YsXVPsluys5say/mInQCAEDTCMQAxMRMRjortSrti4eTJO1HeM2Ku35POGPxHkOPzevRuti/2ezYyxVXz6bDZuXvctijFnrGS/mJGwPf2wbZjf6mRANG4nvai3sd7kl3fqGlrPEaB2VIvI+PfAKJYVa0DsOCV0V8q8o3jSncAa/VNEXJb//auVlXpTS/Y/qGCVdLO2ScnhACAG9N9S18DjMrPHjhq1vFPHaz8axTVpoq2btE55D/YpuwPNRIgG7KyWgpkiuN+WdH32xs3rrelSVa/zVk5C1y/O8caczoSu10WLowAqKgnIrXyevf2ASD2U1HaAjjp6f9SJWKiWmUtMmPyi5tVybdizt0XHyxmMYA45qR9SV+SQfOPzhg+o2m19SimD+gdZfcHOxwRxRMwZOyqdJj+cKBGNq826p6FKGGjxQHy07HHtvT5Ap3E04tjRs5R1ZioeUs1MrDuqlVTBWxSVlQsAnZL9Ehgg8T4rzrS56yr7q79+fPDt7Xpfue7VuT+1t37X6JiP3BU3q+9qs6X5eZjCdPFo9IHi1XhqC5W4TJPtfm+lkOS+uGMa4xjHgVyqpVQid9tram9vzYcdESjIAQKo3WAHw+2OftKjdQggj0bZZPf0+s2ZTV7Y0tWzo8+NzAf5U+eYCz02bBre8O6K7+/qd6qVVNKBgeAoh6ADabXAYgV7HrLs3bY523SdwGEC9d1t0b7qdr1BhmgIp9ub7p1Y+tu/d5s1fPOO9reeEe7Fp1Dfteaj8XQ9MRut5TbIGKPGLsYwEBuKNRxv24cjshmEbJHfnlsy+qkE9QPIJFwcH/yPxRQlQhANC1a/2Dbsj/UzPEHyskY6oE8iTJb1bysjjctHA5/1mhtfHOGqkgGjKvE+yF/f+EH4L7k94kpJE8DT3PppYGWdXPOdsXfSYy90LFytfUZx+/bEvaURYKQTsb5wIvV8sHGFiN+AJ9z+cpmhFi0pySVEVZvXUBcjBu8Gliwt+lUjV8EJBg87AME2ArgF/fog23I/lAzAWLs76LG1NJlu2Y05Kdd1U2uGE9Q1HHq7FH4vffCC2Aq8T/Ouuru2kV1ou09j7MdY89G2W6MVLtjGNvlLXJqxV+neU5z2PcAwRasw8lErb0EeLQaKf0AnkYP/wARswEUz9MGB9uU/aFGAqTeihUVds73FsFaEFTNngOkDJ+/PWIH8J/E3z5zd8c1a0bnNytQJF1dexLwz33VFT19/YbArNqeIidWJ51IPEDcmHPYB4iqXS8iqJpUDbK/qGAFMHDkwbJBBB2VL0tQWkm8BimFKvL4jKxzrOu7UcQerWiBtfr+3acvL3eaVUiwI2fJTwKNhuXnHHFv22Xb9pT/47NOyPHw/CCo0Vsfy292uWclCAQF/CL63KDTlr5XQ+7+7GiiBnEcUgGyvyjxAPE8qXYNUsOGLAZaYUqPZIVezkp77CvnLXzSCZT4bqqCMfxm5Fe51w9u9/1VZVUJug6kkcE0A+YkPx/9fou6bvom585zftwAMPqLrHM913nGw56cXH1gRC5RBVNiMYKqRoHDJkDEeusxBpFUgOw3BlUQjBvLqAl9p/W67ThP/QNR2ojqD57qisDGTcNmvvfq9qrSechiBwV2B8joSQRtlu8dRC8AQNmsqu+KMdmgZ4voFaFpWUeGziszg8DKOgzg2RNJBMjj+ScdE7NFczwyjnx0VvapD562fJH1uf8SqKXKDhESw7s6DdHizr1AgYP7Qk2UzYHCcWS9Kqhy3MG2ZX/YrwBp1b3vCY74X7Nqv4oW1um/cHKo9BFpoZA5eeXmM/zW9LCqHsYuj+768blyC5vUKAJC8fnqnNzrzjOP2JT2daKPsde06d5/pGe1H+ADUBGMCNH6R3XI69270+znn690VM3BrgJBkCOSh5Fqk+y+yeBQlTcCdZb0vPN4wqH8RukZNn2biLjpmf5TgDLnIuqPIDgOpwCvA3he5BEx0hDANaYxsCgisQv94sSaLl06b1V2bvy5jth3B7Vd/lh1/D5YdO7c2VkWbNR4q/qKlk4YtT75uSAb4nvCanPiVeNhuUVsuQBp06PfP2I2/GDZpa4V4Yg7HDTPiOQF/RtfBT4HyOvZrzPITXbZtnNE4h1vIwLq4A82HpTXre9VsyeMmZnUo6IqCIk2N0d3G5Th96Kf7MosehQYAvGltkeYyOV46pf09DdmPz+8XLu+Vc8770AZVLxQUu00VScgRs9E5GwtTB8A/KlSh2K6ETeeNhZ2GgBbcMxvACx8W/Dvxt0Gh5bExubn+bbGNt8rDi7YdWmZ339VVpVV+6PBQT1zCsCwr3JbgPYCUFhaV4/8FOAPbVcUl8OoWaog4nnOHh8UPjYr57cW85TFjr3ntGUDIV7bAdzVhcJinbOb5eFpL3CaW/GWuJuC98eOCmc5ym1RsQP3pn9UGa16DOiyTBmmotmZxCKndu8/3rEFD82e+Py6GKyIP2ST9BO73ZK1aMILy/c1n4NJuQeFqnKmj0C5CXt53fqentf7niOS79t37hxEpViuwPVtjcsNeFRVJqlyNcI6z+NRYFlSToQGVnwTj+42aHdzSiVxd9E0gKN3ba2DiCNGOgGS13PAsEwTWwfmJRznL1oUXtfqxttL2diy511nOCrJu+4yK7SeM/6p8/0b11+Kxn8wNr6BdqVYv7sh+TpgfA2GTmuepUgbACNEal3+w2ujvmr2+XZv6w+O4zwIRFWcQXceT7isLsdxfgRQo6cAOOhIRBwAE4vd36ft7HLnKKqKByCqteLvkcdm5Q4b+VX278rKephBAhkG6T966gmNAbym2UNsdu764dMaH/vnGc3OGvlV7lys5COmL6IXGMyt9qiCXuJ5/6dws0/LT8sZld+sx2Ozcl8IzW9W4fBsm8635QK06drvdqP2DRXNhvgInBFu8Zzga4DMe+WJFaqsBwg6wSpPPj6UKRcgAsYabVvyo9bd+32sxvclRUXPJT+MZDTuhMTvWBY+/2bc4wvb3nhHOyv2fgCxOn5u9hGnzJ/4xENzxj3RLGLsWYouARAht5GNFU8KjPdBwEk2sTLcIxO2tGvTfcDbqnqPlLRVCDqO82Tx3C1ANHo9iE+VCNgu8155Yh6XXhqIHn3UJISggrVe7M2Svp7a7c6+eT36zW3T4874nT0S3r1bo9UGmQ2cjWj8KT9KK6AzImciUhfVLyIS6zC47ZLXKipYD/kxbqo0HTUj51oRuTT+jc6+64wVlWwwofEmlqMZAKO+yD1D4R7FKTXdf/QkgoKemNBvYrX0JgBr3AhQy8lIe9/nyHQRTi2RLIrlJW9H5K/GyC4AFS01KDLy89z2qrykcHPtiJbbBDyv58Dmmu5f1KZHv3/gyNMCq401l1uhjuL1ADBIx7zuA7rHLwozAcSaXhX7e+hTLkCsYEDyku9b9RjQWUTOAbAqZwJ07Bhy1dqHkjIqdiyAGre9iIiCLSqq8ztCoeIpJf99+akv1Orfku8FrVc2b03UINb1Z8aFxEE0fm6hqidWx6vKlrjlcqIWZlySTOuom/jB6FdrrP+7Nt1vv6FNgxOnJGs5g71nwatjpibl2/TsHzLGPKPIqSh3ABT6osU1iBHq922xcCdG/pvIvzAm9goh2iawcXO9Qe2WnlWyeZRk0PyjM+JlpcVHwakrJTvYRwyb3rxp2XQJf+M1iJUMAC9g6yYK61clxbyc438tSPG1M2K7A7iGLxN6WojggqrFjo2J7RBMD9QddPr3N99z3pofVDTRB9SOSR1PLSEgfl5K6lWN15yl8LvrUBWQa6y1r0VjkVPzJ4z+97xXntg6d9zT4xVdC6DGaxc32/4jbo60b9X9jtYV+nyIU64PIqiHktui5+CGGz3d4ao3TJPDjaJN8nr3Tt9asK2/EdMawMLmOtSdDBBD6zkAqhIIbvw18Nek3lN79s8Sj+L15TGf833ytUq8iaWJGsRfYLZQ3F1P4NFz9qtPTmzds98bqNwAIEZPBt4BsKJ1DGBFftVYouvBTY/r1LC1+vj8CU+NSqpq07N/COXhYtXiDAPIXLZ2l83OTRjoNQCwMe8d4zitEAk6Kk9GjPQeeNnmUqNhg+YfnXF0OP1GxfSWCG1GzKr9YEHb74fVmpW7HSFTiA9fx0eqJNdXK/LF6PycTne1XVbqwB9RPASMY2oB+CPuBuuzGKg//MvjexEr+tr43C6idnc/C0CkxYj83DY7dNeXtSh5NKT0uPu0pRPKlCSelR8dAdTc9NhXTSaGjVkT2eoMBUoOb5f7Qc9+fvi2Nj36LwdyPDHPfP3ac1vK2B9DAOs0AZiTXXdcm+VbBwCnODift+7W/28YPQPku7njnig3NH4oUsEols4WMZcEbPTJRkYcRYsn/omIaCRtpDj25uIxe09enD4xVAQgLi+hdrCISQP3jTbd+40QMVMUPQHVMzASH1my8sWCCaN3P31XVUTQRJMtbNesDrC7uyDYv8x+9amJADYmix0nkQxaFAtZ+zrGnGWgbvypPGtU7fioE3hq4YSRPwK06969XpR6IVRvL04GI5J7agVatHAKC8KJPOM1XMH7TYfUunT1aRjpJNDMZ2XqqFnNvsayFiEsxnga1gvigZCw19I+JNhRs5gJXJT4eGNsl3uar5Y3AczZVvWTYfk5V97bdtln5a6AZ9MBBrRfPGt0fu5/FC52HH0JJ1BaDrXJO76J0Tl0xtr7Rs7KXSrQzMK3g9t+P3FQ+QuMb8Wyv9vs3I3AUSr+D/0VjS+JHDd6EsG3nw1FdzTd/LTFfDJ33BOvW3SpQXJ81mtJKDSz9ZKtncRHJ1VygCYAntW5AIRC1nTtN9AzfCAiQRG6gYDaPQ4AHSpU0Adx/pF40UXQ6wBUyVfVxKiduU2QQPxznTFn03cPJtPOe+WJFYr2SXaKEWmi6M1AB0gEh+q3Rf5IqZ1KNNFJN4km1sLJk3cqWvxcYactGLFb2iue6yVocYA4weBE0N1DuKIFxsgxPoquzOs6oN+pPfu/EpO6y0V2B4eqvn18wZo/JN9vKNxiSfSHrLhHAIRC02P+TVu6qOqMEqV0CkY6IXKVqv4aIbOEzhkSsQMBPM8rvgmoxy33nffdik3bCq4B/R7kSMfKlFGfNCluPqkQr4BNfPGWCFrkbe+uyrxS5WV1PSK/d6JOeyAKYF1J7JbvDUd1NV50UGWnb93VhUIi9rK4HUm72WE974+KfTfxgTegM0Vbmmy5RDG3onoHgFhdDWCNadVq+bbrxeXfKP0Eriy+FuFI8eyC/IlPTvGsOVfhHat8JVb+mGnqVrlTzaFEuRpkzrjHnz+1253NjTEDAFD519wN313fqkHzEQ56Z1LOotOlcHsX3nuv1AjO3HFPj2/Z7dY5Lv5HwFyyuyOvP4GM82KRYd+OL1M1G7sOTFNUitd3iJrPEL1CkaklhwiN46xKvlZo0aJzyL9wcigy+/nh2/K69T1fxZ2ESBNBjgeON+r0VEcxJX4qFjYb7KC54596eW4JO0Jt1xaMnHX814K2VKOLk5/fednm7aqbzxo9O/d6tfpHSh7LQPxOjsh31vNG391u+UvJH+aGYMHwBoXp4vjcmYNOW/IvgKEXrtn02Kc5l2tAZohIXYK+s4FvKK2wuKq4/4z1P8H61qPmZDfVItNGYtFFuz7KWRwKTY8BjPzy2LbGBNoUSMEkgMHtlv8/4P+Vva5lGXTWslmqnDBketYRTm2vnhuQzfe1XLVlxKzcb+I1oWwTQTt2rPP+zmO3XLxFfPMBrHF+cFAQaaker6tRFRFJlMMKg+kze/Kz35fMa8HExz+l3HOiw4MKHxTOm/DUXSd2u+XpgJN5dN2VmflMfy9Wp+PpA3c02TIZ5XSs9595rz5T6YGZiWco1+b17p1OmGOiO2y47uZjf6xsw+mYuANc1bu8WKx4irrLpp5FXp2OxmVqSdloWJb4A8lfu/gC6ZtPAWYDzJ4wZmb7zje3jgRrPaNwMSKlBgJU+V7RKVK4/eE5k1/eQAUYvL+D5LpRp9TmE/Ef/feTJ03i70vrNz/OHyhspMY0iqqzccv6bbNHXL1xB8A9JdKMavXTLuBByjCww7LFw6c1bqUZwTOL/n3c30mcVSrKTwg5RgiUTTOozfKVwMr4u+J7BIPP+GEB1ZhSX96nFVtJTE0fNP/oDInoiYn5YJsAEtdsSjKN63mr1TGIyq8WTHz807Y9+5+vnuZ5jt2+zgu89tOEUdWezX0oc1BOmNof2nfuHAwHG32HSBMAK17vea88XeEd87Retx3nxQKnxkxUnIJdM+ZWEhRlCb2clRbqtXv/4APFYzNzPlFjOqC6cFC7pVVudfRzMPqLrHOtz50OgNoxg9otu72szCndB1ztE31LVTW3cK1v8uTJ3oG280BySMzFqg4zJk8ubP2bO7rjdz4SMI66KyuTnfXys6uBah+8czCCAwDVt4EOVvSAN0fG5uf5ttmto5KdUhNzy81SBnA9jeDGB2xWBoNHApsOnJUHnp/lnPSfm7mvP/2xUe2GMGT2uMf3ax3IocTAM5aPcsTX6O7Tlh3wk3p3smWIEWkLoGoX3XXm4nLTZwBwpHgGQKyw1sGdfX0AOOxqkCSzxz9Z4RPsw50Bbb9dd6DzHDojt6ONz1ZAUWuI9a1UOBqL4o+Ps4f9vlSApPjfJaSY2rNz+1urfyp+nmLtQwNPXzW1sjTRNIn6E/MjHGKZlcn9r5AKkF8go+ZkN1VPfm3ypZtCm8QoLVZ5c3C75X8eWEXaaKxgnT/+oB9x3MN+afCeSAXI/yhDFzSp4wv7vlKVLCMaUZUYqA/w44lTZoFG1EPvv/u0paMqe7iYZNGEF1ac2qP/ZoNNL9KChT+vFwefw7KTnmLPOAX2KJBcEVxF0hEyEQkmp9zHUQWdFpFYh3tOWzpyT8GRTGS98Jmeeq0O1zUe1eGwew6SYu8ZMev464y1l6iRUyQ+b60IYY1VWaOefuaGCyYOPLfi079SxNnnAAk9MuY2LFfjdwaE/nDrN3uWf/YuVK/B0QdCD9z+yT7nGxozBqEpPndQ6A99iqeUjxw5PsPzCt177+2zrTKZkjZkBDJnJ+X31ZbdeqtXFiUpaff+2pGi5tn3JpblFEUvlqi3d7tWqDZX1Q7i7fc+SWmqerlEvVLHHOwq2PFkUTi2KfTIs70rkylpQxn5Shky5JnBoSFj3h4y5LnrKhWqbllUbvf+2VEDHKh8DhfKBUho0qQqdyUMjRjT8M9/Hlc8x0nERKqSL49uHD78xdpjx471AUyaNMmpSCqkWmXwisjW0LCxTYrTG3kD1XdFrVOpTAkbKpKv0FokQ1WvVLHH7MmzqsqiUn9K2FFVueyNHaNHTwomr19Sz96UdXXzqS57upaHMqWaWKGhY3IJ68cIm0DeCqaljfQ81x+J7nwO1WxEjlfVI0RkGsoiRW91jHO2NVbw9G84cm3ogb5fhIY88wzKTYgsReQvoYdueyE0ZMzzqvo7I3K+Qh9UL8WYIag+gLKIAN1oVn+VfLuhh8LNKC0wMjD00G3Pl7IxNOZFRX8rIu+p6qWC/Nfvr9UxEt11D+g5Avep0rVimZ1Dd9sglyblH36477QKbQ6NGQp6o0ITQeYRkOtC9922bMiQZ3soOhjV+ZyOuCAAAAXaSURBVK7r3B2L2QcqKgtOqJ+/R3+GPDui2O7KyiXMzXtlh2eHA10Q7sLqoyLmAUU7VKQzdF/f7yvw4/aK8qn4ej7zH5D1oYf7dg2FxrwLfBMK9R0cGvLMMOBUghk9pbDgyqp8PxwoFdkS4SaFRqisVtUHC8Phq++7r/tmVOuBrAcSK+l2r9WwovWI6esodRxly5Ahz3ZWpS/Ct4DB2udCfxpTcm00wHaFTKzeBzpd0TMkwg18u2GEVX0e1doYHarWjg098uxdldh+iQjDgdxIZFd/VEVVT1fR+pXKlKSEfGU2i+iHSHy9BUKQKOeHhj6bo2pfSHx2XSxmiw+8KVsWe+VPabsrLJdq2FGgqj4s9wO11NG5lemsKH1F+VRxPa2q3jR06LN1QC8GvWvs2LE+VHqgXERhQaga1/KQpWzVtx1ARF6JLy2ngYhoKHT7BSKMAo4VWJyRnrn7LEBrn1A4VsQ89OCDfb9V1fg6cUfuFnhOwcWjkrP/dCOuPAKg6NGZtevfZ0S6ipgQyi1x/VrZkWh/Ji3wTHwdu+5enyHs3KNMSYSdldn88MO3TxVkQlxMHgw9dNsLEtHzFFxBkjPbd58BUqYsquVPKbtLl0t17VC0ISJDQw/0/aIynRWlryifSq+nyhKAcFSvVHAVzLr1sfMUbQjyRWbt+gOqcS0PWUoFiOPwNxH5VtVeg8M5rpG/AoQef/lIhXGoRvH5fjN4cPfiOf+qZIuI5zj2DQAksQw2PfANyYNTlKyKc5cXjafJyW+1treuH1W40qqdBFL1sQlGpktRJL5uWnTpPstU02bVeLAr+hyKg8iMEt+VKotq+VPa7lLlUl07BFlwTEO39K7yZXRWlb4UlZSNiCxOvO4iyZaFx80AIvr2Pvt+iFEqQGLpgV1+X/BchAAx3rRWjgJge8FfVPU4EflD6P4+c0qmEWSBqjoxTybEO4GJ5lc4cqQK8d07koVZLnOz+6IoAT755gFVvUGQURXJl0VVzkgYMW9/ZKpjMxBff4/2MsY9R9DicxnLlUU1/UlSrlyqa4eYR/r06VNq360KdJZLr6oVDPtXXDbq2Pnxr/UiRD4QkR2IXg2gPt87++r7oUbpJtb28JBwpGA9sFjRBla9XkOGjLlZVW8QYYMKTig05umRI8fv3vTN1TsE3lbVDt98u/FpVOIrDWO0ROP7a5nEir+KsI6TvCgG5QJB5gENgYqbRLtT+sF2FWR9RjDzXaSiZzplZEpSUr4Km7XMQIaIJs6BN35rvZDC45AYpSlXFnvhT4V2lymXvbcjMXVIK1zPUlJnRemHTJ7sK5tPpWWTeeQ8EVEFPyJfoCxUJSAi60J/6PNd9a7loUvpAKnlDpH4es54h1ZYrMq1AKrUV6vDFb29oGDH+cUXQY3f5wv0FpHVYBs6jvO8wGY8HYtyk4gssc2P+np3XppJor2sSi2J2dMTuacbo4NAcxTtiugjIoSTa9qLSW52Zhmv0AQjwxNNvoYJe5pUIVPShmL5KmyG4iaOZgLoUTJVhBmq9m3QjoLpBRRUVBZ75U9Juyspl2rYkbyeR5XQX6HOitKHunSJlM2nsrIJ3dl1O2h8x0yRBYjGb4Ia/7+Xvh/ylAqQ0KA+GzMzGzTHmKsc47QJPdT3WTJMLzFyh2Ocs30uWVL3yCMefrjvOxkZmQ8Yca4NuMy+//7f/US6aXtMQ98NDz5465JgMJglojdizB2uoxeFunSJUDtwtxjTR9MCs8lMv14cc5GeVG/aSSfVf0Vc9zT8vnseeuj2mcZwpmOcs0MP3f7HzNoN6pDmL7UDhmMCA43IICNcbkRuzAjWGguQWbv+78Thclz3o8pkStpQUr5Sm4GHH7ptsLhyls8XfBsg1Lfvzocf6nsWPt/puE6Hhx/+/QeVlcXe+FPSjsrKZW/t4KQGvcVxztO0wJTdGVSss8L0FeRTVdn4fentjUjXjLSMmRwl9xpxrsWRpwH2xvcUKVKkSJEiRYoUv0j+P1GPgO+VXwPOAAAAAElFTkSuQmCC">
-          <br/>Platforma Redakcyjna
-        </div>
-       <h1>Serwis tymczasowo niedostępny</h1>
-               <p>Platfroma redakcyjna serwisu <a href="http://wolnelektury.pl/">Wolne Lektury</a> jest
-               tymczasowo niedostępna z powodu prac administracyjnych.
-                       </p>
-                       <p>Prosimy o wyrozumiałość i ponowne odwiedziny.</p>
-               </div>
-               </div>
-    </body>
-</html>
+{% endblock "content" %}
index 971213f..3e725d4 100644 (file)
@@ -21,7 +21,7 @@
                <!-- version: {{ APP_VERSION }} -->
         <div id="content">{% block maincontent %} {% endblock %}</div>
         </div>
-       <!-- <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript"></script> -->
+    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
     {% block extrabody %}{% endblock %}
     </body>
 </html>
diff --git a/redakcja/templates/error_base.html b/redakcja/templates/error_base.html
new file mode 100755 (executable)
index 0000000..58784dc
--- /dev/null
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+    <style type="text/css">
+
+body {
+    margin: 0;
+    font-family: verdana, sans-serif;
+    font-size: 10px;
+}
+
+img {
+    border: 0;
+}
+
+.clr {
+    clear: both;
+}
+
+#tabs-nav {
+    padding: 5px 5px 0 10px;
+    background: #ffdfbf;
+    border-bottom: 1px solid #ff8000;
+    position: relative;
+    height: 25px;
+}
+
+#logo {
+    position: absolute;
+    bottom: 0;
+}
+
+#content {
+    padding: 10px;
+}
+
+a, a:visited, a:active {
+       color: #bf6000;
+       text-decoration: none;
+}
+
+a:hover {
+       text-decoration: underline;
+}
+
+    </style>
+    <title>Platforma Redakcyjna</title>
+</head>
+<body>
+
+<div id="tabs-nav">
+
+<img id="logo" src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8AAAAcCAYAAADxyeavAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAD7AAAA+wBSobVeQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAATYSURBVFiFvdhbjN1VFcfxzzpTR8tUYisJViJWJCCIVKUNYCGdaRPUMBYvqNFSYkyol4iNsalpaOfPf4bLKBiR8kTQCCG0tWMBa4WEgLY0lJBga61FiGKRahPqgxamUGjP8uF/Mr0wlzMHpzvZL2ft9Vvfvfb6r71zIjOd8FHGe/GgMKAnf9iqTO3/iNTcuDHeh02YJS1+K1InFr6MGQ7ZjDbcgDOUcW6rcicOvowzVBk/bJK52t2Ow+huVfLEwB8NTqfr8kXLcx+24DOtyk48fBkn4zeom6RLkXuGbOF+XOzmeHcr0hMLX0YN92EqulyXLxwX/WG0eV1XK/ITnfmb8Ul8UZHPv8m6Ip/FP2kNftJbQhttlPElLMMSRW4ZZeWjmNdKiInJfBkz8HPco8jbx1j9KD6kjPeMN8zEwIcL0GGy7zSx+rGGz7hLpzX4MmrKmDaK6g5w0IwxtcLZqGNkvRHDtDYW43llnDes9bC/4YC6D4+q0hcfkX6Fp53kF+OFaA3+7dbi39jgpjj1TfYi69jJKPA3xmnqNuIl7S63NAeVcW7je2lqjN5tyngXOjG/MUPNVVbm08pYgC3esFUZCxS58zjvHRj+3fKTmOyQDWjXZq7luU9vLMRduAffaAb+uMxH6IvZyuhTxlOq7N6PL2M76uoeU0anIndpMxuv4AlldB4rZbuRMr/fz3A2uq3Iv+uNa6R78XuTLW0GHCKv1y50SVdgAU7Da3hceER6ROGPZOqPqQ7aiI/hK4p8wK3RYdAGXIwvKPK3oIxZeNI0Ha7Ng0MRy1imurw+p8hf64sL1W3GeixS5KHxwG/DR/Gy6g2yXoeHLM3BYT3KOAkDqptziSLvaPy2EXOEhXpynTvjbfbajwsVuaPh+6nGuu8r8jZlnKV6sL2ATkW+1iw4Vdk8XG3DckV+VZEDI4JDkQdMd4WqPlcp4xaFV3XoxlZptTK6Lc438AfRKJ0yTsG9WK/I29wQH1T1+P1YMF5wiMykjDvwLeEqPbm6ae8yvo2fYgO+psNhgx5SldWlWIQDilypjLsxBx9X9fTNCMxR5D/GC34EXoTSXbhaVcsDTSv0xVx1A/gvrsRf8Tu0q1ms7gPYg8cxGy+pSmUKLlXkX1oBZ6jbZOIarMFqZVzZtMLK3GSSWXgRW/FdYQ3OV7dXkWtU38duHFKVSjvmjwhexjvcFNPHCh3H/HuwLtrsch8+j4WK/GXTm6iCfh23qt7vT5hmnmvzYOOFubax6k/oHiqVMk4RZkszMRPnq9pooF9hRSO5Y8BXYjX8GEuEUo/ekZxH2EA7zsJzinx96Pfe6JImq9krfUK6CBfhTNWJPIc/q27mncL7pVvwoKqFHhgb/gjEN7FK9TFerchXjrNPUd2g56kuo2qGpXpyreFG1T43YV8DsJp1zx6z0SMxLlOV8m5VR9pztHlkeOiN+dI67BZWSeccBXo6ErvwpGjMulOFmdJaRf7rKJB5+I/CtnGe5Jmq7E/FZxX5VHPwR5wHcI7qWLcPzcm2WZYvN9a1q3r/IgziGUXObiThB1J/Q3GLNoutyGea3sCP4p1etREX4NOK3NwcfAVWM11b4+IZzn4yHsAlqnZ7Or6nyOkN+wZMUbNMXT8uEa7Xo7/pU6ieIdUGai6zMrf+DykEtvGDDlIUAAAAAElFTkSuQmCC' />
+
+    <div class='clr' ></div>
+</div>
+
+<div id="content">
+{% block "content" %}
+{% endblock %}
+</div>
+
+
+</body>
+</html>
diff --git a/redakcja/templates/pagination/pagination.html b/redakcja/templates/pagination/pagination.html
new file mode 100755 (executable)
index 0000000..fe566a8
--- /dev/null
@@ -0,0 +1,26 @@
+{% if is_paginated %}
+{% load i18n %}
+<div class="pagination">
+    {% if page_obj.has_previous %}
+        <a href="?page={{ page_obj.previous_page_number }}{{ getvars }}{{ hashtag }}" class="prev">&lsaquo;&lsaquo; {% trans "previous" %}</a>
+    {% else %}
+        <span class="disabled prev">&lsaquo;&lsaquo; {% trans "previous" %}</span>
+    {% endif %}
+    {% for page in pages %}
+        {% if page %}
+            {% ifequal page page_obj.number %}
+                <span class="current page">{{ page }}</span>
+            {% else %}
+                <a href="?page={{ page }}{{ getvars }}{{ hashtag }}" class="page">{{ page }}</a>
+            {% endifequal %}
+        {% else %}
+            ...
+        {% endif %}
+    {% endfor %}
+    {% if page_obj.has_next %}
+        <a href="?page={{ page_obj.next_page_number }}{{ getvars }}{{ hashtag }}" class="next">{% trans "next" %} &rsaquo;&rsaquo;</a>
+    {% else %}
+        <span class="disabled next">{% trans "next" %} &rsaquo;&rsaquo;</span>
+    {% endif %}
+</div>
+{% endif %}
index f051213..c82611b 100644 (file)
@@ -2,7 +2,12 @@
 
 {% if user.is_authenticated %}
 <span class="user_name">{{ user.username }}</span> |
-<a href='{% url logout %}'>{% trans "Log Out" %}</a>
+
+{% if user.is_staff %}
+    <a href="{% url admin:index %}">{% trans "Admin" %}</a> |
+{% endif %}
+
+<a href='{% url logout %}{% if logout_to %}?next={{ logout_to }}{% endif %}'>{% trans "Log Out" %}</a>
 {% else %}
 {% url login as login_url %}
 {% ifnotequal login_url request.path %}
index e4b0897..1f0698b 100644 (file)
@@ -1,15 +1,16 @@
-{% extends "base.html" %}
+{% extends "catalogue/base.html" %}
 
 {% block subtitle %} - Logowanie {% endblock subtitle %}
 
-{% block maincontent %}
+{% block content %}
 
 <div class="isection">
-<form method="POST" action="{% url django.contrib.auth.views.login %}">
+<form method="POST" action="{% url login %}">
+{% csrf_token %}
 {{ form.as_p }}
 <p><input type="submit" value="Login" /></p>
 <input type="hidden" name="next" value="{{ next|urlencode }}" />
 </form>
 </div>
 
-{% endblock maincontent %}
+{% endblock content %}
index 08073a4..2343b05 100644 (file)
@@ -4,12 +4,14 @@ from django.conf.urls.defaults import *
 from django.contrib import admin
 from django.conf import settings
 
-import wiki.urls
 
 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'),
 
@@ -18,8 +20,12 @@ urlpatterns = patterns('',
     url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
     (r'^admin/', include(admin.site.urls)),
 
-    url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/images/'}),
-    url(r'^documents/', include('wiki.urls')),
+    (r'^comments/', include('django.contrib.comments.urls')),
+
+    url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/documents/'}),
+    url(r'^documents/', include('catalogue.urls')),
+    url(r'^apiclient/', include('apiclient.urls')),
+    url(r'^editor/', include('wiki.urls')),
     url(r'^images/', include('wiki_img.urls')),
 
     # Static files (should be served by Apache)
@@ -29,8 +35,7 @@ urlpatterns = patterns('',
         {'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}),
-    (r'^documents/', include(wiki.urls)),
-    url(r'^themes$', 'wiki.views.themes', name="themes"),
+
     url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/documents/'}),
 
 )
index fe7944c..3a0f164 100644 (file)
@@ -1,3 +1,4 @@
-django-nose==0.0.3
+django-nose==0.1.3
 nose
 nosexcover
+mock
index 7cca9db..5e94254 100644 (file)
@@ -1,16 +1,22 @@
 ## Python libraries
-lxml>=2.2
-mercurial>=1.6
+lxml>=2.2.2
+mercurial>=1.6,<1.7
 PyYAML>=3.0
 PIL>=1.1
+oauth2
+httplib2 # oauth2 dependency
 
 ## Book conversion library
-git+git://github.com/fnp/librarian.git@master#egg=librarian
+git+git://github.com/fnp/librarian.git@master#egg=librarian
 
 ## Django
-Django>=1.1.1,<1.2
+Django>=1.3,<1.4
 sorl-thumbnail>=3.2
 django-maintenancemode>=0.9
+django-pagination
+django-gravatar
+django-celery
+django-kombu
 
 # migrations
 south>=0.6
diff --git a/scripts/merge.sh b/scripts/merge.sh
new file mode 100644 (file)
index 0000000..ad7271d
--- /dev/null
@@ -0,0 +1,315 @@
+
+./manage.py merge_books  --force --title='Brzozowski / Legenda Mlodej Polski' --slug=brzozowski__legenda_mlodej_polski \
+   brzozowski__legenda_mlodej_polski__cz_1 \
+   brzozowski__legenda_mlodej_polski__cz_2 \
+   brzozowski__legenda_mlodej_polski__cz_3 \
+   brzozowski__legenda_mlodej_polski__cz_4 \
+   brzozowski__legenda_mlodej_polski__cz_5 \
+   brzozowski__legenda_mlodej_polski__cz_6 \
+   brzozowski__legenda_mlodej_polski__cz_7
+
+./manage.py merge_books  --force --title='Cervantes / Don Kiszot' --slug=cervantes__don_kiszot \
+   cervantes__don_kiszot__ksiega_1 \
+   cervantes__don_kiszot__ksiega_2 \
+   cervantes__don_kiszot__ksiega_3 \
+   cervantes__don_kiszot__ksiega_4 \
+   cervantes__don_kiszot__ksiega_5 \
+   cervantes__don_kiszot__ksiega_6 \
+   cervantes__don_kiszot__ksiega_7 \
+   cervantes__don_kiszot__ksiega_8
+
+./manage.py merge_books  --force --title='Conrad / Lord Jim' --slug=conrad__lord_jim \
+   conrad__lord_jim__przedmowa \
+   conrad__lord_jim__rozdzialy_1-4 \
+   conrad__lord_jim__rozdzialy_5-8 \
+   conrad__lord_jim__rozdzialy_9-12 \
+   conrad__lord_jim__rozdzialy_13-14 \
+   conrad__lord_jim__rozdzialy_15-19 \
+   conrad__lord_jim__rozdzialy_20-23 \
+   conrad__lord_jim__rozdzialy_24-30 \
+   conrad__lord_jim__rozdzialy_31-33 \
+   conrad__lord_jim__rozdzialy_34-37
+
+./manage.py merge_books  --force --title='Dante / Boska Komedia / Czysciec' --slug=dante__boska_komedia__czysciec \
+   dante__boska_komedia__czysciec__cz_1 \
+   dante__boska_komedia__czysciec__cz_2 \
+   dante__boska_komedia__czysciec__cz_3
+
+./manage.py merge_books  --force --title='Dante / Boska Komedia / Pieklo' --slug=dante__boska_komedia__pieklo \
+   dante__boska_komedia__pieklo__cz_1 \
+   dante__boska_komedia__pieklo__cz_2 \
+   dante__boska_komedia__pieklo__cz_3
+
+./manage.py merge_books  --force --title='Dante / Boska Komedia / Raj' --slug=dante__boska_komedia__raj \
+   dante__boska_komedia__raj__cz_1 \
+   dante__boska_komedia__raj__cz_2 \
+   dante__boska_komedia__raj__cz_3
+
+./manage.py merge_books  --force --title='Domanska / Historia Zoltej Cizemki' --slug=domanska__historia_zoltej_cizemki \
+   domanska__historia_zoltej_cizemki__cz_1 \
+   domanska__historia_zoltej_cizemki__cz_2
+
+./manage.py merge_books  --force --title='Dumas / Trzej Muszkieterowie / Tom 1' --slug=dumas__trzej_muszkieterowie__tom_1 \
+   dumas__trzej_muszkieterowie__tom_1__rozdzialy_1-7 \
+   dumas__trzej_muszkieterowie__tom_1__rozdzialy_8-14 \
+   dumas__trzej_muszkieterowie__tom_1__rozdzialy_15-21 \
+   dumas__trzej_muszkieterowie__tom_1__rozdzialy_22-27
+
+./manage.py merge_books  --force --title='Gomulicki / Wspomnienia Niebieskiego Mundurka' --slug=gomulicki__wspomnienia_niebieskiego_mundurka \
+   gomulicki__wspomnienia_niebieskiego_mundurka__cz_1 \
+   gomulicki__wspomnienia_niebieskiego_mundurka__cz_2
+
+./manage.py merge_books  --force --title='Goszczynski / Krol Zamczyska' --slug=goszczynski__krol_zamczyska \
+   goszczynski__krol_zamczyska__wstep \
+   goszczynski__krol_zamczyska__cz_1 \
+   goszczynski__krol_zamczyska__cz_2
+
+./manage.py merge_books  --force --title='Mickiewicz / Pan Tadeusz' --slug=mickiewicz__pan_tadeusz \
+   mickiewicz__pan_tadeusz__ksiega_1 \
+   mickiewicz__pan_tadeusz__ksiega_2 \
+   mickiewicz__pan_tadeusz__ksiega_3 \
+   mickiewicz__pan_tadeusz__ksiega_4 \
+   mickiewicz__pan_tadeusz__ksiega_5 \
+   mickiewicz__pan_tadeusz__ksiega_6 \
+   mickiewicz__pan_tadeusz__ksiega_7 \
+   mickiewicz__pan_tadeusz__ksiega_8 \
+   mickiewicz__pan_tadeusz__ksiega_9 \
+   mickiewicz__pan_tadeusz__ksiega_10 \
+   mickiewicz__pan_tadeusz__ksiega_11 \
+   mickiewicz__pan_tadeusz__ksiega_12
+
+./manage.py merge_books  --force --title='Nietzsche / Tako Rzecze Zaratustra' --slug=nietzsche__tako_rzecze_zaratustra \
+   nietzsche__tako_rzecze_zaratustra__cz_11 \
+   nietzsche__tako_rzecze_zaratustra__cz_12 \
+   nietzsche__tako_rzecze_zaratustra__cz_21 \
+   nietzsche__tako_rzecze_zaratustra__cz_22 \
+   nietzsche__tako_rzecze_zaratustra__cz_31 \
+   nietzsche__tako_rzecze_zaratustra__cz_32 \
+   nietzsche__tako_rzecze_zaratustra__cz_41 \
+   nietzsche__tako_rzecze_zaratustra__cz_42
+
+./manage.py merge_books  --force --title='Pasek /  Pamietniki' --slug=pasek___pamietniki \
+   pasek__pamietniki \
+   pasek___pamietniki__czesc_2 \
+   pasek__pamietniki__cz_2 \
+   pasek___pamietniki__czesc_4 \
+   pasek__pamietniki__cz_3 \
+   pasek___pamietniki__czesc_6
+
+./manage.py merge_books  --force --title='Potocki / Wojna Chocimska' --slug=potocki__wojna_chocimska \
+   potocki__wojna_chocimska__wstep \
+   potocki__wojna_chocimska__cz_1 \
+   potocki__wojna_chocimska__cz_2 \
+   potocki__wojna_chocimska__cz_3 \
+   potocki__wojna_chocimska__cz_4 \
+   potocki__wojna_chocimska__cz_5 \
+   potocki__wojna_chocimska__cz_6 \
+   potocki__wojna_chocimska__cz_7 \
+   potocki__wojna_chocimska__cz_8 \
+   potocki__wojna_chocimska__cz_9 \
+   potocki__wojna_chocimska__cz_10
+
+./manage.py merge_books  --force --title='Reymont / Ziemia Obiecana / Tom I' --slug=reymont__ziemia_obiecana__tom_i \
+   reymont__ziemia_obiecana__tom_i__cz_1 \
+   reymont__ziemia_obiecana__tom_i__cz_2 \
+   reymont__ziemia_obiecana__tom_i__cz_3
+
+./manage.py merge_books  --force --title='Reymont / Ziemia Obiecana / Tom Ii' --slug=reymont__ziemia_obiecana__tom_ii \
+   reymont__ziemia_obiecana__tom_ii__cz_1 \
+   reymont__ziemia_obiecana__tom_ii__cz_2 \
+   reymont__ziemia_obiecana__tom_ii__cz_3
+
+./manage.py merge_books  --force --title='Sienkiewicz / Krzyzacy / Tom I' --slug=sienkiewicz__krzyzacy__tom_i \
+   sienkiewicz__krzyzacy__tom_i_rozdzialy_1-15 \
+   sienkiewicz__krzyzacy__tom_i_rozdzialy_11-15 \
+   sienkiewicz__krzyzacy__tom_i_rozdzialy_16-32 \
+   sienkiewicz__krzyzacy__tom_i_rozdzialy_21-25 \
+   sienkiewicz__krzyzacy__tom_i_rozdzialy_26-32
+
+./manage.py merge_books  --force --title='Sienkiewicz / Krzyzacy / Tom Ii' --slug=sienkiewicz__krzyzacy__tom_ii \
+   sienkiewicz__krzyzacy__tom_ii_rozdzialy_1-15 \
+   sienkiewicz__krzyzacy__tom_ii_rozdzialy_16-31 \
+   sienkiewicz__krzyzacy__tom_ii_rozdzialy_32-52 \
+   sienkiewicz__krzyzacy__tom_ii_rozdzialy_42-52
+
+./manage.py merge_books  --force --title='Sienkiewicz / Ogniem I Mieczem / Tom 1' --slug=sienkiewicz__ogniem_i_mieczem__tom_1 \
+   sienkiewicz__ogniem_i_mieczem__tom_1__rozdzialy_1-4 \
+   sienkiewicz__ogniem_i_mieczem__tom_1__rozdzialy_5-8 \
+   sienkiewicz__ogniem_i_mieczem__tom_1__rozdzialy_9-12 \
+   sienkiewicz__ogniem_i_mieczem__tom_1__rozdzialy_13-16 \
+   sienkiewicz__ogniem_i_mieczem__tom_1__rozdzialy_17-21 \
+   sienkiewicz__ogniem_i_mieczem__tom_1__rozdzialy_22-25 \
+   sienkiewicz__ogniem_i_mieczem__tom_1__rozdzialy_26-29 \
+   sienkiewicz__ogniem_i_mieczem__tom_1__rozdzialy_30-33
+
+./manage.py merge_books  --force --title='Sienkiewicz / Ogniem I Mieczem / Tom 2' --slug=sienkiewicz__ogniem_i_mieczem__tom_2 \
+   sienkiewicz__ogniem_i_mieczem__tom_2__rozdzialy_1-4 \
+   sienkiewicz__ogniem_i_mieczem__tom_2__rozdzialy_5-8 \
+   sienkiewicz__ogniem_i_mieczem__tom_2__rozdzialy_9-12 \
+   sienkiewicz__ogniem_i_mieczem__tom_2__rozdzialy_13-16 \
+   sienkiewicz__ogniem_i_mieczem__tom_2__rozdzialy_17-21 \
+   sienkiewicz__ogniem_i_mieczem__tom_2__rozdzialy_22-25 \
+   sienkiewicz__ogniem_i_mieczem__tom_2__rozdzialy_26-30
+
+./manage.py merge_books  --force --title='Sienkiewicz / Potop / Tom 1' --slug=sienkiewicz__potop__tom_1 \
+   sienkiewicz__potop__tom_1__rozdzialy_1-6 \
+   sienkiewicz__potop__tom_1__rozdzial_7 \
+   sienkiewicz__potop__tom_1__rozdzialy_8-10 \
+   sienkiewicz__potop__tom_1__rozdzialy_11-15 \
+   sienkiewicz__potop__tom_1__rozdzialy_16-21 \
+   sienkiewicz__potop__tom_1__rozdzialy_22-26
+
+./manage.py merge_books  --force --title='Sienkiewicz / Potop / Tom 2' --slug=sienkiewicz__potop__tom_2 \
+   sienkiewicz__potop__tom_2_rozdzialy__1-5 \
+   sienkiewicz__potop__tom_2_rozdzialy__6-10 \
+   sienkiewicz__potop__tom_2_rozdzialy__11-15 \
+   sienkiewicz__potop__tom_2_rozdzialy__16-20 \
+   sienkiewicz__potop__tom_2_rozdzialy__21-25 \
+   sienkiewicz__potop__tom_2_rozdzialy__26-30 \
+   sienkiewicz__potop__tom_2_rozdzialy__31-35 \
+   sienkiewicz__potop__tom_2_rozdzialy__36-40
+
+./manage.py merge_books  --force --title='Sienkiewicz / Potop / Tom 3' --slug=sienkiewicz__potop__tom_3 \
+   sienkiewicz__potop__tom_3_rozdzialy_1-15 \
+   sienkiewicz__potop__tom_3_rozdzialy_15-30
+
+./manage.py merge_books  --force --title='Staszic / Przestrogi Dla Polski' --slug=staszic__przestrogi_dla_polski \
+   staszic__przestrogi_dla_polski__cz_1 \
+   staszic__przestrogi_dla_polski__cz_2
+
+./manage.py merge_books  --force --title='Stevenson / Wyspa Skarbow' --slug=stevenson__wyspa_skarbow \
+   stevenson__wyspa_skarbow__cz_1 \
+   stevenson__wyspa_skarbow__cz_2 \
+   stevenson__wyspa_skarbow__cz_3 \
+   stevenson__wyspa_skarbow__cz_4 \
+   stevenson__wyspa_skarbow__cz_5 \
+   stevenson__wyspa_skarbow__cz_6
+
+./manage.py merge_books  --force --title='Swift / Podroze Guliwera' --slug=swift__podroze_guliwera \
+   swift__podroze_guliwera__czesc_1 \
+   swift__podroze_guliwera__czesc_2 \
+   swift__podroze_guliwera__czesc_3
+
+./manage.py merge_books  --force --title='Thackeray / Pierscien I Roza' --slug=thackeray__pierscien_i_roza \
+   thackeray__pierscien_i_roza__cz_1 \
+   thackeray__pierscien_i_roza__cz_2
+
+./manage.py merge_books  --force --title='Twain / Przygody Tomka Sawyera' --slug=twain__przygody_tomka_sawyera \
+   twain__przygody_tomka_sawyera__1-12 \
+   twain__przygody_tomka_sawyera__13-24 \
+   twain__przygody_tomka_sawyera__25-36
+
+./manage.py merge_books  --force --title='Verne / 20 000 Mil Podmorskiej Zeglugi' --slug=verne__20_000_mil_podmorskiej_zeglugi \
+   verne__20_000_mil_podmorskiej_zeglugi__rozdzialy_1-10 \
+   verne__20_000_mil_podmorskiej_zeglugi__rozdzialy_11-20 \
+   verne__20_000_mil_podmorskiej_zeglugi__rozdzialy_21-30 \
+   verne__20_000_mil_podmorskiej_zeglugi__rozdzialy_31-40 \
+   verne__20_000_mil_podmorskiej_zeglugi__rozdzialy_41-47
+
+./manage.py merge_books  --force --title='Verne / W 80 Dni Dookola Swiata' --slug=verne__w_80_dni_dookola_swiata \
+   verne__w_80_dni_dookola_swiata__rozdzialy_1-8 \
+   verne__w_80_dni_dookola_swiata__rozdzialy_9-16 \
+   verne__w_80_dni_dookola_swiata__rozdzialy_17-24 \
+   verne__w_80_dni_dookola_swiata__rozdzialy_25-32 \
+   verne__w_80_dni_dookola_swiata__rozdzialy_33-slowniczek
+
+./manage.py merge_books  --force --title='Zapolska / Kaska Kariatyda' --slug=zapolska__kaska_kariatyda \
+   zapolska__kaska_kariatyda__przedmowa \
+   zapolska__kaska_kariatyda__cz_1 \
+   zapolska__kaska_kariatyda__cz_2
+
+./manage.py merge_books  --force --title='Zeromski Syzyfowe Prace' --slug=zeromski_syzyfowe_prace \
+   zeromski_syzyfowe_prace_1-6 \
+   zeromski_syzyfowe_prace_7-12 \
+   zeromski_syzyfowe_prace_13-18
+
+./manage.py merge_books  --force --title='Zola / Germinal / Tom 1' --slug=zola__germinal__tom_1 \
+   zola__germinal__tom_1__czesc_1 \
+   zola__germinal__tom_1__czesc_2 \
+   zola__germinal__tom_1__czesc_3 \
+   zola__germinal__tom_1__czesc_4
+
+./manage.py merge_books  --force --title='Zola / Germinal / Tom 2' --slug=zola__germinal__tom_2 \
+   zola__germinal__tom_2__czesc_5 \
+   zola__germinal__tom_2__czesc_6 \
+   zola__germinal__tom_2__czesc_7
+
+
+./manage.py merge_books  --force --title='Frycz Modrzewski / O Poprawie Rzeczypospolitej' --slug=frycz_modrzewski__o_poprawie_rzeczypospolitej \
+    frycz_modrzewski__o_poprawie_rzeczypospolitej__ksiegi_i \
+    frycz_modrzewski__o_poprawie_rzeczypospolitej__ksiegi_ii \
+    frycz_modrzewski__o_poprawie_rzeczypospolitej__ksiegi_iii \
+    frycz_modrzewski__o_poprawie_rzeczypospolitej__ksiegi_iv \
+    frycz_modrzewski__o_poprawie_rzeczypospolitej__przypiski \
+    frycz_modrzewski__o_poprawie_rzeczypospolitej__zamknienie_tych_wszystkich_ksiag__przydatek
+
+
+./manage.py merge_books  --force --title='Goethe / Faust / Czesc 1' --slug=goethe__faust__czesc_1 \
+    goethe__faust__czesc_1 \
+    goethe__faust__czesc_1_cd
+
+
+./manage.py merge_books  --force --title='Konopnicka / O Krasnoludkach I Sierotce Marysi' --slug=konopnicka__o_krasnoludkach_i_sierotce_marysi \
+    konopnicka__o_krasnoludkach_i_sierotce_marysi \
+    konopnicka__o_krasnoludkach_i_sierotce_marysi__cz_2
+
+
+
+./manage.py merge_books  --force --title='Malczewski / Maria fr.' --slug=malczewski__maria__fr \
+    malczewski__maria__fr__przedmowa \
+    malczewski__maria__fr__piesn_i \
+    malczewski__maria__fr__piesn_ii
+
+./manage.py merge_books  --force --title='Malczewski / Maria niem.' --slug=malczewski__maria__niem \
+    malczewski__maria__niem__przedmowa \
+    malczewski__maria__niem__piesn_1 \
+    malczewski__maria__niem__piesn_2
+
+./manage.py merge_books  --force --title='Malczewski / Maria' --slug=malczewski__maria \
+    malczewski__maria__wstep \
+    malczewski__maria__piesn_1 \
+    malczewski__maria__piesn_2
+
+
+./manage.py merge_books  --force --title='Meyrink / Golem' --slug=meyrink__golem \
+    meyrink__golem \
+    meyrink__golem__cz_ii \
+    meyrink__golem__iii \
+    meyrink__golem__iv
+
+./manage.py merge_books  --force --title='Norwid / Pierścień wielkiej damy' --slug=norwid__pierscien_wielkiej_damy \
+    norwid__pierscien_wielkiej_damy__akt_1 \
+    norwid__pierscien_wielkiej_damy__akt_2 \
+    norwid__pierscien_wielkiej_damy__akt_3
+
+./manage.py merge_books  --force --title='Reymont / Chłopi / Zima' --slug=reymont_chlopi_zima \
+    reymont_chlopi_zima_i-vi \
+    reymont_chlopi_zima_vii-xiii
+
+./manage.py merge_books  --force --title='Słowacki / Beniowski' --slug=slowacki__beniowski \
+    slowacki__beniowski__piesn_1 \
+    slowacki__beniowski__piesn_2 \
+    slowacki__beniowski__piesn_3 \
+    slowacki__beniowski__piesn_4 \
+    slowacki__beniowski__piesn_5 \
+    slowacki__beniowski__piesn_6 \
+    slowacki__beniowski__piesn_7 \
+    slowacki__beniowski__piesn_8 \
+    slowacki__beniowski__piesn_9 \
+    slowacki__beniowski__piesn_10 \
+    slowacki__beniowski__piesn_11 \
+    slowacki__beniowski__piesn_12 \
+    slowacki__beniowski__piesn_13 \
+    slowacki__beniowski__piesn_14
+
+
+./manage.py merge_books  --force --title='Shakespeare / Poskromienie Złośnicy' --slug=shakespeare_poskromienie_zlosnicy \
+    shakespeare_poskromienie_zlosnicy_i-ii \
+    shakespeare_poskromienie_zlosnicy_iii-v
+
+
+./manage.py merge_books  --force --title='Shakespeare / Wesołe kumoszki z Windsoru' --slug=shakespeare_wesole_kumoszki_z_windsoru \
+    shakespeare_wesole_kumoszki_z_windsoru_i-ii \
+    shakespeare_wesole_kumoszki_z_windsoru_iii-v
+
+
diff --git a/scripts/once_delete_unneeded.py b/scripts/once_delete_unneeded.py
new file mode 100644 (file)
index 0000000..b8c335c
--- /dev/null
@@ -0,0 +1,29 @@
+from catalogue.models import Book
+
+
+slugs = """sienkiewicz__ogniem_i_mieczem__tom_1
+sienkiewicz__ogniem_i_mieczem__tom_2
+czechowicz__dzien_jak_codzien
+czechowicz__erotyk_elegia_niemocy_elegia_zalu_elegia_uspienia
+czechowicz__imieniny_pod_piopiolem_sam_pontorson
+czechowicz__preludjum_ballada_o_matce_przez_kresy
+czechowicz__zdrada_samobojstwo_deszcz_w_przeczucia
+brzozowski__legaenda_mlodej_polski__cz_1
+brzozowski__legaenda_mlodej_polski__cz_2
+brzozowski__legaenda_mlodej_polski__cz_3
+brzozowski__legaenda_mlodej_polski__cz_4
+brzozowski__legaenda_mlodej_polski__cz_5
+brzozowski__legaenda_mlodej_polski__cz_6
+brzozowski__legaenda_mlodej_polski__cz_7
+ayenarius__noc_byla
+mickiewicz__zdania_i_uwagi
+mickiewicz__pan_tadeusz__ksiegi_1-6
+mickiewicz__pan_tadeusz__ksiegi_7-12
+sienkiewicz__potop__tom_1__rozdzialy_7-26
+sienkiewicz__potop__tom_2
+sienkiewicz__pan_wolodyjowski__rozdzialy_53-54_i_epilog"""
+
+Book.objects.filter(slug__in=slugs.split()).delete()
+
+
+
index 61b00b7..054908c 100755 (executable)
@@ -9,7 +9,7 @@
 # Pliki wyjściowe zapisywane są obok plików źródłowych, z rozszerzeniem
 # zmienionym na .png.
 
-find . -iname '*.tiff' -print0 | while read -d $'\0' file 
+find . -iregex '.*\.tiff?' -print0 | while read -d $'\0' file 
 do
        echo "$file"
        convert "$file" -depth 7 -resize 640x960 png:- | pngnq -n 128 -s 1 > "${file%.tiff}.png"