Browse Source

fix #594: renamed user.acl to user.acl_cache and user.acl_ to user.acl

Rafał Pitoń 8 years ago
parent
commit
383346a0b7
69 changed files with 188 additions and 251 deletions
  1. 1 8
      docs/PermissionsFramework.md
  2. 3 9
      misago/acl/api.py
  3. 1 1
      misago/acl/panels.py
  4. 2 2
      misago/acl/tests/test_api.py
  5. 1 1
      misago/acl/testutils.py
  6. 2 2
      misago/categories/permissions.py
  7. 5 2
      misago/categories/utils.py
  8. 3 3
      misago/markup/flavours.py
  9. 3 2
      misago/readtracker/categoriestracker.py
  10. 1 1
      misago/search/api.py
  11. 1 1
      misago/search/tests/test_api.py
  12. 2 2
      misago/search/views.py
  13. 2 2
      misago/templates/misago/navbar.html
  14. 1 1
      misago/templates/misago/thread/posts/event/info.html
  15. 1 1
      misago/templates/misago/threadslist/tabs.html
  16. 2 2
      misago/threads/api/attachments.py
  17. 1 1
      misago/threads/api/postingendpoint/attachments.py
  18. 1 1
      misago/threads/api/postingendpoint/floodprotection.py
  19. 1 1
      misago/threads/api/postingendpoint/participants.py
  20. 1 1
      misago/threads/api/threadendpoints/editor.py
  21. 1 1
      misago/threads/api/threadendpoints/merge.py
  22. 1 1
      misago/threads/api/threadendpoints/patch.py
  23. 1 1
      misago/threads/api/threads.py
  24. 1 1
      misago/threads/middleware.py
  25. 2 1
      misago/threads/permissions/attachments.py
  26. 16 16
      misago/threads/permissions/polls.py
  27. 8 8
      misago/threads/permissions/privatethreads.py
  28. 19 19
      misago/threads/permissions/threads.py
  29. 1 1
      misago/threads/serializers/attachment.py
  30. 1 1
      misago/threads/serializers/post.py
  31. 1 1
      misago/threads/tests/test_attachments_api.py
  32. 1 1
      misago/threads/tests/test_attachmentview.py
  33. 2 2
      misago/threads/tests/test_emailnotification_middleware.py
  34. 1 1
      misago/threads/tests/test_floodprotection.py
  35. 2 2
      misago/threads/tests/test_gotoviews.py
  36. 1 1
      misago/threads/tests/test_post_mentions.py
  37. 1 1
      misago/threads/tests/test_privatethread_patch_api.py
  38. 1 1
      misago/threads/tests/test_privatethreads.py
  39. 1 1
      misago/threads/tests/test_subscription_middleware.py
  40. 1 1
      misago/threads/tests/test_thread_editreply_api.py
  41. 2 2
      misago/threads/tests/test_thread_merge_api.py
  42. 2 2
      misago/threads/tests/test_thread_patch_api.py
  43. 1 1
      misago/threads/tests/test_thread_poll_api.py
  44. 1 1
      misago/threads/tests/test_thread_postmerge_api.py
  45. 3 3
      misago/threads/tests/test_thread_postmove_api.py
  46. 1 1
      misago/threads/tests/test_thread_postpatch_api.py
  47. 3 3
      misago/threads/tests/test_thread_postsplit_api.py
  48. 1 1
      misago/threads/tests/test_thread_reply_api.py
  49. 1 1
      misago/threads/tests/test_thread_start_api.py
  50. 3 3
      misago/threads/tests/test_threads_api.py
  51. 1 1
      misago/threads/tests/test_threads_editor_api.py
  52. 5 4
      misago/threads/tests/test_threadslists.py
  53. 1 1
      misago/threads/tests/test_threadview.py
  54. 1 1
      misago/threads/viewmodels/category.py
  55. 5 3
      misago/threads/viewmodels/threads.py
  56. 1 1
      misago/threads/views/attachment.py
  57. 1 1
      misago/users/api/userendpoints/signature.py
  58. 3 3
      misago/users/api/usernamechanges.py
  59. 3 3
      misago/users/apps.py
  60. 0 57
      misago/users/forms/moderation.py
  61. 12 8
      misago/users/models/user.py
  62. 3 3
      misago/users/namechanges.py
  63. 5 5
      misago/users/permissions/delete.py
  64. 15 17
      misago/users/permissions/moderation.py
  65. 9 11
      misago/users/permissions/profiles.py
  66. 1 1
      misago/users/search.py
  67. 1 1
      misago/users/serializers/auth.py
  68. 4 5
      misago/users/serializers/user.py
  69. 1 1
      misago/users/views/profile.py

+ 1 - 8
docs/PermissionsFramework.md

