Skip to content
Snippets Groups Projects
Commit f742d804 authored by Ole-Christian Bjerkeset's avatar Ole-Christian Bjerkeset
Browse files

Merge branch 'refactor/backend-workouts' into 'master'

Refactor/backend workouts

See merge request !6
parents afe079ec 6f23f39a
No related branches found
No related tags found
1 merge request!6Refactor/backend workouts
Pipeline #172308 passed
......@@ -5,4 +5,5 @@ backend/secfit/db.sqlite3
backend/secfit/htmlcov/
backend/secfit/.coverage
backend/secfit/.coveragerc
.vscode
.idea
......@@ -2,30 +2,12 @@
log workouts (Workout), which contain instances (ExerciseInstance) of various
type of exercises (Exercise). The user can also upload files (WorkoutFile) .
"""
import os
from django.db import models
from django.core.files.storage import FileSystemStorage
from django.conf import settings
from django.contrib.auth import get_user_model
class OverwriteStorage(FileSystemStorage):
"""Filesystem storage for overwriting files. Currently unused."""
def get_available_name(self, name, max_length=None):
"""https://djangosnippets.org/snippets/976/
Returns a filename that's free on the target storage system, and
available for new content to be written to.
Args:
name (str): Name of the file
max_length (int, optional): Maximum length of a file name. Defaults to None.
"""
if self.exists(name):
os.remove(os.path.join(settings.MEDIA_ROOT, name))
# Create your models here.
# Create your models here.
class Workout(models.Model):
"""Django model for a workout that users can log.
......@@ -48,24 +30,29 @@ class Workout(models.Model):
)
# Visibility levels
PUBLIC = "PU" # Visible to all authenticated users
COACH = "CO" # Visible only to owner and their coach
PRIVATE = "PR" # Visible only to owner
# Visible to all authenticated users
PUBLIC = "PU"
# Visible only to owner and their coach
COACH = "CO"
# Visible only to owner
PRIVATE = "PR"
# Choices for visibility level
VISIBILITY_CHOICES = [
(PUBLIC, "Public"),
(COACH, "Coach"),
(PRIVATE, "Private"),
] # Choices for visibility level
]
visibility = models.CharField(
max_length=2, choices=VISIBILITY_CHOICES, default=COACH
)
class Meta:
"""Orders the object by date descendingly"""
ordering = ["-date"]
def __str__(self):
return self.name
return str(self.name)
class Exercise(models.Model):
......@@ -90,7 +77,7 @@ class Exercise(models.Model):
unit = models.CharField(max_length=50)
def __str__(self):
return self.name
return str(self.name)
class ExerciseInstance(models.Model):
......@@ -139,7 +126,8 @@ class WorkoutFile(models.Model):
file: The actual file that's being uploaded
"""
workout = models.ForeignKey(Workout, on_delete=models.CASCADE, related_name="files")
workout = models.ForeignKey(
Workout, on_delete=models.CASCADE, related_name="files")
owner = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, related_name="workout_files"
)
......@@ -156,4 +144,4 @@ class RememberMe(models.Model):
remember_me = models.CharField(max_length=500)
def __str__(self):
return self.remember_me
return str(self.remember_me)
......@@ -19,6 +19,7 @@ class ExerciseInstanceSerializer(serializers.HyperlinkedModelSerializer):
)
class Meta:
"""serialized fields for the model ExerciseInstance"""
model = ExerciseInstance
fields = ["url", "id", "exercise", "sets", "number", "workout"]
......@@ -39,6 +40,7 @@ class WorkoutFileSerializer(serializers.HyperlinkedModelSerializer):
)
class Meta:
"""serialized fields for the model WorkoutFile"""
model = WorkoutFile
fields = ["url", "id", "owner", "file", "workout"]
......@@ -66,6 +68,7 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer):
files = WorkoutFileSerializer(many=True, required=False)
class Meta:
"""serialized fields for the Workout. Owner is read only and cant be modified"""
model = Workout
fields = [
"url",
......@@ -101,10 +104,12 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer):
workout = Workout.objects.create(**validated_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:
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
......@@ -122,67 +127,17 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer):
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.visibility = validated_data.get(
"visibility", instance.visibility)
instance.date = validated_data.get("date", instance.date)
instance.save()
# Handle ExerciseInstances
handle_exercise_instance(validated_data, instance)
# 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_instance.exercise = exercise_instance_data.get(
"exercise", exercise_instance.exercise
)
exercise_instance.number = exercise_instance_data.get(
"number", exercise_instance.number
)
exercise_instance.sets = exercise_instance_data.get(
"sets", exercise_instance.sets
)
exercise_instance.save()
# 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
)
# 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()
# Handle WorkoutFiles
if "files" in validated_data:
files_data = validated_data.pop("files")
files = instance.files
for file, file_data in zip(files.all(), files_data):
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"),
)
# 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()
handle_workout_files(validated_data, instance)
return instance
......@@ -198,6 +153,64 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer):
return obj.owner.username
def handle_exercise_instance(validated_data, instance):
"""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))
"""
exercise_instances_data = validated_data.pop("exercise_instances")
exercise_instances = instance.exercise_instances
for exercise_instance, exercise_instance_data in zip(
exercise_instances.all(), exercise_instances_data
):
exercise_instance.exercise = exercise_instance_data.get(
"exercise", exercise_instance.exercise
)
exercise_instance.number = exercise_instance_data.get(
"number", exercise_instance.number
)
exercise_instance.sets = exercise_instance_data.get(
"sets", exercise_instance.sets
)
exercise_instance.save()
# 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
)
# 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()
def handle_workout_files(validated_data, instance):
"""Handles the updating of exercises in a workout instance."""
if "files" in validated_data:
files_data = validated_data.pop("files")
files = instance.files
for file, file_data in zip(files.all(), files_data):
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"),
)
# 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()
class ExerciseSerializer(serializers.HyperlinkedModelSerializer):
"""Serializer for an Exercise. Hyperlinks are used for relationships by default.
......@@ -212,8 +225,18 @@ class ExerciseSerializer(serializers.HyperlinkedModelSerializer):
)
class Meta:
"""serialized fields for the model Exercise"""
model = Exercise
fields = ["url", "id", "name", "description", "duration", "calories", "muscleGroup", "unit", "instances"]
fields = [
"url",
"id",
"name",
"description",
"duration",
"calories",
"muscleGroup",
"unit",
"instances"]
class RememberMeSerializer(serializers.HyperlinkedModelSerializer):
......@@ -226,5 +249,6 @@ class RememberMeSerializer(serializers.HyperlinkedModelSerializer):
"""
class Meta:
"""serialized fields for the model RememberMe"""
model = RememberMe
fields = ["remember_me"]
"""Contains views for the workouts application. These are mostly class-based views.
"""
from rest_framework import generics, mixins
from rest_framework import permissions
from rest_framework.parsers import (
JSONParser,
)
import base64
import pickle
from collections import namedtuple
from rest_framework import generics, mixins, permissions, 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 django.core.exceptions import PermissionDenied
from django.core.signing import Signer
from workouts.parsers import MultipartJsonParser
from workouts.permissions import (
IsOwner,
......@@ -23,20 +23,19 @@ from workouts.permissions import (
)
from workouts.mixins import CreateListModelMixin
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 workouts.serializers import (
WorkoutSerializer,
ExerciseSerializer,
RememberMeSerializer,
ExerciseInstanceSerializer,
WorkoutFileSerializer
)
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):
"""The API root view"""
return Response(
{
"users": reverse("user-list", request=request, format=format),
......@@ -54,13 +53,16 @@ def api_root(request, format=None):
)
# Allow users to save a persistent session in their browser
class RememberMe(
mixins.ListModelMixin,
mixins.CreateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView,
):
"""Allow users to save a persistent session in their browser.
HTTP methods: GET, POST
"""
serializer_class = RememberMeSerializer
......@@ -68,13 +70,13 @@ class RememberMe(
if request.user.is_authenticated == False:
raise PermissionDenied
else:
return Response({"remember_me": self.rememberme()})
return Response({"remember_me": self.remember_me()})
def post(self, request):
cookieObject = namedtuple("Cookies", request.COOKIES.keys())(
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,15 +85,15 @@ class RememberMe(
}
)
def get_user(self, cookieObject):
decode = base64.b64decode(cookieObject.remember_me)
def get_user(self, cookie_object):
decode = base64.b64decode(cookie_object.remember_me)
user, sign = pickle.loads(decode)
# Validate signature
if sign == self.sign_user(user):
return user
def rememberme(self):
def remember_me(self):
creds = [self.request.user, self.sign_user(str(self.request.user))]
return base64.b64encode(pickle.dumps(creds))
......@@ -111,13 +113,15 @@ class WorkoutList(
"""
serializer_class = WorkoutSerializer
# User must be authenticated to create/view workouts
permission_classes = [
permissions.IsAuthenticated
] # User must be authenticated to create/view workouts
]
# For parsing JSON and Multi-part requests
parser_classes = [
MultipartJsonParser,
JSONParser,
] # For parsing JSON and Multi-part requests
]
filter_backends = [filters.OrderingFilter]
ordering_fields = ["name", "date", "owner__username"]
......@@ -131,18 +135,18 @@ class WorkoutList(
serializer.save(owner=self.request.user)
def get_queryset(self):
qs = Workout.objects.none()
queryset = 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(
queryset = Workout.objects.filter(
Q(visibility="PU")
| (Q(visibility="CO") & Q(owner__coach=self.request.user))
).distinct()
return qs
return queryset
class WorkoutDetail(
......@@ -228,7 +232,10 @@ class ExerciseInstanceList(
CreateListModelMixin,
generics.GenericAPIView,
):
"""Class defining the web response for the creation"""
"""Class defining the web response for the creation
HTTP methods: GET, POST
"""
serializer_class = ExerciseInstanceSerializer
permission_classes = [permissions.IsAuthenticated & IsOwnerOfWorkout]
......@@ -259,6 +266,12 @@ class ExerciseInstanceDetail(
mixins.DestroyModelMixin,
generics.GenericAPIView,
):
"""Class defining the web response for the modification, deletion and getting
exercise instance details.
HTTP methods: GET, PUT, PATCH, DELETE
"""
serializer_class = ExerciseInstanceSerializer
permission_classes = [
permissions.IsAuthenticated
......@@ -287,6 +300,10 @@ class WorkoutFileList(
CreateListModelMixin,
generics.GenericAPIView,
):
"""Class defining the web response for the getting and creating list for workout files.
HTTP methods: GET, POST
"""
queryset = WorkoutFile.objects.all()
serializer_class = WorkoutFileSerializer
......@@ -323,6 +340,10 @@ class WorkoutFileDetail(
mixins.DestroyModelMixin,
generics.GenericAPIView,
):
"""Class defining the web response for the getting deleting workout file details.
HTTP methods: GET, POST
"""
queryset = WorkoutFile.objects.all()
serializer_class = WorkoutFileSerializer
......
......@@ -31,49 +31,57 @@ class MuscleGroup {
}
}
function deleteFormData() {
setReadOnly(true, "#form-exercise");
okButton.className += " hide";
deleteButton.className += " hide";
cancelButton.className += " hide";
editButton.className = editButton.className.replace(" hide", "");
cancelButton.removeEventListener("click", handleCancelButtonDuringEdit);
oldFormData.delete("name");
oldFormData.delete("description");
oldFormData.delete("duration");
oldFormData.delete("calories");
oldFormData.delete("muscleGroup");
oldFormData.delete("unit");
}
function handleCancelButtonDuringEdit() {
setReadOnly(true, "#form-exercise");
document.querySelector("select").setAttribute("disabled", "")
okButton.className += " hide";
deleteButton.className += " hide";
cancelButton.className += " hide";
editButton.className = editButton.className.replace(" hide", "");
cancelButton.removeEventListener("click", handleCancelButtonDuringEdit);
let form = document.querySelector("#form-exercise");
if (oldFormData.has("name")) form.name.value = oldFormData.get("name");
if (oldFormData.has("description")) form.description.value = oldFormData.get("description");
if (oldFormData.has("duration")) form.duration.value = oldFormData.get("duration");
if (oldFormData.has("calories")) form.calories.value = oldFormData.get("calories");
if (oldFormData.has("muscleGroup")) form.muscleGroup.value = oldFormData.get("muscleGroup");
if (oldFormData.has("unit")) form.unit.value = oldFormData.get("unit");
oldFormData.delete("name");
oldFormData.delete("description");
oldFormData.delete("duration");
oldFormData.delete("calories");
oldFormData.delete("muscleGroup");
oldFormData.delete("unit");
deleteFormData();
}
function handleCancelButtonDuringCreate() {
window.location.replace("exercises.html");
}
function addFormData(form) {
let formData = new FormData(form);
let selectedMuscleGroup = new MuscleGroup(formData.get("muscleGroup"));
let updatedMuscleGroup = selectedMuscleGroup.getMuscleGroupType()
return {"name": formData.get("name"),
"description": formData.get("description"),
"duration": formData.get("duration"),
"calories": formData.get("calories"),
"muscleGroup": updatedMuscleGroup ? updatedMuscleGroup : formData.get("muscleGroup"),
"unit": formData.get("unit")};
}
async function createExercise() {
document.querySelector("select").removeAttribute("disabled")
let form = document.querySelector("#form-exercise");
let formData = new FormData(form);
let body = {"name": formData.get("name"),
"description": formData.get("description"),
"duration": formData.get("duration"),
"calories": formData.get("calories"),
"muscleGroup": formData.get("muscleGroup"),
"unit": formData.get("unit")};
let response = await sendRequest("POST", `${HOST}/api/exercises/`, body);
let response = await sendRequest("POST", `${HOST}/api/exercises/`, addFormData(form));
if (response.ok) {
window.location.replace("exercises.html");
......@@ -139,20 +147,10 @@ async function retrieveExercise(id) {
async function updateExercise(id) {
let form = document.querySelector("#form-exercise");
let formData = new FormData(form);
let muscleGroupSelector = document.querySelector("select")
muscleGroupSelector.removeAttribute("disabled")
let selectedMuscleGroup = new MuscleGroup(formData.get("muscleGroup"));
let body = {"name": formData.get("name"),
"description": formData.get("description"),
"duration": formData.get("duration"),
"calories": formData.get("calories"),
"muscleGroup": selectedMuscleGroup.getMuscleGroupType(),
"unit": formData.get("unit")};
let response = await sendRequest("PUT", `${HOST}/api/exercises/${id}/`, body);
let response = await sendRequest("PUT", `${HOST}/api/exercises/${id}/`, addFormData(form));
if (!response.ok) {
let data = await response.json();
......@@ -160,22 +158,7 @@ async function updateExercise(id) {
document.body.prepend(alert);
} else {
muscleGroupSelector.setAttribute("disabled", "")
// duplicate code from handleCancelButtonDuringEdit
// you should refactor this
setReadOnly(true, "#form-exercise");
okButton.className += " hide";
deleteButton.className += " hide";
cancelButton.className += " hide";
editButton.className = editButton.className.replace(" hide", "");
cancelButton.removeEventListener("click", handleCancelButtonDuringEdit);
oldFormData.delete("name");
oldFormData.delete("description");
oldFormData.delete("duration");
oldFormData.delete("calories");
oldFormData.delete("muscleGroup");
oldFormData.delete("unit");
deleteFormData()
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment