Skip to content

Commit

Permalink
Merge pull request #1 from ckc-org/tests_added
Browse files Browse the repository at this point in the history
Populate chit_chat app
  • Loading branch information
gibsonbailey authored Jun 12, 2021
2 parents 9cf65ad + 1e32dc9 commit dce8640
Show file tree
Hide file tree
Showing 26 changed files with 889 additions and 72 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8]
django-version: ['<3', '>=3']
python-version: ['3.8', '3.9']
django-version: ['>=3']

steps:
- uses: actions/checkout@v2
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,39 @@ INSTALLED_APPS = (
)
```

```python
# routing.py
from channels.auth import AuthMiddlewareStack
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter

import chit_chat.routing


application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(
chit_chat.routing.websocket_urlpatterns
)
),
})
```

```python
# urls.py
from rest_framework import routers

from chit_chat.viewsets import RoomViewSet


router = routers.SimpleRouter()
router.register('chatrooms', RoomViewSet)

urlpatterns = router.urls
```


## distributing

```bash
Expand Down
35 changes: 35 additions & 0 deletions chit_chat/consumer_serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from rest_framework import serializers, exceptions
from django.contrib.auth import get_user_model

from chit_chat.models import Room, Message


User = get_user_model()


class ContentSerializer(serializers.Serializer):
message_type = serializers.CharField()

def __init__(self, valid_message_types, *args, **kwargs):
super().__init__(*args, **kwargs)
self.valid_message_types = valid_message_types

def validate_message_type(self, message_type):
if message_type not in self.valid_message_types:
raise exceptions.ValidationError(f'Must be one of the following message types: {self.valid_message_types}')
return message_type


class ChatMessageSerializer(serializers.Serializer):
room = serializers.PrimaryKeyRelatedField(queryset=Room.objects.all())
text = serializers.CharField()
user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all())

def validate(self, attrs):
# Make sure the user is in the room.
if not attrs['room'].members.filter(pk=attrs['user'].pk).exists():
raise exceptions.ValidationError('Cannot send messages to chat rooms that you are not a member of.')
return attrs

def create(self, validated_data):
return Message.objects.create(**validated_data)
116 changes: 116 additions & 0 deletions chit_chat/consumers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import json
from six import string_types
from importlib import import_module

from django.conf import settings
from rest_framework import exceptions
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async

from chit_chat.consumer_serializers import ChatMessageSerializer, ContentSerializer


def import_callable(path_or_callable):
if hasattr(path_or_callable, '__call__'):
return path_or_callable
else:
assert isinstance(path_or_callable, string_types)
package, attr = path_or_callable.rsplit('.', 1)
print('package', package)
return getattr(import_module(package), attr)


def async_validation_exception_handler(func):
async def inner(*args, **kwargs):
self = args[0]
assert AsyncWebsocketConsumer in self.__class__.__mro__
try:
return await func(*args, **kwargs)
except exceptions.ValidationError as e:
errors = e.detail
if 'non_field_errors' not in errors:
errors = {'field_errors': errors}
await self.send(text_data=json.dumps(errors))
except Exception as e: # pragma: no cover
await self.send(text_data=json.dumps({'non_field_errors': ['System Error']}))
raise e
return inner


class ChatRoomConsumer(AsyncWebsocketConsumer):
MESSAGE_TYPES = [
'chat',
]

async def connect(self):
user = self.scope['user']
if not user.is_authenticated:
await self.close()
return
for pk in await self.get_chat_room_pks(user):
await self.channel_layer.group_add(str(pk), self.channel_name)
await self.accept()

async def disconnect(self, close_code):
for pk in await self.get_chat_room_pks(self.scope['user']):
await self.channel_layer.group_discard(str(pk), self.channel_name)

async def receive(self, text_data=None, bytes_data=None):
user = self.scope['user']
if bytes_data is not None:
text_data = bytes_data.decode()
try:
data = json.loads(text_data)
except json.decoder.JSONDecodeError:
await self.send(text_data=json.dumps({'non_field_errors': ['Invalid JSON.']}))
return

valid = await self.validate_content(data)
if valid:
message_type = data.get('message_type')

if message_type == 'chat':
data['user'] = user.pk

if message := await self.validate_chat_message(data):
await self.channel_layer.group_send(
str(message.room.pk),
{
'type': 'chat',
'user': user.pk,
'room': message.room.pk,
'text': message.text,
'time': message.created_when.isoformat(),
}
)

@async_validation_exception_handler
async def validate_content(self, data):
serializer = ContentSerializer(ChatRoomConsumer.MESSAGE_TYPES, data=data)
return serializer.is_valid(raise_exception=True)

@async_validation_exception_handler
@database_sync_to_async
def validate_chat_message(self, data):
serializers = getattr(settings, 'CKC_CHAT_SERIALIZERS', {})
chat_serializer = import_callable(
serializers.get('CHAT_MESSAGE_SERIALIZER', ChatMessageSerializer)
)
serializer = chat_serializer(data=data)
serializer.is_valid(raise_exception=True)
return serializer.save()

