Merge pull request #239 from nephila/feature/instant_articles_improved
Improve instant articles support
This commit is contained in:
commit
d9db4a795f
7 changed files with 134 additions and 27 deletions
|
@ -455,6 +455,10 @@ Global Settings
|
||||||
* BLOG_TAGS_PLUGIN_NAME: Blog tags plugin name (default: ``Tags``)
|
* BLOG_TAGS_PLUGIN_NAME: Blog tags plugin name (default: ``Tags``)
|
||||||
* BLOG_CATEGORY_PLUGIN_NAME: Blog categories plugin name (default: ``Categories``)
|
* BLOG_CATEGORY_PLUGIN_NAME: Blog categories plugin name (default: ``Categories``)
|
||||||
* BLOG_ARCHIVE_PLUGIN_NAME: Blog archive plugin name (default: ``Archive``)
|
* BLOG_ARCHIVE_PLUGIN_NAME: Blog archive plugin name (default: ``Archive``)
|
||||||
|
* BLOG_FEED_CACHE_TIMEOUT: Cache timeout for RSS feeds
|
||||||
|
* BLOG_FEED_INSTANT_ITEMS: Number of items in Instant Article feed
|
||||||
|
* BLOG_FEED_LATEST_ITEMS: Number of items in latest items feed
|
||||||
|
* BLOG_FEED_TAGS_ITEMS: Number of items in per tags feed
|
||||||
|
|
||||||
Read-only settings
|
Read-only settings
|
||||||
++++++++++++++++++
|
++++++++++++++++++
|
||||||
|
|
|
@ -3,7 +3,6 @@ from __future__ import absolute_import, print_function, unicode_literals
|
||||||
|
|
||||||
from cms.apphook_pool import apphook_pool
|
from cms.apphook_pool import apphook_pool
|
||||||
from cms.menu_bases import CMSAttachMenu
|
from cms.menu_bases import CMSAttachMenu
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
|
||||||
from django.core.urlresolvers import resolve
|
from django.core.urlresolvers import resolve
|
||||||
from django.db.models.signals import post_delete, post_save
|
from django.db.models.signals import post_delete, post_save
|
||||||
from django.utils.translation import get_language_from_request, ugettext_lazy as _
|
from django.utils.translation import get_language_from_request, ugettext_lazy as _
|
||||||
|
@ -14,6 +13,11 @@ from .cms_appconfig import BlogConfig
|
||||||
from .models import BlogCategory, Post
|
from .models import BlogCategory, Post
|
||||||
from .settings import MENU_TYPE_CATEGORIES, MENU_TYPE_COMPLETE, MENU_TYPE_POSTS, get_setting
|
from .settings import MENU_TYPE_CATEGORIES, MENU_TYPE_COMPLETE, MENU_TYPE_POSTS, get_setting
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
|
except ImportError:
|
||||||
|
from django.contrib.sites.models import get_current_site
|
||||||
|
|
||||||
|
|
||||||
class BlogCategoryMenu(CMSAttachMenu):
|
class BlogCategoryMenu(CMSAttachMenu):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -7,18 +7,31 @@ from django.contrib.syndication.views import Feed
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.feedgenerator import Rss201rev2Feed, rfc2822_date
|
from django.utils.feedgenerator import Rss201rev2Feed
|
||||||
|
from django.utils.html import strip_tags
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.six import BytesIO
|
||||||
from django.utils.translation import get_language_from_request, ugettext as _
|
from django.utils.translation import get_language_from_request, ugettext as _
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
from djangocms_blog.settings import get_setting
|
from djangocms_blog.settings import get_setting
|
||||||
from djangocms_blog.views import PostDetailView
|
from djangocms_blog.views import PostDetailView
|
||||||
|
|
||||||
from .models import Post
|
from .models import Post
|
||||||
|
|
||||||
|
try:
|
||||||
|
import HTMLParser
|
||||||
|
|
||||||
|
h = HTMLParser.HTMLParser()
|
||||||
|
except ImportError:
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
|
h = HTMLParser()
|
||||||
|
|
||||||
|
|
||||||
class LatestEntriesFeed(Feed):
|
class LatestEntriesFeed(Feed):
|
||||||
feed_type = Rss201rev2Feed
|
feed_type = Rss201rev2Feed
|
||||||
|
feed_items_number = get_setting('FEED_LATEST_ITEMS')
|
||||||
|
|
||||||
def __call__(self, request, *args, **kwargs):
|
def __call__(self, request, *args, **kwargs):
|
||||||
self.request = request
|
self.request = request
|
||||||
|
@ -29,27 +42,48 @@ class LatestEntriesFeed(Feed):
|
||||||
return reverse('%s:posts-latest' % self.namespace, current_app=self.namespace)
|
return reverse('%s:posts-latest' % self.namespace, current_app=self.namespace)
|
||||||
|
|
||||||
def title(self):
|
def title(self):
|
||||||
|
return Site.objects.get_current().name
|
||||||
|
|
||||||
|
def description(self):
|
||||||
return _('Blog articles on %(site_name)s') % {'site_name': Site.objects.get_current().name}
|
return _('Blog articles on %(site_name)s') % {'site_name': Site.objects.get_current().name}
|
||||||
|
|
||||||
def items(self, obj=None):
|
def items(self, obj=None):
|
||||||
return Post.objects.namespace(self.namespace).published().order_by('-date_published')[:10]
|
return Post.objects.namespace(
|
||||||
|
self.namespace
|
||||||
|
).published().order_by('-date_published')[:self.feed_items_number]
|
||||||
|
|
||||||
def item_title(self, item):
|
def item_title(self, item):
|
||||||
return item.safe_translation_getter('title')
|
return mark_safe(item.safe_translation_getter('title'))
|
||||||
|
|
||||||
def item_description(self, item):
|
def item_description(self, item):
|
||||||
if item.app_config.use_abstract:
|
if item.app_config.use_abstract:
|
||||||
return item.safe_translation_getter('abstract')
|
return mark_safe(item.safe_translation_getter('abstract'))
|
||||||
return item.safe_translation_getter('post_text')
|
return mark_safe(item.safe_translation_getter('post_text'))
|
||||||
|
|
||||||
|
def item_updateddate(self, item):
|
||||||
|
return item.date_modified
|
||||||
|
|
||||||
|
def item_pubdate(self, item):
|
||||||
|
return item.date_published
|
||||||
|
|
||||||
|
def item_guid(self, item):
|
||||||
|
return item.guid
|
||||||
|
|
||||||
|
def item_author_name(self, item):
|
||||||
|
return item.get_author_name()
|
||||||
|
|
||||||
|
def item_author_url(self, item):
|
||||||
|
return item.get_author_url()
|
||||||
|
|
||||||
|
|
||||||
class TagFeed(LatestEntriesFeed):
|
class TagFeed(LatestEntriesFeed):
|
||||||
|
feed_items_number = get_setting('FEED_TAGS_ITEMS')
|
||||||
|
|
||||||
def get_object(self, request, tag):
|
def get_object(self, request, tag):
|
||||||
return tag # pragma: no cover
|
return tag # pragma: no cover
|
||||||
|
|
||||||
def items(self, obj=None):
|
def items(self, obj=None):
|
||||||
return Post.objects.published().filter(tags__slug=obj)[:10]
|
return Post.objects.published().filter(tags__slug=obj)[:self.feed_items_number]
|
||||||
|
|
||||||
|
|
||||||
class FBInstantFeed(Rss201rev2Feed):
|
class FBInstantFeed(Rss201rev2Feed):
|
||||||
|
@ -61,31 +95,51 @@ class FBInstantFeed(Rss201rev2Feed):
|
||||||
}
|
}
|
||||||
|
|
||||||
def add_root_elements(self, handler):
|
def add_root_elements(self, handler):
|
||||||
handler.addQuickElement("title", self.feed['title'])
|
handler.addQuickElement('title', self.feed['title'])
|
||||||
handler.addQuickElement("link", self.feed['link'])
|
handler.addQuickElement('link', self.feed['link'])
|
||||||
handler.addQuickElement("description", self.feed['description'])
|
handler.addQuickElement('description', self.feed['description'])
|
||||||
if self.feed['language'] is not None:
|
if self.feed['language'] is not None:
|
||||||
handler.addQuickElement("language", self.feed['language'])
|
handler.addQuickElement('language', self.feed['language'])
|
||||||
for cat in self.feed['categories']:
|
for cat in self.feed['categories']:
|
||||||
handler.addQuickElement("category", cat)
|
handler.addQuickElement('category', cat)
|
||||||
if self.feed['feed_copyright'] is not None:
|
if self.feed['feed_copyright'] is not None:
|
||||||
handler.addQuickElement("copyright", self.feed['feed_copyright'])
|
handler.addQuickElement('copyright', self.feed['feed_copyright'])
|
||||||
handler.addQuickElement("lastBuildDate", rfc2822_date(self.latest_post_date()))
|
handler.addQuickElement('lastBuildDate', self.latest_post_date().isoformat())
|
||||||
if self.feed['ttl'] is not None:
|
if self.feed['ttl'] is not None:
|
||||||
handler.addQuickElement("ttl", self.feed['ttl'])
|
handler.addQuickElement('ttl', self.feed['ttl'])
|
||||||
|
|
||||||
def add_item_elements(self, handler, item):
|
def add_item_elements(self, handler, item):
|
||||||
super(FBInstantFeed, self).add_item_elements(handler, item)
|
super(FBInstantFeed, self).add_item_elements(handler, item)
|
||||||
|
if item['author']:
|
||||||
|
handler.addQuickElement('author', item['author'])
|
||||||
|
if item['date_mod'] is not None:
|
||||||
|
handler.addQuickElement('pubDate', item['date'].isoformat())
|
||||||
|
if item['date_pub'] is not None:
|
||||||
|
handler.addQuickElement('modDate', item['date'].isoformat())
|
||||||
|
handler.startElement('description', {})
|
||||||
|
handler._write('<![CDATA[{0}]]>'.format(h.unescape(force_text(item['abstract']))))
|
||||||
|
handler.endElement('description')
|
||||||
handler.startElement('content:encoded', {})
|
handler.startElement('content:encoded', {})
|
||||||
handler._write('<![CDATA[')
|
handler._write('<![CDATA[')
|
||||||
handler._write(force_text(item['content']))
|
handler._write('<!doctype html>')
|
||||||
|
handler._write(h.unescape(force_text(item['content'])))
|
||||||
handler._write(']]>')
|
handler._write(']]>')
|
||||||
handler.endElement('content:encoded')
|
handler.endElement('content:encoded')
|
||||||
handler.addQuickElement('guid', item['guid'])
|
|
||||||
|
|
||||||
|
|
||||||
class FBInstantArticles(LatestEntriesFeed):
|
class FBInstantArticles(LatestEntriesFeed):
|
||||||
feed_type = FBInstantFeed
|
feed_type = FBInstantFeed
|
||||||
|
feed_items_number = get_setting('FEED_INSTANT_ITEMS')
|
||||||
|
|
||||||
|
def _clean_html(self, content):
|
||||||
|
body = BytesIO(content)
|
||||||
|
document = etree.iterparse(body, html=True)
|
||||||
|
for a, e in document:
|
||||||
|
if not (e.text and e.text.strip()) and len(e) == 0 and e.tag == 'p':
|
||||||
|
e.getparent().remove(e)
|
||||||
|
if e.tag in ('h3', 'h4', 'h5', 'h6') and 'op-kicker' not in e.attrib.get('class', ''):
|
||||||
|
e.tag = 'h2'
|
||||||
|
return etree.tostring(document.root)
|
||||||
|
|
||||||
def item_extra_kwargs(self, item):
|
def item_extra_kwargs(self, item):
|
||||||
if not item:
|
if not item:
|
||||||
|
@ -97,10 +151,32 @@ class FBInstantArticles(LatestEntriesFeed):
|
||||||
view = PostDetailView.as_view(instant_article=True)
|
view = PostDetailView.as_view(instant_article=True)
|
||||||
response = view(self.request, slug=item.safe_translation_getter('slug'))
|
response = view(self.request, slug=item.safe_translation_getter('slug'))
|
||||||
response.render()
|
response.render()
|
||||||
content = mark_safe(response.content)
|
content = self._clean_html(response.content)
|
||||||
cache.set(key, content, timeout=get_setting('FEED_CACHE_TIMEOUT'))
|
cache.set(key, content, timeout=get_setting('FEED_CACHE_TIMEOUT'))
|
||||||
|
if item.app_config.use_abstract:
|
||||||
|
abstract = strip_tags(item.safe_translation_getter('abstract'))
|
||||||
|
else:
|
||||||
|
abstract = strip_tags(item.safe_translation_getter('post_text'))
|
||||||
return {
|
return {
|
||||||
|
'author': item.get_author_name(),
|
||||||
'content': content,
|
'content': content,
|
||||||
'slug': item.safe_translation_getter('slug'),
|
'date': item.date_modified,
|
||||||
'guid': item.guid,
|
'date_pub': item.date_modified,
|
||||||
|
'date_mod': item.date_modified,
|
||||||
|
'abstract': abstract
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def item_categories(self, item):
|
||||||
|
return [category.safe_translation_getter('name') for category in item.categories.all()]
|
||||||
|
|
||||||
|
def item_author_name(self, item):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def item_author_url(self, item):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def item_description(self, item):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def item_pubdate(self, item):
|
||||||
|
return None
|
||||||
|
|
|
@ -205,7 +205,7 @@ class Post(KnockerModel, ModelMeta, TranslatableModel):
|
||||||
def guid(self, language=None):
|
def guid(self, language=None):
|
||||||
if not language:
|
if not language:
|
||||||
language = self.get_current_language()
|
language = self.get_current_language()
|
||||||
base_string = '{0}-{1}-{2}'.format(
|
base_string = '{0}{2}{1}'.format(
|
||||||
language, self.app_config.namespace,
|
language, self.app_config.namespace,
|
||||||
self.safe_translation_getter('slug', language_code=language, any_language=True)
|
self.safe_translation_getter('slug', language_code=language, any_language=True)
|
||||||
)
|
)
|
||||||
|
|
|
@ -123,6 +123,12 @@ def get_setting(name):
|
||||||
settings, 'BLOG_ARCHIVE_PLUGIN_NAME', _('Archive')),
|
settings, 'BLOG_ARCHIVE_PLUGIN_NAME', _('Archive')),
|
||||||
'BLOG_FEED_CACHE_TIMEOUT': getattr(
|
'BLOG_FEED_CACHE_TIMEOUT': getattr(
|
||||||
settings, 'BLOG_FEED_CACHE_TIMEOUT', 3600),
|
settings, 'BLOG_FEED_CACHE_TIMEOUT', 3600),
|
||||||
|
'BLOG_FEED_INSTANT_ITEMS': getattr(
|
||||||
|
settings, 'BLOG_FEED_INSTANT_ITEMS', 50),
|
||||||
|
'BLOG_FEED_LATEST_ITEMS': getattr(
|
||||||
|
settings, 'BLOG_FEED_LATEST_ITEMS', 10),
|
||||||
|
'BLOG_FEED_TAGS_ITEMS': getattr(
|
||||||
|
settings, 'BLOG_FEED_TAGS_ITEMS', 10),
|
||||||
|
|
||||||
}
|
}
|
||||||
return default['BLOG_%s' % name]
|
return default['BLOG_%s' % name]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% load thumbnail cms_tags %}
|
{% load thumbnail cms_tags %}
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" prefix="op: http://media.facebook.com/op#">
|
<html lang="{{ post.get_current_language }}" prefix="op: http://media.facebook.com/op#">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
{% block canonical_url %}<link rel="canonical" href="{{ meta.url }}"/>{% endblock canonical_url %}
|
{% block canonical_url %}<link rel="canonical" href="{{ meta.url }}"/>{% endblock canonical_url %}
|
||||||
|
@ -16,19 +16,17 @@
|
||||||
<time class="op-modified" dateTime="{{ post.date_modified.isoformat }}">{{ post.date_modified|date:"DATE_FORMAT" }}</time>
|
<time class="op-modified" dateTime="{{ post.date_modified.isoformat }}">{{ post.date_modified|date:"DATE_FORMAT" }}</time>
|
||||||
|
|
||||||
<address>
|
<address>
|
||||||
{% if og_author_url %}<a rel="facebook" href="{{ og_author_url }}">{% endif %}
|
<a {% if og_author_url %}rel="facebook" href="{{ og_author_url }}"{% endif %}>{{ post.get_author_name }}</a>
|
||||||
{{ post.author }}
|
|
||||||
{% if og_author_url %}</a>{% endif %}
|
|
||||||
</address>
|
</address>
|
||||||
|
|
||||||
<figure>
|
<figure>
|
||||||
<img src="{{ meta.image }}" alt="{{ post.main_image.default_alt_text }}" />
|
<img src="{{ meta.image }}" alt="{{ post.main_image.default_alt_text|default:'' }}" />
|
||||||
{% if post.main_image.default_caption %}
|
{% if post.main_image.default_caption %}
|
||||||
<figcaption>{{ post.main_image.default_caption }}</figcaption>{% endif %}
|
<figcaption>{{ post.main_image.default_caption }}</figcaption>{% endif %}
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<h3 class="op-kicker">
|
<h3 class="op-kicker">
|
||||||
{{ post.abstract|striptags }}
|
{{ post.abstract|striptags|safe }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import absolute_import, print_function, unicode_literals
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from aldryn_apphooks_config.utils import get_app_instance
|
from aldryn_apphooks_config.utils import get_app_instance
|
||||||
|
from cms.api import add_plugin
|
||||||
from cms.toolbar.items import ModalItem
|
from cms.toolbar.items import ModalItem
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
@ -315,9 +316,13 @@ class ViewTest(BaseTest):
|
||||||
self.assertEqual(context['post_list'][0].title, 'First post')
|
self.assertEqual(context['post_list'][0].title, 'First post')
|
||||||
|
|
||||||
def test_feed(self):
|
def test_feed(self):
|
||||||
|
self.user.first_name = 'Admin'
|
||||||
|
self.user.last_name = 'User'
|
||||||
|
self.user.save()
|
||||||
posts = self.get_posts()
|
posts = self.get_posts()
|
||||||
pages = self.get_pages()
|
pages = self.get_pages()
|
||||||
posts[0].tags.add('tag 1', 'tag 2', 'tag 3', 'tag 4')
|
posts[0].tags.add('tag 1', 'tag 2', 'tag 3', 'tag 4')
|
||||||
|
posts[0].author = self.user
|
||||||
posts[0].save()
|
posts[0].save()
|
||||||
posts[1].tags.add('tag 6', 'tag 2', 'tag 5', 'tag 8')
|
posts[1].tags.add('tag 6', 'tag 2', 'tag 5', 'tag 8')
|
||||||
posts[1].save()
|
posts[1].save()
|
||||||
|
@ -335,6 +340,7 @@ class ViewTest(BaseTest):
|
||||||
xml = feed(request)
|
xml = feed(request)
|
||||||
self.assertContains(xml, posts[0].get_absolute_url())
|
self.assertContains(xml, posts[0].get_absolute_url())
|
||||||
self.assertContains(xml, 'Blog articles on example.com')
|
self.assertContains(xml, 'Blog articles on example.com')
|
||||||
|
self.assertContains(xml, 'Admin User</dc:creator>')
|
||||||
|
|
||||||
with smart_override('it'):
|
with smart_override('it'):
|
||||||
with switch_language(posts[0], 'it'):
|
with switch_language(posts[0], 'it'):
|
||||||
|
@ -352,8 +358,18 @@ class ViewTest(BaseTest):
|
||||||
self.assertEqual(list(feed.items('tag-2')), [posts[0]])
|
self.assertEqual(list(feed.items('tag-2')), [posts[0]])
|
||||||
|
|
||||||
def test_instant_articles(self):
|
def test_instant_articles(self):
|
||||||
|
self.user.first_name = 'Admin'
|
||||||
|
self.user.last_name = 'User'
|
||||||
|
self.user.save()
|
||||||
posts = self.get_posts()
|
posts = self.get_posts()
|
||||||
pages = self.get_pages()
|
pages = self.get_pages()
|
||||||
|
posts[0].tags.add('tag 1', 'tag 2', 'tag 3', 'tag 4')
|
||||||
|
posts[0].categories.add(self.category_1)
|
||||||
|
posts[0].author = self.user
|
||||||
|
posts[0].save()
|
||||||
|
add_plugin(
|
||||||
|
posts[0].content, 'TextPlugin', language='en', body='<h3>Ciao</h3><p></p><p>Ciao</p>'
|
||||||
|
)
|
||||||
|
|
||||||
with smart_override('en'):
|
with smart_override('en'):
|
||||||
with switch_language(posts[0], 'en'):
|
with switch_language(posts[0], 'en'):
|
||||||
|
@ -373,6 +389,9 @@ class ViewTest(BaseTest):
|
||||||
self.assertContains(xml, '<link rel="canonical" href="{0}"/>'.format(
|
self.assertContains(xml, '<link rel="canonical" href="{0}"/>'.format(
|
||||||
posts[0].get_full_url()
|
posts[0].get_full_url()
|
||||||
))
|
))
|
||||||
|
# Assert text transformation
|
||||||
|
self.assertContains(xml, '<h2>Ciao</h2><p>Ciao</p>')
|
||||||
|
self.assertContains(xml, '<a>Admin User</a>')
|
||||||
|
|
||||||
def test_sitemap(self):
|
def test_sitemap(self):
|
||||||
posts = self.get_posts()
|
posts = self.get_posts()
|
||||||
|
|
Loading…
Reference in a new issue