From: Radek Czajka Date: Tue, 4 Oct 2011 21:49:23 +0000 (+0200) Subject: Merge branch 'with-dvcs' X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/ce8d791a5298e0cb2569034aec4c8b57afac97b2?hp=36139e06f33f12a03c270d8e091fe0e8fc6f8077 Merge branch 'with-dvcs' --- 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..7913ac3c --- /dev/null +++ b/apps/apiclient/__init__.py @@ -0,0 +1,46 @@ +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): + # what if not verified? + 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: + resp, content = client.request( + "%s%s.json" % (WL_API_URL, path), + method="POST", + body=urllib.urlencode(data)) + else: + resp, content = client.request( + "%s%s.json" % (WL_API_URL, path)) + status = resp['status'] + if status == '200': + return simplejson.loads(content) + elif status.startswith('2'): + return + 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..5e54965e --- /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='users_oauth'), + url(r'^oauth_callback/$', 'oauth_callback', name='users_oauth_callback'), +) diff --git a/apps/apiclient/views.py b/apps/apiclient/views.py new file mode 100644 index 00000000..f8515904 --- /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("users_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..70e20d20 --- /dev/null +++ b/apps/catalogue/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin + +from catalogue import models + +class BookAdmin(admin.ModelAdmin): + prepopulated_fields = {'slug': ['title']} + + +admin.site.register(models.Book, BookAdmin) +admin.site.register(models.Chunk) + +admin.site.register(models.Chunk.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/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..f6b2dc98 --- /dev/null +++ b/apps/catalogue/forms.py @@ -0,0 +1,126 @@ +# -*- 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.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 = ['gallery', 'parent', 'parent_number'] + prepopulated_fields = {'slug': ['title']} + + 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"]: + raise forms.ValidationError("You must either enter text or upload a file") + + return self.cleaned_data + + +class DocumentsUploadForm(forms.Form): + """ + Form used for uploading new documents. + """ + file = forms.FileField(required=True, label=_('ZIP file')) + 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) + + + class Meta: + model = Chunk + exclude = ['number'] + + 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")) + + +class BookForm(forms.ModelForm): + """ + Form used for editing a Book. + """ + + class Meta: + model = Book + + +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..7bc24819 --- /dev/null +++ b/apps/catalogue/helpers.py @@ -0,0 +1,30 @@ +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 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..4eb72719 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..d7566e38 --- /dev/null +++ b/apps/catalogue/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,570 @@ +# 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-10-03 15:56+0200\n" +"PO-Revision-Date: 2011-10-03 15:56+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:46 +msgid "ZIP file" +msgstr "Plik ZIP" + +#: forms.py:47 +msgid "Directories are documents in chunks" +msgstr "Katalogi zawierają dokumenty w częściach" + +#: forms.py:85 +#: forms.py:99 +msgid "Chunk with this slug already exists" +msgstr "Część z tym slugiem już istnieje" + +#: forms.py:109 +msgid "Append to" +msgstr "Dołącz do" + +#: views.py:137 +#, python-format +msgid "Slug already used for %s" +msgstr "Slug taki sam jak dla pliku %s" + +#: views.py:139 +msgid "Slug already used in repository." +msgstr "Dokument o tym slugu już istnieje w repozytorium." + +#: views.py:145 +msgid "File should be UTF-8 encoded." +msgstr "Plik powinien mieć kodowanie UTF-8." + +#: models/book.py:20 +#: models/chunk.py:24 +msgid "title" +msgstr "tytuł" + +#: models/book.py:21 +#: models/chunk.py:23 +msgid "slug" +msgstr "slug" + +#: models/book.py:22 +msgid "scan gallery name" +msgstr "nazwa galerii skanów" + +#: models/book.py:25 +msgid "parent" +msgstr "rodzic" + +#: models/book.py:26 +msgid "parent number" +msgstr "numeracja rodzica" + +#: models/book.py:43 +#: models/chunk.py:21 +#: models/publish_log.py:17 +msgid "book" +msgstr "książka" + +#: models/book.py:44 +msgid "books" +msgstr "książki" + +#: models/book.py:45 +msgid "Can mark for publishing" +msgstr "Oznacza do publikacji" + +#: models/chunk.py:22 +msgid "number" +msgstr "numer" + +#: models/chunk.py:39 +msgid "chunk" +msgstr "część" + +#: models/chunk.py:40 +msgid "chunks" +msgstr "części" + +#: models/publish_log.py:18 +msgid "time" +msgstr "czas" + +#: models/publish_log.py:19 +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/base.html:9 +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:6 +#: templates/catalogue/book_detail.html:46 +msgid "edit" +msgstr "edytuj" + +#: templates/catalogue/book_detail.html:16 +msgid "add basic document structure" +msgstr "dodaj podstawową strukturę dokumentu" + +#: templates/catalogue/book_detail.html:20 +msgid "change master tag to" +msgstr "zmień tak master na" + +#: templates/catalogue/book_detail.html:24 +msgid "add begin trimming tag" +msgstr "dodaj początkowy ogranicznik" + +#: templates/catalogue/book_detail.html:28 +msgid "add end trimming tag" +msgstr "dodaj końcowy ogranicznik" + +#: templates/catalogue/book_detail.html:34 +msgid "unstructured text" +msgstr "tekst bez struktury" + +#: templates/catalogue/book_detail.html:38 +msgid "unknown XML" +msgstr "nieznany XML" + +#: templates/catalogue/book_detail.html:42 +msgid "broken document" +msgstr "uszkodzony dokument" + +#: templates/catalogue/book_detail.html:61 +msgid "Apply fixes" +msgstr "Wykonaj zmiany" + +#: templates/catalogue/book_detail.html:67 +msgid "Append to other book" +msgstr "Dołącz do innej książki" + +#: templates/catalogue/book_detail.html:69 +msgid "Last published" +msgstr "Ostatnio opublikowano" + +#: templates/catalogue/book_detail.html:73 +msgid "Full XML" +msgstr "Pełny XML" + +#: templates/catalogue/book_detail.html:74 +msgid "HTML version" +msgstr "Wersja HTML" + +#: templates/catalogue/book_detail.html:75 +msgid "TXT version" +msgstr "Wersja TXT" + +#: templates/catalogue/book_detail.html:92 +msgid "Publish" +msgstr "Opublikuj" + +#: templates/catalogue/book_detail.html:96 +msgid "This book cannot be published yet" +msgstr "Ta książka nie może jeszcze zostać opublikowana" + +#: templates/catalogue/book_edit.html:9 +#: templates/catalogue/chunk_edit.html:9 +msgid "Save" +msgstr "Zapisz" + +#: templates/catalogue/chunk_add.html:9 +msgid "Add chunk" +msgstr "Dodaj część" + +#: templates/catalogue/document_create_missing.html:9 +msgid "Create document" +msgstr "Utwórz dokument" + +#: 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 +#: templatetags/catalogue.py:34 +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/user_list.html:7 +#: templatetags/catalogue.py:32 +msgid "Users" +msgstr "Użytkownicy" + +#: 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.html:7 +#: templates/catalogue/book_list/chunk.html:5 +msgid "Chunk settings" +msgstr "Ustawienia części" + +#: 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:81 +msgid "publishable" +msgstr "do publikacji" + +#: templatetags/book_list.py:82 +msgid "changed" +msgstr "zmienione" + +#: templatetags/book_list.py:83 +msgid "published" +msgstr "opublikowane" + +#: templatetags/book_list.py:84 +msgid "unpublished" +msgstr "nie opublikowane" + +#: templatetags/book_list.py:85 +msgid "empty" +msgstr "puste" + +#: templatetags/catalogue.py:28 +msgid "My page" +msgstr "Moja strona" + +#: templatetags/catalogue.py:30 +msgid "Activity" +msgstr "Aktywność" + +#: templatetags/catalogue.py:31 +msgid "All" +msgstr "Wszystkie" + +#: templatetags/catalogue.py:33 +msgid "Add" +msgstr "Dodaj" + +#: templatetags/catalogue.py:37 +msgid "Admin" +msgstr "Administracja" + +#: templatetags/wall.py:43 +msgid "Related edit" +msgstr "Powiązana zmiana" + +#: templatetags/wall.py:45 +msgid "Edit" +msgstr "Zmiana" + +#: templatetags/wall.py:67 +msgid "Publication" +msgstr "Publikacja" + +#: templatetags/wall.py:84 +msgid "Comment" +msgstr "Komentarz" + +#~ 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 "EPUB version" +#~ msgstr "Wersja EPUB" + +#~ msgid "PDF version" +#~ msgstr "Wersja PDF" + +#~ msgid "Old version" +#~ msgstr "Stara wersja" + +#~ msgid "New version" +#~ msgstr "Nowa wersja" + +#~ msgid "Click to open/close gallery" +#~ msgstr "Kliknij, aby (ro)zwinąć galerię" + +#~ msgid "Help" +#~ msgstr "Pomoc" + +#~ msgid "Version" +#~ msgstr "Wersja" + +#~ msgid "Unknown" +#~ msgstr "nieznana" + +#~ msgid "Save attempt in progress" +#~ msgstr "Trwa zapisywanie" + +#~ msgid "There is a newer version of this document!" +#~ msgstr "Istnieje nowsza wersja tego dokumentu!" + +#~ msgid "Clear filter" +#~ msgstr "Wyczyść filtr" + +#~ msgid "Cancel" +#~ msgstr "Anuluj" + +#~ msgid "Revert" +#~ msgstr "Przywróć" + +#~ msgid "all" +#~ msgstr "wszystkie" + +#~ msgid "Annotations" +#~ msgstr "Przypisy" + +#~ msgid "Previous" +#~ msgstr "Poprzednie" + +#~ msgid "Next" +#~ msgstr "Następne" + +#~ msgid "Zoom in" +#~ msgstr "Powiększ" + +#~ msgid "Zoom out" +#~ msgstr "Zmniejsz" + +#~ msgid "Gallery" +#~ msgstr "Galeria" + +#~ msgid "Compare versions" +#~ msgstr "Porównaj wersje" + +#~ msgid "Revert document" +#~ msgstr "Przywróć wersję" + +#~ msgid "View version" +#~ msgstr "Zobacz wersję" + +#~ msgid "History" +#~ msgstr "Historia" + +#~ msgid "Search" +#~ msgstr "Szukaj" + +#~ msgid "Replace with" +#~ msgstr "Zamień na" + +#~ msgid "Replace" +#~ msgstr "Zamień" + +#~ msgid "Options" +#~ msgstr "Opcje" + +#~ msgid "Case sensitive" +#~ msgstr "Rozróżniaj wielkość liter" + +#~ msgid "From cursor" +#~ msgstr "Zacznij od kursora" + +#~ msgid "Search and replace" +#~ msgstr "Znajdź i zamień" + +#~ msgid "Source code" +#~ msgstr "Kod źródłowy" + +#~ msgid "Title" +#~ msgstr "Tytuł" + +#~ msgid "Document ID" +#~ msgstr "ID dokumentu" + +#~ msgid "Current version" +#~ msgstr "Aktualna wersja" + +#~ msgid "Last edited by" +#~ msgstr "Ostatnio edytowane przez" + +#~ msgid "Link to gallery" +#~ msgstr "Link do galerii" + +#~ msgid "Summary" +#~ msgstr "Podsumowanie" + +#~ msgid "Insert theme" +#~ msgstr "Wstaw motyw" + +#~ msgid "Insert annotation" +#~ msgstr "Wstaw przypis" + +#~ msgid "Visual editor" +#~ msgstr "Edytor wizualny" + +#~ msgid "Assigned to me" +#~ msgstr "Przypisane do mnie" + +#~ msgid "Unassigned" +#~ msgstr "Nie przypisane" + +#~ msgid "First correction" +#~ msgstr "Autokorekta" + +#~ msgid "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/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/import_wl.py b/apps/catalogue/management/commands/import_wl.py new file mode 100755 index 00000000..4f15cef8 --- /dev/null +++ b/apps/catalogue/management/commands/import_wl.py @@ -0,0 +1,105 @@ +# -*- 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) + + #~ conflicts = [] + #~ for slug, book_list in slugs.items(): + #~ if len(book_list) > 1: + #~ conflicts.append((slug, book_list)) + #~ if conflicts: + #~ print self.style.ERROR("There is more than one book " + #~ "with the same slug in dc:url. " + #~ "Merge or hide them before proceeding.") + #~ for slug, book_list in sorted(conflicts): + #~ print slug + #~ print "\n".join(b.slug for b in book_list) + #~ print + #~ return + + book_count = 0 + commit_args = { + "author_name": 'Platforma', + "description": 'Automatycznie zaimportowane z Wolnych Lektur', + "publishable": True, + } + + if verbose: + print 'Opening books list' + for book in json.load(urllib2.urlopen(WL_API))[:10]: + book_detail = json.load(urllib2.urlopen(book['href'])) + xml_text = urllib2.urlopen(book_detail['xml']).read() + info = BookInfo.from_string(xml_text) + 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, + slug=info.slug, 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/__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..d9a11dde --- /dev/null +++ b/apps/catalogue/models/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# +# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from catalogue.models.chunk import Chunk +from catalogue.models.publish_log import BookPublishRecord, ChunkPublishRecord +from catalogue.models.book import Book +from catalogue.models.listeners import * diff --git a/apps/catalogue/models/book.py b/apps/catalogue/models/book.py new file mode 100755 index 00000000..1b043a47 --- /dev/null +++ b/apps/catalogue/models/book.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- +# +# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django.db import models +from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ +from slughifi import slughifi +from catalogue.helpers import cached_in_field +from catalogue.models import BookPublishRecord, ChunkPublishRecord +from catalogue.signals import post_publish +from catalogue.tasks import refresh_instance +from catalogue.xml_tools import compile_text, split_xml + + +class Book(models.Model): + """ A document edited on the wiki """ + + title = models.CharField(_('title'), max_length=255, db_index=True) + slug = models.SlugField(_('slug'), max_length=128, unique=True, db_index=True) + gallery = models.CharField(_('scan gallery name'), max_length=255, blank=True) + + #wl_slug = models.CharField(_('title'), max_length=255, null=True, db_index=True, editable=False) + parent = models.ForeignKey('self', null=True, blank=True, verbose_name=_('parent'), related_name="children") + parent_number = models.IntegerField(_('parent number'), null=True, blank=True, db_index=True) + + # Cache + _short_html = models.TextField(null=True, blank=True, editable=False) + _single = models.NullBooleanField(editable=False, db_index=True) + _new_publishable = models.NullBooleanField(editable=False) + _published = models.NullBooleanField(editable=False) + + # Managers + objects = models.Manager() + + class NoTextError(BaseException): + pass + + class Meta: + app_label = 'catalogue' + ordering = ['parent_number', 'title'] + verbose_name = _('book') + verbose_name_plural = _('books') + permissions = [('can_pubmark', 'Can mark for publishing')] + + + # Representing + # ============ + + def __iter__(self): + return iter(self.chunk_set.all()) + + def __getitem__(self, chunk): + return self.chunk_set.all()[chunk] + + def __len__(self): + return self.chunk_set.count() + + def __nonzero__(self): + """ + Necessary so that __len__ isn't used for bool evaluation. + """ + return True + + def __unicode__(self): + return self.title + + @models.permalink + def get_absolute_url(self): + return ("catalogue_book", [self.slug]) + + + # Creating & manipulating + # ======================= + + @classmethod + def create(cls, creator, text, *args, **kwargs): + b = cls.objects.create(*args, **kwargs) + b.chunk_set.all().update(creator=creator) + b[0].commit(text, author=creator) + return b + + def add(self, *args, **kwargs): + """Add a new chunk at the end.""" + return self.chunk_set.reverse()[0].split(*args, **kwargs) + + @classmethod + def import_xml_text(cls, text=u'', previous_book=None, + commit_args=None, **kwargs): + """Imports a book from XML, splitting it into chunks as necessary.""" + texts = split_xml(text) + if previous_book: + instance = previous_book + else: + instance = cls(**kwargs) + instance.save() + + # if there are more parts, set the rest to empty strings + book_len = len(instance) + for i in range(book_len - len(texts)): + texts.append(u'pusta część %d' % (i + 1), u'') + + i = 0 + for i, (title, text) in enumerate(texts): + if not title: + title = u'część %d' % (i + 1) + + slug = slughifi(title) + + if i < book_len: + chunk = instance[i] + chunk.slug = slug + chunk.title = title + chunk.save() + else: + chunk = instance.add(slug, title, adjust_slug=True) + + chunk.commit(text, **commit_args) + + return instance + + def make_chunk_slug(self, proposed): + """ + Finds a chunk slug not yet used in the book. + """ + slugs = set(c.slug for c in self) + i = 1 + new_slug = proposed + while new_slug in slugs: + new_slug = "%s_%d" % (proposed, i) + i += 1 + return new_slug + + def append(self, other, slugs=None, titles=None): + """Add all chunks of another book to self.""" + number = self[len(self) - 1].number + 1 + len_other = len(other) + single = len_other == 1 + + if slugs is not None: + assert len(slugs) == len_other + if titles is not None: + assert len(titles) == len_other + if slugs is None: + slugs = [slughifi(t) for t in titles] + + for i, chunk in enumerate(other): + # move chunk to new book + chunk.book = self + chunk.number = number + + if titles is None: + # try some title guessing + if other.title.startswith(self.title): + other_title_part = other.title[len(self.title):].lstrip(' /') + else: + other_title_part = other.title + + if single: + # special treatment for appending one-parters: + # just use the guessed title and original book slug + chunk.title = other_title_part + if other.slug.startswith(self.slug): + chunk_slug = other.slug[len(self.slug):].lstrip('-_') + else: + chunk_slug = other.slug + chunk.slug = self.make_chunk_slug(chunk_slug) + else: + chunk.title = "%s, %s" % (other_title_part, chunk.title) + else: + chunk.slug = slugs[i] + chunk.title = titles[i] + + chunk.slug = self.make_chunk_slug(chunk.slug) + chunk.save() + number += 1 + other.delete() + + + # State & cache + # ============= + + def last_published(self): + try: + return self.publish_log.all()[0].timestamp + except IndexError: + return None + + def publishable(self): + if not self.chunk_set.exists(): + return False + for chunk in self: + if not chunk.publishable(): + return False + return True + + def hidden(self): + return self.slug.startswith('.') + + def is_new_publishable(self): + """Checks if book is ready for publishing. + + Returns True if there is a publishable version newer than the one + already published. + + """ + new_publishable = False + if not self.chunk_set.exists(): + return False + for chunk in self: + change = chunk.publishable() + if not change: + return False + if not new_publishable and not change.publish_log.exists(): + new_publishable = True + return new_publishable + new_publishable = cached_in_field('_new_publishable')(is_new_publishable) + + def is_published(self): + return self.publish_log.exists() + published = cached_in_field('_published')(is_published) + + def is_single(self): + return len(self) == 1 + single = cached_in_field('_single')(is_single) + + @cached_in_field('_short_html') + def short_html(self): + return render_to_string('catalogue/book_list/book.html', {'book': self}) + + def touch(self): + update = { + "_new_publishable": self.is_new_publishable(), + "_published": self.is_published(), + "_single": self.is_single(), + "_short_html": None, + } + Book.objects.filter(pk=self.pk).update(**update) + refresh_instance(self) + + def refresh(self): + """This should be done offline.""" + self.short_html + self.single + self.new_publishable + self.published + + # Materializing & publishing + # ========================== + + def get_current_changes(self, publishable=True): + """ + Returns a list containing one Change for every Chunk in the Book. + Takes the most recent revision (publishable, if set). + Throws an error, if a proper revision is unavailable for a Chunk. + """ + if publishable: + changes = [chunk.publishable() for chunk in self] + else: + changes = [chunk.head for chunk in self if chunk.head is not None] + if None in changes: + raise self.NoTextError('Some chunks have no available text.') + return changes + + def materialize(self, publishable=False, changes=None): + """ + Get full text of the document compiled from chunks. + Takes the current versions of all texts + or versions most recently tagged for publishing, + or a specified iterable changes. + """ + if changes is None: + changes = self.get_current_changes(publishable) + return compile_text(change.materialize() for change in changes) + + def publish(self, user): + """ + Publishes a book on behalf of a (local) user. + """ + raise NotImplementedError("Publishing not possible yet.") + + from apiclient import api_call + + changes = self.get_current_changes(publishable=True) + book_xml = self.materialize(changes=changes) + #api_call(user, "books", {"book_xml": book_xml}) + # record the publish + br = BookPublishRecord.objects.create(book=self, user=user) + for c in changes: + ChunkPublishRecord.objects.create(book_record=br, change=c) + post_publish.send(sender=br) diff --git a/apps/catalogue/models/chunk.py b/apps/catalogue/models/chunk.py new file mode 100755 index 00000000..e68b1c17 --- /dev/null +++ b/apps/catalogue/models/chunk.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# +# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django.conf import settings +from django.db import models +from django.db.utils import IntegrityError +from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ +from catalogue.helpers import cached_in_field +from catalogue.managers import VisibleManager +from catalogue.tasks import refresh_instance +from dvcs import models as dvcs_models + + +class Chunk(dvcs_models.Document): + """ An editable chunk of text. Every Book text is divided into chunks. """ + REPO_PATH = settings.CATALOGUE_REPO_PATH + + book = models.ForeignKey('Book', editable=False, verbose_name=_('book')) + number = models.IntegerField(_('number')) + slug = models.SlugField(_('slug')) + title = models.CharField(_('title'), max_length=255, blank=True) + + # cache + _short_html = models.TextField(null=True, blank=True, editable=False) + _hidden = models.NullBooleanField(editable=False) + _changed = models.NullBooleanField(editable=False) + + # managers + objects = models.Manager() + visible_objects = VisibleManager() + + class Meta: + app_label = 'catalogue' + unique_together = [['book', 'number'], ['book', 'slug']] + ordering = ['number'] + verbose_name = _('chunk') + verbose_name_plural = _('chunks') + + # Representing + # ============ + + def __unicode__(self): + return "%d:%d: %s" % (self.book_id, self.number, self.title) + + @models.permalink + def get_absolute_url(self): + return ("wiki_editor", [self.book.slug, self.slug]) + + def pretty_name(self, book_length=None): + title = self.book.title + if self.title: + title += ", %s" % self.title + if book_length > 1: + title += " (%d/%d)" % (self.number, book_length) + return title + + + # Creating and manipulation + # ========================= + + def split(self, slug, title='', adjust_slug=False, **kwargs): + """ Create an empty chunk after this one """ + self.book.chunk_set.filter(number__gt=self.number).update( + number=models.F('number')+1) + new_chunk = None + while not new_chunk: + new_slug = self.book.make_chunk_slug(slug) + try: + new_chunk = self.book.chunk_set.create(number=self.number+1, + slug=new_slug, title=title, **kwargs) + except IntegrityError: + pass + return new_chunk + + @classmethod + def get(cls, book_slug, chunk_slug=None): + if chunk_slug is None: + return cls.objects.get(book__slug=book_slug, number=1) + else: + return cls.objects.get(book__slug=book_slug, slug=chunk_slug) + + + # State & cache + # ============= + + def new_publishable(self): + change = self.publishable() + if not change: + return False + return change.publish_log.exists() + + def is_changed(self): + if self.head is None: + return False + return not self.head.publishable + changed = cached_in_field('_changed')(is_changed) + + def is_hidden(self): + return self.book.hidden() + hidden = cached_in_field('_hidden')(is_hidden) + + @cached_in_field('_short_html') + def short_html(self): + return render_to_string( + 'catalogue/book_list/chunk.html', {'chunk': self}) + + def touch(self): + update = { + "_changed": self.is_changed(), + "_hidden": self.is_hidden(), + "_short_html": None, + } + Chunk.objects.filter(pk=self.pk).update(**update) + refresh_instance(self) + + def refresh(self): + """This should be done offline.""" + self.changed + self.hidden + self.short_html diff --git a/apps/catalogue/models/listeners.py b/apps/catalogue/models/listeners.py new file mode 100755 index 00000000..7848974f --- /dev/null +++ b/apps/catalogue/models/listeners.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# +# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django.contrib.auth.models import User +from django.db import models +from catalogue.models import Book, Chunk +from catalogue.signals import post_publish +from dvcs.signals import post_publishable + + +def book_changed(sender, instance, created, **kwargs): + instance.touch() + for c in instance: + c.touch() +models.signals.post_save.connect(book_changed, sender=Book) + + +def chunk_changed(sender, instance, created, **kwargs): + instance.book.touch() + instance.touch() +models.signals.post_save.connect(chunk_changed, sender=Chunk) + + +def user_changed(sender, instance, *args, **kwargs): + books = set() + for c in instance.chunk_set.all(): + books.add(c.book) + c.touch() + for b in books: + b.touch() +models.signals.post_save.connect(user_changed, sender=User) + + +def publish_listener(sender, *args, **kwargs): + sender.book.touch() + for c in sender.book: + c.touch() +post_publish.connect(publish_listener) + + +def listener_create(sender, instance, created, **kwargs): + if created: + instance.chunk_set.create(number=1, slug='1') +models.signals.post_save.connect(listener_create, sender=Book) + + diff --git a/apps/catalogue/models/publish_log.py b/apps/catalogue/models/publish_log.py new file mode 100755 index 00000000..f422e377 --- /dev/null +++ b/apps/catalogue/models/publish_log.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django.contrib.auth.models import User +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from catalogue.models import Chunk + + +class BookPublishRecord(models.Model): + """ + A record left after publishing a Book. + """ + + book = models.ForeignKey('Book', verbose_name=_('book'), related_name='publish_log') + timestamp = models.DateTimeField(_('time'), auto_now_add=True) + user = models.ForeignKey(User, verbose_name=_('user')) + + class Meta: + app_label = 'catalogue' + ordering = ['-timestamp'] + verbose_name = _('book publish record') + verbose_name = _('book publish records') + + +class ChunkPublishRecord(models.Model): + """ + BookPublishRecord details for each Chunk. + """ + + book_record = models.ForeignKey(BookPublishRecord, verbose_name=_('book publish record')) + change = models.ForeignKey(Chunk.change_model, related_name='publish_log', verbose_name=_('change')) + + class Meta: + app_label = 'catalogue' + verbose_name = _('chunk publish record') + verbose_name = _('chunk publish records') diff --git a/apps/catalogue/signals.py b/apps/catalogue/signals.py new file mode 100644 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..e9b8cf9b --- /dev/null +++ b/apps/catalogue/tasks.py @@ -0,0 +1,11 @@ +from celery.task import task + + +@task +def refresh_by_pk(cls, pk): + cls._default_manager.get(pk=pk).refresh() + + +def refresh_instance(instance): + refresh_by_pk.delay(type(instance), instance.pk) + diff --git a/apps/catalogue/templates/catalogue/activity.html b/apps/catalogue/templates/catalogue/activity.html new file mode 100755 index 00000000..4354b337 --- /dev/null +++ b/apps/catalogue/templates/catalogue/activity.html @@ -0,0 +1,7 @@ +{% extends "catalogue/base.html" %} + +{% load wall %} + +{% block leftcolumn %} + {% wall %} +{% endblock leftcolumn %} diff --git a/apps/catalogue/templates/catalogue/base.html b/apps/catalogue/templates/catalogue/base.html new file mode 100644 index 00000000..9b32fe7e --- /dev/null +++ b/apps/catalogue/templates/catalogue/base.html @@ -0,0 +1,51 @@ +{% 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..8e9c1f10 --- /dev/null +++ b/apps/catalogue/templates/catalogue/book_detail.html @@ -0,0 +1,109 @@ +{% extends "catalogue/base.html" %} +{% load comments i18n %} + +{% block leftcolumn %} + +{% trans "edit" %} +

