Merge pull request #232 from nephila/liveblog

Liveblog using channels
This commit is contained in:
Iacopo Spalletti 2016-06-26 13:58:24 +02:00 committed by GitHub
commit 4bd6e4fb3a
26 changed files with 569 additions and 6 deletions

View file

@ -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:

View file

@ -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'

View file

@ -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)

View file

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
default_app_config = 'djangocms_blog.liveblog.apps.LiveBlogAppConfig'

View file

@ -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')

View file

@ -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)

View file

@ -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)

View file

@ -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',),
),
]

View file

@ -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,
),
]

View file

@ -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)

View file

@ -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<apphook>[a-zA-Z0-9_-]+)/'
r'(?P<lang>[a-zA-Z_-]+)/(?P<post>[a-zA-Z0-9_-]+)/$'
),
route(
'websocket.disconnect', liveblog_disconnect,
path=r'^/liveblog/(?P<apphook>[a-zA-Z0-9_-]+)/'
r'(?P<lang>[a-zA-Z_-]+)/(?P<post>[a-zA-Z0-9_-]+)/$'
),
]

View file

@ -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);

View file

@ -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});

View file

@ -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" %}
<script>
var liveblog_apphook = '{{ post.app_config.namespace }}';
var liveblog_language = '{{ post.get_current_language }}';
var liveblog_post = '{{ post.slug }}';
</script>
<div class="blog-content--live" id="liveblog-posts">
{% render_placeholder post.liveblog %}
</div>

View file

@ -0,0 +1,7 @@
{% spaceless %}{% if instance.publish %}
<div class="post" data-post-id="{{ instance.id }}">
<h3>{{ instance.title }}</h3>
<h4>{{ instance.creation_date|date:"D d M Y H:i" }}</h4>
{{ instance.content|safe }}
</div>
{% endif %}{% endspaceless %}

View file

@ -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),
),
]

View file

@ -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'),
),
]

View file

@ -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(

View file

@ -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]

View file

@ -21,6 +21,9 @@
</div>
{% endif %}
{% endspaceless %}
{% if view.liveblog_enabled %}
{% include "liveblog/includes/post_detail.html" %}
{% endif %}
{% if post.app_config.use_placeholder %}
<div class="blog-content">{% render_placeholder post.content %}</div>
{% else %}

View file

@ -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'

178
tests/test_liveblog.py Normal file
View file

@ -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

View file

@ -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'),
]

View file

@ -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