This commit is contained in:
Xamora 2024-03-12 10:29:25 +01:00
commit 3b1f9d91b5
33 changed files with 433 additions and 309 deletions

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,6 +17,11 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\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 #: views/login.py:22
msgid "Invalid username or password." msgid "Invalid username or password."
msgstr "Nom d'utilisateur ou mot de passe incorect." msgstr "Nom d'utilisateur ou mot de passe incorect."

View File

@ -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

View File

@ -1,6 +1,6 @@
from django.urls import path 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 = [ urlpatterns = [
path("register", register.RegisterView.as_view(), name="register"), path("register", register.RegisterView.as_view(), name="register"),
@ -8,6 +8,5 @@ urlpatterns = [
path("logout", logout.LogoutView.as_view(), name="logout"), path("logout", logout.LogoutView.as_view(), name="logout"),
path("logged", logged.LoggedView.as_view(), name="logged"), path("logged", logged.LoggedView.as_view(), name="logged"),
path("delete", delete.DeleteView.as_view(), name="delete"), path("delete", delete.DeleteView.as_view(), name="delete"),
path("edit", edit.EditView.as_view(), name="change_password") path('update_profile', update_profile.UpdateProfileView.as_view(), name='update_profile')
] ]

View File

@ -5,17 +5,18 @@ from django.contrib.auth import logout
from django.http import HttpRequest from django.http import HttpRequest
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
class DeleteView(APIView): class DeleteView(APIView):
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (SessionAuthentication,) authentication_classes = (SessionAuthentication,)
def delete(self, request: HttpRequest): def delete(self, request: HttpRequest):
data: dict = request.data data: dict = request.data
password: str = data["password"] password: str = data["password"]
if (password is None): if (request.user.check_password(password) is False):
return Response({"password": ["This field may not be blank."]}) return Response({"password": ["Password incorrect."]},
if (request.user.check_password(password) == False): status.HTTP_401_UNAUTHORIZED)
return Response({"password": ["Password wrong."]})
request.user.delete() request.user.delete()
logout(request) logout(request)
return Response("user deleted", status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)

View File

@ -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")

View File

@ -2,10 +2,8 @@ from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import permissions, status from rest_framework import permissions, status
from django.http import HttpRequest from django.http import HttpRequest
from django.contrib.auth import login
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
from ..serializers.login import LoginSerializer
class LoggedView(APIView): class LoggedView(APIView):
@ -13,4 +11,4 @@ class LoggedView(APIView):
authentication_classes = (SessionAuthentication,) authentication_classes = (SessionAuthentication,)
def get(self, request: HttpRequest): 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)

View File

@ -8,6 +8,7 @@ from django.utils.translation import gettext as _
from ..serializers.login import LoginSerializer from ..serializers.login import LoginSerializer
class LoginView(APIView): class LoginView(APIView):
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)

View File

@ -5,9 +5,11 @@ from rest_framework.response import Response
from django.http import HttpRequest from django.http import HttpRequest
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
class LogoutView(APIView): class LogoutView(APIView):
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (SessionAuthentication,) authentication_classes = (SessionAuthentication,)
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
logout(request) logout(request)
return Response("user unlogged", status=status.HTTP_200_OK) return Response("user logged out", status.HTTP_200_OK)

View File

@ -5,8 +5,10 @@ from rest_framework.response import Response
from django.http import HttpRequest from django.http import HttpRequest
from django.contrib.auth import login from django.contrib.auth import login
class RegisterView(APIView): class RegisterView(APIView):
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
def post(self, request: HttpRequest): def post(self, request: HttpRequest):
data = request.data data = request.data
serializer = RegisterSerialiser(data=data) serializer = RegisterSerialiser(data=data)

View File

@ -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)

View File

@ -1,11 +1,3 @@
#app #avatar {
max-height: 10em;
max-width: 10em;
min-height: 6em;
min-width: 6em;
}
#popup { #popup {
position: fixed; position: fixed;
font-size: 1.2em; font-size: 1.2em;

View File

@ -1,11 +0,0 @@
#app * {
font-size: 30px;
}
#app #main
{
width: 60%;
display: flex;
flex-direction: column;
}

View File

@ -48,41 +48,24 @@ class Account
} }
/** /**
* Get account data (username) * @param {String} newUsername
* @returns {?Promise<Object>} * @returns {?Promise<Object>}
*/ */
async get() async updateUsername(newUsername)
{ {
let response = await this.client._get("/api/accounts/edit"); const data = {
let response_data = await response.json(); username: newUsername
};
const response = await this.client._patch_json(`/api/accounts/update_profile`, data);
const respondeData = await response.json();
if (response.status === 403) if (response.status === 200) {
{ this.client.me.username = respondeData.username;
this.client._update_logged(false); document.getElementById('navbarDropdownButton').innerHTML = respondeData.username;
document.getElementById('myProfileLink').href = '/profiles/' + respondeData.username;
return null; return null;
} }
return response_data; return respondeData['authorize'] || respondeData['detail'] || respondeData['username']?.join(' ') || 'Error.';
}
/**
*
* @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;
} }
} }

View File

@ -3,7 +3,6 @@ import { MatchMaking } from "./Matchmaking.js";
import { Profiles } from "./Profiles.js"; import { Profiles } from "./Profiles.js";
import { Channels } from './chat/Channels.js'; import { Channels } from './chat/Channels.js';
import { MyProfile } from "./MyProfile.js"; import { MyProfile } from "./MyProfile.js";
import { navigateTo } from "../index.js";
import { Tourmanents } from "./tournament/Tournaments.js"; import { Tourmanents } from "./tournament/Tournaments.js";
import { Notice } from "./chat/Notice.js"; import { Notice } from "./chat/Notice.js";
import { Channel } from "./chat/Channel.js"; import { Channel } from "./chat/Channel.js";
@ -97,6 +96,9 @@ class Client
{ {
let response = await fetch(this._url + uri, { let response = await fetch(this._url + uri, {
method: "GET", method: "GET",
headers: {
'Accept-Language': this.lang.currentLang
},
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
return response; return response;
@ -135,6 +137,7 @@ class Client
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-CSRFToken": getCookie("csrftoken"), "X-CSRFToken": getCookie("csrftoken"),
'Accept-Language': this.lang.currentLang,
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
@ -154,6 +157,7 @@ class Client
headers: { headers: {
"X-CSRFToken": getCookie("csrftoken"), "X-CSRFToken": getCookie("csrftoken"),
"Content-Type": "application/json", "Content-Type": "application/json",
'Accept-Language': this.lang.currentLang,
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
@ -172,6 +176,7 @@ class Client
method: "PATCH", method: "PATCH",
headers: { headers: {
"X-CSRFToken": getCookie("csrftoken"), "X-CSRFToken": getCookie("csrftoken"),
'Accept-Language': this.lang.currentLang,
}, },
body: file, body: file,
}); });

View File

@ -14,15 +14,35 @@ class MyProfile extends Profile
/** /**
* *
* @param {*} form_data * @param {File} selectedFile
* @returns {Promise<Object>} * @returns {Promise<Response>}
*/ */
async change_avatar(form_data) async changeAvatar(selectedFile)
{ {
let response = await this.client._patch_file(`/api/profiles/settings`, form_data); const formData = new FormData();
let response_data = await response.json(); 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;
} }
} }

View File

@ -79,7 +79,7 @@ class Tourmanent
*/ */
async init() 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) if (response.status !== 200)
return response.status; return response.status;

View File

