Merge branch 'calendar_day'

This commit is contained in:
Roland Geider
2015-02-18 16:53:30 +01:00
32 changed files with 553 additions and 317 deletions

View File

@@ -39,7 +39,7 @@ script:
- coverage run --source='.' ./manage.py test
# Formatting
- pep8 --max-line-length=100 --exclude="urls.py,*migrations*" wger
- pep8 wger
# Code coverage
- coverage report

View File

@@ -91,7 +91,7 @@ Contributing
* **Code according to PEP8**: check that the code is structured as per pep8 but
with a maximum line length of 100. This can be checked automatically with the
pep8 tool (pip install pep8) from the command line (travis will do this as part
of the tests): ``pep8 --max-line-length=100--exclude="urls.py,*migrations*" wger``
of the tests): ``pep8 wger``
* **code for python3**: while the application should remain compatible with
python2, use django's suggestion to mantain sanity: code for py3 and treat

4
setup.cfg Normal file
View File

@@ -0,0 +1,4 @@
[pep8]
exclude = urls.py,*migrations*
max-line-length = 100
ignore = W503

View File

@@ -26,4 +26,10 @@ class UserprofileSerializer(serializers.ModelSerializer):
'''
class Meta:
model = UserProfile
exclude = ('user',)
class UsernameSerializer(serializers.Serializer):
'''
Serializer to extract the username
'''
username = serializers.CharField()

View File

@@ -17,11 +17,14 @@
from django.contrib.auth.models import User
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.decorators import link
from wger.core.models import UserProfile
from wger.core.models import Language
from wger.core.models import DaysOfWeek
from wger.core.models import License
from wger.core.api.serializers import UsernameSerializer
from wger.core.api.serializers import UserprofileSerializer
from wger.utils.permissions import UpdateOnlyPermission
from wger.utils.permissions import WgerPermission
@@ -49,6 +52,15 @@ class UserProfileViewSet(viewsets.ModelViewSet):
'''
return [(User, 'user')]
@link()
def username(self, request, pk):
'''
Return the username
'''
user = self.get_object().user
return Response(UsernameSerializer(user).data)
class LanguageViewSet(viewsets.ReadOnlyModelViewSet):
'''

View File

@@ -116,7 +116,7 @@ def create_demo_entries(user):
exercise = Exercise.objects.get(pk=25)
else:
exercise = Exercise.objects.get(pk=84)
day_set = Set(exerciseday=day, sets=4, order=2)
day_set = Set(exerciseday=day, sets=4, order=2)
day_set.save()
day_set.exercises.add(exercise)

View File

@@ -58,11 +58,30 @@ class Command(BaseCommand):
# Exercises, cached template fragments
if options['clear_template']:
if int(options['verbosity']) >= 2:
self.stdout.write("*** Clearing templates")
for user in User.objects.all():
for entry in WorkoutLog.objects.dates('date', 'year'):
for month in range(1, 13):
# print("User {0}, year {1}, month {2}".format(user.pk, entry.year, month))
reset_workout_log(user.id, entry.year, month)
if int(options['verbosity']) >= 2:
self.stdout.write("* Processing user {0}".format(user.username))
for entry in WorkoutLog.objects.filter(user=user).dates('date', 'year'):
if int(options['verbosity']) >= 3:
self.stdout.write(" Year {0}".format(entry.year))
for month in WorkoutLog.objects.filter(user=user,
date__year=entry.year).dates('date',
'month'):
if int(options['verbosity']) >= 3:
self.stdout.write(" Month {0}".format(entry.month))
reset_workout_log(user.id, entry.year, entry.month)
for day in WorkoutLog.objects.filter(user=user,
date__year=entry.year,
date__month=month.month).dates('date',
'day'):
if int(options['verbosity']) >= 3:
self.stdout.write(" Day {0}".format(day.day))
reset_workout_log(user.id, entry.year, entry.month, day)
for language in Language.objects.all():
delete_template_fragment_cache('muscle-overview', language.id)

View File

@@ -165,6 +165,30 @@ function set_profile_field(field, newValue) {
});
}
function get_username() {
/*
* Get the current user's username
*
* Do not use with anonymous users!
*
* Syncronous request, use sparingly!
*/
var user_id = get_profile_field('user');
var result;
$.ajax({
url:'/api/v2/userprofile/' + user_id + '/username/',
type: 'GET',
async: false,
success: function(user) {
result = user.username;
}
});
return result;
}
function get_profile_field(field) {
/*
* Get a single field from the user's profile
@@ -174,13 +198,13 @@ function get_profile_field(field) {
var result;
$.ajax({
url:'/api/v2/userprofile/',
type: 'GET',
async: false,
success: function(userprofile) {
result = userprofile.results[0][field];
}
});
url:'/api/v2/userprofile/',
type: 'GET',
async: false,
success: function(userprofile) {
result = userprofile.results[0][field];
}
});
return result;
}

View File

@@ -76,7 +76,7 @@ def pagination(paginator, page):
page_range = paginator.page_range
# Set the template variables
return {'page': page,
return {'page': page,
'page_range': page_range}

View File

@@ -207,8 +207,8 @@ class PreferencesCalculationsTestCase(WorkoutManagerTestCase):
user = User.objects.get(pk=2)
bmi = user.userprofile.calculate_bmi()
self.assertEqual(bmi,
user.userprofile.weight.quantize(TWOPLACES)
/ decimal.Decimal(1.80 * 1.80).quantize(TWOPLACES))
user.userprofile.weight.quantize(TWOPLACES) /
decimal.Decimal(1.80 * 1.80).quantize(TWOPLACES))
def test_basal_metabolic_rate(self):
'''

View File

@@ -46,7 +46,7 @@ class Command(BaseCommand):
dest='remote_url',
default='https://wger.de',
help='Remote URL to fetch the exercises from (default: https://wger.de)'),
)
)
help = ('Download exercise images from wger.de and update the local database\n'
'\n'

View File

@@ -15,12 +15,16 @@
# You should have received a copy of the GNU Affero General Public License
# along with Workout Manager. If not, see <http://www.gnu.org/licenses/>.
import datetime
from calendar import HTMLCalendar
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.platypus import Paragraph
from reportlab.platypus import Table
from reportlab.platypus import KeepTogether
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from wger.utils.pdf import styleSheet
@@ -148,3 +152,84 @@ def reps_smart_text(settings, set_obj):
setting_list = tmp_reps
return setting_text, setting_list
class WorkoutCalendar(HTMLCalendar):
'''
A calendar renderer, see this blog entry for details:
* http://uggedal.com/journal/creating-a-flexible-monthly-calendar-in-django/
'''
def __init__(self, workout_logs, *args, **kwargs):
super(WorkoutCalendar, self).__init__(*args, **kwargs)
self.workout_logs = workout_logs
def formatday(self, day, weekday):
# days belonging to last or next month are rendered empty
if day == 0:
return self.day_cell('noday', '&nbsp;')
date_obj = datetime.date(self.year, self.month, day)
cssclass = self.cssclasses[weekday]
if datetime.date.today() == date_obj:
cssclass += ' today'
# There are no logs for this day, doesn't need special attention
if date_obj not in self.workout_logs:
return self.day_cell(cssclass, day)
# Day with a log, set background and link
entry = self.workout_logs.get(date_obj)
# Note: due to circular imports we use can't import the workout session
# model to access the impression values directly, so they are hard coded
# here.
if entry['session']:
# Bad
if entry['session'].impression == '1':
background_css = 'btn-danger'
# Good
elif entry['session'].impression == '3':
background_css = 'btn-success'
# Neutral
else:
background_css = 'btn-warning'
else:
background_css = 'btn-warning'
url = reverse('manager:log:log', kwargs={'pk': entry['workout'].id})
formatted_date = date_obj.strftime('%Y-%m-%d')
body = []
body.append('<a href="{0}" '
'data-log="log-{1}" '
'class="btn btn-block {2} calendar-link">'.format(url,
formatted_date,
background_css))
body.append(repr(day))
body.append('</a>')
return self.day_cell(cssclass, '{0}'.format(''.join(body)))
def formatmonth(self, year, month):
'''
Format the table header. This is basically the same code from python's
calendar module but with additional bootstrap classes
'''
self.year, self.month = year, month
out = []
out.append('<table class="month table table-bordered">\n')
out.append(self.formatmonthname(year, month))
out.append('\n')
out.append(self.formatweekheader())
out.append('\n')
for week in self.monthdays2calendar(year, month):
out.append(self.formatweek(week))
out.append('\n')
out.append('</table>\n')
return ''.join(out)
def day_cell(self, cssclass, body):
'''
Renders a day cell
'''
return '<td class="{0}" style="vertical-align: middle;">{1}</td>'.format(cssclass, body)

View File

@@ -660,14 +660,14 @@ class WorkoutLog(models.Model):
'''
Reset cache
'''
reset_workout_log(self.user_id, self.date.year, self.date.month)
reset_workout_log(self.user_id, self.date.year, self.date.month, self.date.day)
super(WorkoutLog, self).save(*args, **kwargs)
def delete(self, *args, **kwargs):
'''
Reset cache
'''
reset_workout_log(self.user_id, self.date.year, self.date.month)
reset_workout_log(self.user_id, self.date.year, self.date.month, self.date.day)
super(WorkoutLog, self).delete(*args, **kwargs)
@@ -677,6 +677,7 @@ class WorkoutSession(models.Model):
Model for a workout session
'''
# Note: values hardcoded in manager.helpers.WorkoutCalendar
IMPRESSION_BAD = '1'
IMPRESSION_NEUTRAL = '2'
IMPRESSION_GOOD = '3'

View File

@@ -83,15 +83,15 @@ def workout_log(request, id, uidb64=None, token=None):
{'description': workout},
styleSheet["HeaderBold"])
elements.append(p)
elements.append(Spacer(10*cm, 0.5*cm))
elements.append(Spacer(10 * cm, 0.5 * cm))
# Iterate through the Workout and render the training days
for day in workout.canonical_representation['day_list']:
elements.append(render_workout_day(day, nr_of_weeks=7))
elements.append(Spacer(10*cm, 0.5*cm))
elements.append(Spacer(10 * cm, 0.5 * cm))
# Footer, date and info
elements.append(Spacer(10*cm, 0.5*cm))
elements.append(Spacer(10 * cm, 0.5 * cm))
elements.append(render_footer(request.build_absolute_uri(workout.get_absolute_url())))
# write the document and send the response to the browser
@@ -277,13 +277,13 @@ def workout_view(request, id, uidb64=None, token=None):
elements.append(p)
# Filler
elements.append(Spacer(10*cm, 0.5*cm))
elements.append(Spacer(10 * cm, 0.5 * cm))
# Append the table
elements.append(t)
# Footer, date and info
elements.append(Spacer(10*cm, 0.5*cm))
elements.append(Spacer(10 * cm, 0.5 * cm))
created = datetime.date.today().strftime("%d.%m.%Y")
p = Paragraph('''<para align="left">
%(date)s -

View File

@@ -0,0 +1,89 @@
{% extends "base.html" %}
{% load i18n staticfiles wger_extras django_bootstrap_breadcrumbs %}
{# #}
{# Opengraph #}
{# #}
{% block opengraph %}
{{ block.super }}
<meta property="og:title" content="{% trans 'Workout log' %}">
{% with username=owner_user.username %}
<meta property="og:description" content="{% blocktrans %}Workout log for {{username}} for {{date}}{% endblocktrans %}">
{% endwith %}
{% endblock %}
{# #}
{# Breadcrumbs #}
{# #}
{% block breadcrumbs %}
{{ block.super }}
{% breadcrumb_raw date|date:'F' "manager:workout:calendar" owner_user date.year date.month %}
{% breadcrumb_raw date.day "manager:workout:calendar-day" owner_user date.year date.month date.day %}
{% endblock %}
{# #}
{# Title #}
{# #}
{% block title %}{% trans "Workout log" %} {{date}}{% endblock %}
{# #}
{# Content #}
{# #}
{% block content %}
{% if logs %}
{% for date, value in logs.items %}
{% include 'calendar/partial_overview_table.html' %}
{% for exercise, logs in value.logs.items %}
<table class="table table-hover table-bordered" style="margin-top: 2em;">
<tr class="active">
<th>{{ exercise }}</th>
</tr>
{% for log in logs %}
<tr>
<td>
{{log.reps}} × {{log.weight}} {% trans_weight_unit 'kg' owner_user %}
{% if is_owner %}
<span class="editoptions">
<a href="{% url 'manager:log:delete' log.pk %}"
title="{% trans 'Delete' %}"
class="wger-modal-dialog">
<img src="{% static 'images/icons/trash.svg' %}"
width="16"
height="16"
alt="{% trans 'Delete' %}"></a>
<a href="{% url 'manager:log:edit' log.pk %}"
title="{% trans 'Edit' %}"
class="wger-modal-dialog">
<img src="{% static 'images/icons/edit.svg' %}"
width="16"
height="16"
alt="{% trans 'Edit' %}"></a>
</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endfor %}
{% endfor %}
{% else %}
{% trans "Nothing found" %}
{% endif %}
{% endblock %}
{# #}
{# Sidebar #}
{# #}
{% block sidebar %}
{% endblock %}

View File

@@ -1,8 +1,5 @@
{% extends "base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load wger_extras %}
{% load cache %}
{% load i18n staticfiles wger_extras %}
{# #}
@@ -45,73 +42,26 @@ $(document).ready(function() {
{# Content #}
{# #}
{% block content %}
{% cache cache_timeout workout-log-full is_owner owner_user.id current_year current_month %}
{{calendar|safe}}
{% for date in logs %}
{% for date, value in logs.items %}
<div id="log-{{date|date:'Y-m-d'}}" class="calendar-log-data">
<h3>{{date}}</h3>
{% regroup logs|get_item:date by exercise as grouped_logs %}
{# TODO: what if some of the entries don't belong to a workout? #}
{% with tmp=grouped_logs|first %}
{% with first_log=tmp.list|first %}
{% with session=first_log.get_workout_session %}
<div class="row">
<div class="col-sm-4">
<a href="{{first_log.workout.get_absolute_url}}" class="btn btn-default btn-block">{{ first_log.workout }}</a>
</div>
<div class="col-sm-4">
<a href="{% url 'manager:log:log' first_log.workout.id %}" class="btn btn-default btn-block">{% trans "Log overview" %}</a>
</div>
<div class="col-sm-4">
{% if is_owner %}
{% if session %}
<a href="{% url 'manager:session:edit' session.pk %}" class="btn btn-default btn-block wger-modal-dialog">{% trans "Edit workout session" %}</a>
{% else %}
<a href="{% url 'manager:session:add' first_log.workout_id date|date:'Y' date|date:'m' date|date:'d' %}" class="btn btn-default btn-block wger-modal-dialog">{% trans "Add workout impression" %}</a>
{% endif %}
{% endif %}
</div>
</div>
{% if session %}
<table class="table" style="margin-top: 2em;">
<tr>
<th style="width: 30%">{% trans "Time" %}</th>
<td>
{% if session.time_start %}
{{session.time_start|time:"H:i"}} - {{session.time_end|time:"H:i"}}
{% else %}
-/-
{% endif %}
</td>
</tr>
<tr>
<th>{% trans "General impression" %}</th>
<td>{% trans session.get_impression_display %}</td>
</tr>
<tr>
<th>{% trans "Notes" %}</th>
<td>{{session.notes|default:'-/-'}}</td>
</tr>
</table>
{% endif %}
{% endwith %}
{% endwith %}
{% endwith %}
<h3>
{{date}}
<small><a href="{% url 'manager:workout:calendar-day' owner_user date.year date.month date.day %}">{% trans 'Detail page' %}</a></small>
</h3>
{% include 'calendar/partial_overview_table.html' %}
<div class="row" style="margin-top: 2em;">
{% for i in grouped_logs %}
{% for exercise, logs in value.logs.items %}
<div class="col-sm-4">
<div class="panel panel-default">
<div class="panel-heading">{{ i.grouper }}</div>
<div class="panel-heading">{{ exercise }}</div>
<div class="panel-body" style="min-height: 8em;">
<ul>
{% for log in i.list %}
{% for log in logs %}
<li>
{{log.reps}} × {{log.weight}}
{% if is_owner %}
@@ -142,7 +92,6 @@ $(document).ready(function() {
</div>
</div>
{% endfor %}
{% endcache %}
{% endblock %}
@@ -188,9 +137,9 @@ day in your current workout:{% endblocktrans %} <strong>{{current_workout}}</str
you can click on them to see the details.{% endblocktrans %}</p>
<p>
<div style="background-color: #C9302C; display: inline;">&nbsp;</div> {{ impressions.0.1 }}<br>
<div style="background-color: #EC971F; display: inline;">&nbsp;</div> {{ impressions.1.1 }}<br>
<div style="background-color: #449D44; display: inline;">&nbsp;</div> {{ impressions.2.1 }}
<span style="background-color: #C9302C; display: inline;">&nbsp;</span> {{ impressions.0.1 }}<br>
<span style="background-color: #EC971F; display: inline;">&nbsp;</span> {{ impressions.1.1 }}<br>
<span style="background-color: #449D44; display: inline;">&nbsp;</span> {{ impressions.2.1 }}
</p>
<h3>{% trans 'Log' %}</h3>

View File

@@ -0,0 +1,43 @@
{% load i18n %}
<div class="row">
<div class="col-sm-4">
<a href="{{value.workout.get_absolute_url}}" class="btn btn-default btn-block">{{ value.workout }}</a>
</div>
<div class="col-sm-4">
<a href="{% url 'manager:log:log' value.workout.id %}" class="btn btn-default btn-block">{% trans "Log overview" %}</a>
</div>
{% if is_owner %}
<div class="col-sm-4">
{% if value.session %}
<a href="{% url 'manager:session:edit' value.session.pk %}" class="btn btn-default btn-block wger-modal-dialog">{% trans "Edit workout session" %}</a>
{% else %}
<a href="{% url 'manager:session:add' value.workout.id date|date:'Y' date|date:'m' date|date:'d' %}" class="btn btn-default btn-block wger-modal-dialog">{% trans "Add workout impression" %}</a>
{% endif %}
</div>
{% endif %}
</div>
{% if value.session %}
<table class="table" style="margin-top: 2em;">
<tr>
<th style="width: 30%">{% trans "Time" %}</th>
<td>
{% if value.session.time_start %}
{{value.session.time_start|time:"H:i"}} - {{value.session.time_end|time:"H:i"}}
{% else %}
-/-
{% endif %}
</td>
</tr>
<tr>
<th>{% trans "General impression" %}</th>
<td>{% trans value.session.get_impression_display %}</td>
</tr>
<tr>
<th>{% trans "Notes" %}</th>
<td>{{value.session.notes|default:'-/-'}}</td>
</tr>
</table>
{% endif %}

View File

@@ -1,8 +1,5 @@
{% extends "base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load wger_extras %}
{% load cache %}
{% load i18n staticfiles wger_extras %}
{# #}
@@ -48,18 +45,15 @@ $(document).ready(function() {
{# Content #}
{# #}
{% block content %}
{% cache cache_timeout workout-log-mobile is_owner owner_user.id current_year current_month %}
{{calendar|safe}}
{% for date in logs %}
{% regroup logs|get_item:date by exercise as grouped_logs %}
{# #}
{# Popups to edit or delete the logs #}
{# #}
{% if is_owner %}
{% for i in grouped_logs %}
{% for log in i.list %}
{% for date, value in logs.items %}
{% for exercise, logs in value.logs.items %}
{% for log in logs %}
<div class="modal fade" id="log-popup-{{log.pk}}">
<div class="modal-dialog">
<div class="modal-content">
@@ -92,71 +86,34 @@ $(document).ready(function() {
</div>
{% endfor %}
{% endfor %}
{% endfor %}
{% endif %}
<div id="log-{{date|date:'Y-m-d'}}" class="calendar-log-data">
<h3>{{date}}</h3>
{% for date, value in logs.items %}
<div id="log-{{date|date:'Y-m-d'}}" class="calendar-log-data">
<h3>
{{date}}
<small><a href="{% url 'manager:workout:calendar-day' owner_user date.year date.month date.day %}">{% trans 'Detail page' %}</a></small>
</h3>
{% include 'calendar/partial_overview_table.html' %}
{# TODO: what if some of the entries don't belong to a workout? #}
{% with tmp=grouped_logs|first %}
{% with first_log=tmp.list|first %}
{% with session=first_log.get_workout_session %}
<a href="{{first_log.workout.get_absolute_url}}"
class="btn btn-default btn-block">{{ first_log.workout }}</a>
<a href="{% url 'manager:log:log' first_log.workout.id %}"
class="btn btn-default btn-block">{% trans "Log overview" %}</a>
{% if is_owner %}
{% if session %}
<a href="{% url 'manager:session:edit' session.pk %}"
class="btn btn-default btn-block wger-modal-dialog">{% trans "Edit workout session" %}</a>
{% else %}
<a href="{% url 'manager:session:add' first_log.workout_id date|date:'Y' date|date:'m' date|date:'d' %}"
class="btn btn-default btn-block wger-modal-dialog">{% trans "Add workout impression" %}</a>
{% endif %}
{% endif %}
{% if session %}
<table class="table" style="margin-top: 2em;">
<tr>
<th style="width: 30%">{% trans "Time" %}</th>
<td>
{% if session.time_start %}
{{session.time_start|time:"H:i"}} - {{session.time_end|time:"H:i"}}
{% else %}
-/-
{% endif %}
</td>
</tr>
<tr>
<th>{% trans "General impression" %}</th>
<td>{% trans session.get_impression_display %}</td>
</tr>
<tr>
<th>{% trans "Notes" %}</th>
<td>{{session.notes|default:'-/-'}}</td>
</tr>
</table>
{% endif %}
{% endwith %}
{% endwith %}
{% endwith %}
{# #}
{# Logs #}
{# #}
<div class="row" style="margin-top: 2em;">
{% for i in grouped_logs %}
{% for exercise, logs in value.logs.items %}
<div class="col-sm-4">
<div class="panel panel-default">
<div class="panel-heading">{{ i.grouper }}</div>
<div class="panel-heading">{{ exercise }}</div>
<div class="panel-body" style="min-height: 8em;">
<ul>
{% for log in i.list %}
{% for log in logs %}
<li>
{% if is_owner %}
<a href="#log-popup-{{log.pk}}" data-toggle="modal">
@@ -176,7 +133,6 @@ $(document).ready(function() {
</div>
{% endfor %}
{% endcache %}
{% endblock %}
@@ -225,9 +181,9 @@ day in your current workout:{% endblocktrans %} {{current_workout}}</p>
you can click on them to see the details.{% endblocktrans %}</p>
<p>
<div style="background-color: #C9302C; display: inline;">&nbsp;</div> {{ impressions.0.1 }}<br>
<div style="background-color: #EC971F; display: inline;">&nbsp;</div> {{ impressions.1.1 }}<br>
<div style="background-color: #449D44; display: inline;">&nbsp;</div> {{ impressions.2.1 }}
<span style="background-color: #C9302C; display: inline;">&nbsp;</span> {{ impressions.0.1 }}<br>
<span style="background-color: #EC971F; display: inline;">&nbsp;</span> {{ impressions.1.1 }}<br>
<span style="background-color: #449D44; display: inline;">&nbsp;</span> {{ impressions.2.1 }}
</p>
<h3>{% trans 'Log' %}</h3>

View File

@@ -239,6 +239,9 @@ $(document).ready(function() {
// After clicking on the save button once, disable it
function( data ) {
var today = new Date();
var date = '/' + today.getFullYear() + '/' + today.getMonth() + '/' + today.getDay();
window.location = '/' + get_current_language() + '/workout/calendar/' + get_username() + date;
$('#save_button_form').unbind("click");
$('#save_button_form').addClass('disabled');
$('#save_button_form').bind("click", function(e) {

View File

@@ -375,94 +375,133 @@ class WorkoutLogCacheTestCase(WorkoutManagerTestCase):
def test_calendar(self):
'''
Test the exercise overview cache is correctly generated on visit
Test the log cache is correctly generated on visit
'''
log_hash = hash((1, 2012, 10))
self.user_login('admin')
self.assertFalse(cache.get(cache_mapper.get_workout_log(1, 2012, 10)))
for cache_key in ('workout-log-full', 'workout-log-mobile'):
self.assertFalse(cache.get(get_template_cache_name(cache_key, True, 1, 2012, 10)))
self.client.get(reverse('manager:workout:calendar', kwargs={'year': 2012, 'month': 10}))
self.assertFalse(cache.get(cache_mapper.get_workout_log_list(log_hash)))
cache_key = 'workout-log-mobile' if self.is_mobile else 'workout-log-full'
self.assertTrue(cache.get(get_template_cache_name(cache_key, True, 1, 2012, 10)))
self.assertTrue(cache.get(cache_mapper.get_workout_log(1, 2012, 10)))
self.client.get(reverse('manager:workout:calendar', kwargs={'year': 2012, 'month': 10}))
self.assertTrue(cache.get(cache_mapper.get_workout_log_list(log_hash)))
def test_calendar_day(self):
'''
Test the log cache on the calendar day view is correctly generated on visit
'''
log_hash = hash((1, 2012, 10, 1))
self.user_login('admin')
self.assertFalse(cache.get(cache_mapper.get_workout_log_list(log_hash)))
self.client.get(reverse('manager:workout:calendar-day', kwargs={'username': 'admin',
'year': 2012,
'month': 10,
'day': 1}))
self.assertTrue(cache.get(cache_mapper.get_workout_log_list(log_hash)))
def test_calendar_anonymous(self):
'''
Test the exercise overview cache is correctly generated on visit
by anonymous users
Test the log cache is correctly generated on visit by anonymous users
'''
log_hash = hash((1, 2012, 10))
self.user_logout()
self.assertFalse(cache.get(cache_mapper.get_workout_log(1, 2012, 10)))
for cache_key in ('workout-log-full', 'workout-log-mobile'):
self.assertFalse(cache.get(get_template_cache_name(cache_key, True, 1, 2012, 10)))
self.assertFalse(cache.get(get_template_cache_name(cache_key, False, 1, 2012, 10)))
self.assertFalse(cache.get(cache_mapper.get_workout_log_list(log_hash)))
self.client.get(reverse('manager:workout:calendar', kwargs={'username': 'admin',
'year': 2012,
'month': 10}))
self.assertTrue(cache.get(cache_mapper.get_workout_log_list(log_hash)))
cache_key = 'workout-log-mobile' if self.is_mobile else 'workout-log-full'
self.assertTrue(cache.get(cache_mapper.get_workout_log(1, 2012, 10)))
self.assertTrue(cache.get(get_template_cache_name(cache_key, False, 1, 2012, 10)))
self.assertFalse(cache.get(get_template_cache_name(cache_key, True, 1, 2012, 10)))
def test_calendar_day_anonymous(self):
'''
Test the log cache is correctly generated on visit by anonymous users
'''
log_hash = hash((1, 2012, 10, 1))
self.user_logout()
self.assertFalse(cache.get(cache_mapper.get_workout_log_list(log_hash)))
self.client.get(reverse('manager:workout:calendar-day', kwargs={'username': 'admin',
'year': 2012,
'month': 10,
'day': 1}))
self.assertTrue(cache.get(cache_mapper.get_workout_log_list(log_hash)))
def test_cache_update_log(self):
'''
Test that the caches are cleared when saving a log
'''
log_hash = hash((1, 2012, 10))
log_hash_day = hash((1, 2012, 10, 1))
self.user_login('admin')
self.client.get(reverse('manager:workout:calendar', kwargs={'year': 2012, 'month': 10}))
self.client.get(reverse('manager:workout:calendar-day', kwargs={'username': 'admin',
'year': 2012,
'month': 10,
'day': 1}))
log = WorkoutLog.objects.get(pk=1)
log.weight = 35
log.save()
cache_key = 'workout-log-mobile' if self.is_mobile else 'workout-log-full'
self.assertFalse(cache.get(cache_mapper.get_workout_log(1, 2012, 10)))
self.assertFalse(cache.get(get_template_cache_name(cache_key, True, 1, 2012, 10)))
self.assertFalse(cache.get(cache_mapper.get_workout_log_list(log_hash)))
self.assertFalse(cache.get(cache_mapper.get_workout_log_list(log_hash_day)))
def test_cache_update_log_2(self):
'''
Test that the caches are only cleared for a the log's month
'''
log_hash = hash((1, 2012, 10))
log_hash_day = hash((1, 2012, 10, 1))
self.user_login('admin')
self.client.get(reverse('manager:workout:calendar', kwargs={'year': 2012, 'month': 10}))
self.client.get(reverse('manager:workout:calendar-day', kwargs={'username': 'admin',
'year': 2012,
'month': 10,
'day': 1}))
log = WorkoutLog.objects.get(pk=3)
log.weight = 35
log.save()
cache_key = 'workout-log-mobile' if self.is_mobile else 'workout-log-full'
self.assertTrue(cache.get(cache_mapper.get_workout_log(1, 2012, 10)))
self.assertTrue(cache.get(get_template_cache_name(cache_key, True, 1, 2012, 10)))
self.assertTrue(cache.get(cache_mapper.get_workout_log_list(log_hash)))
self.assertTrue(cache.get(cache_mapper.get_workout_log_list(log_hash_day)))
def test_cache_delete_log(self):
'''
Test that the caches are cleared when deleting a log
'''
log_hash = hash((1, 2012, 10))
log_hash_day = hash((1, 2012, 10, 1))
self.user_login('admin')
self.client.get(reverse('manager:workout:calendar', kwargs={'year': 2012, 'month': 10}))
self.client.get(reverse('manager:workout:calendar-day', kwargs={'username': 'admin',
'year': 2012,
'month': 10,
'day': 1}))
log = WorkoutLog.objects.get(pk=1)
log.delete()
cache_key = 'workout-log-mobile' if self.is_mobile else 'workout-log-full'
self.assertFalse(cache.get(cache_mapper.get_workout_log(1, 2012, 10)))
self.assertFalse(cache.get(get_template_cache_name(cache_key, True, 1, 2012, 10)))
self.assertFalse(cache.get(cache_mapper.get_workout_log_list(log_hash)))
self.assertFalse(cache.get(cache_mapper.get_workout_log_list(log_hash_day)))
def test_cache_delete_log_2(self):
'''
Test that the caches are only cleared for a the log's month
'''
log_hash = hash((1, 2012, 10))
log_hash_day = hash((1, 2012, 10, 1))
self.user_login('admin')
self.client.get(reverse('manager:workout:calendar', kwargs={'year': 2012, 'month': 10}))
self.client.get(reverse('manager:workout:calendar-day', kwargs={'username': 'admin',
'year': 2012,
'month': 10,
'day': 1}))
log = WorkoutLog.objects.get(pk=3)
log.delete()
cache_key = 'workout-log-mobile' if self.is_mobile else 'workout-log-full'
self.assertTrue(cache.get(cache_mapper.get_workout_log(1, 2012, 10)))
self.assertTrue(cache.get(get_template_cache_name(cache_key, True, 1, 2012, 10)))
self.assertTrue(cache.get(cache_mapper.get_workout_log_list(log_hash)))
self.assertTrue(cache.get(cache_mapper.get_workout_log_list(log_hash_day)))
class WorkoutLogApiTestCase(api_base_test.ApiBaseResourceTestCase):

View File

@@ -141,6 +141,7 @@ class WorkoutLogCacheTestCase(WorkoutManagerTestCase):
'''
Test that the caches are cleared when updating a workout session
'''
log_hash = hash((1, 2012, 10))
self.user_login('admin')
self.client.get(reverse('manager:workout:calendar', kwargs={'year': 2012, 'month': 10}))
@@ -148,14 +149,13 @@ class WorkoutLogCacheTestCase(WorkoutManagerTestCase):
session.notes = 'Lorem ipsum'
session.save()
cache_key = 'workout-log-mobile' if self.is_mobile else 'workout-log-full'
self.assertFalse(cache.get(cache_mapper.get_workout_log(1, 2012, 10)))
self.assertFalse(cache.get(get_template_cache_name(cache_key, True, 1, 2012, 10)))
self.assertFalse(cache.get(cache_mapper.get_workout_log_list(log_hash)))
def test_cache_update_session_2(self):
'''
Test that the caches are only cleared for a the session's month
'''
log_hash = hash((1, 2012, 10))
self.user_login('admin')
self.client.get(reverse('manager:workout:calendar', kwargs={'year': 2012, 'month': 10}))
@@ -164,37 +164,33 @@ class WorkoutLogCacheTestCase(WorkoutManagerTestCase):
session.notes = 'Lorem ipsum'
session.save()
cache_key = 'workout-log-mobile' if self.is_mobile else 'workout-log-full'
self.assertTrue(cache.get(cache_mapper.get_workout_log(1, 2012, 10)))
self.assertTrue(cache.get(get_template_cache_name(cache_key, True, 1, 2012, 10)))
self.assertTrue(cache.get(cache_mapper.get_workout_log_list(log_hash)))
def test_cache_delete_session(self):
'''
Test that the caches are cleared when deleting a workout session
'''
log_hash = hash((1, 2012, 10))
self.user_login('admin')
self.client.get(reverse('manager:workout:calendar', kwargs={'year': 2012, 'month': 10}))
session = WorkoutSession.objects.get(pk=1)
session.delete()
cache_key = 'workout-log-mobile' if self.is_mobile else 'workout-log-full'
self.assertFalse(cache.get(cache_mapper.get_workout_log(1, 2012, 10)))
self.assertFalse(cache.get(get_template_cache_name(cache_key, 'True', 1, 2012, 10)))
self.assertFalse(cache.get(cache_mapper.get_workout_log_list(log_hash)))
def test_cache_delete_session_2(self):
'''
Test that the caches are only cleared for a the session's month
'''
log_hash = hash((1, 2012, 10))
self.user_login('admin')
self.client.get(reverse('manager:workout:calendar', kwargs={'year': 2012, 'month': 10}))
session = WorkoutSession.objects.get(pk=2)
session.delete()
cache_key = 'workout-log-mobile' if self.is_mobile else 'workout-log-full'
self.assertTrue(cache.get(cache_mapper.get_workout_log(1, 2012, 10)))
self.assertTrue(cache.get(get_template_cache_name(cache_key, True, 1, 2012, 10)))
self.assertTrue(cache.get(cache_mapper.get_workout_log_list(log_hash)))
class WorkoutSessionApiTestCase(api_base_test.ApiBaseResourceTestCase):

View File

@@ -77,6 +77,9 @@ patterns_workout = patterns('',
url(r'^calendar/(?P<year>\d{4})/(?P<month>\d{1,2})$',
log.calendar,
name='calendar'),
url(r'^calendar/(?P<username>[\w.@+-]+)/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})$',
log.day,
name='calendar-day'),
url(r'^(?P<pk>\d+)/ical/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})$',
ical.export,
name='ical'),
@@ -208,4 +211,4 @@ urlpatterns = patterns('',
url(r'^session/', include(patterns_session, namespace="session")),
url(r'^schedule/', include(patterns_schedule, namespace="schedule")),
url(r'^schedule/step/', include(patterns_step, namespace="step")),
)
)

View File

@@ -17,13 +17,11 @@
import logging
import uuid
import datetime
from calendar import HTMLCalendar
from django.shortcuts import render
from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect
from django.http import HttpResponseForbidden
from django.core.cache import cache
from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
@@ -36,6 +34,7 @@ from django.views.generic import CreateView
from django.views.generic import DetailView
from django.views.generic import DeleteView
from wger.manager.helpers import WorkoutCalendar
from wger.manager.models import Workout
from wger.manager.models import WorkoutSession
from wger.manager.models import Day
@@ -44,12 +43,12 @@ from wger.manager.models import Schedule
from wger.manager.forms import HelperDateForm
from wger.manager.forms import HelperWorkoutSessionForm
from wger.manager.forms import WorkoutLogForm
from wger.utils.cache import cache_mapper
from wger.utils.generic_views import WgerFormMixin
from wger.utils.generic_views import WgerDeleteMixin
from wger.utils.generic_views import WgerPermissionMixin
from wger.utils.helpers import check_access, make_token
from wger.weight.helpers import process_log_entries
from wger.weight.helpers import process_log_entries, group_log_entries
logger = logging.getLogger(__name__)
@@ -316,106 +315,20 @@ class WorkoutLogDetailView(DetailView, WgerPermissionMixin):
return super(WorkoutLogDetailView, self).dispatch(request, *args, **kwargs)
class WorkoutCalendar(HTMLCalendar):
'''
A calendar renderer, see this blog entry for details:
* http://uggedal.com/journal/creating-a-flexible-monthly-calendar-in-django/
'''
def __init__(self, workout_logs, *args, **kwargs):
super(WorkoutCalendar, self).__init__(*args, **kwargs)
self.workout_logs = workout_logs
def formatday(self, day, weekday):
if day != 0:
cssclass = self.cssclasses[weekday]
date_obj = datetime.date(self.year, self.month, day)
if datetime.date.today() == date_obj:
cssclass += ' today'
if day in self.workout_logs:
current_log = self.workout_logs.get(day)
if current_log['impression'] == WorkoutSession.IMPRESSION_BAD:
background_css = 'btn-danger'
elif current_log['impression'] == WorkoutSession.IMPRESSION_GOOD:
background_css = 'btn-success'
else:
background_css = 'btn-warning'
url = reverse('manager:log:log', kwargs={'pk': current_log['log'].workout_id})
formatted_date = date_obj.strftime('%Y-%m-%d')
body = []
body.append('<a href="{0}"'
'data-log="log-{1}"'
'class="btn btn-block {2} calendar-link">'.format(url,
formatted_date,
background_css))
body.append(repr(day))
body.append('</a>')
return self.day_cell(cssclass, '{0}'.format(''.join(body)))
return self.day_cell(cssclass, day)
return self.day_cell('noday', '&nbsp;')
def formatmonth(self, year, month):
'''
Format the table header. This is basically the same code from python's
calendar module but with additional bootstrap classes
'''
self.year, self.month = year, month
out = []
out.append('<table class="month table table-bordered">\n')
out.append(self.formatmonthname(year, month))
out.append('\n')
out.append(self.formatweekheader())
out.append('\n')
for week in self.monthdays2calendar(year, month):
out.append(self.formatweek(week))
out.append('\n')
out.append('</table>\n')
return ''.join(out)
def day_cell(self, cssclass, body):
'''
Renders a day cell
'''
return '<td class="{0}" style="vertical-align: middle;">{1}</td>'.format(cssclass, body)
def calendar(request, username=None, year=None, month=None):
'''
Show a calendar with all the workout logs
'''
context = {}
is_owner, user = check_access(request.user, username)
logger.info('aa bb cc')
uid, token = make_token(user)
year = int(year) if year else datetime.date.today().year
month = int(month) if month else datetime.date.today().month
context = {}
logs = WorkoutLog.objects.filter(user=user,
date__year=year,
date__month=month).order_by('exercise')
logs_filtered = cache.get(cache_mapper.get_workout_log(user.pk, year, month))
if not logs_filtered:
logs_filtered = {}
# Process the logs. Group by date and check for impressions
for log in logs:
if log.date not in logs_filtered:
session = log.get_workout_session()
if session:
impression = session.impression
else:
# Default is 'neutral'
impression = WorkoutSession.IMPRESSION_NEUTRAL
logs_filtered[log.date.day] = {'impression': impression,
'log': log}
cache.set(cache_mapper.get_workout_log(user.pk, year, month), logs_filtered)
(current_workout, schedule) = Schedule.objects.get_current_workout(user)
context['calendar'] = WorkoutCalendar(logs_filtered).formatmonth(year, month)
context['logs'] = process_log_entries(logs)[0]
grouped_log_entries = group_log_entries(user, year, month)
context['calendar'] = WorkoutCalendar(grouped_log_entries).formatmonth(year, month)
context['logs'] = grouped_log_entries
context['current_year'] = year
context['current_month'] = month
context['current_workout'] = current_workout
@@ -424,4 +337,25 @@ def calendar(request, username=None, year=None, month=None):
context['impressions'] = WorkoutSession.IMPRESSION
context['month_list'] = WorkoutLog.objects.filter(user=user).dates('date', 'month')
context['show_shariff'] = is_owner and user.userprofile.ro_access
return render(request, 'workout/calendar.html', context)
return render(request, 'calendar/month.html', context)
def day(request, username, year, month, day):
'''
Show the logs for a single day
'''
context = {}
is_owner, user = check_access(request.user, username)
try:
date = datetime.date(int(year), int(month), int(day))
except ValueError as e:
logger.error("Error on date: {0}".format(e))
return HttpResponseForbidden()
context['logs'] = group_log_entries(user, date.year, date.month, date.day)
context['date'] = date
context['owner_user'] = user
context['is_owner'] = is_owner
context['show_shariff'] = is_owner and user.userprofile.ro_access
return render(request, 'calendar/day.html', context)

View File

@@ -124,21 +124,21 @@ def export_pdf(request, pk, uidb64=None, token=None):
# Set the title
p = Paragraph(u'<para align="center">{0}</para>'.format(schedule), styleSheet["HeaderBold"])
elements.append(p)
elements.append(Spacer(10*cm, 0.5*cm))
elements.append(Spacer(10 * cm, 0.5 * cm))
# Iterate through the Workout and render the training days
for step in schedule.schedulestep_set.all():
p = Paragraph(u'<para>{0} {1}</para>'.format(step.duration, _('Weeks')),
styleSheet["HeaderBold"])
elements.append(p)
elements.append(Spacer(10*cm, 0.5*cm))
elements.append(Spacer(10 * cm, 0.5 * cm))
for day in step.workout.canonical_representation['day_list']:
elements.append(render_workout_day(day, nr_of_weeks=7))
elements.append(Spacer(10*cm, 0.5*cm))
elements.append(Spacer(10 * cm, 0.5 * cm))
# Footer, date and info
elements.append(Spacer(10*cm, 0.5*cm))
elements.append(Spacer(10 * cm, 0.5 * cm))
url = reverse('manager:schedule:view', kwargs={'pk': schedule.id})
elements.append(render_footer(request.build_absolute_uri(url)))

View File

@@ -107,7 +107,7 @@ def chart_data(request):
{'key': 'obese_class_3', 'height': 150, 'weight': 190},
{'key': 'obese_class_3', 'height': 200, 'weight': 190}
])
])
# Return the results to the client
return HttpResponse(data, 'application/json')

View File

@@ -300,7 +300,7 @@ def export_pdf(request, id, uidb64=None, token=None):
elements.append(p)
# Filler
elements.append(Spacer(10*cm, 0.5*cm))
elements.append(Spacer(10 * cm, 0.5 * cm))
# append the table to the document
elements.append(t)
@@ -356,7 +356,7 @@ def export_pdf(request, id, uidb64=None, token=None):
elements.append(t)
# Footer, date and info
elements.append(Spacer(10*cm, 0.5*cm))
elements.append(Spacer(10 * cm, 0.5 * cm))
created = datetime.date.today().strftime("%d.%m.%Y")
url = reverse('nutrition:plan:view', kwargs={'id': plan.id})
p = Paragraph('''<para align="left">

View File

@@ -267,7 +267,7 @@ THUMBNAIL_ALIASES = {
'large': {'size': (800, 800), 'quality': 90},
'large_cropped': {'size': (800, 800), 'crop': 'smart', 'quality': 90},
},
},
}

View File

@@ -44,6 +44,7 @@
<a href="https://github.com/rolandgeider/wger/issues/108">#100</a>
<a href="https://github.com/rolandgeider/wger/issues/108">#108</a>
<a href="https://github.com/rolandgeider/wger/issues/117">#117</a>
<a href="https://github.com/rolandgeider/wger/issues/131">#131</a>
</li>
</ul>

View File

@@ -43,12 +43,16 @@ def reset_workout_canonical_form(workout_id):
cache.delete(cache_mapper.get_workout_canonical(workout_id))
def reset_workout_log(user_pk, year, month):
delete_template_fragment_cache('workout-log-full', True, user_pk, year, month)
delete_template_fragment_cache('workout-log-full', False, user_pk, year, month)
delete_template_fragment_cache('workout-log-mobile', True, user_pk, year, month)
delete_template_fragment_cache('workout-log-mobile', False, user_pk, year, month)
cache.delete(cache_mapper.get_workout_log(user_pk, year, month))
def reset_workout_log(user_pk, year, month, day=None):
'''
Resets the cached workout logs
'''
log_hash = hash((user_pk, year, month))
cache.delete(cache_mapper.get_workout_log_list(log_hash))
log_hash = hash((user_pk, year, month, day))
cache.delete(cache_mapper.get_workout_log_list(log_hash))
class CacheKeyMapper(object):
@@ -63,7 +67,7 @@ class CacheKeyMapper(object):
EXERCISE_CACHE_KEY_MUSCLE_BG = 'exercise-muscle-bg-{0}'
INGREDIENT_CACHE_KEY = 'ingredient-{0}'
WORKOUT_CANONICAL_REPRESENTATION = 'workout-canonical-representation-{0}'
WORKOUT_LOG = 'workout-log-user{0}-year{1}-month{2}'
WORKOUT_LOG_LIST = 'workout-log-hash-{0}'
def get_exercise_key(self, param):
'''
@@ -131,10 +135,10 @@ class CacheKeyMapper(object):
return self.WORKOUT_CANONICAL_REPRESENTATION.format(pk)
def get_workout_log(self, user_pk, year, month):
def get_workout_log_list(self, hash_value):
'''
Return the workout canonical representation
'''
return self.WORKOUT_LOG.format(user_pk, year, month)
return self.WORKOUT_LOG_LIST.format(hash_value)
cache_mapper = CacheKeyMapper()

View File

@@ -45,8 +45,8 @@ def processor(request):
'request_full_path': full_path,
# The current full path with host
'request_absolute_path': request.build_absolute_uri(),
'image_absolute_path': request.build_absolute_uri(static_path),
'request_absolute_path': request.build_absolute_uri(),
'image_absolute_path': request.build_absolute_uri(static_path),
# Translation links

View File

@@ -47,4 +47,4 @@ class WeightForm(ModelForm):
exclude = []
widgets = {
'user': widgets.HiddenInput(),
}
}

View File

@@ -22,8 +22,13 @@ import csv
import json
from collections import OrderedDict
from django.core.cache import cache
from wger.utils.helpers import DecimalJsonEncoder
from wger.utils.cache import cache_mapper
from wger.weight.models import WeightEntry
from wger.manager.models import WorkoutSession
from wger.manager.models import WorkoutLog
logger = logging.getLogger(__name__)
@@ -56,6 +61,69 @@ def parse_weight_csv(request, cleaned_data):
return (weight_list, error_list)
def group_log_entries(user, year, month, day=None):
'''
Processes and regroups a list of log entries so they can be more easily
used in the different calendar pages
:param user: the user to filter the logs for
:param year: year
:param month: month
:param day: optional, day
:return: a dictionary with grouped logs by date and exercise
'''
if day:
log_hash = hash((user.pk, year, month, day))
else:
log_hash = hash((user.pk, year, month))
# There can be workout sessions without any associated log entries, so it is
# not enough so simply iterate through the logs
if day:
filter_date = datetime.date(year, month, day)
logs = WorkoutLog.objects.filter(user=user, date=filter_date)
sessions = WorkoutSession.objects.filter(user=user, date=filter_date)
else:
logs = WorkoutLog.objects.filter(user=user,
date__year=year,
date__month=month)
sessions = WorkoutSession.objects.filter(user=user,
date__year=year,
date__month=month)
out = cache.get(cache_mapper.get_workout_log_list(log_hash))
if not out:
out = {}
# Logs
for entry in logs:
if not out.get(entry.date):
out[entry.date] = {'date': entry.date,
'workout': entry.workout,
'session': entry.get_workout_session(),
'logs': {}}
if not out[entry.date]['logs'].get(entry.exercise):
out[entry.date]['logs'][entry.exercise] = []
out[entry.date]['logs'][entry.exercise].append(entry)
# Sessions
for entry in sessions:
if not out.get(entry.date):
out[entry.date] = {'date': entry.date,
'workout': entry.workout,
'session': entry,
'logs': {}}
cache.set(cache_mapper.get_workout_log_list(log_hash), out)
return out
def process_log_entries(logs):
'''
Processes and regroups a list of log entries so they can be rendered
@@ -96,10 +164,10 @@ def process_log_entries(logs):
'id': 'manager:workout:log-%s' % entry.id}
# Only unique date, rep and weight combinations
if reps_list.get('{0}-{1}-{2}'.format(entry.date, entry.reps, entry.weight)):
if reps_list.get((entry.date, entry.reps, entry.weight)):
continue
else:
reps_list['{0}-{1}-{2}'.format(entry.date, entry.reps, entry.weight)] = True
reps_list[(entry.date, entry.reps, entry.weight)] = True
# Only add if weight is the maximum for the day
if entry.weight != max_weight[entry.date][entry.reps]: