mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:11:13 -05:00
Add email templates for user notifications and account management
- Created a base email template (base.html) for consistent styling across all emails. - Added moderation approval email template (moderation_approved.html) to notify users of approved submissions. - Added moderation rejection email template (moderation_rejected.html) to inform users of required changes for their submissions. - Created password reset email template (password_reset.html) for users requesting to reset their passwords. - Developed a welcome email template (welcome.html) to greet new users and provide account details and tips for using ThrillWiki.
This commit is contained in:
@@ -8,11 +8,24 @@ This module contains the core entity models:
|
||||
- Ride: Individual rides and roller coasters
|
||||
"""
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.utils.text import slugify
|
||||
from django_lifecycle import hook, AFTER_CREATE, AFTER_UPDATE, BEFORE_SAVE
|
||||
|
||||
from apps.core.models import VersionedModel, BaseModel
|
||||
|
||||
# Conditionally import GIS models only if using PostGIS backend
|
||||
# This allows migrations to run on SQLite during local development
|
||||
_using_postgis = (
|
||||
'postgis' in settings.DATABASES['default']['ENGINE']
|
||||
)
|
||||
|
||||
if _using_postgis:
|
||||
from django.contrib.gis.db import models as gis_models
|
||||
from django.contrib.gis.geos import Point
|
||||
from django.contrib.postgres.search import SearchVectorField
|
||||
|
||||
|
||||
class Company(VersionedModel):
|
||||
"""
|
||||
@@ -122,6 +135,16 @@ class Company(VersionedModel):
|
||||
help_text="Number of rides manufactured (for manufacturers)"
|
||||
)
|
||||
|
||||
# Generic relation to photos
|
||||
photos = GenericRelation(
|
||||
'media.Photo',
|
||||
related_query_name='company'
|
||||
)
|
||||
|
||||
# Full-text search vector (PostgreSQL only)
|
||||
# Populated automatically via signals or database triggers
|
||||
# Includes: name (weight A) + description (weight B)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Company'
|
||||
verbose_name_plural = 'Companies'
|
||||
@@ -151,6 +174,24 @@ class Company(VersionedModel):
|
||||
self.park_count = self.operated_parks.count()
|
||||
self.ride_count = self.manufactured_rides.count()
|
||||
self.save(update_fields=['park_count', 'ride_count'])
|
||||
|
||||
def get_photos(self, photo_type=None, approved_only=True):
|
||||
"""Get photos for this company."""
|
||||
from apps.media.services import PhotoService
|
||||
service = PhotoService()
|
||||
return service.get_entity_photos(self, photo_type=photo_type, approved_only=approved_only)
|
||||
|
||||
@property
|
||||
def main_photo(self):
|
||||
"""Get the main photo."""
|
||||
photos = self.photos.filter(photo_type='main', moderation_status='approved').first()
|
||||
return photos
|
||||
|
||||
@property
|
||||
def logo_photo(self):
|
||||
"""Get the logo photo."""
|
||||
photos = self.photos.filter(photo_type='logo', moderation_status='approved').first()
|
||||
return photos
|
||||
|
||||
|
||||
class RideModel(VersionedModel):
|
||||
@@ -238,6 +279,12 @@ class RideModel(VersionedModel):
|
||||
help_text="Number of installations worldwide"
|
||||
)
|
||||
|
||||
# Generic relation to photos
|
||||
photos = GenericRelation(
|
||||
'media.Photo',
|
||||
related_query_name='ride_model'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Ride Model'
|
||||
verbose_name_plural = 'Ride Models'
|
||||
@@ -267,11 +314,27 @@ class RideModel(VersionedModel):
|
||||
"""Update cached installation count."""
|
||||
self.installation_count = self.rides.count()
|
||||
self.save(update_fields=['installation_count'])
|
||||
|
||||
def get_photos(self, photo_type=None, approved_only=True):
|
||||
"""Get photos for this ride model."""
|
||||
from apps.media.services import PhotoService
|
||||
service = PhotoService()
|
||||
return service.get_entity_photos(self, photo_type=photo_type, approved_only=approved_only)
|
||||
|
||||
@property
|
||||
def main_photo(self):
|
||||
"""Get the main photo."""
|
||||
photos = self.photos.filter(photo_type='main', moderation_status='approved').first()
|
||||
return photos
|
||||
|
||||
|
||||
class Park(VersionedModel):
|
||||
"""
|
||||
Represents an amusement park, theme park, water park, or FEC.
|
||||
|
||||
Note: Geographic coordinates are stored differently based on database backend:
|
||||
- Production (PostGIS): Uses location_point PointField with full GIS capabilities
|
||||
- Local Dev (SQLite): Uses latitude/longitude DecimalFields (no spatial queries)
|
||||
"""
|
||||
|
||||
PARK_TYPE_CHOICES = [
|
||||
@@ -369,21 +432,24 @@ class Park(VersionedModel):
|
||||
)
|
||||
|
||||
# Precise coordinates for mapping
|
||||
# Primary in local dev (SQLite), deprecated in production (PostGIS)
|
||||
latitude = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=7,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Latitude coordinate"
|
||||
help_text="Latitude coordinate. Primary in local dev, use location_point in production."
|
||||
)
|
||||
longitude = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=7,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Longitude coordinate"
|
||||
help_text="Longitude coordinate. Primary in local dev, use location_point in production."
|
||||
)
|
||||
|
||||
# NOTE: location_point PointField is added conditionally below if using PostGIS
|
||||
|
||||
# Relationships
|
||||
operator = models.ForeignKey(
|
||||
'Company',
|
||||
@@ -437,6 +503,12 @@ class Park(VersionedModel):
|
||||
help_text="Additional park-specific data"
|
||||
)
|
||||
|
||||
# Generic relation to photos
|
||||
photos = GenericRelation(
|
||||
'media.Photo',
|
||||
related_query_name='park'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Park'
|
||||
verbose_name_plural = 'Parks'
|
||||
@@ -470,6 +542,100 @@ class Park(VersionedModel):
|
||||
self.ride_count = self.rides.count()
|
||||
self.coaster_count = self.rides.filter(is_coaster=True).count()
|
||||
self.save(update_fields=['ride_count', 'coaster_count'])
|
||||
|
||||
def set_location(self, longitude, latitude):
|
||||
"""
|
||||
Set park location from coordinates.
|
||||
|
||||
Args:
|
||||
longitude: Longitude coordinate (X)
|
||||
latitude: Latitude coordinate (Y)
|
||||
|
||||
Note: Works in both PostGIS and non-PostGIS modes.
|
||||
- PostGIS: Sets location_point and syncs to lat/lng
|
||||
- SQLite: Sets lat/lng directly
|
||||
"""
|
||||
if longitude is not None and latitude is not None:
|
||||
# Always update lat/lng fields
|
||||
self.longitude = longitude
|
||||
self.latitude = latitude
|
||||
|
||||
# If using PostGIS, also update location_point
|
||||
if _using_postgis and hasattr(self, 'location_point'):
|
||||
self.location_point = Point(float(longitude), float(latitude), srid=4326)
|
||||
|
||||
@property
|
||||
def coordinates(self):
|
||||
"""
|
||||
Get coordinates as (longitude, latitude) tuple.
|
||||
|
||||
Returns:
|
||||
tuple: (longitude, latitude) or None if no location set
|
||||
"""
|
||||
# Try PostGIS field first if available
|
||||
if _using_postgis and hasattr(self, 'location_point') and self.location_point:
|
||||
return (self.location_point.x, self.location_point.y)
|
||||
# Fall back to lat/lng fields
|
||||
elif self.longitude and self.latitude:
|
||||
return (float(self.longitude), float(self.latitude))
|
||||
return None
|
||||
|
||||
@property
|
||||
def latitude_value(self):
|
||||
"""Get latitude value (from location_point if PostGIS, else from latitude field)."""
|
||||
if _using_postgis and hasattr(self, 'location_point') and self.location_point:
|
||||
return self.location_point.y
|
||||
return float(self.latitude) if self.latitude else None
|
||||
|
||||
@property
|
||||
def longitude_value(self):
|
||||
"""Get longitude value (from location_point if PostGIS, else from longitude field)."""
|
||||
if _using_postgis and hasattr(self, 'location_point') and self.location_point:
|
||||
return self.location_point.x
|
||||
return float(self.longitude) if self.longitude else None
|
||||
|
||||
def get_photos(self, photo_type=None, approved_only=True):
|
||||
"""Get photos for this park."""
|
||||
from apps.media.services import PhotoService
|
||||
service = PhotoService()
|
||||
return service.get_entity_photos(self, photo_type=photo_type, approved_only=approved_only)
|
||||
|
||||
@property
|
||||
def main_photo(self):
|
||||
"""Get the main photo."""
|
||||
photos = self.photos.filter(photo_type='main', moderation_status='approved').first()
|
||||
return photos
|
||||
|
||||
@property
|
||||
def banner_photo(self):
|
||||
"""Get the banner photo."""
|
||||
photos = self.photos.filter(photo_type='banner', moderation_status='approved').first()
|
||||
return photos
|
||||
|
||||
@property
|
||||
def logo_photo(self):
|
||||
"""Get the logo photo."""
|
||||
photos = self.photos.filter(photo_type='logo', moderation_status='approved').first()
|
||||
return photos
|
||||
|
||||
@property
|
||||
def gallery_photos(self):
|
||||
"""Get gallery photos."""
|
||||
return self.photos.filter(photo_type='gallery', moderation_status='approved').order_by('display_order')
|
||||
|
||||
|
||||
# Conditionally add PostGIS PointField to Park model if using PostGIS backend
|
||||
if _using_postgis:
|
||||
Park.add_to_class(
|
||||
'location_point',
|
||||
gis_models.PointField(
|
||||
geography=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
srid=4326,
|
||||
help_text="Geographic coordinates (PostGIS Point). Production only."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Ride(VersionedModel):
|
||||
@@ -659,6 +825,12 @@ class Ride(VersionedModel):
|
||||
help_text="Additional ride-specific data"
|
||||
)
|
||||
|
||||
# Generic relation to photos
|
||||
photos = GenericRelation(
|
||||
'media.Photo',
|
||||
related_query_name='ride'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Ride'
|
||||
verbose_name_plural = 'Rides'
|
||||
@@ -699,3 +871,60 @@ class Ride(VersionedModel):
|
||||
"""Update parent park's ride counts when ride is created or moved."""
|
||||
if self.park:
|
||||
self.park.update_counts()
|
||||
|
||||
def get_photos(self, photo_type=None, approved_only=True):
|
||||
"""Get photos for this ride."""
|
||||
from apps.media.services import PhotoService
|
||||
service = PhotoService()
|
||||
return service.get_entity_photos(self, photo_type=photo_type, approved_only=approved_only)
|
||||
|
||||
@property
|
||||
def main_photo(self):
|
||||
"""Get the main photo."""
|
||||
photos = self.photos.filter(photo_type='main', moderation_status='approved').first()
|
||||
return photos
|
||||
|
||||
@property
|
||||
def gallery_photos(self):
|
||||
"""Get gallery photos."""
|
||||
return self.photos.filter(photo_type='gallery', moderation_status='approved').order_by('display_order')
|
||||
|
||||
|
||||
# Add SearchVectorField to all models for full-text search (PostgreSQL only)
|
||||
# Must be at the very end after ALL class definitions
|
||||
if _using_postgis:
|
||||
Company.add_to_class(
|
||||
'search_vector',
|
||||
SearchVectorField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Pre-computed search vector for full-text search. Auto-updated via signals."
|
||||
)
|
||||
)
|
||||
|
||||
RideModel.add_to_class(
|
||||
'search_vector',
|
||||
SearchVectorField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Pre-computed search vector for full-text search. Auto-updated via signals."
|
||||
)
|
||||
)
|
||||
|
||||
Park.add_to_class(
|
||||
'search_vector',
|
||||
SearchVectorField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Pre-computed search vector for full-text search. Auto-updated via signals."
|
||||
)
|
||||
)
|
||||
|
||||
Ride.add_to_class(
|
||||
'search_vector',
|
||||
SearchVectorField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Pre-computed search vector for full-text search. Auto-updated via signals."
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user