@@ -20,7 +20,7 @@ if user.acl['forums'].get(forum.pk, {}).get('can_see'):
 Above snippet is edge example of checking forum permission, and luckily we have few alternatives:
 
 ```python
-if forum.pk in userl.acl['visible_forums']:
+if forum.pk in userl.acl_cache['visible_forums']:
     # Not really shorter, but simpler to remember and works in django templates!
 
 from misago.acl import add_acl
@@ -109,13 +109,6 @@ Annotators are functions called when object is being made ACL aware. It always r
 * `user` - user asking to make target aware of its ACL's
 * `target` - target instance, guaranteed to be an single object, not list or other iterable (like queryset)
 
-`target` has `acl` attribute which is dict with incomplete ACL that function should update with new keys.
-
-
-##### Note
-
-This will not work for instances of User model, that already reserves `acl` attribute for user's entire acl. `add_acl_to_target` for User instances will add acl's to `acl_` attribute instead.
-
 
 ### Serializers
 

+ 3 - 9
misago/acl/api.py

@@ -57,10 +57,7 @@ def _add_acl_to_target(user, target):
     """
     Add valid ACL to single target, helper for add_acl function
     """
-    if isinstance(target, get_user_model()):
-        target.acl_ = {}
-    else:
-        target.acl = {}
+    target.acl = {}
 
     for annotator in providers.get_type_annotators(target):
         annotator(user, target)
@@ -68,12 +65,9 @@ def _add_acl_to_target(user, target):
 
 def serialize_acl(target):
     """
-    Serialize single target's ACL
-
-    Serializers shouldn't really serialize ACL's, only prepare acl dict
-    for json serialization
+    Serialize authenticated user's ACL
     """
-    serialized_acl = copy.deepcopy(target.acl)
+    serialized_acl = copy.deepcopy(target.acl_cache)
 
     for serializer in providers.get_type_serializers(target):
         serializer(serialized_acl)

+ 1 - 1
misago/acl/panels.py

@@ -26,7 +26,7 @@ class MisagoACLPanel(Panel):
             misago_user = None
 
         try:
-            misago_acl = misago_user.acl
+            misago_acl = misago_user.acl_cache
         except AttributeError:
             misago_acl = {}
 

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

@@ -17,11 +17,11 @@ class GetUserACLTests(TestCase):
         acl = get_user_acl(test_user)
 
         self.assertTrue(acl)
-        self.assertEqual(acl, test_user.acl)
+        self.assertEqual(acl, test_user.acl_cache)
 
     def test_get_anonymous_acl(self):
         """get ACL for unauthenticated user"""
         acl = get_user_acl(AnonymousUser())
 
         self.assertTrue(acl)
-        self.assertEqual(acl, AnonymousUser().acl)
+        self.assertEqual(acl, AnonymousUser().acl_cache)

+ 1 - 1
misago/acl/testutils.py

@@ -26,7 +26,7 @@ def fake_post_data(target, data_dict):
 
 def override_acl(user, new_acl):
     """overrides user permissions with specified ones"""
-    final_cache = deepcopy(user.acl)
+    final_cache = deepcopy(user.acl_cache)
     final_cache.update(new_acl)
 
     if user.is_authenticated:

+ 2 - 2
misago/categories/permissions.py

@@ -127,13 +127,13 @@ def allow_see_category(user, target):
     except AttributeError:
         category_id = int(target)
 
-    if not category_id in user.acl['visible_categories']:
+    if not category_id in user.acl_cache['visible_categories']:
         raise Http404()
 can_see_category = return_boolean(allow_see_category)
 
 
 def allow_browse_category(user, target):
-    target_acl = user.acl['categories'].get(target.id, {'can_browse': False})
+    target_acl = user.acl_cache['categories'].get(target.id, {'can_browse': False})
     if not target_acl['can_browse']:
         message = _('You don\'t have permission to browse "%(category)s" contents.')
         raise PermissionDenied(message % {'category': target.name})

+ 5 - 2
misago/categories/utils.py

@@ -6,7 +6,7 @@ from .models import Category
 
 
 def get_categories_tree(user, parent=None):
-    if not user.acl['visible_categories']:
+    if not user.acl_cache['visible_categories']:
         return []
 
     if parent:
@@ -14,7 +14,10 @@ def get_categories_tree(user, parent=None):
     else:
         queryset = Category.objects.all_categories()
 
-    queryset_with_acl = queryset.filter(id__in=user.acl['visible_categories'])
+    queryset_with_acl = queryset.filter(
+        id__in=user.acl_cache['visible_categories']
+    )
+
     visible_categories = list(queryset_with_acl)
 
     categories_dict = {}

+ 3 - 3
misago/markup/flavours.py

@@ -32,8 +32,8 @@ def limited(request, text):
 
 def signature(request, owner, text):
     result = parse(text, request, owner, allow_mentions=False,
-                   allow_blocks=owner.acl['allow_signature_blocks'],
-                   allow_links=owner.acl['allow_signature_links'],
-                   allow_images=owner.acl['allow_signature_images'])
+                   allow_blocks=owner.acl_cache['allow_signature_blocks'],
+                   allow_links=owner.acl_cache['allow_signature_links'],
+                   allow_images=owner.acl_cache['allow_signature_images'])
 
     return result['parsed_text']

+ 3 - 2
misago/readtracker/categoriestracker.py

@@ -96,8 +96,9 @@ def sync_record(user, category):
 def read_category(user, category):
     categories = [category.pk]
     if not category.is_leaf_node():
-        queryset = category.get_descendants().filter(id__in=user.acl['visible_categories'])
-        categories += queryset.values_list('id', flat=True)
+        categories += category.get_descendants().filter(
+            id__in=user.acl_cache['visible_categories']
+        ).values_list('id', flat=True)
 
     user.categoryread_set.filter(category_id__in=categories).delete()
     user.threadread_set.filter(category_id__in=categories).delete()

+ 1 - 1
misago/search/api.py

@@ -16,7 +16,7 @@ from .searchproviders import searchproviders
 @api_view()
 def search(request, search_provider=None):
     allowed_providers = searchproviders.get_allowed_providers(request)
-    if not request.user.acl['can_search'] or not allowed_providers:
+    if not request.user.acl_cache['can_search'] or not allowed_providers:
         raise PermissionDenied(_("You don't have permission to search site."))
 
     search_query = get_search_query(request)

+ 1 - 1
misago/search/tests/test_api.py

@@ -39,7 +39,7 @@ class SearchApiTests(AuthenticatedUserTestCase):
             self.assertEqual(provider['results']['results'], [])
             self.assertEqual(int(provider['time']), 0)
 
-    def test_empty_(self):
+    def test_empty_search(self):
         """api handles empty search query"""
         response = self.client.get('%s?q=' % self.test_link)
         self.assertEqual(response.status_code, 200)

+ 2 - 2
misago/search/views.py

@@ -10,7 +10,7 @@ from .searchproviders import searchproviders
 
 def landing(request):
     allowed_providers = searchproviders.get_allowed_providers(request)
-    if not request.user.acl['can_search'] or not allowed_providers:
+    if not request.user.acl_cache['can_search'] or not allowed_providers:
         raise PermissionDenied(_("You don't have permission to search site."))
 
     default_provider = allowed_providers[0]
@@ -19,7 +19,7 @@ def landing(request):
 
 def search(request, search_provider):
     all_providers = searchproviders.get_providers(request)
-    if not request.user.acl['can_search'] or not all_providers:
+    if not request.user.acl_cache['can_search'] or not all_providers:
         raise PermissionDenied(_("You don't have permission to search site."))
 
     for provider in all_providers:

+ 2 - 2
misago/templates/misago/navbar.html

@@ -40,7 +40,7 @@
           {% trans "Users" %}
         </a>
       </li>
-      {% if user.acl.can_search %}
+      {% if user.acl_cache.can_search %}
       <li>
         <a href="{% url 'misago:search' %}">
           {% trans "Search" %}
@@ -100,7 +100,7 @@
         <i class="material-icon">group</i>
       </a>
     </li>
-    {% if user.acl.can_search %}
+    {% if user.acl_cache.can_search %}
       <li>
         <a href="{% url 'misago:search' %}">
           <i class="material-icon">search</i>

+ 1 - 1
misago/templates/misago/thread/posts/event/info.html

@@ -29,7 +29,7 @@
       By {{ event_by }} on {{ event_on }}.
     {% endblocktrans %}
   </li>
-  {% if user.acl.can_see_users_ips %}
+  {% if user.acl_cache.can_see_users_ips %}
     <li class="event-ip">
       <abbr title="{{ post.poster_ip }}">{% trans "IP recorded" %}</abbr>
     </li>

+ 1 - 1
misago/templates/misago/threadslist/tabs.html

@@ -32,7 +32,7 @@
           <span class="hidden-md hidden-lg">{% trans "Subscribed threads" %}</span>
         </a>
       </li>
-      {% if user.acl.can_see_unapproved_content_lists and not hide_unapproved %}
+      {% if user.acl_cache.can_see_unapproved_content_lists and not hide_unapproved %}
         <li{% if list_type == 'unapproved' %} class="active"{% endif %}>
           <a href="{{ category.get_absolute_url }}unapproved/">
             <span class="hidden-xs hidden-sm">{% trans "Unapproved" %}</span>

+ 2 - 2
misago/threads/api/attachments.py

@@ -15,7 +15,7 @@ IMAGE_EXTENSIONS = ('jpg', 'jpeg', 'png', 'gif')
 
 class AttachmentViewSet(viewsets.ViewSet):
     def create(self, request):
-        if not request.user.acl['max_attachment_size']:
+        if not request.user.acl_cache['max_attachment_size']:
             raise PermissionDenied(_("You don't have permission to upload new files."))
 
         try:
@@ -32,7 +32,7 @@ class AttachmentViewSet(viewsets.ViewSet):
 
         user_roles = set(r.pk for r in request.user.get_roles())
         filetype = validate_filetype(upload, user_roles)
-        validate_filesize(upload, filetype, request.user.acl['max_attachment_size'])
+        validate_filesize(upload, filetype, request.user.acl_cache['max_attachment_size'])
 
         attachment = Attachment(
             secret=Attachment.generate_new_secret(),

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

@@ -12,7 +12,7 @@ from . import PostingEndpoint, PostingInterrupt, PostingMiddleware
 
 class AttachmentsMiddleware(PostingMiddleware):
     def use_this_middleware(self):
-        return bool(self.user.acl['max_attachment_size'])
+        return bool(self.user.acl_cache['max_attachment_size'])
 
     def get_serializer(self):
         return AttachmentsSerializer(data=self.request.data, context={

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

@@ -13,7 +13,7 @@ MIN_POSTING_PAUSE = 3
 
 class FloodProtectionMiddleware(PostingMiddleware):
     def use_this_middleware(self):
-        return not self.user.acl['can_omit_flood_protection'] and self.mode != PostingEndpoint.EDIT
+        return not self.user.acl_cache['can_omit_flood_protection'] and self.mode != PostingEndpoint.EDIT
 
     def interrupt_posting(self, serializer):
         now = timezone.now()

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

@@ -57,7 +57,7 @@ class ParticipantsSerializer(serializers.Serializer):
         if not clean_usernames:
             raise serializers.ValidationError(_("You have to enter user names."))
 
-        max_participants = self.context['user'].acl['max_private_thread_participants']
+        max_participants = self.context['user'].acl_cache['max_private_thread_participants']
         if max_participants and len(clean_usernames) > max_participants:
             message = ungettext(
                 "You can't add more than %(users)s user to private thread (you've added %(added)s).",

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

@@ -19,7 +19,7 @@ def thread_start_editor(request):
     categories = []
 
     queryset = Category.objects.filter(
-        pk__in=request.user.acl['browseable_categories'],
+        pk__in=request.user.acl_cache['browseable_categories'],
         tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME)
     ).order_by('-lft')
 

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

@@ -224,7 +224,7 @@ def merge_threads(request, validated_data, threads, poll):
     # add top category to thread
     if validated_data.get('top_category'):
         categories = list(Category.objects.all_categories().filter(
-            id__in=request.user.acl['visible_categories']
+            id__in=request.user.acl_cache['visible_categories']
         ))
         add_categories_to_items(validated_data['top_category'], categories, [new_thread])
     else:

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

@@ -109,7 +109,7 @@ def patch_top_category(request, thread, value):
     )
 
     categories = list(Category.objects.all_categories().filter(
-        id__in=request.user.acl['visible_categories']
+        id__in=request.user.acl_cache['visible_categories']
     ))
     add_categories_to_items(root_category, categories, [thread])
     return {'top_category': CategorySerializer(thread.top_category).data}

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

@@ -125,7 +125,7 @@ class PrivateThreadViewSet(ViewSet):
     @transaction.atomic
     def create(self, request):
         allow_use_private_threads(request.user)
-        if not request.user.acl['can_start_private_threads']:
+        if not request.user.acl_cache['can_start_private_threads']:
             raise PermissionDenied(_("You can't start private threads."))
 
         # Initialize empty instances for new thread

+ 1 - 1
misago/threads/middleware.py

@@ -12,7 +12,7 @@ class UnreadThreadsCountMiddleware(MiddlewareMixin):
         if request.user.is_anonymous:
             return
 
-        if not request.user.acl['can_use_private_threads']:
+        if not request.user.acl_cache['can_use_private_threads']:
             return
 
         if not request.user.sync_unread_private_threads:

+ 2 - 1
misago/threads/permissions/attachments.py

@@ -67,8 +67,9 @@ def add_acl_to_attachment(user, attachment):
             'can_delete': True,
         })
     else:
+        user_can_delete = user.acl_cache['can_delete_other_users_attachments']
         attachment.acl.update({
-            'can_delete': user.is_authenticated and user.acl['can_delete_other_users_attachments'],
+            'can_delete': user.is_authenticated and user_can_delete,
         })
 
 

+ 16 - 16
misago/threads/permissions/polls.py

@@ -131,13 +131,13 @@ def allow_start_poll(user, target):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to start polls."))
 
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_close_threads': False,
     })
 
-    if not user.acl.get('can_start_polls'):
+    if not user.acl_cache.get('can_start_polls'):
         raise PermissionDenied(_("You can't start polls."))
-    if user.acl.get('can_start_polls') < 2 and user.pk != target.starter_id:
+    if user.acl_cache.get('can_start_polls') < 2 and user.pk != target.starter_id:
         raise PermissionDenied(_("You can't start polls in other users threads."))
 
     if not category_acl.get('can_close_threads'):
@@ -152,22 +152,22 @@ def allow_edit_poll(user, target):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to edit polls."))
 
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_close_threads': False,
     })
 
-    if not user.acl.get('can_edit_polls'):
+    if not user.acl_cache.get('can_edit_polls'):
         raise PermissionDenied(_("You can't edit polls."))
 
-    if user.acl.get('can_edit_polls') < 2:
+    if user.acl_cache.get('can_edit_polls') < 2:
         if user.pk != target.poster_id:
             raise PermissionDenied(_("You can't edit other users polls in this category."))
         if not has_time_to_edit_poll(user, target):
             message = ungettext(
                 "You can't edit polls that are older than %(minutes)s minute.",
                 "You can't edit polls that are older than %(minutes)s minutes.",
-                user.acl['poll_edit_time'])
-            raise PermissionDenied(message % {'minutes': user.acl['poll_edit_time']})
+                user.acl_cache['poll_edit_time'])
+            raise PermissionDenied(message % {'minutes': user.acl_cache['poll_edit_time']})
 
         if target.is_over:
             raise PermissionDenied(_("This poll is over. You can't edit it."))
@@ -184,22 +184,22 @@ def allow_delete_poll(user, target):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to delete polls."))
 
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_close_threads': False,
     })
 
-    if not user.acl.get('can_delete_polls'):
+    if not user.acl_cache.get('can_delete_polls'):
         raise PermissionDenied(_("You can't delete polls."))
 
-    if user.acl.get('can_delete_polls') < 2:
+    if user.acl_cache.get('can_delete_polls') < 2:
         if user.pk != target.poster_id:
             raise PermissionDenied(_("You can't delete other users polls in this category."))
         if not has_time_to_edit_poll(user, target):
             message = ungettext(
                 "You can't delete polls that are older than %(minutes)s minute.",
                 "You can't delete polls that are older than %(minutes)s minutes.",
-                user.acl['poll_edit_time'])
-            raise PermissionDenied(message % {'minutes': user.acl['poll_edit_time']})
+                user.acl_cache['poll_edit_time'])
+            raise PermissionDenied(message % {'minutes': user.acl_cache['poll_edit_time']})
         if target.is_over:
             raise PermissionDenied(_("This poll is over. You can't delete it."))
 
@@ -220,7 +220,7 @@ def allow_vote_poll(user, target):
     if target.is_over:
         raise PermissionDenied(_("This poll is over. You can't vote in it."))
 
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_close_threads': False,
     })
 
@@ -233,13 +233,13 @@ can_vote_poll = return_boolean(allow_vote_poll)
 
 
 def allow_see_poll_votes(user, target):
-    if not target.is_public and not user.acl['can_always_see_poll_voters']:
+    if not target.is_public and not user.acl_cache['can_always_see_poll_voters']:
         raise PermissionDenied(_("You dont have permission to this poll's voters."))
 can_see_poll_votes = return_boolean(allow_see_poll_votes)
 
 
 def has_time_to_edit_poll(user, target):
-    edit_time = user.acl['poll_edit_time']
+    edit_time = user.acl_cache['poll_edit_time']
     if edit_time:
         diff = timezone.now() - target.posted_on
         diff_minutes = int(diff.total_seconds() / 60)

+ 8 - 8
misago/threads/permissions/privatethreads.py

@@ -177,13 +177,13 @@ ACL tests
 def allow_use_private_threads(user):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to use private threads."))
-    if not user.acl['can_use_private_threads']:
+    if not user.acl_cache['can_use_private_threads']:
         raise PermissionDenied(_("You can't use private threads."))
 can_use_private_threads = return_boolean(allow_use_private_threads)
 
 
 def allow_see_private_thread(user, target):
-    if user.acl['can_moderate_private_threads']:
+    if user.acl_cache['can_moderate_private_threads']:
         can_see_reported = target.has_reported_posts
     else:
         can_see_reported = False
@@ -196,7 +196,7 @@ can_see_private_thread = return_boolean(allow_see_private_thread)
 
 
 def allow_change_owner(user, target):
-    is_moderator = user.acl['can_moderate_private_threads']
+    is_moderator = user.acl_cache['can_moderate_private_threads']
     is_owner = target.participant and target.participant.is_owner
 
     if not (is_owner or is_moderator):
@@ -210,7 +210,7 @@ can_change_owner = return_boolean(allow_change_owner)
 
 
 def allow_add_participants(user, target):
-    is_moderator = user.acl['can_moderate_private_threads']
+    is_moderator = user.acl_cache['can_moderate_private_threads']
 
     if not is_moderator:
         if not target.participant or not target.participant.is_owner:
@@ -221,7 +221,7 @@ def allow_add_participants(user, target):
             raise PermissionDenied(
                 _("Only moderators can add participants to closed threads."))
 
-    max_participants = user.acl['max_private_thread_participants']
+    max_participants = user.acl_cache['max_private_thread_participants']
     current_participants = len(target.participants_list) - 1
 
     if current_participants >= max_participants:
@@ -231,7 +231,7 @@ can_add_participants = return_boolean(allow_add_participants)
 
 
 def allow_remove_participant(user, thread, target):
-    if user.acl['can_moderate_private_threads']:
+    if user.acl_cache['can_moderate_private_threads']:
         return
 
     if user == target:
@@ -254,10 +254,10 @@ def allow_add_participant(user, target):
         raise PermissionDenied(
             _("%(user)s can't participate in private threads.") % message_format)
 
-    if user.acl['can_add_everyone_to_private_threads']:
+    if user.acl_cache['can_add_everyone_to_private_threads']:
         return
 
-    if user.acl['can_be_blocked'] and target.is_blocking(user):
+    if user.acl_cache['can_be_blocked'] and target.is_blocking(user):
         raise PermissionDenied(_("%(user)s is blocking you.") % message_format)
 
     if target.can_be_messaged_by_nobody:

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

@@ -325,7 +325,7 @@ def build_category_acl(acl, category, categories_roles, key_name):
 ACL's for targets
 """
 def add_acl_to_category(user, category):
-    category_acl = user.acl['categories'].get(category.pk, {})
+    category_acl = user.acl_cache['categories'].get(category.pk, {})
 
     category.acl.update({
         'can_see_all_threads': 0,
@@ -390,7 +390,7 @@ def add_acl_to_category(user, category):
 
 
 def add_acl_to_thread(user, thread):
-    category_acl = user.acl['categories'].get(thread.category_id, {})
+    category_acl = user.acl_cache['categories'].get(thread.category_id, {})
 
     thread.acl.update({
         'can_reply': can_reply_thread(user, thread),
@@ -434,7 +434,7 @@ def add_acl_to_post(user, post):
 
 def add_acl_to_event(user, event):
     if user.is_authenticated:
-        category_acl = user.acl['categories'].get(event.category_id, {})
+        category_acl = user.acl_cache['categories'].get(event.category_id, {})
         can_hide_events = category_acl.get('can_hide_events', 0)
     else:
         can_hide_events = 0
@@ -447,7 +447,7 @@ def add_acl_to_event(user, event):
 
 
 def add_acl_to_reply(user, post):
-    category_acl = user.acl['categories'].get(post.category_id, {})
+    category_acl = user.acl_cache['categories'].get(post.category_id, {})
 
     post.acl.update({
         'can_reply': can_reply_thread(user, post.thread),
@@ -481,7 +481,7 @@ def register_with(registry):
 ACL tests
 """
 def allow_see_thread(user, target):
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_see': False,
         'can_browse': False
     })
@@ -505,7 +505,7 @@ def allow_start_thread(user, target):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to start threads."))
 
-    category_acl = user.acl['categories'].get(target.pk, {
+    category_acl = user.acl_cache['categories'].get(target.pk, {
         'can_close_threads': False,
         'can_start_threads': False
     })
@@ -522,7 +522,7 @@ def allow_reply_thread(user, target):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to reply threads."))
 
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_close_threads': False,
         'can_reply_threads': False
     })
@@ -542,7 +542,7 @@ def allow_edit_thread(user, target):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to edit threads."))
 
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_edit_threads': False
     })
 
@@ -569,7 +569,7 @@ can_edit_thread = return_boolean(allow_edit_thread)
 
 
 def allow_see_post(user, target):
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_approve_content': False,
         'can_hide_events': False
     })
@@ -593,7 +593,7 @@ def allow_edit_post(user, target):
     if target.is_event:
         raise PermissionDenied(_("Events can't be edited."))
 
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_edit_posts': False
     })
 
@@ -629,7 +629,7 @@ def allow_unhide_post(user, target):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to reveal posts."))
 
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_hide_posts': 0,
         'can_hide_own_posts': 0
     })
@@ -666,7 +666,7 @@ def allow_hide_post(user, target):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to hide posts."))
 
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_hide_posts': 0,
         'can_hide_own_posts': 0
     })
@@ -703,7 +703,7 @@ def allow_delete_post(user, target):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to delete posts."))
 
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_hide_posts': 0,
         'can_hide_own_posts': 0
     })
@@ -740,7 +740,7 @@ def allow_protect_post(user, target):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to protect posts."))
 
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_protect_posts': False
     })
 
@@ -755,7 +755,7 @@ def allow_approve_post(user, target):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to approve posts."))
 
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_approve_content': False
     })
 
@@ -772,7 +772,7 @@ def allow_move_post(user, target):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to move posts."))
 
-    category_acl = user.acl['categories'].get(target.category_id, {
+    category_acl = user.acl_cache['categories'].get(target.category_id, {
         'can_move_posts': False
     })
 
@@ -791,7 +791,7 @@ def allow_delete_event(user, target):
     if user.is_anonymous:
         raise PermissionDenied(_("You have to sign in to delete events."))
 
-    category_acl = user.acl['categories'].get(target.category_id)
+    category_acl = user.acl_cache['categories'].get(target.category_id)
 
     if not category_acl or category_acl['can_hide_events'] != 2:
         raise PermissionDenied(_("You can't delete events in this category."))
@@ -812,7 +812,7 @@ def can_change_owned_thread(user, target):
 
 
 def has_time_to_edit_thread(user, target):
-    edit_time = user.acl['categories'].get(target.category_id, {}).get('thread_edit_time', 0)
+    edit_time = user.acl_cache['categories'].get(target.category_id, {}).get('thread_edit_time', 0)
     if edit_time:
         diff = timezone.now() - target.started_on
         diff_minutes = int(diff.total_seconds() / 60)
@@ -823,7 +823,7 @@ def has_time_to_edit_thread(user, target):
 
 
 def has_time_to_edit_post(user, target):
-    edit_time = user.acl['categories'].get(target.category_id, {}).get('post_edit_time', 0)
+    edit_time = user.acl_cache['categories'].get(target.category_id, {}).get('post_edit_time', 0)
     if edit_time:
         diff = timezone.now() - target.posted_on
         diff_minutes = int(diff.total_seconds() / 60)

+ 1 - 1
misago/threads/serializers/attachment.py

@@ -50,7 +50,7 @@ class AttachmentSerializer(serializers.ModelSerializer):
         return obj.filetype.name
 
     def get_uploader_ip(self, obj):
-        if 'user' in self.context and self.context['user'].acl['can_see_users_ips']:
+        if 'user' in self.context and self.context['user'].acl_cache['can_see_users_ips']:
             return obj.uploader_ip
         else:
             return None

+ 1 - 1
misago/threads/serializers/post.py

@@ -71,7 +71,7 @@ class PostSerializer(serializers.ModelSerializer, MutableFields):
         ]
 
     def get_poster_ip(self, obj):
-        if self.context['user'].acl['can_see_users_ips']:
+        if self.context['user'].acl_cache['can_see_users_ips']:
             return obj.poster_ip
         else:
             return None

+ 1 - 1
misago/threads/tests/test_attachments_api.py

@@ -32,7 +32,7 @@ class AttachmentsApiTestCase(AuthenticatedUserTestCase):
 
     def override_acl(self, new_acl=None):
         if new_acl:
-            acl = self.user.acl.copy()
+            acl = self.user.acl_cache.copy()
             acl.update(new_acl)
             override_acl(self.user, acl)
 

+ 1 - 1
misago/threads/tests/test_attachmentview.py

@@ -39,7 +39,7 @@ class AttachmentViewTestCase(AuthenticatedUserTestCase):
         self.override_acl()
 
     def override_acl(self, allow_download=True):
-        acl = self.user.acl.copy()
+        acl = self.user.acl_cache.copy()
         acl.update({
             'max_attachment_size': 1000,
             'can_download_other_users_attachments': allow_download

+ 2 - 2
misago/threads/tests/test_emailnotification_middleware.py

@@ -38,7 +38,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
             'Bob', 'bob@boberson.com', 'pass123')
 
     def override_acl(self):
-        new_acl = deepcopy(self.user.acl)
+        new_acl = deepcopy(self.user.acl_cache)
         new_acl['categories'][self.category.pk].update({
             'can_see': 1,
             'can_browse': 1,
@@ -50,7 +50,7 @@ class EmailNotificationTests(AuthenticatedUserTestCase):
         override_acl(self.user, new_acl)
 
     def override_other_user_acl(self, hide=False):
-        new_acl = deepcopy(self.other_user.acl)
+        new_acl = deepcopy(self.other_user.acl_cache)
         new_acl['categories'][self.category.pk].update({
             'can_see': 1,
             'can_browse': 1,

+ 1 - 1
misago/threads/tests/test_floodprotection.py

@@ -22,7 +22,7 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         })
 
     def override_acl(self):
-        new_acl = self.user.acl
+        new_acl = self.user.acl_cache
         new_acl['categories'][self.category.pk].update({
             'can_see': 1,
             'can_browse': 1,

+ 2 - 2
misago/threads/tests/test_gotoviews.py

@@ -178,8 +178,8 @@ class GotoNewTests(GotoViewTestCase):
 
 class GotoUnapprovedTests(GotoViewTestCase):
     def grant_permission(self):
-        self.user.acl['categories'][self.category.pk]['can_approve_content'] = 1
-        override_acl(self.user, self.user.acl)
+        self.user.acl_cache['categories'][self.category.pk]['can_approve_content'] = 1
+        override_acl(self.user, self.user.acl_cache)
 
     def test_view_validates_permission(self):
         """view validates permission to see unapproved posts"""

+ 1 - 1
misago/threads/tests/test_post_mentions.py

@@ -28,7 +28,7 @@ class PostMentionsTests(AuthenticatedUserTestCase):
         })
 
     def override_acl(self):
-        new_acl = self.user.acl
+        new_acl = self.user.acl_cache
         new_acl['categories'][self.category.pk].update({
             'can_see': 1,
             'can_browse': 1,

+ 1 - 1
misago/threads/tests/test_privatethread_patch_api.py

@@ -101,7 +101,7 @@ class PrivateThreadAddParticipantApiTests(PrivateThreadPatchApiTestCase):
         """can't add user that is already participant"""
         ThreadParticipant.objects.set_owner(self.thread, self.user)
 
-        for i in range(self.user.acl['max_private_thread_participants']):
+        for i in range(self.user.acl_cache['max_private_thread_participants']):
             user = UserModel.objects.create_user(
                 'User{}'.format(i), 'user{}@example.com'.format(i), 'Pass.123')
             ThreadParticipant.objects.add_participants(self.thread, [user])

+ 1 - 1
misago/threads/tests/test_privatethreads.py

@@ -15,7 +15,7 @@ class PrivateThreadsTestCase(AuthenticatedUserTestCase):
         self.override_acl()
 
     def override_acl(self, acl=None):
-        final_acl = self.user.acl['categories'][self.category.pk]
+        final_acl = self.user.acl_cache['categories'][self.category.pk]
         final_acl.update({
             'can_see': 1,
             'can_browse': 1,

+ 1 - 1
misago/threads/tests/test_subscription_middleware.py

@@ -20,7 +20,7 @@ class SubscriptionMiddlewareTestCase(AuthenticatedUserTestCase):
         self.override_acl()
 
     def override_acl(self):
-        new_acl = self.user.acl
+        new_acl = self.user.acl_cache
         new_acl['can_omit_flood_protection'] = True
         new_acl['categories'][self.category.pk].update({
             'can_see': 1,

+ 1 - 1
misago/threads/tests/test_thread_editreply_api.py

@@ -28,7 +28,7 @@ class EditReplyTests(AuthenticatedUserTestCase):
         })
 
     def override_acl(self, extra_acl=None):
-        new_acl = self.user.acl
+        new_acl = self.user.acl_cache
         new_acl['categories'][self.category.pk].update({
             'can_see': 1,
             'can_browse': 1,

+ 2 - 2
misago/threads/tests/test_thread_merge_api.py

@@ -24,7 +24,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         self.api_link = reverse('misago:api:thread-merge', kwargs={'pk': self.thread.pk})
 
     def override_other_acl(self, acl=None):
-        other_category_acl = self.user.acl['categories'][self.category.pk].copy()
+        other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
         other_category_acl.update({
             'can_see': 1,
             'can_browse': 1,
@@ -41,7 +41,7 @@ class ThreadMergeApiTests(ThreadsApiTestCase):
         if acl:
             other_category_acl.update(acl)
 
-        categories_acl = self.user.acl['categories']
+        categories_acl = self.user.acl_cache['categories']
         categories_acl[self.category_b.pk] = other_category_acl
 
         visible_categories = [self.category.pk]

+ 2 - 2
misago/threads/tests/test_thread_patch_api.py

@@ -270,7 +270,7 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
         self.category_b = Category.objects.get(slug='category-b')
 
     def override_other_acl(self, acl):
-        other_category_acl = self.user.acl['categories'][self.category.pk].copy()
+        other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
         other_category_acl.update({
             'can_see': 1,
             'can_browse': 1,
@@ -281,7 +281,7 @@ class ThreadMoveApiTests(ThreadPatchApiTestCase):
         })
         other_category_acl.update(acl)
 
-        categories_acl = self.user.acl['categories']
+        categories_acl = self.user.acl_cache['categories']
         categories_acl[self.category_b.pk] = other_category_acl
 
         visible_categories = [self.category.pk]

+ 1 - 1
misago/threads/tests/test_thread_poll_api.py

@@ -27,7 +27,7 @@ class ThreadPollApiTestCase(AuthenticatedUserTestCase):
         return self.client.put(url, json.dumps(data or {}), content_type='application/json')
 
     def override_acl(self, user=None, category=None):
-        new_acl = self.user.acl
+        new_acl = self.user.acl_cache
         new_acl['categories'][self.category.pk].update({
             'can_see': 1,
             'can_browse': 1,

+ 1 - 1
misago/threads/tests/test_thread_postmerge_api.py

@@ -35,7 +35,7 @@ class ThreadPostMergeApiTestCase(AuthenticatedUserTestCase):
         self.thread = Thread.objects.get(pk=self.thread.pk)
 
     def override_acl(self, extra_acl=None):
-        new_acl = self.user.acl
+        new_acl = self.user.acl_cache
         new_acl['categories'][self.category.pk].update({
             'can_see': 1,
             'can_browse': 1,

+ 3 - 3
misago/threads/tests/test_thread_postmove_api.py

@@ -38,7 +38,7 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         self.thread = Thread.objects.get(pk=self.thread.pk)
 
     def override_acl(self, extra_acl=None):
-        new_acl = self.user.acl
+        new_acl = self.user.acl_cache
         new_acl['categories'][self.category.pk].update({
             'can_see': 1,
             'can_browse': 1,
@@ -55,7 +55,7 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         override_acl(self.user, new_acl)
 
     def override_other_acl(self, acl=None):
-        other_category_acl = self.user.acl['categories'][self.category.pk].copy()
+        other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
         other_category_acl.update({
             'can_see': 1,
             'can_browse': 1,
@@ -69,7 +69,7 @@ class ThreadPostMoveApiTestCase(AuthenticatedUserTestCase):
         if acl:
             other_category_acl.update(acl)
 
-        categories_acl = self.user.acl['categories']
+        categories_acl = self.user.acl_cache['categories']
         categories_acl[self.category_b.pk] = other_category_acl
 
         visible_categories = [self.category.pk]

+ 1 - 1
misago/threads/tests/test_thread_postpatch_api.py

@@ -34,7 +34,7 @@ class ThreadPostPatchApiTestCase(AuthenticatedUserTestCase):
         self.post = self.thread.post_set.get(pk=self.post.pk)
 
     def override_acl(self, extra_acl=None):
-        new_acl = self.user.acl
+        new_acl = self.user.acl_cache
         new_acl['categories'][self.category.pk].update({
             'can_see': 1,
             'can_browse': 1,

+ 3 - 3
misago/threads/tests/test_thread_postsplit_api.py

@@ -43,7 +43,7 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         self.thread = Thread.objects.get(pk=self.thread.pk)
 
     def override_acl(self, extra_acl=None):
-        new_acl = self.user.acl
+        new_acl = self.user.acl_cache
         new_acl['categories'][self.category.pk].update({
             'can_see': 1,
             'can_browse': 1,
@@ -60,7 +60,7 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         override_acl(self.user, new_acl)
 
     def override_other_acl(self, acl=None):
-        other_category_acl = self.user.acl['categories'][self.category.pk].copy()
+        other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy()
         other_category_acl.update({
             'can_see': 1,
             'can_browse': 1,
@@ -74,7 +74,7 @@ class ThreadPostSplitApiTestCase(AuthenticatedUserTestCase):
         if acl:
             other_category_acl.update(acl)
 
-        categories_acl = self.user.acl['categories']
+        categories_acl = self.user.acl_cache['categories']
         categories_acl[self.category_b.pk] = other_category_acl
 
         visible_categories = [self.category.pk]

+ 1 - 1
misago/threads/tests/test_thread_reply_api.py

@@ -25,7 +25,7 @@ class ReplyThreadTests(AuthenticatedUserTestCase):
         })
 
     def override_acl(self, extra_acl=None):
-        new_acl = self.user.acl
+        new_acl = self.user.acl_cache
         new_acl['categories'][self.category.pk].update({
             'can_see': 1,
             'can_browse': 1,

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

@@ -21,7 +21,7 @@ class StartThreadTests(AuthenticatedUserTestCase):
         self.api_link = reverse('misago:api:thread-list')
 
     def override_acl(self, extra_acl=None):
-        new_acl = self.user.acl
+        new_acl = self.user.acl_cache
         new_acl['categories'][self.category.pk].update({
             'can_see': 1,
             'can_browse': 1,

+ 3 - 3
misago/threads/tests/test_threads_api.py

@@ -22,7 +22,7 @@ class ThreadsApiTestCase(AuthenticatedUserTestCase):
         self.api_link = self.thread.get_api_url()
 
     def override_acl(self, acl=None):
-        final_acl = self.user.acl['categories'][self.category.pk]
+        final_acl = self.user.acl_cache['categories'][self.category.pk]
         final_acl.update({
             'can_see': 1,
             'can_browse': 1,
@@ -39,8 +39,8 @@ class ThreadsApiTestCase(AuthenticatedUserTestCase):
         if acl:
             final_acl.update(acl)
 
-        visible_categories = self.user.acl['visible_categories']
-        browseable_categories = self.user.acl['browseable_categories']
+        visible_categories = self.user.acl_cache['visible_categories']
+        browseable_categories = self.user.acl_cache['browseable_categories']
 
         if not final_acl['can_see'] and self.category.pk in visible_categories:
             visible_categories.remove(self.category.pk)

+ 1 - 1
misago/threads/tests/test_threads_editor_api.py

@@ -24,7 +24,7 @@ class EditorApiTestCase(AuthenticatedUserTestCase):
         self.category = Category.objects.get(slug='first-category')
 
     def override_acl(self, acl=None):
-        final_acl = self.user.acl['categories'][self.category.pk]
+        final_acl = self.user.acl_cache['categories'][self.category.pk]
         final_acl.update({
             'can_see': 1,
             'can_browse': 1,

+ 5 - 4
misago/threads/tests/test_threadslists.py

@@ -119,8 +119,9 @@ class ThreadsListTestCase(AuthenticatedUserTestCase):
         }
 
         # copy first category's acl to other categories to make base for overrides
+        first_category_acl = self.user.acl_cache['categories'][self.first_category.pk].copy()
         for category in Category.objects.all_categories():
-            categories_acl['categories'][category.pk] = self.user.acl['categories'][self.first_category.pk].copy()
+            categories_acl['categories'][category.pk] = first_category_acl
 
         if base_acl:
             categories_acl.update(base_acl)
@@ -1602,13 +1603,13 @@ class OwnerOnlyThreadsVisibilityTests(AuthenticatedUserTestCase):
         self.category = Category.objects.get(slug='first-category')
 
     def override_acl(self, user):
-        category_acl = user.acl['categories'][self.category.pk].copy()
+        category_acl = user.acl_cache['categories'][self.category.pk].copy()
         category_acl.update({
             'can_see_all_threads': 0
         })
-        user.acl['categories'][self.category.pk] = category_acl
+        user.acl_cache['categories'][self.category.pk] = category_acl
 
-        override_acl(user, user.acl)
+        override_acl(user, user.acl_cache)
 
     def test_owned_threads_visibility(self):
         """only user-posted threads are visible in category"""

+ 1 - 1
misago/threads/tests/test_threadview.py

@@ -23,7 +23,7 @@ class ThreadViewTestCase(AuthenticatedUserTestCase):
         self.thread = testutils.post_thread(category=self.category)
 
     def override_acl(self, acl=None):
-        category_acl = self.user.acl['categories'][self.category.pk]
+        category_acl = self.user.acl_cache['categories'][self.category.pk]
         category_acl.update({
             'can_see': 1,
             'can_browse': 1,

+ 1 - 1
misago/threads/viewmodels/category.py

@@ -56,7 +56,7 @@ class ThreadsRootCategory(ViewModel):
     def get_categories(self, request):
         return [Category.objects.root_category()] + list(
             Category.objects.all_categories().filter(
-                id__in=request.user.acl['browseable_categories']
+                id__in=request.user.acl_cache['browseable_categories']
             ).select_related('parent'))
 
 

+ 5 - 3
misago/threads/viewmodels/threads.py

@@ -87,8 +87,10 @@ class ViewModel(object):
             if list_type in LIST_DENIED_MESSAGES:
                 raise PermissionDenied(LIST_DENIED_MESSAGES[list_type])
         else:
-            if list_type == 'unapproved' and not request.user.acl['can_see_unapproved_content_lists']:
-                raise PermissionDenied(_("You don't have permission to see unapproved content lists."))
+            has_permission = request.user.acl_cache['can_see_unapproved_content_lists']
+            if list_type == 'unapproved' and not has_permission:
+                raise PermissionDenied(
+                    _("You don't have permission to see unapproved content lists."))
 
     def get_list_name(self, list_type):
         return LISTS_NAMES[list_type]
@@ -156,7 +158,7 @@ class PrivateThreads(ViewModel):
         # limit queryset to threads we are participant of
         participated_threads = request.user.threadparticipant_set.values('thread_id')
 
-        if request.user.acl['can_moderate_private_threads']:
+        if request.user.acl_cache['can_moderate_private_threads']:
             queryset = queryset.filter(
                 Q(id__in=participated_threads) | Q(has_reported_posts=True))
         else:

+ 1 - 1
misago/threads/views/attachment.py

@@ -55,7 +55,7 @@ def allow_file_download(request, attachment):
     if not is_authenticated or request.user.id != attachment.uploader_id:
         if not attachment.post_id:
             raise Http404()
-        if not request.user.acl['can_download_other_users_attachments']:
+        if not request.user.acl_cache['can_download_other_users_attachments']:
             raise PermissionDenied()
 
     allowed_roles = set(r.pk for r in attachment.filetype.limit_downloads_to.all())

+ 1 - 1
misago/users/api/userendpoints/signature.py

@@ -13,7 +13,7 @@ from misago.users.signatures import is_user_signature_valid, set_user_signature
 def signature_endpoint(request):
     user = request.user
 
-    if not user.acl['can_have_signature']:
+    if not user.acl_cache['can_have_signature']:
         raise PermissionDenied(
             _("You don't have permission to change signature."))
 

+ 3 - 3
misago/users/api/usernamechanges.py

@@ -26,9 +26,9 @@ class UsernameChangesViewSetPermission(BasePermission):
 
         if user_pk == request.user.pk:
             return True
-        elif not request.user.acl.get('can_see_users_name_history'):
-            raise PermissionDenied(_("You don't have permission to "
-                                     "see other users name history."))
+        elif not request.user.acl_cache.get('can_see_users_name_history'):
+            raise PermissionDenied(
+                _("You don't have permission to see other users name history."))
         return True
 
 

+ 3 - 3
misago/users/apps.py

@@ -18,7 +18,7 @@ class MisagoUsersConfig(AppConfig):
 
     def register_default_usercp_pages(self):
         def show_signature_cp(request):
-            return request.user.acl['can_have_signature']
+            return request.user.acl_cache['can_have_signature']
 
         usercp.add_section(
             link='misago:usercp-change-forum-options',
@@ -49,14 +49,14 @@ class MisagoUsersConfig(AppConfig):
         def can_see_names_history(request, profile):
             if request.user.is_authenticated:
                 is_account_owner = profile.pk == request.user.pk
-                has_permission = request.user.acl['can_see_users_name_history']
+                has_permission = request.user.acl_cache['can_see_users_name_history']
                 return is_account_owner or has_permission
             else:
                 return False
 
         def can_see_ban_details(request, profile):
             if request.user.is_authenticated:
-                if request.user.acl['can_see_ban_details']:
+                if request.user.acl_cache['can_see_ban_details']:
                     from .bans import get_user_ban
                     return bool(get_user_ban(profile))
                 else:

+ 0 - 57
misago/users/forms/moderation.py

@@ -67,60 +67,3 @@ class ModerateSignatureForm(forms.ModelForm):
                 length_limit) % {'limit': length_limit})
 
         return data
-
-
-class BanForm(forms.Form):
-    user_message = forms.CharField(
-        label=_("User message"), required=False, max_length=1000,
-        help_text=_("Optional message displayed to user "
-                    "instead of default one."),
-        widget=forms.Textarea(attrs={'rows': 3}),
-        error_messages={
-            'max_length': _("Message can't be longer than 1000 characters.")
-        })
-    staff_message = forms.CharField(
-        label=_("Team message"), required=False, max_length=1000,
-        help_text=_("Optional ban message for moderators and administrators."),
-        widget=forms.Textarea(attrs={'rows': 3}),
-        error_messages={
-            'max_length': _("Message can't be longer than 1000 characters.")
-        })
-    expires_on = forms.DateTimeField(
-        label=_("Expires on"),
-        required=False, localize=True,
-        help_text=_('Leave this field empty for this ban to never expire.'))
-
-    def __init__(self, *args, **kwargs):
-        self.user = kwargs.pop('user')
-        super(BanForm, self).__init__(*args, **kwargs)
-
-        if self.user.acl_['max_ban_length']:
-            message = ungettext(
-                "Required. Can't be longer than %(days)s day.",
-                "Required. Can't be longer than %(days)s days.",
-                self.user.acl_['max_ban_length'])
-            message = message % {'days': self.user.acl_['max_ban_length']}
-            self['expires_on'].field.help_text = message
-
-    def clean_expires_on(self):
-        data = self.cleaned_data['expires_on']
-
-        if self.user.acl_['max_ban_length']:
-            max_ban_length = timedelta(days=self.user.acl_['max_ban_length'])
-            if not data or data > (timezone.now() + max_ban_length).date():
-                message = ungettext(
-                    "You can't set bans longer than %(days)s day.",
-                    "You can't set bans longer than %(days)s days.",
-                    self.user.acl_['max_ban_length'])
-                message = message % {'days': self.user.acl_['max_ban_length']}
-                raise forms.ValidationError(message)
-        elif data and data < timezone.now().date():
-            raise forms.ValidationError(_("Expiration date is in past."))
-
-        return data
-
-    def ban_user(self):
-        ban_user(self.user,
-                 user_message=self.cleaned_data['user_message'],
-                 staff_message=self.cleaned_data['staff_message'],
-                 expires_on=self.cleaned_data['expires_on'])

+ 12 - 8
misago/users/models/user.py

@@ -285,16 +285,20 @@ class User(AbstractBaseUser, PermissionsMixin):
         delete_user_content.send(sender=self)
 
     @property
-    def acl(self):
+    def acl_cache(self):
         try:
             return self._acl_cache
         except AttributeError:
             self._acl_cache = get_user_acl(self)
             return self._acl_cache
 
-    @acl.setter
-    def acl(self, value):
-        raise TypeError('Cannot make User instances ACL aware')
+    @acl_cache.setter
+    def acl_cache(self, value):
+        raise TypeError("acl_cache can't be assigned")
+
+    @property
+    def acl_(self):
+        raise NotImplementedError('user.acl_ property was renamed to user.acl')
 
     @property
     def requires_activation_by_admin(self):
@@ -461,16 +465,16 @@ class AnonymousUser(DjangoAnonymousUser):
     acl_key = 'anonymous'
 
     @property
-    def acl(self):
+    def acl_cache(self):
         try:
             return self._acl_cache
         except AttributeError:
             self._acl_cache = get_user_acl(self)
             return self._acl_cache
 
-    @acl.setter
-    def acl(self, value):
-        raise TypeError('Cannot make AnonymousUser instances ACL aware')
+    @acl_cache.setter
+    def acl_cache(self, value):
+        raise TypeError("AnonymousUser instances can't be made ACL aware")
 
     def get_roles(self):
         try:

+ 3 - 3
misago/users/namechanges.py

@@ -13,12 +13,12 @@ class UsernameChanges(object):
         self.left = 0
         self.next_on = None
 
-        if user.acl['name_changes_allowed']:
+        if user.acl_cache['name_changes_allowed']:
             self.count_namechanges(user)
 
     def count_namechanges(self, user):
-        name_changes_allowed = user.acl['name_changes_allowed']
-        name_changes_expire = user.acl['name_changes_expire']
+        name_changes_allowed = user.acl_cache['name_changes_allowed']
+        name_changes_expire = user.acl_cache['name_changes_expire']
 
         valid_changes_qs = user.namechanges.filter(changed_by=user)
         if name_changes_expire:

+ 5 - 5
misago/users/permissions/delete.py

@@ -65,9 +65,9 @@ def build_acl(acl, roles, key_name):
 ACL's for targets
 """
 def add_acl_to_user(user, target):
-    target.acl_['can_delete'] = can_delete_user(user, target)
-    if target.acl_['can_delete']:
-        target.acl_['can_moderate'] = True
+    target.acl['can_delete'] = can_delete_user(user, target)
+    if target.acl['can_delete']:
+        target.acl['can_moderate'] = True
 
 
 def register_with(registry):
@@ -78,8 +78,8 @@ def register_with(registry):
 ACL tests
 """
 def allow_delete_user(user, target):
-    newer_than = user.acl['can_delete_users_newer_than']
-    less_posts_than = user.acl['can_delete_users_with_less_posts_than']
+    newer_than = user.acl_cache['can_delete_users_newer_than']
+    less_posts_than = user.acl_cache['can_delete_users_with_less_posts_than']
     if not newer_than and not less_posts_than:
         raise PermissionDenied(_("You can't delete users."))
 

+ 15 - 17
misago/users/permissions/moderation.py

@@ -90,14 +90,12 @@ def build_acl(acl, roles, key_name):
 ACL's for targets
 """
 def add_acl_to_user(user, target):
-    target_acl = target.acl_
-
-    target_acl['can_rename'] = can_rename_user(user, target)
-    target_acl['can_moderate_avatar'] = can_moderate_avatar(user, target)
-    target_acl['can_moderate_signature'] = can_moderate_signature(user, target)
-    target_acl['can_ban'] = can_ban_user(user, target)
-    target_acl['max_ban_length'] = user.acl['max_ban_length']
-    target_acl['can_lift_ban'] = can_lift_ban(user, target)
+    target.acl['can_rename'] = can_rename_user(user, target)
+    target.acl['can_moderate_avatar'] = can_moderate_avatar(user, target)
+    target.acl['can_moderate_signature'] = can_moderate_signature(user, target)
+    target.acl['can_ban'] = can_ban_user(user, target)
+    target.acl['max_ban_length'] = user.acl_cache['max_ban_length']
+    target.acl['can_lift_ban'] = can_lift_ban(user, target)
 
     mod_permissions = (
         'can_rename',
@@ -107,8 +105,8 @@ def add_acl_to_user(user, target):
     )
 
     for permission in mod_permissions:
-        if target_acl[permission]:
-            target_acl['can_moderate'] = True
+        if target.acl[permission]:
+            target.acl['can_moderate'] = True
             break
 
 
@@ -120,7 +118,7 @@ def register_with(registry):
 ACL tests
 """
 def allow_rename_user(user, target):
-    if not user.acl['can_rename_users']:
+    if not user.acl_cache['can_rename_users']:
         raise PermissionDenied(_("You can't rename users."))
     if not user.is_superuser and (target.is_staff or target.is_superuser):
         raise PermissionDenied(_("You can't rename administrators."))
@@ -128,7 +126,7 @@ can_rename_user = return_boolean(allow_rename_user)
 
 
 def allow_moderate_avatar(user, target):
-    if not user.acl['can_moderate_avatars']:
+    if not user.acl_cache['can_moderate_avatars']:
         raise PermissionDenied(_("You can't moderate avatars."))
     if not user.is_superuser and (target.is_staff or target.is_superuser):
         raise PermissionDenied(_("You can't moderate administrators avatars."))
@@ -136,7 +134,7 @@ can_moderate_avatar = return_boolean(allow_moderate_avatar)
 
 
 def allow_moderate_signature(user, target):
-    if not user.acl['can_moderate_signatures']:
+    if not user.acl_cache['can_moderate_signatures']:
         raise PermissionDenied(_("You can't moderate signatures."))
     if not user.is_superuser and (target.is_staff or target.is_superuser):
         message = _("You can't moderate administrators signatures.")
@@ -145,7 +143,7 @@ can_moderate_signature = return_boolean(allow_moderate_signature)
 
 
 def allow_ban_user(user, target):
-    if not user.acl['can_ban_users']:
+    if not user.acl_cache['can_ban_users']:
         raise PermissionDenied(_("You can't ban users."))
     if target.is_staff or target.is_superuser:
         raise PermissionDenied(_("You can't ban administrators."))
@@ -153,13 +151,13 @@ can_ban_user = return_boolean(allow_ban_user)
 
 
 def allow_lift_ban(user, target):
-    if not user.acl['can_lift_bans']:
+    if not user.acl_cache['can_lift_bans']:
         raise PermissionDenied(_("You can't lift bans."))
     ban = get_user_ban(target)
     if not ban:
         raise PermissionDenied(_("This user is not banned."))
-    if user.acl['max_lifted_ban_length']:
-        expiration_limit = timedelta(days=user.acl['max_lifted_ban_length'])
+    if user.acl_cache['max_lifted_ban_length']:
+        expiration_limit = timedelta(days=user.acl_cache['max_lifted_ban_length'])
         lift_cutoff = (timezone.now() + expiration_limit).date()
         if not ban.valid_until:
             raise PermissionDenied(_("You can't lift permanent bans."))

+ 9 - 11
misago/users/permissions/profiles.py

@@ -120,11 +120,9 @@ def build_acl(acl, roles, key_name):
 ACL's for targets
 """
 def add_acl_to_user(user, target):
-    target_acl = target.acl_
-
-    target_acl['can_have_attitude'] = False
-    target_acl['can_follow'] = can_follow_user(user, target)
-    target_acl['can_block'] = can_block_user(user, target)
+    target.acl['can_have_attitude'] = False
+    target.acl['can_follow'] = can_follow_user(user, target)
+    target.acl['can_block'] = can_block_user(user, target)
 
     mod_permissions = (
         'can_have_attitude',
@@ -133,8 +131,8 @@ def add_acl_to_user(user, target):
     )
 
     for permission in mod_permissions:
-        if target_acl[permission]:
-            target_acl['can_have_attitude'] = True
+        if target.acl[permission]:
+            target.acl['can_have_attitude'] = True
             break
 
 
@@ -146,14 +144,14 @@ def register_with(registry):
 ACL tests
 """
 def allow_browse_users_list(user):
-    if not user.acl['can_browse_users_list']:
+    if not user.acl_cache['can_browse_users_list']:
         raise PermissionDenied(_("You can't browse users list."))
 can_browse_users_list = return_boolean(allow_browse_users_list)
 
 
 @authenticated_only
 def allow_follow_user(user, target):
-    if not user.acl['can_follow_users']:
+    if not user.acl_cache['can_follow_users']:
         raise PermissionDenied(_("You can't follow other users."))
     if user.pk == target.pk:
         raise PermissionDenied(_("You can't add yourself to followed."))
@@ -166,7 +164,7 @@ def allow_block_user(user, target):
         raise PermissionDenied(_("You can't block administrators."))
     if user.pk == target.pk:
         raise PermissionDenied(_("You can't block yourself."))
-    if not target.acl['can_be_blocked'] or target.is_superuser:
+    if not target.acl_cache['can_be_blocked'] or target.is_superuser:
         message = _("%(user)s can't be blocked.") % {'user': target.username}
         raise PermissionDenied(message)
 can_block_user = return_boolean(allow_block_user)
@@ -174,6 +172,6 @@ can_block_user = return_boolean(allow_block_user)
 
 @authenticated_only
 def allow_see_ban_details(user, target):
-    if not user.acl['can_see_ban_details']:
+    if not user.acl_cache['can_see_ban_details']:
         raise PermissionDenied(_("You can't see users bans details."))
 can_see_ban_details = return_boolean(allow_see_ban_details)

+ 1 - 1
misago/users/search.py

@@ -19,7 +19,7 @@ class SearchUsers(SearchProvider):
     url = 'users'
 
     def allow_search(self):
-        if not self.request.user.acl['can_search_users']:
+        if not self.request.user.acl_cache['can_search_users']:
             raise PermissionDenied(
                 _("You don't have permission to search users."))
 

+ 1 - 1
misago/users/serializers/auth.py

@@ -69,7 +69,7 @@ class AnonymousUserSerializer(serializers.Serializer, AuthFlags):
     is_anonymous = serializers.SerializerMethodField()
 
     def get_acl(self, obj):
-        if hasattr(obj, 'acl'):
+        if hasattr(obj, 'acl_cache'):
             return serialize_acl(obj)
         else:
             return {}

+ 4 - 5
misago/users/serializers/user.py

@@ -3,7 +3,6 @@ from rest_framework import serializers
 from django.contrib.auth import get_user_model
 from django.urls import reverse
 
-from misago.acl import serialize_acl
 from misago.core.serializers import MutableFields
 
 from . import RankSerializer
@@ -70,23 +69,23 @@ class UserSerializer(serializers.ModelSerializer, MutableFields):
         )
 
     def get_acl(self, obj):
-        return obj.acl_
+        return obj.acl
 
     def get_email(self, obj):
         if (obj == self.context['user'] or
-                self.context['user'].acl['can_see_users_emails']):
+                self.context['user'].acl_cache['can_see_users_emails']):
             return obj.email
         else:
             return None
 
     def get_is_followed(self, obj):
-        if obj.acl_['can_follow']:
+        if obj.acl['can_follow']:
             return self.context['user'].is_following(obj)
         else:
             return False
 
     def get_is_blocked(self, obj):
-        if obj.acl_['can_block']:
+        if obj.acl['can_block']:
             return self.context['user'].is_blocking(obj)
         else:
             return False

+ 1 - 1
misago/users/views/profile.py

@@ -92,7 +92,7 @@ class ProfileView(View):
             })
 
             if not context['show_email']:
-                context['show_email'] = request.user.acl['can_see_users_emails']
+                context['show_email'] = request.user.acl_cache['can_see_users_emails']
         else:
             context.update({
                 'is_authenticated_user': False,