From a2684067cf30f15d6eeb59cf44d7cdd7ccda979d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigurd=20R=C3=B8stad=20Augdal?= <sigurdra@stud.ntnu.no> Date: Wed, 3 Mar 2021 21:31:49 +0100 Subject: [PATCH] First real commit. Ref #1 #2 #4 #5 #7 #8 Long list of changes: Add Mealtype and Diet to navbar Add Mealtype and Diet pages Add Mealtype and Diet in backend Add Mealtype and Diet in db --- backend/secfit/diets/__init__.py | 0 backend/secfit/diets/admin.py | 11 + backend/secfit/diets/apps.py | 13 + .../secfit/diets/migrations/0001_initial.py | 136 +++++++ .../migrations/0002_auto_20200910_0222.py | 25 ++ .../diets/migrations/0003_rememberme.py | 20 + backend/secfit/diets/migrations/__init__.py | 0 backend/secfit/diets/mixins.py | 26 ++ backend/secfit/diets/models.py | 151 ++++++++ backend/secfit/diets/parsers.py | 38 ++ backend/secfit/diets/permissions.py | 68 ++++ backend/secfit/diets/serializers.py | 230 ++++++++++++ backend/secfit/diets/tests.py | 6 + backend/secfit/diets/urls.py | 52 +++ backend/secfit/diets/views.py | 342 +++++++++++++++++ backend/secfit/secfit/settings.py | 1 + backend/secfit/secfit/urls.py | 1 + frontend/www/diet.html | 129 +++++++ frontend/www/diets.html | 63 ++++ frontend/www/mealtype.html | 54 +++ frontend/www/mealtypes.html | 47 +++ frontend/www/scripts/diet.js | 353 ++++++++++++++++++ frontend/www/scripts/diets.js | 106 ++++++ frontend/www/scripts/mealtype.js | 156 ++++++++ frontend/www/scripts/mealtypes.js | 42 +++ frontend/www/scripts/navbar.js | 2 + frontend/www/scripts/scripts.js | 6 + 27 files changed, 2078 insertions(+) create mode 100644 backend/secfit/diets/__init__.py create mode 100644 backend/secfit/diets/admin.py create mode 100644 backend/secfit/diets/apps.py create mode 100644 backend/secfit/diets/migrations/0001_initial.py create mode 100644 backend/secfit/diets/migrations/0002_auto_20200910_0222.py create mode 100644 backend/secfit/diets/migrations/0003_rememberme.py create mode 100644 backend/secfit/diets/migrations/__init__.py create mode 100644 backend/secfit/diets/mixins.py create mode 100644 backend/secfit/diets/models.py create mode 100644 backend/secfit/diets/parsers.py create mode 100644 backend/secfit/diets/permissions.py create mode 100644 backend/secfit/diets/serializers.py create mode 100644 backend/secfit/diets/tests.py create mode 100644 backend/secfit/diets/urls.py create mode 100644 backend/secfit/diets/views.py create mode 100644 frontend/www/diet.html create mode 100644 frontend/www/diets.html create mode 100644 frontend/www/mealtype.html create mode 100644 frontend/www/mealtypes.html create mode 100644 frontend/www/scripts/diet.js create mode 100644 frontend/www/scripts/diets.js create mode 100644 frontend/www/scripts/mealtype.js create mode 100644 frontend/www/scripts/mealtypes.js diff --git a/backend/secfit/diets/__init__.py b/backend/secfit/diets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/secfit/diets/admin.py b/backend/secfit/diets/admin.py new file mode 100644 index 0000000..48f90a9 --- /dev/null +++ b/backend/secfit/diets/admin.py @@ -0,0 +1,11 @@ +"""Module for registering models from workouts app to admin page so that they appear +""" +from django.contrib import admin + +# Register your models here. +from .models import Mealtype, MealtypeInstance, Diet, DietFile + +admin.site.register(Mealtype) +admin.site.register(MealtypeInstance) +admin.site.register(Diet) +admin.site.register(DietFile) diff --git a/backend/secfit/diets/apps.py b/backend/secfit/diets/apps.py new file mode 100644 index 0000000..1c9b2c8 --- /dev/null +++ b/backend/secfit/diets/apps.py @@ -0,0 +1,13 @@ +"""AppConfig for diets app +""" +from django.apps import AppConfig + + +class DietsConfig(AppConfig): + """AppConfig for diets app + + Attributes: + name (str): The name of the application + """ + + name = "diets" diff --git a/backend/secfit/diets/migrations/0001_initial.py b/backend/secfit/diets/migrations/0001_initial.py new file mode 100644 index 0000000..5fc875c --- /dev/null +++ b/backend/secfit/diets/migrations/0001_initial.py @@ -0,0 +1,136 @@ +# Generated by Django 3.1 on 2020-08-21 03:42 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import diets.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Mealtype", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("description", models.TextField()), + ], + ), + migrations.CreateModel( + name="Diet", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("date", models.DateTimeField()), + ("notes", models.TextField()), + ( + "visibility", + models.CharField( + choices=[("PU", "Public"), ("CO", "Coach"), ("PR", "Private")], + default="CO", + max_length=2, + ), + ), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="diets", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["-date"], + }, + ), + migrations.CreateModel( + name="DietFile", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "file", + models.FileField(upload_to=diets.models.diet_directory_path), + ), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="files", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "diet", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="files", + to="diets.diet", + ), + ), + ], + ), + migrations.CreateModel( + name="MealtypeInstance", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("sets", models.IntegerField()), + ("number", models.IntegerField()), + ( + "mealtype", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="instances", + to="diets.mealtype", + ), + ), + ( + "diet", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="mealtype_instances", + to="diets.diet", + ), + ), + ], + ), + ] diff --git a/backend/secfit/diets/migrations/0002_auto_20200910_0222.py b/backend/secfit/diets/migrations/0002_auto_20200910_0222.py new file mode 100644 index 0000000..0c4ea29 --- /dev/null +++ b/backend/secfit/diets/migrations/0002_auto_20200910_0222.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1 on 2020-09-10 00:22 + +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), + ("diets", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="dietfile", + name="owner", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="diet_files", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/backend/secfit/diets/migrations/0003_rememberme.py b/backend/secfit/diets/migrations/0003_rememberme.py new file mode 100644 index 0000000..15b7732 --- /dev/null +++ b/backend/secfit/diets/migrations/0003_rememberme.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1 on 2021-02-04 10:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('diets', '0002_auto_20200910_0222'), + ] + + operations = [ + migrations.CreateModel( + name='RememberMe', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('remember_me', models.CharField(max_length=500)), + ], + ), + ] diff --git a/backend/secfit/diets/migrations/__init__.py b/backend/secfit/diets/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/secfit/diets/mixins.py b/backend/secfit/diets/mixins.py new file mode 100644 index 0000000..df6cf7e --- /dev/null +++ b/backend/secfit/diets/mixins.py @@ -0,0 +1,26 @@ +""" +Mixins for the diets application +""" + + +class CreateListModelMixin(object): + """Mixin that allows to create multiple objects from lists. + Taken from https://stackoverflow.com/a/48885641 + """ + + def get_serializer(self, *args, **kwargs): + """If an array is passed, set serializer to many. + + kwargs["many"] will be set to true if an array is passed. This argument + is passed when retrieving the serializer. + + Args: + *args: Variable length argument list passed to the serializer. + **kwargs: Arbitrary keyword arguments passed to the serializer, including "many". + + Returns: + [type]: [description] + """ + if isinstance(kwargs.get("data", {}), list): + kwargs["many"] = True + return super(CreateListModelMixin, self).get_serializer(*args, **kwargs) diff --git a/backend/secfit/diets/models.py b/backend/secfit/diets/models.py new file mode 100644 index 0000000..67decd7 --- /dev/null +++ b/backend/secfit/diets/models.py @@ -0,0 +1,151 @@ +"""Contains the models for the diets Django application. Users +log diets (Diet), which contain instances (MealtypeInstance) of various +type of mealtypes (Mealtype). The user can also upload files (DietFile) . +""" +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. +class Diet(models.Model): + """Django model for a diet that users can log. + + A diet has several attributes, and is associated with one or more mealtypes + (instances) and, optionally, files uploaded by the user. + + Attributes: + name: Name of the diet + date: Date the diet was performed or is planned + notes: Notes about the diet + owner: User that logged the diet + visibility: The visibility level of the diet: Public, Coach, or Private + """ + + name = models.CharField(max_length=100) + date = models.DateTimeField() + notes = models.TextField() + owner = models.ForeignKey( + get_user_model(), on_delete=models.CASCADE, related_name="diets" + ) + + # Visibility levels + PUBLIC = "PU" # Visible to all authenticated users + COACH = "CO" # Visible only to owner and their coach + PRIVATE = "PR" # Visible only to owner + 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: + ordering = ["-date"] + + def __str__(self): + return self.name + + +class Mealtype(models.Model): + """Django model for an mealtype type that users can create. + + Each mealtype instance must have an mealtype type, e.g., Pushups, Crunches, or Lunges. + + Attributes: + name: Name of the mealtype type + description: Description of the mealtype type + """ + + name = models.CharField(max_length=100) + description = models.TextField() + + def __str__(self): + return self.name + + +class MealtypeInstance(models.Model): + """Django model for an instance of an mealtype. + + Each diet has one or more mealtype instances, each of a given type. For example, + Kyle's diet on 15.06.2029 had one mealtype instance: 3 (sets) reps (unit) of + 10 (number) pushups (mealtype type) + + Attributes: + diet: The diet associated with this mealtype instance + mealtype: The mealtype type of this instance + sets: The number of sets the owner will perform/performed + number: The number of repetitions in each set the owner will perform/performed + """ + + diet = models.ForeignKey( + Diet, on_delete=models.CASCADE, related_name="mealtype_instances" + ) + mealtype = models.ForeignKey( + Mealtype, on_delete=models.CASCADE, related_name="instances" + ) + sets = models.IntegerField() + number = models.IntegerField() + + +def diet_directory_path(instance, filename): + """Return path for which diet files should be uploaded on the web server + + Args: + instance (DietFile): DietFile instance + filename (str): Name of the file + + Returns: + str: Path where diet file is stored + """ + return f"diets/{instance.diet.id}/{filename}" + + +class DietFile(models.Model): + """Django model for file associated with a diet. Basically a wrapper. + + Attributes: + diet: The diet for which this file has been uploaded + owner: The user who uploaded the file + file: The actual file that's being uploaded + """ + + diet = models.ForeignKey(Diet, on_delete=models.CASCADE, related_name="files") + owner = models.ForeignKey( + get_user_model(), on_delete=models.CASCADE, related_name="diet_files" + ) + file = models.FileField(upload_to=diet_directory_path) + + +class RememberMe(models.Model): + """Django model for an remember_me cookie used for remember me functionality. + + Attributes: + remember_me: Value of cookie used for remember me + """ + + remember_me = models.CharField(max_length=500) + + def __str__(self): + return self.remember_me diff --git a/backend/secfit/diets/parsers.py b/backend/secfit/diets/parsers.py new file mode 100644 index 0000000..cf33375 --- /dev/null +++ b/backend/secfit/diets/parsers.py @@ -0,0 +1,38 @@ +"""Contains custom parsers for serializers from the diets Django app +""" +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. + + This is currently unused. + """ + + def parse(self, stream, media_type=None, parser_context=None): + result = super().parse( + stream, media_type=media_type, parser_context=parser_context + ) + data = {} + new_files = {"files": []} + + # for case1 with nested serializers + # parse each field with json + for key, value in result.data.items(): + if not isinstance(value, str): + data[key] = value + continue + if "{" in value or "[" in value: + try: + data[key] = json.loads(value) + except ValueError: + data[key] = value + else: + data[key] = value + + files = result.files.getlist("files") + for file in files: + new_files["files"].append({"file": file}) + + return parsers.DataAndFiles(data, new_files) diff --git a/backend/secfit/diets/permissions.py b/backend/secfit/diets/permissions.py new file mode 100644 index 0000000..6b2c4a8 --- /dev/null +++ b/backend/secfit/diets/permissions.py @@ -0,0 +1,68 @@ +"""Contains custom DRF permissions classes for the diets app +""" +from rest_framework import permissions +from diets.models import Diet + + +class IsOwner(permissions.BasePermission): + """Checks whether the requesting user is also the owner of the existing object""" + + def has_object_permission(self, request, view, obj): + return obj.owner == request.user + + +class IsOwnerOfDiet(permissions.BasePermission): + """Checks whether the requesting user is also the owner of the new or existing object""" + + def has_permission(self, request, view): + if request.method == "POST": + if request.data.get("diet"): + diet_id = request.data["diet"].split("/")[-2] + diet = Diet.objects.get(pk=diet_id) + if diet: + return diet.owner == request.user + return False + + return True + + def has_object_permission(self, request, view, obj): + return obj.diet.owner == request.user + + +class IsCoachAndVisibleToCoach(permissions.BasePermission): + """Checks whether the requesting user is the existing object's owner's coach + and whether the object (diet) has a visibility of Public or Coach. + """ + + def has_object_permission(self, request, view, obj): + return obj.owner.coach == request.user + + +class IsCoachOfDietAndVisibleToCoach(permissions.BasePermission): + """Checks whether the requesting user is the existing diet's owner's coach + and whether the object has a visibility of Public or Coach. + """ + + def has_object_permission(self, request, view, obj): + return obj.diet.owner.coach == request.user + + +class IsPublic(permissions.BasePermission): + """Checks whether the object (diet) has visibility of Public.""" + + def has_object_permission(self, request, view, obj): + return obj.visibility == "PU" + + +class IsDietPublic(permissions.BasePermission): + """Checks whether the object's diet has visibility of Public.""" + + def has_object_permission(self, request, view, obj): + return obj.diet.visibility == "PU" + + +class IsReadOnly(permissions.BasePermission): + """Checks whether the HTTP request verb is only for retrieving data (GET, HEAD, OPTIONS)""" + + def has_object_permission(self, request, view, obj): + return request.method in permissions.SAFE_METHODS diff --git a/backend/secfit/diets/serializers.py b/backend/secfit/diets/serializers.py new file mode 100644 index 0000000..39a0e7a --- /dev/null +++ b/backend/secfit/diets/serializers.py @@ -0,0 +1,230 @@ +"""Serializers for the diets application +""" +from rest_framework import serializers +from rest_framework.serializers import HyperlinkedRelatedField +from diets.models import Diet, Mealtype, MealtypeInstance, DietFile, RememberMe + + +class MealtypeInstanceSerializer(serializers.HyperlinkedModelSerializer): + """Serializer for an MealtypeInstance. Hyperlinks are used for relationships by default. + + Serialized fields: url, id, mealtype, sets, number, diet + + Attributes: + diet: The associated diet for this instance, represented by a hyperlink + """ + + diet = HyperlinkedRelatedField( + queryset=Diet.objects.all(), view_name="diet-detail", required=False + ) + + class Meta: + model = MealtypeInstance + fields = ["url", "id", "mealtype", "sets", "number", "diet"] + + +class DietFileSerializer(serializers.HyperlinkedModelSerializer): + """Serializer for a DietFile. Hyperlinks are used for relationships by default. + + Serialized fields: url, id, owner, file, diet + + Attributes: + owner: The owner (User) of the DietFile, represented by a username. ReadOnly + diet: The associate diet for this DietFile, represented by a hyperlink + """ + + owner = serializers.ReadOnlyField(source="owner.username") + diet = HyperlinkedRelatedField( + queryset=Diet.objects.all(), view_name="diet-detail", required=False + ) + + class Meta: + model = DietFile + fields = ["url", "id", "owner", "file", "diet"] + + def create(self, validated_data): + return DietFile.objects.create(**validated_data) + + +class DietSerializer(serializers.HyperlinkedModelSerializer): + """Serializer for a Diet. Hyperlinks are used for relationships by default. + + This serializer specifies nested serialization since a diet consists of DietFiles + and MealtypeInstances. + + Serialized fields: url, id, name, date, notes, owner, owner_username, visiblity, + mealtype_instances, files + + Attributes: + owner_username: Username of the owning User + mealtype_instance: Serializer for ExericseInstances + files: Serializer for DietFiles + """ + + owner_username = serializers.SerializerMethodField() + mealtype_instances = MealtypeInstanceSerializer(many=True, required=True) + files = DietFileSerializer(many=True, required=False) + + class Meta: + model = Diet + fields = [ + "url", + "id", + "name", + "date", + "notes", + "owner", + "owner_username", + "visibility", + "mealtype_instances", + "files", + ] + extra_kwargs = {"owner": {"read_only": True}} + + def create(self, validated_data): + """Custom logic for creating MealtypeInstances, DietFiles, and a Diet. + + This is needed to iterate over the files and mealtype instances, since this serializer is + nested. + + Args: + validated_data: Validated files and mealtype_instances + + Returns: + Diet: A newly created Diet + """ + mealtype_instances_data = validated_data.pop("mealtype_instances") + files_data = [] + if "files" in validated_data: + files_data = validated_data.pop("files") + + diet = Diet.objects.create(**validated_data) + + for mealtype_instance_data in mealtype_instances_data: + MealtypeInstance.objects.create(diet=diet, **mealtype_instance_data) + for file_data in files_data: + DietFile.objects.create( + diet=diet, owner=diet.owner, file=file_data.get("file") + ) + + return diet + + def update(self, instance, validated_data): + """Custom logic for updating a Diet with its MealtypeInstances and Diets. + + This is needed because each object in both mealtype_instances and files must be iterated + over and handled individually. + + Args: + instance (Diet): Current Diet object + validated_data: Contains data for validated fields + + Returns: + Diet: Updated Diet instance + """ + mealtype_instances_data = validated_data.pop("mealtype_instances") + mealtype_instances = instance.mealtype_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 MealtypeInstances + + # This updates existing mealtype instances without adding or deleting object. + # zip() will yield n 2-tuples, where n is + # min(len(mealtype_instance), len(mealtype_instance_data)) + for mealtype_instance, mealtype_instance_data in zip( + mealtype_instances.all(), mealtype_instances_data + ): + mealtype_instance.mealtype = mealtype_instance_data.get( + "mealtype", mealtype_instance.mealtype + ) + mealtype_instance.number = mealtype_instance_data.get( + "number", mealtype_instance.number + ) + mealtype_instance.sets = mealtype_instance_data.get( + "sets", mealtype_instance.sets + ) + mealtype_instance.save() + + # If new mealtype instances have been added to the diet, then create them + if len(mealtype_instances_data) > len(mealtype_instances.all()): + for i in range(len(mealtype_instances.all()), len(mealtype_instances_data)): + mealtype_instance_data = mealtype_instances_data[i] + MealtypeInstance.objects.create( + diet=instance, **mealtype_instance_data + ) + # Else if mealtype instances have been removed from the diet, then delete them + elif len(mealtype_instances_data) < len(mealtype_instances.all()): + for i in range(len(mealtype_instances_data), len(mealtype_instances.all())): + mealtype_instances.all()[i].delete() + + # Handle DietFiles + + 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 DietFiles + if len(files_data) > len(files.all()): + for i in range(len(files.all()), len(files_data)): + DietFile.objects.create( + diet=instance, + owner=instance.owner, + file=files_data[i].get("file"), + ) + # Else if files have been removed, delete DietFiles + elif len(files_data) < len(files.all()): + for i in range(len(files_data), len(files.all())): + files.all()[i].delete() + + return instance + + def get_owner_username(self, obj): + """Returns the owning user's username + + Args: + obj (Diet): Current Diet + + Returns: + str: Username of owner + """ + return obj.owner.username + + +class MealtypeSerializer(serializers.HyperlinkedModelSerializer): + """Serializer for an Mealtype. Hyperlinks are used for relationships by default. + + Serialized fields: url, id, name, description, instances + + Attributes: + instances: Associated mealtype instances with this Mealtype type. Hyperlinks. + """ + + instances = serializers.HyperlinkedRelatedField( + many=True, view_name="mealtypeinstance-detail", read_only=True + ) + + class Meta: + model = Mealtype + fields = ["url", "id", "name", "description", "instances"] + + +class RememberMeSerializer(serializers.HyperlinkedModelSerializer): + """Serializer for an RememberMe. Hyperlinks are used for relationships by default. + + Serialized fields: remember_me + + Attributes: + remember_me: Value of cookie used for remember me functionality + """ + + class Meta: + model = RememberMe + fields = ["remember_me"] diff --git a/backend/secfit/diets/tests.py b/backend/secfit/diets/tests.py new file mode 100644 index 0000000..7fbbf78 --- /dev/null +++ b/backend/secfit/diets/tests.py @@ -0,0 +1,6 @@ +""" +Tests for the workouts application. +""" +from django.test import TestCase + +# Create your tests here. diff --git a/backend/secfit/diets/urls.py b/backend/secfit/diets/urls.py new file mode 100644 index 0000000..05f0b7a --- /dev/null +++ b/backend/secfit/diets/urls.py @@ -0,0 +1,52 @@ +from django.urls import path, include +from diets import views +from rest_framework.urlpatterns import format_suffix_patterns +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, +) + +# This is a bit messy and will need to change +urlpatterns = format_suffix_patterns( + [ + path("", views.api_root), + path("api/diets/", views.DietList.as_view(), name="diet-list"), + path( + "api/diets/<int:pk>/", + views.DietDetail.as_view(), + name="diet-detail", + ), + path("api/mealtypes/", views.MealtypeList.as_view(), name="mealtype-list"), + path( + "api/mealtypes/<int:pk>/", + views.MealtypeDetail.as_view(), + name="mealtype-detail", + ), + path( + "api/mealtype-instances/", + views.MealtypeInstanceList.as_view(), + name="mealtype-instance-list", + ), + path( + "api/mealtype-instances/<int:pk>/", + views.MealtypeInstanceDetail.as_view(), + name="mealtypeinstance-detail", + ), + path( + "api/diet-files/", + views.DietFileList.as_view(), + name="diet-file-list", + ), + path( + "api/diet-files/<int:pk>/", + views.DietFileDetail.as_view(), + name="dietfile-detail", + ), + path("", include("users.urls")), + path("", include("comments.urls")), + path("api/auth/", include("rest_framework.urls")), + path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("api/remember_me/", views.RememberMe.as_view(), name="remember_me"), + ] +) diff --git a/backend/secfit/diets/views.py b/backend/secfit/diets/views.py new file mode 100644 index 0000000..676b42b --- /dev/null +++ b/backend/secfit/diets/views.py @@ -0,0 +1,342 @@ +"""Contains views for the diets application. These are mostly class-based views. +""" +from rest_framework import generics, mixins +from rest_framework import permissions + +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 diets.parsers import MultipartJsonParser +from diets.permissions import ( + IsOwner, + IsCoachAndVisibleToCoach, + IsOwnerOfDiet, + IsCoachOfDietAndVisibleToCoach, + IsReadOnly, + IsPublic, + IsDietPublic, +) +from diets.mixins import CreateListModelMixin +from diets.models import Diet, Mealtype, MealtypeInstance, DietFile +from diets.serializers import DietSerializer, MealtypeSerializer +from diets.serializers import RememberMeSerializer +from diets.serializers import MealtypeInstanceSerializer, DietFileSerializer +from django.core.exceptions import PermissionDenied +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): + return Response( + { + "users": reverse("user-list", request=request, format=format), + "diets": reverse("diet-list", request=request, format=format), + "mealtypes": reverse("mealtype-list", request=request, format=format), + "mealtype-instances": reverse( + "mealtype-instance-list", request=request, format=format + ), + "diet-files": reverse( + "diet-file-list", request=request, format=format + ), + "comments": reverse("comment-list", request=request, format=format), + "likes": reverse("like-list", request=request, format=format), + } + ) + + +# Allow users to save a persistent session in their browser +class RememberMe( + mixins.ListModelMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, +): + + serializer_class = RememberMeSerializer + + def get(self, request): + if request.user.is_authenticated == False: + raise PermissionDenied + else: + return Response({"remember_me": self.rememberme()}) + + def post(self, request): + cookieObject = namedtuple("Cookies", request.COOKIES.keys())( + *request.COOKIES.values() + ) + user = self.get_user(cookieObject) + refresh = RefreshToken.for_user(user) + return Response( + { + "refresh": str(refresh), + "access": str(refresh.access_token), + } + ) + + def get_user(self, cookieObject): + decode = base64.b64decode(cookieObject.remember_me) + user, sign = pickle.loads(decode) + + # Validate signature + if sign == self.sign_user(user): + return user + + def rememberme(self): + creds = [self.request.user, self.sign_user(str(self.request.user))] + return base64.b64encode(pickle.dumps(creds)) + + def sign_user(self, username): + signer = Signer() + signed_user = signer.sign(username) + return signed_user + + +class DietList( + mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView +): + """Class defining the web response for the creation of a Diet, or displaying a list + of Diets + + HTTP methods: GET, POST + """ + + serializer_class = DietSerializer + permission_classes = [ + permissions.IsAuthenticated + ] # User must be authenticated to create/view diets + parser_classes = [ + MultipartJsonParser, + JSONParser, + ] # For parsing JSON and Multi-part requests + filter_backends = [filters.OrderingFilter] + ordering_fields = ["name", "date", "owner__username"] + + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) + + def get_queryset(self): + qs = Diet.objects.none() + if self.request.user: + # A diet should be visible to the requesting user if any of the following hold: + # - The diet has public visibility + # - The owner of the diet is the requesting user + # - The diet has coach visibility and the requesting user is the owner's coach + qs = Diet.objects.filter( + Q(visibility="PU") + | (Q(visibility="CO") & Q(owner__coach=self.request.user)) + ).distinct() + + return qs + + +class DietDetail( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, +): + """Class defining the web response for the details of an individual Diet. + + HTTP methods: GET, PUT, DELETE + """ + + queryset = Diet.objects.all() + serializer_class = DietSerializer + permission_classes = [ + permissions.IsAuthenticated + & (IsOwner | (IsReadOnly & (IsCoachAndVisibleToCoach | IsPublic))) + ] + parser_classes = [MultipartJsonParser, JSONParser] + + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) + + +class MealtypeList( + mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView +): + """Class defining the web response for the creation of an Mealtype, or + a list of Mealtypes. + + HTTP methods: GET, POST + """ + + queryset = Mealtype.objects.all() + serializer_class = MealtypeSerializer + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + + +class MealtypeDetail( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, +): + """Class defining the web response for the details of an individual Mealtype. + + HTTP methods: GET, PUT, PATCH, DELETE + """ + + queryset = Mealtype.objects.all() + serializer_class = MealtypeSerializer + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + def patch(self, request, *args, **kwargs): + return self.partial_update(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) + + +class MealtypeInstanceList( + mixins.ListModelMixin, + mixins.CreateModelMixin, + CreateListModelMixin, + generics.GenericAPIView, +): + """Class defining the web response for the creation""" + + serializer_class = MealtypeInstanceSerializer + permission_classes = [permissions.IsAuthenticated & IsOwnerOfDiet] + + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + + def get_queryset(self): + qs = MealtypeInstance.objects.none() + if self.request.user: + qs = MealtypeInstance.objects.filter( + Q(diet__owner=self.request.user) + | ( + (Q(diet__visibility="CO") | Q(diet__visibility="PU")) + & Q(diet__owner__coach=self.request.user) + ) + ).distinct() + + return qs + + +class MealtypeInstanceDetail( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, +): + serializer_class = MealtypeInstanceSerializer + permission_classes = [ + permissions.IsAuthenticated + & ( + IsOwnerOfDiet + | (IsReadOnly & (IsCoachOfDietAndVisibleToCoach | IsDietPublic)) + ) + ] + + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + def patch(self, request, *args, **kwargs): + return self.partial_update(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) + + +class DietFileList( + mixins.ListModelMixin, + mixins.CreateModelMixin, + CreateListModelMixin, + generics.GenericAPIView, +): + + queryset = DietFile.objects.all() + serializer_class = DietFileSerializer + permission_classes = [permissions.IsAuthenticated & IsOwnerOfDiet] + parser_classes = [MultipartJsonParser, JSONParser] + + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) + + def get_queryset(self): + qs = DietFile.objects.none() + if self.request.user: + qs = DietFile.objects.filter( + Q(owner=self.request.user) + | Q(diet__owner=self.request.user) + | ( + Q(diet__visibility="CO") + & Q(diet__owner__coach=self.request.user) + ) + ).distinct() + + return qs + + +class DietFileDetail( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, +): + + queryset = DietFile.objects.all() + serializer_class = DietFileSerializer + permission_classes = [ + permissions.IsAuthenticated + & ( + IsOwner + | IsOwnerOfDiet + | (IsReadOnly & (IsCoachOfDietAndVisibleToCoach | IsDietPublic)) + ) + ] + + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) diff --git a/backend/secfit/secfit/settings.py b/backend/secfit/secfit/settings.py index 3c9ec93..2f71885 100644 --- a/backend/secfit/secfit/settings.py +++ b/backend/secfit/secfit/settings.py @@ -58,6 +58,7 @@ INSTALLED_APPS = [ "django.contrib.staticfiles", "rest_framework", "workouts.apps.WorkoutsConfig", + "diets.apps.DietsConfig", "users.apps.UsersConfig", "comments.apps.CommentsConfig", "corsheaders", diff --git a/backend/secfit/secfit/urls.py b/backend/secfit/secfit/urls.py index 3146886..5c5ff90 100644 --- a/backend/secfit/secfit/urls.py +++ b/backend/secfit/secfit/urls.py @@ -21,6 +21,7 @@ from django.conf.urls.static import static urlpatterns = [ path("admin/", admin.site.urls), path("", include("workouts.urls")), + path("", include("diets.urls")), ] urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/frontend/www/diet.html b/frontend/www/diet.html new file mode 100644 index 0000000..baecd9e --- /dev/null +++ b/frontend/www/diet.html @@ -0,0 +1,129 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Diet</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 Diet</h3> + </div> + </div> + <form class="row g-3 mb-4" id="form-diet"> + <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-diet" value=" OK "> + <input type="button" class="btn btn-primary hide" id="btn-edit-diet" value=" Edit "> + <input type="button" class="btn btn-secondary hide" id="btn-cancel-diet" value="Cancel"> + <input type="button" class="btn btn-danger float-end hide" id="btn-delete-diet" value="Delete"> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-12"> + <h3 class="mt-3">Mealtypes</h3> + </div> + <div id="div-mealtypes" class="col-lg-12"> + </div> + <div class="col-lg-6"> + <input type="button" class="btn btn-primary hide" id="btn-add-mealtype" value="Add mealtype"> + <input type="button" class="btn btn-danger hide" id="btn-remove-mealtype" value="Remove mealtype"> + </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-mealtype"> + <div class="row div-mealtype-container g-3 mb-3"> + <div class="col-lg-6"><h5>Mealtype</h5></div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <label class="form-label mealtype-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 mealtype-sets">Sets</label> + <input type="number" class="form-control" name="sets"> + </div> + <div class="col-lg-3"> + <label class="form-label mealtype-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/diet.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 diff --git a/frontend/www/diets.html b/frontend/www/diets.html new file mode 100644 index 0000000..db92119 --- /dev/null +++ b/frontend/www/diets.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Diets</title> + <link rel="stylesheet" href="styles/style.css"> + <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> + <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 text-center"> + <h3 class="mt-5">View Diets</h3> + <p>Here you can view diets completed by you, your athletes, + or the public. Click on a diet to view its details.</p> + <input type="button" class="btn btn-success" id="btn-create-diet" value="Log new diet"> + </div> + </div> + <div class="row"> + <div class="col-lg text-center"> + <div class="list-group list-group-horizontal d-inline-flex mt-2" id="list-tab" role="tablist"> + <a class="list-group-item list-group-item-action active" id="list-all-diets-list" data-bs-toggle="list" href="#list-all-diets" role="tab" aria-controls="all">All Diets</a> + <a class="list-group-item list-group-item-action" id="list-my-diets-list" data-bs-toggle="list" href="#list-my-diets" role="tab" aria-controls="my">My Diets</a> + <a class="list-group-item list-group-item-action" id="list-athlete-diets-list" data-bs-toggle="list" href="#list-athlete-diets" role="tab" aria-controls="athlete">Athlete Diets</a> + <a class="list-group-item list-group-item-action" id="list-public-diets-list" data-bs-toggle="list" href="#list-public-diets" role="tab" aria-controls="public">Public Diets</a> + </div> + <div class="mt-1">Sort by: <a href="?ordering=date">Date</a> <a href="?ordering=owner">Owner</a> <a href="?ordering=name">Name</a> + <br>Currently sorting by: <span id="current-sort"></span> + </div> + <div class="list-group mt-1" id="div-content"></div> + </div> + </div> + + </div> + + <template id="template-diet"> + <a class="list-group-item list-group-item-action flex-column align-items-start my-1 diet"> + <div class="d-flex w-100 justify-content-between align-items-center"> + <h5 class="mb-1"></h5> + </div> + <div class="d-flex"> + <table class="mb-1 text-start"> + <tr><td>Date:</td><td></td></tr> + <tr><td>Time:</td><td></td></tr> + <tr><td>Owner:</td><td></td></tr> + <tr><td>Mealtypes:</td><td></td></tr> + </table> + </div> + </a> + </template> + + <script src="scripts/defaults.js"></script> + <script src="scripts/scripts.js"></script> + <script src="scripts/diets.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 diff --git a/frontend/www/mealtype.html b/frontend/www/mealtype.html new file mode 100644 index 0000000..c348379 --- /dev/null +++ b/frontend/www/mealtype.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Mealtype</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 Mealtype</h3> + </div> + </div> + <form class="row g-3" id="form-mealtype"> + <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="inputDescription" class="form-label">Description</label> + <textarea class="form-control" id="inputDescription" name="description" readonly></textarea> + </div> + <div class="col-lg-6"></div> + <!-- <div class="col-lg-6"> + <label for="inputUnit" class="form-label">Unit</label> + <input type="text" class="form-control" id="inputUnit" name="unit" readonly> + </div> --> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <input type="button" class="btn btn-primary hide" id="btn-ok-mealtype" value=" OK "> + <input type="button" class="btn btn-primary" id="btn-edit-mealtype" value=" Edit "> + <input type="button" class="btn btn-secondary hide" id="btn-cancel-mealtype" value="Cancel"> + <input type="button" class="btn btn-danger float-end hide" id="btn-delete-mealtype" value="Delete"> + </div> + <div class="col-lg-6"> + + </div> + </form> + </div> + <script src="scripts/defaults.js"></script> + <script src="scripts/scripts.js"></script> + <script src="scripts/mealtype.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 diff --git a/frontend/www/mealtypes.html b/frontend/www/mealtypes.html new file mode 100644 index 0000000..65dd8e5 --- /dev/null +++ b/frontend/www/mealtypes.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Exercises</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 text-center"> + <h3 class="mt-5">View Mealtypes</h3> + <p>Here you can view, create, and edit mealtypes defined by you and other coaches.</p> + <input type="button" class="btn btn-primary" id="btn-create-mealtype" value="Create new mealtype"> + </div> + <div class="row"> + <div class="col-lg text-center"> + <div class="list-group mt-1" id="div-content"></div> + </div> + </div> + </div> + + <template id="template-mealtype"> + <a class="list-group-item list-group-item-action flex-column align-items-start my-1 mealtype" href=""> + <div class="d-flex w-100 justify-content-between align-items-center"> + <h5 class="mb-1"></h5> + </div> + <div class="d-flex"> + <p class="mb-1"> + </p> + </div> + </a> + </template> + + <script src="scripts/defaults.js"></script> + <script src="scripts/scripts.js"></script> + <script src="scripts/mealtypes.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 diff --git a/frontend/www/scripts/diet.js b/frontend/www/scripts/diet.js new file mode 100644 index 0000000..52eae73 --- /dev/null +++ b/frontend/www/scripts/diet.js @@ -0,0 +1,353 @@ +let cancelDietButton; +let okDietButton; +let deleteDietButton; +let editDietButton; +let postCommentButton; + +async function retrieveDiet(id) { + let dietData = null; + let response = await sendRequest("GET", `${HOST}/api/diets/${id}/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve diet data!", data); + document.body.prepend(alert); + } else { + dietData = await response.json(); + let form = document.querySelector("#form-diet"); + 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 = dietData[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 = dietData["visibility"]; + // files + let filesDiv = document.querySelector("#uploaded-files"); + for (let file of dietData.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 mealtypes + + // fetch mealtype types + let mealtypeTypeResponse = await sendRequest("GET", `${HOST}/api/mealtypes/`); + let mealtypeTypes = await mealtypeTypeResponse.json(); + + //TODO: This should be in its own method. + for (let i = 0; i < dietData.mealtype_instances.length; i++) { + let templateMealtype = document.querySelector("#template-mealtype"); + let divMealtypeContainer = templateMealtype.content.firstElementChild.cloneNode(true); + + let mealtypeTypeLabel = divMealtypeContainer.querySelector('.mealtype-type'); + mealtypeTypeLabel.for = `inputMealtypeType${i}`; + + let mealtypeTypeSelect = divMealtypeContainer.querySelector("select"); + mealtypeTypeSelect.id = `inputMealtypeType${i}`; + mealtypeTypeSelect.disabled = true; + + let splitUrl = dietData.mealtype_instances[i].mealtype.split("/"); + let currentMealtypeTypeId = splitUrl[splitUrl.length - 2]; + let currentMealtypeType = ""; + + for (let j = 0; j < mealtypeTypes.count; j++) { + let option = document.createElement("option"); + option.value = mealtypeTypes.results[j].id; + if (currentMealtypeTypeId == mealtypeTypes.results[j].id) { + currentMealtypeType = mealtypeTypes.results[j]; + } + option.innerText = mealtypeTypes.results[j].name; + mealtypeTypeSelect.append(option); + } + + mealtypeTypeSelect.value = currentMealtypeType.id; + + let mealtypeSetLabel = divMealtypeContainer.querySelector('.mealtype-sets'); + mealtypeSetLabel.for = `inputSets${i}`; + + let mealtypeSetInput = divMealtypeContainer.querySelector("input[name='sets']"); + mealtypeSetInput.id = `inputSets${i}`; + mealtypeSetInput.value = dietData.mealtype_instances[i].sets; + mealtypeSetInput.readOnly = true; + + let mealtypeNumberLabel = divMealtypeContainer.querySelector('.mealtype-number'); + mealtypeNumberLabel.for = "for", `inputNumber${i}`; + mealtypeNumberLabel.innerText = currentMealtypeType.unit; + + let mealtypeNumberInput = divMealtypeContainer.querySelector("input[name='number']"); + mealtypeNumberInput.id = `inputNumber${i}`; + mealtypeNumberInput.value = dietData.mealtype_instances[i].number; + mealtypeNumberInput.readOnly = true; + + let mealtypesDiv = document.querySelector("#div-mealtypes"); + mealtypesDiv.appendChild(divMealtypeContainer); + } + } + return dietData; +} + +function handleCancelDuringDietEdit() { + location.reload(); +} + +function handleEditDietButtonClick() { + let addMealtypeButton = document.querySelector("#btn-add-mealtype"); + let removeMealtypeButton = document.querySelector("#btn-remove-mealtype"); + + setReadOnly(false, "#form-diet"); + document.querySelector("#inputOwner").readOnly = true; // owner field should still be readonly + + editDietButton.className += " hide"; + okDietButton.className = okDietButton.className.replace(" hide", ""); + cancelDietButton.className = cancelDietButton.className.replace(" hide", ""); + deleteDietButton.className = deleteDietButton.className.replace(" hide", ""); + addMealtypeButton.className = addMealtypeButton.className.replace(" hide", ""); + removeMealtypeButton.className = removeMealtypeButton.className.replace(" hide", ""); + + cancelDietButton.addEventListener("click", handleCancelDuringDietEdit); + +} + +async function deleteDiet(id) { + let response = await sendRequest("DELETE", `${HOST}/api/diets/${id}/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert(`Could not delete diet ${id}!`, data); + document.body.prepend(alert); + } else { + window.location.replace("diets.html"); + } +} + +async function updateDiet(id) { + let submitForm = generateDietForm(); + + let response = await sendRequest("PUT", `${HOST}/api/diets/${id}/`, submitForm, ""); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not update diet!", data); + document.body.prepend(alert); + } else { + location.reload(); + } +} + +function generateDietForm() { + let form = document.querySelector("#form-diet"); + + 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")); + + // adding mealtype instances + let mealtypeInstances = []; + let mealtypeInstancesTypes = formData.getAll("type"); + let mealtypeInstancesSets = formData.getAll("sets"); + let mealtypeInstancesNumbers = formData.getAll("number"); + for (let i = 0; i < mealtypeInstancesTypes.length; i++) { + mealtypeInstances.push({ + mealtype: `${HOST}/api/mealtypes/${mealtypeInstancesTypes[i]}/`, + number: mealtypeInstancesNumbers[i], + sets: mealtypeInstancesSets[i] + }); + } + + submitForm.append("mealtype_instances", JSON.stringify(mealtypeInstances)); + // adding files + for (let file of formData.getAll("files")) { + submitForm.append("files", file); + } + return submitForm; +} + +async function createDiet() { + let submitForm = generateDietForm(); + + let response = await sendRequest("POST", `${HOST}/api/diets/`, submitForm, ""); + + if (response.ok) { + window.location.replace("diets.html"); + } else { + let data = await response.json(); + let alert = createAlert("Could not create new diet!", data); + document.body.prepend(alert); + } +} + +function handleCancelDuringDietCreate() { + window.location.replace("diets.html"); +} + +async function createBlankMealtype() { + let form = document.querySelector("#form-diet"); + + let mealtypeTypeResponse = await sendRequest("GET", `${HOST}/api/mealtypes/`); + let mealtypeTypes = await mealtypeTypeResponse.json(); + + let mealtypeTemplate = document.querySelector("#template-mealtype"); + let divMealtypeContainer = mealtypeTemplate.content.firstElementChild.cloneNode(true); + let mealtypeTypeSelect = divMealtypeContainer.querySelector("select"); + + for (let i = 0; i < mealtypeTypes.count; i++) { + let option = document.createElement("option"); + option.value = mealtypeTypes.results[i].id; + option.innerText = mealtypeTypes.results[i].name; + mealtypeTypeSelect.append(option); + } + + let currentMealtypeType = mealtypeTypes.results[0]; + mealtypeTypeSelect.value = currentMealtypeType.name; + + let divMealtypes = document.querySelector("#div-mealtypes"); + divMealtypes.appendChild(divMealtypeContainer); +} + +function removeMealtype(event) { + let divMealtypeContainers = document.querySelectorAll(".div-mealtype-container"); + if (divMealtypeContainers && divMealtypeContainers.length > 0) { + divMealtypeContainers[divMealtypeContainers.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"); + listElement.className = "media"; + let commentBody = document.createElement("div"); + commentBody.className = "media-body"; + let dateSpan = document.createElement("span"); + dateSpan.className = "text-muted pull-right me-1"; + let smallText = document.createElement("small"); + smallText.className = "text-muted"; + + if (date != "Now") { + let localDate = new Date(date); + smallText.innerText = localDate.toLocaleString(); + } else { + smallText.innerText = date; + } + + dateSpan.appendChild(smallText); + commentBody.appendChild(dateSpan); + + let strong = document.createElement("strong"); + strong.className = "text-success"; + strong.innerText = author; + commentBody.appendChild(strong); + let p = document.createElement("p"); + p.innerHTML = text; + + commentBody.appendChild(strong); + commentBody.appendChild(p); + listElement.appendChild(commentBody); + + if (append) { + commentList.append(listElement); + } else { + commentList.prepend(listElement); + } + +} + +async function createComment(dietid) { + let commentArea = document.querySelector("#comment-area"); + let content = commentArea.value; + let body = {diet: `${HOST}/api/diets/${dietid}/`, content: content}; + + let response = await sendRequest("POST", `${HOST}/api/comments/`, body); + if (response.ok) { + addComment(sessionStorage.getItem("username"), content, "Now", false); + } else { + let data = await response.json(); + let alert = createAlert("Failed to create comment!", data); + document.body.prepend(alert); + } +} + +async function retrieveComments(dietid) { + let response = await sendRequest("GET", `${HOST}/api/comments/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve comments!", data); + document.body.prepend(alert); + } else { + let data = await response.json(); + let comments = data.results; + for (let comment of comments) { + let splitArray = comment.diet.split("/"); + if (splitArray[splitArray.length - 2] == dietid) { + addComment(comment.owner, comment.content, comment.timestamp, true); + } + } + } +} + +window.addEventListener("DOMContentLoaded", async () => { + cancelDietButton = document.querySelector("#btn-cancel-diet"); + okDietButton = document.querySelector("#btn-ok-diet"); + deleteDietButton = document.querySelector("#btn-delete-diet"); + editDietButton = document.querySelector("#btn-edit-diet"); + let postCommentButton = document.querySelector("#post-comment"); + let divCommentRow = document.querySelector("#div-comment-row"); + let buttonAddMealtype = document.querySelector("#btn-add-mealtype"); + let buttonRemoveMealtype = document.querySelector("#btn-remove-mealtype"); + + buttonAddMealtype.addEventListener("click", createBlankMealtype); + buttonRemoveMealtype.addEventListener("click", removeMealtype); + + const urlParams = new URLSearchParams(window.location.search); + let currentUser = await getCurrentUser(); + + if (urlParams.has('id')) { + const id = urlParams.get('id'); + let dietData = await retrieveDiet(id); + await retrieveComments(id); + + if (dietData["owner"] == currentUser.url) { + editDietButton.classList.remove("hide"); + editDietButton.addEventListener("click", handleEditDietButtonClick); + deleteDietButton.addEventListener("click", (async (id) => await deleteDiet(id)).bind(undefined, id)); + okDietButton.addEventListener("click", (async (id) => await updateDiet(id)).bind(undefined, id)); + postCommentButton.addEventListener("click", (async (id) => await createComment(id)).bind(undefined, id)); + divCommentRow.className = divCommentRow.className.replace(" hide", ""); + } + } else { + await createBlankMealtype(); + let ownerInput = document.querySelector("#inputOwner"); + ownerInput.value = currentUser.username; + setReadOnly(false, "#form-diet"); + ownerInput.readOnly = !ownerInput.readOnly; + + okDietButton.className = okDietButton.className.replace(" hide", ""); + cancelDietButton.className = cancelDietButton.className.replace(" hide", ""); + buttonAddMealtype.className = buttonAddMealtype.className.replace(" hide", ""); + buttonRemoveMealtype.className = buttonRemoveMealtype.className.replace(" hide", ""); + + okDietButton.addEventListener("click", async () => await createDiet()); + cancelDietButton.addEventListener("click", handleCancelDuringDietCreate); + divCommentRow.className += " hide"; + } + +}); \ No newline at end of file diff --git a/frontend/www/scripts/diets.js b/frontend/www/scripts/diets.js new file mode 100644 index 0000000..0ee8125 --- /dev/null +++ b/frontend/www/scripts/diets.js @@ -0,0 +1,106 @@ +async function fetchDiets(ordering) { + let response = await sendRequest("GET", `${HOST}/api/diets/?ordering=${ordering}`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } else { + let data = await response.json(); + + let diets = data.results; + let container = document.getElementById('div-content'); + diets.forEach(diet => { + let templateDiet = document.querySelector("#template-diet"); + let cloneDiet = templateDiet.content.cloneNode(true); + + let aDiet = cloneDiet.querySelector("a"); + aDiet.href = `diet.html?id=${diet.id}`; + + let h5 = aDiet.querySelector("h5"); + h5.textContent = diet.name; + + let localDate = new Date(diet.date); + + let table = aDiet.querySelector("table"); + let rows = table.querySelectorAll("tr"); + rows[0].querySelectorAll("td")[1].textContent = localDate.toLocaleDateString(); // Date + rows[1].querySelectorAll("td")[1].textContent = localDate.toLocaleTimeString(); // Time + rows[2].querySelectorAll("td")[1].textContent = diet.owner_username; //Owner + rows[3].querySelectorAll("td")[1].textContent = diet.mealtype_instances.length; // Mealtypes + + container.appendChild(aDiet); + }); + return diets; + } +} + +function createDiet() { + window.location.replace("diet.html"); +} + +window.addEventListener("DOMContentLoaded", async () => { + let createButton = document.querySelector("#btn-create-diet"); + createButton.addEventListener("click", createDiet); + let ordering = "-date"; + + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.has('ordering')) { + let aSort = null; + ordering = urlParams.get('ordering'); + if (ordering == "name" || ordering == "owner" || ordering == "date") { + let aSort = document.querySelector(`a[href="?ordering=${ordering}"`); + aSort.href = `?ordering=-${ordering}`; + } + } + + let currentSort = document.querySelector("#current-sort"); + currentSort.innerHTML = (ordering.startsWith("-") ? "Descending" : "Ascending") + " " + ordering.replace("-", ""); + + let currentUser = await getCurrentUser(); + // grab username + if (ordering.includes("owner")) { + ordering += "__username"; + } + let diets = await fetchDiets(ordering); + + let tabEls = document.querySelectorAll('a[data-bs-toggle="list"]'); + for (let i = 0; i < tabEls.length; i++) { + let tabEl = tabEls[i]; + tabEl.addEventListener('show.bs.tab', function (event) { + let dietAnchors = document.querySelectorAll('.diet'); + for (let j = 0; j < diets.length; j++) { + // I'm assuming that the order of diet objects matches + // the other of the diet anchor elements. They should, given + // that I just created them. + let diet = diets[j]; + let dietAnchor = dietAnchors[j]; + + switch (event.currentTarget.id) { + case "list-my-diets-list": + if (diet.owner == currentUser.url) { + dietAnchor.classList.remove('hide'); + } else { + dietAnchor.classList.add('hide'); + } + break; + case "list-athlete-diets-list": + if (currentUser.athletes && currentUser.athletes.includes(diet.owner)) { + dietAnchor.classList.remove('hide'); + } else { + dietAnchor.classList.add('hide'); + } + break; + case "list-public-diets-list": + if (diet.visibility == "PU") { + dietAnchor.classList.remove('hide'); + } else { + dietAnchor.classList.add('hide'); + } + break; + default : + dietAnchor.classList.remove('hide'); + break; + } + } + }); + } +}); \ No newline at end of file diff --git a/frontend/www/scripts/mealtype.js b/frontend/www/scripts/mealtype.js new file mode 100644 index 0000000..16a3c4a --- /dev/null +++ b/frontend/www/scripts/mealtype.js @@ -0,0 +1,156 @@ +let cancelButton; +let okButton; +let deleteButton; +let editButton; +let oldFormData; + + +function handleCancelButtonDuringEdit() { + setReadOnly(true, "#form-mealtype"); + okButton.className += " hide"; + deleteButton.className += " hide"; + cancelButton.className += " hide"; + editButton.className = editButton.className.replace(" hide", ""); + + cancelButton.removeEventListener("click", handleCancelButtonDuringEdit); + + let form = document.querySelector("#form-mealtype"); + 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("mealtypes.html"); +} + +async function createMealtype() { + let form = document.querySelector("#form-mealtype"); + 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/mealtypes/`, body); + + if (response.ok) { + window.location.replace("mealtypes.html"); + } else { + let data = await response.json(); + let alert = createAlert("Could not create new mealtype!", data); + document.body.prepend(alert); + } +} + +function handleEditMealtypeButtonClick() { + setReadOnly(false, "#form-mealtype"); + + 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); + + let form = document.querySelector("#form-mealtype"); + oldFormData = new FormData(form); +} + +async function deleteMealtype(id) { + let response = await sendRequest("DELETE", `${HOST}/api/mealtypes/${id}/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert(`Could not delete mealtype ${id}`, data); + document.body.prepend(alert); + } else { + window.location.replace("mealtypes.html"); + } +} + +async function retrieveMealtype(id) { + let response = await sendRequest("GET", `${HOST}/api/mealtypes/${id}/`); + console.log(response.ok); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve mealtype data!", data); + document.body.prepend(alert); + } else { + let mealtypeData = await response.json(); + let form = document.querySelector("#form-mealtype"); + 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 = mealtypeData[key]; + input.value = newVal; + } + } +} + +async function updateMealtype(id) { + let form = document.querySelector("#form-mealtype"); + 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/mealtypes/${id}/`, body); + + if (!response.ok) { + let data = await response.json(); + let alert = createAlert(`Could not update mealtype ${id}`, data); + document.body.prepend(alert); + } else { + // duplicate code from handleCancelButtonDuringEdit + // you should refactor this + setReadOnly(true, "#form-mealtype"); + 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"); + } +} + +window.addEventListener("DOMContentLoaded", async () => { + cancelButton = document.querySelector("#btn-cancel-mealtype"); + okButton = document.querySelector("#btn-ok-mealtype"); + deleteButton = document.querySelector("#btn-delete-mealtype"); + editButton = document.querySelector("#btn-edit-mealtype"); + oldFormData = null; + + const urlParams = new URLSearchParams(window.location.search); + + // view/edit + if (urlParams.has('id')) { + const mealtypeId = urlParams.get('id'); + await retrieveMealtype(mealtypeId); + + editButton.addEventListener("click", handleEditMealtypeButtonClick); + deleteButton.addEventListener("click", (async (id) => await deleteMealtype(id)).bind(undefined, mealtypeId)); + okButton.addEventListener("click", (async (id) => await updateMealtype(id)).bind(undefined, mealtypeId)); + } + //create + else { + setReadOnly(false, "#form-mealtype"); + + editButton.className += " hide"; + okButton.className = okButton.className.replace(" hide", ""); + cancelButton.className = cancelButton.className.replace(" hide", ""); + + okButton.addEventListener("click", async () => await createMealtype()); + cancelButton.addEventListener("click", handleCancelButtonDuringCreate); + } +}); \ No newline at end of file diff --git a/frontend/www/scripts/mealtypes.js b/frontend/www/scripts/mealtypes.js new file mode 100644 index 0000000..5096c71 --- /dev/null +++ b/frontend/www/scripts/mealtypes.js @@ -0,0 +1,42 @@ +async function fetchMealtypes(request) { + let response = await sendRequest("GET", `${HOST}/api/mealtypes/`); + + if (response.ok) { + let data = await response.json(); + + let mealtypes = data.results; + let container = document.getElementById('div-content'); + let mealtypeTemplate = document.querySelector("#template-mealtype"); + mealtypes.forEach(mealtype => { + const mealtypeAnchor = mealtypeTemplate.content.firstElementChild.cloneNode(true); + mealtypeAnchor.href = `mealtype.html?id=${mealtype.id}`; + + const h5 = mealtypeAnchor.querySelector("h5"); + h5.textContent = mealtype.name; + + const p = mealtypeAnchor.querySelector("p"); + p.textContent = mealtype.description; + + container.appendChild(mealtypeAnchor); + }); + } + + return response; +} + +function createMealtype() { + window.location.replace("mealtype.html"); +} + +window.addEventListener("DOMContentLoaded", async () => { + let createButton = document.querySelector("#btn-create-mealtype"); + createButton.addEventListener("click", createMealtype); + + let response = await fetchMealtypes(); + + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve mealtypes!", data); + document.body.prepend(alert); + } +}); \ No newline at end of file diff --git a/frontend/www/scripts/navbar.js b/frontend/www/scripts/navbar.js index d906f61..9b3c29c 100644 --- a/frontend/www/scripts/navbar.js +++ b/frontend/www/scripts/navbar.js @@ -18,6 +18,8 @@ class NavBar extends HTMLElement { <a class="nav-link hide" id="nav-exercises" href="exercises.html">Exercises</a> <a class="nav-link hide" id="nav-mycoach" href="mycoach.html">Coach</a> <a class="nav-link hide" id="nav-myathletes" href="myathletes.html">Athletes</a> + <a class="nav-link hide" id="nav-mealtypes" href="mealtypes.html">Mealtypes</a> + <a class="nav-link hide" id="nav-diets" href="diets.html">Diets</a> <hr> </div> <div class="my-2 my-lg-0 me-5"> diff --git a/frontend/www/scripts/scripts.js b/frontend/www/scripts/scripts.js index 8c55000..b4ea272 100644 --- a/frontend/www/scripts/scripts.js +++ b/frontend/www/scripts/scripts.js @@ -22,6 +22,10 @@ function updateNavBar() { makeNavLinkActive("nav-mycoach") } else if (window.location.pathname == "/myathletes.html") { makeNavLinkActive("nav-myathletes"); + } else if (window.location.pathname == "/mealtypes.html") { + makeNavLinkActive("nav-mealtypes"); + } else if (window.location.pathname == "/diets.html") { + makeNavLinkActive("nav-diets"); } if (isUserAuthenticated()) { @@ -32,6 +36,8 @@ function updateNavBar() { document.querySelector('a[href="mycoach.html"').classList.remove("hide"); document.querySelector('a[href="exercises.html"').classList.remove("hide"); document.querySelector('a[href="myathletes.html"').classList.remove("hide"); + document.querySelector('a[href="mealtypes.html"').classList.remove("hide"); + document.querySelector('a[href="diets.html"').classList.remove("hide"); } else { document.getElementById("btn-login-nav").classList.remove("hide"); document.getElementById("btn-register").classList.remove("hide"); -- GitLab