Skip to content

Commit ff7386a

Browse files
committed
Add serializers to support frontend (ui) views
1 parent 2e692f5 commit ff7386a

File tree

4 files changed

+363
-0
lines changed

4 files changed

+363
-0
lines changed

src/firetower/auth/serializers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.contrib.auth.models import User
2+
from rest_framework import serializers
3+
4+
5+
class UserSerializer(serializers.ModelSerializer):
6+
"""
7+
Basic User serializer for nested representations.
8+
9+
Minimal user info for API responses: name and avatar only.
10+
"""
11+
12+
name = serializers.SerializerMethodField()
13+
avatar_url = serializers.CharField(source="userprofile.avatar_url", read_only=True)
14+
15+
class Meta:
16+
model = User
17+
fields = ["name", "avatar_url"]
18+
read_only_fields = []
19+
20+
def get_name(self, obj):
21+
"""Get user's full name or email as fallback"""
22+
return obj.get_full_name() or obj.email
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import pytest
2+
from django.contrib.auth.models import User
3+
4+
from firetower.auth.serializers import UserSerializer
5+
6+
7+
@pytest.mark.django_db
8+
class TestUserSerializer:
9+
def test_user_serialization(self):
10+
"""Test User serialization"""
11+
user = User.objects.create_user(
12+
username="[email protected]",
13+
14+
first_name="Test",
15+
last_name="User",
16+
)
17+
18+
serializer = UserSerializer(user)
19+
data = serializer.data
20+
21+
# Minimal fields only
22+
assert data["name"] == "Test User"
23+
assert "avatar_url" in data
24+
25+
# Should not include these
26+
assert "id" not in data
27+
assert "email" not in data
28+
assert "username" not in data
29+
assert "first_name" not in data
30+
assert "last_name" not in data
31+
32+
def test_user_serialization_no_full_name(self):
33+
"""Test User serialization when no first/last name"""
34+
user = User.objects.create_user(
35+
36+
)
37+
38+
serializer = UserSerializer(user)
39+
data = serializer.data
40+
41+
assert data["name"] == "[email protected]" # Falls back to email
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from rest_framework import serializers
2+
3+
from firetower.auth.serializers import UserSerializer
4+
5+
from .models import Incident
6+
7+
8+
class IncidentListSerializer(serializers.ModelSerializer):
9+
"""
10+
Serializer for listing incidents.
11+
12+
Minimal fields for list views - just core incident data.
13+
"""
14+
15+
# Use incident_number as "id" field for frontend
16+
id = serializers.CharField(source="incident_number", read_only=True)
17+
18+
class Meta:
19+
model = Incident
20+
fields = [
21+
"id",
22+
"title",
23+
"description",
24+
"impact",
25+
"status",
26+
"severity",
27+
"is_private",
28+
"created_at",
29+
"updated_at",
30+
]
31+
read_only_fields = ["id", "created_at", "updated_at"]
32+
33+
34+
class ParticipantSerializer(serializers.Serializer):
35+
"""
36+
Serializer for participants in incident detail view.
37+
38+
Matches frontend expectation: {name, avatar_url, role}
39+
"""
40+
41+
name = serializers.SerializerMethodField()
42+
avatar_url = serializers.CharField(source="userprofile.avatar_url", read_only=True)
43+
role = serializers.SerializerMethodField()
44+
45+
def get_name(self, obj):
46+
"""Get user's full name or username"""
47+
return obj.get_full_name() or obj.username
48+
49+
def get_role(self, obj):
50+
"""Determine role based on incident context"""
51+
incident = self.context.get("incident")
52+
if not incident:
53+
return "Participant"
54+
55+
if incident.captain == obj:
56+
return "Captain"
57+
elif incident.reporter == obj:
58+
return "Reporter"
59+
return "Participant"
60+
61+
62+
class IncidentDetailSerializer(serializers.ModelSerializer):
63+
"""
64+
Serializer for incident detail view.
65+
66+
Matches frontend expectations from transformers.py
67+
"""
68+
69+
# Use incident_number as "id" field for frontend compatibility
70+
id = serializers.CharField(source="incident_number", read_only=True)
71+
72+
# Full nested user data for captain/reporter
73+
captain = UserSerializer(read_only=True)
74+
reporter = UserSerializer(read_only=True)
75+
76+
# Participants with role information
77+
participants = serializers.SerializerMethodField()
78+
79+
# Tags as arrays of strings (not full objects)
80+
affected_areas = serializers.ListField(
81+
child=serializers.CharField(), read_only=True
82+
)
83+
root_causes = serializers.ListField(child=serializers.CharField(), read_only=True)
84+
85+
# External links as dict for easy frontend access
86+
external_links = serializers.DictField(source="external_links_dict", read_only=True)
87+
88+
class Meta:
89+
model = Incident
90+
fields = [
91+
"id",
92+
"title",
93+
"description",
94+
"impact",
95+
"status",
96+
"severity",
97+
"is_private",
98+
"captain",
99+
"reporter",
100+
"participants",
101+
"affected_areas",
102+
"root_causes",
103+
"external_links",
104+
"created_at",
105+
"updated_at",
106+
]
107+
read_only_fields = ["id", "created_at", "updated_at"]
108+
109+
def get_participants(self, obj):
110+
"""
111+
Get all participants with their roles.
112+
113+
Combines captain, reporter, and participants into one list
114+
matching frontend expectation.
115+
"""
116+
participants_list = []
117+
seen_users = set()
118+
119+
# Add captain
120+
if obj.captain and obj.captain.id not in seen_users:
121+
serializer = ParticipantSerializer(obj.captain, context={"incident": obj})
122+
participants_list.append(serializer.data)
123+
seen_users.add(obj.captain.id)
124+
125+
# Add reporter
126+
if obj.reporter and obj.reporter.id not in seen_users:
127+
serializer = ParticipantSerializer(obj.reporter, context={"incident": obj})
128+
participants_list.append(serializer.data)
129+
seen_users.add(obj.reporter.id)
130+
131+
# Add other participants
132+
for participant in obj.participants.all():
133+
if participant.id not in seen_users:
134+
serializer = ParticipantSerializer(
135+
participant, context={"incident": obj}
136+
)
137+
participants_list.append(serializer.data)
138+
seen_users.add(participant.id)
139+
140+
return participants_list
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import pytest
2+
from django.contrib.auth.models import User
3+
4+
from firetower.incidents.models import (
5+
ExternalLink,
6+
ExternalLinkType,
7+
Incident,
8+
IncidentSeverity,
9+
IncidentStatus,
10+
Tag,
11+
TagType,
12+
)
13+
from firetower.incidents.serializers import (
14+
IncidentDetailSerializer,
15+
IncidentListSerializer,
16+
)
17+
18+
19+
@pytest.mark.django_db
20+
class TestIncidentListSerializer:
21+
def test_incident_list_serialization(self):
22+
"""Test incident serialization for list view"""
23+
captain = User.objects.create_user(
24+
username="[email protected]",
25+
26+
first_name="Jane",
27+
last_name="Captain",
28+
)
29+
reporter = User.objects.create_user(
30+
username="[email protected]",
31+
32+
first_name="John",
33+
last_name="Reporter",
34+
)
35+
36+
incident = Incident.objects.create(
37+
title="Test Incident",
38+
description="Test description",
39+
impact="Test impact",
40+
status=IncidentStatus.ACTIVE,
41+
severity=IncidentSeverity.P1,
42+
is_private=False,
43+
captain=captain,
44+
reporter=reporter,
45+
)
46+
47+
# Add tags
48+
area_tag = Tag.objects.create(name="API", type=TagType.AFFECTED_AREA)
49+
cause_tag = Tag.objects.create(name="Database", type=TagType.ROOT_CAUSE)
50+
incident.affected_area_tags.add(area_tag)
51+
incident.root_cause_tags.add(cause_tag)
52+
53+
serializer = IncidentListSerializer(incident)
54+
data = serializer.data
55+
56+
# Check id is incident_number string (frontend compatibility)
57+
assert data["id"] == f"INC-{incident.id}"
58+
assert data["title"] == "Test Incident"
59+
assert data["status"] == IncidentStatus.ACTIVE
60+
assert data["severity"] == IncidentSeverity.P1
61+
62+
# List view should not include captain/reporter/tags
63+
assert "captain" not in data
64+
assert "reporter" not in data
65+
assert "affected_areas" not in data
66+
assert "root_causes" not in data
67+
68+
69+
@pytest.mark.django_db
70+
class TestIncidentDetailSerializer:
71+
def test_incident_detail_serialization(self):
72+
"""Test incident serialization for detail view (matches frontend expectations)"""
73+
captain = User.objects.create_user(
74+
username="[email protected]",
75+
76+
first_name="Jane",
77+
last_name="Captain",
78+
)
79+
reporter = User.objects.create_user(
80+
username="[email protected]",
81+
82+
first_name="John",
83+
last_name="Reporter",
84+
)
85+
participant = User.objects.create_user(
86+
username="[email protected]",
87+
88+
first_name="Alice",
89+
last_name="Participant",
90+
)
91+
92+
incident = Incident.objects.create(
93+
title="Test Incident",
94+
description="Test description",
95+
status=IncidentStatus.MITIGATED,
96+
severity=IncidentSeverity.P2,
97+
captain=captain,
98+
reporter=reporter,
99+
)
100+
incident.participants.add(participant)
101+
102+
# Add tags
103+
area_tag = Tag.objects.create(name="API", type=TagType.AFFECTED_AREA)
104+
cause_tag = Tag.objects.create(name="Database", type=TagType.ROOT_CAUSE)
105+
incident.affected_area_tags.add(area_tag)
106+
incident.root_cause_tags.add(cause_tag)
107+
108+
# Add external links
109+
ExternalLink.objects.create(
110+
incident=incident,
111+
type=ExternalLinkType.SLACK,
112+
url="https://slack.com/channels/incident-123",
113+
)
114+
115+
serializer = IncidentDetailSerializer(incident)
116+
data = serializer.data
117+
118+
# Check id is incident_number string (frontend compatibility)
119+
assert data["id"] == f"INC-{incident.id}"
120+
assert data["title"] == "Test Incident"
121+
122+
# Check nested users for captain/reporter (name and avatar only)
123+
assert data["captain"]["name"] == "Jane Captain"
124+
assert "avatar_url" in data["captain"]
125+
assert "email" not in data["captain"]
126+
127+
assert data["reporter"]["name"] == "John Reporter"
128+
assert "avatar_url" in data["reporter"]
129+
assert "email" not in data["reporter"]
130+
131+
# Check participants structure (matches frontend expectation)
132+
assert len(data["participants"]) == 3 # captain, reporter, participant
133+
134+
# Find each participant
135+
captain_participant = next(
136+
p for p in data["participants"] if p["role"] == "Captain"
137+
)
138+
assert captain_participant["name"] == "Jane Captain"
139+
assert "avatar_url" in captain_participant
140+
141+
reporter_participant = next(
142+
p for p in data["participants"] if p["role"] == "Reporter"
143+
)
144+
assert reporter_participant["name"] == "John Reporter"
145+
146+
other_participant = next(
147+
p for p in data["participants"] if p["role"] == "Participant"
148+
)
149+
assert other_participant["name"] == "Alice Participant"
150+
151+
# Check affected_areas and root_causes as arrays of strings
152+
assert "API" in data["affected_areas"]
153+
assert "Database" in data["root_causes"]
154+
155+
# Check external links (dict format for frontend compatibility)
156+
assert "slack" in data["external_links"]
157+
assert (
158+
data["external_links"]["slack"] == "https://slack.com/channels/incident-123"
159+
)
160+
assert data["external_links"]["jira"] is None # Not set

0 commit comments

Comments
 (0)