From: Radek Czajka
Date: Wed, 14 Dec 2011 13:44:29 +0000 (+0100)
Subject: Merge master into img-playground. Image support with new management features. Missing...
X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/73ef2b8442dc95f8b7279de812c30ac8626d5f39?hp=5ef8e304790026b27417d8ff3c76c18858ba708f
Merge master into img-playground. Image support with new management features. Missing history-related stuff, needs refactoring (doubled code).
---
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..56b9c425
--- /dev/null
+++ b/.gitmodules
@@ -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
index 00000000..d44e016d
--- /dev/null
+++ b/apps/apiclient/__init__.py
@@ -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
index 00000000..4af28a52
--- /dev/null
+++ b/apps/apiclient/migrations/0001_initial.py
@@ -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
index 00000000..e69de29b
diff --git a/apps/apiclient/models.py b/apps/apiclient/models.py
new file mode 100644
index 00000000..d3c8f620
--- /dev/null
+++ b/apps/apiclient/models.py
@@ -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
index 00000000..5fbf18ee
--- /dev/null
+++ b/apps/apiclient/settings.py
@@ -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
index 00000000..2247054b
--- /dev/null
+++ b/apps/apiclient/tests.py
@@ -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
index 00000000..87d9997d
--- /dev/null
+++ b/apps/apiclient/urls.py
@@ -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
index 00000000..d4960148
--- /dev/null
+++ b/apps/apiclient/views.py
@@ -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
index 00000000..c53f0e73
--- /dev/null
+++ b/apps/catalogue/__init__.py
@@ -0,0 +1 @@
+ # pragma: no cover
diff --git a/apps/catalogue/admin.py b/apps/catalogue/admin.py
new file mode 100644
index 00000000..8ba803e7
--- /dev/null
+++ b/apps/catalogue/admin.py
@@ -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
index 00000000..d75d6b4b
--- /dev/null
+++ b/apps/catalogue/constants.py
@@ -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
index 00000000..1fcf8d33
--- /dev/null
+++ b/apps/catalogue/ebook_utils.py
@@ -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
index 00000000..5a46ec04
--- /dev/null
+++ b/apps/catalogue/fixtures/stages.json
@@ -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
index 00000000..4e5b2cb4
--- /dev/null
+++ b/apps/catalogue/forms.py
@@ -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
index 00000000..df64ade1
--- /dev/null
+++ b/apps/catalogue/helpers.py
@@ -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
index 00000000..e6f7d3b6
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
index 00000000..65d9ba35
--- /dev/null
+++ b/apps/catalogue/locale/pl/LC_MESSAGES/django.po
@@ -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 , 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 \n"
+"Language-Team: Fundacja Nowoczesna Polska \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 .xml
will be ignored."
+msgstr "ProszÄ wskazaÄ archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie koÅczÄ
ce siÄ na .xml
zostanÄ
zignorowane."
+
+#: templates/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 .xml
extension"
+msgstr "Pliki pominiÄte z powodu braku rozszerzenia .xml
."
+
+#: 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
index 00000000..e69de29b
diff --git a/apps/catalogue/management/commands/__init__.py b/apps/catalogue/management/commands/__init__.py
new file mode 100755
index 00000000..e69de29b
diff --git a/apps/catalogue/management/commands/assign_from_redmine.py b/apps/catalogue/management/commands/assign_from_redmine.py
new file mode 100755
index 00000000..9f7b12d4
--- /dev/null
+++ b/apps/catalogue/management/commands/assign_from_redmine.py
@@ -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
index 00000000..c252c208
--- /dev/null
+++ b/apps/catalogue/management/commands/fix_rdf_about.py
@@ -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
index 00000000..5f603883
--- /dev/null
+++ b/apps/catalogue/management/commands/import_wl.py
@@ -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
index 00000000..aec113ed
--- /dev/null
+++ b/apps/catalogue/management/commands/merge_books.py
@@ -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\d*)$'), None))
+ res.append((re.compile(ur'__?(rozdzialy__?)?(?P\d*)-'), None))
+
+ for r, default in res:
+ m = r.search(slug)
+ if m:
+ start = m.start()
+ try:
+ return int(m.group('n')), slug[:start]
+ except IndexError:
+ return default, slug[:start]
+ return None, slug
+
+ def file_to_title(fname):
+ """ Returns a title-like version of a filename. """
+ parts = (p.replace('_', ' ').title() for p in fname.split('__'))
+ return ' / '.join(parts)
+
+ merges = defaultdict(list)
+ slugs = []
+ for b in Book.objects.all():
+ slugs.append(b.slug)
+ n, ns = read_slug(b.slug)
+ if n is not None:
+ merges[ns].append((n, b))
+
+ conflicting_slugs = []
+ for slug in sorted(merges.keys()):
+ merge_list = sorted(merges[slug])
+ if len(merge_list) < 2:
+ continue
+
+ merge_slugs = [b.slug for i, b in merge_list]
+ if slug in slugs and slug not in merge_slugs:
+ conflicting_slugs.append(slug)
+
+ title = file_to_title(slug)
+ print "./manage.py merge_books %s%s--title=%s --slug=%s \\\n %s\n" % (
+ '--dry-run ' if dry_run else '',
+ '--force ' if force else '',
+ quote(title), slug,
+ " \\\n ".join(merge_slugs)
+ )
+
+ if conflicting_slugs:
+ if force:
+ print self.style.NOTICE('# These books will be archived:')
+ else:
+ print self.style.ERROR('# ERROR: Conflicting slugs:')
+ for slug in conflicting_slugs:
+ print '#', slug
+
+
+ def handle(self, *slugs, **options):
+
+ self.style = color_style()
+
+ force = options.get('force')
+ guess = options.get('guess')
+ dry_run = options.get('dry_run')
+ new_slug = options.get('new_slug').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
index 00000000..4f804b84
--- /dev/null
+++ b/apps/catalogue/managers.py
@@ -0,0 +1,5 @@
+from django.db import models
+
+class VisibleManager(models.Manager):
+ def get_query_set(self):
+ return super(VisibleManager, self).get_query_set().exclude(_hidden=True)
diff --git a/apps/catalogue/migrations/0001_initial.py b/apps/catalogue/migrations/0001_initial.py
new file mode 100644
index 00000000..dccd9b7b
--- /dev/null
+++ b/apps/catalogue/migrations/0001_initial.py
@@ -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
index 00000000..71554570
--- /dev/null
+++ b/apps/catalogue/migrations/0002_stages.py
@@ -0,0 +1,122 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+
+ from django.core.management import call_command
+ call_command("loaddata", "stages.json")
+
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'catalogue.book': {
+ 'Meta': {'ordering': "['parent_number', 'title']", 'object_name': 'Book'},
+ '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+ '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+ '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+ 'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+ 'parent_number': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+ },
+ 'catalogue.bookpublishrecord': {
+ 'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'},
+ 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'catalogue.chunk': {
+ 'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+ '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+ '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+ '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+ 'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_documents'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ChunkChange']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'number': ('django.db.models.fields.IntegerField', [], {}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+ 'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'catalogue.chunkchange': {
+ 'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+ 'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+ 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+ 'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}),
+ 'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"})
+ },
+ 'catalogue.chunkpublishrecord': {
+ 'Meta': {'object_name': 'ChunkPublishRecord'},
+ 'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}),
+ 'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'catalogue.chunktag': {
+ 'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'ordering': ('django.db.models.fields.IntegerField', [], {}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['catalogue']
diff --git a/apps/catalogue/migrations/0003_from_hg.py b/apps/catalogue/migrations/0003_from_hg.py
new file mode 100644
index 00000000..1816af90
--- /dev/null
+++ b/apps/catalogue/migrations/0003_from_hg.py
@@ -0,0 +1,280 @@
+# encoding: utf-8
+import datetime
+from zlib import compress
+import os
+import os.path
+import re
+import urllib
+
+from django.db import models
+from mercurial import hg, ui
+from south.db import db
+from south.v2 import DataMigration
+
+from django.conf import settings
+from slughifi import slughifi
+
+META_REGEX = re.compile(r'\s*', re.DOTALL | re.MULTILINE)
+STAGE_TAGS_RE = re.compile(r'^#stage-finished: (.*)$', re.MULTILINE)
+AUTHOR_RE = re.compile(r'\s*(.*?)\s*<(.*)>\s*')
+
+
+def urlunquote(url):
+ """Unqotes URL
+
+ # >>> urlunquote('Za%C5%BC%C3%B3%C5%82%C4%87_g%C4%99%C5%9Bl%C4%85_ja%C5%BA%C5%84')
+ # u'Za\u017c\xf3\u0142\u0107_g\u0119\u015bl\u0105 ja\u017a\u0144'
+ """
+ return unicode(urllib.unquote(url), 'utf-8', 'ignore')
+
+
+def split_name(name):
+ parts = name.split('__')
+ return parts
+
+
+def file_to_title(fname):
+ """ Returns a title-like version of a filename. """
+ parts = (p.replace('_', ' ').title() for p in fname.split('__'))
+ return ' / '.join(parts)
+
+
+def plain_text(text):
+ return re.sub(META_REGEX, '', text, 1)
+
+
+def gallery(slug, text):
+ result = {}
+
+ m = re.match(META_REGEX, text)
+ if m:
+ for line in m.group(1).split('\n'):
+ try:
+ k, v = line.split(':', 1)
+ result[k.strip()] = v.strip()
+ except ValueError:
+ continue
+
+ gallery = result.get('gallery', slughifi(slug))
+
+ if gallery.startswith('/'):
+ gallery = os.path.basename(gallery)
+
+ return gallery
+
+
+def migrate_file_from_hg(orm, fname, entry):
+ fname = urlunquote(fname)
+ print fname
+ if fname.endswith('.xml'):
+ fname = fname[:-4]
+ title = file_to_title(fname)
+ fname = slughifi(fname)
+
+ # create all the needed objects
+ # what if it already exists?
+ book = orm.Book.objects.create(
+ title=title,
+ slug=fname)
+ chunk = orm.Chunk.objects.create(
+ book=book,
+ number=1,
+ slug='1')
+ try:
+ chunk.stage = orm.ChunkTag.objects.order_by('ordering')[0]
+ except IndexError:
+ chunk.stage = None
+
+ maxrev = entry.filerev()
+ gallery_link = None
+
+ # this will fail if directory exists
+ os.makedirs(os.path.join(settings.CATALOGUE_REPO_PATH, str(chunk.pk)))
+
+ for rev in xrange(maxrev + 1):
+ fctx = entry.filectx(rev)
+ data = fctx.data()
+ gallery_link = gallery(fname, data)
+ data = plain_text(data)
+
+ # get tags from description
+ description = fctx.description().decode("utf-8", 'replace')
+ tags = STAGE_TAGS_RE.findall(description)
+ tags = [orm.ChunkTag.objects.get(slug=slug.strip()) for slug in tags]
+
+ if tags:
+ max_ordering = max(tags, key=lambda x: x.ordering).ordering
+ try:
+ chunk.stage = orm.ChunkTag.objects.filter(ordering__gt=max_ordering).order_by('ordering')[0]
+ except IndexError:
+ chunk.stage = None
+
+ description = STAGE_TAGS_RE.sub('', description)
+
+ author = author_name = author_email = None
+ author_desc = fctx.user().decode("utf-8", 'replace')
+ m = AUTHOR_RE.match(author_desc)
+ if m:
+ try:
+ author = orm['auth.User'].objects.get(username=m.group(1), email=m.group(2))
+ except orm['auth.User'].DoesNotExist:
+ author_name = m.group(1)
+ author_email = m.group(2)
+ else:
+ author_name = author_desc
+
+ head = orm.ChunkChange.objects.create(
+ tree=chunk,
+ revision=rev + 1,
+ created_at=datetime.datetime.fromtimestamp(fctx.date()[0]),
+ description=description,
+ author=author,
+ author_name=author_name,
+ author_email=author_email,
+ parent=chunk.head
+ )
+
+ path = "%d/%d" % (chunk.pk, head.pk)
+ abs_path = os.path.join(settings.CATALOGUE_REPO_PATH, path)
+ f = open(abs_path, 'wb')
+ f.write(compress(data))
+ f.close()
+ head.data = path
+
+ head.tags = tags
+ head.save()
+
+ chunk.head = head
+
+ chunk.save()
+ if gallery_link:
+ book.gallery = gallery_link
+ book.save()
+
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ try:
+ hg_path = settings.WIKI_REPOSITORY_PATH
+ except:
+ print 'repository not configured, skipping'
+ else:
+ print 'migrate from', hg_path
+ repo = hg.repository(ui.ui(), hg_path)
+ tip = repo['tip']
+ for fname in tip:
+ if fname.startswith('.'):
+ continue
+ migrate_file_from_hg(orm, fname, tip[fname])
+
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+ pass
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'catalogue.book': {
+ 'Meta': {'ordering': "['parent_number', 'title']", 'object_name': 'Book'},
+ '_new_publishable': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+ '_published': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+ '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ '_single': ('django.db.models.fields.NullBooleanField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+ 'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}),
+ 'parent_number': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+ },
+ 'catalogue.bookpublishrecord': {
+ 'Meta': {'ordering': "['-timestamp']", 'object_name': 'BookPublishRecord'},
+ 'book': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.Book']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'catalogue.chunk': {
+ 'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+ '_changed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+ '_hidden': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+ '_short_html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+ 'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_documents'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ChunkChange']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'number': ('django.db.models.fields.IntegerField', [], {}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+ 'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
+ },
+ 'catalogue.chunkchange': {
+ 'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+ 'author_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+ 'data': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}),
+ 'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}),
+ 'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"})
+ },
+ 'catalogue.chunkpublishrecord': {
+ 'Meta': {'object_name': 'ChunkPublishRecord'},
+ 'book_record': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.BookPublishRecord']"}),
+ 'change': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'publish_log'", 'to': "orm['catalogue.ChunkChange']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'catalogue.chunktag': {
+ 'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'ordering': ('django.db.models.fields.IntegerField', [], {}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['catalogue']
diff --git a/apps/catalogue/migrations/0004_fix_revisions.py b/apps/catalogue/migrations/0004_fix_revisions.py
new file mode 100644
index 00000000..fe5c86be
--- /dev/null
+++ b/apps/catalogue/migrations/0004_fix_revisions.py
@@ -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
index 00000000..71af5f6c
--- /dev/null
+++ b/apps/catalogue/migrations/0005_auto__add_field_chunk_gallery_start.py
@@ -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
index 00000000..fd1cea56
--- /dev/null
+++ b/apps/catalogue/migrations/0006_auto__add_field_book_public.py
@@ -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
index 00000000..5ae20ea3
--- /dev/null
+++ b/apps/catalogue/migrations/0007_auto__add_field_book_dc_slug.py
@@ -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
index 00000000..5276b27b
--- /dev/null
+++ b/apps/catalogue/migrations/0008_auto.py
@@ -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
index 00000000..4de5212e
--- /dev/null
+++ b/apps/catalogue/migrations/0009_auto__add_imagechange__add_unique_imagechange_tree_revision__add_image.py
@@ -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
index 00000000..e69de29b
diff --git a/apps/catalogue/models/__init__.py b/apps/catalogue/models/__init__.py
new file mode 100755
index 00000000..82e1c116
--- /dev/null
+++ b/apps/catalogue/models/__init__.py
@@ -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
index 00000000..46d11e83
--- /dev/null
+++ b/apps/catalogue/models/book.py
@@ -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
index 00000000..171ba533
--- /dev/null
+++ b/apps/catalogue/models/chunk.py
@@ -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
index 00000000..53f8830d
--- /dev/null
+++ b/apps/catalogue/models/image.py
@@ -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
index 00000000..de1387ee
--- /dev/null
+++ b/apps/catalogue/models/listeners.py
@@ -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
index 00000000..6cc86d08
--- /dev/null
+++ b/apps/catalogue/models/publish_log.py
@@ -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
index 00000000..62ca5145
--- /dev/null
+++ b/apps/catalogue/signals.py
@@ -0,0 +1,3 @@
+from django.dispatch import Signal
+
+post_publish = Signal()
diff --git a/apps/catalogue/tasks.py b/apps/catalogue/tasks.py
new file mode 100644
index 00000000..547f36b4
--- /dev/null
+++ b/apps/catalogue/tasks.py
@@ -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
index 00000000..9c2eac51
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/activity.html
@@ -0,0 +1,17 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+{% load url from future %}
+{% load wall %}
+
+
+{% block content %}
+
+<
+ {% trans "Activity" %}: {{ day }}
+ {% if next_day %}
+ >
+ {% endif %}
+
+
+ {% 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
index 00000000..f3ebcd98
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/base.html
@@ -0,0 +1,50 @@
+{% load compressed i18n %}
+{% load catalogue %}
+
+
+
+
+ {% compressed_css 'catalogue' %}
+ {% block title %}{% trans "Platforma Redakcyjna" %}{% endblock title %}
+
+
+
+
+
+
+
+
+
+
+ {% main_tabs %}
+
+
+
+ {% include "registration/head_login.html" %}
+
+
+
+
+
+
+
+{% block content %}
+
+ {% block leftcolumn %}
+ {% endblock leftcolumn %}
+
+
+ {% block rightcolumn %}
+ {% endblock rightcolumn %}
+
+{% endblock content %}
+
+
+
+
+
+{% compressed_js 'catalogue' %}
+{% block extrabody %}
+{% endblock %}
+
+
diff --git a/apps/catalogue/templates/catalogue/book_append_to.html b/apps/catalogue/templates/catalogue/book_append_to.html
new file mode 100755
index 00000000..76a59621
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/book_append_to.html
@@ -0,0 +1,14 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+
+{% 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
index 00000000..bfd4ef5c
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/book_detail.html
@@ -0,0 +1,95 @@
+{% extends "catalogue/base.html" %}
+{% load book_list comments i18n %}
+
+{% block content %}
+
+
+{{ book.title }}
+
+
+{% if editable %}{% endif %}
+
+
+{% if editable %}
+ {% trans "Append to other book" %}
+{% endif %}
+
+
+
+
+
{% trans "Chunks" %}
+
+
+ {% for chunk in book %}
+ {{ chunk.short_html|safe }}
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
{% trans "Comments" %}
+
+ {% render_comment_list for book %}
+ {% with book.get_absolute_url as next %}
+ {% render_comment_form for book %}
+ {% endwith %}
+
+
+{% endblock content %}
diff --git a/apps/catalogue/templates/catalogue/book_edit.html b/apps/catalogue/templates/catalogue/book_edit.html
new file mode 100755
index 00000000..3fffa963
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/book_edit.html
@@ -0,0 +1,14 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+
+{% 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
index 00000000..af4cfa79
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/book_html.html
@@ -0,0 +1,30 @@
+{% load i18n %}
+{% load compressed %}
+
+
+
+
+ {{ book.title }}
+
+
+
+
+ {#% book_info book %#}
+
+
+
+ {{ html|safe }}
+
+
+
diff --git a/apps/catalogue/templates/catalogue/book_list/book.html b/apps/catalogue/templates/catalogue/book_list/book.html
new file mode 100755
index 00000000..46d5ae12
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/book_list/book.html
@@ -0,0 +1,35 @@
+{% load i18n %}
+
+{% if book.single %}
+ {% with book.0 as chunk %}
+
+ [B] |
+ [c] |
+
+ {{ book.title }} |
+ {% if chunk.stage %}
+ {{ chunk.stage }}
+ {% else %}â
+ {% endif %} |
+ {% if chunk.user %}{{ chunk.user.first_name }} {{ chunk.user.last_name }}{% endif %} |
+
+ {% if chunk.published %}P{% endif %}
+ {% if book.new_publishable %}p{% endif %}
+ {% if chunk.changed %}+{% endif %}
+ |
+
+ {% endwith %}
+{% else %}
+
+ [B] |
+ |
+ {{ book.title }} |
+ |
+ |
+
+ {% if book.published %}P{% endif %}
+ {% if book.new_publishable %}p{% endif %}
+ |
+
+{% endif %}
diff --git a/apps/catalogue/templates/catalogue/book_list/book_list.html b/apps/catalogue/templates/catalogue/book_list/book_list.html
new file mode 100755
index 00000000..73811cab
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/book_list/book_list.html
@@ -0,0 +1,81 @@
+{% load i18n %}
+{% load pagination_tags %}
+
+
+
+
+
+{% if not books %}
+ {% trans "No books found." %}
+{% endif %}
diff --git a/apps/catalogue/templates/catalogue/book_list/chunk.html b/apps/catalogue/templates/catalogue/book_list/chunk.html
new file mode 100755
index 00000000..14599428
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/book_list/chunk.html
@@ -0,0 +1,25 @@
+{% load i18n %}
+
+
+ |
+ [c] |
+
+ {{ chunk.number }}.
+ {{ chunk.title }} |
+ {% if chunk.stage %}
+ {{ chunk.stage }}
+ {% else %}
+ â
+ {% endif %} |
+ {% if chunk.user %}
+
+ {{ chunk.user.first_name }} {{ chunk.user.last_name }}
+ {% else %}
+
+ {% endif %} |
+
+
+ {% if chunk.new_publishable %}p{% endif %}
+ {% if chunk.changed %}+{% endif %}
+ |
+
diff --git a/apps/catalogue/templates/catalogue/chunk_add.html b/apps/catalogue/templates/catalogue/chunk_add.html
new file mode 100755
index 00000000..b287479c
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/chunk_add.html
@@ -0,0 +1,16 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block content %}
+ {% trans "Split chunk" %}
+
+
+{% endblock content %}
diff --git a/apps/catalogue/templates/catalogue/chunk_edit.html b/apps/catalogue/templates/catalogue/chunk_edit.html
new file mode 100755
index 00000000..bdacd028
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/chunk_edit.html
@@ -0,0 +1,21 @@
+{% extends "catalogue/base.html" %}
+{% load url from future %}
+{% load i18n %}
+
+{% block content %}
+ {% trans "Chunk settings" %}
+
+
+
+
+ {% trans "Split chunk" %}
+
+{% 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
index 00000000..47c99f98
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/document_create_missing.html
@@ -0,0 +1,14 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block content %}
+ {% trans "Create a new book" %}
+
+
+{% endblock content %}
diff --git a/apps/catalogue/templates/catalogue/document_list.html b/apps/catalogue/templates/catalogue/document_list.html
new file mode 100644
index 00000000..920f25a6
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/document_list.html
@@ -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
index 00000000..87e93e0a
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/document_upload.html
@@ -0,0 +1,69 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+
+{% block leftcolumn %}
+
+
+{% trans "Bulk documents upload" %}
+
+
+{% trans "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with .xml
will be ignored." %}
+
+
+
+
+
+
+{% if error_list %}
+
+ {% trans "There have been some errors. No files have been added to the repository." %}
+
{% trans "Offending files" %}
+
+ {% for filename, title, error in error_list %}
+ - {{ title }} (
{{ filename }}
): {{ error }}
+ {% endfor %}
+
+
+ {% if ok_list %}
+ {% trans "Correct files" %}
+
+ {% for filename, slug, title in ok_list %}
+ - {{ title }} (
{{ filename }}
)
+ {% endfor %}
+
+ {% endif %}
+
+{% else %}
+
+ {% if ok_list %}
+ {% trans "Files have been successfully uploaded to the repository." %}
+ {% trans "Uploaded files" %}
+
+ {% for filename, slug, title in ok_list %}
+ - {{ title }} (
{{ filename }})
+ {% endfor %}
+
+ {% endif %}
+{% endif %}
+
+{% if skipped_list %}
+ {% trans "Skipped files" %}
+ {% trans "Files skipped due to no .xml
extension" %}
+
+ {% for filename in skipped_list %}
+ - {{ filename }}
+ {% endfor %}
+
+{% 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
index 00000000..3ff75bc0
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/image_list.html
@@ -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
index 00000000..2e2b386c
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/image_short.html
@@ -0,0 +1,18 @@
+{% load i18n %}
+
+
+ [B] |
+
+ {{ image.title }} |
+ {% if image.stage %}
+ {{ image.stage }}
+ {% else %}â
+ {% endif %} |
+ {% if image.user %}{{ image.user.first_name }} {{ image.user.last_name }}{% endif %} |
+
+ {% if image.published %}P{% endif %}
+ {% if image.new_publishable %}p{% endif %}
+ {% if image.changed %}+{% endif %}
+ |
+
diff --git a/apps/catalogue/templates/catalogue/image_table.html b/apps/catalogue/templates/catalogue/image_table.html
new file mode 100755
index 00000000..68293e77
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/image_table.html
@@ -0,0 +1,69 @@
+{% load i18n %}
+{% load pagination_tags %}
+
+
+
+
+
+
+ |
+
+
+ |
+ |
+
+ {% if not viewed_user %}
+ |
+ {% endif %}
+
+ |
+
+
+
+ {% with cnt=objects|length %}
+ {% autopaginate objects 100 %}
+
+ {% for item in objects %}
+ {{ item.short_html|safe }}
+ {% endfor %}
+
+ {% paginate %}
+ {% blocktrans count c=cnt %}{{c}} image{% plural %}{{c}} images{% endblocktrans %} |
+
+ {% endwith %}
+
+{% if not objects %}
+ {% trans "No images found." %}
+{% endif %}
diff --git a/apps/catalogue/templates/catalogue/main_tabs.html b/apps/catalogue/templates/catalogue/main_tabs.html
new file mode 100755
index 00000000..82321cc4
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/main_tabs.html
@@ -0,0 +1,3 @@
+{% for tab in tabs %}
+ {{ tab.caption }}
+{% endfor %}
diff --git a/apps/catalogue/templates/catalogue/my_page.html b/apps/catalogue/templates/catalogue/my_page.html
new file mode 100755
index 00000000..48a21796
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/my_page.html
@@ -0,0 +1,24 @@
+{% extends "catalogue/base.html" %}
+
+{% load i18n %}
+{% load catalogue book_list wall %}
+
+
+{% block leftcolumn %}
+ {% book_list request.user %}
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+
+
{% trans "Your last edited documents" %}
+
+ {% for slugs, item in last_books %}
+ - {{ item.title }}
({{ item.time|date:"H:i:s, d/m/Y" }})
+ {% endfor %}
+
+
+
+ {% trans "Recent activity for" %} {{ request.user|nice_name }}
+ {% 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
index 00000000..a9670e47
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/upload_pdf.html
@@ -0,0 +1,17 @@
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+
+{% block content %}
+
+
+{% trans "PDF file upload" %}
+
+
+
+
+{% endblock content %}
diff --git a/apps/catalogue/templates/catalogue/user_list.html b/apps/catalogue/templates/catalogue/user_list.html
new file mode 100755
index 00000000..9e1e83e5
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/user_list.html
@@ -0,0 +1,18 @@
+{% extends "catalogue/base.html" %}
+
+{% load i18n %}
+
+{% block leftcolumn %}
+
+{% trans "Users" %}
+
+
+
+{% endblock leftcolumn %}
diff --git a/apps/catalogue/templates/catalogue/user_page.html b/apps/catalogue/templates/catalogue/user_page.html
new file mode 100755
index 00000000..89b4ecef
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/user_page.html
@@ -0,0 +1,15 @@
+{% extends "catalogue/base.html" %}
+
+{% load i18n %}
+{% load catalogue book_list wall %}
+
+
+{% block leftcolumn %}
+ {{ viewed_user|nice_name }}
+ {% book_list viewed_user %}
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+ {% trans "Recent activity for" %} {{ viewed_user|nice_name }}
+ {% wall viewed_user 10 %}
+{% endblock rightcolumn %}
diff --git a/apps/catalogue/templates/catalogue/wall.html b/apps/catalogue/templates/catalogue/wall.html
new file mode 100755
index 00000000..9227ba19
--- /dev/null
+++ b/apps/catalogue/templates/catalogue/wall.html
@@ -0,0 +1,35 @@
+{% load i18n %}
+{% load gravatar %}
+{% load email %}
+
+
diff --git a/apps/catalogue/templatetags/__init__.py b/apps/catalogue/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/catalogue/templatetags/book_list.py b/apps/catalogue/templatetags/book_list.py
new file mode 100755
index 00000000..5e18b7e2
--- /dev/null
+++ b/apps/catalogue/templatetags/book_list.py
@@ -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
index 00000000..0b57b498
--- /dev/null
+++ b/apps/catalogue/templatetags/catalogue.py
@@ -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
index 00000000..b3d44d73
--- /dev/null
+++ b/apps/catalogue/templatetags/set_get_parameter.py
@@ -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
index 00000000..28671fb7
--- /dev/null
+++ b/apps/catalogue/templatetags/wall.py
@@ -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
index 00000000..b03701f8
--- /dev/null
+++ b/apps/catalogue/tests/__init__.py
@@ -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
index 00000000..8497f606
--- /dev/null
+++ b/apps/catalogue/tests/files/chunk1.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+Mickiewicz, Adam
+Do M***
+Fundacja Nowoczesna Polska
+Romantyzm
+Liryka
+Wiersz
+
+http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m
+http://www.polona.pl/Content/2222
+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
+
+Domena publiczna - Adam Mickiewicz zm. 1855
+1926
+xml
+text
+text
+2007-09-06
+
+
+
+
+Adam Mickiewicz
+Sonety odeskie
+Do M***
+
+Wiérsz napisany w roku 1822
+
+
+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.
+
+
+
+
diff --git a/apps/catalogue/tests/files/chunk2.xml b/apps/catalogue/tests/files/chunk2.xml
new file mode 100755
index 00000000..63a243e1
--- /dev/null
+++ b/apps/catalogue/tests/files/chunk2.xml
@@ -0,0 +1,11 @@
+
+
+
+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.
+
+
+
+
diff --git a/apps/catalogue/tests/files/expected.xml b/apps/catalogue/tests/files/expected.xml
new file mode 100755
index 00000000..ccbeefbb
--- /dev/null
+++ b/apps/catalogue/tests/files/expected.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+Mickiewicz, Adam
+Do M***
+Fundacja Nowoczesna Polska
+Romantyzm
+Liryka
+Wiersz
+
+http://wolnelektury.pl/katalog/lektura/sonety-odeskie-do-m
+http://www.polona.pl/Content/2222
+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
+
+Domena publiczna - Adam Mickiewicz zm. 1855
+1926
+xml
+text
+text
+2007-09-06
+
+
+
+
+Adam Mickiewicz
+Sonety odeskie
+Do M***
+
+Wiérsz napisany w roku 1822
+
+
+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.
+
+
+
+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.
+
+
+
+
diff --git a/apps/catalogue/urls.py b/apps/catalogue/urls.py
new file mode 100644
index 00000000..621eb12a
--- /dev/null
+++ b/apps/catalogue/urls.py
@@ -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[^/]+)/$', 'image', name="catalogue_image"),
+
+ url(r'^catalogue/$', 'document_list', name='catalogue_document_list'),
+ url(r'^user/$', 'my', name='catalogue_user'),
+ url(r'^user/(?P[^/]+)/$', 'user', name='catalogue_user'),
+ url(r'^users/$', 'users', name='catalogue_users'),
+ url(r'^activity/$', 'activity', name='catalogue_activity'),
+ url(r'^activity/(?P\d{4}-\d{2}-\d{2})/$',
+ 'activity', name='catalogue_activity'),
+
+ url(r'^upload/$',
+ 'upload', name='catalogue_upload'),
+
+ url(r'^create/(?P[^/]*)/',
+ 'create_missing', name='catalogue_create_missing'),
+ url(r'^create/',
+ 'create_missing', name='catalogue_create_missing'),
+
+ url(r'^book/(?P[^/]+)/publish$', 'publish', name="catalogue_publish"),
+ #url(r'^(?P[^/]+)/publish/(?P\d+)$', 'publish', name="catalogue_publish"),
+
+ url(r'^book/(?P[^/]+)/$', 'book', name="catalogue_book"),
+ url(r'^book/(?P[^/]+)/xml$', 'book_xml', name="catalogue_book_xml"),
+ url(r'^book/(?P[^/]+)/txt$', 'book_txt', name="catalogue_book_txt"),
+ url(r'^book/(?P[^/]+)/html$', 'book_html', name="catalogue_book_html"),
+ url(r'^book/(?P[^/]+)/epub$', 'book_epub', name="catalogue_book_epub"),
+ url(r'^book/(?P[^/]+)/pdf$', 'book_pdf', name="catalogue_book_pdf"),
+ url(r'^chunk_add/(?P[^/]+)/(?P[^/]+)/$',
+ 'chunk_add', name="catalogue_chunk_add"),
+ url(r'^chunk_edit/(?P[^/]+)/(?P[^/]+)/$',
+ 'chunk_edit', name="catalogue_chunk_edit"),
+ url(r'^book_append/(?P[^/]+)/$',
+ 'book_append', name="catalogue_book_append"),
+
+)
diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py
new file mode 100644
index 00000000..3c37ee60
--- /dev/null
+++ b/apps/catalogue/views.py
@@ -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
index 00000000..242714b6
--- /dev/null
+++ b/apps/catalogue/xml_tools.py
@@ -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("^$" % TRIM_BEGIN, re.M)
+RE_TRIM_END = re.compile("^$" % 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
diff --git a/apps/django_cas/backends.py b/apps/django_cas/backends.py
index d55c9db3..4cf9f625 100644
--- a/apps/django_cas/backends.py
+++ b/apps/django_cas/backends.py
@@ -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
index c81d3b7b..00000000
--- a/apps/dvcs/admin.py
+++ /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
index 00000000..4c3a1ffc
Binary files /dev/null and b/apps/dvcs/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/apps/dvcs/locale/pl/LC_MESSAGES/django.po b/apps/dvcs/locale/pl/LC_MESSAGES/django.po
new file mode 100644
index 00000000..64ddfd78
--- /dev/null
+++ b/apps/dvcs/locale/pl/LC_MESSAGES/django.po
@@ -0,0 +1,115 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-10-03 15:35+0200\n"
+"PO-Revision-Date: 2011-10-03 15:35+0100\n"
+"Last-Translator: Radek Czajka \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2)\n"
+
+#: models.py:19
+msgid "name"
+msgstr "nazwa"
+
+#: models.py:20
+msgid "slug"
+msgstr "slug"
+
+#: models.py:22
+msgid "ordering"
+msgstr "kolejnoÅÄ"
+
+#: models.py:29
+msgid "tag"
+msgstr "tag"
+
+#: models.py:30 models.py:196
+msgid "tags"
+msgstr "tagi"
+
+#: models.py:72
+msgid "author"
+msgstr "autor"
+
+#: models.py:73
+msgid "author name"
+msgstr "imiÄ i nazwisko autora"
+
+#: models.py:75 models.py:79
+msgid "Used if author is not set."
+msgstr "Używane, gdy nie jest ustawiony autor."
+
+#: models.py:77
+msgid "author email"
+msgstr "e-mail autora"
+
+#: models.py:81
+msgid "revision"
+msgstr "rewizja"
+
+#: models.py:85
+msgid "parent"
+msgstr "rodzic"
+
+#: models.py:90
+msgid "merge parent"
+msgstr "drugi rodzic"
+
+#: models.py:93
+msgid "description"
+msgstr "opis"
+
+#: models.py:96
+msgid "publishable"
+msgstr "do publikacji"
+
+#: models.py:102
+msgid "change"
+msgstr "zmiana"
+
+#: models.py:103
+msgid "changes"
+msgstr "zmiany"
+
+#: models.py:195
+msgid "document"
+msgstr "dokument"
+
+#: models.py:197
+msgid "data"
+msgstr "dane"
+
+#: models.py:211
+msgid "stage"
+msgstr "etap"
+
+#: models.py:219
+msgid "head"
+msgstr "gÅowica"
+
+#: models.py:220
+msgid "This document's current head."
+msgstr "Aktualna wersja dokumentu."
+
+#: models.py:224
+msgid "creator"
+msgstr "utworzyÅ"
+
+#: models.py:239
+msgid "user"
+msgstr "użytkownik"
+
+#: models.py:239
+msgid "Work assignment."
+msgstr "Przypisanie pracy użytkownikowi."
diff --git a/apps/dvcs/models.py b/apps/dvcs/models.py
index ea83ff07..d7816fa7 100644
--- a/apps/dvcs/models.py
+++ b/apps/dvcs/models.py
@@ -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
index 00000000..5da075be
--- /dev/null
+++ b/apps/dvcs/signals.py
@@ -0,0 +1,4 @@
+from django.dispatch import Signal
+
+post_commit = Signal()
+post_publishable = Signal(providing_args=['publishable'])
diff --git a/apps/dvcs/storage.py b/apps/dvcs/storage.py
new file mode 100755
index 00000000..6bb5b595
--- /dev/null
+++ b/apps/dvcs/storage.py
@@ -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
index 00000000..868f00a3
--- /dev/null
+++ b/apps/dvcs/tests/__init__.py
@@ -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
index 00000000..e69de29b
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
index 00000000..ed20bfb8
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
index 00000000..046b8835
--- /dev/null
+++ b/apps/email_mangler/locale/pl/LC_MESSAGES/django.po
@@ -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 , 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 \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
+
+#: 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
index 00000000..e69de29b
diff --git a/apps/email_mangler/templatetags/__init__.py b/apps/email_mangler/templatetags/__init__.py
new file mode 100755
index 00000000..e69de29b
diff --git a/apps/email_mangler/templatetags/email.py b/apps/email_mangler/templatetags/email.py
new file mode 100755
index 00000000..376117a8
--- /dev/null
+++ b/apps/email_mangler/templatetags/email.py
@@ -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("%(mangled)s" % {
+ 'name': name.encode('rot13'),
+ 'domain': domain.encode('rot13'),
+ 'mangled': mangled,
+ })
diff --git a/apps/filebrowser/templates/filebrowser/makedir.html b/apps/filebrowser/templates/filebrowser/makedir.html
index 2a4466f5..c0320df0 100644
--- a/apps/filebrowser/templates/filebrowser/makedir.html
+++ b/apps/filebrowser/templates/filebrowser/makedir.html
@@ -34,6 +34,7 @@
{% block content %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/apps/filebrowser/templates/filebrowser/rename.html b/apps/filebrowser/templates/filebrowser/rename.html
index 4c12830a..19e63f99 100644
--- a/apps/filebrowser/templates/filebrowser/rename.html
+++ b/apps/filebrowser/templates/filebrowser/rename.html
@@ -34,6 +34,7 @@
{% block content %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/apps/filebrowser/views.py b/apps/filebrowser/views.py
index 7c2967a4..6c1c92d6 100644
--- a/apps/filebrowser/views.py
+++ b/apps/filebrowser/views.py
@@ -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
index 21feb1f4..00000000
--- a/apps/toolbar/fixtures/initial_data.yaml
+++ /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 ---"], ["\\s*-+\\s*",
- "--- "]]}], ["lineregexp", {"exprs": [["^\\s+|\\s+$", ""],
- ["\\s+", " "], ["(,,)\\s+", "$1"], ["\\s+(\")", "$1"], ["([^\\.])(\\s*)AKT$1"],
- ["^SCENA(\\s\\w*)$", "SCENA$1"], ["([A-Z\u0104\u0106\u0118\u0141\u0143\u00d3\u015a\u017b\u0179]{2}[A-Z\u0104\u0106\u0118\u0141\u0143\u00d3\u015a\u017b\u0179\\s]+)$",
- "$1"]]}'
- 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\u2193"
- 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 ? '\\\
- 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' + 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
index 00000000..c2fb84d5
--- /dev/null
+++ b/apps/toolbar/fixtures/initial_toolbar.yaml
@@ -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 ---"], ["\\s*-+\\s*",
+ "--- "]]}], ["lineregexp", {"exprs": [["^\\s+|\\s+$", ""],
+ ["\\s+", " "], ["(,,)\\s+", "$1"], ["\\s+(\")", "$1"], ["([^\\.])(\\s*)AKT$1"],
+ ["^SCENA(\\s\\w*)$", "SCENA$1"], ["([A-Z\u0104\u0106\u0118\u0141\u0143\u00d3\u015a\u017b\u0179]{2}[A-Z\u0104\u0106\u0118\u0141\u0143\u00d3\u015a\u017b\u0179\\s]+)$",
+ "$1"]]}'
+ 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\u2193"
+ 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\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\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 ? '\\\
+ 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' + 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
index 00000000..b31f3809
--- /dev/null
+++ b/apps/toolbar/migrations/0005_initial_data.py
@@ -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']
diff --git a/apps/wiki/admin.py b/apps/wiki/admin.py
index 9c32b434..90da85e6 100644
--- a/apps/wiki/admin.py
+++ b/apps/wiki/admin.py
@@ -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
index 6781a48e..00000000
--- a/apps/wiki/constants.py
+++ /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)
diff --git a/apps/wiki/forms.py b/apps/wiki/forms.py
index d5c0ed54..3ef3ed14 100644
--- a/apps/wiki/forms.py
+++ b/apps/wiki/forms.py
@@ -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."),
)
diff --git a/apps/wiki/helpers.py b/apps/wiki/helpers.py
index f072ef91..dace3d00 100644
--- a/apps/wiki/helpers.py
+++ b/apps/wiki/helpers.py
@@ -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))
diff --git a/apps/wiki/locale/pl/LC_MESSAGES/django.mo b/apps/wiki/locale/pl/LC_MESSAGES/django.mo
index c334eadc..d886dcec 100644
Binary files a/apps/wiki/locale/pl/LC_MESSAGES/django.mo and b/apps/wiki/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/apps/wiki/locale/pl/LC_MESSAGES/django.po b/apps/wiki/locale/pl/LC_MESSAGES/django.po
index 568e8441..0182abed 100644
--- a/apps/wiki/locale/pl/LC_MESSAGES/django.po
+++ b/apps/wiki/locale/pl/LC_MESSAGES/django.po
@@ -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 \n"
"Language-Team: Fundacja Nowoczesna Polska \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 .xml
will be ignored."
-msgstr "ProszÄ wskazaÄ archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie koÅczÄ
ce siÄ na .xml
zostanÄ
zignorowane."
-
-#: templates/wiki/document_upload.html: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 .xml
extension"
-msgstr "Pliki pominiÄte z powodu braku rozszerzenia .xml
."
-
-#: 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 "
+#~ ".xml
will be ignored."
+#~ msgstr ""
+#~ "ProszÄ wskazaÄ archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie "
+#~ "koÅczÄ
ce siÄ na .xml
zostanÄ
zignorowane."
+
+#~ 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 .xml
extension"
+#~ msgstr "Pliki pominiÄte z powodu braku rozszerzenia .xml
."
+
+#~ 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"
diff --git a/apps/wiki/models.py b/apps/wiki/models.py
index ec9ded50..c539908d 100644
--- a/apps/wiki/models.py
+++ b/apps/wiki/models.py
@@ -4,151 +4,12 @@
# 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"", 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*', 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)
diff --git a/apps/wiki/settings.py b/apps/wiki/settings.py
index 0a227e4a..50f49d8b 100644
--- a/apps/wiki/settings.py
+++ b/apps/wiki/settings.py
@@ -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
index 00000000..1a74c7b9
--- /dev/null
+++ b/apps/wiki/templates/admin/wiki/theme/change_list.html
@@ -0,0 +1,28 @@
+{% extends "admin/change_list.html" %}
+
+{% block extrahead %}
+{{ block.super }}
+
+{% endblock %}
+
+
+
+{% block pretitle %}
+
+
+â {% trans "Table for Redmine wiki" %} â
+
+ |{% for theme in cl.get_query_set %}[[{{ theme }}]]|{% if forloop.counter|divisibleby:7 %}
+ |{% endif %}{% endfor %}
+
+
+{{ block.super }}
+{% endblock %}
diff --git a/apps/wiki/templates/wiki/base.html b/apps/wiki/templates/wiki/base.html
deleted file mode 100644
index f88fac31..00000000
--- a/apps/wiki/templates/wiki/base.html
+++ /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 %}
-{% trans "Platforma Redakcyjna" %}
-
- {% block leftcolumn %}
- {% endblock leftcolumn %}
-
-
- {% block rightcolumn %}
- {% endblock rightcolumn %}
-
-{% 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
index 351e87a2..00000000
--- a/apps/wiki/templates/wiki/document_create_missing.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "wiki/base.html" %}
-{% load i18n %}
-
-{% block leftcolumn %}
-
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-{% endblock rightcolumn %}
\ No newline at end of file
diff --git a/apps/wiki/templates/wiki/document_details.html b/apps/wiki/templates/wiki/document_details.html
index bdb22007..db003d2c 100644
--- a/apps/wiki/templates/wiki/document_details.html
+++ b/apps/wiki/templates/wiki/document_details.html
@@ -11,14 +11,14 @@
{% 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 %}
@@ -40,6 +40,10 @@
{% 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 %}
diff --git a/apps/wiki/templates/wiki/document_details_base.html b/apps/wiki/templates/wiki/document_details_base.html
index 03231330..dbbe7a10 100644
--- a/apps/wiki/templates/wiki/document_details_base.html
+++ b/apps/wiki/templates/wiki/document_details_base.html
@@ -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' %}
@@ -16,21 +16,18 @@
{% block maincontent %}
+ data-chunk-id="{{ chunk.pk }}" style="display:none">
- {% for k, v in document_meta.items %}
- {{ v }}
- {% endfor %}
-
- {% for k, v in document_info.items %}
- {{ v }}
- {% endfor %}
+ {{ chunk.book.gallery }}
+ {% if chunk.gallery_start %}{{ chunk.gallery_start }}{% endif %}
+ {{ revision }}
+ {{ request.GET.diff }}
{% block meta-extra %} {% endblock %}