--- /dev/null
+[submodule "lib/librarian"]
+ path = lib/librarian
+ url = git://github.com/fnp/librarian.git
--- /dev/null
+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")
+
--- /dev/null
+# 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']
--- /dev/null
+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
+
+
--- /dev/null
+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/')
--- /dev/null
+"""
+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
+"""}
+
--- /dev/null
+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'),
+)
--- /dev/null
+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('/')
--- /dev/null
+ # pragma: no cover
--- /dev/null
+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)
--- /dev/null
+# -*- 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',
+ ]
--- /dev/null
+[
+ {
+ "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"
+ }
+ }
+]
--- /dev/null
+# -*- 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'))
+
+ 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))
--- /dev/null
+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
+
+
+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.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], chunk.book_length))
+ 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, book_length):
+ self.book = book
+ self.chunks = chunks
+ self.book_length = book_length
+
--- /dev/null
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Platforma Redakcyjna\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2011-06-21 13:41+0200\n"
+"PO-Revision-Date: 2011-06-21 13:46+0100\n"
+"Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
+"Language-Team: Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org.pl>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:32
+msgid "Publishable"
+msgstr "Gotowe do publikacji"
+
+#: forms.py:68
+msgid "ZIP file"
+msgstr "Plik ZIP"
+
+#: forms.py:99
+#: forms.py:137
+msgid "Author"
+msgstr "Autor"
+
+#: forms.py:100
+#: forms.py:138
+msgid "Your name"
+msgstr "Imię i nazwisko"
+
+#: forms.py:105
+#: forms.py:143
+msgid "Author's email"
+msgstr "E-mail autora"
+
+#: 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:112
+#: forms.py:150
+msgid "Your comments"
+msgstr "Twój komentarz"
+
+#: forms.py:113
+msgid "Describe changes you made."
+msgstr "Opisz swoje zmiany"
+
+#: forms.py:119
+msgid "Completed"
+msgstr "Ukończono"
+
+#: 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:151
+msgid "Describe the reason for reverting."
+msgstr "Opisz powód przywrócenia."
+
+#: 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:206
+msgid "name"
+msgstr "nazwa"
+
+#: models.py:210
+msgid "theme"
+msgstr "motyw"
+
+#: models.py:211
+msgid "themes"
+msgstr "motywy"
+
+#: views.py:241
+#, python-format
+msgid "Slug already used for %s"
+msgstr "Slug taki sam jak dla pliku %s"
+
+#: views.py:243
+msgid "Slug already used in repository."
+msgstr "Dokument o tym slugu już istnieje w repozytorium."
+
+#: views.py:249
+msgid "File should be UTF-8 encoded."
+msgstr "Plik powinien mieć kodowanie UTF-8."
+
+#: views.py:655
+msgid "Tag added"
+msgstr "Dodano tag"
+
+#: 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"
+
+#: templates/wiki/diff_table.html:6
+msgid "New version"
+msgstr "Nowa wersja"
+
+#: templates/wiki/document_create_missing.html:8
+msgid "Create document"
+msgstr "Utwórz dokument"
+
+#: templates/wiki/document_details.html:32
+msgid "Click to open/close gallery"
+msgstr "Kliknij, aby (ro)zwinąć galerię"
+
+#: templates/wiki/document_details_base.html:31
+msgid "Help"
+msgstr "Pomoc"
+
+#: templates/wiki/document_details_base.html:33
+msgid "Version"
+msgstr "Wersja"
+
+#: templates/wiki/document_details_base.html:33
+msgid "Unknown"
+msgstr "nieznana"
+
+#: templates/wiki/document_details_base.html:36
+msgid "Save attempt in progress"
+msgstr "Trwa zapisywanie"
+
+#: 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:31
+msgid "Clear filter"
+msgstr "Wyczyść filtr"
+
+#: 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:8
+msgid "Bulk documents upload"
+msgstr "Hurtowe dodawanie dokumentów"
+
+#: templates/wiki/document_upload.html:11
+msgid "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with <code>.xml</code> will be ignored."
+msgstr "Proszę wskazać archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie kończące się na <code>.xml</code> zostaną zignorowane."
+
+#: templates/wiki/document_upload.html:16
+#: templatetags/wiki.py:36
+msgid "Upload"
+msgstr "Załaduj"
+
+#: 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:24
+msgid "Offending files"
+msgstr "Błędne pliki"
+
+#: templates/wiki/document_upload.html:32
+msgid "Correct files"
+msgstr "Poprawne pliki"
+
+#: 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:44
+msgid "Uploaded files"
+msgstr "Dodane pliki"
+
+#: templates/wiki/document_upload.html:54
+msgid "Skipped files"
+msgstr "Pominięte pliki"
+
+#: templates/wiki/document_upload.html:55
+msgid "Files skipped due to no <code>.xml</code> extension"
+msgstr "Pliki pominięte z powodu braku rozszerzenia <code>.xml</code>."
+
+#: 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"
+
+#: templates/wiki/tabs/gallery_view.html:13
+msgid "Next"
+msgstr "Następne"
+
+#: templates/wiki/tabs/gallery_view.html:15
+msgid "Zoom in"
+msgstr "Powiększ"
+
+#: templates/wiki/tabs/gallery_view.html:16
+msgid "Zoom out"
+msgstr "Zmniejsz"
+
+#: templates/wiki/tabs/gallery_view_item.html:3
+msgid "Gallery"
+msgstr "Galeria"
+
+#: templates/wiki/tabs/history_view.html:5
+msgid "Compare versions"
+msgstr "Porównaj wersje"
+
+#: templates/wiki/tabs/history_view.html:7
+msgid "Mark for publishing"
+msgstr "Oznacz do publikacji"
+
+#: templates/wiki/tabs/history_view.html:9
+msgid "Revert document"
+msgstr "Przywróć wersję"
+
+#: templates/wiki/tabs/history_view.html:12
+msgid "View version"
+msgstr "Zobacz wersję"
+
+#: templates/wiki/tabs/history_view_item.html:3
+msgid "History"
+msgstr "Historia"
+
+#: templates/wiki/tabs/search_view.html:3
+#: templates/wiki/tabs/search_view.html:5
+msgid "Search"
+msgstr "Szukaj"
+
+#: templates/wiki/tabs/search_view.html:8
+msgid "Replace with"
+msgstr "Zamień na"
+
+#: templates/wiki/tabs/search_view.html:10
+msgid "Replace"
+msgstr "Zamień"
+
+#: templates/wiki/tabs/search_view.html:13
+msgid "Options"
+msgstr "Opcje"
+
+#: templates/wiki/tabs/search_view.html:15
+msgid "Case sensitive"
+msgstr "Rozróżniaj wielkość liter"
+
+#: templates/wiki/tabs/search_view.html:17
+msgid "From cursor"
+msgstr "Zacznij od kursora"
+
+#: templates/wiki/tabs/search_view_item.html:3
+msgid "Search and replace"
+msgstr "Znajdź i zamień"
+
+#: templates/wiki/tabs/source_editor_item.html:5
+msgid "Source code"
+msgstr "Kod źródłowy"
+
+#: templates/wiki/tabs/summary_view.html:9
+msgid "Title"
+msgstr "Tytuł"
+
+#: templates/wiki/tabs/summary_view.html:14
+msgid "Document ID"
+msgstr "ID dokumentu"
+
+#: templates/wiki/tabs/summary_view.html:18
+msgid "Current version"
+msgstr "Aktualna wersja"
+
+#: templates/wiki/tabs/summary_view.html:21
+msgid "Last edited by"
+msgstr "Ostatnio edytowane przez"
+
+#: templates/wiki/tabs/summary_view.html:25
+msgid "Link to gallery"
+msgstr "Link do galerii"
+
+#: templates/wiki/tabs/summary_view_item.html:3
+msgid "Summary"
+msgstr "Podsumowanie"
+
+#: templates/wiki/tabs/wysiwyg_editor.html:9
+msgid "Insert theme"
+msgstr "Wstaw motyw"
+
+#: templates/wiki/tabs/wysiwyg_editor.html:12
+msgid "Insert annotation"
+msgstr "Wstaw przypis"
+
+#: templates/wiki/tabs/wysiwyg_editor_item.html:3
+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"
--- /dev/null
+# -*- 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()
+
--- /dev/null
+# encoding: utf-8
+import datetime
+import os.path
+import cPickle
+import re
+import urllib
+
+from django.conf import settings
+from django.db import models
+from mercurial import mdiff, hg, ui
+from south.db import db
+from south.v2 import SchemaMigration
+
+from slughifi import slughifi
+
+META_REGEX = re.compile(r'\s*<!--\s(.*?)-->', re.DOTALL | re.MULTILINE)
+STAGE_TAGS_RE = re.compile(r'^#stage-finished: (.*)$', re.MULTILINE)
+AUTHOR_RE = re.compile(r'\s*(.*?)\s*<(.*)>\s*')
+
+
+def urlunquote(url):
+ """Unqotes URL
+
+ # >>> urlunquote('Za%C5%BC%C3%B3%C5%82%C4%87_g%C4%99%C5%9Bl%C4%85_ja%C5%BA%C5%84')
+ # u'Za\u017c\xf3\u0142\u0107_g\u0119\u015bl\u0105 ja\u017a\u0144'
+ """
+ return unicode(urllib.unquote(url), 'utf-8', 'ignore')
+
+
+def split_name(name):
+ parts = name.split('__')
+ return parts
+
+
+def file_to_title(fname):
+ """ Returns a title-like version of a filename. """
+ parts = (p.replace('_', ' ').title() for p in fname.split('__'))
+ return ' / '.join(parts)
+
+
+def make_patch(src, dst):
+ if isinstance(src, unicode):
+ src = src.encode('utf-8')
+ if isinstance(dst, unicode):
+ dst = dst.encode('utf-8')
+ return cPickle.dumps(mdiff.textdiff(src, dst))
+
+
+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')
+ head = orm.ChunkChange.objects.create(
+ tree=chunk,
+ revision=-1,
+ patch=make_patch('', ''),
+ created_at=datetime.datetime.fromtimestamp(entry.filectx(0).date()[0]),
+ description=''
+ )
+ chunk.head = head
+ try:
+ chunk.stage = orm.ChunkTag.objects.order_by('ordering')[0]
+ except IndexError:
+ chunk.stage = None
+ old_data = ''
+
+ maxrev = entry.filerev()
+ gallery_link = None
+
+ 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,
+ patch=make_patch(old_data, data),
+ created_at=datetime.datetime.fromtimestamp(fctx.date()[0]),
+ description=description,
+ author=author,
+ author_name=author_name,
+ author_email=author_email,
+ parent=chunk.head
+ )
+ head.tags = tags
+ chunk.head = head
+ old_data = data
+
+ chunk.save()
+ if gallery_link:
+ book.gallery = gallery_link
+ book.save()
+
+
+def migrate_from_hg(orm):
+ try:
+ hg_path = settings.WIKI_REPOSITORY_PATH
+ except:
+ pass
+
+ 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])
+
+
+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)),
+ ('last_published', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=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)),
+ ('comment', self.gf('django.db.models.fields.CharField')(max_length=255, 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)),
+ ('patch', self.gf('django.db.models.fields.TextField')(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'])),
+ ))
+ 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'])
+
+ if not db.dry_run:
+ from django.core.management import call_command
+ call_command("loaddata", "stages.json")
+
+ migrate_from_hg(orm)
+
+
+ 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')
+
+
+ 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'},
+ 'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_published': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': '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.chunk': {
+ 'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+ 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ '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'}),
+ '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'}),
+ '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']"}),
+ 'patch': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ '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.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']
--- /dev/null
+# 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 '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')(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')(to=orm['catalogue.ChunkChange'])),
+ ))
+ db.send_create_signal('catalogue', ['ChunkPublishRecord'])
+
+ # Deleting field 'Book.last_published'
+ db.delete_column('catalogue_book', 'last_published')
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'BookPublishRecord'
+ db.delete_table('catalogue_bookpublishrecord')
+
+ # Deleting model 'ChunkPublishRecord'
+ db.delete_table('catalogue_chunkpublishrecord')
+
+ # Adding field 'Book.last_published'
+ db.add_column('catalogue_book', 'last_published', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True), keep_default=False)
+
+
+ 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'},
+ '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', [], {'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'},
+ 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}),
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+ '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'}),
+ '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'}),
+ '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']"}),
+ 'patch': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ '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', [], {'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']
--- /dev/null
+# -*- 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.core.urlresolvers import reverse
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+from dvcs import models as dvcs_models
+from catalogue.xml_tools import compile_text
+
+import logging
+logger = logging.getLogger("fnp.catalogue")
+
+
+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)
+
+ 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)
+
+ class NoTextError(BaseException):
+ pass
+
+ class Meta:
+ ordering = ['parent_number', 'title']
+ verbose_name = _('book')
+ verbose_name_plural = _('books')
+ permissions = [('can_pubmark', 'Can mark for publishing')]
+
+ def __unicode__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ return reverse("catalogue_book", args=[self.slug])
+
+ @classmethod
+ def create(cls, creator=None, text=u'', *args, **kwargs):
+ """
+ >>> Book.create(slug='x', text='abc').materialize()
+ 'abc'
+ """
+ instance = cls(*args, **kwargs)
+ instance.save()
+ instance[0].commit(author=creator, text=text)
+ return instance
+
+ 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 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 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 publishable(self):
+ if not self.chunk_set.exists():
+ return False
+ for chunk in self:
+ if not chunk.publishable():
+ return False
+ return True
+
+ def publish(self, user):
+ """
+ Publishes a book on behalf of a (local) user.
+ """
+ from apiclient import api_call
+
+ changes = self.get_current_changes(publishable=True)
+ book_xml = book.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)
+
+ 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):
+ number = self[len(self) - 1].number + 1
+ single = len(other) == 1
+ for chunk in other:
+ # move chunk to new book
+ chunk.book = self
+ chunk.number = number
+
+ # 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.comment = 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.comment = "%s, %s" % (other_title_part, chunk.comment)
+ chunk.slug = self.make_chunk_slug(chunk.slug)
+ chunk.save()
+ number += 1
+ other.delete()
+
+ @staticmethod
+ def listener_create(sender, instance, created, **kwargs):
+ if created:
+ instance.chunk_set.create(number=1, slug='1')
+
+models.signals.post_save.connect(Book.listener_create, sender=Book)
+
+
+class Chunk(dvcs_models.Document):
+ """ An editable chunk of text. Every Book text is divided into chunks. """
+
+ book = models.ForeignKey(Book, editable=False)
+ number = models.IntegerField()
+ slug = models.SlugField()
+ comment = models.CharField(max_length=255, blank=True)
+
+ class Meta:
+ unique_together = [['book', 'number'], ['book', 'slug']]
+ ordering = ['number']
+
+ def __unicode__(self):
+ return "%d-%d: %s" % (self.book_id, self.number, self.comment)
+
+ def get_absolute_url(self):
+ return reverse("wiki_editor", args=[self.book.slug, self.slug])
+
+ @classmethod
+ def get(cls, slug, chunk=None):
+ if chunk is None:
+ return cls.objects.get(book__slug=slug, number=1)
+ else:
+ return cls.objects.get(book__slug=slug, slug=chunk)
+
+ def pretty_name(self, book_length=None):
+ title = self.book.title
+ if self.comment:
+ title += ", %s" % self.comment
+ if book_length > 1:
+ title += " (%d/%d)" % (self.number, book_length)
+ return title
+
+ def split(self, slug, comment='', creator=None):
+ """ Create an empty chunk after this one """
+ self.book.chunk_set.filter(number__gt=self.number).update(
+ number=models.F('number')+1)
+ new_chunk = self.book.chunk_set.create(number=self.number+1,
+ creator=creator, slug=slug, comment=comment)
+ return new_chunk
+
+ @staticmethod
+ def listener_saved(sender, instance, created, **kwargs):
+ if instance.book:
+ # save book so that its _list_html is reset
+ instance.book.save()
+
+models.signals.post_save.connect(Chunk.listener_saved, sender=Chunk)
+
+
+class BookPublishRecord(models.Model):
+ """
+ A record left after publishing a Book.
+ """
+
+ book = models.ForeignKey(Book)
+ timestamp = models.DateTimeField(auto_now_add=True)
+ user = models.ForeignKey(User)
+
+ class Meta:
+ ordering = ['-timestamp']
+
+
+class ChunkPublishRecord(models.Model):
+ """
+ BookPublishRecord details for each Chunk.
+ """
+
+ book_record = models.ForeignKey(BookPublishRecord)
+ change = models.ForeignKey(Chunk.change_model)
--- /dev/null
+{% load compressed i18n %}
+{% load catalogue %}
+<!DOCTYPE html>
+<html>
+<head>
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+ {% compressed_css 'listing' %}
+ <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}style.css" />
+ <title>{% block title %}{% trans "Platforma Redakcyjna" %}{% endblock title %}</title>
+</head>
+<body>
+
+<div id="tabs-nav">
+
+ <a href="{% url catalogue_document_list %}">
+ <img id="logo" src="{{ STATIC_URL }}img/wl-orange.png" />
+ </a>
+
+ <div id="tabs-nav-left">
+ {% main_tabs %}
+ </div>
+
+ <span id="login-box">
+ {% include "registration/head_login.html" %}
+ </span>
+
+ <div class='clr' ></div>
+</div>
+
+<div id="content">
+
+{% block content %}
+<div id="catalogue_layout_left_column">
+ {% block leftcolumn %}
+ {% endblock leftcolumn %}
+</div>
+<div id="catalogue_layout_right_column">
+ {% block rightcolumn %}
+ {% endblock rightcolumn %}
+</div>
+{% endblock content %}
+
+</div>
+
+{% compressed_js 'listing' %}
+{% block extrabody %}
+{% endblock %}
+</body>
+</html>
--- /dev/null
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+ <form enctype="multipart/form-data" method="POST" action="">
+ {% csrf_token %}
+ {{ form.as_p }}
+
+ <p><button type="submit">{% trans "Append book" %}</button></p>
+ </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
--- /dev/null
+{% extends "catalogue/base.html" %}
+{% load comments i18n %}
+
+{% block leftcolumn %}
+
+<a href="{% url catalogue_book_edit book.slug %}">{% trans "edit" %}</a>
+ <h1>{{ book.title }}</h1>
+
+<table>
+ {% for c in chunks %}
+ <tr class="chunk-{{ c.grade }}">
+ <td><a target="_blank" href="{{ c.chunk.get_absolute_url }}">{{ c.chunk.comment }}</a></td>
+ <td>{% for fix in c.fix %}
+
+ {% ifequal fix "wl" %}<span class="fix"
+ title="{% trans "add basic document structure" %}"
+ ></></span>{% endifequal %}
+
+ {% ifequal fix "bad-master" %}<span class="fix"
+ title='{% trans "change master tag to" %} "{{ first_master }}"'
+ >master</span>{% endifequal %}
+
+ {% ifequal fix "trim-begin" %}<span class="fix"
+ title="{% trans "add begin trimming tag" %}"
+ >✁</span>{% endifequal %}
+
+ {% ifequal fix "trim-end" %}<span class="fix"
+ title="{% trans "add end trimming tag" %}"
+ >✃</span>{% endifequal %}
+
+ {% endfor %}
+
+ {% ifequal c.grade "plain" %}
+ <span class="fix-info">{% trans "unstructured text" %}</span>
+ {% endifequal %}
+
+ {% ifequal c.grade "xml" %}
+ <span class="fix-info">{% trans "unknown XML" %}</span>
+ {% endifequal %}
+
+ {% ifequal c.grade "wl-broken" %}
+ <span class="fix-info">{% trans "broken document" %}</span>
+ {% endifequal %}
+
+ </td>
+ <td><a href="{% url catalogue_chunk_edit book.slug c.chunk.slug%}">[{% trans "edit" %}]</a></td>
+ <td>{% if c.chunk.publishable %}P{% endif %}</td>
+ <td>{% if c.chunk.user.is_authenticated %}
+ <a href="{% url catalogue_user c.chunk.user.username %}">{{ c.chunk.user }}</a>
+ {% endif %}</td>
+ <td><a href="{% url catalogue_chunk_add book.slug c.chunk.slug %}">[+]</a></td>
+ </tr>
+ {% endfor %}
+ {% if need_fixing %}
+ <tr><td></td><td>
+ <form method="POST" action="">
+ {% csrf_token %}
+ {% if choose_master %}
+ {{ form.master }}
+ {% endif %}
+ <button type="submit">{% trans "Apply fixes" %}</button>
+ </form>
+ </td></tr>
+ {% endif %}
+</table>
+
+<p><a href="{% url catalogue_book_append book.slug %}">{% trans "Append to other book" %}</a></p>
+
+<p>{% trans "Last published" %}: {{ book.last_published }}</p>
+
+{% if book.publishable %}
+ <p>
+ <a href="{% url catalogue_book_xml book.slug %}">{% trans "Full XML" %}</a><br/>
+ <a target="_blank" href="{% url catalogue_book_html book.slug %}">{% trans "HTML version" %}</a><br/>
+ <a href="{% url catalogue_book_txt book.slug %}">{% trans "TXT version" %}</a><br/>
+ {% comment %}
+ <a href="{% url catalogue_book_epub book.slug %}">{% trans "EPUB version" %}</a><br/>
+ <a href="{% url catalogue_book_pdf book.slug %}">{% trans "PDF version" %}</a><br/>
+ {% endcomment %}
+ </p>
+
+ <!--
+ Angel photos:
+ Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
+ mira66 (http://www.flickr.com/photos/21804434@N02/) /
+ CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
+ -->
+ <form method="POST" action="{% url catalogue_publish book.slug %}">{% csrf_token %}
+ <img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" />
+ <button id="publish-button" type="submit">
+ <span>{% trans "Publish" %}</span></button>
+ <img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" />
+ </form></p>
+{% 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 %}
--- /dev/null
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+ <form enctype="multipart/form-data" method="POST" action="">
+ {% csrf_token %}
+ {{ form.as_p }}
+
+ <p><button type="submit">{% trans "Save" %}</button></p>
+ </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
--- /dev/null
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+ <form enctype="multipart/form-data" method="POST" action="">
+ {% csrf_token %}
+ {{ form.as_p }}
+
+ <p><button type="submit">{% trans "Add chunk" %}</button></p>
+ </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
--- /dev/null
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+ <form enctype="multipart/form-data" method="POST" action="">
+ {% csrf_token %}
+ {{ form.as_p }}
+
+ <p><button type="submit">{% trans "Save" %}</button></p>
+ </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
--- /dev/null
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+ <form enctype="multipart/form-data" method="POST" action="">
+ {% csrf_token %}
+ {{ form.as_p }}
+
+ <p><button type="submit">{% trans "Create document" %}</button></p>
+ </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
--- /dev/null
+{% extends "catalogue/base.html" %}
+
+{% load i18n %}
+{% load pagination_tags %}
+{% load set_get_parameter catalogue %}
+
+{% block extrabody %}
+{{ block.super }}
+<script type="text/javascript" charset="utf-8">
+$(function() {
+ $("select.filter").change(function() {
+ window.location.href = this.value;
+ });
+});
+</script>
+{% endblock %}
+
+{% block leftcolumn %}
+
+ <form method="get" action="#">
+ <table id="file-list">
+ <thead><tr>
+ <th></th>
+ <th></th>
+ <th></th>
+ <th><select name="stage" class="filter">
+ <option value="{% set_get_parameter stage=,page= %}">- {% trans "filter by stage" %} -</option>
+ <option {% if request.GET.stage == '' %}selected="selected"
+ {% endif %}value="{% set_get_parameter stage='',page= %}">- {% trans "empty" %} -</option>
+ {% for stage in stages %}
+ <option {% if request.GET.stage == stage.slug %}selected="selected"
+ {% endif %}value="{% set_get_parameter stage=stage.slug,page= %}">{{ stage.name }}</option>
+ {% endfor %}
+ </select></th>
+
+ {% if not viewed_user %}
+ <th><select name="user" class="filter">
+ <option value="{% set_get_parameter user=,page= %}">- {% trans "filter by user" %} -</option>
+ <option {% if request.GET.user == '' %}selected="selected"
+ {% endif %}value="{% set_get_parameter user='',page= %}">- {% trans "empty" %} -</option>
+ {% for user in users %}
+ <option {% if request.GET.user == user.username %}selected="selected"
+ {% endif %}value="{% set_get_parameter user=user.username,page= %}">{{ user.first_name }} {{ user.last_name }} ({{ user.count }})</option>
+ {% endfor %}
+ </select></th>
+ {% endif %}
+
+ </tr></thead>
+
+ <tbody>
+ {% autopaginate books 100 %}
+ {% if not books %}
+ <tr><td>{% trans "No books found." %}</td></tr>
+ {% endif %}
+ {% for item in books %}
+ {% with item.book as book %}
+
+ {% ifequal item.book_length 1 %}
+ {% with item.chunks.0 as chunk %}
+ <tr>
+ <td><a target="_blank" href="{% url catalogue_book book.slug %}">[B]</a></td>
+ <td><a href="{% url catalogue_chunk_edit book.slug chunk.slug %}">[c]</a></td>
+ <td><a target="_blank"
+ href="{% url wiki_editor book.slug %}">
+ {{ book.title }}</a></td>
+ <td>{% if chunk.stage %}
+ ({{ chunk.stage }})
+ {% else %}–
+ {% endif %}</td>
+ <td>{% if chunk.user %}<a href="{% url catalogue_user chunk.user.username %}">{{ chunk.user.first_name }} {{ chunk.user.last_name }}</a>{% endif %}</td>
+ </tr>
+ {% endwith %}
+ {% else %}
+ <tr>
+ <td><a target="_blank" href="{% url catalogue_book book.slug %}">[B]</a></td>
+ <td></td>
+ <td>{{ book.title }}</td>
+ </tr>
+ {% for chunk in item.chunks %}
+ <tr>
+ <td></td>
+ <td><a href="{% url catalogue_chunk_edit book.slug chunk.slug %}">[c]</a></td>
+ <td><a target="_blank" href="{{ chunk.get_absolute_url }}">
+ <span class='chunkno'>{{ chunk.number }}.</span>
+ {{ chunk.comment }}</a></td>
+ <td>{% if chunk.stage %}
+ {{ chunk.stage }}
+ {% else %}
+ –
+ {% endif %}</td>
+ {% if not viewed_user %}
+ <td>{% if chunk.user %}
+ <a href="{% url catalogue_user chunk.user.username %}">
+ {{ chunk.user.first_name }} {{ chunk.user.last_name }}
+ </a>{% else %}
+
+ {% endif %}</td>
+ {% endif %}
+ </td></tr>
+ {% endfor %}
+ {% endifequal %}
+ {% endwith %}
+ {% endfor %}
+ <tr><td colspan="3">{% paginate %}</td></tr>
+ </tbody>
+ </table>
+ </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+ <div id="last-edited-list">
+ <h2>{% trans "Your last edited documents" %}</h2>
+ <ol>
+ {% for slugs, item in last_books %}
+ <li><a href="{% url wiki_editor slugs.0 slugs.1 %}"
+ target="_blank">{{ item.title }}</a><br/><span class="date">({{ item.time|date:"H:i:s, d/m/Y" }})</span></li>
+ {% endfor %}
+ </ol>
+ </div>
+
+ {% if viewed_user %}
+ <h2>{% trans "Recent activity for" %} {{ viewed_user }}</h2>
+ {% wall viewed_user %}
+ {% else %}
+ <h2>{% trans "Recent activity" %}</h2>
+ {% wall %}
+ {% endif %}
+{% endblock rightcolumn %}
--- /dev/null
+{% extends "catalogue/base.html" %}
+{% load i18n %}
+
+
+{% block leftcolumn %}
+
+
+<h2>{% trans "Bulk documents upload" %}</h2>
+
+<p>
+{% trans "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with <code>.xml</code> will be ignored." %}
+</p>
+
+<form enctype="multipart/form-data" method="POST" action="">
+{% csrf_token %}
+{{ form.as_p }}
+<p><button type="submit">{% trans "Upload" %}</button></p>
+</form>
+
+<hr/>
+
+{% if error_list %}
+
+ <p class='error'>{% trans "There have been some errors. No files have been added to the repository." %}
+ <h3>{% trans "Offending files" %}</h3>
+ <ul id='error-list'>
+ {% for filename, title, error in error_list %}
+ <li>{{ title }} (<code>{{ filename }}</code>): {{ error }}</li>
+ {% endfor %}
+ </ul>
+
+ {% if ok_list %}
+ <h3>{% trans "Correct files" %}</h3>
+ <ul>
+ {% for filename, slug, title in ok_list %}
+ <li>{{ title }} (<code>{{ filename }}</code>)</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+
+{% else %}
+
+ {% if ok_list %}
+ <p class='success'>{% trans "Files have been successfully uploaded to the repository." %}</p>
+ <h3>{% trans "Uploaded files" %}</h3>
+ <ul id='ok-list'>
+ {% for filename, slug, title in ok_list %}
+ <li><a href='{% url wiki_editor slug %}'>{{ title }}</a> (<code>{{ filename }})</a></li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+{% endif %}
+
+{% if skipped_list %}
+ <h3>{% trans "Skipped files" %}</h3>
+ <p>{% trans "Files skipped due to no <code>.xml</code> extension" %}</p>
+ <ul id='skipped-list'>
+ {% for filename in skipped_list %}
+ <li>{{ filename }}</li>
+ {% endfor %}
+ </ul>
+{% endif %}
+
+
+{% endblock leftcolumn %}
+
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
--- /dev/null
+{% for tab in tabs %}
+ <a {% ifequal active_tab tab.slug %}class="active" {% endifequal %}href="{{ tab.url }}">{{ tab.caption }}</a>
+{% endfor %}
--- /dev/null
+{% extends "catalogue/base.html" %}
+
+{% load i18n %}
+
+{% block leftcolumn %}
+
+<h1>{% trans "Users" %}</h1>
+
+<ul>
+{% for user in users %}
+ <li><a href="{% url catalogue_user user.username %}">
+ <span class="chunkno">{{ forloop.counter }}.</span>
+ {{ user.first_name }} {{ user.last_name }}</a>
+ ({{ user.count }})</li>
+{% endfor %}
+</ul>
+
+{% endblock leftcolumn %}
--- /dev/null
+{% load i18n %}
+{% load gravatar %}
+
+<ul class='wall' style='padding-left: 0; list-style: none;'>
+{% for item in wall %}
+ <li style='clear: left; border-top: 1px dotted gray; padding-bottom:1em; margin-bottom: 1em;'>
+ <div style='float: left;margin-right: 1em;'>
+ {% if item.get_email %}
+ {% gravatar_img_for_email item.get_email 32 %}
+ <br/>
+ {% endif %}
+
+ <!--img src='{{ STATIC_URL }}img/wall/{{ item.tag}}.png' alt='{% trans item.tag %}' /-->
+ </div>
+
+ {{ item.timestamp }}
+ <br/>
+ {% if item.user %}
+ <a href="{% url catalogue_user item.user.username %}">
+ {{ item.user.first_name }} {{ item.user.last_name }}</a>
+ <{{ item.user.email }}>
+ {% else %}
+ {{ item.user_name }}
+ {% if item.get_email %}
+ <{{ item.get_email }}>
+ {% endif %}
+ {% endif %}
+ <br/><a target="_blank" href='{{ item.url }}'>{{ item.title }}</a>
+ <br/>{{ item.summary }}
+ </li>
+{% endfor %}
+</ul>
--- /dev/null
+from __future__ import absolute_import
+
+from django.db.models import Count, Q
+from django.core.urlresolvers import reverse
+from django.contrib.comments.models import Comment
+from django.template.defaultfilters import stringfilter
+from django import template
+from django.utils.translation import ugettext as _
+
+from catalogue.models import Book, Chunk, BookPublishRecord
+
+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('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}
+
+
+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.defer('patch')
+ 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)
+ 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.title = item.book.title
+ #w.summary =
+ w.url = chunk.book.get_absolute_url()
+ 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.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=10):
+ print user
+ 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),
+ )}
--- /dev/null
+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)
--- /dev/null
+from nose.tools import *
+import wiki.models as models
+import shutil
+import tempfile
+
+
+class TestStorageBase:
+ def setUp(self):
+ self.dirpath = tempfile.mkdtemp(prefix='nosetest_')
+
+ def tearDown(self):
+ shutil.rmtree(self.dirpath)
+
+
+class TestDocumentStorage(TestStorageBase):
+
+ def test_storage_empty(self):
+ storage = models.DocumentStorage(self.dirpath)
+ eq_(storage.all(), [])
--- /dev/null
+# -*- 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<username>[^/]+)/$', 'user', name='catalogue_user'),
+ url(r'^users/$', 'users', name='catalogue_users'),
+
+ url(r'^upload/$',
+ 'upload', name='catalogue_upload'),
+
+ url(r'^create/(?P<slug>[^/]*)/',
+ 'create_missing', name='catalogue_create_missing'),
+ url(r'^create/',
+ 'create_missing', name='catalogue_create_missing'),
+
+ url(r'^book/(?P<slug>[^/]+)/publish$', 'publish', name="catalogue_publish"),
+ #url(r'^(?P<name>[^/]+)/publish/(?P<version>\d+)$', 'publish', name="catalogue_publish"),
+
+ url(r'^book/(?P<slug>[^/]+)/$', 'book', name="catalogue_book"),
+ url(r'^book/(?P<slug>[^/]+)/xml$', 'book_xml', name="catalogue_book_xml"),
+ url(r'^book/(?P<slug>[^/]+)/txt$', 'book_txt', name="catalogue_book_txt"),
+ url(r'^book/(?P<slug>[^/]+)/html$', 'book_html', name="catalogue_book_html"),
+ #url(r'^book/(?P<slug>[^/]+)/epub$', 'book_epub', name="catalogue_book_epub"),
+ #url(r'^book/(?P<slug>[^/]+)/pdf$', 'book_pdf', name="catalogue_book_pdf"),
+ url(r'^chunk_add/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
+ 'chunk_add', name="catalogue_chunk_add"),
+ url(r'^chunk_edit/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
+ 'chunk_edit', name="catalogue_chunk_edit"),
+ url(r'^book_append/(?P<slug>[^/]+)/$',
+ 'book_append', name="catalogue_book_append"),
+ url(r'^book_edit/(?P<slug>[^/]+)/$',
+ 'book_edit', name="catalogue_book_edit"),
+
+)
--- /dev/null
+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
+from django import http
+from django.http import Http404
+from django.shortcuts import get_object_or_404
+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")
+
+
+def slug_filter(qs, value, filter_field, model, model_field='slug', unset=''):
+ if value == unset:
+ return qs.filter(**{filter_field: None})
+ if value is None:
+ return qs
+ try:
+ obj = model._default_manager.get(**{model_field: value})
+ except model.DoesNotExist:
+ return qs.none()
+ else:
+ return qs.filter(**{filter_field: obj})
+
+
+@active_tab('all')
+@never_cache
+def document_list(request, filters=None):
+ chunks = Chunk.objects.order_by('book__title', 'book', 'number')
+
+ chunks = slug_filter(chunks, request.GET.get('user', None), 'user', User, 'username')
+ chunks = slug_filter(chunks, request.GET.get('stage', None), 'stage', Chunk.tag_model, 'slug')
+
+ chunks_list = helpers.ChunksList(chunks)
+
+ return direct_to_template(request, 'catalogue/document_list.html', extra_context={
+ 'books': chunks_list,
+ 'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
+ key=lambda x: x[1]['time'], reverse=True),
+ 'stages': Chunk.tag_model.objects.all(),
+ 'users': User.objects.annotate(count=Count('chunk')).order_by('-count', 'last_name', 'first_name'),
+ })
+
+
+@never_cache
+def user(request, username=None):
+ if username is None:
+ if request.user.is_authenticated():
+ user = request.user
+ else:
+ raise Http404
+ else:
+ user = get_object_or_404(User, username=username)
+
+ chunks_list = helpers.ChunksList(Chunk.objects.filter(
+ user=user).order_by('book__title', 'book', 'number'))
+
+ return direct_to_template(request, 'catalogue/document_list.html', extra_context={
+ 'books': chunks_list,
+ 'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
+ key=lambda x: x[1]['time'], reverse=True),
+ 'viewed_user': user,
+ 'stages': Chunk.tag_model.objects.all(),
+ })
+my = login_required(active_tab('my')(user))
+
+
+@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'),
+ })
+
+
+@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(creator=creator,
+ slug=form.cleaned_data['slug'],
+ title=form.cleaned_data['title'],
+ text=form.cleaned_data['text'],
+ )
+
+ 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.create(creator=creator,
+ slug=slug,
+ title=title,
+ text=zip.read(filename).decode('utf-8'),
+ )
+
+ 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'],
+ comment=form.cleaned_data['comment'],
+ )
+
+ return http.HttpResponseRedirect(doc.book.get_absolute_url())
+ else:
+ form = forms.ChunkAddForm(initial={
+ "slug": str(doc.number + 1),
+ "comment": "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())
--- /dev/null
+from functools import wraps
+import re
+
+from lxml import etree
+from catalogue.constants import TRIM_BEGIN, TRIM_END, MASTERS
+
+RE_TRIM_BEGIN = re.compile("^<!--%s-->$" % TRIM_BEGIN, re.M)
+RE_TRIM_END = re.compile("^<!--%s-->$" % TRIM_END, re.M)
+
+
+class ParseError(BaseException):
+ pass
+
+
+def 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("<a/>").is_xml()
+ True
+ >>> GradedText("<a>").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 <utwor> and a master tag.
+
+ >>> GradedText("<utwor><powiesc></powiesc></utwor>").is_wl()
+ True
+ >>> GradedText("<a></a>").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("<utwor><</utwor>").is_broken_wl()
+ True
+ >>> GradedText("some text").is_broken_wl()
+ False
+ """
+ if self.is_wl():
+ return True
+ text = self._text.strip()
+ return text.startswith('<utwor>') and text.endswith('</utwor>')
+
+ def master(self):
+ """
+ Gets the master tag.
+
+ >>> GradedText("<utwor><powiesc></powiesc></utwor>").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
+ # 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 etree.tostring(e, encoding="utf-8")
+
+
+def basic_structure(text, master):
+ e = etree.fromstring('''<utwor>
+<master>
+<!--%s--><!--%s-->
+</master>
+</utwor>''' % (TRIM_BEGIN, TRIM_END))
+ e[0].tag = master
+ e[0][0].tail = "\n"*3 + text + "\n"*3
+ return etree.tostring(e, encoding="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 etree.tostring(e, encoding="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 etree.tostring(e, encoding="utf-8")
--- /dev/null
+from datetime import datetime
+
+from django.db import models
+from django.db.models.base import ModelBase
+from django.contrib.auth.models import User
+from django.utils.translation import ugettext_lazy as _
+from mercurial import mdiff, simplemerge
+import pickle
+
+
+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']
+
+ 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)
+
+
+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 contains a pickled diff needed to reproduce the initial document.
+ """
+ author = models.ForeignKey(User, null=True, blank=True)
+ author_name = models.CharField(max_length=128, null=True, blank=True)
+ author_email = models.CharField(max_length=128, null=True, blank=True)
+ patch = models.TextField(blank=True)
+ revision = models.IntegerField(db_index=True)
+
+ parent = models.ForeignKey('self',
+ null=True, blank=True, default=None,
+ related_name="children")
+
+ merge_parent = models.ForeignKey('self',
+ null=True, blank=True, default=None,
+ related_name="merge_children")
+
+ description = models.TextField(blank=True, default='')
+ created_at = models.DateTimeField(editable=False, db_index=True,
+ default=datetime.now)
+ publishable = models.BooleanField(default=False)
+
+ class Meta:
+ abstract = True
+ ordering = ('created_at',)
+ unique_together = ['tree', 'revision']
+
+ def __unicode__(self):
+ return u"Id: %r, Tree %r, Parent %r, Patch '''\n%s'''" % (self.id, self.tree_id, self.parent_id, self.patch)
+
+ 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:
+ self.revision = self.tree.revision() + 1
+ return super(Change, self).save(*args, **kwargs)
+
+ @staticmethod
+ def make_patch(src, dst):
+ if isinstance(src, unicode):
+ src = src.encode('utf-8')
+ if isinstance(dst, unicode):
+ dst = dst.encode('utf-8')
+ return pickle.dumps(mdiff.textdiff(src, dst))
+
+ def materialize(self):
+ # special care for merged nodes
+ if self.parent is None and self.merge_parent is not None:
+ return self.apply_to(self.merge_parent.materialize())
+
+ changes = self.tree.change_set.exclude(parent=None).filter(
+ revision__lte=self.revision).order_by('revision')
+ text = ''
+ for change in changes:
+ text = change.apply_to(text)
+ return text.decode('utf-8')
+
+ def make_child(self, patch, description, author=None,
+ author_name=None, author_email=None, tags=None):
+ ch = self.children.create(patch=patch,
+ tree=self.tree, author=author,
+ author_name=author_name,
+ author_email=author_email,
+ description=description)
+ if tags is not None:
+ ch.tags = tags
+ return ch
+
+ def make_merge_child(self, patch, description, author=None,
+ author_name=None, author_email=None, tags=None):
+ ch = self.merge_children.create(patch=patch,
+ tree=self.tree, author=author,
+ author_name=author_name,
+ author_email=author_email,
+ description=description,
+ tags=tags)
+ if tags is not None:
+ ch.tags = tags
+ return ch
+
+ def apply_to(self, text):
+ return mdiff.patch(text, pickle.loads(self.patch.encode('ascii')))
+
+ def merge_with(self, other, author=None,
+ author_name=None, author_email=None,
+ description=u"Automatic merge."):
+ assert self.tree_id == other.tree_id # same tree
+ if other.parent_id == self.pk:
+ # immediate child
+ return other
+
+ local = self.materialize()
+ base = other.merge_parent.materialize()
+ remote = other.apply_to(base)
+
+ merge = simplemerge.Merge3Text(base, local, remote)
+ result = ''.join(merge.merge_lines())
+ patch = self.make_patch(local, result)
+ return self.children.create(
+ patch=patch, merge_parent=other, tree=self.tree,
+ author=author,
+ author_name=author_name,
+ author_email=author_email,
+ description=description)
+
+ def revert(self, **kwargs):
+ """ commit this version of a doc as new head """
+ self.tree.commit(text=self.materialize(), **kwargs)
+
+
+def create_tag_model(model):
+ name = model.__name__ + 'Tag'
+ attrs = {
+ '__module__': model.__module__,
+ }
+ return type(name, (Tag,), attrs)
+
+
+def create_change_model(model):
+ name = model.__name__ + 'Change'
+
+ attrs = {
+ '__module__': model.__module__,
+ 'tree': models.ForeignKey(model, related_name='change_set'),
+ 'tags': models.ManyToManyField(model.tag_model, related_name='change_set'),
+ }
+ 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,
+ null=True, blank=True).contribute_to_class(model, 'stage')
+
+ # create real Change model and `head' fk
+ model.change_model = create_change_model(model)
+ models.ForeignKey(model.change_model,
+ null=True, blank=True, default=None,
+ help_text=_("This document's current head."),
+ editable=False).contribute_to_class(model, 'head')
+
+ return model
+
+
+
+class Document(models.Model):
+ """
+ File in repository.
+ """
+ __metaclass__ = DocumentMeta
+
+ creator = models.ForeignKey(User, null=True, blank=True, editable=False,
+ related_name="created_documents")
+
+ user = models.ForeignKey(User, null=True, blank=True)
+
+ class Meta:
+ abstract = True
+
+ def __unicode__(self):
+ return u"{0}, HEAD: {1}".format(self.id, self.head_id)
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('dvcs.views.document_data', (), {
+ 'document_id': self.id,
+ 'version': self.head_id,
+ })
+
+ def materialize(self, 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, **kwargs):
+ if 'parent' not in kwargs:
+ parent = self.head
+ else:
+ parent = kwargs['parent']
+ if not isinstance(parent, Change):
+ parent = self.change_set.objects.get(pk=kwargs['parent'])
+
+ if 'patch' not in kwargs:
+ if 'text' not in kwargs:
+ raise ValueError("You must provide either patch or target document.")
+ patch = Change.make_patch(self.materialize(change=parent), kwargs['text'])
+ else:
+ if 'text' in kwargs:
+ raise ValueError("You can provide only text or patch - not both")
+ patch = kwargs['patch']
+
+ author = kwargs.get('author', None)
+ author_name = kwargs.get('author_name', None)
+ author_email = kwargs.get('author_email', None)
+ 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()
+
+ old_head = self.head
+ if parent != old_head:
+ change = parent.make_merge_child(patch, author=author,
+ author_name=author_name,
+ author_email=author_email,
+ description=kwargs.get('description', ''),
+ tags=tags)
+ # not Fast-Forward - perform a merge
+ self.head = old_head.merge_with(change, author=author,
+ author_name=author_name,
+ author_email=author_email)
+ else:
+ self.head = parent.make_child(patch, author=author,
+ author_name=author_name,
+ author_email=author_email,
+ description=kwargs.get('description', ''),
+ tags=tags)
+
+ self.save()
+ 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 if rev is not None else -1
+
+ def at_revision(self, rev):
+ if rev is not None:
+ return self.change_set.get(revision=rev)
+ else:
+ return self.head
+
+ def publishable(self):
+ changes = self.change_set.filter(publishable=True).order_by('-created_at')[:1]
+ if changes.count():
+ return changes[0]
+ else:
+ return None
+
+ @staticmethod
+ def listener_initial_commit(sender, instance, created, **kwargs):
+ # run for Document and its subclasses
+ if not isinstance(instance, Document):
+ return
+ if created:
+ instance.head = instance.change_model.objects.create(
+ revision=-1,
+ author=instance.creator,
+ patch=Change.make_patch('', ''),
+ tree=instance)
+ instance.save()
+
+models.signals.post_save.connect(Document.listener_initial_commit)
--- /dev/null
+from django.test import TestCase
+from dvcs.models import Change, Document
+from django.contrib.auth.models import User
+
+class DocumentModelTests(TestCase):
+
+ def setUp(self):
+ self.user = User.objects.create_user("tester", "tester@localhost.local")
+
+ 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 = Document.objects.create(name=u"Sample Document", creator=self.user)
+ self.assert_(doc.head is not None)
+ self.assertEqual(doc.materialize(), u"")
+
+ def test_single_commit(self):
+ doc = Document.objects.create(name=u"Sample Document", creator=self.user)
+ doc.commit(text=u"Ala ma kota", description="Commit #1", author=self.user)
+ self.assert_(doc.head is not None)
+ self.assertEqual(doc.change_set.count(), 2)
+ self.assertEqual(doc.materialize(), u"Ala ma kota")
+
+ def test_chained_commits(self):
+ doc = Document.objects.create(name=u"Sample Document", creator=self.user)
+ c1 = doc.commit(description="Commit #1", text=u"""
+ Line #1
+ Line #2 is cool
+ """, author=self.user)
+ c2 = doc.commit(description="Commit #2", text=u"""
+ Line #1
+ Line #2 is hot
+ """, author=self.user)
+ c3 = doc.commit(description="Commit #3", text=u"""
+ Line #1
+ ... is hot
+ Line #3 ate Line #2
+ """, author=self.user)
+ self.assert_(doc.head is not None)
+ self.assertEqual(doc.change_set.count(), 4)
+
+ self.assertEqual(doc.materialize(), u"""
+ Line #1
+ ... is hot
+ Line #3 ate Line #2
+ """)
+ self.assertEqual(doc.materialize(version=c3), u"""
+ Line #1
+ ... is hot
+ Line #3 ate Line #2
+ """)
+ self.assertEqual(doc.materialize(version=c2), u"""
+ Line #1
+ Line #2 is hot
+ """)
+ self.assertEqual(doc.materialize(version=c1), """
+ Line #1
+ Line #2 is cool
+ """)
+
+
+ def test_parallel_commit_noconflict(self):
+ doc = Document.objects.create(name=u"Sample Document", creator=self.user)
+ self.assert_(doc.head is not None)
+ base = doc.head
+ base = doc.commit(description="Commit #1", text=u"""
+ Line #1
+ Line #2
+""", author=self.user)
+
+ c1 = doc.commit(description="Commit #2", text=u"""
+ Line #1 is hot
+ Line #2
+""", parent=base, author=self.user)
+ self.assertTextEqual(c1.materialize(), u"""
+ Line #1 is hot
+ Line #2
+""")
+ c2 = doc.commit(description="Commit #3", text=u"""
+ Line #1
+ Line #2
+ Line #3
+""", parent=base, author=self.user)
+ self.assertEqual(doc.change_set.count(), 5)
+ self.assertTextEqual(doc.materialize(), u"""
+ Line #1 is hot
+ Line #2
+ Line #3
+""")
+
+ def test_parallel_commit_conflict(self):
+ doc = Document.objects.create(name=u"Sample Document", creator=self.user)
+ self.assert_(doc.head is not None)
+ base = doc.head
+ base = doc.commit(description="Commit #1", text=u"""
+Line #1
+Line #2
+Line #3
+""", author=self.user)
+
+ c1 = doc.commit(description="Commit #2", text=u"""
+Line #1
+Line #2 is hot
+Line #3
+""", parent=base, author=self.user)
+ c2 = doc.commit(description="Commit #3", text=u"""
+Line #1
+Line #2 is cool
+Line #3
+""", parent=base, author=self.user)
+ self.assertEqual(doc.change_set.count(), 5)
+ self.assertTextEqual(doc.materialize(), u"""
+Line #1
+<<<<<<<
+Line #2 is hot
+=======
+Line #2 is cool
+>>>>>>>
+Line #3
+""")
+
+ def test_multiply_parallel_commits(self):
+ doc = Document.objects.create(name=u"Sample Document", creator=self.user)
+ self.assert_(doc.head is not None)
+ c1 = doc.commit(description="Commit A1", text=u"""
+Line #1
+
+Line #2
+
+Line #3
+""", author=self.user)
+ c2 = doc.commit(description="Commit A2", text=u"""
+Line #1 *
+
+Line #2
+
+Line #3
+""", author=self.user)
+ c3 = doc.commit(description="Commit B1", text=u"""
+Line #1
+
+Line #2 **
+
+Line #3
+""", parent=c1, author=self.user)
+ c4 = doc.commit(description="Commit C1", text=u"""
+Line #1 *
+
+Line #2
+
+Line #3 ***
+""", parent=c2, author=self.user)
+ self.assertEqual(doc.change_set.count(), 7)
+ self.assertTextEqual(doc.materialize(), u"""
+Line #1 *
+
+Line #2 **
+
+Line #3 ***
+""")
+
--- /dev/null
+# -*- coding: utf-8
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('dvcs.views',
+ url(r'^data/(?P<document_id>[^/]+)/(?P<version>.*)$', 'document_data', name='storage_document_data'),
+)
--- /dev/null
+# Create your views here.
+from django.views.generic.simple import direct_to_template
+from django import http
+from dvcs.models import Document
+
+def document_list(request, template_name="dvcs/document_list.html"):
+ return direct_to_template(request, template_name, {
+ "documents": Document.objects.all(),
+ })
+
+def document_data(request, document_id, version=None):
+ doc = Document.objects.get(pk=document_id)
+ return http.HttpResponse(doc.materialize(version or None), content_type="text/plain")
+
+def document_history(request, docid, template_name="dvcs/document_history.html"):
+ document = Document.objects.get(pk=docid)
+ return direct_to_template(request, template_name, {
+ "document": document,
+ "changes": document.history(),
+ })
+
+++ /dev/null
-# -*- 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)
# 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:
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."),
+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):
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))
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 <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
"Language-Team: Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org.pl>\n"
"Language: \n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: constants.py:6
-msgid "First correction"
-msgstr "Autokorekta"
-
-#: constants.py:7
-msgid "Tagging"
-msgstr "Tagowanie"
-
-#: constants.py:8
-msgid "Initial Proofreading"
-msgstr "Korekta"
-
-#: constants.py:9
-msgid "Annotation Proofreading"
-msgstr "Sprawdzenie przypisów źródła"
-
-#: constants.py:10
-msgid "Modernisation"
-msgstr "Uwspółcześnienie"
-
-#: constants.py:11
-#: templates/wiki/tabs/annotations_view_item.html:3
-msgid "Annotations"
-msgstr "Przypisy"
-
-#: constants.py:12
-msgid "Themes"
-msgstr "Motywy"
-
-#: constants.py:13
-msgid "Editor's Proofreading"
-msgstr "Ostateczna redakcja literacka"
-
-#: constants.py:14
-msgid "Technical Editor's Proofreading"
-msgstr "Ostateczna redakcja techniczna"
-
-#: constants.py:18
-msgid "Ready to publish"
+#: forms.py: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"
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 <code>.xml</code> will be ignored."
msgstr "Proszę wskazać archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie kończące się na <code>.xml</code> zostaną zignorowane."
-#: templates/wiki/document_upload.html:17
+#: 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 <code>.xml</code> extension"
msgstr "Pliki pominięte z powodu braku rozszerzenia <code>.xml</code>."
-#: 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"
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"
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"
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"
-
# 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"<wiki>", u"Document created.")
-
- def delete(self, name, author, comment):
- self.vstorage.delete_page(name, author, comment)
-
- def all(self):
- return list(self.vstorage.all_pages())
-
- def history(self, title):
- def stage_desc(match):
- stage = match.group(1)
- return _("Finished stage: %s") % constants.DOCUMENT_STAGES_DICT[stage]
-
- for changeset in self.vstorage.page_history(title):
- changeset['description'] = STAGE_TAGS_RE.sub(stage_desc, changeset['description'])
- yield changeset
-
- def doc_meta(self, title, revision=None):
- return self.vstorage.page_meta(title, revision)
-
-
-
-class Document(object):
- META_REGEX = re.compile(r'\s*<!--\s(.*?)-->', re.DOTALL | re.MULTILINE)
-
- def __init__(self, storage, **kwargs):
- self.storage = storage
- for attr, value in kwargs.iteritems():
- setattr(self, attr, value)
-
- def add_tag(self, tag, revision, author):
- """ Add document specific tag """
- logger.debug("Adding tag %s to doc %s version %d", tag, self.name, revision)
- self.storage.vstorage.add_page_tag(self.name, revision, tag, user=author)
-
- @property
- def plain_text(self):
- return re.sub(self.META_REGEX, '', self.text, 1)
-
- def meta(self):
- result = {}
-
- m = re.match(self.META_REGEX, self.text)
- if m:
- for line in m.group(1).split('\n'):
- try:
- k, v = line.split(':', 1)
- result[k.strip()] = v.strip()
- except ValueError:
- continue
-
- gallery = result.get('gallery', slughifi(self.name.replace(' ', '_')))
-
- if gallery.startswith('/'):
- gallery = os.path.basename(gallery)
-
- result['gallery'] = gallery
- return result
-
- def info(self):
- return self.storage.vstorage.page_meta(self.name, self.revision)
-
-def getstorage():
- return DocumentStorage(settings.REPOSITORY_PATH)
-
-#
-# Django models
-#
-
class Theme(models.Model):
name = models.CharField(_('name'), max_length=50, unique=True)
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/'
+++ /dev/null
-{% extends "base.html" %}
-{% load compressed i18n %}
-
-{% block title %}{{ document_name }} - {{ block.super }}{% endblock %}
-
-{% block extrahead %}
-{% compressed_css 'listing' %}
-{% endblock %}
-
-{% block extrabody %}
-{% compressed_js 'listing' %}
-{% endblock %}
-
-{% block maincontent %}
-<h1><img src="{{ STATIC_URL }}img/logo.png">{% trans "Platforma Redakcyjna" %}</h1>
-<div id="wiki_layout_left_column">
- {% block leftcolumn %}
- {% endblock leftcolumn %}
-</div>
-<div id="wiki_layout_right_column">
- {% block rightcolumn %}
- {% endblock rightcolumn %}
-</div>
-{% endblock maincontent %}
\ No newline at end of file
+++ /dev/null
-{% extends "wiki/base.html" %}
-{% load i18n %}
-
-{% block leftcolumn %}
- <form enctype="multipart/form-data" method="POST" action="">
- {{ form.as_p }}
-
- <p><button type="submit">{% trans "Create document" %}</button></p>
- </form>
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
-{% endblock rightcolumn %}
\ No newline at end of file
{% include "wiki/save_dialog.html" %}
{% include "wiki/revert_dialog.html" %}
{% include "wiki/tag_dialog.html" %}
+ {% include "wiki/pubmark_dialog.html" %}
{% endblock %}
{% extends "base.html" %}
{% load toolbar_tags i18n %}
-{% block title %}{{ document.name }} - {{ block.super }}{% endblock %}
+{% block title %}{{ book.title }} - {{ block.super }}{% endblock %}
{% block extrahead %}
{% load compressed %}
{% compressed_css 'detail' %}
{% block maincontent %}
<div id="document-meta"
- data-document-name="{{ document.name }}" style="display:none">
+ data-book="{{ chunk.book.slug }}" data-chunk="{{ chunk.slug }}" style="display:none">
- {% for k, v in document_meta.items %}
- <span data-key="{{ k }}">{{ v }}</span>
- {% endfor %}
-
- {% for k, v in document_info.items %}
- <span data-key="{{ k }}">{{ v }}</span>
- {% endfor %}
+ <span data-key="gallery">{{ chunk.book.gallery }}</span>
+ <span data-key="revision">{{ revision }}</span>
+ <span data-key="diff">{{ request.GET.diff }}</span>
{% block meta-extra %} {% endblock %}
</div>
<div id="header">
- <h1><a href="{% url wiki_document_list %}"><img src="{{STATIC_URL}}icons/go-home.png"/><a href="{% url wiki_document_list %}">Strona<br>główna</a></h1>
+ <h1><a href="{% url catalogue_document_list %}"><img src="{{STATIC_URL}}icons/go-home.png"/><a href="{% url catalogue_document_list %}">Strona<br>główna</a></h1>
<div id="tools">
<a href="{{ REDMINE_URL }}projects/wl-publikacje/wiki/Pomoc" target="_blank">
{% trans "Help" %}</a>
+++ /dev/null
-{% extends "wiki/base.html" %}
-
-{% load i18n %}
-{% load wiki %}
-
-{% block extrabody %}
-{{ block.super }}
-<script type="text/javascript" charset="utf-8">
-$(function() {
- function search(event) {
- event.preventDefault();
- var expr = new RegExp(slugify($('#file-list-filter').val()), 'i');
- $('#file-list tbody tr').hide().filter(function(index) {
- return expr.test(slugify( $('a', this).attr('data-id') ));
- }).show();
- }
-
- $('#file-list-find-button').click(search).hide();
- $('#file-list-filter').bind('keyup change DOMAttrModified', search);
-});
-</script>
-{% endblock %}
-
-{% block leftcolumn %}
- <form method="get" action="#">
- <table id="file-list">
- <thead>
- <tr><th>Filtr:</th>
- <th><input autocomplete="off" name="filter" id="file-list-filter" type="text" size="40" /></th>
- <th><input type="reset" value="{% trans "Clear filter" %}" id="file-list-reset-button"/></th>
- </tr>
- </thead>
- <tbody>
- {% for doc in docs %}
- <tr>
- <td colspan="3"><a target="_blank" data-id="{{doc}}"
- href="{% url wiki_editor doc %}">{{ doc|wiki_title }}</a></td>
- <!-- placeholder </td> -->
- </tr>
- {% endfor %}
- </tbody>
- </table>
- </form>
-{% endblock leftcolumn %}
-
-{% block rightcolumn %}
- <div id="last-edited-list">
- <h2>{% trans "Your last edited documents" %}</h2>
- <ol>
- {% for name, date in last_docs %}
- <li><a href="{% url wiki_editor name %}"
- target="_blank">{{ name|wiki_title }}</a><br/><span class="date">({{ date|date:"H:i:s, d/m/Y" }})</span></li>
- {% endfor %}
- </ol>
- </div>
-{% endblock rightcolumn %}
+++ /dev/null
-{% extends "wiki/base.html" %}
-{% load i18n %}
-{% load wiki %}
-
-
-{% block leftcolumn %}
-
-
-<h2>{% trans "Bulk documents upload" %}</h2>
-
-<p>
-{% trans "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with <code>.xml</code> will be ignored." %}
-</p>
-
-<form enctype="multipart/form-data" method="POST" action="">
-{{ form.as_p }}
-<p><button type="submit">{% trans "Upload" %}</button></p>
-</form>
-
-<hr/>
-
-{% if error_list %}
-
- <p class='error'>{% trans "There have been some errors. No files have been added to the repository." %}
- <h3>{% trans "Offending files" %}</h3>
- <ul id='error-list'>
- {% for filename, title, error in error_list %}
- <li>{{title|wiki_title}} (<code>{{ filename }}</code>): {{ error }}</li>
- {% endfor %}
- </ul>
-
- {% if ok_list %}
- <h3>{% trans "Correct files" %}</h3>
- <ul>
- {% for filename, title in ok_list %}
- <li>{{title|wiki_title}} (<code>{{ filename }}</code>)</li>
- {% endfor %}
- </ul>
- {% endif %}
-
-{% else %}
-
- {% if ok_list %}
- <p class='success'>{% trans "Files have been successfully uploaded to the repository." %}</p>
- <h3>{% trans "Uploaded files" %}</h3>
- <ul id='ok-list'>
- {% for filename, title in ok_list %}
- <li><a href='{% url wiki_editor title %}'>{{ title|wiki_title }}</a> (<code>{{ filename }})</a></li>
- {% endfor %}
- </ul>
- {% endif %}
-{% endif %}
-
-{% if skipped_list %}
- <h3>{% trans "Skipped files" %}</h3>
- <p>{% trans "Files skipped due to no <code>.xml</code> extension" %}</p>
- <ul id='skipped-list'>
- {% for filename in skipped_list %}
- <li>{{ filename }}</li>
- {% endfor %}
- </ul>
-{% endif %}
-
-
-{% endblock leftcolumn %}
-
-
-{% block rightcolumn %}
-{% endblock rightcolumn %}
--- /dev/null
+{% load i18n %}
+<div id="pubmark_dialog" class="dialog" data-ui-jsclass="PubmarkDialog">
+ <form method="POST" action="#">
+ {% csrf_token %}
+ {% for field in forms.pubmark.visible_fields %}
+ <p>{{ field.label_tag }} {{ field }} <span data-ui-error-for="{{ field.name }}"> </span></p>
+ <p>{{ field.help_text }}</p>
+ {% endfor %}
+
+ {% for f in forms.pubmark.hidden_fields %}
+ {{ f }}
+ {% endfor %}
+ <p data-ui-error-for="__all__"> </p>
+
+ <p class="action_area">
+ <button type="submit" class="ok" data-ui-action="save">{% trans "Save" %}</button>
+ <button type="button" class="cancel" data-ui-action="cancel">{% trans "Cancel" %}</button>
+ </p>
+ </form>
+</div>
{% load i18n %}
<div id="revert_dialog" class="dialog" data-ui-jsclass="RevertDialog">
<form method="POST" action="">
+ {% csrf_token %}
<p>{{ forms.text_revert.comment.label }}</p>
<p class="help_text">
{{ forms.text_revert.comment.help_text}}
{% load i18n %}
<div id="save_dialog" class="dialog" data-ui-jsclass="SaveDialog">
<form method="POST" action="">
+ {% csrf_token %}
<p>{{ forms.text_save.comment.label }}</p>
<p class="help_text">
{{ forms.text_save.comment.help_text}}
<div class="toolbar">
<button type="button" id="make-diff-button"
data-enabled-when="2" disabled="disabled">{% trans "Compare versions" %}</button>
- <button type="button" id="tag-changeset-button"
- data-enabled-when="1" disabled="disabled">{% trans "Mark version" %}</button>
+ <button type="button" id="pubmark-changeset-button"
+ data-enabled-when="1" disabled="disabled">{% trans "Mark for publishing" %}</button>
<button type="button" id="doc-revert-button"
data-enabled-when="1" disabled="disabled">{% trans "Revert document" %}</button>
<button id="open-preview-button" disabled="disabled"
data-enabled-when="1"
- data-basehref="{% url wiki_editor_readonly document_name %}">{% trans "View version" %}</button>
+ data-basehref="{% url wiki_editor_readonly chunk.book.slug chunk.slug %}">{% trans "View version" %}</button>
</div>
<div id="history-view">
<br />
<span data-stub-value="author"></span>, <span data-stub-value="date"></span>
</td>
- <td data-stub-value="tag">
+ <td>
+ <div data-stub-value="publishable"></div>
+ <div data-stub-value="tag"></div>
</td>
</tr>
</tbody>
{% load toolbar_tags i18n %}
<div id="source-editor" class="editor">
- {% if not document_info.readonly %}{% toolbar %}{% endif %}
+ {% if not readonly %}{% toolbar %}{% endif %}
<textarea id="codemirror_placeholder"><br/></textarea>
- <!-- <input type="hidden" name="name" value="{{ document.name }}" />
- <input type="hidden" name="author" value="annonymous" />
- <input type="hidden" name="comment" value="no comment" />
- <input type="hidden" name="revision" value="{{ document_info.revision }}" />
- -->
-</div>
\ No newline at end of file
+</div>
{% load i18n %}
-{% load wiki %}
<div id="summary-view-editor" class="editor" style="display: none">
<!-- <div class="toolbar">
</div> -->
<h2>
<label for="title">{% trans "Title" %}:</label>
<span data-ui-editable="true" data-edit-target="meta.displayTitle"
- >{{ document.name|wiki_title }}</span>
+ >{{ chunk.pretty_name }}</span>
</h2>
<p>
<label>{% trans "Document ID" %}:</label>
- <span>{{ document.name }}</span>
+ <span>{{ chunk.book.slug }}/{{ chunk.slug }}</span>
</p>
<p>
<label>{% trans "Current version" %}:</label>
- {{ document_info.revision }} ({{document_info.date}})
+ {{ chunk.revision }} ({{ chunk.head.created_at }})
<p>
<label>{% trans "Last edited by" %}:</label>
- {{document_info.author}}
+ {{ chunk.head.author }}
</p>
<p>
<label for="gallery">{% trans "Link to gallery" %}:</label>
<span data-ui-editable="true" data-edit-target="meta.galleryLink"
- >{{ document_meta.gallery}}</span>
+ >{{ chunk.book.gallery }}</span>
</p>
<p>
<label>{% trans "Characters in document" %}:</label>
{% load i18n %}
-{% load wiki %}
<li id="SummaryPerspective" data-ui-related="summary-view-editor" data-ui-jsclass="SummaryPerspective">
<span>{% trans "Summary" %}</span>
</li>
</div>
<div class="toolbar">
- {% if not document_info.readonly %}
+ {% if not readonly %}
<button id="insert-theme-button">
{% trans "Insert theme" %}
</button>
+++ /dev/null
-{% load i18n %}
-<div id="add_tag_dialog" class="dialog" data-ui-jsclass="AddTagDialog">
- <form method="POST" action="#">
- {% for field in forms.add_tag.visible_fields %}
- <p>{{ field.label_tag }} {{ field }} <span data-ui-error-for="{{ field.name }}"> </span></p>
- <p>{{ field.help_text }}</p>
- {% endfor %}
-
- {% for f in forms.add_tag.hidden_fields %}
- {{ f }}
- {% endfor %}
- <p data-ui-error-for="__all__"> </p>
-
- <p class="action_area">
- <button type="submit" class"ok" data-ui-action="save">{% trans "Save" %}</button>
- <button type="button" class="cancel" data-ui-action="cancel">{% trans "Cancel" %}</button>
- </p>
- </form>
-</div>
+++ /dev/null
-from __future__ import absolute_import
-
-from django.template.defaultfilters import stringfilter
-from django import template
-
-register = template.Library()
-
-from wiki.models import split_name
-
-@register.filter
-@stringfilter
-def wiki_title(value):
- parts = (p.replace('_', ' ').title() for p in split_name(value))
- return ' / '.join(parts)
# -*- coding: utf-8
from django.conf.urls.defaults import *
-from django.views.generic.simple import redirect_to
-from django.conf import settings
-PART = ur"""[ ĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9\w_.-]+"""
-
urlpatterns = patterns('wiki.views',
- url(r'^$', redirect_to, {'url': 'catalogue/'}),
-
- url(r'^catalogue/$', 'document_list', name='wiki_document_list'),
- url(r'^catalogue/([^/]+)/$', 'document_list'),
- url(r'^catalogue/([^/]+)/([^/]+)/$', 'document_list'),
- url(r'^catalogue/([^/]+)/([^/]+)/([^/]+)$', 'document_list'),
-
- url(r'^(?P<name>%s)$' % PART,
+ url(r'^edit/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
'editor', name="wiki_editor"),
- url(r'^(?P<name>[^/]+)/readonly$',
+ url(r'^readonly/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
'editor_readonly', name="wiki_editor_readonly"),
- url(r'^upload/$',
- 'upload', name='wiki_upload'),
-
- url(r'^create/(?P<name>[^/]+)',
- 'create_missing', name='wiki_create_missing'),
-
- url(r'^(?P<directory>[^/]+)/gallery$',
+ url(r'^gallery/(?P<directory>[^/]+)/$',
'gallery', name="wiki_gallery"),
- url(r'^(?P<name>[^/]+)/history$',
+ url(r'^history/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
'history', name="wiki_history"),
- url(r'^(?P<name>[^/]+)/rev$',
+ url(r'^rev/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
'revision', name="wiki_revision"),
- url(r'^(?P<name>[^/]+)/text$',
+ url(r'^text/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
'text', name="wiki_text"),
- url(r'^(?P<name>[^/]+)/revert$',
+ url(r'^revert/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
'revert', name='wiki_revert'),
- url(r'^(?P<name>[^/]+)/publish$', 'publish', name="wiki_publish"),
- url(r'^(?P<name>[^/]+)/publish/(?P<version>\d+)$', 'publish', name="wiki_publish"),
-
- url(r'^(?P<name>[^/]+)/diff$', 'diff', name="wiki_diff"),
- url(r'^(?P<name>[^/]+)/tags$', 'add_tag', name="wiki_add_tag"),
-
-
+ url(r'^diff/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$', 'diff', name="wiki_diff"),
+ url(r'^pubmark/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$', 'pubmark', name="wiki_pubmark"),
+ url(r'^themes$', 'themes', name="themes"),
)
+from datetime import datetime
import os
-import functools
import logging
-logger = logging.getLogger("fnp.wiki")
from django.conf import settings
-
-from django.views.generic.simple import direct_to_template
-from django.views.decorators.http import require_POST, require_GET
from django.core.urlresolvers import reverse
-from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
- ajax_require_permission, recursive_groupby)
from django import http
-
-from wiki.models import getstorage, DocumentNotFound, normalize_name, split_name, join_name, Theme
-from wiki.forms import DocumentTextSaveForm, DocumentTextRevertForm, DocumentTagForm, DocumentCreateForm, DocumentsUploadForm
-from datetime import datetime
+from django.http import Http404
+from django.middleware.gzip import GZipMiddleware
+from django.utils.decorators import decorator_from_middleware
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _
-from django.utils.decorators import decorator_from_middleware
-from django.middleware.gzip import GZipMiddleware
+from django.views.decorators.http import require_POST, require_GET
+from django.views.generic.simple import direct_to_template
+from catalogue.models import Book, Chunk
+import nice_diff
+from wiki import forms
+from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
+ ajax_require_permission)
+from wiki.models import Theme
#
# Quick hack around caching problems, TODO: use ETags
#
from django.views.decorators.cache import never_cache
-import wlapi
-import nice_diff
-import operator
+logger = logging.getLogger("fnp.wiki")
MAX_LAST_DOCS = 10
-def normalized_name(view):
-
- @functools.wraps(view)
- def decorated(request, name, *args):
- normalized = normalize_name(name)
- logger.debug('View check %r -> %r', name, normalized)
-
- if normalized != name:
- return http.HttpResponseRedirect(
- reverse('wiki_' + view.__name__, kwargs={'name': normalized}))
-
- return view(request, name, *args)
-
- return decorated
-
-
-@never_cache
-def document_list(request):
- return direct_to_template(request, 'wiki/document_list.html', extra_context={
- 'docs': getstorage().all(),
- 'last_docs': sorted(request.session.get("wiki_last_docs", {}).items(),
- key=operator.itemgetter(1), reverse=True),
- })
-
-
@never_cache
-@normalized_name
-def editor(request, name, template_name='wiki/document_details.html'):
- storage = getstorage()
-
+def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
try:
- document = storage.get(name)
- except DocumentNotFound:
- return http.HttpResponseRedirect(reverse("wiki_create_missing", args=[name]))
+ chunk = Chunk.get(slug, chunk)
+ except Chunk.MultipleObjectsReturned:
+ # TODO: choice page
+ raise Http404
+ except Chunk.DoesNotExist:
+ if chunk is None:
+ try:
+ book = Book.objects.get(slug=slug)
+ except Book.DoesNotExist:
+ return http.HttpResponseRedirect(reverse("catalogue_create_missing", args=[slug]))
+ else:
+ raise Http404
access_time = datetime.now()
- last_documents = request.session.get("wiki_last_docs", {})
- last_documents[name] = access_time
+ last_books = request.session.get("wiki_last_books", {})
+ last_books[slug, chunk.slug] = {
+ 'time': access_time,
+ 'title': chunk.pretty_name(),
+ }
- if len(last_documents) > MAX_LAST_DOCS:
- oldest_key = min(last_documents, key=last_documents.__getitem__)
- del last_documents[oldest_key]
- request.session['wiki_last_docs'] = last_documents
+ if len(last_books) > MAX_LAST_DOCS:
+ oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
+ del last_books[oldest_key]
+ request.session['wiki_last_books'] = last_books
return direct_to_template(request, template_name, extra_context={
- 'document': document,
- 'document_name': document.name,
- 'document_info': document.info,
- 'document_meta': document.meta,
+ 'chunk': chunk,
'forms': {
- "text_save": DocumentTextSaveForm(prefix="textsave"),
- "text_revert": DocumentTextRevertForm(prefix="textrevert"),
- "add_tag": DocumentTagForm(prefix="addtag"),
+ "text_save": forms.DocumentTextSaveForm(prefix="textsave"),
+ "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
+ "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
},
'REDMINE_URL': settings.REDMINE_URL,
})
@require_GET
-@normalized_name
-def editor_readonly(request, name, template_name='wiki/document_details_readonly.html'):
- name = normalize_name(name)
- storage = getstorage()
-
+def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
try:
+ chunk = Chunk.get(slug, chunk)
revision = request.GET['revision']
- document = storage.get(name, revision)
- except (KeyError, DocumentNotFound):
- raise http.Http404
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
+ raise Http404
access_time = datetime.now()
- last_documents = request.session.get("wiki_last_docs", {})
- last_documents[name] = access_time
+ last_books = request.session.get("wiki_last_books", {})
+ last_books[slug, chunk.slug] = {
+ 'time': access_time,
+ 'title': chunk.book.title,
+ }
- if len(last_documents) > MAX_LAST_DOCS:
- oldest_key = min(last_documents, key=last_documents.__getitem__)
- del last_documents[oldest_key]
- request.session['wiki_last_docs'] = last_documents
+ if len(last_books) > MAX_LAST_DOCS:
+ oldest_key = min(last_books, key=lambda x: last_books[x]['time'])
+ del last_books[oldest_key]
+ request.session['wiki_last_books'] = last_books
return direct_to_template(request, template_name, extra_context={
- 'document': document,
- 'document_name': document.name,
- 'document_info': dict(document.info(), readonly=True),
- 'document_meta': document.meta,
+ 'chunk': chunk,
+ 'revision': revision,
+ 'readonly': True,
'REDMINE_URL': settings.REDMINE_URL,
})
-@normalized_name
-def create_missing(request, name):
- storage = getstorage()
-
- if request.method == "POST":
- form = DocumentCreateForm(request.POST, request.FILES)
- if form.is_valid():
- doc = storage.create_document(
- name=form.cleaned_data['id'],
- text=form.cleaned_data['text'],
- )
-
- return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.name]))
- else:
- form = DocumentCreateForm(initial={
- "id": name.replace(" ", "_"),
- "title": name.title(),
- })
-
- return direct_to_template(request, "wiki/document_create_missing.html", extra_context={
- "document_name": name,
- "form": form,
- })
-
-
-def upload(request):
- storage = getstorage()
-
- if request.method == "POST":
- form = DocumentsUploadForm(request.POST, request.FILES)
- if form.is_valid():
- zip = form.cleaned_data['zip']
- skipped_list = []
- ok_list = []
- error_list = []
- titles = {}
- existing = storage.all()
- for filename in zip.namelist():
- if filename[-1] == '/':
- continue
- title = normalize_name(os.path.basename(filename)[:-4])
- if not (title and filename.endswith('.xml')):
- skipped_list.append(filename)
- elif title in titles:
- error_list.append((filename, title, _('Title already used for %s' % titles[title])))
- elif title in existing:
- error_list.append((filename, title, _('Title already used in repository.')))
- else:
- try:
- zip.read(filename).decode('utf-8') # test read
- ok_list.append((filename, title))
- except UnicodeDecodeError:
- error_list.append((filename, title, _('File should be UTF-8 encoded.')))
- titles[title] = filename
-
- if not error_list:
- for filename, title in ok_list:
- storage.create_document(
- name=title,
- text=zip.read(filename).decode('utf-8')
- )
-
- return direct_to_template(request, "wiki/document_upload.html", extra_context={
- "form": form,
- "ok_list": ok_list,
- "skipped_list": skipped_list,
- "error_list": error_list,
- })
- #doc = storage.create_document(
- # name=base,
- # text=form.cleaned_data['text'],
-
-
- return http.HttpResponse('\n'.join(yeslist) + '\n\n' + '\n'.join(nolist))
- else:
- form = DocumentsUploadForm()
-
- return direct_to_template(request, "wiki/document_upload.html", extra_context={
- "form": form,
- })
-
-
@never_cache
-@normalized_name
@decorator_from_middleware(GZipMiddleware)
-def text(request, name):
- storage = getstorage()
+def text(request, slug, chunk=None):
+ try:
+ doc = Chunk.get(slug, chunk)
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
if request.method == 'POST':
- form = DocumentTextSaveForm(request.POST, prefix="textsave")
+ form = forms.DocumentTextSaveForm(request.POST, prefix="textsave")
if form.is_valid():
- revision = form.cleaned_data['parent_revision']
- document = storage.get_or_404(name, revision)
- document.text = form.cleaned_data['text']
- comment = form.cleaned_data['comment']
- if form.cleaned_data['stage_completed']:
- comment += '\n#stage-finished: %s\n' % form.cleaned_data['stage_completed']
if request.user.is_authenticated():
- author_name = request.user
- author_email = request.user.email
+ author = request.user
else:
- author_name = form.cleaned_data['author_name']
- author_email = form.cleaned_data['author_email']
- author = "%s <%s>" % (author_name, author_email)
- storage.put(document, author=author, comment=comment, parent=revision)
- document = storage.get(name)
+ author = None
+ text = form.cleaned_data['text']
+ parent_revision = form.cleaned_data['parent_revision']
+ parent = doc.at_revision(parent_revision)
+ stage = form.cleaned_data['stage_completed']
+ tags = [stage] if stage else []
+ doc.commit(author=author,
+ text=text,
+ parent=parent,
+ description=form.cleaned_data['comment'],
+ tags=tags,
+ )
+ revision = doc.revision()
return JSONResponse({
- 'text': document.plain_text if revision != document.revision else None,
- 'meta': document.meta(),
- 'revision': document.revision,
+ 'text': doc.materialize() if parent_revision != revision else None,
+ 'meta': {},
+ 'revision': revision,
})
else:
return JSONFormInvalid(form)
else:
revision = request.GET.get("revision", None)
-
+
try:
- try:
- revision = revision and int(revision)
- logger.info("Fetching %s", revision)
- document = storage.get(name, revision)
- except ValueError:
- # treat as a tag
- logger.info("Fetching tag %s", revision)
- document = storage.get_by_tag(name, revision)
- except DocumentNotFound:
- raise http.Http404
+ revision = int(revision)
+ except (ValueError, TypeError):
+ revision = None
return JSONResponse({
- 'text': document.plain_text,
- 'meta': document.meta(),
- 'revision': document.revision,
+ 'text': doc.at_revision(revision).materialize(),
+ 'meta': {},
+ 'revision': revision if revision else doc.revision(),
})
@never_cache
-@normalized_name
@require_POST
-def revert(request, name):
- storage = getstorage()
- form = DocumentTextRevertForm(request.POST, prefix="textrevert")
+def revert(request, slug, chunk=None):
+ form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
if form.is_valid():
- print 'valid'
+ try:
+ doc = Chunk.get(slug, chunk)
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
+
revision = form.cleaned_data['revision']
comment = form.cleaned_data['comment']
comment += "\n#revert to %s" % revision
if request.user.is_authenticated():
- author_name = request.user
- author_email = request.user.email
+ author = request.user
else:
- author_name = form.cleaned_data['author_name']
- author_email = form.cleaned_data['author_email']
- author = "%s <%s>" % (author_name, author_email)
-
- before = storage.get(name).revision
- logger.info("Reverting %s to %s", name, revision)
- storage.revert(name, revision, comment=comment, author=author)
- logger.info("Fetching %s", name)
- document = storage.get(name)
+ author = None
+ before = doc.revision()
+ logger.info("Reverting %s to %s", slug, revision)
+ doc.at_revision(revision).revert(author=author, description=comment)
return JSONResponse({
- 'text': document.plain_text if before != document.revision else None,
- 'meta': document.meta(),
- 'revision': document.revision,
+ 'text': doc.materialize() if before != doc.revision() else None,
+ 'meta': {},
+ 'revision': doc.revision(),
})
else:
- print 'invalid'
return JSONFormInvalid(form)
@never_cache
-@normalized_name
-def diff(request, name):
- storage = getstorage()
-
+def diff(request, slug, chunk=None):
revA = int(request.GET.get('from', 0))
revB = int(request.GET.get('to', 0))
if revB == 0:
revB = None
- docA = storage.get_or_404(name, int(revA))
- docB = storage.get_or_404(name, int(revB))
+ try:
+ doc = Chunk.get(slug, chunk)
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
+ docA = doc.at_revision(revA).materialize()
+ docB = doc.at_revision(revB).materialize()
- return http.HttpResponse(nice_diff.html_diff_table(docA.plain_text.splitlines(),
- docB.plain_text.splitlines(), context=3))
+ return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
+ docB.splitlines(), context=3))
@never_cache
-@normalized_name
-def revision(request, name):
- storage = getstorage()
-
+def revision(request, slug, chunk=None):
try:
- return http.HttpResponse(str(storage.doc_meta(name)['revision']))
- except DocumentNotFound:
- raise http.Http404
+ doc = Chunk.get(slug, chunk)
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
+ return http.HttpResponse(str(doc.revision()))
@never_cache
-@normalized_name
-def history(request, name):
- storage = getstorage()
-
+def history(request, slug, chunk=None):
# TODO: pagination
- changesets = list(storage.history(name))
-
- return JSONResponse(changesets)
+ try:
+ doc = Chunk.get(slug, chunk)
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
+
+ changes = []
+ for change in doc.history().order_by('-created_at'):
+ changes.append({
+ "version": change.revision,
+ "description": change.description,
+ "author": change.author_str(),
+ "date": change.created_at,
+ "publishable": "Publishable\n" if change.publishable else "",
+ "tag": ',\n'.join(unicode(tag) for tag in change.tags.all()),
+ })
+ return JSONResponse(changes)
@require_POST
-@ajax_require_permission('wiki.can_change_tags')
-def add_tag(request, name):
- name = normalize_name(name)
- storage = getstorage()
-
- form = DocumentTagForm(request.POST, prefix="addtag")
+@ajax_require_permission('wiki.can_pubmark')
+def pubmark(request, slug, chunk=None):
+ form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
if form.is_valid():
- doc = storage.get_or_404(form.cleaned_data['id'])
- doc.add_tag(tag=form.cleaned_data['tag'],
- revision=form.cleaned_data['revision'],
- author=request.user.username)
- return JSONResponse({"message": _("Tag added")})
+ try:
+ doc = Chunk.get(slug, chunk)
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
+
+ revision = form.cleaned_data['revision']
+ publishable = form.cleaned_data['publishable']
+ change = doc.at_revision(revision)
+ if publishable != change.publishable:
+ change.publishable = publishable
+ change.save()
+ return JSONResponse({"message": _("Revision marked")})
+ else:
+ return JSONResponse({"message": _("Nothing changed")})
else:
return JSONFormInvalid(form)
-@require_POST
-@ajax_require_permission('wiki.can_publish')
-def publish(request, name):
- name = normalize_name(name)
-
- storage = getstorage()
- document = storage.get_by_tag(name, "ready_to_publish")
-
- api = wlapi.WLAPI(**settings.WL_API_CONFIG)
-
- try:
- return JSONResponse({"result": api.publish_book(document)})
- except wlapi.APICallException, e:
- return JSONServerError({"message": str(e)})
-
-
def themes(request):
prefix = request.GET.get('q', '')
return http.HttpResponse('\n'.join([str(t) for t in Theme.objects.filter(name__istartswith=prefix)]))
--- /dev/null
+Subproject commit 28878296bacad453735a520f350ba5a971f8ffc8
sys.path = [
'$APP_DIR',
'$APP_DIR/lib',
+ '$APP_DIR/lib/librarian',
'$APP_DIR/apps',
] + sys.path
sys.path += [os.path.realpath(os.path.join(*x)) for x in (
(PROJECT_ROOT, '..'),
(PROJECT_ROOT, '..', 'apps'),
+ (PROJECT_ROOT, '..', 'lib/librarian'),
(PROJECT_ROOT, '..', 'lib')
)]
from __future__ import absolute_import
+from os import path
from redakcja.settings.common import *
-DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
-DATABASE_NAME = PROJECT_ROOT + '/dev.sqlite' # Or path to database file if using sqlite3.
-DATABASE_USER = '' # Not used with sqlite3.
-DATABASE_PASSWORD = '' # Not used with sqlite3.
-DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
-DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+ 'NAME': path.join(PROJECT_ROOT, 'dev.sqlite'), # Or path to database file if using sqlite3.
+ 'USER': '', # Not used with sqlite3.
+ 'PASSWORD': '', # Not used with sqlite3.
+ 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
+ 'PORT': '', # Set to empty string for default. Not used with sqlite3.
+ }
+}
try:
LOGGING_CONFIG_FILE
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True
+USE_L10N = True
+
# Absolute path to the directory that holds media.
# Example: "/home/media/media.lawrence.com/"
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
- 'django.template.loaders.filesystem.load_template_source',
- 'django.template.loaders.app_directories.load_template_source',
-# 'django.template.loaders.eggs.load_template_source',
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
)
TEMPLATE_CONTEXT_PROCESSORS = (
- "django.core.context_processors.auth",
+ "django.contrib.auth.context_processors.auth",
"django.core.context_processors.debug",
"django.core.context_processors.i18n",
"redakcja.context_processors.settings", # this is instead of media
+ 'django.core.context_processors.csrf',
"django.core.context_processors.request",
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django_cas.middleware.CASMiddleware',
'django.middleware.doc.XViewMiddleware',
+ 'pagination.middleware.PaginationMiddleware',
'maintenancemode.middleware.MaintenanceModeMiddleware',
)
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
- 'django_cas.backends.CASBackend',
)
ROOT_URLCONF = 'redakcja.urls'
FIREPYTHON_LOGGER_NAME = "fnp"
-#
-# Central Auth System
-#
-## Set this to where the CAS server lives
-# CAS_SERVER_URL = "http://cas.fnp.pl/
-CAS_LOGOUT_COMPLETELY = True
-
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sites',
'django.contrib.admin',
'django.contrib.admindocs',
+ 'django.contrib.comments',
- 'django_cas',
'compress',
'south',
'sorl.thumbnail',
'filebrowser',
+ 'pagination',
+ 'gravatar',
+ 'catalogue',
+ 'dvcs',
'wiki',
'toolbar',
+ 'apiclient',
)
+LOGIN_REDIRECT_URL = '/documents/user'
+
+
FILEBROWSER_URL_FILEBROWSER_MEDIA = STATIC_URL + 'filebrowser/'
FILEBROWSER_DIRECTORY = 'images/'
FILEBROWSER_ADMIN_VERSIONS = []
from redakcja.settings.compress import *
except ImportError:
pass
+
# dialogs
'js/wiki/dialog_save.js',
'js/wiki/dialog_revert.js',
- 'js/wiki/dialog_addtag.js',
+ 'js/wiki/dialog_pubmark.js',
# views
'js/wiki/view_history.js',
*/
body {
- background-color: #84BF2A;
+ margin: 0;
+ font-family: verdana, sans-serif;
+ font-size: 10px;
}
-#content {
- background: #EFEFEF;
- border: 1px solid black;
- padding: 0.5em 2em;
- margin: 1em;
- overflow: hidden;
+
+.clr {
+ clear: both;
+}
+
+#tabs-nav {
+ padding: 5px 5px 0 10px;
+ background: #ffdfbf;
+ border-bottom: 1px solid #ff8000;
+ position: relative;
+}
+
+#tabs-nav-left {
+ margin-left: 60px;
+}
+
+#tabs-nav-left a {
+ display: block;
+ float: left;
+ padding: 5px 20px 5px 20px;
+ margin-bottom: -1px;
+ border-width: 1px;
+ border-style: solid;
+ border-color: rgba(0,0,0,0);
}
-#content h1 img {
- vertical-align: middle;
+#tabs-nav-left .active {
+ background: white;
+ border-color: #ff8000 #ff8000 white #ff8000;
}
-#content h1 {
- border-bottom: 2px solid black;
- padding: 0.5em;
- font-size: 2opt;
- font-family: sans-serif;
+#login-box {
+ float: right;
}
-#file-list {
+#logo {
+ position: absolute;
+ bottom: 0;
+}
+
+#content {
+ padding: 10px;
+}
+
+
+
+#catalogue_layout_left_column {
overflow: visible;
float: left;
- max-width: 50%;
+ /*max-width: 50%;*/
padding-right: 2%;
border-right: 1px dashed black;
}
-#last-edited-list {
+#catalogue_layout_right_column {
float: left;
max-width: 35%;
margin-left: 5%;
}
a, a:visited, a:active {
- color: blue;
+ color: #bf6000;
text-decoration: none;
}
}
-#loading-overlay {
- display: none;
-}
.error {
color: red;
#skipped-list {
color: #666;
-}
\ No newline at end of file
+}
+
+.chunkno {
+ font-size: .7em;
+ padding-left: 2em;
+}
+
+td {
+ vertical-align: top;
+}
+
+
+.fix {
+ border: 1px solid gray;
+ font-size: 0.7em;
+ padding: 3px;
+}
+
+.fix-info {
+ font-size: 0.7em;
+ font-style: italic;
+}
+
+.chunk-plain a {color: gray;}
+.chunk-xml a {color: gray; font-style: italic;}
+.chunk-wl-broken a {color: red;}
+.chunk-wl a {color: green;}
+.chunk-wl-fix a {color: black;}
+
+
+/* Big cheesy publish button */
+#publish-button {
+ color: black;
+ border: 2px solid black;
+ border-radius: 20px;
+ box-shadow: 0px 0px 15px #88f;
+ /*moz-border-radius: 20px;
+ -moz-box-shadow: 10px 10px 5px #888;*/
+ font-size:1.5em;
+ padding: 1em;
+ background: -moz-linear-gradient(top, #fff, #44f);
+ -moz-transition: all 0.5s ease-in-out;
+ margin: 20px;
+}
+
+#publish-button:hover {
+ -moz-transition: all 0.5s ease-in-out;
+ -moz-transform: scale(1.1);
+ background: -moz-linear-gradient(top, #fff, #88f);
+ -moz-box-shadow: 0px 0px 30px #ff8;
+}
+
+#publish-button:active {
+ background: -moz-linear-gradient(top, #88f, #fff);
+}
+++ /dev/null
-/*
- * Dialog for saving document to the server
- *
- */
-(function($){
-
- function AddTagDialog(element, options){
- if (!options.revision && options.revision != 0)
- throw "AddTagDialog needs a revision number.";
-
- this.ctx = $.wiki.exitContext();
- this.clearForm();
-
- /* fill out hidden fields */
- this.$form = $('form', element);
-
- $("input[name='addtag-id']", this.$form).val(CurrentDocument.id);
- $("input[name='addtag-revision']", this.$form).val(options.revision);
-
- $.wiki.cls.GenericDialog.call(this, element);
- };
-
- AddTagDialog.prototype = $.extend(new $.wiki.cls.GenericDialog(), {
- cancelAction: function(){
- $.wiki.enterContext(this.ctx);
- this.hide();
- },
-
- saveAction: function(){
- var self = this;
-
- self.$elem.block({
- message: "Dodawanie tagu",
- fadeIn: 0,
- });
-
- CurrentDocument.setTag({
- form: self.$form,
- success: function(doc, changed, info){
- self.$elem.block({
- message: info,
- timeout: 2000,
- fadeOut: 0,
- onUnblock: function(){
- self.hide();
- $.wiki.enterContext(self.ctx);
- }
- });
- },
- failure: function(doc, info){
- console.log("Failure", info);
- self.reportErrors(info);
- self.$elem.unblock();
- }
- });
- }
- });
-
- /* make it global */
- $.wiki.cls.AddTagDialog = AddTagDialog;
-})(jQuery);
--- /dev/null
+/*
+ * Dialog for marking document for publishing
+ *
+ */
+(function($){
+
+ function PubmarkDialog(element, options){
+ if (!options.revision && options.revision != 0)
+ throw "PubmarkDialog needs a revision number.";
+
+ this.ctx = $.wiki.exitContext();
+ this.clearForm();
+
+ /* fill out hidden fields */
+ this.$form = $('form', element);
+
+ $("input[name='pubmark-id']", this.$form).val(CurrentDocument.id);
+ $("input[name='pubmark-revision']", this.$form).val(options.revision);
+
+ $.wiki.cls.GenericDialog.call(this, element);
+ };
+
+ PubmarkDialog.prototype = $.extend(new $.wiki.cls.GenericDialog(), {
+ cancelAction: function(){
+ $.wiki.enterContext(this.ctx);
+ this.hide();
+ },
+
+ saveAction: function(){
+ var self = this;
+
+ self.$elem.block({
+ message: "Oznaczanie wersji",
+ fadeIn: 0,
+ });
+
+ CurrentDocument.pubmark({
+ form: self.$form,
+ success: function(doc, changed, info){
+ self.$elem.block({
+ message: info,
+ timeout: 2000,
+ fadeOut: 0,
+ onUnblock: function(){
+ self.hide();
+ $.wiki.enterContext(self.ctx);
+ }
+ });
+ },
+ failure: function(doc, info){
+ console.log("Failure", info);
+ self.reportErrors(info);
+ self.$elem.unblock();
+ }
+ });
+ }
+ });
+
+ /* make it global */
+ $.wiki.cls.PubmarkDialog = PubmarkDialog;
+})(jQuery);
return _finalize(failure);
html2text({
- element: $('#html-view div').get(0),
+ element: $('#html-view').get(0),
+ stripOuter: true,
success: function(text){
self.doc.setText(text);
_finalize(success);
options.callback = function() {
var self = this;
+ if (CurrentDocument.diff) {
+ rev_from = CurrentDocument.diff[0];
+ rev_to = CurrentDocument.diff[1];
+ this.doc.fetchDiff({
+ from: rev_from,
+ to: rev_to,
+ success: function(doc, data){
+ var result = $.wiki.newTab(doc, ''+rev_from +' -> ' + rev_to, 'DiffPerspective');
+
+ $(result.view).html(data);
+ $.wiki.switchToTab(result.tab);
+ }
+ });
+ }
// first time page is rendered
$('#make-diff-button').click(function() {
self.makeDiff();
});
- $('#tag-changeset-button').click(function() {
- self.showTagForm();
+ $('#pubmark-changeset-button').click(function() {
+ self.showPubmarkForm();
});
$('#doc-revert-button').click(function() {
stub: $stub,
data: this,
filters: {
- tag: function(value) {
- return tags.filter("*[value='"+value+"']").text();
- }
+// tag: function(value) {
+// return tags.filter("*[value='"+value+"']").text();
+// }
// description: function(value) {
// return value.replace('\n', ');
// }
});
};
- HistoryPerspective.prototype.showTagForm = function(){
+ HistoryPerspective.prototype.showPubmarkForm = function(){
var selected = $('#changes-list .entry.selected');
if (selected.length != 1) {
}
var version = parseInt($("*[data-stub-value='version']", selected[0]).text());
- $.wiki.showDialog('#add_tag_dialog', {'revision': version});
+ $.wiki.showDialog('#pubmark_dialog', {'revision': version});
};
HistoryPerspective.prototype.makeDiff = function() {
*/
function reverse() {
var vname = arguments[0];
- var base_path = "/documents";
+ var base_path = "/editor";
if (vname == "ajax_document_text") {
- var path = "/" + arguments[1] + "/text";
+ var path = "/text/" + arguments[1] + '/';
if (arguments[2] !== undefined)
- path += "/" + arguments[2];
+ path += arguments[2] + '/';
return base_path + path;
}
if (vname == "ajax_document_revert") {
- return base_path + "/" + arguments[1] + "/revert";
+ return base_path + "/revert/" + arguments[1] + '/';
}
if (vname == "ajax_document_history") {
- return base_path + "/" + arguments[1] + "/history";
+ return base_path + "/history/" + arguments[1] + '/';
}
if (vname == "ajax_document_gallery") {
- return base_path + "/" + arguments[1] + "/gallery";
+ return base_path + "/gallery/" + arguments[1] + '/';
}
if (vname == "ajax_document_diff")
- return base_path + "/" + arguments[1] + "/diff";
+ return base_path + "/diff/" + arguments[1] + '/';
if (vname == "ajax_document_rev")
- return base_path + "/" + arguments[1] + "/rev";
+ return base_path + "/rev/" + arguments[1] + '/';
- if (vname == "ajax_document_addtag")
- return base_path + "/" + arguments[1] + "/tags";
-
- if (vname == "ajax_publish")
- return base_path + "/" + arguments[1] + "/publish";
+ if (vname == "ajax_document_pubmark")
+ return base_path + "/pubmark/" + arguments[1] + '/';
console.log("Couldn't reverse match:", vname);
return "/404.html";
*/
function WikiDocument(element_id) {
var meta = $('#' + element_id);
- this.id = meta.attr('data-document-name');
+ this.id = meta.attr('data-book') + '/' + meta.attr('data-chunk');
this.revision = $("*[data-key='revision']", meta).text();
this.readonly = !!$("*[data-key='readonly']", meta).text();
this.galleryLink = $("*[data-key='gallery']", meta).text();
+
+ var diff = $("*[data-key='diff']", meta).text();
+ diff = diff.split(',');
+ if (diff.length == 2 && diff[0] < diff[1])
+ this.diff = diff;
+ else if (diff.length == 1) {
+ diff = parseInt(diff);
+ if (diff != NaN)
+ this.diff = [diff - 1, diff];
+ }
+
this.galleryImages = [];
this.text = null;
this.has_local_changes = false;
data[this.name] = this.value;
});
- var metaComment = '<!--';
- metaComment += '\n\tgallery:' + self.galleryLink;
- metaComment += '\n-->\n'
-
- data['textsave-text'] = metaComment + self.text;
+ data['textsave-text'] = self.text;
$.ajax({
url: reverse("ajax_document_text", self.id),
}
});
};
- WikiDocument.prototype.setTag = function(params) {
+
+ WikiDocument.prototype.pubmark = function(params) {
params = $.extend({}, noops, params);
var self = this;
var data = {
- "addtag-id": self.id,
+ "pubmark-id": self.id,
};
/* unpack form */
});
$.ajax({
- url: reverse("ajax_document_addtag", self.id),
+ url: reverse("ajax_document_pubmark", self.id),
type: "POST",
dataType: "json",
data: data,
if (!xml2htmlStylesheet) {
$.blockUI({message: 'Ładowanie arkuszy stylów...'});
$.ajax({
- url: STATIC_URL + 'xsl/wl2html_client.xsl?20110112',
+ url: STATIC_URL + 'xsl/wl2html_client.xsl?20110520',
dataType: 'xml',
timeout: 10000,
success: function(data) {
{
if (typeof withThemes.canon == 'undefined') {
$.ajax({
- url: '/themes',
+ url: '/editor/themes',
dataType: 'text',
success: function(data) {
withThemes.canon = data.split('\n');
source.text('');
options.error(error.text(), source_text);
} else {
- options.success(doc.firstChild);
+ options.success(doc.childNodes);
withThemes(function(canonThemes) {
if (canonThemes != null) {
self.result += text_buffer;
text_buffer = token.node.nodeValue.replace('&', '&').replace('<', '<');
break;
+ case COMMENT_NODE:
+ self.result += text_buffer;
+ text_buffer = '';
+ self.result += '<!--' + token.node.nodeValue + '-->';
+ break;
};
};
self.result += text_buffer;
</xsl:apply-templates>
</span>
</xsl:template>
-</xsl:stylesheet>
\ No newline at end of file
+
+ <xsl:template match="comment()">
+ <xsl:comment><xsl:value-of select="."/></xsl:comment>
+ </xsl:template>
+</xsl:stylesheet>
-{% extends "base.html" %}
+{% extends "catalogue/base.html" %}
{% block subtitle %} - Logowanie {% endblock subtitle %}
-{% block maincontent %}
+{% block content %}
<div class="isection">
-<form method="POST" action="{% url django.contrib.auth.views.login %}">
+<form method="POST" action="{% url login %}">
+{% csrf_token %}
{{ form.as_p }}
<p><input type="submit" value="Login" /></p>
<input type="hidden" name="next" value="{{ next|urlencode }}" />
</form>
</div>
-{% endblock maincontent %}
+{% endblock content %}
from django.contrib import admin
from django.conf import settings
-import wiki.urls
admin.autodiscover()
urlpatterns = patterns('',
# Auth
- url(r'^accounts/login/$', 'django_cas.views.login', name='login'),
- url(r'^accounts/logout/$', 'django_cas.views.logout', name='logout'),
+ url(r'^accounts/login/$', 'django.contrib.auth.views.login', name='login'),
+ url(r'^accounts/logout/$', 'catalogue.views.logout_then_redirect', name='logout'),
# Admin panel
(r'^admin/filebrowser/', include('filebrowser.urls')),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
(r'^admin/', include(admin.site.urls)),
+ (r'^comments/', include('django.contrib.comments.urls')),
+
url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/documents/'}),
- url(r'^documents/', include('wiki.urls')),
+ url(r'^documents/', include('catalogue.urls')),
+ url(r'^apiclient/', include('apiclient.urls')),
+ url(r'^editor/', include('wiki.urls')),
# Static files (should be served by Apache)
url(r'^%s(?P<path>.+)$' % settings.MEDIA_URL[1:], 'django.views.static.serve',
{'document_root': settings.MEDIA_ROOT, 'show_indexes': True}),
url(r'^%s(?P<path>.+)$' % settings.STATIC_URL[1:], 'django.views.static.serve',
{'document_root': settings.STATIC_ROOT, 'show_indexes': True}),
- (r'^documents/', include(wiki.urls)),
- url(r'^themes$', 'wiki.views.themes', name="themes"),
+
url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/documents/'}),
)
## Python libraries
-lxml>=2.2
+lxml>=2.2.2
mercurial>=1.6,<1.7
PyYAML>=3.0
PIL>=1.1
+oauth2
+httplib2 # oauth2 dependency
## Book conversion library
# git+git://github.com/fnp/librarian.git@master#egg=librarian
## Django
-Django>=1.1.1,<1.2
+Django>=1.3,<1.4
sorl-thumbnail>=3.2
django-maintenancemode>=0.9
+django-pagination
+django-gravatar
# migrations
south>=0.6