--- /dev/null
+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")
+
--- /dev/null
+# 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']
--- /dev/null
+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
+
+
--- /dev/null
+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/')
--- /dev/null
+"""
+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
+"""}
+
--- /dev/null
+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'),
+)
--- /dev/null
+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('/')
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)),
('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)),
# 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')
'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': {
'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'})
}
}
+++ /dev/null
-# 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']
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)
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:
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.
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):
('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'])
'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': {
'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'}),
+++ /dev/null
-# 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']
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)
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:
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(
<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 %}
{% include "wiki/save_dialog.html" %}
{% include "wiki/revert_dialog.html" %}
{% include "wiki/tag_dialog.html" %}
+ {% include "wiki/pubmark_dialog.html" %}
{% endblock %}
--- /dev/null
+{% 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>
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"
<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>
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"),
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
import librarian.html
import librarian.text
from wiki import xml_tools
+from apiclient import api_call
#
# Quick hack around caching problems, TODO: use ETags
"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,
})
@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
@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)
@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,
"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)
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', '')
'south',
'sorl.thumbnail',
'filebrowser',
- 'dvcs',
+ 'dvcs',
'wiki',
'toolbar',
+ 'apiclient',
)
FILEBROWSER_URL_FILEBROWSER_MEDIA = STATIC_URL + 'filebrowser/'
'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',
.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);
+}
--- /dev/null
+/*
+ * 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);
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);
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);
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',
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