Rafał Pitoń 10 лет назад
Родитель
Сommit
f9f79e15cc

+ 23 - 0
docs/developers/forms.rst

@@ -62,6 +62,29 @@ Thin wrapper around Django's ``TypedChoiceField``. This field renders nice yes/n
    ``YesNoSwitch`` coerces to ``int``, not to ``bool``! Remember about this when writing code dealing with forms containing this field!
 
 
+Forums Forms Module
+===================
+
+:py:mod:`misago.forums.forms` module defines two fields you may use for making forum selections in your forms:
+
+
+ForumChoiceField
+----------------
+
+Extends ``ModelChoiceField``.
+
+
+ForumsMultipleChoiceField
+-------------------------
+
+Extends ``ModelMultipleChoiceField``.
+
+
+Instead of ``queryset``, both fields expect ``parent`` argument containing ``Forum`` class instance from which forums should be selected. Leave this argument empty for fields to fallback to root category for all categories and forums.
+
+In addition, you can pass field ACL dictionary to further limit choices to forums browserable by ACL owner.
+
+
 Template Tags
 =============
 

+ 11 - 1
misago/core/forms.py

@@ -1,7 +1,11 @@
 from crispy_forms.helper import FormHelper
-from django.utils.translation import ugettext_lazy as _
+
+from mptt.forms import *  # noqa
+
 from django.forms import *  # noqa
 from django.forms import Form as BaseForm, ModelForm as BaseModelForm
+from django.utils.html import conditional_escape, mark_safe
+from django.utils.translation import ugettext_lazy as _
 
 
 TEXT_BASED_FIELDS = (
@@ -9,6 +13,9 @@ TEXT_BASED_FIELDS = (
 )
 
 
+"""
+Fields
+"""
 class YesNoSwitchBase(TypedChoiceField):
     def prepare_value(self, value):
         """normalize bools to binary 1/0 so field works on them too"""
@@ -26,6 +33,9 @@ def YesNoSwitch(**kwargs):
         **kwargs)
 
 
+"""
+Forms
+"""
 class AutoStripWhitespacesMixin(object):
     autostrip_exclude = []
 

+ 0 - 31
misago/core/utils.py

@@ -1,7 +1,5 @@
 from datetime import timedelta
 
-import bleach
-from markdown import Markdown
 from unidecode import unidecode
 
 from django.http import Http404
@@ -165,32 +163,3 @@ MD_SUBSET_FORBID_SYNTAX = (
     # Blocks are evil too
     'hashheader', 'setextheader', 'code', 'quote', 'hr', 'olist', 'ulist',
 )
-
-
-def subset_markdown(text):
-    if not text:
-        return ''
-
-    md = Markdown(safe_mode='escape', extensions=['nl2br'])
-
-    for key in md.preprocessors.keys():
-        if key in MD_SUBSET_FORBID_SYNTAX:
-            del md.preprocessors[key]
-
-    for key in md.inlinePatterns.keys():
-        if key in MD_SUBSET_FORBID_SYNTAX:
-            del md.inlinePatterns[key]
-
-    for key in md.parser.blockprocessors.keys():
-        if key in MD_SUBSET_FORBID_SYNTAX:
-            del md.parser.blockprocessors[key]
-
-    for key in md.treeprocessors.keys():
-        if key in MD_SUBSET_FORBID_SYNTAX:
-            del md.treeprocessors[key]
-
-    for key in md.postprocessors.keys():
-        if key in MD_SUBSET_FORBID_SYNTAX:
-            del md.postprocessors[key]
-
-    return bleach.linkify(md.convert(text))

+ 72 - 2
misago/forums/forms.py

@@ -1,7 +1,8 @@
 from django.db import models
 from django.utils.html import conditional_escape, mark_safe
 from django.utils.translation import ugettext_lazy as _
-from mptt.forms import TreeNodeChoiceField as TreeNodeChoiceField
+
+from mptt.forms import *  # noqa
 
 from misago.core import forms
 from misago.core.validators import validate_sluggable
@@ -9,6 +10,76 @@ from misago.core.validators import validate_sluggable
 from misago.forums.models import Forum, ForumRole
 
 
