Skip to content

Commit bd32a2d

Browse files
authored
Merge pull request #1977 from wbonicki/feature/1694
Feature/1694
2 parents 20d43ce + bf2a32f commit bd32a2d

File tree

6 files changed

+136
-16
lines changed

6 files changed

+136
-16
lines changed

extras/docker/production/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@
101101
WGER_SETTINGS["SYNC_OFF_DAILY_DELTA_CELERY"] = env.bool("SYNC_OFF_DAILY_DELTA_CELERY", False)
102102
WGER_SETTINGS["USE_RECAPTCHA"] = env.bool("USE_RECAPTCHA", False)
103103
WGER_SETTINGS["USE_CELERY"] = env.bool("USE_CELERY", False)
104+
WGER_SETTINGS["CACHE_API_EXERCISES_CELERY"] = env.bool("CACHE_API_EXERCISES_CELERY", False)
105+
WGER_SETTINGS["CACHE_API_EXERCISES_CELERY_FORCE_UPDATE"] = env.bool("CACHE_API_EXERCISES_CELERY_FORCE_UPDATE", False)
104106

105107
#
106108
# Auth Proxy Authentication

wger/exercises/cache.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# This file is part of wger Workout Manager.
2+
#
3+
# wger Workout Manager is free software: you can redistribute it and/or modify
4+
# it under the terms of the GNU Affero General Public License as published by
5+
# the Free Software Foundation, either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# wger Workout Manager is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU Affero General Public License
14+
15+
# Standard Library
16+
from typing import Callable
17+
18+
# wger
19+
from wger.exercises.api.serializers import ExerciseInfoSerializer
20+
from wger.exercises.models import Exercise
21+
from wger.utils.cache import reset_exercise_api_cache
22+
23+
24+
def cache_exercise(
25+
exercise: Exercise, force=False, print_fn: Callable = print, style_fn: Callable = lambda x: x
26+
):
27+
"""
28+
Caches a provided exercise.
29+
"""
30+
if force:
31+
print_fn(f'Force updating cache for exercise {exercise.uuid}')
32+
reset_exercise_api_cache(exercise.uuid)
33+
else:
34+
print_fn(f'Warming cache for exercise {exercise.uuid}')
35+
36+
serializer = ExerciseInfoSerializer(exercise)
37+
serializer.data
38+
39+
40+
def cache_api_exercises(
41+
print_fn: Callable,
42+
force: bool,
43+
style_fn: Callable = lambda x: x,
44+
):
45+
print_fn('*** Caching API exercises ***')
46+
for exercise in Exercise.with_translations.all():
47+
cache_exercise(exercise, force, print_fn, style_fn)
48+
print_fn(style_fn('Exercises cached!\n'))

wger/exercises/management/commands/warmup-exercise-api-cache.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@
1616
from django.core.management.base import BaseCommand
1717

1818
# wger
19-
from wger.exercises.api.serializers import ExerciseInfoSerializer
19+
from wger.exercises.cache import cache_exercise
2020
from wger.exercises.models import Exercise
21-
from wger.utils.cache import reset_exercise_api_cache
2221

2322

2423
class Command(BaseCommand):
@@ -48,20 +47,8 @@ def handle(self, **options):
4847

4948
if exercise_id:
5049
exercise = Exercise.objects.get(pk=exercise_id)
51-
self.handle_cache(exercise, force)
50+
cache_exercise(exercise, force, self.stdout.write)
5251
return
5352

5453
for exercise in Exercise.with_translations.all():
55-
self.handle_cache(exercise, force)
56-
57-
def handle_cache(self, exercise: Exercise, force: bool):
58-
if force:
59-
self.stdout.write(f'Force updating cache for exercise base {exercise.uuid}')
60-
else:
61-
self.stdout.write(f'Warming cache for exercise base {exercise.uuid}')
62-
63-
if force:
64-
reset_exercise_api_cache(exercise.uuid)
65-
66-
serializer = ExerciseInfoSerializer(exercise)
67-
serializer.data
54+
cache_exercise(exercise, force, self.stdout.write)

