diff --git a/backend/secfit/comments/views.py b/backend/secfit/comments/views.py index b74d0f208c9bcf06ee49817541d47742767f0b7d..84310441fff4de46fc4e3f9ea6a02bbebadc5557 100644 --- a/backend/secfit/comments/views.py +++ b/backend/secfit/comments/views.py @@ -1,20 +1,25 @@ -from django.shortcuts import render from rest_framework import generics, mixins -from comments.models import Comment, Like +from rest_framework.filters import OrderingFilter from rest_framework import permissions +from comments.models import Comment, Like from comments.permissions import IsCommentVisibleToUser from workouts.permissions import IsOwner, IsReadOnly from comments.serializers import CommentSerializer, LikeSerializer -from django.db.models import Q -from rest_framework.filters import OrderingFilter # Create your views here. class CommentList( mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView ): - # queryset = Comment.objects.all() + """Class defining the web response for the creation of comments, or + a list of comments. + + HTTP methods: GET, POST + """ + queryset = Comment.objects.all() serializer_class = CommentSerializer - permission_classes = [permissions.IsAuthenticated] + permission_classes = [ + permissions.IsAuthenticated & IsCommentVisibleToUser & (IsOwner | IsReadOnly) + ] filter_backends = [OrderingFilter] ordering_fields = ["timestamp"] @@ -27,35 +32,6 @@ class CommentList( def perform_create(self, serializer): serializer.save(owner=self.request.user) - def get_queryset(self): - workout_pk = self.kwargs.get("pk") - qs = Comment.objects.none() - - if workout_pk: - qs = Comment.objects.filter(workout=workout_pk) - elif self.request.user: - """A comment should be visible to the requesting user if any of the following hold: - - The comment is on a public visibility workout - - The comment was written by the user - - 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() - - return qs - # Details of comment class CommentDetail( mixins.RetrieveModelMixin, @@ -63,6 +39,11 @@ class CommentDetail( mixins.DestroyModelMixin, generics.GenericAPIView, ): + """Class defining the web response for the retrieval of a comment, or + updating/deleting a comment. + + HTTP methods: GET, PUT, DELETE + """ queryset = Comment.objects.all() serializer_class = CommentSerializer permission_classes = [ @@ -81,6 +62,11 @@ class CommentDetail( # List of likes class LikeList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView): + """Class defining the web response for the creation of likes, or + a list of likes. + + HTTP methods: GET, POST + """ serializer_class = LikeSerializer permission_classes = [permissions.IsAuthenticated] @@ -104,6 +90,11 @@ class LikeDetail( mixins.DestroyModelMixin, generics.GenericAPIView, ): + """Class defining the web response for the retrieval of a like, or + updating/deleting a like. + + HTTP methods: GET, PUT, DELETE + """ queryset = Like.objects.all() serializer_class = LikeSerializer permission_classes = [permissions.IsAuthenticated] diff --git a/backend/secfit/users/admin.py b/backend/secfit/users/admin.py index fc0af23c4473e29bcc06045aebfdd0d21989d22d..bce95fcc05c153342ea7dbf0f72f7b32c9709676 100644 --- a/backend/secfit/users/admin.py +++ b/backend/secfit/users/admin.py @@ -1,18 +1,19 @@ +from django.contrib.auth import get_user_model from django.contrib import admin from django.contrib.auth.admin import UserAdmin from .models import Offer, AthleteFile -from django.contrib.auth import get_user_model from .forms import CustomUserChangeForm, CustomUserCreationForm # Register your models here. class CustomUserAdmin(UserAdmin): + """Allows users to have a custom + user page in the Django admin view + """ 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/urls.py b/backend/secfit/users/urls.py index 0458ae56fcb9f2a1872d97a58d5aa09349861a05..378baf7ed21d0fa30149cb31cea725d9ee4600c1 100644 --- a/backend/secfit/users/urls.py +++ b/backend/secfit/users/urls.py @@ -1,16 +1,44 @@ -from django.urls import path, include +"""Contains paths in the api for the users application. +""" +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"), - path("api/users/<int:pk>/", views.UserDetail.as_view(), name="user-detail"), - path("api/profiles/<int:pk>/", views.ProfileUpdate.as_view(), name="profile-update"), - path("api/users/<str:username>/", views.UserDetail.as_view(), name="user-detail"), - path("api/offers/", views.OfferList.as_view(), name="offer-list"), - path("api/offers/<int:pk>/", views.OfferDetail.as_view(), name="offer-detail"), - path( - "api/athlete-files/", views.AthleteFileList.as_view(), name="athlete-file-list" + # Adding all the user paths in the api + path( + "api/users/", + views.UserList.as_view(), + name="user-list" + ), + path( + "api/users/<int:pk>/", + views.UserDetail.as_view(), + name="user-detail" + ), + path( + "api/profiles/<int:pk>/", + views.ProfileUpdate.as_view(), + name="profile-update" + ), + path( + "api/users/<str:username>/", + views.UserDetail.as_view(), + name="user-detail" + ), + path( + "api/offers/", + views.OfferList.as_view(), + name="offer-list" + ), + path( + "api/offers/<int:pk>/", + views.OfferDetail.as_view(), + name="offer-detail" + ), + path( + "api/athlete-files/", + views.AthleteFileList.as_view(), + name="athlete-file-list" ), path( "api/athlete-files/<int:pk>/", diff --git a/backend/secfit/users/views.py b/backend/secfit/users/views.py index e8c1e5ab32259394b11e5cb5c6f7adc52424f87f..5949ca4fb175393d078fd0c10ae09c17dcad28df 100644 --- a/backend/secfit/users/views.py +++ b/backend/secfit/users/views.py @@ -1,7 +1,9 @@ -import django -from rest_framework import mixins, generics +from rest_framework import mixins, generics, permissions +from rest_framework.permissions import IsAuthenticatedOrReadOnly +from django.contrib.auth import get_user_model +from django.db.models import Q +from rest_framework.parsers import MultiPartParser, FormParser from workouts.mixins import CreateListModelMixin -from rest_framework import permissions from users.serializers import ( UserSerializer, OfferSerializer, @@ -10,22 +12,17 @@ from users.serializers import ( UserGetSerializer, ProfilePutSerializer ) -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 # Create your views here. class UserList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView): + """Class defining the web response for the creation of users, or + a list of users. + + HTTP methods: GET, POST + """ serializer_class = UserSerializer users = [] admins = [] @@ -38,15 +35,15 @@ class UserList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericA return self.create(request, *args, **kwargs) def get_queryset(self): - qs = get_user_model().objects.all() + query_set = get_user_model().objects.all() if self.request.user: # Return the currently logged in user status = self.request.query_params.get("user", None) if status and status == "current": - qs = get_user_model().objects.filter(pk=self.request.user.pk) + query_set = get_user_model().objects.filter(pk=self.request.user.pk) - return qs + return query_set class UserDetail( @@ -55,6 +52,11 @@ class UserDetail( mixins.DestroyModelMixin, generics.GenericAPIView, ): + """Class defining the web response for the retrieval of a user, or + updating/deleting a user. + + HTTP methods: GET, DELETE, PUT, PATCH + """ lookup_field_options = ["pk", "username"] serializer_class = UserSerializer queryset = get_user_model().objects.all() @@ -87,6 +89,10 @@ class ProfileUpdate( mixins.UpdateModelMixin, generics.GenericAPIView, ): + """Class defining the web response for updating a profile. + + HTTP methods: PUT + """ serializer_class = ProfilePutSerializer queryset = get_user_model().objects.all() permission_classes = [permissions.IsAuthenticated & IsCurrentUser] @@ -97,6 +103,11 @@ class ProfileUpdate( class OfferList( mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView ): + """Class defining the web response for the creation of offers, or + a list of offers. + + HTTP methods: GET, POST + """ permission_classes = [IsAuthenticatedOrReadOnly] serializer_class = OfferSerializer @@ -110,34 +121,37 @@ class OfferList( serializer.save(owner=self.request.user) def get_queryset(self): - qs = Offer.objects.none() result = Offer.objects.none() - if self.request.user: - qs = Offer.objects.filter( + query_set = Offer.objects.filter( Q(owner=self.request.user) | Q(recipient=self.request.user) ).distinct() - qp = self.request.query_params - u = self.request.user + query_params = self.request.query_params + 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) - if qp.get("status", None) is None: - qs = Offer.objects.filter(Q(owner=u)).distinct() + filter_status(self, query_set, query_params, user) # 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) - return qs + filter_category(query_params, query_set, user) + return query_set else: return result +def filter_status(self, query_set, query_params, user): + status = query_params.get("status", None) + if status is not None and self.request is not None: + query_set = query_set.filter(status=status) + if query_params.get("status", None) is None: + query_set = Offer.objects.filter(Q(owner=user)).distinct() + +def filter_category(query_params, query_set, user): + category = query_params.get("category", None) + if category is not None and query_params is not None: + if category == "sent": + query_set = query_set.filter(owner=user) + elif category == "received": + query_set = query_set.filter(recipient=user) class OfferDetail( mixins.RetrieveModelMixin, @@ -145,6 +159,11 @@ class OfferDetail( mixins.DestroyModelMixin, generics.GenericAPIView, ): + """Class defining the web response for the retrieval of an offer, or + updating/deleting an offer. + + HTTP methods: GET, DELETE, PUT, PATCH + """ permission_classes = [IsAuthenticatedOrReadOnly] queryset = Offer.objects.all() serializer_class = OfferSerializer @@ -168,6 +187,11 @@ class AthleteFileList( CreateListModelMixin, generics.GenericAPIView, ): + """Class defining the web response for the creation of athlete files, or + a list of athlete files. + + HTTP methods: GET, POST + """ queryset = AthleteFile.objects.all() serializer_class = AthleteFileSerializer permission_classes = [permissions.IsAuthenticated & (IsAthlete | IsCoach)] @@ -183,14 +207,14 @@ class AthleteFileList( serializer.save(owner=self.request.user) def get_queryset(self): - qs = AthleteFile.objects.none() + query_set = AthleteFile.objects.none() if self.request.user: - qs = AthleteFile.objects.filter( + query_set = AthleteFile.objects.filter( Q(athlete=self.request.user) | Q(owner=self.request.user) ).distinct() - return qs + return query_set class AthleteFileDetail( @@ -199,6 +223,11 @@ class AthleteFileDetail( mixins.DestroyModelMixin, generics.GenericAPIView, ): + """Class defining the web response for the retrieval of an athlete file, or + deleting an athlete file. + + HTTP methods: GET, DELETE + """ queryset = AthleteFile.objects.all() serializer_class = AthleteFileSerializer permission_classes = [permissions.IsAuthenticated & (IsAthlete | IsOwner)] @@ -207,4 +236,4 @@ class AthleteFileDetail( return self.retrieve(request, *args, **kwargs) def delete(self, request, *args, **kwargs): - return self.destroy(request, *args, **kwargs) \ No newline at end of file + return self.destroy(request, *args, **kwargs) diff --git a/frontend/www/scripts/gallery.js b/frontend/www/scripts/gallery.js index f9c07b449947470c8df29c8f51894758cf38c025..4bb3ecf53078448ad7e71b0ff367ba9921bb4fab 100644 --- a/frontend/www/scripts/gallery.js +++ b/frontend/www/scripts/gallery.js @@ -1,38 +1,58 @@ let goBackButton; -let submitNewFileButton; +let filesDiv = document.getElementById("img-collection"); +let filesDeleteDiv = document.getElementById("img-collection-delete"); +const currentImageFileElement = document.querySelector("#current"); +let isFirstImg = true; +let fileCounter = 0; + +function handleImageElements(){ + const otherImageFileElements = document.querySelectorAll(".imgs img"); + const selectedOpacity = 0.6; + otherImageFileElements[0].style.opacity = selectedOpacity; -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(); + otherImageFileElements.forEach((imageFileElement) => imageFileElement.addEventListener("click", (event) => { + //Changes the main image + currentImageFileElement.src = event.target.src; - document.getElementById("workout-title").innerHTML = "Workout name: " + workoutData["name"]; - document.getElementById("workout-owner").innerHTML = "Owner: " + workoutData["owner_username"]; + //Adds the fade animation + currentImageFileElement.classList.add('fade-in') + setTimeout(() => currentImageFileElement.classList.remove('fade-in'), 500); - let hasNoImages = workoutData.files.length == 0; - let noImageText = document.querySelector("#no-images-text"); + //Sets the opacity of the selected image to 0.4 + otherImageFileElements.forEach((imageFileElement) => imageFileElement.style.opacity = 1) + event.target.style.opacity = selectedOpacity; + })); +} - if(hasNoImages){ - noImageText.classList.remove("hide"); - return; +function appendImage(file){ + 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; - +function addImages(workoutData){ for (let file of workoutData.files) { let a = document.createElement("a"); a.href = file.file; @@ -40,49 +60,42 @@ async function retrieveWorkoutImages(id) { 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++; + appendImage(file); } } + handleImageElements(); +} - const otherImageFileElements = document.querySelectorAll(".imgs img"); - const selectedOpacity = 0.6; - otherImageFileElements[0].style.opacity = selectedOpacity; - otherImageFileElements.forEach((imageFileElement) => imageFileElement.addEventListener("click", (event) => { - //Changes the main image - currentImageFileElement.src = event.target.src; +function handleWorkoutData(workoutData){ + document.getElementById("workout-title").innerHTML = "Workout name: " + workoutData["name"]; + document.getElementById("workout-owner").innerHTML = "Owner: " + workoutData["owner_username"]; - //Adds the fade animation - currentImageFileElement.classList.add('fade-in') - setTimeout(() => currentImageFileElement.classList.remove('fade-in'), 500); + let hasNoImages = workoutData.files.length == 0; + let noImageText = document.querySelector("#no-images-text"); - //Sets the opacity of the selected image to 0.4 - otherImageFileElements.forEach((imageFileElement) => imageFileElement.style.opacity = 1) - event.target.style.opacity = selectedOpacity; - })) + if(hasNoImages){ + noImageText.classList.remove("hide"); + return; + } + + noImageText.classList.add("hide"); + addImages(workoutData); +} + +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(); + handleWorkoutData(workoutData); } return workoutData;