diff --git a/cms_helper.py b/cms_helper.py index 7a2b661..1643ff7 100755 --- a/cms_helper.py +++ b/cms_helper.py @@ -110,10 +110,12 @@ except ImportError: try: import knocker # pragma: no cover # NOQA HELPER_SETTINGS['INSTALLED_APPS'].append('knocker') + HELPER_SETTINGS['INSTALLED_APPS'].append('channels') + HELPER_SETTINGS['INSTALLED_APPS'].append('djangocms_blog.liveblog',) HELPER_SETTINGS['CHANNEL_LAYERS'] = { 'default': { 'BACKEND': 'asgiref.inmemory.ChannelLayer', - 'ROUTING': 'knocker.routing.channel_routing', + 'ROUTING': 'tests.test_utils.routing.channel_routing', }, } except ImportError: diff --git a/djangocms_blog/__init__.py b/djangocms_blog/__init__.py index 61f6854..17ba9e4 100644 --- a/djangocms_blog/__init__.py +++ b/djangocms_blog/__init__.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function, unicode_literals + __author__ = 'Iacopo Spalletti' __email__ = 'i.spalletti@nephila.it' __version__ = '0.9.pre4' diff --git a/djangocms_blog/admin.py b/djangocms_blog/admin.py index 1e834bc..7ea2e7c 100755 --- a/djangocms_blog/admin.py +++ b/djangocms_blog/admin.py @@ -6,6 +6,7 @@ from copy import deepcopy from aldryn_apphooks_config.admin import BaseAppHookConfig, ModelAppHookConfig from cms.admin.placeholderadmin import FrontendEditableAdminMixin, PlaceholderAdminMixin from django import forms +from django.apps import apps from django.conf import settings from django.conf.urls import url from django.contrib import admin @@ -56,19 +57,20 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin, enhance_exclude = ('main_image', 'tags') _fieldsets = [ (None, { - 'fields': [('title', 'categories', 'publish', 'app_config')] + 'fields': [['title', 'categories', 'publish', 'app_config']] }), ('Info', { - 'fields': (['slug', 'tags'], - ('date_published', 'date_published_end', 'enable_comments')), + 'fields': [['slug', 'tags'], + ['date_published', 'date_published_end'], + ['enable_comments']], 'classes': ('collapse',) }), ('Images', { - 'fields': (('main_image', 'main_image_thumbnail', 'main_image_full'),), + 'fields': [['main_image', 'main_image_thumbnail', 'main_image_full']], 'classes': ('collapse',) }), ('SEO', { - 'fields': [('meta_description', 'meta_title', 'meta_keywords')], + 'fields': [['meta_description', 'meta_title', 'meta_keywords']], 'classes': ('collapse',) }), ] @@ -89,6 +91,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 @@ -187,6 +194,8 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin, fsets[1][1]['fields'][0].append('sites') if request.user.is_superuser: fsets[1][1]['fields'][0].append('author') + if apps.is_installed('djangocms_blog.liveblog'): + fsets[1][1]['fields'][2].append('enable_liveblog') filter_function = get_setting('ADMIN_POST_FIELDSET_FILTER') if callable(filter_function): fsets = filter_function(fsets, request, obj=obj) diff --git a/djangocms_blog/liveblog/__init__.py b/djangocms_blog/liveblog/__init__.py new file mode 100644 index 0000000..f3a5710 --- /dev/null +++ b/djangocms_blog/liveblog/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function, unicode_literals + +default_app_config = 'djangocms_blog.liveblog.apps.LiveBlogAppConfig' 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..75a90ba --- /dev/null +++ b/djangocms_blog/liveblog/cms_plugins.py @@ -0,0 +1,26 @@ +# -*- 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 + fields = ('title', 'body', 'publish') + render_template = 'liveblog/plugins/liveblog.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..87fa8d1 --- /dev/null +++ b/djangocms_blog/liveblog/models.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function, unicode_literals + +import json + +from channels import Group +from cms.models import CMSPlugin, python_2_unicode_compatible +from cms.utils.plugins import reorder_plugins +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from djangocms_text_ckeditor.models import AbstractText +from filer.fields.image import FilerImageField + +from djangocms_blog.models import Post, thumbnail_model + +DATE_FORMAT = "%a %d %b %Y %H:%M" + + +@python_2_unicode_compatible +class LiveblogInterface(models.Model): + """ + Abstract Liveblog plugin model, reusable to customize the liveblogging plugins. + + When implementing this, you **must** call ``self._post_save()`` in the concrete + plugin model ``save`` method. + """ + publish = models.BooleanField(_('publish liveblog entry'), default=False) + node_order_by = '-changed_date' + + class Meta: + verbose_name = _('liveblog entry') + verbose_name_plural = _('liveblog entries') + abstract = True + + def __str__(self): + return str(self.pk) + + def _post_save(self): + 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) + + @property + def liveblog_group(self): + post = Post.objects.language(self.language).filter(liveblog=self.placeholder).first() + if post: + return post.liveblog_group + + def render(self): + return self.render_plugin() + + def send(self): + """ + Render the content and send to the related group + """ + if self.liveblog_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), + }) + + +class Liveblog(LiveblogInterface, AbstractText): + """ + Basic liveblog plugin model + """ + 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' + ) + + class Meta: + verbose_name = _('liveblog entry') + verbose_name_plural = _('liveblog entries') + + def save(self, *args, **kwargs): + super(Liveblog, self).save(*args, **kwargs) + self._post_save() + + def __str__(self): + return AbstractText.__str__(self) diff --git a/djangocms_blog/liveblog/routing.py b/djangocms_blog/liveblog/routing.py new file mode 100644 index 0000000..fb1ff06 --- /dev/null +++ b/djangocms_blog/liveblog/routing.py @@ -0,0 +1,19 @@ +# -*- 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_-]+)/' + r'(?P[a-zA-Z_-]+)/(?P[a-zA-Z0-9_-]+)/$' + ), + route( + 'websocket.disconnect', liveblog_disconnect, + path=r'^/liveblog/(?P[a-zA-Z0-9_-]+)/' + r'(?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..3f7ff3a --- /dev/null +++ b/djangocms_blog/liveblog/static/liveblog/js/liveblog.js @@ -0,0 +1,24 @@ +document.addEventListener("DOMContentLoaded", 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 + "/"; + var socket = new ReconnectingWebSocket(ws_path); + // Handle incoming messages + socket.onmessage = function (message) { + // Decode the JSON + 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 = document.querySelectorAll("div[data-post-id*='" + data.id + "']"); + if (existing.length) { + existing.parentNode.replaceChild(data.content, existing); + } else { + var item = document.createElement('div'); + item.innerHTML = data.content; + document.getElementById("liveblog-posts").insertBefore( + item.children[0], document.getElementById("liveblog-posts").children[0] + ); + } + }; + +}, false); + 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..2fdd5a6 --- /dev/null +++ b/djangocms_blog/liveblog/templates/liveblog/plugins/liveblog.html @@ -0,0 +1,7 @@ +{% spaceless %}{% if instance.publish %} +
+

