diff --git a/backend/secfit/workouts/admin.py b/backend/secfit/workouts/admin.py index cb43794b85492adcb933dc4e46f875029dc411cf..43a554dba59b88f819bafadc17c83607b453c851 100644 --- a/backend/secfit/workouts/admin.py +++ b/backend/secfit/workouts/admin.py @@ -3,9 +3,10 @@ from django.contrib import admin # Register your models here. -from .models import Exercise, ExerciseInstance, Workout, WorkoutFile +from .models import Exercise, ExerciseInstance, Workout, WorkoutFile, Statistics admin.site.register(Exercise) admin.site.register(ExerciseInstance) admin.site.register(Workout) admin.site.register(WorkoutFile) +admin.site.register(Statistics) diff --git a/backend/secfit/workouts/migrations/0005_statistics.py b/backend/secfit/workouts/migrations/0005_statistics.py new file mode 100644 index 0000000000000000000000000000000000000000..b991640083bd8b1ce8968fb2a34bc917fc8cd9e9 --- /dev/null +++ b/backend/secfit/workouts/migrations/0005_statistics.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1 on 2022-03-14 14:47 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('workouts', '0004_auto_20211020_0950'), + ] + + operations = [ + migrations.CreateModel( + name='Statistics', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField()), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='statistics', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['date'], + }, + ), + ] diff --git a/backend/secfit/workouts/models.py b/backend/secfit/workouts/models.py index 0f6214f2d9919d17fe68e93416a183a52209bda6..745881aa42608079ba7cc55ca3f0640b69370cb4 100644 --- a/backend/secfit/workouts/models.py +++ b/backend/secfit/workouts/models.py @@ -157,3 +157,16 @@ class RememberMe(models.Model): def __str__(self): return self.remember_me + +class Statistics (models.Model): + ''' + date: timestamp for when the workout was completed + ower: who completed the workout + ''' + date = models.DateTimeField() + owner = models.ForeignKey( + get_user_model(), on_delete=models.CASCADE, related_name="statistics" + ) + + class Meta: + ordering = ['date'] diff --git a/backend/secfit/workouts/serializers.py b/backend/secfit/workouts/serializers.py index 6abbe31ffd71c5e9cbba140e34a03176b127a4bf..19259b50811a1a2f85cd6aced4b593eb3b998a22 100644 --- a/backend/secfit/workouts/serializers.py +++ b/backend/secfit/workouts/serializers.py @@ -2,7 +2,7 @@ """ from rest_framework import serializers from rest_framework.serializers import HyperlinkedRelatedField -from workouts.models import Workout, Exercise, ExerciseInstance, WorkoutFile, RememberMe +from workouts.models import Workout, Exercise, ExerciseInstance, WorkoutFile, RememberMe, Statistics class ExerciseInstanceSerializer(serializers.HyperlinkedModelSerializer): @@ -228,3 +228,9 @@ class RememberMeSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = RememberMe fields = ["remember_me"] + +class StatisticsSerializer(serializers.HyperlinkedModelSerializer): + owner = serializers.ReadOnlyField(source='owner.username') + class Meta: + model = Statistics + fields = ["id", "owner", "date"] diff --git a/backend/secfit/workouts/urls.py b/backend/secfit/workouts/urls.py index 7c46a3f1ff311edc25dd455bb85780c1a1644738..0405fe0c1e7630f65731a3ff36ca7c224b33c213 100644 --- a/backend/secfit/workouts/urls.py +++ b/backend/secfit/workouts/urls.py @@ -42,6 +42,16 @@ urlpatterns = format_suffix_patterns( views.WorkoutFileDetail.as_view(), name="workoutfile-detail", ), + path( + "api/statistics/", + views.StatisticsView.as_view(), + name="statistics" + ), + path( + "api/statistics/<int:pk>/", + views.StatisticsView.as_view(), + name="statistics-detail" + ), path("", include("users.urls")), path("", include("comments.urls")), path("api/auth/", include("rest_framework.urls")), diff --git a/backend/secfit/workouts/views.py b/backend/secfit/workouts/views.py index efddf40454376b23d233f9fe2cecaf9da43fddb8..1b0505be4f13cef152db5b22fb62b55089d82fe0 100644 --- a/backend/secfit/workouts/views.py +++ b/backend/secfit/workouts/views.py @@ -22,10 +22,8 @@ from workouts.permissions import ( IsWorkoutPublic, ) 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 workouts.models import Workout, Exercise, ExerciseInstance, WorkoutFile, Statistics +from workouts.serializers import WorkoutSerializer, ExerciseSerializer, RememberMeSerializer, ExerciseInstanceSerializer, WorkoutFileSerializer, StatisticsSerializer from django.core.exceptions import PermissionDenied from rest_framework_simplejwt.tokens import RefreshToken from rest_framework.response import Response @@ -50,6 +48,7 @@ def api_root(request, format=None): ), "comments": reverse("comment-list", request=request, format=format), "likes": reverse("like-list", request=request, format=format), + "statistics": reverse("statistics", request=request, format=format) } ) @@ -340,3 +339,26 @@ class WorkoutFileDetail( def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) + +class StatisticsView( + mixins.ListModelMixin, + mixins.CreateModelMixin, + generics.GenericAPIView, + ): + serializer_class = StatisticsSerializer + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + queryset = Statistics.objects.none() + if self.request.user: + queryset = Statistics.objects.filter(owner=self.request.user) + return queryset + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) diff --git a/frontend/www/index.html b/frontend/www/index.html index c5b6141d19885a75a80442be41d24e22483894be..d45e12a1458032cb1c84231344d623c86fb5e857 100644 --- a/frontend/www/index.html +++ b/frontend/www/index.html @@ -13,7 +13,7 @@ <body> <navbar-el></navbar-el> - <div class="container"> + <div class="container d-none"> <div class="row mt-3"> <div class="col-lg text-center"> <h2 class="mt-3">Welcome to SecFit</h2> @@ -24,9 +24,26 @@ <img src="img/fitness.jpg" class="img-fluid" alt="DUMBBELLS"> </div> </div> + </div> + <h2 class="text-center">Your workout statistics!</h2> + <p class="text-center">Mark workouts as completed to keep track of how many workouts you complete!</p> + <div class="d-flex gap-2 justify-content-center"> + <span>Sort by: </span> + <div class="form-check"> + <input class="form-check-input" type="radio" name="sorting" id="week" checked> + <label class="form-check-label" for="flexRadioDefault1"> + Week + </label> + </div> + <div class="form-check"> + <input class="form-check-input" type="radio" name="sorting" id="month"> + <label class="form-check-label" for="flexRadioDefault2"> + Month + </label> + </div> </div> <div class="w-50 mx-auto"> - <canvas id="myChart" width="200" height="100"></canvas> + <canvas id="myChart"></canvas> </div> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> diff --git a/frontend/www/scripts/statistics.js b/frontend/www/scripts/statistics.js index 6634b3765950ea8da5b1fd1cc946b9b9143a9bd3..1bd09823795fd629b714ba879de3585fffc59a9c 100644 --- a/frontend/www/scripts/statistics.js +++ b/frontend/www/scripts/statistics.js @@ -1,23 +1,10 @@ let statistics +let graph +let myChart let weeklyWorkoutCount +let monthlyWorkoutCount +let currentSorting = "week" -window.addEventListener("DOMContentLoaded", async () => { - statistics = await getUserWorkoutStatistics(); - weeklyWorkoutCount = countWorkoutsByCurrentWeek(); -}) - -//Retrieves the completed workouts of a user -async function getUserWorkoutStatistics(){ - let response = await sendRequest('GET', `${HOST}/api/statistics/`); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } else { - let data = await response.json(); - return data.results; - } -} - -//create statistics chart const monthLabels = [ 'January', 'February', @@ -43,28 +30,73 @@ const monthLabels = [ 'Sunday' ] - const data = { - labels: weekdayLabels, - datasets: [{ - label: 'How many workouts you have completed', - backgroundColor: 'rgb(255, 99, 132)', - borderColor: 'rgb(255, 99, 132)', - data: weeklyWorkoutCount, - }] - }; +window.addEventListener("DOMContentLoaded", async () => { + statistics = await getUserWorkoutStatistics(); + weeklyWorkoutCount = countWorkoutsByCurrentWeek(); + monthlyWorkoutCount = countWorkoutsByCurrentMonth(); + createStatisticsChart(weeklyWorkoutCount); +}) + +var sortingButtons = document.querySelectorAll('input[name = "sorting"]'); +sortingButtons.forEach((radio) => { + radio.addEventListener("change", () => { + currentSorting = radio.id + if(currentSorting == "week"){ + updateGraph(myChart, weekdayLabels, weeklyWorkoutCount); + }else { + updateGraph(myChart, monthLabels, monthlyWorkoutCount); + } + }) +}) - const config = { - type: 'line', +function updateGraph(chart, labels, data) { + chart.data.datasets.pop(); + chart.data.datasets.push({ + label: 'How many workouts you have completed', data: data, - options: {} - }; + backgroundColor: 'rgb(255, 99, 132)', + borderColor: 'rgb(255, 99, 132)', + borderWidth: 1 + }); + chart.data.labels = []; + chart.data.labels = labels; + chart.update(); +} - const myChart = new Chart( - document.getElementById('myChart'), - config - ); - //count workouts (by week/ month) +//Retrieves the completed workouts of a user +async function getUserWorkoutStatistics(){ + let response = await sendRequest('GET', `${HOST}/api/statistics/`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } else { + let data = await response.json(); + return data.results; + } +} + +//create statistics chart + function createStatisticsChart(data) { + graph = document.getElementById('myChart'); + myChart = new Chart(graph, { + type: 'line', + data: { + labels: weekdayLabels, + datasets: [{ + label: 'How many workouts you have completed', + data: data, + backgroundColor: 'rgb(255, 99, 132)', + borderColor: 'rgb(255, 99, 132)', + borderWidth: 1 + }] + }, + options: { + } + }); + return myChart + } + + //count workouts (by week) function countWorkoutsByCurrentWeek() { let workoutCount = [0, 0, 0, 0, 0, 0, 0]; const currentWeek = getWeek(new Date()) @@ -80,6 +112,20 @@ const monthLabels = [ return workoutCount; } + function countWorkoutsByCurrentMonth() { + let workoutCount = new Array(12).fill(0); + const currentYear = new Date().getFullYear() + statistics.forEach((entity) => { + const workoutDate = new Date(entity.date); + const yearCompleted = workoutDate.getFullYear(); + if(yearCompleted == currentYear){ + const monthCompleted = workoutDate.getMonth(); + workoutCount[monthCompleted] += 1 + } + }) + return workoutCount + } + // Source: https://weeknumber.com/how-to/javascript // Returns the ISO week of the date. function getWeek(date) { @@ -92,4 +138,4 @@ function getWeek(date) { // Adjust to Thursday in week 1 and count number of weeks from date to week1. return 1 + Math.round(((newdate.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7); -} \ No newline at end of file +}