notice but without the t

This commit is contained in:
AdrienLSH 2024-04-25 15:45:32 +02:00
parent dbb8e07d7d
commit 5f58b65a34
29 changed files with 258 additions and 371 deletions

View File

@ -4,7 +4,7 @@ import { Profiles } from "./Profiles.js";
import { Channels } from './chat/Channels.js';
import { MyProfile } from "./MyProfile.js";
import { Tourmanents } from "./tournament/Tournaments.js";
import { Notice } from "./chat/Notice.js";
import Notice from "./notice/Notice.js";
import { Channel } from "./chat/Channel.js";
import LanguageManager from './LanguageManager.js';
@ -217,7 +217,7 @@ class Client
{
this.me = new MyProfile(this);
await this.me.init();
this.notice.connect();
this.notice.start();
document.getElementById('navbarLoggedOut').classList.add('d-none');
document.getElementById('navbarLoggedIn').classList.remove('d-none');
document.getElementById('navbarDropdownButton').innerHTML = this.me.username;
@ -226,7 +226,7 @@ class Client
else
{
this.me = undefined;
this.notice.disconnect();
this.notice.stop();
document.getElementById('navbarLoggedOut').classList.remove('d-none');
document.getElementById('navbarLoggedIn').classList.add('d-none');
document.getElementById('navbarDropdownButton').innerHTML = 'Me';

View File

@ -40,26 +40,26 @@ class MyProfile extends Profile
async getBlockedUsers() {
const response = await this.client._get('/api/profiles/block');
const data = await response.json();
data.forEach(profileData => this.blockedUsers.push(new Profile(this.client, profileData.username, profileData.user_id, profileData.avatar)));
data.forEach(profileData => this.blockedUsers.push(new Profile(this.client, profileData.username, profileData.id, profileData.avatar)));
}
async getFriends() {
const response = await this.client._get('/api/profiles/friends');
const data = await response.json();
data.forEach(profileData => this.friendList.push(new Profile(this.client, profileData.username, profileData.user_id, profileData.avatar)));
data.forEach(profileData => this.friendList.push(new Profile(this.client, profileData.username, profileData.id, profileData.avatar)));
}
async getIncomingFriendRequests() {
const response = await this.client._get('/api/profiles/incoming_friend_requests');
const data = await response.json();
data.forEach(profileData => this.incomingFriendRequests.push(
new Profile(this.client, profileData.username, profileData.user_id, profileData.avatar)
new Profile(this.client, profileData.username, profileData.id, profileData.avatar)
));
}
async getOutgoingFriendRequests() {
const response = await this.client._get('/api/profiles/outgoing_friend_requests');
const data = await response.json();
data.forEach(profileData => this.outgoingFriendRequests.push(
new Profile(this.client, profileData.username, profileData.user_id, profileData.avatar)
new Profile(this.client, profileData.username, profileData.id, profileData.avatar)
));
}

View File

@ -55,17 +55,17 @@ export class Profile extends AExchangeable
return response.status;
let response_data = await response.json();
this.id = response_data.user_id;
this.id = response_data.id;
this.username = response_data.username;
this.avatar = response_data.avatar;
if (!this.client.me || this.client.me.id === this.id)
return;
this.isFriend = this.client.me._isFriend(this);
this.isBlocked = this.client.me._isBlocked(this);
this.hasIncomingRequest = this.client.me._hasIncomingRequestFrom(this);
this.hasOutgoingRequest = this.client.me._hasOutgoingRequestTo(this);
this.isFriend = this.client.me._isFriend(this);
}
/**

View File

@ -24,7 +24,7 @@ class Profiles
let profiles = [];
response_data.forEach((profile) => {
profiles.push(new Profile(this.client, profile.username, profile.user_id, profile.avatar));
profiles.push(new Profile(this.client, profile.username, profile.id, profile.avatar));
});
return profiles;
}

View File

@ -1,308 +0,0 @@
import { navigateTo } from "../../index.js";
import {createNotification} from "../../utils/noticeUtils.js";
class Notice {
constructor(client) {
this.client = client;
this.data = {};
// users online, invited by ..., asked by ..., asker to ...
let data_variable = ["online", "invited", "asked", "asker"];
for (let i in data_variable)
this.data[data_variable[i]] = [];
//this.connect();
}
async connect() {
return
let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/chat/notice`;
this.chatSocket = new WebSocket(url);
this.chatSocket.onmessage = (event) =>{
let send = JSON.parse(event.data);
//console.log("notice: ", send);
try {
this["receive_" + send.type](send);
}
catch (error) {
console.log("receive_" + send.type + ": Function not found");
}
};
this.chatSocket.onopen = (event) => {
this.getOnlineUser();
this.ask_friend();
};
}
async disconnect() {
if (this.chatSocket == undefined)
return ;
this.chatSocket.close();
}
async reconnect() {
this.disconnect();
this.connect();
}
async accept_invite(invitedBy) {
this.sendRequest({
type: "accept_invite",
targets: [invitedBy],
});
}
async receive_accept_invite(send) {
this.data.invited = send.invites;
let id_game = send.id_game;
navigateTo("/game/" + id_game);
}
async refuse_invite(invitedBy) {
this.sendRequest({
type: "refuse_invite",
targets: [invitedBy],
});
}
async receive_refuse_invite(send) {
this.data.invited = send.invites;
if (send.author_id == this.client.me.id) {
if (this.rewrite_invite !== undefined)
this.rewrite_invite();
}
else {
let sender = await this.client.profiles.getProfileId(send.author_id);
createNotification(sender.username + " refuse your invitation");
}
}
async send_invite(id_inviteds) {
this.sendRequest({
type: "invite",
targets: id_inviteds,
time: new Date().getTime(),
});
}
async receive_invite(send) {
if (this.client.me == undefined)
return ;
let content = send.invites;
if (send.author_id == this.client.me.id) {
if (send.status == 200) {
for (let target in send.targets)
return createNotification("Invitation send");
}
else if (send.status == 444)
return createNotification("User not connected");
else if (send.status == 409)
return createNotification("Already invited");
}
else {
// Regarder qu'il est bien invité par l'auteur
// Et qu'il n'est pas déjà invité
if (!content.includes(send.author_id) ||
this.data.invited.includes(send.author_id))
return;
this.data.invited = content;
let sender = await this.client.profiles.getProfileId(send.author_id);
createNotification("Invitation received by " + sender.username);
if (this.rewrite_invite !== undefined)
this.rewrite_invite();
}
}
async getOnlineUser() {
this.online_users = {};
this.sendRequest({
type: "online_users",
targets: [],
time: new Date().getTime(),
});
}
async receive_online_users(send) {
let content = send.online;
if (content !== undefined) {
if (this.data.online.length > 0) {
// get all disconnect user
//let disconnects = this.data["online"].filter(id => !Object.keys(content).includes(id));
let disconnects = [];
for (const [key, value] of Object.entries(this.data.online)) {
if (content[key] == "red" && value == "green")
disconnects.push(key);
}
// delete invite
this.data.invited = this.data.invited.filter(id => !disconnects.includes(id));
//console.log(this.data["invited"]);
}
this.data.online = content;
if (this.rewrite_usernames !== undefined)
this.rewrite_usernames();
}
}
async ask_friend(user_id=undefined) {
this.sendRequest({
type: "ask_friend",
targets: [user_id],
time: new Date().getTime(),
});
}
async receive_ask_friend(send) {
let my_id = (this.client.me && this.client.me.id) || send.author_id;
if (send.author_id == my_id) {
if (send.status == 400)
createNotification("Friend ask error");
else if (send.status == 409)
createNotification("Already asked friend");
}
//if (!send.asked.includes(send.author_id) ||
//this.data["asked"].includes(send.author_id))
//return;
//if (!send.asker.includes(send.author_id) ||
//this.data["asker"].includes(send.author_id))
//return;
this.data.asked = send.asked;
this.data.asker = send.asker;
if (send.author_id != my_id) {
let sender = await this.client.profiles.getProfileId(send.author_id);
if (this.data.asker.includes(send.author_id))
createNotification(sender.username + " ask you as friend");
if (this.rewrite_profile !== undefined)
await this.rewrite_profile();
}
}
async remove_friend(user_id) {
this.sendRequest({
type: "remove_friend",
targets: [user_id],
time: new Date().getTime(),
});
}
async receive_remove_friend(send) {
if (send.author_id == this.client.me.id) {
if (send.status == 400)
createNotification("Error remove Friend");
else if (send.status == 409)
createNotification("Not friend, wtf");
}
if (this.rewrite_profile !== undefined)
await this.rewrite_profile();
this.receive_online_users(send);
}
async accept_friend(user_id) {
this.sendRequest({
type: "accept_friend",
targets: [user_id],
time: new Date().getTime(),
});
}
async receive_accept_friend(send) {
this.data.asked = send.asked;
this.data.asker = send.asker;
let sender = await this.client.profiles.getProfileId(send.author_id);
if (send.author_id == this.client.me.id) {
if (send.status == 400)
createNotification("Error accept Friend");
else if (send.status == 404)
createNotification("Not found request Friend");
else if (send.status == 409)
createNotification("Already Friend, wtf");
}
else {
createNotification(sender.username + " accept your friend request");
}
if (this.rewrite_profile !== undefined)
await this.rewrite_profile();
this.receive_online_users(send);
}
async refuse_friend(user_id) {
this.sendRequest({
type: "refuse_friend",
targets: [user_id],
time: new Date().getTime(),
});
}
async receive_refuse_friend(send) {
this.data.asked = send.asked;
this.data.asker = send.asker;
let sender = await this.client.profiles.getProfileId(send.author_id);
if (send.author_id == this.client.me.id) {
if (send.status == 400)
createNotification("Error refuse Friend");
else if (send.status == 404)
createNotification("Not found request Friend");
else if (send.status == 409)
createNotification("Already Friend, WTF");
}
if (this.rewrite_profile !== undefined)
await this.rewrite_profile();
}
async sendRequest(content) {
if (this.chatSocket == undefined)
return;
this.chatSocket.send(JSON.stringify(content));
}
}
export {Notice};

View File

@ -0,0 +1,83 @@
import {Client} from '../Client.js';
import {createNotification} from '../../utils/noticeUtils.js'
import { client, lastView } from '../../index.js';
import { Profile } from '../Profile.js';
import ProfilePageView from '../../views/ProfilePageView.js';
export default class Notice {
/**
* @param {Client} client
*/
constructor(client) {
/**
* @type {Client}
*/
this.client = client;
this.url = location.origin.replace('http', 'ws') + '/ws/notice';
}
start() {
this._socket = new WebSocket(this.url);
this._socket.onclose = _ => this._socket = undefined;
this._socket.onmessage = message => {
const data = JSON.parse(message.data);
console.log(data)
if (data.type === 'friend_request') {
this.friend_request(data.author);
} else if (data.type === 'new_friend') {
this.new_friend(data.friend);
} else if (data.type === 'friend_removed') {
this.friend_removed(data.friend);
} else if (data.type === 'friend_request_canceled') {
this.friend_request_canceled(data.author);
}
};
}
stop() {
if (this._socket) {
this._socket.close();
this._socket = undefined;
}
}
friend_request(author) {
console.log('hey')
client.me.incomingFriendRequests.push(new Profile(author.username, author.id, author.avatar));
createNotification('Friend Request', `<strong>${author.username}</strong> sent you a friend request.`);
if (lastView instanceof ProfilePageView && lastView.profile.id === author.id) {
lastView.profile.hasIncomingRequest = true;
lastView.loadFriendshipStatus();
}
}
new_friend(friend) {
client.me.friendList.push(new Profile(friend.username, friend.id, friend.avatar));
createNotification('New Friend', `<strong>${friend.username}</strong> accepted your friend request.`);
if (lastView instanceof ProfilePageView && lastView.profile.id === friend.id) {
lastView.profile.isFriend = true;
lastView.profile.hasIncomingRequest = false;
lastView.profile.hasOutgoingRequest = false;
lastView.loadFriendshipStatus();
}
}
friend_removed(exFriend) {
client.me.friendList = client.me.friendList.filter(friend => friend.id !== exFriend.id);
if (lastView instanceof ProfilePageView && lastView.profile.id === exFriend.id) {
lastView.profile.isFriend = false;
lastView.loadFriendshipStatus();
}
}
friend_request_canceled(author) {
client.me.incomingFriendRequests = client.me.incomingFriendRequests.filter(user => user.id !== author.id);
if (lastView instanceof ProfilePageView && lastView.profile.id === author.id) {
lastView.profile.hasIncomingRequest = false;
lastView.loadFriendshipStatus();
}
}
}

