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):
         
+        # 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)),
@@ -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)),
+            ('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'])
 
+        # 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)),
@@ -40,9 +58,15 @@ class Migration(SchemaMigration):
         # 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')
 
+        # Removing M2M table for field tags on 'Change'
+        db.delete_table('dvcs_change_tags')
+
         # 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'}),
+            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
             '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': {
@@ -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'})
+        },
+        '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)
+    publishable = models.BooleanField(default=False)
 
     tags = models.ManyToManyField(Tag)
 
@@ -245,8 +246,8 @@ class Document(models.Model):
         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:
index fb0f958..55b1e54 100644 (file)
@@ -20,6 +20,17 @@ class DocumentTagForm(forms.Form):
     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.
@@ -180,7 +191,8 @@ class BookAppendForm(forms.Form):
         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):
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)),
+            ('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'])
 
@@ -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'}),
+            'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
             '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': {
@@ -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'})
         },
+        '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'},
+            '_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'}),
+            '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'}),
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)
+    last_published = models.DateTimeField(null=True, editable=False)
 
     _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)
 
-    @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
-            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:
@@ -183,9 +180,6 @@ class Chunk(dvcs_models.Document):
         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(
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>{% trans "Last published" %}: {{ book.last_published }}</p>
+
 {% if book.publishable %}
     <p>
     <a href="{% url wiki_book_xml book.slug %}">{% trans "Full XML" %}</a><br/>
     {% 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 %}
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/pubmark_dialog.html" %}
 {% 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>
+               <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"
@@ -26,7 +28,9 @@
                                <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>
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'^(?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'^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"),
index 3739c90..a68fa35 100644 (file)
@@ -7,6 +7,7 @@ from lxml import etree
 
 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
@@ -27,6 +28,7 @@ from django.middleware.gzip import GZipMiddleware
 import librarian.html
 import librarian.text
 from wiki import xml_tools
+from apiclient import api_call
 
 #
 # 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"),
+            "pubmark": forms.DocumentPubmarkForm(prefix="pubmark"),
         },
         'REDMINE_URL': settings.REDMINE_URL,
     })
@@ -254,7 +257,7 @@ def text(request, slug, chunk=None):
 
 @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
@@ -263,7 +266,7 @@ def book_xml(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)
@@ -275,7 +278,7 @@ def book_txt(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,
@@ -391,6 +394,7 @@ def history(request, slug, chunk=None):
                 "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)
@@ -581,32 +585,50 @@ def add_tag(request, slug, chunk=None):
             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)
 
 
-"""
-import wlapi
-
-
 @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:
-        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', '')
index 587374b..f5e90de 100644 (file)
@@ -117,10 +117,11 @@ INSTALLED_APPS = (
     'south',
     'sorl.thumbnail',
     'filebrowser',
-    'dvcs',
 
+    'dvcs',
     'wiki',
     'toolbar',
+    'apiclient',
 )
 
 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_pubmark.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;}
+
+
+/* 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();
                        });
 
+                       $('#pubmark-changeset-button').click(function() {
+                               self.showPubmarkForm();
+                       });
+
                $('#doc-revert-button').click(function() {
                    self.revertDialog();
                });
                $.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);
index 2f79b09..a1f2fb9 100644 (file)
@@ -50,6 +50,9 @@
                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] + '/';
 
                });
        };
 
+       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);
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'^apiclient/', include('apiclient.urls')),
 
     # 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
+oauth2
+httplib2 # oauth2 dependency
 
 ## Book conversion library
 # git+git://github.com/fnp/librarian.git@master#egg=librarian