('author', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
('patch', self.gf('django.db.models.fields.TextField')(blank=True)),
('tree', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['dvcs.Document'])),
+ ('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['dvcs.Change'])),
('merge_parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='merge_children', null=True, blank=True, to=orm['dvcs.Change'])),
('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
- ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, db_index=True, blank=True)),
))
db.send_create_signal('dvcs', ['Change'])
+ # Adding unique constraint on 'Change', fields ['tree', 'revision']
+ db.create_unique('dvcs_change', ['tree_id', 'revision'])
+
# Adding model 'Document'
db.create_table('dvcs_document', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
def backwards(self, orm):
+ # Removing unique constraint on 'Change', fields ['tree', 'revision']
+ db.delete_unique('dvcs_change', ['tree_id', 'revision'])
+
# Deleting model 'Change'
db.delete_table('dvcs_change')
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'dvcs.change': {
- 'Meta': {'ordering': "('created_at',)", 'object_name': 'Change'},
+ 'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'Change'},
'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
- 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': '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['dvcs.Change']"}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['dvcs.Change']"}),
'patch': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
'tree': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dvcs.Document']"})
},
'dvcs.document': {
author = models.ForeignKey(User, null=True, blank=True)
patch = models.TextField(blank=True)
tree = models.ForeignKey('Document')
+ revision = models.IntegerField(db_index=True)
parent = models.ForeignKey('self',
null=True, blank=True, default=None,
related_name="merge_children")
description = models.TextField(blank=True, default='')
- created_at = models.DateTimeField(auto_now_add=True)
+ created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
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 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):
if self.parent is None and self.merge_parent is not None:
return self.apply_to(self.merge_parent.materialize())
- changes = Change.objects.filter(
- ~models.Q(parent=None) | models.Q(merge_parent=None),
+ changes = Change.objects.exclude(parent=None).filter(
tree=self.tree,
- created_at__lte=self.created_at).order_by('created_at')
+ revision__lte=self.revision).order_by('revision')
text = u''
for change in changes:
text = change.apply_to(text)
null=True, blank=True, default=None,
help_text=_("This document's current head."))
- @classmethod
- def create(cls, text='', *args, **kwargs):
- instance = cls(*args, **kwargs)
- instance.save()
- head = instance.head
- head.patch = Change.make_patch('', text)
- head.save()
- return instance
-
def __unicode__(self):
return u"{0}, HEAD: {1}".format(self.id, self.head_id)
raise ValueError("You can provide only text or patch - not both")
patch = kwargs['patch']
+ author = kwargs.get('author', None)
+
old_head = self.head
if parent != old_head:
- change = parent.make_merge_child(patch, kwargs['author'], kwargs.get('description', ''))
+ change = parent.make_merge_child(patch, author, kwargs.get('description', ''))
# not Fast-Forward - perform a merge
- self.head = old_head.merge_with(change, author=kwargs['author'])
+ self.head = old_head.merge_with(change, author=author)
else:
- self.head = parent.make_child(patch, kwargs['author'], kwargs.get('description', ''))
+ self.head = parent.make_child(patch, author, kwargs.get('description', ''))
self.save()
return self.head
def history(self):
- return self.change_set.all()
+ return self.change_set.filter(revision__gt=0)
def revision(self):
- return self.change_set.all().count()
+ rev = self.change_set.aggregate(
+ models.Max('revision'))['revision__max']
+ return rev if rev is not None else 0
def at_revision(self, rev):
if rev:
- return self.change_set.all()[rev-1]
+ return self.change_set.get(revision=rev)
else:
return self.head
def listener_initial_commit(sender, instance, created, **kwargs):
if created:
instance.head = Change.objects.create(
+ revision=0,
author=instance.creator,
patch=Change.make_patch('', ''),
tree=instance)
from wiki import models
-admin.site.register(models.Book)
+class BookAdmin(admin.ModelAdmin):
+ prepopulated_fields = {'slug': ['title']}
+
+
+admin.site.register(models.Book, BookAdmin)
+admin.site.register(models.Chunk)
admin.site.register(models.Theme)
+++ /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 'Book'
- db.create_table('wiki_book', (
- ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
- ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=255, db_index=True)),
- ('title', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
- ('doc', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['dvcs.Document'])),
- ('gallery', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
- ))
- db.send_create_signal('wiki', ['Book'])
-
-
- def backwards(self, orm):
-
- # Deleting model 'Book'
- db.delete_table('wiki_book')
-
-
- 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', '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'})
- },
- 'dvcs.change': {
- 'Meta': {'ordering': "('created_at',)", 'object_name': 'Change'},
- 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
- 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': '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['dvcs.Change']"}),
- 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['dvcs.Change']"}),
- 'patch': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
- 'tree': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dvcs.Document']"})
- },
- 'dvcs.document': {
- 'Meta': {'object_name': 'Document'},
- 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
- 'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['dvcs.Change']", 'null': 'True', 'blank': 'True'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
- },
- 'wiki.book': {
- 'Meta': {'ordering': "['title']", 'object_name': 'Book'},
- 'doc': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dvcs.Document']"}),
- 'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
- 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
- },
- 'wiki.theme': {
- 'Meta': {'ordering': "('name',)", 'object_name': 'Theme'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'})
- }
- }
-
- complete_apps = ['wiki']
--- /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 'Book'
+ db.create_table('wiki_book', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('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['wiki.Book'])),
+ ('parent_number', self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True)),
+ ))
+ db.send_create_signal('wiki', ['Book'])
+
+ # Adding model 'Chunk'
+ db.create_table('wiki_chunk', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('book', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['wiki.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)),
+ ('doc', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['dvcs.Document'], unique=True)),
+ ))
+ db.send_create_signal('wiki', ['Chunk'])
+
+ # Adding unique constraint on 'Chunk', fields ['book', 'number']
+ db.create_unique('wiki_chunk', ['book_id', 'number'])
+
+ # Adding unique constraint on 'Chunk', fields ['book', 'slug']
+ db.create_unique('wiki_chunk', ['book_id', 'slug'])
+
+
+ def backwards(self, orm):
+
+ # Removing unique constraint on 'Chunk', fields ['book', 'slug']
+ db.delete_unique('wiki_chunk', ['book_id', 'slug'])
+
+ # Removing unique constraint on 'Chunk', fields ['book', 'number']
+ db.delete_unique('wiki_chunk', ['book_id', 'number'])
+
+ # Deleting model 'Book'
+ db.delete_table('wiki_book')
+
+ # Deleting model 'Chunk'
+ db.delete_table('wiki_chunk')
+
+
+ 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', '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'})
+ },
+ 'dvcs.change': {
+ 'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'Change'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': '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['dvcs.Change']"}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['dvcs.Change']"}),
+ 'patch': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+ 'tree': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dvcs.Document']"})
+ },
+ 'dvcs.document': {
+ 'Meta': {'object_name': 'Document'},
+ 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['dvcs.Change']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'wiki.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['wiki.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'})
+ },
+ 'wiki.chunk': {
+ 'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'},
+ 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['wiki.Book']"}),
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'doc': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dvcs.Document']", 'unique': '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'})
+ },
+ 'wiki.theme': {
+ 'Meta': {'ordering': "('name',)", 'object_name': 'Theme'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'})
+ }
+ }
+
+ complete_apps = ['wiki']
# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+import itertools
+import re
+
from django.db import models
from django.utils.translation import ugettext_lazy as _
logger = logging.getLogger("fnp.wiki")
+RE_TRIM_BEGIN = re.compile("^<!-- TRIM_BEGIN -->$", re.M)
+RE_TRIM_END = re.compile("^<!-- TRIM_END -->$", re.M)
+
+
class Book(models.Model):
""" A document edited on the wiki """
- slug = models.SlugField(_('slug'), max_length=255, unique=True)
- title = models.CharField(_('displayed title'), max_length=255, blank=True)
- doc = models.ForeignKey(dvcs_models.Document, editable=False)
+ title = models.CharField(_('title'), max_length=255)
+ slug = models.SlugField(_('slug'), max_length=128, unique=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 Meta:
- ordering = ['title']
+ ordering = ['parent_number', 'title']
verbose_name = _('book')
verbose_name_plural = _('books')
@classmethod
def create(cls, creator=None, text=u'', *args, **kwargs):
+ """
+ >>> Book.create(slug='x', text='abc').materialize()
+ 'abc'
+ """
instance = cls(*args, **kwargs)
- instance.doc = dvcs_models.Document.create(creator=creator, text=text)
instance.save()
+ instance.chunk_set.all()[0].doc.commit(author=creator, text=text)
return instance
+ @staticmethod
+ 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 materialize(self):
+ """
+ Get full text of the document compiled from chunks.
+ Takes the current versions of all texts for now, but it should
+ be possible to specify a tag or a point in time for compiling.
+
+ First non-empty text's beginning isn't trimmed,
+ and last non-empty text's end isn't trimmed.
+ """
+ texts = []
+ trim_begin = False
+ text = ''
+ for chunk in self.chunk_set.all():
+ next_text = chunk.doc.materialize()
+ if not next_text:
+ continue
+ if text:
+ # trim the end, because there's more non-empty text
+ # don't trim beginning, if `text' is the first non-empty part
+ texts.append(self.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(self.trim(text, trim_begin=trim_begin, trim_end=False))
+ return "".join(texts)
+
@staticmethod
def listener_create(sender, instance, created, **kwargs):
- if created and instance.doc is None:
- instance.doc = dvcs_models.Document.objects.create()
- instance.save()
+ if created:
+ instance.chunk_set.create(number=1, slug='1')
models.signals.post_save.connect(Book.listener_create, sender=Book)
+class Chunk(models.Model):
+ """ An editable chunk of text. Every Book text is divided into chunks. """
+
+ book = models.ForeignKey(Book)
+ number = models.IntegerField()
+ slug = models.SlugField()
+ comment = models.CharField(max_length=255)
+ doc = models.ForeignKey(dvcs_models.Document, editable=False, unique=True, null=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 save(self, *args, **kwargs):
+ if self.doc is None:
+ self.doc = dvcs_models.Document.objects.create()
+ super(Chunk, self).save(*args, **kwargs)
+
+ @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):
+ return "%s, %s (%d/%d)" % (self.book.title, self.comment,
+ self.number, self.book.chunk_set.count())
+
+
+
'''
from wiki import settings, constants
--- /dev/null
+{% extends "wiki/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+
+ <h1>{{ object.title }}</h1>
+
+ <table>
+ {% for chunk in object.chunk_set.all %}
+ <tr><td>{{ chunk.number }}</td><td>{{ chunk.slug }}</td></tr>
+ {% endfor %}
+ </table>
+
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
{% block maincontent %}
<div id="document-meta"
- data-document-name="{{ book.slug }}" style="display:none">
+ data-book="{{ chunk.book.slug }}" data-chunk="{{ chunk.slug }}" style="display:none">
- <span data-key="gallery">{{ book.gallery }}</span>
+ <span data-key="gallery">{{ chunk.book.gallery }}</span>
<span data-key="revision">{{ revision }}</span>
{% block meta-extra %} {% endblock %}
<tbody>
{% for book in books %}
<tr>
- <td colspan="3"><a target="_blank" data-id="{{ book.slug }}"
- href="{% url wiki_editor book.slug %}">{{ book.title }}</a></td>
+ <td><a target="_blank" data-id="{{ book.slug }}"
+ href="{% url wiki_book book.slug %}">{{ book.title }}</a>
+ </td>
+ <td class='listitem-tools'>
+ {% ifequal book.chunk_set.count 1 %}
+ <a target="_blank" data-id="{{ book.slug }}"
+ href="{% url wiki_editor book.slug %}">
+ [{% trans "edit" %}]</a>
+ {% else %}
+ {% for chunk in book.chunk_set.all %}
+ <a target="_blank" data-id="{{ book.slug }}"
+ href="{% url wiki_editor book.slug chunk.slug %}">
+ [<span class='chunkno'>{{ forloop.counter }}.</span>
+ {{ chunk.comment }}</a>]<br/>
+ {% endfor %}
+ {% endifequal %}
+ </td>
<!-- placeholder </td> -->
</tr>
{% endfor %}
<div id="last-edited-list">
<h2>{% trans "Your last edited documents" %}</h2>
<ol>
- {% for slug, item in last_books %}
- <li><a href="{% url wiki_editor slug %}"
+ {% 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>
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 book.slug %}">{% trans "View version" %}</button>
+ data-basehref="{% url wiki_editor_readonly chunk.book.slug chunk.slug %}">{% trans "View version" %}</button>
</div>
<div id="history-view">
<div id="source-editor" class="editor">
{% if not readonly %}{% toolbar %}{% endif %}
<textarea id="codemirror_placeholder"><br/></textarea>
- <!-- <input type="hidden" name="name" value="{{ book.slug }}" />
- <input type="hidden" name="author" value="annonymous" />
- <input type="hidden" name="comment" value="no comment" />
- <input type="hidden" name="revision" value="{{ book.doc.revision }}" />
- -->
</div>
<h2>
<label for="title">{% trans "Title" %}:</label>
<span data-ui-editable="true" data-edit-target="meta.displayTitle"
- >{{ book.title }}</span>
+ >{{ chunk.pretty_name }}</span>
</h2>
<p>
<label>{% trans "Document ID" %}:</label>
- <span>{{ book.slug }}</span>
+ <span>{{ chunk.book.slug }}/{{ chunk.slug }}</span>
</p>
<p>
<label>{% trans "Current version" %}:</label>
- {{ book.doc.revision }} ({{ book.doc.head.created_at }})
+ {{ chunk.doc.revision }} ({{ chunk.doc.head.created_at }})
<p>
<label>{% trans "Last edited by" %}:</label>
- {{ book.doc.head.author }}
+ {{ chunk.doc.head.author }}
</p>
<p>
<label for="gallery">{% trans "Link to gallery" %}:</label>
<span data-ui-editable="true" data-edit-target="meta.galleryLink"
- >{{ book.gallery }}</span>
+ >{{ chunk.book.gallery }}</span>
</p>
<p><button type="button" id="publish_button">{% trans "Publish" %}</button></p>
# -*- coding: utf-8
from django.conf.urls.defaults import *
from django.views.generic.simple import redirect_to
+from django.views.generic.list_detail import object_detail
from django.conf import settings
+from wiki.models import Book
#PART = ur"""[ ĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9\w_.-]+"""
#url(r'^catalogue/([^/]+)/([^/]+)/$', 'document_list'),
#url(r'^catalogue/([^/]+)/([^/]+)/([^/]+)$', 'document_list'),
- url(r'^(?P<slug>[^/]+)/$',
+ url(r'^edit/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
'editor', name="wiki_editor"),
- url(r'^(?P<slug>[^/]+)/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<slug>[^/]+)',
+ url(r'^create/(?P<slug>[^/]*)/',
'create_missing', name='wiki_create_missing'),
- url(r'^(?P<directory>[^/]+)/gallery$',
+ url(r'^gallery/(?P<directory>[^/]+)/$',
'gallery', name="wiki_gallery"),
- url(r'^(?P<slug>[^/]+)/history$',
+ url(r'^history/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
'history', name="wiki_history"),
- url(r'^(?P<slug>[^/]+)/rev$',
+ url(r'^rev/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
'revision', name="wiki_revision"),
- url(r'^(?P<slug>[^/]+)/text$',
+ url(r'^text/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
'text', name="wiki_text"),
- url(r'^(?P<slug>[^/]+)/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<slug>[^/]+)/diff$', 'diff', name="wiki_diff"),
+ url(r'^diff/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$', 'diff', name="wiki_diff"),
#url(r'^(?P<name>[^/]+)/tags$', 'add_tag', name="wiki_add_tag"),
+ url(r'^full/(?P<slug>[^/]+)/$', 'compiled', name="wiki_compiled"),
-
+ url(r'^book/(?P<slug>[^/]+)/$', object_detail,
+ {"queryset": Book.objects.all()}, name="wiki_book"),
)
ajax_require_permission, recursive_groupby)
from django import http
from django.shortcuts import get_object_or_404, redirect
+from django.http import Http404
-from wiki.models import Book, Theme
+from wiki.models import Book, Chunk, Theme
from wiki.forms import DocumentTextSaveForm, DocumentTextRevertForm, DocumentTagForm, DocumentCreateForm, DocumentsUploadForm
from datetime import datetime
from django.utils.encoding import smart_unicode
@never_cache
-def editor(request, slug, template_name='wiki/document_details.html'):
+def editor(request, slug, chunk=None, template_name='wiki/document_details.html'):
try:
- book = Book.objects.get(slug=slug)
- except Book.DoesNotExist:
- return http.HttpResponseRedirect(reverse("wiki_create_missing", args=[slug]))
+ 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("wiki_create_missing", args=[slug]))
+ else:
+ raise Http404
access_time = datetime.now()
last_books = request.session.get("wiki_last_books", {})
- last_books[slug] = {
+ last_books[slug, chunk.slug] = {
'time': access_time,
- 'title': book.title,
+ 'title': chunk.pretty_name(),
}
if len(last_books) > MAX_LAST_DOCS:
- oldest_key = min(last_books, key=operator.itemgetter('time'))
+ 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={
- 'book': book,
+ 'chunk': chunk,
'forms': {
"text_save": DocumentTextSaveForm(prefix="textsave"),
"text_revert": DocumentTextRevertForm(prefix="textrevert"),
@require_GET
-def editor_readonly(request, slug, template_name='wiki/document_details_readonly.html'):
+def editor_readonly(request, slug, chunk=None, template_name='wiki/document_details_readonly.html'):
try:
- book = Book.objects.get(slug=slug)
+ chunk = Chunk.get(slug, chunk)
revision = request.GET['revision']
- except KeyError:
- raise http.Http404
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist, KeyError):
+ raise Http404
access_time = datetime.now()
last_books = request.session.get("wiki_last_books", {})
- last_books[slug] = {
+ last_books[slug, chunk.slug] = {
'time': access_time,
- 'title': book.title,
+ 'title': chunk.book.title,
}
if len(last_books) > MAX_LAST_DOCS:
- oldest_key = min(last_books, key=operator.itemgetter('time'))
+ 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={
- 'book': book,
+ 'chunk': chunk,
'revision': revision,
'readonly': True,
'REDMINE_URL': settings.REDMINE_URL,
@never_cache
@decorator_from_middleware(GZipMiddleware)
-def text(request, slug):
- doc = get_object_or_404(Book, slug=slug).doc
+def text(request, slug, chunk=None):
+ try:
+ doc = Chunk.get(slug, chunk).doc
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
if request.method == 'POST':
form = DocumentTextSaveForm(request.POST, prefix="textsave")
try:
revision = int(revision)
- except ValueError:
+ except (ValueError, TypeError):
revision = None
return JSONResponse({
})
+@never_cache
+def compiled(request, slug):
+ text = get_object_or_404(Book, slug=slug).materialize()
+
+ response = http.HttpResponse(text, content_type='application/xml', mimetype='application/wl+xml')
+ response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
+ return response
+
+
@never_cache
@require_POST
-def revert(request, slug):
+def revert(request, slug, chunk=None):
form = DocumentTextRevertForm(request.POST, prefix="textrevert")
if form.is_valid():
- doc = get_object_or_404(Book, slug=slug).doc
+ try:
+ doc = Chunk.get(slug, chunk).doc
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
+
revision = form.cleaned_data['revision']
comment = form.cleaned_data['comment']
@never_cache
-def diff(request, slug):
+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
- doc = get_object_or_404(Book, slug=slug).doc
+ try:
+ doc = Chunk.get(slug, chunk).doc
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
docA = doc.at_revision(revA).materialize()
docB = doc.at_revision(revB).materialize()
@never_cache
-def revision(request, slug):
- book = get_object_or_404(Book, slug=slug)
- return http.HttpResponse(str(book.doc.revision()))
+def revision(request, slug, chunk=None):
+ try:
+ doc = Chunk.get(slug, chunk).doc
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
+ return http.HttpResponse(str(doc.revision()))
@never_cache
-def history(request, slug):
+def history(request, slug, chunk=None):
# TODO: pagination
- book = get_object_or_404(Book, slug=slug)
- rev = book.doc.revision()
+ try:
+ doc = Chunk.get(slug, chunk).doc
+ except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+ raise Http404
+
changes = []
- for change in book.doc.history().order_by('-created_at'):
+ for change in doc.history().order_by('-created_at'):
if change.author:
author = "%s %s <%s>" % (
change.author.first_name,
else:
author = None
changes.append({
- "version": rev,
+ "version": change.revision,
"description": change.description,
"author": author,
"date": change.created_at,
"tag": [],
})
- rev -= 1
return JSONResponse(changes)
#skipped-list {
color: #666;
-}
\ No newline at end of file
+}
+
+.chunkno {
+ font-size: .7em;
+}
+
+td {
+ vertical-align: top;
+}
+.listitem-tools {
+ padding-left: 2em;
+}
var base_path = "/documents";
if (vname == "ajax_document_text") {
- var path = "/" + arguments[1] + "/text";
+ var path = "/text/" + arguments[1];
if (arguments[2] !== undefined)
path += "/" + arguments[2];
}
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";
+ return base_path + "/tags/" + arguments[1];
if (vname == "ajax_publish")
- return base_path + "/" + arguments[1] + "/publish";
+ return base_path + "/publish/" + 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();
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) {
self.result += text_buffer;
text_buffer = token.node.nodeValue;
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>