commit
4bd6e4fb3a
26 changed files with 569 additions and 6 deletions
|
@ -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:
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
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'),)}
|
||||
)
|
||||
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(
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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
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: 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
|
||||
|
|
Loading…
Reference in a new issue