Skip to content
Snippets Groups Projects
Commit a5270963 authored by Tor Martin Frøberg Wang's avatar Tor Martin Frøberg Wang
Browse files

Merge branch 'leaderboards' into 'dev'

Leaderboards

See merge request !1
parents d9704ef3 74583c36
No related branches found
No related tags found
3 merge requests!5Ad ci/cs setup,!4Dev,!1Leaderboards
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (venv)" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/tdt4242-base.iml" filepath="$PROJECT_DIR$/.idea/tdt4242-base.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
</component>
</module>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
\ No newline at end of file
This diff is collapsed.
......@@ -136,8 +136,12 @@ MEDIA_URL = "/media/"
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
#"DEFAULT_AUTHENTICATION_CLASSES": (
# "rest_framework_simplejwt.authentication.JWTAuthentication",
#),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
"rest_framework_simplejwt.authentication.JWTAuthentication"
),
}
......
......@@ -27,6 +27,11 @@ urlpatterns = format_suffix_patterns(
views.ExerciseInstanceList.as_view(),
name="exercise-instance-list",
),
path(
"api/leaderboards/<int:pk>/",
views.Leaderboards.as_view(),
name="leaderboards",
),
path(
"api/exercise-instances/<int:pk>/",
views.ExerciseInstanceDetail.as_view(),
......
......@@ -6,10 +6,11 @@ from rest_framework import permissions
from rest_framework.parsers import (
JSONParser,
)
import json
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 django.db.models import Q, Sum, F, IntegerField
from rest_framework import filters
from workouts.parsers import MultipartJsonParser
from workouts.permissions import (
......@@ -34,6 +35,9 @@ from collections import namedtuple
import base64, pickle
from django.core.signing import Signer
from users.models import User
from rest_framework.views import APIView
@api_view(["GET"])
def api_root(request, format=None):
......@@ -204,7 +208,6 @@ class ExerciseDetail(
HTTP methods: GET, PUT, PATCH, DELETE
"""
queryset = Exercise.objects.all()
serializer_class = ExerciseSerializer
permission_classes = [permissions.IsAuthenticated]
......@@ -221,6 +224,43 @@ class ExerciseDetail(
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
class Leaderboards(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, pk):
# User must be logged in
if self.request.user:
leaderboardNumbers = ExerciseInstance.objects.filter(Q(exercise__pk=pk) & Q(workout__visibility='PU')).values('workout__owner__pk').annotate(amount=Sum(F("sets") * F("number"), output_field=IntegerField())).order_by('-amount')
leaderboardResult = []
# Iterates through the top 5 entries in the leaderboard and formats it correctly
for i in range(0, min(5, len(leaderboardNumbers))):
leaderboardResult.append({"name": User.objects.get(pk=leaderboardNumbers[i]['workout__owner__pk']).username, "value": leaderboardNumbers[i]['amount']})
# Applies the rank to the leaderboard entry; if two or more users have the score they get the same rank
if i > 0 and leaderboardNumbers[i-1]["amount"] == leaderboardNumbers[i]["amount"]:
leaderboardResult[i]["rank"] = leaderboardResult[i-1]["rank"]
else:
leaderboardResult[i]["rank"] = i+1
# Finds the user in the leaderboard list. If the user is not in the leaderboard list,
# the user is automatically given a score of 0 and the worst rank
currentLoggedInUser = self.request.user
for j in range(0, len(leaderboardNumbers)):
if leaderboardNumbers[j]['workout__owner__pk'] == currentLoggedInUser.pk:
leaderboardResult.append({"name": currentLoggedInUser.username, "value": leaderboardNumbers[j]["amount"], "rank": j+1})
break
else:
leaderboardResult.append({"name": currentLoggedInUser.username, "value": 0, "rank": len(leaderboardNumbers) + 1})
return Response(leaderboardResult)
class ExerciseInstanceList(
mixins.ListModelMixin,
......
......@@ -45,7 +45,15 @@
</div>
</form>
<div class="row">
<div class="col-lg">
<h3 class="mt-3">Leaderboard</h3>
</div>
</div>
<table id="leaderboardstable" class="table table-striped">
<tr><th>Rank<th>Username<th>Score
</table>
</div>
<script src="scripts/defaults.js"></script>
<script src="scripts/scripts.js"></script>
<script src="scripts/exercise.js"></script>
......
......@@ -4,151 +4,200 @@ let deleteButton;
let editButton;
let oldFormData;
function handleCancelButtonDuringEdit() {
setReadOnly(true, "#form-exercise");
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("unit")) form.unit.value = oldFormData.get("unit");
oldFormData.delete("name");
oldFormData.delete("description");
oldFormData.delete("unit");
setReadOnly(true, "#form-exercise");
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("unit")) form.unit.value = oldFormData.get("unit");
oldFormData.delete("name");
oldFormData.delete("description");
oldFormData.delete("unit");
}
function handleCancelButtonDuringCreate() {
window.location.replace("exercises.html");
window.location.replace("exercises.html");
}
async function createExercise() {
let form = document.querySelector("#form-exercise");
let formData = new FormData(form);
let body = {"name": formData.get("name"),
"description": formData.get("description"),
"unit": formData.get("unit")};
let response = await sendRequest("POST", `${HOST}/api/exercises/`, body);
if (response.ok) {
window.location.replace("exercises.html");
} else {
let data = await response.json();
let alert = createAlert("Could not create new exercise!", data);
document.body.prepend(alert);
}
let form = document.querySelector("#form-exercise");
let formData = new FormData(form);
let body = {
name: formData.get("name"),
description: formData.get("description"),
unit: formData.get("unit"),
};
let response = await sendRequest("POST", `${HOST}/api/exercises/`, body);
if (response.ok) {
window.location.replace("exercises.html");
} else {
let data = await response.json();
let alert = createAlert("Could not create new exercise!", data);
document.body.prepend(alert);
}
}
function handleEditExerciseButtonClick() {
setReadOnly(false, "#form-exercise");
setReadOnly(false, "#form-exercise");
editButton.className += " hide";
okButton.className = okButton.className.replace(" hide", "");
cancelButton.className = cancelButton.className.replace(" hide", "");
deleteButton.className = deleteButton.className.replace(" hide", "");
editButton.className += " hide";
okButton.className = okButton.className.replace(" hide", "");
cancelButton.className = cancelButton.className.replace(" hide", "");
deleteButton.className = deleteButton.className.replace(" hide", "");
cancelButton.addEventListener("click", handleCancelButtonDuringEdit);
cancelButton.addEventListener("click", handleCancelButtonDuringEdit);
let form = document.querySelector("#form-exercise");
oldFormData = new FormData(form);
let form = document.querySelector("#form-exercise");
oldFormData = new FormData(form);
}
async function deleteExercise(id) {
let response = await sendRequest("DELETE", `${HOST}/api/exercises/${id}/`);
if (!response.ok) {
let data = await response.json();
let alert = createAlert(`Could not delete exercise ${id}`, data);
document.body.prepend(alert);
} else {
window.location.replace("exercises.html");
}
let response = await sendRequest("DELETE", `${HOST}/api/exercises/${id}/`);
if (!response.ok) {
let data = await response.json();
let alert = createAlert(`Could not delete exercise ${id}`, data);
document.body.prepend(alert);
} else {
window.location.replace("exercises.html");
}
}
async function retrieveExercise(id) {
let response = await sendRequest("GET", `${HOST}/api/exercises/${id}/`);
console.log(response.ok);
if (!response.ok) {
let data = await response.json();
let alert = createAlert("Could not retrieve exercise data!", data);
document.body.prepend(alert);
} else {
let exerciseData = await response.json();
let form = document.querySelector("#form-exercise");
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 = exerciseData[key];
input.value = newVal;
}
let response = await sendRequest("GET", `${HOST}/api/exercises/${id}/`);
console.log(response.ok);
if (!response.ok) {
let data = await response.json();
let alert = createAlert("Could not retrieve exercise data!", data);
document.body.prepend(alert);
} else {
let exerciseData = await response.json();
let form = document.querySelector("#form-exercise");
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 = exerciseData[key];
input.value = newVal;
}
}
}
async function updateExercise(id) {
let form = document.querySelector("#form-exercise");
let formData = new FormData(form);
let body = {"name": formData.get("name"),
"description": formData.get("description"),
"unit": formData.get("unit")};
let response = await sendRequest("PUT", `${HOST}/api/exercises/${id}/`, body);
if (!response.ok) {
let data = await response.json();
let alert = createAlert(`Could not update exercise ${id}`, data);
document.body.prepend(alert);
} else {
// 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("unit");
let form = document.querySelector("#form-exercise");
let formData = new FormData(form);
let body = {
name: formData.get("name"),
description: formData.get("description"),
unit: formData.get("unit"),
};
let response = await sendRequest("PUT", `${HOST}/api/exercises/${id}/`, body);
if (!response.ok) {
let data = await response.json();
let alert = createAlert(`Could not update exercise ${id}`, data);
document.body.prepend(alert);
} else {
// 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("unit");
}
}
async function fetchLeaderBoards(id) {
// Fetches leaderboard data
let response = await sendRequest("GET", `${HOST}/api/leaderboards/${id}/`);
let data = await response.json();
if (response.ok) {
let table = document.getElementById("leaderboardstable");
let row, cell;
//The user's own score will always be placed last in the JSON response
let userIndex = data.length - 1;
for (let i = 0; i < data.length - 1; i++) {
row = table.insertRow();
cell = row.insertCell();
cell.textContent = data[i].rank;
cell = row.insertCell();
cell.textContent = data[i].name;
cell = row.insertCell();
cell.textContent = data[i].value;
}
//If the user is not in top 5, the users score will also be rendered
if (data[userIndex].rank > 5) {
row = table.insertRow();
cell = row.insertCell();
cell.textContent = data[userIndex].rank;
cell = row.insertCell();
cell.textContent = data[userIndex].name;
cell = row.insertCell();
cell.textContent = data[userIndex].value;
}
}
return data;
}
window.addEventListener("DOMContentLoaded", async () => {
cancelButton = document.querySelector("#btn-cancel-exercise");
okButton = document.querySelector("#btn-ok-exercise");
deleteButton = document.querySelector("#btn-delete-exercise");
editButton = document.querySelector("#btn-edit-exercise");
oldFormData = null;
const urlParams = new URLSearchParams(window.location.search);
// view/edit
if (urlParams.has('id')) {
const exerciseId = urlParams.get('id');
await retrieveExercise(exerciseId);
editButton.addEventListener("click", handleEditExerciseButtonClick);
deleteButton.addEventListener("click", (async (id) => await deleteExercise(id)).bind(undefined, exerciseId));
okButton.addEventListener("click", (async (id) => await updateExercise(id)).bind(undefined, exerciseId));
}
//create
else {
setReadOnly(false, "#form-exercise");
editButton.className += " hide";
okButton.className = okButton.className.replace(" hide", "");
cancelButton.className = cancelButton.className.replace(" hide", "");
okButton.addEventListener("click", async () => await createExercise());
cancelButton.addEventListener("click", handleCancelButtonDuringCreate);
}
});
\ No newline at end of file
cancelButton = document.querySelector("#btn-cancel-exercise");
okButton = document.querySelector("#btn-ok-exercise");
deleteButton = document.querySelector("#btn-delete-exercise");
editButton = document.querySelector("#btn-edit-exercise");
oldFormData = null;
const urlParams = new URLSearchParams(window.location.search);
// view/edit
if (urlParams.has("id")) {
const exerciseId = urlParams.get("id");
await retrieveExercise(exerciseId);
await fetchLeaderBoards(exerciseId);
editButton.addEventListener("click", handleEditExerciseButtonClick);
deleteButton.addEventListener(
"click",
(async (id) => await deleteExercise(id)).bind(undefined, exerciseId)
);
okButton.addEventListener(
"click",
(async (id) => await updateExercise(id)).bind(undefined, exerciseId)
);
}
//create
else {
setReadOnly(false, "#form-exercise");
editButton.className += " hide";
okButton.className = okButton.className.replace(" hide", "");
cancelButton.className = cancelButton.className.replace(" hide", "");
okButton.addEventListener("click", async () => await createExercise());
cancelButton.addEventListener("click", handleCancelButtonDuringCreate);
}
});
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