mirror of
https://github.com/workhardbekind/workout-challenge.git
synced 2026-07-04 01:13:32 -04:00
177 lines
7.2 KiB
Python
177 lines
7.2 KiB
Python
import datetime
|
|
|
|
from django.db.models import Min
|
|
|
|
from django.core.cache import cache
|
|
from workout_challenge.celery import app, is_task_already_executing
|
|
from django.apps import apps
|
|
from django.contrib.auth import get_user_model
|
|
|
|
|
|
def trigger_recalc_points():
|
|
last_recalc = cache.get('last_recalc_points', None)
|
|
|
|
if last_recalc is None or last_recalc < datetime.datetime.now() - datetime.timedelta(seconds=30):
|
|
cache.set('last_recalc_points', datetime.datetime.now(), 60 * 10)
|
|
eta = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=10)
|
|
recalc_points.apply_async(eta=eta)
|
|
else:
|
|
print('Recalc points task skipped because it was triggered less than 30 seconds ago')
|
|
|
|
|
|
@app.task(bind=True, time_limit=60 * 30, max_retries=3) # 30 min time limit
|
|
def recalc_points(self):
|
|
if is_task_already_executing('recalc_points'):
|
|
print('Recalc points task skipped because it is already running')
|
|
return 'Skipped because it is already running.'
|
|
|
|
print('Recalculating points...')
|
|
|
|
ActivityGoal = apps.get_model('competition', 'ActivityGoal')
|
|
Points = apps.get_model('competition', 'Points')
|
|
RecalcRequest = apps.get_model('custom_user', 'RecalcRequest')
|
|
|
|
all_tasks = RecalcRequest.objects.filter(done=False)
|
|
grouped_tasks = all_tasks.values('user', 'goal').annotate(start_datetime=Min('start_datetime'))
|
|
for task_group in grouped_tasks:
|
|
points_lst = Points.objects.filter(goal=task_group['goal'], workout__user=task_group['user'], workout__start_datetime__gte=task_group['start_datetime']).order_by('workout__start_datetime')
|
|
|
|
goal = ActivityGoal.objects.get(pk=task_group['goal'])
|
|
|
|
scorer = Scorer()
|
|
scorer.set_goal(goal)
|
|
|
|
for points in points_lst:
|
|
earned_points = scorer.calculate_points(points)
|
|
setattr(points, 'points_capped', earned_points)
|
|
points.save()
|
|
|
|
all_tasks.delete()
|
|
print('All points recalculated.')
|
|
return [{k: str(v) for k, v in i.items()} for i in grouped_tasks]
|
|
|
|
|
|
|
|
|
|
|
|
class Scorer:
|
|
def __init__(self):
|
|
self.memory_today = None
|
|
self.memory_today_points_raw = 0
|
|
self.memory_today_points_capped = 0
|
|
self.memory_this_week = None
|
|
self.memory_week_points_raw = 0
|
|
self.memory_week_points_capped = 0
|
|
|
|
def set_goal(self, goal):
|
|
self.goal = goal
|
|
self.floor_workout = 0 if goal.min_per_workout is None else goal.min_per_workout / goal.goal * 100
|
|
self.cap_workout = None if goal.max_per_workout is None else goal.max_per_workout / goal.goal * 100
|
|
self.floor_day = 0 if goal.min_per_day is None else goal.min_per_day / goal.goal * 100
|
|
self.cap_day = None if goal.max_per_day is None else goal.max_per_day / goal.goal * 100
|
|
self.floor_week = 0 if goal.min_per_week is None else goal.min_per_week / goal.goal * 100
|
|
self.cap_week = None if goal.max_per_week is None else goal.max_per_week / goal.goal * 100
|
|
|
|
def calculate_points(self, points):
|
|
# potentially reset the memory if new day / week
|
|
if points.workout.start_datetime.date() != self.memory_today:
|
|
self.memory_today = points.workout.start_datetime.date()
|
|
self.memory_today_points_raw = 0
|
|
self.memory_today_points_capped = 0
|
|
if points.workout.start_datetime.isocalendar()[1] != self.memory_this_week:
|
|
self.memory_this_week = points.workout.start_datetime.isocalendar()[1]
|
|
self.memory_week_points_raw = 0
|
|
self.memory_week_points_capped = 0
|
|
|
|
earned_points = points.points_raw
|
|
self.memory_today_points_raw += earned_points
|
|
self.memory_week_points_raw += earned_points
|
|
earned_points = points.points_raw
|
|
|
|
# workout floor
|
|
earned_points = max(earned_points - self.floor_workout, 0)
|
|
|
|
# workout cap
|
|
if self.cap_workout is not None:
|
|
adjusted_cap = self.cap_workout - self.floor_workout
|
|
earned_points = min(earned_points, adjusted_cap)
|
|
|
|
# day floor
|
|
earned_points = max(min(earned_points, self.memory_today_points_raw - self.floor_day), 0)
|
|
|
|
# day cap
|
|
if self.cap_day is not None:
|
|
max_points_to_earn_today = self.cap_day - self.floor_day
|
|
earned_points = max(min(earned_points, max_points_to_earn_today - self.memory_today_points_capped),0)
|
|
|
|
# week floor
|
|
earned_points = max(min(earned_points, self.memory_week_points_raw - self.floor_week), 0)
|
|
|
|
# week cap
|
|
if self.cap_week is not None:
|
|
max_points_to_earn_week = self.cap_week - self.floor_week
|
|
earned_points = max(min(earned_points, max_points_to_earn_week - self.memory_week_points_capped),0)
|
|
|
|
|
|
self.memory_today_points_capped += earned_points
|
|
self.memory_week_points_capped += earned_points
|
|
return earned_points
|
|
|
|
|
|
class DummyObject:
|
|
def __init__(self, **kwargs):
|
|
self.min_per_workout = None
|
|
self.max_per_workout = None
|
|
self.min_per_day = None
|
|
self.max_per_day = None
|
|
self.min_per_week = None
|
|
self.max_per_week = None
|
|
for key, value in kwargs.items():
|
|
setattr(self, key, value)
|
|
|
|
|
|
def test_scorer():
|
|
|
|
for goal_kwargs, points, expected_result in (
|
|
({'goal': 100, 'min_per_workout': 10}, [10], 0),
|
|
({'goal': 100, 'min_per_workout': 10}, [20], 10),
|
|
({'goal': 100, 'max_per_workout': 30}, [30], 30),
|
|
({'goal': 100, 'max_per_workout': 30}, [40], 30),
|
|
({'goal': 100, 'min_per_workout': 10, 'max_per_workout': 30}, [40], 20),
|
|
({'goal': 100, 'min_per_day': 10}, [10], 0),
|
|
({'goal': 100, 'min_per_day': 10}, [20], 10),
|
|
({'goal': 100, 'min_per_day': 10}, [8, 8], 6),
|
|
({'goal': 100, 'max_per_day': 30}, [20], 20),
|
|
({'goal': 100, 'max_per_day': 30}, [20, 20], 30),
|
|
({'goal': 100, 'min_per_day': 10, 'max_per_day': 30}, [8, 12, 8, 8, 14], 20),
|
|
({'goal': 100, 'min_per_week': 10}, [10], 0),
|
|
({'goal': 100, 'min_per_week': 10}, [20], 10),
|
|
({'goal': 100, 'max_per_week': 30}, [20], 20),
|
|
({'goal': 100, 'max_per_week': 30}, [20, 20], 30),
|
|
({'goal': 100, 'min_per_week': 10, 'max_per_week': 30}, [8, 12, 8, 8, 14], 20),
|
|
({'goal': 100, 'min_per_workout': 10, 'min_per_day': 20}, [5, 20, 5, 20], 15),
|
|
({'goal': 100, 'min_per_workout': 20, 'min_per_day': 10}, [5, 30, 30], 20),
|
|
({'goal': 100, 'max_per_workout': 20, 'max_per_day': 30}, [20, 25, 25, 25], 30),
|
|
({'goal': 100, 'max_per_workout': 30, 'max_per_day': 20}, [20, 25, 25, 25], 20),
|
|
({'goal': 100, 'min_per_workout': 10, 'max_per_day': 15}, [5, 5, 5, 5], 0),
|
|
({'goal': 100, 'min_per_workout': 10, 'max_per_day': 30}, [15, 35, 5, 15], 30),
|
|
):
|
|
workout = DummyObject(start_datetime=datetime.datetime.fromisoformat('2023-01-01T00:00:00'))
|
|
goal = DummyObject(**goal_kwargs)
|
|
scorer = Scorer()
|
|
scorer.set_goal(goal)
|
|
|
|
earned_points = 0
|
|
for point in points:
|
|
point_obj = DummyObject(points_raw=point, workout = workout)
|
|
earned_points += scorer.calculate_points(point_obj)
|
|
|
|
assert earned_points == expected_result, f'Expected {expected_result}, got {earned_points} for goal {goal_kwargs}'
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
test_scorer()
|
|
|