Improve meta
* Made HTML description and title fields length configurable * Added meta representation for CategoryEntriesView
This commit is contained in:
parent
bdfbfc438a
commit
a47b083a98
10 changed files with 209 additions and 44 deletions
|
@ -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
|
||||
|
||||
|
||||
*******************
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
46
djangocms_blog/migrations/0032_auto_20180109_0023.py
Normal file
46
djangocms_blog/migrations/0032_auto_20180109_0023.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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``)
|
||||
|
||||
|
||||
******************
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue