Skip to content

Commit

Permalink
feat(implement-cache) Enable Cache nutritional values for plan


Browse files Browse the repository at this point in the history
This commit cache the nutritional_info dictionary in
NutritionPlans and add signals that delete the cache

Currently, the nutrition plans is not cached so If a user has many plans,
the overview renders slowly as too many DB-queries are fired just to
calculate the total calories. Some caching of the values will
speed up rendering of the overview

[Delivers #157731610]
  • Loading branch information
hariclerry committed Jun 18, 2018
1 parent ed956c8 commit 2397166
Show file tree
Hide file tree
Showing 17 changed files with 207 additions and 60 deletions.
1 change: 0 additions & 1 deletion wger/exercises/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,3 @@ class MuscleSerializer(serializers.ModelSerializer):
'''
class Meta:
model = Muscle

1 change: 0 additions & 1 deletion wger/exercises/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,3 @@ class MuscleViewSet(viewsets.ReadOnlyModelViewSet):
ordering_fields = '__all__'
filter_fields = ('name',
'is_front')

4 changes: 3 additions & 1 deletion wger/exercises/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from wger.exercises.models import ExerciseImage, Muscle, Exercise
from wger.utils.cache import delete_template_fragment_cache, get_template_cache_name, cache


@receiver(pre_delete, sender=Muscle)
def delete_exercise_template_on_delete(sender, instance, **kwargs):
'''
Expand All @@ -33,12 +34,13 @@ def delete_exercise_template_on_delete(sender, instance, **kwargs):

delete_template_fragment_cache('muscle-overview')
exercises_to_update = Exercise.objects.filter(muscles=instance)
if len(exercises_to_update) >0:
if len(exercises_to_update) > 0:
for exc in exercises_to_update:

delete_template_fragment_cache('exercise-detail-muscles', exc.id, exc.language.id)
delete_template_fragment_cache('exercise-overview', exc.language.id)


@receiver(post_delete, sender=ExerciseImage)
def delete_exercise_image_on_delete(sender, instance, **kwargs):
'''
Expand Down
1 change: 0 additions & 1 deletion wger/exercises/tests/test_exercise.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,4 +553,3 @@ def test_exercise_read_only(self):
response = client.post('/api/v2/exercises/', data=data)
# Test for method not allowed
self.assertEqual(response.status_code, 405)

2 changes: 2 additions & 0 deletions wger/nutrition/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@
from wger import get_version

VERSION = get_version()

default_app_config = 'wger.nutrition.apps.NutritionPlanConfig'
25 changes: 25 additions & 0 deletions wger/nutrition/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-

# This file is part of wger Workout Manager.
#
# wger Workout Manager is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# wger Workout Manager is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License

from django.apps import AppConfig


class NutritionPlanConfig(AppConfig):
name = 'wger.nutrition'
verbose_name = "Nutrition"

def ready(self):
import wger.nutrition.signals
95 changes: 51 additions & 44 deletions wger/nutrition/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,50 +109,57 @@ def get_nutritional_values(self):
'''
Sums the nutritional info of all items in the plan
'''
use_metric = self.user.userprofile.use_metric
unit = 'kg' if use_metric else 'lb'
result = {'total': {'energy': 0,
'protein': 0,
'carbohydrates': 0,
'carbohydrates_sugar': 0,
'fat': 0,
'fat_saturated': 0,
'fibres': 0,
'sodium': 0},
'percent': {'protein': 0,
'carbohydrates': 0,
'fat': 0},
'per_kg': {'protein': 0,
'carbohydrates': 0,
'fat': 0},
}

# Energy
for meal in self.meal_set.select_related():
values = meal.get_nutritional_values(use_metric=use_metric)
for key in result['total'].keys():
result['total'][key] += values[key]

energy = result['total']['energy']

# In percent
if energy:
for key in result['percent'].keys():
result['percent'][key] = \
result['total'][key] * ENERGY_FACTOR[key][unit] / energy * 100

# Per body weight
weight_entry = self.get_closest_weight_entry()
if weight_entry:
for key in result['per_kg'].keys():
result['per_kg'][key] = result['total'][key] / weight_entry.weight

# Only 2 decimal places, anything else doesn't make sense
for key in result.keys():
for i in result[key]:
result[key][i] = Decimal(result[key][i]).quantize(TWOPLACES)

return result
nutritional_values_canonical_form = cache.get(
cache_mapper.get_nutritional_values_canonical(self.pk))
if not nutritional_values_canonical_form:
use_metric = self.user.userprofile.use_metric
unit = 'kg' if use_metric else 'lb'
result = {'total': {'energy': 0,
'protein': 0,
'carbohydrates': 0,
'carbohydrates_sugar': 0,
'fat': 0,
'fat_saturated': 0,
'fibres': 0,
'sodium': 0},
'percent': {'protein': 0,
'carbohydrates': 0,
'fat': 0},
'per_kg': {'protein': 0,
'carbohydrates': 0,
'fat': 0},
}

# Energy
for meal in self.meal_set.select_related():
values = meal.get_nutritional_values(use_metric=use_metric)
for key in result['total'].keys():
result['total'][key] += values[key]

energy = result['total']['energy']

# In percent
if energy:
for key in result['percent'].keys():
result['percent'][key] = \
result['total'][key] * ENERGY_FACTOR[key][unit] / energy * 100

# Per body weight
weight_entry = self.get_closest_weight_entry()
if weight_entry:
for key in result['per_kg'].keys():
result['per_kg'][key] = result['total'][key] / weight_entry.weight

# Only 2 decimal places, anything else doesn't make sense
for key in result.keys():
for i in result[key]:
result[key][i] = Decimal(result[key][i]).quantize(TWOPLACES)

nutritional_values_canonical_form = result

cache.set(cache_mapper.get_nutritional_values_canonical(self.pk),
nutritional_values_canonical_form)
return nutritional_values_canonical_form

def get_closest_weight_entry(self):
'''
Expand Down
42 changes: 42 additions & 0 deletions wger/nutrition/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-

# This file is part of wger Workout Manager.
#
# wger Workout Manager is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# wger Workout Manager is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License


from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver

from wger.nutrition.models import NutritionPlan, Meal, MealItem
from wger.utils.cache import reset_nutritional_values_canonical_form


@receiver(post_save, sender=NutritionPlan)
@receiver(post_save, sender=Meal)
@receiver(post_save, sender=MealItem)
@receiver(post_delete, sender=NutritionPlan)
@receiver(post_delete, sender=Meal)
@receiver(post_delete, sender=MealItem)
def delete_cache(sender, **kwargs):
""" Function for intercepting signals """

sender_instance = kwargs['instance']
if sender == NutritionPlan:
pk = sender_instance.pk
elif sender == Meal:
pk = sender_instance.plan.pk
elif sender == MealItem:
pk = sender_instance.meal.plan.pk

reset_nutritional_values_canonical_form(pk)
1 change: 1 addition & 0 deletions wger/nutrition/templates/plan/overview.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

{% block content %}
<div class="list-group">

{% for plan in plans %}
<a href="{{ plan.get_absolute_url }}" class="list-group-item">
<span class="glyphicon glyphicon-chevron-right pull-right"></span>
Expand Down
5 changes: 3 additions & 2 deletions wger/nutrition/tests/test_meal.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
WorkoutManagerEditTestCase,
WorkoutManagerAddTestCase
)
from wger.nutrition.models import Meal,MealItem
from wger.nutrition.models import Meal, MealItem
from wger.nutrition.models import NutritionPlan


Expand Down Expand Up @@ -61,13 +61,14 @@ class AddMealTestCase(WorkoutManagerAddTestCase):
user_success = 'test'
user_fail = 'admin'


class AddMealItemTestCase(WorkoutManagerAddTestCase):
'''
Tests adding a Meal with meal items
'''
object_class = MealItem
url = reverse('nutrition:meal_item:add_meal', kwargs={'plan_pk': 4})
data = {'time': datetime.time(9, 2),'ingredient': 1,'amount': 1}
data = {'time': datetime.time(9, 2), 'ingredient': 1, 'amount': 1}
user_success = 'test'
user_fail = 'test1'

Expand Down
59 changes: 59 additions & 0 deletions wger/nutrition/tests/test_nutritional_values_canonical.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# This file is part of wger Workout Manager.
#
# wger Workout Manager is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# wger Workout Manager is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License


import datetime
from django.core.cache import cache
from wger.utils.cache import cache_mapper
from wger.core.tests.base_testcase import WorkoutManagerTestCase
from wger.nutrition.models import NutritionPlan


class NutritionInfoCacheTestCase(WorkoutManagerTestCase):
'''
Test case for the nutritional values caching
'''

def test_meal_nutritional_values_cache(self):
'''
Tests that the nutrition cache of the canonical form is
correctly generated
'''
self.assertFalse(cache.get(cache_mapper.get_nutritional_values_canonical(1)))

