Repository: django-wiki/django-wiki

Issue:
Error while trying to delete an article
Stack trace:

```
In [2]: wiki.models.Article.objects.all().delete()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-d36bb49ac83b> in <module>()
----> 1 wiki.models.Article.objects.all().delete()

~/programming/SIStema/venv/lib/python3.5/site-packages/django/db/models/query.py in delete(self)
    660         collector = Collector(using=del_query.db)
    661         collector.collect(del_query)
--> 662         deleted, _rows_count = collector.delete()
    663 
    664         # Clear the result cache, in case this QuerySet gets reused.

~/programming/SIStema/venv/lib/python3.5/site-packages/django/db/models/deletion.py in delete(self)
    276                 if not model._meta.auto_created:
    277                     signals.pre_delete.send(
--> 278                         sender=model, instance=obj, using=self.using
    279                     )
    280 

~/programming/SIStema/venv/lib/python3.5/site-packages/django/dispatch/dispatcher.py in send(self, sender, **named)
    176         return [
    177             (receiver, receiver(signal=self, sender=sender, **named))
--> 178             for receiver in self._live_receivers(sender)
    179         ]
    180 

~/programming/SIStema/venv/lib/python3.5/site-packages/django/dispatch/dispatcher.py in <listcomp>(.0)
    176         return [
    177             (receiver, receiver(signal=self, sender=sender, **named))
--> 178             for receiver in self._live_receivers(sender)
    179         ]
    180 

~/programming/SIStema/venv/lib/python3.5/site-packages/wiki/decorators.py in wrapper(*args, **kwargs)
    178         if kwargs.get('raw', False):
    179             return
--> 180         return signal_handler(*args, **kwargs)
    181     return wrapper

~/programming/SIStema/venv/lib/python3.5/site-packages/wiki/models/article.py in on_article_delete_clear_cache(instance, **kwargs)
    410 def on_article_delete_clear_cache(instance, **kwargs):
    411     _clear_ancestor_cache(instance)
--> 412     instance.clear_cache()
    413 
    414 

~/programming/SIStema/venv/lib/python3.5/site-packages/wiki/models/article.py in clear_cache(self)
    239 
    240     def clear_cache(self):
--> 241         cache.delete(self.get_cache_key())
    242 
    243     def get_absolute_url(self):

~/programming/SIStema/venv/lib/python3.5/site-packages/wiki/models/article.py in get_cache_key(self)
    203         return "wiki:article:{id:d}:{lang:s}".format(
    204             id=self.current_revision.id if self.current_revision else self.id,
--> 205             lang=lang)
    206 
    207     def get_cache_content_key(self, user=None):

TypeError: non-empty format string passed to object.__format__
```