+"""
+Fields
+"""
+class ForumListLazyQueryset(object):
+    def __init__(self, parent, acl):
+        self._cache = None
+        self.parent = parent
+        self.acl = acl
+
+    def get_parent(self):
+        if not self.parent:
+            self.parent = Forum.objects.root_category()
+        return self.parent
+
+    def _all(self):
+        queryset = self.get_parent().get_descendants()
+        if self.acl:
+            allowed_ids = [0]
+            for forum_id, perms in self.acl.get('forums', {}).items():
+                if perms.get('can_see') and perms.get('can_browse'):
+                    allowed_ids.append(forum_id)
+            queryset = queryset.filter(id__in=allowed_ids)
+        return queryset.all()
+
+    def all(self):
+        if not self._cache:
+            self._cache = self._all()
+        return self._cache
+
+    def filter(self, *args, **kwargs):
+        return self.all().filter(*args, **kwargs)
+
+    def exclude(self, *args, **kwargs):
+        return self.all().exclude(*args, **kwargs)
+
+    def __len__(self):
+        return len(self.all())
+
+
+class MisagoForumMixin(object):
+    def get_queryset(self, acl=None):
+        queryset = self.parent.get_descendants()
+        if acl:
+            allowed_ids = [0]
+            for forum_id, perms in acl.get('forums', {}).items():
+                if perms.get('can_see') and perms.get('can_browse'):
+                    allowed_ids.append(forum_id)
+            queryset = queryset.filter(id__in=allowed_ids)
+        return queryset
+
+    def _get_level_indicator(self, obj):
+        level = obj.level - self.queryset.get_parent().level - 1
+        return mark_safe(conditional_escape('- - ') * level)
+
+
+class ForumChoiceField(MisagoForumMixin, TreeNodeChoiceField):
+    def __init__(self, parent=None, acl=None, *args, **kwargs):
+        kwargs['queryset'] = ForumListLazyQueryset(parent, acl)
+        super(ForumChoiceField, self).__init__(*args, **kwargs)
+
+
+class ForumsMultipleChoiceField(MisagoForumMixin, TreeNodeMultipleChoiceField):
+    def __init__(self, parent=None, acl=None, *args, **kwargs):
+        kwargs['queryset'] = ForumListLazyQueryset(parent, acl)
+        super(ForumsMultipleChoiceField, self).__init__(*args, **kwargs)
+
+
+"""
+Forms
+"""
 class ForumChoiceField(TreeNodeChoiceField):
     def __init__(self, *args, **kwargs):
         self.base_level = kwargs.pop('base_level', 1)
@@ -100,7 +171,6 @@ class ForumFormBase(forms.ModelForm):
         data = super(ForumFormBase, self).clean()
 
         self.instance.set_name(data.get('name'))
-        self.instance.set_description(data.get('description'))
 
         if data['role'] != 'category':
             if not data['new_parent'].level:

+ 0 - 1
misago/forums/migrations/0001_initial.py

@@ -22,7 +22,6 @@ class Migration(migrations.Migration):
                 ('name', models.CharField(max_length=255)),
                 ('slug', models.SlugField(max_length=255)),
                 ('description', models.TextField(null=True, blank=True)),
-                ('description_as_html', models.TextField(null=True, blank=True)),
                 ('is_closed', models.BooleanField(default=False)),
                 ('redirect_url', models.CharField(max_length=255, null=True, blank=True)),
                 ('redirects', models.PositiveIntegerField(default=0)),

+ 3 - 8
misago/forums/models.py

@@ -13,7 +13,7 @@ from misago.acl.models import BaseRole
 from misago.core import serializer
 from misago.core.cache import cache
 from misago.core.signals import secret_key_changed
-from misago.core.utils import subset_markdown, slugify
+from misago.core.utils import slugify
 
 
 CACHE_NAME = 'misago_forums_tree'
@@ -46,7 +46,7 @@ class ForumManager(TreeManager):
             forums_dict[forum.pk] = forum
         return forums_dict
 
-    def clear_forums_cache(self):
+    def clear_cache(self):
         cache.delete(CACHE_NAME)
 
 
@@ -58,7 +58,6 @@ class Forum(MPTTModel):
     name = models.CharField(max_length=255)
     slug = models.SlugField(max_length=255)
     description = models.TextField(null=True, blank=True)
-    description_as_html = models.TextField(null=True, blank=True)
     is_closed = models.BooleanField(default=False)
     redirect_url = models.CharField(max_length=255, null=True, blank=True)
     redirects = models.PositiveIntegerField(default=0)
@@ -89,7 +88,7 @@ class Forum(MPTTModel):
         return super(Forum, self).save(*args, **kwargs)
 
     def delete(self, *args, **kwargs):
