Check-in: Add rule for number of days with entries since (#3808)

This commit is contained in:
Raphael Michel
2024-01-12 17:09:51 +01:00
committed by GitHub
parent bae1512235
commit 0220965ca9
6 changed files with 175 additions and 6 deletions

View File

@@ -280,7 +280,8 @@ class CheckinList(LoggedModel):
'<', '<=', '>', '>=', '==', '!=', 'inList', 'isBefore', 'isAfter', 'or', 'and'
}
allowed_operators = top_level_operators | {
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'
'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before', 'entries_days_since',
'entries_days_before',
}
allowed_vars = {
'product', 'variation', 'now', 'now_isoweekday', 'entries_number', 'entries_today', 'entries_days',
@@ -309,7 +310,7 @@ class CheckinList(LoggedModel):
raise ValidationError(f'Logic variable "{values[0]}" is currently not allowed.')
return rules
if operator in ('entries_since', 'entries_before'):
if operator in ('entries_since', 'entries_before', 'entries_days_since', 'entries_days_before'):
if len(values) != 1 or "buildTime" not in values[0]:
raise ValidationError(f'Operator "{operator}" takes exactly one "buildTime" argument.')

View File

@@ -202,7 +202,7 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
'var': values[0]["var"],
'rhs': values[1:],
}
elif "entries_since" in values[0] or "entries_before" in values[0]:
elif any(t in values[0] for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before")):
_var_explanations[new_var_name] = {
'operator': operator,
'var': values[0],
@@ -280,11 +280,13 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
var_weights[vname] = (500, 0)
var_texts[vname] = _('Wrong entrance gate')
elif var in ('entries_number', 'entries_today', 'entries_days', 'minutes_since_last_entry', 'minutes_since_first_entry', 'now_isoweekday') \
or (isinstance(var, dict) and ("entries_since" in var or "entries_before" in var)):
or (isinstance(var, dict) and any(t in var for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before"))):
w = {
'minutes_since_first_entry': 80,
'minutes_since_last_entry': 90,
'entries_days': 100,
'entries_days_since': 105,
'entries_days_before': 105,
'entries_since': 110,
'entries_before': 110,
'entries_number': 120,
@@ -307,10 +309,12 @@ def _logic_explain(rules, ev, rule_data, now_dt=None):
'entries_today': _('number of entries today'),
'entries_since': _('number of entries since {datetime}'),
'entries_before': _('number of entries before {datetime}'),
'entries_days_since': _('number of days with an entry since {datetime}'),
'entries_days_before': _('number of days with an entry before {datetime}'),
'now_isoweekday': _('week day'),
}
if isinstance(var, dict) and ("entries_since" in var or "entries_before" in var):
if isinstance(var, dict) and any(t in var for t in ("entries_since", "entries_before", "entries_days_since", "entries_days_before")):
varname = list(var.keys())[0]
cutoff = _build_time(*var[varname][0]['buildTime'], ev=ev, now_dt=now_dt).astimezone(ev.timezone)
if abs(now_dt - cutoff) < timedelta(hours=12):
@@ -410,6 +414,8 @@ def _get_logic_environment(ev, rule_data, now_dt):
logic.add_operation('isAfter', lambda t1, t2, tol=None: is_before(t2, t1, tol))
logic.add_operation('entries_since', lambda t1: rule_data.entries_since(t1))
logic.add_operation('entries_before', lambda t1: rule_data.entries_before(t1))
logic.add_operation('entries_days_since', lambda t1: rule_data.entries_days_since(t1))
logic.add_operation('entries_days_before', lambda t1: rule_data.entries_days_before(t1))
return logic
@@ -467,6 +473,32 @@ class LazyRuleVars:
self.__cache['entries_before', cutoff] = self._position.checkins.filter(type=Checkin.TYPE_ENTRY, list=self._clist, datetime__lt=cutoff).count()
return self.__cache['entries_before', cutoff]
def entries_days_since(self, cutoff):
tz = self._clist.event.timezone
with override(tz):
if ('entries_days_since', cutoff) not in self.__cache:
self.__cache['entries_days_since', cutoff] = self._position.checkins.filter(
type=Checkin.TYPE_ENTRY,
list=self._clist,
datetime__gte=cutoff
).annotate(
day=TruncDate('datetime', tzinfo=tz)
).values('day').distinct().count()
return self.__cache['entries_days_since', cutoff]
def entries_days_before(self, cutoff):
tz = self._clist.event.timezone
with override(tz):
if ('entries_days_before', cutoff) not in self.__cache:
self.__cache['entries_days_before', cutoff] = self._position.checkins.filter(
type=Checkin.TYPE_ENTRY,
list=self._clist,
datetime__lt=cutoff
).annotate(
day=TruncDate('datetime', tzinfo=tz)
).values('day').distinct().count()
return self.__cache['entries_days_before', cutoff]
@cached_property
def entries_days(self):
tz = self._clist.event.timezone
@@ -533,7 +565,8 @@ class SQLLogic:
"isBefore": partial(self.comparison_to_q, operator=LowerThan, modifier=partial(tolerance, sign=1)),
"isAfter": partial(self.comparison_to_q, operator=GreaterThan, modifier=partial(tolerance, sign=-1)),
}
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before'}
self.expression_ops = {'buildTime', 'objectList', 'lookup', 'var', 'entries_since', 'entries_before',
'entries_days_since', 'entries_days_before'}
def operation_to_expression(self, rule):
if not isinstance(rule, dict):
@@ -611,6 +644,42 @@ class SQLLogic:
Value(0),
output_field=IntegerField()
)
elif operator == 'entries_days_since':
tz = self.list.event.timezone
return Coalesce(
Subquery(
Checkin.objects.filter(
position_id=OuterRef('pk'),
type=Checkin.TYPE_ENTRY,
list_id=self.list.pk,
datetime__gte=self.operation_to_expression(values[0]),
).annotate(
day=TruncDate('datetime', tzinfo=tz)
).values('position_id').order_by().annotate(
c=Count('day', distinct=True)
).values('c')
),
Value(0),
output_field=IntegerField()
)
elif operator == 'entries_days_before':
tz = self.list.event.timezone
return Coalesce(
Subquery(
Checkin.objects.filter(
position_id=OuterRef('pk'),
type=Checkin.TYPE_ENTRY,
list_id=self.list.pk,
datetime__lt=self.operation_to_expression(values[0]),
).annotate(
day=TruncDate('datetime', tzinfo=tz)
).values('position_id').order_by().annotate(
c=Count('day', distinct=True)
).values('c')
),
Value(0),
output_field=IntegerField()
)
elif operator == 'var':
if values[0] == 'now':
return Value(now().astimezone(timezone.utc))