Merge pull request #410 from nephila/feature/improve_meta

Improved meta fields configuration
This commit is contained in:
Iacopo Spalletti 2018-01-09 10:56:28 +01:00 committed by GitHub
commit 8c1660d84a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 209 additions and 44 deletions

View file

@ -22,7 +22,9 @@ History
* Improved admin filtering. * Improved admin filtering.
* Added featured date to post. * Added featured date to post.
* Fixed issue with urls in sitemap if apphook is not published * Fixed issue with urls in sitemap if apphook is not published
* Use the easy_thumbnails_tags template tag. Require easy_thumbnails >= 2.4.1 * Moved template to easy_thumbnails_tags template tag. Require easy_thumbnails >= 2.4.1
* Made HTML description and title fields length configurable
* Added meta representation for CategoryEntriesView
******************* *******************

View file

@ -6,7 +6,6 @@ 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 cms.models import CMSPlugin, ValidationError from cms.models import CMSPlugin, ValidationError
from django import forms
from django.apps import apps 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
@ -262,16 +261,6 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
except KeyError: except KeyError:
return HttpResponseRedirect(reverse('djangocms_blog:posts-latest')) return HttpResponseRedirect(reverse('djangocms_blog:posts-latest'))
def formfield_for_dbfield(self, db_field, **kwargs):
field = super(PostAdmin, self).formfield_for_dbfield(db_field, **kwargs)
if db_field.name == 'meta_description':
original_attrs = field.widget.attrs
original_attrs['maxlength'] = 160
field.widget = forms.TextInput(original_attrs)
elif db_field.name == 'meta_title':
field.max_length = 70
return field
def has_restricted_sites(self, request): def has_restricted_sites(self, request):
""" """
Whether the current user has permission on one site only Whether the current user has permission on one site only

View file

