Richtig geiler shit
This commit is contained in:
parent
1c26681068
commit
233206e77e
|
|
@ -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__/*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ChatConfig(AppConfig):
|
||||
name = 'chat'
|
||||
|
|
@ -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}'
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<style type="text/css">
|
||||
.roundimg {
|
||||
border-radius: 50%;
|
||||
z-index: 999;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.icon-container {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-circle {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
<div class="card col-2" style="position: fixed; right: 35px; bottom: 75px;">
|
||||
<div class="card-body">
|
||||
<h4>Chat starten
|
||||
<button class="btn btn-sm btn-secondary" style="float: right;" onclick="javascript:$('#chat_alluserscontent').fadeOut()"><small><i class="fas fa-times"></i></small></button>
|
||||
</h4>
|
||||
<hr>
|
||||
{% for user in usersofagency %}
|
||||
<span>
|
||||
|
||||
<div class='icon-container'>
|
||||
<img class="img-profile roundimg" src="{{ user.profile.get_photo_url }}">
|
||||
<div class='status-circle' style="background-color: {% if user in onlineusers %} green {% else %} grey {% endif %};">
|
||||
</div>
|
||||
</div>
|
||||
{{user.first_name}} {{user.last_name}}
|
||||
<button class="btn btn-sm btn-secondary" style="float: right;"><small><i class="far fa-comment"></i></small></button>
|
||||
</span>
|
||||
<br />
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-popup">
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
{% if roomdata.creator == user %}
|
||||
<h4 id="chattitle">Gespräch mit {{roomdata.chatmember_single.first_name}} {{roomdata.chatmember_single.last_name}}</h4>
|
||||
{% else %}
|
||||
<h4 id="chattitle">Gespräch mit {{roomdata.creator.first_name}} {{roomdata.creator.last_name}}</h4>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
<div class="card" >
|
||||
<div class="card-body scroll" id="chatcontentcomplete">
|
||||
|
||||
<div id="roomstart" style="min-width: 100%; text-align: center;">
|
||||
|
||||
<small>Unterhaltung gestartet am {{roomdata.chatroom_createddate}}</small>
|
||||
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div id="chatmessages">
|
||||
|
||||
{% for message in roomdata.messages.all %}
|
||||
{% if message.author == request.user %}
|
||||
<div style="" class="chatmessageele_me col-7 mb-3 ">
|
||||
<div class='icon-container ml-2 mt-1 ' style="float: right;">
|
||||
<img class="img-profile roundimg" src="{{ message.author.profile.get_photo_url }}">
|
||||
</div>
|
||||
<h6 class="mt-3"><small>{{message.author.first_name}} {{message.author.last_name}}, {{message.sendtime}}</small></h6>
|
||||
<div style="text-align: left;" class="mt-1">
|
||||
<span style="float: right !important; font-size: 1.0em;">
|
||||
{{message.content}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="" class="chatmessageele_other col-7 mb-3">
|
||||
<div class='icon-container mr-2 mt-1 ' style="float: left;">
|
||||
<img class="img-profile roundimg" src="{{ message.author.profile.get_photo_url }}">
|
||||
</div>
|
||||
<h6 class="mt-3"><small>{{message.author.first_name}} {{message.author.last_name}}, {{message.sendtime}}</small></h6>
|
||||
<div style="text-align: left;" class="mt-1">
|
||||
<span style="float: left !important; font-size: 1.0em;">
|
||||
{{message.content}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
<!-- SINGLE MESSAGE -->
|
||||
<!--
|
||||
<div style="" class="chatmessageele_me col-7 mb-3 ">
|
||||
<div class='icon-container ml-2 mt-1 ' style="float: right;">
|
||||
<img class="img-profile roundimg" src="{{ user.profile.get_photo_url }}">
|
||||
</div>
|
||||
<h6 class="mt-3"><small>{{user.first_name}} {{user.last_name}}, 13:45 18.05.2020</small></h6>
|
||||
<div style="text-align: left;" class="mt-1">
|
||||
<span style="float: right !important; font-size: 1.0em;">
|
||||
Ich finde schon, dass wir hier langsam mal mit den Daten arbeiten sollten...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="" class="chatmessageele_other col-7 mb-3 ">
|
||||
<div class='icon-container mr-2 mt-1 ' style="float: left;">
|
||||
<img class="img-profile roundimg" src="{{ user.profile.get_photo_url }}">
|
||||
</div>
|
||||
<h6 class="mt-3"><small>{{user.first_name}} {{user.last_name}}, 13:45 18.05.2020</small></h6>
|
||||
<div style="text-align: left;" class="mt-1">
|
||||
<span style="float: left !important; font-size: 1.0em;">
|
||||
Ich finde schon, dass wir hier langsam mal mit den Daten arbeiten sollten...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
</div><!-- END CHAT MESSAGES -->
|
||||
<span id="scrolltarget"> </span>
|
||||
</div><!-- END CARD BODY-->
|
||||
|
||||
<div id="is_typing" class="ml-2">
|
||||
<div class="spinner-grow spinner-border-sm" id="typingspinner" role="status" style="display: none;">
|
||||
<span class="sr-only">Jemand tippt...</span>
|
||||
</div>
|
||||
<small id="is_typing_name" class="">
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="card-footer bg-transparent border-success">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="Neue Nachricht" id="message" aria-describedby="">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" id="sendNewMessgeButton" onclick="javascript:sendNewMessage()" type="button"><i class="fas fa-location-arrow"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
function scrollDown(){
|
||||
//$('#chatmessages').animate({scrollTop: $('#chatmessages').prop("scrollHeight")}, 500);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
$('#chatcontentcomplete').scrollTop( $('#chatcontentcomplete').height()*200 )
|
||||
});
|
||||
|
||||
userownid = "{{user.pk}}";
|
||||
|
||||
creator_id = {{roomdata.creator.pk}};
|
||||
chatmember_id = {{roomdata.chatmember_single.pk}};
|
||||
|
||||
ws_string = 'wss://'
|
||||
if (location.protocol !== 'https:') {
|
||||
ws_string = 'ws://'
|
||||
}
|
||||
if(typeof chatwebsocket != "undefined"){
|
||||
chatwebsocket = new WebSocket(ws_string+window.location.host+"/chat/{{roomdata.creator.pk}}/{{roomdata.chatmember_single.pk}}/")
|
||||
}
|
||||
else{
|
||||
chatwebsocket = new WebSocket(ws_string+window.location.host+"/chat/{{roomdata.creator.pk}}/{{roomdata.chatmember_single.pk}}/")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
chatwebsocket.onmessage = function(e) {
|
||||
datainfo = e["data"].split("__");
|
||||
if(datainfo[0] == "starttyping")
|
||||
{
|
||||
typingname = (datainfo[1].split("_"))[1];
|
||||
typingid = (datainfo[1].split("_"))[0];
|
||||
if(typingid != userownid){
|
||||
$("#is_typing_name").html("" + typingname);
|
||||
$("#typingspinner").show();
|
||||
}
|
||||
}
|
||||
|
||||
if(datainfo[0] == "stoptyping")
|
||||
{
|
||||
$("#is_typing_name").html(" ");
|
||||
$("#typingspinner").hide();
|
||||
}
|
||||
|
||||
if(datainfo[0] == "reloadmessages")
|
||||
{
|
||||
$.ajax(
|
||||
{
|
||||
type: "GET",
|
||||
url: "{% url 'chat:chat-ajax' %}",
|
||||
data : {
|
||||
action : "loadnewestmessage",
|
||||
room : {{roomdata.pk}}
|
||||
},
|
||||
success: function( data )
|
||||
{
|
||||
$("#chatmessages").append(data);
|
||||
$('#chatcontentcomplete').scrollTop( $('#chatcontentcomplete').height()*200 );
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
chatwebsocket.onclose = function(e) {
|
||||
console.error('Chat socket closed unexpectedly');
|
||||
};
|
||||
|
||||
|
||||
$("#message").keyup(function(){
|
||||
if($("#message").val().length > 0){
|
||||
chatwebsocket.send("starttyping__{{user.pk}}__privatechat_{{roomdata.creator.pk}}_{{roomdata.chatmember_single.pk}}");
|
||||
}
|
||||
else{
|
||||
chatwebsocket.send("stoptyping__{{user.pk}}__privatechat_{{roomdata.creator.pk}}_{{roomdata.chatmember_single.pk}}");
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
function sendNewMessage(){
|
||||
$.ajax(
|
||||
{
|
||||
type: "GET",
|
||||
url: "{% url 'chat:chat-ajax' %}",
|
||||
data : {
|
||||
action : "addnewmessage",
|
||||
new_chat_userid : userownid,
|
||||
message : $("#message").val(),
|
||||
room : {{roomdata.pk}}
|
||||
},
|
||||
success: function( data )
|
||||
{
|
||||
$("#message").val("");
|
||||
chatwebsocket.send("load__{{user.pk}}__privatechat_{{roomdata.creator.pk}}_{{roomdata.chatmember_single.pk}}");
|
||||
chatwebsocket.send("stoptyping__{{user.pk}}__privatechat_{{roomdata.creator.pk}}_{{roomdata.chatmember_single.pk}}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$(document).on('keypress',function(e) {
|
||||
if(e.which == 13) {
|
||||
if($("#message").val().length > 0){
|
||||
sendNewMessage();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<div style="" class="chatmessageele_other col-7 mb-3 ">
|
||||
<div class='icon-container mr-2 mt-1 ' style="float: left;">
|
||||
<img class="img-profile roundimg" src="{{ newmessage.author.profile.get_photo_url }}">
|
||||
</div>
|
||||
<h6 class="mt-3"><small>{{newmessage.author.first_name}} {{newmessage.author.last_name}}, {{newmessage.sendtime}}</small></h6>
|
||||
<div style="text-align: left;" class="mt-1">
|
||||
<span style="float: left !important; font-size: 1.0em;">
|
||||
{{newmessage.content}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<div style="" class="chatmessageele_me col-7 mb-3 ">
|
||||
<div class='icon-container ml-2 mt-1 ' style="float: right;">
|
||||
<img class="img-profile roundimg" src="{{ newmessage.author.profile.get_photo_url }}">
|
||||
</div>
|
||||
<h6 class="mt-3"><small>{{newmessage.author.first_name}} {{newmessage.author.last_name}}, {{newmessage.sendtime}}</small></h6>
|
||||
<div style="text-align: left;" class="mt-1">
|
||||
<span style="float: right !important; font-size: 1.0em;">
|
||||
{{newmessage.content}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
{% extends "users/base.html" %}
|
||||
{% block content %}
|
||||
{% if request.user.profile.agency.module_chat %}
|
||||
<style type="text/css">
|
||||
.roundimg {
|
||||
border-radius: 50%;
|
||||
z-index: 999;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.icon-container {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.status-circle {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.chatmessageele_me
|
||||
{
|
||||
padding: 5px;
|
||||
border-radius: 15px;
|
||||
background-color: #f8f9fc;
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.chatmessageele_other
|
||||
{
|
||||
padding: 5px;
|
||||
border-radius: 15px;
|
||||
background-color: #858796;
|
||||
float: left;
|
||||
text-align: left;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
max-height: 600px;
|
||||
min-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<div class="content-section col-12">
|
||||
<h3>Chat <small><i data-toggle="tooltip" data-placement="top" title="Verwalten Sie hier Ihre Chatverläufe und starten Sie neue Unterhaltungen mit Mitarbeitern und anderen Agenturen." class="far fa-question-circle"></i></small>
|
||||
</h3>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div class="row col">
|
||||
<div class="col-3">
|
||||
<h4>Mitarbeiter und Räume</h4>
|
||||
<hr>
|
||||
{% for user in usersofagency %}
|
||||
<div class="card mb-2 hoverchatcard" id="userchat_{{user.pk}}">
|
||||
<div class="card-body">
|
||||
<div style="float: left;" class="col-12 ">
|
||||
<div class='icon-container mr-2'>
|
||||
<img class="img-profile roundimg" src="{{ user.profile.get_photo_url }}">
|
||||
<div class='status-circle' id="userstatus_circle_{{user.pk}}" style="background-color: {% if user in onlineusers %} green {% else %} grey {% endif %};">
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="mt-3">{{user.first_name}} {{user.last_name}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col-9" style="" id="mainchatcontent">
|
||||
|
||||
</div><!-- END CHATAREA -->
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
creator_id = false;
|
||||
chatmember_id = false;
|
||||
|
||||
$(".hoverchatcard").hover(function(){
|
||||
var $this = $(this);
|
||||
$this.data('bgcolor', $this.css('background-color')).css('background-color', '#f8f9fc');
|
||||
}, function(){
|
||||
var $this = $(this);
|
||||
$this.data('bgcolor', $this.css('background-color')).css('background-color', '#FFFFFF');
|
||||
}
|
||||
)
|
||||
|
||||
$(".hoverchatcard").click(function(){
|
||||
|
||||
$.ajax(
|
||||
{
|
||||
type: "GET",
|
||||
url: "{% url 'chat:chat-ajax' %}",
|
||||
data : {
|
||||
action : "startnewchat_user_user",
|
||||
new_chat_userid : $(this)[0]["id"].split("_")[1]
|
||||
},
|
||||
success: function( data )
|
||||
{
|
||||
if(creator_id != false && chatmember_id != false){
|
||||
chatwebsocket.close();
|
||||
}
|
||||
$("#mainchatcontent").html(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function updatePresenceLive(e) {
|
||||
$.ajax(
|
||||
{
|
||||
type: "GET",
|
||||
url: "{% url 'chat:chtaajax-getloggedusers-data' %}",
|
||||
data : {
|
||||
action : "getloggedusers"
|
||||
},
|
||||
success: function( data )
|
||||
{
|
||||
console.log(data);
|
||||
$( ".status-circle" ).each(function( index ) {
|
||||
if(data["onlineusers"].indexOf($(this)[0]["id"].split("_")[2]) !== -1){
|
||||
$("#" + $(this)[0]["id"]).css("background-color", "green");
|
||||
}
|
||||
else{
|
||||
$("#" + $(this)[0]["id"]).css("background-color", "grey");
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{% else %}
|
||||
<h3>Das Module Chat wurde in ihrer Agentur deaktiviert.</h3>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -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")
|
||||
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
||||
|
||||
'''
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
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")
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<creator>\w+)/(?P<single>\w+)/$', mainwebsocket.UsersChat),
|
||||
re_path('main/', mainwebsocket.UsersConsumer),
|
||||
]
|
||||
|
|
@ -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()
|
||||
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'})
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@
|
|||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Page Wrapper -->
|
||||
<div id="wrapper">
|
||||
<!-- Sidebar -->
|
||||
|
|
@ -72,7 +73,6 @@
|
|||
<i class="fas fa-laptop"></i>
|
||||
<div class="sidebar-brand-text mx-2" style="">Digitale Agentur</div>
|
||||
</a>
|
||||
|
||||
<!-- Divider -->
|
||||
<hr class="sidebar-divider my-0">
|
||||
|
||||
|
|
@ -161,6 +161,20 @@
|
|||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user.profile.agency.module_chat %}
|
||||
{% if active_link == 'chat' %}
|
||||
<li class="nav-item active">
|
||||
{% else%}
|
||||
<li class="nav-item">
|
||||
{%endif%}
|
||||
|
||||
<a class="nav-link " href="{% url 'chat:chat-management' %}" aria-expanded="true">
|
||||
<i class="fas fa-comments"></i>
|
||||
<span>Chat</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user.profile.agency.module_timemanagement %}
|
||||
{% if active_link == 'abscence' %}
|
||||
<li class="nav-item active">
|
||||
|
|
@ -379,8 +393,17 @@
|
|||
<div style="height: 300px"> </div>
|
||||
</div> <!-- End of Main Content CONTAINER FLUID-->
|
||||
<!-- End of Content Wrapper -->
|
||||
{% if active_link != 'chat' %}
|
||||
<div id="chat_alluserscontent" style="position: fixed; bottom: 75px; right: 36px; z-index: 999;"></div>
|
||||
<button id="chatButton" class="btn btn-primary" style="position: fixed; right: 36px; bottom: 30px;"><i class="far fa-comments"></i></button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- CHAT BUTTON -->
|
||||
|
||||
|
||||
<!-- End of Page Wrapper -->
|
||||
<!--
|
||||
<footer class="sticky-footer bg-white" style="width: 86.2%;position: absolute;
|
||||
|
|
@ -619,17 +642,15 @@ function removeNotification(notifyid){
|
|||
//$("#allnotificationsarea").show();
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
|
||||
$("#notification_items").html("");
|
||||
loadUnsendNotifications();
|
||||
loadUnviewnNotifications();
|
||||
});
|
||||
|
||||
$(window).click(function() {
|
||||
$("#notification_items").html("");
|
||||
loadUnsendNotifications();
|
||||
loadUnviewnNotifications();
|
||||
|
||||
$(document).on('click', function (e) {
|
||||
|
||||
if(e.target["id"] != 'chatButton'){
|
||||
if ($(e.target).closest("#chat_alluserscontent").length === 0) {
|
||||
$("#chat_alluserscontent").fadeOut();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -637,16 +658,95 @@ $(window).click(function() {
|
|||
<!-- WEBSOCKETS -->
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
const mainwebsocket = new WebSocket('wss://'+window.location.host)
|
||||
|
||||
$("#chat_alluserscontent").hide();
|
||||
|
||||
ws_string = 'wss://'
|
||||
if (location.protocol !== 'https:') {
|
||||
ws_string = 'ws://'
|
||||
}
|
||||
|
||||
const mainwebsocket = new WebSocket(ws_string+window.location.host+"/main/")
|
||||
|
||||
mainwebsocket.onmessage = function(e) {
|
||||
console.log(e);
|
||||
mainwebsocket.onmessage = function(e) {
|
||||
if(e["data"] != "presence_update")
|
||||
{
|
||||
var notify = new Notification('Digitale Agentur', {
|
||||
body: e["data"]
|
||||
});
|
||||
loadUnsendNotifications();
|
||||
loadUnviewnNotifications();
|
||||
}
|
||||
else{
|
||||
|
||||
{% if active_link == "chat" %}
|
||||
updatePresenceLive(e);
|
||||
{% endif %}
|
||||
}
|
||||
};
|
||||
|
||||
mainwebsocket.onclose = function(e) {
|
||||
console.error('Chat socket closed unexpectedly');
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
//HEARTBEAT every minute
|
||||
setInterval(function()
|
||||
{
|
||||
mainwebsocket.send(JSON.stringify("heartbeat"));
|
||||
console.log("heartbeat is alive...");
|
||||
},60000);
|
||||
|
||||
|
||||
});
|
||||
|
||||
window.onerror = function (msg, url, line) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!window.Notification) {
|
||||
console.log('Browser does not support notifications.');
|
||||
} else {
|
||||
// check if permission is already granted
|
||||
if (Notification.permission === 'granted') {
|
||||
// show notification here
|
||||
} else {
|
||||
// request permission from user
|
||||
Notification.requestPermission().then(function(p) {
|
||||
if(p === 'granted') {
|
||||
// show notification here
|
||||
console.log("OK!")
|
||||
} else {
|
||||
console.log('User blocked notifications.');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
$("#chatButton").click(function(){
|
||||
$.ajax(
|
||||
{
|
||||
type: "GET",
|
||||
url: "{% url 'chat:chtaajax-getloggedusers' %}",
|
||||
data : {
|
||||
action : "getloggedusers"
|
||||
},
|
||||
success: function( data )
|
||||
{
|
||||
$("#chat_alluserscontent").fadeToggle();
|
||||
$("#chat_alluserscontent").html(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<style type="text/css">
|
||||
.roundimg {
|
||||
border-radius: 50%;
|
||||
z-index: 999;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.icon-container {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-circle {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
<div class="card col-2" style="position: fixed; right: 35px; bottom: 75px;">
|
||||
<div class="card-body">
|
||||
<h4>Chat starten
|
||||
<button class="btn btn-sm btn-secondary" style="float: right;" onclick="javascript:$('#chat_alluserscontent').fadeOut()"><small><i class="fas fa-times"></i></small></button>
|
||||
</h4>
|
||||
<hr>
|
||||
{% for user in usersofagency %}
|
||||
<span>
|
||||
|
||||
<div class='icon-container'>
|
||||
<img class="img-profile roundimg" src="{{ user.profile.get_photo_url }}">
|
||||
<div class='status-circle' style="background-color: {% if user in onlineusers %} green {% else %} grey {% endif %};">
|
||||
</div>
|
||||
</div>
|
||||
{{user.first_name}} {{user.last_name}}
|
||||
<button class="btn btn-sm btn-secondary" style="float: right;"><small><i class="far fa-comment"></i></small></button>
|
||||
</span>
|
||||
<br />
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-popup">
|
||||
|
||||
|
||||
|
|
@ -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/<slug:code>', views.cronactions, name="cronmain")
|
||||
path('dacron/<slug:code>', views.cronactions, name="cronmain")
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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', {})
|
||||
Loading…
Reference in New Issue