View File

@ -167,4 +167,4 @@ document.addEventListener("DOMContentLoaded", async () => {
document.querySelector('a[href=\'' + location.pathname + '\']')?.classList.add('active');
});
export { client, lang, navigateTo, reloadView };
export { client, lang, lastView, navigateTo, reloadView };

View File

@ -1,22 +1,15 @@
export function createNotification(text, timer = 3000) {
export function createNotification(title = 'New notification', content, delay = 3000) {
if (!createNotification.templateToast) {
createNotification.templateToast = new DOMParser().parseFromString(`
<div class='toast' role='alert' data-bs-delay='${timer}'>
<div class='toast-header'>
<strong class='me-auto'>Notification</strong>
<button type='button' class='btn-close' data-bs-dismiss='toast'></button>
</div>
<div class='toast-body'>
</div>
</div>
`, 'text/html')
.querySelector('body')
.firstChild;
}
const toastElement = createNotification.templateToast.cloneNode(true);
toastElement.getElementsByClassName('toast-body')[0].innerHTML = text;
const toastElement = document.createElement('div');
toastElement.classList.add('toast');
toastElement.role = 'alert';
toastElement.setAttribute('data-bs-delay', delay);
toastElement.innerHTML =
`<div class='toast-header'>
<strong class='me-auto'>${title}</strong>
<button type='button' class='btn-close' data-bs-dismiss='toast'></button>
</div>
<div class='toast-body'>${content}</div>`
toastElement.addEventListener('hidden.bs.toast', e => e.target.remove());
new bootstrap.Toast(toastElement).show();

View File

@ -16,23 +16,15 @@ export default class extends AbstractView {
if (!this.profile)
return 404;
if (this.profile.id === client.me.id)
return;
const addFriendButton = document.getElementById('addFriendButton'),
removeFriendButton = document.getElementById('removeFriendButton'),
blockButton = document.getElementById('blockButton'),
unblockButton = document.getElementById('unblockButton');
if (this.profile.hasIncomingRequest) {
addFriendButton.classList.remove('d-none');
addFriendButton.innerHTML = 'Accept Request';
} else if (this.profile.hasOutgoingRequest) {
removeFriendButton.classList.remove('d-none');
removeFriendButton.classList.replace('btn-danger', 'btn-secondary');
removeFriendButton.innerHTML = 'Cancel Request'
} else if (this.profile.isFriend)
removeFriendButton.classList.remove('d-none');
else
addFriendButton.classList.remove('d-none');
this.loadFriendshipStatus();
if (this.profile.isBlocked)
unblockButton.classList.remove('d-none');
else
@ -44,6 +36,31 @@ export default class extends AbstractView {
blockButton.onclick = _ => this.blockUser();
}
loadFriendshipStatus() {
const addFriendButton = document.getElementById('addFriendButton'),
removeFriendButton = document.getElementById('removeFriendButton');
if (this.profile.hasIncomingRequest) {
removeFriendButton.classList.add('d-none');
addFriendButton.classList.remove('d-none');
addFriendButton.innerHTML = 'Accept Request';
} else if (this.profile.hasOutgoingRequest) {
addFriendButton.classList.add('d-none');
removeFriendButton.classList.remove('d-none');
removeFriendButton.classList.replace('btn-danger', 'btn-secondary');
removeFriendButton.innerHTML = 'Cancel Request';
} else if (this.profile.isFriend) {
addFriendButton.classList.add('d-none');
removeFriendButton.classList.remove('d-none');
removeFriendButton.classList.replace('btn-secondary', 'btn-danger');
removeFriendButton.innerHTML = 'Remove Friend';
} else {
addFriendButton.innerHTML = 'Add Friend';
removeFriendButton.classList.add('d-none');
addFriendButton.classList.remove('d-none');
}
}
async getHtml() {
this.profile = await client.profiles.getProfile(this.username);

View File

@ -10,7 +10,6 @@ export default class extends AbstractAuthenticatedView
async postInit() {
await client.logout();
await client.notice.disconnect();
navigateTo(this.lastPageUrl);
}
}

View File

@ -45,9 +45,8 @@
</div>
</div>
</nav>
<div class='toast-container position-fixed end-0 p-3 pt-0' id='toastContainer'></div>
<div id="app" class="m-3"></div>
<div class='toast-container position-fixed bottom-0 end-0 p-3' id='toastContainer'>
</div>
<script type="module" src="{% static 'js/index.js' %}"></script>
<script src="{% static 'js/bootstrap/bootstrap.bundle.min.js' %}"></script>
</body>

View File

@ -4,7 +4,7 @@ from django.contrib.auth.models import User
from django.db.models import QuerySet
from .models import GameModel
from profiles.serializers.ProfileSerializer import ProfileSerializer
from profiles.serializers import ProfileSerializer
class GameSerializer(serializers.ModelSerializer):

0
notice/__init__.py Normal file
View File

3
notice/admin.py Normal file
View File

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

6
notice/apps.py Normal file
View File

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

68
notice/consumers.py Normal file
View File

@ -0,0 +1,68 @@
from __future__ import annotations
import json
from channels.generic.websocket import WebsocketConsumer
from django.contrib.auth.models import User
from profiles.serializers import ProfileSerializer
from profiles.models import ProfileModel
from .models import NoticeModel
class NoticeManager:
def __init__(self):
self._list: list[NoticeConsumer] = []
def add(self, consumer: NoticeConsumer):
self._list.append(consumer)
unsend_notices = NoticeModel.objects.filter(user=consumer.user)
for notice in unsend_notices:
self.notify_user(consumer.user, json_data=notice.data)
notice.delete()
def remove(self, consumer: NoticeConsumer):
self._list.remove(consumer)
def get_consumer_by_user(self, user: User):
for consumer in self._list:
if consumer.user == user:
return consumer
def notify_user(self, user: User, data: dict = None, json_data: str = None):
consumer = self.get_consumer_by_user(user)
data_str: str = json.dumps(data) if json_data is None else json_data
if consumer:
consumer.send(data_str)
else:
NoticeModel(user=user, data=data_str).save()
def notify_friend_request(self, user: User, friend: ProfileModel):
self.notify_user(user, {'type': 'friend_request', 'author': ProfileSerializer(friend).data})
def notify_friend_request_canceled(self, user: User, friend: ProfileModel):
self.notify_user(user, {'type': 'friend_request_canceled', 'author': ProfileSerializer(friend).data})
def notify_new_friend(self, user: User, friend: ProfileModel):
self.notify_user(user, {'type': 'new_friend', 'friend': ProfileSerializer(friend).data})
def notify_friend_removed(self, user: User, friend: ProfileModel):
self.notify_user(user, {'type': 'friend_removed', 'friend': ProfileSerializer(friend).data})
notice_manager = NoticeManager()
class NoticeConsumer(WebsocketConsumer):
def connect(self):
self.user: User = self.scope['user']
if not self.user.is_authenticated:
self.close()
return
self.accept()
notice_manager.add(self)
def disconnect(self, code):
notice_manager.remove(self)
super().disconnect(code)

7
notice/models.py Normal file
View File

@ -0,0 +1,7 @@
from django.db.models import Model, ForeignKey, CharField, CASCADE
from django.contrib.auth.models import User
class NoticeModel(Model):
user = ForeignKey(User, on_delete=CASCADE)
data = CharField(max_length=100)

7
notice/routing.py Normal file
View File

@ -0,0 +1,7 @@
from django.urls import re_path
from .consumers import NoticeConsumer
websocket_urlpatterns = [
re_path(r'ws/notice$', NoticeConsumer.as_asgi()),
]

3
notice/tests.py Normal file
View File

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

3
notice/views.py Normal file
View File

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

View File

@ -3,7 +3,7 @@ from django.utils.translation import gettext as _
from rest_framework import serializers
from ..models import ProfileModel
from .models import ProfileModel
class ProfileSerializer(serializers.ModelSerializer):
@ -13,7 +13,7 @@ class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = ProfileModel
fields = ["username", "avatar", "user_id"]
fields = ["username", "avatar", "id"]
def validate_avatar(self, value):
'''

View File

@ -8,7 +8,7 @@ from django.utils.translation import gettext as _
from django.shortcuts import get_object_or_404
from ..models import BlockModel, ProfileModel
from ..serializers.ProfileSerializer import ProfileSerializer
from ..serializers import ProfileSerializer
class GetBlocksView(APIView):

View File

@ -7,7 +7,8 @@ from django.utils.translation import gettext as _
from django.shortcuts import get_object_or_404
from ..models import ProfileModel, FriendRequestModel
from ..serializers.ProfileSerializer import ProfileSerializer
from ..serializers import ProfileSerializer
from notice.consumers import notice_manager
class GetFriendsView(APIView):
@ -41,9 +42,11 @@ class EditFriendView(APIView):
incoming_request = user_profile.get_received_friend_request_from(friend_profile)
if incoming_request:
incoming_request.accept()
return Response(_('Friendship succssfully created.'), status.HTTP_201_CREATED)
notice_manager.notify_new_friend(friend_profile.user, user_profile)
return Response(_('Friendship successfully created.'), status.HTTP_201_CREATED)
FriendRequestModel(author=user_profile, target=friend_profile).save()
notice_manager.notify_friend_request(friend_profile.user, user_profile)
return Response(_('Friend request sent.'), status.HTTP_200_OK)
def delete(self, request, pk=None):
@ -53,13 +56,15 @@ class EditFriendView(APIView):
outgoing_request = user_profile.get_outgoing_friend_request_to(friend_profile)
if outgoing_request:
outgoing_request.delete()
notice_manager.notify_friend_request_canceled(friend_profile.user, user_profile)
return Response(_('Friend request cancelled.'))
if not user_profile.is_friend(friend_profile):
return Response(_('You are not friend with this user.'), status.HTTP_400_BAD_REQUEST)
user_profile.delete_friend(friend_profile)
return Response(_('Friendship succssfully deleted.'), status.HTTP_201_CREATED)
notice_manager.notify_friend_removed(friend_profile.user, user_profile)
return Response(_('Friendship successfully deleted.'), status.HTTP_201_CREATED)
class GetIncomingFriendRequestView(APIView):

View File

@ -3,7 +3,7 @@ from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.authentication import SessionAuthentication
from ..serializers.ProfileSerializer import ProfileSerializer
from ..serializers import ProfileSerializer
from ..models import ProfileModel

View File

@ -5,7 +5,7 @@ from rest_framework import permissions
from rest_framework import viewsets
from rest_framework.response import Response
from ..serializers.ProfileSerializer import ProfileSerializer
from ..serializers import ProfileSerializer
from ..models import ProfileModel

View File

@ -9,7 +9,7 @@ from django.utils.translation import gettext as _
from games.models import GameModel
from games.serializers import GameSerializer
from profiles.models import ProfileModel
from profiles.serializers.ProfileSerializer import ProfileSerializer
from profiles.serializers import ProfileSerializer
from .models import TournamentModel
import json

View File

@ -2,7 +2,7 @@ from rest_framework import serializers
from .models import TournamentModel, TournamentGameModel
from profiles.serializers.ProfileSerializer import ProfileSerializer
from profiles.serializers import ProfileSerializer
from games.serializers import GameSerializer
nb_participants = [2 ** i for i in range(2, 6)]

View File

@ -15,20 +15,21 @@ import chat.routing
import matchmaking.routing
import tournament.routing
import games.routing
import notice.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(
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns +
matchmaking.routing.websocket_urlpatterns +
tournament.routing.websocket_urlpatterns +
games.routing.websocket_urlpatterns
games.routing.websocket_urlpatterns +
notice.routing.websocket_urlpatterns
)
)
})

View File

@ -51,6 +51,7 @@ INSTALLED_APPS = [
'profiles.apps.ProfilesConfig',
'frontend.apps.FrontendConfig',
'chat.apps.ChatConfig',
'notice.apps.NoticeConfig',
'corsheaders',
'rest_framework',