{{ book.title }}

+ + + {% for c in chunks %} + + + + + + + + + {% endfor %} + {% if need_fixing %} + + {% endif %} +
{{ c.chunk.title }}{% for fix in c.fix %} + + {% ifequal fix "wl" %}</>{% endifequal %} + + {% ifequal fix "bad-master" %}master{% endifequal %} + + {% ifequal fix "trim-begin" %}{% endifequal %} + + {% ifequal fix "trim-end" %}{% endifequal %} + + {% endfor %} + + {% ifequal c.grade "plain" %} + {% trans "unstructured text" %} + {% endifequal %} + + {% ifequal c.grade "xml" %} + {% trans "unknown XML" %} + {% endifequal %} + + {% ifequal c.grade "wl-broken" %} + {% trans "broken document" %} + {% endifequal %} + + [{% trans "edit" %}]{% if c.chunk.publishable %}P{% endif %}{% if c.chunk.user.is_authenticated %} + {{ c.chunk.user }} + {% endif %}[+]
+
+ {% csrf_token %} + {% if choose_master %} + {{ form.master }} + {% endif %} + +
+
+ +

{% trans "Append to other book" %}

+ +

{% trans "Last published" %}: {{ book.last_published }}

+ +{% if book.publishable %} +

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

+ + {% trans "This book cannot be published yet" %} + {% comment %} + +
{% csrf_token %} + + + +
+ {% endcomment %} +{% else %} + {% trans "This book cannot be published yet" %} +{% endif %} + +{% endblock leftcolumn %} + +{% block rightcolumn %} +{% render_comment_list for book %} +{% with book.get_absolute_url as next %} + {% render_comment_form for book %} +{% endwith %} + +{% endblock rightcolumn %} 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_list/book.html b/apps/catalogue/templates/catalogue/book_list/book.html new file mode 100755 index 00000000..a45a3578 --- /dev/null +++ b/apps/catalogue/templates/catalogue/book_list/book.html @@ -0,0 +1,34 @@ +{% 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..3897d78b --- /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..800a7e45 --- /dev/null +++ b/apps/catalogue/templates/catalogue/chunk_add.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/chunk_edit.html b/apps/catalogue/templates/catalogue/chunk_edit.html new file mode 100755 index 00000000..3fffa963 --- /dev/null +++ b/apps/catalogue/templates/catalogue/chunk_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/document_create_missing.html b/apps/catalogue/templates/catalogue/document_create_missing.html new file mode 100644 index 00000000..dcb61752 --- /dev/null +++ b/apps/catalogue/templates/catalogue/document_create_missing.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/document_list.html b/apps/catalogue/templates/catalogue/document_list.html new file mode 100644 index 00000000..d5343a7d --- /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 leftcolumn %} + {% book_list %} +{% endblock leftcolumn %} 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/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/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..2ec3c0ac --- /dev/null +++ b/apps/catalogue/templates/catalogue/wall.html @@ -0,0 +1,32 @@ +{% load i18n %} +{% load gravatar %} + +
    +{% for item in wall %} +
  • +
    + {% if item.get_email %} + {% gravatar_img_for_email item.get_email 32 %} +
    + {% endif %} + + +
    + + {{ item.timestamp }} +

    {{ item.header }}

    + {% if item.user %} + + {{ item.user.first_name }} {{ item.user.last_name }} + <{{ item.user.email }}> + {% else %} + {{ item.user_name }} + {% if item.get_email %} + <{{ item.get_email }}> + {% endif %} + {% endif %} +
    {{ item.title }} +
    {{ item.summary }} +
  • +{% 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..15654be2 --- /dev/null +++ b/apps/catalogue/templatetags/book_list.py @@ -0,0 +1,134 @@ +from __future__ import absolute_import + +from re import split +from django.db.models import Q, Count +from django import template +from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.models import User +from catalogue.models import Chunk + +register = template.Library() + + +class ChunksList(object): + def __init__(self, chunk_qs): + #self.chunk_qs = chunk_qs#.annotate( + #book_length=Count('book__chunk')).select_related( + #'book')#, 'stage__name', + #'user') + self.chunk_qs = chunk_qs.select_related('book__hidden') + + self.book_qs = chunk_qs.values('book_id') + + def __getitem__(self, key): + if isinstance(key, slice): + return self.get_slice(key) + elif isinstance(key, int): + return self.get_slice(slice(key, key+1))[0] + else: + raise TypeError('Unsupported list index. Must be a slice or an int.') + + def __len__(self): + return self.book_qs.count() + + def get_slice(self, slice_): + book_ids = [x['book_id'] for x in self.book_qs[slice_]] + chunk_qs = self.chunk_qs.filter(book__in=book_ids) + + chunks_list = [] + book = None + for chunk in chunk_qs: + if chunk.book != book: + book = chunk.book + chunks_list.append(ChoiceChunks(book, [chunk])) + else: + chunks_list[-1].chunks.append(chunk) + return chunks_list + + +class ChoiceChunks(object): + """ + Associates the given chunks iterable for a book. + """ + + chunks = None + + def __init__(self, book, chunks): + self.book = book + self.chunks = chunks + + +def foreign_filter(qs, value, filter_field, model, model_field='slug', unset='-'): + if value == unset: + return qs.filter(**{filter_field: None}) + if not value: + return qs + try: + obj = model._default_manager.get(**{model_field: value}) + except model.DoesNotExist: + return qs.none() + else: + return qs.filter(**{filter_field: obj}) + + +def search_filter(qs, value, filter_field): + if not value: + return qs + return qs.filter(**{"%s__icontains" % filter_field: value}) + + +_states = [ + ('publishable', _('publishable'), Q(book___new_publishable=True)), + ('changed', _('changed'), Q(_changed=True)), + ('published', _('published'), Q(book___published=True)), + ('unpublished', _('unpublished'), Q(book___published=False)), + ('empty', _('empty'), Q(head=None)), + ] +_states_options = [s[:2] for s in _states] +_states_dict = dict([(s[0], s[2]) for s in _states]) + + +def document_list_filter(request, **kwargs): + + def arg_or_GET(field): + return kwargs.get(field, request.GET.get(field)) + + if arg_or_GET('all'): + chunks = Chunk.objects.all() + else: + chunks = Chunk.visible_objects.all() + + chunks = chunks.order_by('book__title', 'book', 'number') + + state = arg_or_GET('status') + if state in _states_dict: + chunks = chunks.filter(_states_dict[state]) + + chunks = foreign_filter(chunks, arg_or_GET('user'), 'user', User, 'username') + chunks = foreign_filter(chunks, arg_or_GET('stage'), 'stage', Chunk.tag_model, 'slug') + chunks = search_filter(chunks, arg_or_GET('title'), 'book__title') + return chunks + + +@register.inclusion_tag('catalogue/book_list/book_list.html', takes_context=True) +def book_list(context, user=None): + request = context['request'] + + if user: + filters = {"user": user} + new_context = {"viewed_user": user} + else: + filters = {} + new_context = {"users": User.objects.annotate( + count=Count('chunk')).filter(count__gt=0).order_by( + '-count', 'last_name', 'first_name')} + + new_context.update({ + "request": request, + "books": ChunksList(document_list_filter(request, **filters)), + "stages": Chunk.tag_model.objects.all(), + "states": _states_options, + }) + + return new_context + diff --git a/apps/catalogue/templatetags/catalogue.py b/apps/catalogue/templatetags/catalogue.py new file mode 100644 index 00000000..bfb900bf --- /dev/null +++ b/apps/catalogue/templatetags/catalogue.py @@ -0,0 +1,45 @@ +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'] + if user.is_authenticated(): + tabs.append(Tab('my', _('My page'), reverse("catalogue_user"))) + + tabs.append(Tab('activity', _('Activity'), reverse("catalogue_activity"))) + tabs.append(Tab('all', _('All'), reverse("catalogue_document_list"))) + tabs.append(Tab('users', _('Users'), reverse("catalogue_users"))) + tabs.append(Tab('create', _('Add'), reverse("catalogue_create_missing"))) + tabs.append(Tab('upload', _('Upload'), reverse("catalogue_upload"))) + + if user.is_staff: + tabs.append(Tab('admin', _('Admin'), reverse("admin:index"))) + + 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..5236eedf --- /dev/null +++ b/apps/catalogue/templatetags/wall.py @@ -0,0 +1,125 @@ +from __future__ import absolute_import + +from django.db.models import Q +from django.core.urlresolvers import reverse +from django.contrib.comments.models import Comment +from django import template +from django.utils.translation import ugettext as _ + +from catalogue.models import Chunk, BookPublishRecord + +register = template.Library() + + +class WallItem(object): + title = '' + summary = '' + url = '' + timestamp = '' + user = None + email = '' + + def __init__(self, tag): + self.tag = tag + + def get_email(self): + if self.user: + return self.user.email + else: + return self.email + + +def changes_wall(user, max_len): + qs = Chunk.change_model.objects.filter(revision__gt=-1).order_by('-created_at') + qs = qs.select_related('author', 'tree', 'tree__book__title') + if user: + qs = qs.filter(Q(author=user) | Q(tree__user=user)) + qs = qs[:max_len] + for item in qs: + tag = 'stage' if item.tags.count() else 'change' + chunk = item.tree + w = WallItem(tag) + if user and item.author != user: + w.header = _('Related edit') + else: + w.header = _('Edit') + w.title = chunk.pretty_name() + w.summary = item.description + w.url = reverse('wiki_editor', + args=[chunk.book.slug, chunk.slug]) + '?diff=%d' % item.revision + w.timestamp = item.created_at + w.user = item.author + w.email = item.author_email + yield w + + +# TODO: marked for publishing + + +def published_wall(user, max_len): + qs = BookPublishRecord.objects.select_related('book__title') + if user: + # TODO: published my book + qs = qs.filter(Q(user=user)) + qs = qs[:max_len] + for item in qs: + w = WallItem('publish') + w.header = _('Publication') + w.title = item.book.title + w.timestamp = item.timestamp + w.url = item.book.get_absolute_url() + w.user = item.user + w.email = item.user.email + yield w + + +def comments_wall(user, max_len): + qs = Comment.objects.filter(is_public=True).select_related().order_by('-submit_date') + if user: + # TODO: comments concerning my books + qs = qs.filter(Q(user=user)) + qs = qs[:max_len] + for item in qs: + w = WallItem('comment') + w.header = _('Comment') + w.title = item.content_object + w.summary = item.comment + w.url = item.content_object.get_absolute_url() + w.timestamp = item.submit_date + w.user = item.user + w.email = item.user_email + yield w + + +def big_wall(max_len, *args): + """ + Takes some WallItem iterators and zips them into one big wall. + Input iterators must already be sorted by timestamp. + """ + subwalls = [] + for w in args: + try: + subwalls.append([next(w), w]) + except StopIteration: + pass + + while max_len and subwalls: + i, next_item = max(enumerate(subwalls), key=lambda x: x[1][0].timestamp) + yield next_item[0] + max_len -= 1 + try: + next_item[0] = next(next_item[1]) + except StopIteration: + del subwalls[i] + + +@register.inclusion_tag("catalogue/wall.html", takes_context=True) +def wall(context, user=None, max_len=100): + return { + "request": context['request'], + "STATIC_URL": context['STATIC_URL'], + "wall": big_wall(max_len, + changes_wall(user, max_len), + published_wall(user, max_len), + comments_wall(user, max_len), + )} diff --git a/apps/catalogue/urls.py b/apps/catalogue/urls.py new file mode 100644 index 00000000..73fd3ee6 --- /dev/null +++ b/apps/catalogue/urls.py @@ -0,0 +1,41 @@ +# -*- 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'^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'^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"), + url(r'^book_edit/(?P[^/]+)/$', + 'book_edit', name="catalogue_book_edit"), + +) diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py new file mode 100644 index 00000000..ef042de2 --- /dev/null +++ b/apps/catalogue/views.py @@ -0,0 +1,396 @@ +from datetime import datetime +import logging +import os +from StringIO import StringIO + +from django.contrib import auth +from django.contrib.auth.models import User +from django.contrib.auth.decorators import login_required +from django.core.urlresolvers import reverse +from django.db.models import Count, Q +from django import http +from django.http import Http404 +from django.shortcuts import get_object_or_404, render +from django.utils.http import urlquote_plus +from django.utils.translation import ugettext_lazy as _ +from django.views.decorators.http import require_POST +from django.views.generic.simple import direct_to_template + +import librarian.html +import librarian.text + +from catalogue import forms +from catalogue import helpers +from catalogue.helpers import active_tab +from catalogue.models import Book, Chunk, BookPublishRecord, ChunkPublishRecord +from catalogue import xml_tools + +# +# 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') + + +@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), + }) + + +@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): + return render(request, 'catalogue/activity.html') + + +@never_cache +def logout_then_redirect(request): + auth.logout(request) + return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?=')) + + +@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'], + ) + + return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug])) + else: + form = forms.DocumentCreateForm(initial={ + "slug": slug, + "title": slug.replace('-', ' ').title(), + }) + + return direct_to_template(request, "catalogue/document_create_missing.html", extra_context={ + "slug": slug, + "form": form, + }) + + +@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, + }) + else: + form = forms.DocumentsUploadForm() + + return direct_to_template(request, "catalogue/document_upload.html", extra_context={ + "form": form, + }) + + +@never_cache +def book_xml(request, slug): + xml = get_object_or_404(Book, slug=slug).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): + xml = get_object_or_404(Book, slug=slug).materialize() + output = StringIO() + # errors? + 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): + xml = get_object_or_404(Book, slug=slug).materialize() + output = StringIO() + # errors? + 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 revision(request, slug, chunk=None): + try: + doc = Chunk.get(slug, chunk) + except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist): + raise Http404 + return http.HttpResponse(str(doc.revision())) + + +def book(request, slug): + book = get_object_or_404(Book, slug=slug) + + # TODO: most of this should go somewhere else + + # do we need some automation? + first_master = None + chunks = [] + need_fixing = False + choose_master = False + + length = book.chunk_set.count() + for i, chunk in enumerate(book): + chunk_dict = { + "chunk": chunk, + "fix": [], + "grade": "" + } + graded = xml_tools.GradedText(chunk.materialize()) + if graded.is_wl(): + master = graded.master() + if first_master is None: + first_master = master + elif master != first_master: + chunk_dict['fix'].append('bad-master') + + if i > 0 and not graded.has_trim_begin(): + chunk_dict['fix'].append('trim-begin') + if i < length - 1 and not graded.has_trim_end(): + chunk_dict['fix'].append('trim-end') + + if chunk_dict['fix']: + chunk_dict['grade'] = 'wl-fix' + else: + chunk_dict['grade'] = 'wl' + + elif graded.is_broken_wl(): + chunk_dict['grade'] = 'wl-broken' + elif graded.is_xml(): + chunk_dict['grade'] = 'xml' + else: + chunk_dict['grade'] = 'plain' + chunk_dict['fix'].append('wl') + choose_master = True + + if chunk_dict['fix']: + need_fixing = True + chunks.append(chunk_dict) + + if first_master or not need_fixing: + choose_master = False + + if request.method == "POST": + form = forms.ChooseMasterForm(request.POST) + if not choose_master or form.is_valid(): + if choose_master: + first_master = form.cleaned_data['master'] + + # do the actual fixing + for c in chunks: + if not c['fix']: + continue + + text = c['chunk'].materialize() + for fix in c['fix']: + if fix == 'bad-master': + text = xml_tools.change_master(text, first_master) + elif fix == 'trim-begin': + text = xml_tools.add_trim_begin(text) + elif fix == 'trim-end': + text = xml_tools.add_trim_end(text) + elif fix == 'wl': + text = xml_tools.basic_structure(text, first_master) + author = request.user if request.user.is_authenticated() else None + description = "auto-fix: " + ", ".join(c['fix']) + c['chunk'].commit(text=text, author=author, + description=description) + + return http.HttpResponseRedirect(book.get_absolute_url()) + elif choose_master: + form = forms.ChooseMasterForm() + else: + form = None + + return direct_to_template(request, "catalogue/book_detail.html", extra_context={ + "book": book, + "chunks": chunks, + "need_fixing": need_fixing, + "choose_master": choose_master, + "first_master": first_master, + "form": form, + }) + + +def chunk_add(request, slug, chunk): + try: + doc = Chunk.get(slug, chunk) + except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist): + raise Http404 + + 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'], + ) + + 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 request.method == "POST": + form = forms.ChunkForm(request.POST, instance=doc) + if form.is_valid(): + form.save() + return http.HttpResponseRedirect(doc.book.get_absolute_url()) + else: + form = forms.ChunkForm(instance=doc) + return direct_to_template(request, "catalogue/chunk_edit.html", extra_context={ + "chunk": doc, + "form": form, + }) + + +def book_append(request, slug): + book = get_object_or_404(Book, slug=slug) + if request.method == "POST": + form = forms.BookAppendForm(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() + return direct_to_template(request, "catalogue/book_append_to.html", extra_context={ + "book": book, + "form": form, + }) + + +def book_edit(request, slug): + book = get_object_or_404(Book, slug=slug) + 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) + return direct_to_template(request, "catalogue/book_edit.html", extra_context={ + "book": book, + "form": form, + }) + + +@require_POST +@login_required +def publish(request, slug): + book = get_object_or_404(Book, slug=slug) + try: + book.publish(request.user) + 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..d6a9333b --- /dev/null +++ b/apps/catalogue/xml_tools.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +from copy import deepcopy +from functools import wraps +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 obj_memoized(f): + """ + A decorator that caches return value of object methods. + The cache is kept with the object, in a _obj_memoized property. + """ + @wraps(f) + def wrapper(self, *args, **kwargs): + if not hasattr(self, '_obj_memoized'): + self._obj_memoized = {} + key = (f.__name__,) + args + tuple(sorted(kwargs.iteritems())) + try: + return self._obj_memoized[key] + except TypeError: + return f(self, *args, **kwargs) + except KeyError: + self._obj_memoized[key] = f(self, *args, **kwargs) + return self._obj_memoized[key] + return wrapper + + +class GradedText(object): + _edoc = None + + ROOT = 'utwor' + RDF = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}RDF' + + def __init__(self, text): + self._text = text + + @obj_memoized + def is_xml(self): + """ + Determines if it's a well-formed XML. + + >>> GradedText("").is_xml() + True + >>> GradedText("").is_xml() + False + """ + try: + self._edoc = etree.fromstring(self._text) + except etree.XMLSyntaxError: + return False + return True + + @obj_memoized + def is_wl(self): + """ + Determines if it's an XML with a and a master tag. + + >>> GradedText("").is_wl() + True + >>> GradedText("").is_wl() + False + """ + if self.is_xml(): + e = self._edoc + # FIXME: there could be comments + ret = e.tag == self.ROOT and ( + len(e) == 1 and e[0].tag in MASTERS or + len(e) == 2 and e[0].tag == self.RDF + and e[1].tag in MASTERS) + if ret: + self._master = e[-1].tag + del self._edoc + return ret + else: + return False + + @obj_memoized + def is_broken_wl(self): + """ + Determines if it at least looks like broken WL file + and not just some untagged text. + + >>> GradedText("<").is_broken_wl() + True + >>> GradedText("some text").is_broken_wl() + False + """ + if self.is_wl(): + return True + text = self._text.strip() + return text.startswith('') and text.endswith('') + + def master(self): + """ + Gets the master tag. + + >>> GradedText("").master() + 'powiesc' + """ + assert self.is_wl() + return self._master + + @obj_memoized + def has_trim_begin(self): + return RE_TRIM_BEGIN.search(self._text) + + @obj_memoized + def has_trim_end(self): + return RE_TRIM_END.search(self._text) + + +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 change_master(text, master): + """ + Changes the master tag in a WL document. + """ + e = etree.fromstring(text) + e[-1].tag = master + return unicode(etree.tostring(e, encoding="utf-8"), 'utf-8') + + +def basic_structure(text, master): + e = etree.fromstring(''' + + + +''' % (TRIM_BEGIN, TRIM_END)) + e[0].tag = master + e[0][0].tail = "\n"*3 + text + "\n"*3 + return unicode(etree.tostring(e, encoding="utf-8"), 'utf-8') + + +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/dvcs/__init__.py b/apps/dvcs/__init__.py new file mode 100644 index 00000000..e69de29b 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 new file mode 100644 index 00000000..ab5f77d9 --- /dev/null +++ b/apps/dvcs/models.py @@ -0,0 +1,326 @@ +from datetime import datetime +import os.path + +from django.contrib.auth.models import User +from django.core.files.base import ContentFile +from django.core.files.storage import FileSystemStorage +from django.db import models +from django.db.models.base import ModelBase +from django.utils.translation import ugettext_lazy as _ +from mercurial import mdiff, simplemerge + +from django.conf import settings +from dvcs.signals import post_commit +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 Tag.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): + """ + Single document change related to previous change. The "parent" + argument points to the version against which this change has been + recorded. Initial text will have a null parent. + + Data file contains a gzipped text of the 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(_('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, 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 + ) + + + 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 = 0 + else: + self.revision = tree_rev + 1 + return super(Change, self).save(*args, **kwargs) + + def materialize(self): + 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 - fast forward + return other + + 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()) + 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) + + def set_publishable(self, publishable): + self.publishable = publishable + self.save() + post_publishable(sender=self, publishable=publishable).send() + + +def create_tag_model(model): + name = model.__name__ + 'Tag' + + 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, + 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) + + def materialize(self, change=None): + if self.head is None: + return u'' + 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. + + :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 parent is not None and not isinstance(parent, Change): + parent = self.change_set.objects.get(pk=kwargs['parent']) + + 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 = change + self.save() + + post_commit.send(sender=self.head) + + return self.head + + def history(self): + return self.change_set.filter(revision__gt=-1) + + 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.change_set.filter(publishable=True) + if changes.exists(): + return changes.order_by('-created_at')[0] + else: + return None 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..de77d991 --- /dev/null +++ b/apps/dvcs/tests/__init__.py @@ -0,0 +1,158 @@ +from nose.tools import * +from django.test import TestCase +from dvcs.models import Document + + +class ADocument(Document): + pass + + +class DocumentModelTests(TestCase): + + def assertTextEqual(self, given, expected): + return self.assertEqual(given, expected, + "Expected '''%s'''\n differs from text: '''%s'''" % (expected, given) + ) + + def test_empty_file(self): + doc = ADocument.objects.create() + self.assertTextEqual(doc.materialize(), u"") + + def test_single_commit(self): + doc = ADocument.objects.create() + doc.commit(text=u"Ala ma kota", description="Commit #1") + self.assertTextEqual(doc.materialize(), u"Ala ma kota") + + def test_chained_commits(self): + doc = ADocument.objects.create() + text1 = u""" + Line #1 + Line #2 is cool + """ + text2 = u""" + Line #1 + Line #2 is hot + """ + text3 = u""" + Line #1 + ... is hot + Line #3 ate Line #2 + """ + + c1 = doc.commit(description="Commit #1", text=text1) + c2 = doc.commit(description="Commit #2", text=text2) + c3 = doc.commit(description="Commit #3", text=text3) + + self.assertTextEqual(doc.materialize(), text3) + self.assertTextEqual(doc.materialize(change=c3), text3) + self.assertTextEqual(doc.materialize(change=c2), text2) + self.assertTextEqual(doc.materialize(change=c1), text1) + + def test_parallel_commit_noconflict(self): + doc = ADocument.objects.create() + text1 = u""" + Line #1 + Line #2 + """ + text2 = u""" + Line #1 is hot + Line #2 + """ + text3 = u""" + Line #1 + Line #2 + Line #3 + """ + text_merged = u""" + Line #1 is hot + Line #2 + Line #3 + """ + + base = doc.commit(description="Commit #1", text=text1) + c1 = doc.commit(description="Commit #2", text=text2) + commits = doc.change_set.count() + c2 = doc.commit(description="Commit #3", text=text3, parent=base) + self.assertEqual(doc.change_set.count(), commits + 2, + u"Parallel commits should create an additional merge commit") + self.assertTextEqual(doc.materialize(), text_merged) + + def test_parallel_commit_conflict(self): + doc = ADocument.objects.create() + text1 = u""" + Line #1 + Line #2 + Line #3 + """ + text2 = u""" + Line #1 + Line #2 is hot + Line #3 + """ + text3 = u""" + Line #1 + Line #2 is cool + Line #3 + """ + text_merged = u""" + Line #1 +<<<<<<< + Line #2 is hot +======= + Line #2 is cool +>>>>>>> + Line #3 + """ + base = doc.commit(description="Commit #1", text=text1) + c1 = doc.commit(description="Commit #2", text=text2) + commits = doc.change_set.count() + c2 = doc.commit(description="Commit #3", text=text3, parent=base) + self.assertEqual(doc.change_set.count(), commits + 2, + u"Parallel commits should create an additional merge commit") + self.assertTextEqual(doc.materialize(), text_merged) + + + def test_multiple_parallel_commits(self): + text_a1 = u""" + Line #1 + + Line #2 + + Line #3 + """ + text_a2 = u""" + Line #1 * + + Line #2 + + Line #3 + """ + text_b1 = u""" + Line #1 + + Line #2 ** + + Line #3 + """ + text_c1 = u""" + Line #1 + + Line #2 + + Line #3 *** + """ + text_merged = u""" + Line #1 * + + Line #2 ** + + Line #3 *** + """ + + + doc = ADocument.objects.create() + c1 = doc.commit(description="Commit A1", text=text_a1) + c2 = doc.commit(description="Commit A2", text=text_a2, parent=c1) + c3 = doc.commit(description="Commit B1", text=text_b1, parent=c1) + c4 = doc.commit(description="Commit C1", text=text_c1, parent=c1) + self.assertTextEqual(doc.materialize(), text_merged) diff --git a/apps/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 74934bd2..1d4d8549 100644 --- a/apps/wiki/forms.py +++ b/apps/wiki/forms.py @@ -4,64 +4,22 @@ # 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. - """ - title = forms.CharField() - id = forms.RegexField(regex=ur"^[-\wąćęłńóśźżĄĆĘŁŃÓŚŹŻ]+$") - file = forms.FileField(required=False) - text = forms.CharField(required=False, widget=forms.Textarea) - - def clean(self): - file = self.cleaned_data['file'] - - if file is not None: - try: - self.cleaned_data['text'] = file.read().decode('utf-8') - except UnicodeDecodeError: - raise forms.ValidationError("Text file must be UTF-8 encoded.") - - if not self.cleaned_data["text"]: - raise forms.ValidationError("You must either enter text or upload a file") - - return self.cleaned_data - - -class DocumentsUploadForm(forms.Form): - """ - Form used for uploading new documents. - """ - file = forms.FileField(required=True, label=_('ZIP file')) - - def clean(self): - file = self.cleaned_data['file'] - - import zipfile - try: - z = self.cleaned_data['zip'] = zipfile.ZipFile(file) - except zipfile.BadZipfile: - raise forms.ValidationError("Should be a ZIP file.") - if z.testzip(): - raise forms.ValidationError("ZIP file corrupt.") - - return self.cleaned_data - - class DocumentTextSaveForm(forms.Form): """ Form for saving document's text: @@ -72,7 +30,7 @@ class DocumentTextSaveForm(forms.Form): """ - parent_revision = forms.IntegerField(widget=forms.HiddenInput) + parent_revision = forms.IntegerField(widget=forms.HiddenInput, required=False) text = forms.CharField(widget=forms.HiddenInput) author_name = forms.CharField( @@ -94,8 +52,8 @@ class DocumentTextSaveForm(forms.Form): help_text=_(u"Describe changes you made."), ) - stage_completed = forms.ChoiceField( - choices=DOCUMENT_STAGES, + 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."), 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 8ec3fdf2..f841945b 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 0dbb13a5..c760f3a7 100644 --- a/apps/wiki/locale/pl/LC_MESSAGES/django.po +++ b/apps/wiki/locale/pl/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Platforma Redakcyjna\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-03-07 12:31+0100\n" -"PO-Revision-Date: 2011-03-07 12:31+0100\n" +"POT-Creation-Date: 2011-06-21 13:41+0200\n" +"PO-Revision-Date: 2011-06-21 13:46+0100\n" "Last-Translator: Radek Czajka \n" "Language-Team: Fundacja Nowoczesna Polska \n" "Language: \n" @@ -16,130 +16,223 @@ msgstr "" "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:32 +msgid "Publishable" msgstr "Gotowe do publikacji" -#: forms.py:49 +#: forms.py:68 msgid "ZIP file" msgstr "Plik ZIP" -#: forms.py:80 -#: forms.py:118 +#: forms.py:99 +#: forms.py:137 msgid "Author" msgstr "Autor" -#: forms.py:81 -#: forms.py:119 +#: forms.py:100 +#: forms.py:138 msgid "Your name" msgstr "Imię i nazwisko" -#: forms.py:86 -#: forms.py:124 +#: forms.py:105 +#: forms.py:143 msgid "Author's email" msgstr "E-mail autora" -#: forms.py:87 -#: forms.py:125 +#: forms.py:106 +#: forms.py:144 msgid "Your email address, so we can show a gravatar :)" msgstr "Adres e-mail, żebyśmy mogli pokazać gravatar :)" -#: forms.py:93 -#: forms.py:131 +#: forms.py:112 +#: forms.py:150 msgid "Your comments" msgstr "Twój komentarz" -#: forms.py:94 +#: forms.py:113 msgid "Describe changes you made." msgstr "Opisz swoje zmiany" -#: forms.py:100 +#: forms.py:119 msgid "Completed" msgstr "Ukończono" -#: forms.py:101 +#: forms.py:120 msgid "If you completed a life cycle stage, select it." msgstr "Jeśli został ukończony etap prac, wskaż go." -#: forms.py:132 +#: forms.py:151 msgid "Describe the reason for reverting." msgstr "Opisz powód przywrócenia." -#: models.py:93 -#, python-format -msgid "Finished stage: %s" -msgstr "Ukończony etap: %s" +#: forms.py:176 +#: forms.py:190 +msgid "Chunk with this slug already exists" +msgstr "Część z tym slugiem już istnieje" + +#: forms.py:202 +msgid "Append to" +msgstr "Dołącz do" + +#: models.py:25 +msgid "title" +msgstr "tytuł" + +#: models.py:26 +msgid "slug" +msgstr "" + +#: models.py:27 +msgid "scan gallery name" +msgstr "nazwa galerii skanów" + +#: models.py:29 +msgid "parent" +msgstr "rodzic" + +#: models.py:30 +msgid "parent number" +msgstr "numeracja rodzica" + +#: models.py:40 +msgid "book" +msgstr "książka" + +#: models.py:41 +msgid "books" +msgstr "książki" -#: models.py:152 +#: models.py:206 msgid "name" msgstr "nazwa" -#: models.py:156 +#: models.py:210 msgid "theme" msgstr "motyw" -#: models.py:157 +#: models.py:211 msgid "themes" msgstr "motywy" -#: views.py:168 +#: views.py:241 #, python-format -msgid "Title already used for %s" -msgstr "Nazwa taka sama jak dla pliku %s" +msgid "Slug already used for %s" +msgstr "Slug taki sam jak dla pliku %s" -#: views.py:170 -msgid "Title already used in repository." -msgstr "Plik o tej nazwie już istnieje w repozytorium." +#: views.py:243 +msgid "Slug already used in repository." +msgstr "Dokument o tym slugu już istnieje w repozytorium." -#: views.py:176 +#: views.py:249 msgid "File should be UTF-8 encoded." msgstr "Plik powinien mieć kodowanie UTF-8." -#: views.py:379 +#: views.py:655 msgid "Tag added" msgstr "Dodano tag" -#: templates/wiki/base.html:15 +#: views.py:677 +msgid "Revision marked" +msgstr "Wersja oznaczona" + +#: views.py:679 +msgid "Nothing changed" +msgstr "Nic nie uległo zmianie" + +#: templates/wiki/base.html:9 msgid "Platforma Redakcyjna" msgstr "" +#: templates/wiki/book_append_to.html:8 +msgid "Append book" +msgstr "Dołącz książkę" + +#: templates/wiki/book_detail.html:6 +#: templates/wiki/book_detail.html.py:46 +msgid "edit" +msgstr "edytuj" + +#: templates/wiki/book_detail.html:16 +msgid "add basic document structure" +msgstr "dodaj podstawową strukturę dokumentu" + +#: templates/wiki/book_detail.html:20 +msgid "change master tag to" +msgstr "zmień tak master na" + +#: templates/wiki/book_detail.html:24 +msgid "add begin trimming tag" +msgstr "dodaj początkowy ogranicznik" + +#: templates/wiki/book_detail.html:28 +msgid "add end trimming tag" +msgstr "dodaj końcowy ogranicznik" + +#: templates/wiki/book_detail.html:34 +msgid "unstructured text" +msgstr "tekst bez struktury" + +#: templates/wiki/book_detail.html:38 +msgid "unknown XML" +msgstr "nieznany XML" + +#: templates/wiki/book_detail.html:42 +msgid "broken document" +msgstr "uszkodzony dokument" + +#: templates/wiki/book_detail.html:60 +msgid "Apply fixes" +msgstr "Wykonaj zmiany" + +#: templates/wiki/book_detail.html:66 +msgid "Append to other book" +msgstr "Dołącz do innej książki" + +#: templates/wiki/book_detail.html:68 +msgid "Last published" +msgstr "Ostatnio opublikowano" + +#: templates/wiki/book_detail.html:72 +msgid "Full XML" +msgstr "Pełny XML" + +#: templates/wiki/book_detail.html:73 +msgid "HTML version" +msgstr "Wersja HTML" + +#: templates/wiki/book_detail.html:74 +msgid "TXT version" +msgstr "Wersja TXT" + +#: templates/wiki/book_detail.html:76 +msgid "EPUB version" +msgstr "Wersja EPUB" + +#: templates/wiki/book_detail.html:77 +msgid "PDF version" +msgstr "Wersja PDF" + +#: templates/wiki/book_detail.html:90 +#: templates/wiki/tabs/summary_view.html:30 +msgid "Publish" +msgstr "Opublikuj" + +#: templates/wiki/book_detail.html:94 +msgid "This book cannot be published yet" +msgstr "Ta książka nie może jeszcze zostać opublikowana" + +#: templates/wiki/book_edit.html:8 +#: templates/wiki/chunk_edit.html:8 +#: templates/wiki/document_details_base.html:35 +#: templates/wiki/pubmark_dialog.html:15 +#: templates/wiki/tag_dialog.html:15 +msgid "Save" +msgstr "Zapisz" + +#: templates/wiki/chunk_add.html:8 +msgid "Add chunk" +msgstr "Dodaj część" + #: templates/wiki/diff_table.html:5 msgid "Old version" msgstr "Stara wersja" @@ -156,92 +249,102 @@ msgstr "Utwórz dokument" 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:31 msgid "Help" msgstr "Pomoc" -#: templates/wiki/document_details_base.html:38 +#: templates/wiki/document_details_base.html:33 msgid "Version" msgstr "Wersja" -#: templates/wiki/document_details_base.html:38 +#: templates/wiki/document_details_base.html:33 msgid "Unknown" msgstr "nieznana" -#: templates/wiki/document_details_base.html:40 -#: templates/wiki/tag_dialog.html:15 -msgid "Save" -msgstr "Zapisz" - -#: templates/wiki/document_details_base.html:41 +#: templates/wiki/document_details_base.html:36 msgid "Save attempt in progress" msgstr "Trwa zapisywanie" -#: templates/wiki/document_details_base.html:42 +#: templates/wiki/document_details_base.html:37 msgid "There is a newer version of this document!" msgstr "Istnieje nowsza wersja tego dokumentu!" -#: templates/wiki/document_list.html:30 +#: templates/wiki/document_list.html:31 msgid "Clear filter" msgstr "Wyczyść filtr" -#: templates/wiki/document_list.html:48 +#: templates/wiki/document_list.html:46 +msgid "No books found." +msgstr "Nie znaleziono książek." + +#: templates/wiki/document_list.html:89 msgid "Your last edited documents" msgstr "Twoje ostatnie edycje" -#: templates/wiki/document_upload.html:9 +#: templates/wiki/document_upload.html:8 msgid "Bulk documents upload" msgstr "Hurtowe dodawanie dokumentów" -#: templates/wiki/document_upload.html:12 +#: templates/wiki/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/wiki/document_upload.html:17 +#: templates/wiki/document_upload.html:16 +#: templatetags/wiki.py:36 msgid "Upload" -msgstr "Dodaj" +msgstr "Załaduj" -#: templates/wiki/document_upload.html:24 +#: templates/wiki/document_upload.html:23 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 +#: templates/wiki/document_upload.html:24 msgid "Offending files" msgstr "Błędne pliki" -#: templates/wiki/document_upload.html:33 +#: templates/wiki/document_upload.html:32 msgid "Correct files" msgstr "Poprawne pliki" -#: templates/wiki/document_upload.html:44 +#: templates/wiki/document_upload.html:43 msgid "Files have been successfully uploaded to the repository." msgstr "Pliki zostały dodane do repozytorium." -#: templates/wiki/document_upload.html:45 +#: templates/wiki/document_upload.html:44 msgid "Uploaded files" msgstr "Dodane pliki" -#: templates/wiki/document_upload.html:55 +#: templates/wiki/document_upload.html:54 msgid "Skipped files" msgstr "Pominięte pliki" -#: templates/wiki/document_upload.html:56 +#: templates/wiki/document_upload.html:55 msgid "Files skipped due to no .xml extension" msgstr "Pliki pominięte z powodu braku rozszerzenia .xml." -#: templates/wiki/revert_dialog.html:38 -msgid "Revert" -msgstr "Przywróć" - +#: templates/wiki/pubmark_dialog.html:16 #: templates/wiki/revert_dialog.html:39 #: templates/wiki/tag_dialog.html:16 msgid "Cancel" msgstr "Anuluj" +#: templates/wiki/revert_dialog.html:38 +msgid "Revert" +msgstr "Przywróć" + +#: templates/wiki/user_list.html:7 +#: templatetags/wiki.py:33 +msgid "Users" +msgstr "Użytkownicy" + #: 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" msgstr "Poprzednie" @@ -267,8 +370,8 @@ msgid "Compare versions" msgstr "Porównaj wersje" #: templates/wiki/tabs/history_view.html:7 -msgid "Mark version" -msgstr "Oznacz wersję" +msgid "Mark for publishing" +msgstr "Oznacz do publikacji" #: templates/wiki/tabs/history_view.html:9 msgid "Revert document" @@ -315,43 +418,27 @@ 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:14 msgid "Document ID" msgstr "ID dokumentu" -#: templates/wiki/tabs/summary_view.html:19 +#: templates/wiki/tabs/summary_view.html:18 msgid "Current version" msgstr "Aktualna wersja" -#: templates/wiki/tabs/summary_view.html:22 +#: templates/wiki/tabs/summary_view.html:21 msgid "Last edited by" msgstr "Ostatnio edytowane przez" -#: templates/wiki/tabs/summary_view.html:26 +#: templates/wiki/tabs/summary_view.html:25 msgid "Link to gallery" msgstr "Link do galerii" -#: templates/wiki/tabs/summary_view.html:31 -msgid "Characters in document" -msgstr "Znaków w dokumencie" - -#: templates/wiki/tabs/summary_view.html:32 -msgid "pages" -msgstr "stron" - -#: templates/wiki/tabs/summary_view.html:32 -msgid "untagged" -msgstr "nieotagowane" - -#: templates/wiki/tabs/summary_view.html:35 -msgid "Publish" -msgstr "Opublikuj" - -#: templates/wiki/tabs/summary_view_item.html:4 +#: templates/wiki/tabs/summary_view_item.html:3 msgid "Summary" msgstr "Podsumowanie" @@ -367,8 +454,55 @@ msgstr "Wstaw przypis" msgid "Visual editor" msgstr "Edytor wizualny" +#: templatetags/wiki.py:30 +msgid "Assigned to me" +msgstr "Przypisane do mnie" + +#: templatetags/wiki.py:32 +msgid "Unassigned" +msgstr "Nie przypisane" + +#: templatetags/wiki.py:34 +msgid "All" +msgstr "Wszystkie" + +#: templatetags/wiki.py:35 +msgid "Add" +msgstr "Dodaj" + +#: templatetags/wiki.py:39 +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 7cb20c65..c539908d 100644 --- a/apps/wiki/models.py +++ b/apps/wiki/models.py @@ -4,150 +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, **commit_args): - self.vstorage.revert(name, revision, **commit_args) - - 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/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 934a8ac6..d95603f6 100644 --- a/apps/wiki/templates/wiki/document_details.html +++ b/apps/wiki/templates/wiki/document_details.html @@ -43,4 +43,5 @@ {% include "wiki/save_dialog.html" %} {% include "wiki/revert_dialog.html" %} {% include "wiki/tag_dialog.html" %} + {% include "wiki/pubmark_dialog.html" %} {% endblock %} diff --git a/apps/wiki/templates/wiki/document_details_base.html b/apps/wiki/templates/wiki/document_details_base.html index 03231330..76711dd9 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,17 @@ {% block maincontent %}