From cbd7199b1eba76b78f00a7edf761cf3719345177 Mon Sep 17 00:00:00 2001 From: wasswa-derick Date: Wed, 29 Aug 2018 14:33:21 +0300 Subject: [PATCH] [ft] Add buttons to trigger exportation and importations * Create the export url action handler * Implement export and import workouts functionality * Test export and import workouts functions * Resolve flake8 errors * Remove unused modules [Delivers #159236209] --- wger/manager/templates/workout/overview.html | 45 +++++ wger/manager/tests/test_workout.py | 202 +++++++++++++++++++ wger/manager/urls.py | 3 + wger/manager/views/workout.py | 198 +++++++++++++++++- 4 files changed, 446 insertions(+), 2 deletions(-) diff --git a/wger/manager/templates/workout/overview.html b/wger/manager/templates/workout/overview.html index 28900b42..820270f3 100644 --- a/wger/manager/templates/workout/overview.html +++ b/wger/manager/templates/workout/overview.html @@ -36,16 +36,61 @@

{{ workout }}

{% endblock %} + +{% block sidebar %} +

{% trans "Export Workouts" %}

+
+
+ {% if workouts|length < 1 %} + + {% else %} + + + + {% endif %} +
+
+ +
+

{% trans "Import Workouts" %}

+
+ {% csrf_token %} +
+
+ +
+ +
+ +
+
+
+ +{% if messages %} +
{{ message }}
+{% endif %} + +{% endblock %} + {% block options %} {% endblock %} diff --git a/wger/manager/tests/test_workout.py b/wger/manager/tests/test_workout.py index 3b9eb3e8..029efc5d 100644 --- a/wger/manager/tests/test_workout.py +++ b/wger/manager/tests/test_workout.py @@ -21,6 +21,9 @@ from wger.core.tests.base_testcase import WorkoutManagerEditTestCase from wger.core.tests.base_testcase import WorkoutManagerTestCase from wger.manager.models import Workout +from wger.manager.views.workout import export_user_workouts +import tempfile +from django.contrib.messages import get_messages class WorkoutShareButtonTestCase(WorkoutManagerTestCase): @@ -124,6 +127,205 @@ def test_create_workout_logged_in(self): self.user_logout() +class ExportWorkoutTestCase(WorkoutManagerTestCase): + ''' + Tests adding a Workout + ''' + + def create_workout(self): + ''' + Helper function to test creating workouts + ''' + + # Create a workout + count_before = Workout.objects.count() + response = self.client.get(reverse('manager:workout:add')) + count_after = Workout.objects.count() + + # There is always a redirect + self.assertEqual(response.status_code, 302) + + # Test creating workout + self.assertGreater(count_after, count_before) + + # Test accessing workout + response = self.client.get( + reverse('manager:workout:view', kwargs={'pk': 1})) + + workout = Workout.objects.get(pk=1) + self.assertEqual(response.context['workout'], workout) + self.assertEqual(response.status_code, 200) + + def export_workouts(self): + ''' + Helper function to test exporting workouts + ''' + + # Export workout(s) + response = self.client.get(reverse('manager:workout:export')) + self.assertEqual(response.status_code, 200) + + def test_export_workout_logged_in(self): + ''' + Test creating a workout a logged in user + ''' + + self.user_login() + self.create_workout() + self.export_workouts() + self.user_logout() + + +class ImportWorkoutTestCase(WorkoutManagerTestCase): + ''' + Tests Importing Workout(s) + ''' + + def import_workouts(self): + ''' + Helper function to test importing workouts + ''' + workout = [ + { + "id": 1, + "cycle_kind": "microcycle", + "comment": "", + "creation_date": "2018-08-27", + "day": [ + { + "id": 46, + "description": "Run through the amazon", + "daysofweek": [{"dayofweek": 1}], + "sets": [ + { + "id": 20, + "sets": 4, + "order": 1, + "exercises": [ + { + "id": 269, + "license_author": "foguinho.peruca", + "status": "2", + "description": "

Run on a treadmill

", + "name": "Run - treadmill", + "creation_date": "2014-09-03", + "uuid": "51d56238-31a1-4e5a-b747-da30eece4871", + "category": 9, + "muscles": [], + "muscles_secondary": [], + "equipment": [], + "settings": [ + { + "reps": 5, + "order": 1, + "comment": "", + "weight": "1.00", + "repetition_unit_id": 6, + "weight_unit_id": 6 + }, + { + "reps": 5, + "order": 1, + "comment": "", + "weight": "1.00", + "repetition_unit_id": 6, + "weight_unit_id": 6 + }, + { + "reps": 5, + "order": 1, + "comment": "", + "weight": "1.00", + "repetition_unit_id": 6, + "weight_unit_id": 6 + }, + { + "reps": 5, + "order": 1, + "comment": "", + "weight": "1.00", + "repetition_unit_id": 6, + "weight_unit_id": 6 + } + ] + } + ] + } + ] + } + ] + } + ] + + tmp_file = tempfile.NamedTemporaryFile(suffix='.json') + with open(tmp_file.name, 'w') as f: + f.write(str(workout)) + + response = self.client.post( + reverse('manager:workout:overview'), + {'workoutfile': tmp_file}, + format='multipart' + ) + self.assertEqual(302, response.status_code) + + def test_import_workout_logged_in(self): + ''' + Test importing workout(s) with a logged in user + ''' + + self.user_login() + self.import_workouts() + self.user_logout() + + def test_import_workout_logged_in_without_json_file(self): + ''' + Test importing workout(s) with a logged in user without json file + ''' + + self.user_login() + response = self.client.post(reverse('manager:workout:overview')) + + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, '/en/workout/overview') + + all_messages = [msg for msg in get_messages(response.wsgi_request)] + + # here's how you test the first message + self.assertEqual(all_messages[0].tags, "info") + self.assertEqual(all_messages[0].message, "No File was Chosen for Importation!") + self.user_logout() + + def test_import_workout_logged_in_invalid_json(self): + + self.user_login() + + incorrect_json_data = [ + { + "id": 1, + "cycle_kind": "microcycle", + "comment": "", + "creation_date": "2018-08-27", + "day": [ + ], + } + ] + tmp_file = tempfile.NamedTemporaryFile(suffix='.json') + with open(tmp_file.name, 'w') as f: + f.write(str(incorrect_json_data)) + + response = self.client.post( + reverse('manager:workout:overview'), + {'workoutfile': tmp_file}, + format='multipart' + ) + all_messages = [msg for msg in get_messages(response.wsgi_request)] + + # here's how you test the first message + self.assertEqual(all_messages[0].tags, "info") + self.assertEqual(all_messages[0].message, "The Workout JSON file is invalid.") + self.user_logout() + + class DeleteTestWorkoutTestCase(WorkoutManagerDeleteTestCase): ''' Tests deleting a Workout diff --git a/wger/manager/urls.py b/wger/manager/urls.py index 574125c1..0f87de5b 100644 --- a/wger/manager/urls.py +++ b/wger/manager/urls.py @@ -49,6 +49,9 @@ url(r'^overview$', workout.overview, name='overview'), + url(r'^export/$', + workout.export_user_workouts, + name='export'), url(r'^add$', workout.add, name='add'), diff --git a/wger/manager/views/workout.py b/wger/manager/views/workout.py index 076b34fd..3fb8e7bc 100644 --- a/wger/manager/views/workout.py +++ b/wger/manager/views/workout.py @@ -17,9 +17,12 @@ import logging import uuid import datetime +import json +from decimal import Decimal -from django.shortcuts import render, get_object_or_404 +from django.shortcuts import render, redirect, get_object_or_404 from django.http import HttpResponseRedirect, HttpResponseForbidden +from django.contrib import messages from django.template.context_processors import csrf from django.core.urlresolvers import reverse, reverse_lazy from django.utils.translation import ugettext_lazy, ugettext as _ @@ -27,16 +30,29 @@ from django.contrib.auth.decorators import login_required from django.views.generic import DeleteView, UpdateView +from django.db.models import Q from wger.core.models import ( RepetitionUnit, WeightUnit ) from wger.manager.models import ( Workout, + Day, + Set, + Setting, WorkoutSession, WorkoutLog, Schedule, - Day +) +from wger.exercises.models import ( + Exercise, + Muscle, + Equipment, + ExerciseCategory +) +from wger.core.models import ( + Language, + License ) from wger.manager.forms import ( WorkoutForm, @@ -48,6 +64,8 @@ WgerDeleteMixin ) from wger.utils.helpers import make_token +from django.http import HttpResponse +from django.utils.datastructures import MultiValueDictKeyError logger = logging.getLogger(__name__) @@ -62,6 +80,91 @@ def overview(request): An overview of all the user's workouts ''' + if request.method == 'POST': + try: + request.FILES['workoutfile'] + except MultiValueDictKeyError: + messages.info(request, 'No File was Chosen for Importation!') + return HttpResponseRedirect('overview') + + file = request.FILES['workoutfile'] + + try: + data_bytes = file.read() + data = data_bytes.decode("utf8") + workouts = json.loads(data) + + for workout in workouts: + new_workout = Workout(cycle_kind=workout['cycle_kind'], comment=workout['comment']) + new_workout.user = request.user + new_workout.save() + + for day in workout['day']: + workout_day = Day(description=day['description'], training=new_workout) + workout_day.save() + + for day_item in day['daysofweek']: + workout_day.day.add(day_item['dayofweek']) + + for workout_set in day['sets']: + new_workout_set = Set( + order=workout_set['order'], + sets=workout_set['sets'], + exerciseday=workout_day + ) + new_workout_set.save() + + exercise_id_value = 0 + + for exercise in workout_set['exercises']: + workout_exercise = Exercise.objects.filter( + name=exercise['name'], + uuid=exercise['uuid'] + ).first() + + if workout_exercise is None: + workout_exercise = Exercise( + license_author=exercise['license_author'], + status=exercise['status'], + description=exercise['description'], + name=exercise['name'], + creation_date=exercise['creation_date'], + category_id=exercise['category'] + ) + workout_exercise.save() + + for muscle in exercise['muscles']: + workout_exercise.muscles.add(muscle['id']) + + for muscle_secondary in exercise['muscles']: + workout_exercise.muscles_secondary.add(muscle_secondary['id']) + + for equipment in exercise['equipment']: + workout_exercise.equipment.add(equipment['id']) + + exercise_id_value = workout_exercise.id + + # AFTER VERIFY THAT THE EXERCISE EXISTS OR CREATING IT + # CREATE THE RELATIONSHIP WITH SET_EXERCISES TABLE + new_workout_set.exercises.add(exercise_id_value, 1) + + for setting in exercise['settings']: + set_setting = Setting( + reps=setting['reps'], + order=setting['order'], + comment=setting['comment'], + weight=Decimal(setting['weight']), + repetition_unit_id=setting['repetition_unit_id'], + weight_unit_id=setting['weight_unit_id'], + exercise_id=exercise_id_value, + set=new_workout_set + ) + + set_setting.save() + except (ValueError, KeyError, Exception) as e: + messages.info(request, 'The Workout JSON file is invalid.') + return HttpResponseRedirect('overview') + template_data = {} workouts = Workout.objects.filter(user=request.user) @@ -225,6 +328,97 @@ def add(request): return HttpResponseRedirect(workout.get_absolute_url()) +@login_required +def export_user_workouts(request): + user = request.user + workouts = Workout.objects.filter(user=user).all() + user_workouts = [] + for workout in workouts: + days = Day.objects.filter(training=workout).all() + + user_workouts.append({ + 'id': workout.id, + 'cycle_kind': workout.cycle_kind, + 'comment': workout.comment, + 'creation_date': str(workout.creation_date), + 'day': [ + { + 'id': day.id, + 'description': day.description, + 'daysofweek': [ + { + 'dayofweek': dayofweek.id + } for dayofweek in day.day.all() + ], + 'sets': [ + { + 'id': workout_set.id, + 'sets': workout_set.sets, + 'order': workout_set.order, + 'exercises': [ + { + 'id': exercise.id, + 'license_author': exercise.license_author, + 'status': exercise.status, + 'description': exercise.description, + 'name': exercise.name, + 'creation_date': str(exercise.creation_date), + 'uuid': exercise.uuid, + 'category': exercise.category_id, + 'muscles': [ + { + 'id': muscle.id, + 'name': muscle.name, + 'is_front': muscle.is_front + } for muscle in Muscle.objects.filter( + exercise=exercise + ).all() + ], + 'muscles_secondary': [ + { + 'id': muscle_secondary.id, + 'name': muscle_secondary.name, + 'is_front': muscle_secondary.is_front + } for muscle_secondary in exercise.muscles_secondary.all() + ], + 'equipment': [ + { + 'id': equipment.id, + 'name': equipment.name, + } for equipment in Equipment.objects.filter( + exercise=exercise + ).all() + ], + 'settings': [ + { + 'reps': setting.reps, + 'order': setting.order, + 'comment': setting.comment, + 'weight': str(setting.weight), + 'repetition_unit_id': setting.repetition_unit_id, + 'weight_unit_id': setting.repetition_unit_id + } for setting in Setting.objects.filter( + Q(exercise=exercise) | Q(set=workout_set) + ).all() + ] + } for exercise in workout_set.exercises.all() + ] + } for workout_set in Set.objects.filter( + Q(order=workout.id) | Q(exerciseday_id=day.id) + ).all() + ] + + } for day in days + ] + }) + + json_workouts = json.dumps(user_workouts) + filename = '{0}-workouts'.format(user.username) + response = HttpResponse(json_workouts, content_type='application/json') + response['Content-Disposition'] = 'attachment; filename="{}.json"'.format(filename) + return response + + class WorkoutDeleteView(WgerDeleteMixin, LoginRequiredMixin, DeleteView): ''' Generic view to delete a workout routine