Skip to content

Add verbose_name for rules and permissions #154

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 44 additions & 12 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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']
<Predicate:is_group_member:special object at 0x10eeaa500>
>>> 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 --
Expand Down Expand Up @@ -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)``
Expand All @@ -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
Expand Down Expand Up @@ -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``.

Expand All @@ -1065,17 +1089,21 @@ 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``.


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)``
Expand All @@ -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``.

Expand Down
5 changes: 4 additions & 1 deletion rules/contrib/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please rename the pred attribute to predicate?

else:
add_perm(new_class.get_perm(perm_type), predicate)
return new_class


Expand Down
12 changes: 8 additions & 4 deletions rules/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)

Expand Down
5 changes: 4 additions & 1 deletion rules/predicates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
71 changes: 56 additions & 15 deletions rules/rulesets.py
Original file line number Diff line number Diff line change
@@ -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
Copy link
Owner

@dfunckt dfunckt Dec 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return self["get_%s_display" % name] or name
return self["get_%s_display" % name]

This must return None if no verbose name has been provided for the rule and callers can choose whether to fallback to 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'])

Comment on lines +32 to +42
Copy link
Owner

@dfunckt dfunckt Dec 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this can just be:

Suggested change
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'])
if isinstance(pred, dict):
pred['pred'] = predicate(pred['pred'])
else:
pred = {'pred': predicate(pred), 'verbose_name': None}

Or even:

Suggested change
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'])
if not isinstance(pred, dict):
pred = {'pred': predicate(pred), 'verbose_name': None}

if we assume anyone passing a dict know what they're doing.

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)
Comment on lines +46 to +58
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this can just be:

Suggested change
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)
prefix = "get_"
suffix = "_display"
if name.startswith(prefix) and name.endswith(suffix):
_name = name[len(prefix):-len(suffix)]
return super(RuleSet, self).__getitem__(_name)['verbose_name']

Right?

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)
8 changes: 6 additions & 2 deletions tests/testapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions tests/testapp/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
2 changes: 2 additions & 0 deletions tests/testsuite/contrib/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions tests/testsuite/test_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
add_perm,
has_perm,
perm_exists,
perm_verbose_name,
permissions,
remove_perm,
set_perm,
Expand Down Expand Up @@ -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
Expand Down
Loading