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: try:
import knocker # pragma: no cover # NOQA import knocker # pragma: no cover # NOQA
HELPER_SETTINGS['INSTALLED_APPS'].append('knocker') HELPER_SETTINGS['INSTALLED_APPS'].append('knocker')
HELPER_SETTINGS['INSTALLED_APPS'].append('channels')
HELPER_SETTINGS['INSTALLED_APPS'].append('djangocms_blog.liveblog',)
HELPER_SETTINGS['CHANNEL_LAYERS'] = { HELPER_SETTINGS['CHANNEL_LAYERS'] = {
'default': { 'default': {
'BACKEND': 'asgiref.inmemory.ChannelLayer', 'BACKEND': 'asgiref.inmemory.ChannelLayer',
'ROUTING': 'knocker.routing.channel_routing', 'ROUTING': 'tests.test_utils.routing.channel_routing',
}, },
} }
except ImportError: except ImportError:

View file

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
__author__ = 'Iacopo Spalletti' __author__ = 'Iacopo Spalletti'
__email__ = 'i.spalletti@nephila.it' __email__ = 'i.spalletti@nephila.it'
__version__ = '0.9.pre4' __version__ = '0.9.pre4'

View file

@ -6,6 +6,7 @@ from copy import deepcopy
from aldryn_apphooks_config.admin import BaseAppHookConfig, ModelAppHookConfig from aldryn_apphooks_config.admin import BaseAppHookConfig, ModelAppHookConfig
from cms.admin.placeholderadmin import FrontendEditableAdminMixin, PlaceholderAdminMixin from cms.admin.placeholderadmin import FrontendEditableAdminMixin, PlaceholderAdminMixin
from django import forms from django import forms
from django.apps import apps
from django.conf import settings from django.conf import settings
from django.conf.urls import url from django.conf.urls import url
from django.contrib import admin from django.contrib import admin
@ -56,19 +57,20 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
enhance_exclude = ('main_image', 'tags') enhance_exclude = ('main_image', 'tags')
_fieldsets = [ _fieldsets = [
(None, { (None, {
'fields': [('title', 'categories', 'publish', 'app_config')] 'fields': [['title', 'categories', 'publish', 'app_config']]
}), }),
('Info', { ('Info', {
'fields': (['slug', 'tags'], 'fields': [['slug', 'tags'],
('date_published', 'date_published_end', 'enable_comments')), ['date_published', 'date_published_end'],
['enable_comments']],
'classes': ('collapse',) 'classes': ('collapse',)
}), }),
('Images', { ('Images', {
'fields': (('main_image', 'main_image_thumbnail', 'main_image_full'),), 'fields': [['main_image', 'main_image_thumbnail', 'main_image_full']],
'classes': ('collapse',) 'classes': ('collapse',)
}), }),
('SEO', { ('SEO', {
'fields': [('meta_description', 'meta_title', 'meta_keywords')], 'fields': [['meta_description', 'meta_title', 'meta_keywords']],
'classes': ('collapse',) 'classes': ('collapse',)
}), }),
] ]
@ -89,6 +91,11 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
urls.extend(super(PostAdmin, self).get_urls()) urls.extend(super(PostAdmin, self).get_urls())
return 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): def publish_post(self, request, pk):
""" """
Admin view to publish a single post Admin view to publish a single post
@ -187,6 +194,8 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
fsets[1][1]['fields'][0].append('sites') fsets[1][1]['fields'][0].append('sites')
if request.user.is_superuser: if request.user.is_superuser:
fsets[1][1]['fields'][0].append('author') 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') filter_function = get_setting('ADMIN_POST_FIELDSET_FILTER')
if callable(filter_function): if callable(filter_function):
fsets = filter_function(fsets, request, obj=obj) 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'),)} meta={'unique_together': (('language_code', 'slug'),)}
) )
content = PlaceholderField('post_content', related_name='post_content') 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() objects = GenericDateTaggedManager()
tags = TaggableManager(blank=True, related_name='djangocms_blog_tags') 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): def get_cache_key(self, language, prefix):
return 'djangocms-blog:{2}:{0}:{1}'.format(language, self.guid, 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): class BasePostPlugin(CMSPlugin):
app_config = AppHookConfigField( app_config = AppHookConfigField(

View file

@ -129,6 +129,8 @@ def get_setting(name):
settings, 'BLOG_FEED_LATEST_ITEMS', 10), settings, 'BLOG_FEED_LATEST_ITEMS', 10),
'BLOG_FEED_TAGS_ITEMS': getattr( 'BLOG_FEED_TAGS_ITEMS': getattr(
settings, 'BLOG_FEED_TAGS_ITEMS', 10), settings, 'BLOG_FEED_TAGS_ITEMS', 10),
'BLOG_LIVEBLOG_PLUGINS': getattr(
settings, 'BLOG_LIVEBLOG_PLUGINS', ('LiveblogPlugin',)),
} }
return default['BLOG_%s' % name] return default['BLOG_%s' % name]

View file

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

View file

@ -4,6 +4,7 @@ from __future__ import absolute_import, print_function, unicode_literals
import os.path import os.path
from aldryn_apphooks_config.mixins import AppConfigMixin from aldryn_apphooks_config.mixins import AppConfigMixin
from django.apps import apps
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@ -72,6 +73,9 @@ class PostDetailView(TranslatableSlugMixin, BaseBlogView, DetailView):
view_url_name = 'djangocms_blog:post-detail' view_url_name = 'djangocms_blog:post-detail'
instant_article = False instant_article = False
def liveblog_enabled(self):
return self.object.enable_liveblog and apps.is_installed('djangocms_blog.liveblog')
def get_template_names(self): def get_template_names(self):
if self.instant_article: if self.instant_article:
template_path = (self.config and self.config.template_prefix) or 'djangocms_blog' 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: https://github.com/divio/django-cms/archive/release/3.3.x.zip
cms33: djangocms-text-ckeditor>=3.0 cms33: djangocms-text-ckeditor>=3.0
knocker: https://github.com/divio/django-cms/archive/release/3.2.x.zip 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: https://github.com/nephila/django-knocker/archive/master.zip?0.1.1
knocker: djangocms-text-ckeditor<3.0 knocker: djangocms-text-ckeditor<3.0
django-meta>=1.2 django-meta>=1.2