diff --git a/backend/secfit/workouts/mixins.py b/backend/secfit/workouts/mixins.py index 321ffe551c07ff8f14284b2e1bd4ece2892051b4..d37b6c4381c3ab3fa05ffb29315594056ebc55d7 100644 --- a/backend/secfit/workouts/mixins.py +++ b/backend/secfit/workouts/mixins.py @@ -3,7 +3,7 @@ Mixins for the workouts application """ -class CreateListModelMixin(object): +class CreateListModelMixin: """Mixin that allows to create multiple objects from lists. Taken from https://stackoverflow.com/a/48885641 """ diff --git a/backend/secfit/workouts/models.py b/backend/secfit/workouts/models.py index 5e3c6d1614d54992b42491bee8207a65410c5961..deb67b34a8fbd185f9cb0552daf24081d595a61b 100644 --- a/backend/secfit/workouts/models.py +++ b/backend/secfit/workouts/models.py @@ -71,7 +71,7 @@ class Workout(models.Model): class Exercise(models.Model): """Django model for an exercise type that users can create. - Each exercise instance must have an exercise type, e.g., Pushups, Crunches, or Lunges. + Each exercise instance must have an exercise type, e.g., Push-ups, Crunches, or Lunges. Attributes: name: Name of the exercise type diff --git a/backend/secfit/workouts/parsers.py b/backend/secfit/workouts/parsers.py index 3255481ce8d327bdfa92f434bf1f03e04d158443..7cd6da975911dff54841e78d8ddaad0ab0907590 100644 --- a/backend/secfit/workouts/parsers.py +++ b/backend/secfit/workouts/parsers.py @@ -3,6 +3,7 @@ import json from rest_framework import parsers + # Thanks to https://stackoverflow.com/a/50514630 class MultipartJsonParser(parsers.MultiPartParser): """Parser for serializing multipart data containing both files and JSON. @@ -24,10 +25,7 @@ class MultipartJsonParser(parsers.MultiPartParser): data[key] = value continue if "{" in value or "[" in value: - try: - data[key] = json.loads(value) - except ValueError: - data[key] = value + data[key] = get_key(value) else: data[key] = value @@ -36,3 +34,14 @@ class MultipartJsonParser(parsers.MultiPartParser): new_files["files"].append({"file": file}) return parsers.DataAndFiles(data, new_files) + + +def get_key(value): + """ + Tries to fetch a key from the dataset + """ + try: + key = json.loads(value) + except ValueError: + key = value + return key diff --git a/backend/secfit/workouts/serializers.py b/backend/secfit/workouts/serializers.py index a966ed3d752dcf54767a10f2b4b53d416e095a33..baa300e7642e75e59f9e446f57c70bffa24f5ca5 100644 --- a/backend/secfit/workouts/serializers.py +++ b/backend/secfit/workouts/serializers.py @@ -94,50 +94,84 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): Workout: A newly created Workout """ exercise_instances_data = validated_data.pop("exercise_instances") - files_data = [] + validated_files = [] if "files" in validated_data: - files_data = validated_data.pop("files") + validated_files = validated_data.pop("files") workout = Workout.objects.create(**validated_data) for exercise_instance_data in exercise_instances_data: ExerciseInstance.objects.create(workout=workout, **exercise_instance_data) - for file_data in files_data: + for file_data in validated_files: WorkoutFile.objects.create( workout=workout, owner=workout.owner, file=file_data.get("file") ) return workout - def update(self, instance, validated_data): - """Custom logic for updating a Workout with its ExerciseInstances and Workouts. + @staticmethod + def delete_files(instance, data): + """"Removes the selected data from the instanced object - This is needed because each object in both exercise_instances and files must be iterated - over and handled individually. + Args: + instance: Data object + data: Data to remove + """ + for i in range(len(data), len(instance.all())): + instance.all()[i].delete() + + @staticmethod + def create_workout_files(instance, files, validated_files): + """Creates the workout files Args: - instance (Workout): Current Workout object - validated_data: Contains data for validated fields + instance: Workout instance + files: Collection of files + validated_files: Data for validated fields Returns: - Workout: Updated Workout instance + instance: Workout instance """ - exercise_instances_data = validated_data.pop("exercise_instances") - exercise_instances = instance.exercise_instances + for i in range(len(files.all()), len(validated_files)): + WorkoutFile.objects.create( + workout=instance, + owner=instance.owner, + file=validated_files[i].get("file"), + ) + return instance - instance.name = validated_data.get("name", instance.name) - instance.notes = validated_data.get("notes", instance.notes) - instance.visibility = validated_data.get("visibility", instance.visibility) - instance.date = validated_data.get("date", instance.date) - instance.save() + @staticmethod + def create_exercise_instance(instance, exercise_instances, exercise_instances_data): + """Creates the workout files - # Handle ExerciseInstances + Args: + instance: Workout instance + exercise_instances: Collection of workouts + exercise_instances_data: Data for workout fields + + Returns: + instance: Workout instance + """ + for i in range(len(exercise_instances.all()), len(exercise_instances_data)): + exercise_instance_data = exercise_instances_data[i] + ExerciseInstance.objects.create( + workout=instance, **exercise_instance_data + ) + return instance + + @staticmethod + def update_exercise_instances(exercise_instances, exercise_instances_data): + """Updates existing exercise instances without adding or deleting object + + Args: + exercise_instances: Collection of workouts + exercise_instances_data: Data for workout fields + """ - # This updates existing exercise instances without adding or deleting object. # zip() will yield n 2-tuples, where n is # min(len(exercise_instance), len(exercise_instance_data)) for exercise_instance, exercise_instance_data in zip( - exercise_instances.all(), exercise_instances_data + exercise_instances.all(), exercise_instances_data ): exercise_instance.exercise = exercise_instance_data.get( "exercise", exercise_instance.exercise @@ -150,43 +184,59 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): ) exercise_instance.save() + def update(self, instance, validated_data): + """Custom logic for updating a Workout with its ExerciseInstances and Workouts. + + This is needed because each object in both exercise_instances and files must be iterated + over and handled individually. + + Args: + instance (Workout): Current Workout object + validated_data: Contains data for validated fields + + Returns: + Workout: Updated Workout instance + """ + exercise_instances_data = validated_data.pop("exercise_instances") + exercise_instances = instance.exercise_instances + + instance.name = validated_data.get("name", instance.name) + instance.notes = validated_data.get("notes", instance.notes) + instance.visibility = validated_data.get("visibility", instance.visibility) + instance.date = validated_data.get("date", instance.date) + instance.save() + + # Handle ExerciseInstances + self.update_exercise_instances(exercise_instances, exercise_instances_data) + # If new exercise instances have been added to the workout, then create them if len(exercise_instances_data) > len(exercise_instances.all()): - for i in range(len(exercise_instances.all()), len(exercise_instances_data)): - exercise_instance_data = exercise_instances_data[i] - ExerciseInstance.objects.create( - workout=instance, **exercise_instance_data - ) + instance = self.create_exercise_instance(instance, exercise_instances, exercise_instances_data) # Else if exercise instances have been removed from the workout, then delete them elif len(exercise_instances_data) < len(exercise_instances.all()): - for i in range(len(exercise_instances_data), len(exercise_instances.all())): - exercise_instances.all()[i].delete() + self.delete_files(exercise_instances_data, exercise_instances) # Handle WorkoutFiles if "files" in validated_data: - files_data = validated_data.pop("files") + validated_files = validated_data.pop("files") files = instance.files - for file, file_data in zip(files.all(), files_data): + for file, file_data in zip(files.all(), validated_files): file.file = file_data.get("file", file.file) # If new files have been added, creating new WorkoutFiles - if len(files_data) > len(files.all()): - for i in range(len(files.all()), len(files_data)): - WorkoutFile.objects.create( - workout=instance, - owner=instance.owner, - file=files_data[i].get("file"), - ) + if len(validated_files) > len(files.all()): + instance = self.create_workout_files(instance, files, validated_files) + # Else if files have been removed, delete WorkoutFiles - elif len(files_data) < len(files.all()): - for i in range(len(files_data), len(files.all())): - files.all()[i].delete() + elif len(validated_files) < len(files.all()): + self.delete_files(files, validated_files) return instance - def get_owner_username(self, obj): + @staticmethod + def get_owner_username(obj): """Returns the owning user's username Args: diff --git a/backend/secfit/workouts/tests/__init__.py b/backend/secfit/workouts/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..30b86107a15a80baf443f9caa65f5009d9a3541e --- /dev/null +++ b/backend/secfit/workouts/tests/__init__.py @@ -0,0 +1,2 @@ +from workouts.tests.workouts_serializer import WorkoutSerializerTestCase +from workouts.tests.test import IsOwnerTestCase, IsOwnerOfWorkoutTestCase, IsCoachAndVisibleToCoachTestCase, IsCoachOfWorkoutAndVisibleToCoachTestCase, IsPublicTestCase, IsWorkoutPublicTestCase, IsReadOnlyTestCase \ No newline at end of file diff --git a/backend/secfit/workouts/tests.py b/backend/secfit/workouts/tests/test.py similarity index 52% rename from backend/secfit/workouts/tests.py rename to backend/secfit/workouts/tests/test.py index 03baa0f8cde96fec83f758ee284a28bc05840f52..e7e72ce4600b60b1e459b2db24970250ebe66794 100644 --- a/backend/secfit/workouts/tests.py +++ b/backend/secfit/workouts/tests/test.py @@ -1,27 +1,36 @@ """ Tests for the workouts application. """ -from rest_framework import permissions -from workouts.permissions import * -from users.models import User -from workouts.models import Workout -from django.test import TestCase import datetime +from rest_framework import permissions import pytz +from django.test import TestCase +from workouts.permissions import \ + IsOwner, IsOwnerOfWorkout, IsCoachAndVisibleToCoach, \ + IsCoachOfWorkoutAndVisibleToCoach, IsPublic, IsWorkoutPublic, \ + IsReadOnly +from workouts.models import Workout +from users.models import User -# Create your tests here. -class MockRequest(): +class MockRequest: + """ + Imitates a (mutable) request + """ def __init__(self): self.method = "" self.data = "" self.user = None -class MockWorkout(): + +class MockWorkout: + """ + Imitates a workout + """ def __init__(self): try: user = User.objects.get(pk='1') - except: + except User.DoesNotExist: user = User.objects.create() workout_data = { @@ -34,7 +43,11 @@ class MockWorkout(): self.workout = Workout.objects.create(**workout_data) self.workout.owner.coach = User() + class IsOwnerTestCase(TestCase): + """ + Ownership permissions + """ def setUp(self): self.is_owner = IsOwner() self.request = MockRequest() @@ -42,128 +55,193 @@ class IsOwnerTestCase(TestCase): self.request.user = User() def test_has_object_permission(self): - self.assertFalse(self.is_owner.has_object_permission(self.request, None, self.workout.workout)) + """ + Object permission + """ + self.assertIs(self.is_owner.has_object_permission( + self.request, None, self.workout.workout + ), False) self.request.user = self.workout.workout.owner - self.assertTrue(self.is_owner.has_object_permission(self.request, None, self.workout.workout)) + self.assertIs(self.is_owner.has_object_permission( + self.request, None, self.workout.workout + ), True) + class IsOwnerOfWorkoutTestCase(TestCase): + """ + Owner of workout permissions + """ def setUp(self): self.is_owner_of_workout = IsOwnerOfWorkout() self.request = MockRequest() self.workout = MockWorkout() def test_has_permission_method(self): + """ + GET permission + """ request = MockRequest() request.method = "GET" - self.assertTrue(self.is_owner_of_workout.has_permission(request, None)) + self.assertIs(self.is_owner_of_workout.has_permission(request, None), True) def test_has_permission_workout(self): + """ + POST workout permission + """ request = MockRequest() request.method = "POST" request.data = {"workout": ""} - self.assertFalse(self.is_owner_of_workout.has_permission(request, None)) + self.assertIs(self.is_owner_of_workout.has_permission(request, None), False) def test_has_permission_user(self): + """ + POST workout permission via REST API + """ request = MockRequest() request.method = "POST" request.user = self.workout.workout.owner - request.data = {"workout": "http://localhost:8000/api/workouts/1/"} - self.assertTrue(self.is_owner_of_workout.has_permission(request, None)) + request.data = { + "workout": "http://localhost:8000/api/workouts/1/" + } + self.assertIs(self.is_owner_of_workout.has_permission( + request, None + ), True) def test_has_object_permission(self): - self.assertFalse(self.is_owner_of_workout.has_object_permission(self.request, None, self.workout)) + """ + Ownership permissions + """ + self.assertIs(self.is_owner_of_workout.has_object_permission( + self.request, None, self.workout + ), False) self.request.user = self.workout.workout.owner - self.assertTrue(self.is_owner_of_workout.has_object_permission(self.request, None, self.workout)) + self.assertIs(self.is_owner_of_workout.has_object_permission( + self.request, None, self.workout + ), True) + class IsCoachAndVisibleToCoachTestCase(TestCase): + """ + Coach permissions + """ def setUp(self): - self.is_coach_and_visable_to_coach = IsCoachAndVisibleToCoach() + self.is_coach_and_visible_to_coach = IsCoachAndVisibleToCoach() self.request = MockRequest() self.request.user = User() self.workout = MockWorkout() def test_has_object_permission(self): - self.assertFalse(self.is_coach_and_visable_to_coach.has_object_permission( + """ + Coach has object permissions + """ + self.assertIs(self.is_coach_and_visible_to_coach.has_object_permission( self.request, None, self.workout.workout - )) + ), False) self.workout.workout.owner.coach = self.request.user - self.assertTrue(self.is_coach_and_visable_to_coach.has_object_permission( + self.assertIs(self.is_coach_and_visible_to_coach.has_object_permission( self.request, None, self.workout.workout - )) + ), True) + class IsCoachOfWorkoutAndVisibleToCoachTestCase(TestCase): + """ + Coach permissions for a specific workout + """ def setUp(self): - self.is_coach_of_workout_and_visable_to_coach = IsCoachOfWorkoutAndVisibleToCoach() + self.is_coach_of_workout_and_visible_to_coach = IsCoachOfWorkoutAndVisibleToCoach() self.request = MockRequest() self.request.user = User() self.workout = MockWorkout() def test_has_object_permission(self): - self.assertFalse(self.is_coach_of_workout_and_visable_to_coach.has_object_permission( + """ + Coach permissions + """ + self.assertIs(self.is_coach_of_workout_and_visible_to_coach.has_object_permission( self.request, None, self.workout - )) + ), False) self.workout.workout.owner.coach = self.request.user - self.assertTrue(self.is_coach_of_workout_and_visable_to_coach.has_object_permission( + self.assertIs(self.is_coach_of_workout_and_visible_to_coach.has_object_permission( self.request, None, self.workout - )) + ), True) class IsPublicTestCase(TestCase): + """ + Test case public permissions + """ def setUp(self): self.workout = MockWorkout() self.is_public = IsPublic() def test_has_object_permission(self): - self.assertTrue(self.is_public.has_object_permission( + """ + Workout permissions + """ + self.assertIs(self.is_public.has_object_permission( None, None, - self.workout.workout) - ) + self.workout.workout + ), True) self.workout.workout.visibility = "CO" - self.assertFalse(self.is_public.has_object_permission( + self.assertIs(self.is_public.has_object_permission( None, None, - self.workout.workout) - ) + self.workout.workout + ), False) + class IsWorkoutPublicTestCase(TestCase): + """ + Workout is publicly available + """ def setUp(self): self.workout = MockWorkout() self.is_workout_public = IsWorkoutPublic() def test_has_object_permission(self): - self.assertTrue(self.is_workout_public.has_object_permission( + """ + Workout permissions + """ + self.assertIs(self.is_workout_public.has_object_permission( None, None, - self.workout) - ) + self.workout + ), True) self.workout.workout.visibility = "N" - self.assertFalse(self.is_workout_public.has_object_permission( + self.assertIs(self.is_workout_public.has_object_permission( None, None, - self.workout) - ) + self.workout + ), False) + class IsReadOnlyTestCase(TestCase): + """ + Read only permission + """ def setUp(self): self.is_read_only = IsReadOnly() self.request = MockRequest() self.request.method = permissions.SAFE_METHODS.__getitem__(1) def test_has_object_permission(self): - self.assertTrue(self.is_read_only.has_object_permission(self.request, None, None)) + """ + Object permissions + """ + self.assertIs(self.is_read_only.has_object_permission(self.request, None, None), True) self.request.method = None - self.assertFalse(self.is_read_only.has_object_permission(self.request, None, None)) + self.assertIs(self.is_read_only.has_object_permission(self.request, None, None), False) diff --git a/backend/secfit/workouts/tests/test_data.py b/backend/secfit/workouts/tests/test_data.py new file mode 100644 index 0000000000000000000000000000000000000000..8418d21a806a6a8dcc8780fb3d42891598f2e99b --- /dev/null +++ b/backend/secfit/workouts/tests/test_data.py @@ -0,0 +1,21 @@ +test_data = { + "name": "testCreate", + "date": "2021-03-20 19:00+0100", + "owner_id": "1", + "pk": "1", + "exercise_instances": {}, +} +test_data1 = { + "name": "testCreate", + "date": "2021-03-20 19:00+0100", + "owner_id": "1", + "pk": "1", + "exercise_instances": {}, +} +test_exercise = { + "id": "1", + "name": "walk", + "description": "hard", + "unit": "1", + "instances": {} +} diff --git a/backend/secfit/workouts/tests/workouts_serializer.py b/backend/secfit/workouts/tests/workouts_serializer.py new file mode 100644 index 0000000000000000000000000000000000000000..132c2537b008257859d23fa99d18b179dbb5a7bf --- /dev/null +++ b/backend/secfit/workouts/tests/workouts_serializer.py @@ -0,0 +1,82 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model +import datetime +from workouts.serializers import WorkoutSerializer, ExerciseSerializer +from workouts.models import Workout, Exercise, ExerciseInstance +from workouts.tests.test_data import test_data, test_data1, test_exercise + + +class WorkoutSerializerTestCase(TestCase): + """WorkoutSerializer test class""" + + def setUp(self): + self.owner_model = get_user_model() + owner = self.owner_model( + username="test", + email="test@test.com", + phone_number="12345678", + country="Norway", + city="Oslo", + street_address="address 10" + ) + password = "password" + owner.set_password(password) + owner.save() + self.workout = Workout( + name="testCreate", + date="2021-03-20 19:00+0100", + owner=owner, + owner_id="1", + pk=1, + ) + self.exercise = Exercise( + name="walk", + description="hard", + unit="1", + ) + self.instance = ExerciseInstance( + workout=self.workout, + exercise=self.exercise, + sets=10, + number=10, + id=1, + exercise_id=1, + ) + + def test_create(self): + workout_create = WorkoutSerializer.create(WorkoutSerializer(), validated_data=test_data) + self.assertEqual(workout_create, self.workout) + + def test_update(self): + """ + Compare mocked exercise and workout to serialized versions respectively + + """ + + e = test_data1.copy() + e["exercise_instances"] = {0: { + "sets": "10", + "number": "10", + "exercise_id": "1", + }} + ExerciseSerializer.create(ExerciseSerializer(), validated_data=test_exercise) + workout_create = WorkoutSerializer.create(WorkoutSerializer(), validated_data=test_data1.copy()) + + # Adds the exercise instance to the workout + workout_updated = WorkoutSerializer.update( + WorkoutSerializer(), + instance=workout_create, + validated_data=e + ) + + # Remove state identifier from the objects + delattr(workout_updated, "_state") + delattr(self.workout, "_state") + real_exercise = ExerciseInstance.objects.all()[0] + delattr(real_exercise, "_state") + mock_exercise = self.instance + delattr(mock_exercise, "_state") + + # Compare the fields in the objects directly using vars() + self.assertEqual(vars(workout_updated), vars(self.workout)) + self.assertEqual(vars(real_exercise), vars(mock_exercise)) diff --git a/backend/secfit/workouts/urls.py b/backend/secfit/workouts/urls.py index 7c46a3f1ff311edc25dd455bb85780c1a1644738..dabed96f5d78fd2484c2b9a454301cb581acbfc4 100644 --- a/backend/secfit/workouts/urls.py +++ b/backend/secfit/workouts/urls.py @@ -1,11 +1,13 @@ +""" +Connecting URL paths to django.apps.workouts +""" from django.urls import path, include -from workouts import views from rest_framework.urlpatterns import format_suffix_patterns from rest_framework_simplejwt.views import ( TokenObtainPairView, TokenRefreshView, ) - +from workouts import views # This is a bit messy and will need to change urlpatterns = format_suffix_patterns( [ diff --git a/backend/secfit/workouts/views.py b/backend/secfit/workouts/views.py index efddf40454376b23d233f9fe2cecaf9da43fddb8..5dc85e216b8e31fc8522082fdbc6a01bfefbe533 100644 --- a/backend/secfit/workouts/views.py +++ b/backend/secfit/workouts/views.py @@ -1,16 +1,22 @@ -"""Contains views for the workouts application. These are mostly class-based views. """ +Contains views for the workouts application. These are mostly class-based views. +""" +import base64 +import pickle +from collections import namedtuple +from django.db.models import Q +from django.core.exceptions import PermissionDenied +from django.core.signing import Signer from rest_framework import generics, mixins from rest_framework import permissions - +from rest_framework import filters from rest_framework.parsers import ( JSONParser, ) from rest_framework.decorators import api_view -from rest_framework.response import Response from rest_framework.reverse import reverse -from django.db.models import Q -from rest_framework import filters +from rest_framework.response import Response +from rest_framework_simplejwt.tokens import RefreshToken from workouts.parsers import MultipartJsonParser from workouts.permissions import ( IsOwner, @@ -26,55 +32,59 @@ from workouts.models import Workout, Exercise, ExerciseInstance, WorkoutFile from workouts.serializers import WorkoutSerializer, ExerciseSerializer from workouts.serializers import RememberMeSerializer from workouts.serializers import ExerciseInstanceSerializer, WorkoutFileSerializer -from django.core.exceptions import PermissionDenied -from rest_framework_simplejwt.tokens import RefreshToken -from rest_framework.response import Response -import json -from collections import namedtuple -import base64, pickle -from django.core.signing import Signer @api_view(["GET"]) -def api_root(request, format=None): +def api_root(request, serializer_format=None): + """ + Define api root + """ return Response( { - "users": reverse("user-list", request=request, format=format), - "workouts": reverse("workout-list", request=request, format=format), - "exercises": reverse("exercise-list", request=request, format=format), + "users": reverse("user-list", request=request, format=serializer_format), + "workouts": reverse("workout-list", request=request, format=serializer_format), + "exercises": reverse("exercise-list", request=request, format=serializer_format), "exercise-instances": reverse( - "exercise-instance-list", request=request, format=format + "exercise-instance-list", request=request, format=serializer_format ), "workout-files": reverse( - "workout-file-list", request=request, format=format + "workout-file-list", request=request, format=serializer_format ), - "comments": reverse("comment-list", request=request, format=format), - "likes": reverse("like-list", request=request, format=format), + "comments": reverse("comment-list", request=request, format=serializer_format), + "likes": reverse("like-list", request=request, format=serializer_format), } ) # Allow users to save a persistent session in their browser class RememberMe( - mixins.ListModelMixin, - mixins.CreateModelMixin, - mixins.DestroyModelMixin, - generics.GenericAPIView, + mixins.ListModelMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, ): + """ + Creates cookie for session storage + """ serializer_class = RememberMeSerializer def get(self, request): - if request.user.is_authenticated == False: + """ + Retrieves cookie if user is authenticated + """ + if not request.user.is_authenticated: raise PermissionDenied - else: - return Response({"remember_me": self.rememberme()}) + return Response({"remember_me": self.rememberme()}) def post(self, request): - cookieObject = namedtuple("Cookies", request.COOKIES.keys())( + """ + Use refresh token to get new cookies + """ + cookie_object = namedtuple("Cookies", request.COOKIES.keys())( *request.COOKIES.values() ) - user = self.get_user(cookieObject) + user = self.get_user(cookie_object) refresh = RefreshToken.for_user(user) return Response( { @@ -83,26 +93,37 @@ class RememberMe( } ) - def get_user(self, cookieObject): - decode = base64.b64decode(cookieObject.remember_me) + def get_user(self, cookie_object): + """ + Fetches user + """ + decode = base64.b64decode(cookie_object.remember_me) user, sign = pickle.loads(decode) # Validate signature if sign == self.sign_user(user): return user + raise PermissionDenied def rememberme(self): - creds = [self.request.user, self.sign_user(str(self.request.user))] - return base64.b64encode(pickle.dumps(creds)) - - def sign_user(self, username): + """ + Returns the rememberme token + """ + credentials = [self.request.user, self.sign_user(str(self.request.user))] + return base64.b64encode(pickle.dumps(credentials)) + + @staticmethod + def sign_user(username): + """ + Creates a signing hash based on the username + """ signer = Signer() signed_user = signer.sign(username) return signed_user class WorkoutList( - mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView + mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView ): """Class defining the web response for the creation of a Workout, or displaying a list of Workouts @@ -122,34 +143,46 @@ class WorkoutList( ordering_fields = ["name", "date", "owner__username"] def get(self, request, *args, **kwargs): + """ + Get list of visible workouts + """ return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): + """ + Create multiple workouts + """ return self.create(request, *args, **kwargs) def perform_create(self, serializer): + """ + Saves ownership + """ serializer.save(owner=self.request.user) def get_queryset(self): - qs = Workout.objects.none() + """ + Return all workouts you should be able to view + """ + query_set = Workout.objects.none() if self.request.user: # A workout should be visible to the requesting user if any of the following hold: # - The workout has public visibility # - The owner of the workout is the requesting user # - The workout has coach visibility and the requesting user is the owner's coach - qs = Workout.objects.filter( + query_set = Workout.objects.filter( Q(visibility="PU") | (Q(visibility="CO") & Q(owner__coach=self.request.user)) ).distinct() - return qs + return query_set class WorkoutDetail( - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - generics.GenericAPIView, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, ): """Class defining the web response for the details of an individual Workout. @@ -160,22 +193,31 @@ class WorkoutDetail( serializer_class = WorkoutSerializer permission_classes = [ permissions.IsAuthenticated - & (IsOwner | (IsReadOnly & (IsCoachAndVisibleToCoach | IsPublic))) + and (IsOwner or (IsReadOnly and (IsCoachAndVisibleToCoach or IsPublic))) ] parser_classes = [MultipartJsonParser, JSONParser] def get(self, request, *args, **kwargs): + """ + Retrieve a workout + """ return self.retrieve(request, *args, **kwargs) def put(self, request, *args, **kwargs): + """ + Update a workout + """ return self.update(request, *args, **kwargs) def delete(self, request, *args, **kwargs): + """ + Delete a workout + """ return self.destroy(request, *args, **kwargs) class ExerciseList( - mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView + mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView ): """Class defining the web response for the creation of an Exercise, or a list of Exercises. @@ -188,17 +230,23 @@ class ExerciseList( permission_classes = [permissions.IsAuthenticated] def get(self, request, *args, **kwargs): + """ + Retrieve visible exercises + """ return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): + """ + Create exercise(s) + """ return self.create(request, *args, **kwargs) class ExerciseDetail( - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - generics.GenericAPIView, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, ): """Class defining the web response for the details of an individual Exercise. @@ -210,39 +258,63 @@ class ExerciseDetail( permission_classes = [permissions.IsAuthenticated] def get(self, request, *args, **kwargs): + """ + Retrieve exercise + """ return self.retrieve(request, *args, **kwargs) def put(self, request, *args, **kwargs): + """ + Update exercise + """ return self.update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): + """ + Update parts of exercise + """ return self.partial_update(request, *args, **kwargs) def delete(self, request, *args, **kwargs): + """ + Delete exercise + """ return self.destroy(request, *args, **kwargs) class ExerciseInstanceList( - mixins.ListModelMixin, - mixins.CreateModelMixin, - CreateListModelMixin, - generics.GenericAPIView, + mixins.ListModelMixin, + mixins.CreateModelMixin, + CreateListModelMixin, + generics.GenericAPIView, ): - """Class defining the web response for the creation""" + """Class defining the web response for a list of excercise instances. + + HTTP methods: GET, POST + """ serializer_class = ExerciseInstanceSerializer permission_classes = [permissions.IsAuthenticated & IsOwnerOfWorkout] def get(self, request, *args, **kwargs): + """ + Retrieve visible exercise instances + """ return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): + """ + Create exercise instance(s) + """ return self.create(request, *args, **kwargs) def get_queryset(self): - qs = ExerciseInstance.objects.none() + """ + Return all exercise instances you should be able to view + """ + query_set = ExerciseInstance.objects.none() if self.request.user: - qs = ExerciseInstance.objects.filter( + query_set = ExerciseInstance.objects.filter( Q(workout__owner=self.request.user) | ( (Q(workout__visibility="CO") | Q(workout__visibility="PU")) @@ -250,15 +322,20 @@ class ExerciseInstanceList( ) ).distinct() - return qs + return query_set class ExerciseInstanceDetail( - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - generics.GenericAPIView, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, ): + """Class defining the web response for a list of excercise instances. + + HTTP methods: GET, PUT, PATCH, DELETE + """ + serializer_class = ExerciseInstanceSerializer permission_classes = [ permissions.IsAuthenticated @@ -269,24 +346,40 @@ class ExerciseInstanceDetail( ] def get(self, request, *args, **kwargs): + """ + Retrieve an exercise instance + """ return self.retrieve(request, *args, **kwargs) def put(self, request, *args, **kwargs): + """ + Update an exercise instance + """ return self.update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): + """ + Update parts of an exercise instance + """ return self.partial_update(request, *args, **kwargs) def delete(self, request, *args, **kwargs): + """ + Delete an exercise instance + """ return self.destroy(request, *args, **kwargs) class WorkoutFileList( - mixins.ListModelMixin, - mixins.CreateModelMixin, - CreateListModelMixin, - generics.GenericAPIView, + mixins.ListModelMixin, + mixins.CreateModelMixin, + CreateListModelMixin, + generics.GenericAPIView, ): + """Class defining the web response for a list of workouts. + + HTTP methods: GET, POST + """ queryset = WorkoutFile.objects.all() serializer_class = WorkoutFileSerializer @@ -294,18 +387,30 @@ class WorkoutFileList( parser_classes = [MultipartJsonParser, JSONParser] def get(self, request, *args, **kwargs): + """ + Retrieve workout files + """ return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): + """ + Create workout file(s) + """ return self.create(request, *args, **kwargs) def perform_create(self, serializer): + """ + Assign user as owner + """ serializer.save(owner=self.request.user) def get_queryset(self): - qs = WorkoutFile.objects.none() + """ + Return all workout files you should be able to view + """ + query_set = WorkoutFile.objects.none() if self.request.user: - qs = WorkoutFile.objects.filter( + query_set = WorkoutFile.objects.filter( Q(owner=self.request.user) | Q(workout__owner=self.request.user) | ( @@ -314,15 +419,19 @@ class WorkoutFileList( ) ).distinct() - return qs + return query_set class WorkoutFileDetail( - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - generics.GenericAPIView, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, ): + """Class defining the web response for a workout file. + + HTTP methods: GET, DELETE + """ queryset = WorkoutFile.objects.all() serializer_class = WorkoutFileSerializer @@ -336,7 +445,13 @@ class WorkoutFileDetail( ] def get(self, request, *args, **kwargs): + """ + Retrieve workout file + """ return self.retrieve(request, *args, **kwargs) def delete(self, request, *args, **kwargs): + """ + Delete workout file + """ return self.destroy(request, *args, **kwargs)