mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:11:10 -05:00
1753 lines
55 KiB
Markdown
1753 lines
55 KiB
Markdown
# ThrillWiki Project Documentation
|
|
|
|
## Table of Contents
|
|
|
|
1. [Project Overview](#project-overview)
|
|
2. [Technical Stack and Architecture](#technical-stack-and-architecture)
|
|
3. [Database Models and Relationships](#database-models-and-relationships)
|
|
4. [Visual Theme and Design System](#visual-theme-and-design-system)
|
|
5. [Frontend Implementation Patterns](#frontend-implementation-patterns)
|
|
6. [User Experience and Key Features](#user-experience-and-key-features)
|
|
7. [Page Structure and Templates](#page-structure-and-templates)
|
|
8. [Services and Business Logic](#services-and-business-logic)
|
|
9. [Development Workflow](#development-workflow)
|
|
10. [API Endpoints and URL Structure](#api-endpoints-and-url-structure)
|
|
|
|
---
|
|
|
|
## Project Overview
|
|
|
|
ThrillWiki is a sophisticated Django-based web application designed for theme park and roller coaster enthusiasts. It provides comprehensive information management for parks, rides, companies, and user-generated content with advanced features including geographic mapping, moderation systems, and real-time interactions.
|
|
|
|
### Key Characteristics
|
|
|
|
- **Enterprise-Grade Architecture**: Service-oriented design with clear separation of concerns
|
|
- **Modern Frontend**: HTMX + Alpine.js for dynamic interactions without heavy JavaScript frameworks
|
|
- **Geographic Intelligence**: PostGIS integration for location-based features and mapping
|
|
- **Content Moderation**: Comprehensive workflow for user-generated content approval
|
|
- **Audit Trail**: Complete history tracking using django-pghistory
|
|
- **Responsive Design**: Mobile-first approach with sophisticated dark theme support
|
|
|
|
---
|
|
|
|
## Technical Stack and Architecture
|
|
|
|
### Core Technologies
|
|
|
|
| Component | Technology | Version | Purpose |
|
|
|-----------|------------|---------|---------|
|
|
| **Backend Framework** | Django | 5.0+ | Main web framework |
|
|
| **Database** | PostgreSQL + PostGIS | Latest | Relational database with geographic extension |
|
|
| **Frontend** | HTMX + Alpine.js | 1.9.6 + Latest | Dynamic interactions and client-side behavior |
|
|
| **Styling** | Tailwind CSS | Latest | Utility-first CSS framework |
|
|
| **Package Manager** | UV | Latest | Python dependency management |
|
|
| **Authentication** | Django Allauth | 0.60.1+ | OAuth and user management |
|
|
| **History Tracking** | django-pghistory | 3.5.2+ | Audit trails and versioning |
|
|
| **Testing** | Pytest + Playwright | Latest | Unit and E2E testing |
|
|
|
|
### Architecture Patterns
|
|
|
|
#### Service-Oriented Architecture
|
|
|
|
```
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
│ Presentation │ │ Business │ │ Data │
|
|
│ Layer │ │ Logic │ │ Layer │
|
|
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
|
|
│ • Templates │◄──►│ • Services │◄──►│ • Models │
|
|
│ • Views │ │ • Map Service │ │ • Database │
|
|
│ • HTMX/Alpine │ │ • Search │ │ • PostGIS │
|
|
│ • Tailwind CSS │ │ • Moderation │ │ • Caching │
|
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
```
|
|
|
|
#### Django App Organization
|
|
|
|
The project follows a domain-driven design approach with clear app boundaries:
|
|
|
|
```
|
|
thrillwiki_django_no_react/
|
|
├── core/ # Core business logic and shared services
|
|
│ ├── services/ # Unified map service, clustering, caching
|
|
│ ├── search/ # Search functionality
|
|
│ ├── mixins/ # Reusable view mixins
|
|
│ └── history/ # History tracking utilities
|
|
├── accounts/ # User management and authentication
|
|
├── parks/ # Theme park entities
|
|
│ └── models/ # Park, Company, Location models
|
|
├── rides/ # Ride entities and ride-specific logic
|
|
│ └── models/ # Ride, RideModel, Company models
|
|
├── location/ # Geographic location handling
|
|
├── media/ # Media file management and photo handling
|
|
├── moderation/ # Content moderation workflow
|
|
├── email_service/ # Email handling and notifications
|
|
└── static/ # Frontend assets (CSS, JS, images)
|
|
```
|
|
|
|
### Package Management with UV
|
|
|
|
ThrillWiki exclusively uses UV for Python package management, providing:
|
|
|
|
- **Faster dependency resolution**: Significantly faster than pip
|
|
- **Lock file support**: Ensures reproducible environments
|
|
- **Virtual environment management**: Automatic environment handling
|
|
- **Cross-platform compatibility**: Consistent behavior across development environments
|
|
|
|
#### Critical Commands
|
|
|
|
```bash
|
|
# Add new dependencies
|
|
uv add <package>
|
|
|
|
# Django management (NEVER use python manage.py)
|
|
uv run manage.py <command>
|
|
|
|
# Development server startup
|
|
lsof -ti :8000 | xargs kill -9; find . -type d -name "__pycache__" -exec rm -r {} +; uv run manage.py tailwind runserver
|
|
```
|
|
|
|
---
|
|
|
|
## Database Models and Relationships
|
|
|
|
### Entity Relationship Architecture
|
|
|
|
ThrillWiki implements a sophisticated entity relationship model that enforces business rules at the database level:
|
|
|
|
#### Core Business Rules (from .clinerules)
|
|
|
|
1. **Park Relationships**
|
|
- Parks MUST have an Operator (required relationship)
|
|
- Parks MAY have a PropertyOwner (optional, usually same as Operator)
|
|
- Parks CANNOT directly reference Company entities
|
|
|
|
2. **Ride Relationships**
|
|
- Rides MUST belong to a Park (required relationship)
|
|
- Rides MAY have a Manufacturer (optional relationship)
|
|
- Rides MAY have a Designer (optional relationship)
|
|
- Rides CANNOT directly reference Company entities
|
|
|
|
3. **Entity Definitions**
|
|
- **Operators**: Companies that operate theme parks
|
|
- **PropertyOwners**: Companies that own park property (new concept)
|
|
- **Manufacturers**: Companies that manufacture rides
|
|
- **Designers**: Companies/individuals that design rides
|
|
|
|
### Core Model Structure
|
|
|
|
#### Park Models (`parks/models/`)
|
|
|
|
```python
|
|
# Park Entity
|
|
class Park(TrackedModel):
|
|
# Core identifiers
|
|
name = CharField(max_length=255)
|
|
slug = SlugField(max_length=255, unique=True)
|
|
|
|
# Business relationships (enforced by .clinerules)
|
|
operator = ForeignKey('Company', related_name='operated_parks')
|
|
property_owner = ForeignKey('Company', related_name='owned_parks', null=True)
|
|
|
|
# Operational data
|
|
status = CharField(choices=STATUS_CHOICES, default="OPERATING")
|
|
opening_date = DateField(null=True, blank=True)
|
|
size_acres = DecimalField(max_digits=10, decimal_places=2)
|
|
|
|
# Statistics
|
|
average_rating = DecimalField(max_digits=3, decimal_places=2)
|
|
ride_count = IntegerField(null=True, blank=True)
|
|
coaster_count = IntegerField(null=True, blank=True)
|
|
```
|
|
|
|
#### Ride Models (`rides/models/`)
|
|
|
|
```python
|
|
# Ride Entity
|
|
class Ride(TrackedModel):
|
|
# Core identifiers
|
|
name = CharField(max_length=255)
|
|
slug = SlugField(max_length=255)
|
|
|
|
# Required relationships (enforced by .clinerules)
|
|
park = ForeignKey('parks.Park', related_name='rides')
|
|
|
|
# Optional business relationships
|
|
manufacturer = ForeignKey('Company', related_name='manufactured_rides')
|
|
designer = ForeignKey('Company', related_name='designed_rides')
|
|
ride_model = ForeignKey('RideModel', related_name='rides')
|
|
|
|
# Classification
|
|
category = CharField(choices=CATEGORY_CHOICES)
|
|
status = CharField(choices=STATUS_CHOICES, default='OPERATING')
|
|
```
|
|
|
|
#### Company Models (Shared across apps)
|
|
|
|
```python
|
|
# Company Entity (supports multiple roles)
|
|
class Company(TrackedModel):
|
|
class CompanyRole(TextChoices):
|
|
OPERATOR = 'OPERATOR', 'Park Operator'
|
|
PROPERTY_OWNER = 'PROPERTY_OWNER', 'Property Owner'
|
|
MANUFACTURER = 'MANUFACTURER', 'Ride Manufacturer'
|
|
DESIGNER = 'DESIGNER', 'Ride Designer'
|
|
|
|
name = CharField(max_length=255)
|
|
slug = SlugField(max_length=255, unique=True)
|
|
roles = ArrayField(CharField(choices=CompanyRole.choices))
|
|
```
|
|
|
|
### Geographic Models (`location/models/`)
|
|
|
|
```python
|
|
# Generic Location Model
|
|
class Location(TrackedModel):
|
|
# Generic relationship (can attach to any model)
|
|
content_type = ForeignKey(ContentType)
|
|
object_id = PositiveIntegerField()
|
|
content_object = GenericForeignKey('content_type', 'object_id')
|
|
|
|
# Geographic data (dual storage for compatibility)
|
|
latitude = DecimalField(max_digits=9, decimal_places=6)
|
|
longitude = DecimalField(max_digits=9, decimal_places=6)
|
|
point = PointField(srid=4326) # PostGIS geometry field
|
|
|
|
# Address components
|
|
street_address = CharField(max_length=255)
|
|
city = CharField(max_length=100)
|
|
state = CharField(max_length=100)
|
|
country = CharField(max_length=100)
|
|
postal_code = CharField(max_length=20)
|
|
```
|
|
|
|
### History Tracking with pghistory
|
|
|
|
Every critical model uses `@pghistory.track()` decoration for comprehensive audit trails:
|
|
|
|
```python
|
|
@pghistory.track()
|
|
class Park(TrackedModel):
|
|
# All field changes are automatically tracked
|
|
# Creates parallel history tables with full change logs
|
|
```
|
|
|
|
### Media and Content Models
|
|
|
|
```python
|
|
# Generic Photo Model
|
|
class Photo(TrackedModel):
|
|
# Generic relationship support
|
|
content_type = ForeignKey(ContentType)
|
|
object_id = PositiveIntegerField()
|
|
content_object = GenericForeignKey('content_type', 'object_id')
|
|
|
|
# Media handling
|
|
image = ImageField(upload_to=photo_upload_path, storage=MediaStorage())
|
|
is_primary = BooleanField(default=False)
|
|
is_approved = BooleanField(default=False)
|
|
|
|
# Metadata extraction
|
|
date_taken = DateTimeField(null=True) # Auto-extracted from EXIF
|
|
uploaded_by = ForeignKey(User, related_name='uploaded_photos')
|
|
```
|
|
|
|
### User and Authentication Models
|
|
|
|
```python
|
|
# Extended User Model
|
|
class User(AbstractUser):
|
|
class Roles(TextChoices):
|
|
USER = 'USER', 'User'
|
|
MODERATOR = 'MODERATOR', 'Moderator'
|
|
ADMIN = 'ADMIN', 'Admin'
|
|
SUPERUSER = 'SUPERUSER', 'Superuser'
|
|
|
|
# Immutable identifier
|
|
user_id = CharField(max_length=10, unique=True, editable=False)
|
|
|
|
# Permission system
|
|
role = CharField(choices=Roles.choices, default=Roles.USER)
|
|
|
|
# User preferences
|
|
theme_preference = CharField(choices=ThemePreference.choices)
|
|
```
|
|
|
|
---
|
|
|
|
## Visual Theme and Design System
|
|
|
|
### Design Philosophy
|
|
|
|
ThrillWiki implements a sophisticated **dark-first design system** with vibrant accent colors that reflect the excitement of theme parks and roller coasters.
|
|
|
|
#### Color Palette
|
|
|
|
```css
|
|
:root {
|
|
--primary: #4f46e5; /* Vibrant indigo */
|
|
--secondary: #e11d48; /* Vibrant rose */
|
|
--accent: #8b5cf6; /* Purple accent */
|
|
}
|
|
```
|
|
|
|
#### Background Gradients
|
|
|
|
```css
|
|
/* Light theme */
|
|
body {
|
|
background: linear-gradient(to bottom right,
|
|
white,
|
|
rgb(239 246 255), /* blue-50 */
|
|
rgb(238 242 255) /* indigo-50 */
|
|
);
|
|
}
|
|
|
|
/* Dark theme */
|
|
body.dark {
|
|
background: linear-gradient(to bottom right,
|
|
rgb(3 7 18), /* gray-950 */
|
|
rgb(49 46 129), /* indigo-950 */
|
|
rgb(59 7 100) /* purple-950 */
|
|
);
|
|
}
|
|
```
|
|
|
|
### Tailwind CSS Configuration
|
|
|
|
#### Custom Configuration (`tailwind.config.js`)
|
|
|
|
```javascript
|
|
module.exports = {
|
|
darkMode: 'class', // Class-based dark mode
|
|
content: [
|
|
'./templates/**/*.html',
|
|
'./assets/css/src/**/*.css',
|
|
],
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: '#4f46e5',
|
|
secondary: '#e11d48',
|
|
accent: '#8b5cf6'
|
|
},
|
|
fontFamily: {
|
|
'sans': ['Poppins', 'sans-serif'],
|
|
},
|
|
},
|
|
},
|
|
plugins: [
|
|
require("@tailwindcss/typography"),
|
|
require("@tailwindcss/forms"),
|
|
require("@tailwindcss/aspect-ratio"),
|
|
require("@tailwindcss/container-queries"),
|
|
// Custom HTMX variants
|
|
plugin(function ({ addVariant }) {
|
|
addVariant("htmx-settling", ["&.htmx-settling", ".htmx-settling &"]);
|
|
addVariant("htmx-request", ["&.htmx-request", ".htmx-request &"]);
|
|
addVariant("htmx-swapping", ["&.htmx-swapping", ".htmx-swapping &"]);
|
|
addVariant("htmx-added", ["&.htmx-added", ".htmx-added &"]);
|
|
}),
|
|
],
|
|
}
|
|
```
|
|
|
|
### Component System (`static/css/src/input.css`)
|
|
|
|
#### Button Components
|
|
|
|
```css
|
|
.btn-primary {
|
|
@apply inline-flex items-center px-6 py-2.5 border border-transparent
|
|
rounded-full shadow-md text-sm font-medium text-white
|
|
bg-gradient-to-r from-primary to-secondary
|
|
hover:from-primary/90 hover:to-secondary/90
|
|
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary/50
|
|
transform hover:scale-105 transition-all;
|
|
}
|
|
|
|
.btn-secondary {
|
|
@apply inline-flex items-center px-6 py-2.5 border border-gray-200
|
|
dark:border-gray-700 rounded-full shadow-md text-sm font-medium
|
|
text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800
|
|
hover:bg-gray-50 dark:hover:bg-gray-700
|
|
transform hover:scale-105 transition-all;
|
|
}
|
|
```
|
|
|
|
#### Navigation Components
|
|
|
|
```css
|
|
.nav-link {
|
|
@apply flex items-center text-gray-700 dark:text-gray-200
|
|
px-6 py-2.5 rounded-lg font-medium border border-transparent
|
|
hover:border-primary/20 dark:hover:border-primary/30
|
|
hover:text-primary dark:text-primary
|
|
hover:bg-primary/10 dark:bg-primary/20;
|
|
}
|
|
```
|
|
|
|
#### Card System
|
|
|
|
```css
|
|
.card {
|
|
@apply p-6 bg-white dark:bg-gray-800 border rounded-lg shadow-lg
|
|
border-gray-200/50 dark:border-gray-700/50;
|
|
}
|
|
|
|
.card-hover {
|
|
@apply transition-transform transform hover:-translate-y-1;
|
|
}
|
|
```
|
|
|
|
#### Responsive Grid System
|
|
|
|
```css
|
|
/* Adaptive grid with content-aware sizing */
|
|
.grid-adaptive {
|
|
@apply grid gap-6;
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
}
|
|
|
|
/* Stats grid with even layouts */
|
|
.grid-stats {
|
|
@apply grid gap-4;
|
|
grid-template-columns: repeat(2, 1fr); /* Mobile: 2 columns */
|
|
}
|
|
|
|
@media (min-width: 1024px) {
|
|
.grid-stats {
|
|
grid-template-columns: repeat(3, 1fr); /* Desktop: 3 columns */
|
|
}
|
|
}
|
|
|
|
@media (min-width: 1280px) {
|
|
.grid-stats {
|
|
grid-template-columns: repeat(5, 1fr); /* Large: 5 columns */
|
|
}
|
|
}
|
|
```
|
|
|
|
### Dark Mode Implementation
|
|
|
|
#### Theme Toggle System
|
|
|
|
The theme system provides:
|
|
- **Automatic detection** of system preference
|
|
- **Manual toggle** with persistent storage
|
|
- **Flash prevention** during page load
|
|
- **Smooth transitions** between themes
|
|
|
|
```javascript
|
|
// Theme initialization (prevents flash)
|
|
let theme = localStorage.getItem("theme");
|
|
if (!theme) {
|
|
theme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
? "dark" : "light";
|
|
localStorage.setItem("theme", theme);
|
|
}
|
|
if (theme === "dark") {
|
|
document.documentElement.classList.add("dark");
|
|
}
|
|
```
|
|
|
|
#### CSS Custom Properties for Theme Switching
|
|
|
|
```css
|
|
/* Theme-aware components */
|
|
.auth-card {
|
|
@apply w-full max-w-md p-8 mx-auto border shadow-xl
|
|
bg-white/90 dark:bg-gray-800/90 rounded-2xl backdrop-blur-sm
|
|
border-gray-200/50 dark:border-gray-700/50;
|
|
}
|
|
|
|
/* Status badges with theme support */
|
|
.status-operating {
|
|
@apply text-green-800 bg-green-100 dark:bg-green-700 dark:text-green-50;
|
|
}
|
|
|
|
.status-closed {
|
|
@apply text-red-800 bg-red-100 dark:bg-red-700 dark:text-red-50;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Frontend Implementation Patterns
|
|
|
|
### HTMX Integration Patterns
|
|
|
|
ThrillWiki leverages HTMX for dynamic interactions while maintaining server-side rendering benefits:
|
|
|
|
#### Dynamic Content Loading
|
|
|
|
```html
|
|
<!-- Search with live results -->
|
|
<form hx-get="{% url 'parks:search' %}"
|
|
hx-target="#search-results"
|
|
hx-trigger="input changed delay:300ms">
|
|
<input type="text" name="q" placeholder="Search parks and rides...">
|
|
</form>
|
|
|
|
<div id="search-results"
|
|
hx-indicator="#loading-spinner">
|
|
<!-- Results loaded dynamically -->
|
|
</div>
|
|
```
|
|
|
|
#### Modal Interactions
|
|
|
|
```html
|
|
<!-- HTMX-powered modals -->
|
|
<div hx-get="{% url 'account_login' %}"
|
|
hx-target="body"
|
|
hx-swap="beforeend"
|
|
class="cursor-pointer menu-item">
|
|
<span>Login</span>
|
|
</div>
|
|
```
|
|
|
|
#### Custom HTMX Variants
|
|
|
|
```css
|
|
/* Loading states */
|
|
.htmx-request .htmx-indicator {
|
|
display: block;
|
|
}
|
|
|
|
/* Transition effects */
|
|
.htmx-settling {
|
|
opacity: 0.7;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.htmx-swapping {
|
|
transform: scale(0.98);
|
|
transition: transform 0.2s ease;
|
|
}
|
|
```
|
|
|
|
### Alpine.js Patterns
|
|
|
|
Alpine.js handles client-side state management and interactions:
|
|
|
|
#### Dropdown Components
|
|
|
|
```html
|
|
<div x-data="{ open: false }"
|
|
@click.outside="open = false">
|
|
<!-- Profile dropdown -->
|
|
<img @click="open = !open"
|
|
src="{{ user.profile.avatar.url }}"
|
|
class="cursor-pointer">
|
|
|
|
<div x-cloak
|
|
x-show="open"
|
|
x-transition:enter="transition ease-out duration-100"
|
|
x-transition:enter-start="transform opacity-0 scale-95"
|
|
x-transition:enter-end="transform opacity-100 scale-100">
|
|
<!-- Dropdown content -->
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
#### Modal Management
|
|
|
|
```html
|
|
<div x-data="{
|
|
show: false,
|
|
editingPhoto: null,
|
|
init() { this.editingPhoto = { caption: '' }; }
|
|
}"
|
|
@show-photo-upload.window="show = true; init()">
|
|
<!-- Modal implementation -->
|
|
</div>
|
|
```
|
|
|
|
### JavaScript Architecture (`static/js/`)
|
|
|
|
#### Modular JavaScript Organization
|
|
|
|
```
|
|
static/js/
|
|
├── main.js # Core functionality (theme, navigation)
|
|
├── alerts.js # Alert system management
|
|
├── photo-gallery.js # Photo gallery interactions
|
|
├── park-map.js # Leaflet map integration
|
|
├── location-autocomplete.js # Geographic search
|
|
└── alpine.min.js # Alpine.js framework
|
|
```
|
|
|
|
#### Theme Management (`static/js/main.js`)
|
|
|
|
```javascript
|
|
// Theme handling with system preference detection
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
const html = document.documentElement;
|
|
|
|
// Initialize toggle state
|
|
if (themeToggle) {
|
|
themeToggle.checked = html.classList.contains('dark');
|
|
|
|
// Handle toggle changes
|
|
themeToggle.addEventListener('change', function() {
|
|
const isDark = this.checked;
|
|
html.classList.toggle('dark', isDark);
|
|
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
});
|
|
|
|
// Listen for system theme changes
|
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
mediaQuery.addEventListener('change', (e) => {
|
|
if (!localStorage.getItem('theme')) {
|
|
const isDark = e.matches;
|
|
html.classList.toggle('dark', isDark);
|
|
themeToggle.checked = isDark;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
```
|
|
|
|
#### Mobile Navigation
|
|
|
|
```javascript
|
|
// Mobile menu with smooth transitions
|
|
const toggleMenu = () => {
|
|
isMenuOpen = !isMenuOpen;
|
|
mobileMenu.classList.toggle('show', isMenuOpen);
|
|
mobileMenuBtn.setAttribute('aria-expanded', isMenuOpen.toString());
|
|
|
|
// Update icon
|
|
const icon = mobileMenuBtn.querySelector('i');
|
|
icon.classList.remove(isMenuOpen ? 'fa-bars' : 'fa-times');
|
|
icon.classList.add(isMenuOpen ? 'fa-times' : 'fa-bars');
|
|
};
|
|
```
|
|
|
|
### Template System Patterns
|
|
|
|
#### Component-Based Architecture
|
|
|
|
```
|
|
templates/
|
|
├── base/
|
|
│ └── base.html # Base template with navigation
|
|
├── core/
|
|
│ ├── search/
|
|
│ │ ├── components/ # Search UI components
|
|
│ │ ├── layouts/ # Search layout templates
|
|
│ │ └── partials/ # Reusable search elements
|
|
├── parks/
|
|
│ ├── partials/
|
|
│ │ ├── park_list_item.html # Reusable park card
|
|
│ │ ├── park_actions.html # Action buttons
|
|
│ │ └── park_stats.html # Statistics display
|
|
│ ├── park_detail.html # Main park page
|
|
│ └── park_list.html # Park listing page
|
|
└── media/
|
|
└── partials/
|
|
├── photo_display.html # Photo gallery component
|
|
└── photo_upload.html # Upload interface
|
|
```
|
|
|
|
#### Template Inheritance Pattern
|
|
|
|
```html
|
|
<!-- base/base.html -->
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>{% block title %}ThrillWiki{% endblock %}</title>
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body class="bg-gradient-to-br from-white via-blue-50 to-indigo-50
|
|
dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950">
|
|
|
|
{% include "base/navigation.html" %}
|
|
|
|
<main class="container flex-grow px-6 py-8 mx-auto">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
{% block extra_js %}{% endblock %}
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
#### HTMX Template Patterns
|
|
|
|
```html
|
|
<!-- Responsive template selection -->
|
|
{% extends "base/base.html" %}
|
|
|
|
{% block content %}
|
|
<div hx-get="{% url 'parks:park_list' %}"
|
|
hx-target="#park-grid"
|
|
hx-swap="innerHTML"
|
|
hx-trigger="load, view-change from:body">
|
|
|
|
<div id="park-grid" class="grid-adaptive">
|
|
{% include "parks/partials/park_list_item.html" %}
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
```
|
|
|
|
---
|
|
|
|
## User Experience and Key Features
|
|
|
|
### Navigation and Information Architecture
|
|
|
|
#### Primary Navigation Structure
|
|
|
|
```
|
|
ThrillWiki Navigation
|
|
├── Home # Dashboard with featured content
|
|
├── Parks # Theme park directory
|
|
│ ├── Browse Parks # Filterable park listings
|
|
│ ├── Add New Park # User contribution form
|
|
│ └── [Park Detail] # Individual park pages
|
|
├── Rides # Ride directory (global)
|
|
│ ├── Browse Rides # Cross-park ride search
|
|
│ ├── Add New Ride # User contribution form
|
|
│ └── [Ride Detail] # Individual ride pages
|
|
├── Search # Universal search interface
|
|
└── User Account
|
|
├── Profile # User profile and stats
|
|
├── Settings # Preferences and account
|
|
├── Moderation # Content review (if authorized)
|
|
└── Admin # System administration (if authorized)
|
|
```
|
|
|
|
#### Responsive Navigation Patterns
|
|
|
|
- **Desktop**: Full horizontal navigation with search bar
|
|
- **Tablet**: Collapsible navigation with maintained search functionality
|
|
- **Mobile**: Hamburger menu with slide-out panel
|
|
|
|
### Core Feature Set
|
|
|
|
#### 1. Park Management System
|
|
|
|
**Park Detail Pages** provide comprehensive information:
|
|
|
|
```
|
|
Park Information Hierarchy
|
|
├── Header Section
|
|
│ ├── Park name and location
|
|
│ ├── Status badge (Operating, Closed, etc.)
|
|
│ ├── Average rating display
|
|
│ └── Quick action buttons
|
|
├── Statistics Dashboard
|
|
│ ├── Operator information (priority display)
|
|
│ ├── Property owner (if different)
|
|
│ ├── Total ride count (linked)
|
|
│ ├── Roller coaster count
|
|
│ ├── Opening date
|
|
│ └── Website link
|
|
├── Content Sections
|
|
│ ├── Photo gallery (if photos exist)
|
|
│ ├── About section (description)
|
|
│ ├── Rides & Attractions (preview list)
|
|
│ └── Location map (if coordinates available)
|
|
└── Additional Information
|
|
├── History timeline
|
|
├── Related parks
|
|
└── User contributions
|
|
```
|
|
|
|
**Key UX Features**:
|
|
- **Smart statistics layout**: Responsive grid that prevents awkward spacing
|
|
- **Priority content placement**: Operator information prominently featured
|
|
- **Contextual actions**: Edit/moderate buttons appear based on user permissions
|
|
- **Progressive disclosure**: Detailed information revealed as needed
|
|
|
|
#### 2. Advanced Search and Filtering
|
|
|
|
**Unified Search System** supports:
|
|
|
|
- **Cross-content search**: Parks, rides, companies in single interface
|
|
- **Geographic filtering**: Search within specific regions or distances
|
|
- **Attribute filtering**: Status, ride types, ratings, opening dates
|
|
- **Real-time results**: HTMX-powered instant search feedback
|
|
|
|
**Search Result Patterns**:
|
|
```html
|
|
<!-- Search results with context -->
|
|
<div class="search-result-item">
|
|
<div class="result-type-badge">Park</div>
|
|
<h3 class="result-title">{{ park.name }}</h3>
|
|
<p class="result-location">{{ park.formatted_location }}</p>
|
|
<div class="result-stats">
|
|
<span>{{ park.ride_count }} rides</span>
|
|
<span>{{ park.get_status_display }}</span>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
#### 3. Geographic and Mapping Features
|
|
|
|
**Unified Map Service** provides:
|
|
|
|
- **Multi-layer mapping**: Parks, rides, and companies on single map
|
|
- **Intelligent clustering**: Zoom-level appropriate point grouping
|
|
- **Performance optimization**: Smart caching and result limiting
|
|
- **Geographic bounds**: Efficient spatial queries using PostGIS
|
|
|
|
**Map Integration Pattern**:
|
|
```javascript
|
|
// Park detail map initialization
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
{% with location=park.location.first %}
|
|
initParkMap({{ location.latitude }}, {{ location.longitude }}, "{{ park.name }}");
|
|
{% endwith %}
|
|
});
|
|
```
|
|
|
|
#### 4. Content Moderation Workflow
|
|
|
|
**Submission Process**:
|
|
|
|
```
|
|
User Contribution Flow
|
|
├── Content Creation
|
|
│ ├── Form submission (parks, rides, photos)
|
|
│ ├── Validation and sanitization
|
|
│ └── EditSubmission/PhotoSubmission creation
|
|
├── Review Process
|
|
│ ├── Moderator dashboard listing
|
|
│ ├── Side-by-side comparison view
|
|
│ ├── Edit capability before approval
|
|
│ └── Approval/rejection with notes
|
|
├── Publication
|
|
│ ├── Automatic publication for moderators
|
|
│ ├── Content integration into main database
|
|
│ └── User notification system
|
|
└── History Tracking
|
|
├── Complete audit trail
|
|
├── Revert capability
|
|
└── Performance metrics
|
|
```
|
|
|
|
**Moderation Features**:
|
|
- **Auto-approval**: Moderators bypass review process
|
|
- **Edit before approval**: Moderators can refine submissions
|
|
- **Batch operations**: Efficient handling of multiple submissions
|
|
- **Escalation system**: Complex cases forwarded to administrators
|
|
|
|
#### 5. Photo and Media Management
|
|
|
|
**Photo System Features**:
|
|
|
|
- **Multi-format support**: JPEG, PNG with automatic optimization
|
|
- **EXIF extraction**: Automatic date/time capture from metadata
|
|
- **Approval workflow**: Moderation for user-uploaded content
|
|
- **Smart storage**: Organized directory structure by content type
|
|
- **Primary photo designation**: Featured image selection per entity
|
|
|
|
**Upload Interface**:
|
|
```html
|
|
<!-- Modal photo upload -->
|
|
<div x-data="photoUploadModal()"
|
|
@show-photo-upload.window="show = true">
|
|
<form hx-post="{% url 'media:upload' %}"
|
|
hx-encoding="multipart/form-data">
|
|
{% csrf_token %}
|
|
<input type="file" multiple accept="image/*">
|
|
<input type="text" placeholder="Caption (optional)">
|
|
</form>
|
|
</div>
|
|
```
|
|
|
|
#### 6. User Authentication and Profiles
|
|
|
|
**Authentication Features**:
|
|
- **Social OAuth**: Google and Discord integration
|
|
- **Custom profiles**: Display names, avatars, bio information
|
|
- **Role-based permissions**: User, Moderator, Admin, Superuser levels
|
|
- **Theme preferences**: User-specific dark/light mode settings
|
|
|
|
**Profile Statistics**:
|
|
```html
|
|
<!-- User profile stats -->
|
|
<div class="profile-stats grid-stats">
|
|
<div class="stat-card">
|
|
<span class="stat-value">{{ profile.coaster_credits }}</span>
|
|
<span class="stat-label">Coaster Credits</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">{{ profile.dark_ride_credits }}</span>
|
|
<span class="stat-label">Dark Rides</span>
|
|
</div>
|
|
<!-- Additional stat types... -->
|
|
</div>
|
|
```
|
|
|
|
#### 7. History and Audit System
|
|
|
|
**Change Tracking Features**:
|
|
- **Complete audit trails**: Every modification recorded
|
|
- **Diff visualization**: Before/after comparisons
|
|
- **User attribution**: Change tracking by user
|
|
- **Revert capability**: Rollback to previous versions
|
|
- **Performance monitoring**: Query and response time tracking
|
|
|
|
### Accessibility and Responsive Design
|
|
|
|
#### Mobile-First Approach
|
|
|
|
- **Responsive breakpoints**: 540px, 768px, 1024px, 1280px+
|
|
- **Touch-friendly interfaces**: Appropriate button sizes and spacing
|
|
- **Optimized content hierarchy**: Essential information prioritized on small screens
|
|
|
|
#### Accessibility Features
|
|
|
|
- **Semantic HTML**: Proper heading structure and landmarks
|
|
- **ARIA labels**: Screen reader support for interactive elements
|
|
- **Keyboard navigation**: Full keyboard accessibility
|
|
- **Color contrast**: WCAG AA compliant color schemes
|
|
- **Focus indicators**: Clear focus states for interactive elements
|
|
|
|
---
|
|
|
|
## Page Structure and Templates
|
|
|
|
### Template Hierarchy and Organization
|
|
|
|
#### Base Template Architecture
|
|
|
|
```html
|
|
<!-- templates/base/base.html -->
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="csrf-token" content="{{ csrf_token }}">
|
|
<title>{% block title %}ThrillWiki{% endblock %}</title>
|
|
|
|
<!-- Theme prevention script (prevents flash) -->
|
|
<script>
|
|
let theme = localStorage.getItem("theme");
|
|
if (!theme) {
|
|
theme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
? "dark" : "light";
|
|
localStorage.setItem("theme", theme);
|
|
}
|
|
if (theme === "dark") {
|
|
document.documentElement.classList.add("dark");
|
|
}
|
|
</script>
|
|
|
|
<!-- External dependencies -->
|
|
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
|
|
<script defer src="{% static 'js/alpine.min.js' %}"></script>
|
|
|
|
<!-- Styling -->
|
|
<link href="{% static 'css/tailwind.css' %}" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
|
|
{% block extra_head %}{% endblock %}
|
|
</head>
|
|
<body class="flex flex-col min-h-screen text-gray-900
|
|
bg-gradient-to-br from-white via-blue-50 to-indigo-50
|
|
dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950
|
|
dark:text-white">
|
|
|
|
{% include "base/header.html" %}
|
|
|
|
{% include "base/flash_messages.html" %}
|
|
|
|
<main class="container flex-grow px-6 py-8 mx-auto">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
{% include "base/footer.html" %}
|
|
|
|
<script src="{% static 'js/main.js' %}"></script>
|
|
{% block extra_js %}{% endblock %}
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
#### Component-Based Template System
|
|
|
|
##### Navigation Component (`templates/base/header.html`)
|
|
|
|
```html
|
|
<header class="sticky top-0 z-40 border-b shadow-lg
|
|
bg-white/90 dark:bg-gray-800/90 backdrop-blur-lg
|
|
border-gray-200/50 dark:border-gray-700/50">
|
|
<nav class="container mx-auto nav-container">
|
|
<div class="flex items-center justify-between">
|
|
|
|
<!-- Logo -->
|
|
<div class="flex items-center">
|
|
<a href="{% url 'home' %}"
|
|
class="font-bold text-transparent transition-transform site-logo
|
|
bg-gradient-to-r from-primary to-secondary bg-clip-text
|
|
hover:scale-105">
|
|
ThrillWiki
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Main Navigation Links -->
|
|
<div class="flex items-center space-x-2 sm:space-x-4">
|
|
<a href="{% url 'parks:park_list' %}" class="nav-link">
|
|
<i class="fas fa-map-marker-alt"></i>
|
|
<span>Parks</span>
|
|
</a>
|
|
<a href="{% url 'rides:global_ride_list' %}" class="nav-link">
|
|
<i class="fas fa-rocket"></i>
|
|
<span>Rides</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Search Bar (Desktop) -->
|
|
<div class="flex-1 hidden max-w-md mx-8 lg:flex">
|
|
<form action="{% url 'search:search' %}" method="get" class="w-full">
|
|
<div class="relative">
|
|
<input type="text" name="q"
|
|
placeholder="Search parks and rides..."
|
|
class="form-input">
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- User Menu and Theme Toggle -->
|
|
<div class="flex items-center space-x-2 sm:space-x-6">
|
|
{% include "base/theme_toggle.html" %}
|
|
{% include "base/user_menu.html" %}
|
|
{% include "base/mobile_menu_button.html" %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mobile Menu -->
|
|
{% include "base/mobile_menu.html" %}
|
|
</nav>
|
|
</header>
|
|
```
|
|
|
|
##### Theme Toggle Component
|
|
|
|
```html
|
|
<!-- templates/base/theme_toggle.html -->
|
|
<label for="theme-toggle" class="cursor-pointer">
|
|
<input type="checkbox" id="theme-toggle" class="hidden">
|
|
<div class="inline-flex items-center justify-center p-2
|
|
text-gray-500 transition-colors hover:text-primary
|
|
dark:text-gray-400 dark:hover:text-primary theme-toggle-btn"
|
|
role="button" aria-label="Toggle dark mode">
|
|
<i class="text-xl fas"></i>
|
|
</div>
|
|
</label>
|
|
```
|
|
|
|
### Page-Specific Template Patterns
|
|
|
|
#### Park Detail Page Structure
|
|
|
|
```html
|
|
<!-- templates/parks/park_detail.html -->
|
|
{% extends "base/base.html" %}
|
|
{% load static park_tags %}
|
|
|
|
{% block title %}{{ park.name }} - ThrillWiki{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
{% if park.location.exists %}
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container px-4 mx-auto sm:px-6 lg:px-8">
|
|
|
|
<!-- Dynamic Action Buttons -->
|
|
<div hx-get="{% url 'parks:park_actions' park.slug %}"
|
|
hx-trigger="load, auth-changed from:body"
|
|
hx-swap="innerHTML">
|
|
</div>
|
|
|
|
<!-- Park Header -->
|
|
<div class="p-compact mb-6 bg-white rounded-lg shadow-lg dark:bg-gray-800">
|
|
<div class="text-center">
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white lg:text-4xl">
|
|
{{ park.name }}
|
|
</h1>
|
|
{% if park.formatted_location %}
|
|
<div class="flex items-center justify-center mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
<i class="mr-1 fas fa-map-marker-alt"></i>
|
|
<p>{{ park.formatted_location }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Grid -->
|
|
<div class="grid-stats mb-6">
|
|
{% include "parks/partials/park_stats.html" %}
|
|
</div>
|
|
|
|
<!-- Photo Gallery -->
|
|
{% if park.photos.exists %}
|
|
<div class="p-optimized mb-8 bg-white rounded-lg shadow dark:bg-gray-800">
|
|
<h2 class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">Photos</h2>
|
|
{% include "media/partials/photo_display.html" %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Main Content Grid -->
|
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
|
|
<!-- Left Column: Description and Rides -->
|
|
<div class="lg:col-span-2">
|
|
{% include "parks/partials/park_description.html" %}
|
|
{% include "parks/partials/park_rides.html" %}
|
|
</div>
|
|
|
|
<!-- Right Column: Map and Additional Info -->
|
|
<div class="lg:col-span-1">
|
|
{% include "parks/partials/park_map.html" %}
|
|
{% include "parks/partials/park_history.html" %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Photo Upload Modal -->
|
|
{% include "media/partials/photo_upload_modal.html" %}
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="{% static 'js/photo-gallery.js' %}"></script>
|
|
{% if park.location.exists %}
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script src="{% static 'js/park-map.js' %}"></script>
|
|
{% endif %}
|
|
{% endblock %}
|
|
```
|
|
|
|
#### Reusable Partial Templates
|
|
|
|
##### Park Statistics Component
|
|
|
|
```html
|
|
<!-- templates/parks/partials/park_stats.html -->
|
|
<!-- Operator - Priority Card (First Position) -->
|
|
{% if park.operator %}
|
|
<div class="bg-white rounded-lg shadow-lg dark:bg-gray-800 p-compact card-stats card-stats-priority">
|
|
<div class="text-center">
|
|
<dt class="text-sm font-semibold text-gray-900 dark:text-white">Operator</dt>
|
|
<dd class="mt-1">
|
|
<a href="{% url 'operators:operator_detail' park.operator.slug %}"
|
|
class="text-sm font-bold text-sky-900 dark:text-sky-400 hover:text-sky-800 dark:hover:text-sky-300">
|
|
{{ park.operator.name }}
|
|
</a>
|
|
</dd>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Additional stats cards... -->
|
|
<a href="{% url 'parks:rides:ride_list' park.slug %}"
|
|
class="bg-white rounded-lg shadow-lg dark:bg-gray-800 p-compact card-stats transition-transform hover:scale-[1.02]">
|
|
<div class="text-center">
|
|
<dt class="text-sm font-semibold text-gray-900 dark:text-white">Total Rides</dt>
|
|
<dd class="mt-1 text-2xl font-bold text-sky-900 dark:text-sky-400">
|
|
{{ park.ride_count|default:"N/A" }}
|
|
</dd>
|
|
</div>
|
|
</a>
|
|
```
|
|
|
|
##### Photo Display Component
|
|
|
|
```html
|
|
<!-- templates/media/partials/photo_display.html -->
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
|
|
x-data="photoGallery()">
|
|
{% for photo in photos %}
|
|
<div class="relative group">
|
|
<img src="{{ photo.image.url }}"
|
|
alt="{{ photo.alt_text|default:photo.caption }}"
|
|
class="w-full h-48 object-cover rounded-lg shadow-md
|
|
transition-transform group-hover:scale-105 cursor-pointer"
|
|
@click="openModal('{{ photo.image.url }}', '{{ photo.caption }}')">
|
|
|
|
{% if photo.caption %}
|
|
<div class="absolute bottom-0 left-0 right-0 p-2
|
|
bg-gradient-to-t from-black/70 to-transparent rounded-b-lg">
|
|
<p class="text-white text-sm">{{ photo.caption }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
```
|
|
|
|
### Form Templates and User Input
|
|
|
|
#### Dynamic Form Rendering
|
|
|
|
```html
|
|
<!-- templates/parks/park_form.html -->
|
|
{% extends "base/base.html" %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-4xl mx-auto">
|
|
<div class="auth-card">
|
|
<h1 class="auth-title">
|
|
{% if is_edit %}Edit Park{% else %}Add New Park{% endif %}
|
|
</h1>
|
|
|
|
<form method="post" enctype="multipart/form-data"
|
|
class="space-y-6"
|
|
hx-post="{% if is_edit %}{% url 'parks:park_edit' object.slug %}{% else %}{% url 'parks:park_create' %}{% endif %}"
|
|
hx-target="#form-container"
|
|
hx-swap="innerHTML">
|
|
{% csrf_token %}
|
|
|
|
<!-- Dynamic field rendering -->
|
|
{% for field in form %}
|
|
<div class="form-group">
|
|
<label for="{{ field.id_for_label }}" class="form-label">
|
|
{{ field.label }}
|
|
{% if field.field.required %}<span class="text-red-500">*</span>{% endif %}
|
|
</label>
|
|
{{ field|add_class:"form-input" }}
|
|
{% if field.help_text %}
|
|
<div class="form-hint">{{ field.help_text }}</div>
|
|
{% endif %}
|
|
{% if field.errors %}
|
|
<div class="form-error">{{ field.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
|
|
<div class="flex justify-end space-x-4">
|
|
<a href="{% if is_edit %}{{ object.get_absolute_url }}{% else %}{% url 'parks:park_list' %}{% endif %}"
|
|
class="btn-secondary">Cancel</a>
|
|
<button type="submit" class="btn-primary">
|
|
{% if is_edit %}Update Park{% else %}Add Park{% endif %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
```
|
|
|
|
### Static File Organization
|
|
|
|
#### Asset Structure
|
|
|
|
```
|
|
static/
|
|
├── css/
|
|
│ ├── src/
|
|
│ │ └── input.css # Tailwind source
|
|
│ ├── tailwind.css # Compiled Tailwind
|
|
│ └── alerts.css # Custom alert styles
|
|
├── js/
|
|
│ ├── main.js # Core functionality
|
|
│ ├── alerts.js # Alert management
|
|
│ ├── photo-gallery.js # Photo interactions
|
|
│ ├── park-map.js # Map functionality
|
|
│ ├── location-autocomplete.js # Geographic search
|
|
│ └── alpine.min.js # Alpine.js framework
|
|
└── images/
|
|
├── placeholders/ # Default images
|
|
└── icons/ # Custom icons
|
|
```
|
|
|
|
#### Template Tag Usage
|
|
|
|
Custom template tags enhance template functionality:
|
|
|
|
```html
|
|
<!-- Using custom park tags -->
|
|
{% load park_tags %}
|
|
|
|
<!-- Status badge with automatic styling -->
|
|
<span class="{% park_status_class park.status %}">
|
|
{{ park.get_status_display }}
|
|
</span>
|
|
|
|
<!-- Rating display with stars -->
|
|
{% if park.average_rating %}
|
|
{% rating_stars park.average_rating %}
|
|
{% endif %}
|
|
```
|
|
|
|
---
|
|
|
|
## Services and Business Logic
|
|
|
|
### Unified Map Service Architecture
|
|
|
|
The `UnifiedMapService` provides the core geographic functionality for ThrillWiki, handling location data for parks, rides, and companies through a sophisticated service layer.
|
|
|
|
#### Service Architecture Overview
|
|
|
|
```
|
|
UnifiedMapService
|
|
├── LocationAbstractionLayer # Data source abstraction
|
|
├── ClusteringService # Point clustering for performance
|
|
├── MapCacheService # Intelligent caching
|
|
└── Data Structures # Type-safe data containers
|
|
```
|
|
|
|
#### Core Service Implementation
|
|
|
|
```python
|
|
# core/services/map_service.py
|
|
class UnifiedMapService:
|
|
"""
|
|
Main service orchestrating map data retrieval, filtering, clustering, and caching.
|
|
Provides a unified interface for all location types with performance optimization.
|
|
"""
|
|
|
|
# Performance thresholds
|
|
MAX_UNCLUSTERED_POINTS = 500
|
|
MAX_CLUSTERED_POINTS = 2000
|
|
DEFAULT_ZOOM_LEVEL = 10
|
|
|
|
def __init__(self):
|
|
self.location_layer = LocationAbstractionLayer()
|
|
self.clustering_service = ClusteringService()
|
|
self.cache_service = MapCacheService()
|
|
|
|
def get_map_data(
|
|
self,
|
|
bounds: Optional[GeoBounds] = None,
|
|
filters: Optional[MapFilters] = None,
|
|
zoom_level: int = DEFAULT_ZOOM_LEVEL,
|
|
cluster: bool = True,
|
|
use_cache: bool = True
|
|
) -> MapResponse:
|
|
"""
|
|
Primary method for retrieving unified map data with intelligent
|
|
caching, clustering, and performance optimization.
|
|
"""
|
|
# Implementation handles cache checking, database queries,
|
|
# smart limiting, clustering decisions, and response caching
|
|
pass
|
|
|
|
def get_location_details(self, location_type: str, location_id: int) -> Optional[UnifiedLocation]:
|
|
"""Get detailed information for a specific location with caching."""
|
|
pass
|
|
|
|
def search_locations(
|
|
self,
|
|
query: str,
|
|
bounds: Optional[GeoBounds] = None,
|
|
location_types: Optional[Set[LocationType]] = None,
|
|
limit: int = 50
|
|
) -> List[UnifiedLocation]:
|
|
"""Search locations with text query and geographic bounds."""
|
|
pass
|
|
```
|
|
|
|
#### Data Structure System
|
|
|
|
The service uses type-safe data structures for all map operations:
|
|
|
|
```python
|
|
# core/services/data_structures.py
|
|
|
|
class LocationType(Enum):
|
|
"""Types of locations supported by the map service."""
|
|
PARK = "park"
|
|
RIDE = "ride"
|
|
COMPANY = "company"
|
|
GENERIC = "generic"
|
|
|
|
@dataclass
|
|
class GeoBounds:
|
|
"""Geographic boundary box for spatial queries."""
|
|
north: float
|
|
south: float
|
|
east: float
|
|
west: float
|
|
|
|
def to_polygon(self) -> Polygon:
|
|
"""Convert bounds to PostGIS Polygon for database queries."""
|
|
return Polygon.from_bbox((self.west, self.south, self.east, self.north))
|
|
|
|
def expand(self, factor: float = 1.1) -> 'GeoBounds':
|
|
"""Expand bounds by factor for buffer queries."""
|
|
pass
|
|
|
|
@dataclass
|
|
class MapFilters:
|
|
"""Filtering options for map queries."""
|
|
location_types: Optional[Set[LocationType]] = None
|
|
park_status: Optional[Set[str]] = None
|
|
ride_types: Optional[Set[str]] = None
|
|
search_query: Optional[str] = None
|
|
min_rating: Optional[float] = None
|
|
has_coordinates: bool = True
|
|
country: Optional[str] = None
|
|
state: Optional[str] = None
|
|
city: Optional[str] = None
|
|
|
|
@dataclass
|
|
class UnifiedLocation:
|
|
"""Standardized location representation across all entity types."""
|
|
id: int
|
|
location_type: LocationType
|
|
name: str
|
|
coordinates: Point
|
|
url: str
|
|
additional_data: Dict[str, Any] = field(default_factory=dict)
|
|
```
|
|
|
|
### Moderation System
|
|
|
|
ThrillWiki implements a comprehensive moderation system for user-generated content edits:
|
|
|
|
#### Edit Submission Workflow
|
|
|
|
```python
|
|
# moderation/models.py
|
|
@pghistory.track()
|
|
class EditSubmission(TrackedModel):
|
|
"""Tracks all proposed changes to parks and rides."""
|
|
|
|
STATUS_CHOICES = [
|
|
("PENDING", "Pending"),
|
|
("APPROVED", "Approved"),
|
|
("REJECTED", "Rejected"),
|
|
("ESCALATED", "Escalated"),
|
|
]
|
|
|
|
SUBMISSION_TYPE_CHOICES = [
|
|
("EDIT", "Edit Existing"),
|
|
("CREATE", "Create New"),
|
|
]
|
|
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
|
object_id = models.PositiveIntegerField(null=True, blank=True)
|
|
content_object = GenericForeignKey("content_type", "object_id")
|
|
|
|
submission_type = models.CharField(max_length=10, choices=SUBMISSION_TYPE_CHOICES)
|
|
proposed_changes = models.JSONField() # Stores field-level changes
|
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="PENDING")
|
|
|
|
# Moderation tracking
|
|
reviewed_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True, blank=True,
|
|
related_name="reviewed_submissions"
|
|
)
|
|
reviewed_at = models.DateTimeField(null=True, blank=True)
|
|
reviewer_notes = models.TextField(blank=True)
|
|
```
|
|
|
|
#### Moderation Features
|
|
|
|
- **Change Tracking**: Every edit submission tracked with `django-pghistory`
|
|
- **Field-Level Changes**: JSON storage of specific field modifications
|
|
- **Review Workflow**: Pending → Approved/Rejected/Escalated states
|
|
- **Reviewer Assignment**: Track who reviewed each submission
|
|
- **Audit Trail**: Complete history of all moderation decisions
|
|
|
|
### Search and Autocomplete
|
|
|
|
#### Location-Based Search
|
|
|
|
```python
|
|
# Autocomplete integration for geographic search
|
|
class LocationAutocompleteView(autocomplete.Select2QuerySetView):
|
|
"""AJAX autocomplete for geographic locations."""
|
|
|
|
def get_queryset(self):
|
|
if not self.request.user.is_authenticated:
|
|
return Location.objects.none()
|
|
|
|
qs = Location.objects.filter(is_active=True)
|
|
|
|
if self.q:
|
|
qs = qs.filter(
|
|
Q(name__icontains=self.q) |
|
|
Q(city__icontains=self.q) |
|
|
Q(state__icontains=self.q) |
|
|
Q(country__icontains=self.q)
|
|
)
|
|
|
|
return qs.select_related('country', 'state').order_by('name')[:20]
|
|
```
|
|
|
|
#### Search Integration
|
|
|
|
- **HTMX-Powered Search**: Real-time search suggestions without page reloads
|
|
- **Geographic Filtering**: Search within specific bounds or regions
|
|
- **Multi-Model Search**: Unified search across parks, rides, and companies
|
|
- **Performance Optimized**: Cached results and smart query limiting
|
|
|
|
---
|
|
|
|
## Development Workflow
|
|
|
|
### Required Development Environment
|
|
|
|
#### UV Package Manager Integration
|
|
|
|
ThrillWiki exclusively uses [UV](https://github.com/astral-sh/uv) for all Python package management and Django commands:
|
|
|
|
```bash
|
|
# CRITICAL: Always use these exact commands
|
|
|
|
# Package Installation
|
|
uv add <package> # Add new dependencies
|
|
uv add --dev <package> # Add development dependencies
|
|
|
|
# Django Management Commands
|
|
uv run manage.py makemigrations # Create migrations
|
|
uv run manage.py migrate # Apply migrations
|
|
uv run manage.py createsuperuser # Create admin user
|
|
uv run manage.py shell # Start Django shell
|
|
uv run manage.py collectstatic # Collect static files
|
|
|
|
# Development Server (CRITICAL - use exactly as shown)
|
|
lsof -ti :8000 | xargs kill -9; find . -type d -name "__pycache__" -exec rm -r {} +; uv run manage.py tailwind runserver
|
|
```
|
|
|
|
**IMPORTANT**: Never use `python manage.py` or `pip install`. The project is configured exclusively for UV.
|
|
|
|
#### Local Development Setup
|
|
|
|
```bash
|
|
# Initial setup
|
|
git clone <repository-url>
|
|
cd thrillwiki_django_no_react
|
|
|
|
# Install dependencies
|
|
uv sync
|
|
|
|
# Database setup (requires PostgreSQL with PostGIS)
|
|
uv run manage.py migrate
|
|
|
|
# Create superuser
|
|
uv run manage.py createsuperuser
|
|
|
|
# Install Tailwind CSS and build
|
|
uv run manage.py tailwind install
|
|
uv run manage.py tailwind build
|
|
|
|
# Start development server
|
|
lsof -ti :8000 | xargs kill -9; find . -type d -name "__pycache__" -exec rm -r {} +; uv run manage.py tailwind runserver
|
|
```
|
|
|
|
### Database Requirements
|
|
|
|
#### PostgreSQL with PostGIS
|
|
|
|
```sql
|
|
-- Required PostgreSQL extensions
|
|
CREATE EXTENSION IF NOT EXISTS postgis;
|
|
CREATE EXTENSION IF NOT EXISTS postgis_topology;
|
|
CREATE EXTENSION IF NOT EXISTS postgis_raster;
|
|
```
|
|
|
|
#### Geographic Data
|
|
|
|
- **Coordinate System**: WGS84 (SRID: 4326) for all geographic data
|
|
- **Point Storage**: All locations stored as PostGIS Point geometry
|
|
- **Spatial Queries**: Optimized with GiST indexes for geographic searches
|
|
- **Distance Calculations**: Native PostGIS distance functions
|
|
|
|
### Frontend Development
|
|
|
|
#### Tailwind CSS Workflow
|
|
|
|
```bash
|
|
# Development mode (watches for changes)
|
|
uv run manage.py tailwind start
|
|
|
|
# Production build
|
|
uv run manage.py tailwind build
|
|
|
|
# Custom CSS location
|
|
static/css/src/input.css # Source file
|
|
static/css/tailwind.css # Compiled output
|
|
```
|
|
|
|
#### JavaScript Integration
|
|
|
|
- **Alpine.js**: Reactive components and state management
|
|
- **HTMX**: AJAX interactions and partial page updates
|
|
- **Custom Scripts**: Modular JavaScript in `static/js/` directory
|
|
|
|
---
|
|
|
|
## API Endpoints and URL Structure
|
|
|
|
### Primary URL Configuration
|
|
|
|
#### Main Application Routes
|
|
|
|
```python
|
|
# thrillwiki/urls.py
|
|
urlpatterns = [
|
|
path("admin/", admin.site.urls),
|
|
path("", HomeView.as_view(), name="home"),
|
|
|
|
# Autocomplete URLs (must be before other URLs)
|
|
path("ac/", autocomplete_urls),
|
|
|
|
# Core functionality
|
|
path("parks/", include("parks.urls", namespace="parks")),
|
|
path("rides/", include("rides.urls", namespace="rides")),
|
|
path("photos/", include("media.urls", namespace="photos")),
|
|
|
|
# Search and API
|
|
path("search/", include("core.urls.search", namespace="search")),
|
|
path("api/map/", include("core.urls.map_urls", namespace="map_api")),
|
|
|
|
# User management
|
|
path("accounts/", include("accounts.urls")),
|
|
path("accounts/", include("allauth.urls")),
|
|
path("user/<str:username>/", ProfileView.as_view(), name="user_profile"),
|
|
path("settings/", SettingsView.as_view(), name="settings"),
|
|
|
|
# Moderation system
|
|
path("moderation/", include("moderation.urls", namespace="moderation")),
|
|
|
|
# Static pages
|
|
path("terms/", TemplateView.as_view(template_name="pages/terms.html"), name="terms"),
|
|
path("privacy/", TemplateView.as_view(template_name="pages/privacy.html"), name="privacy"),
|
|
]
|
|
```
|
|
|
|
#### Parks URL Structure
|
|
|
|
```python
|
|
# parks/urls.py
|
|
app_name = "parks"
|
|
|
|
urlpatterns = [
|
|
# Main park views
|
|
path("", ParkSearchView.as_view(), name="park_list"),
|
|
path("create/", ParkCreateView.as_view(), name="park_create"),
|
|
path("<slug:slug>/", ParkDetailView.as_view(), name="park_detail"),
|
|
path("<slug:slug>/edit/", ParkUpdateView.as_view(), name="park_update"),
|
|
|
|
# HTMX endpoints
|
|
path("add-park-button/", add_park_button, name="add_park_button"),
|
|
path("search/location/", location_search, name="location_search"),
|
|
path("search/reverse-geocode/", reverse_geocode, name="reverse_geocode"),
|
|
path("areas/", get_park_areas, name="get_park_areas"),
|
|
path("suggest_parks/", suggest_parks, name="suggest_parks"),
|
|
|
|
# Park areas
|
|
path("<slug:park_slug>/areas/<slug:area_slug>/", ParkAreaDetailView.as_view(), name="area_detail"),
|
|
|
|
# Category-specific rides within parks
|
|
path("<slug:park_slug>/roller_coasters/", ParkSingleCategoryListView.as_view(),
|
|
{'category': 'RC'}, name="park_roller_coasters"),
|
|
path("<slug:park_slug>/dark_rides/", ParkSingleCategoryListView.as_view(),
|
|
{'category': 'DR'}, name="park_dark_rides"),
|
|
path("<slug:park_slug>/flat_rides/", ParkSingleCategoryListView.as_view(),
|
|
{'category': 'FR'}, name="park_flat_rides"),
|
|
path("<slug:park_slug>/water_rides/", ParkSingleCategoryListView.as_view(),
|
|
{'category': 'WR'}, name="park_water_rides"),
|
|
path("<slug:park_slug>/transports/", ParkSingleCategoryListView.as_view(),
|
|
{'category': 'TR'}, name="park_transports"),
|
|
path("<slug:park_slug>/others/", ParkSingleCategoryListView.as_view(),
|
|
{'category': 'OT'}, name="park_others"),
|
|
|
|
# Nested rides URLs
|
|
path("<slug:park_slug>/rides/", include("rides.park_urls", namespace="rides")),
|
|
]
|
|
```
|
|
|
|
### API Endpoints
|
|
|
|
#### Map API
|
|
|
|
```
|
|
GET /api/map/data/
|
|
- bounds: Geographic bounds (north,south,east,west)
|
|
- zoom: Map zoom level
|
|
- filters: JSON-encoded filter parameters
|
|
- Returns: Unified location data with clustering
|
|
|
|
GET /api/map/location/<type>/<id>/
|
|
- Returns: Detailed location information
|
|
|
|
POST /api/map/search/
|
|
- query: Search text
|
|
- bounds: Optional geographic bounds
|
|
- types: Location types to search
|
|
- Returns: Matching locations
|
|
```
|
|
|
|
#### Search API
|
|
|
|
```
|
|
GET /search/
|
|
- q: Search query
|
|
- type: Entity type (park, ride, company)
|
|
- location: Geographic filter
|
|
- Returns: Search results with pagination
|
|
|
|
GET /search/suggest/
|
|
- q: Partial query for autocomplete
|
|
- Returns: Quick suggestions
|
|
```
|
|
|
|
#### HTMX Endpoints
|
|
|
|
All HTMX endpoints return HTML fragments for seamless page updates:
|
|
|
|
```
|
|
POST /parks/suggest_parks/ # Park suggestions for autocomplete
|
|
GET /parks/areas/ # Dynamic area loading
|
|
POST /parks/search/location/ # Location search with coordinates
|
|
POST /parks/search/reverse-geocode/ # Address lookup from coordinates
|
|
```
|
|
|
|
---
|
|
|
|
## Conclusion
|
|
|
|
ThrillWiki represents a sophisticated Django application implementing modern web development practices with a focus on performance, user experience, and maintainability. The project successfully combines:
|
|
|
|
### Technical Excellence
|
|
|
|
- **Modern Django Patterns**: Service-oriented architecture with clear separation of concerns
|
|
- **Geographic Capabilities**: Full PostGIS integration for spatial data and mapping
|
|
- **Performance Optimization**: Intelligent caching, query optimization, and clustering
|
|
- **Type Safety**: Comprehensive use of dataclasses and enums for data integrity
|
|
|
|
### User Experience
|
|
|
|
- **Responsive Design**: Mobile-first approach with Tailwind CSS
|
|
- **Progressive Enhancement**: HTMX for seamless interactions without JavaScript complexity
|
|
- **Dark Mode Support**: Complete theming system with user preferences
|
|
- **Accessibility**: WCAG-compliant components and navigation
|
|
|
|
### Development Workflow
|
|
|
|
- **UV Integration**: Modern Python package management with reproducible builds
|
|
- **Comprehensive Testing**: Model validation, service testing, and frontend integration
|
|
- **Documentation**: Extensive inline documentation and architectural decisions
|
|
- **Moderation System**: Complete workflow for user-generated content management
|
|
|
|
### Architectural Strengths
|
|
|
|
1. **Scalability**: Service layer architecture supports growth and feature expansion
|
|
2. **Maintainability**: Clear code organization with consistent patterns
|
|
3. **Performance**: Optimized database queries and intelligent caching strategies
|
|
4. **Security**: Authentication, authorization, and input validation throughout
|
|
5. **Extensibility**: Plugin-ready architecture for additional features
|
|
|
|
The project demonstrates enterprise-level Django development practices while maintaining simplicity and developer experience. The combination of modern frontend techniques (HTMX, Alpine.js, Tailwind) with robust backend services creates a powerful platform for theme park and ride enthusiasts.
|
|
|
|
This documentation serves as both a technical reference and architectural guide for understanding and extending the ThrillWiki platform.
|