Improve meta

* Made HTML description and title fields length configurable
* Added meta representation for CategoryEntriesView
This commit is contained in:
Iacopo Spalletti 2018-01-09 00:54:10 +01:00
parent bdfbfc438a
commit a47b083a98
No known key found for this signature in database
GPG key ID: BDCBC2EB289F60C6
10 changed files with 209 additions and 44 deletions

View file

@ -22,7 +22,9 @@ History
* Improved admin filtering.
* Added featured date to post.
* 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 cms.admin.placeholderadmin import FrontendEditableAdminMixin, PlaceholderAdminMixin
from cms.models import CMSPlugin, ValidationError
from django import forms
from django.apps import apps
from django.conf import settings
from django.conf.urls import url
@ -268,16 +267,6 @@ class PostAdmin(PlaceholderAdminMixin, FrontendEditableAdminMixin,
except KeyError:
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):
"""
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.conf import settings
from django.core.validators import MaxLengthValidator
from parler.forms import TranslatableModelForm
from taggit_autosuggest.widgets import TagAutoSuggest
from djangocms_blog.settings import get_setting
from .models import BlogCategory, BlogConfig, Post
class CategoryAdminForm(TranslatableModelForm):
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)
if 'parent' in self.fields:
@ -52,6 +65,19 @@ class PostAdminForm(TranslatableModelForm):
fields = '__all__'
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)
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
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
class BlogCategory(TranslatableModel):
class BlogCategory(BlogMetaMixin, TranslatableModel):
"""
Blog category
"""
@ -73,13 +92,38 @@ class BlogCategory(TranslatableModel):
)
translations = TranslatedFields(
name=models.CharField(_('name'), max_length=255),
slug=models.SlugField(_('slug'), max_length=255, blank=True, db_index=True),
name=models.CharField(_('name'), max_length=2000),
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'),)}
)
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:
verbose_name = _('blog category')
verbose_name_plural = _('blog categories')
@ -105,9 +149,11 @@ class BlogCategory(TranslatableModel):
return self.linked_posts.published(current_site=False).count()
def get_absolute_url(self, lang=None):
if not lang:
if not lang or lang not in self.get_available_languages():
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)
return reverse(
'%s:posts-category' % self.app_config.namespace,
@ -131,9 +177,17 @@ class BlogCategory(TranslatableModel):
self.slug = slugify(force_text(self.name))
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
class Post(KnockerModel, ModelMeta, TranslatableModel):
class Post(KnockerModel, BlogMetaMixin, TranslatableModel):
"""
Blog post
"""
@ -173,8 +227,8 @@ class Post(KnockerModel, ModelMeta, TranslatableModel):
)
translations = TranslatedFields(
title=models.CharField(_('title'), max_length=255),
slug=AutoSlugField(_('slug'), max_length=255, blank=True,
title=models.CharField(_('title'), max_length=2000),
slug=AutoSlugField(_('slug'), max_length=2000, blank=True,
db_index=True, allow_unicode=True),
abstract=HTMLField(_('abstract'), blank=True, default=''),
meta_description=models.TextField(verbose_name=_('post meta description'),
@ -183,7 +237,7 @@ class Post(KnockerModel, ModelMeta, TranslatableModel):
blank=True, default=''),
meta_title=models.CharField(verbose_name=_('post meta title'),
help_text=_('used in title tag and social sharing'),
max_length=255,
max_length=2000,
blank=True, default=''),
post_text=HTMLField(_('text'), default='', blank=True),
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
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):
title = self.safe_translation_getter('meta_title', any_language=True)
if not title:
@ -320,9 +367,6 @@ class Post(KnockerModel, ModelMeta, TranslatableModel):
"""
return self.safe_translation_getter('meta_keywords', default='').strip().split(',')
def get_locale(self):
return self.get_current_language()
def get_description(self):
description = self.safe_translation_getter('meta_description', any_language=True)
if not description:
@ -367,12 +411,6 @@ class Post(KnockerModel, ModelMeta, TranslatableModel):
else:
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
def is_published(self):
"""

View file

@ -58,6 +58,12 @@ def get_setting(name):
'BLOG_POSTS_LIST_TRUNCWORDS_COUNT': getattr(
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_EMPTY_CATEGORIES': getattr(settings, 'MENU_EMPTY_CATEGORIES', True),
'BLOG_TYPE': getattr(settings, 'BLOG_TYPE', 'Article'),

View file

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

View file

@ -92,6 +92,8 @@ Global Settings
* BLOG_FEED_LATEST_ITEMS: Number of items in latest items 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_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.sites.models import Site
from django.core.urlresolvers import reverse
from django.test import override_settings
from django.utils.encoding import force_text
from django.utils.html import strip_tags
from django.utils.timezone import now
@ -25,6 +26,7 @@ from parler.utils.context import smart_override
from taggit.models import Tag
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.settings import MENU_TYPE_NONE, get_setting
@ -80,12 +82,13 @@ class AdminTest(BaseTest):
# Add view only contains the apphook selection widget
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)
# Changeview is 'normal'
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)
# Test for publish view
@ -176,19 +179,55 @@ class AdminTest(BaseTest):
self.assertContains(response, 'sample_app')
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)
# Add view only has an empty form - no type
response = post_admin.add_view(request)
self.assertNotContains(response, 'id="id_name" maxlength="255" name="name" type="text" value="category 1"')
response = category_admin.add_view(request)
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)
# Changeview is 'normal', with a few preselected items
response = post_admin.change_view(request, str(self.category_1.pk))
self.assertContains(response, 'id="id_name" maxlength="255" name="name" type="text" value="category 1"')
response = category_admin.change_view(request, str(self.category_1.pk))
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)
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):
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)
@ -618,6 +657,21 @@ class ModelsTest(BaseTest):
self.assertNotEqual(meta_it.title, meta_en.title)
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'):
post.set_current_language(get_language())
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(context['paginator'].count, 3)
self.assertEqual(context['post_list'][0].title, 'First post')
self.assertTrue(context['meta'])
request = self.get_page_request(pages[1], self.user, edit=False)
view_obj.request = request