Based on the git diff provided, here's a concise and descriptive commit message:

feat: add security event taxonomy and optimize park queryset

- Add comprehensive security_event_types ChoiceGroup with categories for authentication, MFA, password, account, session, and API key events
- Include severity levels, icons, and CSS classes for each event type
- Fix park queryset optimization by using select_related for OneToOne location relationship
- Remove location property fields (latitude/longitude) from values() call as they are not actual DB columns
- Add proper location fields (city, state, country) to values() for map display

This change enhances security event tracking capabilities and resolves a queryset optimization issue where property decorators were incorrectly used in values() queries.
This commit is contained in:
pacnpal
2026-01-10 16:41:31 -05:00
parent 96df23242e
commit 2b66814d82
26 changed files with 2055 additions and 112 deletions

View File

@@ -0,0 +1,166 @@
"""
Rich Choice Objects for Support Domain
This module defines all choice objects for the support domain,
using the RichChoices pattern for consistent UI rendering and validation.
Note: Values are kept lowercase for backward compatibility with existing data.
"""
from apps.core.choices import ChoiceCategory, RichChoice
from apps.core.choices.registry import register_choices
# ============================================================================
# Ticket Status Choices
# ============================================================================
TICKET_STATUSES = [
RichChoice(
value="open",
label="Open",
description="Ticket is awaiting response",
metadata={
"color": "yellow",
"icon": "inbox",
"css_class": "bg-yellow-100 text-yellow-800 border-yellow-200",
"sort_order": 1,
"can_transition_to": ["in_progress", "closed"],
"is_actionable": True,
},
category=ChoiceCategory.STATUS,
),
RichChoice(
value="in_progress",
label="In Progress",
description="Ticket is being worked on",
metadata={
"color": "blue",
"icon": "clock",
"css_class": "bg-blue-100 text-blue-800 border-blue-200",
"sort_order": 2,
"can_transition_to": ["closed"],
"is_actionable": True,
},
category=ChoiceCategory.STATUS,
),
RichChoice(
value="closed",
label="Closed",
description="Ticket has been resolved",
metadata={
"color": "green",
"icon": "check-circle",
"css_class": "bg-green-100 text-green-800 border-green-200",
"sort_order": 3,
"can_transition_to": ["open"],
"is_actionable": False,
"is_final": True,
},
category=ChoiceCategory.STATUS,
),
]
# ============================================================================
# Ticket Category Choices
# ============================================================================
TICKET_CATEGORIES = [
RichChoice(
value="general",
label="General Inquiry",
description="General questions or feedback",
metadata={
"color": "gray",
"icon": "chat-bubble-left",
"css_class": "bg-gray-100 text-gray-800 border-gray-200",
"sort_order": 1,
"default_priority": "low",
},
category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="bug",
label="Bug Report",
description="Report a bug or issue with the platform",
metadata={
"color": "red",
"icon": "bug-ant",
"css_class": "bg-red-100 text-red-800 border-red-200",
"sort_order": 2,
"default_priority": "high",
},
category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="partnership",
label="Partnership",
description="Partnership or collaboration inquiries",
metadata={
"color": "purple",
"icon": "handshake",
"css_class": "bg-purple-100 text-purple-800 border-purple-200",
"sort_order": 3,
"default_priority": "medium",
},
category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="press",
label="Press/Media",
description="Press inquiries and media requests",
metadata={
"color": "indigo",
"icon": "newspaper",
"css_class": "bg-indigo-100 text-indigo-800 border-indigo-200",
"sort_order": 4,
"default_priority": "medium",
},
category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="data",
label="Data Correction",
description="Request corrections to park or ride data",
metadata={
"color": "orange",
"icon": "pencil-square",
"css_class": "bg-orange-100 text-orange-800 border-orange-200",
"sort_order": 5,
"default_priority": "medium",
},
category=ChoiceCategory.CLASSIFICATION,
),
RichChoice(
value="account",
label="Account Issue",
description="Account-related problems or requests",
metadata={
"color": "cyan",
"icon": "user-circle",
"css_class": "bg-cyan-100 text-cyan-800 border-cyan-200",
"sort_order": 6,
"default_priority": "high",
},
category=ChoiceCategory.CLASSIFICATION,
),
]
def register_support_choices() -> None:
"""Register all support domain choices with the global registry."""
register_choices(
"ticket_statuses",
TICKET_STATUSES,
domain="support",
description="Status options for support tickets",
)
register_choices(
"ticket_categories",
TICKET_CATEGORIES,
domain="support",
description="Category options for support tickets",
)
# Auto-register choices when module is imported
register_support_choices()

View File

@@ -0,0 +1,48 @@
# Generated by Django 5.2.10 on 2026-01-10 19:33
import apps.core.choices.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("support", "0003_add_incident_and_report_models"),
]
operations = [
migrations.AlterField(
model_name="ticket",
name="category",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="ticket_categories",
choices=[
("general", "General Inquiry"),
("bug", "Bug Report"),
("partnership", "Partnership"),
("press", "Press/Media"),
("data", "Data Correction"),
("account", "Account Issue"),
],
db_index=True,
default="general",
domain="support",
help_text="Category of the ticket",
max_length=20,
),
),
migrations.AlterField(
model_name="ticket",
name="status",
field=apps.core.choices.fields.RichChoiceField(
allow_deprecated=False,
choice_group="ticket_statuses",
choices=[("open", "Open"), ("in_progress", "In Progress"), ("closed", "Closed")],
db_index=True,
default="open",
domain="support",
max_length=20,
),
),
]

View File

@@ -1,36 +1,14 @@
from django.conf import settings
from django.db import models
from apps.core.choices.fields import RichChoiceField
from apps.core.history import TrackedModel
# Import choices to ensure registration on app load
from . import choices # noqa: F401
class Ticket(TrackedModel):
STATUS_OPEN = "open"
STATUS_IN_PROGRESS = "in_progress"
STATUS_CLOSED = "closed"
STATUS_CHOICES = [
(STATUS_OPEN, "Open"),
(STATUS_IN_PROGRESS, "In Progress"),
(STATUS_CLOSED, "Closed"),
]
CATEGORY_GENERAL = "general"
CATEGORY_BUG = "bug"
CATEGORY_PARTNERSHIP = "partnership"
CATEGORY_PRESS = "press"
CATEGORY_DATA = "data"
CATEGORY_ACCOUNT = "account"
CATEGORY_CHOICES = [
(CATEGORY_GENERAL, "General Inquiry"),
(CATEGORY_BUG, "Bug Report"),
(CATEGORY_PARTNERSHIP, "Partnership"),
(CATEGORY_PRESS, "Press/Media"),
(CATEGORY_DATA, "Data Correction"),
(CATEGORY_ACCOUNT, "Account Issue"),
]
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
@@ -40,10 +18,11 @@ class Ticket(TrackedModel):
help_text="User who submitted the ticket (optional)",
)
category = models.CharField(
category = RichChoiceField(
choice_group="ticket_categories",
domain="support",
max_length=20,
choices=CATEGORY_CHOICES,
default=CATEGORY_GENERAL,
default="general",
db_index=True,
help_text="Category of the ticket",
)
@@ -51,7 +30,13 @@ class Ticket(TrackedModel):
message = models.TextField()
email = models.EmailField(help_text="Contact email", blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_OPEN, db_index=True)
status = RichChoiceField(
choice_group="ticket_statuses",
domain="support",
max_length=20,
default="open",
db_index=True,
)
class Meta(TrackedModel.Meta):
verbose_name = "Ticket"

View File

@@ -134,9 +134,14 @@ class ReportCreateSerializer(serializers.ModelSerializer):
class ReportResolveSerializer(serializers.Serializer):
"""Serializer for resolving reports."""
from .models import Report
status = serializers.ChoiceField(
choices=[("resolved", "Resolved"), ("dismissed", "Dismissed")],
default="resolved",
choices=[
(Report.Status.RESOLVED, "Resolved"),
(Report.Status.DISMISSED, "Dismissed"),
],
default=Report.Status.RESOLVED,
)
notes = serializers.CharField(required=False, allow_blank=True)