Skip to content

Commit e27c024

Browse files
authored
chore: Remove temporary jira migration endpoint (#147)
Migration is done in prod. Removes IncidentImportAPIView, IncidentImportSerializer, URL route, and associated tests. Inlines _set_tags_and_links back into create().
1 parent bf2eebb commit e27c024

File tree

4 files changed

+19
-220
lines changed

4 files changed

+19
-220
lines changed

src/firetower/incidents/serializers.py

Lines changed: 18 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from django.conf import settings
55
from django.contrib.auth.models import User
66
from django.core.exceptions import ValidationError as DjangoValidationError
7-
from django.db import transaction
87
from django.db.models.functions import Lower
98
from rest_framework import serializers
109

@@ -19,7 +18,6 @@
1918
on_visibility_changed,
2019
)
2120
from .models import (
22-
INCIDENT_ID_START,
2321
USER_ADDABLE_TAG_TYPES,
2422
ExternalLink,
2523
ExternalLinkType,
@@ -458,16 +456,22 @@ def validate_external_links(
458456

459457
return value
460458

461-
def _set_tags_and_links(
462-
self,
463-
incident: Incident,
464-
external_links_data: dict[str, str | None] | None,
465-
affected_service_tag_names: list[str] | None,
466-
affected_region_tag_names: list[str] | None,
467-
root_cause_tag_names: list[str] | None,
468-
impact_type_tag_names: list[str] | None,
469-
) -> None:
470-
"""Set external links and tags on a newly created incident."""
459+
def create(self, validated_data: dict) -> Incident:
460+
"""Create incident with external links and tags"""
461+
external_links_data = validated_data.pop("external_links", None)
462+
affected_service_tag_names = validated_data.pop(
463+
"affected_service_tag_names", None
464+
)
465+
affected_region_tag_names = validated_data.pop(
466+
"affected_region_tag_names", None
467+
)
468+
root_cause_tag_names = validated_data.pop("root_cause_tag_names", None)
469+
impact_type_tag_names = validated_data.pop("impact_type_tag_names", None)
470+
471+
# Create the incident
472+
incident = super().create(validated_data)
473+
474+
# Create external links if provided
471475
if external_links_data:
472476
for link_type, url in external_links_data.items():
473477
if url is not None: # Skip null values on create
@@ -477,6 +481,7 @@ def _set_tags_and_links(
477481
url=url,
478482
)
479483

484+
# Set tags if provided
480485
if affected_service_tag_names:
481486
tags = Tag.objects.annotate(name_lower=Lower("name")).filter(
482487
name_lower__in=[n.lower() for n in affected_service_tag_names],
@@ -505,32 +510,9 @@ def _set_tags_and_links(
505510
)
506511
incident.impact_type_tags.set(tags)
507512

508-
def create(self, validated_data: dict, skip_hooks: bool = False) -> Incident:
509-
"""Create incident with external links and tags"""
510-
external_links_data = validated_data.pop("external_links", None)
511-
affected_service_tag_names = validated_data.pop(
512-
"affected_service_tag_names", None
513-
)
514-
affected_region_tag_names = validated_data.pop(
515-
"affected_region_tag_names", None
516-
)
517-
root_cause_tag_names = validated_data.pop("root_cause_tag_names", None)
518-
impact_type_tag_names = validated_data.pop("impact_type_tag_names", None)
519-
520-
incident = super().create(validated_data)
521-
522-
self._set_tags_and_links(
523-
incident,
524-
external_links_data,
525-
affected_service_tag_names,
526-
affected_region_tag_names,
527-
root_cause_tag_names,
528-
impact_type_tag_names,
529-
)
530-
531513
# Runs synchronously — Slack API calls may add latency to the response.
532514
# Consider deferring to a background task if this becomes a problem.
533-
if settings.HOOKS_ENABLED and not skip_hooks:
515+
if settings.HOOKS_ENABLED:
534516
on_incident_created(incident)
535517

536518
return incident
@@ -627,50 +609,6 @@ def update(self, instance: Incident, validated_data: dict) -> Incident:
627609
return instance
628610

629611

630-
class IncidentImportSerializer(IncidentWriteSerializer):
631-
"""Temporary: used for one-time Jira incident migration."""
632-
633-
id = serializers.IntegerField(required=True)
634-
created_at = serializers.DateTimeField(required=False)
635-
updated_at = serializers.DateTimeField(required=False)
636-
637-
class Meta(IncidentWriteSerializer.Meta):
638-
fields = [*IncidentWriteSerializer.Meta.fields, "created_at", "updated_at"]
639-
640-
def validate_id(self, value: int) -> int:
641-
if value < 1:
642-
raise serializers.ValidationError("ID must be >= 1.")
643-
if value >= INCIDENT_ID_START:
644-
raise serializers.ValidationError(
645-
f"ID must be less than {INCIDENT_ID_START}."
646-
)
647-
if Incident.objects.filter(pk=value).exists():
648-
raise serializers.ValidationError(
649-
f"Incident with ID {value} already exists."
650-
)
651-
return value
652-
653-
@transaction.atomic
654-
def create(self, validated_data: dict, skip_hooks: bool = False) -> Incident:
655-
created_at = validated_data.pop("created_at", None)
656-
updated_at = validated_data.pop("updated_at", None)
657-
658-
# Always skip hooks — no Slack channel creation or notifications for
659-
# historical incidents being imported from Jira.
660-
incident = super().create(validated_data, skip_hooks=True)
661-
662-
timestamp_updates = {}
663-
if created_at is not None:
664-
timestamp_updates["created_at"] = created_at
665-
if updated_at is not None:
666-
timestamp_updates["updated_at"] = updated_at
667-
if timestamp_updates:
668-
Incident.objects.filter(pk=incident.pk).update(**timestamp_updates)
669-
incident.refresh_from_db()
670-
671-
return incident
672-
673-
674612
class TagSerializer(serializers.ModelSerializer):
675613
class Meta:
676614
model = Tag

src/firetower/incidents/tests/test_views.py

Lines changed: 0 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from rest_framework.test import APIClient
99

1010
from firetower.incidents.models import (
11-
INCIDENT_ID_START,
1211
ExternalLink,
1312
ExternalLinkType,
1413
Incident,
@@ -1482,126 +1481,3 @@ def test_list_tags_sorted_by_usage(self):
14821481

14831482
assert response.status_code == 200
14841483
assert response.data == ["UsedTwice", "UsedOnce", "Unused"]
1485-
1486-
1487-
@pytest.mark.django_db
1488-
class TestIncidentImportAPIView:
1489-
def setup_method(self):
1490-
self.client = APIClient()
1491-
self.staff_user = User.objects.create_user(
1492-
username="admin@example.com",
1493-
email="admin@example.com",
1494-
password="testpass123",
1495-
is_staff=True,
1496-
)
1497-
self.regular_user = User.objects.create_user(
1498-
username="user@example.com",
1499-
email="user@example.com",
1500-
password="testpass123",
1501-
)
1502-
self.captain = User.objects.create_user(
1503-
username="captain@example.com",
1504-
email="captain@example.com",
1505-
password="testpass123",
1506-
)
1507-
self.reporter = User.objects.create_user(
1508-
username="reporter@example.com",
1509-
email="reporter@example.com",
1510-
password="testpass123",
1511-
)
1512-
1513-
def _import_data(self, **overrides):
1514-
data = {
1515-
"id": 1,
1516-
"title": "Imported Incident",
1517-
"severity": IncidentSeverity.P1,
1518-
"captain": self.captain.email,
1519-
"reporter": self.reporter.email,
1520-
}
1521-
data.update(overrides)
1522-
return data
1523-
1524-
def test_import_with_explicit_id(self):
1525-
self.client.force_authenticate(user=self.staff_user)
1526-
data = self._import_data(id=42)
1527-
response = self.client.post("/api/incidents/import/", data, format="json")
1528-
1529-
assert response.status_code == 201
1530-
assert Incident.objects.filter(pk=42).exists()
1531-
1532-
def test_import_with_timestamp_overrides(self):
1533-
self.client.force_authenticate(user=self.staff_user)
1534-
data = self._import_data(
1535-
created_at="2020-01-15T10:30:00Z",
1536-
updated_at="2020-06-20T14:00:00Z",
1537-
)
1538-
response = self.client.post("/api/incidents/import/", data, format="json")
1539-
1540-
assert response.status_code == 201
1541-
incident = Incident.objects.get(pk=data["id"])
1542-
assert incident.created_at.year == 2020
1543-
assert incident.created_at.month == 1
1544-
assert incident.updated_at.year == 2020
1545-
assert incident.updated_at.month == 6
1546-
1547-
def test_import_with_metadata(self):
1548-
Tag.objects.create(name="TestService", type=TagType.AFFECTED_SERVICE)
1549-
self.client.force_authenticate(user=self.staff_user)
1550-
data = self._import_data(
1551-
affected_service_tags=["TestService"],
1552-
external_links={"jira": "https://jira.example.com/browse/INC-1"},
1553-
)
1554-
response = self.client.post("/api/incidents/import/", data, format="json")
1555-
1556-
assert response.status_code == 201
1557-
incident = Incident.objects.get(pk=data["id"])
1558-
assert list(incident.affected_service_tags.values_list("name", flat=True)) == [
1559-
"TestService"
1560-
]
1561-
assert incident.external_links.filter(type=ExternalLinkType.JIRA).exists()
1562-
1563-
def test_rejects_id_at_or_above_threshold(self):
1564-
self.client.force_authenticate(user=self.staff_user)
1565-
data = self._import_data(id=INCIDENT_ID_START)
1566-
response = self.client.post("/api/incidents/import/", data, format="json")
1567-
1568-
assert response.status_code == 400
1569-
assert "id" in response.data
1570-
1571-
def test_rejects_id_below_one(self):
1572-
self.client.force_authenticate(user=self.staff_user)
1573-
data = self._import_data(id=0)
1574-
response = self.client.post("/api/incidents/import/", data, format="json")
1575-
1576-
assert response.status_code == 400
1577-
assert "id" in response.data
1578-
1579-
def test_rejects_duplicate_id(self):
1580-
Incident.objects.create(
1581-
id=100,
1582-
title="Existing",
1583-
severity=IncidentSeverity.P1,
1584-
status=IncidentStatus.ACTIVE,
1585-
)
1586-
self.client.force_authenticate(user=self.staff_user)
1587-
data = self._import_data(id=100)
1588-
response = self.client.post("/api/incidents/import/", data, format="json")
1589-
1590-
assert response.status_code == 400
1591-
assert "id" in response.data
1592-
1593-
def test_requires_staff_user(self):
1594-
self.client.force_authenticate(user=self.regular_user)
1595-
data = self._import_data()
1596-
response = self.client.post("/api/incidents/import/", data, format="json")
1597-
1598-
assert response.status_code == 403
1599-
1600-
@patch("firetower.incidents.serializers.on_incident_created")
1601-
def test_does_not_trigger_on_incident_created(self, mock_hook):
1602-
self.client.force_authenticate(user=self.staff_user)
1603-
data = self._import_data()
1604-
response = self.client.post("/api/incidents/import/", data, format="json")
1605-
1606-
assert response.status_code == 201
1607-
mock_hook.assert_not_called()

src/firetower/incidents/urls.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from .views import (
44
AvailabilityView,
5-
IncidentImportAPIView,
65
IncidentListCreateAPIView,
76
IncidentRetrieveUpdateAPIView,
87
TagListCreateAPIView,
@@ -21,12 +20,6 @@
2120
name="incident-detail-ui",
2221
),
2322
# Service API endpoints
24-
# Temporary: one-time Jira migration import endpoint
25-
path(
26-
"incidents/import/",
27-
IncidentImportAPIView.as_view(),
28-
name="incident-import",
29-
),
3023
path(
3124
"incidents/",
3225
IncidentListCreateAPIView.as_view(),

src/firetower/incidents/views.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from django.db.models import Count, QuerySet
88
from django.shortcuts import get_object_or_404
99
from django.utils import timezone
10-
from rest_framework import generics, permissions, serializers
10+
from rest_framework import generics, serializers
1111
from rest_framework.exceptions import ValidationError
1212
from rest_framework.request import Request
1313
from rest_framework.response import Response
@@ -39,7 +39,6 @@
3939
get_year_periods,
4040
)
4141
from .serializers import (
42-
IncidentImportSerializer,
4342
IncidentListUISerializer,
4443
IncidentOrRedirectReadSerializer,
4544
IncidentReadSerializer,
@@ -262,13 +261,6 @@ def get_object(self) -> Incident:
262261
return obj
263262

264263

265-
class IncidentImportAPIView(generics.CreateAPIView):
266-
"""Temporary: admin-only endpoint for one-time Jira incident migration."""
267-
268-
serializer_class = IncidentImportSerializer
269-
permission_classes = [permissions.IsAdminUser]
270-
271-
272264
class SyncIncidentParticipantsView(generics.GenericAPIView):
273265
"""
274266
Force sync incident participants from Slack channel.

0 commit comments

Comments
 (0)