diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40bca316db8fa0d9dda23e518a851d0dfc306f99 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1 @@ +/env \ No newline at end of file diff --git a/backend/secfit/users/models.py b/backend/secfit/users/models.py index d48528655225b97a32833eb1a6629e0d266ba6df..3d93602214e6e4acf10cd5b05e25eaec77d5b9bf 100644 --- a/backend/secfit/users/models.py +++ b/backend/secfit/users/models.py @@ -18,6 +18,10 @@ class User(AbstractUser): country = models.TextField(max_length=50, blank=True) city = models.TextField(max_length=50, blank=True) street_address = models.TextField(max_length=50, blank=True) + age = models.PositiveIntegerField(blank=True, null=True) + expirience = models.PositiveIntegerField(blank=True, null=True) + favorite_dicipline = models.TextField(max_length=50, blank=True, null=True) + bio = models.TextField(max_length=200, blank=True, null=True) def athlete_directory_path(instance, filename): diff --git a/backend/secfit/users/serializers.py b/backend/secfit/users/serializers.py index 0e1a83ba88f61b5fd0b3c8ff34ed7e9fee668b63..5dffea411e9dc71368d3f7b276d5b663ada6d32a 100644 --- a/backend/secfit/users/serializers.py +++ b/backend/secfit/users/serializers.py @@ -73,21 +73,47 @@ class UserGetSerializer(serializers.HyperlinkedModelSerializer): "workouts", "coach_files", "athlete_files", + "age", + "expirience", + "favorite_dicipline", + "bio" ] class UserPutSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() - fields = ["athletes"] + fields = [ + "athletes", + "age", + "expirience", + "favorite_dicipline", + "bio" + ] def update(self, instance, validated_data): athletes_data = validated_data["athletes"] + age_data = validated_data["age"] + expirience_data = validated_data["expirience"] + favorite_dicipline_data = validated_data["favorite_dicipline"] + bio_data = validated_data["bio"] instance.athletes.set(athletes_data) + instance.age.set(age_data) + instance.expirience.set(expirience_data) + instance.favorite_dicipline.set(favorite_dicipline_data) + instance.bio.set(bio_data) return instance - +class ProfilePutSerializer(serializers.ModelSerializer): + class Meta: + model = get_user_model() + fields = [ + "age", + "expirience", + "favorite_dicipline", + "bio" + ] class AthleteFileSerializer(serializers.HyperlinkedModelSerializer): owner = serializers.ReadOnlyField(source="owner.username") diff --git a/backend/secfit/users/urls.py b/backend/secfit/users/urls.py index 507c27008e8b0997e486945a27bfe3afc55d89de..0458ae56fcb9f2a1872d97a58d5aa09349861a05 100644 --- a/backend/secfit/users/urls.py +++ b/backend/secfit/users/urls.py @@ -5,6 +5,7 @@ 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"), diff --git a/backend/secfit/users/views.py b/backend/secfit/users/views.py index f5efef5c2ce82566ab380cecad344e3143c31813..e8c1e5ab32259394b11e5cb5c6f7adc52424f87f 100644 --- a/backend/secfit/users/views.py +++ b/backend/secfit/users/views.py @@ -8,6 +8,7 @@ from users.serializers import ( AthleteFileSerializer, UserPutSerializer, UserGetSerializer, + ProfilePutSerializer ) from rest_framework.permissions import ( AllowAny, @@ -81,6 +82,17 @@ class UserDetail( def patch(self, request, *args, **kwargs): return self.partial_update(request, *args, **kwargs) +class ProfileUpdate( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + generics.GenericAPIView, +): + serializer_class = ProfilePutSerializer + queryset = get_user_model().objects.all() + permission_classes = [permissions.IsAuthenticated & IsCurrentUser] + + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) class OfferList( mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView @@ -195,4 +207,4 @@ class AthleteFileDetail( return self.retrieve(request, *args, **kwargs) def delete(self, request, *args, **kwargs): - return self.destroy(request, *args, **kwargs) + return self.destroy(request, *args, **kwargs) \ No newline at end of file diff --git a/frontend/www/profile.html b/frontend/www/profile.html new file mode 100644 index 0000000000000000000000000000000000000000..194931f0af0429c1228144d4eeb3826fc58cd7ee --- /dev/null +++ b/frontend/www/profile.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Group</title> + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> + + <script src="https://kit.fontawesome.com/0ce6c392ca.js" crossorigin="anonymous"></script> + <link rel="stylesheet" href="styles/style.css"> + <script src="scripts/navbar.js" type="text/javascript" defer></script> +</head> + <body> + <navbar-el></navbar-el> + + <div class="container"> + <div class="row"> + <div class="col-lg"> + <h3 class="mt-3" id="title"></h3> + <p>Edit you fitness information by clicking the edit button below!</p> + </div> + </div> + <form class="row g-3" id="form-profile"> + <div class="col-lg-6 "> + <label for="inputAge" class="form-label">Age</label> + <input type="number" class="form-control" id="inputAge" name="age" readonly> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <label for="inputExpirience" class="form-label">Workout experience(years)</label> + <input type="number" class="form-control" id="inputExpirience" name="expirience" readonly> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <label for="inputDicipline" class="form-label">Favorite diciplines</label> + <textarea class="form-control" id="inputDicipline" name="favorite_dicipline" readonly></textarea> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <label for="inputBio" class="form-label">About me</label> + <textarea class="form-control" id="inputBio" name="bio" readonly></textarea> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <input type="button" class="btn btn-primary hide" id="btn-ok-profile" value=" OK "> + <input type="button" class="btn btn-secondary hide" id="btn-cancel-profile" value="Cancel"> + <input type="button" class="btn btn-primary" id="btn-edit-profile" value=" Edit "> + </div> + <div class="col-lg-6"></div> + </form> + </div> + <script src="scripts/defaults.js"></script> + <script src="scripts/scripts.js"></script> + <script src="scripts/profile.js"></script> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script> + </body> +</html> \ No newline at end of file diff --git a/frontend/www/scripts/navbar.js b/frontend/www/scripts/navbar.js index 15bf24e7fcb91a549370f050eb9c53e5a284d97b..7bffdc13752dfc5ea062dd9fa5a4a53f48e3446a 100644 --- a/frontend/www/scripts/navbar.js +++ b/frontend/www/scripts/navbar.js @@ -20,6 +20,7 @@ class NavBar extends HTMLElement { <a class="nav-link hide" id="nav-myathletes" href="myathletes.html">Athletes</a> <a class="nav-link hide" id="nav-meals" href="meals.html">Meal registration</a> <a class="nav-link hide" id="nav-groups" href="groups.html">Groups</a> + <a class="nav-link hide" id="nav-profile" href="profile.html">Fitness Profile</a> <hr> </div> <div class="my-2 my-lg-0 me-5"> diff --git a/frontend/www/scripts/profile.js b/frontend/www/scripts/profile.js new file mode 100644 index 0000000000000000000000000000000000000000..344851608f7444978d33deecd65fd20836ab12db --- /dev/null +++ b/frontend/www/scripts/profile.js @@ -0,0 +1,139 @@ +let cancelButton; +let okButton; +let editButton; +let oldFormData; + + +function handleCancelButtonDuringCreate() { + window.location.replace("profile.html"); +} + +/** + * If user presses "cancel" during editing a group the + * form fields become read only and the form data is deleted. + * And the buttons change. + */ +function handleCancelButtonDuringEdit() { + setReadOnly(true, "#form-profile"); + okButton.className += " hide"; + cancelButton.className += " hide"; + editButton.className = editButton.className.replace(" hide", ""); + + cancelButton.removeEventListener("click", handleCancelButtonDuringEdit); + + let form = document.querySelector("#form-profile"); + if (oldFormData.has("age")) form.name.value = oldFormData.get("age"); + if (oldFormData.has("expirience")) form.description.value = oldFormData.get("expirience"); + if (oldFormData.has("dicipline")) form.description.value = oldFormData.get("dicipline"); + if (oldFormData.has("bio")) form.description.value = oldFormData.get("bio"); + + oldFormData.delete("age"); + oldFormData.delete("expirience"); + oldFormData.delete("dicipline"); + oldFormData.delete("bio"); +} + +/** + * If the user clicks on the edit button the form fields can be edited. + * And the form is updated with the data of the group that the user wants to edit. + */ +function handleEditProfileButtonClick() { + setReadOnly(false, "#form-profile"); + + editButton.className += " hide"; + okButton.className = okButton.className.replace(" hide", ""); + cancelButton.className = cancelButton.className.replace(" hide", ""); + + cancelButton.addEventListener("click", handleCancelButtonDuringEdit); + + let form = document.querySelector("#form-profile"); + oldFormData = new FormData(form); +} + +/** + * sends an API request to retrieve the profile information of the user that is currently active + */ + +async function getCurrentProfile() { + let res = await sendRequest("GET", `${HOST}/api/users/?user=current`); + if (!res.ok) { + console.log("COULD NOT RETRIEVE CURRENTLY LOGGED IN USER"); + } else { + let data = await res.json(); + userID = data.results[0].id; + userName = data.results[0].username + } + document.getElementById("title").innerHTML = userName + "'s Fitness Profile"; + let response = await sendRequest("GET", `${HOST}/api/users/${userID}/`); + if (!response.ok) { + console.log("COULD NOT RETRIEVE CURRENTLY LOGGED IN USER"); + } else { + let profileData = await response.json(); + let form = document.querySelector("#form-profile"); + let formData = new FormData(form); + for (let key of formData.keys()) { + let selector + selector = `input[name="${key}"], textarea[name="${key}"]`; + let input = form.querySelector(selector); + let newVal = profileData[key]; + input.value = newVal; + } + } +} + +/** + * Sends a PUT request to the API to update a group's information. + * @param {integer} id of the group to be updated + */ +async function updateProfile() { + let res = await sendRequest("GET", `${HOST}/api/users/?user=current`); + if (!res.ok) { + console.log("COULD NOT RETRIEVE CURRENTLY LOGGED IN USER"); + } else { + let data = await res.json(); + userID = data.results[0].id; + } + let form = document.querySelector("#form-profile"); + let formData = new FormData(form); + + let body = {"age": formData.get("age"), + "expirience": formData.get("expirience"), + "favorite_dicipline": formData.get("favorite_dicipline"), + "bio": formData.get("bio"), + }; + let response = await sendRequest("PUT", `${HOST}/api/profiles/${userID}/`, body); + + if (!response.ok) { + let data = await response.json(); + let alert = createAlert(`Could not update profile`, data); + document.body.prepend(alert); + } else { + setReadOnly(true, "#form-profile"); + okButton.className += " hide"; + cancelButton.className += " hide"; + editButton.className = editButton.className.replace(" hide", ""); + + cancelButton.removeEventListener("click", handleCancelButtonDuringEdit); + + oldFormData.delete("age"); + oldFormData.delete("expirience"); + oldFormData.delete("dicipline"); + oldFormData.delete("bio"); + } +} + +/** + * When a user enters the group.html this decides whether it + * is entered in view/edit mode or in create mode. If the html contains + * a url parameter with an id it is entered in view/edit mode. + */ + +window.addEventListener("DOMContentLoaded", async () => { + cancelButton = document.querySelector("#btn-cancel-profile"); + okButton = document.querySelector("#btn-ok-profile"); + editButton = document.querySelector("#btn-edit-profile"); + oldFormData = null; + await getCurrentProfile(); + editButton.addEventListener("click", handleEditProfileButtonClick); + okButton.addEventListener("click", async () => await updateProfile()); +}); \ No newline at end of file diff --git a/frontend/www/scripts/scripts.js b/frontend/www/scripts/scripts.js index 1cd174452b91bba96cb5840dd42a2ee9996e923e..70948f4ebbb6ae4d784158b18a5ddd66bf8aa326 100644 --- a/frontend/www/scripts/scripts.js +++ b/frontend/www/scripts/scripts.js @@ -27,6 +27,9 @@ function updateNavBar() { } else if (window.location.pathname == "/groups.html") { makeNavLinkActive("nav-groups"); } + else if (window.location.pathname == "/profile.html") { + makeNavLinkActive("nav-profile"); + } if (isUserAuthenticated()) { document.getElementById("btn-logout").classList.remove("hide"); @@ -38,6 +41,7 @@ function updateNavBar() { document.querySelector('a[href="myathletes.html"').classList.remove("hide"); document.querySelector('a[href="meals.html"').classList.remove("hide"); document.querySelector('a[href="groups.html"').classList.remove("hide"); + document.querySelector('a[href="profile.html"').classList.remove("hide"); } else { document.getElementById("btn-login-nav").classList.remove("hide"); document.getElementById("btn-register").classList.remove("hide");