1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00
Arthur Hanson af27bf5eff 15094 Add missing gettext to error strings for internationalization (#15155)
* 15049 add missing gettext to error strings

* 15049 add missing gettext to error strings

* 15094 review change

* 15094 review change

* Formatting cleanup

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-02-20 09:44:02 -05:00

162 lines
4.5 KiB
Python

import functools
import re
from django.utils.translation import gettext as _
__all__ = (
'Condition',
'ConditionSet',
)
AND = 'and'
OR = 'or'
def is_ruleset(data):
"""
Determine whether the given dictionary looks like a rule set.
"""
return type(data) is dict and len(data) == 1 and list(data.keys())[0] in (AND, OR)
class Condition:
"""
An individual conditional rule that evaluates a single attribute and its value.
:param attr: The name of the attribute being evaluated
:param value: The value being compared
:param op: The logical operation to use when evaluating the value (default: 'eq')
"""
EQ = 'eq'
GT = 'gt'
GTE = 'gte'
LT = 'lt'
LTE = 'lte'
IN = 'in'
CONTAINS = 'contains'
REGEX = 'regex'
OPERATORS = (
EQ, GT, GTE, LT, LTE, IN, CONTAINS, REGEX
)
TYPES = {
str: (EQ, CONTAINS, REGEX),
bool: (EQ, CONTAINS),
int: (EQ, GT, GTE, LT, LTE, CONTAINS),
float: (EQ, GT, GTE, LT, LTE, CONTAINS),
list: (EQ, IN, CONTAINS),
type(None): (EQ,)
}
def __init__(self, attr, value, op=EQ, negate=False):
if op not in self.OPERATORS:
raise ValueError(_("Unknown operator: {op}. Must be one of: {operators}").format(
op=op, operators=', '.join(self.OPERATORS)
))
if type(value) not in self.TYPES:
raise ValueError(_("Unsupported value type: {value}").format(value=type(value)))
if op not in self.TYPES[type(value)]:
raise ValueError(_("Invalid type for {op} operation: {value}").format(op=op, value=type(value)))
self.attr = attr
self.value = value
self.eval_func = getattr(self, f'eval_{op}')
self.negate = negate
def eval(self, data):
"""
Evaluate the provided data to determine whether it matches the condition.
"""
def _get(obj, key):
if isinstance(obj, list):
return [dict.get(i, key) for i in obj]
return dict.get(obj, key)
try:
value = functools.reduce(_get, self.attr.split('.'), data)
except TypeError:
# Invalid key path
value = None
result = self.eval_func(value)
if self.negate:
return not result
return result
# Equivalency
def eval_eq(self, value):
return value == self.value
def eval_neq(self, value):
return value != self.value
# Numeric comparisons
def eval_gt(self, value):
return value > self.value
def eval_gte(self, value):
return value >= self.value
def eval_lt(self, value):
return value < self.value
def eval_lte(self, value):
return value <= self.value
# Membership
def eval_in(self, value):
return value in self.value
def eval_contains(self, value):
return self.value in value
# Regular expressions
def eval_regex(self, value):
return re.match(self.value, value) is not None
class ConditionSet:
"""
A set of one or more Condition to be evaluated per the prescribed logic (AND or OR). Example:
{"and": [
{"attr": "foo", "op": "eq", "value": 1},
{"attr": "bar", "op": "eq", "value": 2, "negate": true}
]}
:param ruleset: A dictionary mapping a logical operator to a list of conditional rules
"""
def __init__(self, ruleset):
if type(ruleset) is not dict:
raise ValueError(_("Ruleset must be a dictionary, not {ruleset}.").format(ruleset=type(ruleset)))
if len(ruleset) != 1:
raise ValueError(_("Ruleset must have exactly one logical operator (found {ruleset})").format(
ruleset=len(ruleset)))
# Determine the logic type
logic = list(ruleset.keys())[0]
if type(logic) is not str or logic.lower() not in (AND, OR):
raise ValueError(_("Invalid logic type: {logic} (must be '{op_and}' or '{op_or}')").format(
logic=logic, op_and=AND, op_or=OR
))
self.logic = logic.lower()
# Compile the set of Conditions
self.conditions = [
ConditionSet(rule) if is_ruleset(rule) else Condition(**rule)
for rule in ruleset[self.logic]
]
def eval(self, data):
"""
Evaluate the provided data to determine whether it matches this set of conditions.
"""
func = any if self.logic == 'or' else all
return func(d.eval(data) for d in self.conditions)