Skip to content

Commit

Permalink
Merge pull request #116 from gcivil-nyu-org/added-chat-functionality
Browse files Browse the repository at this point in the history
added chat functionality
  • Loading branch information
zackaidja committed Nov 12, 2023
2 parents 676a711 + 2318cfe commit 215728e
Show file tree
Hide file tree
Showing 19 changed files with 358 additions and 4 deletions.
6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ install:

script:
- python -m black . --check
- python -m flake8 --ignore=E303,E501 --max-line-length=88
- python -m flake8 --ignore=E303,E501,W503 --max-line-length=88
- python -m coverage run manage.py test
env:
global:
- AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
- DB_KEY=DB_KEY
- DB_KEYP=DB_KEYP
- DB_KEY=$DB_KEY
- DB_KEYP=$DB_KEYP


after_success:
Expand Down
16 changes: 15 additions & 1 deletion CheerUp/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,21 @@
import os

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator

import chat.routing

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CheerUp.settings")

application = get_asgi_application()
django_asgi_app = get_asgi_application()

application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(chat.routing.websocket_urlpatterns))
),
}
)
6 changes: 6 additions & 0 deletions CheerUp/routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from channels.routing import ProtocolTypeRouter, URLRouter
from chat import routing as core_routing

application = ProtocolTypeRouter(
{"websocket": URLRouter(core_routing.websocket_urlpatterns)}
)
24 changes: 24 additions & 0 deletions CheerUp/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"127.0.0.1",
]

ASGI_APPLICATION = "CheerUp.asgi.application"


DATABASES = {
"default": {
Expand All @@ -54,6 +56,7 @@
# Application definition

INSTALLED_APPS = [
"daphne",
"location.apps.LocationConfig",
"events.apps.EventsConfig",
"django.contrib.admin",
Expand All @@ -63,6 +66,8 @@
"django.contrib.messages",
"django.contrib.staticfiles",
"accounts",
"channels",
"chat",
]

MIDDLEWARE = [
Expand Down Expand Up @@ -144,3 +149,22 @@

# Where to redirect after a successful login
LOGIN_REDIRECT_URL = "/events/"

CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "ec2-3-80-98-101.compute-1.amazonaws.com:6379",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}

CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("ec2-3-80-98-101.compute-1.amazonaws.com", 6379)],
},
},
}
1 change: 1 addition & 0 deletions CheerUp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@
path("locations/", include("location.urls")),
path("accounts/", include("accounts.urls")),
path("accounts/", include("django.contrib.auth.urls")),
path("chat/", include("chat.urls")),
]
Empty file added chat/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions chat/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Register your models here.
from django.contrib import admin

from .models import Message

admin.site.register(Message)
6 changes: 6 additions & 0 deletions chat/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class ChatConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "chat"
48 changes: 48 additions & 0 deletions chat/consumers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# consumers.py

import json
from channels.generic.websocket import AsyncWebsocketConsumer
from .models import Message
from asgiref.sync import sync_to_async
from django.contrib.auth.models import User # <-- Add this import


class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
self.room_group_name = f"chat_{self.room_name}"
# Join room group
await self.channel_layer.group_add(self.room_group_name, self.channel_name)

await self.accept()

async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)

async def receive(self, text_data):
data = json.loads(text_data)
message = data["message"]
sender_id = data["sender_id"]
recipient_id = data["recipient_id"]

await self.save_message(sender_id, recipient_id, message)

# Send message to room group
await self.channel_layer.group_send(
self.room_group_name, {"type": "chat.message", "message": message}
)

@sync_to_async
def save_message(self, sender_id, recipient_id, message):
sender = User.objects.get(id=sender_id)
recipient = User.objects.get(id=recipient_id)

Message.objects.create(sender=sender, recipient=recipient, content=message)

# Receive message from room group
async def chat_message(self, event):
message = event["message"]

# Send message to WebSocket
await self.send(text_data=json.dumps({"message": message}))
9 changes: 9 additions & 0 deletions chat/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# chat/forms.py

from django import forms


class MessageForm(forms.Form):
message = forms.CharField(
widget=forms.TextInput(attrs={"placeholder": "Type your message..."})
)
48 changes: 48 additions & 0 deletions chat/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 4.2.5 on 2023-11-12 01:24

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


class Migration(migrations.Migration):
initial = True

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

operations = [
migrations.CreateModel(
name="Message",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("content", models.TextField()),
("timestamp", models.DateTimeField(auto_now_add=True)),
(
"recipient",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="received_messages",
to=settings.AUTH_USER_MODEL,
),
),
(
"sender",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="sent_messages",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]
Empty file added chat/migrations/__init__.py
Empty file.
18 changes: 18 additions & 0 deletions chat/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# chat/models.py

from django.db import models
from django.contrib.auth.models import User


class Message(models.Model):
sender = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="sent_messages"
)
recipient = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="received_messages"
)
content = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)

def __str__(self):
return f"{self.sender.username} to {self.recipient.username}: {self.content}"
7 changes: 7 additions & 0 deletions chat/routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import path

from . import consumers

websocket_urlpatterns = [
path("ws/chat/<str:room_name>/", consumers.ChatConsumer.as_asgi()),
]
52 changes: 52 additions & 0 deletions chat/templates/chat/chat.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!-- chat/chat.html -->

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat with {{ recipient.username }}</title>
</head>
<body>
<h2>Chat with {{ recipient.username }}</h2>
<div id="chat-log">
{% for message in messages %}
<p>{{ message.sender.username }}: {{ message.content }}</p>
{% endfor %}
</div>
<form id="chat-form" method="post" action="">
{% csrf_token %}
{{ form.message }}
<button type="submit">Send</button>
</form>

<script>
// WebSocket connection setup
const chatSocket = new WebSocket(
'ws://' + window.location.host + '/ws/chat/{{ recipient.id }}/'
);

// Handle incoming messages from WebSocket
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
document.querySelector('#chat-log').innerHTML += '<p>' + data.message + '</p>';
};

// Handle form submission
document.querySelector('#chat-form').addEventListener('submit', function(e) {
e.preventDefault();
const messageInput = document.querySelector('#id_message');
const message = messageInput.value.trim();

if (message) {
chatSocket.send(JSON.stringify({
'message': message,
'sender_id': {{ request.user.id }},
'recipient_id': {{ recipient.id }}
}));
messageInput.value = '';
}
});
</script>
</body>
</html>
69 changes: 69 additions & 0 deletions chat/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# chat/tests.py

from django.test import TestCase
from django.contrib.auth.models import User
from django.urls import reverse
from .models import Message


class ChatViewTests(TestCase):
def setUp(self):
# Create users for testing
self.user1 = User.objects.create_user(username="user1", password="password1")
self.user2 = User.objects.create_user(username="user2", password="password2")

# Create a message for testing
self.message = Message.objects.create(
sender=self.user1, recipient=self.user2, content="Test message"
)

# URL for the chat view
self.url = reverse(
"chats:chat-with-user", kwargs={"recipient_id": self.user2.id}
)

def test_chat_view_requires_authentication(self):
# Ensure the view requires authentication
response = self.client.get(self.url)
self.assertEqual(response.status_code, 302) # Redirect to login page

def test_chat_view_accessible_after_authentication(self):
# Log in user1
self.client.login(username="user1", password="password1")

# Ensure the view is accessible after authentication
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)

def test_chat_view_displays_messages(self):
# Log in user1
self.client.login(username="user1", password="password1")

# Ensure the view displays messages
response = self.client.get(self.url)
self.assertContains(response, "Test message")

def test_chat_post_form_creates_message(self):
# Log in user1
self.client.login(username="user1", password="password1")

# Ensure posting the form creates a new message
response = self.client.post(self.url, {"message": "New test message"})
self.assertEqual(response.status_code, 302) # Redirect after successful post

# Verify that the new message exists in the database
new_message = Message.objects.get(content="New test message")
self.assertEqual(new_message.sender, self.user1)
self.assertEqual(new_message.recipient, self.user2)

def test_chat_post_form_redirects_correctly(self):
# Log in user1
self.client.login(username="user1", password="password1")

# Ensure posting the form redirects to the correct chat page
response = self.client.post(self.url, {"message": "Another message"})
self.assertEqual(response.status_code, 302) # Redirect after successful post
expected_redirect_url = reverse(
"chats:chat-with-user", kwargs={"recipient_id": self.user2.id}
)
self.assertRedirects(response, expected_redirect_url)
8 changes: 8 additions & 0 deletions chat/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.urls import path
from . import views

app_name = "chats"
urlpatterns = [
path("<str:recipient_id>/", views.chatting, name="chat-with-user"),
# Add other chat app URLs here
]

0 comments on commit 215728e

Please sign in to comment.