mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 17:35:18 -05:00
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.
221 lines
7.3 KiB
Python
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"]),
|
|
]
|