From e56d5aa137aa38c1f47bde3c71fc4801902ae708 Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Wed, 3 Aug 2016 09:00:46 +0200 Subject: [PATCH 1/5] Add very stupid check to avoid circular relationships for BlogCategory --- djangocms_blog/admin.py | 6 +++++- djangocms_blog/forms.py | 14 +++++++++++++ .../migrations/0025_auto_20160803_0858.py | 21 +++++++++++++++++++ djangocms_blog/models.py | 12 ++++++++++- 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 djangocms_blog/migrations/0025_auto_20160803_0858.py diff --git a/djangocms_blog/admin.py b/djangocms_blog/admin.py index 8d479e0..5770c6a 100755 --- a/djangocms_blog/admin.py +++ b/djangocms_blog/admin.py @@ -19,7 +19,7 @@ from django.utils.translation import get_language_from_request, ugettext_lazy as from parler.admin import TranslatableAdmin from .cms_appconfig import BlogConfig -from .forms import PostAdminForm +from .forms import CategoryAdminForm, PostAdminForm from .models import BlogCategory, Post from .settings import get_setting @@ -31,6 +31,10 @@ except ImportError: class BlogCategoryAdmin(EnhancedModelAdminMixin, ModelAppHookConfig, TranslatableAdmin): + form = CategoryAdminForm + list_display = [ + 'name', 'parent', 'all_languages_column', + ] def get_prepopulated_fields(self, request, obj=None): app_config_default = self._app_config_select(request, obj) diff --git a/djangocms_blog/forms.py b/djangocms_blog/forms.py index c1be621..5a67515 100644 --- a/djangocms_blog/forms.py +++ b/djangocms_blog/forms.py @@ -9,6 +9,20 @@ from taggit_autosuggest.widgets import TagAutoSuggest from .models import BlogCategory, BlogConfig, Post +class CategoryAdminForm(TranslatableModelForm): + + def __init__(self, *args, **kwargs): + super(CategoryAdminForm, self).__init__(*args, **kwargs) + if self.instance.pk: + self.fields['parent'].queryset = self.fields['parent'].queryset.exclude( + pk__in=[self.instance.pk] + [child.pk for child in self.instance.descendants()] + ) + + class Meta: + model = BlogCategory + fields = '__all__' + + class LatestEntriesForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(LatestEntriesForm, self).__init__(*args, **kwargs) diff --git a/djangocms_blog/migrations/0025_auto_20160803_0858.py b/djangocms_blog/migrations/0025_auto_20160803_0858.py new file mode 100644 index 0000000..f613c9b --- /dev/null +++ b/djangocms_blog/migrations/0025_auto_20160803_0858.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.9 on 2016-08-03 06:58 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangocms_blog', '0024_auto_20160706_1524'), + ] + + operations = [ + migrations.AlterField( + model_name='blogcategory', + name='parent', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='djangocms_blog.BlogCategory', verbose_name='parent'), + ), + ] diff --git a/djangocms_blog/models.py b/djangocms_blog/models.py index 1bd1df5..f601d46 100644 --- a/djangocms_blog/models.py +++ b/djangocms_blog/models.py @@ -60,7 +60,9 @@ class BlogCategory(TranslatableModel): """ Blog category """ - parent = models.ForeignKey('self', verbose_name=_('parent'), null=True, blank=True) + parent = models.ForeignKey( + 'self', verbose_name=_('parent'), null=True, blank=True, related_name='children' + ) date_created = models.DateTimeField(_('created at'), auto_now_add=True) date_modified = models.DateTimeField(_('modified at'), auto_now=True) app_config = AppHookConfigField( @@ -79,6 +81,14 @@ class BlogCategory(TranslatableModel): verbose_name = _('blog category') verbose_name_plural = _('blog categories') + def descendants(self): + children = [] + if self.children.exists(): + children.extend(self.children.all()) + for child in self.children.all(): + children.extend(child.descendants()) + return children + @cached_property def linked_posts(self): return self.blog_posts.namespace(self.app_config.namespace) From c14afead36826f943b5c9a51149c375ee7f56efc Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Wed, 3 Aug 2016 09:18:55 +0200 Subject: [PATCH 2/5] Filter parent categories on apphook --- djangocms_blog/admin.py | 2 +- djangocms_blog/forms.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/djangocms_blog/admin.py b/djangocms_blog/admin.py index 5770c6a..7de8b86 100755 --- a/djangocms_blog/admin.py +++ b/djangocms_blog/admin.py @@ -33,7 +33,7 @@ except ImportError: class BlogCategoryAdmin(EnhancedModelAdminMixin, ModelAppHookConfig, TranslatableAdmin): form = CategoryAdminForm list_display = [ - 'name', 'parent', 'all_languages_column', + 'name', 'parent', 'app_config', 'all_languages_column', ] def get_prepopulated_fields(self, request, obj=None): diff --git a/djangocms_blog/forms.py b/djangocms_blog/forms.py index 5a67515..7ccadd7 100644 --- a/djangocms_blog/forms.py +++ b/djangocms_blog/forms.py @@ -13,10 +13,20 @@ class CategoryAdminForm(TranslatableModelForm): def __init__(self, *args, **kwargs): super(CategoryAdminForm, self).__init__(*args, **kwargs) - if self.instance.pk: - self.fields['parent'].queryset = self.fields['parent'].queryset.exclude( - pk__in=[self.instance.pk] + [child.pk for child in self.instance.descendants()] - ) + + if 'parent' in self.fields: + qs = self.fields['parent'].queryset + if self.instance.pk: + qs = qs.exclude( + pk__in=[self.instance.pk] + [child.pk for child in self.instance.descendants()] + ) + + if getattr(self.instance, 'app_config_id', None): + qs = qs.namespace(self.instance.app_config.namespace) + elif 'initial' in kwargs and 'app_config' in kwargs['initial']: + config = BlogConfig.objects.get(pk=kwargs['initial']['app_config']) + qs = qs.namespace(config.namespace) + self.fields['parent'].queryset = qs class Meta: model = BlogCategory From b2b962610aedeb3f67c4f5a00b5a3f45e4a41625 Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Wed, 3 Aug 2016 09:19:45 +0200 Subject: [PATCH 3/5] Add tests for category parent filtering --- tests/test_models.py | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index 81e169d..b4d25f7 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -120,11 +120,47 @@ class AdminTest(BaseTest): # Changeview is 'normal', with a few preselected items response = post_admin.change_view(request, str(self.category_1.pk)) - # response.render() - # print(response.content.decode('utf-8')) self.assertContains(response, '') self.assertContains(response, '' % self.app_config_1.pk) + 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) + category3 = BlogCategory.objects.create(name='tree category 3', parent=category2, app_config=self.app_config_1) + BlogCategory.objects.create(name='tree category 4', parent=category3, app_config=self.app_config_1) + BlogCategory.objects.create(name='category different branch', app_config=self.app_config_2) + + post_admin = admin.site._registry[BlogCategory] + request = self.get_page_request('/', self.user, r'/en/blog/?app_config=%s' % self.app_config_1.pk, edit=False) + + # Add view shows all the exising categories + response = post_admin.add_view(request) + self.assertContains(response, '') + self.assertContains(response, '') + self.assertContains(response, '') + self.assertContains(response, '') + self.assertContains(response, '') + self.assertNotContains(response, 'category different branch') + + # Changeview hides the children of the current category + response = post_admin.change_view(request, str(category2.pk)) + self.assertContains(response, '') + self.assertContains(response, '') + self.assertNotContains(response, '') + self.assertNotContains(response, '') + self.assertNotContains(response, '') + self.assertNotContains(response, 'category different branch') + + # Test second apphook categories + request = self.get_page_request('/', self.user, r'/en/blog/?app_config=%s' % self.app_config_2.pk, edit=False) + response = post_admin.add_view(request) + self.assertNotContains(response, '') + self.assertNotContains(response, '') + self.assertNotContains(response, '') + self.assertNotContains(response, '') + self.assertNotContains(response, '') + self.assertContains(response, 'category different branch') + def test_admin_fieldsets(self): post_admin = admin.site._registry[Post] request = self.get_page_request('/', self.user_staff, r'/en/blog/?app_config=%s' % self.app_config_1.pk, edit=False) From 0410ac1f8cfc7e41e68b37ce0f3ec3d5a638320c Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Wed, 3 Aug 2016 09:24:43 +0200 Subject: [PATCH 4/5] Add changelog entry --- HISTORY.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 4325564..61c5d9e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -15,7 +15,8 @@ History * Added option to hide empty categories from menu. * Added standalone documentation at https://djangocms-blog.readthedocs.io. * Enabled cached version of BlogLatestEntriesPlugin. -* Added plugins templateset +* Added plugins templateset. +* Improved category admin to avoid circular relationships. ****************** 0.8.6 (unreleased) From 56b472eefc6c48b5a3ceedd370161323688c68ef Mon Sep 17 00:00:00 2001 From: Iacopo Spalletti Date: Wed, 3 Aug 2016 10:58:47 +0200 Subject: [PATCH 5/5] Fix tests --- tests/test_models.py | 4 ++-- tox.ini | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index b4d25f7..2af2b04 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -115,12 +115,12 @@ class AdminTest(BaseTest): # Add view only has an empty form - no type response = post_admin.add_view(request) - self.assertNotContains(response, '') + self.assertNotContains(response, 'id="id_name" maxlength="255" name="name" type="text" value="category 1" />') self.assertContains(response, '' % 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, '') + self.assertContains(response, 'id="id_name" maxlength="255" name="name" type="text" value="category 1" />') self.assertContains(response, '' % self.app_config_1.pk) def test_admin_category_parents(self): diff --git a/tox.ini b/tox.ini index 9841c1d..3f03d6b 100644 --- a/tox.ini +++ b/tox.ini @@ -43,6 +43,7 @@ deps = sphinx sphinx-rtd-theme html5lib<0.99999999 + Django>=1.9,<1.10 -rrequirements-test.txt changedir=docs skip_install = true