@ -3,15 +3,28 @@ from __future__ import absolute_import, print_function, unicode_literals
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.validators import MaxLengthValidator
from parler.forms import TranslatableModelForm from parler.forms import TranslatableModelForm
from taggit_autosuggest.widgets import TagAutoSuggest from taggit_autosuggest.widgets import TagAutoSuggest
from djangocms_blog.settings import get_setting
from .models import BlogCategory, BlogConfig, Post from .models import BlogCategory, BlogConfig, Post
class CategoryAdminForm(TranslatableModelForm): class CategoryAdminForm(TranslatableModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.base_fields['meta_description'].validators = [
MaxLengthValidator(get_setting('META_DESCRIPTION_LENGTH'))
]
original_attrs = self.base_fields['meta_description'].widget.attrs
if 'cols' in original_attrs:
del original_attrs['cols']
if 'rows' in original_attrs:
del original_attrs['rows']
original_attrs['maxlength'] = get_setting('META_DESCRIPTION_LENGTH')
self.base_fields['meta_description'].widget = forms.TextInput(original_attrs)
super(CategoryAdminForm, self).__init__(*args, **kwargs) super(CategoryAdminForm, self).__init__(*args, **kwargs)
if 'parent' in self.fields: if 'parent' in self.fields:
@ -52,6 +65,19 @@ class PostAdminForm(TranslatableModelForm):
fields = '__all__' fields = '__all__'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.base_fields['meta_description'].validators = [
MaxLengthValidator(get_setting('META_DESCRIPTION_LENGTH'))
]
original_attrs = self.base_fields['meta_description'].widget.attrs
if 'cols' in original_attrs:
del original_attrs['cols']
if 'rows' in original_attrs:
del original_attrs['rows']
original_attrs['maxlength'] = get_setting('META_DESCRIPTION_LENGTH')
self.base_fields['meta_description'].widget = forms.TextInput(original_attrs)
self.base_fields['meta_title'].validators = [
MaxLengthValidator(get_setting('META_TITLE_LENGTH'))
]
super(PostAdminForm, self).__init__(*args, **kwargs) super(PostAdminForm, self).__init__(*args, **kwargs)
qs = BlogCategory.objects qs = BlogCategory.objects

View file

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2018-01-08 23:23
from __future__ import unicode_literals
from django.db import migrations, models
import djangocms_blog.fields
class Migration(migrations.Migration):
dependencies = [
('djangocms_blog', '0031_auto_20170610_1744'),
]
operations = [
migrations.AddField(
model_name='blogcategorytranslation',
name='meta_description',
field=models.TextField(blank=True, default='', verbose_name='post meta description'),
),
migrations.AlterField(
model_name='blogcategorytranslation',
name='name',
field=models.CharField(max_length=2000, verbose_name='name'),
),
migrations.AlterField(
model_name='blogcategorytranslation',
name='slug',
field=models.SlugField(blank=True, max_length=2000, verbose_name='slug'),
),
migrations.AlterField(
model_name='posttranslation',
name='meta_title',
field=models.CharField(blank=True, default='', help_text='used in title tag and social sharing', max_length=2000, verbose_name='post meta title'),
),
migrations.AlterField(
model_name='posttranslation',
name='slug',
field=djangocms_blog.fields.AutoSlugField(allow_unicode=True, blank=True, db_index=False, max_length=2000, verbose_name='slug'),
),
migrations.AlterField(
model_name='posttranslation',
name='title',
field=models.CharField(max_length=2000, verbose_name='title'),
),
]

View file

@ -58,8 +58,27 @@ except ImportError:
pass pass
class BlogMetaMixin(ModelMeta):
def get_meta_attribute(self, param):
"""
Retrieves django-meta attributes from apphook config instance
:param param: django-meta attribute passed as key
"""
return self._get_meta_value(param, getattr(self.app_config, param)) or ''
def get_locale(self):
return self.get_current_language()
def get_full_url(self):
"""
Return the url with protocol and domain url
"""
return self.build_absolute_uri(self.get_absolute_url())
@python_2_unicode_compatible @python_2_unicode_compatible
class BlogCategory(TranslatableModel): class BlogCategory(BlogMetaMixin, TranslatableModel):
""" """
Blog category Blog category
""" """
@ -73,13 +92,38 @@ class BlogCategory(TranslatableModel):
) )
translations = TranslatedFields( translations = TranslatedFields(
name=models.CharField(_('name'), max_length=255), name=models.CharField(_('name'), max_length=2000),
slug=models.SlugField(_('slug'), max_length=255, blank=True, db_index=True), slug=models.SlugField(_('slug'), max_length=2000, blank=True, db_index=True),
meta_description=models.TextField(
verbose_name=_('category meta description'), blank=True, default=''
),
meta={'unique_together': (('language_code', 'slug'),)} meta={'unique_together': (('language_code', 'slug'),)}
) )
objects = AppHookConfigTranslatableManager() objects = AppHookConfigTranslatableManager()
_metadata = {
'title': 'get_title',
'description': 'get_description',
'og_description': 'get_description',
'twitter_description': 'get_description',
'gplus_description': 'get_description',
'locale': 'get_locale',
'object_type': 'get_meta_attribute',
'og_type': 'get_meta_attribute',
'og_app_id': 'get_meta_attribute',
'og_profile_id': 'get_meta_attribute',
'og_publisher': 'get_meta_attribute',
'og_author_url': 'get_meta_attribute',
'og_author': 'get_meta_attribute',
'twitter_type': 'get_meta_attribute',
'twitter_site': 'get_meta_attribute',
'twitter_author': 'get_meta_attribute',
'gplus_type': 'get_meta_attribute',
'gplus_author': 'get_meta_attribute',
'url': 'get_absolute_url',
}
class Meta: class Meta:
verbose_name = _('blog category') verbose_name = _('blog category')
verbose_name_plural = _('blog categories') verbose_name_plural = _('blog categories')
@ -105,9 +149,11 @@ class BlogCategory(TranslatableModel):
return self.linked_posts.published(current_site=False).count() return self.linked_posts.published(current_site=False).count()
def get_absolute_url(self, lang=None): def get_absolute_url(self, lang=None):
if not lang: if not lang or lang not in self.get_available_languages():
lang = get_language() lang = get_language()
if self.has_translation(lang, ): if not lang or lang not in self.get_available_languages():
lang = self.get_current_language()
if self.has_translation(lang):
slug = self.safe_translation_getter('slug', language_code=lang) slug = self.safe_translation_getter('slug', language_code=lang)
return reverse( return reverse(
'%s:posts-category' % self.app_config.namespace, '%s:posts-category' % self.app_config.namespace,
@ -131,9 +177,17 @@ class BlogCategory(TranslatableModel):
self.slug = slugify(force_text(self.name)) self.slug = slugify(force_text(self.name))
self.save_translations() self.save_translations()
def get_title(self):
title = self.safe_translation_getter('name', any_language=True)
return title.strip()
def get_description(self):
description = self.safe_translation_getter('meta_description', any_language=True)
return escape(strip_tags(description)).strip()
@python_2_unicode_compatible @python_2_unicode_compatible
class Post(KnockerModel, ModelMeta, TranslatableModel): class Post(KnockerModel, BlogMetaMixin, TranslatableModel):
""" """
Blog post Blog post
""" """
@ -173,8 +227,8 @@ class Post(KnockerModel, ModelMeta, TranslatableModel):
) )
translations = TranslatedFields( translations = TranslatedFields(
title=models.CharField(_('title'), max_length=255), title=models.CharField(_('title'), max_length=2000),
slug=AutoSlugField(_('slug'), max_length=255, blank=True, slug=AutoSlugField(_('slug'), max_length=2000, blank=True,
db_index=True, allow_unicode=True), db_index=True, allow_unicode=True),
abstract=HTMLField(_('abstract'), blank=True, default=''), abstract=HTMLField(_('abstract'), blank=True, default=''),
meta_description=models.TextField(verbose_name=_('post meta description'), meta_description=models.TextField(verbose_name=_('post meta description'),
@ -183,7 +237,7 @@ class Post(KnockerModel, ModelMeta, TranslatableModel):
blank=True, default=''), blank=True, default=''),
meta_title=models.CharField(verbose_name=_('post meta title'), meta_title=models.CharField(verbose_name=_('post meta title'),
help_text=_('used in title tag and social sharing'), help_text=_('used in title tag and social sharing'),
max_length=255, max_length=2000,
blank=True, default=''), blank=True, default=''),
post_text=HTMLField(_('text'), default='', blank=True), post_text=HTMLField(_('text'), default='', blank=True),
meta={'unique_together': (('language_code', 'slug'),)} meta={'unique_together': (('language_code', 'slug'),)}
@ -300,13 +354,6 @@ class Post(KnockerModel, ModelMeta, TranslatableModel):
kwargs['category'] = category.safe_translation_getter('slug', language_code=lang, any_language=True) # NOQA kwargs['category'] = category.safe_translation_getter('slug', language_code=lang, any_language=True) # NOQA
return reverse('%s:post-detail' % self.app_config.namespace, kwargs=kwargs) return reverse('%s:post-detail' % self.app_config.namespace, kwargs=kwargs)
def get_meta_attribute(self, param):
"""
Retrieves django-meta attributes from apphook config instance
:param param: django-meta attribute passed as key
"""
return self._get_meta_value(param, getattr(self.app_config, param)) or ''
def get_title(self): def get_title(self):
title = self.safe_translation_getter('meta_title', any_language=True) title = self.safe_translation_getter('meta_title', any_language=True)
if not title: if not title:
@ -320,9 +367,6 @@ class Post(KnockerModel, ModelMeta, TranslatableModel):
""" """
return self.safe_translation_getter('meta_keywords', default='').strip().split(',') return self.safe_translation_getter('meta_keywords', default='').strip().split(',')
def get_locale(self):
return self.get_current_language()
def get_description(self): def get_description(self):
description = self.safe_translation_getter('meta_description', any_language=True) description = self.safe_translation_getter('meta_description', any_language=True)
if not description: if not description:
@ -367,12 +411,6 @@ class Post(KnockerModel, ModelMeta, TranslatableModel):
else: else:
return get_setting('IMAGE_FULL_SIZE') return get_setting('IMAGE_FULL_SIZE')
def get_full_url(self):
"""
Return the url with protocol and domain url
"""
return self.build_absolute_uri(self.get_absolute_url())
@property @property
def is_published(self): def is_published(self):
""" """

View file

@ -58,6 +58,12 @@ def get_setting(name):
'BLOG_POSTS_LIST_TRUNCWORDS_COUNT': getattr( 'BLOG_POSTS_LIST_TRUNCWORDS_COUNT': getattr(
settings, 'BLOG_POSTS_LIST_TRUNCWORDS_COUNT', 100 settings, 'BLOG_POSTS_LIST_TRUNCWORDS_COUNT', 100
), ),
'BLOG_META_DESCRIPTION_LENGTH': getattr(
settings, 'BLOG_META_DESCRIPTION_LENGTH', 320
),
'BLOG_META_TITLE_LENGTH': getattr(
settings, 'BLOG_META_TITLE_LENGTH', 70
),
'BLOG_MENU_TYPES': MENU_TYPES, 'BLOG_MENU_TYPES': MENU_TYPES,
'BLOG_MENU_EMPTY_CATEGORIES': getattr(settings, 'MENU_EMPTY_CATEGORIES', True), 'BLOG_MENU_EMPTY_CATEGORIES': getattr(settings, 'MENU_EMPTY_CATEGORIES', True),
'BLOG_TYPE': getattr(settings, 'BLOG_TYPE', 'Article'), 'BLOG_TYPE': getattr(settings, 'BLOG_TYPE', 'Article'),

View file

@ -201,4 +201,5 @@ class CategoryEntriesView(BaseBlogListView, ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['category'] = self.category kwargs['category'] = self.category
context = super(CategoryEntriesView, self).get_context_data(**kwargs) context = super(CategoryEntriesView, self).get_context_data(**kwargs)
context['meta'] = self.category.as_meta()
return context return context

View file

@ -92,6 +92,8 @@ Global Settings
* BLOG_FEED_LATEST_ITEMS: Number of items in latest items feed * BLOG_FEED_LATEST_ITEMS: Number of items in latest items feed
* BLOG_FEED_TAGS_ITEMS: Number of items in per tags feed * BLOG_FEED_TAGS_ITEMS: Number of items in per tags feed
* BLOG_PLUGIN_TEMPLATE_FOLDERS: (Sub-)folder from which the plugin templates are loaded. The default folder is ``plugins``. It goes into the ``djangocms_blog`` template folder (or, if set, the folder named in the app hook). This allows, e.g., different templates for showing a post list as tables, columns, ... . New templates have the same names as the standard templates in the ``plugins`` folder (``latest_entries.html``, ``authors.html``, ``tags.html``, ``categories.html``, ``archive.html``). Default behavior corresponds to this setting being ``( ("plugins", _("Default template") )``. To add new templates add to this setting, e.g., ``('timeline', _('Vertical timeline') )``. * BLOG_PLUGIN_TEMPLATE_FOLDERS: (Sub-)folder from which the plugin templates are loaded. The default folder is ``plugins``. It goes into the ``djangocms_blog`` template folder (or, if set, the folder named in the app hook). This allows, e.g., different templates for showing a post list as tables, columns, ... . New templates have the same names as the standard templates in the ``plugins`` folder (``latest_entries.html``, ``authors.html``, ``tags.html``, ``categories.html``, ``archive.html``). Default behavior corresponds to this setting being ``( ("plugins", _("Default template") )``. To add new templates add to this setting, e.g., ``('timeline', _('Vertical timeline') )``.
* BLOG_META_DESCRIPTION_LENGTH: Maximum length for the Meta description field (default: ``320``)
* BLOG_META_TITLE_LENGTH: Maximum length for the Meta title field (default: ``70``)
****************** ******************

View file

@ -15,6 +15,7 @@ from django.contrib.auth.models import AnonymousUser
from django.contrib.messages.middleware import MessageMiddleware from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import override_settings
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.utils.timezone import now from django.utils.timezone import now
@ -25,6 +26,7 @@ from parler.utils.context import smart_override
from taggit.models import Tag from taggit.models import Tag
from djangocms_blog.cms_appconfig import BlogConfig, BlogConfigForm from djangocms_blog.cms_appconfig import BlogConfig, BlogConfigForm
from djangocms_blog.forms import CategoryAdminForm, PostAdminForm
from djangocms_blog.models import BlogCategory, Post from djangocms_blog.models import BlogCategory, Post
from djangocms_blog.settings import MENU_TYPE_NONE, get_setting from djangocms_blog.settings import MENU_TYPE_NONE, get_setting
@ -80,12 +82,13 @@ class AdminTest(BaseTest):
# Add view only contains the apphook selection widget # Add view only contains the apphook selection widget
response = post_admin.add_view(request) response = post_admin.add_view(request)
self.assertNotContains(response, '<input id="id_slug" maxlength="255" name="slug" type="text"') self.assertNotContains(response, '<input id="id_slug" maxlength="2000" name="slug" type="text"')
self.assertContains(response, '<option value="%s">Blog / sample_app</option>' % self.app_config_1.pk) self.assertContains(response, '<option value="%s">Blog / sample_app</option>' % self.app_config_1.pk)
# Changeview is 'normal' # Changeview is 'normal'
response = post_admin.change_view(request, str(post.pk)) response = post_admin.change_view(request, str(post.pk))
self.assertContains(response, '<input id="id_slug" maxlength="255" name="slug" type="text" value="first-post" />') self.assertContains(response, '<input id="id_slug" maxlength="2000" name="slug" type="text" value="first-post" />')
self.assertContains(response, 'id="id_meta_description" maxlength="320"')
self.assertContains(response, '<option value="%s" selected="selected">Blog / sample_app</option>' % self.app_config_1.pk) self.assertContains(response, '<option value="%s" selected="selected">Blog / sample_app</option>' % self.app_config_1.pk)
# Test for publish view # Test for publish view
@ -176,19 +179,55 @@ class AdminTest(BaseTest):
self.assertContains(response, 'sample_app') self.assertContains(response, 'sample_app')
def test_admin_category_views(self): def test_admin_category_views(self):
post_admin = admin.site._registry[BlogCategory] category_admin = admin.site._registry[BlogCategory]
request = self.get_page_request('/', self.user, r'/en/blog/', edit=False) request = self.get_page_request('/', self.user, r'/en/blog/', edit=False)
# Add view only has an empty form - no type # Add view only has an empty form - no type
response = post_admin.add_view(request) response = category_admin.add_view(request)
self.assertNotContains(response, 'id="id_name" maxlength="255" name="name" type="text" value="category 1"') self.assertNotContains(response, 'id="id_name" maxlength="2000" name="name" type="text" value="category 1"')
self.assertContains(response, '<option value="%s">Blog / sample_app</option>' % self.app_config_1.pk) self.assertContains(response, '<option value="%s">Blog / sample_app</option>' % self.app_config_1.pk)
# Changeview is 'normal', with a few preselected items # Changeview is 'normal', with a few preselected items
response = post_admin.change_view(request, str(self.category_1.pk)) response = category_admin.change_view(request, str(self.category_1.pk))
self.assertContains(response, 'id="id_name" maxlength="255" name="name" type="text" value="category 1"') self.assertContains(response, 'id="id_name" maxlength="2000" name="name" type="text" value="category 1"')
self.assertContains(response, 'id="id_meta_description" maxlength="320"')
self.assertContains(response, '<option value="%s" selected="selected">Blog / sample_app</option>' % self.app_config_1.pk) self.assertContains(response, '<option value="%s" selected="selected">Blog / sample_app</option>' % self.app_config_1.pk)
def test_form(self):
posts = self.get_posts()
with override_settings(BLOG_META_DESCRIPTION_LENGTH=20, BLOG_META_TITLE_LENGTH=20):
form = PostAdminForm(
data={'meta_description': 'major text over 20 characters long'},
instance=posts[0]
)
self.assertFalse(form.is_valid())
form = PostAdminForm(
data={'meta_title': 'major text over 20 characters long'},
instance=posts[0]
)
self.assertFalse(form.is_valid())
form = CategoryAdminForm(
data={'meta_description': 'major text over 20 characters long'},
instance=self.category_1
)
self.assertFalse(form.is_valid())
form = PostAdminForm(
data={'meta_description': 'mini text'},
instance=posts[0]
)
self.assertFalse(form.is_valid())
form = PostAdminForm(
data={'meta_title': 'mini text'},
instance=posts[0]
)
self.assertFalse(form.is_valid())
form = CategoryAdminForm(
data={'meta_description': 'mini text'},
instance=self.category_1
)
self.assertFalse(form.is_valid())
def test_admin_category_parents(self): def test_admin_category_parents(self):
category1 = BlogCategory.objects.create(name='tree category 1', app_config=self.app_config_1) category1 = BlogCategory.objects.create(name='tree category 1', app_config=self.app_config_1)
category2 = BlogCategory.objects.create(name='tree category 2', parent=category1, app_config=self.app_config_1) category2 = BlogCategory.objects.create(name='tree category 2', parent=category1, app_config=self.app_config_1)
@ -618,6 +657,21 @@ class ModelsTest(BaseTest):
self.assertNotEqual(meta_it.title, meta_en.title) self.assertNotEqual(meta_it.title, meta_en.title)
self.assertEqual(meta_it.description, post.meta_description) self.assertEqual(meta_it.description, post.meta_description)
category = post.categories.first()
meta_cat = category.as_meta()
self.assertEqual(meta_cat.og_type, get_setting('FB_TYPE'))
self.assertEqual(meta_cat.title, category.name)
self.assertEqual(meta_cat.description, category.meta_description)
self.assertEqual(meta_cat.locale, 'en')
self.assertEqual(meta_cat.twitter_site, '')
self.assertEqual(meta_cat.twitter_author, '')
self.assertEqual(meta_cat.twitter_type, 'summary')
self.assertEqual(meta_cat.gplus_author, 'RandomJoe')
self.assertEqual(meta_cat.gplus_type, 'Blog')
self.assertEqual(meta_cat.og_type, 'Article')
self.assertEqual(meta_cat.facebook_app_id, None)
self.assertTrue(meta_cat.url.endswith(category.get_absolute_url()))
with override('en'): with override('en'):
post.set_current_language(get_language()) post.set_current_language(get_language())
kwargs = {'year': post.date_published.year, kwargs = {'year': post.date_published.year,

View file

@ -254,6 +254,7 @@ class ViewTest(BaseTest):
self.assertEqual(list(context['post_list']), [posts[0]]) self.assertEqual(list(context['post_list']), [posts[0]])
self.assertEqual(context['paginator'].count, 3) self.assertEqual(context['paginator'].count, 3)
self.assertEqual(context['post_list'][0].title, 'First post') self.assertEqual(context['post_list'][0].title, 'First post')
self.assertTrue(context['meta'])
request = self.get_page_request(pages[1], self.user, edit=False) request = self.get_page_request(pages[1], self.user, edit=False)
view_obj.request = request view_obj.request = request