Просмотр исходного кода

FW updates

* faster ACL implementation (dict lookups instead of isinstance tests)
* ACL serializers
* Auth/AnonUser serializers
* Ember.js Aauth service initialization fixed
* Basic user avatar component
Rafał Pitoń 10 лет назад
Родитель
Сommit
9ddf16ce0a

+ 53 - 6
docs/developers/acls.rst

@@ -10,7 +10,7 @@ Checking permissions
 
 Permissions are stored on special models named "roles" and assigned to users either directly or trough ranks. Guest users always have permissions from "Guest" role, and users always have permissions form "Member" role.
 
-There are two kinds of objects in Misago: aware and unaware of their ACL's. Aware objects have "acl" attribute containing dict with their permissions for given ACL, while unaware objects don't. Instances of ``User`` and ``AnonymousUser`` classes are always ACL aware, while other objects need you to make them aware of their ACLs through use of ``add_acl`` functions ofered by ``misago.acl`` module. However this is not always needed (or possible), in which cases you have to introspect user's acl attribute directly.
+There are two kinds of objects in Misago: aware and unaware of their ACL's. Aware objects have "acl" annotation containing dict with their permissions for given ACL, while unaware objects don't. Instances of ``User`` and ``AnonymousUser`` classes are always ACL aware, while other objects need you to make them aware of their ACLs through use of ``add_acl`` functions ofered by ``misago.acl`` module. However this is not always needed (or possible), in which cases you have to introspect user's acl attribute directly.
 
 ACL's are simple dictionaries, and their contents differ depending on objects they are belonging to. This means that to see if forum is visible to user, you have to perform following check::
 
@@ -32,6 +32,8 @@ Above snippet is edge example of checking forum permission, and luckily we have
 
 Because ACL framework is very flexible, different features can have different ways to check their permissions.
 
+Misago comes with its own debug page titled "Misago User ACL" that is available from Django Debug Toolbar menu. This page display user roles permissions as well as final ACL assigned to current user.
+
 
 Permissions cache
 -----------------
@@ -67,23 +69,68 @@ Required. This function is called when change permissions form for role is being
 Required. Is used in process of building new ACL. Its supplied dict with incomplete ACL, list of user roles and name of key under which its permissions values are stored in roles ``permissions`` attributes. Its expected to access roles ``permissions`` attributes which are dicts of values coming from permission change forms and return updated ``acl`` dict.
 
 
-.. function:: add_acl_to_target(user, target)
+.. function:: register_with(registry)
+
+Optional. Is called by providers registry after provider module was imported, to allow it to register annotators and serializers for ACL's. Receives only one argument:
+
+* **registry** - istance of PermissionProviders that imported module.
+
+
+Registering Annotators and Serializers
+======================================
+
+When module's ``register_with`` function is called, its passed ``PermissionProviders`` instance that exposes following methods:
+
+
+.. function:: acl_annotator(hashable_type, func)
+
+Registers ``func`` as ACL annotator for ``hashable_type``.
+
+
+.. function:: acl_serializer(hashable_type, func)
+
+Registers ``func`` as ACL serializer for ``hashable_type``.
+
 
-Optional. Is called when Misago is trying to make ``target`` aware of its ACLs. Its called with two arguments:
+.. function:: get_type_annotators(obj)
+
+Returns list of annotators registered for type of ``obj`` or empty list is none exist.
+
+
+.. function:: get_type_serializers(obj)
+
+Returns list of serializers registered for type of ``obj`` or empty list is none exist.
+
+
+Annotators
+----------
+
+Annotators are functions called when object is being made ACL aware. It always receives two arguments:
 
 * **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 can change and update with new keys.
+``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 reserve ``acl`` attribute for their own acls. Instead add_acl_to_target for User instances will add acl's to `acl_` attribute.
 
-Misago comes with its own debug page titled "Misago User ACL" that is available from Django Debug Toolbar menu. This page display user roles permissions as well as final ACL assigned to current user.
+
+Serializers
+-----------
+
+Serializers are functions called when ACL-aware object is being prepared for JSON serialization. Because python's ``dict`` type isnt 1:1 interchangeable with JSON, serializers allow ACL extensions to perform additional convertion or cleanup before model's ACL is serialized. They always receive single argument:
+
+* **serialized_acl** - ACL that will be JSON serialized
+
+Example serializer for extension setting dict using integers for keys could for example remove this dictionary from ACL to avoid problems during ACL serialization::
+
+    def serialize_forums_acl(user_acl):
+        user_acl.pop('forums', None)
 
 
 Algebra
