Merge pull request #236 from nephila/feature/instant_articles
preliminary work for FB instant articles
This commit is contained in:
commit
95efd8778c
10 changed files with 229 additions and 52 deletions
|
@ -4,16 +4,24 @@ from __future__ import absolute_import, print_function, unicode_literals
|
|||
from aldryn_apphooks_config.utils import get_app_instance
|
||||
from django.contrib.sites.models import Site
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.core.cache import cache
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.feedgenerator import Rss201rev2Feed, rfc2822_date
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import get_language_from_request, ugettext as _
|
||||
|
||||
from djangocms_blog.settings import get_setting
|
||||
from djangocms_blog.views import PostDetailView
|
||||
|
||||
from .models import Post
|
||||
from .settings import get_setting
|
||||
|
||||
|
||||
class LatestEntriesFeed(Feed):
|
||||
feed_type = Rss201rev2Feed
|
||||
|
||||
def __call__(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
self.namespace, self.config = get_app_instance(request)
|
||||
return super(LatestEntriesFeed, self).__call__(request, *args, **kwargs)
|
||||
|
||||
|
@ -30,7 +38,7 @@ class LatestEntriesFeed(Feed):
|
|||
return item.safe_translation_getter('title')
|
||||
|
||||
def item_description(self, item):
|
||||
if get_setting('USE_ABSTRACT'):
|
||||
if item.app_config.use_abstract:
|
||||
return item.safe_translation_getter('abstract')
|
||||
return item.safe_translation_getter('post_text')
|
||||
|
||||
|
@ -42,3 +50,57 @@ class TagFeed(LatestEntriesFeed):
|
|||
|
||||
def items(self, obj=None):
|
||||
return Post.objects.published().filter(tags__slug=obj)[:10]
|
||||
|
||||
|
||||
class FBInstantFeed(Rss201rev2Feed):
|
||||
|
||||
def rss_attributes(self):
|
||||
return {
|
||||
'version': self._version,
|
||||
'xmlns:content': 'http://purl.org/rss/1.0/modules/content/'
|
||||
}
|
||||
|
||||
def add_root_elements(self, handler):
|
||||
handler.addQuickElement("title", self.feed['title'])
|
||||
handler.addQuickElement("link", self.feed['link'])
|
||||
handler.addQuickElement("description", self.feed['description'])
|
||||
if self.feed['language'] is not None:
|
||||
handler.addQuickElement("language", self.feed['language'])
|
||||
for cat in self.feed['categories']:
|
||||
handler.addQuickElement("category", cat)
|
||||
if self.feed['feed_copyright'] is not None:
|
||||
handler.addQuickElement("copyright", self.feed['feed_copyright'])
|
||||
handler.addQuickElement("lastBuildDate", rfc2822_date(self.latest_post_date()))
|
||||
if self.feed['ttl'] is not None:
|
||||
handler.addQuickElement("ttl", self.feed['ttl'])
|
||||
|
||||
def add_item_elements(self, handler, item):
|
||||
super(FBInstantFeed, self).add_item_elements(handler, item)
|
||||
handler.startElement('content:encoded', {})
|
||||
handler._write('<![CDATA[')
|
||||
handler._write(force_text(item['content']))
|
||||
handler._write(']]>')
|
||||
handler.endElement('content:encoded')
|
||||
handler.addQuickElement('guid', item['guid'])
|
||||
|
||||
|
||||
class FBInstantArticles(LatestEntriesFeed):
|
||||
feed_type = FBInstantFeed
|
||||
|
||||
def item_extra_kwargs(self, item):
|
||||
if not item:
|
||||
return {}
|
||||
language = get_language_from_request(self.request, check_path=True)
|
||||
key = item.get_cache_key(language, 'feed')
|
||||
content = cache.get(key)
|
||||
if not content:
|
||||
view = PostDetailView.as_view(instant_article=True)
|
||||
response = view(self.request, slug=item.safe_translation_getter('slug'))
|
||||
response.render()
|
||||
content = mark_safe(response.content)
|
||||
cache.set(key, content, timeout=get_setting('FEED_CACHE_TIMEOUT'))
|
||||
return {
|
||||
'content': content,
|
||||
'slug': item.safe_translation_getter('slug'),
|
||||
'guid': item.guid,
|
||||
}
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, print_function, unicode_literals
|
||||
|
||||
import hashlib
|
||||
|
||||
from aldryn_apphooks_config.fields import AppHookConfigField
|
||||
from aldryn_apphooks_config.managers.parler import AppHookConfigTranslatableManager
|
||||
from cms.models import CMSPlugin, PlaceholderField
|
||||
from django.conf import settings as dj_settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.cache import cache
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
from django.utils.encoding import force_bytes, force_text, python_2_unicode_compatible
|
||||
from django.utils.html import escape, strip_tags
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
|
@ -196,6 +201,16 @@ class Post(KnockerModel, ModelMeta, TranslatableModel):
|
|||
def __str__(self):
|
||||
return self.safe_translation_getter('title')
|
||||
|
||||
@property
|
||||
def guid(self, language=None):
|
||||
if not language:
|
||||
language = self.get_current_language()
|
||||
base_string = '{0}-{1}-{2}'.format(
|
||||
language, self.app_config.namespace,
|
||||
self.safe_translation_getter('slug', language_code=language, any_language=True)
|
||||
)
|
||||
return hashlib.sha256(force_bytes(base_string)).hexdigest()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Handle some auto configuration during save
|
||||
|
@ -329,6 +344,9 @@ class Post(KnockerModel, ModelMeta, TranslatableModel):
|
|||
updated = self.app_config.send_knock_update and self.is_published
|
||||
return new or updated
|
||||
|
||||
def get_cache_key(self, language, prefix):
|
||||
return 'djangocms-blog:{2}:{0}:{1}'.format(language, self.guid, prefix)
|
||||
|
||||
|
||||
class BasePostPlugin(CMSPlugin):
|
||||
app_config = AppHookConfigField(
|
||||
|
@ -422,3 +440,17 @@ class GenericBlogPlugin(BasePostPlugin):
|
|||
|
||||
def __str__(self):
|
||||
return force_text(_('generic blog plugin'))
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Post)
|
||||
def pre_delete_post(sender, instance, **kwargs):
|
||||
for language in instance.get_available_languages():
|
||||
key = instance.get_cache_key(language, 'feed')
|
||||
cache.delete(key)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Post)
|
||||
def post_save_post(sender, instance, **kwargs):
|
||||
for language in instance.get_available_languages():
|
||||
key = instance.get_cache_key(language, 'feed')
|
||||
cache.delete(key)
|
||||
|
|
|
@ -121,6 +121,8 @@ def get_setting(name):
|
|||
settings, 'BLOG_CATEGORY_PLUGIN_NAME', _('Categories')),
|
||||
'BLOG_ARCHIVE_PLUGIN_NAME': getattr(
|
||||
settings, 'BLOG_ARCHIVE_PLUGIN_NAME', _('Archive')),
|
||||
'BLOG_FEED_CACHE_TIMEOUT': getattr(
|
||||
settings, 'BLOG_FEED_CACHE_TIMEOUT', 3600),
|
||||
|
||||
}
|
||||
return default['BLOG_%s' % name]
|
||||
|
|
|
@ -23,4 +23,4 @@
|
|||
<li class="tag_{{ forloop.counter }}"><a href="{% url 'djangocms_blog:posts-tagged' tag=tag.slug %}" class="blog-tag blog-tag-{{ tag.count }}">{{ tag.name }}</a>{% if not forloop.last %}, {% endif %}</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</ul>
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
{% load thumbnail cms_tags %}
|
||||
<!doctype html>
|
||||
<html lang="en" prefix="op: http://media.facebook.com/op#">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
{% block canonical_url %}<link rel="canonical" href="{{ meta.url }}"/>{% endblock canonical_url %}
|
||||
<meta property="op:markup_version" content="v1.0">
|
||||
</head>
|
||||
<body>
|
||||
<article>
|
||||
<header>
|
||||
<h1>{{ post.title }}</h1>
|
||||
|
||||
<time class="op-published" datetime="{{ post.date_published.isoformat }}">{{ post.date_published|date:"DATE_FORMAT" }}</time>
|
||||
|
||||
<time class="op-modified" dateTime="{{ post.date_modified.isoformat }}">{{ post.date_modified|date:"DATE_FORMAT" }}</time>
|
||||
|
||||
<address>
|
||||
{% if og_author_url %}<a rel="facebook" href="{{ og_author_url }}">{% endif %}
|
||||
{{ post.author }}
|
||||
{% if og_author_url %}</a>{% endif %}
|
||||
</address>
|
||||
|
||||
<figure>
|
||||
<img src="{{ meta.image }}" alt="{{ post.main_image.default_alt_text }}" />
|
||||
{% if post.main_image.default_caption %}
|
||||
<figcaption>{{ post.main_image.default_caption }}</figcaption>{% endif %}
|
||||
</figure>
|
||||
|
||||
<h3 class="op-kicker">
|
||||
{{ post.abstract|striptags }}
|
||||
</h3>
|
||||
|
||||
</header>
|
||||
|
||||
{% if post.app_config.use_placeholder %}
|
||||
<div class="blog-content">{% render_placeholder post.content %}</div>
|
||||
{% else %}
|
||||
<div class="blog-content">{% render_model post "post_text" "post_text" %}</div>
|
||||
{% endif %}
|
||||
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
|
@ -3,7 +3,7 @@ from __future__ import absolute_import, print_function, unicode_literals
|
|||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .feeds import LatestEntriesFeed, TagFeed
|
||||
from .feeds import FBInstantArticles, LatestEntriesFeed, TagFeed
|
||||
from .settings import get_setting
|
||||
from .views import (
|
||||
AuthorEntriesView, CategoryEntriesView, PostArchiveView, PostDetailView, PostListView,
|
||||
|
@ -27,6 +27,8 @@ urlpatterns = [
|
|||
PostListView.as_view(), name='posts-latest'),
|
||||
url(r'^feed/$',
|
||||
LatestEntriesFeed(), name='posts-latest-feed'),
|
||||
url(r'^feed/fb/$',
|
||||
FBInstantArticles(), name='posts-latest-feed-fb'),
|
||||
url(r'^(?P<year>\d{4})/$',
|
||||
PostArchiveView.as_view(), name='posts-archive'),
|
||||
url(r'^(?P<year>\d{4})/(?P<month>\d{1,2})/$',
|
||||
|
|
|
@ -72,6 +72,14 @@ class PostDetailView(TranslatableSlugMixin, BaseBlogView, DetailView):
|
|||
base_template_name = 'post_detail.html'
|
||||
slug_field = 'slug'
|
||||
view_url_name = 'djangocms_blog:post-detail'
|
||||
instant_article = False
|
||||
|
||||
def get_template_names(self):
|
||||
if self.instant_article:
|
||||
template_path = (self.config and self.config.template_prefix) or 'djangocms_blog'
|
||||
return os.path.join(template_path, 'post_instant_article.html')
|
||||
else:
|
||||
return super(PostDetailView, self).get_template_names()
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.model._default_manager.all()
|
||||
|
@ -88,6 +96,7 @@ class PostDetailView(TranslatableSlugMixin, BaseBlogView, DetailView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super(PostDetailView, self).get_context_data(**kwargs)
|
||||
context['meta'] = self.get_object().as_meta()
|
||||
context['instant_article'] = self.instant_article
|
||||
context['use_placeholder'] = get_setting('USE_PLACEHOLDER')
|
||||
setattr(self.request, get_setting('CURRENT_POST_IDENTIFIER'), self.get_object())
|
||||
return context
|
||||
|
|
|
@ -14,7 +14,6 @@ from parler.utils.context import smart_override
|
|||
from djangocms_blog.cms_appconfig import BlogConfig
|
||||
from djangocms_blog.models import BlogCategory, Post
|
||||
|
||||
|
||||
try:
|
||||
from filer.models import ThumbnailOption # NOQA
|
||||
except ImportError:
|
||||
|
|
|
@ -77,50 +77,6 @@ class PluginTest(BaseTest):
|
|||
self.assertEqual(casted_categories.tags.count(), 0)
|
||||
self.assertEqual(casted_categories.categories.count(), 1)
|
||||
|
||||
def test_plugin_authors(self):
|
||||
pages = self.get_pages()
|
||||
posts = self.get_posts()
|
||||
posts[0].publish = True
|
||||
posts[0].save()
|
||||
posts[1].publish = True
|
||||
posts[1].save()
|
||||
ph = pages[0].placeholders.get(slot='content')
|
||||
plugin = add_plugin(ph, 'BlogAuthorPostsPlugin', language='en', app_config=self.app_config_1)
|
||||
|
||||
context = self.get_plugin_context(pages[0], 'en', plugin, edit=True)
|
||||
rendered = plugin.render_plugin(context, ph)
|
||||
self.assertTrue(rendered.find('No article found') > -1)
|
||||
|
||||
plugin.authors.add(self.user)
|
||||
context = self.get_plugin_context(pages[0], 'en', plugin, edit=True)
|
||||
rendered = plugin.render_plugin(context, ph)
|
||||
self.assertTrue(rendered.find('/en/blog/author/admin/') > -1)
|
||||
self.assertTrue(rendered.find('2 articles') > -1)
|
||||
|
||||
plugin.authors.add(self.user_staff)
|
||||
context = self.get_plugin_context(pages[0], 'en', plugin, edit=True)
|
||||
rendered = plugin.render_plugin(context, ph)
|
||||
self.assertTrue(rendered.find('/en/blog/author/staff/') > -1)
|
||||
self.assertTrue(rendered.find('0 articles') > -1)
|
||||
|
||||
plugin.authors.add(self.user_normal)
|
||||
context = self.get_plugin_context(pages[0], 'en', plugin, edit=True)
|
||||
rendered = plugin.render_plugin(context, ph)
|
||||
self.assertTrue(rendered.find('/en/blog/author/normal/') > -1)
|
||||
self.assertTrue(rendered.find('0 articles') > -1)
|
||||
|
||||
# Checking copy relations
|
||||
ph = pages[0].placeholders.get(slot='content')
|
||||
original = ph.get_plugins('en')
|
||||
pages[0].publish('en')
|
||||
published = pages[0].get_public_object()
|
||||
ph = published.placeholders.get(slot='content')
|
||||
new = ph.get_plugins('en')
|
||||
self.assertNotEqual(original, new)
|
||||
|
||||
casted_authors, __ = new[0].get_plugin_instance()
|
||||
self.assertEqual(casted_authors.authors.count(), 3)
|
||||
|
||||
def test_plugin_tags(self):
|
||||
pages = self.get_pages()
|
||||
posts = self.get_posts()
|
||||
|
@ -196,3 +152,50 @@ class PluginTest(BaseTest):
|
|||
self.assertEqual(plugin_class.get_render_template(context, plugin, ph), os.path.join('whatever', plugin_class.base_render_template))
|
||||
self.app_config_1.app_data.config.template_prefix = ''
|
||||
self.app_config_1.save()
|
||||
|
||||
|
||||
class PluginTest2(BaseTest):
|
||||
|
||||
def test_plugin_authors(self):
|
||||
pages = self.get_pages()
|
||||
posts = self.get_posts()
|
||||
posts[0].publish = True
|
||||
posts[0].save()
|
||||
posts[1].publish = True
|
||||
posts[1].save()
|
||||
ph = pages[0].placeholders.get(slot='content')
|
||||
plugin = add_plugin(ph, 'BlogAuthorPostsPlugin', language='en', app_config=self.app_config_1)
|
||||
|
||||
context = self.get_plugin_context(pages[0], 'en', plugin, edit=True)
|
||||
rendered = plugin.render_plugin(context, ph)
|
||||
self.assertTrue(rendered.find('No article found') > -1)
|
||||
|
||||
plugin.authors.add(self.user)
|
||||
context = self.get_plugin_context(pages[0], 'en', plugin, edit=True)
|
||||
rendered = plugin.render_plugin(context, ph)
|
||||
self.assertTrue(rendered.find('/en/blog/author/admin/') > -1)
|
||||
self.assertTrue(rendered.find('2 articles') > -1)
|
||||
|
||||
plugin.authors.add(self.user_staff)
|
||||
context = self.get_plugin_context(pages[0], 'en', plugin, edit=True)
|
||||
rendered = plugin.render_plugin(context, ph)
|
||||
self.assertTrue(rendered.find('/en/blog/author/staff/') > -1)
|
||||
self.assertTrue(rendered.find('0 articles') > -1)
|
||||
|
||||
plugin.authors.add(self.user_normal)
|
||||
context = self.get_plugin_context(pages[0], 'en', plugin, edit=True)
|
||||
rendered = plugin.render_plugin(context, ph)
|
||||
self.assertTrue(rendered.find('/en/blog/author/normal/') > -1)
|
||||
self.assertTrue(rendered.find('0 articles') > -1)
|
||||
|
||||
# Checking copy relations
|
||||
ph = pages[0].placeholders.get(slot='content')
|
||||
original = ph.get_plugins('en')
|
||||
pages[0].publish('en')
|
||||
published = pages[0].get_public_object()
|
||||
ph = published.placeholders.get(slot='content')
|
||||
new = ph.get_plugins('en')
|
||||
self.assertNotEqual(original, new)
|
||||
|
||||
casted_authors, __ = new[0].get_plugin_instance()
|
||||
self.assertEqual(casted_authors.authors.count(), 3)
|
||||
|
|
|
@ -9,13 +9,14 @@ from django.contrib.auth.models import AnonymousUser
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from parler.tests.utils import override_parler_settings
|
||||
from parler.utils.conf import add_default_language_settings
|
||||
from parler.utils.context import smart_override, switch_language
|
||||
|
||||
from djangocms_blog.feeds import LatestEntriesFeed, TagFeed
|
||||
from djangocms_blog.feeds import FBInstantArticles, LatestEntriesFeed, TagFeed
|
||||
from djangocms_blog.models import BLOG_CURRENT_NAMESPACE
|
||||
from djangocms_blog.settings import get_setting
|
||||
from djangocms_blog.sitemaps import BlogSitemap
|
||||
|
@ -350,6 +351,29 @@ class ViewTest(BaseTest):
|
|||
feed.config = self.app_config_1
|
||||
self.assertEqual(list(feed.items('tag-2')), [posts[0]])
|
||||
|
||||
def test_instant_articles(self):
|
||||
posts = self.get_posts()
|
||||
pages = self.get_pages()
|
||||
|
||||
with smart_override('en'):
|
||||
with switch_language(posts[0], 'en'):
|
||||
request = self.get_page_request(
|
||||
pages[1], self.user, path=posts[0].get_absolute_url()
|
||||
)
|
||||
|
||||
feed = FBInstantArticles()
|
||||
feed.namespace, feed.config = get_app_instance(request)
|
||||
self.assertEqual(list(feed.items()), [posts[0]])
|
||||
xml = feed(request)
|
||||
self.assertContains(xml, '<guid>{0}</guid>'.format(posts[0].guid))
|
||||
self.assertContains(xml, 'content:encoded')
|
||||
self.assertContains(xml, 'class="op-published" datetime="{0}"'.format(
|
||||
posts[0].date_published.isoformat()
|
||||
))
|
||||
self.assertContains(xml, '<link rel="canonical" href="{0}"/>'.format(
|
||||
posts[0].get_full_url()
|
||||
))
|
||||
|
||||
def test_sitemap(self):
|
||||
posts = self.get_posts()
|
||||
self.get_pages()
|
||||
|
|
Loading…
Add table
Reference in a new issue