-        Forum.objects.clear_forums_cache()
+        Forum.objects.clear_cache()
         acl_version.invalidate()
         return super(Forum, self).delete(*args, **kwargs)
 
@@ -113,10 +112,6 @@ class Forum(MPTTModel):
         self.name = name
         self.slug = slugify(name)
 
-    def set_description(self, description):
-        self.description = description
-        self.description_as_html = subset_markdown(description)
-
     def has_child(self, child):
         return child.lft > self.lft and child.rght < self.rght
 

+ 4 - 4
misago/forums/views/forumsadmin.py

@@ -62,12 +62,12 @@ class ForumFormMixin(object):
                                       position='last-child')
             form.instance.save()
             if form.instance.parent_id != form.cleaned_data['new_parent'].pk:
-                Forum.objects.clear_forums_cache()
+                Forum.objects.clear_cache()
         else:
             form.instance.insert_at(form.cleaned_data['new_parent'],
                                     position='last-child',
                                     save=True)
-            Forum.objects.clear_forums_cache()
+            Forum.objects.clear_cache()
 
         if form.cleaned_data.get('copy_permissions'):
             form.instance.forum_role_set.all().delete()
@@ -132,7 +132,7 @@ class MoveDownForum(ForumAdmin, generic.ButtonView):
 
         if other_target:
             Forum.objects.move_node(target, other_target, 'right')
-            Forum.objects.clear_forums_cache()
+            Forum.objects.clear_cache()
 
             message = _('Forum "%s" has been moved below "%s".')
             targets_names = (target.name, other_target.name)
@@ -148,7 +148,7 @@ class MoveUpForum(ForumAdmin, generic.ButtonView):
 
         if other_target:
             Forum.objects.move_node(target, other_target, 'left')
-            Forum.objects.clear_forums_cache()
+            Forum.objects.clear_cache()
 
             message = _('Forum "%s" has been moved above "%s".')
             targets_names = (target.name, other_target.name)

+ 62 - 0
misago/templates/misago/admin/prefixes/form.html

@@ -0,0 +1,62 @@
+{% extends "misago/admin/generic/form.html" %}
+{% load i18n misago_forms %}
+
+
+{% block title %}
+{% if target.pk %}
+{{ target.name }}
+{% else %}
+{% trans "New prefix" %}
+{% endif %} | {{ active_link.name }} | {{ block.super }}
+{% endblock title %}
+
+
+{% block page-target %}
+{% if target.pk %}
+{{ target.name }}
+{% else %}
+{% trans "New prefix" %}
+{% endif %}
+{% endblock page-target %}
+
+
+{% block form-header %}
+<h1>
+  {% if target.pk %}
+  {{ target.name }}
+  {% else %}
+  {% trans "New prefix" %}
+  {% endif %}
+</h1>
+{% endblock %}
+
+
+{% block form-extra %}
+class="form-horizontal"
+{% endblock form-extra%}
+
+
+{% block form-body %}
+<div class="form-body">
+  {% with label_class="col-md-3" field_class="col-md-9" %}
+  <fieldset>
+    <legend>{% trans "Name and appearance" %}</legend>
+
+    {% form_row form.name label_class field_class %}
+    {% form_row form.css_class label_class field_class %}
+
+  </fieldset>
+  <fieldset>
+    <legend>{% trans "Availability" %}</legend>
+
+    {% form_row form.forums label_class field_class %}
+
+  </fieldset>
+  {% endwith %}
+</div>
+{% endblock form-body %}
+
+
+{% block form-footer-class %}
+col-md-offset-3
+{% endblock form-footer-class %}

+ 70 - 0
misago/templates/misago/admin/prefixes/list.html

