16 Commits

Author SHA1 Message Date
ae20be25fb add: matchmaking 2023-12-12 12:06:13 +01:00
ad6cfdf08a Merge branch 'server' into feat/matchmaking 2023-12-12 10:01:12 +01:00
08ce682980 rename id to user_id 2023-12-12 09:53:28 +01:00
982130a02f 🤓 2023-12-11 15:59:15 +01:00
326724930a Merge branch 'server' into feat/matchmaking 2023-12-11 15:55:12 +01:00
85787760b9 fix: back button is back 2023-12-11 15:54:56 +01:00
fb1b71ade6 Merge branch 'server' into feat/matchmaking 2023-12-11 15:24:12 +01:00
2ccfc5464a fix: fix: + ratio 2023-12-11 15:23:42 +01:00
d8a279f4d8 fix: profiles list display new profiles 2023-12-11 14:36:36 +01:00
336257d1d0 init view 2023-12-11 13:40:01 +01:00
f66b3883c1 init app 2023-12-11 13:39:53 +01:00
9c1dd30db7 add: css to me 2023-12-11 13:25:00 +01:00
079be0bb46 delete accounts auto logout 2023-12-11 12:43:49 +01:00
df436e0b88 fix: user can change avatar 2023-12-11 12:43:36 +01:00
c3c83b3168 add: profiles and profile page 2023-12-09 21:51:32 +01:00
dd19e15e7d fix: add return to profiles.all() 2023-12-09 15:13:40 +01:00
36 changed files with 340 additions and 64 deletions

View File

@ -22,8 +22,8 @@ pip install -r requirements.txt
``` ```
- Setup database - Setup database
``` ```
python manage.py runserver makemigrations profiles python manage.py runserver makemigrations profiles games
python manage.py migrate profiles python manage.py migrate profiles games
``` ```
- Start the developpement server - Start the developpement server
``` ```

View File

@ -1,6 +1,7 @@
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import permissions, status from rest_framework import permissions, status
from rest_framework.response import Response from rest_framework.response import Response
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
@ -16,4 +17,5 @@ class DeleteView(APIView):
if (request.user.check_password(password) == False): if (request.user.check_password(password) == False):
return Response({"password": ["Password wrong."]}) return Response({"password": ["Password wrong."]})
request.user.delete() request.user.delete()
logout(request)
return Response("user deleted", status=status.HTTP_200_OK) return Response("user deleted", status=status.HTTP_200_OK)

View File

@ -0,0 +1,19 @@
#app .account
{
background-color: red;
}
#app .account, #app .profile
{
width: 60%;
display: flex;
margin-left: auto;
margin-right: auto;
flex-direction: column;
flex-wrap: wrap;
}
#app .profile
{
background-color: green;
}

View File

@ -0,0 +1,11 @@
#app #avatar
{
height: 100px;
width: 100px;
}
#app #username
{
height: 100px;
width: 100px;
}

View File

@ -0,0 +1,11 @@
#app .item img
{
height: 100px;
width: 100px;
}
#app .item a
{
height: 100px;
width: 100px;
}

View File

@ -1,4 +1,5 @@
import { Account } from "./account.js"; import { Account } from "./account.js";
import { MatchMaking } from "./matchmaking.js";
import { Profile } from "./profile.js"; import { Profile } from "./profile.js";
import { Profiles } from "./profiles.js"; import { Profiles } from "./profiles.js";
@ -19,6 +20,7 @@ class Client
this._url = url; this._url = url;
this.account = new Account(this); this.account = new Account(this);
this.profiles = new Profiles(this); this.profiles = new Profiles(this);
this.matchmaking = new MatchMaking(this);
this._logged = undefined; this._logged = undefined;
} }

View File

@ -0,0 +1,42 @@
import { client, navigateTo } from "../index.js"
class MatchMaking
{
/**
* @param {client} client
*/
constructor(client)
{
/**
* @type {client}
*/
this.client = client
}
async start(func)
{
if (!await this.client.isAuthentificate())
return null;
console.log(func)
this.callback = func
console.log(this.callback)
let url = `wss://${window.location.host}/ws/matchmaking/`;
this._chatSocket = new WebSocket(url);
this._chatSocket.onmessage = function (event) {
const data = JSON.parse(event.data);
console.log(func, data)
func(data.game_id)
};
}
async stop()
{
this._chatSocket.close()
}
}
export {MatchMaking}

View File

@ -8,19 +8,19 @@ class Profile
this.user_id = user_id this.user_id = user_id
} }
async init(id) async init(user_id)
{ {
let response = await this.client._get(`/api/profiles/${id}`); let response = await this.client._get(`/api/profiles/${user_id}`);
let response_data = await response.json(); let response_data = await response.json();
this.id = id; this.user_id = user_id;
this.username = response_data.username; this.username = response_data.username;
this.avatar_url = response_data.avatar_url; this.avatar_url = response_data.avatar_url;
} }
async change_avatar(form_data) async change_avatar(form_data)
{ {
let response = await this.client._patch_file(`/api/profiles/${this.id}`, form_data); let response = await this.client._patch_file(`/api/profiles/${this.user_id}`, form_data);
let response_data = await response.json() let response_data = await response.json()
return response_data; return response_data;

View File

@ -16,7 +16,14 @@ class Profiles
response_data.forEach((profile) => { response_data.forEach((profile) => {
profiles.push(new Profile(this.client, profile.username, profile.avatar_url, profile.user_id)) profiles.push(new Profile(this.client, profile.username, profile.avatar_url, profile.user_id))
}); });
console.log(profiles); return profiles;
}
async getProfile(user_id)
{
let profile = new Profile(this.client);
await profile.init(user_id);
return profile;
} }
} }

View File

@ -1,7 +1,5 @@
import LoginView from "./views/accounts/LoginView.js"; import LoginView from "./views/accounts/LoginView.js";
import Dashboard from "./views/Dashboard.js"; import Dashboard from "./views/Dashboard.js";
import Posts from "./views/Posts.js";
import PostView from "./views/PostView.js";
import Settings from "./views/Settings.js"; import Settings from "./views/Settings.js";
import Chat from "./views/Chat.js"; import Chat from "./views/Chat.js";
import HomeView from "./views/HomeView.js"; import HomeView from "./views/HomeView.js";
@ -11,6 +9,9 @@ import LogoutView from "./views/accounts/LogoutView.js";
import { Client } from "./api/client.js"; import { Client } from "./api/client.js";
import AbstractRedirectView from "./views/AbstractRedirectView.js"; import AbstractRedirectView from "./views/AbstractRedirectView.js";
import MeView from "./views/MeView.js"; import MeView from "./views/MeView.js";
import ProfilePageView from "./views/profiles/ProfilePageView.js";
import ProfilesView from "./views/profiles/ProfilesView.js";
import MatchMakingView from "./views/MatchMakingView.js";
let client = new Client(location.protocol + "//" + location.host) let client = new Client(location.protocol + "//" + location.host)
@ -32,11 +33,11 @@ const navigateTo = async (uri) => {
history.pushState(null, null, uri); history.pushState(null, null, uri);
}; };
const router = async (uri = "") => { const router = async (uri) => {
const routes = [ const routes = [
{ path: "/", view: Dashboard }, { path: "/", view: Dashboard },
{ path: "/posts", view: Posts }, { path: "/profiles", view: ProfilesView},
{ path: "/posts/:id", view: PostView }, { path: "/profiles/:id", view: ProfilePageView },
{ path: "/settings", view: Settings }, { path: "/settings", view: Settings },
{ path: "/login", view: LoginView }, { path: "/login", view: LoginView },
{ path: "/logout", view: LogoutView }, { path: "/logout", view: LogoutView },
@ -44,6 +45,7 @@ const router = async (uri = "") => {
{ path: "/chat", view: Chat }, { path: "/chat", view: Chat },
{ path: "/home", view: HomeView }, { path: "/home", view: HomeView },
{ path: "/me", view: MeView }, { path: "/me", view: MeView },
{ path: "/matchmaking", view: MatchMakingView },
]; ];
// Test each route for potential match // Test each route for potential match
@ -84,7 +86,7 @@ const router = async (uri = "") => {
return 0; return 0;
}; };
window.addEventListener("popstate", router); window.addEventListener("popstate", function() {router(location.pathname)});
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
document.body.addEventListener("click", e => { document.body.addEventListener("click", e => {

View File

@ -1,4 +1,3 @@
import { client } from "../index.js";
import AbstractAuthentificateView from "./AbstractAuthentifiedView.js"; import AbstractAuthentificateView from "./AbstractAuthentifiedView.js";
export default class extends AbstractAuthentificateView { export default class extends AbstractAuthentificateView {
@ -10,6 +9,7 @@ export default class extends AbstractAuthentificateView {
async getHtml() { async getHtml() {
return ` return `
<h1>HOME</h1> <h1>HOME</h1>
<a href="/matchmaking" class="nav__link" data-link>Play a game</a>
<a href="/me" class="nav__link" data-link>Me</a> <a href="/me" class="nav__link" data-link>Me</a>
<a href="/logout" class="nav__link" data-link>Logout</a> <a href="/logout" class="nav__link" data-link>Logout</a>
`; `;

View File

@ -0,0 +1,30 @@
import { client, navigateTo } from "../index.js";
import AbstractView from "./AbstractView.js";
function game_found(game_id)
{
navigateTo(`/games/${game_id}`)
}
export default class extends AbstractView {
constructor(params) {
super(params, "Dashboard");
}
async postInit()
{
await client.matchmaking.start(game_found)
console.log("start matchmaking")
}
async getHtml() {
return `
<h1>finding<h1>
`;
}
async leavePage()
{
await client.matchmaking.stop();
}
}

View File

@ -12,8 +12,8 @@ export default class extends AbstractAuthentificateView
{ {
if (this.fill() === null) if (this.fill() === null)
return; return;
document.getElementById("save-button").onclick = this.save; document.getElementById("save-account-button").onclick = this.acccount_save;
document.getElementById("delete-button").onclick = this.delete_accounts; document.getElementById("delete-account-button").onclick = this.account_delete_accounts;
} }
async fill() async fill()
@ -104,22 +104,27 @@ export default class extends AbstractAuthentificateView
async getHtml() async getHtml()
{ {
return ` return `
<link rel="stylesheet" href="static/css/me.css">
<h1>ME</h1> <h1>ME</h1>
<div class="accounts"> <div class="account">
<h3>Account</h3>
<input type="text" placeholder="username" id="username"> <input type="text" placeholder="username" id="username">
<span id="error_username"></span> <span id="error_username"></span>
<input type=password placeholder="new password" id="new_password"> <input type=password placeholder="new password" id="new_password">
<span id="error_new_password"></span> <span id="error_new_password"></span>
<input type=password placeholder="current password" id="current_password"> <input type=password placeholder="current password" id="current_password">
<span id="error_current_password"></span> <span id="error_current_password"></span>
<input type="button" value="Save Credentials" id="save-account-button">
<span id="error_save"></span>
<input type="button" value="Delete Account" id="delete-account-button">
<span id="error_delete"></span>
</div> </div>
<div class="profile"> <div class="profile">
<input type="file" placeholder="username" id="avatar" accept="image/png, image/jpeg"> <h3>Profile</h3>
</div> <input type="file" id="avatar" accept="image/png, image/jpeg">
<input type="button" value="Save" id="save-button"> <input type="button" value="Save profile" id="save-profile-button">
<span id="error_save"></span> <span id="error_save"></span>
<input type="button" value="Delete" id="delete-button"> </div>
<span id="error_delete"></span>
<a href="/logout" class="nav__link" data-link>Logout</a> <a href="/logout" class="nav__link" data-link>Logout</a>
`; `;
} }

View File

@ -1,15 +0,0 @@
import AbstractView from "./AbstractView.js";
export default class extends AbstractView {
constructor(params) {
super(params, "Viewing Post");
this.postId = params.id;
}
async getHtml() {
return `
<h1>Post</h1>
<p>You are viewing post #${this.postId}.</p>
`;
}
}

View File

@ -1,14 +0,0 @@
import AbstractView from "./AbstractView.js";
export default class extends AbstractView {
constructor(params) {
super(params, "Posts");
}
async getHtml() {
return `
<h1>Posts</h1>
<p>You are viewing the posts!</p>
`;
}
}

View File

@ -0,0 +1,29 @@
import AbstractView from "../AbstractView.js";
import { client } from "../../index.js"
export default class extends AbstractView {
constructor(params) {
super(params, "Profile ");
this.user_id = params.id;
}
async postInit()
{
let profile = await client.profiles.getProfile(this.user_id);
let username_element = document.getElementById("username");
username_element.href = `/profiles/${this.user_id}`;
username_element.appendChild(document.createTextNode(profile.username));
let avatar_element = document.getElementById("avatar");
avatar_element.src = profile.avatar_url;
}
async getHtml() {
return `
<link rel="stylesheet" href="/static/css/profiles/profile.css">
<img id="avatar"/>
<a id="username"></a>
`;
}
}

View File

@ -0,0 +1,40 @@
import AbstractView from "../AbstractView.js";
import { client } from "../../index.js"
export default class extends AbstractView {
constructor(params) {
super(params, "Profiles");
}
async postInit()
{
let profiles = await client.profiles.all()
let profile_list_element = document.getElementById("profile-list")
profiles.forEach((profile) => {
let profile_element = document.createElement("div");
profile_element.className = "item";
let avatar = document.createElement("img");
avatar.src = profile.avatar_url;
let username = document.createElement("a");
username.href = `/profiles/${profile.user_id}`;
username.appendChild(document.createTextNode(profile.username));
profile_element.appendChild(avatar);
profile_element.appendChild(username);
profile_list_element.appendChild(profile_element)
});
}
async getHtml() {
return `
<link rel="stylesheet" href="/static/css/profiles/profiles.css">
<div id="profile-list">
</div>
`;
}
}

View File

@ -10,8 +10,7 @@
<body> <body>
<nav class="nav"> <nav class="nav">
<a href="/" class="nav__link" data-link>Dashboard</a> <a href="/" class="nav__link" data-link>Dashboard</a>
<a href="/posts" class="nav__link" data-link>Posts</a> <a href="/profiles" class="nav__link" data-link>Profiles</a>
<a href="/settings" class="nav__link" data-link>Settings</a>
<a href="/login" class="nav__link" data-link>Login</a> <a href="/login" class="nav__link" data-link>Login</a>
<a href="/register" class="nav__link" data-link>Register</a> <a href="/register" class="nav__link" data-link>Register</a>
<a href="/chat" class="nav__link" data-link>Chat</a> <a href="/chat" class="nav__link" data-link>Chat</a>

0
games/__init__.py Normal file
View File

3
games/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
games/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class GamesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'games'

14
games/models.py Normal file
View File

@ -0,0 +1,14 @@
from django.db import models
# Create your models here.
class GameModel(models.Model):
def create(self, users_id: [int]):
self.save()
for user_id in users_id:
GameMembersModel(game_id=self.pk, member_id=user_id)
return self.pk
class GameMembersModel(models.Model):
game_id = models.IntegerField()
member_id = models.IntegerField()

3
games/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
games/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
matchmaking/__init__.py Normal file
View File

3
matchmaking/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
matchmaking/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class MatchmakingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'matchmaking'

48
matchmaking/consumers.py Normal file
View File

@ -0,0 +1,48 @@
from channels.generic.websocket import WebsocketConsumer
from django.contrib.auth.models import User
from games.models import GameModel
import json
queue_id: [int] = []
queue_ws: [WebsocketConsumer] = []
class MatchMaking(WebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.channel_name = "matchmaking"
self.group_name = "matchmaking"
def connect(self):
user: User = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated):
return
self.channel_layer.group_add(self.group_name, self.channel_name)
self.accept()
global queue_id, queue_ws
queue_id.append(user.pk)
queue_ws.append(self)
if len(set(queue_id)) == 2:
game_id: int = GameModel().create(set(queue_id))
event = {"game_id": game_id}
for ws in queue_ws:
ws.send(text_data=json.dumps({'game_id': game_id}))
queue_id.clear()
queue_ws.clear()
def disconnect(self, close_code):
user: User = self.scope["user"]
global queue_id, queue_ws
if (user.pk in queue_id):
queue_ws.pop(queue_id.index(user.pk))
queue_id.remove(user.pk)
self.channel_layer.group_discard(self.group_name, self.channel_name)

3
matchmaking/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

6
matchmaking/routing.py Normal file
View File

@ -0,0 +1,6 @@
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/matchmaking/', consumers.MatchMaking.as_asgi())
]

3
matchmaking/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
matchmaking/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -11,7 +11,7 @@ def upload_to(instance, filename: str):
# Create your models here. # Create your models here.
class ProfileModel(models.Model): class ProfileModel(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
avatar_url = models.ImageField(upload_to=upload_to, default="../static/avatars/default.avif") #blank=True, null=True) avatar_url = models.ImageField(upload_to=upload_to, default="./profiles/static/avatars/default.avif") #blank=True, null=True)
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def on_user_created(sender, instance, created, **kwargs): def on_user_created(sender, instance, created, **kwargs):

View File

@ -11,22 +11,22 @@ from .serializers import ProfileSerializer
from .models import ProfileModel from .models import ProfileModel
class ProfileViewSet(viewsets.ModelViewSet): class ProfileViewSet(viewsets.ModelViewSet):
queryset = ProfileModel.objects.order_by('-pk') queryset = ProfileModel.objects.all
serializer_class = ProfileSerializer serializer_class = ProfileSerializer
parser_classes = (MultiPartParser, FormParser) parser_classes = (MultiPartParser, FormParser)
permission_classes = (permissions.IsAuthenticatedOrReadOnly,) permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
def retrieve(self, request: HttpRequest, pk=None): def retrieve(self, request: HttpRequest, pk=None):
instance = self.get_object() instance = ProfileModel.objects.get(pk=pk)
instance.avatar_url.name = instance.avatar_url.name.replace("profiles", "", 1) instance.avatar_url.name = instance.avatar_url.name[instance.avatar_url.name.find("static") - 1:]
return Response(self.serializer_class(instance).data, return Response(self.serializer_class(instance).data,
status=status.HTTP_200_OK) status=status.HTTP_200_OK)
def get_queryset(self): def list(self, request: HttpRequest):
profiles = ProfileModel.objects.all() serializer = ProfileSerializer(self.queryset(), many=True)
for profile in profiles: for profile in serializer.data:
profile.avatar_url.name = profile.avatar_url.name.replace("profiles", "", 1) profile["avatar_url"] = profile["avatar_url"][profile["avatar_url"].find("static") - 1:]
return profiles return Response(serializer.data)
def perform_create(self, serializer): def perform_create(self, serializer):
serializer.save(user=self.request.user) serializer.save(user=self.request.user)
@ -38,7 +38,7 @@ class ProfileViewSet(viewsets.ModelViewSet):
profile: ProfileModel = ProfileModel.objects.get(pk=self.request.user.pk) profile: ProfileModel = ProfileModel.objects.get(pk=self.request.user.pk)
avatar = self.request.data.get("file", None) avatar = self.request.data.get("file", None)
if (avatar is not None): if (avatar is not None):
if (profile.avatar_url.name != "default.avif"): if (profile.avatar_url.name != "./profiles/static/avatars/default.avif"):
profile.avatar_url.storage.delete(profile.avatar_url.name) profile.avatar_url.storage.delete(profile.avatar_url.name)
profile.avatar_url = avatar profile.avatar_url = avatar
profile.save() profile.save()

View File

@ -10,7 +10,9 @@ https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
import os import os
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack from channels.auth import AuthMiddlewareStack
import chat.routing import chat.routing
import matchmaking.routing
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
@ -20,7 +22,8 @@ application = ProtocolTypeRouter({
'http':get_asgi_application(), 'http':get_asgi_application(),
'websocket':AuthMiddlewareStack( 'websocket':AuthMiddlewareStack(
URLRouter( URLRouter(
chat.routing.websocket_urlpatterns chat.routing.websocket_urlpatterns +
matchmaking.routing.websocket_urlpatterns
) )
) )
}) })

View File

@ -43,6 +43,8 @@ INSTALLED_APPS = [
'channels', 'channels',
'daphne', 'daphne',
'matchmaking.apps.MatchmakingConfig',
'games.apps.GamesConfig',
'accounts.apps.AccountsConfig', 'accounts.apps.AccountsConfig',
'profiles.apps.ProfilesConfig', 'profiles.apps.ProfilesConfig',
'frontend.apps.FrontendConfig', 'frontend.apps.FrontendConfig',