--------
+=======
 
 Consider those three simple permission sets::
 

+ 20 - 4
misago/acl/api.py

@@ -1,3 +1,5 @@
+import copy
+
 from django.contrib.auth import get_user_model
 
 from misago.core import threadstore
@@ -8,7 +10,7 @@ from misago.acl.builder import build_acl
 from misago.acl.providers import providers
 
 
-__ALL__ = ['get_user_acl', 'add_acl']
+__ALL__ = ['get_user_acl', 'add_acl', 'serialize_acl']
 
 
 """
@@ -65,6 +67,20 @@ def _add_acl_to_target(user, target):
     else:
         target.acl = {}
 
-    for extension, module in providers.list():
-        if hasattr(module, 'add_acl_to_target'):
-            module.add_acl_to_target(user, target)
+    for annotator in providers.get_type_annotators(target):
+        annotator(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
+    """
+    serialized_acl = copy.deepcopy(target.acl)
+
+    for serializer in providers.get_type_serializers(target):
+        serializer(serialized_acl)
+
+    return serialized_acl

+ 0 - 11
misago/acl/decorators.py

@@ -2,17 +2,6 @@ from django.core.exceptions import PermissionDenied
 from django.http import Http404
 
 
-def require_target_type(supported_type):
-    def wrap(f):
-        def decorator(user, target):
-            if isinstance(target, supported_type):
-                return f(user, target)
-            else:
-                return None
-        return decorator
-    return wrap
-
-
 def return_boolean(f):
     def decorator(*args, **kwargs):
         try:

+ 37 - 5
misago/acl/providers.py

@@ -13,22 +13,54 @@ class PermissionProviders(object):
         self._providers = []
         self._providers_dict = {}
 
-    def _assert_providers_imported(self):
+        self._annotators = {}
+        self._serializers = {}
+
+    def _assert_providers_registered(self):
         if not self._initialized:
-            self._import_providers()
+            self._register_providers()
+            self._change_lists_to_tupes(self._annotators)
+            self._change_lists_to_tupes(self._serializers)
             self._initialized = True
 
-    def _import_providers(self):
+    def _register_providers(self):
         for namespace in settings.MISAGO_ACL_EXTENSIONS:
             self._providers.append((namespace, import_module(namespace)))
             self._providers_dict[namespace] = import_module(namespace)
 
+            if hasattr(self._providers_dict[namespace], 'register_with'):
+                self._providers_dict[namespace].register_with(self)
+
+    def _change_lists_to_tupes(self, types_dict):
+        for hashType in types_dict.keys():
+            types_dict[hashType] = tuple(types_dict[hashType])
+
+    def acl_annotator(self, hashable_type, func):
+        """
+        registers ACL annotator for specified types
+        """
+        self._annotators.setdefault(hashable_type, []).append(func)
+
+    def acl_serializer(self, hashable_type, func):
+        """
+        registers ACL serializer for specified types
+        """
+        self._serializers.setdefault(hashable_type, []).append(func)
+
+    def get_type_annotators(self, obj):
+        self._assert_providers_registered()
+        return self._annotators.get(obj.__class__, [])
+
+    def get_type_serializers(self, obj):
+        self._assert_providers_registered()
+        return self._serializers.get(obj.__class__, [])
+
     def list(self):
-        self._assert_providers_imported()
+        self._assert_providers_registered()
         return self._providers
 
     def dict(self):
-        self._assert_providers_imported()
+        self._assert_providers_registered()
         return self._providers_dict
 
 

+ 27 - 0
misago/acl/tests/test_providers.py

@@ -6,6 +6,10 @@ from django.test import TestCase
 from misago.acl.providers import PermissionProviders
 
 
+class TestType(object):
+    pass
+
+
 class PermissionProvidersTests(TestCase):
     def test_initialization(self):
         """providers manager is lazily initialized"""
@@ -55,3 +59,26 @@ class PermissionProvidersTests(TestCase):
         for extension, module in providers_dict.items():
             self.assertTrue(isinstance(extension, basestring))
             self.assertEqual(type(module), ModuleType)
