diff --git a/accounts/locale/fr/LC_MESSAGES/django.po b/accounts/locale/fr/LC_MESSAGES/django.po index eed9cf2..73661a4 100644 --- a/accounts/locale/fr/LC_MESSAGES/django.po +++ b/accounts/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-02-01 13:59+0100\n" +"POT-Creation-Date: 2024-03-11 11:02+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,6 +17,11 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: serializers/update_user.py:15 +msgid "You dont have permission for this user." +msgstr "Vous n'avez pas de permissions pour cet utilisateur." + #: views/login.py:22 msgid "Invalid username or password." msgstr "Nom d'utilisateur ou mot de passe incorect." diff --git a/accounts/serializers/update_user.py b/accounts/serializers/update_user.py new file mode 100644 index 0000000..25c91f5 --- /dev/null +++ b/accounts/serializers/update_user.py @@ -0,0 +1,20 @@ +from rest_framework.serializers import ModelSerializer, ValidationError +from django.contrib.auth.models import User +from django.utils.translation import gettext as _ + + +class UpdateUserSerializer(ModelSerializer): + class Meta: + model = User + fields = ['username'] + + def update(self, instance, validated_data): + user = self.context['request'].user + + if user.pk != instance.pk: + raise ValidationError({'authorize': _('You dont have permission for this user.')}) + + instance.username = validated_data.get('username', instance.username) + + instance.save() + return instance diff --git a/accounts/urls.py b/accounts/urls.py index 73ae6d7..dcac548 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from .views import register, login, logout, delete, edit, logged +from .views import register, login, logout, delete, logged, update_profile urlpatterns = [ path("register", register.RegisterView.as_view(), name="register"), @@ -8,6 +8,5 @@ urlpatterns = [ path("logout", logout.LogoutView.as_view(), name="logout"), path("logged", logged.LoggedView.as_view(), name="logged"), path("delete", delete.DeleteView.as_view(), name="delete"), - path("edit", edit.EditView.as_view(), name="change_password") - -] \ No newline at end of file + path('update_profile', update_profile.UpdateProfileView.as_view(), name='update_profile') +] diff --git a/accounts/views/delete.py b/accounts/views/delete.py index 1333f7e..66fd084 100644 --- a/accounts/views/delete.py +++ b/accounts/views/delete.py @@ -5,17 +5,18 @@ from django.contrib.auth import logout from django.http import HttpRequest from rest_framework.authentication import SessionAuthentication + class DeleteView(APIView): permission_classes = (permissions.IsAuthenticated,) authentication_classes = (SessionAuthentication,) + def delete(self, request: HttpRequest): data: dict = request.data password: str = data["password"] - if (password is None): - return Response({"password": ["This field may not be blank."]}) - if (request.user.check_password(password) == False): - return Response({"password": ["Password wrong."]}) + if (request.user.check_password(password) is False): + return Response({"password": ["Password incorrect."]}, + status.HTTP_401_UNAUTHORIZED) request.user.delete() logout(request) - return Response("user deleted", status=status.HTTP_200_OK) \ No newline at end of file + return Response(status=status.HTTP_200_OK) diff --git a/accounts/views/edit.py b/accounts/views/edit.py deleted file mode 100644 index c62a201..0000000 --- a/accounts/views/edit.py +++ /dev/null @@ -1,45 +0,0 @@ -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import permissions, status -from django.http import HttpRequest -from django.contrib.auth import login -from rest_framework.authentication import SessionAuthentication -from django.contrib.auth.models import User -import re - -class EditView(APIView): - - permission_classes = (permissions.IsAuthenticated,) - authentication_classes = (SessionAuthentication,) - - def get(self, request: HttpRequest): - return Response({"username": request.user.username, "id": request.user.pk}) - - def patch(self, request: HttpRequest): - data: dict = request.data - - current_password: str = data.get("current_password") - if (current_password is None): - return Response({"current_password": ["This field may not be blank."]}) - - user_object = request.user - - if (user_object.check_password(current_password) == False): - return Response({"current_password": ["Password is wrong."]}) - - new_username = data.get("username", user_object.username) - if (new_username != user_object.username): - if (User.objects.filter(username=new_username).exists()): - return Response({"username": ["A user with that username already exists."]}) - if (set(new_username) == {' '}): - return Response({"username": ["This field may not be blank."]}) - if (re.search('^([a-z]||\@||\+||\-||\_)+$', new_username) is None): - return Response({"username":["Enter a valid username. This value may contain only letters, numbers, and @/./+/-/_ characters."]}) - - new_password: str = data.get("password") - if (new_password is not None): - user_object.set_password(new_password) - - user_object.save() - - return Response("data has been alterate") \ No newline at end of file diff --git a/accounts/views/logged.py b/accounts/views/logged.py index b67abe4..ff7f54d 100644 --- a/accounts/views/logged.py +++ b/accounts/views/logged.py @@ -2,15 +2,13 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import permissions, status from django.http import HttpRequest -from django.contrib.auth import login from rest_framework.authentication import SessionAuthentication -from ..serializers.login import LoginSerializer class LoggedView(APIView): - + permission_classes = (permissions.AllowAny,) authentication_classes = (SessionAuthentication,) def get(self, request: HttpRequest): - return Response(status = (status.HTTP_200_OK if request.user.is_authenticated else status.HTTP_400_BAD_REQUEST)) + return Response(status=status.HTTP_200_OK if request.user.is_authenticated else status.HTTP_400_BAD_REQUEST) diff --git a/accounts/views/login.py b/accounts/views/login.py index d981f41..0f10c7a 100644 --- a/accounts/views/login.py +++ b/accounts/views/login.py @@ -8,8 +8,9 @@ from django.utils.translation import gettext as _ from ..serializers.login import LoginSerializer + class LoginView(APIView): - + permission_classes = (permissions.AllowAny,) authentication_classes = (SessionAuthentication,) diff --git a/accounts/views/logout.py b/accounts/views/logout.py index cee8868..f4d7ad1 100644 --- a/accounts/views/logout.py +++ b/accounts/views/logout.py @@ -5,9 +5,11 @@ from rest_framework.response import Response from django.http import HttpRequest from rest_framework.authentication import SessionAuthentication + class LogoutView(APIView): permission_classes = (permissions.IsAuthenticated,) authentication_classes = (SessionAuthentication,) + def get(self, request: HttpRequest): logout(request) - return Response("user unlogged", status=status.HTTP_200_OK) \ No newline at end of file + return Response("user logged out", status.HTTP_200_OK) diff --git a/accounts/views/register.py b/accounts/views/register.py index 903609a..926a560 100644 --- a/accounts/views/register.py +++ b/accounts/views/register.py @@ -5,8 +5,10 @@ from rest_framework.response import Response from django.http import HttpRequest from django.contrib.auth import login + class RegisterView(APIView): permission_classes = (permissions.AllowAny,) + def post(self, request: HttpRequest): data = request.data serializer = RegisterSerialiser(data=data) diff --git a/accounts/views/update_profile.py b/accounts/views/update_profile.py new file mode 100644 index 0000000..eca35f0 --- /dev/null +++ b/accounts/views/update_profile.py @@ -0,0 +1,14 @@ +from ..serializers.update_user import UpdateUserSerializer +from rest_framework.generics import UpdateAPIView +from rest_framework.permissions import IsAuthenticated +from django.contrib.auth.models import User + + +class UpdateProfileView(UpdateAPIView): + + queryset = User.objects.all() + permission_classes = (IsAuthenticated,) + serializer_class = UpdateUserSerializer + + def get_object(self): + return self.queryset.get(pk=self.request.user.pk) diff --git a/frontend/static/css/index.css b/frontend/static/css/index.css index 6c04bb5..816eaec 100644 --- a/frontend/static/css/index.css +++ b/frontend/static/css/index.css @@ -1,11 +1,3 @@ -#app #avatar { - max-height: 10em; - max-width: 10em; - min-height: 6em; - min-width: 6em; -} - - #popup { position: fixed; font-size: 1.2em; diff --git a/frontend/static/css/settings.css b/frontend/static/css/settings.css deleted file mode 100644 index 6879d41..0000000 --- a/frontend/static/css/settings.css +++ /dev/null @@ -1,11 +0,0 @@ -#app * { - font-size: 30px; -} - - -#app #main -{ - width: 60%; - display: flex; - flex-direction: column; -} diff --git a/frontend/static/js/api/Account.js b/frontend/static/js/api/Account.js index 66c8e0c..b3604a2 100644 --- a/frontend/static/js/api/Account.js +++ b/frontend/static/js/api/Account.js @@ -48,41 +48,24 @@ class Account } /** - * Get account data (username) - * @returns {?Promise} - */ - async get() + * @param {String} newUsername + * @returns {?Promise} + */ + async updateUsername(newUsername) { - let response = await this.client._get("/api/accounts/edit"); - let response_data = await response.json(); + const data = { + username: newUsername + }; + const response = await this.client._patch_json(`/api/accounts/update_profile`, data); + const respondeData = await response.json(); - if (response.status === 403) - { - this.client._update_logged(false); + if (response.status === 200) { + this.client.me.username = respondeData.username; + document.getElementById('navbarDropdownButton').innerHTML = respondeData.username; + document.getElementById('myProfileLink').href = '/profiles/' + respondeData.username; return null; } - return response_data; - } - - /** - * - * @param {*} data - * @param {Number} password - * @returns {?Object} - */ - async update(data, password) - { - data.current_password = password; - let response = await this.client._patch_json("/api/accounts/edit", data); - let response_data = await response.json(); - - if (response.status === 403) - { - this.client._update_logged(false); - return null; - } - - return response_data; + return respondeData['authorize'] || respondeData['detail'] || respondeData['username']?.join(' ') || 'Error.'; } } diff --git a/frontend/static/js/api/Client.js b/frontend/static/js/api/Client.js index ba54f0c..38c4767 100644 --- a/frontend/static/js/api/Client.js +++ b/frontend/static/js/api/Client.js @@ -3,7 +3,6 @@ import { MatchMaking } from "./Matchmaking.js"; import { Profiles } from "./Profiles.js"; import { Channels } from './chat/Channels.js'; import { MyProfile } from "./MyProfile.js"; -import { navigateTo } from "../index.js"; import { Tourmanents } from "./tournament/Tournaments.js"; import { Notice } from "./chat/Notice.js"; import { Channel } from "./chat/Channel.js"; @@ -97,6 +96,9 @@ class Client { let response = await fetch(this._url + uri, { method: "GET", + headers: { + 'Accept-Language': this.lang.currentLang + }, body: JSON.stringify(data), }); return response; @@ -135,7 +137,8 @@ class Client headers: { "Content-Type": "application/json", "X-CSRFToken": getCookie("csrftoken"), - }, + 'Accept-Language': this.lang.currentLang, + }, body: JSON.stringify(data), }); return response; @@ -154,7 +157,8 @@ class Client headers: { "X-CSRFToken": getCookie("csrftoken"), "Content-Type": "application/json", - }, + 'Accept-Language': this.lang.currentLang, + }, body: JSON.stringify(data), }); return response; @@ -172,7 +176,8 @@ class Client method: "PATCH", headers: { "X-CSRFToken": getCookie("csrftoken"), - }, + 'Accept-Language': this.lang.currentLang, + }, body: file, }); return response; diff --git a/frontend/static/js/api/MyProfile.js b/frontend/static/js/api/MyProfile.js index a866fea..0773c5f 100644 --- a/frontend/static/js/api/MyProfile.js +++ b/frontend/static/js/api/MyProfile.js @@ -14,15 +14,35 @@ class MyProfile extends Profile /** * - * @param {*} form_data - * @returns {Promise} + * @param {File} selectedFile + * @returns {Promise} */ - async change_avatar(form_data) + async changeAvatar(selectedFile) { - let response = await this.client._patch_file(`/api/profiles/settings`, form_data); - let response_data = await response.json(); + const formData = new FormData(); + formData.append('avatar', selectedFile); - return response_data; + const response = await this.client._patch_file(`/api/profiles/settings`, formData); + const responseData = await response.json(); + + if (response.ok) { + console.log('save', responseData); + this.avatar_url = responseData.avatar.substr(responseData.avatar.indexOf('static') - 1); + return null; + } + return responseData; + } + + async deleteAvatar() { + const response = await this.client._delete('/api/profiles/settings'); + const responseData = await response.json(); + + if (response.ok) { + console.log('delete', responseData); + this.avatar_url = responseData.avatar.substr(responseData.avatar.indexOf('static') - 1); + return null; + } + return responseData; } } diff --git a/frontend/static/js/api/tournament/Tournament.js b/frontend/static/js/api/tournament/Tournament.js index 2a4a391..e5e4682 100644 --- a/frontend/static/js/api/tournament/Tournament.js +++ b/frontend/static/js/api/tournament/Tournament.js @@ -79,7 +79,7 @@ class Tourmanent */ async init() { - let response = await this.client._get(`/api/tournaments/${id}`); + let response = await this.client._get(`/api/tournaments/${this.id}`); if (response.status !== 200) return response.status; diff --git a/frontend/static/js/api/tournament/Tournaments.js b/frontend/static/js/api/tournament/Tournaments.js index dc29178..eed0092 100644 --- a/frontend/static/js/api/tournament/Tournaments.js +++ b/frontend/static/js/api/tournament/Tournaments.js @@ -17,12 +17,12 @@ class Tourmanents /** * * @param {Number} id - * @returns {?Promise} + * @returns {Promise} */ async getTournament(id) { - let tournament = new Tourmanent(this.client); - if (await tournament.init(id)) + let tournament = new Tourmanent(this.client, id); + if (await tournament.init()) return null; return tournament; } @@ -32,17 +32,13 @@ class Tourmanents * @param {Number} nb_players * @param {Number} nb_players_by_game * @param {String} name - * @returns + * @returns {Response} */ async createTournament(nb_players, nb_players_by_game, name = "") { let response = await this.client._post("/api/tournaments/", {nb_players: nb_players, nb_players_by_game: nb_players_by_game, name: name}); - if (response.status !== 200) - return response.status; - - let response_data = await response.json(); - return response_data; + return response; } /** @@ -71,7 +67,8 @@ class Tourmanents tournament_data.started, tournament_data.finished, tournament_data.levels, - tournament_data.id)); + tournament_data.id, + tournament_data.state)); }); return tournaments; diff --git a/frontend/static/js/index.js b/frontend/static/js/index.js index 1bf21fe..7c3856e 100644 --- a/frontend/static/js/index.js +++ b/frontend/static/js/index.js @@ -15,7 +15,7 @@ import SettingsView from "./views/SettingsView.js"; import ProfilePageView from "./views/ProfilePageView.js"; import MatchMakingView from "./views/MatchMakingView.js"; import TournamentPageView from "./views/tournament/TournamentPageView.js"; -import TournamentsView from "./views/tournament/TournamentsListView.js"; +import TournamentsListView from "./views/tournament/TournamentsListView.js"; import TournamentCreateView from "./views/tournament/TournamentCreateView.js"; import AuthenticationView from "./views/accounts/AuthenticationView.js"; import TicTacToeView from "./views/TicTacToeView.js"; @@ -82,7 +82,7 @@ const router = async(uri) => { { path: "/profiles/:username", view: ProfilePageView }, { path: "/tournaments/create", view: TournamentCreateView }, { path: "/tournaments/:id", view: TournamentPageView }, - { path: "/tournaments/", view: TournamentsView }, + { path: "/tournaments/", view: TournamentsListView }, { path: "/login", view: AuthenticationView }, { path: "/register", view: AuthenticationView }, { path: "/logout", view: LogoutView }, @@ -155,7 +155,6 @@ document.addEventListener("DOMContentLoaded", async () => { el.onclick = async _ => { if (await lang.changeLanguage(el.value)) return; - console.log(lang); document.querySelector('#languageSelector > .active')?.classList.remove('active'); el.classList.add('active'); }; diff --git a/frontend/static/js/lang/cr.json b/frontend/static/js/lang/cr.json index d0108d0..238a581 100644 --- a/frontend/static/js/lang/cr.json +++ b/frontend/static/js/lang/cr.json @@ -42,5 +42,14 @@ "ruleTitle" : "Règles cramptés", "ruleBase" : "cramptun. Vous devez quouicougagner sur une des 9 quoicougrilles pour gagner la croustipartie", "ruleMovement" : "quoicoudeux. Vous quoicommencez sur le morpion quoicoucentral, et jouez sur le quoicoumorpion correspondant a votre croustichoix a votre prochain cramptour", - "ruleDraw" : "cramptrois. Si votre quoicouchoix rempli entièrement un quoicoumorpion et provoque une cramptégalité, vous perdez" + "ruleDraw" : "cramptrois. Si votre quoicouchoix rempli entièrement un quoicoumorpion et provoque une cramptégalité, vous perdez", + "matchmakingTitle": "Matchmaking crampté", + "matchmakingStartSearch": "Cramptrouver une partie", + "matchmakingStopSearch": "Crampter le matchmaking", + "matchmakingNbPlayers": "Nombre de crampteurs", + "TournamentCreateTitle": "Créer un cramptournoi", + "TournamentCreateButton": "Créer le cramptournoi", + "TournamentCreateTournamentName": "Nom du cramptournoi", + "TournamentCreateNbPlayerByGame": "Nombre de crampteurs en crampté", + "TournamentCreateNbPlayer": "Nombre de crampteurs dans le cramptournoi" } diff --git a/frontend/static/js/lang/en.json b/frontend/static/js/lang/en.json index 7d47ddf..406142b 100644 --- a/frontend/static/js/lang/en.json +++ b/frontend/static/js/lang/en.json @@ -42,5 +42,14 @@ "ruleTitle" : "Rules", "ruleBase" : "1. Win on one of the 9 tictactoe to win the game", "ruleMovement" : "2. You start on the central tictactoe, and play on the one corresponding to your choice on the next turn", - "ruleDraw" : "3. If your play cause a tictactoe to be full and a draw, you lose the game" + "ruleDraw" : "3. If your play cause a tictactoe to be full and a draw, you lose the game", + "matchmakingTitle": "Matchmaking", + "matchmakingStartSearch": "Find a game", + "matchmakingStopSearch": "Stop matchmaking", + "matchmakingNbPlayers": "Number of players", + "TournamentCreateTitle": "Create tournament", + "TournamentCreateButton": "Create tournament", + "TournamentCreateTournamentName": "Tournament Name", + "TournamentCreateNbPlayerByGame": "Number of player in a game", + "TournamentCreateNbPlayer": "Number of players in the tournament" } diff --git a/frontend/static/js/lang/fr.json b/frontend/static/js/lang/fr.json index b867d41..523e068 100644 --- a/frontend/static/js/lang/fr.json +++ b/frontend/static/js/lang/fr.json @@ -42,5 +42,14 @@ "ruleTitle" : "Règles", "ruleBase" : "1. Vous devez gagner sur une des 9 grilles pour gagner la partie", "ruleMovement" : "2. Vous commencez sur le morpion central, et jouez sur le morpion correspondant a votre choix a votre prochain tour", - "ruleDraw" : "3. Si votre choix rempli entièrement un morpion et provoque une égalité, vous perdez" + "ruleDraw" : "3. Si votre choix rempli entièrement un morpion et provoque une égalité, vous perdez", + "matchmakingTitle": "Matchmaking", + "matchmakingStartSearch": "Trouver une partie", + "matchmakingStopSearch": "Arrêter le matchmaking", + "matchmakingNbPlayers": "Nombre de joueurs", + "TournamentCreateTitle": "Créer un tournoi", + "TournamentCreateButton": "Créer le tournoi", + "TournamentCreateTournamentName": "Nom du tournoi", + "TournamentCreateNbPlayerByGame": "Nombre de joueurs en jeu", + "TournamentCreateNbPlayer": "Nombre de joueurs dans le tournoi" } diff --git a/frontend/static/js/lang/tp.json b/frontend/static/js/lang/tp.json index 43f96c2..a9b104d 100644 --- a/frontend/static/js/lang/tp.json +++ b/frontend/static/js/lang/tp.json @@ -40,6 +40,15 @@ "ruleTitle" : "Rules", "ruleBase" : "1. Win on wan pi the 9 tictactoe tawa win the game", "ruleMovement" : "2. Sina open on the central tictactoe, en play on the wan corresponding tawa your choice on the next turn", - "ruleDraw" : "3. If your play cause a tictactoe tawa be full en a draw, sina lose the game" + "ruleDraw" : "3. If your play cause a tictactoe tawa be full en a draw, sina lose the game", + "matchmakingTitle": "Matchmaking", + "matchmakingStartSearch": "lukin e ilo musi", + "matchmakingStopSearch": "o pini e pana sona e jan pi pana sona e jan ante.", + "matchmakingNbPlayers": "nanpa pi jan ante", + "TournamentCreateTitle": "o pana e musi ante e musi", + "TournamentCreateButton": "jo ala pona li jo e ijo li pali e ijo li pana e ijo li toki e ijo li kama jo e ijo li kama pali e ijo li kama pana e ijo li kama toki e ijo li kama jo e ijo li kama pali e ijo li kama pana e ijo", + "TournamentCreateTournamentName": "ilo kipisi", + "TournamentCreateNbPlayerByGame": "ilo jan lon poki pi lon anpa en sike pimeja li kama.", + "TournamentCreateNbPlayer": "nanpa pi jan pona lon soweli musi" } diff --git a/frontend/static/js/views/Dashboard.js b/frontend/static/js/views/Dashboard.js index a7c0e87..7b8be35 100644 --- a/frontend/static/js/views/Dashboard.js +++ b/frontend/static/js/views/Dashboard.js @@ -12,7 +12,7 @@ export default class extends AbstractView { Akel is a game engine designed to be easy to use. The purpose of the project is learning about game engine development, discovering new rendering processes and learning to use new tools. It is mainly coded on and for Linux but is cross-platform and has been tested on Windows and MacOS.

