Skip to content

✨(frontend+backend) show per-thread message count badge in mailbox list#751

Open
nicolasaunai wants to merge 1 commit into
suitenumerique:mainfrom
nicolasaunai:feature/thread-message-count-badge
Open

✨(frontend+backend) show per-thread message count badge in mailbox list#751
nicolasaunai wants to merge 1 commit into
suitenumerique:mainfrom
nicolasaunai:feature/thread-message-count-badge

Conversation

@nicolasaunai

@nicolasaunai nicolasaunai commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Purpose

Adds a per-thread email-count badge in the mailbox list so users can see at a glance how many messages a thread contains.

Proposal

  • Backend: ThreadSerializer gains a message_count field, annotated as Count("messages", filter=Q(messages__is_draft=False), distinct=True) on the thread list queryset to avoid JOIN multiplication from the accesses join.

  • Frontend: a neutral Badge renders the count (only when > 1) in the thread list item, with an i18n aria-label/title.

  • Tests: TestThreadListMessageCount covers zero, exact count, draft exclusion, and distinct-per-thread regression.

  • signoff

  • signed commits

  • lint

  • tests

Summary by CodeRabbit

  • New Features

    • Added a message count badge to mailbox thread entries when a thread contains multiple messages.
    • Message counts exclude draft messages and are calculated separately for each thread.
  • Documentation

    • Updated the changelog to document the new thread message count badge.

Adds a message_count field to the ThreadSerializer (counting non-draft
messages, distinct to avoid JOIN multiplication) and renders a neutral
badge in the thread list when the count is greater than one.

Signed-off-by: Nicolas Aunai <nicolas.aunai@lpp.polytechnique.fr>
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The thread API now returns distinct non-draft message counts, and the mailbox thread item displays counts greater than one as an internationalized badge. API tests cover zero, draft, duplicate-join, and normal message-count cases.

Changes

Thread message count

Layer / File(s) Summary
Thread response contract
src/backend/core/api/openapi.json, src/backend/core/api/serializers.py
The Thread response includes a required read-only integer message_count field.
Message count annotation and validation
src/backend/core/api/viewsets/thread.py, src/backend/core/tests/api/test_threads_list.py
Thread list queries count distinct non-draft messages, with tests covering empty, normal, draft, and additional-access cases.
Mailbox message count badge
src/frontend/src/features/layouts/components/thread-panel/components/thread-item/index.tsx, CHANGELOG.md
Thread items display counts above one as compact translated badges, and the feature is documented under Unreleased changes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: jbpenrath

Sequence Diagram(s)

sequenceDiagram
  participant Mailbox as Mailbox list
  participant ThreadViewSet
  participant Database
  participant ThreadSerializer
  participant ThreadItem
  Mailbox->>ThreadViewSet: request threads-list
  ThreadViewSet->>Database: count distinct non-draft messages
  Database-->>ThreadViewSet: annotated threads
  ThreadViewSet->>ThreadSerializer: serialize thread data
  ThreadSerializer-->>Mailbox: response with message_count
  Mailbox->>ThreadItem: render thread
  ThreadItem-->>Mailbox: show badge when message_count > 1
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a per-thread message count badge in the mailbox list.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/core/api/serializers.py`:
- Line 840: Ensure the split response serializer populates the read-only
message_count field even though new_thread is not annotated. Update the
split-response construction to set message_count before serialization, or change
the serializer field implementation to compute a fallback count when the
annotation is absent, while preserving the existing list/retrieve behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9777168b-a166-455b-90e3-c7e505e7bf60

📥 Commits

Reviewing files that changed from the base of the PR and between 39f2d9c and a9ab4e6.

⛔ Files ignored due to path filters (1)
  • src/frontend/src/features/api/gen/models/thread.ts is excluded by !**/gen/**
📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/backend/core/api/openapi.json
  • src/backend/core/api/serializers.py
  • src/backend/core/api/viewsets/thread.py
  • src/backend/core/tests/api/test_threads_list.py
  • src/frontend/src/features/layouts/components/thread-panel/components/thread-item/index.tsx

labels = serializers.SerializerMethodField()
summary = serializers.CharField(read_only=True)
events_count = serializers.IntegerField(read_only=True)
message_count = serializers.IntegerField(read_only=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if `message_count` property is defined on the Thread model.
rg 'def message_count' src/backend/core/models/

Repository: suitenumerique/messages

Length of output: 226


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate Thread model, message_count usages, and split action.
git ls-files | rg '(^|/)serializers\.py$|(^|/)models?\.py$|(^|/)views?\.py$|(^|/)viewsets?\.py$|(^|/)api/'
printf '\n--- search class Thread ---\n'
rg -n "class Thread\b|message_count|def split\b|`@action`\(.*split|split\(" src/backend -S

Repository: suitenumerique/messages

Length of output: 36523


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map likely files for Thread-related logic before reading them.
fd -a 'serializers.py|models.py|views.py|viewsets.py' src/backend 2>/dev/null | sed 's#^\./##'

Repository: suitenumerique/messages

Length of output: 262


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- src/backend/core/models.py (Thread excerpt) ---'
sed -n '1032,1135p' src/backend/core/models.py

printf '\n%s\n' '--- src/backend/core/api/serializers.py (ThreadSerializer excerpt) ---'
sed -n '780,890p' src/backend/core/api/serializers.py

printf '\n%s\n' '--- src/backend/core/api/viewsets/thread.py (split excerpt) ---'
sed -n '820,930p' src/backend/core/api/viewsets/thread.py

printf '\n%s\n' '--- tests for split endpoint ---'
sed -n '1,260p' src/backend/core/tests/api/test_thread_split.py

Repository: suitenumerique/messages

Length of output: 21021


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- search Thread.message_count definition ---'
rg -n "message_count" src/backend/core/models.py src/backend/core/api/viewsets/thread.py src/backend/core/api/serializers.py src/backend/core/tests/api/test_thread_split.py -S

printf '\n%s\n' '--- src/backend/core/models.py (later Thread methods) ---'
sed -n '1135,1245p' src/backend/core/models.py

printf '\n%s\n' '--- src/backend/core/api/viewsets/thread.py (split continuation) ---'
sed -n '930,1045p' src/backend/core/api/viewsets/thread.py

printf '\n%s\n' '--- src/backend/core/tests/api/test_thread_split.py (success tests) ---'
sed -n '260,520p' src/backend/core/tests/api/test_thread_split.py

Repository: suitenumerique/messages

Length of output: 18819


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
try:
    from rest_framework import serializers
except Exception as exc:
    print(f"IMPORT_FAIL: {type(exc).__name__}: {exc}")
    raise SystemExit(0)

class Obj:
    pass

class S(serializers.Serializer):
    x = serializers.IntegerField(read_only=True)

print(S(Obj()).data)
PY

Repository: suitenumerique/messages

Length of output: 228


🌐 Web query:

DRF read-only IntegerField missing attribute serialization AttributeError SkipField get_attribute

💡 Result:

The AttributeError combined with SkipField in Django REST Framework (DRF) typically occurs during the serialization process when the framework attempts to access an attribute on an object that does not exist [1][2][3]. ### Why this happens In DRF, the Field.get_attribute() method is responsible for retrieving the value for a field from the object instance [4][3]. To handle cases where an attribute might be missing (often due to required=False or specific model states), DRF wraps the attribute access in a try-except block that catches AttributeError and KeyError [1][2][3]. When such an exception is caught: 1. If the field is not required and has no default value, DRF raises a SkipField exception [1][3]. 2. The SkipField exception is caught by the parent serializer, causing the field to be silently omitted from the serialized output [1][5][6]. If you are seeing this error, it often means that get_attribute is encountering an AttributeError that it interprets as "the field is missing," but it may actually be masking a legitimate error in your code (such as an incorrectly named field or a property that raises an AttributeError internally) [1][2][7]. ### Common Causes and Solutions * Incorrect Source: Ensure the source argument on your serializer field correctly matches an attribute or method on your model instance [1][2]. If the path is wrong, get_attribute will fail to find it [1]. * Property Methods: If a @property on your model raises an AttributeError internally, DRF may catch it and treat the field as missing [1][2]. Ensure your property logic is robust and does not raise AttributeError for expected null or missing states [7]. * Providing a Default: If you want the field to appear in the output even if the attribute access fails (e.g., returning None instead of skipping the field), you can explicitly set default=None on the field [7]. This prevents the logic from reaching the SkipField path [3]. * Debugging: Because this behavior suppresses the original traceback, it can be difficult to debug [1][2]. If you suspect the error is not simply a missing attribute, you can temporarily override get_attribute on your field to inspect the instance and source_attrs directly, allowing you to see what is causing the AttributeError [5]. For more information, refer to the DRF documentation on serializer fields, specifically the behavior of read_only fields and get_attribute [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- frontend generated thread model ---'
sed -n '1,220p' src/frontend/src/features/api/gen/models/thread.ts

printf '\n%s\n' '--- any split response usage of message_count ---'
rg -n "message_count|messageCount" src/frontend/src/features/api/gen/models src/frontend/src/features/api/gen/threads src/frontend/src/features/api/gen/thread* src/frontend/src/features/api/gen -S

Repository: suitenumerique/messages

Length of output: 2999


Populate message_count on the split response. new_thread isn’t annotated here, so DRF will skip this field and the split payload will be missing message_count compared with list/retrieve. Set it before serialization or switch to a serializer method with a fallback count.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/core/api/serializers.py` at line 840, Ensure the split response
serializer populates the read-only message_count field even though new_thread is
not annotated. Update the split-response construction to set message_count
before serialization, or change the serializer field implementation to compute a
fallback count when the annotation is absent, while preserving the existing
list/retrieve behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants