From a6f296226a2a2ab56489001429c08edd5eeb00ba Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Sun, 10 Apr 2016 19:47:36 +0200 Subject: [PATCH] Add experimental liveblogging support --- djangocms_blog/admin.py | 5 + djangocms_blog/liveblog/__init__.py | 2 + djangocms_blog/liveblog/apps.py | 10 + djangocms_blog/liveblog/cms_plugins.py | 30 +++ djangocms_blog/liveblog/consumers.py | 50 ++++ .../liveblog/migrations/0001_initial.py | 32 +++ .../migrations/0002_liveblog_title.py | 20 ++ .../liveblog/migrations/__init__.py | 0 djangocms_blog/liveblog/models.py | 67 ++++++ djangocms_blog/liveblog/routing.py | 17 ++ .../liveblog/static/liveblog/js/liveblog.js | 27 +++ .../liveblog/js/reconnecting-websocket.min.js | 1 + .../liveblog/includes/post_detail.html | 11 + .../templates/liveblog/plugins/liveblog.html | 4 + .../liveblog/plugins/unpublished.html | 0 .../migrations/0016_post_liveblog.py | 21 ++ djangocms_blog/models.py | 9 + djangocms_blog/settings.py | 2 + .../0018_auto__add_field_post_liveblog.py | 222 ++++++++++++++++++ djangocms_blog/views.py | 6 + 20 files changed, 536 insertions(+) create mode 100644 djangocms_blog/liveblog/__init__.py create mode 100644 djangocms_blog/liveblog/apps.py create mode 100644 djangocms_blog/liveblog/cms_plugins.py create mode 100644 djangocms_blog/liveblog/consumers.py create mode 100644 djangocms_blog/liveblog/migrations/0001_initial.py create mode 100644 djangocms_blog/liveblog/migrations/0002_liveblog_title.py create mode 100644 djangocms_blog/liveblog/migrations/__init__.py create mode 100644 djangocms_blog/liveblog/models.py create mode 100644 djangocms_blog/liveblog/routing.py create mode 100644 djangocms_blog/liveblog/static/liveblog/js/liveblog.js create mode 100644 djangocms_blog/liveblog/static/liveblog/js/reconnecting-websocket.min.js create mode 100644 djangocms_blog/liveblog/templates/liveblog/includes/post_detail.html create mode 100644 djangocms_blog/liveblog/templates/liveblog/plugins/liveblog.html create mode 100644 djangocms_blog/liveblog/templates/liveblog/plugins/unpublished.html create mode 100644 djangocms_blog/migrations/0016_post_liveblog.py create mode 100644 djangocms_blog/south_migrations/0018_auto__add_field_post_liveblog.py diff --git a/djangocms_blog/admin.py b/djangocms_blog/admin.py index 1e834bc..51d32a1 100755 --- a/djangocms_blog/admin.py +++ b/djangocms_blog/admin.py @@ -89,6 +89,11 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin, urls.extend(super(PostAdmin, self).get_urls()) return urls + def post_add_plugin(self, request, placeholder, plugin): + if plugin.plugin_type in get_setting('LIVEBLOG_PLUGINS'): + plugin = plugin.move(plugin.get_siblings().first(), 'first-sibling') + return super(PostAdmin, self).post_add_plugin(request, placeholder, plugin) + def publish_post(self, request, pk): """ Admin view to publish a single post diff --git a/djangocms_blog/liveblog/__init__.py b/djangocms_blog/liveblog/__init__.py new file mode 100644 index 0000000..ba25ec7 --- /dev/null +++ b/djangocms_blog/liveblog/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function, unicode_literals diff --git a/djangocms_blog/liveblog/apps.py b/djangocms_blog/liveblog/apps.py new file mode 100644 index 0000000..7655e85 --- /dev/null +++ b/djangocms_blog/liveblog/apps.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function, unicode_literals + +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class LiveBlogAppConfig(AppConfig): + name = 'djangocms_blog.liveblog' + verbose_name = _('Liveblog') diff --git a/djangocms_blog/liveblog/cms_plugins.py b/djangocms_blog/liveblog/cms_plugins.py new file mode 100644 index 0000000..5487a85 --- /dev/null +++ b/djangocms_blog/liveblog/cms_plugins.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function, unicode_literals + +from cms.plugin_pool import plugin_pool +from django.utils.translation import ugettext_lazy as _ +from djangocms_text_ckeditor.cms_plugins import TextPlugin + +from djangocms_blog.settings import get_setting + +from .models import Liveblog + + +class LiveblogPlugin(TextPlugin): + module = get_setting('PLUGIN_MODULE_NAME') + name = _('Liveblog item') + model = Liveblog + + def _get_render_template(self, context, instance, placeholder): + if instance.publish: + return 'liveblog/plugins/liveblog.html' + else: + return 'liveblog/plugins/unpublished.html' + + def render(self, context, instance, placeholder): + context = super(LiveblogPlugin, self).render(context, instance, placeholder) + instance.content = context['body'] + context['instance'] = instance + return context + +plugin_pool.register_plugin(LiveblogPlugin) diff --git a/djangocms_blog/liveblog/consumers.py b/djangocms_blog/liveblog/consumers.py new file mode 100644 index 0000000..91041e9 --- /dev/null +++ b/djangocms_blog/liveblog/consumers.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function, unicode_literals + +import json + +from channels import Group + +from djangocms_blog.models import Post + + +def liveblog_connect(message, apphook, lang, post): + """ + Connect users to the group of the given post according to the given language + + Return with an error message if a post cannot be found + + :param message: channel connect message + :param apphook: apphook config namespace + :param lang: language + :param post: post slug + """ + try: + post = Post.objects.namespace(apphook).language(lang).active_translations(slug=post).get() + except Post.DoesNotExist: + message.reply_channel.send({ + 'text': json.dumps({'error': 'no_post'}), + }) + return + Group(post.liveblog_group).add(message.reply_channel) + + +def liveblog_disconnect(message, apphook, lang, post): + """ + Disconnect users to the group of the given post according to the given language + + Return with an error message if a post cannot be found + + :param message: channel connect message + :param apphook: apphook config namespace + :param lang: language + :param post: post slug + """ + try: + post = Post.objects.namespace(apphook).language(lang).active_translations(slug=post).get() + except Post.DoesNotExist: + message.reply_channel.send({ + 'text': json.dumps({'error': 'no_post'}), + }) + return + Group(post.liveblog_group).discard(message.reply_channel) diff --git a/djangocms_blog/liveblog/migrations/0001_initial.py b/djangocms_blog/liveblog/migrations/0001_initial.py new file mode 100644 index 0000000..6ceb53e --- /dev/null +++ b/djangocms_blog/liveblog/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import filer.fields.image +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cms', '0013_urlconfrevision'), + ('filer', '0003_thumbnailoption'), + ] + + operations = [ + migrations.CreateModel( + name='Liveblog', + fields=[ + ('cmsplugin_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='cms.CMSPlugin')), + ('body', models.TextField(verbose_name='body')), + ('publish', models.BooleanField(default=False, verbose_name='publish liveblog entry')), + ('image', filer.fields.image.FilerImageField(related_name='djangocms_blog_liveblog_image', on_delete=django.db.models.deletion.SET_NULL, verbose_name='image', blank=True, to='filer.Image', null=True)), + ('thumbnail', models.ForeignKey(related_name='djangocms_blog_liveblog_thumbnail', on_delete=django.db.models.deletion.SET_NULL, verbose_name='thumbnail size', blank=True, to='filer.ThumbnailOption', null=True)), + ], + options={ + 'verbose_name': 'liveblog entry', + 'verbose_name_plural': 'liveblog entries', + }, + bases=('cms.cmsplugin',), + ), + ] diff --git a/djangocms_blog/liveblog/migrations/0002_liveblog_title.py b/djangocms_blog/liveblog/migrations/0002_liveblog_title.py new file mode 100644 index 0000000..b59c5ad --- /dev/null +++ b/djangocms_blog/liveblog/migrations/0002_liveblog_title.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('liveblog', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='liveblog', + name='title', + field=models.CharField(default='', max_length=255, verbose_name='title'), + preserve_default=False, + ), + ] diff --git a/djangocms_blog/liveblog/migrations/__init__.py b/djangocms_blog/liveblog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/djangocms_blog/liveblog/models.py b/djangocms_blog/liveblog/models.py new file mode 100644 index 0000000..b0172f0 --- /dev/null +++ b/djangocms_blog/liveblog/models.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function, unicode_literals + +import json + +from channels import Group +from cms.models import CMSPlugin +from cms.utils.plugins import reorder_plugins +from django.db import models +from djangocms_text_ckeditor.models import AbstractText +from django.utils.translation import ugettext_lazy as _ +from filer.fields.image import FilerImageField + +from djangocms_blog.models import thumbnail_model, Post + +DATE_FORMAT = "%a %d %b %Y %H:%M" + + +class Liveblog(AbstractText): + title = models.CharField(_('title'), max_length=255) + image = FilerImageField( + verbose_name=_('image'), blank=True, null=True, on_delete=models.SET_NULL, + related_name='djangocms_blog_liveblog_image' + ) + thumbnail = models.ForeignKey( + thumbnail_model, verbose_name=_('thumbnail size'), on_delete=models.SET_NULL, + blank=True, null=True, related_name='djangocms_blog_liveblog_thumbnail' + ) + publish = models.BooleanField(_('publish liveblog entry'), default=False) + node_order_by = '-changed_date' + + class Meta: + verbose_name = _('liveblog entry') + verbose_name_plural = _('liveblog entries') + + def save(self, no_signals=False, *args, **kwargs): + if not self.pk: + self.position = 0 + saved = super(Liveblog, self).save(*args, **kwargs) + if self.publish: + self.send() + order = CMSPlugin.objects.filter(placeholder=self.placeholder).order_by('placeholder', 'path').values_list('pk', flat=True) + reorder_plugins(self.placeholder, None, self.language, order) + return saved + + @property + def liveblog_group(self): + post = Post.objects.language(self.language).filter(liveblog=self.placeholder).first() + return post.liveblog_group + + def render(self): + print(self.position, self.path) + return self.render_plugin() + + def send(self): + """ + Render the content and send to the related group + """ + notification = { + 'id': self.pk, + 'content': self.render(), + 'creation_date': self.creation_date.strftime(DATE_FORMAT), + 'changed_date': self.changed_date.strftime(DATE_FORMAT), + } + Group(self.liveblog_group).send({ + 'text': json.dumps(notification), + }) diff --git a/djangocms_blog/liveblog/routing.py b/djangocms_blog/liveblog/routing.py new file mode 100644 index 0000000..7751c4c --- /dev/null +++ b/djangocms_blog/liveblog/routing.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function, unicode_literals + +from channels import route + +from .consumers import liveblog_connect, liveblog_disconnect + +channel_routing = [ + route( + 'websocket.connect', liveblog_connect, + path=r'^/liveblog/(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z_-]+)/(?P[a-zA-Z0-9_-]+)/$' + ), + route( + 'websocket.disconnect', liveblog_disconnect, + path=r'^/liveblog/(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z_-]+)/(?P[a-zA-Z0-9_-]+)/$' + ), +] diff --git a/djangocms_blog/liveblog/static/liveblog/js/liveblog.js b/djangocms_blog/liveblog/static/liveblog/js/liveblog.js new file mode 100644 index 0000000..1a2105f --- /dev/null +++ b/djangocms_blog/liveblog/static/liveblog/js/liveblog.js @@ -0,0 +1,27 @@ +$(function () { + // Correctly decide between ws:// and wss:// + var ws_scheme = window.location.protocol == "https:" ? "wss" : "ws"; + var ws_path = ws_scheme + '://' + window.location.host + "/liveblog/liveblog/" + liveblog_apphook + "/" + liveblog_language + "/" + liveblog_post + "/"; + console.log("Connecting to " + ws_path); + var socket = new ReconnectingWebSocket(ws_path); + // Handle incoming messages + socket.onmessage = function (message) { + // Decode the JSON + console.log("Got message " + message.data); + var data = JSON.parse(message.data); + // See if there's a div to replace it in, or if we should add a new one + var existing = $("div[data-post-id=" + data.id + "]"); + if (existing.length) { + existing.replaceWith(data.content); + } else { + $("#liveblog-posts").prepend(data.content); + } + }; + // Helpful debugging + socket.onopen = function () { + console.log("Connected to notification socket"); + } + socket.onclose = function () { + console.log("Disconnected to notification socket"); + } +}); diff --git a/djangocms_blog/liveblog/static/liveblog/js/reconnecting-websocket.min.js b/djangocms_blog/liveblog/static/liveblog/js/reconnecting-websocket.min.js new file mode 100644 index 0000000..3015099 --- /dev/null +++ b/djangocms_blog/liveblog/static/liveblog/js/reconnecting-websocket.min.js @@ -0,0 +1 @@ +!function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.ReconnectingWebSocket=b()}(this,function(){function a(b,c,d){function l(a,b){var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,!1,!1,b),c}var e={debug:!1,automaticOpen:!0,reconnectInterval:1e3,maxReconnectInterval:3e4,reconnectDecay:1.5,timeoutInterval:2e3};d||(d={});for(var f in e)this[f]="undefined"!=typeof d[f]?d[f]:e[f];this.url=b,this.reconnectAttempts=0,this.readyState=WebSocket.CONNECTING,this.protocol=null;var h,g=this,i=!1,j=!1,k=document.createElement("div");k.addEventListener("open",function(a){g.onopen(a)}),k.addEventListener("close",function(a){g.onclose(a)}),k.addEventListener("connecting",function(a){g.onconnecting(a)}),k.addEventListener("message",function(a){g.onmessage(a)}),k.addEventListener("error",function(a){g.onerror(a)}),this.addEventListener=k.addEventListener.bind(k),this.removeEventListener=k.removeEventListener.bind(k),this.dispatchEvent=k.dispatchEvent.bind(k),this.open=function(b){h=new WebSocket(g.url,c||[]),b||k.dispatchEvent(l("connecting")),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","attempt-connect",g.url);var d=h,e=setTimeout(function(){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","connection-timeout",g.url),j=!0,d.close(),j=!1},g.timeoutInterval);h.onopen=function(){clearTimeout(e),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onopen",g.url),g.protocol=h.protocol,g.readyState=WebSocket.OPEN,g.reconnectAttempts=0;var d=l("open");d.isReconnect=b,b=!1,k.dispatchEvent(d)},h.onclose=function(c){if(clearTimeout(e),h=null,i)g.readyState=WebSocket.CLOSED,k.dispatchEvent(l("close"));else{g.readyState=WebSocket.CONNECTING;var d=l("connecting");d.code=c.code,d.reason=c.reason,d.wasClean=c.wasClean,k.dispatchEvent(d),b||j||((g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onclose",g.url),k.dispatchEvent(l("close")));var e=g.reconnectInterval*Math.pow(g.reconnectDecay,g.reconnectAttempts);setTimeout(function(){g.reconnectAttempts++,g.open(!0)},e>g.maxReconnectInterval?g.maxReconnectInterval:e)}},h.onmessage=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onmessage",g.url,b.data);var c=l("message");c.data=b.data,k.dispatchEvent(c)},h.onerror=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onerror",g.url,b),k.dispatchEvent(l("error"))}},1==this.automaticOpen&&this.open(!1),this.send=function(b){if(h)return(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","send",g.url,b),h.send(b);throw"INVALID_STATE_ERR : Pausing to reconnect websocket"},this.close=function(a,b){"undefined"==typeof a&&(a=1e3),i=!0,h&&h.close(a,b)},this.refresh=function(){h&&h.close()}}return a.prototype.onopen=function(){},a.prototype.onclose=function(){},a.prototype.onconnecting=function(){},a.prototype.onmessage=function(){},a.prototype.onerror=function(){},a.debugAll=!1,a.CONNECTING=WebSocket.CONNECTING,a.OPEN=WebSocket.OPEN,a.CLOSING=WebSocket.CLOSING,a.CLOSED=WebSocket.CLOSED,a}); diff --git a/djangocms_blog/liveblog/templates/liveblog/includes/post_detail.html b/djangocms_blog/liveblog/templates/liveblog/includes/post_detail.html new file mode 100644 index 0000000..a318eb6 --- /dev/null +++ b/djangocms_blog/liveblog/templates/liveblog/includes/post_detail.html @@ -0,0 +1,11 @@ +{% load cms_tags sekizai_tags %} +{% add_data "js-script" "liveblog/js/reconnecting-websocket.min.js" %} +{% add_data "js-script" "liveblog/js/liveblog.js" %} + +
+ {% render_placeholder post.liveblog %} +
diff --git a/djangocms_blog/liveblog/templates/liveblog/plugins/liveblog.html b/djangocms_blog/liveblog/templates/liveblog/plugins/liveblog.html new file mode 100644 index 0000000..526af0e --- /dev/null +++ b/djangocms_blog/liveblog/templates/liveblog/plugins/liveblog.html @@ -0,0 +1,4 @@ +
+

{{ instance.title }}{{ instance.creation_date|date:"D d M Y H:i" }}

+ {{ instance.content|safe }} +
diff --git a/djangocms_blog/liveblog/templates/liveblog/plugins/unpublished.html b/djangocms_blog/liveblog/templates/liveblog/plugins/unpublished.html new file mode 100644 index 0000000..e69de29 diff --git a/djangocms_blog/migrations/0016_post_liveblog.py b/djangocms_blog/migrations/0016_post_liveblog.py new file mode 100644 index 0000000..52555d3 --- /dev/null +++ b/djangocms_blog/migrations/0016_post_liveblog.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import cms.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('cms', '0013_urlconfrevision'), + ('djangocms_blog', '0015_auto_20160408_1849'), + ] + + operations = [ + migrations.AddField( + model_name='post', + name='liveblog', + field=cms.models.fields.PlaceholderField(related_name='live_blog', slotname='live_blog', editable=False, to='cms.Placeholder', null=True), + ), + ] diff --git a/djangocms_blog/models.py b/djangocms_blog/models.py index 3a62aee..afa8212 100644 --- a/djangocms_blog/models.py +++ b/djangocms_blog/models.py @@ -163,6 +163,7 @@ class Post(KnockerModel, ModelMeta, TranslatableModel): meta={'unique_together': (('language_code', 'slug'),)} ) content = PlaceholderField('post_content', related_name='post_content') + liveblog = PlaceholderField('live_blog', related_name='live_blog') objects = GenericDateTaggedManager() tags = TaggableManager(blank=True, related_name='djangocms_blog_tags') @@ -350,6 +351,14 @@ class Post(KnockerModel, ModelMeta, TranslatableModel): def get_cache_key(self, language, prefix): return 'djangocms-blog:{2}:{0}:{1}'.format(language, self.guid, prefix) + @property + def liveblog_group(self): + return 'liveblog/{apphook}/{lang}/{post}'.format( + lang=self.get_current_language(), + apphook=self.app_config.namespace, + post=self.safe_translation_getter('slug', any_language=True) + ) + class BasePostPlugin(CMSPlugin): app_config = AppHookConfigField( diff --git a/djangocms_blog/settings.py b/djangocms_blog/settings.py index de47dfb..a1b220c 100644 --- a/djangocms_blog/settings.py +++ b/djangocms_blog/settings.py @@ -129,6 +129,8 @@ def get_setting(name): settings, 'BLOG_FEED_LATEST_ITEMS', 10), 'BLOG_FEED_TAGS_ITEMS': getattr( settings, 'BLOG_FEED_TAGS_ITEMS', 10), + 'BLOG_LIVEBLOG_PLUGINS': getattr( + settings, 'BLOG_LIVEBLOG_PLUGINS', ('LiveblogPlugin',)), } return default['BLOG_%s' % name] diff --git a/djangocms_blog/south_migrations/0018_auto__add_field_post_liveblog.py b/djangocms_blog/south_migrations/0018_auto__add_field_post_liveblog.py new file mode 100644 index 0000000..4cebb73 --- /dev/null +++ b/djangocms_blog/south_migrations/0018_auto__add_field_post_liveblog.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as 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 'Post.liveblog' + db.add_column('djangocms_blog_post', 'liveblog', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['cms.Placeholder'], null=True, related_name='live_blog'), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Post.liveblog' + db.delete_column('djangocms_blog_post', 'liveblog_id') + + + 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']", 'blank': 'True', 'symmetrical': 'False'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', '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', [], {'blank': 'True', 'max_length': '75'}), + 'first_name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'max_length': '30'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True', 'related_name': "'user_set'", 'symmetrical': 'False'}), + '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', [], {'blank': 'True', 'max_length': '30'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'related_name': "'user_set'", 'symmetrical': 'False'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'cms.cmsplugin': { + 'Meta': {'object_name': 'CMSPlugin'}, + 'changed_date': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now': 'True'}), + 'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'depth': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}), + 'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'to': "orm['cms.CMSPlugin']", 'null': 'True'}), + 'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'placeholder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True'}), + 'plugin_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'position': ('django.db.models.fields.PositiveSmallIntegerField', [], {'blank': 'True', 'null': 'True'}) + }, + 'cms.placeholder': { + 'Meta': {'object_name': 'Placeholder'}, + 'default_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slot': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) + }, + 'cmsplugin_filer_image.thumbnailoption': { + 'Meta': {'ordering': "('width', 'height')", 'object_name': 'ThumbnailOption'}, + 'crop': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'height': ('django.db.models.fields.IntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'upscale': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'width': ('django.db.models.fields.IntegerField', [], {}) + }, + 'contenttypes.contenttype': { + 'Meta': {'db_table': "'django_content_type'", 'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType'}, + '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'}) + }, + 'djangocms_blog.authorentriesplugin': { + 'Meta': {'object_name': 'AuthorEntriesPlugin'}, + 'app_config': ('aldryn_apphooks_config.fields.AppHookConfigField', [], {'blank': 'True', 'to': "orm['djangocms_blog.BlogConfig']", 'null': 'True'}), + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False'}), + 'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'primary_key': 'True', 'unique': 'True'}), + 'latest_posts': ('django.db.models.fields.IntegerField', [], {'default': '5'}) + }, + 'djangocms_blog.blogcategory': { + 'Meta': {'object_name': 'BlogCategory'}, + 'app_config': ('aldryn_apphooks_config.fields.AppHookConfigField', [], {'to': "orm['djangocms_blog.BlogConfig']", 'null': 'True'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'date_modified': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'to': "orm['djangocms_blog.BlogCategory']", 'null': 'True'}) + }, + 'djangocms_blog.blogcategorytranslation': { + 'Meta': {'db_table': "'djangocms_blog_blogcategory_translation'", 'unique_together': "[('language_code', 'slug'), ('language_code', 'master')]", 'object_name': 'BlogCategoryTranslation'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language_code': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}), + 'master': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['djangocms_blog.BlogCategory']", 'null': 'True', 'related_name': "'translations'"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'blank': 'True', 'max_length': '50'}) + }, + 'djangocms_blog.blogconfig': { + 'Meta': {'object_name': 'BlogConfig'}, + 'app_data': ('app_data.fields.AppDataField', [], {'default': "'{}'"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'namespace': ('django.db.models.fields.CharField', [], {'unique': 'True', 'default': 'None', 'max_length': '100'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'djangocms_blog.blogconfigtranslation': { + 'Meta': {'db_table': "'djangocms_blog_blogconfig_translation'", 'unique_together': "[('language_code', 'master')]", 'object_name': 'BlogConfigTranslation'}, + 'app_title': ('django.db.models.fields.CharField', [], {'max_length': '234'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language_code': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}), + 'master': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['djangocms_blog.BlogConfig']", 'null': 'True', 'related_name': "'translations'"}), + 'object_name': ('django.db.models.fields.CharField', [], {'default': "'Article'", 'max_length': '234'}) + }, + 'djangocms_blog.genericblogplugin': { + 'Meta': {'object_name': 'GenericBlogPlugin'}, + 'app_config': ('aldryn_apphooks_config.fields.AppHookConfigField', [], {'blank': 'True', 'to': "orm['djangocms_blog.BlogConfig']", 'null': 'True'}), + 'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'primary_key': 'True', 'unique': 'True'}) + }, + 'djangocms_blog.latestpostsplugin': { + 'Meta': {'object_name': 'LatestPostsPlugin'}, + 'app_config': ('aldryn_apphooks_config.fields.AppHookConfigField', [], {'blank': 'True', 'to': "orm['djangocms_blog.BlogConfig']", 'null': 'True'}), + 'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['djangocms_blog.BlogCategory']", 'blank': 'True', 'symmetrical': 'False'}), + 'cmsplugin_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['cms.CMSPlugin']", 'primary_key': 'True', 'unique': 'True'}), + 'latest_posts': ('django.db.models.fields.IntegerField', [], {'default': '5'}) + }, + 'djangocms_blog.post': { + 'Meta': {'ordering': "('-date_published', '-date_created')", 'object_name': 'Post'}, + 'app_config': ('aldryn_apphooks_config.fields.AppHookConfigField', [], {'to': "orm['djangocms_blog.BlogConfig']", 'null': 'True'}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'to': "orm['auth.User']", 'null': 'True', 'related_name': "'djangocms_blog_post_author'"}), + 'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['djangocms_blog.BlogCategory']", 'blank': 'True', 'related_name': "'blog_posts'", 'symmetrical': 'False'}), + 'content': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True', 'related_name': "'post_content'"}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'date_modified': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now': 'True'}), + 'date_published': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'null': 'True'}), + 'date_published_end': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'null': 'True'}), + 'enable_comments': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'liveblog': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cms.Placeholder']", 'null': 'True', 'related_name': "'live_blog'"}), + 'main_image': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'to': "orm['filer.Image']", 'on_delete': 'models.SET_NULL', 'null': 'True', 'related_name': "'djangocms_blog_post_image'"}), + 'main_image_full': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'to': "orm['cmsplugin_filer_image.ThumbnailOption']", 'on_delete': 'models.SET_NULL', 'null': 'True', 'related_name': "'djangocms_blog_post_full'"}), + 'main_image_thumbnail': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'to': "orm['cmsplugin_filer_image.ThumbnailOption']", 'on_delete': 'models.SET_NULL', 'null': 'True', 'related_name': "'djangocms_blog_post_thumbnail'"}), + 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'sites': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['sites.Site']", 'blank': 'True', 'symmetrical': 'False'}) + }, + 'djangocms_blog.posttranslation': { + 'Meta': {'db_table': "'djangocms_blog_post_translation'", 'unique_together': "[('language_code', 'slug'), ('language_code', 'master')]", 'object_name': 'PostTranslation'}, + 'abstract': ('djangocms_text_ckeditor.fields.HTMLField', [], {'blank': 'True', 'default': "''"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language_code': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}), + 'master': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['djangocms_blog.Post']", 'null': 'True', 'related_name': "'translations'"}), + 'meta_description': ('django.db.models.fields.TextField', [], {'blank': 'True', 'default': "''"}), + 'meta_keywords': ('django.db.models.fields.TextField', [], {'blank': 'True', 'default': "''"}), + 'meta_title': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "''", 'max_length': '255'}), + 'post_text': ('djangocms_text_ckeditor.fields.HTMLField', [], {'blank': 'True', 'default': "''"}), + 'slug': ('django.db.models.fields.SlugField', [], {'blank': 'True', 'max_length': '50'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'filer.file': { + 'Meta': {'object_name': 'File'}, + '_file_size': ('django.db.models.fields.IntegerField', [], {'blank': 'True', 'null': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True', 'null': 'True'}), + 'file': ('django.db.models.fields.files.FileField', [], {'blank': 'True', 'null': 'True', 'max_length': '255'}), + 'folder': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'to': "orm['filer.Folder']", 'null': 'True', 'related_name': "'all_files'"}), + 'has_all_mandatory_data': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'modified_at': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "''", 'max_length': '255'}), + 'original_filename': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'max_length': '255'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'to': "orm['auth.User']", 'null': 'True', 'related_name': "'owned_files'"}), + 'polymorphic_ctype': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'related_name': "'polymorphic_filer.file_set+'"}), + 'sha1': ('django.db.models.fields.CharField', [], {'blank': 'True', 'default': "''", 'max_length': '40'}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'filer.folder': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('parent', 'name'),)", 'object_name': 'Folder'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'modified_at': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'auto_now': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'to': "orm['auth.User']", 'null': 'True', 'related_name': "'filer_owned_folders'"}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'to': "orm['filer.Folder']", 'null': 'True', 'related_name': "'children'"}), + 'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'filer.image': { + 'Meta': {'object_name': 'Image'}, + '_height': ('django.db.models.fields.IntegerField', [], {'blank': 'True', 'null': 'True'}), + '_width': ('django.db.models.fields.IntegerField', [], {'blank': 'True', 'null': 'True'}), + 'author': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'max_length': '255'}), + 'date_taken': ('django.db.models.fields.DateTimeField', [], {'blank': 'True', 'null': 'True'}), + 'default_alt_text': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'max_length': '255'}), + 'default_caption': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'max_length': '255'}), + 'file_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['filer.File']", 'primary_key': 'True', 'unique': 'True'}), + 'must_always_publish_author_credit': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'must_always_publish_copyright': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'subject_location': ('django.db.models.fields.CharField', [], {'blank': 'True', 'null': 'True', 'default': 'None', 'max_length': '64'}) + }, + 'sites.site': { + 'Meta': {'db_table': "'django_site'", 'ordering': "('domain',)", 'object_name': 'Site'}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + } + } + + complete_apps = ['djangocms_blog'] \ No newline at end of file diff --git a/djangocms_blog/views.py b/djangocms_blog/views.py index 6e09503..2e99e14 100644 --- a/djangocms_blog/views.py +++ b/djangocms_blog/views.py @@ -51,6 +51,12 @@ class BaseBlogView(AppConfigMixin, ViewUrlMixin): template_path = (self.config and self.config.template_prefix) or 'djangocms_blog' return os.path.join(template_path, self.base_template_name) + def liveblog_enabled(self): + try: + from django.apps import apps + return apps.is_installed('djangocms_blog.liveblog') + except ImportError: + return False class BaseBlogListView(BaseBlogView): context_object_name = 'post_list'