- Akel Engine. + Akel Engine. View recent posts.

`; diff --git a/frontend/static/js/views/MatchMakingView.js b/frontend/static/js/views/MatchMakingView.js index 40f9922..c6c1404 100644 --- a/frontend/static/js/views/MatchMakingView.js +++ b/frontend/static/js/views/MatchMakingView.js @@ -1,4 +1,4 @@ -import { client, navigateTo } from "../index.js"; +import { client, lang, navigateTo } from "../index.js"; import { clear, fill_errors } from "../utils/formUtils.js"; import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js"; @@ -7,56 +7,36 @@ export default class extends AbstractAuthenticatedView { constructor(params) { super(params, "Matchmaking"); - this.game_mode = 0; // 0 -> 2D; 1 -> 3D } - async press_button() + async toggle_search() { clear("innerText", ["detail"]); if (client.matchmaking.searching) { client.matchmaking.stop(); - document.getElementById("button").value = "Find a game"; + this.button.innerHTML = lang.get("matchmakingStartSearch"); } else { - let nb_players = document.getElementById("nb_players-input").value; + let nb_players = this.input.value; await client.matchmaking.start(this.onreceive.bind(this), this.ondisconnect.bind(this), nb_players); - document.getElementById("button").value = "Stop matchmaking"; + this.button.innerHTML = lang.get("matchmakingStopSearch"); } } - async press_button_game_mode() - { - if(this.game_mode === 0) - { - document.getElementById("game-mode").value = "3D"; - this.game_mode = 1; - } - else - { - document.getElementById("game-mode").value = "2D"; - this.game_mode = 0; - } - } - ondisconnect(event) { - let button = document.getElementById("button") - - if (button === null) - return - - button.value = "Find a game"; + this.button.innerHTML = lang.get("matchmakingStartSearch"); } onreceive(data) { if (data.detail === "game_found") { - navigateTo(`/games/${data.game_id}/${this.game_mode}`); + navigateTo(`/games/${data.game_id}`); return; } this.display_data(data); @@ -70,46 +50,44 @@ export default class extends AbstractAuthenticatedView { async postInit() { - let button = document.getElementById("button"); + this.button = document.getElementById("toggle-search"); + this.input = document.getElementById("nb-players-input"); - button.onclick = this.press_button.bind(this); + this.button.onclick = this.toggle_search.bind(this); - let input = document.getElementById("nb_players-input"); + this.input.addEventListener('keydown', async ev => { - input.addEventListener('keydown', async ev => { if (ev.key !== 'Enter') return; - if (client.matchmaking.searching) - client.matchmaking.stop(); - - let nb_players = document.getElementById("nb_players-input").value; - - await client.matchmaking.start(this.onreceive.bind(this), this.ondisconnect.bind(this), nb_players); - - document.getElementById("button").value = "Stop matchmaking"; + await this.toggle_search.bind(this); }); let update = () => { - if (input.value < 2 || input.value > 4) - button.disabled = true; - else - button.disabled = false; + this.button.disabled = (this.input.value < 2 || this.input.value > 4); }; ["change", "oninput"].forEach((event_name) => { - input.addEventListener(event_name, update); + this.input.addEventListener(event_name, update); }); - document.getElementById("game-mode").onclick = this.press_button_game_mode.bind(this); } async getHtml() { - return ` -