Discussion:

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -36,7 +36,7 @@ def get_path(fname):
 
 test_requirements = [
     'django-functest>=1.0.3,<1.1',
-    'pytest>=5.2,<5.3',
+    'pytest>=5.3,<5.4',
     'pytest-django',
     'pytest-cov',
     'pytest-pythonpath',
@@ -84,7 +84,6 @@ def get_path(fname):
         'Intended Audience :: Developers',
         'Operating System :: OS Independent',
         'Programming Language :: Python',
-        'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: Implementation :: CPython',
diff --git a/src/wiki/models/article.py b/src/wiki/models/article.py
--- a/src/wiki/models/article.py
+++ b/src/wiki/models/article.py
@@ -204,7 +204,7 @@ def get_cache_key(self):
         """Returns per-article cache key."""
         lang = translation.get_language()
 
-        return "wiki:article:{id:d}:{lang:s}".format(
+        return "wiki:article:{id}:{lang}".format(
             id=self.current_revision.id if self.current_revision else self.id,
             lang=lang)
 
diff --git a/src/wiki/views/article.py b/src/wiki/views/article.py
--- a/src/wiki/views/article.py
+++ b/src/wiki/views/article.py
@@ -41,7 +41,6 @@ def get_context_data(self, **kwargs):
 
 
 class Create(FormView, ArticleMixin):
-
     form_class = forms.CreateForm
     template_name = "wiki/create.html"
 
@@ -105,10 +104,7 @@ def get_success_url(self):
 
     def get_context_data(self, **kwargs):
         c = ArticleMixin.get_context_data(self, **kwargs)
-        # Needed since Django 1.9 because get_context_data is no longer called
-        # with the form instance
-        if 'form' not in c:
-            c['form'] = self.get_form()
+        c['form'] = self.get_form()
         c['parent_urlpath'] = self.urlpath
         c['parent_article'] = self.article
         c['create_form'] = c.pop('form', None)
@@ -121,11 +117,7 @@ class Delete(FormView, ArticleMixin):
     form_class = forms.DeleteForm
     template_name = "wiki/delete.html"
 
-    @method_decorator(
-        get_article(
-            can_write=True,
-            not_locked=True,
-            can_delete=True))
+    @method_decorator(get_article(can_write=True, not_locked=True, can_delete=True))
     def dispatch(self, request, article, *args, **kwargs):
         return self.dispatch1(request, article, *args, **kwargs)
 
@@ -149,8 +141,8 @@ def dispatch1(self, request, article, *args, **kwargs):
             for art_obj in article.articleforobject_set.filter(is_mptt=True):
                 if art_obj.content_object.parent:
                     self.next = reverse(
-                        'wiki:get', kwargs={
-                            'article_id': art_obj.content_object.parent.article.id})
+                        'wiki:get', kwargs={'article_id': art_obj.content_object.parent.article.id}
+                    )
                 else:
                     self.cannot_delete_root = True
 
@@ -166,7 +158,7 @@ def get_form(self, form_class=None):
         return form
 
     def get_form_kwargs(self):
-        kwargs = FormView.get_form_kwargs(self)
+        kwargs = super().get_form_kwargs()
         kwargs['article'] = self.article
         kwargs['has_children'] = bool(self.children_slice)
         return kwargs
@@ -213,15 +205,11 @@ def get_success_url(self):
 
     def get_context_data(self, **kwargs):
         cannot_delete_children = False
-        if self.children_slice and not self.article.can_moderate(
-                self.request.user):
+        if self.children_slice and not self.article.can_moderate(self.request.user):
             cannot_delete_children = True
 
-        # Needed since Django 1.9 because get_context_data is no longer called
-        # with the form instance
-        if 'form' not in kwargs:
-            kwargs['form'] = self.get_form()
-        kwargs['delete_form'] = kwargs.pop('form', None)
+        kwargs['delete_form'] = self.get_form()
+        kwargs['form'] = kwargs['delete_form']
         kwargs['cannot_delete_root'] = self.cannot_delete_root
         kwargs['delete_children'] = self.children_slice[:20]
         kwargs['delete_children_more'] = len(self.children_slice) > 20
@@ -232,7 +220,6 @@ def get_context_data(self, **kwargs):
 class Edit(ArticleMixin, FormView):
 
     """Edit an article and process sidebar plugins."""
-
     form_class = forms.EditForm
     template_name = "wiki/edit.html"
 
@@ -262,9 +249,7 @@ def get_form(self, form_class=None):
         if form_class is None:
             form_class = self.get_form_class()
         kwargs = self.get_form_kwargs()
-        if self.request.POST.get(
-                'save',
-                '') != '1' and self.request.POST.get('preview') != '1':
+        if self.request.POST.get('save', '') != '1' and self.request.POST.get('preview') != '1':
             kwargs['data'] = None
             kwargs['files'] = None
             kwargs['no_clean'] = True
@@ -276,13 +261,9 @@ def get_sidebar_form_classes(self):
         to identify which form is being saved."""
         form_classes = {}
         for cnt, plugin in enumerate(self.sidebar_plugins):
-            form_classes[
-                'form%d' %
-                cnt] = (
-                plugin,
-                plugin.sidebar.get(
-                    'form_class',
-                    None))
+            form_classes['form%d' % cnt] = (
+                plugin, plugin.sidebar.get('form_class', None)
+            )
         return form_classes
 
     def get(self, request, *args, **kwargs):
@@ -322,23 +303,15 @@ def post(self, request, *args, **kwargs):
                         content = form.cleaned_data['unsaved_article_content']
 
                         if title != self.article.current_revision.title or content != self.article.current_revision.content:
-                            request.session[
-                                'unsaved_article_title_%d' %
-                                self.article.id] = title
-                            request.session[
-                                'unsaved_article_content_%d' %
-                                self.article.id] = content
+                            request.session['unsaved_article_title_%d' % self.article.id] = title
+                            request.session['unsaved_article_content_%d' % self.article.id] = content
                             messages.warning(
                                 request,
                                 _('Please note that your article text has not yet been saved!'))
 
                         if self.urlpath:
-                            return redirect(
-                                'wiki:edit',
-                                path=self.urlpath.path)
-                        return redirect(
-                            'wiki:edit',
-                            article_id=self.article.id)
+                            return redirect('wiki:edit', path=self.urlpath.path)
+                        return redirect('wiki:edit', article_id=self.article.id)
 
                 else:
                     form = Form(self.article, self.request)
@@ -371,10 +344,7 @@ def get_success_url(self):
         return redirect('wiki:get', article_id=self.article.id)
 
     def get_context_data(self, **kwargs):
-        # Needed for Django 1.9 because get_context_data is no longer called
-        # with the form instance
-        if 'form' not in kwargs:
-            kwargs['form'] = self.get_form()
+        kwargs['form'] = self.get_form()
         kwargs['edit_form'] = kwargs['form']
         kwargs['editor'] = editors.getEditor()
         kwargs['selected_tab'] = 'edit'
@@ -399,10 +369,8 @@ def get_form(self, form_class=None):
         return form_class(**kwargs)
 
     def get_context_data(self, **kwargs):
-        if 'form' not in kwargs:
-            kwargs['form'] = self.get_form()
+        kwargs['form'] = self.get_form()
         kwargs['root_path'] = models.URLPath.root()
-
         return super().get_context_data(**kwargs)
 
     @transaction.atomic
@@ -552,20 +520,18 @@ def dispatch(self, request, article, *args, **kwargs):
         return super().dispatch1(request, article, *args, **kwargs)
 
     def get_initial(self):
-        return {'revision': self.article.current_revision,
-                'purge': True}
+        return {
+            'revision': self.article.current_revision,
+            'purge': True,
+        }
 
     def get_context_data(self, **kwargs):
-        # Needed since Django 1.9 because get_context_data is no longer called
-        # with the form instance
-        if 'form' not in kwargs:
-            kwargs['form'] = self.get_form()
-        kwargs['purge_form'] = kwargs.pop('form', None)
-        return super(Delete, self).get_context_data(**kwargs)
+        kwargs['purge_form'] = self.get_form()
+        kwargs['form'] = kwargs['purge_form']
+        return super().get_context_data(**kwargs)
 
 
 class Source(ArticleMixin, TemplateView):
-
     template_name = "wiki/source.html"
 
     @method_decorator(get_article(can_read=True))
@@ -574,7 +540,7 @@ def dispatch(self, request, article, *args, **kwargs):
 
     def get_context_data(self, **kwargs):
         kwargs['selected_tab'] = 'source'
-        return ArticleMixin.get_context_data(self, **kwargs)
+        return super().get_context_data(**kwargs)
 
 
 class History(ListView, ArticleMixin):
@@ -625,8 +591,8 @@ def get_queryset(self):
         children = self.urlpath.get_children().can_read(self.request.user)
         if self.query:
             children = children.filter(
-                Q(article__current_revision__title__contains=self.query) |
-                Q(slug__contains=self.query))
+                Q(article__current_revision__title__icontains=self.query) | Q(slug__icontains=self.query)
+            )
         if not self.article.can_moderate(self.request.user):
             children = children.active()
         children = children.select_related_common().order_by(
@@ -684,8 +650,8 @@ def get_queryset(self):
             except (NoRootURL, models.URLPath.DoesNotExist):
                 raise Http404
         articles = articles.filter(
-            Q(current_revision__title__icontains=self.query) |
-            Q(current_revision__content__icontains=self.query))
+            Q(current_revision__title__icontains=self.query) | Q(current_revision__content__icontains=self.query)
+        )
         if not permissions.can_moderate(
                 models.URLPath.root().article,
                 self.request.user):
@@ -720,7 +686,7 @@ class Settings(ArticleMixin, TemplateView):
     def dispatch(self, request, article, *args, **kwargs):
         return super().dispatch(request, article, *args, **kwargs)
 
-    def get_form_classes(self,):
+    def get_form_classes(self):
         """
         Return all settings forms that can be filled in
         """
@@ -740,23 +706,19 @@ def get_form_classes(self,):
 
     def post(self, *args, **kwargs):
         self.forms = []
-        for Form in self.get_form_classes():
-            if Form.action == self.request.GET.get('f', None):
-                form = Form(self.article, self.request, self.request.POST)
+        for form_class in self.get_form_classes():
+            if form_class.action == self.request.GET.get('f', None):
+                form = form_class(self.article, self.request, self.request.POST)
                 if form.is_valid():
                     form.save()
                     usermessage = form.get_usermessage()
                     if usermessage:
                         messages.success(self.request, usermessage)
                     if self.urlpath:
-                        return redirect(
-                            'wiki:settings',
-                            path=self.urlpath.path)
-                    return redirect(
-                        'wiki:settings',
-                        article_id=self.article.id)
+                        return redirect('wiki:settings', path=self.urlpath.path)
+                    return redirect('wiki:settings', article_id=self.article.id)
             else:
-                form = Form(self.article, self.request)
+                form = form_class(self.article, self.request)
             self.forms.append(form)
         return super().get(*args, **kwargs)
 
@@ -967,7 +929,6 @@ class CreateRootView(FormView):
     template_name = 'wiki/create_root.html'
 
     def dispatch(self, request, *args, **kwargs):
-
         if not request.user.is_superuser:
             return redirect("wiki:root_missing")
 
@@ -996,10 +957,6 @@ def form_valid(self, form):
     def get_context_data(self, **kwargs):
         kwargs = super().get_context_data(**kwargs)
         kwargs['editor'] = editors.getEditor()
-        # Needed since Django 1.9 because get_context_data is no longer called
-        # with the form instance
-        if 'form' not in kwargs:
-            kwargs['form'] = self.get_form()
         return kwargs
 
 


Test patch:
diff --git a/tests/core/test_managers.py b/tests/core/test_managers.py
--- a/tests/core/test_managers.py
+++ b/tests/core/test_managers.py
@@ -22,6 +22,15 @@ def test_queryset_methods_directly_on_manager(self):
         )
         self.assertEqual(Article.objects.active().count(), 1)
 
+    def test_mass_deletion(self):
+        """
+        https://github.com/django-wiki/django-wiki/issues/857
+        """
+        Article.objects.all().delete()
+        self.assertEqual(
+            Article.objects.all().count(), 0
+        )
+
     def test_queryset_methods_on_querysets(self):
 
         self.assertEqual(
diff --git a/tests/core/test_views.py b/tests/core/test_views.py
--- a/tests/core/test_views.py
+++ b/tests/core/test_views.py
@@ -1,15 +1,18 @@
 import pprint
 
+from django.contrib.messages import constants
 from django.http import JsonResponse
 from django.shortcuts import resolve_url
+from django.test import override_settings
 from django.utils import translation
 from django.utils.html import escape
 from django_functest import FuncBaseMixin
+from tests.testdata.models import CustomGroup
 from wiki import models
-from wiki.forms import validate_slug_numbers
+from wiki.forms import PermissionsForm, validate_slug_numbers
 from wiki.models import ArticleRevision, URLPath, reverse
 
-from ..base import ArticleWebTestUtils, DjangoClientTestBase, RequireRootArticleMixin, SeleniumBase, WebTestBase
+from ..base import SUPERUSER1_USERNAME, ArticleWebTestUtils, DjangoClientTestBase, RequireRootArticleMixin, SeleniumBase, WebTestBase
 
 
 class RootArticleViewTestsBase(FuncBaseMixin):
@@ -306,6 +309,20 @@ def test_translation(self):
 
 class DeleteViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase):
 
+    def test_render_delete_view(self):
+        """
+        Other tests do not render the delete view but just sends a POST
+        """
+        self.client.post(
+            resolve_url('wiki:create', path=''),
+            {'title': 'Test delete', 'slug': 'testdelete', 'content': 'To be deleted'}
+        )
+        response = self.client.get(
+            resolve_url('wiki:delete', path='testdelete/'),
+        )
+        # test the cache
+        self.assertContains(response, 'Delete article')
+
     def test_articles_cache_is_cleared_after_deleting(self):
 
         # That bug is tested by one individual test, otherwise it could be
@@ -340,9 +357,55 @@ def test_articles_cache_is_cleared_after_deleting(self):
             response,
             resolve_url('wiki:get', path='testcache/')
         )
-        # test the cache
         self.assertContains(self.get_by_path('TestCache/'), 'Content 2')
 
+    def test_deleted_view(self):
+        """
+        Test that a special page is shown for restoring/purging a deleted
+        article.
+        """
+        # 1. Create the article
+        self.client.post(
+            resolve_url('wiki:create', path=''),
+            {'title': 'Test delete', 'slug': 'testdelete', 'content': 'To be deleted'}
+        )
+        # 2. Soft delete it
+        self.client.post(
+            resolve_url('wiki:delete', path='testdelete/'),
+            {'confirm': 'on', 'purge': '',
+             'revision': str(URLPath.objects.get(slug='testdelete').article.current_revision.id)}
+        )
+        # 3. Get and test that it redirects to the deleted page
+        response = self.client.get(
+            resolve_url('wiki:get', path='testdelete/'),
+            follow=True,
+        )
+        # test that it's the Deleted page
+        self.assertContains(response, 'Article deleted')
+
+        # 4. Test that we can purge the page now
+        self.client.post(
+            resolve_url('wiki:deleted', path='testdelete/'),
+            {'confirm': 'on', 'purge': 'on',
+             'revision': str(URLPath.objects.get(slug='testdelete').article.current_revision.id)}
+        )
+        # 5. Test that it's not found anymore
+        response = self.client.get(
+            resolve_url('wiki:get', path='testdelete/'),
+            follow=True,
+        )
+        self.assertContains(response, "Add new article")
+
+    # def test_delete_article_without_urlpath(self):
+    #     """
+    #     We need a test that tests that articles without URLpaths are correctly
+    #     deleted.
+    #     """
+    #     pass
+
+    # def test_dont_delete_children(self):
+    #    Article.objects.create()
+
 
 class EditViewTest(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase):
 
@@ -594,3 +657,155 @@ def test_merge_preview(self):
             response,
             '#{rev_number}'.format(rev_number=new_revision.revision_number)
         )
