commit 5deab02e61ab107516c6c33b687e5e64a66c8729 Author: Mark Steadman Date: Sat May 3 22:37:15 2014 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/README b/README new file mode 100644 index 0000000..a519ed8 --- /dev/null +++ b/README @@ -0,0 +1,4 @@ +Bambu Buffer +============ + +Post to Buffer and manage profile settings through a Django-powered site. diff --git a/README.md b/README.md new file mode 100644 index 0000000..aaa1fc7 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Bambu Buffer + +Post to Buffer and manage profile settings through a Django-powered site. diff --git a/bambu_buffer/__init__.py b/bambu_buffer/__init__.py new file mode 100644 index 0000000..f6843aa --- /dev/null +++ b/bambu_buffer/__init__.py @@ -0,0 +1,109 @@ +from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.models import Site +from django.db.models import Model +from bambu_buffer.exceptions import * +from bambu_buffer.models import BufferToken, BufferProfile, BufferedItem +from bambu_buffer.settings import POST_URL, TIMEOUT, AUTOPOST_MODELS +from bambu_buffer.sites import BufferSite +from datetime import datetime, date +from threading import Thread +import requests + +__version__ = '2.0' +site = BufferSite() + +class BufferThread(Thread): + def __init__(self, token, data, *args, **kwargs): + self.data = data + self.token = token + super(BufferThread, self).__init__(*args, **kwargs) + + def run(self): + from bambu_buffer import log + + response = requests.post( + '%s?access_token=%s' % (POST_URL, self.token), + self.data, + timeout = TIMEOUT + ) + + if response.status_code != 200: + log.error(response.json()) + +def post(item, author, **kwargs): + try: + token = author.buffer_tokens.get() + except BufferToken.DoesNotExist: + return + + if 'url' in kwargs: + url = kwargs.get('url') + elif isinstance(item, Model): + url = u'http://%s%s' % ( + Site.objects.get_current().domain, item.get_absolute_url() + ) + + content_type = ContentType.objects.get_for_model(item) + if BufferedItem.objects.filter( + object_id = item.pk, + content_type = content_type + ).exists(): + print '%s %d has already been sent to Buffer' % ( + unicode(item._meta.verbose_name).capitalize(), + item.pk + ) + + return + + BufferedItem.objects.create( + content_type = content_type, + object_id = item.pk + ) + else: + url = None + + data = { + 'text': u'%s%s' % ( + unicode(kwargs.get('text') or item), + url and (u' %s' % url) or u'' + ), + 'profile_ids[]': kwargs.get('profile_ids') or BufferProfile.objects.filter( + service__token = token, + selected = True + ).values_list('remote_id', flat = True), + 'media[description]': kwargs.get('description') + } + + if 'picture' in kwargs: + data['media[picture]'] = kwargs['picture'] + if not 'thumbnail' in kwargs: + raise TypeError( + 'For image-based updates, the thumbnail parameter is required.' + ) + + if 'thumbnail' in kwargs: + data['media[thumbnail]'] = kwargs['thumbnail'] + + if 'shorten' in kwargs: + data['shorten'] = kwargs['shorten'] and 'true' or 'false' + + if 'now' in kwargs: + data['now'] = kwargs['now'] and 'true' or 'false' + + if 'top' in kwargs: + data['top'] = kwargs['top'] and 'true' or 'false' + + if 'scheduled_at' in kwargs: + if isinstance(kwargs['scheduled_at'], (datetime, date)): + data['scheduled_at'] = kwargs['scheduled_at'].isoformat() + else: + try: + data['scheduled_at'] = int(kwargs['scheduled_at']) + except: + raise TypeError( + 'scheduled_at must be an integer or DateTime' + ) + + BufferThread(token.token, data).start() + +site.hookup_signals(AUTOPOST_MODELS) diff --git a/bambu_buffer/exceptions.py b/bambu_buffer/exceptions.py new file mode 100644 index 0000000..c6ad0e6 --- /dev/null +++ b/bambu_buffer/exceptions.py @@ -0,0 +1,5 @@ +class BufferException(Exception): + pass + +class NoBufferTokenException(Exception): + pass diff --git a/bambu_buffer/log.py b/bambu_buffer/log.py new file mode 100644 index 0000000..02ce21e --- /dev/null +++ b/bambu_buffer/log.py @@ -0,0 +1,39 @@ +from django.contrib import messages +from django.utils.translation import ugettext as _ +from bambu_buffer.settings import SUCCESS_MESSAGE, ERROR_MESSAGE +from bambu_buffer.exceptions import BufferException +import logging + +logger = logging.getLogger('bambu_buffer') +def error(data, request = None, raise_error = False): + error = data.get('error_description', + data.get('message', + 'error' in data and data.get('error').capitalize().replace('_', ' ') or '' + ) + ) or u'Unknown error' + + if 'message' in data: + data['error_message'] = data.pop('message') + + logger.error(error, extra = data) + + if request and ERROR_MESSAGE: + messages.error(request, + _(ERROR_MESSAGE) % error + ) + + if raise_error: + raise BufferException(error) + +def success(data, request = None): + message = None + if 'access_token' in data: + message = u'Buffer access token created' + + if message: + logger.info(message) + + if request and SUCCESS_MESSAGE: + messages.success(request, + _(SUCCESS_MESSAGE) + ) diff --git a/bambu_buffer/management/__init__.py b/bambu_buffer/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bambu_buffer/management/commands/__init__.py b/bambu_buffer/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bambu_buffer/management/commands/fakebuffer.py b/bambu_buffer/management/commands/fakebuffer.py new file mode 100644 index 0000000..83e2b74 --- /dev/null +++ b/bambu_buffer/management/commands/fakebuffer.py @@ -0,0 +1,40 @@ +from django.db import transaction +from django.core.management.base import BaseCommand, CommandError +from optparse import make_option +from os import sys + +class Command(BaseCommand): + help = 'Fake Buffer records for items in settings.BUFFER_AUTOPOST_MODELS' + + @transaction.commit_on_success + def handle(self, *args, **options): + from django.contrib.contenttypes.models import ContentType + from bambu_buffer import site + from bambu_buffer.models import BufferedItem + + for model, info in site._registry.items(): + query = dict( + [ + (key, callable(value) and value() or value) + for (key, value) in info['conditions'].items() + ] + ) + + count = 0 + for pk in model.objects.filter(**query).values_list('pk', flat = True): + item, created = BufferedItem.objects.get_or_create( + content_type = ContentType.objects.get_for_model(model), + object_id = pk + ) + + if not created: + count += 1 + + sys.stdout.write( + 'Added fake Buffer item for %d %s\n' % ( + count, + unicode( + count == 1 and model._meta.verbose_name or model._meta.verbose_name_plural + ) + ) + ) diff --git a/bambu_buffer/migrations/0001_initial.py b/bambu_buffer/migrations/0001_initial.py new file mode 100644 index 0000000..ff65abc --- /dev/null +++ b/bambu_buffer/migrations/0001_initial.py @@ -0,0 +1,118 @@ +# -*- coding: 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 'BufferToken' + db.create_table('buffer_token', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='buffer_tokens', unique=True, to=orm['auth.User'])), + ('token', self.gf('django.db.models.fields.CharField')(max_length=36)), + )) + db.send_create_signal(u'buffer', ['BufferToken']) + + # Adding model 'BufferService' + db.create_table('buffer_service', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('token', self.gf('django.db.models.fields.related.ForeignKey')(related_name='services', to=orm['buffer.BufferToken'])), + ('name', self.gf('django.db.models.fields.CharField')(max_length=30)), + ('remote_id', self.gf('django.db.models.fields.CharField')(max_length=36)), + ('username', self.gf('django.db.models.fields.CharField')(max_length=30)), + )) + db.send_create_signal(u'buffer', ['BufferService']) + + # Adding model 'BufferProfile' + db.create_table('buffer_profile', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('service', self.gf('django.db.models.fields.related.ForeignKey')(related_name='profiles', to=orm['buffer.BufferService'])), + ('avatar', self.gf('django.db.models.fields.URLField')(max_length=255)), + ('created_at', self.gf('django.db.models.fields.DateTimeField')()), + ('default', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('formatted_username', self.gf('django.db.models.fields.CharField')(max_length=100)), + ('remote_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=36)), + ('schedules', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal(u'buffer', ['BufferProfile']) + + + def backwards(self, orm): + # Deleting model 'BufferToken' + db.delete_table('buffer_token') + + # Deleting model 'BufferService' + db.delete_table('buffer_service') + + # Deleting model 'BufferProfile' + db.delete_table('buffer_profile') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'buffer.bufferprofile': { + 'Meta': {'object_name': 'BufferProfile', 'db_table': "'buffer_profile'"}, + 'avatar': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {}), + 'default': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'formatted_username': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'remote_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}), + 'schedules': ('django.db.models.fields.TextField', [], {}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'profiles'", 'to': u"orm['buffer.BufferService']"}) + }, + u'buffer.bufferservice': { + 'Meta': {'object_name': 'BufferService', 'db_table': "'buffer_service'"}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'remote_id': ('django.db.models.fields.CharField', [], {'max_length': '36'}), + 'token': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'services'", 'to': u"orm['buffer.BufferToken']"}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '30'}) + }, + u'buffer.buffertoken': { + 'Meta': {'object_name': 'BufferToken', 'db_table': "'buffer_token'"}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '36'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'buffer_tokens'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + u'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'}), + u'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 = ['buffer'] \ No newline at end of file diff --git a/bambu_buffer/migrations/0002_auto__add_field_bufferprofile_selected.py b/bambu_buffer/migrations/0002_auto__add_field_bufferprofile_selected.py new file mode 100644 index 0000000..560b055 --- /dev/null +++ b/bambu_buffer/migrations/0002_auto__add_field_bufferprofile_selected.py @@ -0,0 +1,87 @@ +# -*- coding: 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 'BufferProfile.selected' + db.add_column('buffer_profile', 'selected', + self.gf('django.db.models.fields.BooleanField')(default=1), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'BufferProfile.selected' + db.delete_column('buffer_profile', 'selected') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'buffer.bufferprofile': { + 'Meta': {'object_name': 'BufferProfile', 'db_table': "'buffer_profile'"}, + 'avatar': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {}), + 'default': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'formatted_username': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'remote_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}), + 'schedules': ('django.db.models.fields.TextField', [], {}), + 'selected': ('django.db.models.fields.BooleanField', [], {}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'profiles'", 'to': u"orm['buffer.BufferService']"}) + }, + u'buffer.bufferservice': { + 'Meta': {'object_name': 'BufferService', 'db_table': "'buffer_service'"}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'remote_id': ('django.db.models.fields.CharField', [], {'max_length': '36'}), + 'token': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'services'", 'to': u"orm['buffer.BufferToken']"}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '30'}) + }, + u'buffer.buffertoken': { + 'Meta': {'object_name': 'BufferToken', 'db_table': "'buffer_token'"}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '36'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'buffer_tokens'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + u'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'}), + u'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 = ['buffer'] \ No newline at end of file diff --git a/bambu_buffer/migrations/0003_auto__add_buffereditem__add_unique_buffereditem_content_type_object_id.py b/bambu_buffer/migrations/0003_auto__add_buffereditem__add_unique_buffereditem_content_type_object_id.py new file mode 100644 index 0000000..6fdd19a --- /dev/null +++ b/bambu_buffer/migrations/0003_auto__add_buffereditem__add_unique_buffereditem_content_type_object_id.py @@ -0,0 +1,102 @@ +# -*- coding: 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 'BufferedItem' + db.create_table(u'buffer_buffereditem', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), + ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), + )) + db.send_create_signal(u'buffer', ['BufferedItem']) + + # Adding unique constraint on 'BufferedItem', fields ['content_type', 'object_id'] + db.create_unique(u'buffer_buffereditem', ['content_type_id', 'object_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'BufferedItem', fields ['content_type', 'object_id'] + db.delete_unique(u'buffer_buffereditem', ['content_type_id', 'object_id']) + + # Deleting model 'BufferedItem' + db.delete_table(u'buffer_buffereditem') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'buffer.buffereditem': { + 'Meta': {'unique_together': "(('content_type', 'object_id'),)", 'object_name': 'BufferedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}) + }, + u'buffer.bufferprofile': { + 'Meta': {'object_name': 'BufferProfile', 'db_table': "'buffer_profile'"}, + 'avatar': ('django.db.models.fields.URLField', [], {'max_length': '255'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {}), + 'default': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'formatted_username': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'remote_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}), + 'schedules': ('django.db.models.fields.TextField', [], {}), + 'selected': ('django.db.models.fields.BooleanField', [], {}), + 'service': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'profiles'", 'to': u"orm['buffer.BufferService']"}) + }, + u'buffer.bufferservice': { + 'Meta': {'object_name': 'BufferService', 'db_table': "'buffer_service'"}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'remote_id': ('django.db.models.fields.CharField', [], {'max_length': '36'}), + 'token': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'services'", 'to': u"orm['buffer.BufferToken']"}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '30'}) + }, + u'buffer.buffertoken': { + 'Meta': {'object_name': 'BufferToken', 'db_table': "'buffer_token'"}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '36'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'buffer_tokens'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + u'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'}), + u'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 = ['buffer'] \ No newline at end of file diff --git a/bambu_buffer/migrations/__init__.py b/bambu_buffer/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bambu_buffer/models.py b/bambu_buffer/models.py new file mode 100644 index 0000000..f8b129f --- /dev/null +++ b/bambu_buffer/models.py @@ -0,0 +1,116 @@ +from django.db import models +from django.utils.timezone import pytz +from bambu_buffer.settings import PROFILES_URL, TIMEOUT +from bambu_buffer import log, helpers +from datetime import datetime, timedelta +import requests, json + +class BufferToken(models.Model): + user = models.ForeignKey('auth.User', related_name = 'buffer_tokens', unique = True) + token = models.CharField(max_length = 36) + + def __unicode__(self): + return self.token + + def save(self, *args, **kwargs): + new = not self.pk + super(BufferToken, self).save(*args, **kwargs) + + if new: + self.refresh_services() + + def refresh_services(self): + response = requests.get( + '%s?access_token=%s' % ( + PROFILES_URL, + self.token + ), + timeout = TIMEOUT + ) + + if response.status_code == 200: + self.services.all().delete() + services = {} + for profile in response.json(): + remote_id = profile['formatted_service'] + if remote_id in services: + service = services[remote_id] + else: + service = self.services.create( + remote_id = profile[u'service_id'], + name = remote_id, + username = profile['service_username'] + ) + + services[service.remote_id] = service + + epoch = datetime(1970, 1, 1, 0, 0, 0, + tzinfo = pytz.timezone( + profile['timezone'] + ) + ) + + service.profiles.create( + avatar = profile.get('avatar_https', + profile.get('avatar') + ), + created_at = epoch + timedelta( + seconds = profile['created_at'] + ), + default = profile['default'], + selected = profile['default'], + formatted_username = profile['formatted_username'], + remote_id = profile['id'], + schedules = json.dumps( + profile['schedules'] + ) + ) + else: + log.error(response.json(), request) + + class Meta: + db_table = 'buffer_token' + +class BufferService(models.Model): + token = models.ForeignKey(BufferToken, related_name = 'services') + name = models.CharField(max_length = 30) + remote_id = models.CharField(max_length = 36) + username = models.CharField(max_length = 30) + + def __unicode__(self): + return self.name + + @property + def icon(self): + return self.name.lower().split(' ')[0].replace('+', '-plus') + + class Meta: + db_table = 'buffer_service' + +class BufferProfile(models.Model): + service = models.ForeignKey(BufferService, related_name = 'profiles') + avatar = models.URLField(max_length = 255) + created_at = models.DateTimeField() + default = models.BooleanField(default = True) + formatted_username = models.CharField(max_length = 100) + remote_id = models.CharField(max_length = 36, unique = True) + schedules = models.TextField() + selected = models.BooleanField() + + def __unicode__(self): + return self.formatted_username + + @property + def icon(self): + return self.service.icon + + class Meta: + db_table = 'buffer_profile' + +class BufferedItem(models.Model): + content_type = models.ForeignKey('contenttypes.ContentType') + object_id = models.PositiveIntegerField() + + class Meta: + unique_together = ('content_type', 'object_id') + db_table = 'buffer_buffereditem' diff --git a/bambu_buffer/settings.py b/bambu_buffer/settings.py new file mode 100644 index 0000000..6b43774 --- /dev/null +++ b/bambu_buffer/settings.py @@ -0,0 +1,44 @@ +from django.conf import settings as s +from django.utils.timezone import now + +CLIENT_ID = s.BUFFER_CLIENT_ID +CLIENT_SECRET = s.BUFFER_CLIENT_SECRET +AUTH_REDIRECT = getattr(s, 'BUFFER_AUTH_REDIRECT', '/') +SUCCESS_MESSAGE = getattr(s, 'BUFFER_SUCCESS_MESSAGE', + u'Your Buffer account is now conntected.' +) + +ERROR_MESSAGE = getattr(s, 'BUFFER_ERROR_MESSAGE', + u'Sorry, your account could not be connected to Buffer: %s.' +) + +UPDATED_MESSAGE = getattr(s, 'BUFFER_UPDATED_MESSAGE', + u'Your Buffer settings have been updated.' +) + +REFRESHED_MESSAGES = getattr(s, 'BUFFER_REFRESHED_MESSAGES', + u'Your Buffer profiles have been refreshed.' +) + +AUTOPOST_MODELS = getattr(s, 'BUFFER_AUTOPOST_MODELS', + ( + ( + 'blog.Post', + 'author', { + 'published': True, + 'date__lte': now + }, + { + 'top': True + } + ), + ) +) + +TIMEOUT = getattr(s, 'BUFFER_TIMEOUT', 5) +AUTHORISE_URL = 'https://bufferapp.com/oauth2/authorize' +TOKEN_URL = 'https://api.bufferapp.com/1/oauth2/token.json' +PROFILES_URL = 'https://api.bufferapp.com/1/profiles.json' +POST_URL = 'https://api.bufferapp.com/1/updates/create.json' +RESPONSE_TYPE = 'code' +AUTHORISATION_CODE = 'authorization_code' diff --git a/bambu_buffer/sites.py b/bambu_buffer/sites.py new file mode 100644 index 0000000..5c8e497 --- /dev/null +++ b/bambu_buffer/sites.py @@ -0,0 +1,101 @@ +from logging import getLogger +from django.db.models.loading import get_model +from django.db.models.signals import post_save + +def post_save_receiver(sender, instance, **kwargs): + from bambu_buffer import post, site + + model = site.get_info(type(instance)) + if not model or not any(model): + print '%s not registered' % ( + unicode(instance._meta.verbose_name).capitalize() + ) + + return + + if any(model['conditions']): + query = dict( + [ + (key, callable(value) and value() or value) + for (key, value) in model['conditions'].items() + ] + ) + + if not type(instance).objects.filter( + pk = instance.pk, + **query + ).exists(): + print '%s does not match Buffer criteria' % unicode( + unicode(instance._meta.verbose_name).capitalize() + ) + + return + + post( + instance, + getattr(instance, + model['author_field'] + ), + **dict( + [ + (key, callable(value) and value() or value) + for (key, value) in model['post_kwargs'].items() + ] + ) + ) + +class BufferSite(object): + def __init__(self, *args, **kwargs): + self._registry = {} + + def register(self, model, author_field, conditions = {}, post_kwargs = {}): + self._registry[model] = { + 'author_field': author_field, + 'conditions': conditions, + 'post_kwargs': post_kwargs + } + + def get_info(self, model): + return self._registry.get(model) + + def hookup_signals(self, models): + logger = getLogger('bambu_buffer') + for m in [list(m) for m in models]: + if not any(m): + continue + + name = m.pop(0) + if any(m): + author_field = m.pop(0) + else: + author_field = 'author' + + if any(m): + conditions = m.pop(0) + else: + conditions = {} + + if any(m): + post_kwargs = m.pop(0) + else: + post_kwargs = {} + + try: + model = get_model(*name.split('.')) + except: + logger.warn('Model %s not found' % name) + continue + + field = model._meta.get_field_by_name(author_field) + if not any(field) or field[0] is None: + raise Exception( + 'Field %s not found in model %s' % (author_field, name) + ) + + self.register(model, + author_field = author_field, + conditions = conditions, + post_kwargs = post_kwargs + ) + + post_save.connect(post_save_receiver, sender = model) diff --git a/bambu_buffer/templates/buffer/base.html b/bambu_buffer/templates/buffer/base.html new file mode 100644 index 0000000..21f5da2 --- /dev/null +++ b/bambu_buffer/templates/buffer/base.html @@ -0,0 +1 @@ +{% extends 'base.html' %} diff --git a/bambu_buffer/templates/buffer/profiles.html b/bambu_buffer/templates/buffer/profiles.html new file mode 100644 index 0000000..93fbd8a --- /dev/null +++ b/bambu_buffer/templates/buffer/profiles.html @@ -0,0 +1,35 @@ +{% extends 'buffer/base.html' %} +{% load i18n icons %} + +{% block page_header %} +

{% trans 'Your Buffer profiles' %}

+

{% trans 'Select the Buffer profiles you want to automatically post to.' %}

+{% endblock page_header %} + +{% block form_content %} +
+ + + {% csrf_token %} + + {% icon 'refresh' %} {% trans 'Reload my Buffer profiles' %} +
+{% endblock form_content %} diff --git a/bambu_buffer/tests.py b/bambu_buffer/tests.py new file mode 100644 index 0000000..56319c2 --- /dev/null +++ b/bambu_buffer/tests.py @@ -0,0 +1,12 @@ +from django.test import TestCase +from django.test.client import RequestFactory +from bambu_buffer.views import auth + +class AuthorisationTestCase(TestCase): + def setUp(self): + self.client = RequestFactory() + + def test_authorisation(self): + request = self.client.get('/buffer/auth/') + response = auth(request) + response diff --git a/bambu_buffer/urls.py b/bambu_buffer/urls.py new file mode 100644 index 0000000..f2c0a92 --- /dev/null +++ b/bambu_buffer/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import patterns, url + +urlpatterns = patterns('bambu_buffer.views', + url('^$', 'profiles', name = 'buffer_profiles'), + url('^refresh/$', 'refresh', name = 'buffer_refresh'), + url('^auth/$', 'auth', name = 'buffer_auth'), + url('^callback/$', 'callback', name = 'buffer_callback') +) diff --git a/bambu_buffer/views.py b/bambu_buffer/views.py new file mode 100644 index 0000000..0706606 --- /dev/null +++ b/bambu_buffer/views.py @@ -0,0 +1,135 @@ +from django.contrib.auth.decorators import login_required +from django.contrib.sites.models import Site +from django.core.urlresolvers import reverse +from django.contrib import messages +from django.db import transaction +from django.http import HttpResponseRedirect +from django.template.response import TemplateResponse +from django.utils.http import urlencode +from bambu_buffer import settings, log +from bambu_buffer.models import BufferToken, BufferProfile +import requests + +@login_required +def auth(request): + next = request.GET.get('next', settings.AUTH_REDIRECT) + request.session['bambu_buffer.next'] = next + + return HttpResponseRedirect( + '%s?%s' % ( + settings.AUTHORISE_URL, + urlencode( + { + 'client_id': settings.CLIENT_ID, + 'redirect_uri': 'http%s://%s%s' % ( + request.is_secure() and 's' or '', + Site.objects.get_current(), + reverse('buffer_callback') + ), + 'response_type': settings.RESPONSE_TYPE + } + ) + ) + ) + +@login_required +def callback(request): + response = requests.post( + settings.TOKEN_URL, + data = { + 'client_id': settings.CLIENT_ID, + 'client_secret': settings.CLIENT_SECRET, + 'redirect_uri': 'http%s://%s%s' % ( + request.is_secure() and 's' or '', + Site.objects.get_current(), + reverse('buffer_callback') + ), + 'code': request.GET.get('code'), + 'grant_type': settings.AUTHORISATION_CODE + }, + timeout = settings.TIMEOUT + ) + + next = request.session.get('bambu_buffer.next', + settings.AUTH_REDIRECT + ) + + if response.status_code == 200: + data = response.json() + token = data.get('access_token') + + with transaction.commit_on_success(): + request.user.buffer_tokens.all().delete() + request.user.buffer_tokens.create( + token = token + ) + + log.success(data, request) + return HttpResponseRedirect( + reverse('buffer_profiles') + ) + else: + log.error(response.json(), request) + + return HttpResponseRedirect(next) + +@login_required +def profiles(request): + try: + token = request.user.buffer_tokens.get() + except BufferToken.DoesNotExist: + return HttpResponseRedirect( + reverse('buffer_auth') + ) + + if request.method == 'POST': + selected = request.POST.getlist('profiles') + for pk in selected: + BufferProfile.objects.filter( + service__token = token, + pk = pk + ).update( + selected = True + ) + + BufferProfile.objects.filter( + service__token = token + ).exclude( + pk__in = selected + ).update( + selected = False + ) + + if settings.UPDATED_MESSAGE: + messages.success(request, settings.UPDATED_MESSAGE) + + return HttpResponseRedirect( + reverse('buffer_profiles') + ) + + return TemplateResponse( + request, + 'buffer/profiles.html', + { + 'profiles': BufferProfile.objects.filter( + service__token = token + ).select_related() + } + ) + +@login_required +def refresh(request): + try: + token = request.user.buffer_tokens.get() + except BufferToken.DoesNotExist: + return HttpResponseRedirect( + reverse('buffer_auth') + ) + + token.refresh_services() + if settings.REFRESHED_MESSAGES: + messages.success(request, settings.REFRESHED_MESSAGES) + + return HttpResponseRedirect( + reverse('buffer_profiles') + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9d58c11 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Django>=1.6 +requests>=2.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fe4517b --- /dev/null +++ b/setup.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +from setuptools import setup +from os import path + +setup( + name = 'bambu-buffer', + version = '2.0', + description = 'Post to Buffer and manage profile settings through a Django-powered site', + author = 'Steadman', + author_email = 'mark@steadman.io', + url = 'https://github.com/iamsteadman/bambu-buffer', + long_description = open(path.join(path.dirname(__file__), 'README')).read(), + packages = [ + 'bambu_buffer', + 'bambu_buffer.migrations', + 'bambu_buffer.management', + 'bambu_buffer.management.commands' + ], + package_data = { + 'bambu_buffer': [ + 'templates/buffer/*.html' + ] + }, + install_requires = [ + 'Django>=1.6', + 'requests>=2.0' + ], + classifiers = [ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Framework :: Django' + ] +)