teh legendary `Publish' button, oh yeah!
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 8 Jun 2011 13:03:59 +0000 (15:03 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 8 Jun 2011 13:03:59 +0000 (15:03 +0200)
authorization to WLAPI through OAuth,
marking revisions for publication

31 files changed:
apps/apiclient/__init__.py [new file with mode: 0644]
apps/apiclient/migrations/0001_initial.py [new file with mode: 0644]
apps/apiclient/migrations/__init__.py [new file with mode: 0644]
apps/apiclient/models.py [new file with mode: 0644]
apps/apiclient/settings.py [new file with mode: 0755]
apps/apiclient/tests.py [new file with mode: 0644]
apps/apiclient/urls.py [new file with mode: 0755]
apps/apiclient/views.py [new file with mode: 0644]
apps/dvcs/migrations/0001_initial.py
apps/dvcs/migrations/0002_auto__add_tag.py [deleted file]
apps/dvcs/models.py
apps/wiki/forms.py
apps/wiki/migrations/0003_auto__add_book__add_chunk__add_unique_chunk_book_number__add_unique_ch.py
apps/wiki/migrations/0004_auto__add_field_book__list_html.py [deleted file]
apps/wiki/models.py
apps/wiki/templates/wiki/book_detail.html
apps/wiki/templates/wiki/document_details.html
apps/wiki/templates/wiki/pubmark_dialog.html [new file with mode: 0755]
apps/wiki/templates/wiki/tabs/history_view.html
apps/wiki/urls.py
apps/wiki/views.py
redakcja/settings/common.py
redakcja/settings/compress.py
redakcja/static/css/filelist.css
redakcja/static/img/angel-left.png [new file with mode: 0644]
redakcja/static/img/angel-right.png [new file with mode: 0644]
redakcja/static/js/wiki/dialog_pubmark.js [new file with mode: 0755]
redakcja/static/js/wiki/view_history.js
redakcja/static/js/wiki/wikiapi.js
redakcja/urls.py
requirements.txt

diff --git a/apps/apiclient/__init__.py b/apps/apiclient/__init__.py
new file mode 100644 (file)
index 0000000..7913ac3
--- /dev/null
@@ -0,0 +1,46 @@
+import urllib
+
+from django.utils import simplejson
+import oauth2
+
+from apiclient.models import OAuthConnection
+from apiclient.settings import WL_CONSUMER_KEY, WL_CONSUMER_SECRET, WL_API_URL
+
+
+if WL_CONSUMER_KEY and WL_CONSUMER_SECRET:
+    wl_consumer = oauth2.Consumer(WL_CONSUMER_KEY, WL_CONSUMER_SECRET)
+else:
+    wl_consumer = None
+
+
+class ApiError(BaseException):
+    pass
+
+
+class NotAuthorizedError(BaseException):
+    pass
+
+
+def api_call(user, path, data=None):
+    # what if not verified?
+    conn = OAuthConnection.get(user)
+    if not conn.access:
+        raise NotAuthorizedError("No WL authorization for user %s." % user)
+    token = oauth2.Token(conn.token, conn.token_secret)
+    client = oauth2.Client(wl_consumer, token)
+    if data is not None:
+        resp, content = client.request(
+                "%s%s.json" % (WL_API_URL, path),
+                method="POST",
+                body=urllib.urlencode(data))
+    else:
+        resp, content = client.request(
+                "%s%s.json" % (WL_API_URL, path))
+    status = resp['status']
+    if status == '200':
+        return simplejson.loads(content)
+    elif status.startswith('2'):
+        return
+    else:
+        raise ApiError("WL API call error")
+
diff --git a/apps/apiclient/migrations/0001_initial.py b/apps/apiclient/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..4af28a5
--- /dev/null
@@ -0,0 +1,75 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Adding model 'OAuthConnection'
+        db.create_table('apiclient_oauthconnection', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('user', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['auth.User'], unique=True)),
+            ('access', self.gf('django.db.models.fields.BooleanField')(default=False)),
+            ('token', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)),
+            ('token_secret', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)),
+        ))
+        db.send_create_signal('apiclient', ['OAuthConnection'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'OAuthConnection'
+        db.delete_table('apiclient_oauthconnection')
+
+
+    models = {
+        'apiclient.oauthconnection': {
+            'Meta': {'object_name': 'OAuthConnection'},
+            'access': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+            'token_secret': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
+        },
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['apiclient']
diff --git a/apps/apiclient/migrations/__init__.py b/apps/apiclient/migrations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/apiclient/models.py b/apps/apiclient/models.py
new file mode 100644 (file)
index 0000000..d3c8f62
--- /dev/null
@@ -0,0 +1,20 @@
+from django.db import models
+from django.contrib.auth.models import User
+
+
+class OAuthConnection(models.Model):
+    user = models.OneToOneField(User)
+    access = models.BooleanField(default=False)
+    token = models.CharField(max_length=64, null=True, blank=True)
+    token_secret = models.CharField(max_length=64, null=True, blank=True)
+
+    @classmethod
+    def get(cls, user):
+        try:
+            return cls.objects.get(user=user)
+        except cls.DoesNotExist:
+            o = cls(user=user)
+            o.save()
+            return o
+
+
diff --git a/apps/apiclient/settings.py b/apps/apiclient/settings.py
new file mode 100755 (executable)
index 0000000..5fbf18e
--- /dev/null
@@ -0,0 +1,15 @@
+from django.conf import settings
+
+
+WL_CONSUMER_KEY = getattr(settings, 'APICLIENT_WL_CONSUMER_KEY', None)
+WL_CONSUMER_SECRET = getattr(settings, 'APICLIENT_WL_CONSUMER_SECRET', None)
+
+WL_API_URL = getattr(settings, 'APICLIENT_WL_API_URL', 
+        'http://www.wolnelektury.pl/api/')
+
+WL_REQUEST_TOKEN_URL = getattr(settings, 'APICLIENT_WL_REQUEST_TOKEN_URL', 
+        WL_API_URL + 'oauth/request_token/')
+WL_ACCESS_TOKEN_URL = getattr(settings, 'APICLIENT_WL_ACCESS_TOKEN_URL', 
+        WL_API_URL + 'oauth/access_token/')
+WL_AUTHORIZE_URL = getattr(settings, 'APICLIENT_WL_AUTHORIZE_URL', 
+        WL_API_URL + 'oauth/authorize/')
diff --git a/apps/apiclient/tests.py b/apps/apiclient/tests.py
new file mode 100644 (file)
index 0000000..2247054
--- /dev/null
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+    def test_basic_addition(self):
+        """
+        Tests that 1 + 1 always equals 2.
+        """
+        self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
diff --git a/apps/apiclient/urls.py b/apps/apiclient/urls.py
new file mode 100755 (executable)
index 0000000..5e54965
--- /dev/null
@@ -0,0 +1,6 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('apiclient.views',
+    url(r'^oauth/$', 'oauth', name='users_oauth'),
+    url(r'^oauth_callback/$', 'oauth_callback', name='users_oauth_callback'),
+)
diff --git a/apps/apiclient/views.py b/apps/apiclient/views.py
new file mode 100644 (file)
index 0000000..f851590
--- /dev/null
@@ -0,0 +1,60 @@
+import cgi
+
+from django.contrib.auth.decorators import login_required
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect, HttpResponse
+import oauth2
+
+from apiclient.models import OAuthConnection
+from apiclient import wl_consumer
+from apiclient.settings import (WL_REQUEST_TOKEN_URL, WL_ACCESS_TOKEN_URL, 
+        WL_AUTHORIZE_URL)
+
+
+@login_required
+def oauth(request):
+    if wl_consumer is None:
+        return HttpResponse("OAuth consumer not configured.")
+
+    client = oauth2.Client(wl_consumer)
+    resp, content = client.request(WL_REQUEST_TOKEN_URL)
+    if resp['status'] != '200':
+        raise Exception("Invalid response %s." % resp['status'])
+
+    request_token = dict(cgi.parse_qsl(content))
+    
+    conn = OAuthConnection.get(request.user)
+    # this might reset existing auth!
+    conn.access = False
+    conn.token = request_token['oauth_token']
+    conn.token_secret = request_token['oauth_token_secret']
+    conn.save()
+
+    url = "%s?oauth_token=%s&oauth_callback=%s" % (
+            WL_AUTHORIZE_URL, 
+            request_token['oauth_token'],
+            request.build_absolute_uri(reverse("users_oauth_callback")),
+            )
+
+    return HttpResponseRedirect(url)
+
+
+@login_required
+def oauth_callback(request):
+    if wl_consumer is None:
+        return HttpResponse("OAuth consumer not configured.")
+
+    oauth_verifier = request.GET.get('oauth_verifier')
+    conn = OAuthConnection.get(request.user)
+    token = oauth2.Token(conn.token, conn.token_secret)
+    token.set_verifier(oauth_verifier)
+    client = oauth2.Client(wl_consumer, token)
+    resp, content = client.request(WL_ACCESS_TOKEN_URL, method="POST")
+    access_token = dict(cgi.parse_qsl(content))
+
+    conn.access = True
+    conn.token = access_token['oauth_token']
+    conn.token_secret = access_token['oauth_token_secret']
+    conn.save()
+
+    return HttpResponseRedirect('/')
index 7510e8a..d9f0984 100644 (file)
@@ -8,6 +8,15 @@ class Migration(SchemaMigration):
 
     def forwards(self, orm):
         
 
     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 model 'Change'
         db.create_table('dvcs_change', (
             ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
         # Adding model 'Change'
         db.create_table('dvcs_change', (
             ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
@@ -20,12 +29,21 @@ class Migration(SchemaMigration):
             ('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')(default=datetime.datetime.now, db_index=True)),
             ('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')(default=datetime.datetime.now, db_index=True)),
+            ('publishable', self.gf('django.db.models.fields.BooleanField')(default=False)),
         ))
         db.send_create_signal('dvcs', ['Change'])
 
         # Adding unique constraint on 'Change', fields ['tree', 'revision']
         db.create_unique('dvcs_change', ['tree_id', 'revision'])
 
         ))
         db.send_create_signal('dvcs', ['Change'])
 
         # Adding unique constraint on 'Change', fields ['tree', 'revision']
         db.create_unique('dvcs_change', ['tree_id', 'revision'])
 
+        # 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'])
+
         # Adding model 'Document'
         db.create_table('dvcs_document', (
             ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
         # Adding model 'Document'
         db.create_table('dvcs_document', (
             ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
@@ -40,9 +58,15 @@ class Migration(SchemaMigration):
         # Removing unique constraint on 'Change', fields ['tree', 'revision']
         db.delete_unique('dvcs_change', ['tree_id', 'revision'])
 
         # Removing unique constraint on 'Change', fields ['tree', 'revision']
         db.delete_unique('dvcs_change', ['tree_id', 'revision'])
 
+        # Deleting model 'Tag'
+        db.delete_table('dvcs_tag')
+
         # Deleting model 'Change'
         db.delete_table('dvcs_change')
 
         # Deleting model 'Change'
         db.delete_table('dvcs_change')
 
+        # Removing M2M table for field tags on 'Change'
+        db.delete_table('dvcs_change_tags')
+
         # Deleting model 'Document'
         db.delete_table('dvcs_document')
 
         # Deleting model 'Document'
         db.delete_table('dvcs_document')
 
@@ -94,7 +118,9 @@ class Migration(SchemaMigration):
             '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'}),
             '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'}),
+            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
             'revision': ('django.db.models.fields.IntegerField', [], {'db_index': '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': {
             'tree': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dvcs.Document']"})
         },
         'dvcs.document': {
@@ -102,6 +128,13 @@ class Migration(SchemaMigration):
             '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'})
             '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'})
         }
     }
 
         }
     }
 
diff --git a/apps/dvcs/migrations/0002_auto__add_tag.py b/apps/dvcs/migrations/0002_auto__add_tag.py
deleted file mode 100644 (file)
index 7f5b9a3..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-# 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 dddbf3a..262472a 100644 (file)
@@ -66,6 +66,7 @@ class Change(models.Model):
     description = models.TextField(blank=True, default='')
     created_at = models.DateTimeField(editable=False, db_index=True, 
                         default=datetime.now)
     description = models.TextField(blank=True, default='')
     created_at = models.DateTimeField(editable=False, db_index=True, 
                         default=datetime.now)
+    publishable = models.BooleanField(default=False)
 
     tags = models.ManyToManyField(Tag)
 
 
     tags = models.ManyToManyField(Tag)
 
@@ -245,8 +246,8 @@ 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]
+    def publishable(self):
+        changes = self.change_set.filter(publishable=True).order_by('-created_at')[:1]
         if changes.count():
             return changes[0]
         else:
         if changes.count():
             return changes[0]
         else:
index fb0f958..55b1e54 100644 (file)
@@ -20,6 +20,17 @@ class DocumentTagForm(forms.Form):
     revision = forms.IntegerField(widget=forms.HiddenInput)
 
 
     revision = forms.IntegerField(widget=forms.HiddenInput)
 
 
+class DocumentPubmarkForm(forms.Form):
+    """
+        Form for marking revisions for publishing.
+    """
+
+    id = forms.CharField(widget=forms.HiddenInput)
+    publishable = forms.BooleanField(required=False, initial=True,
+            label=_('Publishable'))
+    revision = forms.IntegerField(widget=forms.HiddenInput)
+
+
 class DocumentCreateForm(forms.ModelForm):
     """
         Form used for creating new documents.
 class DocumentCreateForm(forms.ModelForm):
     """
         Form used for creating new documents.
@@ -180,7 +191,8 @@ class BookAppendForm(forms.Form):
         It means moving all chunks from book A to book B and deleting A.
     """
 
         It means moving all chunks from book A to book B and deleting A.
     """
 
-    append_to = forms.ModelChoiceField(queryset=Book.objects.all())
+    append_to = forms.ModelChoiceField(queryset=Book.objects.all(),
+        label=_("Append to"))
 
 
 class BookForm(forms.ModelForm):
 
 
 class BookForm(forms.ModelForm):
index 229825d..010ed2a 100644 (file)
@@ -151,6 +151,8 @@ class Migration(SchemaMigration):
             ('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)),
             ('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)),
+            ('last_published', self.gf('django.db.models.fields.DateTimeField')(null=True)),
+            ('_list_html', self.gf('django.db.models.fields.TextField')(null=True)),
         ))
         db.send_create_signal('wiki', ['Book'])
 
         ))
         db.send_create_signal('wiki', ['Book'])
 
@@ -235,7 +237,9 @@ class Migration(SchemaMigration):
             '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'}),
             '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'}),
+            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
             'revision': ('django.db.models.fields.IntegerField', [], {'db_index': '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': {
             'tree': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dvcs.Document']"})
         },
         'dvcs.document': {
@@ -244,10 +248,19 @@ class Migration(SchemaMigration):
             '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'})
         },
             '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'})
+        },
         'wiki.book': {
             'Meta': {'ordering': "['parent_number', 'title']", 'object_name': 'Book'},
         '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'}),
             'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
             'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_published': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
             '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'}),
             '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'}),
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
deleted file mode 100644 (file)
index a09dc90..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-# 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 4dc7017..7887e5d 100644 (file)
@@ -28,6 +28,7 @@ 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)
+    last_published = models.DateTimeField(null=True, editable=False)
 
     _list_html = models.TextField(editable=False, null=True)
 
 
     _list_html = models.TextField(editable=False, null=True)
 
@@ -78,18 +79,14 @@ class Book(models.Model):
             self.save(reset_list_html=False)
         return mark_safe(self._list_html)
 
             self.save(reset_list_html=False)
         return mark_safe(self._list_html)
 
-    @staticmethod
-    def publish_tag():
-        return dvcs_models.Tag.get('publish')
-
-    def materialize(self, tag=None):
+    def materialize(self, publishable=True):
         """ 
             Get full text of the document compiled from chunks.
             Takes the current versions of all texts
         """ 
             Get full text of the document compiled from chunks.
             Takes the current versions of all texts
-            or versions most recently tagged by a given tag.
+            or versions most recently tagged for publishing.
         """
         """
-        if tag:
-            changes = [chunk.last_tagged(tag) for chunk in self]
+        if publishable:
+            changes = [chunk.publishable() for chunk in self]
         else:
             changes = [chunk.head for chunk in self]
         if None in changes:
         else:
             changes = [chunk.head for chunk in self]
         if None in changes:
@@ -183,9 +180,6 @@ 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())
-
     def split(self, slug, comment='', creator=None):
         """ Create an empty chunk after this one """
         self.book.chunk_set.filter(number__gt=self.number).update(
     def split(self, slug, comment='', creator=None):
         """ Create an empty chunk after this one """
         self.book.chunk_set.filter(number__gt=self.number).update(
index 26f5214..f4b15a9 100755 (executable)
@@ -62,6 +62,8 @@
 
 <p><a href="{% url wiki_book_append book.slug %}">{% trans "Append to other book" %}</a></p>
 
 
 <p><a href="{% url wiki_book_append book.slug %}">{% trans "Append to other book" %}</a></p>
 
+<p>{% trans "Last published" %}: {{ book.last_published }}</p>
+
 {% if book.publishable %}
     <p>
     <a href="{% url wiki_book_xml book.slug %}">{% trans "Full XML" %}</a><br/>
 {% if book.publishable %}
     <p>
     <a href="{% url wiki_book_xml book.slug %}">{% trans "Full XML" %}</a><br/>
     {% endcomment %}
     </p>
 
     {% endcomment %}
     </p>
 
-    <p style='width:200px; height: 75px; border: 1px dotted gray; border-corners: 4px;'></p>
+    <!--
+    Angel photos:
+    Angels in Ely Cathedral (http://www.flickr.com/photos/21804434@N02/4483220595/) /
+    mira66 (http://www.flickr.com/photos/21804434@N02/) /
+    CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/)
+    -->
+    <form method="POST" action="{% url wiki_publish book.slug %}">{% csrf_token %}
+        <img src="{{ STATIC_URL }}img/angel-left.png" style="vertical-align: middle" />
+        <button id="publish-button" type="submit">
+            <span>{% trans "Publish" %}</span></button>
+        <img src="{{ STATIC_URL }}img/angel-right.png" style="vertical-align: middle" />
+        </form></form></p>
 {% else %}
     {% trans "This book cannot be published yet" %}
 {% endif %}
 {% else %}
     {% trans "This book cannot be published yet" %}
 {% endif %}
index 934a8ac..d95603f 100644 (file)
@@ -43,4 +43,5 @@
     {% include "wiki/save_dialog.html" %}
     {% include "wiki/revert_dialog.html" %}
     {% include "wiki/tag_dialog.html" %}
     {% include "wiki/save_dialog.html" %}
     {% include "wiki/revert_dialog.html" %}
     {% include "wiki/tag_dialog.html" %}
+    {% include "wiki/pubmark_dialog.html" %}
 {% endblock %}
 {% endblock %}
diff --git a/apps/wiki/templates/wiki/pubmark_dialog.html b/apps/wiki/templates/wiki/pubmark_dialog.html
new file mode 100755 (executable)
index 0000000..93a9856
--- /dev/null
@@ -0,0 +1,19 @@
+{% load i18n %}
+<div id="pubmark_dialog" class="dialog" data-ui-jsclass="PubmarkDialog">
+       <form method="POST" action="#">
+               {% for field in forms.pubmark.visible_fields %}
+               <p>{{ field.label_tag }} {{ field }} <span data-ui-error-for="{{ field.name }}"> </span></p>
+               <p>{{ field.help_text }}</p>
+               {% endfor %}
+
+               {% for f in forms.pubmark.hidden_fields %}
+                       {{ f }}
+               {% endfor %}
+               <p data-ui-error-for="__all__"> </p>
+
+               <p class="action_area">
+                       <button type="submit" class="ok" data-ui-action="save">{% trans "Save" %}</button>
+                       <button type="button" class="cancel" data-ui-action="cancel">{% trans "Cancel" %}</button>
+               </p>
+       </form>
+</div>
index 60c59a9..db207bb 100644 (file)
@@ -5,6 +5,8 @@
                        data-enabled-when="2" disabled="disabled">{% trans "Compare versions" %}</button>
                <button type="button" id="tag-changeset-button"
                        data-enabled-when="1" disabled="disabled">{% trans "Mark version" %}</button>
                        data-enabled-when="2" disabled="disabled">{% trans "Compare versions" %}</button>
                <button type="button" id="tag-changeset-button"
                        data-enabled-when="1" disabled="disabled">{% trans "Mark version" %}</button>
+               <button type="button" id="pubmark-changeset-button"
+                       data-enabled-when="1" disabled="disabled">{% trans "Mark for publishing" %}</button>
                <button type="button" id="doc-revert-button"
                        data-enabled-when="1" disabled="disabled">{% trans "Revert document" %}</button>
                <button id="open-preview-button" disabled="disabled"
                <button type="button" id="doc-revert-button"
                        data-enabled-when="1" disabled="disabled">{% trans "Revert document" %}</button>
                <button id="open-preview-button" disabled="disabled"
@@ -26,7 +28,9 @@
                                <br />
                <span data-stub-value="author"></span>, <span data-stub-value="date"></span>
                        </td>
                                <br />
                <span data-stub-value="author"></span>, <span data-stub-value="date"></span>
                        </td>
-                       <td data-stub-value="tag">
+                       <td>
+                <div data-stub-value="publishable"></div>
+                <div data-stub-value="tag"></div>
                        </td>
                </tr>
                </tbody>
                        </td>
                </tr>
                </tbody>
index 393afa5..c7da6ef 100644 (file)
@@ -43,11 +43,12 @@ urlpatterns = patterns('wiki.views',
     url(r'^revert/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
         'revert', name='wiki_revert'),
 
     url(r'^revert/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$',
         'revert', name='wiki_revert'),
 
-    #url(r'^(?P<name>[^/]+)/publish$', 'publish', name="wiki_publish"),
+    url(r'^book/(?P<slug>[^/]+)/publish$', 'publish', name="wiki_publish"),
     #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'^tag/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$', 'add_tag', name="wiki_add_tag"),
     #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'^tag/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$', 'add_tag', name="wiki_add_tag"),
+    url(r'^pubmark/(?P<slug>[^/]+)/(?:(?P<chunk>[^/]+)/)?$', 'pubmark', name="wiki_pubmark"),
 
     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 3739c90..a68fa35 100644 (file)
@@ -7,6 +7,7 @@ from lxml import etree
 
 from django.conf import settings
 
 
 from django.conf import settings
 
+from django.contrib.auth.decorators import login_required
 from django.views.generic.simple import direct_to_template
 from django.views.decorators.http import require_POST, require_GET
 from django.core.urlresolvers import reverse
 from django.views.generic.simple import direct_to_template
 from django.views.decorators.http import require_POST, require_GET
 from django.core.urlresolvers import reverse
@@ -27,6 +28,7 @@ from django.middleware.gzip import GZipMiddleware
 import librarian.html
 import librarian.text
 from wiki import xml_tools
 import librarian.html
 import librarian.text
 from wiki import xml_tools
+from apiclient import api_call
 
 #
 # Quick hack around caching problems, TODO: use ETags
 
 #
 # Quick hack around caching problems, TODO: use ETags
@@ -82,6 +84,7 @@ def editor(request, slug, chunk=None, template_name='wiki/document_details.html'
             "text_save": forms.DocumentTextSaveForm(prefix="textsave"),
             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
             "add_tag": forms.DocumentTagForm(prefix="addtag"),
             "text_save": forms.DocumentTextSaveForm(prefix="textsave"),
             "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
             "add_tag": forms.DocumentTagForm(prefix="addtag"),
+            "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
         },
         'REDMINE_URL': settings.REDMINE_URL,
     })
         },
         'REDMINE_URL': settings.REDMINE_URL,
     })
@@ -254,7 +257,7 @@ 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(Book.publish_tag())
+    xml = get_object_or_404(Book, slug=slug).materialize()
 
     response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
 
     response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
@@ -263,7 +266,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(Book.publish_tag())
+    xml = get_object_or_404(Book, slug=slug).materialize()
     output = StringIO()
     # errors?
     librarian.text.transform(StringIO(xml), output)
     output = StringIO()
     # errors?
     librarian.text.transform(StringIO(xml), output)
@@ -275,7 +278,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(Book.publish_tag())
+    xml = get_object_or_404(Book, slug=slug).materialize()
     output = StringIO()
     # errors?
     librarian.html.transform(StringIO(xml), output, parse_dublincore=False,
     output = StringIO()
     # errors?
     librarian.html.transform(StringIO(xml), output, parse_dublincore=False,
@@ -391,6 +394,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,
+                "publishable": "Publishable\n" if change.publishable else "",
                 "tag": ',\n'.join(unicode(tag) for tag in change.tags.all()),
             })
     return JSONResponse(changes)
                 "tag": ',\n'.join(unicode(tag) for tag in change.tags.all()),
             })
     return JSONResponse(changes)
@@ -581,32 +585,50 @@ def add_tag(request, slug, chunk=None):
             raise Http404
 
         tag = form.cleaned_data['tag']
             raise Http404
 
         tag = form.cleaned_data['tag']
-        revision = revision=form.cleaned_data['revision']
+        revision = form.cleaned_data['revision']
         doc.at_revision(revision).tags.add(tag)
         return JSONResponse({"message": _("Tag added")})
     else:
         return JSONFormInvalid(form)
 
 
         doc.at_revision(revision).tags.add(tag)
         return JSONResponse({"message": _("Tag added")})
     else:
         return JSONFormInvalid(form)
 
 
-"""
-import wlapi
-
-
 @require_POST
 @require_POST
-@ajax_require_permission('wiki.can_publish')
-def publish(request, name):
-    name = normalize_name(name)
+@ajax_require_permission('wiki.can_pubmark')
+def pubmark(request, slug, chunk=None):
+    form = forms.DocumentPubmarkForm(request.POST, prefix="pubmark")
+    if form.is_valid():
+        try:
+            doc = Chunk.get(slug, chunk)
+        except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+            raise Http404
 
 
-    storage = getstorage()
-    document = storage.get_by_tag(name, "ready_to_publish")
+        revision = form.cleaned_data['revision']
+        publishable = form.cleaned_data['publishable']
+        change = doc.at_revision(revision)
+        print publishable, change.publishable
+        if publishable != change.publishable:
+            change.publishable = publishable
+            change.save()
+            return JSONResponse({"message": _("Revision marked")})
+        else:
+            return JSONResponse({"message": _("Nothing changed")})
+    else:
+        return JSONFormInvalid(form)
 
 
-    api = wlapi.WLAPI(**settings.WL_API_CONFIG)
 
 
+@require_POST
+@login_required
+def publish(request, slug):
+    book = get_object_or_404(Book, slug=slug)
     try:
     try:
-        return JSONResponse({"result": api.publish_book(document)})
-    except wlapi.APICallException, e:
-        return JSONServerError({"message": str(e)})
-"""
+        ret = api_call(request.user, "books", {"book_xml": book.materialize()})
+    except BaseException, e:
+        return http.HttpResponse(e)
+    else:
+        book.last_published = datetime.now()
+        book.save()
+        return http.HttpResponseRedirect(book.get_absolute_url())
+
 
 def themes(request):
     prefix = request.GET.get('q', '')
 
 def themes(request):
     prefix = request.GET.get('q', '')
index 587374b..f5e90de 100644 (file)
@@ -117,10 +117,11 @@ INSTALLED_APPS = (
     'south',
     'sorl.thumbnail',
     'filebrowser',
     'south',
     'sorl.thumbnail',
     'filebrowser',
-    'dvcs',
 
 
+    'dvcs',
     'wiki',
     'toolbar',
     'wiki',
     'toolbar',
+    'apiclient',
 )
 
 FILEBROWSER_URL_FILEBROWSER_MEDIA = STATIC_URL + 'filebrowser/'
 )
 
 FILEBROWSER_URL_FILEBROWSER_MEDIA = STATIC_URL + 'filebrowser/'
index 8bade8d..5554aca 100644 (file)
@@ -45,6 +45,7 @@ COMPRESS_JS = {
                 'js/wiki/dialog_save.js',
                 'js/wiki/dialog_revert.js',
                 'js/wiki/dialog_addtag.js',
                 'js/wiki/dialog_save.js',
                 'js/wiki/dialog_revert.js',
                 'js/wiki/dialog_addtag.js',
+                'js/wiki/dialog_pubmark.js',
 
                 # views
                 'js/wiki/view_history.js',
 
                 # views
                 'js/wiki/view_history.js',
index 91323c0..f6f55d7 100644 (file)
@@ -116,3 +116,30 @@ td {
 .chunk-wl-broken a {color: red;}
 .chunk-wl a {color: green;}
 .chunk-wl-fix a {color: black;}
 .chunk-wl-broken a {color: red;}
 .chunk-wl a {color: green;}
 .chunk-wl-fix a {color: black;}
+
+
+/* Big cheesy publish button */
+#publish-button {
+        color: black;
+        border: 2px solid black; 
+        border-radius: 20px;
+        box-shadow: 0px 0px 15px #88f;
+        /*moz-border-radius: 20px;
+        -moz-box-shadow: 10px 10px 5px #888;*/
+        font-size:1.5em; 
+        padding: 1em; 
+        background: -moz-linear-gradient(top,  #fff,  #44f);
+        -moz-transition: all 0.5s ease-in-out;        
+        margin: 20px;
+}
+
+#publish-button:hover {
+    -moz-transition: all 0.5s ease-in-out;        
+    -moz-transform: scale(1.1);
+    background: -moz-linear-gradient(top,  #fff,  #88f);
+    -moz-box-shadow: 0px 0px 30px #ff8;
+}
+
+#publish-button:active {
+    background: -moz-linear-gradient(top,  #88f,  #fff);
+}
diff --git a/redakcja/static/img/angel-left.png b/redakcja/static/img/angel-left.png
new file mode 100644 (file)
index 0000000..7744103
Binary files /dev/null and b/redakcja/static/img/angel-left.png differ
diff --git a/redakcja/static/img/angel-right.png b/redakcja/static/img/angel-right.png
new file mode 100644 (file)
index 0000000..df85d33
Binary files /dev/null and b/redakcja/static/img/angel-right.png differ
diff --git a/redakcja/static/js/wiki/dialog_pubmark.js b/redakcja/static/js/wiki/dialog_pubmark.js
new file mode 100755 (executable)
index 0000000..902a737
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Dialog for marking document for publishing
+ *
+ */
+(function($){
+
+    function PubmarkDialog(element, options){
+        if (!options.revision  && options.revision != 0)
+            throw "PubmarkDialog needs a revision number.";
+
+        this.ctx = $.wiki.exitContext();
+        this.clearForm();
+
+        /* fill out hidden fields */
+        this.$form = $('form', element);
+
+        $("input[name='pubmark-id']", this.$form).val(CurrentDocument.id);
+        $("input[name='pubmark-revision']", this.$form).val(options.revision);
+
+        $.wiki.cls.GenericDialog.call(this, element);
+    };
+
+    PubmarkDialog.prototype = $.extend(new $.wiki.cls.GenericDialog(), {
+        cancelAction: function(){
+            $.wiki.enterContext(this.ctx);
+            this.hide();
+        },
+
+        saveAction: function(){
+            var self = this;
+
+            self.$elem.block({
+                message: "Oznaczanie wersji",
+                fadeIn: 0,
+            });
+
+            CurrentDocument.pubmark({
+                form: self.$form,
+                success: function(doc, changed, info){
+                    self.$elem.block({
+                        message: info,
+                        timeout: 2000,
+                        fadeOut: 0,
+                        onUnblock: function(){
+                            self.hide();
+                            $.wiki.enterContext(self.ctx);
+                        }
+                    });
+                },
+                failure: function(doc, info){
+                    console.log("Failure", info);
+                    self.reportErrors(info);
+                    self.$elem.unblock();
+                }
+            });
+        }
+    });
+
+    /* make it global */
+    $.wiki.cls.PubmarkDialog = PubmarkDialog;
+})(jQuery);
index 4fe20e2..fe3af69 100644 (file)
                                self.showTagForm();
                        });
 
                                self.showTagForm();
                        });
 
+                       $('#pubmark-changeset-button').click(function() {
+                               self.showPubmarkForm();
+                       });
+
                $('#doc-revert-button').click(function() {
                    self.revertDialog();
                });
                $('#doc-revert-button').click(function() {
                    self.revertDialog();
                });
                $.wiki.showDialog('#add_tag_dialog', {'revision': version});
        };
 
                $.wiki.showDialog('#add_tag_dialog', {'revision': version});
        };
 
+       HistoryPerspective.prototype.showPubmarkForm = function(){
+               var selected = $('#changes-list .entry.selected');
+
+               if (selected.length != 1) {
+            window.alert("Musisz zaznaczyć dokładnie jedną wersję.");
+            return;
+        }
+
+               var version = parseInt($("*[data-stub-value='version']", selected[0]).text());
+               $.wiki.showDialog('#pubmark_dialog', {'revision': version});
+       };
+
        HistoryPerspective.prototype.makeDiff = function() {
         var changelist = $('#changes-list');
         var selected = $('.entry.selected', changelist);
        HistoryPerspective.prototype.makeDiff = function() {
         var changelist = $('#changes-list');
         var selected = $('.entry.selected', changelist);
index 2f79b09..a1f2fb9 100644 (file)
@@ -50,6 +50,9 @@
                if (vname == "ajax_document_addtag")
                        return base_path + "/tag/" + arguments[1] + '/';
 
                if (vname == "ajax_document_addtag")
                        return base_path + "/tag/" + arguments[1] + '/';
 
+               if (vname == "ajax_document_pubmark")
+                       return base_path + "/pubmark/" + arguments[1] + '/';
+
                if (vname == "ajax_publish")
                        return base_path + "/publish/" + arguments[1] + '/';
 
                if (vname == "ajax_publish")
                        return base_path + "/publish/" + arguments[1] + '/';
 
                });
        };
 
                });
        };
 
+       WikiDocument.prototype.pubmark = function(params) {
+               params = $.extend({}, noops, params);
+               var self = this;
+               var data = {
+                       "pubmark-id": self.id,
+               };
+
+               /* unpack form */
+               $.each(params.form.serializeArray(), function() {
+                       data[this.name] = this.value;
+               });
+
+               $.ajax({
+                       url: reverse("ajax_document_pubmark", self.id),
+                       type: "POST",
+                       dataType: "json",
+                       data: data,
+                       success: function(data) {
+                               params.success(self, data.message);
+                       },
+                       error: function(xhr) {
+                               if (xhr.status == 403 || xhr.status == 401) {
+                                       params.failure(self, {
+                                               "__all__": ["Nie masz uprawnień lub nie jesteś zalogowany."]
+                                       });
+                               }
+                               else {
+                                       try {
+                                               params.failure(self, $.parseJSON(xhr.responseText));
+                                       }
+                                       catch (e) {
+                                               params.failure(self, {
+                                                       "__all__": ["Nie udało się - błąd serwera."]
+                                               });
+                                       };
+                               };
+                       }
+               });
+       };
+
        $.wikiapi.WikiDocument = WikiDocument;
 })(jQuery);
        $.wikiapi.WikiDocument = WikiDocument;
 })(jQuery);
index 20994ce..8aa569b 100644 (file)
@@ -21,6 +21,7 @@ urlpatterns = patterns('',
     url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/documents/'}),
     url(r'^documents/', include('wiki.urls')),
     url(r'^storage/', include('dvcs.urls')),
     url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/documents/'}),
     url(r'^documents/', include('wiki.urls')),
     url(r'^storage/', include('dvcs.urls')),
+    url(r'^apiclient/', include('apiclient.urls')),
 
     # Static files (should be served by Apache)
     url(r'^%s(?P<path>.+)$' % settings.MEDIA_URL[1:], 'django.views.static.serve',
 
     # Static files (should be served by Apache)
     url(r'^%s(?P<path>.+)$' % settings.MEDIA_URL[1:], 'django.views.static.serve',
index c2ed840..6e84562 100644 (file)
@@ -3,6 +3,8 @@ lxml>=2.2.2
 mercurial>=1.6,<1.7
 PyYAML>=3.0
 PIL>=1.1
 mercurial>=1.6,<1.7
 PyYAML>=3.0
 PIL>=1.1
+oauth2
+httplib2 # oauth2 dependency
 
 ## Book conversion library
 # git+git://github.com/fnp/librarian.git@master#egg=librarian
 
 ## Book conversion library
 # git+git://github.com/fnp/librarian.git@master#egg=librarian