Skip to content

Commit 1f88f39

Browse files
authored
Merge pull request #149 from UNLV-CS472-672/chat_extended
Chat refactorization
2 parents a822d76 + 3cb0d06 commit 1f88f39

File tree

6 files changed

+246
-66
lines changed

6 files changed

+246
-66
lines changed

backend/apps/chat/managers.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from django.db import models
2+
from django.utils.timezone import now
3+
4+
# https://chatgpt.com/share/67fda93c-b758-8005-b5d1-56978a0bda6e
5+
class ChatManager(models.Manager):
6+
def get_or_create_chat(self, user1, user2):
7+
"""Ensure consistent ordering of users in the chat model."""
8+
if user1.id > user2.id:
9+
user1, user2 = user2, user1
10+
chat, created = self.get_or_create(user1=user1, user2=user2)
11+
return chat, created
12+
13+
14+
class MessageManager(models.Manager):
15+
def create_message(self, chat, sender, content):
16+
"""Create a new message and update the chat's updatedAt field."""
17+
# Create the message
18+
message = self.create(chat=chat, sender=sender, content=content)
19+
20+
# Update the chat's updatedAt field
21+
chat.updatedAt = now()
22+
chat.save()
23+
24+
return message

backend/apps/chat/models.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,34 @@
11
from django.db import models
2-
from django.db import models
32
from django.contrib.auth.models import User
43
from django.utils.timezone import now
4+
from .managers import ChatManager, MessageManager
55

66
class Chat(models.Model):
77
user1 = models.ForeignKey(User, on_delete=models.CASCADE, related_name="chats_initiated")
88
user2 = models.ForeignKey(User, on_delete=models.CASCADE, related_name="chats_received")
99
createdAt = models.DateTimeField(auto_now_add=True)
1010
updatedAt = models.DateTimeField(auto_now=True)
1111

12+
objects = ChatManager()
13+
1214
class Meta:
1315
unique_together = ('user1', 'user2')
1416
ordering = ['-updatedAt']
1517

1618
def __str__(self):
1719
return f"Chat between {self.user1} and {self.user2}"
1820

19-
@classmethod
20-
def get_or_create_chat(cls, user1, user2):
21-
"""Ensure consistent ordering of users in the chat model."""
22-
if user1.id > user2.id:
23-
user1, user2 = user2, user1
24-
chat, created = cls.objects.get_or_create(user1=user1, user2=user2)
25-
return chat
26-
2721
class Message(models.Model):
2822
chat = models.ForeignKey(Chat, on_delete=models.CASCADE, related_name="messages")
2923
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sent_messages")
3024
content = models.TextField()
3125
timestamp = models.DateTimeField(default=now)
3226

27+
objects = MessageManager()
28+
3329
class Meta:
3430
ordering = ['timestamp']
3531

3632
def __str__(self):
3733
return f"From {self.sender} in Chat {self.chat.id} at {self.timestamp}"
3834

39-
def save(self, *args, **kwargs):
40-
"""Update the chat's updatedAt field whenever a new message is saved."""
41-
self.chat.updatedAt = now()
42-
self.chat.save()
43-
super().save(*args, **kwargs)
44-

backend/apps/chat/serializers.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from rest_framework import serializers
2+
from django.contrib.auth.models import User
3+
from .models import Chat, Message
4+
5+
# https://chatgpt.com/share/67fd9c70-d378-8005-8c39-b0453f0f790f
6+
class UserSerializer(serializers.ModelSerializer):
7+
class Meta:
8+
model = User
9+
fields = ['id', 'username']
10+
11+
class MessageSerializer(serializers.ModelSerializer):
12+
sender = UserSerializer(read_only=True)
13+
14+
class Meta:
15+
model = Message
16+
fields = ['id', 'chat', 'sender', 'content', 'timestamp']
17+
read_only_fields = ['timestamp']
18+
19+
class ChatSerializer(serializers.ModelSerializer):
20+
user1 = UserSerializer(read_only=True)
21+
user2 = UserSerializer(read_only=True)
22+
messages = MessageSerializer(many=True, read_only=True)
23+
24+
class Meta:
25+
model = Chat
26+
fields = ['id', 'user1', 'user2', 'createdAt', 'updatedAt', 'messages']

backend/apps/chat/tests.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,78 @@
1-
from django.test import TestCase
1+
from django.contrib.auth.models import User
2+
from django.urls import reverse
3+
from rest_framework import status
4+
from rest_framework.test import APITestCase, APIClient
5+
from .models import Chat, Message
26

3-
# Create your tests here.
7+
class ChatAPITestCase(APITestCase):
8+
9+
def setUp(self):
10+
self.user1 = User.objects.create_user(username='alice', password='pass1234')
11+
self.user2 = User.objects.create_user(username='bob', password='pass1234')
12+
self.user3 = User.objects.create_user(username='charlie', password='pass1234')
13+
14+
self.client = APIClient()
15+
# Obtain JWT tokens
16+
self.login_url = "/users/login/"
17+
response = self.client.post(self.login_url, {"username": "alice", "password": "pass1234"})
18+
self.assertEqual(response.status_code, status.HTTP_200_OK)
19+
self.refresh_token = response.json().get("refreshToken")
20+
self.access_token = response.json().get("accessToken")
21+
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {self.access_token}")
22+
23+
def test_create_chat(self):
24+
self.assertEqual(Chat.objects.count(), 0)
25+
url = reverse('chat-list')
26+
data = {'user2': self.user2.id}
27+
response = self.client.post(url, data)
28+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
29+
self.assertEqual(Chat.objects.count(), 1)
30+
31+
def test_prevent_duplicate_chat(self):
32+
Chat.objects.get_or_create_chat(self.user1, self.user2)
33+
url = reverse('chat-list')
34+
data = {'user2': self.user2.id}
35+
response = self.client.post(url, data)
36+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
37+
38+
def test_list_user_chats(self):
39+
Chat.objects.get_or_create_chat(self.user1, self.user2)
40+
Chat.objects.get_or_create_chat(self.user1, self.user3)
41+
url = reverse('chat-list')
42+
response = self.client.get(url)
43+
self.assertEqual(response.status_code, status.HTTP_200_OK)
44+
self.assertEqual(len(response.data), 2)
45+
# ensure that the receivers match
46+
response_receiver_ids = {chat['user2']['id'] for chat in response.data}
47+
expected_receiver_ids = {self.user2.id, self.user3.id}
48+
self.assertSetEqual(response_receiver_ids, expected_receiver_ids)
49+
50+
def test_send_message(self):
51+
chat, _ = Chat.objects.get_or_create_chat(self.user1, self.user2)
52+
url = reverse('message-list')
53+
data = {'chat': chat.id, 'content': 'Hello Bob!'}
54+
response = self.client.post(url, data)
55+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
56+
self.assertEqual(Message.objects.count(), 1)
57+
message = Message.objects.first()
58+
self.assertEqual(message.content, 'Hello Bob!')
59+
self.assertEqual(message.sender, self.user1)
60+
61+
62+
def test_list_chat_messages(self):
63+
chat, created = Chat.objects.get_or_create_chat(self.user1, self.user2)
64+
Message.objects.create(chat=chat, sender=self.user1, content='Hi Bob!')
65+
Message.objects.create(chat=chat, sender=self.user2, content='Hey Alice!')
66+
67+
url = reverse('chat-messages', kwargs={'pk': chat.id})
68+
response = self.client.get(url)
69+
self.assertEqual(response.status_code, status.HTTP_200_OK)
70+
self.assertEqual(len(response.data), 2)
71+
self.assertEqual(response.data[0]['content'], 'Hi Bob!')
72+
self.assertEqual(response.data[1]['content'], 'Hey Alice!')
73+
74+
def test_unauthenticated_access(self):
75+
self.client.logout()
76+
url = reverse('chat-list')
77+
response = self.client.get(url)
78+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

backend/apps/chat/urls.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
# urls.py
2-
from django.urls import path
3-
from .views import select_send_message, send_message, chat_list, chat_detail
2+
from django.urls import path, include
3+
# from .views import select_send_message, send_message, chat_list, chat_detail
4+
# https://chatgpt.com/share/67fd9c70-d378-8005-8c39-b0453f0f790f
5+
from rest_framework.routers import DefaultRouter
6+
from .views import ChatViewSet, MessageViewSet
7+
8+
router = DefaultRouter()
9+
router.register(r'chats', ChatViewSet, basename='chat')
10+
router.register(r'messages', MessageViewSet, basename='message')
411

512
urlpatterns = [
6-
path('', chat_list, name='chat_list'),
7-
path('<int:chat_id>/', chat_detail, name='chat_detail'),
8-
path('send/', select_send_message, name='select_send_message'),
9-
path('send/<int:user_id>/', send_message, name='send_message'),
13+
path('', include(router.urls)),
14+
# path('legacy/', chat_list, name='chat_list'),
15+
# path('<int:chat_id>/', chat_detail, name='chat_detail'),
16+
# path('send/', select_send_message, name='select_send_message'),
17+
# path('send/<int:user_id>/', send_message, name='send_message'),
1018
]
1119

backend/apps/chat/views.py

Lines changed: 100 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,108 @@
44
from .models import Chat, User
55
from .forms import MessageForm
66

