Commit 728f8a56 authored by Elias Larsen's avatar Elias Larsen
Browse files

Merge branch 'master' into elias/fix/comments

parents 69dbd8d9 82a077c0
......@@ -12,21 +12,24 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
from pathlib import Path
import os
from typing import Dict
import dj_database_url
import dotenv
# 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
# 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_HOST = "mx.ntnu.no"
EMAIL_USE_TLS = False
EMAIL_PORT = 25
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
dotenv_file = os.path.join(BASE_DIR, ".env")
......@@ -42,8 +45,8 @@ ALLOWED_HOSTS = [
"127.0.0.1",
"localhost",
"0.0.0.0",
"10." + groupid + ".0.6",
"10." + groupid + ".0.4",
"10." + group_id + ".0.6",
"10." + group_id + ".0.4",
"molde.idi.ntnu.no",
"10.0.2.2",
"tdt4242-t11.herokuapp.com",
......@@ -96,12 +99,11 @@ TEMPLATES = [
WSGI_APPLICATION = "secfit.wsgi.application"
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
if 'DATABASE_URL' in os.environ:
DATABASES = {
DATABASES: Dict[str, Dict] = {
"default": {}
}
db_from_env = dj_database_url.config(conn_max_age=500)
......@@ -114,8 +116,6 @@ else:
}
}
# CORS Policy
CORS_ORIGIN_ALLOW_ALL = (
True
......@@ -134,7 +134,6 @@ USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
......@@ -146,7 +145,6 @@ STATIC_URL = "/static/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
......
......@@ -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
"""
......
......@@ -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
......
......@@ -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
......@@ -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:
......
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.
"""
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)
<
test_data = {
"name": "testCreate",
"date": "2021-03-20 19:00+0100",