async def chat(self, event):
await self.send(
text_data=json.dumps({
'type': 'chat',
'user': event.get('user'),
'text': event.get('text'),
'room': event.get('room'),
'time': event.get('time'),
})
)

@database_sync_to_async
def get_chat_room_pks(self, user):
return list(user.chat_rooms.values_list('pk', flat=True))
48 changes: 48 additions & 0 deletions chit_chat/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 3.0.7 on 2021-03-15 23:22

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='Room',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_when', models.DateTimeField(default=django.utils.timezone.now)),
('members', models.ManyToManyField(related_name='chat_rooms', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='RoomMembership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('archived', models.BooleanField(default=False)),
('ignore_notifications', models.BooleanField(default=False)),
('created_when', models.DateTimeField(default=django.utils.timezone.now)),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='chit_chat.Room')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chat_room_memberships', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Message',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
('created_when', models.DateTimeField(default=django.utils.timezone.now)),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='chit_chat.Room')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chat_room_messages', to=settings.AUTH_USER_MODEL)),
('users_who_viewed', models.ManyToManyField(related_name='chat_room_messages_viewed', to=settings.AUTH_USER_MODEL)),
],
),
]
Empty file.
27 changes: 27 additions & 0 deletions chit_chat/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.db import models


User = get_user_model()


class Room(models.Model):
members = models.ManyToManyField(User, related_name='chat_rooms')
created_when = models.DateTimeField(default=timezone.now)


class RoomMembership(models.Model):
room = models.ForeignKey(Room, related_name='memberships', on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name='chat_room_memberships', on_delete=models.CASCADE)
archived = models.BooleanField(default=False)
ignore_notifications = models.BooleanField(default=False)
created_when = models.DateTimeField(default=timezone.now)


class Message(models.Model):
text = models.TextField()
room = models.ForeignKey(Room, related_name='messages', on_delete=models.CASCADE)
user = models.ForeignKey(User, related_name='chat_room_messages', on_delete=models.CASCADE)
created_when = models.DateTimeField(default=timezone.now)
users_who_viewed = models.ManyToManyField(User, related_name='chat_room_messages_viewed')
7 changes: 7 additions & 0 deletions chit_chat/routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
re_path('ws/chatroom/', consumers.ChatRoomConsumer.as_asgi()),
]
57 changes: 57 additions & 0 deletions chit_chat/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers, exceptions

from chit_chat.models import Room, Message
from .utils import PrimaryKeyWriteSerializerReadField


User = get_user_model()


class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
fields = (
'id',
'text',
'created_when',
'room',
'user',
'users_who_viewed',
)


class ChatUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
'id',
'first_name',
'last_name',
'avatar',
)


class RoomSerializer(serializers.ModelSerializer):
messages = MessageSerializer(read_only=True, many=True)
members = PrimaryKeyWriteSerializerReadField(
queryset=User.objects.all(),
read_serializer=ChatUserSerializer,
many=True,
)

class Meta:
model = Room
fields = (
'id',
'messages',
'members',
)

def validate_members(self, users):
requestor = self.context['request'].user
non_requestor_users = [user for user in users if user != requestor]
if len(non_requestor_users) < 1:
raise exceptions.ValidationError('Must contain at least one user other than the requestor in this list.')
non_requestor_users.append(requestor)
return users
32 changes: 32 additions & 0 deletions chit_chat/templates/test_chat.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chat test</title>
</head>
<body>
<textarea id="chat_area"></textarea>

<script src="https://cdnjs.cloudflare.com/ajax/libs/reconnecting-websocket/1.0.0/reconnecting-websocket.min.js" integrity="sha512-B4skI5FiLurS86aioJx9VfozI1wjqrn6aTdJH+YQUmCZum/ZibPBTX55k5d9XM6EsKePDInkLVrN7vPmJxc1qA==" crossorigin="anonymous"></script>
<script>
const chat_area = document.getElementById("chat_area")
const ws = new WebSocket('ws://localhost:8000/ws/chatroom/')
// const ws = new ReconnectingWebSocket('ws://localhost:8000/ws/chatroom/', {"Sec-WebSocket-Key": "eac1ce7c5bd217c34c11db0d0455122f8c76c452"}, {debug: true})
// const ws = new ReconnectingWebSocket('ws://localhost:8000/ws/chatroom/', "eac1ce7c5bd217c34c11db0d0455122f8c76c452", {debug: true})

ws.addEventListener('open', () => {
// ws.send(JSON.stringify({"chat": "I am goku"}))
})

setInterval(() => {
ws.send(JSON.stringify({"message_type": "chat", "text": "I am goku", "room": "1"}))
}, 2500)

ws.addEventListener('onmessage', (message) => {
console.log('received')
console.log(message)
chat_area.value += message.data + "\n"
})
</script>
</body>
</html>
Loading

0 comments on commit dce8640

Please sign in to comment.