@@ -0,0 +1,70 @@
+{% extends "misago/admin/generic/list.html" %}
+{% load i18n %}
+
+
+{% block page-actions %}
+<div class="page-actions">
+  <a href="{% url 'misago:admin:forums:prefixes:new' %}" class="btn btn-success">
+    <span class="fa fa-plus-circle"></span>
+    {% trans "New prefix" %}
+  </a>
+</div>
+{% endblock %}
+
+
+{% block table-header %}
+<th>{% trans "Prefix" %}</th>
+<th style="width: 40%;">{% trans "CSS class" %}</th>
+{% for action in extra_actions %}
+<th style="width: 1%;">&nbsp;</th>
+{% endfor %}
+<th style="width: 1%;">&nbsp;</th>
+<th style="width: 1%;">&nbsp;</th>
+{% endblock table-header %}
+
+
+{% block table-row %}
+<td class="item-name">
+  {{ item.name }}
+</td>
+<td>{% if item.css_class %}{{ item.css_class }}{% else %}&nbsp;{% endif %}</td>
+{% for action in extra_actions %}
+<td class="row-action">
+  <a href="{% url action.link prefix_id=item.id %}" class="btn btn-{% if action.style %}{{ action.style }}{% else %}default{% endif %} tooltip-top" title="{{ action.name }}">
+    <span class="{{ action.icon }}"></span>
+  </a>
+</td>
+{% endfor %}
+<td class="row-action">
+  <a href="{% url 'misago:admin:forums:prefixes:edit' prefix_id=item.id %}" class="btn btn-primary tooltip-top" title="{% trans "Edit" %}">
+    <span class="fa fa-pencil"></span>
+  </a>
+</td>
+<td class="row-action">
+  <form action="{% url 'misago:admin:forums:prefixes:delete' prefix_id=item.id %}" method="post" class="delete-prompt">
+    <button class="btn btn-danger tooltip-top" title="{% trans "Delete" %}">
+      {% csrf_token %}
+      <span class="fa fa-times"></span>
+    </button>
+  </form>
+</td>
+{% endblock %}
+
+
+{% block emptylist %}
+<td colspan="{{ 4|add:extra_actions_len }}">
+  <p>{% trans "No thread prefixes are currently defined." %}</p>
+</td>
+{% endblock emptylist %}
+
+
+{% block javascripts %}
+<script type="text/javascript">
+  $(function() {
+    $('.delete-prompt').submit(function() {
+      var decision = confirm("{% trans "Are you sure you want to delete this prefix?" %}");
+      return decision;
+    });
+  });
+</script>
+{% endblock %}

+ 3 - 3
misago/templates/misago/admin/warnings/list.html

@@ -14,9 +14,9 @@
 
 {% block table-header %}
 <th>{% trans "Warning level" %}</th>
-<th style="width: 240px">{% trans "Length" %}</th>
-<th style="width: 200px;">{% trans "Replying" %}</th>
-<th style="width: 200px;">{% trans "Starting threads" %}</th>
+<th style="width: 20%">{% trans "Length" %}</th>
+<th style="width: 20%;">{% trans "Replying" %}</th>
+<th style="width: 20%;">{% trans "Starting threads" %}</th>
 {% for action in extra_actions %}
 <th style="width: 1%;">&nbsp;</th>
 {% endfor %}

+ 25 - 0
misago/threads/admin.py

@@ -0,0 +1,25 @@
+from django.conf.urls import url
+from django.utils.translation import ugettext_lazy as _
+
+from misago.threads.views.prefixesadmin import (PrefixesList, NewPrefix,
+                                                EditPrefix, DeletePrefix)
+
+
+class MisagoAdminExtension(object):
+    def register_urlpatterns(self, urlpatterns):
+        # Threads Prefixes
+        urlpatterns.namespace(r'^prefixes/', 'prefixes', 'forums')
+        urlpatterns.patterns('forums:prefixes',
+            url(r'^$', PrefixesList.as_view(), name='index'),
+            url(r'^new/$', NewPrefix.as_view(), name='new'),
+            url(r'^edit/(?P<prefix_id>\d+)/$', EditPrefix.as_view(), name='edit'),
+            url(r'^delete/(?P<prefix_id>\d+)/$', DeletePrefix.as_view(), name='delete'),
+        )
+
+    def register_navigation_nodes(self, site):
+        site.add_node(name=_("Thread prefixes"),
+                      icon='fa fa-tags',
+                      parent='misago:admin:forums',
+                      after='misago:admin:forums:nodes:index',
+                      namespace='misago:admin:forums:prefixes',
+                      link='misago:admin:forums:prefixes:index')

+ 0 - 0
misago/threads/forms/__init__.py


+ 29 - 0
misago/threads/forms/admin.py

