mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 06:05:18 -05:00
Based on the git diff provided, here's a concise and descriptive commit message:
feat: add passkey authentication and enhance user preferences - Add passkey login security event type with fingerprint icon - Include request and site context in email confirmation for backend - Add user_id exact match filter to prevent incorrect user lookups - Enable PATCH method for updating user preferences via API - Add moderation_preferences support to user settings - Optimize ticket queries with select_related and prefetch_related This commit introduces passkey authentication tracking, improves user profile filtering accuracy, and extends the preferences API to support updates. Query optimizations reduce database hits for ticket listings.
This commit is contained in:
@@ -1,31 +1,126 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.accounts.serializers import UserSerializer
|
||||
from apps.core.choices.serializers import RichChoiceSerializerField
|
||||
|
||||
from .models import Ticket
|
||||
from .models import EmailThread, Ticket
|
||||
|
||||
|
||||
class SubmitterProfileSerializer(serializers.Serializer):
|
||||
"""Nested serializer for submitter profile data."""
|
||||
|
||||
display_name = serializers.CharField(source="profile.display_name", allow_null=True)
|
||||
created_at = serializers.DateTimeField(source="date_joined", allow_null=True)
|
||||
coaster_count = serializers.IntegerField(source="profile.coaster_credit_count", allow_null=True, default=0)
|
||||
ride_count = serializers.IntegerField(source="profile.ride_credit_count", allow_null=True, default=0)
|
||||
park_count = serializers.IntegerField(source="profile.park_credit_count", allow_null=True, default=0)
|
||||
review_count = serializers.IntegerField(source="profile.review_count", allow_null=True, default=0)
|
||||
avatar_url = serializers.CharField(source="profile.avatar_url", allow_null=True)
|
||||
|
||||
|
||||
class TicketSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for Ticket model with full frontend compatibility."""
|
||||
|
||||
# User fields
|
||||
user = UserSerializer(read_only=True)
|
||||
user_id = serializers.UUIDField(source="user.id", read_only=True, allow_null=True)
|
||||
submitter_username = serializers.CharField(source="user.username", read_only=True, allow_null=True)
|
||||
submitter_reputation = serializers.SerializerMethodField()
|
||||
submitter_profile = serializers.SerializerMethodField()
|
||||
|
||||
# Choice display fields
|
||||
category_display = serializers.CharField(source="get_category_display", read_only=True)
|
||||
status_display = serializers.CharField(source="get_status_display", read_only=True)
|
||||
|
||||
# Computed fields
|
||||
thread_id = serializers.CharField(read_only=True)
|
||||
response_count = serializers.IntegerField(read_only=True)
|
||||
last_admin_response_at = serializers.DateTimeField(read_only=True, allow_null=True)
|
||||
|
||||
# Resolution tracking (alias resolved_by username)
|
||||
resolved_by_username = serializers.CharField(source="resolved_by.username", read_only=True, allow_null=True)
|
||||
|
||||
# Admin fields
|
||||
assigned_to_username = serializers.CharField(source="assigned_to.username", read_only=True, allow_null=True)
|
||||
archived_by_username = serializers.CharField(source="archived_by.username", read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = [
|
||||
# Core fields
|
||||
"id",
|
||||
"user",
|
||||
"category",
|
||||
"category_display",
|
||||
"subject",
|
||||
"message",
|
||||
"email",
|
||||
"status",
|
||||
"status_display",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
# User/submitter fields
|
||||
"user",
|
||||
"user_id",
|
||||
"submitter_username",
|
||||
"submitter_reputation",
|
||||
"submitter_profile",
|
||||
"name",
|
||||
"email",
|
||||
# Ticket content
|
||||
"subject",
|
||||
"message",
|
||||
"category",
|
||||
"category_display",
|
||||
# Status
|
||||
"status",
|
||||
"status_display",
|
||||
# Ticket number
|
||||
"ticket_number",
|
||||
# Admin management
|
||||
"admin_notes",
|
||||
"assigned_to",
|
||||
"assigned_to_username",
|
||||
# Resolution
|
||||
"resolved_at",
|
||||
"resolved_by",
|
||||
"resolved_by_username",
|
||||
# Thread info
|
||||
"thread_id",
|
||||
"last_admin_response_at",
|
||||
"response_count",
|
||||
# Archive
|
||||
"archived_at",
|
||||
"archived_by",
|
||||
"archived_by_username",
|
||||
]
|
||||
read_only_fields = ["id", "status", "created_at", "updated_at", "user"]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"user",
|
||||
"user_id",
|
||||
"submitter_username",
|
||||
"submitter_reputation",
|
||||
"submitter_profile",
|
||||
"ticket_number",
|
||||
"thread_id",
|
||||
"response_count",
|
||||
"last_admin_response_at",
|
||||
]
|
||||
|
||||
def get_submitter_reputation(self, obj):
|
||||
"""Get the submitter's reputation score."""
|
||||
if obj.user and hasattr(obj.user, "profile"):
|
||||
return getattr(obj.user.profile, "reputation", 0)
|
||||
return None
|
||||
|
||||
def get_submitter_profile(self, obj):
|
||||
"""Get a subset of profile data for display."""
|
||||
if not obj.user or not hasattr(obj.user, "profile"):
|
||||
return None
|
||||
profile = obj.user.profile
|
||||
return {
|
||||
"display_name": getattr(profile, "display_name", None),
|
||||
"created_at": obj.user.date_joined.isoformat() if obj.user.date_joined else None,
|
||||
"coaster_count": getattr(profile, "coaster_credit_count", 0),
|
||||
"ride_count": getattr(profile, "ride_credit_count", 0),
|
||||
"park_count": getattr(profile, "park_credit_count", 0),
|
||||
"review_count": getattr(profile, "review_count", 0),
|
||||
"avatar_url": getattr(profile, "avatar_url", None),
|
||||
}
|
||||
|
||||
def validate(self, data):
|
||||
# Ensure email is provided if user is anonymous
|
||||
@@ -35,6 +130,31 @@ class TicketSerializer(serializers.ModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class EmailThreadSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for EmailThread model."""
|
||||
|
||||
# NOTE: Frontend uses submission_id, we provide ticket as that field
|
||||
submission_id = serializers.UUIDField(source="ticket.id", read_only=True)
|
||||
sent_by_username = serializers.CharField(source="sent_by.username", read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = EmailThread
|
||||
fields = [
|
||||
"id",
|
||||
"created_at",
|
||||
"submission_id",
|
||||
"message_id",
|
||||
"from_email",
|
||||
"to_email",
|
||||
"subject",
|
||||
"body_text",
|
||||
"direction",
|
||||
"sent_by",
|
||||
"sent_by_username",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "submission_id"]
|
||||
|
||||
|
||||
class ReportSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for Report model."""
|
||||
|
||||
@@ -134,14 +254,141 @@ class ReportCreateSerializer(serializers.ModelSerializer):
|
||||
class ReportResolveSerializer(serializers.Serializer):
|
||||
"""Serializer for resolving reports."""
|
||||
|
||||
from .models import Report
|
||||
|
||||
status = serializers.ChoiceField(
|
||||
choices=[
|
||||
(Report.Status.RESOLVED, "Resolved"),
|
||||
(Report.Status.DISMISSED, "Dismissed"),
|
||||
],
|
||||
default=Report.Status.RESOLVED,
|
||||
# Use RichChoiceSerializerField with only resolution statuses
|
||||
status = RichChoiceSerializerField(
|
||||
choice_group="report_statuses",
|
||||
domain="support",
|
||||
default="resolved",
|
||||
)
|
||||
notes = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Support Ticket Action Serializers
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class SendReplySerializer(serializers.Serializer):
|
||||
"""
|
||||
Input serializer for send_reply action.
|
||||
|
||||
Validates the request body for sending an email reply to a ticket.
|
||||
Supports both snake_case and camelCase field names for frontend compatibility.
|
||||
"""
|
||||
|
||||
# Primary fields (required=False because we validate manually to support camelCase)
|
||||
reply_body = serializers.CharField(
|
||||
required=False,
|
||||
min_length=1,
|
||||
max_length=50000,
|
||||
help_text="The body text of the email reply",
|
||||
)
|
||||
new_status = RichChoiceSerializerField(
|
||||
choice_group="ticket_statuses",
|
||||
domain="support",
|
||||
required=False,
|
||||
allow_null=True,
|
||||
help_text="Optionally update the ticket status after sending reply",
|
||||
)
|
||||
|
||||
# camelCase aliases for frontend compatibility
|
||||
replyBody = serializers.CharField(required=False, write_only=True)
|
||||
newStatus = serializers.CharField(required=False, write_only=True, allow_null=True)
|
||||
|
||||
def validate(self, data: dict) -> dict:
|
||||
"""Normalize camelCase to snake_case and validate required fields."""
|
||||
# Normalize camelCase to snake_case
|
||||
if "replyBody" in data and data["replyBody"]:
|
||||
data["reply_body"] = data.pop("replyBody")
|
||||
elif "replyBody" in data:
|
||||
data.pop("replyBody")
|
||||
|
||||
if "newStatus" in data and data["newStatus"]:
|
||||
data["new_status"] = data.pop("newStatus")
|
||||
elif "newStatus" in data:
|
||||
data.pop("newStatus")
|
||||
|
||||
# Validate required fields
|
||||
if not data.get("reply_body"):
|
||||
raise serializers.ValidationError({"reply_body": "This field is required."})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class SendReplyResponseSerializer(serializers.Serializer):
|
||||
"""Response serializer for send_reply action."""
|
||||
|
||||
detail = serializers.CharField()
|
||||
thread_id = serializers.UUIDField()
|
||||
ticket_number = serializers.CharField()
|
||||
|
||||
|
||||
class MergeTicketsSerializer(serializers.Serializer):
|
||||
"""
|
||||
Input serializer for merge tickets action.
|
||||
|
||||
Validates the request body for merging multiple tickets into one.
|
||||
Supports both snake_case and camelCase field names for frontend compatibility.
|
||||
"""
|
||||
|
||||
# Primary fields (required=False because we validate manually to support camelCase)
|
||||
primary_ticket_id = serializers.UUIDField(
|
||||
required=False,
|
||||
help_text="UUID of the primary ticket that will absorb others",
|
||||
)
|
||||
merge_ticket_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
min_length=1,
|
||||
help_text="List of ticket UUIDs to merge into the primary",
|
||||
)
|
||||
merge_reason = serializers.CharField(
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
max_length=500,
|
||||
help_text="Optional reason for the merge",
|
||||
)
|
||||
|
||||
# camelCase aliases for frontend compatibility
|
||||
primaryTicketId = serializers.UUIDField(required=False, write_only=True)
|
||||
mergeTicketIds = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=False,
|
||||
write_only=True,
|
||||
)
|
||||
mergeReason = serializers.CharField(required=False, write_only=True, allow_blank=True)
|
||||
|
||||
def validate(self, data: dict) -> dict:
|
||||
"""Normalize camelCase to snake_case and validate required fields."""
|
||||
# Normalize camelCase to snake_case
|
||||
if "primaryTicketId" in data and data["primaryTicketId"]:
|
||||
data["primary_ticket_id"] = data.pop("primaryTicketId")
|
||||
elif "primaryTicketId" in data:
|
||||
data.pop("primaryTicketId")
|
||||
|
||||
if "mergeTicketIds" in data and data["mergeTicketIds"]:
|
||||
data["merge_ticket_ids"] = data.pop("mergeTicketIds")
|
||||
elif "mergeTicketIds" in data:
|
||||
data.pop("mergeTicketIds")
|
||||
|
||||
if "mergeReason" in data and data["mergeReason"]:
|
||||
data["merge_reason"] = data.pop("mergeReason")
|
||||
elif "mergeReason" in data:
|
||||
data.pop("mergeReason")
|
||||
|
||||
# Validate required fields
|
||||
if not data.get("primary_ticket_id"):
|
||||
raise serializers.ValidationError({"primary_ticket_id": "This field is required."})
|
||||
if not data.get("merge_ticket_ids"):
|
||||
raise serializers.ValidationError({"merge_ticket_ids": "This field is required."})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class MergeTicketsResponseSerializer(serializers.Serializer):
|
||||
"""Response serializer for merge tickets action."""
|
||||
|
||||
detail = serializers.CharField()
|
||||
primaryTicketNumber = serializers.CharField()
|
||||
mergedCount = serializers.IntegerField()
|
||||
threadsConsolidated = serializers.IntegerField()
|
||||
|
||||
Reference in New Issue
Block a user