changeset tagging in dvcs,
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 1 Jun 2011 10:03:43 +0000 (12:03 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 1 Jun 2011 10:03:43 +0000 (12:03 +0200)
document list items caching

12 files changed:
apps/dvcs/admin.py
apps/dvcs/migrations/0002_auto__add_tag.py [new file with mode: 0644]
apps/dvcs/models.py
apps/wiki/forms.py
apps/wiki/migrations/0004_auto__add_field_book__list_html.py [new file with mode: 0644]
apps/wiki/models.py
apps/wiki/templates/wiki/document_list.html
apps/wiki/templates/wiki/document_list_item.html [new file with mode: 0755]
apps/wiki/urls.py
apps/wiki/views.py
redakcja/static/js/wiki/view_history.js
redakcja/static/js/wiki/wikiapi.js

index c81d3b7..984798d 100644 (file)
@@ -1,5 +1,6 @@
 from django.contrib.admin import site
 from django.contrib.admin import site
-from dvcs.models import Document, Change
+from dvcs.models import Document, Change, Tag
 
 
+site.register(Tag)
 site.register(Document)
 site.register(Change)
 site.register(Document)
 site.register(Change)
diff --git a/apps/dvcs/migrations/0002_auto__add_tag.py b/apps/dvcs/migrations/0002_auto__add_tag.py
new file mode 100644 (file)
index 0000000..7f5b9a3
--- /dev/null
@@ -0,0 +1,104 @@
+# 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 'Tag'
+        db.create_table('dvcs_tag', (
+            ('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('dvcs', ['Tag'])
+
+        # Adding M2M table for field tags on 'Change'
+        db.create_table('dvcs_change_tags', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('change', models.ForeignKey(orm['dvcs.change'], null=False)),
+            ('tag', models.ForeignKey(orm['dvcs.tag'], null=False))
+        ))
+        db.create_unique('dvcs_change_tags', ['change_id', 'tag_id'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'Tag'
+        db.delete_table('dvcs_tag')
+
+        # Removing M2M table for field tags on 'Change'
+        db.delete_table('dvcs_change_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', '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'}),
+            'author_desc': ('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['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'}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dvcs.Tag']", 'symmetrical': 'False'}),
+            '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'})
+        },
+        'dvcs.tag': {
+            'Meta': {'ordering': "['ordering']", 'object_name': 'Tag'},
+            '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'})
+        }
+    }
+
+    complete_apps = ['dvcs']
index 7ac7cbe..6c5796a 100644 (file)
@@ -6,6 +6,41 @@ from django.utils.translation import ugettext_lazy as _
 from mercurial import mdiff, simplemerge
 import pickle
 
 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:
+        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 = {}
+
+models.signals.pre_save.connect(Tag.listener_changed, sender=Tag)
+
+
 class Change(models.Model):
     """
         Single document change related to previous change. The "parent"
 class Change(models.Model):
     """
         Single document change related to previous change. The "parent"
@@ -32,6 +67,8 @@ class Change(models.Model):
     created_at = models.DateTimeField(editable=False, db_index=True, 
                         default=datetime.now)
 
     created_at = models.DateTimeField(editable=False, db_index=True, 
                         default=datetime.now)
 
+    tags = models.ManyToManyField(Tag)
+
     class Meta:
         ordering = ('created_at',)
         unique_together = ['tree', 'revision']
     class Meta:
         ordering = ('created_at',)
         unique_together = ['tree', 'revision']
@@ -78,18 +115,26 @@ class Change(models.Model):
             text = change.apply_to(text)
         return text
 
             text = change.apply_to(text)
         return text
 
-    def make_child(self, patch, description, author=None, author_desc=None):
-        return self.children.create(patch=patch,
+    def make_child(self, patch, description, author=None,
+            author_desc=None, tags=None):
+        ch = self.children.create(patch=patch,
                         tree=self.tree, author=author,
                         author_desc=author_desc,
                         description=description)
                         tree=self.tree, author=author,
                         author_desc=author_desc,
                         description=description)
