diff --git a/djangocms_blog/cms_app.py b/djangocms_blog/cms_app.py index f00c96f..fd9a38d 100644 --- a/djangocms_blog/cms_app.py +++ b/djangocms_blog/cms_app.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- from cms.app_base import CMSApp from cms.apphook_pool import apphook_pool -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, get_language +from .menu import BlogCategoryMenu class BlogApp(CMSApp): name = _('Blog') urls = ['djangocms_blog.urls'] app_name = 'djangocms_blog' + menus = [BlogCategoryMenu] apphook_pool.register(BlogApp) diff --git a/djangocms_blog/menu.py b/djangocms_blog/menu.py new file mode 100644 index 0000000..3650b27 --- /dev/null +++ b/djangocms_blog/menu.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +from cms.menu_bases import CMSAttachMenu +from menus.base import Modifier, NavigationNode +from menus.menu_pool import menu_pool +from django.db.models.signals import post_save, post_delete +from django.utils.translation import ugettext_lazy as _, get_language +from .models import BlogCategory + + +class BlogCategoryMenu(CMSAttachMenu): + name = _('Blog Category menu') + + def get_nodes(self, request): + nodes = [] + qs = BlogCategory.objects.translated(get_language()) + qs = qs.order_by('parent__id', 'translations__name') + for category in qs: + node = NavigationNode( + category.name, + category.get_absolute_url(), + category.pk, + category.parent_id + ) + nodes.append(node) + return nodes + +menu_pool.register_menu(BlogCategoryMenu) + + +class BlogNavModifier(Modifier): + """ + This navigation modifier makes sure that when + a particular blog post is viewed, + a corresponding category is selected in menu + """ + def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb): + if post_cut: return nodes + if not hasattr(request, 'toolbar'): + return nodes + models = ('djangocms_blog.post', 'djangocms_blog.blogcategory') + model = request.toolbar.get_object_model() + if model not in models: + return nodes + if model == 'djangocms_blog.blogcategory': + cat = request.toolbar.obj + else: + cat = request.toolbar.obj.categories.first() + if not cat: return nodes + + for node in nodes: + if (node.namespace.startswith(BlogCategoryMenu.__name__) and + cat.pk == node.id): + node.selected = True + # no break here because django-cms maintains two menu structures + # for every apphook (attached to published page and draft page) + return nodes + +menu_pool.register_modifier(BlogNavModifier) + +def clear_menu_cache(**kwargs): + menu_pool.clear(all=True) + +post_save.connect(clear_menu_cache, sender=BlogCategory) +post_delete.connect(clear_menu_cache, sender=BlogCategory) diff --git a/djangocms_blog/models.py b/djangocms_blog/models.py index 8bc821e..ba6a63a 100644 --- a/djangocms_blog/models.py +++ b/djangocms_blog/models.py @@ -49,6 +49,15 @@ class BlogCategory(TranslatableModel): def count(self): return self.blog_posts.filter(publish=True).count() + def get_absolute_url(self): + lang = get_language() + if self.has_translation(lang): + slug = self.safe_translation_getter('slug', language_code=lang) + return reverse('djangocms_blog:posts-category', kwargs={'category': slug}) + # in case category doesn't exist in this language, gracefully fallback + # to posts-latest + return reverse('djangocms_blog:posts-latest') + def __str__(self): return self.safe_translation_getter('name') diff --git a/djangocms_blog/views.py b/djangocms_blog/views.py index 86885bf..fdecf5f 100644 --- a/djangocms_blog/views.py +++ b/djangocms_blog/views.py @@ -47,6 +47,12 @@ class PostDetailView(TranslatableSlugMixin, BaseBlogView, DetailView): slug_field = 'slug' view_url_name = 'djangocms_blog:post-detail' + def get(self, *args, **kwargs): + # submit object to cms to get corrent language switcher and selected category behavior + if hasattr(self.request, 'toolbar'): + self.request.toolbar.set_object(self.get_object()) + return super(PostDetailView, self).get(*args, **kwargs) + def get_context_data(self, **kwargs): context = super(PostDetailView, self).get_context_data(**kwargs) context['meta'] = self.get_object().as_meta() @@ -136,6 +142,12 @@ class CategoryEntriesView(BaseBlogView, ListView): self._category = BlogCategory.objects.active_translations(get_language(), slug=self.kwargs['category']).latest('pk') return self._category + def get(self, *args, **kwargs): + # submit object to cms toolbar to get correct language switcher behavior + if hasattr(self.request, 'toolbar'): + self.request.toolbar.set_object(self.category) + return super(CategoryEntriesView, self).get(*args, **kwargs) + def get_queryset(self): qs = super(CategoryEntriesView, self).get_queryset() if 'category' in self.kwargs: diff --git a/tests/__init__.py b/tests/__init__.py index c4e61d0..eae2eda 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -22,6 +22,9 @@ from djangocms_blog.models import BlogCategory, Post User = get_user_model() +def _get_cat_pk(lang, name): + return lambda: BlogCategory.objects.translated(lang, name=name).get().pk + class BaseTest(TestCase): """ @@ -59,6 +62,22 @@ class BaseTest(TestCase): ] } + cat_data = { + 'it': [ + {'name': u'Fortissimo'}, + {'name': u'Pianississimo'}, + {'name': u'Mezzo'}, + {'name': u'Forte', 'parent_id': _get_cat_pk('it', 'Mezzo')}, + ], + 'en': [ + {'name': u'Very loud'}, + {'name': u'Very very silent'}, + {'name': u'Almost'}, + {'name': u'Loud', 'parent_id': _get_cat_pk('en', 'Almost')}, + {'name': u'Silent', 'parent_id': _get_cat_pk('en', 'Almost')}, + ] + } + @classmethod def setUpClass(cls): cls.request_factory = RequestFactory() @@ -91,6 +110,19 @@ class BaseTest(TestCase): original_filename=self.image_name, file=file_obj) + def _get_category(self, data, category=None, lang='en'): + for k, v in data.items(): + if hasattr(v, '__call__'): + data[k] = v() + if not category: + category = BlogCategory.objects.create(**data) + else: + category.set_current_language(lang) + for attr, val in data.items(): + setattr(category, attr, val) + category.save() + return category + def _get_post(self, data, post=None, lang='en', sites=None): if not post: post_data = { @@ -117,7 +149,7 @@ class BaseTest(TestCase): @classmethod def tearDownClass(cls): User.objects.all().delete() - + def tearDown(self): for post in Post.objects.all(): post.delete() diff --git a/tests/test_menu.py b/tests/test_menu.py new file mode 100644 index 0000000..4e76c15 --- /dev/null +++ b/tests/test_menu.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +import copy +from django.utils.translation import activate +from menus.menu_pool import menu_pool +from parler.utils.context import switch_language +from djangocms_blog.views import PostDetailView, CategoryEntriesView +from . import BaseTest + + +class MenuTest(BaseTest): + def setUp(self): + super(MenuTest, self).setUp() + self.cats = [self.category_1] + for i, cat_data in enumerate(self.cat_data['en']): + cat = self._get_category(cat_data) + if i < len(self.cat_data['it']): + cat = self._get_category(self.cat_data['it'][i], cat, 'it') + self.cats.append(cat) + + activate('en') + menu_pool.discover_menus() + # All cms menu modifiers should be removed from menu_pool.modifiers + # so that they do not interfere with our menu nodes + menu_pool.modifiers = [m for m in menu_pool.modifiers if m.__module__.startswith('djangocms_blog')] + + def test_menu_nodes(self): + """ + Tests if all categories are present in the menu + """ + for lang in ('en', 'it'): + request = self.get_page_request(None, self.user, + r'/%s/blog/' % lang, edit=False) + activate(lang) + nodes = menu_pool.get_nodes(request, namespace='BlogCategoryMenu') + nodes_copy = copy.deepcopy(nodes) + for cat in self.cats: + if not cat.has_translation(lang): + continue + with switch_language(cat, lang): + # find in node list + found = None + for node in nodes_copy: + if node.url == cat.get_absolute_url(): + found = node + break + self.assertIsNotNone(found) + nodes_copy.remove(found) + self.assertEqual(node.id, cat.id) + self.assertEqual(node.title, cat.name) + # check that all categories were found in menu + self.assertEqual(len(nodes_copy), 0) + + def test_modifier(self): + """ + Tests if correct category is selected in the menu + according to context (view object) + """ + post1, post2 = self.get_posts() + tests = ( + # view class, view kwarg, view object, category + (PostDetailView, 'slug', post1, post1.categories.first()), + (CategoryEntriesView, 'category', self.cats[2], self.cats[2]) + ) + for view_cls, kwarg, obj, cat in tests: + request = self.get_page_request(None, self.user, r'/en/blog/', edit=False) + activate('en') + with switch_language(obj, 'en'): + view_obj = view_cls() + view_obj.request = request + view_obj.kwargs = {kwarg: obj.slug} + view_obj.get(request) + # check if selected menu node points to cat + nodes = menu_pool.get_nodes(request, namespace='BlogCategoryMenu') + found = False + for node in nodes: + if node.selected: + self.assertEqual(node.url, cat.get_absolute_url()) + found = True + break + self.assertTrue(found)