working prototype without mercurial repo
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 18 May 2011 14:46:36 +0000 (16:46 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 18 May 2011 14:46:36 +0000 (16:46 +0200)
18 files changed:
apps/dvcs/migrations/0001_initial.py [new file with mode: 0644]
apps/dvcs/migrations/__init__.py [new file with mode: 0644]
apps/dvcs/models.py
apps/wiki/admin.py
apps/wiki/forms.py
apps/wiki/migrations/0003_auto__add_book.py [new file with mode: 0644]
apps/wiki/models.py
apps/wiki/templates/wiki/document_details_base.html
apps/wiki/templates/wiki/document_list.html
apps/wiki/templates/wiki/document_upload.html
apps/wiki/templates/wiki/tabs/history_view.html
apps/wiki/templates/wiki/tabs/source_editor.html
apps/wiki/templates/wiki/tabs/summary_view.html
apps/wiki/templates/wiki/tabs/summary_view_item.html
apps/wiki/templates/wiki/tabs/wysiwyg_editor.html
apps/wiki/urls.py
apps/wiki/views.py
redakcja/static/js/wiki/wikiapi.js

diff --git a/apps/dvcs/migrations/0001_initial.py b/apps/dvcs/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..c5e75c9
--- /dev/null
@@ -0,0 +1,98 @@
+# 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 'Change'
+        db.create_table('dvcs_change', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('author', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
+            ('patch', self.gf('django.db.models.fields.TextField')(blank=True)),
+            ('tree', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['dvcs.Document'])),
+            ('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)),
+        ))
+        db.send_create_signal('dvcs', ['Change'])
+
+        # Adding model 'Document'
+        db.create_table('dvcs_document', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('creator', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
+            ('head', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['dvcs.Change'], null=True, blank=True)),
+        ))
+        db.send_create_signal('dvcs', ['Document'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'Change'
+        db.delete_table('dvcs_change')
+
+        # Deleting model 'Document'
+        db.delete_table('dvcs_document')
+
+
+    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'})
+        }
+    }
+
+    complete_apps = ['dvcs']
diff --git a/apps/dvcs/migrations/__init__.py b/apps/dvcs/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
index 18fe18f..1c30387 100644 (file)
@@ -42,7 +42,12 @@ class Change(models.Model):
         return pickle.dumps(mdiff.textdiff(src, dst))
 
     def materialize(self):
-        changes = Change.objects.exclude(parent=None).filter(
+        # special care for merged nodes
+        if self.parent is None and self.merge_parent is not None:
+            return self.apply_to(self.merge_parent.materialize())
+
+        changes = Change.objects.filter(
+                        ~models.Q(parent=None) | models.Q(merge_parent=None),
                         tree=self.tree,
                         created_at__lte=self.created_at).order_by('created_at')
         text = u''
@@ -80,6 +85,10 @@ class Change(models.Model):
                     patch=patch, merge_parent=other, tree=self.tree,
                     author=author, description=description)
 
