From abbc65725e75f82056e7ec772ed32085ffb25a51 Mon Sep 17 00:00:00 2001 From: olavhdi <32062632+olavhdi@users.noreply.github.com> Date: Thu, 8 Apr 2021 11:47:02 +0200 Subject: [PATCH 01/11] Workouts PYLINT #28 --- backend/secfit/workouts/mixins.py | 2 +- backend/secfit/workouts/serializers.py | 5 +- backend/secfit/workouts/tests.py | 135 +++++++++++--- backend/secfit/workouts/urls.py | 6 +- backend/secfit/workouts/views.py | 239 ++++++++++++++++++------- 5 files changed, 292 insertions(+), 95 deletions(-) diff --git a/backend/secfit/workouts/mixins.py b/backend/secfit/workouts/mixins.py index 321ffe5..d37b6c4 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/serializers.py b/backend/secfit/workouts/serializers.py index a966ed3..a71d9c2 100644 --- a/backend/secfit/workouts/serializers.py +++ b/backend/secfit/workouts/serializers.py @@ -137,7 +137,7 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): # 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 @@ -186,7 +186,8 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): 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.py b/backend/secfit/workouts/tests.py index 03baa0f..a4f5aa6 100644 --- a/backend/secfit/workouts/tests.py +++ b/backend/secfit/workouts/tests.py @@ -1,27 +1,37 @@ """ 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: + """ + Mocks a request + """ def __init__(self): self.method = "" self.data = "" self.user = None -class MockWorkout(): + +class MockWorkout: + """ + Mocks a workout for testing + """ def __init__(self): try: user = User.objects.get(pk='1') - except: + except User.DoesNotExist: user = User.objects.create() workout_data = { @@ -34,7 +44,11 @@ class MockWorkout(): self.workout = Workout.objects.create(**workout_data) self.workout.owner.coach = User() + class IsOwnerTestCase(TestCase): + """ + Checks ownership permissions + """ def setUp(self): self.is_owner = IsOwner() self.request = MockRequest() @@ -42,76 +56,121 @@ 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)) + """ + Validates object permission + """ + self.assertFalse(self.is_owner.has_object_permission( + self.request, None, self.workout.workout + )) self.request.user = self.workout.workout.owner - self.assertTrue(self.is_owner.has_object_permission(self.request, None, self.workout.workout)) + self.assertTrue(self.is_owner.has_object_permission( + self.request, None, self.workout.workout + )) + class IsOwnerOfWorkoutTestCase(TestCase): + """ + Validates 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)) 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)) 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.assertTrue(self.is_owner_of_workout.has_permission( + request, None + )) def test_has_object_permission(self): - self.assertFalse(self.is_owner_of_workout.has_object_permission(self.request, None, self.workout)) + """ + Ownership permissions + """ + self.assertFalse(self.is_owner_of_workout.has_object_permission( + self.request, None, self.workout + )) self.request.user = self.workout.workout.owner - self.assertTrue(self.is_owner_of_workout.has_object_permission(self.request, None, self.workout)) + self.assertTrue(self.is_owner_of_workout.has_object_permission( + self.request, None, self.workout + )) + 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( + """ + Validates object permissions + """ + self.assertFalse(self.is_coach_and_visible_to_coach.has_object_permission( self.request, None, self.workout.workout )) self.workout.workout.owner.coach = self.request.user - self.assertTrue(self.is_coach_and_visable_to_coach.has_object_permission( + self.assertTrue(self.is_coach_and_visible_to_coach.has_object_permission( self.request, None, self.workout.workout )) + class IsCoachOfWorkoutAndVisibleToCoachTestCase(TestCase): + """ + Validates coach relation and 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( + """ + Validates coach permissions + """ + self.assertFalse(self.is_coach_of_workout_and_visible_to_coach.has_object_permission( self.request, None, self.workout )) self.workout.workout.owner.coach = self.request.user - self.assertTrue(self.is_coach_of_workout_and_visable_to_coach.has_object_permission( + self.assertTrue(self.is_coach_of_workout_and_visible_to_coach.has_object_permission( self.request, None, self.workout @@ -119,50 +178,70 @@ class IsCoachOfWorkoutAndVisibleToCoachTestCase(TestCase): class IsPublicTestCase(TestCase): + """ + Validates test case public permissions + """ def setUp(self): self.workout = MockWorkout() self.is_public = IsPublic() def test_has_object_permission(self): + """ + Validates workout permissions + """ self.assertTrue(self.is_public.has_object_permission( None, None, - self.workout.workout) - ) + self.workout.workout + )) self.workout.workout.visibility = "CO" self.assertFalse(self.is_public.has_object_permission( None, None, - self.workout.workout) - ) + self.workout.workout + )) + class IsWorkoutPublicTestCase(TestCase): + """ + Checks that a workout is publicly available + """ def setUp(self): self.workout = MockWorkout() self.is_workout_public = IsWorkoutPublic() def test_has_object_permission(self): + """ + Validates workout permissions + """ self.assertTrue(self.is_workout_public.has_object_permission( None, None, - self.workout) - ) + self.workout + )) self.workout.workout.visibility = "N" self.assertFalse(self.is_workout_public.has_object_permission( None, None, - self.workout) - ) + self.workout + )) + class IsReadOnlyTestCase(TestCase): + """ + Checks that objects have the correct 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): + """ + Validates that the test has the correct object permissions + """ self.assertTrue(self.is_read_only.has_object_permission(self.request, None, None)) self.request.method = None diff --git a/backend/secfit/workouts/urls.py b/backend/secfit/workouts/urls.py index 7c46a3f..dabed96 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 efddf40..d99e1f3 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,15 +143,27 @@ 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): + """ + Return all workouts you should be able to view + """ qs = Workout.objects.none() if self.request.user: # A workout should be visible to the requesting user if any of the following hold: @@ -146,10 +179,10 @@ class WorkoutList( 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. @@ -165,17 +198,26 @@ class WorkoutDetail( 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,36 +258,60 @@ 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): + """ + Return all exercise instances you should be able to view + """ qs = ExerciseInstance.objects.none() if self.request.user: qs = ExerciseInstance.objects.filter( @@ -254,11 +326,16 @@ class ExerciseInstanceList( 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,15 +387,27 @@ 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): + """ + Return all workout files you should be able to view + """ qs = WorkoutFile.objects.none() if self.request.user: qs = WorkoutFile.objects.filter( @@ -318,11 +423,15 @@ class WorkoutFileList( 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) -- GitLab From f1b901c263e264400d832ad4973865b2eaf3c833 Mon Sep 17 00:00:00 2001 From: olavhdi <32062632+olavhdi@users.noreply.github.com> Date: Thu, 8 Apr 2021 12:27:36 +0200 Subject: [PATCH 02/11] Workouts more PYLINT #28 --- backend/secfit/workouts/mixins.py | 1 - backend/secfit/workouts/tests.py | 74 +++++++++++++++---------------- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/backend/secfit/workouts/mixins.py b/backend/secfit/workouts/mixins.py index d37b6c4..4910d29 100644 --- a/backend/secfit/workouts/mixins.py +++ b/backend/secfit/workouts/mixins.py @@ -2,7 +2,6 @@ Mixins for the workouts application """ - class CreateListModelMixin: """Mixin that allows to create multiple objects from lists. Taken from https://stackoverflow.com/a/48885641 diff --git a/backend/secfit/workouts/tests.py b/backend/secfit/workouts/tests.py index a4f5aa6..4bf357a 100644 --- a/backend/secfit/workouts/tests.py +++ b/backend/secfit/workouts/tests.py @@ -2,6 +2,7 @@ Tests for the workouts application. """ import datetime +from recordclass import recordclass from rest_framework import permissions import pytz from django.test import TestCase @@ -13,36 +14,33 @@ from workouts.models import Workout from users.models import User -# Create your tests here. -class MockRequest: +def mock_request(): """ - Mocks a request + Mocks a mutable request """ - def __init__(self): - self.method = "" - self.data = "" - self.user = None + mock = recordclass('MockRequest', ['method', 'data', 'user']) + return mock("", "", None) -class MockWorkout: +def mock_workout(): """ - Mocks a workout for testing + Mocks a workout """ - def __init__(self): - try: - user = User.objects.get(pk='1') - except User.DoesNotExist: - user = User.objects.create() - - workout_data = { - "name": "Test Workout", - "date": datetime.datetime(2021, 2, 19, 9, 33, 0, 0, pytz.UTC), - "notes": "notes", - "visibility": "PU", - "owner": user - } - self.workout = Workout.objects.create(**workout_data) - self.workout.owner.coach = User() + try: + user = User.objects.get(pk='1') + except User.DoesNotExist: + user = User.objects.create() + workout_data = { + "name": "Test Workout", + "date": datetime.datetime(2021, 2, 19, 9, 33, 0, 0, pytz.UTC), + "notes": "notes", + "visibility": "PU", + "owner": user + } + workout = Workout.objects.create(**workout_data) + workout.owner.coach = User() + mock = recordclass('MockWorkout', ['workout']) + return mock(workout) class IsOwnerTestCase(TestCase): @@ -51,8 +49,8 @@ class IsOwnerTestCase(TestCase): """ def setUp(self): self.is_owner = IsOwner() - self.request = MockRequest() - self.workout = MockWorkout() + self.request = mock_request() + self.workout = mock_workout() self.request.user = User() def test_has_object_permission(self): @@ -74,14 +72,14 @@ class IsOwnerOfWorkoutTestCase(TestCase): """ def setUp(self): self.is_owner_of_workout = IsOwnerOfWorkout() - self.request = MockRequest() - self.workout = MockWorkout() + self.request = mock_request() + self.workout = mock_workout() def test_has_permission_method(self): """ Get permission """ - request = MockRequest() + request = mock_request() request.method = "GET" self.assertTrue(self.is_owner_of_workout.has_permission(request, None)) @@ -89,7 +87,7 @@ class IsOwnerOfWorkoutTestCase(TestCase): """ POST workout permission """ - request = MockRequest() + request = mock_request() request.method = "POST" request.data = {"workout": ""} self.assertFalse(self.is_owner_of_workout.has_permission(request, None)) @@ -98,7 +96,7 @@ class IsOwnerOfWorkoutTestCase(TestCase): """ POST workout permission via REST API """ - request = MockRequest() + request = mock_request() request.method = "POST" request.user = self.workout.workout.owner request.data = { @@ -127,9 +125,9 @@ class IsCoachAndVisibleToCoachTestCase(TestCase): """ def setUp(self): self.is_coach_and_visible_to_coach = IsCoachAndVisibleToCoach() - self.request = MockRequest() + self.request = mock_request() self.request.user = User() - self.workout = MockWorkout() + self.workout = mock_workout() def test_has_object_permission(self): """ @@ -155,9 +153,9 @@ class IsCoachOfWorkoutAndVisibleToCoachTestCase(TestCase): """ def setUp(self): self.is_coach_of_workout_and_visible_to_coach = IsCoachOfWorkoutAndVisibleToCoach() - self.request = MockRequest() + self.request = mock_request() self.request.user = User() - self.workout = MockWorkout() + self.workout = mock_workout() def test_has_object_permission(self): """ @@ -182,7 +180,7 @@ class IsPublicTestCase(TestCase): Validates test case public permissions """ def setUp(self): - self.workout = MockWorkout() + self.workout = mock_workout() self.is_public = IsPublic() def test_has_object_permission(self): @@ -208,7 +206,7 @@ class IsWorkoutPublicTestCase(TestCase): Checks that a workout is publicly available """ def setUp(self): - self.workout = MockWorkout() + self.workout = mock_workout() self.is_workout_public = IsWorkoutPublic() def test_has_object_permission(self): @@ -235,7 +233,7 @@ class IsReadOnlyTestCase(TestCase): """ def setUp(self): self.is_read_only = IsReadOnly() - self.request = MockRequest() + self.request = mock_request() self.request.method = permissions.SAFE_METHODS.__getitem__(1) def test_has_object_permission(self): -- GitLab From 2cdb711fa1ee498b223f0e71a84c5f5217a016a7 Mon Sep 17 00:00:00 2001 From: olavhdi <32062632+olavhdi@users.noreply.github.com> Date: Thu, 8 Apr 2021 12:50:08 +0200 Subject: [PATCH 03/11] workouts PEP-8 and Django Code Styles #29 --- backend/secfit/workouts/mixins.py | 1 + backend/secfit/workouts/models.py | 2 +- backend/secfit/workouts/parsers.py | 1 + backend/secfit/workouts/tests.py | 86 +++++++++++++++--------------- 4 files changed, 46 insertions(+), 44 deletions(-) diff --git a/backend/secfit/workouts/mixins.py b/backend/secfit/workouts/mixins.py index 4910d29..d37b6c4 100644 --- a/backend/secfit/workouts/mixins.py +++ b/backend/secfit/workouts/mixins.py @@ -2,6 +2,7 @@ Mixins for the workouts application """ + 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 5e3c6d1..deb67b3 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 3255481..8011ffa 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. diff --git a/backend/secfit/workouts/tests.py b/backend/secfit/workouts/tests.py index 4bf357a..8fd410e 100644 --- a/backend/secfit/workouts/tests.py +++ b/backend/secfit/workouts/tests.py @@ -45,7 +45,7 @@ def mock_workout(): class IsOwnerTestCase(TestCase): """ - Checks ownership permissions + Ownership permissions """ def setUp(self): self.is_owner = IsOwner() @@ -55,20 +55,20 @@ class IsOwnerTestCase(TestCase): def test_has_object_permission(self): """ - Validates object permission + Object permission """ - self.assertFalse(self.is_owner.has_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.assertIs(self.is_owner.has_object_permission( self.request, None, self.workout.workout - )) + ), True) class IsOwnerOfWorkoutTestCase(TestCase): """ - Validates owner of workout permissions + Owner of workout permissions """ def setUp(self): self.is_owner_of_workout = IsOwnerOfWorkout() @@ -77,11 +77,11 @@ class IsOwnerOfWorkoutTestCase(TestCase): def test_has_permission_method(self): """ - Get permission + GET permission """ request = mock_request() 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): """ @@ -90,7 +90,7 @@ class IsOwnerOfWorkoutTestCase(TestCase): request = mock_request() 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): """ @@ -102,21 +102,21 @@ class IsOwnerOfWorkoutTestCase(TestCase): request.data = { "workout": "http://localhost:8000/api/workouts/1/" } - self.assertTrue(self.is_owner_of_workout.has_permission( + self.assertIs(self.is_owner_of_workout.has_permission( request, None - )) + ), True) def test_has_object_permission(self): """ Ownership permissions """ - self.assertFalse(self.is_owner_of_workout.has_object_permission( + 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.assertIs(self.is_owner_of_workout.has_object_permission( self.request, None, self.workout - )) + ), True) class IsCoachAndVisibleToCoachTestCase(TestCase): @@ -131,25 +131,25 @@ class IsCoachAndVisibleToCoachTestCase(TestCase): def test_has_object_permission(self): """ - Validates object permissions + Coach has object permissions """ - self.assertFalse(self.is_coach_and_visible_to_coach.has_object_permission( + 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_visible_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): """ - Validates coach relation and permissions for a specific workout + Coach permissions for a specific workout """ def setUp(self): self.is_coach_of_workout_and_visible_to_coach = IsCoachOfWorkoutAndVisibleToCoach() @@ -159,25 +159,25 @@ class IsCoachOfWorkoutAndVisibleToCoachTestCase(TestCase): def test_has_object_permission(self): """ - Validates coach permissions + Coach permissions """ - self.assertFalse(self.is_coach_of_workout_and_visible_to_coach.has_object_permission( + 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_visible_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): """ - Validates test case public permissions + Test case public permissions """ def setUp(self): self.workout = mock_workout() @@ -185,25 +185,25 @@ class IsPublicTestCase(TestCase): def test_has_object_permission(self): """ - Validates workout permissions + Workout permissions """ - self.assertTrue(self.is_public.has_object_permission( + self.assertIs(self.is_public.has_object_permission( None, None, 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 - )) + ), False) class IsWorkoutPublicTestCase(TestCase): """ - Checks that a workout is publicly available + Workout is publicly available """ def setUp(self): self.workout = mock_workout() @@ -211,25 +211,25 @@ class IsWorkoutPublicTestCase(TestCase): def test_has_object_permission(self): """ - Validates workout permissions + Workout permissions """ - self.assertTrue(self.is_workout_public.has_object_permission( + self.assertIs(self.is_workout_public.has_object_permission( None, None, 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 - )) + ), False) class IsReadOnlyTestCase(TestCase): """ - Checks that objects have the correct read only permission + Read only permission """ def setUp(self): self.is_read_only = IsReadOnly() @@ -238,9 +238,9 @@ class IsReadOnlyTestCase(TestCase): def test_has_object_permission(self): """ - Validates that the test has the correct object permissions + Object permissions """ - self.assertTrue(self.is_read_only.has_object_permission(self.request, None, None)) + 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) -- GitLab From 10d45d730d9db4c8a100663d7c889f2283a52acf Mon Sep 17 00:00:00 2001 From: olavhdi <32062632+olavhdi@users.noreply.github.com> Date: Thu, 8 Apr 2021 13:11:12 +0200 Subject: [PATCH 04/11] workouts logical naming #34 --- backend/secfit/workouts/serializers.py | 18 +++++++++--------- backend/secfit/workouts/views.py | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/secfit/workouts/serializers.py b/backend/secfit/workouts/serializers.py index a71d9c2..df585b4 100644 --- a/backend/secfit/workouts/serializers.py +++ b/backend/secfit/workouts/serializers.py @@ -94,15 +94,15 @@ 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_data: WorkoutFile.objects.create( workout=workout, owner=workout.owner, file=file_data.get("file") ) @@ -165,23 +165,23 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): # 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)): + for i in range(len(files.all()), len(validated_files)): WorkoutFile.objects.create( workout=instance, owner=instance.owner, - file=files_data[i].get("file"), + file=validated_files[i].get("file"), ) # 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())): + elif len(validated_files) < len(files.all()): + for i in range(len(validated_files), len(files.all())): files.all()[i].delete() return instance diff --git a/backend/secfit/workouts/views.py b/backend/secfit/workouts/views.py index d99e1f3..cccd692 100644 --- a/backend/secfit/workouts/views.py +++ b/backend/secfit/workouts/views.py @@ -164,18 +164,18 @@ class WorkoutList( """ Return all workouts you should be able to view """ - qs = Workout.objects.none() + 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( @@ -312,9 +312,9 @@ class ExerciseInstanceList( """ Return all exercise instances you should be able to view """ - qs = ExerciseInstance.objects.none() + 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")) @@ -322,7 +322,7 @@ class ExerciseInstanceList( ) ).distinct() - return qs + return query_set class ExerciseInstanceDetail( @@ -408,9 +408,9 @@ class WorkoutFileList( """ Return all workout files you should be able to view """ - qs = WorkoutFile.objects.none() + 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) | ( @@ -419,7 +419,7 @@ class WorkoutFileList( ) ).distinct() - return qs + return query_set class WorkoutFileDetail( -- GitLab From 1538a5ab4178457045c8acfa234f00ff91e64038 Mon Sep 17 00:00:00 2001 From: olavhdi <32062632+olavhdi@users.noreply.github.com> Date: Wed, 14 Apr 2021 21:22:15 +0200 Subject: [PATCH 05/11] #33, also mccabe of 12 to 7 --- backend/secfit/workouts/serializers.py | 115 ++++++++++++++++++------- 1 file changed, 82 insertions(+), 33 deletions(-) diff --git a/backend/secfit/workouts/serializers.py b/backend/secfit/workouts/serializers.py index df585b4..baa300e 100644 --- a/backend/secfit/workouts/serializers.py +++ b/backend/secfit/workouts/serializers.py @@ -102,38 +102,72 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): for exercise_instance_data in exercise_instances_data: ExerciseInstance.objects.create(workout=workout, **exercise_instance_data) - for file_data in validated_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( @@ -150,17 +184,37 @@ 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 @@ -172,17 +226,12 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): 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(validated_files)): - WorkoutFile.objects.create( - workout=instance, - owner=instance.owner, - file=validated_files[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(validated_files) < len(files.all()): - for i in range(len(validated_files), len(files.all())): - files.all()[i].delete() + self.delete_files(files, validated_files) return instance -- GitLab From a9cbe9d7dd7fef80acf2bf01c9332f94a02a8b99 Mon Sep 17 00:00:00 2001 From: olavhdi <32062632+olavhdi@users.noreply.github.com> Date: Wed, 14 Apr 2021 21:32:53 +0200 Subject: [PATCH 06/11] #33 workouts/parsers --- backend/secfit/workouts/parsers.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/backend/secfit/workouts/parsers.py b/backend/secfit/workouts/parsers.py index 8011ffa..7cd6da9 100644 --- a/backend/secfit/workouts/parsers.py +++ b/backend/secfit/workouts/parsers.py @@ -25,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 @@ -37,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 -- GitLab From 1efb492fd61f61a9579ce91998b468d1497ba8be Mon Sep 17 00:00:00 2001 From: olavhdi <32062632+olavhdi@users.noreply.github.com> Date: Wed, 14 Apr 2021 21:42:33 +0200 Subject: [PATCH 07/11] #28 workouts/views using mypy plugin --- backend/secfit/workouts/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/secfit/workouts/views.py b/backend/secfit/workouts/views.py index cccd692..5dc85e2 100644 --- a/backend/secfit/workouts/views.py +++ b/backend/secfit/workouts/views.py @@ -193,7 +193,7 @@ 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] -- GitLab From 346052431c67c381a400298cb4267b86a180812c Mon Sep 17 00:00:00 2001 From: olavhdi <32062632+olavhdi@users.noreply.github.com> Date: Wed, 14 Apr 2021 22:09:39 +0200 Subject: [PATCH 08/11] update req --- backend/secfit/requirements.txt | Bin 1322 -> 1364 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/backend/secfit/requirements.txt b/backend/secfit/requirements.txt index 3df0a645654d6ea7e5824c693301c41039c0b776..ae094e9b8c667fad8fdc08155dc773cb99ba1ad6 100644 GIT binary patch delta 50 ycmZ3*b%kq#7ONI70~bRPLn=cuLq3pB0n#}Pi44U+XbXe}40;TP3?@L*m;nF|s|e-* delta 7 Ocmcb@wTf$l7ApV?@&a-I -- GitLab From 4ff469bbe19c64cefcaa1934376b357d851ad83d Mon Sep 17 00:00:00 2001 From: olavhdi <32062632+olavhdi@users.noreply.github.com> Date: Thu, 15 Apr 2021 11:14:41 +0200 Subject: [PATCH 09/11] dependency rollback --- backend/secfit/requirements.txt | Bin 1364 -> 1322 bytes backend/secfit/workouts/tests.py | 73 ++++++++++++++++--------------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/backend/secfit/requirements.txt b/backend/secfit/requirements.txt index ae094e9b8c667fad8fdc08155dc773cb99ba1ad6..3df0a645654d6ea7e5824c693301c41039c0b776 100644 GIT binary patch delta 7 Ocmcb@wTf$l7ApV?@&a-I delta 50 ycmZ3*b%kq#7ONI70~bRPLn=cuLq3pB0n#}Pi44U+XbXe}40;TP3?@L*m;nF|s|e-* diff --git a/backend/secfit/workouts/tests.py b/backend/secfit/workouts/tests.py index 8fd410e..e7e72ce 100644 --- a/backend/secfit/workouts/tests.py +++ b/backend/secfit/workouts/tests.py @@ -2,7 +2,6 @@ Tests for the workouts application. """ import datetime -from recordclass import recordclass from rest_framework import permissions import pytz from django.test import TestCase @@ -14,33 +13,35 @@ from workouts.models import Workout from users.models import User -def mock_request(): +class MockRequest: """ - Mocks a mutable request + Imitates a (mutable) request """ - mock = recordclass('MockRequest', ['method', 'data', 'user']) - return mock("", "", None) + def __init__(self): + self.method = "" + self.data = "" + self.user = None -def mock_workout(): +class MockWorkout: """ - Mocks a workout + Imitates a workout """ - try: - user = User.objects.get(pk='1') - except User.DoesNotExist: - user = User.objects.create() - workout_data = { - "name": "Test Workout", - "date": datetime.datetime(2021, 2, 19, 9, 33, 0, 0, pytz.UTC), - "notes": "notes", - "visibility": "PU", - "owner": user - } - workout = Workout.objects.create(**workout_data) - workout.owner.coach = User() - mock = recordclass('MockWorkout', ['workout']) - return mock(workout) + def __init__(self): + try: + user = User.objects.get(pk='1') + except User.DoesNotExist: + user = User.objects.create() + + workout_data = { + "name": "Test Workout", + "date": datetime.datetime(2021, 2, 19, 9, 33, 0, 0, pytz.UTC), + "notes": "notes", + "visibility": "PU", + "owner": user + } + self.workout = Workout.objects.create(**workout_data) + self.workout.owner.coach = User() class IsOwnerTestCase(TestCase): @@ -49,8 +50,8 @@ class IsOwnerTestCase(TestCase): """ def setUp(self): self.is_owner = IsOwner() - self.request = mock_request() - self.workout = mock_workout() + self.request = MockRequest() + self.workout = MockWorkout() self.request.user = User() def test_has_object_permission(self): @@ -72,14 +73,14 @@ class IsOwnerOfWorkoutTestCase(TestCase): """ def setUp(self): self.is_owner_of_workout = IsOwnerOfWorkout() - self.request = mock_request() - self.workout = mock_workout() + self.request = MockRequest() + self.workout = MockWorkout() def test_has_permission_method(self): """ GET permission """ - request = mock_request() + request = MockRequest() request.method = "GET" self.assertIs(self.is_owner_of_workout.has_permission(request, None), True) @@ -87,7 +88,7 @@ class IsOwnerOfWorkoutTestCase(TestCase): """ POST workout permission """ - request = mock_request() + request = MockRequest() request.method = "POST" request.data = {"workout": ""} self.assertIs(self.is_owner_of_workout.has_permission(request, None), False) @@ -96,7 +97,7 @@ class IsOwnerOfWorkoutTestCase(TestCase): """ POST workout permission via REST API """ - request = mock_request() + request = MockRequest() request.method = "POST" request.user = self.workout.workout.owner request.data = { @@ -125,9 +126,9 @@ class IsCoachAndVisibleToCoachTestCase(TestCase): """ def setUp(self): self.is_coach_and_visible_to_coach = IsCoachAndVisibleToCoach() - self.request = mock_request() + self.request = MockRequest() self.request.user = User() - self.workout = mock_workout() + self.workout = MockWorkout() def test_has_object_permission(self): """ @@ -153,9 +154,9 @@ class IsCoachOfWorkoutAndVisibleToCoachTestCase(TestCase): """ def setUp(self): self.is_coach_of_workout_and_visible_to_coach = IsCoachOfWorkoutAndVisibleToCoach() - self.request = mock_request() + self.request = MockRequest() self.request.user = User() - self.workout = mock_workout() + self.workout = MockWorkout() def test_has_object_permission(self): """ @@ -180,7 +181,7 @@ class IsPublicTestCase(TestCase): Test case public permissions """ def setUp(self): - self.workout = mock_workout() + self.workout = MockWorkout() self.is_public = IsPublic() def test_has_object_permission(self): @@ -206,7 +207,7 @@ class IsWorkoutPublicTestCase(TestCase): Workout is publicly available """ def setUp(self): - self.workout = mock_workout() + self.workout = MockWorkout() self.is_workout_public = IsWorkoutPublic() def test_has_object_permission(self): @@ -233,7 +234,7 @@ class IsReadOnlyTestCase(TestCase): """ def setUp(self): self.is_read_only = IsReadOnly() - self.request = mock_request() + self.request = MockRequest() self.request.method = permissions.SAFE_METHODS.__getitem__(1) def test_has_object_permission(self): -- GitLab From 4acf62ce2092492f1b01ed482e26edce93ad47ec Mon Sep 17 00:00:00 2001 From: olavhdi <32062632+olavhdi@users.noreply.github.com> Date: Mon, 19 Apr 2021 11:44:41 +0200 Subject: [PATCH 10/11] move workouts tests, add workoutserializer tests --- backend/secfit/workouts/tests/__init__.py | 2 + .../workouts/{tests.py => tests/test.py} | 0 backend/secfit/workouts/tests/test_data.py | 21 +++++ .../workouts/tests/workouts_serializer.py | 81 +++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 backend/secfit/workouts/tests/__init__.py rename backend/secfit/workouts/{tests.py => tests/test.py} (100%) create mode 100644 backend/secfit/workouts/tests/test_data.py create mode 100644 backend/secfit/workouts/tests/workouts_serializer.py diff --git a/backend/secfit/workouts/tests/__init__.py b/backend/secfit/workouts/tests/__init__.py new file mode 100644 index 0000000..30b8610 --- /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 100% rename from backend/secfit/workouts/tests.py rename to backend/secfit/workouts/tests/test.py diff --git a/backend/secfit/workouts/tests/test_data.py b/backend/secfit/workouts/tests/test_data.py new file mode 100644 index 0000000..08b9b6b --- /dev/null +++ b/backend/secfit/workouts/tests/test_data.py @@ -0,0 +1,21 @@ +test_data = { + "name": "testCreate", + "date": "2021-03-20", + "owner_id": "1", + "pk": "1", + "exercise_instances": {}, +} +test_data1 = { + "name": "testCreate", + "date": "2021-03-20", + "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 0000000..6c40a58 --- /dev/null +++ b/backend/secfit/workouts/tests/workouts_serializer.py @@ -0,0 +1,81 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model +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", + 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)) -- GitLab From a5031c1340da00e63165deb542f773fc9897d3c6 Mon Sep 17 00:00:00 2001 From: olavhdi <32062632+olavhdi@users.noreply.github.com> Date: Mon, 19 Apr 2021 12:09:27 +0200 Subject: [PATCH 11/11] add timezone to workoutserializer test to avoid runtime warnings --- backend/secfit/workouts/tests/test_data.py | 4 ++-- backend/secfit/workouts/tests/workouts_serializer.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/secfit/workouts/tests/test_data.py b/backend/secfit/workouts/tests/test_data.py index 08b9b6b..8418d21 100644 --- a/backend/secfit/workouts/tests/test_data.py +++ b/backend/secfit/workouts/tests/test_data.py @@ -1,13 +1,13 @@ test_data = { "name": "testCreate", - "date": "2021-03-20", + "date": "2021-03-20 19:00+0100", "owner_id": "1", "pk": "1", "exercise_instances": {}, } test_data1 = { "name": "testCreate", - "date": "2021-03-20", + "date": "2021-03-20 19:00+0100", "owner_id": "1", "pk": "1", "exercise_instances": {}, diff --git a/backend/secfit/workouts/tests/workouts_serializer.py b/backend/secfit/workouts/tests/workouts_serializer.py index 6c40a58..132c253 100644 --- a/backend/secfit/workouts/tests/workouts_serializer.py +++ b/backend/secfit/workouts/tests/workouts_serializer.py @@ -1,5 +1,6 @@ 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 @@ -23,7 +24,7 @@ class WorkoutSerializerTestCase(TestCase): owner.save() self.workout = Workout( name="testCreate", - date="2021-03-20", + date="2021-03-20 19:00+0100", owner=owner, owner_id="1", pk=1, -- GitLab