diff --git a/.gitignore b/.gitignore index a70c028..018df26 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,10 @@ cloud/migrations/* !cloud/migrations/__init__.py cloud/__pycache__/* +chat/migrations/* +!chat/migrations/__init__.py +chat/__pycache__/* + notificsys/migrations/* !notificsys/migrations/__init__.py notificsys/__pycache__/* diff --git a/api/views.py b/api/views.py index 1b9af62..2b8176e 100644 --- a/api/views.py +++ b/api/views.py @@ -7,6 +7,8 @@ from rest_framework import serializers from .serializers import StandardsSerializer from rest_framework.decorators import api_view, permission_classes from rest_framework import status +from rest_framework.authentication import SessionAuthentication, BasicAuthentication, TokenAuthentication +from rest_framework.decorators import authentication_classes class HelloView(APIView): permission_classes = (IsAuthenticated,) # <-- And here diff --git a/chat/__init__.py b/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chat/admin.py b/chat/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/chat/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/chat/apps.py b/chat/apps.py new file mode 100644 index 0000000..8ebb9f0 --- /dev/null +++ b/chat/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ChatConfig(AppConfig): + name = 'chat' diff --git a/chat/migrations/__init__.py b/chat/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chat/models.py b/chat/models.py new file mode 100644 index 0000000..dd56b89 --- /dev/null +++ b/chat/models.py @@ -0,0 +1,56 @@ +from django.db import models +from django.contrib.auth.models import User +from users.models import Agency +from django.urls import reverse +from django.utils import timezone + + +''' + +MODEL ChatMessage + + +''' +class ChatMessage(models.Model): + author = models.ForeignKey(User, on_delete=models.CASCADE) + content = models.CharField(max_length=5000, blank=False, default="") + sendtime = models.DateTimeField(default=timezone.now, blank=True) + room = models.ForeignKey("ChatRoom", on_delete=models.CASCADE) +''' + +Model ChatRoom + +''' + +class ChatRoom(models.Model): + + creator = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) + ''' + chatroomtype + + 0 - User-User-Chat + 1 - Group-Chat + + ''' + chatroomtype = models.IntegerField(default=0) + roomname = models.CharField(max_length=200, blank=False, default="") + # This field is for random-String Django Channels + roomname_channel = models.CharField(max_length=200, blank=False) + chatmembers = models.ManyToManyField(User, blank=True, related_name='users_in_chatroom') + chatroom_createddate = models.DateTimeField(blank=True) + chatmember_single = models.ForeignKey(User, related_name='singleuserchat', on_delete=models.CASCADE, null=True, blank=True) + ''' + VIEWSTATUS + + 0 - Hide and Close + 1 - Hide, but visible in base.html + 2 - Full visible + + ''' + viewstatus = models.IntegerField(default=True) + messages = models.ManyToManyField("ChatMessage", blank=True, related_name='all_chatmessages') + + def __str__(self): + return f'{self.roomname}' + + diff --git a/chat/templates/chat/chat_allusers.html b/chat/templates/chat/chat_allusers.html new file mode 100644 index 0000000..3b4b3db --- /dev/null +++ b/chat/templates/chat/chat_allusers.html @@ -0,0 +1,47 @@ + +
+
+

Chat starten + +

+
+ {% for user in usersofagency %} + + +
+ +
+
+
+ {{user.first_name}} {{user.last_name}} + +
+
+ {% endfor %} +
+
+
+ + \ No newline at end of file diff --git a/chat/templates/chat/chat_content.html b/chat/templates/chat/chat_content.html new file mode 100644 index 0000000..c4549b8 --- /dev/null +++ b/chat/templates/chat/chat_content.html @@ -0,0 +1,215 @@ +{% if roomdata.creator == user %} +

Gespräch mit {{roomdata.chatmember_single.first_name}} {{roomdata.chatmember_single.last_name}}

+{% else %} +

Gespräch mit {{roomdata.creator.first_name}} {{roomdata.creator.last_name}}

+{% endif %} + +
+
+
+ +
+ + Unterhaltung gestartet am {{roomdata.chatroom_createddate}} + +
+
+ +
+ + {% for message in roomdata.messages.all %} + {% if message.author == request.user %} +
+
+ +
+
{{message.author.first_name}} {{message.author.last_name}}, {{message.sendtime}}
+
+ + {{message.content}} + +
+
+ {% else %} +
+
+ +
+
{{message.author.first_name}} {{message.author.last_name}}, {{message.sendtime}}
+
+ + {{message.content}} + +
+
+ + {% endif %} + + {% endfor %} + + + + +
+   +
+ +
+ +   + +
+ + + + +
+ + \ No newline at end of file diff --git a/chat/templates/chat/chat_othermessage.html b/chat/templates/chat/chat_othermessage.html new file mode 100644 index 0000000..7f8fc6c --- /dev/null +++ b/chat/templates/chat/chat_othermessage.html @@ -0,0 +1,11 @@ +
+
+ +
+
{{newmessage.author.first_name}} {{newmessage.author.last_name}}, {{newmessage.sendtime}}
+
+ + {{newmessage.content}} + +
+
\ No newline at end of file diff --git a/chat/templates/chat/chat_ownmessage.html b/chat/templates/chat/chat_ownmessage.html new file mode 100644 index 0000000..e695ff6 --- /dev/null +++ b/chat/templates/chat/chat_ownmessage.html @@ -0,0 +1,11 @@ +
+
+ +
+
{{newmessage.author.first_name}} {{newmessage.author.last_name}}, {{newmessage.sendtime}}
+
+ + {{newmessage.content}} + +
+
\ No newline at end of file diff --git a/chat/templates/chat/chatmanagement.html b/chat/templates/chat/chatmanagement.html new file mode 100644 index 0000000..bb14ecc --- /dev/null +++ b/chat/templates/chat/chatmanagement.html @@ -0,0 +1,152 @@ +{% extends "users/base.html" %} +{% block content %} +{% if request.user.profile.agency.module_chat %} + + + +
+

Chat  +

+
+
+ +
+
+

Mitarbeiter und Räume

+
+ {% for user in usersofagency %} +
+
+
+
+ +
+
+
+
{{user.first_name}} {{user.last_name}}
+
+
+
+ {% endfor %} +
+
+ +
+
+ + + + +{% else %} +

Das Module Chat wurde in ihrer Agentur deaktiviert.

+{% endif %} +{% endblock content %} diff --git a/chat/tests.py b/chat/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/chat/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/chat/urls.py b/chat/urls.py new file mode 100644 index 0000000..37f28d5 --- /dev/null +++ b/chat/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from . import views + +app_name = 'chat' +urlpatterns = [ + path('managemenet/', views.chatmanagement, name='chat-management'), + path('ajaxchat', views.chatajaxmain, name="chat-ajax"), + path('ajaxchat/getloggedusers', views.getloggedusers, name="chtaajax-getloggedusers"), + path('ajaxchat/getloggedusersdata', views.getloggedusersdata, name="chtaajax-getloggedusers-data") + +] \ No newline at end of file diff --git a/chat/views.py b/chat/views.py new file mode 100644 index 0000000..4fa3b54 --- /dev/null +++ b/chat/views.py @@ -0,0 +1,115 @@ +from django.shortcuts import render +from django.contrib.auth.decorators import login_required +from channels_presence.models import Presence +from django.http import HttpResponseRedirect,HttpResponse, JsonResponse +from django.contrib.auth.models import User +from channels_presence.models import Room +from channels_presence.models import Presence +import channels.layers +from django.utils import timezone +from .models import ChatRoom, ChatMessage + +# Create your views here. +@login_required +def chatmanagement(request): + + users_online = Room.objects.get(channel_name="agency_" + str(request.user.profile.agency.pk)) + + context = { + 'active_link' : 'chat', + "usersofagency" : User.objects.filter(profile__agency=request.user.profile.agency).exclude(pk=request.user.pk).order_by("last_name"), + "onlineusers" : users_online.get_users() + + } + return render(request, 'chat/chatmanagement.html', context) + + + + +@login_required +def getloggedusers(request): + if request.method == "GET": + + users_online = Room.objects.get(channel_name="agency_" + str(request.user.profile.agency.pk)) + + context = { + "usersofagency" : User.objects.filter(profile__agency=request.user.profile.agency).exclude(pk=request.user.pk).oder_by("last_name"), + "onlineusers" : users_online.get_users() + } + + return render(request, "chat/chat_allusers.html", context) + else: + return JsonResponse({}) + +@login_required +def getloggedusersdata(request): + if request.method == "GET": + + users_online = Room.objects.get(channel_name="agency_" + str(request.user.profile.agency.pk)).get_users() + users_agency = User.objects.filter(profile__agency=request.user.profile.agency).exclude(pk=request.user.pk) + + user_online_final = [] + for u in users_agency: + if(u in users_online): + user_online_final.append("" + str(u.pk)) + + return JsonResponse({"onlineusers" : user_online_final}) + else: + return JsonResponse({}) + +@login_required +def chatajaxmain(request): + if request.method == "GET": + + context = {} + choosenroom = "" + if request.GET["action"] == "startnewchat_user_user": + singleuserid = request.GET["new_chat_userid"] + getroom = ChatRoom.objects.filter(chatmember_single__pk=singleuserid, creator=request.user) | ChatRoom.objects.filter(creator__pk=singleuserid, chatmember_single=request.user) + singleuser = User.objects.get(pk=singleuserid) + # NO PRIVATE CHAT THERE, CREATE ONE! + if(len(getroom) == 0): + newchatroom = ChatRoom(creator=request.user, chatroomtype=0, roomname="Gespräch mit " + singleuser.first_name + " " + singleuser.last_name, roomname_channel="privatechat_" + str(request.user.pk) + "_" + singleuserid, chatmember_single=singleuser, chatroom_createddate=timezone.now(), viewstatus=0) + newchatroom.save() + context = { + "roomdata" : newchatroom + } + else: + context = { + "roomdata" : list(getroom)[0] + } + + return render(request, "chat/chat_content.html", context) + elif request.GET["action"] == "addnewmessage": + room = ChatRoom.objects.get(pk=request.GET["room"]) + if(request.user == room.creator or request.user == room.chatmember_single): + + newmessage = ChatMessage(room=room, author=request.user, content=request.GET["message"]) + newmessage.save() + room.messages.add(newmessage) + room.save() + return render(request, "chat/chat_ownmessage.html", {"newmessage" : newmessage}) + else: + JsonResponse({"status" : "Error on CHATAJAXMAIN"}) + elif request.GET["action"] == "loadnewestmessage": + room = ChatRoom.objects.get(pk=request.GET["room"]) + last_message = list(room.messages.order_by('-sendtime'))[0] + + if(last_message.author == request.user): + return render(request, "chat/chat_ownmessage.html", {"newmessage" : last_message}) + else: + return render(request, "chat/chat_othermessage.html", {"newmessage" : last_message}) + + else: + return JsonResponse({"status" : "Error on CHATAJAXMAIN"}) + + + +''' +author = models.ForeignKey(User, on_delete=models.CASCADE) + content = models.CharField(max_length=5000, blank=False, default="") + sendtime = models.DateField(default=timezone.now, blank=True) + room = models.ForeignKey("ChatRoom", on_delete=models.CASCADE) + + +''' \ No newline at end of file diff --git a/digitaleagentur/__pycache__/settings.cpython-38.pyc b/digitaleagentur/__pycache__/settings.cpython-38.pyc index 3e83899..3720ab2 100644 Binary files a/digitaleagentur/__pycache__/settings.cpython-38.pyc and b/digitaleagentur/__pycache__/settings.cpython-38.pyc differ diff --git a/digitaleagentur/__pycache__/urls.cpython-38.pyc b/digitaleagentur/__pycache__/urls.cpython-38.pyc index 4086f1b..2629cd8 100644 Binary files a/digitaleagentur/__pycache__/urls.cpython-38.pyc and b/digitaleagentur/__pycache__/urls.cpython-38.pyc differ diff --git a/digitaleagentur/settings.py b/digitaleagentur/settings.py index a35825b..a3cc97d 100644 --- a/digitaleagentur/settings.py +++ b/digitaleagentur/settings.py @@ -10,7 +10,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.2/ref/settings/ """ import os - +from datetime import datetime, timedelta # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -49,6 +49,8 @@ REDIS_URL = ("localhost", 6379) # Nach zehn Stunden läuft der Cookie ab! SESSION_COOKIE_AGE = 8*60*60 +CHANNELS_PRESENCE_MAX_AGE = 30 + # FOR SUMMERNOTE ORIGIN X_FRAME_OPTIONS = 'SAMEORIGIN' @@ -67,6 +69,7 @@ INSTALLED_APPS = [ 'dasettings.apps.DASettingsConfig', 'areas.apps.AreasConfig', 'orga.apps.OrgaConfig', + 'chat.apps.ChatConfig', 'message.apps.MessageConfig', 'cloud.apps.CloudConfig', 'tasks.apps.TasksConfig', @@ -90,7 +93,8 @@ INSTALLED_APPS = [ 'django_user_agents', 'rest_framework', 'rest_framework.authtoken', - 'channels' + 'channels', + 'channels_presence', ] MIDDLEWARE = [ @@ -125,7 +129,6 @@ TEMPLATES = [ REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.TokenAuthentication', - 'rest_framework.authentication.SessionAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', diff --git a/digitaleagentur/urls.py b/digitaleagentur/urls.py index 4ed0e56..c655777 100644 --- a/digitaleagentur/urls.py +++ b/digitaleagentur/urls.py @@ -7,21 +7,7 @@ from users.views import AgencyCreateView, registerNewAgency from . import views from django.contrib.auth.decorators import login_required from rest_framework.authtoken.views import obtain_auth_token -''' - Main URLS - - Apps: - areas - tasks - standards - orga - news - quicklinkgs - - -> Rest ist von Django - -''' urlpatterns = [ path('login/', auth_views.LoginView.as_view(template_name='users/login.html'), name='login'), path('', include('users.urls'), name="dashboard-first"), @@ -46,6 +32,7 @@ urlpatterns = [ path('notifications/', include('notificsys.urls'), name="notifications"), path('tm/', include('timemanagement.urls'), name="timemanagement"), path('api/', include('api.urls', namespace='api')), + path('chat/', include('chat.urls'), name='chat'), path('api-token-auth/', obtain_auth_token, name='api-token-auth'), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) if settings.DEBUG: diff --git a/users/mainwebsocket.py b/users/mainwebsocket.py index ee33275..bc91490 100644 --- a/users/mainwebsocket.py +++ b/users/mainwebsocket.py @@ -1,6 +1,11 @@ import json from channels.generic.websocket import WebsocketConsumer from asgiref.sync import async_to_sync +from channels_presence.models import Room +from channels_presence.decorators import touch_presence, remove_presence +from channels_presence.models import Presence +import channels +from django.contrib.auth.models import User class UsersConsumer(WebsocketConsumer): @@ -12,17 +17,17 @@ class UsersConsumer(WebsocketConsumer): auch nur Clients innerhalb der Agentur treffen! ''' - + def connect(self): - loggeduser = self.scope["user"] - async_to_sync(self.channel_layer.group_add)( - "agency_" + str(loggeduser.profile.agency.pk), - self.channel_name - ) - self.accept() + super().connect() + loggeduser = self.scope["user"] + Presence.objects.touch(self.channel_name) + Room.objects.add("agency_" + str(loggeduser.profile.agency.pk), self.channel_name, self.scope["user"]) def disconnect(self, close_code): - pass + loggeduser = self.scope["user"] + Room.objects.remove("agency_" + str(loggeduser.profile.agency.pk), self.channel_name) + Presence.objects.touch(self.channel_name) ''' def receive(self, text_data): @@ -36,6 +41,9 @@ class UsersConsumer(WebsocketConsumer): ) ''' # WEBSOCKET-DATA-CONTENT + def receive(self, text_data=None, bytes_data=None): + if text_data == '"heartbeat"': + Presence.objects.touch(self.channel_name) # UPDATET STANDARD def update_standard(self, event): @@ -43,5 +51,54 @@ class UsersConsumer(WebsocketConsumer): # NEW AGENCY NEWS def agency_newnews(self, event): - print(event) - self.send("agency_newnews") \ No newline at end of file + self.send("Neue Agenturnews!") + + # SOMETHING IN PRESENCE CHANGED + def update_presence_live(self, event): + self.send("presence_update") + + +class UsersChat(UsersConsumer): + + ''' + + CONNECT A WEBSOCKET + + Die Clients werden in Channel-Layer pro Agentur gepackt, damit gesendete Websocket-Nachrichten + auch nur Clients innerhalb der Agentur treffen! + + ''' + + def connect(self): + super().connect() + loggeduser = self.scope["user"] + roomname = "privatechat_" + str(self.scope["url_route"]["kwargs"]["creator"]) + "_" + str(self.scope["url_route"]["kwargs"]["single"]) + Room.objects.add(roomname, self.channel_name, self.scope["user"]) + + def disconnect(self, close_code): + Room.objects.remove("", self.channel_name) + + # WEBSOCKET-DATA-CONTENT + def receive(self, text_data=None, bytes_data=None): + datainfo = text_data.split("__") + typinguserid = datainfo[1] + if datainfo[0] == 'starttyping': + channel_layer = channels.layers.get_channel_layer() + async_to_sync(channel_layer.group_send)(datainfo[2], {'type' : 'start_typing', 'typingname' : typinguserid}) + elif datainfo[0] == 'stoptyping': + channel_layer = channels.layers.get_channel_layer() + async_to_sync(channel_layer.group_send)(datainfo[2], {'type' : 'stop_typing'}) + elif datainfo[0] == 'load': + channel_layer = channels.layers.get_channel_layer() + async_to_sync(channel_layer.group_send)(datainfo[2], {'type' : 'reloadmessages'}) + + + def start_typing(self, event): + useristyping = User.objects.get(pk=event["typingname"]) + self.send("starttyping__" + str(useristyping.pk) + "_" + useristyping.first_name + " " + useristyping.last_name + " tippt...") + + def stop_typing(self, event): + self.send("stoptyping") + + def reloadmessages(self, event): + self.send("reloadmessages") \ No newline at end of file diff --git a/users/models.py b/users/models.py index 7c6f541..57c8d0a 100644 --- a/users/models.py +++ b/users/models.py @@ -101,6 +101,9 @@ class Agency(models.Model): dynamicprofile = models.BooleanField(default=True) module_messages = models.BooleanField(default=True) + + module_chat = models.BooleanField(default=True) + # KOSTENPFLICHTIGE MODULE diff --git a/users/routing.py b/users/routing.py index b4a7cc9..f5bf447 100644 --- a/users/routing.py +++ b/users/routing.py @@ -2,6 +2,7 @@ from django.urls import re_path from . import mainwebsocket -websocket_urlpatterns = [ - re_path(r'', mainwebsocket.UsersConsumer), +websocket_urlpatterns = [ + re_path(r'chat/(?P\w+)/(?P\w+)/$', mainwebsocket.UsersChat), + re_path('main/', mainwebsocket.UsersConsumer), ] \ No newline at end of file diff --git a/users/signals.py b/users/signals.py index 1077735..45f3428 100644 --- a/users/signals.py +++ b/users/signals.py @@ -22,6 +22,8 @@ import requests, csv, os from django.templatetags.static import static from django.conf import settings from datetime import date +import channels.layers +from asgiref.sync import async_to_sync def loadingFreeDays(plz): # Getting land @@ -192,14 +194,9 @@ def adjust_group_notifications(instance, action, reverse, model, pk_set, using, ) -import channels.layers -from asgiref.sync import async_to_sync - - # SIGNAL FOR STANDARDS POST SAVE @receiver(post_save, sender=Standards) def save_standard(sender, instance, **kwargs): - print(kwargs) GLOBALSENDMAILS = True # NEW STANDARD AND DIRECT PUBLIC if(kwargs["created"]): @@ -283,6 +280,9 @@ def save_news(sender, instance, **kwargs): else: instance.agnotify = False instance.save() + else: + channel_layer = channels.layers.get_channel_layer() + async_to_sync(channel_layer.group_send)("agency_" + str(instance.agency.pk), {'type' : 'agency_newnews'}) @@ -344,4 +344,23 @@ def adjust_group_notifications_task(instance, action, reverse, model, pk_set, us @receiver(signal=post_save, sender=AgencyNetworkPreperation) def save_agjoin_prep(sender, instance, **kwargs): newnotification = UserNotification(touser=instance.target_network.creator, notificationtext="Eine Agentur möchte Ihrem Verbund beitreten.", notificationtype="wantedag", elementid=instance.pk) - newnotification.save() \ No newline at end of file + newnotification.save() + + +from django.core.signals import request_started +from channels_presence.models import Room +from channels_presence.models import Presence +from channels_presence.signals import presence_changed +# REQUEST MAIN STUFF +@receiver(signal=request_started) +def receiver_function(sender, **kwargs): + # DELETES ALL PRESENCE-OBJETS LOWER THAN 15 MINUTES + now_minus = datetime.datetime.now() - datetime.timedelta(minutes=2) + Presence.objects.filter(last_seen__lt=now_minus).delete() + + +# PREENCE CHANGED +@receiver(signal=presence_changed) +def update_presence_live(sender, **kwargs): + channel_layer = channels.layers.get_channel_layer() + async_to_sync(channel_layer.group_send)(str(kwargs["room"]), {'type' : 'update_presence_live'}) diff --git a/users/templates/users/base.html b/users/templates/users/base.html index d055bf8..86e0aab 100644 --- a/users/templates/users/base.html +++ b/users/templates/users/base.html @@ -61,6 +61,7 @@ +
@@ -72,7 +73,6 @@ - @@ -161,6 +161,20 @@ {% endif %} + {% if request.user.profile.agency.module_chat %} + {% if active_link == 'chat' %} + + {% endif %} + {% if request.user.profile.agency.module_timemanagement %} {% if active_link == 'abscence' %}
+ {% if active_link != 'chat' %} +
+ + {% endif %}
+ + + + + \ No newline at end of file diff --git a/users/templates/users/chat_allusers.html b/users/templates/users/chat_allusers.html new file mode 100644 index 0000000..c5da743 --- /dev/null +++ b/users/templates/users/chat_allusers.html @@ -0,0 +1,47 @@ + +
+
+

Chat starten + +

+
+ {% for user in usersofagency %} + + +
+ +
+
+
+ {{user.first_name}} {{user.last_name}} + +
+
+ {% endfor %} +
+
+
+ + \ No newline at end of file diff --git a/users/urls.py b/users/urls.py index 7e83199..c8d5f33 100644 --- a/users/urls.py +++ b/users/urls.py @@ -35,7 +35,7 @@ urlpatterns = [ path('impressum/', views.impressum, name="impressumda"), path('setuserparent/', views.setuserparent, name="users-setuserparent"), path('sendpassmail/', views.sendpassmail, name="users-sendpassmail"), - path('dacron/', views.cronactions, name="cronmain") + path('dacron/', views.cronactions, name="cronmain") ] diff --git a/users/views.py b/users/views.py index 67d4408..2a3fde1 100644 --- a/users/views.py +++ b/users/views.py @@ -35,9 +35,11 @@ from cloud.models import DataDir from message.models import Message from notificsys.models import UserNotification from organizer.models import AGContacts, AGPassword -import socket import sys, os - +from asgiref.sync import async_to_sync +from channels_presence.models import Room +from channels_presence.models import Presence +import channels.layers def randomString(stringLength=10): """Generate a random string of fixed length """ @@ -853,6 +855,3 @@ def cronactions(request, code): print("API CODE FAILED") data.update({"status" : "failed"}) return JsonResponse(data) - -def index(request): - return render(request, 'users/websocket.html', {}) \ No newline at end of file