7-
@login_required
8-
def select_send_message(request):
9-
"""Allow the user to select a recipient for messaging."""
10-
if request.method == "POST":
11-
user_id = request.POST.get("receiver")
12-
return redirect(reverse('send_message', args=[user_id])) # Redirect to send_message view
13-
14-
users = User.objects.exclude(id=request.user.id) # Exclude the logged-in user
15-
return render(request, 'select_send_message.html', {'users': users})
16-
17-
@login_required
18-
def send_message(request, user_id):
19-
receiver = get_object_or_404(User, id=user_id)
20-
chat = Chat.get_or_create_chat(request.user, receiver)
21-
22-
if request.method == "POST":
23-
form = MessageForm(request.POST)
24-
if form.is_valid():
25-
message = form.save(commit=False)
26-
message.sender = request.user
27-
message.chat = chat
28-
message.save()
29-
return redirect('chat_detail', chat_id=chat.id)
30-
# Handle GET request by preloading the message form
31-
else:
32-
form = MessageForm()
33-
34-
return render(request, 'send_message.html', {'form': form, 'receiver': receiver})
35-
36-
@login_required
37-
def chat_list(request):
38-
chats = Chat.objects.filter(user1=request.user) | Chat.objects.filter(user2=request.user)
39-
return render(request, 'chat_list.html', {'chats': chats})
40-
41-
@login_required
42-
def chat_detail(request, chat_id):
43-
chat = get_object_or_404(Chat, id=chat_id)
7+
# https://chatgpt.com/share/67fd9c70-d378-8005-8c39-b0453f0f790f
8+
from rest_framework import viewsets, permissions, serializers
9+
from rest_framework.response import Response
10+
from rest_framework.decorators import action
11+
from django.contrib.auth.models import User
12+
from django.db.models import Q
13+
from .models import Chat, Message
14+
from .serializers import ChatSerializer, MessageSerializer
15+
16+
class ChatViewSet(viewsets.ModelViewSet):
17+
queryset = Chat.objects.all()
18+
serializer_class = ChatSerializer
19+
permission_classes = [permissions.IsAuthenticated]
20+
21+
def get_queryset(self):
22+
"""Restrict to chats where the user is a participant."""
23+
user = self.request.user
24+
return Chat.objects.filter(Q(user1=user) | Q(user2=user)).order_by('-updatedAt')
25+
# return Chat.objects.filter(user1=user).union(Chat.objects.filter(user2=user)).order_by('-updatedAt')
26+
27+
def perform_create(self, serializer):
28+
"""Prevent duplicate chats, enforce user1-user2 ordering."""
29+
user1 = self.request.user
30+
user2_id = self.request.data.get('user2')
31+
if not user2_id:
32+
raise serializers.ValidationError({"user2": "This field is required."})
33+
34+
try:
35+
user2 = User.objects.get(id=user2_id)
36+
except User.DoesNotExist:
37+
raise serializers.ValidationError({"user2": "User not found."})
38+
39+
chat, created = Chat.objects.get_or_create_chat(user1, user2)
40+
if not created:
41+
raise serializers.ValidationError("Chat already exists.")
42+
serializer.instance = chat
43+
44+
@action(detail=True, methods=['get'])
45+
def messages(self, request, pk=None):
46+
"""List messages for a chat."""
47+
chat = self.get_object()
48+
messages = chat.messages.all()
49+
serializer = MessageSerializer(messages, many=True)
50+
return Response(serializer.data)
51+
52+
class MessageViewSet(viewsets.ModelViewSet):
53+
queryset = Message.objects.all()
54+
serializer_class = MessageSerializer
55+
permission_classes = [permissions.IsAuthenticated]
56+
57+
def perform_create(self, serializer):
58+
serializer.save(sender=self.request.user)
59+
60+
# =============================================
61+
# NOTE: ENPOINTS BELOW ARE CONSIDERED LEGACY
62+
# =============================================
63+
64+
# @login_required
65+
# def select_send_message(request):
66+
# """Allow the user to select a recipient for messaging."""
67+
# if request.method == "POST":
68+
# user_id = request.POST.get("receiver")
69+
# return redirect(reverse('send_message', args=[user_id])) # Redirect to send_message view
70+
71+
# users = User.objects.exclude(id=request.user.id) # Exclude the logged-in user
72+
# return render(request, 'select_send_message.html', {'users': users})
73+
74+
# @login_required
75+
# def send_message(request, user_id):
76+
# receiver = get_object_or_404(User, id=user_id)
77+
# chat = Chat.get_or_create_chat(request.user, receiver)
78+
79+
# if request.method == "POST":
80+
# form = MessageForm(request.POST)
81+
# if form.is_valid():
82+
# message = form.save(commit=False)
83+
# message.sender = request.user
84+
# message.chat = chat
85+
# message.save()
86+
# return redirect('chat_detail', chat_id=chat.id)
87+
# # Handle GET request by preloading the message form
88+
# else:
89+
# form = MessageForm()
90+
91+
# return render(request, 'send_message.html', {'form': form, 'receiver': receiver})
92+
93+
# @login_required
94+
# def chat_list(request):
95+
# chats = Chat.objects.filter(user1=request.user) | Chat.objects.filter(user2=request.user)
96+
# return render(request, 'chat_list.html', {'chats': chats})
97+
98+
# @login_required
99+
# def chat_detail(request, chat_id):
100+
# chat = get_object_or_404(Chat, id=chat_id)
44101

45-
# Ensure the logged-in user is part of this chat
46-
if request.user != chat.user1 and request.user != chat.user2:
47-
return redirect('chat_list')
102+
# # Ensure the logged-in user is part of this chat
103+
# if request.user != chat.user1 and request.user != chat.user2:
104+
# return redirect('chat_list')
48105

49-
messages = chat.messages.all()
50-
form = MessageForm()
106+
# messages = chat.messages.all()
107+
# form = MessageForm()
51108

52-
return render(request, 'chat_detail.html', {'chat': chat, 'messages': messages, 'form': form})
109+
# return render(request, 'chat_detail.html', {'chat': chat, 'messages': messages, 'form': form})
53110

54111

0 commit comments

Comments
 (0)