chat package
Subpackages
Submodules
chat.admin module
- class chat.admin.MessageAdmin(model, admin_site)[source]
Bases:
ModelAdmin- list_display = ('sender_email', 'receiver_email', 'timestamp', 'is_read')
- list_filter = ('is_read', 'timestamp')
- search_fields = ('sender_email', 'receiver_email', 'content')
- ordering = ('-timestamp',)
- property media
chat.apps module
- class chat.apps.ChatConfig(app_name, app_module)[source]
Bases:
AppConfig- default_auto_field = 'django_mongodb_backend.fields.ObjectIdAutoField'
- name = 'chat'
chat.consumers module
- class chat.consumers.ChatConsumer(*args, **kwargs)[source]
Bases:
AsyncWebsocketConsumerAsync WebSocket consumer for real-time chat between matched mentor-mentee pairs.
Manages the full WebSocket lifecycle — connection authentication, message routing, typing indicators, read receipts, and graceful disconnection. All database operations are wrapped with
@database_sync_to_asyncto avoid blocking the async event loop.Connection URL:
ws://<host>/ws/chat/<partner_email>/
Authentication: relies on the Django session cookie attached to the WebSocket handshake (
credentials: 'include'on the client). The session is resolved by Django Channels’AuthMiddlewareStack.Close codes:
4001— WebSocket closed because the user is not authenticated.4003— WebSocket closed because the user has no active match, or is not matched with the requested partner.
Inbound message types (JSON sent from the client):
chat_message—{"type": "chat_message", "content": "Hello!"}typing—{"type": "typing", "is_typing": true}mark_read—{"type": "mark_read"}
Outbound message types (JSON pushed to the client):
connection_established— Sent immediately after a successful handshake.chat_message— A new message broadcast to both participants.typing— Typing indicator forwarded to the other participant only.read_receipt— Acknowledgement that messages were read, forwarded to the other participant only.force_disconnect— Sent before the server closes the connection (e.g. after an unmatch).error— Sent to the originating client when a message cannot be processed.
- async connect()[source]
Handle an incoming WebSocket connection request.
Performs the following checks in order before accepting:
Authentication — closes with code
4001if the scope user is absent or unauthenticated.Active match — closes with code
4003if the user has no current match (role-aware: checksisMatchedfor mentees,current_menteesfor mentors).Match relationship — closes with code
4003if the user is not specifically matched with<partner_email>from the URL.
On success, joins the deterministic channel-layer group for the pair (derived from sorted emails), accepts the connection, and sends a
connection_establishedmessage.- Raises:
None – All error conditions result in
self.close()rather than raised exceptions.
- async disconnect(close_code)[source]
Handle WebSocket disconnection.
Removes this channel from the room group so it no longer receives broadcast events. Safe to call even if
connectnever completed (guarded byhasattrcheck onroom_group_name).- Parameters:
close_code (int) – The WebSocket close code sent by the client or server. Not used internally but required by the Channels interface.
- async receive(text_data)[source]
Route an incoming WebSocket frame to the appropriate handler.
Parses the JSON payload and dispatches on
type:'chat_message'→handle_chat_message()'typing'→handle_typing()'mark_read'→handle_mark_read()anything else →
send_error()with'Unknown message type'
JSON parse errors and unexpected handler exceptions are caught and forwarded to the client via
send_error()rather than crashing the consumer.- Parameters:
text_data (str) – Raw text payload received from the WebSocket client.
- async handle_chat_message(data)[source]
Validate, persist, and broadcast a chat message.
Validates that
contentis non-empty and does not exceed 5 000 characters, then delegates persistence tosave_message()and broadcasts the result to all members of the room group viachat_message_broadcast.- Parameters:
data (dict) – Parsed JSON payload from the client. Expected to contain a
'content'key with the message text.
- async handle_typing(data)[source]
Broadcast a typing indicator to all room participants.
The broadcast is sent to the entire room group; filtering so that the originating client does not receive its own indicator is handled in
typing_broadcast().- Parameters:
data (dict) – Parsed JSON payload from the client. Expected to contain an
'is_typing'boolean key. Defaults toFalseif absent.
- async handle_mark_read()[source]
Mark all unread messages from the partner as read and notify the room.
Delegates the database update to
mark_messages_read(), then broadcasts aread_receipt_broadcastevent to the room group. Filtering so that the reader does not receive their own receipt is handled inread_receipt_broadcast().
- async chat_message_broadcast(event)[source]
Channel-layer event handler: forward a chat message to this WebSocket.
Called by the Channels layer when a
chat_message_broadcastevent is dispatched to the room group. Forwards the payload to the connected client as achat_messageJSON frame.- Parameters:
event (dict) – Channel-layer event dict containing
message_id,sender_email,sender_name,content, andtimestamp.
- async typing_broadcast(event)[source]
Channel-layer event handler: forward a typing indicator to this WebSocket.
Called by the Channels layer when a
typing_broadcastevent is dispatched to the room group. The indicator is suppressed for the originating sender so that a user does not receive their own typing status.- Parameters:
event (dict) – Channel-layer event dict containing
sender_emailandis_typing.
- async read_receipt_broadcast(event)[source]
Channel-layer event handler: forward a read receipt to this WebSocket.
Called by the Channels layer when a
read_receipt_broadcastevent is dispatched to the room group. The receipt is suppressed for the reader themselves so that only the other participant is notified.- Parameters:
event (dict) – Channel-layer event dict containing
reader_emailandcount.
- async force_disconnect(event)[source]
Channel-layer event handler: notify the client and close the connection.
Called by the Channels layer when a
force_disconnectevent is dispatched to the room group — for example, after two users are unmatched. Sends aforce_disconnectframe to the client before closing so the frontend can display an appropriate message.- Parameters:
event (dict) – Channel-layer event dict optionally containing a
'reason'string. Defaults to'disconnected'if absent.
- async send_error(message)[source]
Send an error frame to the connected WebSocket client.
Used internally to surface validation failures and unexpected exceptions without closing the connection, allowing the client to recover and retry.
- Parameters:
message (str) – Human-readable error description to include in the
'message'field of the outbound JSON frame.
- async validate_match()[source]
Verify that the session user and
partner_emailare actively matched.Applies the same role-aware relationship rules used in
matching/views.pyandchat/views.py:MENTEE — valid if
isMatchedisTrue,matchedMentorEmailequalspartner_email, and the partner holds theMENTORrole.MENTOR — valid if
partner_emailis incurrent_menteesand the partner holds theMENTEErole.
Wrapped with
@database_sync_to_asyncso it can be safely awaited from the asyncconnectmethod without blocking the event loop.- Returns:
Trueif the relationship is valid,Falseif the partner does not exist or the match condition is not satisfied.- Return type:
- async save_message(content)[source]
Persist a new
Messageto the database.Wrapped with
@database_sync_to_asyncso it can be safely awaited from the async consumer without blocking the event loop.- Parameters:
content (str) – Validated plain-text message body to store.
- Returns:
The newly created
Messageinstance.- Return type:
chat.models.Message
- async mark_messages_read()[source]
Bulk-mark all unread messages from the partner as read.
Delegates to
mark_as_read(), which issues a singleUPDATEquery. Wrapped with@database_sync_to_asyncso it can be safely awaited from the async consumer.- Returns:
The number of
Messagerows updated.- Return type:
chat.middleware module
chat.models module
- class chat.models.Message(*args, **kwargs)[source]
Bases:
ModelRepresents a single chat message exchanged between a matched mentor and mentee.
Uses email addresses as identifiers to stay consistent with the matching layer (
matchedMentorEmail,current_mentees). Messages are ordered bytimestampascending by default, and the composite indexes on(sender_email, receiver_email)and(receiver_email, is_read)support efficient conversation retrieval and unread-count queries respectively.- sender_email
Email address of the user who sent the message.
- Type:
EmailField
- receiver_email
Email address of the intended recipient.
- Type:
EmailField
- content
Plain-text body of the message.
- Type:
TextField
- timestamp
When the message was created. Defaults to the current time at the moment of instantiation.
- Type:
DateTimeField
- is_read
Whether the recipient has read the message. Defaults to
False.- Type:
BooleanField
- sender_email
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- receiver_email
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- content
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- timestamp
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- is_read
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- classmethod get_conversation(email1, email2, limit=50, offset=0)[source]
Retrieve paginated messages exchanged between two users.
Returns messages in descending timestamp order (newest first) so that pagination offsets naturally surface the most recent content first. Both directions of the conversation (
email1 → email2andemail2 → email1) are included.- Parameters:
email1 (str) – Email address of one participant in the conversation.
email2 (str) – Email address of the other participant in the conversation.
limit (int) – Maximum number of messages to return. Defaults to
50.offset (int) – Number of messages to skip before returning results, used for pagination. Defaults to
0.
- Returns:
Queryset of
Messageinstances ordered newest-first, sliced to[offset : offset + limit].- Return type:
django.db.models.QuerySet
Example:
>>> page1 = Message.get_conversation('mentee@g.ucla.edu', 'mentor@ucla.edu') >>> page2 = Message.get_conversation('mentee@g.ucla.edu', 'mentor@ucla.edu', offset=50)
- classmethod get_unread_count(receiver_email, sender_email=None)[source]
Count unread messages for a given recipient.
When
sender_emailis provided the count is scoped to messages from that specific sender only, which is useful for per-conversation unread badges. When omitted, all unread messages across every sender are counted.- Parameters:
- Returns:
Total number of unread messages matching the query.
- Return type:
Example:
>>> Message.get_unread_count('mentee@g.ucla.edu') 7 >>> Message.get_unread_count('mentee@g.ucla.edu', sender_email='mentor@ucla.edu') 3
- classmethod mark_as_read(receiver_email, sender_email)[source]
Mark all unread messages from a specific sender as read.
Issues a single bulk
UPDATEquery rather than loading and saving individual instances, making it efficient for conversations with a large number of unread messages.- Parameters:
- Returns:
The number of
Messagerows updated.- Return type:
Example:
>>> updated = Message.mark_as_read('mentee@g.ucla.edu', 'mentor@ucla.edu') >>> print(f'{updated} messages marked as read.')
- exception DoesNotExist
Bases:
ObjectDoesNotExist
- exception MultipleObjectsReturned
Bases:
MultipleObjectsReturned
- exception NotUpdated
Bases:
ObjectNotUpdated,DatabaseError
- get_next_by_timestamp(*, field=<django.db.models.fields.DateTimeField: timestamp>, is_next=True, **kwargs)
- get_previous_by_timestamp(*, field=<django.db.models.fields.DateTimeField: timestamp>, is_next=False, **kwargs)
- id
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- objects = <django.db.models.manager.Manager object>
chat.routing module
chat.tests module
- chat.tests.make_mentee(email)[source]
- chat.tests.make_mentor(email)[source]
- chat.tests.match(mentee, mentor)[source]
Wire up both sides of a mentee-mentor match.
- chat.tests.send(sender_email, receiver_email, content='hello')[source]
- class chat.tests.UnmatchChatDeletionTests(methodName='runTest')[source]
Bases:
TestCase- setUp()[source]
Hook method for setting up the test fixture before exercising it.
- test_mentee_unmatch_deletes_messages()[source]
- test_mentor_unmatch_deletes_messages()[source]
- test_unmatch_only_deletes_between_pair()[source]
- class chat.tests.AccountDeletionChatDeletionTests(methodName='runTest')[source]
Bases:
TestCase- test_mentee_deletion_deletes_sent_messages()[source]
- test_mentee_deletion_deletes_received_messages()[source]
- test_mentor_deletion_deletes_all_messages()[source]
- test_deletion_only_deletes_own_messages()[source]
- class chat.tests.ChatModelMethodTests(methodName='runTest')[source]
Bases:
TestCase- setUp()[source]
Hook method for setting up the test fixture before exercising it.
- test_get_unread_count()[source]
Verifies unread counts are calculated correctly per user
- test_mark_as_read()[source]
Verifies marking messages as read updates the database
- test_get_conversation_pagination()[source]
Verifies conversation history is retrieved in the correct order with limits
- class chat.tests.ChatAPIEndpointTests(methodName='runTest')[source]
Bases:
TestCase- setUp()[source]
Hook method for setting up the test fixture before exercising it.
- test_get_chat_partners_success()[source]
Verifies a matched user can fetch their chat partners list
- test_unauthorized_message_sending_blocked()[source]
Ensures an unmatched user gets a 403 Forbidden when trying to send a message
- test_authorized_message_sending_success()[source]
Ensures a matched user can successfully send a message via the API
- test_unauthenticated_access_blocked()[source]
Ensures all endpoints return 401 Unauthorized if not logged in
- test_get_chat_history_success()[source]
Verifies a matched user can fetch their paginated chat history in chronological order
- class chat.tests.ChatWebSocketTests(methodName='runTest')[source]
Bases:
TransactionTestCase- setUp()[source]
Hook method for setting up the test fixture before exercising it.
- async test_valid_connection_and_messaging()[source]
Ensures matched users can connect and send live messages
- async test_unauthorized_connection_rejected()[source]
Ensures unmatched users are instantly rejected and disconnected
chat.urls module
chat.views module
- chat.views.has_active_match(user)[source]
Determine whether a user currently has at least one active match.
Checks match state based on role, bypassing the
isMatchedflag alone to guard against stale flag values:MENTEE — requires both
isMatched=Trueand a non-emptymatchedMentorEmail.MENTOR — requires a non-empty
current_menteeslist.
- Parameters:
user (users.models.User) – The user whose match state is being evaluated.
- Returns:
Trueif the user has at least one active match,Falsefor any other role or if match fields are absent.- Return type:
Example:
>>> has_active_match(mentee_user) True >>> has_active_match(unmatched_mentor) False
- chat.views.validate_match_relationship(user, partner_email)[source]
Verify that
useris permitted to chat with the given partner.Enforces the same match-relationship rules used in
matching/views.py:MENTEE — may only chat with the mentor stored in
matchedMentorEmail, and that mentor must hold theMENTORrole.MENTOR — may only chat with emails present in
current_mentees, and each must hold theMENTEErole.
- Parameters:
user (users.models.User) – The authenticated user initiating the chat action.
partner_email (str) – Email address of the intended chat partner.
- Returns:
A three-tuple of:
is_valid(bool) —Trueif the relationship is permitted.error_message(str or None) — Human-readable reason for rejection, orNoneon success.partner_user(Useror None) — The resolved partner instance on success, orNoneon failure.
- Return type:
Example:
>>> is_valid, error, partner = validate_match_relationship(mentee, 'mentor@ucla.edu') >>> if not is_valid: ... return JsonResponse({'error': error}, status=403)
- chat.views.get_room_name(email1, email2)[source]
Derive a deterministic Django Channels room name for two participants.
Sorts the two email addresses lexicographically before building the name so that
get_room_name(A, B)andget_room_name(B, A)always return the same string.@and.characters are replaced to satisfy channel-layer naming constraints.- Parameters:
- Returns:
A channel-layer-safe room name string, e.g.
'chat_jdoe_at_g_dot_ucla_dot_edu_mentor_at_ucla_dot_edu'.- Return type:
Example:
>>> get_room_name('b@ucla.edu', 'a@ucla.edu') == get_room_name('a@ucla.edu', 'b@ucla.edu') True
- chat.views.send_message(request)[source]
Send a text message from the authenticated user to their matched partner.
Persists the message via
Messageand attempts a real-time broadcast to the relevant Django Channels group. If the WebSocket broadcast fails the message is still saved and a success response is returned — the failure is silently swallowed to avoid disrupting the REST flow.Pre-conditions: both sender and receiver must be actively matched (validated by
has_active_match()andvalidate_match_relationship()).Post-conditions: the
Messagerow is written to the database and, on a successful broadcast, the payload appears in the recipient’s open chat window via WebSocket.- Parameters:
request (django.http.HttpRequest) – The incoming HTTP POST request containing a JSON body with
receiver_emailandcontent. Must belong to an authenticated user.- Returns:
HTTP 200 with
success,message_id, andtimestampon success.HTTP 400 if
receiver_emailis absent,contentis empty or exceeds 5 000 characters, or the body is not valid JSON.HTTP 401 if the request is unauthenticated.
HTTP 403 if the sender has no active match or the recipient is not the sender’s matched partner.
HTTP 500 for unexpected server errors.
- Return type:
Request body (JSON):
{ "receiver_email": "mentor@ucla.edu", "content": "Hi, can we meet this week?" }
Success response (HTTP 200):
{ "success": true, "message_id": "42", "timestamp": "2024-01-15T10:30:00+00:00" }
- chat.views.get_chat_history(request, partner_email)[source]
Return paginated chat history between the authenticated user and a matched partner.
Messages are fetched newest-first from
Message.get_conversationand then reversed before serialization so the response list is in chronological (oldest-first) order, matching typical chat UI expectations. Ahas_moreflag indicates whether a subsequent page is available.- Parameters:
request (django.http.HttpRequest) – The incoming HTTP GET request. Accepts optional
limitandoffsetquery parameters. Must belong to an authenticated, actively matched user.partner_email (str) – Email address of the chat partner, supplied as a URL path parameter.
- Returns:
HTTP 200 with
messages,partnerprofile data, andhas_moreon success.HTTP 401 if the request is unauthenticated.
HTTP 403 if the user has no active match or is not matched with
partner_email.
- Return type:
Query parameters:
limit— Maximum messages to return. Capped at100. Defaults to50.offset— Number of messages to skip for pagination. Defaults to0.
Success response (HTTP 200):
{ "messages": [ { "id": "1", "sender_email": "mentor@ucla.edu", "sender_name": "John Smith", "content": "Welcome!", "timestamp": "2024-01-15T09:00:00+00:00", "is_read": true, "is_mine": false } ], "partner": { "email": "mentor@ucla.edu", "name": "John Smith", "major": ["Computer Science"], ... }, "has_more": false }
- chat.views.get_unread_counts(request)[source]
Return per-partner unread message counts for the authenticated user.
Behaviour differs by role:
MENTEE — returns a single entry keyed by
matchedMentorEmail, if matched.MENTOR — returns one entry per email in
current_mentees.
- Parameters:
request (django.http.HttpRequest) – The incoming HTTP GET request. Must belong to an authenticated user.
- Returns:
HTTP 200 with
counts(dict mapping partner email → unread integer) andtotal(sum of all counts) on success.HTTP 401 if the request is unauthenticated.
- Return type:
Success response (HTTP 200):
{ "counts": { "mentor@ucla.edu": 3 }, "total": 3 }
- chat.views.mark_messages_read(request, partner_email)[source]
Mark all unread messages from a matched partner as read.
Delegates to
Message.mark_as_read, which issues a single bulkUPDATEquery. The match relationship is validated before any write is performed.- Parameters:
request (django.http.HttpRequest) – The incoming HTTP POST request. No body is required. Must belong to an authenticated, actively matched user.
partner_email (str) – Email address of the partner whose messages should be marked as read, supplied as a URL path parameter.
- Returns:
HTTP 200 with
marked_read(number of rows updated) on success.HTTP 401 if the request is unauthenticated.
HTTP 403 if the user has no active match or is not matched with
partner_email.
- Return type:
Success response (HTTP 200):
{ "marked_read": 4 }
- chat.views.get_chat_partners(request)[source]
Return all chat partners for the authenticated user with previews and unread counts.
Behaviour differs by role:
MENTEE — returns at most one partner (the matched mentor).
MENTOR — returns one entry per email in
current_mentees.
Each partner entry includes a truncated last-message preview and an unread count. Partners with no message history have
lastMessageandlastMessageTimeset tonull. The list is sorted bylastMessageTimedescending (most recently active first); partners with no messages sort to the end.Stale emails in
matchedMentorEmailorcurrent_menteesthat no longer correspond to an existingUserare silently skipped.- Parameters:
request (django.http.HttpRequest) – The incoming HTTP GET request. Must belong to an authenticated user.
- Returns:
HTTP 200 with a
partnerslist on success. The list may be empty if the user has no current matches or all partner records have been deleted.HTTP 401 if the request is unauthenticated.
- Return type:
Success response (HTTP 200):
{ "partners": [ { "email": "mentor@ucla.edu", "name": "John Smith", "role": "MENTOR", "major": ["Computer Science"], "minor": [], "year": 3, "hobbies": "chess", "clubs": "ACM", "goals": "...", "profilePictureUrl": "https://...", "lastMessage": "See you tomorrow!", "lastMessageTime": "2024-01-15T10:30:00+00:00", "unreadCount": 2 } ] }
Module contents
- show-inheritance:
- undoc-members: