clean: rm online tournament
This commit is contained in:
parent
ae76b82169
commit
bb6353f578
@ -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 { Tourmanents } from "./tournament/Tournaments.js";
|
||||
import { Channel } from "./chat/Channel.js";
|
||||
import Notice from "./Notice.js";
|
||||
import LanguageManager from './LanguageManager.js';
|
||||
@ -46,11 +45,6 @@ class Client
|
||||
*/
|
||||
this.matchmaking = new MatchMaking(this);
|
||||
|
||||
/**
|
||||
* @type {Tourmanents}
|
||||
*/
|
||||
this.tournaments = new Tourmanents(this);
|
||||
|
||||
/**
|
||||
* @type {Boolean} A private var represent if the is is log NEVER USE IT use await isAuthenticated()
|
||||
*/
|
||||
|
@ -1,201 +0,0 @@
|
||||
import { AExchangeable } from "../AExchangable.js";
|
||||
import { Client } from "../Client.js";
|
||||
import { Profile } from "../Profile.js";
|
||||
|
||||
class Tourmanent extends AExchangeable
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {Client} client
|
||||
* @param {Number} id the id of the tournament
|
||||
*/
|
||||
constructor(client, id)
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.nb_participants;
|
||||
|
||||
/**
|
||||
* @type {[Profile]} proutman à encore frappé
|
||||
*/
|
||||
this.participantList = []
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.started;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.finished;
|
||||
|
||||
/**
|
||||
* @type {"finished" | "started" | "waiting"} must be "finished", or "started", or "waiting". Any other return all elements
|
||||
*/
|
||||
this.state;
|
||||
|
||||
/**
|
||||
* @type {Boolean} the client is a participant of the tournament
|
||||
*/
|
||||
this.is_participating;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Boolean} newParticipation
|
||||
*/
|
||||
async setParticipation(newParticipation)
|
||||
{
|
||||
if (this.isParticipating == newParticipation)
|
||||
return;
|
||||
|
||||
this.isParticipating = newParticipation;
|
||||
|
||||
this._socket.send(JSON.stringify({"detail": "update_participating",
|
||||
"is_participating": newParticipation})
|
||||
);
|
||||
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<?>}
|
||||
*/
|
||||
async init()
|
||||
{
|
||||
let response = await this.client._get(`/api/tournaments/${this.id}`);
|
||||
|
||||
if (response.status !== 200)
|
||||
return response.status;
|
||||
|
||||
let response_data = await response.json();
|
||||
|
||||
this.import(response_data);
|
||||
}
|
||||
|
||||
leave(event)
|
||||
{
|
||||
if (this.connected == false)
|
||||
return;
|
||||
this.connected = false;
|
||||
this._socket.close();
|
||||
if (this.disconnectHandler != null)
|
||||
this.disconnectHandler(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} data
|
||||
*/
|
||||
async _receiveAddParticipant(data)
|
||||
{
|
||||
const participant = new Profile(this.client, undefined, data.participant.user_id);
|
||||
participant.import(data.participant)
|
||||
|
||||
this.participantList.push(participant);
|
||||
|
||||
await this._addParticipantHandler(this.participantList.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} data
|
||||
*/
|
||||
async _receiveDelParticipant(data)
|
||||
{
|
||||
const index = this.participantList.indexOf((profile) => profile.id === data.profile.user_id)
|
||||
|
||||
this.participantList.splice(index, 1);
|
||||
|
||||
await this._delParticipantHandler(this.participantList.length);
|
||||
}
|
||||
|
||||
async _receiveError(data)
|
||||
{
|
||||
await this.errorHandler(data);
|
||||
}
|
||||
|
||||
async _receiveGoTo(data)
|
||||
{
|
||||
await this._goToHandler(data)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {MessageEvent} event
|
||||
*/
|
||||
async onReceive(event)
|
||||
{
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.detail) {
|
||||
case "error":
|
||||
await this._receiveError(data)
|
||||
break;
|
||||
|
||||
case "add_participant":
|
||||
await this._receiveAddParticipant(data);
|
||||
break;
|
||||
|
||||
case "del_participant":
|
||||
await this._receiveDelParticipant(data);
|
||||
break;
|
||||
|
||||
case "go_to":
|
||||
await this._receiveGoTo(data);
|
||||
break
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Join the tournament Websocket
|
||||
* @param {CallableFunction} errorHandler
|
||||
* @param {CallableFunction} addParticipantHandler called when a participants join the tournament
|
||||
* @param {CallableFunction} delParticipantHandler called when a participants leave the tournament
|
||||
* @param {CallableFunction} disconnectHandler
|
||||
* @param {CallableFunction} goToHandler called when the next game will start
|
||||
* @param {CallableFunction} startHandler called when tournament start
|
||||
* @param {CallableFunction} finishHandler called when tournament finish
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async join(addParticipantHandler, delParticipantHandler, startHandler, finishHandler, errorHandler, goToHandler, disconnectHandler)
|
||||
{
|
||||
if (!await this.client.isAuthenticated())
|
||||
return null;
|
||||
|
||||
let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/tournaments/${this.id}`;
|
||||
|
||||
this._socket = new WebSocket(url);
|
||||
|
||||
this.connected = true;
|
||||
this.isParticipating = false;
|
||||
|
||||
this._startHandler = startHandler;
|
||||
this._finishHandler = finishHandler;
|
||||
this._addParticipantHandler = addParticipantHandler;
|
||||
this._delParticipantHandler = delParticipantHandler;
|
||||
this._errorHandler = errorHandler;
|
||||
this._disconnectHandler = disconnectHandler;
|
||||
this._goToHandler = goToHandler;
|
||||
|
||||
this._socket.onmessage = this.onReceive.bind(this);
|
||||
|
||||
this._socket.onclose = this.leave.bind(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Tourmanent };
|
@ -1,80 +0,0 @@
|
||||
import { Client } from "../Client.js";
|
||||
import { Tourmanent } from "./Tournament.js";
|
||||
|
||||
class Tourmanents
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor(client)
|
||||
{
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} id
|
||||
* @returns {Promise<Tournament>}
|
||||
*/
|
||||
async getTournament(id)
|
||||
{
|
||||
let tournament = new Tourmanent(this.client, id);
|
||||
if (await tournament.init())
|
||||
return null;
|
||||
return tournament;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} nb_participants
|
||||
* @param {String} name
|
||||
* @returns {Response}
|
||||
*/
|
||||
async createTournament(nb_participants, name = "")
|
||||
{
|
||||
let response = await this.client._post("/api/tournaments/", {nb_participants: nb_participants, name: name});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} state must be "finished", or "started", or "waiting". Any other return all elements
|
||||
* @returns {?Promise<[Tourmanent]>}
|
||||
*/
|
||||
async search(state)
|
||||
{
|
||||
let response = await this.client._get(`/api/tournaments/search/${state}`);
|
||||
let response_data = await response.json();
|
||||
|
||||
if (response.status === 403)
|
||||
{
|
||||
this.client._update_logged(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
let tournaments = [];``
|
||||
|
||||
response_data.forEach(tournament_data => {
|
||||
let tournament = new Tourmanent(this.client, tournament_data.id);
|
||||
tournament.import(tournament_data);
|
||||
tournaments.push(tournament);
|
||||
});
|
||||
|
||||
return tournaments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tournaments
|
||||
* @returns {?Promise<[Tourmanent]>}
|
||||
*/
|
||||
async all()
|
||||
{
|
||||
return await this.search("");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Tourmanents };
|
@ -16,9 +16,6 @@ import AbstractRedirectView from "./views/abstracts/AbstractRedirectView.js";
|
||||
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 TournamentsListView from "./views/tournament/TournamentsListView.js";
|
||||
import TournamentCreateView from "./views/tournament/TournamentCreateView.js";
|
||||
import AuthenticationView from "./views/accounts/AuthenticationView.js";
|
||||
|
||||
let client = new Client(location.origin);
|
||||
@ -30,9 +27,9 @@ let lastPageUrlBeforeLogin;
|
||||
const pathToRegex = path => new RegExp("^" + path.replace(/\//g, "\\/").replace(/:\w+/g, "(.+)") + "$");
|
||||
|
||||
const getParams = match => {
|
||||
|
||||
const values = match.result.slice(1);
|
||||
const keys = Array.from(match.route.path.matchAll(/:(\w+)/g)).map(result => result[1]);
|
||||
|
||||
return Object.fromEntries(keys.map((key, i) => {
|
||||
return [key, values[i]];
|
||||
}));
|
||||
@ -79,9 +76,6 @@ const router = async(uri) => {
|
||||
const routes = [
|
||||
{ path: "/", view: HomeView},
|
||||
{ path: "/profiles/:username", view: ProfilePageView },
|
||||
{ path: "/tournaments/create", view: TournamentCreateView },
|
||||
{ path: "/tournaments/:id", view: TournamentPageView },
|
||||
{ path: "/tournaments/", view: TournamentsListView },
|
||||
{ path: "/login", view: AuthenticationView },
|
||||
{ path: "/register", view: AuthenticationView },
|
||||
{ path: "/logout", view: LogoutView },
|
||||
|
@ -1,71 +0,0 @@
|
||||
import {client, lang, navigateTo} from "../../index.js";
|
||||
import { clearIds, fill_errors } from "../../utils/formUtils.js";
|
||||
import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js";
|
||||
|
||||
export default class extends AbstractAuthenticatedView
|
||||
{
|
||||
constructor(params)
|
||||
{
|
||||
super(params, "Create tournament");
|
||||
this.id = params.id;
|
||||
}
|
||||
|
||||
async create()
|
||||
{
|
||||
let name = document.getElementById("name-input").value;
|
||||
if (name.length == 0)
|
||||
name = lang.get("TournamentCreateTournamentName");
|
||||
|
||||
let nb_participant = document.getElementById("nb-participant-input").value;
|
||||
|
||||
let response = await client.tournaments.createTournament(nb_participant, name);
|
||||
let response_data = await response.json();
|
||||
|
||||
let id = response_data.id;
|
||||
if (id !== undefined)
|
||||
{
|
||||
navigateTo(`/tournaments/${id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
clearIds("innerHTML", ["name", "nb_participants"]);
|
||||
fill_errors(response_data, "innerHTML");
|
||||
}
|
||||
|
||||
async postInit()
|
||||
{
|
||||
document.getElementById("create-button").onclick = this.create;
|
||||
}
|
||||
|
||||
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'>
|
||||
<select class='form-control' id="nb-participant-input">
|
||||
<option value="2">2</option>
|
||||
<option value="4">4</option>
|
||||
<option value="8">8</option>
|
||||
<option value="16">16</option>
|
||||
<option value="32">32</option>
|
||||
<option value="64">64</option>
|
||||
</select>
|
||||
<label for='nb-participant-input' id='nb-participant-label'>${lang.get("TournamentCreateNbPlayer")}</label>
|
||||
<span class='text-danger' id='nb_participants'></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>
|
||||
`;
|
||||
}
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
import { Profile } from "../../api/Profile.js";
|
||||
import { Tourmanent } from "../../api/tournament/Tournament.js";
|
||||
import {client, navigateTo} from "../../index.js";
|
||||
import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js";
|
||||
|
||||
const TEXT_CONVENTION = {
|
||||
"error": "[ERROR]"
|
||||
}
|
||||
|
||||
export default class extends AbstractAuthenticatedView
|
||||
{
|
||||
constructor(params)
|
||||
{
|
||||
super(params, "Tournament");
|
||||
this.id = params.id;
|
||||
}
|
||||
|
||||
pressButton()
|
||||
{
|
||||
this.tournament.setParticipation(!this.tournament.isParticipating);
|
||||
this.updateParticipating()
|
||||
}
|
||||
|
||||
updateParticipating()
|
||||
{
|
||||
document.getElementById("button").value = this.tournament.isParticipating ? `Leave ${this.tournament.name}` : `Join ${this.tournament.name}`;
|
||||
document.getElementById("display").innerText = this.tournament.isParticipating ? "You are a particpant" : "You are not a participant";
|
||||
}
|
||||
|
||||
async onDisconnect(event)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
async onError(data)
|
||||
{
|
||||
this.addChatMessage(`${TEXT_CONVENTION} data.error_message`);
|
||||
}
|
||||
|
||||
async onFinish()
|
||||
{
|
||||
document.getElementById("state").innerText = "finished"
|
||||
}
|
||||
|
||||
async onStart()
|
||||
{
|
||||
document.getElementById("state").innerText = "started"
|
||||
}
|
||||
|
||||
async onGoTo(data)
|
||||
{
|
||||
await navigateTo(`/games/pong/${data.game_id}`)
|
||||
}
|
||||
|
||||
async onAddParticipant(nb_participants)
|
||||
{
|
||||
document.getElementById("nb_participants").innerText = nb_participants;
|
||||
}
|
||||
|
||||
async onDelParticipant(nb_participants)
|
||||
{
|
||||
document.getElementById("nb_participants").innerText = nb_participants;
|
||||
}
|
||||
|
||||
async postInit()
|
||||
{
|
||||
/**
|
||||
* @type {Tourmanent}
|
||||
*/
|
||||
this.tournament = await client.tournaments.getTournament(this.id);
|
||||
|
||||
if (this.tournament === null)
|
||||
return 404;
|
||||
|
||||
this.tournament.join(this.onAddParticipant, this.onDelParticipant, this.onStart, this.onFinish, this.onError, this.onGoTo, this.onDisconnect);
|
||||
|
||||
let button = document.getElementById("button");
|
||||
|
||||
button.onclick = this.pressButton.bind(this);
|
||||
|
||||
document.getElementById("name").innerText = this.tournament.name;
|
||||
document.getElementById("nb_participants").innerText = this.tournament.participantList.length;
|
||||
document.getElementById("expected_nb_participants").innerText = this.tournament.nb_participants;
|
||||
document.getElementById("round").innerText = this.tournament.round;
|
||||
document.getElementById("state").innerText = this.tournament.state;
|
||||
|
||||
if (this.tournament.started === false)
|
||||
button.disabled = false;
|
||||
|
||||
this.chat = document.getElementById("chat");
|
||||
|
||||
console.log(this.tournament);
|
||||
this.display_tree_tournament(this.tournament.nb_participants, this.tournament.participantList);
|
||||
}
|
||||
|
||||
addChatMessage(message)
|
||||
{
|
||||
this.chat.innerText += message;
|
||||
}
|
||||
|
||||
async display_tree_tournament(nb_participants, participants) {
|
||||
const svg = document.getElementById('tree');
|
||||
|
||||
svg.innerHTML = '';
|
||||
|
||||
let width = 100;
|
||||
let height = 25;
|
||||
let gap_y = height + 25;
|
||||
let gap_x = width + 25;
|
||||
let start_y = height / 2;
|
||||
|
||||
let rounds = Math.log2(nb_participants) + 1;
|
||||
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
let number_square = nb_participants / Math.pow(2, i)
|
||||
for(let j = 0; j < number_square; j++) {
|
||||
const y = start_y + gap_y * j * Math.pow(2, i) + (gap_y / 2 * Math.pow(2, i));
|
||||
svg.appendChild(await this.create_square(gap_x * i, y, width, height, "white", "black"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async create_square(x, y, width, height, fill, stroke) {
|
||||
const square = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
square.setAttribute("id", square)
|
||||
square.setAttribute("x", x);
|
||||
square.setAttribute("y", y);
|
||||
square.setAttribute("width", width);
|
||||
square.setAttribute("height", height);
|
||||
square.setAttribute("fill", fill);
|
||||
square.setAttribute("stroke", stroke);
|
||||
return square
|
||||
}
|
||||
|
||||
async getHtml()
|
||||
{
|
||||
return `
|
||||
<link rel="stylesheet" href="/static/css/TournamentPage.css">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="name">Loading...</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Number of round</td>
|
||||
<td id="round">Loading...</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Number of participants</td>
|
||||
<td id="nb_participants">Loading...</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Expected number of participants</td>
|
||||
<td id="expected_nb_participants">Loading...</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>state</td>
|
||||
<td id="state">Loading...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<input type="button" id="button" value="Join tournament" disabled>
|
||||
<textarea id="chat" rows="4" cols="50" readonly>
|
||||
</textarea>
|
||||
<br>
|
||||
<svg id="tree" height="3000" width="3000">
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
import {client, navigateTo} from "../../index.js";
|
||||
import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js";
|
||||
|
||||
export default class extends AbstractAuthenticatedView
|
||||
{
|
||||
constructor(params)
|
||||
{
|
||||
super(params, "Tournament");
|
||||
this.id = params.id;
|
||||
}
|
||||
|
||||
async external_search()
|
||||
{
|
||||
let state = document.getElementById("state-select").value;
|
||||
this.tournaments = await client.tournaments.search(state);
|
||||
//console.log(this.tournaments);
|
||||
}
|
||||
|
||||
internal_search()
|
||||
{
|
||||
|
||||
this.display_tournaments = [];
|
||||
this.tournaments.forEach(tournament => {
|
||||
this.display_tournaments.push(tournament);
|
||||
});
|
||||
}
|
||||
|
||||
display_result()
|
||||
{
|
||||
const tournaments_list = document.getElementById("tournaments-list");
|
||||
|
||||
const new_children = [];
|
||||
|
||||
console.log(this.display_tournaments);
|
||||
this.display_tournaments.forEach(tournament => {
|
||||
|
||||
let tr = document.createElement("tr");
|
||||
|
||||
// name
|
||||
let td = document.createElement("td");
|
||||
let button_tournament = document.createElement("a");
|
||||
button_tournament.innerText = tournament.name;
|
||||
button_tournament.onclick = async () => {
|
||||
await navigateTo(String(tournament.id));
|
||||
}
|
||||
button_tournament.href = "";
|
||||
td.appendChild(button_tournament);
|
||||
tr.appendChild(td);
|
||||
|
||||
// state
|
||||
td = document.createElement("td");
|
||||
td.innerText = tournament.state;
|
||||
tr.appendChild(td);
|
||||
|
||||
// nb_participants
|
||||
td = document.createElement("td");
|
||||
td.innerText = tournament.nb_participants;
|
||||
tr.appendChild(td);
|
||||
|
||||
new_children.push(tr);
|
||||
});
|
||||
tournaments_list.replaceChildren(...new_children);
|
||||
}
|
||||
|
||||
async update_query()
|
||||
{
|
||||
this.internal_search();
|
||||
this.display_result();
|
||||
}
|
||||
|
||||
async update_search()
|
||||
{
|
||||
await this.external_search();
|
||||
this.update_query();
|
||||
}
|
||||
|
||||
async postInit()
|
||||
{
|
||||
await this.update_search();
|
||||
document.getElementById("state-select").onchange = this.update_search.bind(this);
|
||||
}
|
||||
|
||||
async getHtml()
|
||||
{
|
||||
return `
|
||||
<select id="state-select">
|
||||
<option value="waiting">Waiting</option>
|
||||
<option value="started">Started</option>
|
||||
<option value="finished">Finished</option>
|
||||
<option value="all">All</option>
|
||||
</select>
|
||||
<table>
|
||||
<thead>
|
||||
<td>Name</td>
|
||||
<td>Status</td>
|
||||
<td>Max numbers of participants</td>
|
||||
</thead>
|
||||
<tbody id="tournaments-list">
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
}
|
@ -7,11 +7,6 @@ from django.contrib.auth.models import User
|
||||
|
||||
import time
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tournament.models import TournamentGameModel
|
||||
|
||||
class GameModel(models.Model):
|
||||
|
||||
finished = models.BooleanField(default = False)
|
||||
|
@ -6,8 +6,6 @@ from .ASpectator import ASpectator
|
||||
|
||||
from ..models import GameModel
|
||||
|
||||
from tournament.models import TournamentGameModel
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
class AGame(AbstractRoom):
|
||||
@ -18,11 +16,7 @@ class AGame(AbstractRoom):
|
||||
|
||||
self.game_manager = game_manager
|
||||
|
||||
query = TournamentGameModel.objects.filter(pk = game_id, game_type = game_type)
|
||||
if (query.exists()):
|
||||
self.model: TournamentGameModel | GameModel = query[0]
|
||||
else:
|
||||
self.model: TournamentGameModel | GameModel = GameModel.objects.get(pk = game_id, game_type = game_type)
|
||||
self.model: GameModel = GameModel.objects.get(pk = game_id, game_type = game_type)
|
||||
|
||||
players: list[User] = self.model.get_players()
|
||||
|
||||
|
@ -3,8 +3,6 @@ from ..models import GameModel
|
||||
from .pong.PongGame import PongGame
|
||||
from .tictactoe.TicTacToeGame import TicTacToeGame
|
||||
|
||||
from tournament.models import TournamentGameModel
|
||||
|
||||
class GameManager():
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
1
run.sh
1
run.sh
@ -6,7 +6,6 @@ pip install -r requirements.txt
|
||||
python manage.py makemigrations games
|
||||
python manage.py makemigrations profiles
|
||||
python manage.py makemigrations chat
|
||||
python manage.py makemigrations tournament
|
||||
python manage.py makemigrations notice
|
||||
python manage.py migrate
|
||||
python manage.py compilemessages
|
||||
|
@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TournamentConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'tournament'
|
@ -1,235 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from channels.generic.websocket import WebsocketConsumer
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from profiles.models import ProfileModel
|
||||
from profiles.serializers import ProfileSerializer
|
||||
from .models import TournamentModel
|
||||
|
||||
from .models import TournamentGameModel
|
||||
from .serializers import TournamentGameSerializer
|
||||
|
||||
import json
|
||||
|
||||
class TournamentMember:
|
||||
|
||||
def __init__(self, socket: TournamentWebConsumer, room: TournamentRoom, is_participating: bool = False) -> None:
|
||||
self._socket: TournamentWebConsumer = socket
|
||||
self._room: TournamentRoom = room
|
||||
self.is_participating: bool = is_participating
|
||||
|
||||
def send(self, detail: str, data: dict = {}):
|
||||
data_to_send: dict = {"detail": detail}
|
||||
data_to_send.update(data)
|
||||
|
||||
self._socket.send(json.dumps(data_to_send))
|
||||
|
||||
def send_error(self, error_message: str, data: dict = {}):
|
||||
data_to_send: dict = {"error_message": error_message}
|
||||
data_to_send.update(data)
|
||||
|
||||
self.send("error", data_to_send)
|
||||
|
||||
def send_goto(self, game: TournamentGameModel):
|
||||
self.send("go_to", {"game_id": game.pk})
|
||||
|
||||
def _receive_participating(self, data: dict) -> None:
|
||||
|
||||
is_participating: bool | None = data.get("is_participating")
|
||||
if (is_participating is None):
|
||||
self.send_error(_("Missing is_participating statement."))
|
||||
return
|
||||
|
||||
self._room.set_participation(self, is_participating)
|
||||
|
||||
def receive(self, data: dict):
|
||||
|
||||
detail: str | None = data.get("detail")
|
||||
if (detail is None):
|
||||
return
|
||||
|
||||
match(detail):
|
||||
case "update_participating":
|
||||
self._receive_participating(data)
|
||||
case _:
|
||||
print("bozo_send")
|
||||
|
||||
|
||||
class TournamentRoomManager:
|
||||
|
||||
def __init__(self):
|
||||
self._room_list: list[TournamentRoom] = []
|
||||
|
||||
def get(self, tournament: TournamentModel) -> TournamentRoom:
|
||||
|
||||
for room in self._room_list:
|
||||
if room._model is tournament:
|
||||
return room
|
||||
|
||||
room: TournamentRoom = TournamentRoom(self, tournament)
|
||||
|
||||
self._room_list.append(room)
|
||||
|
||||
return room
|
||||
|
||||
def remove(self, room: TournamentRoom) -> None:
|
||||
self._room_list.remove(room)
|
||||
|
||||
class TournamentRoom:
|
||||
|
||||
def __init__(self, room_manager: TournamentRoomManager, tournament: TournamentModel):
|
||||
self._room_manager: TournamentRoomManager = room_manager
|
||||
self._member_list: set[TournamentMember] = set()
|
||||
self._model: TournamentModel = tournament
|
||||
self._game_in_progress_list: set[TournamentGameModel] = set()
|
||||
self._current_round = 0
|
||||
|
||||
def join(self, socket: TournamentWebConsumer) -> TournamentMember:
|
||||
|
||||
member: TournamentMember = TournamentMember(socket, self)
|
||||
self._member_list.add(member)
|
||||
|
||||
return member
|
||||
|
||||
def set_game_as_finished(self, game: TournamentGameModel):
|
||||
|
||||
self._game_in_progress_list.remove(game)
|
||||
|
||||
self.broadcast("game_update", TournamentGameSerializer(game).data)
|
||||
|
||||
if len(self._game_in_progress_list) == 0:
|
||||
self._round_finished()
|
||||
|
||||
def set_game_as_started(self, game: TournamentGameModel):
|
||||
|
||||
self._game_in_progress_list.add(game)
|
||||
|
||||
self.broadcast("game_update", TournamentGameSerializer(game).data)
|
||||
|
||||
def _finish(self, winner: User):
|
||||
self._model.finish(winner)
|
||||
|
||||
def _round_finished(self):
|
||||
|
||||
if self._current_round == self._model.round:
|
||||
last_game: TournamentGameSerializer = self._model.get_games_by_round(self._current_round)[0]
|
||||
self._finish(last_game.winner)
|
||||
return
|
||||
|
||||
self._start_round()
|
||||
|
||||
def _start_round(self):
|
||||
|
||||
self._current_round += 1
|
||||
|
||||
participant_list: set[User] = self._model.get_participants_by_round(self._current_round)
|
||||
|
||||
game_list = self._model.create_round(participant_list, self._current_round)
|
||||
|
||||
for game in game_list:
|
||||
for player in game.get_players():
|
||||
participant: TournamentMember = self.get_participant_by_profile(player)
|
||||
participant.send_goto(game)
|
||||
|
||||
def get_participants_profiles(self) -> list[ProfileModel]:
|
||||
return [participant._socket.user.profilemodel for participant in self.get_participants()]
|
||||
|
||||
def start(self) -> None:
|
||||
|
||||
self._model.start()
|
||||
|
||||
self.broadcast("start")
|
||||
|
||||
self._start_round()
|
||||
|
||||
def get_participant_by_profile(self, profile: ProfileModel):
|
||||
for participant in self.get_participants():
|
||||
if (participant._socket.user.profilemodel == profile):
|
||||
return participant
|
||||
|
||||
def leave(self, member: TournamentMember) -> None:
|
||||
|
||||
# Delete room if nobody connected, no cringe memory leak
|
||||
if (len(self._member_list) == 1):
|
||||
self._room_manager.remove(self)
|
||||
return
|
||||
|
||||
self._member_list.remove(member)
|
||||
|
||||
self.set_participation(member, False)
|
||||
|
||||
def everybody_is_here(self):
|
||||
return len(self.get_participants()) == self._model.nb_participants
|
||||
|
||||
def broadcast(self, detail: str, data: dict, excludes: set[TournamentMember] = set()) -> None:
|
||||
|
||||
member_list: list[TournamentMember] = self._member_list - excludes
|
||||
|
||||
for member in member_list:
|
||||
member.send(detail, data)
|
||||
|
||||
def get_participants(self) -> list[TournamentMember]:
|
||||
return [member for member in self._member_list if member.is_participating]
|
||||
|
||||
def set_participation(self, member: TournamentMember, is_participating: bool) -> None:
|
||||
|
||||
if (self._model.started):
|
||||
return
|
||||
|
||||
if (member.is_participating == is_participating):
|
||||
return
|
||||
|
||||
if (is_participating == True):
|
||||
self.broadcast("add_participant", {"participant": ProfileSerializer(member._socket.user.profilemodel).data})
|
||||
else:
|
||||
self.broadcast("del_participant", {"participant": ProfileSerializer(member._socket.user.profilemodel).data})
|
||||
|
||||
member.is_participating = is_participating
|
||||
|
||||
if self.everybody_is_here():
|
||||
self.start()
|
||||
|
||||
tournament_manager: TournamentRoomManager = TournamentRoomManager()
|
||||
|
||||
class TournamentWebConsumer(WebsocketConsumer):
|
||||
|
||||
def connect(self):
|
||||
|
||||
self.user: User = self.scope["user"]
|
||||
if (not self.user.is_authenticated):
|
||||
return
|
||||
|
||||
tournament_id: int = int(self.scope['url_route']['kwargs']['tournament_id'])
|
||||
|
||||
query: QuerySet = TournamentModel.objects.filter(pk=tournament_id)
|
||||
|
||||
if (not query.exists()):
|
||||
return
|
||||
|
||||
tournament: TournamentModel = query[0]
|
||||
|
||||
self.room = tournament_manager.get(tournament)
|
||||
|
||||
self.member: TournamentMember = self.room.join(self)
|
||||
|
||||
self.accept()
|
||||
|
||||
def receive(self, text_data: str = None, bytes_data: bytes = None):
|
||||
|
||||
user: User = self.scope["user"]
|
||||
if (user != self.user):
|
||||
return
|
||||
|
||||
data: dict = json.loads(text_data)
|
||||
|
||||
self.member.receive(data)
|
||||
|
||||
def disconnect(self, close_code):
|
||||
|
||||
self.room.leave(self.member)
|
||||
|
||||
super().disconnect(close_code) # proutman à encore frappé
|
@ -1,116 +0,0 @@
|
||||
from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from games.models import GameModel
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import CASCADE
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TournamentModel(models.Model):
|
||||
|
||||
name = models.CharField(max_length = 100)
|
||||
nb_participants = models.IntegerField()
|
||||
round = models.IntegerField()
|
||||
started = models.BooleanField(default = False)
|
||||
finished = models.BooleanField(default = False)
|
||||
winner = models.ForeignKey(User, on_delete=CASCADE, blank=True, null=True)
|
||||
|
||||
def _register_participant(self, participant: User) -> None:
|
||||
TournamentParticipantModel(participant=participant, tournament=self).save()
|
||||
|
||||
def create_round(self, participants: set[User], round: int) -> set[GameModel]:
|
||||
|
||||
game_list: set[GameModel] = set()
|
||||
|
||||
for i, (participant1, participant2) in enumerate(zip(participants[0::2], participants[1::2])):
|
||||
game: GameModel = self.create_game([participant1, participant2], round, i)
|
||||
game_list.add(game)
|
||||
|
||||
return game_list
|
||||
|
||||
def start(self, participant_list: set[User]) -> None:
|
||||
|
||||
self.started = True
|
||||
self.round = 1
|
||||
|
||||
for participant in participant_list:
|
||||
self._register_participant(participant)
|
||||
|
||||
self.save()
|
||||
|
||||
def finish(self, winner: User):
|
||||
|
||||
self.finished = True
|
||||
|
||||
self.winner = winner
|
||||
|
||||
self.save()
|
||||
|
||||
def create_game(self, participants: set[User], round: int, pos: int) -> GameModel:
|
||||
|
||||
if (self.started == False):
|
||||
return None
|
||||
|
||||
if (len(participants) != 2):
|
||||
return None
|
||||
|
||||
from games.models import GameModel
|
||||
|
||||
game: GameModel = GameModel().create(participants)
|
||||
|
||||
TournamentGameModel(tournament=self, game=game, round=round, pos=pos).save()
|
||||
|
||||
return game
|
||||
|
||||
def get_games(self) -> set[GameModel]:
|
||||
return [{games for games in self.get_games_by_round(i)} for i in range(1, self.round)]
|
||||
|
||||
def get_games_by_round(self, round: int) -> set[GameModel]:
|
||||
return {tournament_game for tournament_game in TournamentGameModel.objects.filter(tournament=self, round=round)}
|
||||
|
||||
def get_participants_by_round(self, round: int) -> set[User]:
|
||||
if round == 1:
|
||||
return self.get_participants()
|
||||
return {game.winner for game in self.get_games_by_round(round - 1)}
|
||||
|
||||
def get_winners_by_round(self, round: int) -> set[User]:
|
||||
return {game.winner for game in self.get_games_by_round(round)}
|
||||
|
||||
def get_participants(self) -> set[User]:
|
||||
return {tournament_participant.participant for tournament_participant in TournamentParticipantModel.objects.filter(tournament=self.pk)}
|
||||
|
||||
def get_state(self) -> str:
|
||||
return ("waiting to start", "in progress", "finish")[self.started + self.finished]
|
||||
|
||||
def is_participating(self, profile: User) -> bool:
|
||||
return TournamentParticipantModel.objects.filter(participant=profile, tournament=self).exists()
|
||||
|
||||
class TournamentParticipantModel(models.Model):
|
||||
participant = models.ForeignKey(User, on_delete=CASCADE)
|
||||
tournament = models.ForeignKey(TournamentModel, on_delete=CASCADE)
|
||||
|
||||
class TournamentGameModel(GameModel):
|
||||
|
||||
tournament = models.ForeignKey(TournamentModel, on_delete=CASCADE, null=True, blank=True)
|
||||
round = models.IntegerField()
|
||||
pos = models.IntegerField()
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
from .consumers import tournament_manager
|
||||
self.room = tournament_manager.get(self.tournament)
|
||||
|
||||
|
||||
def start(self):
|
||||
super().start()
|
||||
|
||||
self.room.set_game_as_started(self)
|
||||
|
||||
|
||||
def finish(self, winner_id):
|
||||
super().finish(winner_id)
|
||||
|
||||
self.room.set_game_as_finished(self)
|
@ -1,6 +0,0 @@
|
||||
from django.urls import re_path
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/tournaments/(?P<tournament_id>\d+)$', consumers.TournamentWebConsumer.as_asgi())
|
||||
]
|
@ -1,63 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import TournamentModel, TournamentGameModel
|
||||
|
||||
from profiles.serializers import ProfileSerializer
|
||||
from games.serializers import GameSerializer
|
||||
|
||||
nb_participants = [2 ** i for i in range(1, 6)]
|
||||
|
||||
class TournamentSerializer(serializers.ModelSerializer):
|
||||
|
||||
state = serializers.SerializerMethodField(read_only=True, required=False)
|
||||
participants = serializers.SerializerMethodField(read_only=True, required=False)
|
||||
round = serializers.ReadOnlyField()
|
||||
games = serializers.SerializerMethodField(read_only=True, required=False)
|
||||
started = serializers.ReadOnlyField()
|
||||
finished = serializers.ReadOnlyField()
|
||||
name = serializers.CharField(default="")
|
||||
|
||||
class Meta:
|
||||
model = TournamentModel
|
||||
fields = ["name", "nb_participants", "round", "started", "finished", "id", "state", "participants", "games"]
|
||||
|
||||
def get_participants(self, instance: TournamentModel):
|
||||
return ProfileSerializer(instance.get_participants(), many=True).data
|
||||
|
||||
def get_games(self, instance: TournamentModel):
|
||||
return [GameSerializer(games, many=True).data for games in instance.get_games()]
|
||||
|
||||
def get_state(self, instance: TournamentModel):
|
||||
return ["waiting", "started", "finished"][instance.started + instance.finished]
|
||||
|
||||
def validate_nb_participants(self, value: int):
|
||||
if (value not in nb_participants):
|
||||
raise serializers.ValidationError(f"The numbers of participants must be {str(nb_participants)}.")
|
||||
return value
|
||||
|
||||
class TournamentGameSerializer(serializers.ModelSerializer):
|
||||
|
||||
players = serializers.SerializerMethodField()
|
||||
winner_id = serializers.ReadOnlyField()
|
||||
state = serializers.SerializerMethodField()
|
||||
started = serializers.ReadOnlyField()
|
||||
finished = serializers.ReadOnlyField()
|
||||
start_timestamp = serializers.ReadOnlyField()
|
||||
stop_timestamp = serializers.ReadOnlyField()
|
||||
gamemode = serializers.ReadOnlyField()
|
||||
round = serializers.ReadOnlyField()
|
||||
pos = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = TournamentGameModel
|
||||
fields = ["id", "winner_id", "state", "started", "finished", "players", "start_timestamp", "stop_timestamp", "game_type", "round", "pos"]
|
||||
|
||||
def get_state(self, instance: TournamentGameModel):
|
||||
if (instance.finished):
|
||||
return "finished"
|
||||
if (instance.started):
|
||||
return "started"
|
||||
return "waiting"
|
||||
|
||||
def get_players(self, instance: TournamentGameModel):
|
||||
return ProfileSerializer(instance.get_players_profiles(), many=True).data
|
@ -1,44 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from django.test.client import Client
|
||||
from django.http import HttpResponse
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
import json
|
||||
import uuid
|
||||
|
||||
class CreateTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
self.url = "/api/tournaments/"
|
||||
|
||||
self.username = str(uuid.uuid4())
|
||||
self.password = str(uuid.uuid4())
|
||||
|
||||
self.nb_players_by_game = 2
|
||||
self.nb_players = 8
|
||||
|
||||
user: User = User.objects.create_user(username=self.username, password=self.password)
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
|
||||
def test_normal(self):
|
||||
response: HttpResponse = self.client.post(self.url, {"nb_players_by_game": self.nb_players_by_game, "nb_players": self.nb_players})
|
||||
response_data: dict = json.loads(response.content)
|
||||
self.assertDictContainsSubset({"name": ""}, response_data)
|
||||
|
||||
def test_too_small_nb_players_by_game(self):
|
||||
response: HttpResponse = self.client.post(self.url, {"nb_players_by_game": 1, "nb_players": self.nb_players})
|
||||
response_data = json.loads(response.content)
|
||||
self.assertDictEqual(response_data, {'nb_players_by_game': ['The numbers of players by game must be greather than 2.']})
|
||||
|
||||
def test_too_small_nb_players(self):
|
||||
response: HttpResponse = self.client.post(self.url, {"nb_players_by_game": self.nb_players_by_game, "nb_players": 1})
|
||||
response_data = json.loads(response.content)
|
||||
self.assertDictEqual(response_data, {'nb_players': ['The numbers of players must be greather than 2.'], 'nb_players_by_game': ['The numbers of players by game must be smaller than the numbers of players.']})
|
||||
|
||||
def test_nb_players_smaller_nb_players_by_game(self):
|
||||
response: HttpResponse = self.client.post(self.url, {"nb_players_by_game": 5, "nb_players": 3})
|
||||
response_data = json.loads(response.content)
|
||||
self.assertDictEqual(response_data, {'nb_players_by_game': ['The numbers of players by game must be smaller than the numbers of players.']})
|
@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@ -1,11 +0,0 @@
|
||||
from django.urls import path, re_path
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
from .viewset import TournamentViewSet
|
||||
|
||||
urlpatterns = [
|
||||
path("<int:pk>", TournamentViewSet.as_view({"get": "retrieve"}), name="tournament_page"),
|
||||
path("", TournamentViewSet.as_view({"post": "create"}), name="tournament_page"),
|
||||
re_path(r"search/(?P<state>\w*)", TournamentViewSet.as_view({"get": "list", }), name="tournaments"),
|
||||
]
|
@ -1,49 +0,0 @@
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.contrib.auth import login
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from .models import TournamentModel
|
||||
from .serializers import TournamentSerializer
|
||||
|
||||
import math
|
||||
|
||||
# Create your views here.
|
||||
class TournamentViewSet(viewsets.ModelViewSet):
|
||||
|
||||
queryset = TournamentModel.objects.all
|
||||
serializer_class = TournamentSerializer
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
authentication_classes = (SessionAuthentication,)
|
||||
|
||||
def perform_create(self, serializer: TournamentSerializer):
|
||||
tournament = serializer.save(round=math.log2(serializer.validated_data['nb_participants']) + 1)
|
||||
|
||||
return Response(self.serializer_class(tournament).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def list(self, request: HttpRequest, state: str = ""):
|
||||
query: QuerySet
|
||||
match state:
|
||||
case "started":
|
||||
query = TournamentModel.objects.filter(started=True, finished=False)
|
||||
case "finished":
|
||||
query = TournamentModel.objects.filter(finished=True)
|
||||
case "waiting":
|
||||
query = TournamentModel.objects.filter(started=False, finished=False)
|
||||
case _:
|
||||
query = TournamentModel.objects.all()
|
||||
serializer = self.serializer_class(query, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def retrieve(self, request: HttpRequest, pk):
|
||||
|
||||
if (not TournamentModel.objects.filter(pk=pk).exists()):
|
||||
return Response({"detail": "Tournament not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
tournament = TournamentModel.objects.get(pk=pk)
|
||||
|
||||
return Response(self.serializer_class(tournament).data, status=status.HTTP_200_OK)
|
@ -13,7 +13,6 @@ from channels.auth import AuthMiddlewareStack
|
||||
|
||||
import chat.routing
|
||||
import matchmaking.routing
|
||||
import tournament.routing
|
||||
import games.routing
|
||||
import notice.routing
|
||||
|
||||
@ -27,7 +26,6 @@ application = ProtocolTypeRouter({
|
||||
URLRouter(
|
||||
chat.routing.websocket_urlpatterns +
|
||||
matchmaking.routing.websocket_urlpatterns +
|
||||
tournament.routing.websocket_urlpatterns +
|
||||
games.routing.websocket_urlpatterns +
|
||||
notice.routing.websocket_urlpatterns
|
||||
)
|
||||
|
@ -44,7 +44,6 @@ INSTALLED_APPS = [
|
||||
'channels',
|
||||
'daphne',
|
||||
|
||||
'tournament.apps.TournamentConfig',
|
||||
'matchmaking.apps.MatchmakingConfig',
|
||||
'games.apps.GamesConfig',
|
||||
'accounts.apps.AccountsConfig',
|
||||
|
@ -23,7 +23,6 @@ urlpatterns = [
|
||||
path('api/profiles/', include('profiles.urls')),
|
||||
path('api/accounts/', include('accounts.urls')),
|
||||
path('api/chat/', include('chat.urls')),
|
||||
path('api/tournaments/', include('tournament.urls')),
|
||||
path('api/games/', include('games.urls')),
|
||||
re_path(r'^api/', handler_404_view),
|
||||
path('', include('frontend.urls')),
|
||||
|
Loading…
Reference in New Issue
Block a user