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 %} +
+ {% csrf_token %} + {{ form.as_p }} + +

+
+{% 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 %}
{% csrf_token %}{% endif %} + + {{ form.as_table }} + {% if editable %} + + {% endif %} +
+{% if editable %}
{% endif %} + + +{% if editable %} +

{% trans "Append to other book" %}

+{% endif %} + + +
+ +

{% trans "Chunks" %}

+ + + {% for chunk in book %} + {{ chunk.short_html|safe }} + {% endfor %} +
+ +
+ + + +
+ + +

{% trans "Publication" %}

+ +

{% trans "Last published" %}: + {% if book.last_published %} + {{ book.last_published }} + {% else %} + — + {% endif %} +

+ +{% if publishable %} +

+ {% trans "Full XML" %}
+ {% trans "HTML version" %}
+ {% trans "TXT version" %}
+ {% trans "PDF version" %}
+ {% trans "EPUB version" %}
+

+ + {% if user.is_authenticated %} + +
{% csrf_token %} + + + +
+ {% else %} + {% trans "Log in to publish." %} + {% endif %} +{% else %} +

{% trans "This book can't be published yet, because:" %}

+
  • {{ publishable_error }}
+{% endif %} + +
+ + + +
+

{% 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 %} +
+ {% csrf_token %} + {{ form.as_p }} + +

+
+{% 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 viewed_user %} + +{% endif %} + + +
+ + + + + + + + + {% if not viewed_user %} + + {% endif %} + + + + + + {% with cnt=books|length %} + {% autopaginate books 100 %} + + {% for item in books %} + {% with item.book as book %} + {{ book.short_html|safe }} + {% if not book.single %} + {% for chunk in item.chunks %} + {{ chunk.short_html|safe }} + {% endfor %} + {% endif %} + {% endwith %} + {% endfor %} + + + {% endwith %} +
+ + +
+ +
+
+ {% paginate %} + {% blocktrans count c=cnt %}{{c}} book{% plural %}{{c}} books{% endblocktrans %}
+{% 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" %}

+ +
+ {% csrf_token %} + + + + {{ form.as_table }} + +
{% trans "Insert empty chunk after" %}:{{ chunk.pretty_name }}
+
+{% 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" %}

+ +
+ {% csrf_token %} + + + {{ form.as_table}} + +
{% trans "Book" %}:{{ chunk.book }} ({{ chunk.number }}/{{ chunk.book|length }})
+ +
+ + +

{% 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" %}

+ +
+ {% csrf_token %} + + {{ form.as_table}} + +
+
+{% 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." %} +

+ +
+{% csrf_token %} +{{ form.as_p }} +

+
+ +
+ +{% 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 %} + +
+ + + + + + + + {% if not viewed_user %} + + {% endif %} + + + + + + {% with cnt=objects|length %} + {% autopaginate objects 100 %} + + {% for item in objects %} + {{ item.short_html|safe }} + {% endfor %} + + + {% endwith %} +
+
+ +
+
+ {% paginate %} + {% blocktrans count c=cnt %}{{c}} image{% plural %}{{c}} images{% endblocktrans %}
+{% 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 %} +
  1. {{ item.title }}
    ({{ item.time|date:"H:i:s, d/m/Y" }})
  2. + {% 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" %}

+ +
+{% csrf_token %} +{{ form.as_p }} +

+
+ + +{% 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 %} + +
    +{% for item in wall %} +
  • +
    + {% if item.get_email %} + {% gravatar_img_for_email item.get_email 32 %} +
    + {% endif %} +
    + +
    {{ item.timestamp }}
    +

    {{ item.header }}

    + {{ item.title }} +
    {% trans "user" %}: + {% if item.user %} + + {{ item.user.first_name }} {{ item.user.last_name }} + <{{ item.user.email|email_link }}> + {% else %} + {{ item.user_name }} + {% if item.email %} + <{{ item.email|email_link }}> + {% endif %} + ({% trans "not logged in" %}) + {% endif %} +
    {{ item.summary|linebreaksbr }} +
  • +{% empty %} +
  • {% trans "No activity recorded." %}
  • +{% endfor %} +
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 %}
+ {% csrf_token %}
{% if form.errors %}

{% trans 'Please correct the following errors.' %}

{% endif %}
@@ -59,4 +60,4 @@
-{% 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 %}
+ {% csrf_token %}
{% if form.errors %}

{% trans 'Please correct the following errors.' %}

{% endif %}
@@ -60,4 +61,4 @@
-{% 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 = '';\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 = '';\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" %} ↓ + + +{{ 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 %} -
- {{ form.as_p }} - -

-
-{% 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 %}