diff --git a/.gitignore b/.gitignore index bdd4074d7d98ff4c226296bfaf9fd16a18e1283d..959c40e9c80563d38398a7c47fb79f24c54ecbf8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ backend/secfit/.vscode/ backend/secfit/*/migrations/__pycache__/ backend/secfit/*/__pycache__/ backend/secfit/db.sqlite3 +venv/ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 960342d75f9ec243d7ba801dd24e0570a1e0144e..b5bb6adac444779c99188739679a4d8586717fed 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,25 +1,26 @@ stages: - test - staging - + test: - image: python:3.8 - stage: test - script: - # this configures Django application to use attached postgres database that is run on `postgres` host - - cd backend/secfit - - apt-get update -qy - - pip install -r requirements.txt + image: python:3.8 + stage: test + script: + # this configures Django application to use attached postgres database that is run on `postgres` host + - cd backend/secfit + - apt-get update -qy + - pip install -r requirements.txt + - python manage.py test staging: - type: deploy - image: ruby - stage: staging - script: - - apt-get update -qy - - apt-get install -y ruby-dev - - gem install dpl - - dpl --provider=heroku --app=<your-frontend-app-name> --api-key=$HEROKU_STAGING_API_KEY - - dpl --provider=heroku --app=<your-backend-app-name> --api-key=$HEROKU_STAGING_API_KEY - only: - - master + type: deploy + image: ruby + stage: staging + script: + - apt-get update -qy + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=tdt4242-frontend --api-key=$HEROKU_STAGING_API_KEY + - dpl --provider=heroku --app=tdt4242-backend --api-key=$HEROKU_STAGING_API_KEY + only: + - master diff --git a/backend/secfit/.coverage b/backend/secfit/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..ce5050a7db5d6dd8f0dac60db75d9bc448a84a1d Binary files /dev/null and b/backend/secfit/.coverage differ diff --git a/backend/secfit/.coveragerc b/backend/secfit/.coveragerc new file mode 100644 index 0000000000000000000000000000000000000000..a852abd2d0c861a680f3921ead12cbe30a2e310f --- /dev/null +++ b/backend/secfit/.coveragerc @@ -0,0 +1,3 @@ +# .coveragerc +[report] +show_missing = True diff --git a/backend/secfit/challenges/__init__.py b/backend/secfit/challenges/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/secfit/challenges/admin.py b/backend/secfit/challenges/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..654de84c13ee0ed30bdfaeada17ae89b1fcc60c2 --- /dev/null +++ b/backend/secfit/challenges/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from challenges.models import Challenge + +# Register your models here. +admin.site.register(Challenge) diff --git a/backend/secfit/challenges/apps.py b/backend/secfit/challenges/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..621ad57def4152f467f9ef369522aaad2d7e0824 --- /dev/null +++ b/backend/secfit/challenges/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ChallengesConfig(AppConfig): + name = 'challenges' diff --git a/backend/secfit/challenges/migrations/0001_initial.py b/backend/secfit/challenges/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..75bed3d8e8e6dfb659d3a64f2fd55ccc0591059a --- /dev/null +++ b/backend/secfit/challenges/migrations/0001_initial.py @@ -0,0 +1,50 @@ +# Generated by Django 3.1 on 2022-03-12 15:29 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('groups', '0004_group_athletes'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Challenge', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(default='Challenge', max_length=24)), + ], + ), + migrations.CreateModel( + name='ChallengeCompleted', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('completed', models.BooleanField(default=False)), + ('Challenge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='challenges.challenge')), + ('athlete', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='groups.group')), + ], + ), + migrations.AddField( + model_name='challenge', + name='athletes', + field=models.ManyToManyField(related_name='_challenge_athletes_+', through='challenges.ChallengeCompleted', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='challenge', + name='coach', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='challenge', + name='groups', + field=models.ManyToManyField(through='challenges.ChallengeCompleted', to='groups.Group'), + ), + ] diff --git a/backend/secfit/challenges/migrations/0002_auto_20220312_1532.py b/backend/secfit/challenges/migrations/0002_auto_20220312_1532.py new file mode 100644 index 0000000000000000000000000000000000000000..5f276e94392f3d6ffc7e79f77447ad11e1dd9f08 --- /dev/null +++ b/backend/secfit/challenges/migrations/0002_auto_20220312_1532.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2022-03-12 15:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('challenges', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='challengecompleted', + old_name='Challenge', + new_name='challenge', + ), + ] diff --git a/backend/secfit/challenges/migrations/0003_auto_20220313_1647.py b/backend/secfit/challenges/migrations/0003_auto_20220313_1647.py new file mode 100644 index 0000000000000000000000000000000000000000..6c87bca4c4bbfaa4e1fb01362b6af0a111b70e7b --- /dev/null +++ b/backend/secfit/challenges/migrations/0003_auto_20220313_1647.py @@ -0,0 +1,21 @@ +# Generated by Django 3.1 on 2022-03-13 16:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('challenges', '0002_auto_20220312_1532'), + ] + + operations = [ + migrations.RemoveField( + model_name='challenge', + name='groups', + ), + migrations.RemoveField( + model_name='challengecompleted', + name='group', + ), + ] diff --git a/backend/secfit/challenges/migrations/0004_auto_20220313_1942.py b/backend/secfit/challenges/migrations/0004_auto_20220313_1942.py new file mode 100644 index 0000000000000000000000000000000000000000..94a272906bdf353d4d5788ef7b6e26d2d1d2ad8f --- /dev/null +++ b/backend/secfit/challenges/migrations/0004_auto_20220313_1942.py @@ -0,0 +1,33 @@ +# Generated by Django 3.1 on 2022-03-13 19:42 + +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), + ('challenges', '0003_auto_20220313_1647'), + ] + + operations = [ + migrations.RemoveField( + model_name='challenge', + name='athletes', + ), + migrations.AddField( + model_name='challenge', + name='athlete', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='challenge', + name='completed', + field=models.BooleanField(default=False), + ), + migrations.DeleteModel( + name='ChallengeCompleted', + ), + ] diff --git a/backend/secfit/challenges/migrations/__init__.py b/backend/secfit/challenges/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/secfit/challenges/models.py b/backend/secfit/challenges/models.py new file mode 100644 index 0000000000000000000000000000000000000000..be9e44c94ed2586d3ff7708a9090d269a59b3f4a --- /dev/null +++ b/backend/secfit/challenges/models.py @@ -0,0 +1,20 @@ +from django.db import models +from django.contrib.auth import get_user_model + + +# Create your models here. +class Challenge(models.Model): + """Django model for a group of athletes + + Each group has a coach + + Attributes: + coach: Who coaches the group + title: Name of group + athletes All athletes that are assigned to challenge + """ + title = models.CharField(max_length=24, default="Challenge") + coach = models.ForeignKey( + get_user_model(), on_delete=models.CASCADE, related_name="+") + athlete = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, blank=True, null=True) + completed = models.BooleanField(default=False) diff --git a/backend/secfit/challenges/serializers.py b/backend/secfit/challenges/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..64fa8e082390fff4fb03126bc6cd1e1f5b08d552 --- /dev/null +++ b/backend/secfit/challenges/serializers.py @@ -0,0 +1,27 @@ +from rest_framework import serializers +from challenges.models import Challenge + +class ChallengeSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Challenge + fields = [ + "title", + "athlete", + "coach", + "completed" + ] + + def create(self, validated_data): + return Challenge.objects.create(**validated_data) + + + +class ChallengeGetSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Challenge + fields = [ + "title", + "athlete", + "coach", + "completed" + ] \ No newline at end of file diff --git a/backend/secfit/challenges/tests.py b/backend/secfit/challenges/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/backend/secfit/challenges/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/secfit/challenges/urls.py b/backend/secfit/challenges/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..296979cfd18df66a89bb9cba556e96866935ba88 --- /dev/null +++ b/backend/secfit/challenges/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from challenges import views + +urlpatterns = [ + path("api/challenges/", views.ChallengeList.as_view(), name="challenge-list"), + path("api/challenges/<int:pk>/", views.ChallengeDetail.as_view(), name="challenge-detail"), +] diff --git a/backend/secfit/challenges/views.py b/backend/secfit/challenges/views.py new file mode 100644 index 0000000000000000000000000000000000000000..98f5a0d84df4a4f8a8fc61f1edd68d24bd80f448 --- /dev/null +++ b/backend/secfit/challenges/views.py @@ -0,0 +1,44 @@ +from rest_framework import mixins, generics +from challenges.models import Challenge +from challenges.serializers import ChallengeSerializer, ChallengeGetSerializer + +# Create your views here + +class ChallengeList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView): + serializer_class = ChallengeSerializer + + + def get(self, request, *args, **kwargs): + self.serializer_class = ChallengeGetSerializer + return self.list(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + + def get_queryset(self): + # Tror man filtrerer her + return Challenge.objects.all() + + +class ChallengeDetail( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, +): + lookup_field_options = ["pk"] + # serializer_class = UserSerializer + # queryset = get_user_model().objects.all() + # permission_classes = [permissions.IsAuthenticated & (IsCurrentUser | IsReadOnly)] + + def get_object(self): + for field in self.lookup_field_options: + if field in self.kwargs: + self.lookup_field = field + break + + return super().get_object() + + def get(self, request, *args, **kwargs): + #self.serializer_class = UserGetSerializer + return self.retrieve(request, *args, **kwargs) diff --git a/backend/secfit/comments/permissions.py b/backend/secfit/comments/permissions.py index b863cc7782ff295afe3ca1b2cc95519c70eb48eb..8ef3d7b1cc6bff9200254103c8efd5e710d103f5 100644 --- a/backend/secfit/comments/permissions.py +++ b/backend/secfit/comments/permissions.py @@ -11,7 +11,7 @@ class IsCommentVisibleToUser(permissions.BasePermission): - The comment is on a workout owned by the user """ - def has_object_permission(self, request, view, obj): + def has_object_permission(self, request, obj): # Write permissions are only allowed to the owner. return ( obj.workout.visibility == "PU" diff --git a/backend/secfit/comments/views.py b/backend/secfit/comments/views.py index b74d0f208c9bcf06ee49817541d47742767f0b7d..5256fb2078a1cd3814aca2f755cd7c5c9e44dd82 100644 --- a/backend/secfit/comments/views.py +++ b/backend/secfit/comments/views.py @@ -1,4 +1,3 @@ -from django.shortcuts import render from rest_framework import generics, mixins from comments.models import Comment, Like from rest_framework import permissions @@ -12,7 +11,6 @@ from rest_framework.filters import OrderingFilter class CommentList( mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView ): - # queryset = Comment.objects.all() serializer_class = CommentSerializer permission_classes = [permissions.IsAuthenticated] filter_backends = [OrderingFilter] @@ -40,29 +38,24 @@ class CommentList( - The comment is on a coach visibility workout and the user is the workout owner's coach - The comment is on a workout owned by the user """ - # The code below is kind of duplicate of the one in ./permissions.py - # We should replace it with a better solution. - # Or maybe not. - - qs = Comment.objects.filter( - Q(workout__visibility="PU") - | Q(owner=self.request.user) - | ( - Q(workout__visibility="CO") - & Q(workout__owner__coach=self.request.user) - ) - | Q(workout__owner=self.request.user) - ).distinct() + + qs = Comment.objects.all() + for index in range(len(qs)): + if not IsCommentVisibleToUser().has_object_permission(self.request, qs[index]): + qs.remove(qs[index]) + return qs -# Details of comment class CommentDetail( mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, generics.GenericAPIView, ): + '''Details of comment + + ''' queryset = Comment.objects.all() serializer_class = CommentSerializer permission_classes = [ @@ -81,6 +74,10 @@ class CommentDetail( # List of likes class LikeList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView): + '''Details of likes + + ''' + serializer_class = LikeSerializer permission_classes = [permissions.IsAuthenticated] @@ -97,13 +94,15 @@ class LikeList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericA return Like.objects.filter(owner=self.request.user) -# Details of like class LikeDetail( mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, generics.GenericAPIView, ): + '''Details of like + + ''' queryset = Like.objects.all() serializer_class = LikeSerializer permission_classes = [permissions.IsAuthenticated] diff --git a/backend/secfit/groups/__init__.py b/backend/secfit/groups/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/secfit/groups/admin.py b/backend/secfit/groups/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..164edd42892507c7fdfa60a8595d9e30ddd0950a --- /dev/null +++ b/backend/secfit/groups/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import Group + + +# Register your models here. +admin.site.register(Group) diff --git a/backend/secfit/groups/apps.py b/backend/secfit/groups/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..e6984a81f64b2c3bf2ebae54e2d9501333bafa38 --- /dev/null +++ b/backend/secfit/groups/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class GroupsConfig(AppConfig): + name = 'groups' diff --git a/backend/secfit/groups/migrations/0001_initial.py b/backend/secfit/groups/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..95b55317937311dd07693dcecc0800f686ad87ff --- /dev/null +++ b/backend/secfit/groups/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 3.1 on 2022-03-10 20:19 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Group', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='Group', max_length=24)), + ('athletes', models.ManyToManyField(to=settings.AUTH_USER_MODEL)), + ('coach', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='coached_groups', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/secfit/groups/migrations/0002_auto_20220310_2024.py b/backend/secfit/groups/migrations/0002_auto_20220310_2024.py new file mode 100644 index 0000000000000000000000000000000000000000..f26b194c39e1568b3388ce874a2829a36d005bc1 --- /dev/null +++ b/backend/secfit/groups/migrations/0002_auto_20220310_2024.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1 on 2022-03-10 20:24 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('groups', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='group', + name='athletes', + field=models.ManyToManyField(blank=True, null=True, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/secfit/groups/migrations/0003_remove_group_athletes.py b/backend/secfit/groups/migrations/0003_remove_group_athletes.py new file mode 100644 index 0000000000000000000000000000000000000000..62e72f26c17a6c23e3d5c60597bc6494ca52c462 --- /dev/null +++ b/backend/secfit/groups/migrations/0003_remove_group_athletes.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1 on 2022-03-11 13:38 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('groups', '0002_auto_20220310_2024'), + ] + + operations = [ + migrations.RemoveField( + model_name='group', + name='athletes', + ), + ] diff --git a/backend/secfit/groups/migrations/0004_group_athletes.py b/backend/secfit/groups/migrations/0004_group_athletes.py new file mode 100644 index 0000000000000000000000000000000000000000..fee7653e4499366fbf20642de12386dc7f3cbd71 --- /dev/null +++ b/backend/secfit/groups/migrations/0004_group_athletes.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1 on 2022-03-11 14:03 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('groups', '0003_remove_group_athletes'), + ] + + operations = [ + migrations.AddField( + model_name='group', + name='athletes', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/secfit/groups/migrations/__init__.py b/backend/secfit/groups/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/secfit/groups/models.py b/backend/secfit/groups/models.py new file mode 100644 index 0000000000000000000000000000000000000000..80786867c0873aef9c5e3f52a205f59f3ac3682f --- /dev/null +++ b/backend/secfit/groups/models.py @@ -0,0 +1,18 @@ +from curses.ascii import US +from django.db import models +from django.contrib.auth import get_user_model + +# Create your models here. +class Group(models.Model): + """Django model for a group of athletes + + Each group has a coach + + Attributes: + coach: Who coaches the group + name: Name of group + """ + coach = models.ForeignKey( + "users.User", on_delete=models.CASCADE, related_name="coached_groups") + name = models.CharField(max_length=24, default="Group") + athletes = models.ManyToManyField(get_user_model()) \ No newline at end of file diff --git a/backend/secfit/groups/serializers.py b/backend/secfit/groups/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..f468e2e2d58eb432d22e5faf4194af7fa59f39fc --- /dev/null +++ b/backend/secfit/groups/serializers.py @@ -0,0 +1,33 @@ +from rest_framework import serializers +from groups.models import Group +from django.contrib.auth import get_user_model + +class GroupSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Group + fields = [ + "name", + "athletes", + "coach", + ] + + def create(self, validated_data): + + files_data = [] + if "athletes" in validated_data: + files_data = validated_data.pop("athletes") + + group_obj = Group.objects.create(**validated_data) + group_obj.athletes.set(files_data) + + return group_obj + + +class GroupGetSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Group + fields = [ + "name", + "athletes", + "coach", + ] \ No newline at end of file diff --git a/backend/secfit/groups/tests.py b/backend/secfit/groups/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/backend/secfit/groups/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/secfit/groups/urls.py b/backend/secfit/groups/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..43e705113b764a7a883638dcd890a5b0df3ec2c9 --- /dev/null +++ b/backend/secfit/groups/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from groups import views + +urlpatterns = [ + path("api/groups/", views.GroupList.as_view(), name="group-list"), + path("api/groups/<int:pk>/", views.GroupDetail.as_view(), name="group-detail"), +] diff --git a/backend/secfit/groups/views.py b/backend/secfit/groups/views.py new file mode 100644 index 0000000000000000000000000000000000000000..09a5a610e02fc77b8d086a9cfa817c0eae6dc1b7 --- /dev/null +++ b/backend/secfit/groups/views.py @@ -0,0 +1,49 @@ +from rest_framework import mixins, generics +from groups.models import Group +from groups.serializers import GroupSerializer, GroupGetSerializer +# Create your views here + +class GroupList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView): + serializer_class = GroupSerializer + + + def get(self, request, *args, **kwargs): + self.serializer_class = GroupGetSerializer + return self.list(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + + def get_queryset(self): + # Tror man filtrerer her + qs = Group.objects.all() + + if self.request.user: + # Return the currently logged in user + # status = self.request.query_params.get("user", None) + qs = Group.objects.filter(coach=self.request.user.pk) + + return qs + +class GroupDetail( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, +): + lookup_field_options = ["pk"] + # serializer_class = UserSerializer + # queryset = get_user_model().objects.all() + # permission_classes = [permissions.IsAuthenticated & (IsCurrentUser | IsReadOnly)] + + def get_object(self): + for field in self.lookup_field_options: + if field in self.kwargs: + self.lookup_field = field + break + + return super().get_object() + + def get(self, request, *args, **kwargs): + #self.serializer_class = UserGetSerializer + return self.retrieve(request, *args, **kwargs) diff --git a/backend/secfit/requirements.txt b/backend/secfit/requirements.txt index 9feb375bde1e8fb7befe6c102dd29beeee7c6940..0fc007f159667113fad281604f4927f3c616a406 100644 Binary files a/backend/secfit/requirements.txt and b/backend/secfit/requirements.txt differ diff --git a/backend/secfit/secfit/settings.py b/backend/secfit/secfit/settings.py index 96dbdcde7b3df80ade7cb75f08ebb31353b918c7..705e53b7d6226269ced4fef460e93bb861bfd088 100644 --- a/backend/secfit/secfit/settings.py +++ b/backend/secfit/secfit/settings.py @@ -61,8 +61,11 @@ INSTALLED_APPS = [ "workouts.apps.WorkoutsConfig", "meals.apps.MealsConfig", "users.apps.UsersConfig", + "groups.apps.GroupsConfig", + "challenges.apps.ChallengesConfig", "comments.apps.CommentsConfig", "corsheaders", + 'django_nose', ] MIDDLEWARE = [ @@ -158,3 +161,28 @@ REST_FRAMEWORK = { AUTH_USER_MODEL = "users.User" DEBUG = True + +# Django Logging Information +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": {"class": "logging.StreamHandler"}, + }, + "loggers": { + "django": { + "handlers": ["console"], + "level": "INFO", + }, + } +} + +# Use nose to run all tests +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + +# Tell nose to measure coverage on the 'foo' and 'bar' apps +NOSE_ARGS = [ + '--with-coverage', + '--cover-package=workouts.permissions, users.serializers', +] + diff --git a/backend/secfit/secfit/urls.py b/backend/secfit/secfit/urls.py index 93fc1902dfcf8c10bce7f680b763d92756d6eae9..9ca3400bc544a897078218ce132582079dce9ffc 100644 --- a/backend/secfit/secfit/urls.py +++ b/backend/secfit/secfit/urls.py @@ -22,6 +22,8 @@ urlpatterns = [ path("admin/", admin.site.urls), path("", include("workouts.urls")), path("", include("meals.urls")), + path("", include("groups.urls")), + path("", include("challenges.urls")), ] urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/backend/secfit/users/admin.py b/backend/secfit/users/admin.py index fc0af23c4473e29bcc06045aebfdd0d21989d22d..9d8f7d0ffeb2311ecf7725f81985738be096b166 100644 --- a/backend/secfit/users/admin.py +++ b/backend/secfit/users/admin.py @@ -11,7 +11,6 @@ class CustomUserAdmin(UserAdmin): add_form = CustomUserCreationForm form = CustomUserChangeForm model = get_user_model() - # list_display = UserAdmin.list_display + ('coach',) fieldsets = UserAdmin.fieldsets + ((None, {"fields": ("coach",)}),) add_fieldsets = UserAdmin.add_fieldsets + ((None, {"fields": ("coach",)}),) diff --git a/backend/secfit/users/migrations/0010_group_groupmembership.py b/backend/secfit/users/migrations/0010_group_groupmembership.py new file mode 100644 index 0000000000000000000000000000000000000000..53287d3bbd1d77cf8358f2153c843a093d7d52c3 --- /dev/null +++ b/backend/secfit/users/migrations/0010_group_groupmembership.py @@ -0,0 +1,30 @@ +# Generated by Django 3.1 on 2022-03-07 12:02 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0009_auto_20210204_1055'), + ] + + operations = [ + migrations.CreateModel( + name='Group', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('coach', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='coached_groups', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='GroupMembership', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('athlete', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='joined_groups', to=settings.AUTH_USER_MODEL)), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='athletes', to='users.group')), + ], + ), + ] diff --git a/backend/secfit/users/migrations/0011_auto_20220310_1949.py b/backend/secfit/users/migrations/0011_auto_20220310_1949.py new file mode 100644 index 0000000000000000000000000000000000000000..d94ecf33264072c0f37633acad0d9754cbe85ed1 --- /dev/null +++ b/backend/secfit/users/migrations/0011_auto_20220310_1949.py @@ -0,0 +1,30 @@ +# Generated by Django 3.1 on 2022-03-10 19:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0010_group_groupmembership'), + ] + + operations = [ + migrations.RemoveField( + model_name='group', + name='coach', + ), + migrations.AddField( + model_name='group', + name='name', + field=models.CharField(default='Group', max_length=24), + ), + migrations.AlterField( + model_name='user', + name='groups', + field=models.ManyToManyField(to='users.Group'), + ), + migrations.DeleteModel( + name='GroupMembership', + ), + ] diff --git a/backend/secfit/users/migrations/0012_auto_20220310_2019.py b/backend/secfit/users/migrations/0012_auto_20220310_2019.py new file mode 100644 index 0000000000000000000000000000000000000000..7ea0d8835753a9b9a91a6008fbf689e87b4826d5 --- /dev/null +++ b/backend/secfit/users/migrations/0012_auto_20220310_2019.py @@ -0,0 +1,22 @@ +# Generated by Django 3.1 on 2022-03-10 20:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('groups', '0001_initial'), + ('users', '0011_auto_20220310_1949'), + ] + + operations = [ + migrations.DeleteModel( + name='Group', + ), + migrations.AlterField( + model_name='user', + name='groups', + field=models.ManyToManyField(to='groups.Group'), + ), + ] diff --git a/backend/secfit/users/migrations/0013_auto_20220310_2024.py b/backend/secfit/users/migrations/0013_auto_20220310_2024.py new file mode 100644 index 0000000000000000000000000000000000000000..21f645b9b1b1901b8b53395e30b6131720640a6d --- /dev/null +++ b/backend/secfit/users/migrations/0013_auto_20220310_2024.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1 on 2022-03-10 20:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('groups', '0002_auto_20220310_2024'), + ('users', '0012_auto_20220310_2019'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='groups', + field=models.ManyToManyField(blank=True, null=True, to='groups.Group'), + ), + ] diff --git a/backend/secfit/users/migrations/0014_auto_20220311_1403.py b/backend/secfit/users/migrations/0014_auto_20220311_1403.py new file mode 100644 index 0000000000000000000000000000000000000000..4b4b0034cad60efe8d3a8d962d1df305bc3f20d7 --- /dev/null +++ b/backend/secfit/users/migrations/0014_auto_20220311_1403.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1 on 2022-03-11 14:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('users', '0013_auto_20220310_2024'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='groups', + field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), + ), + ] diff --git a/backend/secfit/users/models.py b/backend/secfit/users/models.py index d48528655225b97a32833eb1a6629e0d266ba6df..c261d859d1c753b6a90e3fba26271923955386f9 100644 --- a/backend/secfit/users/models.py +++ b/backend/secfit/users/models.py @@ -2,10 +2,8 @@ from django.db import models from django.contrib.auth.models import AbstractUser from django.contrib.auth import get_user_model - # Create your models here. - class User(AbstractUser): """ Standard Django User model with an added field for a user's coach. @@ -73,4 +71,4 @@ class Offer(models.Model): ) status = models.CharField(max_length=8, choices=STATUS_CHOICES, default=PENDING) - timestamp = models.DateTimeField(auto_now_add=True) + timestamp = models.DateTimeField(auto_now_add=True) \ No newline at end of file diff --git a/backend/secfit/users/tests.py b/backend/secfit/users/tests.py index 7ce503c2dd97ba78597f6ff6e4393132753573f6..34560ebb7094d495bda786cfade12f9cb822892e 100644 --- a/backend/secfit/users/tests.py +++ b/backend/secfit/users/tests.py @@ -1,3 +1,42 @@ from django.test import TestCase +from users.serializers import UserSerializer, UserPutSerializer, AthleteFileSerializer +from users.models import User + # Create your tests here. + + +class UserTestCase(TestCase): + def setUp(self): + # Animal.objects.create(name="lion", sound="roar") + # Animal.objects.create(name="cat", sound="meow") + # get_user_model().objects.create() + self.user_data = { + "username": "NikoTest", + "password": "", + "email": "nikolaidokken", + "phone_number": "12345678", + "country": "Norway", + "city": "Baerum", + "street_address": "Veien 2" + } + self.testUser = User.objects.create(username="Niko", password="niko") + + def test_user_is_created(self): + """User is correctly created""" + newUser = UserSerializer().create(validated_data=self.user_data) + foundUser = User.objects.get(username="NikoTest") + self.assertEqual(newUser, foundUser) + + def test_user_valid_password(self): + serializer = UserSerializer(data=self.user_data) + self.assertEqual(serializer.validate_password(True), True) + + def test_user_put_athletes(self): + newAthlete = User.objects.create(username="athlete", password="athlete") + UserPutSerializer().update(self.testUser, {"athletes": [newAthlete]}) + + self.assertEqual(self.testUser.athletes.get(), newAthlete) + + def test_create_athlete_file(self): + AthleteFileSerializer().create({"athlete": self.testUser, "owner_id": self.testUser.id}) diff --git a/backend/secfit/users/urls.py b/backend/secfit/users/urls.py index 507c27008e8b0997e486945a27bfe3afc55d89de..2d22bf70408aed78d70be76910d1b15485166e5f 100644 --- a/backend/secfit/users/urls.py +++ b/backend/secfit/users/urls.py @@ -1,6 +1,6 @@ -from django.urls import path, include +from django.urls import path from users import views -from rest_framework.urlpatterns import format_suffix_patterns + urlpatterns = [ path("api/users/", views.UserList.as_view(), name="user-list"), diff --git a/backend/secfit/users/views.py b/backend/secfit/users/views.py index f5efef5c2ce82566ab380cecad344e3143c31813..7e8f4b56f15eea7b21a06a176dff9a566566d308 100644 --- a/backend/secfit/users/views.py +++ b/backend/secfit/users/views.py @@ -1,4 +1,3 @@ -import django from rest_framework import mixins, generics from workouts.mixins import CreateListModelMixin from rest_framework import permissions @@ -10,15 +9,11 @@ from users.serializers import ( UserGetSerializer, ) from rest_framework.permissions import ( - AllowAny, - IsAdminUser, - IsAuthenticated, IsAuthenticatedOrReadOnly, ) from users.models import Offer, AthleteFile from django.contrib.auth import get_user_model from django.db.models import Q -from django.shortcuts import get_object_or_404 from rest_framework.parsers import MultiPartParser, FormParser from users.permissions import IsCurrentUser, IsAthlete, IsCoach from workouts.permissions import IsOwner, IsReadOnly @@ -98,7 +93,6 @@ class OfferList( serializer.save(owner=self.request.user) def get_queryset(self): - qs = Offer.objects.none() result = Offer.objects.none() if self.request.user: @@ -106,22 +100,20 @@ class OfferList( Q(owner=self.request.user) | Q(recipient=self.request.user) ).distinct() qp = self.request.query_params - u = self.request.user + user = self.request.user - # filtering by status (if provided) - s = qp.get("status", None) - if s is not None and self.request is not None: - qs = qs.filter(status=s) + status = qp.get("status", None) + if status is not None and self.request is not None: + qs = qs.filter(status=status) if qp.get("status", None) is None: - qs = Offer.objects.filter(Q(owner=u)).distinct() - - # filtering by category (sent or received) - c = qp.get("category", None) - if c is not None and qp is not None: - if c == "sent": - qs = qs.filter(owner=u) - elif c == "received": - qs = qs.filter(recipient=u) + qs = Offer.objects.filter(Q(owner=user)).distinct() + + category = qp.get("category", None) + if category is not None and qp is not None: + if category == "sent": + qs = qs.filter(owner=user) + elif category == "received": + qs = qs.filter(recipient=user) return qs else: return result diff --git a/backend/secfit/workouts/models.py b/backend/secfit/workouts/models.py index 0f6214f2d9919d17fe68e93416a183a52209bda6..44c88cbbf370d9c0645e46268f2e38b236cb296e 100644 --- a/backend/secfit/workouts/models.py +++ b/backend/secfit/workouts/models.py @@ -2,29 +2,9 @@ log workouts (Workout), which contain instances (ExerciseInstance) of various type of exercises (Exercise). The user can also upload files (WorkoutFile) . """ -import os from django.db import models -from django.core.files.storage import FileSystemStorage -from django.conf import settings from django.contrib.auth import get_user_model - -class OverwriteStorage(FileSystemStorage): - """Filesystem storage for overwriting files. Currently unused.""" - - def get_available_name(self, name, max_length=None): - """https://djangosnippets.org/snippets/976/ - Returns a filename that's free on the target storage system, and - available for new content to be written to. - - Args: - name (str): Name of the file - max_length (int, optional): Maximum length of a file name. Defaults to None. - """ - if self.exists(name): - os.remove(os.path.join(settings.MEDIA_ROOT, name)) - - # Create your models here. class Workout(models.Model): """Django model for a workout that users can log. diff --git a/backend/secfit/workouts/serializers.py b/backend/secfit/workouts/serializers.py index 6abbe31ffd71c5e9cbba140e34a03176b127a4bf..eb9b497b3f71a1822a40fc600625140ef9976108 100644 --- a/backend/secfit/workouts/serializers.py +++ b/backend/secfit/workouts/serializers.py @@ -109,33 +109,20 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): return workout - def update(self, instance, validated_data): - """Custom logic for updating a Workout with its ExerciseInstances and Workouts. - - This is needed because each object in both exercise_instances and files must be iterated - over and handled individually. - + def update_exercises(self, validated_data, instance): + """Custom logic and helper function for updating a Workout with its ExerciseInstances. + This updates existing exercise instances without adding or deleting object. + zip() will yield n 2-tuples, where n is min(len(exercise_instance), len(exercise_instance_data)) + Args: - instance (Workout): Current Workout object validated_data: Contains data for validated fields + instance (Workout): Current Workout object - Returns: - Workout: Updated Workout instance """ + exercise_instances_data = validated_data.pop("exercise_instances") exercise_instances = instance.exercise_instances - instance.name = validated_data.get("name", instance.name) - instance.notes = validated_data.get("notes", instance.notes) - instance.visibility = validated_data.get("visibility", instance.visibility) - instance.date = validated_data.get("date", instance.date) - instance.save() - - # Handle ExerciseInstances - - # This updates existing exercise instances without adding or deleting object. - # zip() will yield n 2-tuples, where n is - # min(len(exercise_instance), len(exercise_instance_data)) for exercise_instance, exercise_instance_data in zip( exercise_instances.all(), exercise_instances_data ): @@ -162,7 +149,15 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): for i in range(len(exercise_instances_data), len(exercise_instances.all())): exercise_instances.all()[i].delete() - # Handle WorkoutFiles + + def update_files(self, validated_data, instance): + """Custom logic and helper function for updating a Workout with its WorkoutFiles. + + Args: + validated_data: Contains data for validated fields + instance (Workout): Current Workout object + + """ if "files" in validated_data: files_data = validated_data.pop("files") @@ -184,6 +179,31 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): for i in range(len(files_data), len(files.all())): files.all()[i].delete() + def update(self, instance, validated_data): + """Custom logic for updating a Workout with its ExerciseInstances and Workouts. + + This is needed because each object in both exercise_instances and files must be iterated + over and handled individually. + + Args: + instance (Workout): Current Workout object + validated_data: Contains data for validated fields + + Returns: + Workout: Updated Workout instance + """ + + + 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() + + + self.update_exercises(validated_data, instance) + self.update_files(validated_data, instance) + return instance def get_owner_username(self, obj): diff --git a/backend/secfit/workouts/tests.py b/backend/secfit/workouts/tests.py index 7fbbf7847f5b0f201d408d4017cc865d614e2615..e1c514554ef80001e41222e78b2fe12776c507f6 100644 --- a/backend/secfit/workouts/tests.py +++ b/backend/secfit/workouts/tests.py @@ -2,5 +2,99 @@ Tests for the workouts application. """ from django.test import TestCase +from workouts.permissions import IsOwner, IsOwnerOfWorkout, IsCoachAndVisibleToCoach, IsCoachOfWorkoutAndVisibleToCoach, IsPublic, IsWorkoutPublic, IsReadOnly +from workouts.models import Workout +from users.models import User +from django.utils import timezone + + +class Obj(): + def __init__(self, owner, workout=None, visibility="PU"): + self.owner = owner + self.workout = workout + self.visibility = visibility + +class Request(): + def __init__(self, user, data, method="GET"): + self.user = user + self.method = method + self.data = data # Create your tests here. +class WorkoutTestCase(TestCase): + def setUp(self): + coach = User.objects.create(username="Coach", password="coach") + user = User.objects.create(username="Niko", password="niko", coach=coach) + otherUser = User.objects.create(username="Ian", password="ian") + + workout = Workout.objects.create(date=timezone.now(), owner_id=user.id, visibility="PU") + workoutUrl = "http://noe:8000/api/workouts/" + str(workout.id) + "/" + + self.request = Request(user=user, data={"workout": workoutUrl}) + self.requestWithoutValidWorkout = Request(user=user, data={"test": None}) + self.otherRequest = Request(user=otherUser, data={"workout": workoutUrl}) + self.coachRequest = Request(user=coach, data={"workout": workoutUrl}) + self.obj = Obj(owner=user, workout=workout) + + def test_is_owner(self): + """User is correctly created""" + has_permission = IsOwner().has_object_permission(request=self.request,view={}, obj=self.obj) + self.assertEqual(has_permission, True) + + def test_is_owner_of_workout(self): + self.request.method = "POST" + has_permission = IsOwnerOfWorkout().has_permission(self.request, {}) + self.assertEqual(has_permission, True) + + self.otherRequest.method = "POST" + has_permission = IsOwnerOfWorkout().has_permission(self.otherRequest, {}) + self.assertEqual(has_permission, False) + + self.request.method = "GET" + has_permission = IsOwnerOfWorkout().has_permission(self.request, {}) + self.assertEqual(has_permission, True) + + has_permission = IsOwnerOfWorkout().has_object_permission(self.request, {}, self.obj) + self.assertEqual(has_permission, True) + + self.requestWithoutValidWorkout.method = "POST" + has_permission = IsOwnerOfWorkout().has_permission(self.requestWithoutValidWorkout, {}) + self.assertEqual(has_permission, False) + + def test_is_coach_and_visible_to_coach(self): + has_permission = IsCoachAndVisibleToCoach().has_object_permission(self.request, {}, self.obj) + self.assertEqual(has_permission, False) + + has_permission = IsCoachAndVisibleToCoach().has_object_permission(self.coachRequest, {}, self.obj) + self.assertEqual(has_permission, True) + + def test_is_coach_of_workout_and_visible_to_coach(self): + has_permission = IsCoachOfWorkoutAndVisibleToCoach().has_object_permission(self.coachRequest, {}, self.obj) + self.assertEqual(has_permission, True) + + has_permission = IsCoachOfWorkoutAndVisibleToCoach().has_object_permission(self.request, {}, self.obj) + self.assertEqual(has_permission, False) + + def test_is_public(self): + is_public = IsPublic().has_object_permission(self.request, {}, self.obj) + self.assertEqual(is_public, True) + + self.obj.visibility = "PR" + is_public = IsPublic().has_object_permission(self.request, {}, self.obj) + self.assertEqual(is_public, False) + + def test_is_workout_public(self): + is_public = IsWorkoutPublic().has_object_permission(self.request, {}, self.obj) + self.assertEqual(is_public, True) + + self.obj.workout.visibility = "PR" + is_public = IsWorkoutPublic().has_object_permission(self.request, {}, self.obj) + self.assertEqual(is_public, False) + + def test_is_read_only(self): + is_read_only = IsReadOnly().has_object_permission(self.request, {}, self.obj) + self.assertEqual(is_read_only, True) + + self.request.method = "POST" + is_read_only = IsReadOnly().has_object_permission(self.request, {}, self.obj) + self.assertEqual(is_read_only, False) diff --git a/backend/secfit/workouts/views.py b/backend/secfit/workouts/views.py index efddf40454376b23d233f9fe2cecaf9da43fddb8..590e49d5576c6cf50c79faaec52ceb9e8246ea52 100644 --- a/backend/secfit/workouts/views.py +++ b/backend/secfit/workouts/views.py @@ -54,13 +54,15 @@ def api_root(request, format=None): ) -# Allow users to save a persistent session in their browser class RememberMe( mixins.ListModelMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin, generics.GenericAPIView, ): + """Allow users to save a persistent session in their browser + + """ serializer_class = RememberMeSerializer @@ -71,10 +73,10 @@ class RememberMe( return Response({"remember_me": self.rememberme()}) def post(self, request): - cookieObject = namedtuple("Cookies", request.COOKIES.keys())( + cookie_object = namedtuple("Cookies", request.COOKIES.keys())( *request.COOKIES.values() ) - user = self.get_user(cookieObject) + user = self.get_user(cookie_object) refresh = RefreshToken.for_user(user) return Response( { @@ -83,8 +85,8 @@ class RememberMe( } ) - def get_user(self, cookieObject): - decode = base64.b64decode(cookieObject.remember_me) + def get_user(self, cookie_object): + decode = base64.b64decode(cookie_object.remember_me) user, sign = pickle.loads(decode) # Validate signature diff --git a/frontend/www/index.html b/frontend/www/index.html index bf98ea0f0c0ae222bfc8a58927361a99f513473c..f2b44849bca482114208c6fff30d3028b3bccce5 100644 --- a/frontend/www/index.html +++ b/frontend/www/index.html @@ -1,13 +1,21 @@ <!DOCTYPE html> <html lang="en"> <head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Home</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"> + <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="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> @@ -17,17 +25,27 @@ <div class="row mt-3"> <div class="col-lg text-center"> <h2 class="mt-3">Welcome to SecFit!</h2> - <p>SecFit (coming from "SuperSecure" and "Fitness") is the most secure fitness logging app on the net. - You can conveniently log a workout using either our website or our app. You can also view and comment on others' + <p> + SecFit (coming from "SuperSecure" and "Fitness") is the + most secure fitness logging app on the net. You can + conveniently log a workout using either our website or + our app. You can also view and comment on others' workouts! </p> - <img src="img/fitness.jpg" class="img-fluid" alt="DUMBBELLS"> + <img + src="img/fitness.jpg" + class="img-fluid" + alt="DUMBBELLS" + /> </div> </div> </div> <script src="scripts/scripts.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> + <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 +</html> diff --git a/frontend/www/myathletes.html b/frontend/www/myathletes.html index 5c92ea7accd91493df3263116e77f0c4a06c4f6d..1f862d8d3c575c4d648dd677407fa65c47d8f074 100644 --- a/frontend/www/myathletes.html +++ b/frontend/www/myathletes.html @@ -42,10 +42,27 @@ <div class="list-group" id="list-tab" role="tablist"></div> </div> <div class="col-lg-4"> - <div class="tab-content" id="nav-tabContent"></div> + <div class="tab-content" id="nav-tabContent"></div> </div> <div class="col-lg-6"></div> - </div> + </div> + <form id="form-group" class="row"> + <div id="list-groups-div" class="col-lg-6"> + <label for="group-control" class="form-label mt-2">Groups</label> + <div id="group-control"></div> + <input class="form-control" name="group" type="text" placeholder="Group name: username1, username2" id="group-input" /> + <input type="button" class="btn btn-primary mt-1 mb-2" id="button-submit-group" value="Submit"> + </div> + </form> + <form id="form-challenge" class="row"> + <div id="list-challenges-div" class="col-lg-6"> + <label for="challenge-control" class="form-label mt-2">Challenges</label> + <div id="challenge-control"></div> + <input class="form-control" name="challenge-title" type="text" placeholder="Run 5 km" id="challenge-title-input" /> + <input class="form-control" name="challenge-user" type="text" placeholder="username1, username2" id="challenge-user-input" /> + <input type="button" class="btn btn-primary mt-1 mb-2" id="button-submit-challenge" value="Submit"> + </div> + </form> </div> <template id="template-filled-athlete"> @@ -57,6 +74,13 @@ </span> </div> </template> + + <template id="template-filled-group"> + <div class="card mb-2"> + <div class="card-body" id="card-body"> + </div> + </div> + </template> <template id="template-empty-athlete"> <div class="entry input-group"> @@ -68,6 +92,16 @@ </div> </template> + <template id="template-empty-group"> + <div class="entry input-group"> + <input class="form-control" name="group" type="text" placeholder="Comma-separated usernames"/> + <button class="btn btn-success btn-add" type="button"> + <i class="fas fa-plus"></i> + </button> + </span> + </div> + </template> + <template id="template-athlete-tab"> <a class="list-group-item list-group-item-action" data-bs-toggle="list" href="#list-tab" role="tab"></a> </template> @@ -90,6 +124,9 @@ <script src="scripts/defaults.js"></script> <script src="scripts/scripts.js"></script> <script src="scripts/myathletes.js"></script> + <script src="scripts/groups.js"></script> + <script src="scripts/group.js"></script> + <script src="scripts/challenge.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/profile.html b/frontend/www/profile.html new file mode 100644 index 0000000000000000000000000000000000000000..876f14cadff51234b7c43bd9627dc181afc85bea --- /dev/null +++ b/frontend/www/profile.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Profile</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 mt-3"> + <h2 class="mt-3">My Profile</h2> + <form class="row g-3" id="form-user"> + <div class="col-lg-6 "> + <label for="inputName" class="form-label">Name</label> + <input type="text" class="form-control" id="inputName" name="username" readonly> + </div> + <div class="col-lg-6"> + <label for="inputMail" class="form-label">E-mail</label> + <input type="text" class="form-control" id="inputMail" name="email" readonly> + </div> + <div class="col-lg-6"> + <label for="inputPhone" class="form-label">Phone</label> + <input type="text" class="form-control" id="inputPhone" name="phone_number" readonly></input> + </div> + <div class="col-lg-6"> + <label for="inputAddress" class="form-label">Address</label> + <input type="text" class="form-control" id="inputAddress" name="address" readonly></input> + </div> + <label for="inputAddress" class="form-label fs-5">Challenges</label> + + <div class="col-lg-4"> + <div id="challenge-control"></div> + + </div> + <div class="col-lg-4"> + + </div> + <div class="col-lg-6"></div> + <div class="col-lg-6"></div> + <div class="col-lg-6"></div> + </div> + </form> + </div> + </div> + <template id="template-challenge-joined"> + <div class="col-lg-4"> + <div class="card mb-2" style="width: 18rem;"> + <div class="card-body"> + <h5 class="card-title"></h5> + <p id="card-body-completed"></p> + </div> + </div> + </div> + </template> + + <script src="scripts/defaults.js"></script> + <script src="scripts/scripts.js"></script> + <script src="scripts/profile.js"></script> + <script src="scripts/challenges.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> diff --git a/frontend/www/scripts/challenge.js b/frontend/www/scripts/challenge.js new file mode 100644 index 0000000000000000000000000000000000000000..5d04ed2c904b01151a1d2290fde71287dc571aef --- /dev/null +++ b/frontend/www/scripts/challenge.js @@ -0,0 +1,45 @@ +async function generateChallengeForm(athlete) { + let form = document.querySelector("#form-challenge"); + + let formData = new FormData(form); + let submitForm = new FormData(); + + let inputTitle = formData.get("challenge-title"); + submitForm.append("title", inputTitle); + + let response = await sendRequest("GET", `${HOST}/api/users/${athlete}/`); + let result = await response.json(); + submitForm.append("athlete", result.url); + + let currentUser = await getCurrentUser(); + submitForm.append("coach", currentUser.url); + return submitForm; +} + +async function createChallenge() { + let inputAthletes = document.querySelector("#challenge-user-input").value; + console.log(inputAthletes); + let athletes = inputAthletes.replaceAll(" ", "").split(","); + + for (let athlete of athletes) { + let submitForm = await generateChallengeForm(athlete); + let response = await sendRequest( + "POST", + `${HOST}/api/challenges/`, + submitForm, + "" + ); + + if (response.ok) { + console.log("GUCCI"); + } else { + let data = await response.json(); + let alert = createAlert("Could not create new challenge!", data); + document.body.prepend(alert); + } + } + document.getElementById("challenge-title-input").value = ""; + document.getElementById("challenge-user-input").value = ""; + window.location.reload(); + return; +} diff --git a/frontend/www/scripts/challenges.js b/frontend/www/scripts/challenges.js new file mode 100644 index 0000000000000000000000000000000000000000..0920e508e4160f975fcb0033fb7801a1a44c3388 --- /dev/null +++ b/frontend/www/scripts/challenges.js @@ -0,0 +1,51 @@ +async function fetchChallenges() { + let response = await sendRequest("GET", `${HOST}/api/challenges/`); + let currentUser = await getCurrentUser(); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } else { + let data = await response.json(); + + let challenges = data.results; + let container = document.getElementById("challenge-control"); + + let templateChallenge = document.querySelector( + "#template-challenge-joined" + ); + + challenges + .filter((challenge) => challenge.athlete == currentUser.url) + .forEach((challenge) => { + let cloneChallenge = templateChallenge.content.cloneNode(true); + let titleChallenge = cloneChallenge.querySelector("h5"); + let statusChallenge = cloneChallenge.querySelector( + "#card-body-completed" + ); + + // let h5 = titleChallenge.querySelector("h5"); + titleChallenge.innerHTML = challenge.title; + statusChallenge.innerHTML = challenge.completed + ? "Completed" + : "Not Completed"; + + container.appendChild(cloneChallenge); + }); + return challenges; + } +} + +window.addEventListener("DOMContentLoaded", async () => { + await fetchChallenges(); + + // let buttonSetCoach = document.querySelector("#button-set-coach"); + // let buttonEditCoach = document.querySelector("#button-edit-coach"); + // let buttonCancelCoach = document.querySelector("#button-cancel-coach"); + + // buttonSetCoach.addEventListener( + // "click", + // async (event) => await setCoach(event) + // ); + // buttonEditCoach.addEventListener("click", editCoach); + // buttonCancelCoach.addEventListener("click", cancelCoach); +}); diff --git a/frontend/www/scripts/exercise.js b/frontend/www/scripts/exercise.js index f845fe1844b633cf1b0bf1365eee4323c4c84bcc..81af9320ec690a67be3db463836eab1b8b00d006 100644 --- a/frontend/www/scripts/exercise.js +++ b/frontend/www/scripts/exercise.js @@ -4,36 +4,32 @@ let deleteButton; let editButton; let oldFormData; -class MuscleGroup { - constructor(type) { - this.isValidType = false; - this.validTypes = ["Legs", "Chest", "Back", "Arms", "Abdomen", "Shoulders"] +class MuscleGroup { + isValidType = false; + validTypes = ["Legs", "Chest", "Back", "Arms", "Abdomen", "Shoulders"]; + constructor(type) { this.type = this.validTypes.includes(type) ? type : undefined; - }; + } setMuscleGroupType = (newType) => { - this.isValidType = false; - - if(this.validTypes.includes(newType)){ - this.isValidType = true; - this.type = newType; - } - else{ + if (!this.validTypes.includes(newType)) { + this.isValidType = false; alert("Invalid muscle group!"); + return; } - + this.isValidType = true; + this.type = newType; }; - + getMuscleGroupType = () => { - console.log(this.type, "SWIOEFIWEUFH") return this.type; - } + }; } -function handleCancelButtonDuringEdit() { +function handleCancelButtonDuringEdit(e, afterUpdate = false) { setReadOnly(true, "#form-exercise"); - document.querySelector("select").setAttribute("disabled", "") + document.querySelector("select").setAttribute("disabled", ""); okButton.className += " hide"; deleteButton.className += " hide"; cancelButton.className += " hide"; @@ -41,21 +37,22 @@ function handleCancelButtonDuringEdit() { cancelButton.removeEventListener("click", handleCancelButtonDuringEdit); - let form = document.querySelector("#form-exercise"); - if (oldFormData.has("name")) form.name.value = oldFormData.get("name"); - if (oldFormData.has("description")) form.description.value = oldFormData.get("description"); - if (oldFormData.has("duration")) form.duration.value = oldFormData.get("duration"); - if (oldFormData.has("calories")) form.calories.value = oldFormData.get("calories"); - if (oldFormData.has("muscleGroup")) form.muscleGroup.value = oldFormData.get("muscleGroup"); - if (oldFormData.has("unit")) form.unit.value = oldFormData.get("unit"); - + if (!afterUpdate) { + let form = document.querySelector("#form-exercise"); + if (oldFormData.has("name")) form.name.value = oldFormData.get("name"); + if (oldFormData.has("description")) form.description.value = oldFormData.get("description"); + if (oldFormData.has("duration")) form.duration.value = oldFormData.get("duration"); + if (oldFormData.has("calories")) form.calories.value = oldFormData.get("calories"); + if (oldFormData.has("muscleGroup")) form.muscleGroup.value = oldFormData.get("muscleGroup"); + if (oldFormData.has("unit")) form.unit.value = oldFormData.get("unit"); + } + oldFormData.delete("name"); oldFormData.delete("description"); oldFormData.delete("duration"); oldFormData.delete("calories"); oldFormData.delete("muscleGroup"); oldFormData.delete("unit"); - } function handleCancelButtonDuringCreate() { @@ -63,15 +60,17 @@ function handleCancelButtonDuringCreate() { } async function createExercise() { - document.querySelector("select").removeAttribute("disabled") + document.querySelector("select").removeAttribute("disabled"); let form = document.querySelector("#form-exercise"); let formData = new FormData(form); - let body = {"name": formData.get("name"), - "description": formData.get("description"), - "duration": formData.get("duration"), - "calories": formData.get("calories"), - "muscleGroup": formData.get("muscleGroup"), - "unit": formData.get("unit")}; + let body = { + name: formData.get("name"), + description: formData.get("description"), + duration: formData.get("duration"), + calories: formData.get("calories"), + muscleGroup: formData.get("muscleGroup"), + unit: formData.get("unit"), + }; let response = await sendRequest("POST", `${HOST}/api/exercises/`, body); @@ -87,7 +86,7 @@ async function createExercise() { function handleEditExerciseButtonClick() { setReadOnly(false, "#form-exercise"); - document.querySelector("select").removeAttribute("disabled") + document.querySelector("select").removeAttribute("disabled"); editButton.className += " hide"; okButton.className = okButton.className.replace(" hide", ""); @@ -114,26 +113,26 @@ async function deleteExercise(id) { async function retrieveExercise(id) { let response = await sendRequest("GET", `${HOST}/api/exercises/${id}/`); - console.log(response.ok) + 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 { - document.querySelector("select").removeAttribute("disabled") + document.querySelector("select").removeAttribute("disabled"); let exerciseData = await response.json(); let form = document.querySelector("#form-exercise"); let formData = new FormData(form); for (let key of formData.keys()) { - let selector - key !== "muscleGroup" ? selector = `input[name="${key}"], textarea[name="${key}"]` : selector = `select[name=${key}]` + let selector = + key !== "muscleGroup" ? `input[name="${key}"], textarea[name="${key}"]` : `select[name=${key}]`; let input = form.querySelector(selector); let newVal = exerciseData[key]; input.value = newVal; } - document.querySelector("select").setAttribute("disabled", "") + document.querySelector("select").setAttribute("disabled", ""); } } @@ -141,17 +140,19 @@ async function updateExercise(id) { let form = document.querySelector("#form-exercise"); let formData = new FormData(form); - let muscleGroupSelector = document.querySelector("select") - muscleGroupSelector.removeAttribute("disabled") + let muscleGroupSelector = document.querySelector("select"); + muscleGroupSelector.removeAttribute("disabled"); let selectedMuscleGroup = new MuscleGroup(formData.get("muscleGroup")); - let body = {"name": formData.get("name"), - "description": formData.get("description"), - "duration": formData.get("duration"), - "calories": formData.get("calories"), - "muscleGroup": selectedMuscleGroup.getMuscleGroupType(), - "unit": formData.get("unit")}; + let body = { + name: formData.get("name"), + description: formData.get("description"), + duration: formData.get("duration"), + calories: formData.get("calories"), + muscleGroup: selectedMuscleGroup.getMuscleGroupType(), + unit: formData.get("unit"), + }; let response = await sendRequest("PUT", `${HOST}/api/exercises/${id}/`, body); if (!response.ok) { @@ -159,23 +160,7 @@ async function updateExercise(id) { let alert = createAlert(`Could not update exercise ${id}`, data); document.body.prepend(alert); } else { - muscleGroupSelector.setAttribute("disabled", "") - // duplicate code from handleCancelButtonDuringEdit - // you should refactor this - setReadOnly(true, "#form-exercise"); - okButton.className += " hide"; - deleteButton.className += " hide"; - cancelButton.className += " hide"; - editButton.className = editButton.className.replace(" hide", ""); - - cancelButton.removeEventListener("click", handleCancelButtonDuringEdit); - - oldFormData.delete("name"); - oldFormData.delete("description"); - oldFormData.delete("duration"); - oldFormData.delete("calories"); - oldFormData.delete("muscleGroup"); - oldFormData.delete("unit"); + handleCancelButtonDuringEdit(true); } } @@ -189,23 +174,22 @@ window.addEventListener("DOMContentLoaded", async () => { const urlParams = new URLSearchParams(window.location.search); // view/edit - if (urlParams.has('id')) { - const exerciseId = urlParams.get('id'); + 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)); - } + deleteButton.addEventListener("click", (async (id) => deleteExercise(id)).bind(undefined, exerciseId)); + okButton.addEventListener("click", (async (id) => updateExercise(id)).bind(undefined, exerciseId)); + return; + } //create - else { - setReadOnly(false, "#form-exercise"); + setReadOnly(false, "#form-exercise"); - editButton.className += " hide"; - okButton.className = okButton.className.replace(" hide", ""); - cancelButton.className = cancelButton.className.replace(" hide", ""); + 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 + okButton.addEventListener("click", async () => createExercise()); + cancelButton.addEventListener("click", handleCancelButtonDuringCreate); +}); diff --git a/frontend/www/scripts/gallery.js b/frontend/www/scripts/gallery.js index f9c07b449947470c8df29c8f51894758cf38c025..6ceb0878f9a8d9a00fadf30735cf52bd36019b48 100644 --- a/frontend/www/scripts/gallery.js +++ b/frontend/www/scripts/gallery.js @@ -1,105 +1,110 @@ let goBackButton; let submitNewFileButton; -async function retrieveWorkoutImages(id) { +async function retrieveWorkoutImages(id) { let workoutData = null; let response = await sendRequest("GET", `${HOST}/api/workouts/${id}/`); if (!response.ok) { let data = await response.json(); let alert = createAlert("Could not retrieve workout data!", data); document.body.prepend(alert); - } else { - workoutData = await response.json(); + return workoutData; + } + workoutData = await response.json(); - document.getElementById("workout-title").innerHTML = "Workout name: " + workoutData["name"]; - document.getElementById("workout-owner").innerHTML = "Owner: " + workoutData["owner_username"]; + document.getElementById("workout-title").innerHTML = "Workout name: " + workoutData["name"]; + document.getElementById("workout-owner").innerHTML = "Owner: " + workoutData["owner_username"]; - let hasNoImages = workoutData.files.length == 0; - let noImageText = document.querySelector("#no-images-text"); + let hasNoImages = workoutData.files.length == 0; + let noImageText = document.querySelector("#no-images-text"); - if(hasNoImages){ - noImageText.classList.remove("hide"); - return; - } + if (hasNoImages) { + noImageText.classList.remove("hide"); + return; + } - noImageText.classList.add("hide"); - - - let filesDiv = document.getElementById("img-collection"); - let filesDeleteDiv = document.getElementById("img-collection-delete"); - - const currentImageFileElement = document.querySelector("#current"); - let isFirstImg = true; - - let fileCounter = 0; - - for (let file of workoutData.files) { - let a = document.createElement("a"); - a.href = file.file; - let pathArray = file.file.split("/"); - a.text = pathArray[pathArray.length - 1]; - a.className = "me-2"; - - - - let isImage = ["jpg", "png", "gif", "jpeg", "JPG", "PNG", "GIF", "JPEG"].includes(a.text.split(".")[1]); - - if(isImage){ - let deleteImgButton = document.createElement("input"); - deleteImgButton.type = "button"; - deleteImgButton.className = "btn btn-close"; - deleteImgButton.id = file.url.split("/")[file.url.split("/").length - 2]; - deleteImgButton.addEventListener('click', () => handleDeleteImgClick(deleteImgButton.id, "DELETE", `Could not delete workout ${deleteImgButton.id}!`, HOST, ["jpg", "png", "gif", "jpeg", "JPG", "PNG", "GIF", "JPEG"])); - filesDeleteDiv.appendChild(deleteImgButton); - - let img = document.createElement("img"); - img.src = file.file; - - filesDiv.appendChild(img); - deleteImgButton.style.left = `${(fileCounter % 4) * 191}px`; - deleteImgButton.style.top = `${Math.floor(fileCounter / 4) * 105}px`; - - if(isFirstImg){ - currentImageFileElement.src = file.file; - isFirstImg = false; - } - fileCounter++; + noImageText.classList.add("hide"); + + let filesDiv = document.getElementById("img-collection"); + let filesDeleteDiv = document.getElementById("img-collection-delete"); + + const currentImageFileElement = document.querySelector("#current"); + let isFirstImg = true; + + let fileCounter = 0; + + for (let file of workoutData.files) { + let a = document.createElement("a"); + a.href = file.file; + let pathArray = file.file.split("/"); + a.text = pathArray[pathArray.length - 1]; + a.className = "me-2"; + + let isImage = ["jpg", "png", "gif", "jpeg", "JPG", "PNG", "GIF", "JPEG"].includes(a.text.split(".")[1]); + + if (isImage) { + let deleteImgButton = document.createElement("input"); + deleteImgButton.type = "button"; + deleteImgButton.className = "btn btn-close"; + deleteImgButton.id = file.url.split("/")[file.url.split("/").length - 2]; + deleteImgButton.addEventListener("click", () => + handleDeleteImgClick( + deleteImgButton.id, + "DELETE", + `Could not delete workout ${deleteImgButton.id}!`, + HOST, + ["jpg", "png", "gif", "jpeg", "JPG", "PNG", "GIF", "JPEG"] + ) + ); + filesDeleteDiv.appendChild(deleteImgButton); + + let img = document.createElement("img"); + img.src = file.file; + + filesDiv.appendChild(img); + deleteImgButton.style.left = `${(fileCounter % 4) * 191}px`; + deleteImgButton.style.top = `${Math.floor(fileCounter / 4) * 105}px`; + + if (isFirstImg) { + currentImageFileElement.src = file.file; + isFirstImg = false; } + fileCounter++; } + } - const otherImageFileElements = document.querySelectorAll(".imgs img"); - const selectedOpacity = 0.6; - otherImageFileElements[0].style.opacity = selectedOpacity; + const otherImageFileElements = document.querySelectorAll(".imgs img"); + const selectedOpacity = 0.6; + otherImageFileElements[0].style.opacity = selectedOpacity; - otherImageFileElements.forEach((imageFileElement) => imageFileElement.addEventListener("click", (event) => { + otherImageFileElements.forEach((imageFileElement) => + imageFileElement.addEventListener("click", (event) => { //Changes the main image currentImageFileElement.src = event.target.src; //Adds the fade animation - currentImageFileElement.classList.add('fade-in') - setTimeout(() => currentImageFileElement.classList.remove('fade-in'), 500); + currentImageFileElement.classList.add("fade-in"); + setTimeout(() => currentImageFileElement.classList.remove("fade-in"), 500); //Sets the opacity of the selected image to 0.4 - otherImageFileElements.forEach((imageFileElement) => imageFileElement.style.opacity = 1) + otherImageFileElements.forEach((otherimageFileElement) => (otherimageFileElement.style.opacity = 1)); event.target.style.opacity = selectedOpacity; - })) - - } - return workoutData; + }) + ); + return workoutData; } async function validateImgFileType(id, host_variable, acceptedFileTypes) { let file = await sendRequest("GET", `${host_variable}/api/workout-files/${id}/`); let fileData = await file.json(); let fileType = fileData.file.split("/")[fileData.file.split("/").length - 1].split(".")[1]; - + return acceptedFileTypes.includes(fileType); } -async function handleDeleteImgClick (id, http_keyword, fail_alert_text, host_variable, acceptedFileTypes) { - - if(validateImgFileType(id, host_variable, acceptedFileTypes, )){ - return +async function handleDeleteImgClick(id, http_keyword, fail_alert_text, host_variable, acceptedFileTypes) { + if (validateImgFileType(id, host_variable, acceptedFileTypes)) { + return; } let response = await sendRequest(http_keyword, `${host_variable}/api/workout-files/${id}/`); @@ -115,17 +120,15 @@ async function handleDeleteImgClick (id, http_keyword, fail_alert_text, host_var function handleGoBackToWorkoutClick() { const urlParams = new URLSearchParams(window.location.search); - const id = urlParams.get('id'); + const id = urlParams.get("id"); window.location.replace(`workout.html?id=${id}`); } window.addEventListener("DOMContentLoaded", async () => { - goBackButton = document.querySelector("#btn-back-workout"); - goBackButton.addEventListener('click', handleGoBackToWorkoutClick); + goBackButton.addEventListener("click", handleGoBackToWorkoutClick); const urlParams = new URLSearchParams(window.location.search); - const id = urlParams.get('id'); - let workoutData = await retrieveWorkoutImages(id); - -}); \ No newline at end of file + const id = urlParams.get("id"); + await retrieveWorkoutImages(id); +}); diff --git a/frontend/www/scripts/group.js b/frontend/www/scripts/group.js new file mode 100644 index 0000000000000000000000000000000000000000..51b5a1ebad408df1f3ebb9135caed7e21d4bb54b --- /dev/null +++ b/frontend/www/scripts/group.js @@ -0,0 +1,47 @@ +async function generateGroupForm() { + let form = document.querySelector("#form-group"); + + let formData = new FormData(form); + let submitForm = new FormData(); + + let input = formData.get("group"); + let groupName = input.split(":")[0]; + submitForm.append("name", groupName); + + let athletes = input.split(":")[1].replaceAll(" ", "").split(","); + let currentUser = await getCurrentUser(); + + for (let athlete of athletes) { + let response = await sendRequest( + "GET", + `${HOST}/api/users/${athlete}/` + ); + let result = await response.json(); + submitForm.append("athletes", result.url); + } + + console.log(currentUser.url); + submitForm.append("coach", currentUser.url); + return submitForm; +} + +async function createGroup() { + let submitForm = await generateGroupForm(); + + let response = await sendRequest( + "POST", + `${HOST}/api/groups/`, + submitForm, + "" + ); + + if (response.ok) { + document.getElementById("group-input").value = ""; + window.location.reload(); + return; + } else { + let data = await response.json(); + let alert = createAlert("Could not create new workout!", data); + document.body.prepend(alert); + } +} diff --git a/frontend/www/scripts/groups.js b/frontend/www/scripts/groups.js new file mode 100644 index 0000000000000000000000000000000000000000..0134c6c4448f3490d4d466587a12224e5b7a0d47 --- /dev/null +++ b/frontend/www/scripts/groups.js @@ -0,0 +1,12 @@ +async function getGroups() { + let groups = null; + let response = await sendRequest("GET", `${HOST}/api/groups/`); + if (!response.ok) { + console.log("COULD NOT RETRIEVE GROUPS"); + } else { + let data = await response.json(); + groups = data.results; + } + + return groups; +} diff --git a/frontend/www/scripts/myathletes.js b/frontend/www/scripts/myathletes.js index e02c573e3f1b0e7497a86b658a493bfd7ba45135..48f2a65b0ea57b1ccb64424366b3177d44ea53ce 100644 --- a/frontend/www/scripts/myathletes.js +++ b/frontend/www/scripts/myathletes.js @@ -1,6 +1,10 @@ async function displayCurrentRoster() { - let templateFilledAthlete = document.querySelector("#template-filled-athlete"); - let templateEmptyAthlete = document.querySelector("#template-empty-athlete"); + let templateFilledAthlete = document.querySelector( + "#template-filled-athlete" + ); + let templateEmptyAthlete = document.querySelector( + "#template-empty-athlete" + ); let controls = document.querySelector("#controls"); let currentUser = await getCurrentUser(); @@ -8,12 +12,20 @@ async function displayCurrentRoster() { let response = await sendRequest("GET", athleteUrl); let athlete = await response.json(); - createFilledRow(templateFilledAthlete, athlete.username, controls, false); + createFilledRow( + templateFilledAthlete, + athlete.username, + controls, + false + ); } - - let status = "p"; // pending - let category = "sent"; - let response = await sendRequest("GET", `${HOST}/api/offers/?status=${status}&category=${category}`); + + let status = "p"; // pending + let category = "sent"; + let response = await sendRequest( + "GET", + `${HOST}/api/offers/?status=${status}&category=${category}` + ); if (!response.ok) { let data = await response.json(); let alert = createAlert("Could not retrieve offers!", data); @@ -24,7 +36,12 @@ async function displayCurrentRoster() { for (let offer of offers.results) { let response = await sendRequest("GET", offer.recipient); let recipient = await response.json(); - createFilledRow(templateFilledAthlete, `${recipient.username} (pending)`, controls, true); + createFilledRow( + templateFilledAthlete, + `${recipient.username} (pending)`, + controls, + true + ); } } @@ -32,10 +49,16 @@ async function displayCurrentRoster() { let emptyDiv = emptyClone.querySelector("div"); let emptyButton = emptyDiv.querySelector("button"); emptyButton.addEventListener("click", addAthleteRow); + controls.appendChild(emptyDiv); } -function createFilledRow(templateFilledAthlete, inputValue, controls, disabled) { +function createFilledRow( + templateFilledAthlete, + inputValue, + controls, + disabled +) { let filledClone = templateFilledAthlete.content.cloneNode(true); let filledDiv = filledClone.querySelector("div"); let filledInput = filledDiv.querySelector("input"); @@ -44,13 +67,27 @@ function createFilledRow(templateFilledAthlete, inputValue, controls, disabled) filledInput.disabled = disabled; if (!disabled) { filledButton.addEventListener("click", removeAthleteRow); - } - else { + } else { filledButton.disabled = true; } controls.appendChild(filledDiv); } +function createFilledGroupRow( + templateFilledGroup, + inputValue, + controls, + disabled +) { + let filledClone = templateFilledGroup.content.cloneNode(true); + let filledDiv = filledClone.querySelector("div"); + let filledInput = filledDiv.querySelector("#card-body"); + let filledButton = filledDiv.querySelector("button"); + filledInput.innerHTML = inputValue; + + controls.appendChild(filledDiv); +} + async function displayFiles() { let user = await getCurrentUser(); @@ -67,10 +104,18 @@ async function displayFiles() { response = await sendRequest("GET", file.athlete); let athlete = await response.json(); - let tabPanel = document.querySelector(`#tab-contents-${athlete.username}`) + let tabPanel = document.querySelector( + `#tab-contents-${athlete.username}` + ); if (!tabPanel) { - tabPanel = createTabContents(templateAthlete, athlete, listTab, templateFiles, navTabContent); - } + tabPanel = createTabContents( + templateAthlete, + athlete, + listTab, + templateFiles, + navTabContent + ); + } let divFiles = tabPanel.querySelector(".uploaded-files"); let aFile = createFileLink(templateFile, file.file); @@ -82,13 +127,26 @@ async function displayFiles() { let response = await sendRequest("GET", athleteUrl); let athlete = await response.json(); - let tabPanel = document.querySelector(`#tab-contents-${athlete.username}`) + let tabPanel = document.querySelector( + `#tab-contents-${athlete.username}` + ); if (!tabPanel) { - tabPanel = createTabContents(templateAthlete, athlete, listTab, templateFiles, navTabContent); + tabPanel = createTabContents( + templateAthlete, + athlete, + listTab, + templateFiles, + navTabContent + ); } - let uploadBtn = document.querySelector(`#btn-upload-${athlete.username}`); + let uploadBtn = document.querySelector( + `#btn-upload-${athlete.username}` + ); uploadBtn.disabled = false; - uploadBtn.addEventListener("click", async (event) => await uploadFiles(event, athlete)); + uploadBtn.addEventListener( + "click", + async (event) => await uploadFiles(event, athlete) + ); let fileInput = tabPanel.querySelector(".form-control"); fileInput.disabled = false; @@ -101,7 +159,44 @@ async function displayFiles() { } } -function createTabContents(templateAthlete, athlete, listTab, templateFiles, navTabContent) { +async function displayGroups() { + let templateFilledGroup = document.querySelector("#template-filled-group"); + let groupControls = document.querySelector("#group-control"); + + // let currentUser = await getCurrentUser(); + let groups = await getGroups(); + + // console.log(currentUser); + for (let group of groups) { + let members = ""; + for (let athleteUrl of group.athletes) { + let response = await sendRequest("GET", athleteUrl); + let athlete = await response.json(); + members += athlete.username + ", "; + } + if (members.length > 0) { + members = members.substring(0, members.length - 2); + } + + // let response = await sendRequest("GET", groupUrl); + // let group = await response.json(); + + createFilledGroupRow( + templateFilledGroup, + "<strong>" + group.name + "</strong><br />" + members, + groupControls, + false + ); + } +} + +function createTabContents( + templateAthlete, + athlete, + listTab, + templateFiles, + navTabContent +) { let cloneAthlete = templateAthlete.content.cloneNode(true); let a = cloneAthlete.querySelector("a"); @@ -144,37 +239,65 @@ function addAthleteRow(event) { event.currentTarget.addEventListener("click", removeAthleteRow); } +function addGroupRow(event) { + let templateFilledGroup = document.querySelector("#template-filled-group"); + let groupControls = document.querySelector("#group-control"); + + let filledGroupClone = templateFilledGroup.content.cloneNode(true); + let filledGroupDiv = filledGroupClone.querySelector("div"); + let filledGroupButton = filledGroupDiv.querySelector("button"); + filledGroupButton.addEventListener("click", removeGroupRow); + + groupControls.appendChild(filledGroupDiv); +} + function removeAthleteRow(event) { event.currentTarget.parentElement.remove(); } +function removeGroupRow(event) { + event.currentTarget.parentElement.remove(); +} async function submitRoster() { let rosterInputs = document.querySelectorAll('input[name="athlete"]'); - let body = {"athletes": []}; + let body = { athletes: [] }; let currentUser = await getCurrentUser(); for (let rosterInput of rosterInputs) { if (!rosterInput.disabled && rosterInput.value) { // get user - let response = await sendRequest("GET", `${HOST}/api/users/${rosterInput.value}/`); + let response = await sendRequest( + "GET", + `${HOST}/api/users/${rosterInput.value}/` + ); if (response.ok) { let athlete = await response.json(); if (athlete.coach == currentUser.url) { body.athletes.push(athlete.id); } else { // create offer - let body = {'status': 'p', 'recipient': athlete.url}; - let response = await sendRequest("POST", `${HOST}/api/offers/`, body); + let body = { status: "p", recipient: athlete.url }; + let response = await sendRequest( + "POST", + `${HOST}/api/offers/`, + body + ); if (!response.ok) { let data = await response.json(); - let alert = createAlert("Could not create offer!", data); + let alert = createAlert( + "Could not create offer!", + data + ); document.body.prepend(alert); } } } else { let data = await response.json(); - let alert = createAlert(`Could not retrieve user ${rosterInput.value}!`, data); + let alert = createAlert( + `Could not retrieve user ${rosterInput.value}!`, + data + ); document.body.prepend(alert); } } @@ -183,6 +306,62 @@ async function submitRoster() { location.reload(); } +async function submitGroup() { + // return; + let groupInput = document.querySelector("#group-input").value; + + let users = groupInput.replaceAll(" ", "").split(","); + console.log(users); + + // let body = { athletes: [] }; + let currentUser = await getCurrentUser(); + + // Check if all users exist + for (let user of users) { + // get user + let response = await sendRequest("GET", `${HOST}/api/users/${user}/`); + if (!response.ok) { + console.log("User does not exist"); + return; + } + } + // Create group + + // Check if all users exist + for (let user of users) { + // get user + let response = await sendRequest("GET", `${HOST}/api/users/${user}/`); + if (response.ok) { + let athlete = await response.json(); + if (athlete.coach == currentUser.url) { + body.athletes.push(athlete.id); + } else { + // create offer + let body = { status: "p", recipient: athlete.url }; + let response = await sendRequest( + "POST", + `${HOST}/api/offers/`, + body + ); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not create offer!", data); + document.body.prepend(alert); + } + } + } else { + let data = await response.json(); + let alert = createAlert( + `Could not retrieve user ${rosterInput.value}!`, + data + ); + document.body.prepend(alert); + } + } + let response = await sendRequest("PUT", currentUser.url, body); + location.reload(); +} + async function uploadFiles(event, athlete) { let form = event.currentTarget.parentElement; let inputFormData = new FormData(form); @@ -194,11 +373,18 @@ async function uploadFiles(event, athlete) { submitForm.append("file", file); submitForm.append("athlete", athlete.url); - let response = await sendRequest("POST", `${HOST}/api/athlete-files/`, submitForm, ""); + let response = await sendRequest( + "POST", + `${HOST}/api/athlete-files/`, + submitForm, + "" + ); if (response.ok) { let data = await response.json(); - let tabPanel = document.querySelector(`#tab-contents-${athlete.username}`) + let tabPanel = document.querySelector( + `#tab-contents-${athlete.username}` + ); let divFiles = tabPanel.querySelector(".uploaded-files"); let aFile = createFileLink(templateFile, data["file"]); divFiles.appendChild(aFile); @@ -214,7 +400,23 @@ async function uploadFiles(event, athlete) { window.addEventListener("DOMContentLoaded", async () => { await displayCurrentRoster(); await displayFiles(); - + await displayGroups(); + let buttonSubmitRoster = document.querySelector("#button-submit-roster"); - buttonSubmitRoster.addEventListener("click", async () => await submitRoster()); + buttonSubmitRoster.addEventListener( + "click", + async () => await submitRoster() + ); + let buttonSubmitGroup = document.querySelector("#button-submit-group"); + let buttonSubmitChallenge = document.querySelector( + "#button-submit-challenge" + ); + buttonSubmitGroup.addEventListener( + "click", + async () => await createGroup() + ); + buttonSubmitChallenge.addEventListener( + "click", + async () => await createChallenge() + ); }); diff --git a/frontend/www/scripts/mycoach.js b/frontend/www/scripts/mycoach.js index 9441ba15efffbd0e6f6ca063d1b400b67f1bdf19..b12f78dffd4e44d625cc16cc4e11dee584f38c92 100644 --- a/frontend/www/scripts/mycoach.js +++ b/frontend/www/scripts/mycoach.js @@ -14,7 +14,7 @@ async function displayCurrentCoach() { input.value = coach.username; } else { - console.log("NO USER.COACH") + console.log("NO USER.COACH"); } } @@ -24,9 +24,12 @@ async function displayOffers() { let templateOffer = document.querySelector("#template-offer"); let listOffers = document.querySelector("#list-offers"); - let status = "p"; // pending - let category = "received"; - let response = await sendRequest("GET", `${HOST}/api/offers/?status=${status}&category=${category}`); + let status = "p"; // pending + let category = "received"; + let response = await sendRequest( + "GET", + `${HOST}/api/offers/?status=${status}&category=${category}` + ); if (!response.ok) { let data = await response.json(); let alert = createAlert("Could not retrieve offers!", data); @@ -38,16 +41,23 @@ async function displayOffers() { let li = cloneOffer.querySelector("li"); let span = li.querySelector("span"); span.textContent = `${offer.owner} wants to be your coach`; - + let buttons = li.querySelectorAll("button"); let acceptButton = buttons[0]; let declineButton = buttons[1]; //acceptButton.id = `btn-accept-${offer.id}`; - acceptButton.addEventListener("click", async (event) => await acceptOffer(event, offer.url, offer.owner)); + acceptButton.addEventListener( + "click", + async (event) => + await acceptOffer(event, offer.url, offer.owner) + ); //declineButton.id = `btn-decline-${offer.id}`; - declineButton.addEventListener("click", async (event) => await declineOffer(event, offer.url)); + declineButton.addEventListener( + "click", + async (event) => await declineOffer(event, offer.url) + ); listOffers.appendChild(li); } @@ -62,7 +72,7 @@ async function displayOffers() { async function acceptOffer(event, offerUrl, ownerUsername) { let button = event.currentTarget; - let body = {"status": "d"}; + let body = { status: "d" }; let response = await sendRequest("PATCH", offerUrl, body); if (!response.ok) { @@ -70,11 +80,14 @@ async function acceptOffer(event, offerUrl, ownerUsername) { let alert = createAlert("Could not accept offer!", data); document.body.prepend(alert); } else { - let response = await sendRequest("GET", `${HOST}/api/users/${ownerUsername}/`); + let response = await sendRequest( + "GET", + `${HOST}/api/users/${ownerUsername}/` + ); let owner = await response.json(); let user = await getCurrentUser(); - let body = {'coach': owner.url}; + let body = { coach: owner.url }; response = await sendRequest("PATCH", user.url, body); if (!response.ok) { @@ -86,12 +99,11 @@ async function acceptOffer(event, offerUrl, ownerUsername) { return false; } } - } async function declineOffer(event, offerUrl) { let button = event.currentTarget; - let body = {'status': 'd'}; + let body = { status: "d" }; let response = await sendRequest("PATCH", offerUrl, body); if (!response.ok) { @@ -109,7 +121,7 @@ async function displayFiles() { let templateOwner = document.querySelector("#template-owner-tab"); let templateFiles = document.querySelector("#template-files"); - let templateFile = document.querySelector("#template-file") + let templateFile = document.querySelector("#template-file"); let listTab = document.querySelector("#list-tab"); let navTabContent = document.querySelector("#nav-tabContent"); @@ -155,7 +167,7 @@ async function displayFiles() { } async function getReceivedRequests() { - let response = await sendRequest("GET", `${HOST}/api/athlete-requests/`) + let response = await sendRequest("GET", `${HOST}/api/athlete-requests/`); if (!response.ok) { let data = await response.json(); let alert = createAlert("Could not retrieve athlete request!", data); @@ -164,13 +176,19 @@ async function getReceivedRequests() { let data = await response.json(); let athleteRequests = data.results; for (let athleteRequest of athleteRequests) { - if (athleteRequest.recipient == sessionStorage.getItem("username")) { - let div = document.querySelector("#div-received-athlete-requests"); - let template = document.querySelector("#template-athlete-request"); + if ( + athleteRequest.recipient == sessionStorage.getItem("username") + ) { + let div = document.querySelector( + "#div-received-athlete-requests" + ); + let template = document.querySelector( + "#template-athlete-request" + ); let clone = template.content.firstElementChild.cloneNode(true); let button = clone.querySelector("button"); - button.textContent = `${athleteRequest.owner} wants to be your coach!` + button.textContent = `${athleteRequest.owner} wants to be your coach!`; div.appendChild(clone); } @@ -187,7 +205,10 @@ function editCoach(event) { buttonEditCoach.className += " hide"; buttonSetCoach.className = buttonSetCoach.className.replace(" hide", ""); - buttonCancelCoach.className = buttonCancelCoach.className.replace(" hide", ""); + buttonCancelCoach.className = buttonCancelCoach.className.replace( + " hide", + "" + ); } function cancelCoach() { @@ -201,19 +222,25 @@ async function setCoach() { let newCoach = document.querySelector("#input-coach").value; let body = {}; if (!newCoach) { - body['coach'] = null; + body["coach"] = null; } else { - let response = await sendRequest("GET", `${HOST}/api/users/${newCoach}/`) + let response = await sendRequest( + "GET", + `${HOST}/api/users/${newCoach}/` + ); if (!response.ok) { let data = await response.json(); - let alert = createAlert(`Could not retrieve user ${newCoach}`, data); + let alert = createAlert( + `Could not retrieve user ${newCoach}`, + data + ); document.body.prepend(alert); } let newCoachObject = await response.json(); - body['coach'] = newCoachObject.url; + body["coach"] = newCoachObject.url; } - if ('coach' in body) { + if ("coach" in body) { let response = await sendRequest("PATCH", user.url, body); if (!response.ok) { let data = await response.json(); @@ -234,8 +261,11 @@ window.addEventListener("DOMContentLoaded", async () => { let buttonSetCoach = document.querySelector("#button-set-coach"); let buttonEditCoach = document.querySelector("#button-edit-coach"); let buttonCancelCoach = document.querySelector("#button-cancel-coach"); - - buttonSetCoach.addEventListener("click", async (event) => await setCoach(event)); + + buttonSetCoach.addEventListener( + "click", + async (event) => await setCoach(event) + ); buttonEditCoach.addEventListener("click", editCoach); buttonCancelCoach.addEventListener("click", cancelCoach); -}); \ No newline at end of file +}); diff --git a/frontend/www/scripts/navbar.js b/frontend/www/scripts/navbar.js index 8dec8c1a20089ea9bbb85e5df99001473ec64c2c..2e87897562c4c1200e96a25e49d09a97d432c76f 100644 --- a/frontend/www/scripts/navbar.js +++ b/frontend/www/scripts/navbar.js @@ -24,15 +24,14 @@ class NavBar extends HTMLElement { <div class="my-2 my-lg-0 me-5"> <a role="button" class="btn btn-primary hide" id="btn-register" href="register.html">Register</a> <a role="button" class="btn btn-secondary hide" id="btn-login-nav" href="login.html">Log in</a> + <a role="button" class="btn btn-primary hide" id="btn-logout" href="profile.html">Profile</a> <a role="button" class="btn btn-secondary hide" id="btn-logout" href="logout.html">Log out</a> </div> </div> </div> </nav> `; - - } } -customElements.define('navbar-el', NavBar); \ No newline at end of file +customElements.define("navbar-el", NavBar); diff --git a/frontend/www/scripts/profile.js b/frontend/www/scripts/profile.js new file mode 100644 index 0000000000000000000000000000000000000000..e34c94de41542d4c4e16d982ac4555c8e2d994f9 --- /dev/null +++ b/frontend/www/scripts/profile.js @@ -0,0 +1,40 @@ +async function displayUser() { + let user = await getCurrentUser(); + + if (user) { + let form = document.querySelector("#form-user"); + let formData = new FormData(form); + + for (let key of formData.keys()) { + let selector; + key !== "muscleGroup" + ? (selector = `input[name="${key}"], textarea[name="${key}"]`) + : (selector = `select[name=${key}]`); + let input = form.querySelector(selector); + let newVal = user[key]; + if (!newVal) newVal = "N/A"; + input.value = newVal; + } + document.querySelector("select").setAttribute("disabled", ""); + } else { + let alert = createAlert("Could not find user", { + detail: "No user is found", + }); + document.body.prepend(alert); + } +} + +window.addEventListener("DOMContentLoaded", async () => { + await displayUser(); + + // let buttonSetCoach = document.querySelector("#button-set-coach"); + // let buttonEditCoach = document.querySelector("#button-edit-coach"); + // let buttonCancelCoach = document.querySelector("#button-cancel-coach"); + + // buttonSetCoach.addEventListener( + // "click", + // async (event) => await setCoach(event) + // ); + // buttonEditCoach.addEventListener("click", editCoach); + // buttonCancelCoach.addEventListener("click", cancelCoach); +}); diff --git a/frontend/www/scripts/scripts.js b/frontend/www/scripts/scripts.js index 9bfc1efb688400bcabfa9457b05be97f8dbd9c92..a87eb48083f0787acb2e3293efec796be908f02d 100644 --- a/frontend/www/scripts/scripts.js +++ b/frontend/www/scripts/scripts.js @@ -1,197 +1,217 @@ function makeNavLinkActive(id) { - let link = document.getElementById(id); - link.classList.add("active"); - link.setAttribute("aria-current", "page"); + let link = document.getElementById(id); + link.classList.add("active"); + link.setAttribute("aria-current", "page"); } function isUserAuthenticated() { - return (getCookieValue("access") != null) || (getCookieValue("refresh") != null); + return ( + getCookieValue("access") != null || getCookieValue("refresh") != null + ); } function updateNavBar() { - let nav = document.querySelector("nav"); - - // Emphasize link to current page - if (window.location.pathname == "/" || window.location.pathname == "/index.html") { - makeNavLinkActive("nav-index"); - } else if (window.location.pathname == "/workouts.html") { - makeNavLinkActive("nav-workouts"); - } else if (window.location.pathname == "/exercises.html") { - makeNavLinkActive("nav-exercises"); - } else if (window.location.pathname == "/mycoach.html") { - makeNavLinkActive("nav-mycoach") - } else if (window.location.pathname == "/myathletes.html") { - makeNavLinkActive("nav-myathletes"); - } else if (window.location.pathname == "/meals.html") { - makeNavLinkActive("nav-myathletes"); - } - - if (isUserAuthenticated()) { - document.getElementById("btn-logout").classList.remove("hide"); - - document.querySelector('a[href="logout.html"').classList.remove("hide"); - document.querySelector('a[href="workouts.html"').classList.remove("hide"); - 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="meals.html"').classList.remove("hide"); - } else { - document.getElementById("btn-login-nav").classList.remove("hide"); - document.getElementById("btn-register").classList.remove("hide"); - } + let nav = document.querySelector("nav"); + + // Emphasize link to current page + if ( + window.location.pathname == "/" || + window.location.pathname == "/index.html" + ) { + makeNavLinkActive("nav-index"); + } else if (window.location.pathname == "/workouts.html") { + makeNavLinkActive("nav-workouts"); + } else if (window.location.pathname == "/exercises.html") { + makeNavLinkActive("nav-exercises"); + } else if (window.location.pathname == "/mycoach.html") { + makeNavLinkActive("nav-mycoach"); + } else if (window.location.pathname == "/myathletes.html") { + makeNavLinkActive("nav-myathletes"); + } else if (window.location.pathname == "/meals.html") { + makeNavLinkActive("nav-myathletes"); + } + if (isUserAuthenticated()) { + document.getElementById("btn-logout").classList.remove("hide"); + + document.querySelector('a[href="logout.html"').classList.remove("hide"); + document + .querySelector('a[href="workouts.html"') + .classList.remove("hide"); + 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="meals.html"').classList.remove("hide"); + } else { + document.getElementById("btn-login-nav").classList.remove("hide"); + document.getElementById("btn-register").classList.remove("hide"); + } } - -function setCookie(name, value, maxage, path="") { - document.cookie = `${name}=${value}; max-age=${maxage}; path=${path}`; +function setCookie(name, value, maxage, path = "") { + document.cookie = `${name}=${value}; max-age=${maxage}; path=${path}`; } function deleteCookie(name) { - setCookie(name, "", 0, "/"); + setCookie(name, "", 0, "/"); } function getCookieValue(name) { - let cookieValue = null; - let cookieByName = document.cookie.split("; ").find(row => row.startsWith(name)); + let cookieValue = null; + let cookieByName = document.cookie + .split("; ") + .find((row) => row.startsWith(name)); - if (cookieByName) { - cookieValue = cookieByName.split("=")[1]; - } + if (cookieByName) { + cookieValue = cookieByName.split("=")[1]; + } - return cookieValue; + return cookieValue; } -async function sendRequest(method, url, body, contentType="application/json; charset=UTF-8") { - if (body && contentType.includes("json")) { - body = JSON.stringify(body); - } - - let myHeaders = new Headers(); - - if (contentType) myHeaders.set("Content-Type", contentType); - if (getCookieValue("access")) myHeaders.set("Authorization", "Bearer " + getCookieValue("access")); - let myInit = {headers: myHeaders, method: method, body: body}; - let myRequest = new Request(url, myInit); - - let response = await fetch(myRequest); - if (response.status == 401 && getCookieValue("refresh")) { - // access token not accepted. getting refresh token - myHeaders = new Headers({"Content-Type": "application/json; charset=UTF-8"}); - let tokenBody = JSON.stringify({"refresh": getCookieValue("refresh")}); - myInit = {headers: myHeaders, method: "POST", body: tokenBody}; - myRequest = new Request(`${HOST}/api/token/refresh/`, myInit); - response = await fetch(myRequest); - - if (response.ok) { - // refresh successful, received new access token - let data = await response.json(); - setCookie("access", data.access, 86400, "/"); - - let myHeaders = new Headers({"Authorization": "Bearer " + getCookieValue("access"), - "Content-Type": contentType}); - let myInit = {headers: myHeaders, method: method, body: body}; - let myRequest = new Request(url, myInit); +async function sendRequest( + method, + url, + body, + contentType = "application/json; charset=UTF-8" +) { + if (body && contentType.includes("json")) { + body = JSON.stringify(body); + } + + let myHeaders = new Headers(); + + if (contentType) myHeaders.set("Content-Type", contentType); + if (getCookieValue("access")) + myHeaders.set("Authorization", "Bearer " + getCookieValue("access")); + let myInit = { headers: myHeaders, method: method, body: body }; + let myRequest = new Request(url, myInit); + + let response = await fetch(myRequest); + if (response.status == 401 && getCookieValue("refresh")) { + // access token not accepted. getting refresh token + myHeaders = new Headers({ + "Content-Type": "application/json; charset=UTF-8", + }); + let tokenBody = JSON.stringify({ refresh: getCookieValue("refresh") }); + myInit = { headers: myHeaders, method: "POST", body: tokenBody }; + myRequest = new Request(`${HOST}/api/token/refresh/`, myInit); response = await fetch(myRequest); - if (!response.ok) window.location.replace("logout.html"); + if (response.ok) { + // refresh successful, received new access token + let data = await response.json(); + setCookie("access", data.access, 86400, "/"); + + let myHeaders = new Headers({ + Authorization: "Bearer " + getCookieValue("access"), + "Content-Type": contentType, + }); + let myInit = { headers: myHeaders, method: method, body: body }; + let myRequest = new Request(url, myInit); + response = await fetch(myRequest); + + if (!response.ok) window.location.replace("logout.html"); + } } - } - return response; + return response; } function setReadOnly(readOnly, selector) { - let form = document.querySelector(selector); - let formData = new FormData(form); - - for (let key of formData.keys()) { - let selector = `input[name="${key}"], textarea[name="${key}"]`; - for (let input of form.querySelectorAll(selector)) { - - if (!readOnly && input.hasAttribute("readonly")) - { - input.readOnly = false; - } - else if (readOnly && !input.hasAttribute("readonly")) { - input.readOnly = true; - } + let form = document.querySelector(selector); + let formData = new FormData(form); + + for (let key of formData.keys()) { + let selector = `input[name="${key}"], textarea[name="${key}"]`; + for (let input of form.querySelectorAll(selector)) { + if (!readOnly && input.hasAttribute("readonly")) { + input.readOnly = false; + } else if (readOnly && !input.hasAttribute("readonly")) { + input.readOnly = true; + } + } + + selector = `input[type="file"], select[name="${key}`; + for (let input of form.querySelectorAll(selector)) { + if (!readOnly && input.hasAttribute("disabled")) { + input.disabled = false; + } else if (readOnly && !input.hasAttribute("disabled")) { + input.disabled = true; + } + } } - selector = `input[type="file"], select[name="${key}`; - for (let input of form.querySelectorAll(selector)) { - if ((!readOnly && input.hasAttribute("disabled"))) - { - input.disabled = false; - } - else if (readOnly && !input.hasAttribute("disabled")) { - input.disabled = true; - } + for (let input of document.querySelectorAll( + "input:disabled, select:disabled" + )) { + if ( + (!readOnly && input.hasAttribute("disabled")) || + (readOnly && !input.hasAttribute("disabled")) + ) { + input.disabled = !input.disabled; + } } - } - - for (let input of document.querySelectorAll("input:disabled, select:disabled")) { - if ((!readOnly && input.hasAttribute("disabled")) || - (readOnly && !input.hasAttribute("disabled"))) { - input.disabled = !input.disabled; - } - } } async function getCurrentUser() { - let user = null; - let response = await sendRequest("GET", `${HOST}/api/users/?user=current`) - if (!response.ok) { - console.log("COULD NOT RETRIEVE CURRENTLY LOGGED IN USER"); - } else { - let data = await response.json(); - user = data.results[0]; - } - - return user; + let user = null; + let response = await sendRequest("GET", `${HOST}/api/users/?user=current`); + if (!response.ok) { + console.log("COULD NOT RETRIEVE CURRENTLY LOGGED IN USER"); + } else { + let data = await response.json(); + user = data.results[0]; + } + + return user; } function createAlert(header, data) { - let alertDiv = document.createElement("div"); - alertDiv.className = "alert alert-warning alert-dismissible fade show" - alertDiv.setAttribute("role", "alert"); - - let strong = document.createElement("strong"); - strong.innerText = header; - alertDiv.appendChild(strong); - - let button = document.createElement("button"); - button.type = "button"; - button.className = "btn-close"; - button.setAttribute("data-bs-dismiss", "alert"); - button.setAttribute("aria-label", "Close"); - alertDiv.appendChild(button); - - let ul = document.createElement("ul"); - if ("detail" in data) { - let li = document.createElement("li"); - li.innerText = data["detail"]; - ul.appendChild(li); - } else { - for (let key in data) { - let li = document.createElement("li"); - li.innerText = key; - - let innerUl = document.createElement("ul"); - for (let message of data[key]) { - let innerLi = document.createElement("li"); - innerLi.innerText = message; - innerUl.appendChild(innerLi); - } - li.appendChild(innerUl); - ul.appendChild(li); + let alertDiv = document.createElement("div"); + alertDiv.className = "alert alert-warning alert-dismissible fade show"; + alertDiv.setAttribute("role", "alert"); + + let strong = document.createElement("strong"); + strong.innerText = header; + alertDiv.appendChild(strong); + + let button = document.createElement("button"); + button.type = "button"; + button.className = "btn-close"; + button.setAttribute("data-bs-dismiss", "alert"); + button.setAttribute("aria-label", "Close"); + alertDiv.appendChild(button); + + let ul = document.createElement("ul"); + if ("detail" in data) { + let li = document.createElement("li"); + li.innerText = data["detail"]; + ul.appendChild(li); + } else { + for (let key in data) { + let li = document.createElement("li"); + li.innerText = key; + + let innerUl = document.createElement("ul"); + for (let message of data[key]) { + let innerLi = document.createElement("li"); + innerLi.innerText = message; + innerUl.appendChild(innerLi); + } + li.appendChild(innerUl); + ul.appendChild(li); + } } - } - alertDiv.appendChild(ul); - - return alertDiv; + alertDiv.appendChild(ul); + return alertDiv; } window.addEventListener("DOMContentLoaded", updateNavBar); - diff --git a/requirements.txt b/requirements.txt index 9feb375bde1e8fb7befe6c102dd29beeee7c6940..90ad84fd05c418f46c879cbf5397a00bd210408b 100644 Binary files a/requirements.txt and b/requirements.txt differ