daisylb / bridgekeeper

Django permissions, but with QuerySets
https://bridgekeeper.leigh.party/
MIT License
41 stars 14 forks source link

Comparison lhs / value (gt, gte, lt, lte) #40

Open weber-s opened 1 year ago

weber-s commented 1 year ago

Hello!

It would be nice to have comparison and not strict equality between lhs and value. For now, we can do:

my_rule = R(field=5)

but we can not do

my_rule = R(field__lt=5)

and that would be terrific!

This would allow stuff like:

start_date_in_futur = Compare(start_date__gte=lambda _: now().date())

can_edit_stuff = R(status="DRAFT") & start_date_in_futur

perms["app.change_model"] = can_edit_stuff

Are you interested in a PR?

The PR would look like this (adapted from R):

ALLOWED_OPERATOR = ["gt", "gte", "le", "lte"]
OPERATOR_MAP = {
    "gt": "__gt__",
    "gte": "__ge__",
    "lt": "__lt__",
    "lte": "__le__",
}

class Compare(R):
    """
    Django-like query for comparison :

        - `Compare(field__gt=3)`
        - `Compare(field__gte=3)`
        - `Compare(field__lt=3)`
        - `Compare(field__gte=3)`
    """

    def __repr__(self):
        return "R({})".format(
            ", ".join("{}__{}={!r}".format(*k.rsplit("__", 1), v) for k, v in self.kwargs.items())
        )

    def check(self, user, instance=None):
        if instance is None:
            return False

        # This loop exits early, returning False, if any argument
        # doesn't match.
        for key, value in self.kwargs.items():

            # Find the appropriate LHS on this object, traversing
            # foreign keys if necessary.
            lhs = instance
            key, operator = key.rsplit("__", 1)

            if operator not in ALLOWED_OPERATOR:
                raise NotImplementedError("Operator must be in %s" % ALLOWED_OPERATOR)

            for key_fragment in key.split("__"):
                field = lhs.__class__._meta.get_field(
                    key_fragment,
                )
                if isinstance(field, ForeignObjectRel):
                    attr = field.get_accessor_name()
                else:
                    attr = key_fragment
                lhs = getattr(lhs, attr)

            # Compare it against the RHS.
            # Note that the LHS will usually be a value, but in the case
            # of a ManyToMany or the 'other side' of a ForeignKey it
            # will be a RelatedManager. In this case, we need to check
            # if there is at least one model that matches the RHS.

            if isinstance(value, Rule):
                raise NotImplementedError("value can not be a Rule")
            # if isinstance(value, Rule):
            #     if isinstance(lhs, Manager):
            #         if not value.filter(user, lhs.all()).exists():
            #             return False
            #     else:
            #         if not value.check(user, lhs):
            #             return False
            # else:
            resolved_value = value(user) if callable(value) else value
            if isinstance(lhs, Manager):
                raise NotImplementedError("lhs cannot be a Manager")
                # if resolved_value not in lhs.all():
                #     return False
            else:
                if not getattr(lhs, OPERATOR_MAP[operator])(resolved_value):
                    return False

        # Woohoo, everything matches!
        return True