Select mode

- - - - + return /* HTML */ ` +
+
+

${lang.get("matchmakingTitle")}

+
+ + + +
+
+ + +
+
+
`; } diff --git a/frontend/static/js/views/SettingsView.js b/frontend/static/js/views/SettingsView.js index ffb7662..387cf80 100644 --- a/frontend/static/js/views/SettingsView.js +++ b/frontend/static/js/views/SettingsView.js @@ -1,55 +1,82 @@ -import { client, navigateTo } from "../index.js"; -import { clear, fill_errors } from "../utils/formUtils.js"; -import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js"; +import { client, navigateTo } from '../index.js'; +import { clear, fill_errors } from '../utils/formUtils.js'; +import AbstractAuthenticatedView from './abstracts/AbstractAuthenticatedView.js'; export default class extends AbstractAuthenticatedView { constructor(params) { - super(params, "Settings"); + super(params, 'Settings'); this.PROFILE_PICTURE_MAX_SIZE = 2 * 1024 * 1024; // 2MB } async postInit() { - this.display_avatar(); - document.getElementById("save-account-button").onclick = () => this.save_account(); - document.getElementById("delete-account-button").onclick = () => this.delete_account(); - document.getElementById("save-profile-button").onclick = () => this.save_profile(); + this.avatarInit(); + this.usernameInit(); + + // document.getElementById('delete-account-button').onclick = () => this.delete_account(); } - async display_avatar() { - let profile = await client.profiles.getProfile(client.me.username); - if (profile !== undefined || profile !== null) { - if (document.getElementById("avatar") != undefined) - document.getElementById("avatar").remove(); - let avatar = document.createElement("img"); - avatar.id = "avatar"; - avatar.src = profile.avatar_url + '?t=' +new Date().getTime(); - document.getElementsByClassName("avatar")[0].appendChild(avatar); + usernameInit() { + const usernameInput = document.getElementById('usernameInput'); + const usernameSave = document.getElementById('usernameSave'); + + usernameInput.oninput = e => { + const value = e.target.value; + if (value != client.me.username && value.length) + usernameSave.classList.remove('disabled'); + else + usernameSave.classList.add('disabled'); } + usernameSave.onclick = _ => this.saveUsername(); + } + + avatarInit() { + const avatar = document.getElementById('avatar'); + const avatarInput = document.getElementById('avatarInput'); + const avatarUpload = document.getElementById('avatarUpload'); + const avatarDelete = document.getElementById('avatarDelete'); + + avatar.onclick = _ => avatarInput.click(); + avatarInput.onchange = function () { + const selectedFile = this.files[0]; + if (!selectedFile) + return; + + avatar.src = URL.createObjectURL(selectedFile); + avatarUpload.classList.remove('d-none'); + } + avatarUpload.onclick = _ => this.saveAvatar(); + avatarDelete.onclick = _ => this.deleteAvatar(); + } + + async displayAvatar() { + let avatar = document.getElementById('avatar'); + avatar.src = client.me.avatar_url + '?t=' + new Date().getTime(); + console.log(avatar.src); } async delete_account() { - let current_password = document.getElementById("current_password-input").value; + let current_password = document.getElementById('current_password-input').value; let response_data = await client.account.delete(current_password); - if (response_data === null || response_data === "user deleted") + if (response_data === null || response_data === 'user deleted') { - navigateTo("/login"); + navigateTo('/login'); return; } - clear("innerHTML", ["current_password-input"]); - fill_errors({"current_password-input": response_data.password}, "innerHTML"); + clear('innerHTML', ['current_password-input']); + fill_errors({'current_password-input': response_data.password}, 'innerHTML'); } async save_account() { - let username = document.getElementById("username-input").value; - let new_password = document.getElementById("new_password-input").value; - let current_password = document.getElementById("current_password-input").value; + let username = document.getElementById('username-input').value; + let new_password = document.getElementById('new_password-input').value; + let current_password = document.getElementById('current_password-input').value; let data = {}; @@ -61,67 +88,139 @@ export default class extends AbstractAuthenticatedView if (response_data === null) { - navigateTo("/login"); + navigateTo('/login'); return; } - if (response_data === "data has been alterate") - response_data = {"save-account": "saved"}; + if (response_data === 'data has been alterate') + response_data = {'save-account': 'saved'}; - clear("innerHTML", ["username", "new_password", "current_password", "save-account", "delete-account"]); - fill_errors(response_data, "innerHTML"); - } - - async save_profile() - { - let avatar = document.getElementById("avatar-input"); - - if (avatar.files[0] !== undefined) - { - if (avatar.files[0].size > this.PROFILE_PICTURE_MAX_SIZE) { - document.getElementById("save-profile").classList.add('text-danger'); - document.getElementById("save-profile").innerHTML = "Image too large :/"; - return; - } - let form_data = new FormData(); - form_data.append("avatar", avatar.files[0]); - await client.me.change_avatar(form_data); - this.display_avatar(); - } - document.getElementById("save-profile").classList.remove('text-danger'); - document.getElementById("save-profile").innerHTML = "Saved"; + clear('innerHTML', ['username', 'new_password', 'current_password', 'save-account', 'delete-account']); + fill_errors(response_data, 'innerHTML'); } - async getHtml() - { - return /* HTML */ ` - -

ME

-
-
-
- -
-

Profile

- - - -
- Logout -
- `; - } + if (!username.length || username === client.me.username) + return; + + const error = await client.account.updateUsername(username); + if (!error) { + usernameDetail.classList.remove('text-danger'); + usernameDetail.classList.add('text-success'); + usernameDetail.innerHTML = 'Username Saved.'; + setTimeout(_ => usernameDetail.innerHTML = '', 2000); + document.getElementById('usernameSave').classList.add('disabled'); + } else { + usernameDetail.classList.remove('text-success'); + usernameDetail.classList.add('text-danger'); + usernameDetail.innerHTML = error; + document.getElementById('usernameSave').classList.add('disabled'); + console.log(error); + } + } + + async saveAvatar() + { + const avatarInput = document.getElementById('avatarInput'); + const selectedFile = avatarInput.files[0]; + const avatarDetail = document.getElementById('avatarDetail'); + + if (!selectedFile) + return; + + if (selectedFile.size > this.PROFILE_PICTURE_MAX_SIZE) { + avatarDetail.classList.remove('text-success'); + avatarDetail.classList.add('text-danger'); + avatarDetail.innerHTML = 'Image is too large.'; //to translate + return; + } + + const error = await client.me.changeAvatar(selectedFile); + if (!error) { + avatarDetail.classList.remove('text-danger'); + avatarDetail.classList.add('text-success'); + avatarDetail.innerHTML = 'Avatar saved.'; //to translate + setTimeout(_ => avatarDetail.innerHTML = '', 2000); + document.getElementById('avatarDelete').classList.remove('d-none'); + document.getElementById('avatarUpload').classList.add('d-none'); + avatarInput.value = null; + } else { + avatarDetail.classList.remove('text-success'); + avatarDetail.classList.add('text-danger'); + avatarDetail.innerHTML = error.avatar[0]; + document.getElementById('avatarUpload').classList.add('d-none'); + avatarInput.value = null; + console.log(error); + } + this.displayAvatar(); + } + + async deleteAvatar() { + const avatarDetail = document.getElementById('avatarDetail'); + + const error = await client.me.deleteAvatar(); + if (!error) { + avatarDetail.classList.remove('text-danger'); + avatarDetail.classList.add('text-success'); + avatarDetail.innerHTML = 'Avatar deleted.'; //to translate + setTimeout(_ => avatarDetail.innerHTML = '', 2000); + document.getElementById('avatarDelete').classList.add('d-none'); + } else { + avatarDetail.classList.remove('text-success'); + avatarDetail.classList.add('text-danger'); + avatarDetail.innerHTML = 'Something went wrong.'; //to translate + } + this.displayAvatar(); + } + + async getHtml() + { + const avatarUnchanged = client.me.avatar_url === '/static/avatars/default.avif'; + + return /* HTML */ ` +
+
+

