Compare commits
No commits in common. "main" and "feat/matchmaking" have entirely different histories.
main
...
feat/match
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.env
|
||||||
|
*.pyc
|
||||||
|
db.sqlite3
|
||||||
|
**/migrations/**
|
||||||
|
/profiles/static/avatars/*
|
||||||
|
!/profiles/static/avatars/default
|
33
README.md
33
README.md
@ -1,4 +1,31 @@
|
|||||||
# TRANSCENDENCE
|
# BACKEND
|
||||||
The last project of the 42 common core
|
|
||||||
|
|
||||||
# adrien est une merde
|
## Installation
|
||||||
|
|
||||||
|
- Clone the project:
|
||||||
|
``` bash
|
||||||
|
git clone https://git.chauvet.pro/michel/ft_transcendence
|
||||||
|
cd ft_transcendence
|
||||||
|
git switch server
|
||||||
|
```
|
||||||
|
- Create python virtual environnement.
|
||||||
|
``` bash
|
||||||
|
python3 -m venv .env
|
||||||
|
```
|
||||||
|
- Source the environnement.
|
||||||
|
``` bash
|
||||||
|
source .env/bin/activate
|
||||||
|
```
|
||||||
|
- Install the requirements
|
||||||
|
``` bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
- Setup database
|
||||||
|
```
|
||||||
|
python manage.py runserver makemigrations profiles games
|
||||||
|
python manage.py migrate profiles games
|
||||||
|
```
|
||||||
|
- Start the developpement server
|
||||||
|
```
|
||||||
|
python manage.py runserver 0.0.0.0:8000
|
||||||
|
```
|
||||||
|
0
accounts/__init__.py
Normal file
0
accounts/__init__.py
Normal file
3
accounts/admin.py
Normal file
3
accounts/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
accounts/apps.py
Normal file
6
accounts/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'accounts'
|
12
accounts/serializers/login.py
Normal file
12
accounts/serializers/login.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from rest_framework.serializers import Serializer, CharField
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
class LoginSerializer(Serializer):
|
||||||
|
|
||||||
|
username = CharField()
|
||||||
|
password = CharField()
|
||||||
|
|
||||||
|
def get_user(self, data):
|
||||||
|
user = authenticate(username=data['username'], password=data['password'])
|
||||||
|
return user
|
12
accounts/serializers/register.py
Normal file
12
accounts/serializers/register.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
class RegisterSerialiser(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ['username', 'password']
|
||||||
|
|
||||||
|
def create(self, data):
|
||||||
|
user_obj = User.objects.create_user(username=data['username'], password=data['password'])
|
||||||
|
user_obj.save()
|
||||||
|
return user_obj
|
5
accounts/tests/__init__.py
Normal file
5
accounts/tests/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from .register import *
|
||||||
|
from .login import *
|
||||||
|
from .logout import *
|
||||||
|
from .edit import *
|
||||||
|
from .delete import *
|
37
accounts/tests/delete.py
Normal file
37
accounts/tests/delete.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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 uuid
|
||||||
|
|
||||||
|
class DeleteTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
self.url = "/api/accounts/delete"
|
||||||
|
|
||||||
|
self.username: str = str(uuid.uuid4())
|
||||||
|
self.password: str = str(uuid.uuid4())
|
||||||
|
|
||||||
|
user: User = User.objects.create_user(username=self.username, password=self.password)
|
||||||
|
self.client.login(username=self.username, password=self.password)
|
||||||
|
|
||||||
|
|
||||||
|
def test_normal_delete(self):
|
||||||
|
response: HttpResponse = self.client.delete(self.url, {"password": self.password}, content_type='application/json')
|
||||||
|
response_text: str = response.content.decode("utf-8")
|
||||||
|
self.assertEqual(response_text, '"user deleted"')
|
||||||
|
|
||||||
|
def test_wrong_pass(self):
|
||||||
|
response: HttpResponse = self.client.delete(self.url, {"password": "cacaman a frapper"}, content_type='application/json')
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
self.assertDictEqual(errors, {"password": ["Password wrong."]})
|
||||||
|
|
||||||
|
def test_no_logged(self):
|
||||||
|
self.client.logout()
|
||||||
|
response: HttpResponse = self.client.delete(self.url, {"password": self.password}, content_type='application/json')
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
self.assertDictEqual(errors, {"detail":"Authentication credentials were not provided."})
|
49
accounts/tests/edit.py
Normal file
49
accounts/tests/edit.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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 uuid
|
||||||
|
|
||||||
|
class EditTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
self.url = "/api/accounts/edit"
|
||||||
|
|
||||||
|
self.username: str = str(uuid.uuid4())
|
||||||
|
self.password: str = str(uuid.uuid4())
|
||||||
|
self.new_password: str = str(uuid.uuid4())
|
||||||
|
|
||||||
|
User.objects.create_user(username = self.username, password = self.password)
|
||||||
|
|
||||||
|
def test_normal(self):
|
||||||
|
self.client.login(username = self.username, password = self.password)
|
||||||
|
response: HttpResponse = self.client.patch(self.url, {"current_password": self.password, "new_password": self.new_password, "username": "bozo"}, content_type='application/json')
|
||||||
|
response_text: str = response.content.decode('utf-8')
|
||||||
|
self.assertEqual(response_text, '"data has been alterate"')
|
||||||
|
|
||||||
|
def test_invalid_current_password(self):
|
||||||
|
self.client.login(username = self.username, password = self.password)
|
||||||
|
response: HttpResponse = self.client.patch(self.url, {"current_password": "bozo", "new_password": self.new_password, "username": "bozo"}, content_type='application/json')
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
self.assertDictEqual(errors, {"current_password":["Password is wrong."]})
|
||||||
|
|
||||||
|
def test_invalid_new_username_blank(self):
|
||||||
|
self.client.login(username = self.username, password = self.password)
|
||||||
|
response: HttpResponse = self.client.patch(self.url, {"current_password": self.password, "username": " "}, content_type='application/json')
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
self.assertDictEqual(errors, {'username': ['This field may not be blank.']})
|
||||||
|
|
||||||
|
def test_invalid_new_username_char(self):
|
||||||
|
self.client.login(username = self.username, password = self.password)
|
||||||
|
response: HttpResponse = self.client.patch(self.url, {"current_password": self.password, "username": "*&"}, content_type='application/json')
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
self.assertDictEqual(errors, {'username': ['Enter a valid username. This value may contain only letters, numbers, and @/./+/-/_ characters.']})
|
||||||
|
|
||||||
|
def test_nologged(self):
|
||||||
|
response: HttpResponse = self.client.patch(self.url, {"current_password": self.password, "new_password": self.new_password}, content_type='application/json')
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
self.assertDictEqual(errors, {'detail': 'Authentication credentials were not provided.'})
|
53
accounts/tests/login.py
Normal file
53
accounts/tests/login.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
from django.test.client import Client
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.http import HttpResponse
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
class LoginTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
self.url = "/api/accounts/login"
|
||||||
|
|
||||||
|
self.username: str = str(uuid.uuid4())
|
||||||
|
self.password: str = str(uuid.uuid4())
|
||||||
|
|
||||||
|
User.objects.create_user(username=self.username, password=self.password)
|
||||||
|
|
||||||
|
def test_normal_login(self):
|
||||||
|
response: HttpResponse = self.client.post(self.url, {'username': self.username, 'password': self.password})
|
||||||
|
response_text = response.content.decode('utf-8')
|
||||||
|
#self.assertEqual(response_text, 'user connected')
|
||||||
|
|
||||||
|
def test_invalid_username(self):
|
||||||
|
response: HttpResponse = self.client.post(self.url, {"username": self.password, "password": self.password})
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
errors_expected: dict = {'user': ['Username or password wrong.']}
|
||||||
|
self.assertEqual(errors, errors_expected)
|
||||||
|
|
||||||
|
def test_invalid_password(self):
|
||||||
|
response: HttpResponse = self.client.post(self.url, {"username": self.username, "password": self.username})
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
errors_expected: dict = {'user': ['Username or password wrong.']}
|
||||||
|
self.assertEqual(errors, errors_expected)
|
||||||
|
|
||||||
|
def test_invalid_no_username(self):
|
||||||
|
response: HttpResponse = self.client.post(self.url, {"password": self.password})
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
errors_expected: dict = {'username': ['This field is required.']}
|
||||||
|
self.assertEqual(errors, errors_expected)
|
||||||
|
|
||||||
|
def test_invalid_no_password(self):
|
||||||
|
response: HttpResponse = self.client.post(self.url, {"username": self.username})
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
errors_expected: dict = {'password': ['This field is required.']}
|
||||||
|
self.assertEqual(errors, errors_expected)
|
||||||
|
|
||||||
|
def test_invalid_no_password_no_username(self):
|
||||||
|
response: HttpResponse = self.client.post(self.url, {})
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
errors_expected: dict = {'username': ['This field is required.'], 'password': ['This field is required.']}
|
||||||
|
self.assertEqual(errors, errors_expected)
|
17
accounts/tests/logout.py
Normal file
17
accounts/tests/logout.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from django.test.client import Client
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.auth import login
|
||||||
|
|
||||||
|
class LoginTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
self.url = "/api/accounts/logout"
|
||||||
|
|
||||||
|
self.client.login()
|
||||||
|
|
||||||
|
def test_normal_logout(self):
|
||||||
|
response: HttpResponse = self.client.post(self.url)
|
||||||
|
self.assertNotIn('_auth_user_id', self.client.session)
|
52
accounts/tests/register.py
Normal file
52
accounts/tests/register.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
from rest_framework import status
|
||||||
|
from django.test.client import Client
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.http import HttpResponse
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
class RegisterTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
self.url: str = "/api/accounts/register"
|
||||||
|
|
||||||
|
self.username: str = str(uuid.uuid4())
|
||||||
|
self.password: str = str(uuid.uuid4())
|
||||||
|
|
||||||
|
def test_normal_register(self):
|
||||||
|
response: HttpResponse = self.client.post(self.url, {'username': self.username, 'password': self.password})
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def test_incomplet_form_no_username_no_password(self):
|
||||||
|
response: HttpResponse = self.client.post(self.url)
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
errors_expected: dict = {'username': ['This field is required.'], 'password': ['This field is required.']}
|
||||||
|
self.assertEqual(errors, errors_expected)
|
||||||
|
|
||||||
|
def test_incomplet_form_no_password(self):
|
||||||
|
response: HttpResponse = self.client.post(self.url, {"username": self.username})
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
errors_expected: dict = {'password': ['This field is required.']}
|
||||||
|
self.assertEqual(errors, errors_expected)
|
||||||
|
|
||||||
|
def test_incomplet_form_no_username(self):
|
||||||
|
response: HttpResponse = self.client.post(self.url, {"password": self.password})
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
errors_expected: dict = {}
|
||||||
|
self.assertEqual(errors, errors_expected)
|
||||||
|
|
||||||
|
def test_incomplet_form_no_username(self):
|
||||||
|
response: HttpResponse = self.client.post(self.url, {"password": self.password})
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
errors_expected: dict = {'username': ['This field is required.']}
|
||||||
|
self.assertEqual(errors, errors_expected)
|
||||||
|
|
||||||
|
def test_already_registered(self):
|
||||||
|
User(username=self.username, password=self.password).save()
|
||||||
|
response: HttpResponse = self.client.post(self.url, {'username': self.username, 'password': self.password})
|
||||||
|
errors: dict = eval(response.content)
|
||||||
|
errors_expected: dict = {'username': ['A user with that username already exists.']}
|
||||||
|
self.assertEqual(errors, errors_expected)
|
13
accounts/urls.py
Normal file
13
accounts/urls.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from .views import register, login, logout, delete, edit, logged
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("register", register.RegisterView.as_view(), name="register"),
|
||||||
|
path("login", login.LoginView.as_view(), name="login"),
|
||||||
|
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")
|
||||||
|
|
||||||
|
]
|
21
accounts/views/delete.py
Normal file
21
accounts/views/delete.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework import permissions, status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
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."]})
|
||||||
|
request.user.delete()
|
||||||
|
logout(request)
|
||||||
|
return Response("user deleted", status=status.HTTP_200_OK)
|
45
accounts/views/edit.py
Normal file
45
accounts/views/edit.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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")
|
18
accounts/views/logged.py
Normal file
18
accounts/views/logged.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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):
|
||||||
|
if (request.user.is_authenticated):
|
||||||
|
return Response({'id': request.user.pk}, status=status.HTTP_200_OK)
|
||||||
|
return Response('false', status=status.HTTP_200_OK)
|
23
accounts/views/login.py
Normal file
23
accounts/views/login.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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 LoginView(APIView):
|
||||||
|
|
||||||
|
permission_classes = (permissions.AllowAny,)
|
||||||
|
authentication_classes = (SessionAuthentication,)
|
||||||
|
|
||||||
|
def post(self, request: HttpRequest):
|
||||||
|
data = request.data
|
||||||
|
serializer = LoginSerializer(data=data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
user = serializer.get_user(data)
|
||||||
|
if user is None:
|
||||||
|
return Response({'user': ['Username or password wrong.']}, status.HTTP_200_OK)
|
||||||
|
login(request, user)
|
||||||
|
return Response({'id': user.pk}, status=status.HTTP_200_OK)
|
13
accounts/views/logout.py
Normal file
13
accounts/views/logout.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from rest_framework.views import APIView
|
||||||
|
from django.contrib.auth import logout
|
||||||
|
from rest_framework import permissions, status
|
||||||
|
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)
|
18
accounts/views/register.py
Normal file
18
accounts/views/register.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from rest_framework import permissions, status
|
||||||
|
from ..serializers.register import RegisterSerialiser
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
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)
|
||||||
|
if serializer.is_valid(raise_exception=True):
|
||||||
|
user = serializer.create(data)
|
||||||
|
if user:
|
||||||
|
login(request, user)
|
||||||
|
return Response("user created", status=status.HTTP_201_CREATED)
|
||||||
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
0
chat/__init__.py
Normal file
0
chat/__init__.py
Normal file
3
chat/admin.py
Normal file
3
chat/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
chat/apps.py
Normal file
6
chat/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ChatConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'chat'
|
36
chat/consumers.py
Normal file
36
chat/consumers.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import json
|
||||||
|
from channels.generic.websocket import WebsocketConsumer
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
|
||||||
|
class ChatConsumer(WebsocketConsumer):
|
||||||
|
def connect(self):
|
||||||
|
self.room_group_name = 'test'
|
||||||
|
|
||||||
|
async_to_sync(self.channel_layer.group_add)(
|
||||||
|
self.room_group_name,
|
||||||
|
self.channel_name
|
||||||
|
)
|
||||||
|
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
|
||||||
|
def receive(self, text_data):
|
||||||
|
text_data_json = json.loads(text_data)
|
||||||
|
message = text_data_json['message']
|
||||||
|
|
||||||
|
async_to_sync(self.channel_layer.group_send)(
|
||||||
|
self.room_group_name,
|
||||||
|
{
|
||||||
|
'type':'chat_message',
|
||||||
|
'message':message
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def chat_message(self, event):
|
||||||
|
message = event['message']
|
||||||
|
|
||||||
|
self.send(text_data=json.dumps({
|
||||||
|
'type':'chat',
|
||||||
|
'username':self.scope["user"].username,
|
||||||
|
'message':message
|
||||||
|
}))
|
3
chat/models.py
Normal file
3
chat/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
6
chat/routing.py
Normal file
6
chat/routing.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.urls import re_path
|
||||||
|
from . import consumers
|
||||||
|
|
||||||
|
websocket_urlpatterns = [
|
||||||
|
re_path(r'ws/socket-server/', consumers.ChatConsumer.as_asgi())
|
||||||
|
]
|
3
chat/tests.py
Normal file
3
chat/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
chat/views.py
Normal file
3
chat/views.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
0
frontend/__init__.py
Normal file
0
frontend/__init__.py
Normal file
3
frontend/admin.py
Normal file
3
frontend/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
frontend/apps.py
Normal file
6
frontend/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FrontendConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'frontend'
|
3
frontend/models.py
Normal file
3
frontend/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
12
frontend/static/css/accounts/login.css
Normal file
12
frontend/static/css/accounts/login.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#app .form {
|
||||||
|
background-color: red;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
grid-gap: 10px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-top: 90px;
|
||||||
|
border: 15px black solid;
|
||||||
|
}
|
12
frontend/static/css/accounts/register.css
Normal file
12
frontend/static/css/accounts/register.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#app .form {
|
||||||
|
background-color: red;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
grid-gap: 10px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-top: 90px;
|
||||||
|
border: 15px black solid;
|
||||||
|
}
|
9
frontend/static/css/index.css
Normal file
9
frontend/static/css/index.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
body {
|
||||||
|
margin: 10;
|
||||||
|
font-family: 'Quicksand', sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #009579;
|
||||||
|
}
|
19
frontend/static/css/me.css
Normal file
19
frontend/static/css/me.css
Normal 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;
|
||||||
|
}
|
11
frontend/static/css/profiles/profile.css
Normal file
11
frontend/static/css/profiles/profile.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#app #avatar
|
||||||
|
{
|
||||||
|
height: 100px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app #username
|
||||||
|
{
|
||||||
|
height: 100px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
11
frontend/static/css/profiles/profiles.css
Normal file
11
frontend/static/css/profiles/profiles.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#app .item img
|
||||||
|
{
|
||||||
|
height: 100px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app .item a
|
||||||
|
{
|
||||||
|
height: 100px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
65
frontend/static/js/api/account.js
Normal file
65
frontend/static/js/api/account.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
class Account
|
||||||
|
{
|
||||||
|
constructor (client)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(username, password)
|
||||||
|
{
|
||||||
|
let response = await this.client._post("/api/accounts/register", {username: username, password: password});
|
||||||
|
let response_data = await response.json()
|
||||||
|
|
||||||
|
if (response_data == "user created")
|
||||||
|
{
|
||||||
|
this._logged = true;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return response_data
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(password)
|
||||||
|
{
|
||||||
|
let response = await this.client._delete("/api/accounts/delete", {password: password});
|
||||||
|
let response_data = await response.json();
|
||||||
|
|
||||||
|
if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'}))
|
||||||
|
{
|
||||||
|
this.client._logged = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
console.log(response_data)
|
||||||
|
if (response_data == "user deleted")
|
||||||
|
this.client._logged = false;
|
||||||
|
return response_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get()
|
||||||
|
{
|
||||||
|
let response = await this.client._get("/api/accounts/edit");
|
||||||
|
let response_data = await response.json();
|
||||||
|
|
||||||
|
if (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'}))
|
||||||
|
{
|
||||||
|
this.client._logged = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return response_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (JSON.stringify(response_data) == JSON.stringify({'detail': 'Authentication credentials were not provided.'}))
|
||||||
|
{
|
||||||
|
this.client._logged = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return response_data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Account }
|
127
frontend/static/js/api/client.js
Normal file
127
frontend/static/js/api/client.js
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { Account } from "./account.js";
|
||||||
|
import { MatchMaking } from "./matchmaking.js";
|
||||||
|
import { Profile } from "./profile.js";
|
||||||
|
import { Profiles } from "./profiles.js";
|
||||||
|
|
||||||
|
function getCookie(name)
|
||||||
|
{
|
||||||
|
let cookie = {};
|
||||||
|
document.cookie.split(';').forEach(function(el) {
|
||||||
|
let split = el.split('=');
|
||||||
|
cookie[split[0].trim()] = split.slice(1).join("=");
|
||||||
|
})
|
||||||
|
return cookie[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
class Client
|
||||||
|
{
|
||||||
|
constructor(url)
|
||||||
|
{
|
||||||
|
this._url = url;
|
||||||
|
this.account = new Account(this);
|
||||||
|
this.profiles = new Profiles(this);
|
||||||
|
this.matchmaking = new MatchMaking(this);
|
||||||
|
this._logged = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isAuthentificate()
|
||||||
|
{
|
||||||
|
if (this._logged == undefined)
|
||||||
|
this.logged = await this._test_logged();
|
||||||
|
return this.logged;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _get(uri)
|
||||||
|
{
|
||||||
|
let response = await fetch(this._url + uri, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _post(uri, data)
|
||||||
|
{
|
||||||
|
let response = await fetch(this._url + uri, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRFToken": getCookie("csrftoken"),
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _delete(uri, data)
|
||||||
|
{
|
||||||
|
let response = await fetch(this._url + uri, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-CSRFToken": getCookie("csrftoken"),
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _patch_json(uri, data)
|
||||||
|
{
|
||||||
|
let response = await fetch(this._url + uri, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"X-CSRFToken": getCookie("csrftoken"),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _patch_file(uri, file)
|
||||||
|
{
|
||||||
|
let response = await fetch(this._url + uri, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"X-CSRFToken": getCookie("csrftoken"),
|
||||||
|
},
|
||||||
|
body: file,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(username, password)
|
||||||
|
{
|
||||||
|
let response = await this._post("/api/accounts/login", {username: username, password: password})
|
||||||
|
let data = await response.json();
|
||||||
|
if (data.id != undefined)
|
||||||
|
{
|
||||||
|
this.me = new Profile(this)
|
||||||
|
await this.me.init(data.id)
|
||||||
|
this.logged = true;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout()
|
||||||
|
{
|
||||||
|
await this._get("/api/accounts/logout");
|
||||||
|
this.logged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _test_logged()
|
||||||
|
{
|
||||||
|
let response = await this._get("/api/accounts/logged");
|
||||||
|
let data = await response.json();
|
||||||
|
|
||||||
|
if (data.id !== undefined)
|
||||||
|
{
|
||||||
|
this.me = new Profile(this)
|
||||||
|
await this.me.init(data.id)
|
||||||
|
}
|
||||||
|
return data.id !== undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {Client}
|
42
frontend/static/js/api/matchmaking.js
Normal file
42
frontend/static/js/api/matchmaking.js
Normal 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}
|
35
frontend/static/js/api/profile.js
Normal file
35
frontend/static/js/api/profile.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
class Profile
|
||||||
|
{
|
||||||
|
constructor (client, username = undefined, avatar_url = undefined, user_id = undefined)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
this.username = username;
|
||||||
|
this.avatar_url = avatar_url
|
||||||
|
this.user_id = user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(user_id)
|
||||||
|
{
|
||||||
|
let response = await this.client._get(`/api/profiles/${user_id}`);
|
||||||
|
let response_data = await response.json();
|
||||||
|
|
||||||
|
this.user_id = user_id;
|
||||||
|
this.username = response_data.username;
|
||||||
|
this.avatar_url = response_data.avatar_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async change_avatar(form_data)
|
||||||
|
{
|
||||||
|
let response = await this.client._patch_file(`/api/profiles/${this.user_id}`, form_data);
|
||||||
|
let response_data = await response.json()
|
||||||
|
|
||||||
|
return response_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setData (data)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {Profile}
|
30
frontend/static/js/api/profiles.js
Normal file
30
frontend/static/js/api/profiles.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Profile } from "./profile.js";
|
||||||
|
|
||||||
|
class Profiles
|
||||||
|
{
|
||||||
|
constructor (client)
|
||||||
|
{
|
||||||
|
this.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
async all()
|
||||||
|
{
|
||||||
|
let response = await this.client._get("/api/profiles/");
|
||||||
|
let response_data = await response.json();
|
||||||
|
|
||||||
|
let profiles = []
|
||||||
|
response_data.forEach((profile) => {
|
||||||
|
profiles.push(new Profile(this.client, profile.username, profile.avatar_url, profile.user_id))
|
||||||
|
});
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProfile(user_id)
|
||||||
|
{
|
||||||
|
let profile = new Profile(this.client);
|
||||||
|
await profile.init(user_id);
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {Profiles}
|
101
frontend/static/js/index.js
Normal file
101
frontend/static/js/index.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import LoginView from "./views/accounts/LoginView.js";
|
||||||
|
import Dashboard from "./views/Dashboard.js";
|
||||||
|
import Settings from "./views/Settings.js";
|
||||||
|
import Chat from "./views/Chat.js";
|
||||||
|
import HomeView from "./views/HomeView.js";
|
||||||
|
import RegisterView from "./views/accounts/RegisterView.js";
|
||||||
|
import LogoutView from "./views/accounts/LogoutView.js";
|
||||||
|
|
||||||
|
import { Client } from "./api/client.js";
|
||||||
|
import AbstractRedirectView from "./views/AbstractRedirectView.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 lastView = undefined
|
||||||
|
|
||||||
|
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]];
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateTo = async (uri) => {
|
||||||
|
if (await router(uri) === 0)
|
||||||
|
history.pushState(null, null, uri);
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = async (uri) => {
|
||||||
|
const routes = [
|
||||||
|
{ path: "/", view: Dashboard },
|
||||||
|
{ path: "/profiles", view: ProfilesView},
|
||||||
|
{ path: "/profiles/:id", view: ProfilePageView },
|
||||||
|
{ path: "/settings", view: Settings },
|
||||||
|
{ path: "/login", view: LoginView },
|
||||||
|
{ path: "/logout", view: LogoutView },
|
||||||
|
{ path: "/register", view: RegisterView },
|
||||||
|
{ path: "/chat", view: Chat },
|
||||||
|
{ path: "/home", view: HomeView },
|
||||||
|
{ path: "/me", view: MeView },
|
||||||
|
{ path: "/matchmaking", view: MatchMakingView },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test each route for potential match
|
||||||
|
const potentialMatches = routes.map(route => {
|
||||||
|
return {
|
||||||
|
route: route,
|
||||||
|
result: uri.match(pathToRegex(route.path))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let match = potentialMatches.find(potentialMatch => potentialMatch.result !== null);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
match = {
|
||||||
|
route: routes[0],
|
||||||
|
result: [uri]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastView !== undefined)
|
||||||
|
await lastView.leavePage();
|
||||||
|
|
||||||
|
const view = new match.route.view(getParams(match));
|
||||||
|
|
||||||
|
if (view instanceof AbstractRedirectView && await view.redirect())
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
lastView = view;
|
||||||
|
|
||||||
|
let content = await view.getHtml();
|
||||||
|
if (content == null)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
view.setTitle();
|
||||||
|
document.querySelector("#app").innerHTML = content
|
||||||
|
|
||||||
|
await view.postInit();
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("popstate", function() {router(location.pathname)});
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.body.addEventListener("click", e => {
|
||||||
|
if (e.target.matches("[data-link]")) {
|
||||||
|
e.preventDefault();
|
||||||
|
navigateTo(e.target.href.slice(location.origin.length));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
router(location.pathname);
|
||||||
|
});
|
||||||
|
|
||||||
|
export { client, navigateTo }
|
18
frontend/static/js/views/AbstractAuthentifiedView.js
Normal file
18
frontend/static/js/views/AbstractAuthentifiedView.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { client, navigateTo } from "../index.js";
|
||||||
|
import AbstractRedirectView from "./AbstractRedirectView.js";
|
||||||
|
|
||||||
|
export default class extends AbstractRedirectView{
|
||||||
|
constructor(params, title) {
|
||||||
|
super(params, title, "/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
async redirect()
|
||||||
|
{
|
||||||
|
if (await client.isAuthentificate() === false)
|
||||||
|
{
|
||||||
|
navigateTo(this.redirect_url);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
16
frontend/static/js/views/AbstractNonAuthentified.js
Normal file
16
frontend/static/js/views/AbstractNonAuthentified.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { client, navigateTo } from "../index.js";
|
||||||
|
import AbstractRedirectView from "./AbstractRedirectView.js";
|
||||||
|
|
||||||
|
export default class extends AbstractRedirectView{
|
||||||
|
constructor(params, title, url) {
|
||||||
|
super(params, title, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async redirect()
|
||||||
|
{
|
||||||
|
if (await client.isAuthentificate() === false)
|
||||||
|
return 0;
|
||||||
|
navigateTo(this.redirect_url);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
15
frontend/static/js/views/AbstractRedirectView.js
Normal file
15
frontend/static/js/views/AbstractRedirectView.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { navigateTo } from "../index.js";
|
||||||
|
import AbstractView from "./AbstractView.js";
|
||||||
|
|
||||||
|
export default class extends AbstractView{
|
||||||
|
constructor(params, title, url)
|
||||||
|
{
|
||||||
|
super(params, title);
|
||||||
|
this.redirect_url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async redirect()
|
||||||
|
{
|
||||||
|
navigateTo(url);
|
||||||
|
}
|
||||||
|
}
|
20
frontend/static/js/views/AbstractView.js
Normal file
20
frontend/static/js/views/AbstractView.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export default class {
|
||||||
|
constructor(params, title) {
|
||||||
|
this.params = params;
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
async postInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async leavePage() {
|
||||||
|
}
|
||||||
|
|
||||||
|
setTitle() {
|
||||||
|
document.title = this.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHtml() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
56
frontend/static/js/views/Chat.js
Normal file
56
frontend/static/js/views/Chat.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import AbstractAuthentifiedView from "./AbstractAuthentifiedView.js";
|
||||||
|
|
||||||
|
export default class extends AbstractAuthentifiedView {
|
||||||
|
constructor(params) {
|
||||||
|
super(params, "Chat");
|
||||||
|
|
||||||
|
let url = `wss://${window.location.host}/ws/socket-server/`
|
||||||
|
|
||||||
|
this.chatSocket = new WebSocket(url)
|
||||||
|
this.chatSocket.onmessage = function(e){
|
||||||
|
let data = JSON.parse(e.data)
|
||||||
|
console.log('Data:', data)
|
||||||
|
if (data.type === 'chat') {
|
||||||
|
let messages = document.getElementById('messages')
|
||||||
|
|
||||||
|
let username = data.username === null || data.username.length <= 0 ? "NoName" : data.username;
|
||||||
|
messages.insertAdjacentHTML('beforeend', `
|
||||||
|
<div><p>${username}: ${data.message}</p></div>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async postInit() {
|
||||||
|
let form = document.getElementById('form')
|
||||||
|
form.addEventListener('submit', (e)=> {
|
||||||
|
e.preventDefault()
|
||||||
|
let message = e.target.message.value
|
||||||
|
this.chatSocket.send(JSON.stringify({
|
||||||
|
'message':message
|
||||||
|
}))
|
||||||
|
form.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async leavePage() {
|
||||||
|
this.chatSocket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHtml() {
|
||||||
|
return `
|
||||||
|
<h1>Chat</h1>
|
||||||
|
|
||||||
|
<form id="form">
|
||||||
|
<input type="text" name="message" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="messages">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
19
frontend/static/js/views/Dashboard.js
Normal file
19
frontend/static/js/views/Dashboard.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import AbstractView from "./AbstractView.js";
|
||||||
|
|
||||||
|
export default class extends AbstractView {
|
||||||
|
constructor(params) {
|
||||||
|
super(params, "Dashboard");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHtml() {
|
||||||
|
return `
|
||||||
|
<h1>Welcome back, Dom</h1>
|
||||||
|
<p>
|
||||||
|
Fugiat voluptate et nisi Lorem cillum anim sit do eiusmod occaecat irure do. Reprehenderit anim fugiat sint exercitation consequat. Sit anim laborum sit amet Lorem adipisicing ullamco duis. Anim in do magna ea pariatur et.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="/posts" data-link>View recent posts</a>.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
17
frontend/static/js/views/HomeView.js
Normal file
17
frontend/static/js/views/HomeView.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import AbstractAuthentificateView from "./AbstractAuthentifiedView.js";
|
||||||
|
|
||||||
|
export default class extends AbstractAuthentificateView {
|
||||||
|
constructor(params) {
|
||||||
|
super(params, "Home");
|
||||||
|
this.redirect_url = "/login"
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHtml() {
|
||||||
|
return `
|
||||||
|
<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="/logout" class="nav__link" data-link>Logout</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
30
frontend/static/js/views/MatchMakingView.js
Normal file
30
frontend/static/js/views/MatchMakingView.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
131
frontend/static/js/views/MeView.js
Normal file
131
frontend/static/js/views/MeView.js
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { client, navigateTo } from "../index.js";
|
||||||
|
import AbstractAuthentificateView from "./AbstractAuthentifiedView.js";
|
||||||
|
|
||||||
|
export default class extends AbstractAuthentificateView
|
||||||
|
{
|
||||||
|
constructor(params)
|
||||||
|
{
|
||||||
|
super(params, "Me");
|
||||||
|
}
|
||||||
|
|
||||||
|
async postInit()
|
||||||
|
{
|
||||||
|
if (this.fill() === null)
|
||||||
|
return;
|
||||||
|
document.getElementById("save-account-button").onclick = this.acccount_save;
|
||||||
|
document.getElementById("delete-account-button").onclick = this.account_delete_accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fill()
|
||||||
|
{
|
||||||
|
let data = await client.account.get();
|
||||||
|
|
||||||
|
if (data === null)
|
||||||
|
{
|
||||||
|
navigateTo("/login")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById("username").value = data.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete_accounts()
|
||||||
|
{
|
||||||
|
let current_password = document.getElementById("current_password").value;
|
||||||
|
|
||||||
|
let response_data = await client.account.delete(current_password);
|
||||||
|
|
||||||
|
if (response_data === null)
|
||||||
|
{
|
||||||
|
navigateTo("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
["delete", "current_password"].forEach(error_field => {
|
||||||
|
let error_display = document.getElementById(`error_${error_field}`);
|
||||||
|
if (error_display != null)
|
||||||
|
error_display.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response_data === "user deleted")
|
||||||
|
{
|
||||||
|
document.getElementById(`error_delete`).innerHTML = "OK";
|
||||||
|
navigateTo("/login")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("error_current_password").innerHTML = response_data["password"]
|
||||||
|
}
|
||||||
|
|
||||||
|
async save()
|
||||||
|
{
|
||||||
|
let username = document.getElementById("username").value;
|
||||||
|
let new_password = document.getElementById("new_password").value;
|
||||||
|
let current_password = document.getElementById("current_password").value;
|
||||||
|
|
||||||
|
let data = {};
|
||||||
|
|
||||||
|
data.username = username;
|
||||||
|
if (new_password.length != 0)
|
||||||
|
data.new_password = new_password;
|
||||||
|
let response_data = await client.account.update(data, current_password);
|
||||||
|
|
||||||
|
if (response_data === null)
|
||||||
|
{
|
||||||
|
navigateTo("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (response_data === "data has been alterate")
|
||||||
|
{
|
||||||
|
navigateTo("/me");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
["username", "new_password", "current_password"].forEach(error_field => {
|
||||||
|
let error_display = document.getElementById(`error_${error_field}`);
|
||||||
|
if (error_display != null)
|
||||||
|
error_display.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(response_data).forEach(error_field => {
|
||||||
|
let error_display = document.getElementById(`error_${error_field}`);
|
||||||
|
if (error_display != null)
|
||||||
|
error_display.innerHTML = response_data[error_field];
|
||||||
|
});
|
||||||
|
let avatar = document.getElementById("avatar");
|
||||||
|
|
||||||
|
if (avatar.files[0] !== undefined)
|
||||||
|
{
|
||||||
|
let form_data = new FormData();
|
||||||
|
form_data.append("file", avatar.files[0]);
|
||||||
|
await client.me.change_avatar(form_data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHtml()
|
||||||
|
{
|
||||||
|
return `
|
||||||
|
<link rel="stylesheet" href="static/css/me.css">
|
||||||
|
<h1>ME</h1>
|
||||||
|
<div class="account">
|
||||||
|
<h3>Account</h3>
|
||||||
|
<input type="text" placeholder="username" id="username">
|
||||||
|
<span id="error_username"></span>
|
||||||
|
<input type=password placeholder="new password" id="new_password">
|
||||||
|
<span id="error_new_password"></span>
|
||||||
|
<input type=password placeholder="current password" id="current_password">
|
||||||
|
<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 class="profile">
|
||||||
|
<h3>Profile</h3>
|
||||||
|
<input type="file" id="avatar" accept="image/png, image/jpeg">
|
||||||
|
<input type="button" value="Save profile" id="save-profile-button">
|
||||||
|
<span id="error_save"></span>
|
||||||
|
</div>
|
||||||
|
<a href="/logout" class="nav__link" data-link>Logout</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
14
frontend/static/js/views/Settings.js
Normal file
14
frontend/static/js/views/Settings.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import AbstractView from "./AbstractView.js";
|
||||||
|
|
||||||
|
export default class extends AbstractView {
|
||||||
|
constructor(params) {
|
||||||
|
super(params, "Settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHtml() {
|
||||||
|
return `
|
||||||
|
<h1>Settings</h1>
|
||||||
|
<p>Manage your privacy and configuration.</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
55
frontend/static/js/views/accounts/LoginView.js
Normal file
55
frontend/static/js/views/accounts/LoginView.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { client, navigateTo } from "../../index.js";
|
||||||
|
import AbstractNonAuthentifiedView from "../AbstractNonAuthentified.js";
|
||||||
|
|
||||||
|
async function login()
|
||||||
|
{
|
||||||
|
let username = document.getElementById("username").value;
|
||||||
|
let password = document.getElementById("password").value;
|
||||||
|
|
||||||
|
let response_data = await client.login(username, password);
|
||||||
|
|
||||||
|
if (response_data == null)
|
||||||
|
{
|
||||||
|
navigateTo("/home");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
["username", "user", "password"].forEach(error_field => {
|
||||||
|
let error_display = document.getElementById(`error_${error_field}`);
|
||||||
|
if (error_display != null)
|
||||||
|
error_display.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(response_data).forEach(error_field => {
|
||||||
|
let error_display = document.getElementById(`error_${error_field}`);
|
||||||
|
if (error_display != null)
|
||||||
|
error_display.innerHTML = response_data[error_field];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class extends AbstractNonAuthentifiedView {
|
||||||
|
constructor(params) {
|
||||||
|
super(params, "Login", "/home");
|
||||||
|
}
|
||||||
|
|
||||||
|
async postInit()
|
||||||
|
{
|
||||||
|
document.getElementById("button").onclick = login;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHtml() {
|
||||||
|
return `
|
||||||
|
<div class=form>
|
||||||
|
<label>Login</label>
|
||||||
|
<link rel="stylesheet" href="static/css/accounts/login.css">
|
||||||
|
<input type="text" id="username" placeholder="username">
|
||||||
|
<span id="error_username"></span>
|
||||||
|
<input type="password" id="password" placeholder="password">
|
||||||
|
<span id="error_password"></span>
|
||||||
|
<input type="button" value="login" id="button">
|
||||||
|
<span id="error_user"></span>
|
||||||
|
<a href="/register" class="nav__link" data-link>Register</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
11
frontend/static/js/views/accounts/LogoutView.js
Normal file
11
frontend/static/js/views/accounts/LogoutView.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { client, navigateTo } from "../../index.js";
|
||||||
|
import AbstractAuthentifiedView from "../AbstractAuthentifiedView.js";
|
||||||
|
|
||||||
|
export default class extends AbstractAuthentifiedView
|
||||||
|
{
|
||||||
|
constructor(params) {
|
||||||
|
super(params, "Logout");
|
||||||
|
client.logout();
|
||||||
|
navigateTo("/login")
|
||||||
|
}
|
||||||
|
}
|
55
frontend/static/js/views/accounts/RegisterView.js
Normal file
55
frontend/static/js/views/accounts/RegisterView.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { client, navigateTo } from "../../index.js";
|
||||||
|
import AbstractNonAuthentifiedView from "../AbstractNonAuthentified.js";
|
||||||
|
|
||||||
|
async function register()
|
||||||
|
{
|
||||||
|
let username = document.getElementById("username").value;
|
||||||
|
let password = document.getElementById("password").value;
|
||||||
|
|
||||||
|
let response_data = await client.account.create(username, password);
|
||||||
|
|
||||||
|
if (response_data == null)
|
||||||
|
{
|
||||||
|
navigateTo("/home");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
["username", "user", "password"].forEach(error_field => {
|
||||||
|
let error_display = document.getElementById(`error_${error_field}`);
|
||||||
|
if (error_display != null)
|
||||||
|
error_display.innerHTML = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(response_data).forEach(error_field => {
|
||||||
|
let error_display = document.getElementById(`error_${error_field}`);
|
||||||
|
if (error_display != null)
|
||||||
|
error_display.innerHTML = response_data[error_field];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class extends AbstractNonAuthentifiedView {
|
||||||
|
constructor(params) {
|
||||||
|
super(params, "Register", "/home");
|
||||||
|
}
|
||||||
|
|
||||||
|
async postInit()
|
||||||
|
{
|
||||||
|
document.getElementById("button").onclick = register;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHtml() {
|
||||||
|
return `
|
||||||
|
<div class=form>
|
||||||
|
<label>Register</label>
|
||||||
|
<link rel="stylesheet" href="static/css/accounts/register.css">
|
||||||
|
<input type="text" id="username" placeholder="username">
|
||||||
|
<span id="error_username"></span>
|
||||||
|
<input type="password" id="password" placeholder="password">
|
||||||
|
<span id="error_password"></span>
|
||||||
|
<input type="button" value="register" id="button">
|
||||||
|
<span id="error_user"></span>
|
||||||
|
<a href="/login" class="nav__link" data-link>Login</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
29
frontend/static/js/views/profiles/ProfilePageView.js
Normal file
29
frontend/static/js/views/profiles/ProfilePageView.js
Normal 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
40
frontend/static/js/views/profiles/ProfilesView.js
Normal file
40
frontend/static/js/views/profiles/ProfilesView.js
Normal 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
21
frontend/templates/index.html
Normal file
21
frontend/templates/index.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Single Page App</title>
|
||||||
|
<link rel="stylesheet" href="{% static 'css/index.css' %}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="/" class="nav__link" data-link>Dashboard</a>
|
||||||
|
<a href="/profiles" class="nav__link" data-link>Profiles</a>
|
||||||
|
<a href="/login" class="nav__link" data-link>Login</a>
|
||||||
|
<a href="/register" class="nav__link" data-link>Register</a>
|
||||||
|
<a href="/chat" class="nav__link" data-link>Chat</a>
|
||||||
|
</nav>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="{% static 'js/index.js' %}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
3
frontend/tests.py
Normal file
3
frontend/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
7
frontend/urls.py
Normal file
7
frontend/urls.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from django.urls import path, re_path
|
||||||
|
|
||||||
|
from .views import index_view
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
re_path(r'^', index_view ,name="index"),
|
||||||
|
]
|
7
frontend/views.py
Normal file
7
frontend/views.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
|
|
||||||
|
|
||||||
|
def index_view(req):
|
||||||
|
return render(req, 'index.html');
|
0
games/__init__.py
Normal file
0
games/__init__.py
Normal file
3
games/admin.py
Normal file
3
games/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
games/apps.py
Normal file
6
games/apps.py
Normal 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
14
games/models.py
Normal 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
3
games/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
games/views.py
Normal file
3
games/views.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
22
manage.py
Executable file
22
manage.py
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trancendence.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
0
matchmaking/__init__.py
Normal file
0
matchmaking/__init__.py
Normal file
3
matchmaking/admin.py
Normal file
3
matchmaking/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
matchmaking/apps.py
Normal file
6
matchmaking/apps.py
Normal 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
48
matchmaking/consumers.py
Normal 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
3
matchmaking/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
6
matchmaking/routing.py
Normal file
6
matchmaking/routing.py
Normal 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
3
matchmaking/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
matchmaking/views.py
Normal file
3
matchmaking/views.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
0
profiles/__init__.py
Normal file
0
profiles/__init__.py
Normal file
6
profiles/admin.py
Normal file
6
profiles/admin.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import ProfileModel
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
admin.site.register(ProfileModel)
|
6
profiles/apps.py
Normal file
6
profiles/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ProfilesConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'profiles'
|
20
profiles/models.py
Normal file
20
profiles/models.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
def upload_to(instance, filename: str):
|
||||||
|
return f"./profiles/static/avatars/{instance.pk}.{filename.split('.')[1]}"
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
class ProfileModel(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
avatar_url = models.ImageField(upload_to=upload_to, default="./profiles/static/avatars/default.avif") #blank=True, null=True)
|
||||||
|
|
||||||
|
@receiver(post_save, sender=User)
|
||||||
|
def on_user_created(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
profile: ProfileModel = ProfileModel.objects.create(pk = instance.pk, user = instance)
|
||||||
|
profile.save()
|
11
profiles/serializers.py
Normal file
11
profiles/serializers.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from .models import ProfileModel
|
||||||
|
|
||||||
|
class ProfileSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
username = serializers.ReadOnlyField(source='user.username')
|
||||||
|
avatar_url = serializers.ImageField(required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ProfileModel
|
||||||
|
fields = ["username", "avatar_url", "user_id"]
|
BIN
profiles/static/avatars/default.avif
Normal file
BIN
profiles/static/avatars/default.avif
Normal file
Binary file not shown.
18
profiles/tests.py
Normal file
18
profiles/tests.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.http import HttpResponse, HttpRequest
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
class ProfileTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user: User = User.objects.create(username='bozo', password='password')
|
||||||
|
self.user.save()
|
||||||
|
self.expected_response = {"name": "bozo",
|
||||||
|
"title": ""}
|
||||||
|
self.url = "/api/profiles/"
|
||||||
|
|
||||||
|
def test_profile_create_on_user_created(self):
|
||||||
|
response: HttpResponse = self.client.get(self.url + str(self.user.pk))
|
||||||
|
response_dict: dict = eval(response.content)
|
||||||
|
self.assertDictEqual(self.expected_response, response_dict)
|
||||||
|
|
11
profiles/urls.py
Normal file
11
profiles/urls.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
|
from . import viewsets
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("<int:pk>", viewsets.ProfileViewSet.as_view({'get': 'retrieve', 'patch': 'partial_update'}), name="profile_page"),
|
||||||
|
path("", viewsets.ProfileViewSet.as_view({'get': 'list'}), name="profiles_list"),
|
||||||
|
#path("me", viewsets.ProfileViewSet.as_view(), name="my_profile_page"),
|
||||||
|
] + static("/static/avatars/", document_root="./avatars")
|
44
profiles/viewsets.py
Normal file
44
profiles/viewsets.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from rest_framework import permissions
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser
|
||||||
|
from rest_framework import permissions, status
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
|
from .serializers import ProfileSerializer
|
||||||
|
from .models import ProfileModel
|
||||||
|
|
||||||
|
class ProfileViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = ProfileModel.objects.all
|
||||||
|
serializer_class = ProfileSerializer
|
||||||
|
parser_classes = (MultiPartParser, FormParser)
|
||||||
|
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||||
|
|
||||||
|
def retrieve(self, request: HttpRequest, pk=None):
|
||||||
|
instance = ProfileModel.objects.get(pk=pk)
|
||||||
|
instance.avatar_url.name = instance.avatar_url.name[instance.avatar_url.name.find("static") - 1:]
|
||||||
|
return Response(self.serializer_class(instance).data,
|
||||||
|
status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def list(self, request: HttpRequest):
|
||||||
|
serializer = ProfileSerializer(self.queryset(), many=True)
|
||||||
|
for profile in serializer.data:
|
||||||
|
profile["avatar_url"] = profile["avatar_url"][profile["avatar_url"].find("static") - 1:]
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(user=self.request.user)
|
||||||
|
|
||||||
|
def perform_update(self, serializer):
|
||||||
|
query: QuerySet = ProfileModel.objects.filter(pk=self.request.user.pk)
|
||||||
|
if (not query.exists()):
|
||||||
|
return Response("profile not found", status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
profile: ProfileModel = ProfileModel.objects.get(pk=self.request.user.pk)
|
||||||
|
avatar = self.request.data.get("file", None)
|
||||||
|
if (avatar is not None):
|
||||||
|
if (profile.avatar_url.name != "./profiles/static/avatars/default.avif"):
|
||||||
|
profile.avatar_url.storage.delete(profile.avatar_url.name)
|
||||||
|
profile.avatar_url = avatar
|
||||||
|
profile.save()
|
10
requirements.txt
Normal file
10
requirements.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
asgiref==3.7.2
|
||||||
|
Django==4.2.6
|
||||||
|
django-cors-headers==4.3.0
|
||||||
|
django-restapi==0.1.3
|
||||||
|
djangorestframework==3.14.0
|
||||||
|
install==1.3.5
|
||||||
|
pytz==2023.3.post1
|
||||||
|
sqlparse==0.4.4
|
||||||
|
channels==4.0.0
|
||||||
|
daphne
|
0
trancendence/__init__.py
Normal file
0
trancendence/__init__.py
Normal file
30
trancendence/asgi.py
Normal file
30
trancendence/asgi.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for trancendence project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||||
|
from channels.auth import AuthMiddlewareStack
|
||||||
|
|
||||||
|
import chat.routing
|
||||||
|
import matchmaking.routing
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trancendence.settings')
|
||||||
|
|
||||||
|
application = ProtocolTypeRouter({
|
||||||
|
'http':get_asgi_application(),
|
||||||
|
'websocket':AuthMiddlewareStack(
|
||||||
|
URLRouter(
|
||||||
|
chat.routing.websocket_urlpatterns +
|
||||||
|
matchmaking.routing.websocket_urlpatterns
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
153
trancendence/settings.py
Normal file
153
trancendence/settings.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
"""
|
||||||
|
Django settings for trancendence project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 4.2.6.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'django-insecure-18!@88-wm-!skec9^n-85n(f$my^#mh3!#@f=_e@=*arh_yyjj'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
|
CORS_ORIGIN_ALLOW_ALL = False
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS = ["https://django.chauvet.pro"]
|
||||||
|
|
||||||
|
CORS_ORIGIN_WHITELIST = (
|
||||||
|
'http://localhost:8000',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'channels',
|
||||||
|
'daphne',
|
||||||
|
|
||||||
|
'matchmaking.apps.MatchmakingConfig',
|
||||||
|
'games.apps.GamesConfig',
|
||||||
|
'accounts.apps.AccountsConfig',
|
||||||
|
'profiles.apps.ProfilesConfig',
|
||||||
|
'frontend.apps.FrontendConfig',
|
||||||
|
|
||||||
|
'corsheaders',
|
||||||
|
'rest_framework',
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
]
|
||||||
|
|
||||||
|
ASGI_APPLICATION = 'trancendence.asgi.application'
|
||||||
|
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
'default' :{
|
||||||
|
'BACKEND':'channels.layers.InMemoryChannelLayer'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'trancendence.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'trancendence.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = 'static/'
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
25
trancendence/urls.py
Normal file
25
trancendence/urls.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
URL configuration for trancendence project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/4.2/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('api/profiles/', include('profiles.urls')),
|
||||||
|
path('api/accounts/', include('accounts.urls')),
|
||||||
|
path('', include('frontend.urls')),
|
||||||
|
]
|
16
trancendence/wsgi.py
Normal file
16
trancendence/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for trancendence project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trancendence.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
Loading…
Reference in New Issue
Block a user