+
+
+class SourceViewTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase):
+    def test_template_used(self):
+        response = self.client.get(reverse('wiki:source', kwargs={
+            'article_id': self.root_article.pk,
+        }))
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, template_name='wiki/source.html')
+
+    def test_can_read_permission(self):
+        # everybody can see the source of an article
+        self.client.logout()
+        response = self.client.get(reverse('wiki:source', kwargs={
+            'article_id': self.root_article.pk,
+        }))
+        self.assertEqual(response.status_code, 200)
+
+    def test_content(self):
+        response = self.client.get(reverse('wiki:source', kwargs={
+            'article_id': self.root_article.pk,
+        }))
+        self.assertIn('Source of ', str(response.content))
+        self.assertEqual(response.context['selected_tab'], 'source')
+
+
+class HistoryViewTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase):
+
+    def test_can_read_permission(self):
+        response = self.client.get(reverse('wiki:history', kwargs={
+            'article_id': self.root_article.pk,
+        }))
+        self.assertEqual(response.status_code, 200)
+
+    def test_content(self):
+        response = self.client.get(reverse('wiki:history', kwargs={
+            'article_id': self.root_article.pk,
+        }))
+        self.assertContains(response, 'History:')
+        self.assertEqual(response.context['selected_tab'], 'history')
+
+
+class DirViewTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase):
+
+    def test_browse_root(self):
+        response = self.client.get(
+            reverse('wiki:dir', kwargs={'path': ''}),
+        )
+        self.assertRegex(
+            response.rendered_content,
+            r'Browsing\s+<strong><a href=".+">/</a></strong>'
+        )
+
+    def test_browse_root_query(self):
+        self.client.post(
+            resolve_url('wiki:create', path=''),
+            {'title': 'Test', 'slug': 'test0', 'content': 'Content .0.'}
+        )
+        self.client.post(
+            resolve_url('wiki:create', path='test0/'),
+            {'title': 'Test00', 'slug': 'test00', 'content': 'Content .00.'}
+        )
+        response = self.client.get(
+            reverse('wiki:dir', kwargs={'path': ''}),
+            {'query': "Test"},
+        )
+        self.assertRegex(
+            response.rendered_content,
+            r'1 article'
+        )
+        response = self.client.get(
+            reverse('wiki:dir', kwargs={'path': 'test0/'}),
+            {'query': "Test00"},
+        )
+        self.assertRegex(
+            response.rendered_content,
+            r'1 article'
+        )
+
+
+class SettingsViewTests(RequireRootArticleMixin, ArticleWebTestUtils, DjangoClientTestBase):
+
+    def test_change_group(self):
+        group = CustomGroup.objects.create()
+        response = self.client.post(
+            resolve_url('wiki:settings', article_id=self.root_article.pk) + "?f=form0",
+            {
+                'group': group.pk,
+                'owner_username': SUPERUSER1_USERNAME,
+            },
+            follow=True
+        )
+        self.root_article.refresh_from_db()
+        self.assertEqual(self.root_article.group, group)
+        self.assertEqual(self.root_article.owner, self.superuser1)
+        self.assertEqual(len(response.context.get('messages')), 1)
+        message = response.context.get('messages')._loaded_messages[0]
+        self.assertEqual(message.level, constants.SUCCESS)
+        self.assertEqual(message.message, 'Permission settings for the article were updated.')
+
+    def test_change_invalid_owner(self):
+        self.assertIsNone(self.root_article.owner)
+        response = self.client.post(resolve_url('wiki:settings', article_id=self.root_article.pk) + "?f=form0", {
+            'owner_username': 'invalid'
+        }, follow=True)
+        self.assertEqual(response.context['forms'][0].errors['owner_username'], ['No user with that username'])
+
+    def test_unchanged_message(self):
+        # 1. This is not pretty: Constructs a request object to use to construct
+        # the PermissionForm
+        get_response = self.client.get(
+            resolve_url(
+                'wiki:settings',
+                article_id=self.root_article.pk
+            )
+        )
+        # 2. Construct a PermissionForm
+        form = PermissionsForm(self.root_article, get_response.wsgi_request)
+        # 3. ...in order to get the POST form values that will be transmitted
+        form_values = {
+            field.html_name: field.value() or "" for field in form
+        }
+        # 4. Send an unchanged form
+        response = self.client.post(
+            resolve_url(
+                'wiki:settings',
+                article_id=self.root_article.pk
+            ) + "?f=form0",
+            form_values,
+            follow=True
+        )
+        self.assertEqual(len(response.context.get('messages')), 1)
+        message = response.context.get('messages')._loaded_messages[0]
+        self.assertEqual(message.level, constants.SUCCESS)
+        self.assertEqual(message.message, 'Your permission settings were unchanged, so nothing saved.')
+
+    @override_settings(ACCOUNT_HANDLING=True)
+    def test_login_required(self):
+        self.client.logout()
+        response = self.client.get(reverse('wiki:settings', kwargs={'article_id': self.root_article.pk}))
+        # it's redirecting
+        self.assertEqual(response.status_code, 302)
+
+    def test_auth_user(self):
+        response = self.client.get(reverse('wiki:settings', kwargs={'article_id': self.root_article.pk}))
+        self.assertEqual(response.status_code, 200)
+
+    def test_content(self):
+        response = self.client.get(reverse('wiki:settings', kwargs={
+            'article_id': self.root_article.pk,
+        }))
+        self.assertEqual(response.context['selected_tab'], 'settings')
diff --git a/tests/settings.py b/tests/settings.py
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -25,6 +25,7 @@
     'django.contrib.admin.apps.AdminConfig',
     'django.contrib.humanize.apps.HumanizeConfig',
     'django.contrib.sites.apps.SitesConfig',
+    'django.contrib.messages',
     'django_nyt.apps.DjangoNytConfig',
     'mptt',
     'sekizai',