{{ instance.title }}

+

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

+ {{ instance.content|safe }} +
+{% endif %}{% endspaceless %} 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/0021_post_liveblog.py b/djangocms_blog/migrations/0021_post_liveblog.py new file mode 100644 index 0000000..28e9aa4 --- /dev/null +++ b/djangocms_blog/migrations/0021_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', '__first__'), + ('djangocms_blog', '0020_thumbnail_move4'), + ] + + 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/migrations/0022_auto_20160605_2305.py b/djangocms_blog/migrations/0022_auto_20160605_2305.py new file mode 100644 index 0000000..25e7892 --- /dev/null +++ b/djangocms_blog/migrations/0022_auto_20160605_2305.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-06-05 21:05 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangocms_blog', '0021_post_liveblog'), + ] + + operations = [ + migrations.AddField( + model_name='post', + name='enable_liveblog', + field=models.BooleanField(default=False, verbose_name='enable liveblog on post'), + ), + ] diff --git a/djangocms_blog/models.py b/djangocms_blog/models.py index 3a62aee..ba72459 100644 --- a/djangocms_blog/models.py +++ b/djangocms_blog/models.py @@ -163,6 +163,8 @@ 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') + enable_liveblog = models.BooleanField(verbose_name=_('enable liveblog on post'), default=False) objects = GenericDateTaggedManager() tags = TaggableManager(blank=True, related_name='djangocms_blog_tags') @@ -350,6 +352,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/templates/djangocms_blog/post_detail.html b/djangocms_blog/templates/djangocms_blog/post_detail.html index 6c5fc8b..d9ab105 100644 --- a/djangocms_blog/templates/djangocms_blog/post_detail.html +++ b/djangocms_blog/templates/djangocms_blog/post_detail.html @@ -21,6 +21,9 @@ {% endif %} {% endspaceless %} + {% if view.liveblog_enabled %} + {% include "liveblog/includes/post_detail.html" %} + {% endif %} {% if post.app_config.use_placeholder %}
{% render_placeholder post.content %}
{% else %} diff --git a/djangocms_blog/views.py b/djangocms_blog/views.py index 6e09503..10ee0b0 100644 --- a/djangocms_blog/views.py +++ b/djangocms_blog/views.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, print_function, unicode_literals import os.path from aldryn_apphooks_config.mixins import AppConfigMixin +from django.apps import apps from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse @@ -72,6 +73,9 @@ class PostDetailView(TranslatableSlugMixin, BaseBlogView, DetailView): view_url_name = 'djangocms_blog:post-detail' instant_article = False + def liveblog_enabled(self): + return self.object.enable_liveblog and apps.is_installed('djangocms_blog.liveblog') + def get_template_names(self): if self.instant_article: template_path = (self.config and self.config.template_prefix) or 'djangocms_blog' diff --git a/tests/test_liveblog.py b/tests/test_liveblog.py new file mode 100644 index 0000000..e9b06ed --- /dev/null +++ b/tests/test_liveblog.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function, unicode_literals + +import json +from unittest import SkipTest + +try: + from channels import Channel + from channels.tests import ChannelTestCase + from cms.api import add_plugin + + from djangocms_blog.liveblog.consumers import liveblog_connect, liveblog_disconnect + from djangocms_blog.liveblog.models import DATE_FORMAT + from .base import BaseTest + + + class LiveBlogTest(BaseTest, ChannelTestCase): + + @classmethod + def setUpClass(cls): + try: + import knocker + super(LiveBlogTest, cls).setUpClass() + except ImportError: + raise SkipTest('channels not installed, skipping tests') + + def test_add_plugin(self): + posts = self.get_posts() + self.get_pages() + post = posts[0] + post.enable_liveblog = True + post.save() + + Channel('setup').send({'connect': 1, 'reply_channel': 'reply'}) + message = self.get_next_message('setup', require=True) + liveblog_connect(message, self.app_config_1.namespace, 'en', post.slug) + + plugin = add_plugin( + post.liveblog, 'LiveblogPlugin', language='en', body='live text', publish=True + ) + result = self.get_next_message(message.reply_channel.name, require=True) + self.assertTrue(result['text']) + + rendered = json.loads(result['text']) + self.assertEqual(plugin.pk, rendered['id']) + self.assertEqual(plugin.creation_date.strftime(DATE_FORMAT), rendered['creation_date']) + self.assertEqual(plugin.changed_date.strftime(DATE_FORMAT), rendered['changed_date']) + self.assertTrue(rendered['content'].find('data-post-id="{}"'.format(plugin.pk)) > -1) + self.assertTrue(rendered['content'].find('live text') > -1) + + plugin.body = 'modified text' + plugin.save() + + result = self.get_next_message(message.reply_channel.name, require=True) + self.assertTrue(result['text']) + + rendered = json.loads(result['text']) + self.assertEqual(plugin.pk, rendered['id']) + self.assertEqual(plugin.creation_date.strftime(DATE_FORMAT), rendered['creation_date']) + self.assertEqual(plugin.changed_date.strftime(DATE_FORMAT), rendered['changed_date']) + self.assertTrue(rendered['content'].find('data-post-id="{}"'.format(plugin.pk)) > -1) + self.assertTrue(rendered['content'].find('modified text') > -1) + self.assertTrue(rendered['content'].find('live text') == -1) + + def test_add_plugin_no_publish(self): + posts = self.get_posts() + self.get_pages() + post = posts[0] + post.enable_liveblog = True + post.save() + + Channel('setup').send({'connect': 1, 'reply_channel': 'reply'}) + message = self.get_next_message('setup', require=True) + liveblog_connect(message, self.app_config_1.namespace, 'en', post.slug) + + plugin = add_plugin( + post.liveblog, 'LiveblogPlugin', language='en', body='live text', publish=False + ) + result = self.get_next_message(message.reply_channel.name, require=False) + self.assertIsNone(result) + + plugin.publish = True + plugin.save() + + result = self.get_next_message(message.reply_channel.name, require=True) + self.assertTrue(result['text']) + + rendered = json.loads(result['text']) + self.assertEqual(plugin.pk, rendered['id']) + self.assertEqual(plugin.creation_date.strftime(DATE_FORMAT), rendered['creation_date']) + self.assertEqual(plugin.changed_date.strftime(DATE_FORMAT), rendered['changed_date']) + self.assertTrue(rendered['content'].find('data-post-id="{}"'.format(plugin.pk)) > -1) + self.assertTrue(rendered['content'].find('live text') > -1) + + def test_disconnect(self): + posts = self.get_posts() + self.get_pages() + post = posts[0] + post.enable_liveblog = True + post.save() + + Channel('setup').send({'connect': 1, 'reply_channel': 'reply'}) + message = self.get_next_message('setup', require=True) + liveblog_connect(message, self.app_config_1.namespace, 'en', post.slug) + + plugin = add_plugin( + post.liveblog, 'LiveblogPlugin', language='en', body='live text', publish=True + ) + result = self.get_next_message(message.reply_channel.name, require=True) + self.assertTrue(result['text']) + + liveblog_disconnect(message, self.app_config_1.namespace, 'en', post.slug) + + plugin.body = 'modified text' + plugin.save() + + result = self.get_next_message(message.reply_channel.name, require=False) + self.assertIsNone(result) + + def test_nopost(self): + + self.get_pages() + + Channel('setup').send({'connect': 1, 'reply_channel': 'reply'}) + message = self.get_next_message('setup', require=True) + liveblog_connect(message, self.app_config_1.namespace, 'en', 'random-post') + + result = self.get_next_message(message.reply_channel.name, require=True) + self.assertTrue(result['text']) + rendered = json.loads(result['text']) + self.assertTrue(rendered['error'], 'no_post') + + liveblog_disconnect(message, self.app_config_1.namespace, 'en', 'random-post') + result = self.get_next_message(message.reply_channel.name, require=True) + self.assertTrue(result['text']) + rendered = json.loads(result['text']) + self.assertTrue(rendered['error'], 'no_post') + + def test_plugin_without_post(self): + + pages = self.get_pages() + + placeholder = pages[0].get_placeholders().get(slot='content') + + Channel('setup').send({'connect': 1, 'reply_channel': 'reply'}) + message = self.get_next_message('setup', require=True) + liveblog_connect(message, self.app_config_1.namespace, 'en', 'random post') + self.get_next_message(message.reply_channel.name, require=True) + + plugin = add_plugin( + placeholder, 'LiveblogPlugin', language='en', body='live text', publish=True + ) + self.assertIsNone(plugin.liveblog_group) + result = self.get_next_message(message.reply_channel.name, require=False) + self.assertIsNone(result) + + def test_plugin_render(self): + posts = self.get_posts() + pages = self.get_pages() + post = posts[0] + post.enable_liveblog = True + post.save() + plugin = add_plugin( + post.liveblog, 'LiveblogPlugin', language='en', body='live text', publish=False + ) + context = self.get_plugin_context(pages[0], 'en', plugin, edit=False) + rendered = plugin.render_plugin(context, post.liveblog) + self.assertFalse(rendered.strip()) + + plugin.publish = True + plugin.save() + context = self.get_plugin_context(pages[0], 'en', plugin, edit=False) + rendered = plugin.render_plugin(context, post.liveblog) + self.assertTrue(rendered.find('data-post-id="{}"'.format(plugin.pk)) > -1) + self.assertTrue(rendered.find('live text') > -1) + +except ImportError: # pragma: no cover + pass diff --git a/tests/test_utils/routing.py b/tests/test_utils/routing.py new file mode 100644 index 0000000..38d1771 --- /dev/null +++ b/tests/test_utils/routing.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function, unicode_literals + +from channels import include +from knocker.routing import channel_routing as knocker_routing + +from djangocms_blog.liveblog.routing import channel_routing as djangocms_blog_routing + +channel_routing = [ + include(djangocms_blog_routing, path=r'^/liveblog'), + include(knocker_routing, path=r'^/knocker'), +] diff --git a/tox.ini b/tox.ini index 454f361..e1f679f 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,7 @@ deps = cms33: https://github.com/divio/django-cms/archive/release/3.3.x.zip cms33: djangocms-text-ckeditor>=3.0 knocker: https://github.com/divio/django-cms/archive/release/3.2.x.zip + knocker: channels>=0.15 knocker: https://github.com/nephila/django-knocker/archive/master.zip?0.1.1 knocker: djangocms-text-ckeditor<3.0 django-meta>=1.2