+        if tags is not None:
+            ch.tags = tags
+        return ch
 
     def make_merge_child(self, patch, description, author=None, 
 
     def make_merge_child(self, patch, description, author=None, 
-            author_desc=None):
-        return self.merge_children.create(patch=patch,
+            author_desc=None, tags=None):
+        ch = self.merge_children.create(patch=patch,
                         tree=self.tree, author=author,
                         author_desc=author_desc,
                         tree=self.tree, author=author,
                         author_desc=author_desc,
-                        description=description)
+                        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 apply_to(self, text):
         return mdiff.patch(text, pickle.loads(self.patch.encode('ascii')))
@@ -165,19 +210,22 @@ class Document(models.Model):
 
         author = kwargs.get('author', None)
         author_desc = kwargs.get('author_desc', None)
 
         author = kwargs.get('author', None)
         author_desc = kwargs.get('author_desc', None)
+        tags = kwargs.get('tags', [])
 
         old_head = self.head
         if parent != old_head:
             change = parent.make_merge_child(patch, author=author, 
                     author_desc=author_desc,
 
         old_head = self.head
         if parent != old_head:
             change = parent.make_merge_child(patch, author=author, 
                     author_desc=author_desc,
-                    description=kwargs.get('description', ''))
+                    description=kwargs.get('description', ''),
+                    tags=tags)
             # not Fast-Forward - perform a merge
             self.head = old_head.merge_with(change, author=author,
                     author_desc=author_desc)
         else:
             self.head = parent.make_child(patch, author=author, 
                     author_desc=author_desc, 
             # not Fast-Forward - perform a merge
             self.head = old_head.merge_with(change, author=author,
                     author_desc=author_desc)
         else:
             self.head = parent.make_child(patch, author=author, 
                     author_desc=author_desc, 
-                    description=kwargs.get('description', ''))
+                    description=kwargs.get('description', ''),
+                    tags=tags)
 
         self.save()
         return self.head
 
         self.save()
         return self.head
@@ -196,6 +244,13 @@ class Document(models.Model):
         else:
             return self.head
 
         else:
             return self.head
 
+    def last_tagged(self, tag):
+        changes = tag.change_set.filter(tree=self).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
     @staticmethod
     def listener_initial_commit(sender, instance, created, **kwargs):
         # run for Document and its subclasses
index b057be0..f3362e8 100644 (file)
@@ -4,10 +4,10 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django import forms
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django import forms
-from wiki.constants import DOCUMENT_TAGS, DOCUMENT_STAGES
 from wiki.models import Book
 from django.utils.translation import ugettext_lazy as _
 
 from wiki.models import Book
 from django.utils.translation import ugettext_lazy as _
 
+from dvcs.models import Tag
 
 class DocumentTagForm(forms.Form):
     """
 
 class DocumentTagForm(forms.Form):
     """
@@ -15,7 +15,7 @@ class DocumentTagForm(forms.Form):
     """
 
     id = forms.CharField(widget=forms.HiddenInput)
     """
 
     id = forms.CharField(widget=forms.HiddenInput)
-    tag = forms.ChoiceField(choices=DOCUMENT_TAGS)
+    tag = forms.ModelChoiceField(queryset=Tag.objects.all())
     revision = forms.IntegerField(widget=forms.HiddenInput)
 
 
     revision = forms.IntegerField(widget=forms.HiddenInput)
 
 
@@ -98,8 +98,8 @@ class DocumentTextSaveForm(forms.Form):
         help_text=_(u"Describe changes you made."),
     )
 
         help_text=_(u"Describe changes you made."),
     )
 
