Commit 82a077c0 authored by Elias Larsen's avatar Elias Larsen
Browse files

Merge branch 'master' into elias/fix/frontend

parents 2ae117b7 de11c207
...@@ -12,21 +12,24 @@ https://docs.djangoproject.com/en/3.1/ref/settings/ ...@@ -12,21 +12,24 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
from pathlib import Path from pathlib import Path
import os import os
from typing import Dict
import dj_database_url import dj_database_url
import dotenv import dotenv
# Get the GROUPID variable to accept connections from the application server and NGINX # Get the GROUPID variable to accept connections from the application server and NGINX
groupid = os.environ.get("GROUPID", "0") group_id = os.environ.get("GROUPID", "0")
# Email configuration # Email configuration
# The host must be running within NTNU's VPN (vpn.ntnu.no) to allow this config # The host must be running within NTNU's VPN (vpn.ntnu.no) to allow this config
# Usage: https://docs.djangoproject.com/en/3.1/topics/email/#obtaining-an-instance-of-an-email-backend # Usage:
# https://docs.djangoproject.com/en/3.1/topics/email/#obtaining-an-instance-of-an-email-backend
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "mx.ntnu.no" EMAIL_HOST = "mx.ntnu.no"
EMAIL_USE_TLS = False EMAIL_USE_TLS = False
EMAIL_PORT = 25 EMAIL_PORT = 25
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
dotenv_file = os.path.join(BASE_DIR, ".env") dotenv_file = os.path.join(BASE_DIR, ".env")
...@@ -42,8 +45,8 @@ ALLOWED_HOSTS = [ ...@@ -42,8 +45,8 @@ ALLOWED_HOSTS = [
"127.0.0.1", "127.0.0.1",
"localhost", "localhost",
"0.0.0.0", "0.0.0.0",
"10." + groupid + ".0.6", "10." + group_id + ".0.6",
"10." + groupid + ".0.4", "10." + group_id + ".0.4",
"molde.idi.ntnu.no", "molde.idi.ntnu.no",
"10.0.2.2", "10.0.2.2",
"tdt4242-t11.herokuapp.com", "tdt4242-t11.herokuapp.com",
...@@ -96,12 +99,11 @@ TEMPLATES = [ ...@@ -96,12 +99,11 @@ TEMPLATES = [
WSGI_APPLICATION = "secfit.wsgi.application" WSGI_APPLICATION = "secfit.wsgi.application"
# Database # Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases # https://docs.djangoproject.com/en/3.1/ref/settings/#databases
if 'DATABASE_URL' in os.environ: if 'DATABASE_URL' in os.environ:
DATABASES = { DATABASES: Dict[str, Dict] = {
"default": {} "default": {}
} }
db_from_env = dj_database_url.config(conn_max_age=500) db_from_env = dj_database_url.config(conn_max_age=500)
...@@ -114,8 +116,6 @@ else: ...@@ -114,8 +116,6 @@ else:
} }
} }
# CORS Policy # CORS Policy
CORS_ORIGIN_ALLOW_ALL = ( CORS_ORIGIN_ALLOW_ALL = (
True True
...@@ -134,7 +134,6 @@ USE_L10N = True ...@@ -134,7 +134,6 @@ USE_L10N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/ # https://docs.djangoproject.com/en/3.1/howto/static-files/
...@@ -146,7 +145,6 @@ STATIC_URL = "/static/" ...@@ -146,7 +145,6 @@ STATIC_URL = "/static/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media") MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
REST_FRAMEWORK = { REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10, "PAGE_SIZE": 10,
......
...@@ -3,7 +3,7 @@ Mixins for the workouts application ...@@ -3,7 +3,7 @@ Mixins for the workouts application
""" """
class CreateListModelMixin(object): class CreateListModelMixin:
"""Mixin that allows to create multiple objects from lists. """Mixin that allows to create multiple objects from lists.
Taken from https://stackoverflow.com/a/48885641 Taken from https://stackoverflow.com/a/48885641
""" """
......
...@@ -71,7 +71,7 @@ class Workout(models.Model): ...@@ -71,7 +71,7 @@ class Workout(models.Model):
class Exercise(models.Model): class Exercise(models.Model):
"""Django model for an exercise type that users can create. """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: Attributes:
name: Name of the exercise type name: Name of the exercise type
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import json import json
from rest_framework import parsers from rest_framework import parsers
# Thanks to https://stackoverflow.com/a/50514630 # Thanks to https://stackoverflow.com/a/50514630
class MultipartJsonParser(parsers.MultiPartParser): class MultipartJsonParser(parsers.MultiPartParser):
"""Parser for serializing multipart data containing both files and JSON. """Parser for serializing multipart data containing both files and JSON.
...@@ -24,10 +25,7 @@ class MultipartJsonParser(parsers.MultiPartParser): ...@@ -24,10 +25,7 @@ class MultipartJsonParser(parsers.MultiPartParser):
data[key] = value data[key] = value
continue continue
if "{" in value or "[" in value: if "{" in value or "[" in value:
try: data[key] = get_key(value)
data[key] = json.loads(value)
except ValueError:
data[key] = value
else: else:
data[key] = value data[key] = value
...@@ -36,3 +34,14 @@ class MultipartJsonParser(parsers.MultiPartParser): ...@@ -36,3 +34,14 @@ class MultipartJsonParser(parsers.MultiPartParser):
new_files["files"].append({"file": file}) new_files["files"].append({"file": file})
return parsers.DataAndFiles(data, new_files) 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
...@@ -94,50 +94,84 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): ...@@ -94,50 +94,84 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer):
Workout: A newly created Workout Workout: A newly created Workout
""" """
exercise_instances_data = validated_data.pop("exercise_instances") exercise_instances_data = validated_data.pop("exercise_instances")
files_data = [] validated_files = []
if "files" in validated_data: if "files" in validated_data:
files_data = validated_data.pop("files") validated_files = validated_data.pop("files")
workout = Workout.objects.create(**validated_data) workout = Workout.objects.create(**validated_data)
for exercise_instance_data in exercise_instances_data: for exercise_instance_data in exercise_instances_data:
ExerciseInstance.objects.create(workout=workout, **exercise_instance_data) ExerciseInstance.objects.create(workout=workout, **exercise_instance_data)
for file_data in files_data: for file_data in validated_files:
WorkoutFile.objects.create( WorkoutFile.objects.create(
workout=workout, owner=workout.owner, file=file_data.get("file") workout=workout, owner=workout.owner, file=file_data.get("file")
) )
return workout return workout
def update(self, instance, validated_data): @staticmethod
"""Custom logic for updating a Workout with its ExerciseInstances and Workouts. 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 Args:
over and handled individually. 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: Args:
instance (Workout): Current Workout object instance: Workout instance
validated_data: Contains data for validated fields files: Collection of files
validated_files: Data for validated fields
Returns: Returns:
Workout: Updated Workout instance instance: Workout instance
""" """
exercise_instances_data = validated_data.pop("exercise_instances") for i in range(len(files.all()), len(validated_files)):
exercise_instances = instance.exercise_instances WorkoutFile.objects.create(
workout=instance,
owner=instance.owner,
file=validated_files[i].get("file"),
)
return instance
instance.name = validated_data.get("name", instance.name) @staticmethod
instance.notes = validated_data.get("notes", instance.notes) def create_exercise_instance(instance, exercise_instances, exercise_instances_data):
instance.visibility = validated_data.get("visibility", instance.visibility) """Creates the workout files
instance.date = validated_data.get("date", instance.date)
instance.save()
# 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 # zip() will yield n 2-tuples, where n is
# min(len(exercise_instance), len(exercise_instance_data)) # min(len(exercise_instance), len(exercise_instance_data))
for exercise_instance, exercise_instance_data in zip( 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_instance.exercise = exercise_instance_data.get(
"exercise", exercise_instance.exercise "exercise", exercise_instance.exercise
...@@ -150,43 +184,59 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): ...@@ -150,43 +184,59 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer):
) )
exercise_instance.save() 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 new exercise instances have been added to the workout, then create them
if len(exercise_instances_data) > len(exercise_instances.all()): if len(exercise_instances_data) > len(exercise_instances.all()):
for i in range(len(exercise_instances.all()), len(exercise_instances_data)): instance = self.create_exercise_instance(instance, exercise_instances, exercise_instances_data)
exercise_instance_data = exercise_instances_data[i]
ExerciseInstance.objects.create(
workout=instance, **exercise_instance_data
)
# Else if exercise instances have been removed from the workout, then delete them # Else if exercise instances have been removed from the workout, then delete them
elif len(exercise_instances_data) < len(exercise_instances.all()): elif len(exercise_instances_data) < len(exercise_instances.all()):
for i in range(len(exercise_instances_data), len(exercise_instances.all())): self.delete_files(exercise_instances_data, exercise_instances)
exercise_instances.all()[i].delete()
# Handle WorkoutFiles # Handle WorkoutFiles
if "files" in validated_data: if "files" in validated_data:
files_data = validated_data.pop("files") validated_files = validated_data.pop("files")
files = instance.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) file.file = file_data.get("file", file.file)
# If new files have been added, creating new WorkoutFiles # If new files have been added, creating new WorkoutFiles
if len(files_data) > len(files.all()): if len(validated_files) > len(files.all()):
for i in range(len(files.all()), len(files_data)): instance = self.create_workout_files(instance, files, validated_files)
WorkoutFile.objects.create(
workout=instance,
owner=instance.owner,
file=files_data[i].get("file"),
)
# Else if files have been removed, delete WorkoutFiles # Else if files have been removed, delete WorkoutFiles
elif len(files_data) < len(files.all()): elif len(validated_files) < len(files.all()):
for i in range(len(files_data), len(files.all())): self.delete_files(files, validated_files)
files.all()[i].delete()
return instance return instance
def get_owner_username(self, obj): @staticmethod
def get_owner_username(obj):
"""Returns the owning user's username """Returns the owning user's username
Args: Args:
......
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
""" """
Tests for the workouts application. 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 import datetime
from rest_framework import permissions
import pytz 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): def __init__(self):
self.method = "" self.method = ""
self.data = "" self.data = ""
self.user = None self.user = None
class MockWorkout():
class MockWorkout:
"""
Imitates a workout
"""
def __init__(self): def __init__(self):
try: try:
user = User.objects.get(pk='1') user = User.objects.get(pk='1')
except: except User.DoesNotExist:
user = User.objects.create() user = User.objects.create()
workout_data = { workout_data = {
...@@ -34,7 +43,11 @@ class MockWorkout(): ...@@ -34,7 +43,11 @@ class MockWorkout():
self.workout = Workout.objects.create(**workout_data) self.workout = Workout.objects.create(**workout_data)
self.workout.owner.coach = User() self.workout.owner.coach = User()
class IsOwnerTestCase(TestCase): class IsOwnerTestCase(TestCase):
"""
Ownership permissions
"""
def setUp(self): def setUp(self):
self.is_owner = IsOwner() self.is_owner = IsOwner()
self.request = MockRequest() self.request = MockRequest()
...@@ -42,128 +55,193 @@ class IsOwnerTestCase(TestCase): ...@@ -42,128 +55,193 @@ class IsOwnerTestCase(TestCase):
self.request.user = User() self.request.user = User()
def test_has_object_permission(self): 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.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): class IsOwnerOfWorkoutTestCase(TestCase):
"""
Owner of workout permissions
"""
def setUp(self): def setUp(self):
self.is_owner_of_workout = IsOwnerOfWorkout() self.is_owner_of_workout = IsOwnerOfWorkout()
self.request = MockRequest() self.request = MockRequest()
self.workout = MockWorkout() self.workout = MockWorkout()
def test_has_permission_method(self): def test_has_permission_method(self):
"""
GET permission
"""
request = MockRequest() request = MockRequest()
request.method = "GET" 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): def test_has_permission_workout(self):
"""
POST workout permission
"""
request = MockRequest() request = MockRequest()
request.method = "POST" request.method = "POST"
request.data = {"workout": ""} 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): def test_has_permission_user(self):
"""
POST workout permission via REST API
"""
request = MockRequest() request = MockRequest()
request.method = "POST" request.method = "POST"
request.user = self.workout.workout.owner request.user = self.workout.workout.owner
request.data = {"workout": "http://localhost:8000/api/workouts/1/"} request.data = {
self.assertTrue(self.is_owner_of_workout.has_permission(request, None)) "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): 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.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): class IsCoachAndVisibleToCoachTestCase(TestCase):
"""
Coach permissions
"""
def setUp(self): def setUp(self):
self.is_coach_and_visable_to_coach = IsCoachAndVisibleToCoach() self.is_coach_and_visible_to_coach = IsCoachAndVisibleToCoach()
self.request = MockRequest() self.request = MockRequest()
self.request.user = User() self.request.user = User()
self.workout = MockWorkout() self.workout = MockWorkout()