From 793b39c86e9583467f1cbc41f8b1a4677d079f23 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 20 May 2011 15:30:30 +0200 Subject: [PATCH] text splitted into chunks, parts can be automagically joined (but no parts adding mechanism yet), some URL changes --- apps/dvcs/migrations/0001_initial.py | 14 ++- apps/dvcs/models.py | 43 ++++---- apps/wiki/admin.py | 7 +- ...nique_chunk_book_number__add_unique_ch.py} | 56 ++++++++-- apps/wiki/models.py | 101 ++++++++++++++++-- apps/wiki/templates/wiki/book_detail.html | 17 +++ .../templates/wiki/document_details_base.html | 4 +- apps/wiki/templates/wiki/document_list.html | 23 +++- .../templates/wiki/tabs/history_view.html | 2 +- .../templates/wiki/tabs/source_editor.html | 5 - .../templates/wiki/tabs/summary_view.html | 10 +- apps/wiki/urls.py | 24 +++-- apps/wiki/views.py | 100 +++++++++++------ redakcja/static/css/filelist.css | 13 ++- redakcja/static/js/wiki/wikiapi.js | 18 ++-- redakcja/static/js/wiki/xslt.js | 7 +- redakcja/static/xsl/wl2html_client.xsl | 6 +- 17 files changed, 338 insertions(+), 112 deletions(-) rename apps/wiki/migrations/{0003_auto__add_book.py => 0003_auto__add_book__add_chunk__add_unique_chunk_book_number__add_unique_ch.py} (67%) create mode 100755 apps/wiki/templates/wiki/book_detail.html diff --git a/apps/dvcs/migrations/0001_initial.py b/apps/dvcs/migrations/0001_initial.py index c5e75c97..0e01d03b 100644 --- a/apps/dvcs/migrations/0001_initial.py +++ b/apps/dvcs/migrations/0001_initial.py @@ -14,13 +14,17 @@ class Migration(SchemaMigration): ('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)), @@ -32,6 +36,9 @@ class Migration(SchemaMigration): 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') @@ -77,14 +84,15 @@ class Migration(SchemaMigration): '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': { diff --git a/apps/dvcs/models.py b/apps/dvcs/models.py index 1c303873..9c9d350b 100644 --- a/apps/dvcs/models.py +++ b/apps/dvcs/models.py @@ -15,6 +15,7 @@ class Change(models.Model): 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, @@ -25,14 +26,23 @@ class Change(models.Model): 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): @@ -46,10 +56,9 @@ class Change(models.Model): 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) @@ -99,15 +108,6 @@ class Document(models.Model): 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) @@ -144,26 +144,30 @@ class Document(models.Model): 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 @@ -171,6 +175,7 @@ class Document(models.Model): 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) diff --git a/apps/wiki/admin.py b/apps/wiki/admin.py index 60a78f4a..9725d0bf 100644 --- a/apps/wiki/admin.py +++ b/apps/wiki/admin.py @@ -2,5 +2,10 @@ from django.contrib import admin 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) diff --git a/apps/wiki/migrations/0003_auto__add_book.py b/apps/wiki/migrations/0003_auto__add_book__add_chunk__add_unique_chunk_book_number__add_unique_ch.py similarity index 67% rename from apps/wiki/migrations/0003_auto__add_book.py rename to apps/wiki/migrations/0003_auto__add_book__add_chunk__add_unique_chunk_book_number__add_unique_ch.py index 1c570043..39154ac1 100644 --- a/apps/wiki/migrations/0003_auto__add_book.py +++ b/apps/wiki/migrations/0003_auto__add_book__add_chunk__add_unique_chunk_book_number__add_unique_ch.py @@ -11,19 +11,46 @@ class Migration(SchemaMigration): # 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'])), + ('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': { @@ -63,14 +90,15 @@ class Migration(SchemaMigration): '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': { @@ -80,12 +108,22 @@ class Migration(SchemaMigration): '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']"}), + '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'}), - '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'}) + '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'}, diff --git a/apps/wiki/models.py b/apps/wiki/models.py index a9264934..66f8a288 100644 --- a/apps/wiki/models.py +++ b/apps/wiki/models.py @@ -3,6 +3,9 @@ # 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 _ @@ -13,16 +16,22 @@ import logging logger = logging.getLogger("fnp.wiki") +RE_TRIM_BEGIN = re.compile("^$", re.M) +RE_TRIM_END = re.compile("^$", 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') @@ -31,20 +40,96 @@ class Book(models.Model): @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 diff --git a/apps/wiki/templates/wiki/book_detail.html b/apps/wiki/templates/wiki/book_detail.html new file mode 100755 index 00000000..ee645f13 --- /dev/null +++ b/apps/wiki/templates/wiki/book_detail.html @@ -0,0 +1,17 @@ +{% extends "wiki/base.html" %} +{% load i18n %} + +{% block leftcolumn %} + +

{{ object.title }}

+ + + {% for chunk in object.chunk_set.all %} + + {% endfor %} +
{{ chunk.number }}{{ chunk.slug }}
+ +{% endblock leftcolumn %} + +{% block rightcolumn %} +{% endblock rightcolumn %} diff --git a/apps/wiki/templates/wiki/document_details_base.html b/apps/wiki/templates/wiki/document_details_base.html index 183da3de..3eda939b 100644 --- a/apps/wiki/templates/wiki/document_details_base.html +++ b/apps/wiki/templates/wiki/document_details_base.html @@ -16,9 +16,9 @@ {% block maincontent %}