commit
4bd6e4fb3a
26 changed files with 569 additions and 6 deletions
|
@ -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:
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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)
|
||||||
|
|
4
djangocms_blog/liveblog/__init__.py
Normal file
4
djangocms_blog/liveblog/__init__.py
Normal 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'
|
10
djangocms_blog/liveblog/apps.py
Normal file
10
djangocms_blog/liveblog/apps.py
Normal 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')
|
26
djangocms_blog/liveblog/cms_plugins.py
Normal file
26
djangocms_blog/liveblog/cms_plugins.py
Normal 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)
|
50
djangocms_blog/liveblog/consumers.py
Normal file
50
djangocms_blog/liveblog/consumers.py
Normal 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)
|
32
djangocms_blog/liveblog/migrations/0001_initial.py
Normal file
32
djangocms_blog/liveblog/migrations/0001_initial.py
Normal 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',),
|
||||||
|
),
|
||||||
|
]
|
20
djangocms_blog/liveblog/migrations/0002_liveblog_title.py
Normal file
20
djangocms_blog/liveblog/migrations/0002_liveblog_title.py
Normal 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,
|
||||||
|
),
|
||||||
|
]
|
0
djangocms_blog/liveblog/migrations/__init__.py
Normal file
0
djangocms_blog/liveblog/migrations/__init__.py
Normal file
94
djangocms_blog/liveblog/models.py
Normal file
94
djangocms_blog/liveblog/models.py
Normal 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)
|
19
djangocms_blog/liveblog/routing.py
Normal file
19
djangocms_blog/liveblog/routing.py
Normal 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_-]+)/$'
|
||||||
|
),
|
||||||
|
]
|
24
djangocms_blog/liveblog/static/liveblog/js/liveblog.js
Normal file
24
djangocms_blog/liveblog/static/liveblog/js/liveblog.js
Normal 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);
|
||||||
|
|
1
djangocms_blog/liveblog/static/liveblog/js/reconnecting-websocket.min.js
vendored
Normal file
1
djangocms_blog/liveblog/static/liveblog/js/reconnecting-websocket.min.js
vendored
Normal 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});
|
|
@ -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>
|
|
@ -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 %}
|
21
djangocms_blog/migrations/0021_post_liveblog.py
Normal file
21
djangocms_blog/migrations/0021_post_liveblog.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
21
djangocms_blog/migrations/0022_auto_20160605_2305.py
Normal file
21
djangocms_blog/migrations/0022_auto_20160605_2305.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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(
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
178
tests/test_liveblog.py
Normal 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
|
12
tests/test_utils/routing.py
Normal file
12
tests/test_utils/routing.py
Normal 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'),
|
||||||
|
]
|
1
tox.ini
1
tox.ini
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue