Skip to content

Commit 0bfcef3

Browse files
committed
✨(api) add labels info on Thread
Add details of labels info on thread list
1 parent d5b099d commit 0bfcef3

File tree

3 files changed

+171
-2
lines changed

3 files changed

+171
-2
lines changed

src/backend/core/api/serializers.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Client serializers for the messages core app."""
22

3-
from django.db.models import Count, Q
3+
from django.db.models import Count, Q, Exists, OuterRef
44

55
from drf_spectacular.utils import extend_schema_field
66
from rest_framework import serializers
@@ -141,6 +141,7 @@ class ThreadSerializer(serializers.ModelSerializer):
141141
sender_names = serializers.ListField(child=serializers.CharField(), read_only=True)
142142
user_role = serializers.SerializerMethodField()
143143
accesses = serializers.SerializerMethodField()
144+
labels = serializers.SerializerMethodField()
144145

145146
@extend_schema_field(ThreadAccessDetailSerializer(many=True))
146147
def get_accesses(self, instance):
@@ -170,6 +171,22 @@ def get_user_role(self, instance):
170171
return None
171172
return None
172173

174+
def get_labels(self, instance):
175+
"""Get labels for the thread, filtered by user's mailbox access."""
176+
request = self.context.get("request")
177+
if not request or not hasattr(request, "user"):
178+
return []
179+
180+
labels = instance.labels.filter(
181+
Exists(
182+
models.MailboxAccess.objects.filter(
183+
mailbox=OuterRef("mailbox"),
184+
user=request.user,
185+
)
186+
)
187+
).distinct()
188+
return LabelSerializer(labels, many=True).data
189+
173190
class Meta:
174191
model = models.Thread
175192
fields = [
@@ -188,6 +205,7 @@ class Meta:
188205
"updated_at",
189206
"user_role",
190207
"accesses",
208+
"labels",
191209
]
192210
read_only_fields = fields # Mark all as read-only for safety
193211

src/backend/core/api/viewsets/thread.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919

2020

2121
class ThreadViewSet(
22-
viewsets.GenericViewSet, mixins.ListModelMixin, mixins.DestroyModelMixin
22+
viewsets.GenericViewSet,
23+
mixins.ListModelMixin,
24+
mixins.RetrieveModelMixin,
25+
mixins.DestroyModelMixin,
2326
):
2427
"""ViewSet for Thread model."""
2528

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""Tests for label functionality in thread responses."""
2+
3+
import pytest
4+
from django.urls import reverse
5+
from rest_framework import status
6+
7+
from core import enums, factories, models
8+
9+
pytestmark = pytest.mark.django_db
10+
11+
12+
@pytest.fixture
13+
def user():
14+
"""Create a test user."""
15+
return factories.UserFactory()
16+
17+
18+
@pytest.fixture
19+
def mailbox(user):
20+
"""Create a mailbox with user access."""
21+
mailbox = factories.MailboxFactory()
22+
factories.MailboxAccessFactory(
23+
mailbox=mailbox,
24+
user=user,
25+
role=enums.MailboxRoleChoices.EDITOR,
26+
)
27+
return mailbox
28+
29+
30+
@pytest.fixture
31+
def thread(mailbox):
32+
"""Create a thread with mailbox access and a message."""
33+
thread = factories.ThreadFactory()
34+
factories.ThreadAccessFactory(
35+
mailbox=mailbox,
36+
thread=thread,
37+
role=enums.ThreadAccessRoleChoices.EDITOR,
38+
)
39+
# Add a message to the thread
40+
factories.MessageFactory(thread=thread)
41+
thread.update_stats()
42+
return thread
43+
44+
45+
@pytest.fixture
46+
def label(mailbox):
47+
"""Create a label in the mailbox."""
48+
return factories.LabelFactory(mailbox=mailbox)
49+
50+
51+
def test_thread_includes_labels(api_client, user, thread, label):
52+
"""Test that thread responses include labels."""
53+
# Add the label to the thread
54+
thread.labels.add(label)
55+
56+
api_client.force_authenticate(user=user)
57+
response = api_client.get(reverse("threads-list"))
58+
59+
assert response.status_code == status.HTTP_200_OK
60+
assert response.data["count"] == 1 # We should have exactly one thread
61+
thread_data = response.data["results"][0] # Get the first (and only) thread
62+
assert "labels" in thread_data
63+
assert len(thread_data["labels"]) == 1
64+
label_data = thread_data["labels"][0]
65+
assert label_data["id"] == str(label.id)
66+
assert label_data["name"] == label.name
67+
assert label_data["slug"] == label.slug
68+
assert label_data["color"] == label.color
69+
assert label_data["mailbox"] == label.mailbox.id
70+
71+
72+
def test_thread_labels_filtered_by_access(api_client, user, thread, mailbox):
73+
"""Test that thread responses only include labels from mailboxes the user has access to."""
74+
# Create a label in a mailbox the user has access to
75+
accessible_label = factories.LabelFactory(mailbox=mailbox)
76+
77+
# Create a label in a mailbox the user doesn't have access to
78+
other_mailbox = factories.MailboxFactory()
79+
inaccessible_label = factories.LabelFactory(mailbox=other_mailbox)
80+
81+
# Add both labels to the thread
82+
thread.labels.add(accessible_label, inaccessible_label)
83+
84+
api_client.force_authenticate(user=user)
85+
response = api_client.get(reverse("threads-list"))
86+
87+
assert response.status_code == status.HTTP_200_OK
88+
assert response.data["count"] == 1 # We should have exactly one thread
89+
thread_data = response.data["results"][0] # Get the first (and only) thread
90+
assert "labels" in thread_data
91+
assert len(thread_data["labels"]) == 1
92+
assert thread_data["labels"][0]["id"] == str(accessible_label.id)
93+
94+
95+
def test_thread_labels_empty_when_no_labels(api_client, user, thread):
96+
"""Test that thread responses include an empty labels list when the thread has no labels."""
97+
api_client.force_authenticate(user=user)
98+
response = api_client.get(reverse("threads-list"))
99+
100+
assert response.status_code == status.HTTP_200_OK
101+
assert response.data["count"] == 1 # We should have exactly one thread
102+
thread_data = response.data["results"][0] # Get the first (and only) thread
103+
assert "labels" in thread_data
104+
assert thread_data["labels"] == []
105+
106+
107+
def test_thread_labels_updated_after_label_changes(api_client, user, thread, label):
108+
"""Test that thread responses reflect label changes."""
109+
# Add the label to the thread
110+
thread.labels.add(label)
111+
112+
api_client.force_authenticate(user=user)
113+
114+
# Check initial state
115+
response = api_client.get(reverse("threads-list"))
116+
assert response.status_code == status.HTTP_200_OK
117+
assert response.data["count"] == 1 # We should have exactly one thread
118+
thread_data = response.data["results"][0] # Get the first (and only) thread
119+
assert len(thread_data["labels"]) == 1
120+
121+
# Remove the label
122+
thread.labels.remove(label)
123+
124+
# Check updated state
125+
response = api_client.get(reverse("threads-list"))
126+
assert response.status_code == status.HTTP_200_OK
127+
assert response.data["count"] == 1 # We should have exactly one thread
128+
thread_data = response.data["results"][0] # Get the first (and only) thread
129+
assert thread_data["labels"] == []
130+
131+
132+
def test_thread_labels_in_detail_view(api_client, user, thread, label):
133+
"""Test that labels are included in thread detail view."""
134+
# Add the label to the thread
135+
thread.labels.add(label)
136+
137+
api_client.force_authenticate(user=user)
138+
response = api_client.get(reverse("threads-detail", args=[thread.id]))
139+
140+
assert response.status_code == status.HTTP_200_OK
141+
assert "labels" in response.data
142+
assert len(response.data["labels"]) == 1
143+
label_data = response.data["labels"][0]
144+
assert label_data["id"] == str(label.id)
145+
assert label_data["name"] == label.name
146+
assert label_data["slug"] == label.slug
147+
assert label_data["color"] == label.color
148+
assert label_data["mailbox"] == label.mailbox.id

0 commit comments

Comments
 (0)