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 ""
"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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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."

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 .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")
]
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 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)
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,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)

View File

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

View File

@ -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)
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.contrib.auth import login
class RegisterView(APIView):
permission_classes = (permissions.AllowAny,)
def post(self, request: HttpRequest):
data = request.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 {
position: fixed;
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)
* @returns {?Promise<Object>}
*/
async get()
* @param {String} newUsername
* @returns {?Promise<Object>}
*/
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.';
}
}

View File

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

View File

@ -14,15 +14,35 @@ class MyProfile extends Profile
/**
*
* @param {*} form_data
* @returns {Promise<Object>}
* @param {File} selectedFile
* @returns {Promise<Response>}
*/
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;
}
}

View File

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

View File

@ -17,12 +17,12 @@ class Tourmanents
/**
*
* @param {Number} id
* @returns {?Promise<Tournament>}
* @returns {Promise<Tournament>}
*/
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;

View File

@ -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');
};

View File

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

View File

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

View File

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

View File

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

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.
</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>.
</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 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 `
<h1>Select mode</h1>
<input type="number" value="2" min="1" max="4" id="nb_players-input">
<input type="button" value="Find a game" id="button">
<input type="button" value="2D" id="game-mode">
<span id="detail"></span>
return /* HTML */ `
<div class='container-fluid'>
<div class='border border-2 rounded bg-light-subtle mx-auto p-2 col-md-7 col-lg-4'>
<h4 class='text-center fw-semibold mb-4' id="title">${lang.get("matchmakingTitle")}</h4>
<div class='form-floating mb-2'>
<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 { 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 */ `
<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>
async saveUsername()
{
const usernameInput = document.getElementById('usernameInput');
const username = usernameInput.value;
const usernameDetail = document.getElementById('usernameDetail');
<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>
</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>
<a href="/logout" class="nav__link" data-link>Logout</a>
</div>
`;
}
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 */ `
<div class='container col-sm-10 col-8 d-flex rounded border border-2 bg-light-subtle py-3'>
<div class='row col-4 bg-body-tertiary border rounded p-2 m-2 d-flex justify-content-center align-items-center'>
<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>
`;
// <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 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 `
<input type="text" id="name-input" placeholder="Tournament name">
<span id="name"></span>
<input type="number" id="nb_players-input" placeholder="Number of players in tournament">
<span id="nb_players"></span>
<input type="number" id="nb_players_by_game-input" placeholder="Number of players by game">
<span id="nb_players_by_game"></span>
<input type="button" id="create-button" value="Create tournament">
async getHtml() {
return /* HTML */ `
<div class='container-fluid'>
<div class='border border-2 rounded bg-light-subtle mx-auto p-2 col-md-7 col-lg-4'>
<h4 class='text-center fw-semibold mb-4' id="title">${lang.get("TournamentCreateTitle")}</h4>
<div class='form-floating mb-2'>
<input type='text' class='form-control' id='name-input' placeholder='${lang.get("TournamentCreateTournamentName")}'>
<label for='name-input' id='name-label'>${lang.get("TournamentCreateTournamentName")}</label>
<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("state").innerText = this.tournament.state;
if (this.tournament.state === "waiting")
if (this.tournament.started === false)
button.disabled = false;
}

View File

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

View File

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

View File

@ -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/<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"),
]

View File

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

View File

@ -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]] = []

View File

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