@ -17,12 +17,12 @@ class Tourmanents
/** /**
* *
* @param {Number} id * @param {Number} id
* @returns {?Promise<Tournament>} * @returns {Promise<Tournament>}
*/ */
async getTournament(id) async getTournament(id)
{ {
let tournament = new Tourmanent(this.client); let tournament = new Tourmanent(this.client, id);
if (await tournament.init(id)) if (await tournament.init())
return null; return null;
return tournament; return tournament;
} }
@ -32,17 +32,13 @@ class Tourmanents
* @param {Number} nb_players * @param {Number} nb_players
* @param {Number} nb_players_by_game * @param {Number} nb_players_by_game
* @param {String} name * @param {String} name
* @returns * @returns {Response}
*/ */
async createTournament(nb_players, nb_players_by_game, name = "") 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}); 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;
return response.status;
let response_data = await response.json();
return response_data;
} }
/** /**
@ -71,7 +67,8 @@ class Tourmanents
tournament_data.started, tournament_data.started,
tournament_data.finished, tournament_data.finished,
tournament_data.levels, tournament_data.levels,
tournament_data.id)); tournament_data.id,
tournament_data.state));
}); });
return tournaments; return tournaments;

View File

@ -15,7 +15,7 @@ import SettingsView from "./views/SettingsView.js";
import ProfilePageView from "./views/ProfilePageView.js"; import ProfilePageView from "./views/ProfilePageView.js";
import MatchMakingView from "./views/MatchMakingView.js"; import MatchMakingView from "./views/MatchMakingView.js";
import TournamentPageView from "./views/tournament/TournamentPageView.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 TournamentCreateView from "./views/tournament/TournamentCreateView.js";
import AuthenticationView from "./views/accounts/AuthenticationView.js"; import AuthenticationView from "./views/accounts/AuthenticationView.js";
import TicTacToeView from "./views/TicTacToeView.js"; import TicTacToeView from "./views/TicTacToeView.js";
@ -82,7 +82,7 @@ const router = async(uri) => {
{ path: "/profiles/:username", view: ProfilePageView }, { path: "/profiles/:username", view: ProfilePageView },
{ path: "/tournaments/create", view: TournamentCreateView }, { path: "/tournaments/create", view: TournamentCreateView },
{ path: "/tournaments/:id", view: TournamentPageView }, { path: "/tournaments/:id", view: TournamentPageView },
{ path: "/tournaments/", view: TournamentsView }, { path: "/tournaments/", view: TournamentsListView },
{ path: "/login", view: AuthenticationView }, { path: "/login", view: AuthenticationView },
{ path: "/register", view: AuthenticationView }, { path: "/register", view: AuthenticationView },
{ path: "/logout", view: LogoutView }, { path: "/logout", view: LogoutView },
@ -155,7 +155,6 @@ document.addEventListener("DOMContentLoaded", async () => {
el.onclick = async _ => { el.onclick = async _ => {
if (await lang.changeLanguage(el.value)) if (await lang.changeLanguage(el.value))
return; return;
console.log(lang);
document.querySelector('#languageSelector > .active')?.classList.remove('active'); document.querySelector('#languageSelector > .active')?.classList.remove('active');
el.classList.add('active'); el.classList.add('active');
}; };

View File

@ -42,5 +42,14 @@
"ruleTitle" : "Règles cramptés", "ruleTitle" : "Règles cramptés",
"ruleBase" : "cramptun. Vous devez quouicougagner sur une des 9 quoicougrilles pour gagner la croustipartie", "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", "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"
} }

View File

@ -42,5 +42,14 @@
"ruleTitle" : "Rules", "ruleTitle" : "Rules",
"ruleBase" : "1. Win on one of the 9 tictactoe to win the game", "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", "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"
} }

View File

@ -42,5 +42,14 @@
"ruleTitle" : "Règles", "ruleTitle" : "Règles",
"ruleBase" : "1. Vous devez gagner sur une des 9 grilles pour gagner la partie", "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", "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"
} }

View File

@ -40,6 +40,15 @@
"ruleTitle" : "Rules", "ruleTitle" : "Rules",
"ruleBase" : "1. Win on wan pi the 9 tictactoe tawa win the game", "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", "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"
} }

View File

@ -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 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.
</p> </p>
<p> <p>
<a href="https://akel-engine.com" data-link>Akel Engine</a>. <a href="https://cdn.discordapp.com/attachments/1198285289236463699/1211450599007064074/malonerd.png?ex=6600b34a&is=65ee3e4a&hm=359877a4259663411dc24383562193e0e8862774022ca9989b6960f6628f1e2c&">Akel Engine</a>.
<a href="/posts" data-link>View recent posts</a>. <a href="/posts" data-link>View recent posts</a>.
</p> </p>
`; `;

View File

@ -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 { clear, fill_errors } from "../utils/formUtils.js";
import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js"; import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js";
@ -7,56 +7,36 @@ export default class extends AbstractAuthenticatedView {
constructor(params) constructor(params)
{ {
super(params, "Matchmaking"); super(params, "Matchmaking");
this.game_mode = 0; // 0 -> 2D; 1 -> 3D
} }
async press_button() async toggle_search()
{ {
clear("innerText", ["detail"]); clear("innerText", ["detail"]);
if (client.matchmaking.searching) if (client.matchmaking.searching)
{ {
client.matchmaking.stop(); client.matchmaking.stop();
document.getElementById("button").value = "Find a game"; this.button.innerHTML = lang.get("matchmakingStartSearch");
} }
else 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); 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) ondisconnect(event)
{ {
let button = document.getElementById("button") this.button.innerHTML = lang.get("matchmakingStartSearch");
if (button === null)
return
button.value = "Find a game";
} }
onreceive(data) onreceive(data)
{ {
if (data.detail === "game_found") if (data.detail === "game_found")
{ {
navigateTo(`/games/${data.game_id}/${this.game_mode}`); navigateTo(`/games/${data.game_id}`);
return; return;
} }
this.display_data(data); this.display_data(data);
@ -70,46 +50,44 @@ export default class extends AbstractAuthenticatedView {
async postInit() 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') if (ev.key !== 'Enter')
return; return;
if (client.matchmaking.searching) await this.toggle_search.bind(this);
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";
}); });
let update = () => { let update = () => {
if (input.value < 2 || input.value > 4) this.button.disabled = (this.input.value < 2 || this.input.value > 4);
button.disabled = true;
else
button.disabled = false;
}; };
["change", "oninput"].forEach((event_name) => { ["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() { async getHtml() {
return ` return /* HTML */ `
<h1>Select mode</h1> <div class='container-fluid'>
<input type="number" value="2" min="1" max="4" id="nb_players-input"> <div class='border border-2 rounded bg-light-subtle mx-auto p-2 col-md-7 col-lg-4'>
<input type="button" value="Find a game" id="button"> <h4 class='text-center fw-semibold mb-4' id="title">${lang.get("matchmakingTitle")}</h4>
<input type="button" value="2D" id="game-mode"> <div class='form-floating mb-2'>
<span id="detail"></span> <input type='number' min='2' value='2' max='4' class='form-control' id='nb-players-input' placeholder='${lang.get("matchmakingNbPlayers")}'>
<label for='nb-players-input' id='username-label'>${lang.get("matchmakingNbPlayers")}</label>
<span class='text-danger' id='username'></span>
</div>
<div class='d-flex'>
<button type='button' class='btn btn-primary mt-3 mb-2 mx-2' id='toggle-search'>${lang.get("matchmakingStartSearch")}</button>
<span class='text-danger my-auto mx-2' id='detail'></span>
</div>
</div>
</div>
`; `;
} }

View File

@ -1,55 +1,82 @@
import { client, navigateTo } from "../index.js"; import { client, navigateTo } from '../index.js';
import { clear, fill_errors } from "../utils/formUtils.js"; import { clear, fill_errors } from '../utils/formUtils.js';
import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js"; import AbstractAuthenticatedView from './abstracts/AbstractAuthenticatedView.js';
export default class extends AbstractAuthenticatedView export default class extends AbstractAuthenticatedView
{ {
constructor(params) constructor(params)
{ {
super(params, "Settings"); super(params, 'Settings');
this.PROFILE_PICTURE_MAX_SIZE = 2 * 1024 * 1024; // 2MB this.PROFILE_PICTURE_MAX_SIZE = 2 * 1024 * 1024; // 2MB
} }
async postInit() async postInit()
{ {
this.display_avatar(); this.avatarInit();
document.getElementById("save-account-button").onclick = () => this.save_account(); this.usernameInit();
document.getElementById("delete-account-button").onclick = () => this.delete_account();
document.getElementById("save-profile-button").onclick = () => this.save_profile(); // document.getElementById('delete-account-button').onclick = () => this.delete_account();
} }
async display_avatar() { usernameInit() {
let profile = await client.profiles.getProfile(client.me.username); const usernameInput = document.getElementById('usernameInput');
if (profile !== undefined || profile !== null) { const usernameSave = document.getElementById('usernameSave');
if (document.getElementById("avatar") != undefined)
document.getElementById("avatar").remove(); usernameInput.oninput = e => {
let avatar = document.createElement("img"); const value = e.target.value;
avatar.id = "avatar"; if (value != client.me.username && value.length)
avatar.src = profile.avatar_url + '?t=' +new Date().getTime(); usernameSave.classList.remove('disabled');
document.getElementsByClassName("avatar")[0].appendChild(avatar); 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() 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); 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; return;
} }
clear("innerHTML", ["current_password-input"]); clear('innerHTML', ['current_password-input']);
fill_errors({"current_password-input": response_data.password}, "innerHTML"); fill_errors({'current_password-input': response_data.password}, 'innerHTML');
} }
async save_account() async save_account()
{ {
let username = document.getElementById("username-input").value; let username = document.getElementById('username-input').value;
let new_password = document.getElementById("new_password-input").value; let new_password = document.getElementById('new_password-input').value;
let current_password = document.getElementById("current_password-input").value; let current_password = document.getElementById('current_password-input').value;
let data = {}; let data = {};
@ -61,67 +88,139 @@ export default class extends AbstractAuthenticatedView
if (response_data === null) if (response_data === null)
{ {
navigateTo("/login"); navigateTo('/login');
return; return;
} }
if (response_data === "data has been alterate") if (response_data === 'data has been alterate')
response_data = {"save-account": "saved"}; response_data = {'save-account': 'saved'};
clear("innerHTML", ["username", "new_password", "current_password", "save-account", "delete-account"]); clear('innerHTML', ['username', 'new_password', 'current_password', 'save-account', 'delete-account']);
fill_errors(response_data, "innerHTML"); fill_errors(response_data, 'innerHTML');
} }
async save_profile() async saveUsername()
{ {
let avatar = document.getElementById("avatar-input"); const usernameInput = document.getElementById('usernameInput');
const username = usernameInput.value;
const usernameDetail = document.getElementById('usernameDetail');
if (avatar.files[0] !== undefined) 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()
{ {
if (avatar.files[0].size > this.PROFILE_PICTURE_MAX_SIZE) { const avatarInput = document.getElementById('avatarInput');
document.getElementById("save-profile").classList.add('text-danger'); const selectedFile = avatarInput.files[0];
document.getElementById("save-profile").innerHTML = "Image too large :/"; 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; return;
} }
let form_data = new FormData();
form_data.append("avatar", avatar.files[0]); const error = await client.me.changeAvatar(selectedFile);
await client.me.change_avatar(form_data); if (!error) {
this.display_avatar(); 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);
} }
document.getElementById("save-profile").classList.remove('text-danger'); this.displayAvatar();
document.getElementById("save-profile").innerHTML = "Saved"; }
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() async getHtml()
{ {
return /* HTML */ ` const avatarUnchanged = client.me.avatar_url === '/static/avatars/default.avif';
<link rel="stylesheet" href="/static/css/settings.css">
<h1>ME</h1>
<div id="main">
<div class="avatar">
</div>
<div class="account">
<h3>Account</h3>
<input type="text" placeholder="username" id="username-input" text=${client.me.username}>
<span id="username"></span>
<input type=password placeholder="new_password" id="new_password-input">
<span id="new_password"></span>
<input type=password placeholder="current_password" id="current_password-input">
<span id="current_password"></span>
<input type="button" value="Save Credentials" id="save-account-button"> return /* HTML */ `
<span id="save-account"></span> <div class='container col-sm-10 col-8 d-flex rounded border border-2 bg-light-subtle py-3'>
<input type="button" value="Delete Account" id="delete-account-button"> <div class='row col-4 bg-body-tertiary border rounded p-2 m-2 d-flex justify-content-center align-items-center'>
<span id="delete-account"></span> <h2 class='border-bottom'>Avatar</h2>
<img id='avatar' class='rounded p-0' src=${client.me.avatar_url} style='cursor: pointer;'>
<input id='avatarInput' class='d-none' type='file' accept='image/*'>
<div class='d-flex gap-2 mt-1 px-0'>
<span class='my-auto ms-1 me-auto' id='avatarDetail'></span>
<button class='btn btn-primary d-none' id='avatarUpload'>Save</button>
<button class='btn btn-danger${avatarUnchanged ? ' d-none' : ''}' id='avatarDelete'>Delete</button>
</div>
</div>
<div class='flex-grow-1'>
<h1 class='border-bottom ps-1 mb-3'>Account</h1>
<div>
<div class='input-group'>
<div class='form-floating'>
<input type='text' class='form-control' id='usernameInput' placeholder='username' value=${client.me.username}>
<label for='usernameInput'>Username</label>
</div>
<button class='input-group-text btn btn-success disabled' id='usernameSave'>Save</button>
</div>
<span class='form-text' id='usernameDetail'></span>
</div> </div>
<div class="profile">
<h3>Profile</h3>
<input type="file" id="avatar-input" accept="image/*">
<input type="button" value="Save profile" id="save-profile-button">
<span id="save-profile"></span>
</div> </div>
<a href="/logout" class="nav__link" data-link>Logout</a>
</div> </div>
`; `;
// <input class='form-control' type='text' placeholder='Username' id='username-input' value=${client.me.username}>
// <h1>Settings</h1>
// <input class='form-control d-inline-block' type='text' placeholder='Username' id='username-input' value=${client.me.username}>
// <span id='username'></span>
// <input type=password placeholder='New Password' id='new_password-input'>
// <span id='new_password'></span>
// <input type=password placeholder='Current Password' id='current_password-input'>
// <span id='current_password'></span>
// <input type='button' value='Save Credentials' id='save-account-button'>
// <span id='save-account'></span>
//
// <input type='button' value='Delete Account' id='delete-account-button'>
// <span id='delete-account'></span>
} }
} }

View File

@ -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 { clear, fill_errors } from "../../utils/formUtils.js";
import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js"; import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js";
@ -13,13 +13,11 @@ export default class extends AbstractAuthenticatedView
async create() async create()
{ {
let name = document.getElementById("name-input").value; let name = document.getElementById("name-input").value;
let nb_players = document.getElementById("nb_players-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_by_game = document.getElementById("nb-players-by-game-input").value;
let response_data = await client.tournaments.createTournament(nb_players, nb_players_by_game, name); let response = await client.tournaments.createTournament(nb_players, nb_players_by_game, name);
let response_data = await response.json();
if (response_data === null)
return;
let id = response_data.id; let id = response_data.id;
if (id !== undefined) if (id !== undefined)
@ -37,16 +35,33 @@ export default class extends AbstractAuthenticatedView
document.getElementById("create-button").onclick = this.create; document.getElementById("create-button").onclick = this.create;
} }
async getHtml() async getHtml() {
{
return ` return /* HTML */ `
<input type="text" id="name-input" placeholder="Tournament name"> <div class='container-fluid'>
<span id="name"></span> <div class='border border-2 rounded bg-light-subtle mx-auto p-2 col-md-7 col-lg-4'>
<input type="number" id="nb_players-input" placeholder="Number of players in tournament"> <h4 class='text-center fw-semibold mb-4' id="title">${lang.get("TournamentCreateTitle")}</h4>
<span id="nb_players"></span> <div class='form-floating mb-2'>
<input type="number" id="nb_players_by_game-input" placeholder="Number of players by game"> <input type='text' class='form-control' id='name-input' placeholder='${lang.get("TournamentCreateTournamentName")}'>
<span id="nb_players_by_game"></span> <label for='name-input' id='name-label'>${lang.get("TournamentCreateTournamentName")}</label>
<input type="button" id="create-button" value="Create tournament"> <span class='text-danger' id='name'></span>
</div>
<div class='form-floating mb-2'>
<input type='number' class='form-control' min='2' max='4' value='2' id='nb-players-by-game-input' placeholder='${lang.get("TournamentCreateNbPlayerByGame")}'>
<label for='nb-players-by-game-input' id='nb-players-by-game-label'>${lang.get("TournamentCreateNbPlayerByGame")}</label>
<span class='text-danger' id='nb_players_by_game'></span>
</div>
<div class='form-floating mb-2'>
<input type='number' class='form-control' min='2' value='4' id='nb-players-input' placeholder='${lang.get("TournamentCreateNbPlayer")}'>
<label for='nb-players-input' id='nb-players-label'>${lang.get("TournamentCreateNbPlayer")}</label>
<span class='text-danger' id='nb_players'></span>
</div>
<div class='d-flex'>
<button type='button' class='btn btn-primary mt-3 mb-2 mx-2' id='create-button'>${lang.get("TournamentCreateButton")}</button>
<span class='text-danger my-auto mx-2' id='detail'></span>
</div>
</div>
</div>
`; `;
} }
} }

View File

@ -55,7 +55,7 @@ export default class extends AbstractAuthenticatedView
document.getElementById("level").innerText = this.tournament.level; document.getElementById("level").innerText = this.tournament.level;
document.getElementById("state").innerText = this.tournament.state; document.getElementById("state").innerText = this.tournament.state;
if (this.tournament.state === "waiting") if (this.tournament.started === false)
button.disabled = false; button.disabled = false;
} }

View File

@ -3,13 +3,16 @@ from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models.signals import post_save, pre_delete from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.conf import settings
from django.db.models import IntegerField from django.db.models import IntegerField
from games.consumers import game_manager from games.consumers import game_manager
from os.path import splitext
def upload_to(instance, filename: str): 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. # Create your models here.
class ProfileModel(models.Model): class ProfileModel(models.Model):

View File

@ -1,6 +1,8 @@
from rest_framework import serializers from rest_framework import serializers
from .models import ProfileModel from .models import ProfileModel
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext as _
class ProfileSerializer(serializers.ModelSerializer): class ProfileSerializer(serializers.ModelSerializer):
@ -16,5 +18,5 @@ class ProfileSerializer(serializers.ModelSerializer):
Check that the image is not too large Check that the image is not too large
''' '''
if value.size > settings.PROFILE_PICTURE_MAX_SIZE: if value.size > settings.PROFILE_PICTURE_MAX_SIZE:
raise serializers.ValidationError('Image is too large.'); raise serializers.ValidationError(_('Image is too large.'))
return value; return value

View File

@ -4,7 +4,7 @@ from . import viewsets
from . import views from . import views
urlpatterns = [ 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("me", viewsets.MyProfileViewSet.as_view({'get': 'retrieve'}), name="my_profile_page"),
path("", viewsets.ProfileViewSet.as_view({'get': 'list'}), name="profiles_list"), path("", viewsets.ProfileViewSet.as_view({'get': 'list'}), name="profiles_list"),
path("block", views.BlocksView.as_view(), name="block_page"), path("block", views.BlocksView.as_view(), name="block_page"),
@ -12,5 +12,4 @@ urlpatterns = [
path("friend", views.FriendsView.as_view(), name="friend_page"), path("friend", views.FriendsView.as_view(), name="friend_page"),
path("user/<str:username>", viewsets.ProfileViewSet.as_view({'get': 'retrieve'}), name="profile_page"), path("user/<str:username>", viewsets.ProfileViewSet.as_view({'get': 'retrieve'}), name="profile_page"),
path("id/<int:pk>", viewsets.ProfileViewSet.as_view({'get': 'retrieve_id'}), name="profile_page"), path("id/<int:pk>", viewsets.ProfileViewSet.as_view({'get': 'retrieve_id'}), name="profile_page"),
] ]

View File

@ -1,4 +1,3 @@
from rest_framework import permissions
from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework import permissions, status from rest_framework import permissions, status
from rest_framework import viewsets from rest_framework import viewsets
@ -42,27 +41,34 @@ class ProfileViewSet(viewsets.ModelViewSet):
profile["avatar"] = profile["avatar"][profile["avatar"].find("static") - 1:] profile["avatar"] = profile["avatar"][profile["avatar"].find("static") - 1:]
return Response(serializer.data) return Response(serializer.data)
class MyProfileViewSet(viewsets.ModelViewSet):
class MyProfileViewSet(viewsets.ModelViewSet):
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (SessionAuthentication,) authentication_classes = (SessionAuthentication,)
serializer_class = ProfileSerializer serializer_class = ProfileSerializer
queryset = ProfileModel.objects.all queryset = ProfileModel.objects.all()
def get_object(self): def get_object(self):
obj = self.queryset().get(pk=self.request.user.pk) obj = self.queryset.get(pk=self.request.user.pk)
return obj return obj
def perform_update(self, serializer, pk=None): def perform_update(self, serializer: ProfileSerializer, pk=None):
serializer.is_valid(raise_exception=True); serializer.is_valid(raise_exception=True)
profile: ProfileModel = self.get_object(); avatar = serializer.validated_data.get('avatar')
avatar = serializer.validated_data.get('avatar'); profile: ProfileModel = self.get_object()
if (avatar is not None): if (avatar is not None):
if (profile.avatar.name != "./profiles/static/avatars/default.avif"): if (profile.avatar.name != "./profiles/static/avatars/default.avif"):
profile.avatar.storage.delete(profile.avatar.name) profile.avatar.storage.delete(profile.avatar.name)
profile.avatar = avatar 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() profile.save()
return Response(ProfileSerializer(profile).data)
def retrieve(self, request: HttpRequest, pk=None): def retrieve(self, request: HttpRequest, pk=None):
instance: ProfileModel = self.get_object() instance: ProfileModel = self.get_object()

View File

@ -5,6 +5,7 @@ from games.serializers import GameSerializer
class TournamentSerializer(serializers.ModelSerializer): class TournamentSerializer(serializers.ModelSerializer):
levels = serializers.SerializerMethodField(read_only=True, required=False) levels = serializers.SerializerMethodField(read_only=True, required=False)
state = serializers.SerializerMethodField(read_only=True, required=False)
level = serializers.ReadOnlyField() level = serializers.ReadOnlyField()
started = serializers.ReadOnlyField() started = serializers.ReadOnlyField()
finished = serializers.ReadOnlyField() finished = serializers.ReadOnlyField()
@ -12,7 +13,10 @@ class TournamentSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = TournamentModel 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): def get_levels(self, instance: TournamentModel):
levels: list[list[int]] = [] levels: list[list[int]] = []

View File

@ -43,7 +43,7 @@ class TournamentViewSet(viewsets.ModelViewSet):
query = TournamentModel.objects.filter(started=False, finished=False) query = TournamentModel.objects.filter(started=False, finished=False)
case _: case _:
query = TournamentModel.objects.all() query = TournamentModel.objects.all()
serializer = TournamentSerializer(query, many=True) serializer = self.serializer_class(query, many=True)
return Response(serializer.data) return Response(serializer.data)
def retrieve(self, request: HttpRequest, pk): def retrieve(self, request: HttpRequest, pk):