@@ -0,0 +1,29 @@
+from django.utils.translation import ugettext_lazy as _
+
+from misago.core import forms
+from misago.core.validators import validate_sluggable
+from misago.forums.forms import ForumsMultipleChoiceField
+from misago.forums.models import Forum
+
+from misago.threads.models import Prefix
+
+
+class PrefixForm(forms.ModelForm):
+    name = forms.CharField(
+        label=_("Prefix name"), validators=[validate_sluggable()])
+    css_class = forms.CharField(
+        label=_("CSS class"), required=False,
+        help_text=_("Optional CSS clas used to style this prefix."))
+    forums = ForumsMultipleChoiceField(
+        label=_('Forums'), required=False,
+        widget=forms.CheckboxSelectMultiple(),
+        help_text=_('Select forums this prefix will be available in.'))
+
+    class Meta:
+        model = Prefix
+        fields = ['name', 'css_class', 'forums']
+
+    def clean_name(self):
+        data = self.cleaned_data['name']
+        self.instance.set_name(data)
+        return data

+ 0 - 0
misago/threads/forms/posting.py


+ 27 - 0
misago/threads/migrations/0001_initial.py

@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('misago_forums', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Prefix',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('name', models.CharField(max_length=255)),
+                ('slug', models.SlugField(max_length=255)),
+                ('css_class', models.CharField(max_length=255, null=True, blank=True)),
+                ('forums', models.ManyToManyField(to='misago_forums.Forum')),
+            ],
+            options={
+            },
+            bases=(models.Model,),
+        ),
+    ]

+ 0 - 0
misago/threads/migrations/__init__.py


+ 3 - 0
misago/threads/models/__init__.py

@@ -0,0 +1,3 @@
+#from misago.threads.models.thread import Thread
+#from misago.threads.models.post import Post
+from misago.threads.models.prefix import Prefix

+ 7 - 0
misago/threads/models/post.py

@@ -0,0 +1,7 @@
+from django.db import models
+
+from misago.conf import settings
+
+
+class Post(object):#(models.Model):
+    pass

+ 55 - 0
misago/threads/models/prefix.py

@@ -0,0 +1,55 @@
+from django.db import models
+
+from misago.core.cache import cache
+from misago.core.utils import slugify
+
+
+CACHE_NAME = 'misago_threads_prefixes'
+
+
+class PrefixManager(models.Manager):
+    def get_forum_prefixes(self, forum):
+        prefixes = []
+        for prefix in self.get_cached_prefixes():
+            if forum.pk in prefix.forums_ids:
+                prefixes.append(prefix)
+        return prefixes
+
+    def get_cached_prefixes_dict(self):
+        prefixes_dict = {}
+        for prefix in self.get_cached_prefixes():
+            prefixes_dict[prefix.pk] = prefix
+        return prefixes_dict
+
+    def get_cached_prefixes(self):
+        prefixes = cache.get(CACHE_NAME, 'nada')
+        if prefixes == 'nada':
+            prefixes = []
+            prefixes_qs = self.all().prefetch_related('forums')
+            for prefix in prefixes_qs.order_by('name'):
+                prefix.forums_ids = [f.pk for f in prefix.forums.all()]
+                prefixes.append(prefix)
+            cache.set(CACHE_NAME, prefixes)
+        return prefixes
+
+    def clear_cache(self):
+        cache.delete(CACHE_NAME)
+
+
+class Prefix(models.Model):
+    forums = models.ManyToManyField('misago_forums.Forum',
+                                    related_name='prefixes')
+    name = models.CharField(max_length=255)
+    slug = models.SlugField(max_length=255)
+    css_class = models.CharField(max_length=255, null=True, blank=True)
+
+    objects = PrefixManager()
+
+
+    def delete(self, *args, **kwargs):
+        Prefix.objects.clear_cache()
+        return super(Prefix, self).delete(*args, **kwargs)
+
+    def set_name(self, name):
+        self.name = name
+        self.slug = slugify(name)

+ 29 - 0
misago/threads/models/thread.py