+
+    def test_annotators(self):
+        """its possible to register and get annotators"""
+        providers = PermissionProviders()
+
+        def test_annotator(*args):
+            pass
+
+        providers.acl_annotator(TestType, test_annotator)
+        annotators_list = providers.get_type_annotators(TestType())
+        self.assertEqual(annotators_list[0], test_annotator)
+
+    def test_serializers(self):
+        """its possible to register and get annotators"""
+        providers = PermissionProviders()
+
+        def test_serializer(*args):
+            pass
+
+        providers.acl_serializer(TestType, test_serializer)
+        serializers_list = providers.get_type_serializers(TestType())
+        self.assertEqual(serializers_list[0], test_serializer)
+

+ 17 - 0
misago/emberapp/app/components/user-avatar.js

@@ -0,0 +1,17 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+  tagName: 'img',
+  classNames: 'user-avatar',
+  attributeBindings: ['src', 'alt'],
+
+  size: 100,
+
+  src: function() {
+    return '/user-avatar/' + this.get('size') + '/' + this.get('id') + '.png';
+  }.property('id', 'size'),
+
+  alt: function() {
+    return '';
+  }.property()
+});

+ 5 - 2
misago/emberapp/app/initializers/auth-service.js

@@ -3,10 +3,13 @@ import PreloadStore from 'misago/services/preload-store';
 import Auth from 'misago/services/auth';
 
 export function initialize(container, application) {
+  application.register('misago:user', Ember.Object.create(PreloadStore.get('user')), { instantiate: false });
+  application.register('misago:isAuthenticated', PreloadStore.get('isAuthenticated'), { instantiate: false });
+
   application.register('service:auth', Auth, { singleton: true });
 
-  application.inject('isAuthenticated', PreloadStore.get('isAuthenticated'), 'service:auth');
-  application.inject('user', Ember.Object.create(PreloadStore.get('user')), 'service:auth');
+  application.inject('service:auth', 'isAuthenticated', 'misago:isAuthenticated');
+  application.inject('service:auth', 'user', 'misago:user');
 
   application.inject('route', 'auth', 'service:auth');
   application.inject('controller', 'auth', 'service:auth');

+ 4 - 5
misago/emberapp/app/templates/components/user-nav.hbs

@@ -1,5 +1,4 @@
-<p class="navbar-text">{{user.username}}</p>
-
-<button type="button" class="btn btn-info navbar-btn btn-sm" {{action "logout"}}>
-  {{gettext "Log out"}}
-</button>
+<p class="navbar-text">
+  {{user-avatar id=auth.user.id size=200}}
+  {{auth.user.username}}
+</p>

+ 15 - 3
misago/forums/permissions.py

@@ -1,11 +1,13 @@
+from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied
 from django.http import Http404
 from django.utils.translation import ugettext_lazy as _
 
 
 from misago.acl import algebra
-from misago.acl.decorators import require_target_type, return_boolean
+from misago.acl.decorators import return_boolean
 from misago.core import forms
+from misago.users.models import AnonymousUser
 
 from misago.forums.models import Forum, RoleForumACL, ForumRole
 
@@ -85,12 +87,22 @@ def build_forum_acl(acl, forum, forums_roles, key_name):
 """
 ACL's for targets
 """
-@require_target_type(Forum)
-def add_acl_to_target(user, target):
+def add_acl_to_forum(user, target):
     target.acl['can_see'] = can_see_forum(user, target)
     target.acl['can_browse'] = can_browse_forum(user, target)
 
 
+def serialize_forums_alcs(serialized_acl):
+    serialized_acl.pop('forums')
+
+
+def register_with(registry):
+    registry.acl_annotator(Forum, add_acl_to_forum)
+
+    registry.acl_serializer(get_user_model(), serialize_forums_alcs)
+    registry.acl_serializer(AnonymousUser, serialize_forums_alcs)
+
+
 """
 ACL tests
 """

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

@@ -14,7 +14,7 @@ from misago.threads.models import Thread, Post, Event
 
 
 __all__ = [
-    'add_acl_to_target',
+    'register_with',
     'allow_see_thread',
     'can_see_thread',
     'allow_start_thread',
@@ -238,17 +238,6 @@ def build_forum_acl(acl, forum, forums_roles, key_name):
 """
 ACL's for targets
 """
-def add_acl_to_target(user, target):
-    if isinstance(target, Forum):
-        add_acl_to_forum(user, target)
-    if isinstance(target, Thread):
-        add_acl_to_thread(user, target)
-    if isinstance(target, Post):
-        add_acl_to_post(user, target)
-    if isinstance(target, Event):
-        add_acl_to_event(user, target)
-
-
 def add_acl_to_forum(user, forum):
     forum_acl = user.acl['forums'].get(forum.pk, {})
 
@@ -378,6 +367,13 @@ def add_acl_to_event(user, event):
     event.acl['can_delete'] = can_hide_events == 2
 
 
+def register_with(registry):
+    registry.acl_annotator(Forum, add_acl_to_forum)
+    registry.acl_annotator(Thread, add_acl_to_thread)
+    registry.acl_annotator(Post, add_acl_to_post)
+    registry.acl_annotator(Event, add_acl_to_event)
+
+
 """
 ACL tests
 """

+ 5 - 7
misago/users/api/auth.py

@@ -13,7 +13,8 @@ from misago.core.mail import mail_user
 from misago.users.forms.auth import (AuthenticationForm, ResendActivationForm,
                                      ResetPasswordForm)
 from misago.users.rest_permissions import UnbannedAnonOnly
-from misago.users.serializers import AuthenticatedUserSerializer
+from misago.users.serializers import (AuthenticatedUserSerializer,
+                                      AnonymousUserSerializer)
 from misago.users.tokens import (make_activation_token,
                                  is_activation_token_valid,
                                  make_password_change_token,
@@ -51,14 +52,11 @@ GET /auth/ will return current auth user, either User or AnonymousUser
 @api_view()
 def session_user(request):
     if request.user.is_authenticated():
-        serialized_user = AuthenticatedUserSerializer(request.user).data
+        UserSerializer = AuthenticatedUserSerializer
     else:
-        serialized_user = {
-            'id': None,
-            'acl': {'is_implemented': False}
-        }
+        UserSerializer = AnonymousUserSerializer
 
-    return Response(serialized_user)
+    return Response(UserSerializer(request.user).data)
 
 
 """

+ 4 - 9
misago/users/middleware.py

@@ -9,6 +9,8 @@ from misago.conf import settings
 from misago.users.bans import get_request_ip_ban, get_user_ban
 from misago.users.models import AnonymousUser, Online
 from misago.users.online import tracker
+from misago.users.serializers import (AuthenticatedUserSerializer,
+                                      AnonymousUserSerializer)
 
 
 class RealIPMiddleware(object):
@@ -45,18 +47,11 @@ class PreloadUserMiddleware(object):
 
         if request.user.is_authenticated():
             request.preloaded_ember_data.update({
-                'user': {
-                    'username': request.user.username,
-                    'isAuthenticated': True,
-                    'isAnonymous': False
-                }
+                'user': AuthenticatedUserSerializer(request.user).data
             })
         else:
             request.preloaded_ember_data.update({
-                'user': {
-                    'isAuthenticated': False,
-                    'isAnonymous': True,
-                }
+                'user': AnonymousUserSerializer(request.user).data
             })
 
 

+ 6 - 3
misago/users/permissions/delete.py

@@ -6,7 +6,7 @@ from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _, ungettext
 
 from misago.acl import algebra
-from misago.acl.decorators import require_target_type, return_boolean
+from misago.acl.decorators import return_boolean
 from misago.acl.models import Role
 from misago.core import forms
 
@@ -55,13 +55,16 @@ def build_acl(acl, roles, key_name):
 """
 ACL's for targets
 """
-@require_target_type(get_user_model())
-def add_acl_to_target(user, target):
+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
 
 
+def register_with(registry):
+    registry.acl_annotator(get_user_model(), add_acl_to_user)
+
+
 """
 ACL tests
 """

+ 6 - 3
misago/users/permissions/moderation.py

@@ -7,7 +7,7 @@ from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 
 from misago.acl import algebra
-from misago.acl.decorators import require_target_type, return_boolean
+from misago.acl.decorators import return_boolean
 from misago.acl.models import Role
 from misago.core import forms
 
@@ -75,8 +75,7 @@ def build_acl(acl, roles, key_name):
 """
 ACL's for targets
 """
-@require_target_type(get_user_model())
-def add_acl_to_target(user, target):
+def add_acl_to_user(user, target):
     target_acl = target.acl_
 
     target_acl['can_rename'] = can_rename_user(user, target)
@@ -99,6 +98,10 @@ def add_acl_to_target(user, target):
             break
 
 
+def register_with(registry):
+    registry.acl_annotator(get_user_model(), add_acl_to_user)
+
+
 """
 ACL tests
 """

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

@@ -3,7 +3,7 @@ from django.core.exceptions import PermissionDenied
 from django.utils.translation import ugettext_lazy as _
 
 from misago.acl import algebra
-from misago.acl.decorators import require_target_type, return_boolean
+from misago.acl.decorators import return_boolean
 from misago.acl.models import Role
 from misago.core import forms
 
@@ -105,8 +105,7 @@ def build_acl(acl, roles, key_name):
 """
 ACL's for targets
 """
-@require_target_type(get_user_model())
-def add_acl_to_target(user, target):
+def add_acl_to_user(user, target):
     target_acl = target.acl_
 
     target_acl['can_have_attitude'] = False
@@ -125,6 +124,10 @@ def add_acl_to_target(user, target):
             break
 
 
+def register_with(registry):
+    registry.acl_annotator(get_user_model(), add_acl_to_user)
+
+
 """
 ACL tests
 """

+ 5 - 7
misago/users/permissions/warnings.py

@@ -74,13 +74,6 @@ def build_acl(acl, roles, key_name):
 """
 ACL's for targets
 """
-def add_acl_to_target(user, target):
-    if isinstance(target, get_user_model()):
-        add_acl_to_user(user, target)
-    elif isinstance(target, UserWarning):
-        add_acl_to_warning(user, target)
-
-
 def add_acl_to_user(user, target):
     target_acl = target.acl_
 
@@ -101,6 +94,11 @@ def add_acl_to_warning(user, target):
     target.acl['can_moderate'] = can_moderate
 
 
+def register_with(registry):
+    registry.acl_annotator(get_user_model(), add_acl_to_user)
+    registry.acl_annotator(UserWarning, add_acl_to_warning)
+
+
 """
 ACL tests
 """

+ 0 - 3
misago/users/serializers.py

@@ -1,3 +0,0 @@
-from rest_framework import serializers
-
-from misago.users.models import User

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

@@ -1,2 +1,2 @@
-from misago.users.serializers.user import *
 from misago.users.serializers.ban import *
+from misago.users.serializers.user import *

+ 3 - 0
misago/users/serializers/ban.py

@@ -7,6 +7,9 @@ from misago.core.utils import format_plaintext_for_html
 from misago.users.models import Ban, BAN_IP
 
 
+__ALL__ = ['BanMessageSerializer']
+
+
 class BanMessageSerializer(serializers.ModelSerializer):
     message = serializers.SerializerMethodField()
 

+ 35 - 1
misago/users/serializers/user.py

@@ -2,8 +2,42 @@ from django.contrib.auth import get_user_model
 
 from rest_framework import serializers
 
+from misago.acl import serialize_acl
+
+
+__ALL__ = ['AuthenticatedUserSerializer', 'AnonymousUserSerializer']
+
 
 class AuthenticatedUserSerializer(serializers.ModelSerializer):
+    acl = serializers.SerializerMethodField()
+
     class Meta:
         model = get_user_model()
-        fields = ('id', 'username', 'slug')
+        fields = (
+            'id',
+            'username',
+            'slug',
+            'email',
+            'joined_on',
+            'is_hiding_presence',
+            'title',
+            'new_notifications',
+            'limits_private_thread_invites_to',
+            'unread_private_threads',
+            'sync_unread_private_threads',
+            'subscribe_to_started_threads',
+            'subscribe_to_replied_threads',
+            'threads',
+            'posts',
+            'acl')
+
+    def get_acl(self, obj):
+        return serialize_acl(obj)
+
+
+class AnonymousUserSerializer(serializers.Serializer):
+    id = serializers.ReadOnlyField()
+    acl = serializers.SerializerMethodField()
+
+    def get_acl(self, obj):
+        return serialize_acl(obj)