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: AsyncWebsocketConsumer

Async 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_async to 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:

  1. Authentication — closes with code 4001 if the scope user is absent or unauthenticated.

  2. Active match — closes with code 4003 if the user has no current match (role-aware: checks isMatched for mentees, current_mentees for mentors).

  3. Match relationship — closes with code 4003 if 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_established message.

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 connect never completed (guarded by hasattr check on room_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 content is non-empty and does not exceed 5 000 characters, then delegates persistence to save_message() and broadcasts the result to all members of the room group via chat_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 to False if 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 a read_receipt_broadcast event to the room group. Filtering so that the reader does not receive their own receipt is handled in read_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_broadcast event is dispatched to the room group. Forwards the payload to the connected client as a chat_message JSON frame.

Parameters:

event (dict) – Channel-layer event dict containing message_id, sender_email, sender_name, content, and timestamp.

async typing_broadcast(event)[source]

Channel-layer event handler: forward a typing indicator to this WebSocket.

Called by the Channels layer when a typing_broadcast event 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_email and is_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_broadcast event 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_email and count.

async force_disconnect(event)[source]

Channel-layer event handler: notify the client and close the connection.

Called by the Channels layer when a force_disconnect event is dispatched to the room group — for example, after two users are unmatched. Sends a force_disconnect frame 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_email are actively matched.

Applies the same role-aware relationship rules used in matching/views.py and chat/views.py:

  • MENTEE — valid if isMatched is True, matchedMentorEmail equals partner_email, and the partner holds the MENTOR role.

  • MENTOR — valid if partner_email is in current_mentees and the partner holds the MENTEE role.

Wrapped with @database_sync_to_async so it can be safely awaited from the async connect method without blocking the event loop.

Returns:

True if the relationship is valid, False if the partner does not exist or the match condition is not satisfied.

Return type:

bool

async save_message(content)[source]

Persist a new Message to the database.

Wrapped with @database_sync_to_async so 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 Message instance.

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 single UPDATE query. Wrapped with @database_sync_to_async so it can be safely awaited from the async consumer.

Returns:

The number of Message rows updated.

Return type:

int

chat.middleware module

class chat.middleware.SessionAuthMiddleware(inner)[source]

Bases: BaseMiddleware

Custom middleware to authenticate WebSocket connections using Django session. Reads sessionid from cookies (sent via credentials: ‘include’).

async get_user_from_session(session_id)[source]

Get user from session ID.

chat.models module

class chat.models.Message(*args, **kwargs)[source]

Bases: Model

Represents 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 by timestamp ascending 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 email2 and email2 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 Message instances 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_email is 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:
  • receiver_email (str) – Email address of the recipient whose unread messages are being counted.

  • sender_email (str, optional) – If supplied, restrict the count to messages from this sender. Defaults to None (count across all senders).

Returns:

Total number of unread messages matching the query.

Return type:

int

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 UPDATE query rather than loading and saving individual instances, making it efficient for conversations with a large number of unread messages.

Parameters:
  • receiver_email (str) – Email address of the recipient for whom messages are being marked as read.

  • sender_email (str) – Email address of the sender whose messages should be marked as read.

Returns:

The number of Message rows updated.

Return type:

int

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 isMatched flag alone to guard against stale flag values:

  • MENTEE — requires both isMatched=True and a non-empty matchedMentorEmail.

  • MENTOR — requires a non-empty current_mentees list.

Parameters:

user (users.models.User) – The user whose match state is being evaluated.

Returns:

True if the user has at least one active match, False for any other role or if match fields are absent.

Return type:

bool

Example:

>>> has_active_match(mentee_user)
True
>>> has_active_match(unmatched_mentor)
False
chat.views.validate_match_relationship(user, partner_email)[source]

Verify that user is 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 the MENTOR role.

  • MENTOR — may only chat with emails present in current_mentees, and each must hold the MENTEE role.

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) — True if the relationship is permitted.

  • error_message (str or None) — Human-readable reason for rejection, or None on success.

  • partner_user (User or None) — The resolved partner instance on success, or None on failure.

Return type:

tuple[bool, str | None, users.models.User | None]

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) and get_room_name(B, A) always return the same string. @ and . characters are replaced to satisfy channel-layer naming constraints.

Parameters:
  • email1 (str) – Email address of one participant.

  • email2 (str) – Email address of the other participant.

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:

str

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 Message and 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() and validate_match_relationship()).

Post-conditions: the Message row 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_email and content. Must belong to an authenticated user.

Returns:

  • HTTP 200 with success, message_id, and timestamp on success.

  • HTTP 400 if receiver_email is absent, content is 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:

django.http.JsonResponse

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_conversation and then reversed before serialization so the response list is in chronological (oldest-first) order, matching typical chat UI expectations. A has_more flag indicates whether a subsequent page is available.

Parameters:
  • request (django.http.HttpRequest) – The incoming HTTP GET request. Accepts optional limit and offset query 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, partner profile data, and has_more 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:

django.http.JsonResponse

Query parameters:

  • limit — Maximum messages to return. Capped at 100. Defaults to 50.

  • offset — Number of messages to skip for pagination. Defaults to 0.

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) and total (sum of all counts) on success.

  • HTTP 401 if the request is unauthenticated.

Return type:

django.http.JsonResponse

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 bulk UPDATE query. 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:

django.http.JsonResponse

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 lastMessage and lastMessageTime set to null. The list is sorted by lastMessageTime descending (most recently active first); partners with no messages sort to the end.

Stale emails in matchedMentorEmail or current_mentees that no longer correspond to an existing User are silently skipped.

Parameters:

request (django.http.HttpRequest) – The incoming HTTP GET request. Must belong to an authenticated user.

Returns:

  • HTTP 200 with a partners list 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:

django.http.JsonResponse

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: