Files
pacnpal d631f3183c 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.
2026-01-12 19:13:05 -05:00

221 lines
7.3 KiB
Python

import pghistory
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.text import slugify
from apps.core.choices.fields import RichChoiceField
from apps.core.models import TrackedModel
@pghistory.track()
class Company(TrackedModel):
# Import managers
from ..managers import CompanyManager
objects = CompanyManager()
# Core Fields
name = models.CharField(max_length=255, help_text="Company name")
slug = models.SlugField(max_length=255, unique=True, help_text="URL-friendly identifier")
roles = ArrayField(
RichChoiceField(choice_group="company_roles", domain="parks", max_length=20),
default=list,
blank=True,
help_text="Company roles (operator, manufacturer, etc.)",
)
description = models.TextField(blank=True, help_text="Detailed company description")
website = models.URLField(blank=True, help_text="Company website URL")
# Person/Entity Type - using RichChoiceField
person_type = RichChoiceField(
choice_group="person_types",
domain="parks",
max_length=20,
blank=True,
help_text="Type of entity (individual, firm, organization, etc.)",
)
# Company Status - using RichChoiceField
status = RichChoiceField(
choice_group="company_statuses",
domain="parks",
max_length=20,
default="ACTIVE",
help_text="Current operational status of the company",
)
# Founding Information (enhanced from just founded_year)
founded_year = models.PositiveIntegerField(blank=True, null=True, help_text="Year the company was founded")
founded_date = models.DateField(blank=True, null=True, help_text="Full founding date if known")
founded_date_precision = RichChoiceField(
choice_group="date_precision",
domain="parks",
max_length=20,
blank=True,
help_text="Precision of the founding date",
)
# Image URLs (ported from legacy)
logo_url = models.URLField(blank=True, help_text="Company logo image URL")
banner_image_url = models.URLField(blank=True, help_text="Banner image for company page header")
card_image_url = models.URLField(blank=True, help_text="Card/thumbnail image for listings")
# Image ID fields (for frontend submissions - Cloudflare image IDs)
logo_image_id = models.CharField(
max_length=255,
blank=True,
help_text="Cloudflare image ID for logo image",
)
banner_image_id = models.CharField(
max_length=255,
blank=True,
help_text="Cloudflare image ID for banner image",
)
card_image_id = models.CharField(
max_length=255,
blank=True,
help_text="Cloudflare image ID for card image",
)
# Location relationship (for headquarters coordinates)
location = models.ForeignKey(
"ParkLocation",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="companies_hq",
help_text="Linked location record for headquarters",
)
# Text-based headquarters location (matches frontend schema)
headquarters_location = models.CharField(
max_length=200,
blank=True,
help_text="Headquarters location description (e.g., 'Los Angeles, CA, USA')",
)
# Rating & Review Aggregates (computed fields, updated by triggers/signals)
average_rating = models.DecimalField(
max_digits=3,
decimal_places=2,
blank=True,
null=True,
help_text="Average rating from reviews (auto-calculated)",
)
review_count = models.PositiveIntegerField(
default=0,
help_text="Total number of reviews (auto-calculated)",
)
# Counts (auto-calculated)
parks_count = models.IntegerField(default=0, help_text="Number of parks operated (auto-calculated)")
rides_count = models.IntegerField(default=0, help_text="Number of rides manufactured (auto-calculated)")
# Submission metadata fields (from frontend schema)
source_url = models.URLField(
blank=True,
help_text="Source URL for the data (e.g., official website, Wikipedia)",
)
is_test_data = models.BooleanField(
default=False,
help_text="Whether this is test/development data",
)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Meta(TrackedModel.Meta):
app_label = "parks"
verbose_name = "Company"
verbose_name_plural = "Companies"
ordering = ["name"]
@pghistory.track()
class CompanyHeadquarters(models.Model):
"""
Simple address storage for company headquarters without coordinate tracking.
Focus on human-readable location information for display purposes.
"""
# Relationships
company = models.OneToOneField(
"Company",
on_delete=models.CASCADE,
related_name="headquarters",
help_text="Company this headquarters belongs to",
)
# Address Fields (No coordinates needed)
street_address = models.CharField(
max_length=255,
blank=True,
help_text="Mailing address if publicly available",
)
city = models.CharField(max_length=100, db_index=True, help_text="Headquarters city")
state_province = models.CharField(
max_length=100,
blank=True,
db_index=True,
help_text="State/Province/Region",
)
country = models.CharField(
max_length=100,
default="USA",
db_index=True,
help_text="Country where headquarters is located",
)
postal_code = models.CharField(max_length=20, blank=True, help_text="ZIP or postal code")
# Optional mailing address if different or more complete
mailing_address = models.TextField(
blank=True,
help_text="Complete mailing address if different from basic address",
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@property
def formatted_location(self):
"""Returns a formatted address string for display."""
components = []
if self.street_address:
components.append(self.street_address)
if self.city:
components.append(self.city)
if self.state_province:
components.append(self.state_province)
if self.postal_code:
components.append(self.postal_code)
if self.country and self.country != "USA":
components.append(self.country)
return ", ".join(components) if components else f"{self.city}, {self.country}"
@property
def location_display(self):
"""Simple city, state/country display for compact views."""
parts = [self.city]
if self.state_province:
parts.append(self.state_province)
elif self.country != "USA":
parts.append(self.country)
return ", ".join(parts) if parts else "Unknown Location"
def __str__(self):
return f"{self.company.name} Headquarters - {self.location_display}"
class Meta:
verbose_name = "Company Headquarters"
verbose_name_plural = "Company Headquarters"
ordering = ["company__name"]
indexes = [
models.Index(fields=["city", "country"]),
]