Skip to content
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

Add verbose_name for rules and permissions #154

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

jacklinke
Copy link

Adds verbose_name for rules and permissions, resolving #59.

This change should be transparent to any existing users of django-rules, but adds the new capability to add and display verbose names for rules & permissions through the use of new/updated RuleSet instance methods and shortcuts for shared and permissions rule sets.

  • add_rule(name, predicate, verbose_name=None): Updated to allow optional verbose_name.
  • set_rule(name, predicate, verbose_name=None): Updated to allow optional verbose_name
  • rule_verbose_name(name): A new method which returns the verbose_name if it was supplied when adding or setting the rule. Defaults to name if no verbose_name was supplied.
  • (similar method names for permissions)

Models can now also optionally add verbose names to permissions using the following conventions:

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, verbose_name= "Can read this book")
rules.add_perm("app_label.delete_book", rules.is_staff)

Tasks:

  • Update code
  • Add and run tests
  • Update Readme

I welcome any feedback.

@dfunckt
Copy link
Owner

dfunckt commented Nov 29, 2021

Great stuff! 👍

My only concern is we're changing the structure of the items stored in rule sets -- RuleSets used to be a map of strings to predicates, whereas now it is going to be a map of strings to dicts. This is technically a breaking change and can catch users by surprise.

Maybe override __getitem__ et al to return the predicate like it did before? The important use cases are using the rule set as a dict (ie. get, set, delete items) and iteration which should continue to behave as before. Tests that ensure that would be much appreciated.

Otherwise this looks like a great addition, thank you for that!

@jacklinke
Copy link
Author

Glad this is seen as something beneficial, and thanks for the advice/direction.

I updated the code, returning the predicate if the dict is accessed directly (e.g.: rule_set_instance["permission_name"]), but added the option to prefix/suffix the permission name with 'get_' and '_display' respectively to return the verbose_name (e.g.: rule_set_instance["get_permission_name_display"])

I figured following a similar approach as Django's get_FOO_display() method for getting the display names of choice fields made sense here.

If that sounds sensible, I'll add tests and docs this week to finish this out.

Copy link
Owner

@dfunckt dfunckt left a comment

Choose a reason for hiding this comment

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

@jacklinke apologies for the delayed review. Great stuff 👍

I left a few comments. There also seem to be a few lint/formatting issues highlighted by the checks -- can you please take a look at those too?

@@ -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?

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.

Comment on lines +32 to +42
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'])

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.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants