Commit a526e5cb authored by Pernille Nødtvedt Welle-Watne's avatar Pernille Nødtvedt Welle-Watne
Browse files

Merge branch '2-model-for-planned-workout' into 'master'

Add planned workout

Closes #2

See merge request !2
parents c478f097 f8768ddb
Pipeline #113021 failed with stages
in 46 seconds
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -9,3 +9,4 @@ admin.site.register(Exercise)
admin.site.register(ExerciseInstance)
admin.site.register(Workout)
admin.site.register(WorkoutFile)
# Generated by Django 3.1 on 2021-02-27 12:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('workouts', '0003_rememberme'),
]
operations = [
migrations.AddField(
model_name='workout',
name='planned',
field=models.BooleanField(default=False),
),
]
......@@ -38,6 +38,7 @@ class Workout(models.Model):
notes: Notes about the workout
owner: User that logged the workout
visibility: The visibility level of the workout: Public, Coach, or Private
planned: Indicates if it is a planned workout
"""
name = models.CharField(max_length=100)
......@@ -46,6 +47,7 @@ class Workout(models.Model):
owner = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, related_name="workouts"
)
planned = models.BooleanField(default=False)
# Visibility levels
PUBLIC = "PU" # Visible to all authenticated users
......@@ -67,7 +69,6 @@ class Workout(models.Model):
def __str__(self):
return self.name
class Exercise(models.Model):
"""Django model for an exercise type that users can create.
......
......@@ -3,6 +3,8 @@
from rest_framework import serializers
from rest_framework.serializers import HyperlinkedRelatedField
from workouts.models import Workout, Exercise, ExerciseInstance, WorkoutFile, RememberMe
from datetime import datetime
import pytz
class ExerciseInstanceSerializer(serializers.HyperlinkedModelSerializer):
......@@ -52,7 +54,7 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer):
This serializer specifies nested serialization since a workout consists of WorkoutFiles
and ExerciseInstances.
Serialized fields: url, id, name, date, notes, owner, owner_username, visiblity,
Serialized fields: url, id, name, date, notes, owner, planned, owner_username, visiblity,
exercise_instances, files
Attributes:
......@@ -74,6 +76,7 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer):
"date",
"notes",
"owner",
"planned",
"owner_username",
"visibility",
"exercise_instances",
......@@ -93,6 +96,19 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer):
Returns:
Workout: A newly created Workout
"""
# Check if date is valid
timeNow = datetime.now()
timeNowAdjusted = pytz.utc.localize(timeNow)
if validated_data["planned"]:
if timeNowAdjusted >= validated_data["date"]:
raise serializers.ValidationError(
{"date": ["Date must be a future date"]})
else:
if timeNowAdjusted <= validated_data["date"]:
raise serializers.ValidationError(
{"date": ["Date must be an old date"]})
exercise_instances_data = validated_data.pop("exercise_instances")
files_data = []
if "files" in validated_data:
......@@ -101,10 +117,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,12 +140,27 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer):
Returns:
Workout: Updated Workout instance
"""
# Add date and planned check
# Check if date is valid
timeNow = datetime.now()
timeNowAdjusted = pytz.utc.localize(timeNow)
if validated_data["planned"]:
if timeNowAdjusted >= validated_data["date"]:
raise serializers.ValidationError(
{"date": ["Date must be a future date"]})
else:
if timeNowAdjusted <= validated_data["date"]:
raise serializers.ValidationError(
{"date": ["Date must be an old date"]})
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()
......
......@@ -31,8 +31,11 @@ from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.response import Response
import json
from collections import namedtuple
import base64, pickle
import base64
import pickle
from django.core.signing import Signer
from datetime import datetime
import pytz
@api_view(["GET"])
......@@ -141,6 +144,16 @@ class WorkoutList(
Q(visibility="PU")
| (Q(visibility="CO") & Q(owner__coach=self.request.user))
).distinct()
# Check if the planned workout has happened
if len(qs) > 0:
timeNow = datetime.now()
timeNowAdjusted = pytz.utc.localize(timeNow)
for i in range(0, len(qs)):
if qs[i].planned:
if timeNowAdjusted > qs[i].date:
# Update: set planned to false
qs[i].planned = False
qs[i].save()
return qs
......@@ -155,7 +168,6 @@ class WorkoutDetail(
HTTP methods: GET, PUT, DELETE
"""
queryset = Workout.objects.all()
serializer_class = WorkoutSerializer
permission_classes = [
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workout</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<script src="https://kit.fontawesome.com/0ce6c392ca.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="styles/style.css">
<script src="scripts/navbar.js" type="text/javascript" defer></script>
</head>
<body>
<navbar-el></navbar-el>
<div class="container">
<div class="row">
<div class="col-lg">
<h3 class="mt-3">View/Edit Planned Workout</h3>
</div>
</div>
<div class="row">
<div class="col-lg">
<p class="mt-4">A planned workout is a future workout that will be autologged</h3>
</div>
</div>
<form class="row g-3 mb-4" id="form-workout">
<div class="col-lg-6 ">
<label for="inputName" class="form-label">Name</label>
<input type="text" class="form-control" id="inputName" name="name" readonly>
</div>
<div class="col-lg-6"></div>
<div class="col-lg-6">
<label for="inputDateTime" class="form-label">Date/Time</label>
<input type="datetime-local" class="form-control" id="inputDateTime" name="date" readonly>
</div>
<div class="col-lg-6"></div>
<div class="col-lg-6">
<label for="inputOwner" class="form-label">Owner</label>
<input type="text" class="form-control" id="inputOwner" name="owner_username" readonly>
</div>
<div class="col-lg-6">
<label for="inputVisibility" class="form-label">Visibility</label>
<select id="inputVisibility" class="form-select" name="visibility" disabled>
<option value="PU">Public</option>
<option value="CO">Coach</option>
<option value="PR">Private</option>
</select>
</div>
<div class="col-lg-6">
<label for="inputNotes" class="form-label">Notes</label>
<textarea class="form-control" id="inputNotes" name="notes" readonly></textarea>
</div>
<div class="col-lg-6"></div>
<div class="col-lg-6">
<div class="input-group">
<input type="file" class="form-control" id="customFile" name="files" multiple disabled>
</div>
<div id="uploaded-files" class="ms-1 mt-2">
</div>
</div>
<div class="col-lg-6">
</div>
<div class="col-lg-6">
<input type="button" class="btn btn-primary hide" id="btn-ok-workout" value=" OK ">
<input type="button" class="btn btn-primary hide" id="btn-edit-workout" value=" Edit ">
<input type="button" class="btn btn-secondary hide" id="btn-cancel-workout" value="Cancel">
<input type="button" class="btn btn-danger float-end hide" id="btn-delete-workout" value="Delete">
</div>
<div class="col-lg-6"></div>
<div class="col-lg-12">
<h3 class="mt-3">Exercises</h3>
</div>
<div id="div-exercises" class="col-lg-12">
</div>
<div class="col-lg-6">
<input type="button" class="btn btn-primary hide" id="btn-add-exercise" value="Add exercise">
<input type="button" class="btn btn-danger hide" id="btn-remove-exercise" value="Remove exercise">
</div>
<div class="col-lg-6"></div>
</form>
<div class="row bootstrap snippets bootdeys" id="div-comment-row">
<div class="col-md-8 col-sm-12">
<div class="comment-wrapper">
<div class="card">
<div class="card-header bg-primary text-light">
Comment panel
</div>
<div class="card-body">
<textarea class="form-control" id="comment-area" placeholder="write a comment..." rows="3"></textarea>
<br>
<button type="button" id="post-comment" class="btn btn-info pull-right">Post</button>
<div class="clearfix"></div>
<hr>
<ul id="comment-list" class="list-unstyled">
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<template id="template-exercise">
<div class="row div-exercise-container g-3 mb-3">
<div class="col-lg-6"><h5>Exercise</h5></div>
<div class="col-lg-6"></div>
<div class="col-lg-6">
<label class="form-label exercise-type">Type</label>
<select class="form-select" name="type">
</select>
</div>
<div class="col-lg-6"></div>
<div class="col-lg-3">
<label class="form-label exercise-sets">Sets</label>
<input type="number" class="form-control" name="sets">
</div>
<div class="col-lg-3">
<label class="form-label exercise-number">Number</label>
<input type="number" class="form-control" name="number">
</div>
<div class="col-lg-6"></div>
</div>
</template>
<script src="scripts/defaults.js"></script>
<script src="scripts/scripts.js"></script>
<script src="scripts/plannedWorkout.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
</body>
</html>
\ No newline at end of file
let cancelWorkoutButton;
let okWorkoutButton;
let deleteWorkoutButton;
let editWorkoutButton;
let postCommentButton;
async function retrieveWorkout(id) {
let workoutData = null;
let response = await sendRequest("GET", `${HOST}/api/workouts/${id}/`);
if (!response.ok) {
let data = await response.json();
let alert = createAlert("Could not retrieve workout data!", data);
document.body.prepend(alert);
} else {
workoutData = await response.json();
let form = document.querySelector("#form-workout");
let formData = new FormData(form);
for (let key of formData.keys()) {
let selector = `input[name="${key}"], textarea[name="${key}"]`;
let input = form.querySelector(selector);
let newVal = workoutData[key];
if (key == "date") {
// Creating a valid datetime-local string with the correct local time
let date = new Date(newVal);
date = new Date(
date.getTime() - date.getTimezoneOffset() * 60 * 1000
).toISOString(); // get ISO format for local time
newVal = date.substring(0, newVal.length - 1); // remove Z (since this is a local time, not UTC)
}
if (key != "files") {
input.value = newVal;
}
}
let input = form.querySelector("select:disabled");
input.value = workoutData["visibility"];
// files
let filesDiv = document.querySelector("#uploaded-files");
for (let file of workoutData.files) {
let a = document.createElement("a");
a.href = file.file;
let pathArray = file.file.split("/");
a.text = pathArray[pathArray.length - 1];
a.className = "me-2";
filesDiv.appendChild(a);
}
// create exercises
// fetch exercise types
let exerciseTypeResponse = await sendRequest(
"GET",
`${HOST}/api/exercises/`
);
let exerciseTypes = await exerciseTypeResponse.json();
//TODO: This should be in its own method.
for (let i = 0; i < workoutData.exercise_instances.length; i++) {
let templateExercise = document.querySelector("#template-exercise");
let divExerciseContainer = templateExercise.content.firstElementChild.cloneNode(
true
);
let exerciseTypeLabel = divExerciseContainer.querySelector(
".exercise-type"
);
exerciseTypeLabel.for = `inputExerciseType${i}`;
let exerciseTypeSelect = divExerciseContainer.querySelector("select");
exerciseTypeSelect.id = `inputExerciseType${i}`;
exerciseTypeSelect.disabled = true;
let splitUrl = workoutData.exercise_instances[i].exercise.split("/");
let currentExerciseTypeId = splitUrl[splitUrl.length - 2];
let currentExerciseType = "";
for (let j = 0; j < exerciseTypes.count; j++) {
let option = document.createElement("option");
option.value = exerciseTypes.results[j].id;
if (currentExerciseTypeId == exerciseTypes.results[j].id) {
currentExerciseType = exerciseTypes.results[j];
}
option.innerText = exerciseTypes.results[j].name;
exerciseTypeSelect.append(option);
}
exerciseTypeSelect.value = currentExerciseType.id;
let exerciseSetLabel = divExerciseContainer.querySelector(
".exercise-sets"
);
exerciseSetLabel.for = `inputSets${i}`;
let exerciseSetInput = divExerciseContainer.querySelector(
"input[name='sets']"
);
exerciseSetInput.id = `inputSets${i}`;
exerciseSetInput.value = workoutData.exercise_instances[i].sets;
exerciseSetInput.readOnly = true;
let exerciseNumberLabel = divExerciseContainer.querySelector(
".exercise-number"
);
(exerciseNumberLabel.for = "for"), `inputNumber${i}`;
exerciseNumberLabel.innerText = currentExerciseType.unit;
let exerciseNumberInput = divExerciseContainer.querySelector(
"input[name='number']"
);
exerciseNumberInput.id = `inputNumber${i}`;
exerciseNumberInput.value = workoutData.exercise_instances[i].number;
exerciseNumberInput.readOnly = true;
let exercisesDiv = document.querySelector("#div-exercises");
exercisesDiv.appendChild(divExerciseContainer);
}
}
return workoutData;
}
function handleCancelDuringWorkoutEdit() {
location.reload();
}
function handleEditWorkoutButtonClick() {
let addExerciseButton = document.querySelector("#btn-add-exercise");
let removeExerciseButton = document.querySelector("#btn-remove-exercise");
setReadOnly(false, "#form-workout");
document.querySelector("#inputOwner").readOnly = true; // owner field should still be readonly
editWorkoutButton.className += " hide";
okWorkoutButton.className = okWorkoutButton.className.replace(" hide", "");
cancelWorkoutButton.className = cancelWorkoutButton.className.replace(
" hide",
""
);
deleteWorkoutButton.className = deleteWorkoutButton.className.replace(
" hide",
""
);
addExerciseButton.className = addExerciseButton.className.replace(
" hide",
""
);
removeExerciseButton.className = removeExerciseButton.className.replace(
" hide",
""
);
cancelWorkoutButton.addEventListener("click", handleCancelDuringWorkoutEdit);
}
async function deleteWorkout(id) {
let response = await sendRequest("DELETE", `${HOST}/api/workouts/${id}/`);
if (!response.ok) {
let data = await response.json();
let alert = createAlert(`Could not delete workout ${id}!`, data);
document.body.prepend(alert);
} else {
window.location.replace("workouts.html");
}
}
async function updateWorkout(id) {
let submitForm = generateWorkoutForm();
let response = await sendRequest(
"PUT",
`${HOST}/api/workouts/${id}/`,
submitForm,
""
);
if (!response.ok) {
let data = await response.json();
let alert = createAlert("Could not update workout!", data);
document.body.prepend(alert);
} else {
location.reload();
}
}
function generateWorkoutForm() {
// TODO: Add check for future date
var today = new Date().toISOString();
document.querySelector("#inputDateTime").min = today;
let form = document.querySelector("#form-workout");
let formData = new FormData(form);
let submitForm = new FormData();
submitForm.append("name", formData.get("name"));
let date = new Date(formData.get("date")).toISOString();
submitForm.append("date", date);
submitForm.append("notes", formData.get("notes"));
submitForm.append("visibility", formData.get("visibility"));
submitForm.append("planned", true);
// adding exercise instances
let exerciseInstances = [];
let exerciseInstancesTypes = formData.getAll("type");
let exerciseInstancesSets = formData.getAll("sets");
let exerciseInstancesNumbers = formData.getAll("number");
for (let i = 0; i < exerciseInstancesTypes.length; i++) {
exerciseInstances.push({
exercise: `${HOST}/api/exercises/${exerciseInstancesTypes[i]}/`,
number: exerciseInstancesNumbers[i],
sets: exerciseInstancesSets[i],
});
}
submitForm.append("exercise_instances", JSON.stringify(exerciseInstances));
// adding files
for (let file of formData.getAll("files")) {
submitForm.append("files", file);
}
return submitForm;
}
async function createWorkout() {
let submitForm = generateWorkoutForm();
let response = await sendRequest(
"POST",
`${HOST}/api/workouts/`,
submitForm,
""
);
if (response.ok) {
window.location.replace("workouts.html");
} else {
let data = await response.json();
let alert = createAlert("Could not create new workout!", data);
document.body.prepend(alert);
}
}
function handleCancelDuringWorkoutCreate() {
window.location.replace("workouts.html");
}
async function createBlankExercise() {
let form = document.querySelector("#form-workout");
let exerciseTypeResponse = await sendRequest("GET", `${HOST}/api/exercises/`);
let exerciseTypes = await exerciseTypeResponse.json();
let exerciseTemplate = document.querySelector("#template-exercise");
let divExerciseContainer = exerciseTemplate.content.firstElementChild.cloneNode(
true
);
let exerciseTypeSelect = divExerciseContainer.querySelector("select");
for (let i = 0; i < exerciseTypes.count; i++) {
let option = document.createElement("option");
option.value = exerciseTypes.results[i].id;
option.innerText = exerciseTypes.results[i].name;
exerciseTypeSelect.append(option);
}
let currentExerciseType = exerciseTypes.results[0];
exerciseTypeSelect.value = currentExerciseType.name;
let divExercises = document.querySelector("#div-exercises");
divExercises.appendChild(divExerciseContainer);
}
function removeExercise(event) {
let divExerciseContainers = document.querySelectorAll(
".div-exercise-container"
);
if (divExerciseContainers && divExerciseContainers.length > 0) {
divExerciseContainers[divExerciseContainers.length - 1].remove();
}
}
function addComment(author, text, date, append) {
/* Taken from https://www.bootdey.com/snippets/view/Simple-Comment-panel#css*/
let commentList = document.querySelector("#comment-list");
let listElement = document.createElement("li");