diff --git a/README.rst b/README.rst index d323491..8e7e3a4 100644 --- a/README.rst +++ b/README.rst @@ -237,12 +237,13 @@ together into a *rule set*. ``rules`` has two predefined rule sets: context. So, let's define our first couple of rules, adding them to the shared rule -set. We can use the ``is_book_author`` predicate we defined earlier: +set. We can use the ``is_book_author`` predicate we defined earlier, optionally +adding a ``verbose_name``: .. code:: python >>> rules.add_rule('can_edit_book', is_book_author) - >>> rules.add_rule('can_delete_book', is_book_author) + >>> rules.add_rule('can_delete_book', is_book_author, verbose_name="Can Delete Book") Assuming we've got some data, we can now test our rules: @@ -361,12 +362,13 @@ Creating permissions The convention for naming permissions in Django is ``app_label.action_object``, and we like to adhere to that. Let's add rules for the ``books.change_book`` -and ``books.delete_book`` permissions: +and ``books.delete_book`` permissions, again, with the option to add a +``verbose_name``: .. code:: python >>> rules.add_perm('books.change_book', is_book_author | is_editor) - >>> rules.add_perm('books.delete_book', is_book_author) + >>> rules.add_perm('books.delete_book', is_book_author, verbose_name="Books app: Delete book") See the difference in the API? ``add_perm`` adds to a permissions-specific rule set, whereas ``add_rule`` adds to a default shared rule set. It's @@ -461,10 +463,26 @@ directly:: "read": rules.is_authenticated, } +You can mix and match use of the optional ``verbose_name`` with your permissions by +supplying a dictionary with the predicate using the ``pred`` key and the verbose_name +using the ``verbose_name`` key, like so:: + + import rules + from rules.contrib.models import RulesModel + + class Book(RulesModel): + class Meta: + rules_permissions = { + "add": rules.is_staff, + "read": {"pred": rules.is_authenticated, "verbose_name": "Can read this book"}, + "delete": {"pred": rules.is_staff}, + } + This would be equivalent to the following calls:: rules.add_perm("app_label.add_book", rules.is_staff) - rules.add_perm("app_label.read_book", rules.is_authenticated) + rules.add_perm("app_label.read_book", rules.is_authenticated, verbose_name= "Can read this book") + rules.add_perm("app_label.delete_book", rules.is_staff) There are methods in ``RulesModelMixin`` that you can overwrite in order to customize how a model's permissions are registered. See the documented source code for details @@ -735,13 +753,15 @@ And manipulate them by adding, removing, querying and testing rules: >>> features.rule_exists('has_super_feature') False >>> is_special_user = rules.is_group_member('special') - >>> features.add_rule('has_super_feature', is_special_user) + >>> features.add_rule('has_super_feature', is_special_user, verbose_name="Has Super!") >>> 'has_super_feature' in features True >>> features['has_super_feature'] >>> features.test_rule('has_super_feature', adrian) True + >>> features.rule_verbose_name('has_super_feature') + Has Super! >>> features.remove_rule('has_super_feature') Note however that custom rule sets are *not available* in Django templates -- @@ -959,11 +979,11 @@ and use a rule set any way you'd use a dict. Instance methods ++++++++++++++++ -``add_rule(name, predicate)`` +``add_rule(name, predicate, verbose_name=None)`` Adds a predicate to the rule set, assigning it to the given rule name. Raises ``KeyError`` if another rule with that name already exists. -``set_rule(name, predicate)`` +``set_rule(name, predicate, verbose_name=None)`` Set the rule with the given name, regardless if one already exists. ``remove_rule(name)`` @@ -973,6 +993,10 @@ Instance methods ``rule_exists(name)`` Returns ``True`` if a rule with the given name exists, ``False`` otherwise. +``rule_verbose_name(name)`` + Returns the ``verbose_name``, if it was supplied when adding or setting + the rule. Defaults to ``name`` if no ``verbose_name`` was supplied. + ``test_rule(name, obj=None, target=None)`` Returns the result of calling ``predicate.test(obj, target)`` where ``predicate`` is the predicate for the rule with the given name. Returns @@ -1051,10 +1075,10 @@ Shortcuts Managing the shared rule set ++++++++++++++++++++++++++++ -``add_rule(name, predicate)`` +``add_rule(name, predicate, verbose_name=None)`` Adds a rule to the shared rule set. See ``RuleSet.add_rule``. -``set_rule(name, predicate)`` +``set_rule(name, predicate, verbose_name=None)`` Set the rule with the given name from the shared rule set. See ``RuleSet.set_rule``. @@ -1065,6 +1089,10 @@ Managing the shared rule set Returns whether a rule exists in the shared rule set. See ``RuleSet.rule_exists``. +``rule_verbose_name(name)`` + Returns the ``verbose_name``, if it was supplied when adding or setting + the rule. Defaults to ``name`` if no ``verbose_name`` was supplied. + ``test_rule(name, obj=None, target=None)`` Tests the rule with the given name. See ``RuleSet.test_rule``. @@ -1072,10 +1100,10 @@ Managing the shared rule set Managing the permissions rule set +++++++++++++++++++++++++++++++++ -``add_perm(name, predicate)`` +``add_perm(name, predicate, verbose_name=None)`` Adds a rule to the permissions rule set. See ``RuleSet.add_rule``. -``set_perm(name, predicate)`` +``set_perm(name, predicate, verbose_name=None)`` Replace a rule from the permissions rule set. See ``RuleSet.set_rule``. ``remove_perm(name)`` @@ -1085,6 +1113,10 @@ Managing the permissions rule set Returns whether a rule exists in the permissions rule set. See ``RuleSet.rule_exists``. +``perm_verbose_name(name)`` + Returns the ``verbose_name``, if it was supplied when adding or setting + the permission. Defaults to ``name`` if no ``verbose_name`` was supplied. + ``has_perm(name, user=None, obj=None)`` Tests the rule with the given name. See ``RuleSet.test_rule``. diff --git a/rules/contrib/models.py b/rules/contrib/models.py index 7577c06..a2340ec 100644 --- a/rules/contrib/models.py +++ b/rules/contrib/models.py @@ -36,7 +36,10 @@ def __new__(cls, name, bases, attrs, **kwargs): new_class._meta.rules_permissions = perms new_class.preprocess_rules_permissions(perms) for perm_type, predicate in perms.items(): - add_perm(new_class.get_perm(perm_type), predicate) + if isinstance(predicate, dict): + add_perm(new_class.get_perm(perm_type), predicate['pred'], verbose_name=predicate['verbose_name']) + else: + add_perm(new_class.get_perm(perm_type), predicate) return new_class diff --git a/rules/permissions.py b/rules/permissions.py index 9dda1a0..f475b5a 100644 --- a/rules/permissions.py +++ b/rules/permissions.py @@ -3,12 +3,12 @@ permissions = RuleSet() -def add_perm(name, pred): - permissions.add_rule(name, pred) +def add_perm(name, pred, verbose_name=None): + permissions.add_rule(name, pred, verbose_name=verbose_name) -def set_perm(name, pred): - permissions.set_rule(name, pred) +def set_perm(name, pred, verbose_name=None): + permissions.set_rule(name, pred, verbose_name=verbose_name) def remove_perm(name): @@ -19,6 +19,10 @@ def perm_exists(name): return permissions.rule_exists(name) +def perm_verbose_name(name): + return permissions.rule_verbose_name(name) + + def has_perm(name, *args, **kwargs): return permissions.test_rule(name, *args, **kwargs) diff --git a/rules/predicates.py b/rules/predicates.py index bd3c52b..fe8c877 100644 --- a/rules/predicates.py +++ b/rules/predicates.py @@ -242,7 +242,10 @@ def predicate(fn=None, name=None, **options): ... if self.context.args: ... return user == book.author """ - if not name and not callable(fn): + + if type(fn) is dict: + fn = fn['pred'] + elif not name and not callable(fn): name = fn fn = None diff --git a/rules/rulesets.py b/rules/rulesets.py index b9fb51d..3657721 100644 --- a/rules/rulesets.py +++ b/rules/rulesets.py @@ -1,49 +1,90 @@ -from .predicates import predicate +from .predicates import predicate, Predicate +from inspect import isfunction class RuleSet(dict): + def __init__(self, *args, **kwargs): + self.update(*args, **kwargs) + def test_rule(self, name, *args, **kwargs): + # return name in self and self[name]['pred'].test(*args, **kwargs) return name in self and self[name].test(*args, **kwargs) def rule_exists(self, name): return name in self - def add_rule(self, name, pred): + def rule_verbose_name(self, name): + return self["get_%s_display" % name] or name + + def add_rule(self, name, pred, verbose_name=None): if name in self: raise KeyError("A rule with name `%s` already exists" % name) - self[name] = pred - def set_rule(self, name, pred): - self[name] = pred + self[name] = {'pred': predicate(pred), 'verbose_name': verbose_name} + + def set_rule(self, name, pred, verbose_name=None): + self[name] = {'pred': predicate(pred), 'verbose_name': verbose_name} def remove_rule(self, name): del self[name] def __setitem__(self, name, pred): - fn = predicate(pred) - super(RuleSet, self).__setitem__(name, fn) - + if isfunction(pred): + # If a function as passed in (as might be done with legacy django-rules) + # convert the value to the dictionary form + pred = {'pred': predicate(pred), 'verbose_name': verbose_name} + + if isinstance(pred, dict): + # Check if internal pred is already a Predicate, or an basic + # unwrapped function. Wrap as a Predicate if needed + if not isinstance(pred['pred'], Predicate): + pred['pred'] = predicate(pred['pred']) + + super(RuleSet, self).__setitem__(name, pred) + + def __getitem__(self, name): + def _check_name(_name): + if (not super(RuleSet, self).__contains__(_name)): + raise KeyError("Provided name '`%s`' not found" % _name) + + if name[0] != '_': + prefix = "get_" + suffix = "_display" + if name.startswith(prefix) and name.endswith(suffix): + _name = name[len(prefix):-len(suffix)] + _check_name(_name) + return super(RuleSet, self).__getitem__(_name)['verbose_name'] + + _check_name(name) + return super().__getitem__(name)['pred'] + + def __iter__(self): + for name in range(len(self)): + yield self[name] + + def __reversed__(self): + for name in reversed(range(len(self))): + yield self[name] # Shared rule set default_rules = RuleSet() -def add_rule(name, pred): - default_rules.add_rule(name, pred) - - -def set_rule(name, pred): - default_rules.set_rule(name, pred) +def add_rule(name, pred, verbose_name=None): + default_rules.add_rule(name, pred, verbose_name=verbose_name) +def set_rule(name, pred, verbose_name=None): + default_rules.set_rule(name, pred, verbose_name=verbose_name) def remove_rule(name): default_rules.remove_rule(name) - def rule_exists(name): return default_rules.rule_exists(name) +def rule_verbose_name(name): + return default_rules.rule_verbose_name(name) def test_rule(name, *args, **kwargs): return default_rules.test_rule(name, *args, **kwargs) diff --git a/tests/testapp/models.py b/tests/testapp/models.py index 3ec7be8..e4ae314 100644 --- a/tests/testapp/models.py +++ b/tests/testapp/models.py @@ -18,8 +18,12 @@ def __str__(self): class TestModel(RulesModel): class Meta: - rules_permissions = {"add": rules.always_true, "view": rules.always_true} + rules_permissions = { + "add": {'pred': rules.always_true, 'verbose_name': "Add"}, + "view": rules.always_true, + } @classmethod def preprocess_rules_permissions(cls, perms): - perms["custom"] = rules.always_true + perms["custom"] = {'pred': rules.always_true, 'verbose_name': "Custom Perm"} + perms["custom2"] = rules.always_true diff --git a/tests/testapp/rules.py b/tests/testapp/rules.py index 1d938b9..076807a 100644 --- a/tests/testapp/rules.py +++ b/tests/testapp/rules.py @@ -24,8 +24,10 @@ def is_boss(user): rules.add_rule("change_book", is_book_author | is_editor) rules.add_rule("delete_book", is_book_author) rules.add_rule("create_book", is_boss) +rules.add_rule("borrow_book", is_boss, verbose_name="Borrow the book") # Permissions rules.add_perm("testapp.change_book", is_book_author | is_editor) rules.add_perm("testapp.delete_book", is_book_author) +rules.add_perm("testapp.borrow_book", is_boss, verbose_name="Borrow the book") diff --git a/tests/testsuite/contrib/test_models.py b/tests/testsuite/contrib/test_models.py index 0996e4b..f2e39c8 100644 --- a/tests/testsuite/contrib/test_models.py +++ b/tests/testsuite/contrib/test_models.py @@ -9,7 +9,9 @@ class RulesModelTests(TestCase): def test_preprocess(self): self.assertTrue(rules.perm_exists("testapp.add_testmodel")) + self.assertTrue(rules.perm_exists("testapp.view_testmodel")) self.assertTrue(rules.perm_exists("testapp.custom_testmodel")) + self.assertTrue(rules.perm_exists("testapp.custom2_testmodel")) def test_invalid_config(self): from rules.contrib.models import RulesModel diff --git a/tests/testsuite/test_permissions.py b/tests/testsuite/test_permissions.py index c392647..e2cd95b 100644 --- a/tests/testsuite/test_permissions.py +++ b/tests/testsuite/test_permissions.py @@ -5,6 +5,7 @@ add_perm, has_perm, perm_exists, + perm_verbose_name, permissions, remove_perm, set_perm, @@ -36,6 +37,20 @@ def test_permissions_ruleset(self): remove_perm("can_edit_book") assert not perm_exists("can_edit_book") + def test_permissions_verbose_name(self): + perm_name = "can_shred_book" + add_perm(perm_name, always_true, verbose_name="Can this user shred book?") + assert perm_exists(perm_name) + assert "Can this user shred book?" in perm_verbose_name(perm_name) + assert has_perm(perm_name) + with self.assertRaises(KeyError): + add_perm(perm_name, always_false) + set_perm(perm_name, always_false, verbose_name="User cannot shred book!") + assert "User cannot shred book!" in perm_verbose_name(perm_name) + assert not has_perm(perm_name) + remove_perm(perm_name) + assert not perm_exists(perm_name) + def test_backend(self): backend = ObjectPermissionBackend() assert backend.authenticate("someuser", "password") is None diff --git a/tests/testsuite/test_rulesets.py b/tests/testsuite/test_rulesets.py index 965b873..9f27ea4 100644 --- a/tests/testsuite/test_rulesets.py +++ b/tests/testsuite/test_rulesets.py @@ -7,6 +7,7 @@ default_rules, remove_rule, rule_exists, + rule_verbose_name, set_rule, test_rule, ) @@ -37,6 +38,19 @@ def test_shared_ruleset(self): remove_rule("somerule") assert not rule_exists("somerule") + def test_shared_ruleset_verbose(self): + add_rule("somerule", always_true, verbose_name="Some Rule which is always true") + assert "somerule" in default_rules + assert rule_exists("somerule") + assert test_rule("somerule") + assert "Some Rule which is always true" in rule_verbose_name("somerule") + with self.assertRaises(KeyError): + add_rule("somerule", always_false) + set_rule("somerule", always_false) + assert not test_rule("somerule") + remove_rule("somerule") + assert not rule_exists("somerule") + def test_ruleset(self): ruleset = RuleSet() ruleset.add_rule("somerule", always_true) @@ -49,3 +63,17 @@ def test_ruleset(self): assert not test_rule("somerule") ruleset.remove_rule("somerule") assert not ruleset.rule_exists("somerule") + + def test_ruleset_verbose(self): + ruleset = RuleSet() + ruleset.add_rule("somerule", always_true, verbose_name="Some Rule which is always true") + assert "somerule" in ruleset + assert ruleset.rule_exists("somerule") + assert ruleset.test_rule("somerule") + assert "Some Rule which is always true" in ruleset.rule_verbose_name("somerule") + with self.assertRaises(KeyError): + ruleset.add_rule("somerule", always_true) + ruleset.set_rule("somerule", always_false) + assert not test_rule("somerule") + ruleset.remove_rule("somerule") + assert not ruleset.rule_exists("somerule")