plan = NutritionPlan.objects.get(pk=1)
plan.get_nutritional_values()
self.assertTrue(cache.get(cache_mapper.get_nutritional_values_canonical(1)))

def test_nutritional_values_cache_save(self):
'''
Tests nutritional values cache when saving
'''
plan = NutritionPlan.objects.get(pk=1)
plan.get_nutritional_values()
self.assertTrue(cache.get(cache_mapper.get_nutritional_values_canonical(1)))

plan.save()
self.assertFalse(cache.get(cache_mapper.get_nutritional_values_canonical(1)))

def test_nutritional_values_cache_delete(self):
'''
Tests the nutritional values cache when deleting
'''
plan = NutritionPlan.objects.get(pk=1)
plan.get_nutritional_values()
self.assertTrue(cache.get(cache_mapper.get_nutritional_values_canonical(1)))

plan.delete()
self.assertFalse(cache.get(cache_mapper.get_nutritional_values_canonical(1)))
11 changes: 5 additions & 6 deletions wger/nutrition/views/meal_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ def dispatch(self, request, *args, **kwargs):
else:
return HttpResponseForbidden()
return super(MealItemCreateView, self).dispatch(request, *args, **kwargs)


def get_success_url(self):
return reverse('nutrition:plan:view', kwargs={'id': self.meal.plan.id})
Expand All @@ -85,26 +84,26 @@ def get_context_data(self, **kwargs):
context = super(MealItemCreateView, self).get_context_data(**kwargs)
if self.meal_id:
context['form_action'] = reverse('nutrition:meal_item:add',
kwargs={'meal_id': self.meal.id})
kwargs={'meal_id': self.meal.id})
else:
#create meal based on the nutrition plan id
# create meal based on the nutrition plan id
context['form_action'] = reverse('nutrition:meal_item:add_meal',
kwargs={'plan_pk': self.kwargs['plan_pk']})
kwargs={'plan_pk': self.kwargs['plan_pk']})
context['ingredient_searchfield'] = self.request.POST.get('ingredient_searchfield', '')
context['plan_pk'] = self.plan_pk
return context

def form_valid(self, form):
'''
Manually set the corresponding meal
'''
'''
if not self.meal_id:
plan = get_object_or_404(NutritionPlan, pk=self.kwargs['plan_pk'])
meal = Meal.objects.create(plan=plan, order=1)
meal.time = form.cleaned_data['time']
meal.save()
self.meal = meal

form.instance.meal = self.meal
form.instance.order = 1
return super(MealItemCreateView, self).form_valid(form)
Expand Down
1 change: 0 additions & 1 deletion wger/settings_global.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,4 +412,3 @@

if 'DATABASE_URL' in os.environ:
DATABASES = {'default': dj_database_url.config()}

11 changes: 11 additions & 0 deletions wger/utils/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ def reset_workout_canonical_form(workout_id):
cache.delete(cache_mapper.get_workout_canonical(workout_id))


def reset_nutritional_values_canonical_form(value_id):
cache.delete(cache_mapper.get_nutritional_values_canonical(value_id))


def reset_workout_log(user_pk, year, month, day=None):
'''
Resets the cached workout logs
Expand All @@ -67,6 +71,7 @@ class CacheKeyMapper(object):
INGREDIENT_CACHE_KEY = 'ingredient-{0}'
WORKOUT_CANONICAL_REPRESENTATION = 'workout-canonical-representation-{0}'
WORKOUT_LOG_LIST = 'workout-log-hash-{0}'
NUTRITIONAL_VALUES_CANONICAL_REPRESENTATION = 'nutritional-canonical-representation-{0}'

def get_pk(self, param):
'''
Expand Down Expand Up @@ -115,5 +120,11 @@ def get_workout_log_list(self, hash_value):
'''
return self.WORKOUT_LOG_LIST.format(hash_value)

def get_nutritional_values_canonical(self, param):
'''
Return the nutritional values canonical representation
'''
return self.NUTRITIONAL_VALUES_CANONICAL_REPRESENTATION.format(self.get_pk(param))


cache_mapper = CacheKeyMapper()
Loading

0 comments on commit 2397166

Please sign in to comment.