wger/exercises/tasks.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
# wger
2626
from wger.celery_configuration import app
27+
from wger.exercises.cache import cache_api_exercises
2728
from wger.exercises.sync import (
2829
download_exercise_images,
2930
download_exercise_videos,
@@ -70,6 +71,15 @@ def sync_videos_task():
7071
download_exercise_videos(logger.info)
7172

7273

74+
@app.task
75+
def cache_api_exercises_task():
76+
"""
77+
Fetches all exercises from database and caches them.
78+
"""
79+
force = settings.WGER_SETTINGS['CACHE_API_EXERCISES_CELERY_FORCE_UPDATE']
80+
cache_api_exercises(logger.info, force)
81+
82+
7383
@app.on_after_finalize.connect
7484
def setup_periodic_tasks(sender, **kwargs):
7585
if settings.WGER_SETTINGS['SYNC_EXERCISES_CELERY']:
@@ -104,3 +114,14 @@ def setup_periodic_tasks(sender, **kwargs):
104114
sync_videos_task.s(),
105115
name='Sync exercise videos',
106116
)
117+
118+
if settings.WGER_SETTINGS['CACHE_API_EXERCISES_CELERY']:
119+
sender.add_periodic_task(
120+
crontab(
121+
hour=str(random.randint(0, 23)),
122+
minute=str(random.randint(0, 59)),
123+
day_of_week=str(random.randint(0, 6)),
124+
),
125+
cache_api_exercises_task.s(),
126+
name='Cache API exercises',
127+
)

wger/exercises/tests/test_cache.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Standard Library
2+
from unittest.mock import (
3+
MagicMock,
4+
patch,
5+
)
6+
7+
# wger
8+
from wger.core.tests.base_testcase import WgerTestCase
9+
from wger.exercises.cache import (
10+
cache_api_exercises,
11+
cache_exercise,
12+
)
13+
from wger.exercises.models import Exercise
14+
15+
16+
class TestCacheExercise(WgerTestCase):
17+
@patch('wger.exercises.cache.reset_exercise_api_cache')
18+
@patch('wger.exercises.cache.ExerciseInfoSerializer')
19+
def test_cache_exercise_force_true(self, mock_serializer, mock_reset_cache):
20+
exercise = Exercise.objects.first()
21+
output = []
22+
23+
serializer_instance = MagicMock()
24+
serializer_instance.data = {'mocked': True}
25+
mock_serializer.return_value = serializer_instance
26+
27+
cache_exercise(exercise, force=True, print_fn=output.append)
28+
29+
mock_reset_cache.assert_called_once_with(exercise.uuid)
30+
mock_serializer.assert_called_once_with(exercise)
31+
self.assertTrue(any('Force updating cache' in msg for msg in output))
32+
33+
@patch('wger.exercises.cache.reset_exercise_api_cache')
34+
@patch('wger.exercises.cache.ExerciseInfoSerializer')
35+
def test_cache_exercise_force_false(self, mock_serializer, mock_reset_cache):
36+
exercise = Exercise.objects.first()
37+
output = []
38+
39+
serializer_instance = MagicMock()
40+
serializer_instance.data = {'mocked': True}
41+
mock_serializer.return_value = serializer_instance
42+
43+
cache_exercise(exercise, force=False, print_fn=output.append)
44+
45+
mock_reset_cache.assert_not_called()
46+
mock_serializer.assert_called_once_with(exercise)
47+
self.assertTrue(any('Warming cache' in msg for msg in output))
48+
49+
@patch('wger.exercises.cache.cache_exercise')
50+
def test_cache_api_exercises_calls_all(self, mock_cache_exercise):
51+
output = []
52+
called_exercises = []
53+
54+
def fake_cache(exercise, force, print_fn, style_fn):
55+
called_exercises.append(exercise)
56+
57+
mock_cache_exercise.side_effect = fake_cache
58+
cache_api_exercises(print_fn=output.append, force=True)
59+
60+
self.assertTrue(len(called_exercises) > 0)

wger/settings_global.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,8 @@
556556
'SYNC_EXERCISE_VIDEOS_CELERY': False,
557557
'SYNC_INGREDIENTS_CELERY': False,
558558
'SYNC_OFF_DAILY_DELTA_CELERY': False,
559+
'CACHE_API_EXERCISES_CELERY': False,
560+
'CACHE_API_EXERCISES_CELERY_FORCE_UPDATE': False,
559561
'TWITTER': False,
560562
'MASTODON': 'https://fosstodon.org/@wger',
561563
'USE_CELERY': False,

0 commit comments

Comments
 (0)