Permalink
Browse files

Fixes ticket #2.

Also added a test case for the feature + misc minor cleanup.
  • Loading branch information...
1 parent b32a645 commit 126d43f8e2b67c153e540f88667857ab5b1929d3 @seldon committed Oct 20, 2011
Showing with 124 additions and 81 deletions.
  1. +2 −2 README.rst
  2. +69 −67 flexi_auth/models.py
  3. +5 −0 flexi_auth/tests/models.py
  4. +17 −3 flexi_auth/tests/settings.py
  5. +31 −9 flexi_auth/tests/tests.py
View
@@ -45,8 +45,8 @@ VALID_PARAMS_FOR_ROLES
:Name: VALID_PARAMS_FOR_ROLES
:Type:
A dictionary made by entries of the form ``{<role name>: {<parameter name>: <parameter type>, ..}}``, where ``<role name>`` is the name of one of the
- general roles allowed within the application domain, ``<parameter name>`` is the name of one of the parameters that can be tied to that role
- (as declared by ``PARAM_CHOICES``), and ``<parameter type>`` is the type of that parameter (a model), expressed as a string of the format
+ general roles allowed within the application domain, ``<parameter name>`` is the name of one of the parameters that may be bound to that role
+ (must be an element of ``PARAM_CHOICES``), and ``<parameter type>`` is the type of that parameter (a model), expressed as a string of the format
``app_label.model_name``.
:Default: ``{}``
:Description:
View
@@ -1,6 +1,7 @@
from django.db import models
from django.db.models import signals
from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import ugettext, ugettext_lazy as _
from django.contrib.auth.models import User, Group
@@ -11,6 +12,8 @@
from flexi_auth.managers import RoleManager
+import functools
+
ROLES_DICT = dict(settings.ROLES_LIST)
class ObjectWithContext(object):
@@ -57,51 +60,7 @@ def can_edit(self, user, context):
def can_delete(self, user, context):
return True
-
-class ParamByName(object):
- """
- Helper class used to setup a convenient access API for ``ParamRole``'s parameters.
- """
-
- def _get_param(self, param_role, name):
- """
- If this role has a "%s" parameter, return it; else return None
- """
- # TODO: if a parameter is not set, an exception should be raised
- # Retrieve the value of parameter named ``name``; if it's not set, return ``None``
- # Duck typing
- try:
- rv = param_role.param_set.get(name=name).value
- except Param.DoesNotExist:
- rv = None
-
- return rv
-
-# def set_param(self, param_role, name, value):
-#
-# param_names = map(lambda x : x[0], Param.PARAM_CHOICES)
-#
-# #Sanity check
-# if name in param_names:
-# # TODO: check also content type
-# param_role.param_set.add(Param(name=name, param=value))
-# else:
-# raise NameError(ugettext("Wrong param name %s. Allowed param names are %s") % (value, param_names))
-
- def contribute_to_class(self, cls, name):
- """
- Create a property to retrieve role parameters by name
- """
-
- p = property(
- lambda obj : self._get_param(obj, name),
- None,
- None,
- self._get_param.__doc__ % name
- )
-
- setattr(cls, name, p)
-
+
class Param(models.Model):
"""
A trivial wrapper model class around a generic ``ForeignKey``;
@@ -123,7 +82,67 @@ def __repr__(self):
class Meta:
# forbid duplicated ``Param`` entries in the DB
unique_together = ('name', 'content_type', 'object_id')
+
+
+def param_by_name(cls):
+ """
+ This function is meant to be used as a class decorator for the ``ParamRole`` model.
+ Its job is to dynamically augmenting the ``ParamRole`` object interface
+ by automagically adding accessor methods (implemented as properties) to ease the
+ task of retrieving the parameters bound to a parametric role.
+
+ This way, a convenience access API is automatically built and added - at runtime -
+ to the ``ParamRole`` model, based on the domain-specific set of allowed parameters
+ (as declared via the ``settings.PARAM_CHOICES`` config option.
+
+ Usage
+ =====
+ To retrieve the value of parameter ``<param_name>`` bound to the parametric role
+ ``p_role`` (a ``ParamRole`` instance), just use ``p_role.<param_name>``.
+
+ If ``p_role`` doesn't have a parameter named ``<param_name>`` attached to it,
+ a ``AttributeError`` exception is raised.
+
+ Note that this is, by design, a read-only access; parameter assignment is managed by the
+ ``register_parametric_role()`` factory function.
+ """
+
+ # retrieve allowed parameter names for project configuration
+ allowed_params = [name for (name, desc) in settings.PARAM_CHOICES]
+
+ def _get_param(p_role, name):
+ """
+ If this role has a "%s" parameter, return it;
+ else, raise an ``AttributeError`` exception.
+ """
+ try:
+ return p_role.param_set.get(name=name).value
+ except Param.DoesNotExist:
+ role_name = p_role.role.name
+ raise AttributeError("The parametric role %(p_role)s doesn't have a `%(name)s' parameter" % {'p_role': p_role, 'name': name})
+
+ for name in allowed_params:
+ # prevent overriding of existing class attributes
+ if name in cls.__dict__.keys():
+ msg = """`%(name)s' is not a valid name for a parameter,
+ since the model class %(cls)s already contains an attribute
+ with that name"""
+ raise ImproperlyConfigured(msg % {'name': name, 'cls': cls})
+ # value of the ``name`` argument is already known, so stash it for later calls
+ fget = functools.partial(_get_param, name=name)
+ doc = _get_param.__doc__ % name
+ p = property(
+ fget,
+ None,
+ None,
+ doc,
+ )
+ # FIXME: what happens when string ``name`` is not a valid Python variable's identifier ?
+ setattr(cls, name, p)
+ return cls
+
+@param_by_name
class ParamRole(models.Model):
"""
A custom role model class inspired by ``django-permissions``'s ``Role`` model.
@@ -136,24 +155,8 @@ class ParamRole(models.Model):
# the basic ``Role`` to which additional context info is added (binding it to parameters)
role = models.ForeignKey(Role)
# parameters describing the context attached to this role
- param_set = models.ManyToManyField(Param)
-
- ## A simple API providing easier access to the parameters attached to this role.
- # Usage: to retrieve the value of parameter ``<param_name>`` from the parametric role
- # instance ``p_role``, just use ``p_role.<param_name>``
- # If ``p_role`` doesn't have a parameter ``<param_name>`` attached to it,
- # a ``RoleParameterNotAllowed`` exception is raised.
-
- # Since the set of allowed parameters is domain-specific
- # (declared using the project-level ``PARAM_CHOICES`` setting),
- # the access API must be dynamically built; to this end, we leverage the
- # ``contribute_to_class`` Django machinery.
-
- # note that this access is read-only; parameter assignment is managed by the
- #``register_parametric_role()`` factory function.
-
- # TOOD: use ``ParamByName()`` field-like object to implement the access API for parameters.
-
+ param_set = models.ManyToManyField(Param)
+
objects = RoleManager()
def __unicode__(self):
@@ -170,7 +173,7 @@ def param(self):
If this role has only one parameter, return it; else raise a ``MultipleObjectsReturned`` exception.
This is just a convenience method, useful when dealing with simple parametric roles
- depending only on one parameter (a common situation).
+ depending only on one parameter (a common scenario).
"""
params = self.params
@@ -190,12 +193,10 @@ def get_role(cls, role_name, **params):
* If ``role_name`` is not a valid identifier for a role, raises ``RoleNotAllowed`` exception.
* If ``params`` contains an invalid parameter name, raises ``RoleParameterNotAllowed`` exception.
* If provided parameter names are valid, but one of them is assigned to a wrong type,
- (based on domain constraints), raises ``RoleParameterWrongSpecsProvided`` exception.
-
+ (based on domain constraints), raises ``RoleParameterWrongSpecsProvided`` exception.
"""
qs = cls.objects.get_param_roles(role_name, **params)
- # TODO UNITTEST: write unit tests for this method
if len(qs) > 1:
raise cls.MultipleObjectsReturned("Warning: duplicate parametric role instances in the DB: %s with params %s" % role_name, params)
return qs[0]
@@ -286,6 +287,7 @@ def is_archived(self):
##---------------------------------------##
+
class PrincipalParamRoleRelation(models.Model):
"""
This model is a relation describing the fact that a parametric role (``ParamRole``)
@@ -6,10 +6,15 @@ class Author(models.Model):
name = models.CharField(max_length=50)
surname = models.CharField(max_length=50)
+class Magazine(models.Model):
+ name = models.CharField(max_length=50)
+ printing = models.IntegerField()
+
class Article(models.Model):
title = models.CharField(max_length=50)
body = models.TextField()
author = models.ForeignKey(Author)
+ published_to = models.ManyToManyField(Magazine)
def __unicode__(self):
return "An article with title '%s'" % self.title
@@ -1,3 +1,5 @@
+from django.utils.translation import ugettext_lazy as _
+
import os
DIRNAME = os.path.dirname(__file__)
@@ -55,8 +57,20 @@
LOGIN_REDIRECT_URL = '/accounts/profile/'
# app-specific settings
-ROLES_LIST = ()
+ROLES_LIST = (
+ ('EDITOR', _('Editor')),
+ ('PUBLISHER', 'Publisher'),
+ ('SPONSOR', 'Sponsor'),
+)
-PARAM_CHOICES = ()
+PARAM_CHOICES = (
+ ('article', _('Article')),
+ ('book', 'Book'),
+ ('magazine', 'Magazine'),
+)
-VALID_PARAMS_FOR_ROLES = {}
+VALID_PARAMS_FOR_ROLES = {
+ 'EDITOR' : {'article': 'tests.Article'},
+ 'PUBLISHER' : {'book': 'tests.Book'},
+ 'SPONSOR' : {'article': 'tests.Article', 'magazine': 'tests.Magazine'},
+}
View
@@ -3,13 +3,14 @@
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth import SESSION_KEY
-from flexi_auth.utils import get_ctype_from_model_label
+from flexi_auth.utils import get_ctype_from_model_label, register_parametric_role
from flexi_auth.exceptions import WrongPermissionCheck
from flexi_auth.models import ObjectWithContext
from flexi_auth.decorators import object_permission_required
+from flexi_auth.exceptions import RoleParameterNotAllowed
from flexi_auth.tests import settings
-from flexi_auth.tests.models import Article, Book, Author
+from flexi_auth.tests.models import Article, Book, Author, Magazine
from flexi_auth.tests.views import CallableView, normal_view
@@ -19,7 +20,7 @@ class GetCtypeFromModelLabelTest(TestCase):
"""Tests for the ``get_ctype_from_model_label()`` function"""
def setUp(self):
- pass
+ pass
def testOK(self):
"""Verify the right ContentType is returned if the model label is good"""
@@ -42,15 +43,36 @@ class ParamByNameTest(TestCase):
"""Test if parameters of a parametric role can be accessed by name"""
def setUp(self):
- pass
-
+ self.author1 = Author.objects.create(name="Bilbo", surname="Baggins")
+ self.author2 = Author.objects.create(name="Luke", surname="Skywalker")
+
+ self.magazine1 = Magazine.objects.create(name="Lorem Magazine", printing=1)
+ self.magazine2 = Magazine.objects.create(name="Ipsum Magazine", printing=100)
+
+ self.article = Article.objects.create(title="Lorem Ipsum", body="Neque porro quisquam est qui dolorem ipsum quia dolor sit amet...", author=self.author1)
+ self.article.published_to.add(self.magazine1, self.magazine2)
+ self.article.save()
+
+ self.book = Book.objects.create(title="Lorem Ipsum - The book", content="Neque porro quisquam est qui dolorem ipsum quia dolor sit amet...")
+ self.book.authors.add(self.author1, self.author2)
+ self.book.save()
+
+ self.pr1 = register_parametric_role('EDITOR', article=self.article)
+ self.pr2 = register_parametric_role('PUBLISHER', book=self.book)
+ self.pr3 = register_parametric_role('SPONSOR', article=self.article, magazine=self.magazine1)
+#
def testGetOK(self):
"""Verify that an existing parameter can be retrieved by its name"""
- pass
-
+ self.assertEqual(self.pr1.article, self.article)
+ self.assertEqual(self.pr2.book, self.book)
+ self.assertEqual(self.pr3.article, self.article)
+ self.assertEqual(self.pr3.magazine, self.magazine1)
+
def testGetFailIfParameterNotSet(self):
- """When trying to retrieve an unset parameter, raise ``RoleParameterNotAllowed``"""
- pass
+ """When trying to retrieve an unset parameter, raise ``AttributeError``"""
+ self.assertRaises(AttributeError, lambda x: self.pr1.book, 1)
+ self.assertRaises(AttributeError, lambda x: self.pr2.article, 1)
+ self.assertRaises(AttributeError, lambda x: self.pr3.book, 1)
class ParamModelTest(TestCase):

1 comment on commit 126d43f

feroda commented on 126d43f Oct 21, 2011

great solution, many compliments

Please sign in to comment.