@@ -0,0 +1,29 @@
+from django.db import models
+
+from misago.conf import settings
+
+
+class Thread(object):#(models.Model):
+    forum = models.ForeignKey('Forum')
+    weight = models.PositiveIntegerField(default=0)
+    prefix = models.ForeignKey('ThreadPrefix', null=True, blank=True, on_delete=models.SET_NULL)
+    name = models.CharField(max_length=255)
+    slug = models.SlugField(max_length=255)
+    replies = models.PositiveIntegerField(default=0)
+    has_reported_posts = models.BooleanField(default=False)
+    has_moderated_posts = models.BooleanField(default=False)
+    has_deleted_posts = models.BooleanField(default=False)
+    start = models.DateTimeField()
+    start_post = models.ForeignKey('Post', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
+    start_poster = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
+    start_poster_name = models.CharField(max_length=255)
+    start_poster_slug = models.SlugField(max_length=255)
+    last = models.DateTimeField()
+    last_post = models.ForeignKey('Post', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
+    last_poster = models.ForeignKey('User', related_name='+', null=True, blank=True, on_delete=models.SET_NULL)
+    last_poster_name = models.CharField(max_length=255, null=True, blank=True)
+    last_poster_slug = models.SlugField(max_length=255, null=True, blank=True)
+    is_poll = models.BooleanField(default=False)
+    is_moderated = models.BooleanField(default=False)
+    is_hidden = models.BooleanField(default=False)
+    is_closed = models.BooleanField(default=False)

+ 0 - 0
misago/threads/tests/__init__.py


+ 51 - 0
misago/threads/tests/test_prefix_model.py

@@ -0,0 +1,51 @@
+from django.core.exceptions import ValidationError
+from django.test import TestCase
+
+from misago.forums.models import Forum
+
+from misago.threads.models import Prefix
+
+
+class PrefixesManagerTests(TestCase):
+    def setUp(self):
+        Prefix.objects.clear_cache()
+
+    def test_get_cached_prefixes(self):
+        """get_cached_prefixes and get_cached_prefixes_dict work as intented"""
+        test_prefixes = (
+            Prefix.objects.create(name="Prefix 1"),
+            Prefix.objects.create(name="Prefix 2"),
+            Prefix.objects.create(name="Prefix 3"),
+            Prefix.objects.create(name="Prefix 4"),
+        )
+
+        db_prefixes = Prefix.objects.get_cached_prefixes()
+        self.assertEqual(len(db_prefixes), len(test_prefixes))
+        for prefix in db_prefixes:
+            self.assertIn(prefix, test_prefixes)
+
+        db_prefixes = Prefix.objects.get_cached_prefixes_dict()
+        self.assertEqual(len(db_prefixes), len(test_prefixes))
+        for prefix in test_prefixes:
+            self.assertEqual(db_prefixes[prefix.pk], prefix)
+
+    def test_get_forum_prefixes(self):
+        """get_forum_prefixes returns prefixes for forum"""
+        forum = Forum.objects.all_forums().filter(role='forum')[:1][0]
+
+        test_prefixes = (
+            Prefix.objects.create(name="Prefix 1"),
+            Prefix.objects.create(name="Prefix 2"),
+            Prefix.objects.create(name="Prefix 3"),
+            Prefix.objects.create(name="Prefix 4"),
+        )
+
+        test_prefixes[0].forums.add(forum)
+        test_prefixes[2].forums.add(forum)
+
+        forum_prefixes = Prefix.objects.get_forum_prefixes(forum)
+        self.assertEqual(len(forum_prefixes), 2)
+        self.assertIn(test_prefixes[0], forum_prefixes)
+        self.assertIn(test_prefixes[2], forum_prefixes)
+        self.assertNotIn(test_prefixes[1], forum_prefixes)
+        self.assertNotIn(test_prefixes[3], forum_prefixes)

+ 108 - 0
misago/threads/tests/test_prefixesadmin_views.py

@@ -0,0 +1,108 @@
+from django.core.urlresolvers import reverse
+
+from misago.admin.testutils import AdminTestCase
+from misago.forums.models import Forum
+
+from misago.threads.models import Prefix
+
+
+class PrefixAdminViewsTests(AdminTestCase):
+    def test_link_registered(self):
+        """admin nav contains prefixes link"""
+        response = self.client.get(
+            reverse('misago:admin:forums:nodes:index'))
+        self.assertIn(reverse('misago:admin:forums:prefixes:index'),
+                      response.content)
+
+    def test_list_view(self):
+        """prefixes list view returns 200"""
+        response = self.client.get(
+            reverse('misago:admin:forums:prefixes:index'))
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('No thread prefixes', response.content)
+
+    def test_new_view(self):
+        """new prefix view has no showstoppers"""
+        response = self.client.get(
+            reverse('misago:admin:forums:prefixes:new'))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:admin:forums:prefixes:new'),
+            data={
+                'name': 'Test Prefix',
+                'css_class': 'test_prefix',
+                'forums': [f.pk for f in Forum.objects.all_forums()],
+            })
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.get(reverse('misago:admin:forums:prefixes:index'))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('Test Prefix', response.content)
+        self.assertIn('test_prefix', response.content)
+
+        test_prefix = Prefix.objects.get(slug='test-prefix')
+        self.assertEqual(len(test_prefix.forums.all()),
+                         len(Forum.objects.all_forums()))
+        for forum in Forum.objects.all_forums():
+            self.assertIn(forum, test_prefix.forums.all())
+
+    def test_edit_view(self):
+        """edit prefix view has no showstoppers"""
+        self.client.post(
+            reverse('misago:admin:forums:prefixes:new'),
+            data={
+                'name': 'Test Prefix',
+                'css_class': 'test_prefix',
+                'forums': [f.pk for f in Forum.objects.all_forums()],
+            })
+        test_prefix = Prefix.objects.get(slug='test-prefix')
+
+        response = self.client.get(
+            reverse('misago:admin:forums:prefixes:edit',
+                    kwargs={'prefix_id': test_prefix.pk}))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(test_prefix.name, response.content)
+        self.assertIn(test_prefix.css_class, response.content)
+
+        response = self.client.post(
+            reverse('misago:admin:forums:prefixes:edit',
+                    kwargs={'prefix_id': test_prefix.pk}),
+            data={
+                'name': 'Top Lel',
+                'css_class': 'test_lel',
+                'forums': [f.pk for f in Forum.objects.all_forums()],
+            })
+        self.assertEqual(response.status_code, 302)
+
+        test_prefix = Prefix.objects.get(slug='top-lel')
+        response = self.client.get(
+            reverse('misago:admin:forums:prefixes:index'))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(test_prefix.name, response.content)
+        self.assertIn(test_prefix.css_class, response.content)
+
+    def test_delete_view(self):
+        """delete prefix view has no showstoppers"""
+        self.client.post(
+            reverse('misago:admin:forums:prefixes:new'),
+            data={
+                'name': 'Test Prefix',
+                'css_class': 'test_prefix',
+                'forums': [f.pk for f in Forum.objects.all_forums()],
+            })
+        test_prefix = Prefix.objects.get(slug='test-prefix')
+
+        response = self.client.post(
+            reverse('misago:admin:forums:prefixes:delete',
+                    kwargs={'prefix_id': test_prefix.pk}))
+        self.assertEqual(response.status_code, 302)
+
+        self.client.get(reverse('misago:admin:forums:prefixes:index'))
+        response = self.client.get(
+            reverse('misago:admin:forums:prefixes:index'))
+        self.assertEqual(response.status_code, 200)
+
+        self.assertNotIn(test_prefix.name, response.content)
+        self.assertNotIn(test_prefix.css_class, response.content)

+ 0 - 0
misago/threads/views/__init__.py


+ 45 - 0
misago/threads/views/prefixesadmin.py

@@ -0,0 +1,45 @@
+from django.contrib import messages
+from django.utils.translation import ugettext_lazy as _
+
+from misago.admin.views import generic
+from misago.core import cachebuster
+
+from misago.threads.models import Prefix
+from misago.threads.forms.admin import PrefixForm
+
+
+class PrefixesAdmin(generic.AdminBaseMixin):
+    root_link = 'misago:admin:forums:prefixes:index'
+    Model = Prefix
+    Form = PrefixForm
+    templates_dir = 'misago/admin/prefixes'
+    message_404 = _("Requested thread prefix does not exist.")
+
+    def handle_form(self, form, request, target):
+        target.save()
+        target.forums.clear()
+        if form.cleaned_data.get('forums'):
+            target.forums.add(*[f for f in form.cleaned_data.get('forums')])
+        Prefix.objects.clear_cache()
+
+        if self.message_submit:
+            messages.success(request, self.message_submit % target.name)
+
+
+class PrefixesList(PrefixesAdmin, generic.ListView):
+    ordering = (('name', None),)
+
+
+class NewPrefix(PrefixesAdmin, generic.ModelFormView):
+    message_submit = _('New prefix "%s" has been saved.')
+
+
+class EditPrefix(PrefixesAdmin, generic.ModelFormView):
+    message_submit = _('Prefix "%s" has been edited.')
+
+
+class DeletePrefix(PrefixesAdmin, generic.ButtonView):
+    def button_action(self, request, target):
+        target.delete()
+        message = _('Prefix "%s" has been deleted.')
+        messages.success(request, message % unicode(target.name))