diff --git a/.travis.yml b/.travis.yml index ae2c27a17..a273b2dd0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/docs/development.rst b/docs/development.rst index e2cda51d4..6c53aa5e1 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -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 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..719cf062f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[pep8] +exclude = urls.py,*migrations* +max-line-length = 100 +ignore = W503 diff --git a/wger/core/api/serializers.py b/wger/core/api/serializers.py index 0d679b73e..b2a330ac2 100644 --- a/wger/core/api/serializers.py +++ b/wger/core/api/serializers.py @@ -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() diff --git a/wger/core/api/views.py b/wger/core/api/views.py index 85af2bcf0..0c7bac669 100644 --- a/wger/core/api/views.py +++ b/wger/core/api/views.py @@ -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): ''' diff --git a/wger/core/demo.py b/wger/core/demo.py index 1dd0786ef..688baa429 100644 --- a/wger/core/demo.py +++ b/wger/core/demo.py @@ -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) diff --git a/wger/core/management/commands/clear-cache.py b/wger/core/management/commands/clear-cache.py index 4a2a32185..9720ac8f4 100644 --- a/wger/core/management/commands/clear-cache.py +++ b/wger/core/management/commands/clear-cache.py @@ -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) diff --git a/wger/core/static/js/workout-manager.js b/wger/core/static/js/workout-manager.js index 1d0e3c728..a105bff85 100644 --- a/wger/core/static/js/workout-manager.js +++ b/wger/core/static/js/workout-manager.js @@ -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; } diff --git a/wger/core/templatetags/wger_extras.py b/wger/core/templatetags/wger_extras.py index 898cab06b..0fa3b1c2c 100644 --- a/wger/core/templatetags/wger_extras.py +++ b/wger/core/templatetags/wger_extras.py @@ -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} diff --git a/wger/core/tests/test_preferences.py b/wger/core/tests/test_preferences.py index 834a73fa3..59cce80dd 100644 --- a/wger/core/tests/test_preferences.py +++ b/wger/core/tests/test_preferences.py @@ -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): ''' diff --git a/wger/exercises/management/commands/download-exercise-images.py b/wger/exercises/management/commands/download-exercise-images.py index bf5b6836e..1af1fb143 100644 --- a/wger/exercises/management/commands/download-exercise-images.py +++ b/wger/exercises/management/commands/download-exercise-images.py @@ -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' diff --git a/wger/manager/helpers.py b/wger/manager/helpers.py index a8836c8b8..cd11c0d15 100644 --- a/wger/manager/helpers.py +++ b/wger/manager/helpers.py @@ -15,12 +15,16 @@ # You should have received a copy of the GNU Affero General Public License # along with Workout Manager. If not, see . +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', ' ') + + 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(''.format(url, + formatted_date, + background_css)) + body.append(repr(day)) + body.append('') + 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('\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('
\n') + return ''.join(out) + + def day_cell(self, cssclass, body): + ''' + Renders a day cell + ''' + return '{1}'.format(cssclass, body) diff --git a/wger/manager/models.py b/wger/manager/models.py index fdb29df5b..3b3e1be84 100644 --- a/wger/manager/models.py +++ b/wger/manager/models.py @@ -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' diff --git a/wger/manager/pdf.py b/wger/manager/pdf.py index d2fc2a63e..256fde8bc 100644 --- a/wger/manager/pdf.py +++ b/wger/manager/pdf.py @@ -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(''' %(date)s - diff --git a/wger/manager/templates/calendar/day.html b/wger/manager/templates/calendar/day.html new file mode 100644 index 000000000..436fac99f --- /dev/null +++ b/wger/manager/templates/calendar/day.html @@ -0,0 +1,89 @@ +{% extends "base.html" %} +{% load i18n staticfiles wger_extras django_bootstrap_breadcrumbs %} + +{# #} +{# Opengraph #} +{# #} +{% block opengraph %} + {{ block.super }} + + + {% with username=owner_user.username %} + + {% 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 %} + + + + + {% for log in logs %} + + + + {% endfor %} +
{{ exercise }}
+ {{log.reps}} × {{log.weight}} {% trans_weight_unit 'kg' owner_user %} + + {% if is_owner %} + + + {% trans 'Delete' %} + + {% trans 'Edit' %} + + {% endif %} +
+ + {% endfor %} + {% endfor %} +{% else %} + {% trans "Nothing found" %} +{% endif %} +{% endblock %} + + +{# #} +{# Sidebar #} +{# #} +{% block sidebar %} +{% endblock %} diff --git a/wger/manager/templates/workout/calendar.html b/wger/manager/templates/calendar/month.html similarity index 64% rename from wger/manager/templates/workout/calendar.html rename to wger/manager/templates/calendar/month.html index 619b1e0a6..951d4e9a0 100644 --- a/wger/manager/templates/workout/calendar.html +++ b/wger/manager/templates/calendar/month.html @@ -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 %}
-

{{date}}

- {% 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 %} -
- - - - -
- {% if is_owner %} - {% if session %} - {% trans "Edit workout session" %} - {% else %} - {% trans "Add workout impression" %} - {% endif %} - {% endif %} -
-
- {% if session %} - - - - - - - - - - - - - -
{% trans "Time" %} - {% if session.time_start %} - {{session.time_start|time:"H:i"}} - {{session.time_end|time:"H:i"}} - {% else %} - -/- - {% endif %} -
{% trans "General impression" %}{% trans session.get_impression_display %}
{% trans "Notes" %}{{session.notes|default:'-/-'}}
- {% endif %} - {% endwith %} - {% endwith %} - {% endwith %} +

+ {{date}} + {% trans 'Detail page' %} +

+ {% include 'calendar/partial_overview_table.html' %}
- {% for i in grouped_logs %} + {% for exercise, logs in value.logs.items %}
-
{{ i.grouper }}
+
{{ exercise }}
    - {% for log in i.list %} + {% for log in logs %}
  • {{log.reps}} × {{log.weight}} {% if is_owner %} @@ -142,7 +92,6 @@ $(document).ready(function() {
{% endfor %} -{% endcache %} {% endblock %} @@ -188,9 +137,9 @@ day in your current workout:{% endblocktrans %} {{current_workout}}

-

 
{{ impressions.0.1 }}
-
 
{{ impressions.1.1 }}
-
 
{{ impressions.2.1 }} +   {{ impressions.0.1 }}
+   {{ impressions.1.1 }}
+   {{ impressions.2.1 }}

{% trans 'Log' %}

diff --git a/wger/manager/templates/calendar/partial_overview_table.html b/wger/manager/templates/calendar/partial_overview_table.html new file mode 100644 index 000000000..a67f35e0b --- /dev/null +++ b/wger/manager/templates/calendar/partial_overview_table.html @@ -0,0 +1,43 @@ +{% load i18n %} + +
+ + + + + {% if is_owner %} +
+ {% if value.session %} + {% trans "Edit workout session" %} + {% else %} + {% trans "Add workout impression" %} + {% endif %} +
+ {% endif %} +
+{% if value.session %} + + + + + + + + + + + + + +
{% trans "Time" %} + {% if value.session.time_start %} + {{value.session.time_start|time:"H:i"}} - {{value.session.time_end|time:"H:i"}} + {% else %} + -/- + {% endif %} +
{% trans "General impression" %}{% trans value.session.get_impression_display %}
{% trans "Notes" %}{{value.session.notes|default:'-/-'}}
+{% endif %} \ No newline at end of file diff --git a/wger/manager/templates/mobile/workout/calendar.html b/wger/manager/templates/mobile/calendar/month.html similarity index 70% rename from wger/manager/templates/mobile/workout/calendar.html rename to wger/manager/templates/mobile/calendar/month.html index 9cb8e3d47..8e5dcd6c9 100644 --- a/wger/manager/templates/mobile/workout/calendar.html +++ b/wger/manager/templates/mobile/calendar/month.html @@ -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 %}