Browse Source

Merge pull request #729 from rafalp/constants-cleanup

fix #648
Rafał Pitoń 8 years ago
parent
commit
60871b4c9a
60 changed files with 318 additions and 292 deletions
  1. 0 1
      misago/acl/__init__.py
  2. 1 0
      misago/acl/constants.py
  3. 3 1
      misago/acl/migrations/0002_acl_version_tracker.py
  4. 8 2
      misago/acl/tests/test_api.py
  5. 6 6
      misago/acl/tests/test_providers.py
  6. 5 6
      misago/acl/version.py
  7. 7 5
      misago/admin/views/index.py
  8. 3 0
      misago/categories/__init__.py
  9. 2 0
      misago/categories/constants.py
  10. 2 1
      misago/categories/forms.py
  11. 2 2
      misago/categories/models.py
  12. 2 1
      misago/categories/tests/test_category_model.py
  13. 3 2
      misago/categories/views/categoriesadmin.py
  14. 1 1
      misago/core/errorpages.py
  15. 9 9
      misago/core/tests/test_apipatch.py
  16. 5 5
      misago/faker/management/commands/createfakebans.py
  17. 1 1
      misago/readtracker/signals.py
  18. 2 1
      misago/threads/api/postingendpoint/category.py
  19. 1 1
      misago/threads/api/postingendpoint/participants.py
  20. 2 1
      misago/threads/api/postingendpoint/privatethread.py
  21. 8 5
      misago/threads/api/postingendpoint/subscribe.py
  22. 1 1
      misago/threads/api/postingendpoint/syncprivatethreads.py
  23. 1 1
      misago/threads/api/postingendpoint/updatestats.py
  24. 2 1
      misago/threads/api/threadendpoints/editor.py
  25. 2 1
      misago/threads/api/threadendpoints/merge.py
  26. 2 1
      misago/threads/api/threadendpoints/read.py
  27. 1 1
      misago/threads/api/threads.py
  28. 2 19
      misago/threads/permissions/privatethreads.py
  29. 8 3
      misago/threads/tests/test_privatethread_start_api.py
  30. 13 10
      misago/threads/tests/test_subscription_middleware.py
  31. 5 2
      misago/threads/tests/test_subscriptions.py
  32. 2 1
      misago/threads/tests/test_thread_start_api.py
  33. 2 1
      misago/threads/tests/test_threads_api.py
  34. 1 1
      misago/threads/threadtypes/privatethread.py
  35. 1 1
      misago/threads/threadtypes/thread.py
  36. 2 1
      misago/threads/validators.py
  37. 2 1
      misago/threads/viewmodels/thread.py
  38. 2 2
      misago/users/api/rest_permissions.py
  39. 6 5
      misago/users/api/userendpoints/create.py
  40. 12 12
      misago/users/bans.py
  41. 1 0
      misago/users/constants.py
  42. 4 3
      misago/users/decorators.py
  43. 19 20
      misago/users/forms/admin.py
  44. 14 8
      misago/users/forms/options.py
  45. 3 1
      misago/users/migrations/0003_bans_version_tracker.py
  46. 22 32
      misago/users/models/ban.py
  47. 36 54
      misago/users/models/user.py
  48. 3 3
      misago/users/permissions/profiles.py
  49. 2 2
      misago/users/serializers/ban.py
  50. 2 2
      misago/users/tests/test_activation_views.py
  51. 6 6
      misago/users/tests/test_auth_api.py
  52. 13 4
      misago/users/tests/test_ban_model.py
  53. 14 12
      misago/users/tests/test_bans.py
  54. 7 5
      misago/users/tests/test_decorators.py
  55. 2 2
      misago/users/tests/test_forgottenpassword_views.py
  56. 7 5
      misago/users/tests/test_rest_permissions.py
  57. 6 4
      misago/users/tests/test_users_api.py
  58. 9 3
      misago/users/tests/test_validators.py
  59. 1 2
      misago/users/views/activation.py
  60. 7 8
      misago/users/views/admin/users.py

+ 0 - 1
misago/acl/__init__.py

@@ -1,4 +1,3 @@
 from .api import get_user_acl, add_acl, serialize_acl
 
-
 default_app_config = 'misago.acl.apps.MisagoACLsConfig'

+ 1 - 0
misago/acl/constants.py

@@ -0,0 +1 @@
+ACL_CACHEBUSTER = 'misago_acl'

+ 3 - 1
misago/acl/migrations/0002_acl_version_tracker.py

@@ -6,9 +6,11 @@ from django.db import migrations, models
 
 from misago.core.migrationutils import cachebuster_register_cache
 
+from misago.acl.constants import ACL_CACHEBUSTER
+
 
 def register_acl_version_tracker(apps, schema_editor):
-    cachebuster_register_cache(apps, 'misago_acl')
+    cachebuster_register_cache(apps, ACL_CACHEBUSTER)
 
 
 class Migration(migrations.Migration):

+ 8 - 2
misago/acl/tests/test_api.py

@@ -1,14 +1,20 @@
+from django.contrib.auth import get_user_model
 from django.test import TestCase
 
-from misago.users.models import AnonymousUser, User
+from misago.users.models import AnonymousUser
 
 from ..api import get_user_acl
 
 
+UserModel = get_user_model()
+
+
 class GetUserACLTests(TestCase):
     def test_get_authenticated_acl(self):
         """get ACL for authenticated user"""
-        test_user = User.objects.create_user('Bob', 'bob@bob.com', 'pass123')
+        test_user = UserModel.objects.create_user(
+            'Bob', 'bob@bob.com', 'pass123')
+
         acl = get_user_acl(test_user)
 
         self.assertTrue(acl)

+ 6 - 6
misago/acl/tests/test_providers.py

@@ -67,20 +67,20 @@ class PermissionProvidersTests(TestCase):
         """its possible to register and get annotators"""
         providers = PermissionProviders()
 
-        def test_annotator(*args):
+        def mock_annotator(*args):
             pass
 
-        providers.acl_annotator(TestType, test_annotator)
+        providers.acl_annotator(TestType, mock_annotator)
         annotators_list = providers.get_type_annotators(TestType())
-        self.assertEqual(annotators_list[0], test_annotator)
+        self.assertEqual(annotators_list[0], mock_annotator)
 
     def test_serializers(self):
         """its possible to register and get annotators"""
         providers = PermissionProviders()
 
-        def test_serializer(*args):
+        def mock_serializer(*args):
             pass
 
-        providers.acl_serializer(TestType, test_serializer)
+        providers.acl_serializer(TestType, mock_serializer)
         serializers_list = providers.get_type_serializers(TestType())
-        self.assertEqual(serializers_list[0], test_serializer)
+        self.assertEqual(serializers_list[0], mock_serializer)

+ 5 - 6
misago/acl/version.py

@@ -1,16 +1,15 @@
-from misago.core import cachebuster as cb
+from misago.core import cachebuster
 
-
-ACL_CACHE_NAME = 'misago_acl'
+from .constants import ACL_CACHEBUSTER
 
 
 def get_version():
-    return cb.get_version(ACL_CACHE_NAME)
+    return cachebuster.get_version(ACL_CACHEBUSTER)
 
 
 def is_valid(version):
-    return cb.is_valid(ACL_CACHE_NAME, version)
+    return cachebuster.is_valid(ACL_CACHEBUSTER, version)
 
 
 def invalidate():
-    cb.invalidate(ACL_CACHE_NAME)
+    cachebuster.invalidate(ACL_CACHEBUSTER)

+ 7 - 5
misago/admin/views/index.py

@@ -3,6 +3,7 @@ import json
 import requests
 from requests.exceptions import RequestException
 
+from django.contrib.auth import get_user_model
 from django.http import Http404, JsonResponse
 from django.utils.six.moves import range
 from django.utils.translation import ugettext as _
@@ -10,21 +11,22 @@ from django.utils.translation import ugettext as _
 from misago import __version__
 from misago.core.cache import cache
 from misago.threads.models import Post, Thread
-from misago.users.models import ACTIVATION_REQUIRED_NONE, User
 
 from . import render
 
-
 VERSION_CHECK_CACHE_KEY = "misago_version_check"
 
+UserModel = get_user_model()
+
 
 def admin_index(request):
-    inactive_users = {'requires_activation__gt': ACTIVATION_REQUIRED_NONE}
     db_stats = {
         'threads': Thread.objects.count(),
         'posts': Post.objects.count(),
-        'users': User.objects.count(),
-        'inactive_users': User.objects.filter(**inactive_users).count()
+        'users': UserModel.objects.count(),
+        'inactive_users': UserModel.objects.exclude(
+            requires_activation=UserModel.ACTIVATION_NONE
+        ).count()
     }
 
     return render(request, 'misago/admin/index.html', {

+ 3 - 0
misago/categories/__init__.py

@@ -1 +1,4 @@
+from .constants import *
+
+
 default_app_config = 'misago.categories.apps.MisagoCategoriesConfig'

+ 2 - 0
misago/categories/constants.py

@@ -0,0 +1,2 @@
+PRIVATE_THREADS_ROOT_NAME = 'private_threads'
+THREADS_ROOT_NAME = 'root_category'

+ 2 - 1
misago/categories/forms.py

@@ -9,7 +9,8 @@ from misago.core.forms import YesNoSwitch
 from misago.core.validators import validate_sluggable
 from misago.threads.threadtypes import trees_map
 
-from .models import THREADS_ROOT_NAME, Category, CategoryRole
+from . import THREADS_ROOT_NAME
+from .models import Category, CategoryRole
 
 
 """

+ 2 - 2
misago/categories/models.py

@@ -12,10 +12,10 @@ from misago.core.cache import cache
 from misago.core.utils import slugify
 from misago.threads.threadtypes import trees_map
 
+from . import PRIVATE_THREADS_ROOT_NAME, THREADS_ROOT_NAME
+
 
 CACHE_NAME = 'misago_categories_tree'
-PRIVATE_THREADS_ROOT_NAME = 'private_threads'
-THREADS_ROOT_NAME = 'root_category'
 
 
 class CategoryManager(TreeManager):

+ 2 - 1
misago/categories/tests/test_category_model.py

@@ -4,7 +4,8 @@ from misago.core.testutils import MisagoTestCase
 from misago.threads import testutils
 from misago.threads.threadtypes import trees_map
 
-from ..models import THREADS_ROOT_NAME, Category
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.models import Category
 
 
 class CategoryManagerTests(MisagoTestCase):

+ 3 - 2
misago/categories/views/categoriesadmin.py

@@ -6,8 +6,9 @@ from misago.acl import version as acl_version
 from misago.admin.views import generic
 from misago.threads.threadtypes import trees_map
 
-from ..forms import CategoryFormFactory, DeleteFormFactory
-from ..models import THREADS_ROOT_NAME, Category, RoleCategoryACL
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.forms import CategoryFormFactory, DeleteFormFactory
+from misago.categories.models import Category, RoleCategoryACL
 
 
 class CategoryAdmin(generic.AdminBaseMixin):

+ 1 - 1
misago/core/errorpages.py

@@ -24,7 +24,7 @@ def _error_page(request, code, message=None):
 
 def banned(request, ban):
     request.frontend_context.update({
-        'BAN_MESSAGE': ban.get_serialized_message(),
+        'MESSAGE': ban.get_serialized_message(),
         'CURRENT_LINK': 'misago:error-banned'
     })
 

+ 9 - 9
misago/core/tests/test_apipatch.py

@@ -21,40 +21,40 @@ class ApiPatchTests(TestCase):
         """add method adds function to patch object"""
         patch = ApiPatch()
 
-        def test_function():
+        def mock_function():
             pass
-        patch.add('test-add', test_function)
+        patch.add('test-add', mock_function)
 
         self.assertEqual(len(patch._actions), 1)
         self.assertEqual(patch._actions[0]['op'], 'add')
         self.assertEqual(patch._actions[0]['path'], 'test-add')
-        self.assertEqual(patch._actions[0]['handler'], test_function)
+        self.assertEqual(patch._actions[0]['handler'], mock_function)
 
     def test_remove(self):
         """remove method adds function to patch object"""
         patch = ApiPatch()
 
-        def test_function():
+        def mock_function():
             pass
-        patch.remove('test-remove', test_function)
+        patch.remove('test-remove', mock_function)
 
         self.assertEqual(len(patch._actions), 1)
         self.assertEqual(patch._actions[0]['op'], 'remove')
         self.assertEqual(patch._actions[0]['path'], 'test-remove')
-        self.assertEqual(patch._actions[0]['handler'], test_function)
+        self.assertEqual(patch._actions[0]['handler'], mock_function)
 
     def test_replace(self):
         """replace method adds function to patch object"""
         patch = ApiPatch()
 
-        def test_function():
+        def mock_function():
             pass
-        patch.replace('test-replace', test_function)
+        patch.replace('test-replace', mock_function)
 
         self.assertEqual(len(patch._actions), 1)
         self.assertEqual(patch._actions[0]['op'], 'replace')
         self.assertEqual(patch._actions[0]['path'], 'test-replace')
-        self.assertEqual(patch._actions[0]['handler'], test_function)
+        self.assertEqual(patch._actions[0]['handler'], mock_function)
 
     def test_validate_action(self):
         """validate_action method validates action dict"""

+ 5 - 5
misago/faker/management/commands/createfakebans.py

@@ -9,7 +9,7 @@ from django.utils import timezone
 from django.utils.six.moves import range
 
 from misago.core.management.progressbar import show_progress
-from misago.users.models import BAN_EMAIL, BAN_IP, BAN_USERNAME, Ban
+from misago.users.models import Ban
 
 
 def fake_username_ban(fake):
@@ -69,11 +69,11 @@ def fake_ip_ban(fake):
 
 
 def create_fake_test(fake, test_type):
-    if test_type == BAN_USERNAME:
+    if test_type == Ban.USERNAME:
         return fake_username_ban(fake)
-    elif test_type == BAN_EMAIL:
+    elif test_type == Ban.EMAIL:
         return fake_email_ban(fake)
-    elif test_type == BAN_IP:
+    elif test_type == Ban.IP:
         return fake_ip_ban(fake)
 
 
@@ -99,7 +99,7 @@ class Command(BaseCommand):
         created_count = 0
         show_progress(self, created_count, fake_bans_to_create)
         for i in range(fake_bans_to_create):
-            ban = Ban(check_type=random.randint(BAN_USERNAME, BAN_IP))
+            ban = Ban(check_type=random.randint(Ban.USERNAME, Ban.IP))
             ban.banned_value = create_fake_test(fake, ban.check_type)
 
             if random.randint(0, 10) == 0:

+ 1 - 1
misago/readtracker/signals.py

@@ -1,6 +1,6 @@
 from django.dispatch import Signal, receiver
 
-from misago.categories.models import PRIVATE_THREADS_ROOT_NAME
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
 from misago.categories.signals import delete_category_content, move_category_content
 from misago.threads.signals import move_thread
 

+ 2 - 1
misago/threads/api/postingendpoint/category.py

@@ -5,7 +5,8 @@ from django.utils.translation import ugettext_lazy
 from rest_framework import serializers
 
 from misago.acl import add_acl
-from misago.categories.models import THREADS_ROOT_NAME, Category
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.categories.permissions import can_browse_category, can_see_category
 
 from . import PostingEndpoint, PostingMiddleware

+ 1 - 1
misago/threads/api/postingendpoint/participants.py

@@ -6,7 +6,7 @@ from django.utils.translation import ungettext
 
 from rest_framework import serializers
 
-from misago.categories.models import PRIVATE_THREADS_ROOT_NAME
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
 
 from . import PostingEndpoint, PostingMiddleware
 from ...participants import add_participants, set_owner

+ 2 - 1
misago/threads/api/postingendpoint/privatethread.py

@@ -1,5 +1,6 @@
 from misago.acl import add_acl
-from misago.categories.models import PRIVATE_THREADS_ROOT_NAME, Category
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
+from misago.categories.models import Category
 
 from . import PostingEndpoint, PostingMiddleware
 

+ 8 - 5
misago/threads/api/postingendpoint/subscribe.py

@@ -1,9 +1,12 @@
-from misago.users.models import AUTO_SUBSCRIBE_NONE, AUTO_SUBSCRIBE_NOTIFY, AUTO_SUBSCRIBE_NOTIFY_AND_EMAIL
+from django.contrib.auth import get_user_model
 
 from . import PostingEndpoint, PostingMiddleware
 from ...models import Subscription
 
 
+UserModel = get_user_model()
+
+
 class SubscribeMiddleware(PostingMiddleware):
     def use_this_middleware(self):
         return self.mode != PostingEndpoint.EDIT
@@ -16,20 +19,20 @@ class SubscribeMiddleware(PostingMiddleware):
         if self.mode != PostingEndpoint.START:
             return
 
-        if self.user.subscribe_to_started_threads == AUTO_SUBSCRIBE_NONE:
+        if self.user.subscribe_to_started_threads == UserModel.SUBSCRIBE_NONE:
             return
 
         self.user.subscription_set.create(
             category=self.thread.category,
             thread=self.thread,
-            send_email=self.user.subscribe_to_started_threads == AUTO_SUBSCRIBE_NOTIFY_AND_EMAIL
+            send_email=self.user.subscribe_to_started_threads == UserModel.SUBSCRIBE_ALL
         )
 
     def subscribe_replied_thread(self):
         if self.mode != PostingEndpoint.REPLY:
             return
 
-        if self.user.subscribe_to_replied_threads == AUTO_SUBSCRIBE_NONE:
+        if self.user.subscribe_to_replied_threads == UserModel.SUBSCRIBE_NONE:
             return
 
         try:
@@ -45,5 +48,5 @@ class SubscribeMiddleware(PostingMiddleware):
         self.user.subscription_set.create(
             category=self.thread.category,
             thread=self.thread,
-            send_email=self.user.subscribe_to_replied_threads == AUTO_SUBSCRIBE_NOTIFY_AND_EMAIL
+            send_email=self.user.subscribe_to_replied_threads == UserModel.SUBSCRIBE_ALL
         )

+ 1 - 1
misago/threads/api/postingendpoint/syncprivatethreads.py

@@ -1,4 +1,4 @@
-from misago.categories.models import PRIVATE_THREADS_ROOT_NAME
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
 
 from ...participants import set_users_unread_private_threads_sync
 from . import PostingEndpoint, PostingMiddleware

+ 1 - 1
misago/threads/api/postingendpoint/updatestats.py

@@ -1,6 +1,6 @@
 from django.db.models import F
 
-from misago.categories.models import THREADS_ROOT_NAME
+from misago.categories import THREADS_ROOT_NAME
 
 from . import PostingEndpoint, PostingMiddleware
 

+ 2 - 1
misago/threads/api/threadendpoints/editor.py

@@ -4,7 +4,8 @@ from django.utils.translation import gettext as _
 from rest_framework.response import Response
 
 from misago.acl import add_acl
-from misago.categories.models import THREADS_ROOT_NAME, Category
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.models import Category
 
 from ...permissions.threads import can_start_thread
 from ...threadtypes import trees_map

+ 2 - 1
misago/threads/api/threadendpoints/merge.py

@@ -6,7 +6,8 @@ from django.utils.translation import ungettext
 from rest_framework.response import Response
 
 from misago.acl import add_acl
-from misago.categories.models import THREADS_ROOT_NAME, Category
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.models import Category
 
 from ...events import record_event
 from ...models import THREAD_WEIGHT_GLOBAL, Thread

+ 2 - 1
misago/threads/api/threadendpoints/read.py

@@ -1,4 +1,5 @@
-from misago.categories.models import THREADS_ROOT_NAME, Category
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.categories.permissions import allow_browse_category, allow_see_category
 from misago.core.shortcuts import get_int_or_404, get_object_or_404
 from misago.readtracker.categoriestracker import read_category

+ 1 - 1
misago/threads/api/threads.py

@@ -6,7 +6,7 @@ from rest_framework import viewsets
 from rest_framework.decorators import detail_route, list_route
 from rest_framework.response import Response
 
-from misago.categories.models import PRIVATE_THREADS_ROOT_NAME, THREADS_ROOT_NAME
+from misago.categories import PRIVATE_THREADS_ROOT_NAME, THREADS_ROOT_NAME
 from misago.core.shortcuts import get_int_or_404
 
 from ..models import Post, Thread

+ 2 - 19
misago/threads/permissions/privatethreads.py

@@ -8,30 +8,13 @@ from django.utils.translation import ugettext_lazy as _
 from misago.acl import add_acl, algebra
 from misago.acl.decorators import return_boolean
 from misago.acl.models import Role
-from misago.categories.models import PRIVATE_THREADS_ROOT_NAME, Category
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.core.forms import YesNoSwitch
 
 from ..models import Thread
 
 
-__all__ = [
-    'allow_use_private_threads',
-    'can_use_private_threads',
-    'allow_see_private_thread',
-    'can_see_private_thread',
-    'allow_change_owner',
-    'can_change_owner',
-    'allow_add_participants',
-    'can_add_participants',
-    'allow_remove_participant',
-    'can_remove_participant',
-    'allow_add_participant',
-    'can_add_participant',
-    'allow_message_user',
-    'can_message_user',
-]
-
-
 """
 Admin Permissions Form
 """

+ 8 - 3
misago/threads/tests/test_privatethread_start_api.py

@@ -10,7 +10,6 @@ from django.utils.encoding import smart_str
 
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
-from misago.users.models import LIMITS_PRIVATE_THREAD_INVITES_TO_FOLLOWED, LIMITS_PRIVATE_THREAD_INVITES_TO_NOBODY
 from misago.users.testutils import AuthenticatedUserTestCase
 
 from ..models import Thread, ThreadParticipant
@@ -194,7 +193,10 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
     def test_cant_invite_followers_only(self):
         """api validates that you cant invite followers-only user to thread"""
-        self.other_user.limits_private_thread_invites_to = LIMITS_PRIVATE_THREAD_INVITES_TO_FOLLOWED
+        User = get_user_model()
+
+        user_constant = User.LIMIT_INVITES_TO_FOLLOWED
+        self.other_user.limits_private_thread_invites_to = user_constant
         self.other_user.save()
 
         response = self.client.post(self.api_link, data={
@@ -245,7 +247,10 @@ class StartPrivateThreadTests(AuthenticatedUserTestCase):
 
     def test_cant_invite_anyone(self):
         """api validates that you cant invite nobody user to thread"""
-        self.other_user.limits_private_thread_invites_to = LIMITS_PRIVATE_THREAD_INVITES_TO_NOBODY
+        User = get_user_model()
+
+        user_constant = User.LIMIT_INVITES_TO_NOBODY
+        self.other_user.limits_private_thread_invites_to = user_constant
         self.other_user.save()
 
         response = self.client.post(self.api_link, data={

+ 13 - 10
misago/threads/tests/test_subscription_middleware.py

@@ -1,16 +1,19 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
+from django.contrib.auth import get_user_model
 from django.urls import reverse
 
 from misago.acl.testutils import override_acl
 from misago.categories.models import Category
-from misago.users.models import AUTO_SUBSCRIBE_NONE, AUTO_SUBSCRIBE_NOTIFY, AUTO_SUBSCRIBE_NOTIFY_AND_EMAIL
 from misago.users.testutils import AuthenticatedUserTestCase
 
 from .. import testutils
 
 
+UserModel = get_user_model()
+
+
 class SubscriptionMiddlewareTestCase(AuthenticatedUserTestCase):
     def setUp(self):
         super(SubscriptionMiddlewareTestCase, self).setUp()
@@ -37,8 +40,8 @@ class SubscribeStartedThreadTests(SubscriptionMiddlewareTestCase):
 
     def test_dont_subscribe(self):
         """middleware makes no subscription to thread"""
-        self.user.subscribe_to_started_threads = AUTO_SUBSCRIBE_NONE
-        self.user.subscribe_to_replied_threads = AUTO_SUBSCRIBE_NOTIFY
+        self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_NONE
+        self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NOTIFY
         self.user.save()
 
         response = self.client.post(self.api_link, data={
@@ -53,7 +56,7 @@ class SubscribeStartedThreadTests(SubscriptionMiddlewareTestCase):
 
     def test_subscribe(self):
         """middleware subscribes thread"""
-        self.user.subscribe_to_started_threads = AUTO_SUBSCRIBE_NOTIFY
+        self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_NOTIFY
         self.user.save()
 
         response = self.client.post(self.api_link, data={
@@ -72,7 +75,7 @@ class SubscribeStartedThreadTests(SubscriptionMiddlewareTestCase):
 
     def test_email_subscribe(self):
         """middleware subscribes thread with an email"""
-        self.user.subscribe_to_started_threads = AUTO_SUBSCRIBE_NOTIFY_AND_EMAIL
+        self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_ALL
         self.user.save()
 
         response = self.client.post(self.api_link, data={
@@ -100,8 +103,8 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
 
     def test_dont_subscribe(self):
         """middleware makes no subscription to thread"""
-        self.user.subscribe_to_started_threads = AUTO_SUBSCRIBE_NOTIFY
-        self.user.subscribe_to_replied_threads = AUTO_SUBSCRIBE_NONE
+        self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_NOTIFY
+        self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NONE
         self.user.save()
 
         response = self.client.post(self.api_link, data={
@@ -114,7 +117,7 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
 
     def test_subscribe(self):
         """middleware subscribes thread"""
-        self.user.subscribe_to_replied_threads = AUTO_SUBSCRIBE_NOTIFY
+        self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NOTIFY
         self.user.save()
 
         response = self.client.post(self.api_link, data={
@@ -130,7 +133,7 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
 
     def test_email_subscribe(self):
         """middleware subscribes thread with an email"""
-        self.user.subscribe_to_replied_threads = AUTO_SUBSCRIBE_NOTIFY_AND_EMAIL
+        self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_ALL
         self.user.save()
 
         response = self.client.post(self.api_link, data={
@@ -146,7 +149,7 @@ class SubscribeRepliedThreadTests(SubscriptionMiddlewareTestCase):
 
     def test_dont_subscribe_replied(self):
         """middleware omits threads user already replied"""
-        self.user.subscribe_to_replied_threads = AUTO_SUBSCRIBE_NOTIFY_AND_EMAIL
+        self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_ALL
         self.user.save()
 
         response = self.client.post(self.api_link, data={

+ 5 - 2
misago/threads/tests/test_subscriptions.py

@@ -12,13 +12,16 @@ from .. import testutils
 from ..subscriptions import make_subscription_aware
 
 
+UserModel = get_user_model()
+
+
 class SubscriptionsTests(TestCase):
     def setUp(self):
         self.category = list(Category.objects.all_categories()[:1])[0]
         self.thread = self.post_thread(timezone.now() - timedelta(days=10))
 
-        User = get_user_model()
-        self.user = User.objects.create_user("Bob", "bob@test.com", "Pass.123")
+        self.user = UserModel.objects.create_user(
+            "Bob", "bob@test.com", "Pass.123")
         self.anon = AnonymousUser()
 
     def post_thread(self, datetime):

+ 2 - 1
misago/threads/tests/test_thread_start_api.py

@@ -4,7 +4,8 @@ from __future__ import unicode_literals
 from django.urls import reverse
 
 from misago.acl.testutils import override_acl
-from misago.categories.models import THREADS_ROOT_NAME, Category
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.users.testutils import AuthenticatedUserTestCase
 
 from ..models import Thread

+ 2 - 1
misago/threads/tests/test_threads_api.py

@@ -1,7 +1,8 @@
 from django.urls import reverse
 
 from misago.acl.testutils import override_acl
-from misago.categories.models import THREADS_ROOT_NAME, Category
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.users.testutils import AuthenticatedUserTestCase
 
 from .. import testutils

+ 1 - 1
misago/threads/threadtypes/privatethread.py

@@ -1,7 +1,7 @@
 from django.urls import reverse
 from django.utils.translation import ugettext_lazy as _
 
-from misago.categories.models import PRIVATE_THREADS_ROOT_NAME
+from misago.categories import PRIVATE_THREADS_ROOT_NAME
 
 from . import ThreadType
 

+ 1 - 1
misago/threads/threadtypes/thread.py

@@ -1,7 +1,7 @@
 from django.urls import reverse
 from django.utils.translation import ugettext_lazy as _
 
-from misago.categories.models import THREADS_ROOT_NAME
+from misago.categories import THREADS_ROOT_NAME
 
 from . import ThreadType
 

+ 2 - 1
misago/threads/validators.py

@@ -2,7 +2,8 @@ from django.core.exceptions import ValidationError
 from django.utils.translation import ugettext as _
 from django.utils.translation import ungettext
 
-from misago.categories.models import THREADS_ROOT_NAME, Category
+from misago.categories import THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.categories.permissions import can_browse_category, can_see_category
 from misago.conf import settings
 from misago.core.validators import validate_sluggable

+ 2 - 1
misago/threads/viewmodels/thread.py

@@ -2,7 +2,8 @@ from django.shortcuts import get_object_or_404
 from django.utils.translation import gettext as _
 
 from misago.acl import add_acl
-from misago.categories.models import PRIVATE_THREADS_ROOT_NAME, THREADS_ROOT_NAME, Category
+from misago.categories import PRIVATE_THREADS_ROOT_NAME, THREADS_ROOT_NAME
+from misago.categories.models import Category
 from misago.core.shortcuts import validate_slug
 from misago.core.viewmodel import ViewModel as BaseViewModel
 from misago.readtracker.threadstracker import make_read_aware

+ 2 - 2
misago/users/api/rest_permissions.py

@@ -6,7 +6,7 @@ from rest_framework.permissions import BasePermission
 from misago.core.exceptions import Banned
 
 from ..bans import get_request_ip_ban
-from ..models import BAN_IP, Ban
+from ..models import Ban
 
 
 __all__ = [
@@ -20,7 +20,7 @@ class UnbannedOnly(BasePermission):
         ban = get_request_ip_ban(request)
         if ban:
             hydrated_ban = Ban(
-                check_type=BAN_IP,
+                check_type=Ban.IP,
                 user_message=ban['message'],
                 expires_on=ban['expires_on'])
             raise Banned(hydrated_ban)

+ 6 - 5
misago/users/api/userendpoints/create.py

@@ -11,11 +11,13 @@ from misago.core.mail import mail_user
 
 from ... import captcha
 from ...forms.register import RegisterForm
-from ...models import ACTIVATION_REQUIRED_ADMIN, ACTIVATION_REQUIRED_USER
 from ...serializers import AuthenticatedUserSerializer
 from ...tokens import make_activation_token
 
 
+UserModel = get_user_model()
+
+
 @csrf_protect
 def create_endpoint(request):
     if settings.account_activation == 'closed':
@@ -34,15 +36,14 @@ def create_endpoint(request):
     activation_kwargs = {}
     if settings.account_activation == 'user':
         activation_kwargs = {
-            'requires_activation': ACTIVATION_REQUIRED_USER
+            'requires_activation': UserModel.ACTIVATION_USER
         }
     elif settings.account_activation == 'admin':
         activation_kwargs = {
-            'requires_activation': ACTIVATION_REQUIRED_ADMIN
+            'requires_activation': UserModel.ACTIVATION_ADMIN
         }
 
-    User = get_user_model()
-    new_user = User.objects.create_user(
+    new_user = UserModel.objects.create_user(
         form.cleaned_data['username'],
         form.cleaned_data['email'],
         form.cleaned_data['password'],

+ 12 - 12
misago/users/bans.py

@@ -12,11 +12,11 @@ from django.utils.dateparse import parse_datetime
 
 from misago.core import cachebuster
 
-from .models import BAN_IP, Ban, BanCache
+from .models import Ban, BanCache
 
 
-BAN_CACHE_SESSION_KEY = 'misago_ip_check'
-BAN_VERSION_KEY = 'misago_bans'
+CACHE_SESSION_KEY = 'misago_ip_check'
+VERSION_KEY = 'misago_bans'
 
 
 def get_username_ban(username):
@@ -64,7 +64,7 @@ def get_user_ban(user):
 
 def _set_user_ban_cache(user):
     ban_cache = user.ban_cache
-    ban_cache.bans_version = cachebuster.get_version(BAN_VERSION_KEY)
+    ban_cache.bans_version = cachebuster.get_version(VERSION_KEY)
 
     try:
         user_ban = Ban.objects.get_ban(
@@ -102,8 +102,8 @@ def get_request_ip_ban(request):
 
     found_ban = get_ip_ban(request.user_ip)
 
-    ban_cache = request.session[BAN_CACHE_SESSION_KEY] = {
-        'version': cachebuster.get_version(BAN_VERSION_KEY),
+    ban_cache = request.session[CACHE_SESSION_KEY] = {
+        'version': cachebuster.get_version(VERSION_KEY),
         'ip': request.user_ip,
     }
 
@@ -117,21 +117,21 @@ def get_request_ip_ban(request):
                 'is_banned': True,
                 'message': found_ban.user_message
             })
-        request.session[BAN_CACHE_SESSION_KEY] = ban_cache
-        return _hydrate_session_cache(request.session[BAN_CACHE_SESSION_KEY])
+        request.session[CACHE_SESSION_KEY] = ban_cache
+        return _hydrate_session_cache(request.session[CACHE_SESSION_KEY])
     else:
         ban_cache['is_banned'] = False
-        request.session[BAN_CACHE_SESSION_KEY] = ban_cache
+        request.session[CACHE_SESSION_KEY] = ban_cache
         return None
 
 
 def _get_session_bancache(request):
     try:
-        ban_cache = request.session[BAN_CACHE_SESSION_KEY]
+        ban_cache = request.session[CACHE_SESSION_KEY]
         ban_cache = _hydrate_session_cache(ban_cache)
         if ban_cache['ip'] != request.user_ip:
             return None
-        if not cachebuster.is_valid(BAN_VERSION_KEY, ban_cache['version']):
+        if not cachebuster.is_valid(VERSION_KEY, ban_cache['version']):
             return None
         if ban_cache.get('expires_on'):
             """
@@ -177,7 +177,7 @@ def ban_ip(ip, user_message=None, staff_message=None, length=None,
         expires_on = timezone.now() + timedelta(**length)
 
     ban = Ban.objects.create(
-        check_type=BAN_IP,
+        check_type=Ban.IP,
         banned_value=ip,
         user_message=user_message,
         staff_message=staff_message,

+ 1 - 0
misago/users/constants.py

@@ -0,0 +1 @@
+BANS_CACHEBUSTER = 'misago_bans'

+ 4 - 3
misago/users/decorators.py

@@ -5,7 +5,7 @@ from django.utils.translation import gettext as _
 from misago.core.exceptions import Banned
 
 from .bans import get_request_ip_ban
-from .models import BAN_IP, Ban
+from .models import Ban
 
 
 def deny_authenticated(f):
@@ -33,9 +33,10 @@ def deny_banned_ips(f):
         ban = get_request_ip_ban(request)
         if ban:
             hydrated_ban = Ban(
-                check_type=BAN_IP,
+                check_type=Ban.IP,
                 user_message=ban['message'],
-                expires_on=ban['expires_on'])
+                expires_on=ban['expires_on']
+            )
             raise Banned(hydrated_ban)
         else:
             return f(request, *args, **kwargs)

+ 19 - 20
misago/users/forms/admin.py

@@ -10,13 +10,13 @@ from misago.core import threadstore
 from misago.core.forms import IsoDateTimeField, YesNoSwitch
 from misago.core.validators import validate_sluggable
 
-from ..models import (
-    AUTO_SUBSCRIBE_CHOICES, BANS_CHOICES, PRIVATE_THREAD_INVITES_LIMITS_CHOICES,
-    Ban, Rank
-)
+from ..models import Ban, Rank
 from ..validators import validate_email, validate_username
 
 
+UserModel = get_user_model()
+
+
 """
 Users
 """
@@ -26,7 +26,7 @@ class UserBaseForm(forms.ModelForm):
     email = forms.EmailField(label=_("E-mail address"))
 
     class Meta:
-        model = get_user_model()
+        model = UserModel
         fields = ['username', 'email', 'title']
 
     def clean_username(self):
@@ -65,7 +65,7 @@ class NewUserForm(UserBaseForm):
     )
 
     class Meta:
-        model = get_user_model()
+        model = UserModel
         fields = ['username', 'email', 'title']
 
 
@@ -164,22 +164,22 @@ class EditUserForm(UserBaseForm):
     limits_private_thread_invites_to = forms.TypedChoiceField(
         label=_("Who can add user to private threads"),
         coerce=int,
-        choices=PRIVATE_THREAD_INVITES_LIMITS_CHOICES
+        choices=UserModel.LIMIT_INVITES_TO_CHOICES
     )
 
     subscribe_to_started_threads = forms.TypedChoiceField(
         label=_("Started threads"),
         coerce=int,
-        choices=AUTO_SUBSCRIBE_CHOICES
+        choices=UserModel.SUBSCRIBE_CHOICES
     )
     subscribe_to_replied_threads = forms.TypedChoiceField(
         label=_("Replid threads"),
         coerce=int,
-        choices=AUTO_SUBSCRIBE_CHOICES
+        choices=UserModel.SUBSCRIBE_CHOICES
     )
 
     class Meta:
-        model = get_user_model()
+        model = UserModel
         fields = [
             'username',
             'email',
@@ -489,7 +489,7 @@ class BanForm(forms.ModelForm):
     check_type = forms.TypedChoiceField(
         label=_("Check type"),
         coerce=int,
-        choices=BANS_CHOICES
+        choices=Ban.CHOICES
     )
     banned_value = forms.CharField(
         label=_("Banned value"),
@@ -551,19 +551,18 @@ class BanForm(forms.ModelForm):
         return data
 
 
-SARCH_BANS_CHOICES = (
-    ('', _('All bans')),
-    ('names', _('Usernames')),
-    ('emails', _('E-mails')),
-    ('ips', _('IPs')),
-)
-
-
 class SearchBansForm(forms.Form):
+    SARCH_CHOICES = (
+        ('', _('All bans')),
+        ('names', _('Usernames')),
+        ('emails', _('E-mails')),
+        ('ips', _('IPs')),
+    )
+
     check_type = forms.ChoiceField(
         label=_("Type"),
         required=False,
-        choices=SARCH_BANS_CHOICES
+        choices=SARCH_CHOICES
     )
     value = forms.CharField(
         label=_("Banned value begins with"),

+ 14 - 8
misago/users/forms/options.py

@@ -7,24 +7,30 @@ from django.utils.translation import ungettext
 from misago.conf import settings
 from misago.core.forms import YesNoSwitch
 
-from ..models import AUTO_SUBSCRIBE_CHOICES, PRIVATE_THREAD_INVITES_LIMITS_CHOICES
 from ..validators import validate_email
 
 
+UserModel = get_user_model()
+
+
 class ForumOptionsForm(forms.ModelForm):
     is_hiding_presence = YesNoSwitch()
 
     limits_private_thread_invites_to = forms.TypedChoiceField(
-        coerce=int, choices=PRIVATE_THREAD_INVITES_LIMITS_CHOICES)
-
+        choices=UserModel.LIMIT_INVITES_TO_CHOICES,
+        coerce=int,
+    )
     subscribe_to_started_threads = forms.TypedChoiceField(
-        coerce=int, choices=AUTO_SUBSCRIBE_CHOICES)
-
+        choices=UserModel.SUBSCRIBE_CHOICES,
+        coerce=int,
+    )
     subscribe_to_replied_threads = forms.TypedChoiceField(
-        coerce=int, choices=AUTO_SUBSCRIBE_CHOICES)
+        choices=UserModel.SUBSCRIBE_CHOICES,
+        coerce=int,
+    )
 
     class Meta:
-        model = get_user_model()
+        model = UserModel
         fields = [
             'is_hiding_presence',
             'limits_private_thread_invites_to',
@@ -37,7 +43,7 @@ class EditSignatureForm(forms.ModelForm):
     signature = forms.CharField(required=False)
 
     class Meta:
-        model = get_user_model()
+        model = UserModel
         fields = ['signature']
 
     def clean(self):

+ 3 - 1
misago/users/migrations/0003_bans_version_tracker.py

@@ -5,9 +5,11 @@ from django.db import migrations, models
 
 from misago.core.migrationutils import cachebuster_register_cache
 
+from misago.users.constants import BANS_CACHEBUSTER
+
 
 def register_bans_version_tracker(apps, schema_editor):
-    cachebuster_register_cache(apps, 'misago_bans')
+    cachebuster_register_cache(apps, BANS_CACHEBUSTER)
 
 
 class Migration(migrations.Migration):

+ 22 - 32
misago/users/models/ban.py

@@ -7,26 +7,7 @@ from django.utils.translation import ugettext_lazy as _
 
 from misago.core import cachebuster
 
-
-__all__ = [
-    'BAN_USERNAME', 'BAN_EMAIL', 'BAN_IP', 'BANS_CHOICES',
-    'Ban', 'BanCache'
-]
-
-
-BAN_CACHEBUSTER = 'misago_bans'
-
-
-BAN_USERNAME = 0
-BAN_EMAIL = 1
-BAN_IP = 2
-
-
-BANS_CHOICES = (
-    (BAN_USERNAME, _('Username')),
-    (BAN_EMAIL, _('E-mail address')),
-    (BAN_IP, _('IP address')),
-)
+from misago.users.constants import BANS_CACHEBUSTER
 
 
 class BansManager(models.Manager):
@@ -40,19 +21,19 @@ class BansManager(models.Manager):
         return self.get_ban(email=email)
 
     def invalidate_cache(self):
-        cachebuster.invalidate(BAN_CACHEBUSTER)
+        cachebuster.invalidate(BANS_CACHEBUSTER)
 
     def get_ban(self, username=None, email=None, ip=None):
         checks = []
 
         if username:
             username = username.lower()
-            checks.append(BAN_USERNAME)
+            checks.append(self.model.USERNAME)
         if email:
             email = email.lower()
-            checks.append(BAN_EMAIL)
+            checks.append(self.model.EMAIL)
         if ip:
-            checks.append(BAN_IP)
+            checks.append(self.model.IP)
 
         queryset = self.filter(is_checked=True)
         if len(checks) == 1:
@@ -63,20 +44,30 @@ class BansManager(models.Manager):
         for ban in queryset.order_by('-id').iterator():
             if ban.is_expired:
                 continue
-            elif (ban.check_type == BAN_USERNAME and username and
+            elif (ban.check_type == self.model.USERNAME and username and
                     ban.check_value(username)):
                 return ban
-            elif (ban.check_type == BAN_EMAIL and email and
+            elif (ban.check_type == self.model.EMAIL and email and
                     ban.check_value(email)):
                 return ban
-            elif ban.check_type == BAN_IP and ip and ban.check_value(ip):
+            elif ban.check_type == self.model.IP and ip and ban.check_value(ip):
                 return ban
         else:
             raise Ban.DoesNotExist('specified values are not banned')
 
 
 class Ban(models.Model):
-    check_type = models.PositiveIntegerField(default=BAN_USERNAME, db_index=True)
+    USERNAME = 0
+    EMAIL = 1
+    IP = 2
+
+    CHOICES = (
+        (USERNAME, _('Username')),
+        (EMAIL, _('E-mail address')),
+        (IP, _('IP address')),
+    )
+
+    check_type = models.PositiveIntegerField(default=USERNAME, db_index=True)
     banned_value = models.CharField(max_length=255, db_index=True)
     user_message = models.TextField(null=True, blank=True)
     staff_message = models.TextField(null=True, blank=True)
@@ -97,7 +88,7 @@ class Ban(models.Model):
 
     @property
     def check_name(self):
-        return BANS_CHOICES[self.check_type][1]
+        return self.CHOICES[self.check_type][1]
 
     @property
     def name(self):
@@ -139,7 +130,7 @@ class BanCache(models.Model):
         from ..serializers import BanMessageSerializer
         temp_ban = Ban(
             id=1,
-            check_type=BAN_USERNAME,
+            check_type=Ban.USERNAME,
             user_message=self.user_message,
             staff_message=self.staff_message,
             expires_on=self.expires_on
@@ -152,8 +143,7 @@ class BanCache(models.Model):
 
     @property
     def is_valid(self):
-        version_is_valid = cachebuster.is_valid(BAN_CACHEBUSTER,
-                                                self.bans_version)
+        version_is_valid = cachebuster.is_valid(BANS_CACHEBUSTER, self.bans_version)
         expired = self.expires_on and self.expires_on < timezone.now()
 
         return version_is_valid and not expired

+ 36 - 54
misago/users/models/user.py

@@ -24,20 +24,6 @@ from .rank import Rank
 
 
 __all__ = [
-    'ACTIVATION_REQUIRED_NONE',
-    'ACTIVATION_REQUIRED_USER',
-    'ACTIVATION_REQUIRED_ADMIN',
-
-    'AUTO_SUBSCRIBE_NONE',
-    'AUTO_SUBSCRIBE_NOTIFY',
-    'AUTO_SUBSCRIBE_NOTIFY_AND_EMAIL',
-    'AUTO_SUBSCRIBE_CHOICES',
-
-    'LIMITS_PRIVATE_THREAD_INVITES_TO_NONE',
-    'LIMITS_PRIVATE_THREAD_INVITES_TO_FOLLOWED',
-    'LIMITS_PRIVATE_THREAD_INVITES_TO_NOBODY',
-    'PRIVATE_THREAD_INVITES_LIMITS_CHOICES',
-
     'AnonymousUser',
     'User',
     'UsernameChange',
@@ -45,34 +31,6 @@ __all__ = [
 ]
 
 
-ACTIVATION_REQUIRED_NONE = 0
-ACTIVATION_REQUIRED_USER = 1
-ACTIVATION_REQUIRED_ADMIN = 2
-
-
-AUTO_SUBSCRIBE_NONE = 0
-AUTO_SUBSCRIBE_NOTIFY = 1
-AUTO_SUBSCRIBE_NOTIFY_AND_EMAIL = 2
-
-AUTO_SUBSCRIBE_CHOICES = (
-    (AUTO_SUBSCRIBE_NONE, _("No")),
-    (AUTO_SUBSCRIBE_NOTIFY, _("Notify")),
-    (AUTO_SUBSCRIBE_NOTIFY_AND_EMAIL,
-     _("Notify with e-mail"))
-)
-
-
-LIMITS_PRIVATE_THREAD_INVITES_TO_NONE = 0
-LIMITS_PRIVATE_THREAD_INVITES_TO_FOLLOWED = 1
-LIMITS_PRIVATE_THREAD_INVITES_TO_NOBODY = 2
-
-PRIVATE_THREAD_INVITES_LIMITS_CHOICES = (
-    (LIMITS_PRIVATE_THREAD_INVITES_TO_NONE, _("Everybody")),
-    (LIMITS_PRIVATE_THREAD_INVITES_TO_FOLLOWED, _("Users I follow")),
-    (LIMITS_PRIVATE_THREAD_INVITES_TO_NOBODY, _("Nobody")),
-)
-
-
 class UserManager(BaseUserManager):
     @transaction.atomic
     def create_user(self, username, email, password=None, set_default_avatar=False, **extra_fields):
@@ -90,9 +48,9 @@ class UserManager(BaseUserManager):
             extra_fields['joined_from_ip'] = '127.0.0.1'
 
         WATCH_DICT = {
-            'no': AUTO_SUBSCRIBE_NONE,
-            'watch': AUTO_SUBSCRIBE_NOTIFY,
-            'watch_email': AUTO_SUBSCRIBE_NOTIFY_AND_EMAIL,
+            'no':  self.model.SUBSCRIBE_NONE,
+            'watch':  self.model.SUBSCRIBE_NOTIFY,
+            'watch_email':  self.model.SUBSCRIBE_ALL,
         }
 
         if not 'subscribe_to_started_threads' in extra_fields:
@@ -185,6 +143,30 @@ class UserManager(BaseUserManager):
 
 
 class User(AbstractBaseUser, PermissionsMixin):
+    ACTIVATION_NONE = 0
+    ACTIVATION_USER = 1
+    ACTIVATION_ADMIN = 2
+
+    SUBSCRIBE_NONE = 0
+    SUBSCRIBE_NOTIFY = 1
+    SUBSCRIBE_ALL = 2
+
+    SUBSCRIBE_CHOICES = (
+        (SUBSCRIBE_NONE, _("No")),
+        (SUBSCRIBE_NOTIFY, _("Notify")),
+        (SUBSCRIBE_ALL, _("Notify with e-mail"))
+    )
+
+    LIMIT_INVITES_TO_NONE = 0
+    LIMIT_INVITES_TO_FOLLOWED = 1
+    LIMIT_INVITES_TO_NOBODY = 2
+
+    LIMIT_INVITES_TO_CHOICES = (
+        (LIMIT_INVITES_TO_NONE, _("Everybody")),
+        (LIMIT_INVITES_TO_FOLLOWED, _("Users I follow")),
+        (LIMIT_INVITES_TO_NOBODY, _("Nobody")),
+    )
+
     """
     Note that "username" field is purely for shows.
     When searching users by their names, always use lowercased string
@@ -209,7 +191,7 @@ class User(AbstractBaseUser, PermissionsMixin):
 
     rank = models.ForeignKey('Rank', null=True, blank=True, on_delete=models.deletion.PROTECT)
     title = models.CharField(max_length=255, null=True, blank=True)
-    requires_activation = models.PositiveIntegerField(default=ACTIVATION_REQUIRED_NONE)
+    requires_activation = models.PositiveIntegerField(default=ACTIVATION_NONE)
 
     is_staff = models.BooleanField(_('staff status'),
         default=False,
@@ -268,16 +250,16 @@ class User(AbstractBaseUser, PermissionsMixin):
     )
 
     limits_private_thread_invites_to = models.PositiveIntegerField(
-        default=LIMITS_PRIVATE_THREAD_INVITES_TO_NONE
+        default=LIMIT_INVITES_TO_NONE
     )
     unread_private_threads = models.PositiveIntegerField(default=0)
     sync_unread_private_threads = models.BooleanField(default=False)
 
     subscribe_to_started_threads = models.PositiveIntegerField(
-        default=AUTO_SUBSCRIBE_NONE
+        default=SUBSCRIBE_NONE
     )
     subscribe_to_replied_threads = models.PositiveIntegerField(
-        default=AUTO_SUBSCRIBE_NONE
+        default=SUBSCRIBE_NONE
     )
 
     threads = models.PositiveIntegerField(default=0)
@@ -332,26 +314,26 @@ class User(AbstractBaseUser, PermissionsMixin):
 
     @property
     def requires_activation_by_admin(self):
-        return self.requires_activation == ACTIVATION_REQUIRED_ADMIN
+        return self.requires_activation == self.ACTIVATION_ADMIN
 
     @property
     def requires_activation_by_user(self):
-        return self.requires_activation == ACTIVATION_REQUIRED_USER
+        return self.requires_activation == self.ACTIVATION_USER
 
     @property
     def can_be_messaged_by_everyone(self):
         preference = self.limits_private_thread_invites_to
-        return preference == LIMITS_PRIVATE_THREAD_INVITES_TO_NONE
+        return preference == self.LIMIT_INVITES_TO_NONE
 
     @property
     def can_be_messaged_by_followed(self):
         preference = self.limits_private_thread_invites_to
-        return preference == LIMITS_PRIVATE_THREAD_INVITES_TO_FOLLOWED
+        return preference == self.LIMIT_INVITES_TO_FOLLOWED
 
     @property
     def can_be_messaged_by_nobody(self):
         preference = self.limits_private_thread_invites_to
-        return preference == LIMITS_PRIVATE_THREAD_INVITES_TO_NOBODY
+        return preference == self.LIMIT_INVITES_TO_NOBODY
 
     @property
     def has_valid_signature(self):

+ 3 - 3
misago/users/permissions/profiles.py

@@ -25,7 +25,7 @@ CAN_SEARCH_USERS = YesNoSwitch(
 CAN_SEE_USER_NAME_HISTORY = YesNoSwitch(
     label=_("Can see other members name history")
 )
-CAN_SEE_BAN_DETAILS = YesNoSwitch(
+CAN_SEE_DETAILS = YesNoSwitch(
     label=_("Can see members bans details"),
     help_text=_("Allows users with this permission to see user and staff ban messages.")
 )
@@ -37,7 +37,7 @@ class LimitedPermissionsForm(forms.Form):
     can_browse_users_list = CAN_BROWSE_USERS_LIST
     can_search_users = CAN_SEARCH_USERS
     can_see_users_name_history = CAN_SEE_USER_NAME_HISTORY
-    can_see_ban_details = CAN_SEE_BAN_DETAILS
+    can_see_ban_details = CAN_SEE_DETAILS
 
 
 class PermissionsForm(LimitedPermissionsForm):
@@ -52,7 +52,7 @@ class PermissionsForm(LimitedPermissionsForm):
         initial=0
     )
     can_see_users_name_history = CAN_SEE_USER_NAME_HISTORY
-    can_see_ban_details = CAN_SEE_BAN_DETAILS
+    can_see_ban_details = CAN_SEE_DETAILS
     can_see_users_emails = YesNoSwitch(
         label=_("Can see members e-mails")
     )

+ 2 - 2
misago/users/serializers/ban.py

@@ -4,7 +4,7 @@ from rest_framework import serializers
 
 from misago.core.utils import format_plaintext_for_html
 
-from ..models import BAN_IP, Ban
+from ..models import Ban
 
 
 __all__ = [
@@ -33,7 +33,7 @@ class BanMessageSerializer(serializers.ModelSerializer):
     def get_message(self, obj):
         if obj.user_message:
             message = obj.user_message
-        elif obj.check_type == BAN_IP:
+        elif obj.check_type == Ban.IP:
             message = _("Your IP address is banned.")
         else:
             message = _("You are banned.")

+ 2 - 2
misago/users/tests/test_activation_views.py

@@ -4,7 +4,7 @@ from django.urls import reverse
 
 from misago.core.utils import encode_json_html
 
-from ..models import BAN_USERNAME, Ban
+from ..models import Ban
 from ..tokens import make_activation_token
 
 
@@ -20,7 +20,7 @@ class ActivationViewsTests(TestCase):
         test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123',
                                              requires_activation=1)
         Ban.objects.create(
-            check_type=BAN_USERNAME,
+            check_type=Ban.USERNAME,
             banned_value='bob',
             user_message='Nope!',
         )

+ 6 - 6
misago/users/tests/test_auth_api.py

@@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model
 from django.core import mail
 from django.test import TestCase
 
-from ..models import BAN_USERNAME, Ban
+from ..models import Ban
 from ..tokens import make_password_change_token
 
 
@@ -51,7 +51,7 @@ class GatewayTests(TestCase):
         User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
 
         ban = Ban.objects.create(
-            check_type=BAN_USERNAME,
+            check_type=Ban.USERNAME,
             banned_value='bob',
             user_message='You are tragically banned.',
         )
@@ -84,7 +84,7 @@ class GatewayTests(TestCase):
         user.save()
 
         ban = Ban.objects.create(
-            check_type=BAN_USERNAME,
+            check_type=Ban.USERNAME,
             banned_value='bob',
             user_message='You are tragically banned.',
         )
@@ -192,7 +192,7 @@ class SendActivationAPITests(TestCase):
     def test_submit_banned(self):
         """request activation link api passes for banned users"""
         Ban.objects.create(
-            check_type=BAN_USERNAME,
+            check_type=Ban.USERNAME,
             banned_value=self.user.username,
             user_message='Nope!',
         )
@@ -271,7 +271,7 @@ class SendPasswordFormAPITests(TestCase):
     def test_submit_banned(self):
         """request change password form link api sends reset link mail"""
         Ban.objects.create(
-            check_type=BAN_USERNAME,
+            check_type=Ban.USERNAME,
             banned_value=self.user.username,
             user_message='Nope!',
         )
@@ -352,7 +352,7 @@ class ChangePasswordAPITests(TestCase):
     def test_banned_user_link(self):
         """request errors because user is banned"""
         Ban.objects.create(
-            check_type=BAN_USERNAME,
+            check_type=Ban.USERNAME,
             banned_value=self.user.username,
             user_message='Nope!',
         )

+ 13 - 4
misago/users/tests/test_ban_model.py

@@ -1,15 +1,24 @@
 #-*- coding: utf-8 -*-
 from django.test import TestCase
 
-from ..models import BAN_EMAIL, BAN_IP, BAN_USERNAME, Ban
+from ..models import Ban
 
 
 class BansManagerTests(TestCase):
     def setUp(self):
         Ban.objects.bulk_create([
-            Ban(check_type=BAN_USERNAME, banned_value='bob'),
-            Ban(check_type=BAN_EMAIL, banned_value='bob@test.com'),
-            Ban(check_type=BAN_IP, banned_value='127.0.0.1'),
+            Ban(
+                check_type=Ban.USERNAME,
+                banned_value='bob'
+            ),
+            Ban(
+                check_type=Ban.EMAIL,
+                banned_value='bob@test.com'
+            ),
+            Ban(
+                check_type=Ban.IP,
+                banned_value='127.0.0.1'
+            ),
         ])
 
     def test_get_ban_for_banned_name(self):

+ 14 - 12
misago/users/tests/test_bans.py

@@ -4,8 +4,10 @@ from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.utils import timezone
 
-from ..bans import ban_ip, ban_user, get_email_ban, get_ip_ban, get_request_ip_ban, get_user_ban, get_username_ban
-from ..models import BAN_EMAIL, BAN_IP, BAN_USERNAME, Ban
+from ..bans import (
+    ban_ip, ban_user, get_email_ban, get_ip_ban,
+    get_request_ip_ban, get_user_ban, get_username_ban)
+from ..models import Ban
 
 
 class GetBanTests(TestCase):
@@ -24,7 +26,7 @@ class GetBanTests(TestCase):
 
         Ban.objects.create(
             banned_value='wrongtype',
-            check_type=BAN_EMAIL
+            check_type=Ban.EMAIL
         )
 
         wrong_type_ban = get_username_ban('wrongtype')
@@ -43,7 +45,7 @@ class GetBanTests(TestCase):
 
         Ban.objects.create(
             banned_value='ex@pired.com',
-            check_type=BAN_EMAIL,
+            check_type=Ban.EMAIL,
             expires_on=timezone.now() - timedelta(days=7)
         )
 
@@ -52,7 +54,7 @@ class GetBanTests(TestCase):
 
         Ban.objects.create(
             banned_value='wrong@type.com',
-            check_type=BAN_IP
+            check_type=Ban.IP
         )
 
         wrong_type_ban = get_email_ban('wrong@type.com')
@@ -60,7 +62,7 @@ class GetBanTests(TestCase):
 
         valid_ban = Ban.objects.create(
             banned_value='*.ru',
-            check_type=BAN_EMAIL,
+            check_type=Ban.EMAIL,
             expires_on=timezone.now() + timedelta(days=7)
         )
         self.assertEqual(get_email_ban('banned@mail.ru').pk, valid_ban.pk)
@@ -72,7 +74,7 @@ class GetBanTests(TestCase):
 
         Ban.objects.create(
             banned_value='124.0.0.1',
-            check_type=BAN_IP,
+            check_type=Ban.IP,
             expires_on=timezone.now() - timedelta(days=7)
         )
 
@@ -81,7 +83,7 @@ class GetBanTests(TestCase):
 
         Ban.objects.create(
             banned_value='wrongtype',
-            check_type=BAN_EMAIL
+            check_type=Ban.EMAIL
         )
 
         wrong_type_ban = get_ip_ban('wrongtype')
@@ -89,7 +91,7 @@ class GetBanTests(TestCase):
 
         valid_ban = Ban.objects.create(
             banned_value='125.0.0.*',
-            check_type=BAN_IP,
+            check_type=Ban.IP,
             expires_on=timezone.now() + timedelta(days=7)
         )
         self.assertEqual(get_ip_ban('125.0.0.1').pk, valid_ban.pk)
@@ -173,7 +175,7 @@ class RequestIPBansTests(TestCase):
     def test_permanent_ban(self):
         """ip is caught by permanent ban"""
         Ban.objects.create(
-            check_type=BAN_IP,
+            check_type=Ban.IP,
             banned_value='127.0.0.1',
             user_message='User reason'
         )
@@ -189,7 +191,7 @@ class RequestIPBansTests(TestCase):
     def test_temporary_ban(self):
         """ip is caught by temporary ban"""
         Ban.objects.create(
-            check_type=BAN_IP,
+            check_type=Ban.IP,
             banned_value='127.0.0.1',
             user_message='User reason',
             expires_on=timezone.now() + timedelta(days=7)
@@ -206,7 +208,7 @@ class RequestIPBansTests(TestCase):
     def test_expired_ban(self):
         """ip is not caught by expired ban"""
         Ban.objects.create(
-            check_type=BAN_IP,
+            check_type=Ban.IP,
             banned_value='127.0.0.1',
             user_message='User reason',
             expires_on=timezone.now() - timedelta(days=7)

+ 7 - 5
misago/users/tests/test_decorators.py

@@ -2,7 +2,7 @@ from django.urls import reverse
 
 from misago.core.utils import encode_json_html
 
-from ..models import BAN_IP, Ban
+from ..models import Ban
 from ..testutils import UserTestCase
 
 
@@ -38,9 +38,10 @@ class DenyBannedIPTests(UserTestCase):
     def test_success(self):
         """deny_banned_ips decorator allowed unbanned request"""
         Ban.objects.create(
-            check_type=BAN_IP,
+            check_type=Ban.IP,
             banned_value='83.*',
-            user_message="Ya got banned!")
+            user_message="Ya got banned!"
+        )
 
         response = self.client.post(reverse('misago:request-activation'))
         self.assertEqual(response.status_code, 200)
@@ -48,9 +49,10 @@ class DenyBannedIPTests(UserTestCase):
     def test_fail(self):
         """deny_banned_ips decorator denied banned request"""
         Ban.objects.create(
-            check_type=BAN_IP,
+            check_type=Ban.IP,
             banned_value='127.*',
-            user_message="Ya got banned!")
+            user_message="Ya got banned!"
+        )
 
         response = self.client.post(reverse('misago:request-activation'))
         self.assertContains(

+ 2 - 2
misago/users/tests/test_forgottenpassword_views.py

@@ -3,7 +3,7 @@ from django.urls import reverse
 
 from misago.core.utils import encode_json_html
 
-from ..models import BAN_USERNAME, Ban
+from ..models import Ban
 from ..testutils import UserTestCase
 from ..tokens import make_password_change_token
 
@@ -27,7 +27,7 @@ class ForgottenPasswordViewsTests(UserTestCase):
         test_user = User.objects.create_user('Bob', 'bob@test.com', 'Pass.123')
 
         Ban.objects.create(
-            check_type=BAN_USERNAME,
+            check_type=Ban.USERNAME,
             banned_value='bob',
             user_message='Nope!',
         )

+ 7 - 5
misago/users/tests/test_rest_permissions.py

@@ -1,6 +1,6 @@
 from django.urls import reverse
 
-from ..models import BAN_IP, Ban
+from ..models import Ban
 from ..testutils import UserTestCase
 
 
@@ -29,9 +29,10 @@ class UnbannedOnlyTests(UserTestCase):
     def test_api_blocks_banned(self):
         """policy blocked banned ip"""
         Ban.objects.create(
-            check_type=BAN_IP,
+            check_type=Ban.IP,
             banned_value='127.*',
-            user_message='Ya got banned!')
+            user_message='Ya got banned!'
+        )
 
         response = self.client.post(
             reverse('misago:api:send-password-form'), data={
@@ -68,9 +69,10 @@ class UnbannedAnonOnlyTests(UserTestCase):
     def test_api_blocks_banned(self):
         """policy blocked banned ip"""
         Ban.objects.create(
-            check_type=BAN_IP,
+            check_type=Ban.IP,
             banned_value='127.*',
-            user_message='Ya got banned!')
+            user_message='Ya got banned!'
+        )
 
         response = self.client.post(
             reverse('misago:api:send-activation'), data={

+ 6 - 4
misago/users/tests/test_users_api.py

@@ -14,7 +14,7 @@ from misago.threads.models import Post, Thread
 from misago.threads.testutils import post_thread
 
 from ..activepostersranking import build_active_posters_ranking
-from ..models import BAN_USERNAME, Ban, Rank
+from ..models import Ban, Rank
 from ..testutils import AuthenticatedUserTestCase
 
 
@@ -439,9 +439,11 @@ class UserBanTests(AuthenticatedUserTestCase):
             'can_see_ban_details': 1
         })
 
-        Ban.objects.create(check_type=BAN_USERNAME,
-                           banned_value=self.other_user.username,
-                           user_message='Nope!')
+        Ban.objects.create(
+            check_type=Ban.USERNAME,
+            banned_value=self.other_user.username,
+            user_message='Nope!'
+        )
 
         response = self.client.get(self.link)
         self.assertEqual(response.status_code, 200)

+ 9 - 3
misago/users/tests/test_validators.py

@@ -5,7 +5,7 @@ from django.test import TestCase
 
 from misago.conf import settings
 
-from ..models import BAN_EMAIL, BAN_USERNAME, Ban
+from ..models import Ban
 from ..validators import (
     validate_email,
     validate_email_available,
@@ -39,7 +39,10 @@ class ValidateEmailAvailableTests(TestCase):
 
 class ValidateEmailBannedTests(TestCase):
     def setUp(self):
-        Ban.objects.create(check_type=BAN_EMAIL, banned_value="ban@test.com")
+        Ban.objects.create(
+            check_type=Ban.EMAIL,
+            banned_value="ban@test.com"
+        )
 
     def test_unbanned_name(self):
         """unbanned email passes validation"""
@@ -87,7 +90,10 @@ class ValidateUsernameAvailableTests(TestCase):
 
 class ValidateUsernameBannedTests(TestCase):
     def setUp(self):
-        Ban.objects.create(check_type=BAN_USERNAME, banned_value="Bob")
+        Ban.objects.create(
+            check_type=Ban.USERNAME,
+            banned_value="Bob"
+        )
 
     def test_unbanned_name(self):
         """unbanned name passes validation"""

+ 1 - 2
misago/users/views/activation.py

@@ -9,7 +9,6 @@ from misago.core.mail import mail_user
 
 from ..bans import get_user_ban
 from ..decorators import deny_authenticated, deny_banned_ips
-from ..models import ACTIVATION_REQUIRED_NONE
 from ..tokens import is_activation_token_valid
 
 
@@ -66,7 +65,7 @@ def activate_by_token(request, pk, token):
                 'message': e.args[0],
             }, status=400)
 
-    inactive_user.requires_activation = ACTIVATION_REQUIRED_NONE
+    inactive_user.requires_activation = User.ACTIVATION_NONE
     inactive_user.save(update_fields=['requires_activation'])
 
     message = _("%(user)s, your account has been activated!")

+ 7 - 8
misago/users/views/admin/users.py

@@ -17,8 +17,7 @@ from ...avatars.dynamic import set_avatar as set_dynamic_avatar
 from ...forms.admin import (
     BanUsersForm, NewUserForm, SearchUsersForm,
     EditUserForm, EditUserFormFactory)
-from ...models import ACTIVATION_REQUIRED_NONE, Ban, User
-from ...models.ban import BAN_EMAIL, BAN_IP, BAN_USERNAME
+from ...models import Ban, User
 from ...signatures import set_user_signature
 
 
@@ -108,7 +107,7 @@ class UsersList(UserAdmin, generic.ListView):
         else:
             activated_users_pks = [u.pk for u in inactive_users]
             queryset = User.objects.filter(pk__in=activated_users_pks)
-            queryset.update(requires_activation=ACTIVATION_REQUIRED_NONE)
+            queryset.update(requires_activation=User.ACTIVATION_NONE)
 
             subject = _("Your account on %(forum_name)s forums has been activated")
             mail_subject = subject % {
@@ -145,25 +144,25 @@ class UsersList(UserAdmin, generic.ListView):
                 for user in users:
                     for ban in cleaned_data['ban_type']:
                         if ban == 'usernames':
-                            check_type = BAN_USERNAME
+                            check_type = Ban.USERNAME
                             banned_value = user.username.lower()
 
                         if ban == 'emails':
-                            check_type = BAN_EMAIL
+                            check_type = Ban.EMAIL
                             banned_value = user.email.lower()
 
                         if ban == 'domains':
-                            check_type = BAN_EMAIL
+                            check_type = Ban.EMAIL
                             banned_value = user.email.lower()
                             at_pos = banned_value.find('@')
                             banned_value = '*%s' % banned_value[at_pos:]
 
                         if ban == 'ip':
-                            check_type = BAN_IP
+                            check_type = Ban.IP
                             banned_value = user.joined_from_ip
 
                         if ban in ('ip_first', 'ip_two'):
-                            check_type = BAN_IP
+                            check_type = Ban.IP
 
                             if ':' in user.joined_from_ip:
                                 ip_separator = ':'