+    def revert(self, **kwargs):
+        """ commit this version of a doc as new head """
+        self.tree.commit(text=self.materialize(), **kwargs)
+
 
 class Document(models.Model):
     """
@@ -90,6 +99,15 @@ 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)
 
@@ -100,14 +118,14 @@ class Document(models.Model):
                         'version': self.head_id,
         })
 
-    def materialize(self, version=None):
+    def materialize(self, change=None):
         if self.head is None:
             return u''
-        if version is None:
-            version = self.head
-        elif not isinstance(version, Change):
-            version = self.change_set.get(pk=version)
-        return version.materialize()
+        if change is None:
+            change = self.head
+        elif not isinstance(change, Change):
+            change = self.change_set.get(pk=change)
+        return change.materialize()
 
     def commit(self, **kwargs):
         if 'parent' not in kwargs:
@@ -120,7 +138,7 @@ class Document(models.Model):
         if 'patch' not in kwargs:
             if 'text' not in kwargs:
                 raise ValueError("You must provide either patch or target document.")
-            patch = Change.make_patch(self.materialize(version=parent), kwargs['text'])
+            patch = Change.make_patch(self.materialize(change=parent), kwargs['text'])
         else:
             if 'text' in kwargs:
                 raise ValueError("You can provide only text or patch - not both")
@@ -133,18 +151,28 @@ class Document(models.Model):
             self.head = old_head.merge_with(change, author=kwargs['author'])
         else:
             self.head = parent.make_child(patch, kwargs['author'], kwargs.get('description', ''))
+
         self.save()
         return self.head
 
     def history(self):
-        return self.changes.all()
+        return self.change_set.all()
+
+    def revision(self):
+        return self.change_set.all().count()
+
+    def at_revision(self, rev):
+        if rev:
+            return self.change_set.all()[rev-1]
+        else:
+            return self.head
 
     @staticmethod
     def listener_initial_commit(sender, instance, created, **kwargs):
         if created:
             instance.head = Change.objects.create(
                     author=instance.creator,
-                    patch=pickle.dumps(mdiff.textdiff('', '')),
+                    patch=Change.make_patch('', ''),
                     tree=instance)
             instance.save()
 
index 1a61b66..60a78f4 100644 (file)
@@ -2,4 +2,5 @@ from django.contrib import admin
 
 from wiki import models
 
+admin.site.register(models.Book)
 admin.site.register(models.Theme)
index 74934bd..b057be0 100644 (file)
@@ -5,6 +5,7 @@
 #
 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 _
 
 
@@ -18,16 +19,19 @@ class DocumentTagForm(forms.Form):
     revision = forms.IntegerField(widget=forms.HiddenInput)
 
 
-class DocumentCreateForm(forms.Form):
+class DocumentCreateForm(forms.ModelForm):
     """
         Form used for creating new documents.
     """
-    title = forms.CharField()
-    id = forms.RegexField(regex=ur"^[-\wąćęłńóśźżĄĆĘŁŃÓŚŹŻ]+$")
     file = forms.FileField(required=False)
     text = forms.CharField(required=False, widget=forms.Textarea)
 
+    class Meta:
+        model = Book
+        exclude = ['gallery']
+
     def clean(self):
+        super(DocumentCreateForm, self).clean()
         file = self.cleaned_data['file']
 
         if file is not None:
diff --git a/apps/wiki/migrations/0003_auto__add_book.py b/apps/wiki/migrations/0003_auto__add_book.py
new file mode 100644 (file)
index 0000000..1c57004
--- /dev/null
@@ -0,0 +1,97 @@
+# 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']
index a3b952a..a926493 100644 (file)
@@ -13,14 +13,36 @@ import logging
 logger = logging.getLogger("fnp.wiki")
 
 
-class Document(models.Model):
+class Book(models.Model):
     """ A document edited on the wiki """
 
-    slug = models.CharField(_('slug'))
-    title = models.CharField(_('displayed title'), blank=True)
-    data = models.ForeignKey(dvcs_models.Document)
-    gallery = models.CharField(_('scan gallery name'), blank=True)
+    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)
+    gallery = models.CharField(_('scan gallery name'), max_length=255, blank=True)
 
+    class Meta:
+        ordering = ['title']
+        verbose_name = _('book')
+        verbose_name_plural = _('books')
+
+    def __unicode__(self):
+        return self.title
+
+    @classmethod
+    def create(cls, creator=None, text=u'', *args, **kwargs):
+        instance = cls(*args, **kwargs)
+        instance.doc = dvcs_models.Document.create(creator=creator, text=text)
+        instance.save()
+        return instance
+
+    @staticmethod
+    def listener_create(sender, instance, created, **kwargs):
+        if created and instance.doc is None:
+            instance.doc = dvcs_models.Document.objects.create()
+            instance.save()
+
+models.signals.post_save.connect(Book.listener_create, sender=Book)
 
 
 
index 0323133..183da3d 100644 (file)
@@ -1,7 +1,7 @@
 {% extends "base.html" %}
 {% load toolbar_tags i18n %}
 
-{% block title %}{{ document.name }} - {{ block.super }}{% endblock %}
+{% block title %}{{ book.title }} - {{ block.super }}{% endblock %}
 {% block extrahead %}
 {% load compressed %}
 {% compressed_css 'detail' %}
 
 {% block maincontent %}
 <div id="document-meta"
-       data-document-name="{{ document.name }}" style="display:none">
+       data-document-name="{{ book.slug }}" style="display:none">
 
-       {% for k, v in document_meta.items %}
-               <span data-key="{{ k }}">{{ v }}</span>
-       {% endfor %}
-
-       {% for k, v in document_info.items %}
-               <span data-key="{{ k }}">{{ v }}</span>
-       {% endfor %}
+       <span data-key="gallery">{{ book.gallery }}</span>
+       <span data-key="revision">{{ revision }}</span>
 
        {% block meta-extra %} {% endblock %}
 </div>
index 6853801..726eaf2 100644 (file)
@@ -1,7 +1,6 @@
 {% extends "wiki/base.html" %}
 
 {% load i18n %}
-{% load wiki %}
 
 {% block extrabody %}
 {{ block.super }}
@@ -31,10 +30,10 @@ $(function() {
                        </tr>
                </thead>
                <tbody>
-       {% for doc in docs %}
+       {% for book in books %}
             <tr>
-               <td colspan="3"><a target="_blank" data-id="{{doc}}"
-                                       href="{% url wiki_editor doc %}">{{ doc|wiki_title }}</a></td>
+               <td colspan="3"><a target="_blank" data-id="{{ book.slug }}"
+                                       href="{% url wiki_editor book.slug %}">{{ book.title }}</a></td>
                                <!-- placeholder </td> -->
                        </tr>
        {% endfor %}
@@ -47,9 +46,9 @@ $(function() {
        <div id="last-edited-list">
                <h2>{% trans "Your last edited documents" %}</h2>
            <ol>
-                       {% for name, date in last_docs %}
-                       <li><a href="{% url wiki_editor name %}"
-                               target="_blank">{{ name|wiki_title }}</a><br/><span class="date">({{ date|date:"H:i:s, d/m/Y" }})</span></li>
+                       {% for slug, item in last_books %}
+                       <li><a href="{% url wiki_editor slug %}"
+                               target="_blank">{{ item.title }}</a><br/><span class="date">({{ item.time|date:"H:i:s, d/m/Y" }})</span></li>
                        {% endfor %}
                </ol>
        </div>
index d4c89d3..929ee4d 100644 (file)
@@ -1,6 +1,5 @@
 {% extends "wiki/base.html" %}
 {% load i18n %}
-{% load wiki %}
 
 
 {% block leftcolumn %}
     <h3>{% trans "Offending files" %}</h3>
     <ul id='error-list'>
         {% for filename, title, error in error_list %}
-            <li>{{title|wiki_title}} (<code>{{ filename }}</code>): {{ error }}</li>
+            <li>{{ title }} (<code>{{ filename }}</code>): {{ error }}</li>
         {% endfor %}
     </ul>
 
     {% if ok_list %}
     <h3>{% trans "Correct files" %}</h3>
         <ul>
-            {% for filename, title in ok_list %}
-                <li>{{title|wiki_title}} (<code>{{ filename }}</code>)</li>
+            {% for filename, slug, title in ok_list %}
+                <li>{{ title }} (<code>{{ filename }}</code>)</li>
             {% endfor %}
         </ul>
     {% endif %}
@@ -44,8 +43,8 @@
         <p class='success'>{% trans "Files have been successfully uploaded to the repository." %}</p>
         <h3>{% trans "Uploaded files" %}</h3>
         <ul id='ok-list'>
-        {% for filename, title in ok_list %}
-            <li><a href='{% url wiki_editor title %}'>{{ title|wiki_title }}</a> (<code>{{ filename }})</a></li>
+        {% for filename, slug, title in ok_list %}
+            <li><a href='{% url wiki_editor slug %}'>{{ title }}</a> (<code>{{ filename }})</a></li>
         {% endfor %}
         </ul>
     {% endif %}
index d9b74dc..7d61f99 100644 (file)
@@ -9,7 +9,7 @@
                        data-enabled-when="1" disabled="disabled">{% trans "Revert document" %}</button>
                <button id="open-preview-button" disabled="disabled"
                        data-enabled-when="1"
-                       data-basehref="{% url wiki_editor_readonly document_name %}">{% trans "View version" %}</button>
+                       data-basehref="{% url wiki_editor_readonly book.slug %}">{% trans "View version" %}</button>
 
        </div>
     <div id="history-view">
index 72d881c..9c3558f 100644 (file)
@@ -1,10 +1,10 @@
 {% load toolbar_tags i18n %}
 <div id="source-editor" class="editor">
-    {% if not document_info.readonly %}{% toolbar %}{% endif %}
+    {% if not readonly %}{% toolbar %}{% endif %}
     <textarea id="codemirror_placeholder">&lt;br/&gt;</textarea>
-    <!-- <input type="hidden" name="name" value="{{ document.name }}" />
+    <!-- <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="{{ document_info.revision }}" />
+       <input type="hidden" name="revision" value="{{ book.doc.revision }}" />
        -->
-</div>
\ No newline at end of file
+</div>
index c33baec..90c2429 100644 (file)
@@ -1,5 +1,4 @@
 {% load i18n %}
-{% load wiki %}
 <div id="summary-view-editor" class="editor" style="display: none">
     <!-- <div class="toolbar">
     </div> -->
@@ -9,23 +8,23 @@
                <h2>
                        <label for="title">{% trans "Title" %}:</label>
                        <span data-ui-editable="true" data-edit-target="meta.displayTitle"
-                       >{{ document.name|wiki_title }}</span>
+                       >{{ book.title }}</span>
                </h2>
                <p>
                        <label>{% trans "Document ID" %}:</label>
-                       <span>{{ document.name }}</span>
+                       <span>{{ book.slug }}</span>
                </p>
                <p>
                        <label>{% trans "Current version" %}:</label>
-                       {{ document_info.revision }} ({{document_info.date}})
+                       {{ book.doc.revision }} ({{ book.doc.head.created_at }})
                <p>
                        <label>{% trans "Last edited by" %}:</label>
-                       {{document_info.author}}
+                       {{ book.doc.head.author }}
                </p>
                <p>
                        <label for="gallery">{% trans "Link to gallery" %}:</label>
                        <span data-ui-editable="true" data-edit-target="meta.galleryLink"
-                       >{{ document_meta.gallery}}</span>
+                       >{{ book.gallery }}</span>
                </p>
 
                <p><button type="button" id="publish_button">{% trans "Publish" %}</button></p>
index 2b4daeb..856b3d7 100644 (file)
@@ -1,5 +1,4 @@
 {% load i18n %}
-{% load wiki %}
 <li id="SummaryPerspective" data-ui-related="summary-view-editor" data-ui-jsclass="SummaryPerspective">
     <span>{% trans "Summary" %}</span>
 </li>
index 5e3c46f..f54f3fb 100644 (file)
@@ -4,7 +4,7 @@
     </div>
 
        <div class="toolbar">
-       {% if not document_info.readonly %}
+       {% if not readonly %}
         <button id="insert-theme-button">
             {% trans "Insert theme" %}
         </button>
index 2b4a65a..ceb7416 100644 (file)
@@ -4,48 +4,48 @@ from django.views.generic.simple import redirect_to
 from django.conf import settings
 
 
-PART = ur"""[ ĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9\w_.-]+"""
+#PART = ur"""[ ĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9\w_.-]+"""
 
 urlpatterns = patterns('wiki.views',
     url(r'^$', redirect_to, {'url': 'catalogue/'}),
 
     url(r'^catalogue/$', 'document_list', name='wiki_document_list'),
-    url(r'^catalogue/([^/]+)/$', 'document_list'),
-    url(r'^catalogue/([^/]+)/([^/]+)/$', 'document_list'),
-    url(r'^catalogue/([^/]+)/([^/]+)/([^/]+)$', 'document_list'),
+    #url(r'^catalogue/([^/]+)/$', 'document_list'),
+    #url(r'^catalogue/([^/]+)/([^/]+)/$', 'document_list'),
+    #url(r'^catalogue/([^/]+)/([^/]+)/([^/]+)$', 'document_list'),
 
-    url(r'^(?P<name>%s)$' % PART,
+    url(r'^(?P<slug>[^/]+)/$',
         'editor', name="wiki_editor"),
 
-    url(r'^(?P<name>[^/]+)/readonly$',
+    url(r'^(?P<slug>[^/]+)/readonly$',
         'editor_readonly', name="wiki_editor_readonly"),
 
     url(r'^upload/$',
         'upload', name='wiki_upload'),
 
-    url(r'^create/(?P<name>[^/]+)',
+    url(r'^create/(?P<slug>[^/]+)',
         'create_missing', name='wiki_create_missing'),
 
     url(r'^(?P<directory>[^/]+)/gallery$',
         'gallery', name="wiki_gallery"),
 
-    url(r'^(?P<name>[^/]+)/history$',
+    url(r'^(?P<slug>[^/]+)/history$',
         'history', name="wiki_history"),
 
-    url(r'^(?P<name>[^/]+)/rev$',
+    url(r'^(?P<slug>[^/]+)/rev$',
         'revision', name="wiki_revision"),
 
-    url(r'^(?P<name>[^/]+)/text$',
+    url(r'^(?P<slug>[^/]+)/text$',
         'text', name="wiki_text"),
 
-    url(r'^(?P<name>[^/]+)/revert$',
+    url(r'^(?P<slug>[^/]+)/revert$',
         'revert', name='wiki_revert'),
 
-    url(r'^(?P<name>[^/]+)/publish$', 'publish', name="wiki_publish"),
-    url(r'^(?P<name>[^/]+)/publish/(?P<version>\d+)$', 'publish', name="wiki_publish"),
+    #url(r'^(?P<name>[^/]+)/publish$', 'publish', name="wiki_publish"),
+    #url(r'^(?P<name>[^/]+)/publish/(?P<version>\d+)$', 'publish', name="wiki_publish"),
 
-    url(r'^(?P<name>[^/]+)/diff$', 'diff', name="wiki_diff"),
-    url(r'^(?P<name>[^/]+)/tags$', 'add_tag', name="wiki_add_tag"),
+    url(r'^(?P<slug>[^/]+)/diff$', 'diff', name="wiki_diff"),
+    #url(r'^(?P<name>[^/]+)/tags$', 'add_tag', name="wiki_add_tag"),
 
 
 
index 918eb91..6f09ddd 100644 (file)
@@ -1,5 +1,4 @@
 import os
-import functools
 import logging
 logger = logging.getLogger("fnp.wiki")
 
@@ -11,8 +10,9 @@ from django.core.urlresolvers import reverse
 from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError,
                 ajax_require_permission, recursive_groupby)
 from django import http
+from django.shortcuts import get_object_or_404, redirect
 
-from wiki.models import getstorage, DocumentNotFound, normalize_name, split_name, join_name, Theme
+from wiki.models import Book, Theme
 from wiki.forms import DocumentTextSaveForm, DocumentTextRevertForm, DocumentTagForm, DocumentCreateForm, DocumentsUploadForm
 from datetime import datetime
 from django.utils.encoding import smart_unicode
@@ -26,62 +26,42 @@ from django.middleware.gzip import GZipMiddleware
 #
 from django.views.decorators.cache import never_cache
 
-import wlapi
 import nice_diff
 import operator
 
 MAX_LAST_DOCS = 10
 
 
-def normalized_name(view):
-
-    @functools.wraps(view)
-    def decorated(request, name, *args):
-        normalized = normalize_name(name)
-        logger.debug('View check %r -> %r', name, normalized)
-
-        if normalized != name:
-            return http.HttpResponseRedirect(
-                        reverse('wiki_' + view.__name__, kwargs={'name': normalized}))
-
-        return view(request, name, *args)
-
-    return decorated
-
-
 @never_cache
 def document_list(request):
     return direct_to_template(request, 'wiki/document_list.html', extra_context={
-        'docs': getstorage().all(),
-        'last_docs': sorted(request.session.get("wiki_last_docs", {}).items(),
-                        key=operator.itemgetter(1), reverse=True),
+        'books': Book.objects.all(),
+        'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
+                        key=lambda x: x[1]['time'], reverse=True),
     })
 
 
 @never_cache
-@normalized_name
-def editor(request, name, template_name='wiki/document_details.html'):
-    storage = getstorage()
-
+def editor(request, slug, template_name='wiki/document_details.html'):
     try:
-        document = storage.get(name)
-    except DocumentNotFound:
-        return http.HttpResponseRedirect(reverse("wiki_create_missing", args=[name]))
+        book = Book.objects.get(slug=slug)
+    except Book.DoesNotExist:
+        return http.HttpResponseRedirect(reverse("wiki_create_missing", args=[slug]))
 
     access_time = datetime.now()
-    last_documents = request.session.get("wiki_last_docs", {})
-    last_documents[name] = access_time
+    last_books = request.session.get("wiki_last_books", {})
+    last_books[slug] = {
+        'time': access_time,
+        'title': book.title,
+        }
 
-    if len(last_documents) > MAX_LAST_DOCS:
-        oldest_key = min(last_documents, key=last_documents.__getitem__)
-        del last_documents[oldest_key]
-    request.session['wiki_last_docs'] = last_documents
+    if len(last_books) > MAX_LAST_DOCS:
+        oldest_key = min(last_books, key=operator.itemgetter('time'))
+        del last_books[oldest_key]
+    request.session['wiki_last_books'] = last_books
 
     return direct_to_template(request, template_name, extra_context={
-        'document': document,
-        'document_name': document.name,
-        'document_info': document.info,
-        'document_meta': document.meta,
+        'book': book,
         'forms': {
             "text_save": DocumentTextSaveForm(prefix="textsave"),
             "text_revert": DocumentTextRevertForm(prefix="textrevert"),
@@ -92,95 +72,105 @@ def editor(request, name, template_name='wiki/document_details.html'):
 
 
 @require_GET
-@normalized_name
-def editor_readonly(request, name, template_name='wiki/document_details_readonly.html'):
-    name = normalize_name(name)
-    storage = getstorage()
-
+def editor_readonly(request, slug, template_name='wiki/document_details_readonly.html'):
     try:
+        book = Book.objects.get(slug=slug)
         revision = request.GET['revision']
-        document = storage.get(name, revision)
-    except (KeyError, DocumentNotFound):
+    except KeyError:
         raise http.Http404
 
     access_time = datetime.now()
-    last_documents = request.session.get("wiki_last_docs", {})
-    last_documents[name] = access_time
+    last_books = request.session.get("wiki_last_books", {})
+    last_books[slug] = {
+        'time': access_time,
+        'title': book.title,
+        }
 
-    if len(last_documents) > MAX_LAST_DOCS:
-        oldest_key = min(last_documents, key=last_documents.__getitem__)
-        del last_documents[oldest_key]
-    request.session['wiki_last_docs'] = last_documents
+    if len(last_books) > MAX_LAST_DOCS:
+        oldest_key = min(last_books, key=operator.itemgetter('time'))
+        del last_books[oldest_key]
+    request.session['wiki_last_books'] = last_books
 
     return direct_to_template(request, template_name, extra_context={
-        'document': document,
-        'document_name': document.name,
-        'document_info': dict(document.info(), readonly=True),
-        'document_meta': document.meta,
+        'book': book,
+        'revision': revision,
+        'readonly': True,
         'REDMINE_URL': settings.REDMINE_URL,
     })
 
 
-@normalized_name
-def create_missing(request, name):
-    storage = getstorage()
+def create_missing(request, slug):
+    slug = slug.replace(' ', '-')
 
     if request.method == "POST":
         form = DocumentCreateForm(request.POST, request.FILES)
         if form.is_valid():
-            doc = storage.create_document(
-                name=form.cleaned_data['id'],
+            
+            if request.user.is_authenticated():
+                creator = request.user
+            else:
+                creator = None
+            book = Book.create(creator=creator,
+                slug=form.cleaned_data['slug'],
+                title=form.cleaned_data['title'],
                 text=form.cleaned_data['text'],
             )
 
-            return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.name]))
+            return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug]))
     else:
         form = DocumentCreateForm(initial={
-                "id": name.replace(" ", "_"),
-                "title": name.title(),
+                "slug": slug,
+                "title": slug.replace('-', ' ').title(),
         })
 
     return direct_to_template(request, "wiki/document_create_missing.html", extra_context={
-        "document_name": name,
+        "slug": slug,
         "form": form,
     })
 
 
 def upload(request):
-    storage = getstorage()
-
     if request.method == "POST":
         form = DocumentsUploadForm(request.POST, request.FILES)
         if form.is_valid():
+            import slughifi
+
+            if request.user.is_authenticated():
+                creator = request.user
+            else:
+                creator = None
+
             zip = form.cleaned_data['zip']
             skipped_list = []
             ok_list = []
             error_list = []
-            titles = {}
-            existing = storage.all()
+            slugs = {}
+            existing = [book.slug for book in Book.objects.all()]
             for filename in zip.namelist():
                 if filename[-1] == '/':
                     continue
-                title = normalize_name(os.path.basename(filename)[:-4])
-                if not (title and filename.endswith('.xml')):
+                title = os.path.basename(filename)[:-4]
+                slug = slughifi(title)
+                if not (slug and filename.endswith('.xml')):
                     skipped_list.append(filename)
-                elif title in titles:
-                    error_list.append((filename, title, _('Title already used for %s' % titles[title])))
-                elif title in existing:
-                    error_list.append((filename, title, _('Title already used in repository.')))
+                elif slug in slugs:
+                    error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
+                elif slug in existing:
+                    error_list.append((filename, slug, _('Slug already used in repository.')))
                 else:
                     try:
                         zip.read(filename).decode('utf-8') # test read
-                        ok_list.append((filename, title))
+                        ok_list.append((filename, slug, title))
                     except UnicodeDecodeError:
                         error_list.append((filename, title, _('File should be UTF-8 encoded.')))
-                    titles[title] = filename
+                    slugs[slug] = filename
 
             if not error_list:
-                for filename, title in ok_list:
-                    storage.create_document(
-                        name=title,
-                        text=zip.read(filename).decode('utf-8')
+                for filename, slug, title in ok_list:
+                    Book.create(creator=creator,
+                        slug=slug,
+                        title=title,
+                        text=zip.read(filename).decode('utf-8'),
                     )
 
             return direct_to_template(request, "wiki/document_upload.html", extra_context={
@@ -189,12 +179,6 @@ def upload(request):
                 "skipped_list": skipped_list,
                 "error_list": error_list,
             })
-                #doc = storage.create_document(
-                #    name=base,
-                #    text=form.cleaned_data['text'],
-
-            
-            return http.HttpResponse('\n'.join(yeslist) + '\n\n' + '\n'.join(nolist))
     else:
         form = DocumentsUploadForm()
 
@@ -204,93 +188,77 @@ def upload(request):
 
 
 @never_cache
-@normalized_name
 @decorator_from_middleware(GZipMiddleware)
-def text(request, name):
-    storage = getstorage()
+def text(request, slug):
+    doc = get_object_or_404(Book, slug=slug).doc
 
     if request.method == 'POST':
         form = DocumentTextSaveForm(request.POST, prefix="textsave")
         if form.is_valid():
-            revision = form.cleaned_data['parent_revision']
-            document = storage.get_or_404(name, revision)          
-            document.text = form.cleaned_data['text']
-            comment = form.cleaned_data['comment']
-            if form.cleaned_data['stage_completed']:        
-                comment += '\n#stage-finished: %s\n' % form.cleaned_data['stage_completed']         
+            # TODO:
+            # - stage completion should be stored (as a relation)
+
             if request.user.is_authenticated():
-                author_name = request.user
-                author_email = request.user.email
+                author = request.user
             else:
-                author_name = form.cleaned_data['author_name']
-                author_email = form.cleaned_data['author_email']
-            author = "%s <%s>" % (author_name, author_email)
-            storage.put(document, author=author, comment=comment, parent=revision)           
-            document = storage.get(name)          
+                author = None
+            text = form.cleaned_data['text']
+            parent_revision = form.cleaned_data['parent_revision']
+            parent = doc.at_revision(parent_revision)
+            doc.commit(author=author,
+                       text=text,
+                       parent=parent,
+                       description=form.cleaned_data['comment'],
+                       )
+            revision = doc.revision()
             return JSONResponse({
-                'text': document.plain_text if revision != document.revision else None,
-                'meta': document.meta(),
-                'revision': document.revision,
+                'text': doc.materialize() if parent_revision != revision else None,
+                'meta': {},
+                'revision': revision,
             })
         else:
             return JSONFormInvalid(form)
     else:
         revision = request.GET.get("revision", None)
-
+        
         try:
-            try:
-                revision = revision and int(revision)
-                logger.info("Fetching %s", revision)
-                document = storage.get(name, revision)
-            except ValueError:
-                # treat as a tag
-                logger.info("Fetching tag %s", revision)
-                document = storage.get_by_tag(name, revision)
-        except DocumentNotFound:
-            raise http.Http404
+            revision = int(revision)
+        except ValueError:
+            revision = None
 
         return JSONResponse({
-            'text': document.plain_text,
-            'meta': document.meta(),
-            'revision': document.revision,
+            'text': doc.at_revision(revision).materialize(),
+            'meta': {},
+            'revision': revision if revision else doc.revision(),
         })
 
 
 @never_cache
-@normalized_name
 @require_POST
-def revert(request, name):
-    storage = getstorage()
+def revert(request, slug):
     form = DocumentTextRevertForm(request.POST, prefix="textrevert")
     if form.is_valid():
-        print 'valid'
+        doc = get_object_or_404(Book, slug=slug).doc
         revision = form.cleaned_data['revision']
 
         comment = form.cleaned_data['comment']
         comment += "\n#revert to %s" % revision
 
         if request.user.is_authenticated():
-            author_name = request.user
-            author_email = request.user.email
+            author = request.user
         else:
-            author_name = form.cleaned_data['author_name']
-            author_email = form.cleaned_data['author_email']
-        author = "%s <%s>" % (author_name, author_email)
-
-        before = storage.get(name).revision
-        logger.info("Reverting %s to %s", name, revision)
-        storage.revert(name, revision, comment=comment, author=author)
-        logger.info("Fetching %s", name)
-        document = storage.get(name)
+            author = None
 
+        before = doc.revision()
+        logger.info("Reverting %s to %s", slug, revision)
+        doc.at_revision(revision).revert(author=author, description=comment)
 
         return JSONResponse({
-            'text': document.plain_text if before != document.revision else None,
-            'meta': document.meta(),
-            'revision': document.revision,
+            'text': doc.materialize() if before != doc.revision() else None,
+            'meta': {},
+            'revision': doc.revision(),
         })
     else:
-        print 'invalid'
         return JSONFormInvalid(form)
 
 
@@ -322,10 +290,7 @@ def gallery(request, directory):
 
 
 @never_cache
-@normalized_name
-def diff(request, name):
-    storage = getstorage()
-
+def diff(request, slug):
     revA = int(request.GET.get('from', 0))
     revB = int(request.GET.get('to', 0))
 
@@ -335,33 +300,48 @@ def diff(request, name):
     if revB == 0:
         revB = None
 
-    docA = storage.get_or_404(name, int(revA))
-    docB = storage.get_or_404(name, int(revB))
+    doc = get_object_or_404(Book, slug=slug).doc
+    docA = doc.at_revision(revA).materialize()
+    docB = doc.at_revision(revB).materialize()
 
-    return http.HttpResponse(nice_diff.html_diff_table(docA.plain_text.splitlines(),
-                                         docB.plain_text.splitlines(), context=3))
+    return http.HttpResponse(nice_diff.html_diff_table(docA.splitlines(),
+                                         docB.splitlines(), context=3))
 
 
 @never_cache
-@normalized_name
-def revision(request, name):
-    storage = getstorage()
-
-    try:
-        return http.HttpResponse(str(storage.doc_meta(name)['revision']))
-    except DocumentNotFound:
-        raise http.Http404
+def revision(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    return http.HttpResponse(str(book.doc.revision()))
 
 
 @never_cache
-@normalized_name
-def history(request, name):
-    storage = getstorage()
-
+def history(request, slug):
     # TODO: pagination
-    changesets = list(storage.history(name))
+    book = get_object_or_404(Book, slug=slug)
+    rev = book.doc.revision()
+    changes = []
+    for change in book.doc.history().order_by('-created_at'):
+        if change.author:
+            author = "%s %s <%s>" % (
+                change.author.first_name,
+                change.author.last_name, 
+                change.author.email)
+        else:
+            author = None
+        changes.append({
+                "version": rev,
+                "description": change.description,
+                "author": author,
+                "date": change.created_at,
+                "tag": [],
+            })
+        rev -= 1
+    return JSONResponse(changes)
+
 
-    return JSONResponse(changesets)
+
+"""
+import wlapi
 
 
 @require_POST
@@ -395,7 +375,7 @@ def publish(request, name):
         return JSONResponse({"result": api.publish_book(document)})
     except wlapi.APICallException, e:
         return JSONServerError({"message": str(e)})
-
+"""
 
 def themes(request):
     prefix = request.GET.get('q', '')
index 8da5929..1fe49d4 100644 (file)
                        data[this.name] = this.value;
                });
 
-               var metaComment = '<!--';
-               metaComment += '\n\tgallery:' + self.galleryLink;
-               metaComment += '\n-->\n'
-
-               data['textsave-text'] = metaComment + self.text;
+               data['textsave-text'] = self.text;
 
                $.ajax({
                        url: reverse("ajax_document_text", self.id),