From: Radek Czajka Date: Wed, 8 Jun 2011 13:03:59 +0000 (+0200) Subject: teh legendary `Publish' button, oh yeah! X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/f4921ca797953ddab6b35786864b2dd523190503?ds=inline teh legendary `Publish' button, oh yeah! authorization to WLAPI through OAuth, marking revisions for publication --- diff --git a/apps/apiclient/__init__.py b/apps/apiclient/__init__.py new file mode 100644 index 00000000..7913ac3c --- /dev/null +++ b/apps/apiclient/__init__.py @@ -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 index 00000000..4af28a52 --- /dev/null +++ b/apps/apiclient/migrations/0001_initial.py @@ -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 index 00000000..e69de29b diff --git a/apps/apiclient/models.py b/apps/apiclient/models.py new file mode 100644 index 00000000..d3c8f620 --- /dev/null +++ b/apps/apiclient/models.py @@ -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 index 00000000..5fbf18ee --- /dev/null +++ b/apps/apiclient/settings.py @@ -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 index 00000000..2247054b --- /dev/null +++ b/apps/apiclient/tests.py @@ -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 index 00000000..5e54965e --- /dev/null +++ b/apps/apiclient/urls.py @@ -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 index 00000000..f8515904 --- /dev/null +++ b/apps/apiclient/views.py @@ -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('/') diff --git a/apps/dvcs/migrations/0001_initial.py b/apps/dvcs/migrations/0001_initial.py index 7510e8ad..d9f0984c 100644 --- a/apps/dvcs/migrations/0001_initial.py +++ b/apps/dvcs/migrations/0001_initial.py @@ -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 index 7f5b9a3e..00000000 --- a/apps/dvcs/migrations/0002_auto__add_tag.py +++ /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'] diff --git a/apps/dvcs/models.py b/apps/dvcs/models.py index dddbf3ad..262472a3 100644 --- a/apps/dvcs/models.py +++ b/apps/dvcs/models.py @@ -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: diff --git a/apps/wiki/forms.py b/apps/wiki/forms.py index fb0f958a..55b1e544 100644 --- a/apps/wiki/forms.py +++ b/apps/wiki/forms.py @@ -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): diff --git a/apps/wiki/migrations/0003_auto__add_book__add_chunk__add_unique_chunk_book_number__add_unique_ch.py b/apps/wiki/migrations/0003_auto__add_book__add_chunk__add_unique_chunk_book_number__add_unique_ch.py index 229825d2..010ed2a6 100644 --- a/apps/wiki/migrations/0003_auto__add_book__add_chunk__add_unique_chunk_book_number__add_unique_ch.py +++ b/apps/wiki/migrations/0003_auto__add_book__add_chunk__add_unique_chunk_book_number__add_unique_ch.py @@ -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 index a09dc90e..00000000 --- a/apps/wiki/migrations/0004_auto__add_field_book__list_html.py +++ /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'] diff --git a/apps/wiki/models.py b/apps/wiki/models.py index 4dc70173..7887e5da 100644 --- a/apps/wiki/models.py +++ b/apps/wiki/models.py @@ -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( diff --git a/apps/wiki/templates/wiki/book_detail.html b/apps/wiki/templates/wiki/book_detail.html index 26f52145..f4b15a9e 100755 --- a/apps/wiki/templates/wiki/book_detail.html +++ b/apps/wiki/templates/wiki/book_detail.html @@ -62,6 +62,8 @@

{% trans "Append to other book" %}

+

{% trans "Last published" %}: {{ book.last_published }}

+ {% if book.publishable %}

{% trans "Full XML" %}
@@ -73,7 +75,18 @@ {% endcomment %}

-

+ +
{% csrf_token %} + + + +

{% else %} {% trans "This book cannot be published yet" %} {% endif %} diff --git a/apps/wiki/templates/wiki/document_details.html b/apps/wiki/templates/wiki/document_details.html index 934a8ac6..d95603f6 100644 --- a/apps/wiki/templates/wiki/document_details.html +++ b/apps/wiki/templates/wiki/document_details.html @@ -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 index 00000000..93a98563 --- /dev/null +++ b/apps/wiki/templates/wiki/pubmark_dialog.html @@ -0,0 +1,19 @@ +{% load i18n %} +
+
+ {% for field in forms.pubmark.visible_fields %} +

{{ field.label_tag }} {{ field }}

+

{{ field.help_text }}

+ {% endfor %} + + {% for f in forms.pubmark.hidden_fields %} + {{ f }} + {% endfor %} +

+ +

+ + +

+
+
diff --git a/apps/wiki/templates/wiki/tabs/history_view.html b/apps/wiki/templates/wiki/tabs/history_view.html index 60c59a95..db207bb1 100644 --- a/apps/wiki/templates/wiki/tabs/history_view.html +++ b/apps/wiki/templates/wiki/tabs/history_view.html @@ -5,6 +5,8 @@ data-enabled-when="2" disabled="disabled">{% trans "Compare versions" %} +