-    stage_completed = forms.ChoiceField(
-        choices=DOCUMENT_STAGES,
+    stage_completed = forms.ModelChoiceField(
+        queryset=Tag.objects.all(),
         required=False,
         label=_(u"Completed"),
         help_text=_(u"If you completed a life cycle stage, select it."),
         required=False,
         label=_(u"Completed"),
         help_text=_(u"If you completed a life cycle stage, select it."),
diff --git a/apps/wiki/migrations/0004_auto__add_field_book__list_html.py b/apps/wiki/migrations/0004_auto__add_field_book__list_html.py
new file mode 100644 (file)
index 0000000..a09dc90
--- /dev/null
@@ -0,0 +1,102 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Adding field 'Book._list_html'
+        db.add_column('wiki_book', '_list_html', self.gf('django.db.models.fields.TextField')(null=True), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'Book._list_html'
+        db.delete_column('wiki_book', '_list_html')
+
+
+    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'}),
+            'author_desc': ('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['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'},
+            '_list_html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['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', '_ormbases': ['dvcs.Document']},
+            'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['wiki.Book']"}),
+            'comment': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'document_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['dvcs.Document']", 'unique': 'True', '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']
index f8c9d25..6070206 100644 (file)
@@ -8,7 +8,9 @@ import re
 
 from django.core.urlresolvers import reverse
 from django.db import models
 
 from django.core.urlresolvers import reverse
 from django.db import models
+from django.utils.safestring import mark_safe
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
+from django.template.loader import render_to_string
 
 from dvcs import models as dvcs_models
 
 
 from dvcs import models as dvcs_models
 
@@ -31,6 +33,11 @@ class Book(models.Model):
     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)
 
     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)
 
+    _list_html = models.TextField(editable=False, null=True)
+
+    class NoTextError(BaseException):
+        pass
+
     class Meta:
         ordering = ['parent_number', 'title']
         verbose_name = _('book')
     class Meta:
         ordering = ['parent_number', 'title']
         verbose_name = _('book')
@@ -39,6 +46,11 @@ class Book(models.Model):
     def __unicode__(self):
         return self.title
 
     def __unicode__(self):
         return self.title
 
+    def save(self, reset_list_html=True, *args, **kwargs):
+        if reset_list_html:
+            self._list_html = None
+        return super(Book, self).save(*args, **kwargs)
+
     @classmethod
     def create(cls, creator=None, text=u'', *args, **kwargs):
         """
     @classmethod
     def create(cls, creator=None, text=u'', *args, **kwargs):
         """
@@ -59,6 +71,14 @@ class Book(models.Model):
     def __len__(self):
         return self.chunk_set.count()
 
     def __len__(self):
         return self.chunk_set.count()
 
+    def list_html(self):
+        if self._list_html is None:
+            print 'rendering', self.title
+            self._list_html = render_to_string('wiki/document_list_item.html',
+                {'book': self})
+            self.save(reset_list_html=False)
+        return mark_safe(self._list_html)
+
     @staticmethod
     def trim(text, trim_begin=True, trim_end=True):
         """ 
     @staticmethod
     def trim(text, trim_begin=True, trim_end=True):
         """ 
@@ -71,7 +91,11 @@ class Book(models.Model):
             text = RE_TRIM_END.split(text, maxsplit=1)[0]
         return text
 
             text = RE_TRIM_END.split(text, maxsplit=1)[0]
         return text
 
-    def materialize(self):
+    @staticmethod
+    def publish_tag():
+        return dvcs_models.Tag.get('publish')
+
+    def materialize(self, tag=None):
         """ 
             Get full text of the document compiled from chunks.
             Takes the current versions of all texts for now, but it should
         """ 
             Get full text of the document compiled from chunks.
             Takes the current versions of all texts for now, but it should
@@ -80,10 +104,16 @@ class Book(models.Model):
             First non-empty text's beginning isn't trimmed,
             and last non-empty text's end isn't trimmed.
         """
             First non-empty text's beginning isn't trimmed,
             and last non-empty text's end isn't trimmed.
         """
+        if tag:
+            changes = [chunk.last_tagged(tag) 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.')
         texts = []
         trim_begin = False
         text = ''
         texts = []
         trim_begin = False
         text = ''
-        for chunk in self:
+        for chunk in changes:
             next_text = chunk.materialize()
             if not next_text:
                 continue
             next_text = chunk.materialize()
             if not next_text:
                 continue
@@ -98,6 +128,14 @@ class Book(models.Model):
         texts.append(self.trim(text, trim_begin=trim_begin, trim_end=False))
         return "".join(texts)
 
         texts.append(self.trim(text, trim_begin=trim_begin, trim_end=False))
         return "".join(texts)
 
+    def publishable(self):
+        if not len(self):
+            return False
+        for chunk in self:
+            if not chunk.publishable():
+                return False
+        return True
+
     @staticmethod
     def listener_create(sender, instance, created, **kwargs):
         if created:
     @staticmethod
     def listener_create(sender, instance, created, **kwargs):
         if created:
@@ -135,55 +173,18 @@ class Chunk(dvcs_models.Document):
         return "%s, %s (%d/%d)" % (self.book.title, self.comment, 
                 self.number, len(self.book))
 
         return "%s, %s (%d/%d)" % (self.book.title, self.comment, 
                 self.number, len(self.book))
 
+    def publishable(self):
+        return self.last_tagged(Book.publish_tag())
 
 
+    @staticmethod
+    def listener_saved(sender, instance, created, **kwargs):
+        if instance.book:
+            # save book so that its _list_html is reset
+            instance.book.save()
 
 
-
-'''
-from wiki import settings, constants
-from slughifi import slughifi
-
-from django.http import Http404
-
-
-
-
-class Document(object):
-
-    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)
-
+models.signals.post_save.connect(Chunk.listener_saved, sender=Chunk)
 
 
 
 
-'''
 class Theme(models.Model):
     name = models.CharField(_('name'), max_length=50, unique=True)
 
 class Theme(models.Model):
     name = models.CharField(_('name'), max_length=50, unique=True)
 
index f808a1d..25b4cf2 100644 (file)
@@ -31,28 +31,7 @@ $(function() {
                </thead>
                <tbody>
        {% for book in books %}
                </thead>
                <tbody>
        {% for book in books %}
-            <tr>
-                <td colspan="3">
-                    <a target="_blank" data-id="{{ book.slug }}"
-                    href="{% url wiki_book book.slug %}">[?]</a>
-                    {% ifequal book.chunk_set.count 1 %}
-                        <a target="_blank" data-id="{{ book.slug }}"
-                                href="{% url wiki_editor book.slug %}">
-                                {{ book.title }}</a>
-                    {% else %}
-                        {{ book.title }}
-                        <div class="chunk-list">
-                        {% for chunk in book %}
-                            <a target="_blank" data-id="{{ book.slug }}"
-                                href="{{ chunk.get_absolute_url }}">
-                                <span class='chunkno'>{{ forloop.counter }}.</span>
-                                {{ chunk.comment }}</a><br/>
-                        {% endfor %}
-                        </div>
-                    {% endifequal %}
-                </td>
-                               <!-- placeholder </td> -->
-                       </tr>
+            {{ book.list_html }}
        {% endfor %}
                </tbody>
     </table>
        {% endfor %}
                </tbody>
     </table>
diff --git a/apps/wiki/templates/wiki/document_list_item.html b/apps/wiki/templates/wiki/document_list_item.html
new file mode 100755 (executable)
index 0000000..f1c4d37
--- /dev/null
@@ -0,0 +1,21 @@
+<tr>
+    <td colspan="3">
+        <a target="_blank" data-id="{{ book.slug }}"
+        href="{% url wiki_book book.slug %}">[?]</a>
+        {% ifequal book.chunk_set.count 1 %}
+            <a target="_blank" data-id="{{ book.slug }}"
+                    href="{% url wiki_editor book.slug %}">
+                    {{ book.title }}</a>
+        {% else %}
+            {{ book.title }}
+            <div class="chunk-list">
+            {% for chunk in book %}
+                <a target="_blank" data-id="{{ book.slug }}"
+                    href="{{ chunk.get_absolute_url }}">
+                    <span class='chunkno'>{{ forloop.counter }}.</span>
+                    {{ chunk.comment }}</a><br/>
+            {% endfor %}
+            </div>
+        {% endifequal %}
+    </td>
+</tr>
index ac731ef..3e5b067 100644 (file)
@@ -47,7 +47,7 @@ urlpatterns = patterns('wiki.views',
     #url(r'^(?P<name>[^/]+)/publish/(?P<version>\d+)$', 'publish', name="wiki_publish"),
 
     url(r'^diff/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$', 'diff', name="wiki_diff"),
     #url(r'^(?P<name>[^/]+)/publish/(?P<version>\d+)$', 'publish', name="wiki_publish"),
 
     url(r'^diff/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$', 'diff', name="wiki_diff"),
-    #url(r'^(?P<name>[^/]+)/tags$', 'add_tag', name="wiki_add_tag"),
+    url(r'^tag/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$', 'add_tag', name="wiki_add_tag"),
 
     url(r'^book/(?P<slug>[^/]+)/$', 'book', name="wiki_book"),
     url(r'^book/(?P<slug>[^/]+)/xml$', 'book_xml', name="wiki_book_xml"),
 
     url(r'^book/(?P<slug>[^/]+)/$', 'book', name="wiki_book"),
     url(r'^book/(?P<slug>[^/]+)/xml$', 'book_xml', name="wiki_book_xml"),
index 8fa1fc9..6063078 100644 (file)
@@ -17,7 +17,8 @@ from django.shortcuts import get_object_or_404, redirect
 from django.http import Http404
 
 from wiki.models import Book, Chunk, Theme
 from django.http import Http404
 
 from wiki.models import Book, Chunk, Theme
-from wiki.forms import DocumentTextSaveForm, DocumentTextRevertForm, DocumentTagForm, DocumentCreateForm, DocumentsUploadForm
+from wiki.forms import (DocumentTextSaveForm, DocumentTextRevertForm, DocumentTagForm, DocumentCreateForm, DocumentsUploadForm,
+        ChunkFormSet)
 from datetime import datetime
 from django.utils.encoding import smart_unicode
 from django.utils.translation import ugettext_lazy as _
 from datetime import datetime
 from django.utils.encoding import smart_unicode
 from django.utils.translation import ugettext_lazy as _
@@ -213,9 +214,6 @@ def text(request, slug, chunk=None):
     if request.method == 'POST':
         form = DocumentTextSaveForm(request.POST, prefix="textsave")
         if form.is_valid():
     if request.method == 'POST':
         form = DocumentTextSaveForm(request.POST, prefix="textsave")
         if form.is_valid():
-            # TODO:
-            # - stage completion should be stored (as a relation)
-
             if request.user.is_authenticated():
                 author = request.user
             else:
             if request.user.is_authenticated():
                 author = request.user
             else:
@@ -223,10 +221,13 @@ def text(request, slug, chunk=None):
             text = form.cleaned_data['text']
             parent_revision = form.cleaned_data['parent_revision']
             parent = doc.at_revision(parent_revision)
             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'],
             doc.commit(author=author,
                        text=text,
                        parent=parent,
                        description=form.cleaned_data['comment'],
+                       tags=tags,
                        )
             revision = doc.revision()
             return JSONResponse({
                        )
             revision = doc.revision()
             return JSONResponse({
@@ -253,8 +254,8 @@ def text(request, slug, chunk=None):
 
 @never_cache
 def book_xml(request, slug):
 
 @never_cache
 def book_xml(request, slug):
-    xml = get_object_or_404(Book, slug=slug).materialize()
-    
+    xml = get_object_or_404(Book, slug=slug).materialize(Book.publish_tag())
+
     response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
     return response
     response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
     return response
@@ -262,7 +263,7 @@ def book_xml(request, slug):
 
 @never_cache
 def book_txt(request, slug):
 
 @never_cache
 def book_txt(request, slug):
-    xml = get_object_or_404(Book, slug=slug).materialize()
+    xml = get_object_or_404(Book, slug=slug).materialize(Book.publish_tag())
     output = StringIO()
     # errors?
     librarian.text.transform(StringIO(xml), output)
     output = StringIO()
     # errors?
     librarian.text.transform(StringIO(xml), output)
@@ -274,7 +275,7 @@ def book_txt(request, slug):
 
 @never_cache
 def book_html(request, slug):
 
 @never_cache
 def book_html(request, slug):
-    xml = get_object_or_404(Book, slug=slug).materialize()
+    xml = get_object_or_404(Book, slug=slug).materialize(Book.publish_tag())
     output = StringIO()
     # errors?
     librarian.html.transform(StringIO(xml), output, parse_dublincore=False,
     output = StringIO()
     # errors?
     librarian.html.transform(StringIO(xml), output, parse_dublincore=False,
@@ -390,7 +391,7 @@ def history(request, slug, chunk=None):
                 "description": change.description,
                 "author": change.author_str(),
                 "date": change.created_at,
                 "description": change.description,
                 "author": change.author_str(),
                 "date": change.created_at,
-                "tag": [],
+                "tag": ',\n'.join(unicode(tag) for tag in change.tags.all()),
             })
     return JSONResponse(changes)
 
             })
     return JSONResponse(changes)
 
@@ -403,28 +404,28 @@ def book(request, slug):
     })
 
 
     })
 
 
-
-"""
-import wlapi
-
-
 @require_POST
 @ajax_require_permission('wiki.can_change_tags')
 @require_POST
 @ajax_require_permission('wiki.can_change_tags')
-def add_tag(request, name):
-    name = normalize_name(name)
-    storage = getstorage()
-
+def add_tag(request, slug, chunk=None):
     form = DocumentTagForm(request.POST, prefix="addtag")
     if form.is_valid():
     form = DocumentTagForm(request.POST, prefix="addtag")
     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)
+        try:
+            doc = Chunk.get(slug, chunk)
+        except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+            raise Http404
+
+        tag = form.cleaned_data['tag']
+        revision = revision=form.cleaned_data['revision']
+        doc.at_revision(revision).tags.add(tag)
         return JSONResponse({"message": _("Tag added")})
     else:
         return JSONFormInvalid(form)
 
 
         return JSONResponse({"message": _("Tag added")})
     else:
         return JSONFormInvalid(form)
 
 
+"""
+import wlapi
+
+
 @require_POST
 @ajax_require_permission('wiki.can_publish')
 def publish(request, name):
 @require_POST
 @ajax_require_permission('wiki.can_publish')
 def publish(request, name):
index 94a369b..4fe20e2 100644 (file)
                                        stub: $stub,
                                        data: this,
                                        filters: {
                                        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', ');
 //                                             }
 //                        description: function(value) {
 //                                                 return value.replace('\n', ');
 //                                             }
index 5786f15..2f79b09 100644 (file)
@@ -48,7 +48,7 @@
             return base_path + "/rev/" + arguments[1] + '/';
 
                if (vname == "ajax_document_addtag")
             return base_path + "/rev/" + arguments[1] + '/';
 
                if (vname == "ajax_document_addtag")
-                       return base_path + "/tags/" + arguments[1] + '/';
+                       return base_path + "/tag/" + arguments[1] + '/';
 
                if (vname == "ajax_publish")
                        return base_path + "/publish/" + arguments[1] + '/';
 
                if (vname == "ajax_publish")
                        return base_path + "/publish/" + arguments[1] + '/';