Avatar

+ + +
+ + + +
+
+
+

Account

+
+
+
+ + +
+ +
+ +
+
+
+ `; + // + //

Settings

+ // + // + // + // + // + // + // + // + // + // + // + } } diff --git a/frontend/static/js/views/tournament/TournamentCreateView.js b/frontend/static/js/views/tournament/TournamentCreateView.js index ffd7690..58e2bb1 100644 --- a/frontend/static/js/views/tournament/TournamentCreateView.js +++ b/frontend/static/js/views/tournament/TournamentCreateView.js @@ -1,4 +1,4 @@ -import {client, navigateTo} from "../../index.js"; +import {client, lang, navigateTo} from "../../index.js"; import { clear, fill_errors } from "../../utils/formUtils.js"; import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js"; @@ -13,13 +13,11 @@ export default class extends AbstractAuthenticatedView async create() { let name = document.getElementById("name-input").value; - let nb_players = document.getElementById("nb_players-input").value; - let nb_players_by_game = document.getElementById("nb_players_by_game-input").value; + let nb_players = document.getElementById("nb-players-input").value; + let nb_players_by_game = document.getElementById("nb-players-by-game-input").value; - let response_data = await client.tournaments.createTournament(nb_players, nb_players_by_game, name); - - if (response_data === null) - return; + let response = await client.tournaments.createTournament(nb_players, nb_players_by_game, name); + let response_data = await response.json(); let id = response_data.id; if (id !== undefined) @@ -37,16 +35,33 @@ export default class extends AbstractAuthenticatedView document.getElementById("create-button").onclick = this.create; } - async getHtml() - { - return ` - - - - - - - + async getHtml() { + + return /* HTML */ ` +
+
+

${lang.get("TournamentCreateTitle")}

+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
+
`; } } diff --git a/frontend/static/js/views/tournament/TournamentPageView.js b/frontend/static/js/views/tournament/TournamentPageView.js index 9081c48..d36fd3d 100644 --- a/frontend/static/js/views/tournament/TournamentPageView.js +++ b/frontend/static/js/views/tournament/TournamentPageView.js @@ -55,7 +55,7 @@ export default class extends AbstractAuthenticatedView document.getElementById("level").innerText = this.tournament.level; document.getElementById("state").innerText = this.tournament.state; - if (this.tournament.state === "waiting") + if (this.tournament.started === false) button.disabled = false; } diff --git a/profiles/models.py b/profiles/models.py index cd7d859..566e664 100644 --- a/profiles/models.py +++ b/profiles/models.py @@ -3,13 +3,16 @@ from django.db import models from django.contrib.auth.models import User from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver -from django.conf import settings from django.db.models import IntegerField from games.consumers import game_manager +from os.path import splitext + + def upload_to(instance, filename: str): - return f"./profiles/static/avatars/{instance.pk}.{filename.split('.')[1]}" + return f"./profiles/static/avatars/{instance.pk}{splitext(filename)[1]}" + # Create your models here. class ProfileModel(models.Model): diff --git a/profiles/serializers.py b/profiles/serializers.py index 893093b..7a9fe97 100644 --- a/profiles/serializers.py +++ b/profiles/serializers.py @@ -1,6 +1,8 @@ from rest_framework import serializers from .models import ProfileModel from django.conf import settings +from django.utils.translation import gettext as _ + class ProfileSerializer(serializers.ModelSerializer): @@ -16,5 +18,5 @@ class ProfileSerializer(serializers.ModelSerializer): Check that the image is not too large ''' if value.size > settings.PROFILE_PICTURE_MAX_SIZE: - raise serializers.ValidationError('Image is too large.'); - return value; + raise serializers.ValidationError(_('Image is too large.')) + return value diff --git a/profiles/urls.py b/profiles/urls.py index c7e9236..1335989 100644 --- a/profiles/urls.py +++ b/profiles/urls.py @@ -4,7 +4,7 @@ from . import viewsets from . import views urlpatterns = [ - path("settings", viewsets.MyProfileViewSet.as_view({'patch': 'partial_update'}), name="my_profile_page"), + path("settings", viewsets.MyProfileViewSet.as_view({'patch': 'partial_update', 'delete': 'delete_avatar'}), name="my_profile_page"), path("me", viewsets.MyProfileViewSet.as_view({'get': 'retrieve'}), name="my_profile_page"), path("", viewsets.ProfileViewSet.as_view({'get': 'list'}), name="profiles_list"), path("block", views.BlocksView.as_view(), name="block_page"), @@ -12,5 +12,4 @@ urlpatterns = [ path("friend", views.FriendsView.as_view(), name="friend_page"), path("user/", viewsets.ProfileViewSet.as_view({'get': 'retrieve'}), name="profile_page"), path("id/", viewsets.ProfileViewSet.as_view({'get': 'retrieve_id'}), name="profile_page"), - ] diff --git a/profiles/viewsets.py b/profiles/viewsets.py index 38e9755..ad8078e 100644 --- a/profiles/viewsets.py +++ b/profiles/viewsets.py @@ -1,4 +1,3 @@ -from rest_framework import permissions from rest_framework.parsers import MultiPartParser, FormParser from rest_framework import permissions, status from rest_framework import viewsets @@ -42,28 +41,35 @@ class ProfileViewSet(viewsets.ModelViewSet): profile["avatar"] = profile["avatar"][profile["avatar"].find("static") - 1:] return Response(serializer.data) -class MyProfileViewSet(viewsets.ModelViewSet): +class MyProfileViewSet(viewsets.ModelViewSet): permission_classes = (permissions.IsAuthenticated,) authentication_classes = (SessionAuthentication,) serializer_class = ProfileSerializer - queryset = ProfileModel.objects.all + queryset = ProfileModel.objects.all() def get_object(self): - obj = self.queryset().get(pk=self.request.user.pk) + obj = self.queryset.get(pk=self.request.user.pk) return obj - def perform_update(self, serializer, pk=None): - serializer.is_valid(raise_exception=True); - profile: ProfileModel = self.get_object(); - avatar = serializer.validated_data.get('avatar'); + def perform_update(self, serializer: ProfileSerializer, pk=None): + serializer.is_valid(raise_exception=True) + avatar = serializer.validated_data.get('avatar') + profile: ProfileModel = self.get_object() if (avatar is not None): if (profile.avatar.name != "./profiles/static/avatars/default.avif"): profile.avatar.storage.delete(profile.avatar.name) - profile.avatar = avatar - profile.save() - + serializer.save() + + def delete_avatar(self, request, pk=None): + profile = self.get_object() + if (profile.avatar.name != './profiles/static/avatars/default.avif'): + profile.avatar.storage.delete(profile.avatar.name) + profile.avatar.name = './profiles/static/avatars/default.avif' + profile.save() + return Response(ProfileSerializer(profile).data) + def retrieve(self, request: HttpRequest, pk=None): instance: ProfileModel = self.get_object() instance.avatar.name = instance.avatar.name[instance.avatar.name.find("static") - 1:] diff --git a/tournament/serializers.py b/tournament/serializers.py index 6f29235..9308e08 100644 --- a/tournament/serializers.py +++ b/tournament/serializers.py @@ -5,6 +5,7 @@ from games.serializers import GameSerializer class TournamentSerializer(serializers.ModelSerializer): levels = serializers.SerializerMethodField(read_only=True, required=False) + state = serializers.SerializerMethodField(read_only=True, required=False) level = serializers.ReadOnlyField() started = serializers.ReadOnlyField() finished = serializers.ReadOnlyField() @@ -12,7 +13,10 @@ class TournamentSerializer(serializers.ModelSerializer): class Meta: model = TournamentModel - fields = ["name", "nb_players", "nb_players_by_game", "level", "started", "finished", "levels", "id"] + fields = ["name", "nb_players", "nb_players_by_game", "level", "started", "finished", "levels", "id", "state"] + + def get_state(self, instance: TournamentModel): + return ["waiting", "started", "finished"][instance.started + instance.finished] def get_levels(self, instance: TournamentModel): levels: list[list[int]] = [] diff --git a/tournament/viewset.py b/tournament/viewset.py index 0ffd2cd..5106928 100644 --- a/tournament/viewset.py +++ b/tournament/viewset.py @@ -43,7 +43,7 @@ class TournamentViewSet(viewsets.ModelViewSet): query = TournamentModel.objects.filter(started=False, finished=False) case _: query = TournamentModel.objects.all() - serializer = TournamentSerializer(query, many=True) + serializer = self.serializer_class(query, many=True) return Response(serializer.data) def retrieve(self, request: HttpRequest, pk):