Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
cbe1dd726f [DEPENDABOT] Update: Bump django-allauth from 65.2.0 to 65.3.0
Bumps [django-allauth](https://github.com/sponsors/pennersr) from 65.2.0 to 65.3.0.
- [Commits](https://github.com/sponsors/pennersr/commits)

---
updated-dependencies:
- dependency-name: django-allauth
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 11:44:28 +00:00
212 changed files with 2907 additions and 13070 deletions

View File

@@ -2,40 +2,29 @@ name: Django CI
on:
push:
branches: [ main ]
branches: [ "main" ]
pull_request:
branches: [ main ]
branches: [ "main" ]
jobs:
test:
runs-on: ${{ matrix.os }}
build:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
os: [ubuntu-latest, macos-latest]
python-version: [3.13.1]
python-version: [3.12]
steps:
- uses: actions/checkout@v4
- name: Install Homebrew on Linux
if: runner.os == 'Linux'
run: |
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo "/home/linuxbrew/.linuxbrew/bin" >> $GITHUB_PATH
- name: Install GDAL with Homebrew
run: brew install gdal
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Tests
run: |
python manage.py test

View File

@@ -1,34 +0,0 @@
name: Claude Code Review
permissions:
contents: read
pull-requests: write
on:
# Run on new/updated PRs
pull_request:
types: [opened, reopened, synchronize]
# Allow manual triggers for existing PRs
workflow_dispatch:
inputs:
pr_number:
description: 'Pull Request Number'
required: true
type: string
jobs:
code-review:
runs-on: ubuntu-latest
environment: development_environment
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Claude Review
uses: pacnpal/claude-code-review@main
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
anthropic-key: ${{ secrets.ANTHROPIC_API_KEY }}
pr-number: ${{ github.event.pull_request.number || inputs.pr_number }}

View File

@@ -1 +0,0 @@
ThrillWiki.com

View File

@@ -1,67 +0,0 @@
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from reviews.models import Review
from parks.models import Park
from rides.models import Ride
from media.models import Photo
User = get_user_model()
class Command(BaseCommand):
help = "Cleans up test users and data created during e2e testing"
def handle(self, *args, **kwargs):
# Delete test users
test_users = User.objects.filter(username__in=["testuser", "moderator"])
count = test_users.count()
test_users.delete()
self.stdout.write(self.style.SUCCESS(f"Deleted {count} test users"))
# Delete test reviews
reviews = Review.objects.filter(user__username__in=["testuser", "moderator"])
count = reviews.count()
reviews.delete()
self.stdout.write(self.style.SUCCESS(f"Deleted {count} test reviews"))
# Delete test photos
photos = Photo.objects.filter(uploader__username__in=["testuser", "moderator"])
count = photos.count()
photos.delete()
self.stdout.write(self.style.SUCCESS(f"Deleted {count} test photos"))
# Delete test parks
parks = Park.objects.filter(name__startswith="Test Park")
count = parks.count()
parks.delete()
self.stdout.write(self.style.SUCCESS(f"Deleted {count} test parks"))
# Delete test rides
rides = Ride.objects.filter(name__startswith="Test Ride")
count = rides.count()
rides.delete()
self.stdout.write(self.style.SUCCESS(f"Deleted {count} test rides"))
# Clean up test files
import os
import glob
# Clean up test uploads
media_patterns = [
"media/uploads/test_*",
"media/avatars/test_*",
"media/park/test_*",
"media/rides/test_*",
]
for pattern in media_patterns:
files = glob.glob(pattern)
for f in files:
try:
os.remove(f)
self.stdout.write(self.style.SUCCESS(f"Deleted {f}"))
except OSError as e:
self.stdout.write(self.style.WARNING(f"Error deleting {f}: {e}"))
self.stdout.write(self.style.SUCCESS("Test data cleanup complete"))

View File

@@ -1,56 +0,0 @@
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
User = get_user_model()
class Command(BaseCommand):
help = "Creates test users for e2e testing"
def handle(self, *args, **kwargs):
# Create regular test user
if not User.objects.filter(username="testuser").exists():
user = User.objects.create_user(
username="testuser",
email="testuser@example.com",
[PASSWORD-REMOVED]",
)
self.stdout.write(self.style.SUCCESS(f"Created test user: {user.username}"))
else:
self.stdout.write(self.style.WARNING("Test user already exists"))
# Create moderator user
if not User.objects.filter(username="moderator").exists():
moderator = User.objects.create_user(
username="moderator",
email="moderator@example.com",
[PASSWORD-REMOVED]",
)
# Create moderator group if it doesn't exist
moderator_group, created = Group.objects.get_or_create(name="Moderators")
# Add relevant permissions
permissions = Permission.objects.filter(
codename__in=[
"change_review",
"delete_review",
"change_park",
"change_ride",
"moderate_photos",
"moderate_comments",
]
)
moderator_group.permissions.add(*permissions)
# Add user to moderator group
moderator.groups.add(moderator_group)
self.stdout.write(
self.style.SUCCESS(f"Created moderator user: {moderator.username}")
)
else:
self.stdout.write(self.style.WARNING("Moderator user already exists"))
self.stdout.write(self.style.SUCCESS("Test users setup complete"))

View File

@@ -22,7 +22,7 @@ class Command(BaseCommand):
self.stdout.write(f'- {site.domain} ({site.name})')
# Show callback URL
callback_url = 'http://localhost:8000/accounts/discord/login/callback/'
callback_url = f'http://localhost:8000/accounts/discord/login/callback/'
self.stdout.write('\nCallback URL to configure in Discord Developer Portal:')
self.stdout.write(callback_url)

View File

@@ -1,11 +1,9 @@
# Generated by Django 5.1.4 on 2025-02-10 01:10
# Generated by Django 5.1.3 on 2024-11-12 18:07
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
import pgtrigger.compiler
import pgtrigger.migrations
from django.conf import settings
from django.db import migrations, models
@@ -17,7 +15,6 @@ class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("contenttypes", "0002_remove_content_type_name"),
("pghistory", "0006_delete_aggregateevent"),
]
operations = [
@@ -232,7 +229,15 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name="TopList",
fields=[
("id", models.BigAutoField(primary_key=True, serialize=False)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=100)),
(
"category",
@@ -263,145 +268,6 @@ class Migration(migrations.Migration):
"ordering": ["-updated_at"],
},
),
migrations.CreateModel(
name="TopListEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),
("title", models.CharField(max_length=100)),
(
"category",
models.CharField(
choices=[
("RC", "Roller Coaster"),
("DR", "Dark Ride"),
("FR", "Flat Ride"),
("WR", "Water Ride"),
("PK", "Park"),
],
max_length=2,
),
),
("description", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"pgh_context",
models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="pghistory.context",
),
),
(
"pgh_obj",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="events",
to="accounts.toplist",
),
),
(
"user",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="TopListItem",
fields=[
("id", models.BigAutoField(primary_key=True, serialize=False)),
("object_id", models.PositiveIntegerField()),
("rank", models.PositiveIntegerField()),
("notes", models.TextField(blank=True)),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
(
"top_list",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="items",
to="accounts.toplist",
),
),
],
options={
"ordering": ["rank"],
},
),
migrations.CreateModel(
name="TopListItemEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),
("object_id", models.PositiveIntegerField()),
("rank", models.PositiveIntegerField()),
("notes", models.TextField(blank=True)),
(
"content_type",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="contenttypes.contenttype",
),
),
(
"pgh_context",
models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="pghistory.context",
),
),
(
"pgh_obj",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="events",
to="accounts.toplistitem",
),
),
(
"top_list",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="accounts.toplist",
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="UserProfile",
fields=[
@@ -452,66 +318,40 @@ class Migration(migrations.Migration):
),
],
),
pgtrigger.migrations.AddTrigger(
model_name="toplist",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "accounts_toplistevent" ("category", "created_at", "description", "id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "title", "updated_at", "user_id") VALUES (NEW."category", NEW."created_at", NEW."description", NEW."id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."title", NEW."updated_at", NEW."user_id"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="INSERT",
pgid="pgtrigger_insert_insert_26546",
table="accounts_toplist",
when="AFTER",
migrations.CreateModel(
name="TopListItem",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="toplist",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "accounts_toplistevent" ("category", "created_at", "description", "id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "title", "updated_at", "user_id") VALUES (NEW."category", NEW."created_at", NEW."description", NEW."id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."title", NEW."updated_at", NEW."user_id"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="UPDATE",
pgid="pgtrigger_update_update_84849",
table="accounts_toplist",
when="AFTER",
("object_id", models.PositiveIntegerField()),
("rank", models.PositiveIntegerField()),
("notes", models.TextField(blank=True)),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
),
),
migrations.AlterUniqueTogether(
name="toplistitem",
unique_together={("top_list", "rank")},
),
pgtrigger.migrations.AddTrigger(
model_name="toplistitem",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "accounts_toplistitemevent" ("content_type_id", "id", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rank", "top_list_id") VALUES (NEW."content_type_id", NEW."id", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rank", NEW."top_list_id"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="INSERT",
pgid="pgtrigger_insert_insert_56dfc",
table="accounts_toplistitem",
when="AFTER",
(
"top_list",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="items",
to="accounts.toplist",
),
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="toplistitem",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "accounts_toplistitemevent" ("content_type_id", "id", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rank", "top_list_id") VALUES (NEW."content_type_id", NEW."id", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rank", NEW."top_list_id"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="UPDATE",
pgid="pgtrigger_update_update_2b6e3",
table="accounts_toplistitem",
when="AFTER",
),
),
],
options={
"ordering": ["rank"],
"unique_together": {("top_list", "rank")},
},
),
]

View File

@@ -26,7 +26,7 @@ class TurnstileMixin:
'remoteip': request.META.get('REMOTE_ADDR'),
}
response = requests.post(settings.TURNSTILE_VERIFY_URL, data=data, timeout=60)
response = requests.post(settings.TURNSTILE_VERIFY_URL, data=data)
result = response.json()
if not result.get('success'):

View File

@@ -2,24 +2,22 @@ from django.contrib.auth.models import AbstractUser
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
import random
from PIL import Image, ImageDraw, ImageFont
from io import BytesIO
import base64
import os
import secrets
from history_tracking.models import TrackedModel
import pghistory
def generate_random_id(model_class, id_field):
"""Generate a random ID starting at 4 digits, expanding to 5 if needed"""
while True:
# Try to get a 4-digit number first
new_id = str(secrets.SystemRandom().randint(1000, 9999))
new_id = str(random.randint(1000, 9999))
if not model_class.objects.filter(**{id_field: new_id}).exists():
return new_id
# If all 4-digit numbers are taken, try 5 digits
new_id = str(secrets.SystemRandom().randint(10000, 99999))
new_id = str(random.randint(10000, 99999))
if not model_class.objects.filter(**{id_field: new_id}).exists():
return new_id
@@ -160,8 +158,7 @@ class PasswordReset(models.Model):
verbose_name = "Password Reset"
verbose_name_plural = "Password Resets"
@pghistory.track()
class TopList(TrackedModel):
class TopList(models.Model):
class Categories(models.TextChoices):
ROLLER_COASTER = 'RC', _('Roller Coaster')
DARK_RIDE = 'DR', _('Dark Ride')
@@ -189,8 +186,7 @@ class TopList(TrackedModel):
def __str__(self):
return f"{self.user.get_display_name()}'s {self.category} Top List: {self.title}"
@pghistory.track()
class TopListItem(TrackedModel):
class TopListItem(models.Model):
top_list = models.ForeignKey(
TopList,
on_delete=models.CASCADE,

View File

@@ -31,7 +31,7 @@ def create_user_profile(sender, instance, created, **kwargs):
if avatar_url:
try:
response = requests.get(avatar_url, timeout=60)
response = requests.get(avatar_url)
if response.status_code == 200:
img_temp = NamedTemporaryFile(delete=True)
img_temp.write(response.content)

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.1.4 on 2025-02-10 01:10
# Generated by Django 5.1.3 on 2024-11-12 18:07
import django.db.models.deletion
from django.db import migrations, models

View File

@@ -55,8 +55,3 @@ class PageView(models.Model):
return model_class.objects.filter(pk__in=id_list).order_by(preserved)
return model_class.objects.none()
def __str__(self):
model_name = self.__class__.__name__
fields_str = ", ".join((f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields))
return f"{model_name}({fields_str})"

View File

@@ -1,208 +0,0 @@
# Frontend Architecture Documentation
Last Updated: 2024-02-21
## Core Technologies
### 1. HTMX
- Used for dynamic updates and server interactions
- Enables partial page updates without full reloads
- Integrated with Django backend for seamless data exchange
- Used for form submissions and dynamic content loading
### 2. AlpineJS
- Handles client-side interactivity and state management
- Used for dropdowns, modals, and other interactive components
- Provides reactive data binding and event handling
- Key features used:
- x-data for component state
- x-show/x-if for conditional rendering
- x-model for two-way data binding
- x-on for event handling
### 3. Tailwind CSS
- Utility-first CSS framework for styling
- Custom configuration in tailwind.config.js
- Responsive design utilities
- Dark mode support with class-based implementation
- Custom color scheme with primary/secondary colors
## Styling System
### 1. Base Styles
- Font: Poppins (400, 500, 600, 700 weights)
- Color Scheme:
- Primary: Indigo (#4F46E5)
- Secondary: Rose (#E11D48)
- Gradients for interactive elements
- Dark mode compatible color palette
### 2. Component Classes
- Button Variants:
- .btn-primary: Gradient background with hover effects
- .btn-secondary: Light/dark mode aware styling
- Social login buttons with brand colors
- Form Elements:
- .form-input: Styled input fields
- .form-label: Consistent label styling
- .form-error: Error message styling
- Cards:
- .card: Base card styling with shadows
- .auth-card: Special styling for authentication forms
- Status Badges:
- .status-operating: Green success state
- .status-closed: Red error state
- .status-construction: Yellow warning state
### 3. Layout Components
- Responsive container system
- Grid system using Tailwind's grid utilities
- Flexbox-based navigation and content layouts
- Mobile-first responsive design
## Interactive Components
### 1. Navigation
- Responsive header with mobile menu
- User dropdown menu with authentication states
- Theme toggle (light/dark mode)
- Mobile-optimized navigation drawer
### 2. Forms
- Location autocomplete system
- Form validation with error states
- CSRF protection integration
- File upload handling
### 3. Alerts System
- Timed auto-dismissing alerts
- Slide animations for entry/exit
- Context-aware styling (success, error, info, warning)
- Accessible notifications
### 4. Modal System
- HTMX-powered dynamic content loading
- Alpine.js state management
- Backdrop blur effects
- Keyboard navigation support
## JavaScript Architecture
### 1. Core Functionality
- Theme management with local storage persistence
- HTMX configuration and setup
- Alpine.js component initialization
- Event delegation and handling
### 2. Location Autocomplete
- Progressive enhancement for location fields
- Country/Region/City hierarchical selection
- Dynamic filtering based on parent selections
- AJAX-powered suggestions
### 3. Form Handling
- Client-side validation
- File upload preview
- Dynamic form updates
- Error state management
## Performance Optimizations
### 1. Asset Loading
- Deferred script loading
- Preloaded critical assets
- Minified production assets
- Cached static resources
### 2. Rendering
- Progressive enhancement
- Partial page updates
- Lazy loading of images
- Optimized animation performance
### 3. State Management
- Efficient DOM updates
- Debounced search inputs
- Throttled scroll handlers
- Memory leak prevention
## Accessibility Features
### 1. Semantic HTML
- Proper heading hierarchy
- ARIA labels and roles
- Semantic landmark regions
- Meaningful alt text
### 2. Keyboard Navigation
- Focus management
- Skip links
- Keyboard shortcuts
- Focus trapping in modals
### 3. Screen Readers
- ARIA live regions for alerts
- Status role for notifications
- Description text for icons
- Form label associations
## Development Workflow
### 1. CSS Organization
- Utility-first approach
- Component-specific styles
- Shared design tokens
- Dark mode variants
### 2. JavaScript Patterns
- Event delegation
- Component encapsulation
- State management
- Error handling
### 3. Testing Considerations
- Browser compatibility
- Responsive design testing
- Accessibility testing
- Performance monitoring
## Browser Support
### 1. Supported Browsers
- Chrome (latest 2 versions)
- Firefox (latest 2 versions)
- Safari (latest 2 versions)
- Edge (latest version)
### 2. Fallbacks
- Graceful degradation
- No-script support
- Legacy browser handling
- Progressive enhancement
## Security Measures
### 1. CSRF Protection
- Token validation
- Secure form submission
- Protected AJAX requests
- Session handling
### 2. XSS Prevention
- Content sanitization
- Escaped output
- Secure cookie handling
- Input validation
## Future Considerations
### 1. Potential Improvements
- Component library development
- Enhanced type checking
- Performance monitoring
- Automated testing
### 2. Maintenance
- Regular dependency updates
- Browser compatibility checks
- Performance optimization
- Security audits

View File

@@ -20,22 +20,22 @@
### Frontend Technologies
1. HTMX
- Dynamic updates and server interactions
- Partial rendering and progressive enhancement
- Server-side processing and form handling
- See frontendArchitecture.md for detailed implementation
- Dynamic updates
- Partial rendering
- Server-side processing
- Progressive enhancement
2. AlpineJS
- UI state management and reactivity
- Component behavior and lifecycle
- Event handling and DOM manipulation
- See frontendArchitecture.md for component patterns
- UI state management
- Component behavior
- Event handling
- DOM manipulation
3. Tailwind CSS
- Utility-first styling with custom configuration
- Component design system
- Responsive layouts and dark mode support
- See frontendArchitecture.md for styling guide
- Utility-first styling
- Component design
- Responsive layouts
- Custom configuration
## Integration Patterns
@@ -87,24 +87,16 @@
### Frontend Libraries
1. CSS Framework
- Tailwind CSS with custom configuration
- Theme system with light/dark mode support
- Component-specific style patterns
- See frontendArchitecture.md for complete styling guide
- Tailwind CSS
- Custom plugins
- Theme configuration
- Utility classes
2. JavaScript
- AlpineJS for reactive components
- HTMX for server interactions
- Location autocomplete system
- Alert and modal components
- See frontendArchitecture.md for component documentation
3. UI Components
- Form elements and validation
- Navigation and menus
- Status indicators and badges
- Modal and alert system
- See frontendArchitecture.md for implementation details
- AlpineJS core
- HTMX library
- Utility functions
- Custom components
## Infrastructure Choices
@@ -151,14 +143,10 @@
### Technology Limitations
1. Frontend
- HTMX/AlpineJS only (no React/Vue/Angular)
- Progressive enhancement approach required
- Must support latest 2 versions of major browsers
- See frontendArchitecture.md for detailed browser support
- Performance targets:
* First contentful paint < 1.5s
* Time to interactive < 2s
* Core Web Vitals compliance
- HTMX/AlpineJS only
- No additional frameworks
- Browser compatibility
- Performance requirements
2. Backend
- Django version constraints

View File

@@ -1,15 +1,16 @@
from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
from .models import Company, Manufacturer
@admin.register(Company)
class CompanyAdmin(admin.ModelAdmin):
class CompanyAdmin(SimpleHistoryAdmin):
list_display = ('id', 'name', 'headquarters', 'website', 'created_at')
search_fields = ('name', 'headquarters', 'description')
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ('created_at', 'updated_at')
@admin.register(Manufacturer)
class ManufacturerAdmin(admin.ModelAdmin):
class ManufacturerAdmin(SimpleHistoryAdmin):
list_display = ('id', 'name', 'headquarters', 'website', 'created_at')
search_fields = ('name', 'headquarters', 'description')
prepopulated_fields = {'slug': ('name',)}

View File

@@ -1,8 +1,5 @@
# Generated by Django 5.1.4 on 2025-02-10 01:10
# Generated by Django 5.1.3 on 2024-11-12 18:07
import django.db.models.deletion
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations, models
@@ -10,15 +7,21 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
("pghistory", "0006_delete_aggregateevent"),
]
dependencies = []
operations = [
migrations.CreateModel(
name="Company",
fields=[
("id", models.BigAutoField(primary_key=True, serialize=False)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
("slug", models.SlugField(max_length=255, unique=True)),
("website", models.URLField(blank=True)),
@@ -34,31 +37,18 @@ class Migration(migrations.Migration):
"ordering": ["name"],
},
),
migrations.CreateModel(
name="CompanyEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),
("name", models.CharField(max_length=255)),
("slug", models.SlugField(db_index=False, max_length=255)),
("website", models.URLField(blank=True)),
("headquarters", models.CharField(blank=True, max_length=255)),
("description", models.TextField(blank=True)),
("total_parks", models.IntegerField(default=0)),
("total_rides", models.IntegerField(default=0)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="Manufacturer",
fields=[
("id", models.BigAutoField(primary_key=True, serialize=False)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
("slug", models.SlugField(max_length=255, unique=True)),
("website", models.URLField(blank=True)),
@@ -73,125 +63,4 @@ class Migration(migrations.Migration):
"ordering": ["name"],
},
),
migrations.CreateModel(
name="ManufacturerEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),
("name", models.CharField(max_length=255)),
("slug", models.SlugField(db_index=False, max_length=255)),
("website", models.URLField(blank=True)),
("headquarters", models.CharField(blank=True, max_length=255)),
("description", models.TextField(blank=True)),
("total_rides", models.IntegerField(default=0)),
("total_roller_coasters", models.IntegerField(default=0)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],
options={
"abstract": False,
},
),
pgtrigger.migrations.AddTrigger(
model_name="company",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "companies_companyevent" ("created_at", "description", "headquarters", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "total_parks", "total_rides", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."headquarters", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."slug", NEW."total_parks", NEW."total_rides", NEW."updated_at", NEW."website"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="INSERT",
pgid="pgtrigger_insert_insert_a4101",
table="companies_company",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="company",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "companies_companyevent" ("created_at", "description", "headquarters", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "total_parks", "total_rides", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."headquarters", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."slug", NEW."total_parks", NEW."total_rides", NEW."updated_at", NEW."website"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="UPDATE",
pgid="pgtrigger_update_update_3d5ae",
table="companies_company",
when="AFTER",
),
),
),
migrations.AddField(
model_name="companyevent",
name="pgh_context",
field=models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="pghistory.context",
),
),
migrations.AddField(
model_name="companyevent",
name="pgh_obj",
field=models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="events",
to="companies.company",
),
),
pgtrigger.migrations.AddTrigger(
model_name="manufacturer",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "companies_manufacturerevent" ("created_at", "description", "headquarters", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "total_rides", "total_roller_coasters", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."headquarters", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."slug", NEW."total_rides", NEW."total_roller_coasters", NEW."updated_at", NEW."website"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="INSERT",
pgid="pgtrigger_insert_insert_5c0b6",
table="companies_manufacturer",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="manufacturer",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "companies_manufacturerevent" ("created_at", "description", "headquarters", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "total_rides", "total_roller_coasters", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."headquarters", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."slug", NEW."total_rides", NEW."total_roller_coasters", NEW."updated_at", NEW."website"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="UPDATE",
pgid="pgtrigger_update_update_81971",
table="companies_manufacturer",
when="AFTER",
),
),
),
migrations.AddField(
model_name="manufacturerevent",
name="pgh_context",
field=models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="pghistory.context",
),
),
migrations.AddField(
model_name="manufacturerevent",
name="pgh_obj",
field=models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="events",
to="companies.manufacturer",
),
),
]

View File

@@ -0,0 +1,28 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('companies', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Designer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('slug', models.SlugField(max_length=255, unique=True)),
('website', models.URLField(blank=True)),
('description', models.TextField(blank=True)),
('total_rides', models.IntegerField(default=0)),
('total_roller_coasters', models.IntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['name'],
},
),
]

View File

@@ -2,11 +2,11 @@ from django.db import models
from django.utils.text import slugify
from django.urls import reverse
from typing import Tuple, Optional, ClassVar, TYPE_CHECKING
import pghistory
from history_tracking.models import TrackedModel, HistoricalSlug
@pghistory.track()
class Company(TrackedModel):
if TYPE_CHECKING:
from history_tracking.models import HistoricalSlug
class Company(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
website = models.URLField(blank=True)
@@ -37,18 +37,8 @@ class Company(TrackedModel):
try:
return cls.objects.get(slug=slug), False
except cls.DoesNotExist:
# Check pghistory first
history_model = cls.get_history_model()
history_entry = (
history_model.objects.filter(slug=slug)
.order_by('-pgh_created_at')
.first()
)
if history_entry:
return cls.objects.get(id=history_entry.pgh_obj_id), True
# Check manual slug history as fallback
# Check historical slugs
from history_tracking.models import HistoricalSlug
try:
historical = HistoricalSlug.objects.get(
content_type__model='company',
@@ -58,8 +48,7 @@ class Company(TrackedModel):
except (HistoricalSlug.DoesNotExist, cls.DoesNotExist):
raise cls.DoesNotExist()
@pghistory.track()
class Manufacturer(TrackedModel):
class Manufacturer(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
website = models.URLField(blank=True)
@@ -89,18 +78,8 @@ class Manufacturer(TrackedModel):
try:
return cls.objects.get(slug=slug), False
except cls.DoesNotExist:
# Check pghistory first
history_model = cls.get_history_model()
history_entry = (
history_model.objects.filter(slug=slug)
.order_by('-pgh_created_at')
.first()
)
if history_entry:
return cls.objects.get(id=history_entry.pgh_obj_id), True
# Check manual slug history as fallback
# Check historical slugs
from history_tracking.models import HistoricalSlug
try:
historical = HistoricalSlug.objects.get(
content_type__model='manufacturer',
@@ -109,3 +88,43 @@ class Manufacturer(TrackedModel):
return cls.objects.get(pk=historical.object_id), True
except (HistoricalSlug.DoesNotExist, cls.DoesNotExist):
raise cls.DoesNotExist()
class Designer(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
website = models.URLField(blank=True)
description = models.TextField(blank=True)
total_rides = models.IntegerField(default=0)
total_roller_coasters = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
objects: ClassVar[models.Manager['Designer']]
class Meta:
ordering = ['name']
def __str__(self) -> str:
return self.name
def save(self, *args, **kwargs) -> None:
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
@classmethod
def get_by_slug(cls, slug: str) -> Tuple['Designer', bool]:
"""Get designer by slug, checking historical slugs if needed"""
try:
return cls.objects.get(slug=slug), False
except cls.DoesNotExist:
# Check historical slugs
from history_tracking.models import HistoricalSlug
try:
historical = HistoricalSlug.objects.get(
content_type__model='designer',
slug=slug
)
return cls.objects.get(pk=historical.object_id), True
except (HistoricalSlug.DoesNotExist, cls.DoesNotExist):
raise cls.DoesNotExist()

View File

@@ -206,7 +206,7 @@ class ManufacturerDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmis
def _handle_submission(
request: Any, form: Any, model: ModelType, success_url: str = ""
request: Any, form: Any, model: ModelType, success_url: str
) -> HttpResponseRedirect:
"""Helper method to handle form submissions"""
cleaned_data = form.cleaned_data.copy()
@@ -214,7 +214,6 @@ def _handle_submission(
user=request.user,
content_type=ContentType.objects.get_for_model(model),
submission_type="CREATE",
status="NEW",
changes=cleaned_data,
reason=request.POST.get("reason", ""),
source=request.POST.get("source", ""),
@@ -230,12 +229,6 @@ def _handle_submission(
submission.status = "APPROVED"
submission.handled_by = request.user
submission.save()
# Generate success URL if not provided
if not success_url:
success_url = reverse(
f"companies:{model.__name__.lower()}_detail", kwargs={"slug": obj.slug}
)
messages.success(request, f'Successfully created {getattr(obj, "name", "")}')
return HttpResponseRedirect(success_url)
@@ -251,7 +244,10 @@ class CompanyCreateView(LoginRequiredMixin, CreateView):
object: Optional[Company]
def form_valid(self, form: CompanyForm) -> HttpResponseRedirect:
return _handle_submission(self.request, form, self.model, "")
success_url = reverse(
"companies:company_detail", kwargs={"slug": form.instance.slug}
)
return _handle_submission(self.request, form, self.model, success_url)
def get_success_url(self) -> str:
if self.object is None:
@@ -266,7 +262,10 @@ class ManufacturerCreateView(LoginRequiredMixin, CreateView):
object: Optional[Manufacturer]
def form_valid(self, form: ManufacturerForm) -> HttpResponseRedirect:
return _handle_submission(self.request, form, self.model, "")
success_url = reverse(
"companies:manufacturer_detail", kwargs={"slug": form.instance.slug}
)
return _handle_submission(self.request, form, self.model, success_url)
def get_success_url(self) -> str:
if self.object is None:

View File

@@ -1,27 +0,0 @@
import pghistory
from django.contrib.auth.models import AnonymousUser
from django.core.handlers.wsgi import WSGIRequest
class RequestContextProvider(pghistory.context):
"""Custom context provider for pghistory that extracts information from the request."""
def __call__(self, request: WSGIRequest) -> dict:
return {
'user': str(request.user) if request.user and not isinstance(request.user, AnonymousUser) else None,
'ip': request.META.get('REMOTE_ADDR'),
'user_agent': request.META.get('HTTP_USER_AGENT'),
'session_key': request.session.session_key if hasattr(request, 'session') else None
}
# Initialize the context provider
request_context = RequestContextProvider()
class PgHistoryContextMiddleware:
"""
Middleware that ensures request object is available to pghistory context.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
return response

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.1.4 on 2025-02-10 01:10
# Generated by Django 5.1.3 on 2024-11-12 18:07
import django.db.models.deletion
from django.db import migrations, models

View File

@@ -2,7 +2,6 @@ from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.utils.text import slugify
from history_tracking.models import TrackedModel
class SlugHistory(models.Model):
"""
@@ -27,7 +26,7 @@ class SlugHistory(models.Model):
def __str__(self):
return f"Old slug '{self.old_slug}' for {self.content_object}"
class SluggedModel(TrackedModel):
class SluggedModel(models.Model):
"""
Abstract base model that provides slug functionality with history tracking.
"""
@@ -77,18 +76,7 @@ class SluggedModel(TrackedModel):
# Try to get by current slug first
return cls.objects.get(slug=slug), False
except cls.DoesNotExist:
# Check pghistory first
history_model = cls.get_history_model()
history_entry = (
history_model.objects.filter(slug=slug)
.order_by('-pgh_created_at')
.first()
)
if history_entry:
return cls.objects.get(id=history_entry.pgh_obj_id), True
# Try to find in manual slug history as fallback
# Try to find in slug history
history = SlugHistory.objects.filter(
content_type=ContentType.objects.get_for_model(cls),
old_slug=slug

View File

@@ -1,13 +1,10 @@
from django.contrib import admin
from django.utils.text import slugify
from simple_history.admin import SimpleHistoryAdmin
from .models import Designer
@admin.register(Designer)
class DesignerAdmin(admin.ModelAdmin):
class DesignerAdmin(SimpleHistoryAdmin):
list_display = ('name', 'headquarters', 'founded_date', 'website')
search_fields = ('name', 'headquarters')
list_filter = ('founded_date',)
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ('created_at', 'updated_at')
def get_queryset(self, request):
return super().get_queryset(request).select_related()

View File

@@ -1,8 +1,8 @@
# Generated by Django 5.1.4 on 2025-02-10 01:10
# Generated by Django 5.1.3 on 2024-11-12 18:07
import django.db.models.deletion
import pgtrigger.compiler
import pgtrigger.migrations
import simple_history.models
from django.conf import settings
from django.db import migrations, models
@@ -11,14 +11,22 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
("pghistory", "0006_delete_aggregateevent"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Designer",
fields=[
("id", models.BigAutoField(primary_key=True, serialize=False)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
("slug", models.SlugField(max_length=255, unique=True)),
("description", models.TextField(blank=True)),
@@ -33,73 +41,48 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name="DesignerEvent",
name="HistoricalDesigner",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),
(
"id",
models.BigIntegerField(
auto_created=True, blank=True, db_index=True, verbose_name="ID"
),
),
("name", models.CharField(max_length=255)),
("slug", models.SlugField(db_index=False, max_length=255)),
("slug", models.SlugField(max_length=255)),
("description", models.TextField(blank=True)),
("website", models.URLField(blank=True)),
("founded_date", models.DateField(blank=True, null=True)),
("headquarters", models.CharField(blank=True, max_length=255)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("created_at", models.DateTimeField(blank=True, editable=False)),
("updated_at", models.DateTimeField(blank=True, editable=False)),
("history_id", models.AutoField(primary_key=True, serialize=False)),
("history_date", models.DateTimeField(db_index=True)),
("history_change_reason", models.CharField(max_length=100, null=True)),
(
"history_type",
models.CharField(
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
max_length=1,
),
),
(
"history_user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
"verbose_name": "historical designer",
"verbose_name_plural": "historical designers",
"ordering": ("-history_date", "-history_id"),
"get_latest_by": ("history_date", "history_id"),
},
),
pgtrigger.migrations.AddTrigger(
model_name="designer",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "designers_designerevent" ("created_at", "description", "founded_date", "headquarters", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."founded_date", NEW."headquarters", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."slug", NEW."updated_at", NEW."website"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="INSERT",
pgid="pgtrigger_insert_insert_9be65",
table="designers_designer",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="designer",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "designers_designerevent" ("created_at", "description", "founded_date", "headquarters", "id", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "updated_at", "website") VALUES (NEW."created_at", NEW."description", NEW."founded_date", NEW."headquarters", NEW."id", NEW."name", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."slug", NEW."updated_at", NEW."website"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="UPDATE",
pgid="pgtrigger_update_update_b5f91",
table="designers_designer",
when="AFTER",
),
),
),
migrations.AddField(
model_name="designerevent",
name="pgh_context",
field=models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="pghistory.context",
),
),
migrations.AddField(
model_name="designerevent",
name="pgh_obj",
field=models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="events",
to="designers.designer",
),
bases=(simple_history.models.HistoricalChanges, models.Model),
),
]

View File

@@ -1,10 +1,8 @@
from django.db import models
from django.utils.text import slugify
from history_tracking.models import TrackedModel
import pghistory
from simple_history.models import HistoricalRecords
@pghistory.track()
class Designer(TrackedModel):
class Designer(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
description = models.TextField(blank=True)
@@ -13,6 +11,7 @@ class Designer(TrackedModel):
headquarters = models.CharField(max_length=255, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
history = HistoricalRecords()
class Meta:
ordering = ['name']
@@ -31,13 +30,8 @@ class Designer(TrackedModel):
try:
return cls.objects.get(slug=slug), False
except cls.DoesNotExist:
# Check historical slugs using pghistory
history_model = cls.get_history_model()
history = (
history_model.objects.filter(slug=slug)
.order_by('-pgh_created_at')
.first()
)
# Check historical slugs
history = cls.history.filter(slug=slug).order_by('-history_date').first()
if history:
return cls.objects.get(id=history.pgh_obj_id), True
return cls.objects.get(id=history.id), True
raise cls.DoesNotExist("No designer found with this slug")

View File

@@ -77,7 +77,7 @@ class Command(BaseCommand):
# If no recipient specified, use the from_email address for testing
to_email = options['to'] or 'test@thrillwiki.com'
self.stdout.write(self.style.SUCCESS('Using configuration:'))
self.stdout.write(self.style.SUCCESS(f'Using configuration:'))
self.stdout.write(f' From: {from_email}')
self.stdout.write(f' To: {to_email}')
self.stdout.write(f' API Key: {"*" * len(api_key)}')
@@ -146,8 +146,8 @@ class Command(BaseCommand):
},
headers={
'Content-Type': 'application/json',
},
timeout=60)
}
)
if response.status_code == 200:
self.stdout.write(self.style.SUCCESS('✓ API endpoint test successful'))

View File

@@ -1,8 +1,6 @@
# Generated by Django 5.1.4 on 2025-02-10 01:10
# Generated by Django 5.1.3 on 2024-11-12 18:07
import django.db.models.deletion
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations, models
@@ -11,7 +9,6 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
("pghistory", "0006_delete_aggregateevent"),
("sites", "0002_alter_domain_unique"),
]
@@ -19,7 +16,15 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name="EmailConfiguration",
fields=[
("id", models.BigAutoField(primary_key=True, serialize=False)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("api_key", models.CharField(max_length=255)),
("from_email", models.EmailField(max_length=254)),
(
@@ -44,86 +49,4 @@ class Migration(migrations.Migration):
"verbose_name_plural": "Email Configurations",
},
),
migrations.CreateModel(
name="EmailConfigurationEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),
("api_key", models.CharField(max_length=255)),
("from_email", models.EmailField(max_length=254)),
(
"from_name",
models.CharField(
help_text="The name that will appear in the From field of emails",
max_length=255,
),
),
("reply_to", models.EmailField(max_length=254)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"pgh_context",
models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="pghistory.context",
),
),
(
"pgh_obj",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="events",
to="email_service.emailconfiguration",
),
),
(
"site",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="sites.site",
),
),
],
options={
"abstract": False,
},
),
pgtrigger.migrations.AddTrigger(
model_name="emailconfiguration",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "email_service_emailconfigurationevent" ("api_key", "created_at", "from_email", "from_name", "id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "reply_to", "site_id", "updated_at") VALUES (NEW."api_key", NEW."created_at", NEW."from_email", NEW."from_name", NEW."id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."reply_to", NEW."site_id", NEW."updated_at"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="INSERT",
pgid="pgtrigger_insert_insert_08c59",
table="email_service_emailconfiguration",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="emailconfiguration",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "email_service_emailconfigurationevent" ("api_key", "created_at", "from_email", "from_name", "id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "reply_to", "site_id", "updated_at") VALUES (NEW."api_key", NEW."created_at", NEW."from_email", NEW."from_name", NEW."id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."reply_to", NEW."site_id", NEW."updated_at"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="UPDATE",
pgid="pgtrigger_update_update_992a4",
table="email_service_emailconfiguration",
when="AFTER",
),
),
),
]

View File

@@ -1,10 +1,7 @@
from django.db import models
from django.contrib.sites.models import Site
from history_tracking.models import TrackedModel
import pghistory
@pghistory.track()
class EmailConfiguration(TrackedModel):
class EmailConfiguration(models.Model):
api_key = models.CharField(max_length=255)
from_email = models.EmailField()
from_name = models.CharField(max_length=255, help_text="The name that will appear in the From field of emails")

View File

@@ -74,7 +74,7 @@ class EmailService:
f"{settings.FORWARD_EMAIL_BASE_URL}/v1/emails",
json=data,
headers=headers,
timeout=60)
)
# Debug output
print(f"Response Status: {response.status_code}")

View File

@@ -1,3 +0,0 @@
const locators = {};
module.exports = { locators };

View File

@@ -1,12 +0,0 @@
from django.apps import AppConfig
class HistoryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'history'
verbose_name = 'History Tracking'
def ready(self):
"""Initialize app and signal handlers"""
from django.dispatch import Signal
# Create a signal for history updates
self.history_updated = Signal()

View File

@@ -1,29 +0,0 @@
<div id="history-timeline"
hx-get="{% url 'history:timeline' content_type_id=content_type.id object_id=object.id %}"
hx-trigger="every 30s, historyUpdate from:body">
<div class="space-y-4">
{% for event in events %}
<div class="component-wrapper bg-white p-4 shadow-sm">
<div class="component-header flex items-center gap-2 mb-2">
<span class="text-sm font-medium">{{ event.pgh_label|title }}</span>
<time class="text-xs text-gray-500">{{ event.pgh_created_at|date:"M j, Y H:i" }}</time>
</div>
<div class="component-content text-sm">
{% if event.pgh_context.metadata.user %}
<div class="flex items-center gap-1">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" />
</svg>
<span>{{ event.pgh_context.metadata.user }}</span>
</div>
{% endif %}
{% if event.pgh_data %}
<div class="mt-2 text-gray-600">
<pre class="text-xs">{{ event.pgh_data|pprint }}</pre>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>

View File

@@ -1,17 +0,0 @@
from django import template
import json
register = template.Library()
@register.filter
def pprint(value):
"""Pretty print JSON data"""
if isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError:
return value
if isinstance(value, (dict, list)):
return json.dumps(value, indent=2)
return str(value)

View File

@@ -1,10 +0,0 @@
from django.urls import path
from .views import HistoryTimelineView
app_name = 'history'
urlpatterns = [
path('timeline/<int:content_type_id>/<int:object_id>/',
HistoryTimelineView.as_view(),
name='timeline'),
]

View File

@@ -1,41 +0,0 @@
from django.views import View
from django.shortcuts import render
from django.http import JsonResponse
from django.contrib.contenttypes.models import ContentType
import pghistory
def serialize_event(event):
"""Serialize a history event for JSON response"""
return {
'label': event.pgh_label,
'created_at': event.pgh_created_at.isoformat(),
'context': event.pgh_context,
'data': event.pgh_data,
}
class HistoryTimelineView(View):
"""View for displaying object history timeline"""
def get(self, request, content_type_id, object_id):
# Get content type and object
content_type = ContentType.objects.get_for_id(content_type_id)
obj = content_type.get_object_for_this_type(id=object_id)
# Get history events
events = pghistory.models.Event.objects.filter(
pgh_obj_model=content_type.model_class(),
pgh_obj_id=object_id
).order_by('-pgh_created_at')[:25]
context = {
'events': events,
'content_type': content_type,
'object': obj,
}
if request.htmx:
return render(request, "history/partials/history_timeline.html", context)
return JsonResponse({
'history': [serialize_event(e) for e in events]
})

View File

@@ -7,9 +7,20 @@ class HistoryTrackingConfig(AppConfig):
name = "history_tracking"
def ready(self):
"""
No initialization needed for pghistory tracking.
History tracking is handled by the @pghistory.track() decorator
and triggers installed in migrations.
"""
pass
from django.apps import apps
from .mixins import HistoricalChangeMixin
# Get the Park model
try:
Park = apps.get_model('parks', 'Park')
ParkArea = apps.get_model('parks', 'ParkArea')
# Apply mixin to historical models
if HistoricalChangeMixin not in Park.history.model.__bases__:
Park.history.model.__bases__ = (HistoricalChangeMixin,) + Park.history.model.__bases__
if HistoricalChangeMixin not in ParkArea.history.model.__bases__:
ParkArea.history.model.__bases__ = (HistoricalChangeMixin,) + ParkArea.history.model.__bases__
except LookupError:
# Models might not be loaded yet
pass

View File

@@ -1,32 +1,50 @@
from django.conf import settings
from django.db import migrations, models
# Generated by Django 5.1.3 on 2024-11-12 18:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
("contenttypes", "0002_remove_content_type_name"),
]
operations = [
migrations.CreateModel(
name='HistoricalSlug',
name="HistoricalSlug",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('slug', models.SlugField(max_length=255)),
('created_at', models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='historical_slugs', to=settings.AUTH_USER_MODEL)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("object_id", models.PositiveIntegerField()),
("slug", models.SlugField(max_length=255)),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
],
options={
'unique_together': {('content_type', 'slug')},
'indexes': [
models.Index(fields=['content_type', 'object_id'], name='history_tra_content_1234ab_idx'),
models.Index(fields=['slug'], name='history_tra_slug_1234ab_idx'),
"indexes": [
models.Index(
fields=["content_type", "object_id"],
name="history_tra_content_63013c_idx",
),
models.Index(fields=["slug"], name="history_tra_slug_f843aa_idx"),
],
"unique_together": {("content_type", "slug")},
},
),
]

View File

@@ -0,0 +1,74 @@
# history_tracking/mixins.py
from django.db import models
from django.conf import settings
class HistoricalChangeMixin(models.Model):
"""Mixin for historical models to track changes"""
id = models.BigIntegerField(db_index=True, auto_created=True, blank=True)
history_date = models.DateTimeField()
history_id = models.AutoField(primary_key=True)
history_type = models.CharField(max_length=1)
history_user = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
on_delete=models.SET_NULL,
related_name='+'
)
history_change_reason = models.CharField(max_length=100, null=True)
class Meta:
abstract = True
ordering = ['-history_date', '-history_id']
@property
def prev_record(self):
"""Get the previous record for this instance"""
try:
return self.__class__.objects.filter(
history_date__lt=self.history_date,
id=self.id
).order_by('-history_date').first()
except (AttributeError, TypeError):
return None
@property
def diff_against_previous(self):
prev_record = self.prev_record
if not prev_record:
return {}
changes = {}
for field in self.__dict__:
if field not in [
"history_date",
"history_id",
"history_type",
"history_user_id",
"history_change_reason",
"history_type",
"id",
"_state",
"_history_user_cache"
] and not field.startswith("_"):
try:
old_value = getattr(prev_record, field)
new_value = getattr(self, field)
if old_value != new_value:
changes[field] = {"old": str(old_value), "new": str(new_value)}
except AttributeError:
continue
return changes
@property
def history_user_display(self):
"""Get a display name for the history user"""
if hasattr(self, 'history_user') and self.history_user:
return str(self.history_user)
return None
def get_instance(self):
"""Get the model instance this history record represents"""
try:
return self.__class__.objects.get(id=self.id)
except self.__class__.DoesNotExist:
return None

View File

@@ -1,75 +1,34 @@
# history_tracking/models.py
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.conf import settings
from typing import Any, Dict, Optional
from simple_history.models import HistoricalRecords
from .mixins import HistoricalChangeMixin
from typing import Any, Type, TypeVar, cast
from django.db.models import QuerySet
class DiffMixin:
"""Mixin to add diffing capabilities to models"""
def get_prev_record(self) -> Optional[Any]:
"""Get the previous record for this instance"""
try:
return type(self).objects.filter(
pgh_created_at__lt=self.pgh_created_at,
pgh_obj_id=self.pgh_obj_id
).order_by('-pgh_created_at').first()
except (AttributeError, TypeError):
return None
T = TypeVar('T', bound=models.Model)
def diff_against_previous(self) -> Dict:
"""Compare this record against the previous one"""
prev_record = self.get_prev_record()
if not prev_record:
return {}
skip_fields = {
'pgh_id', 'pgh_created_at', 'pgh_label',
'pgh_obj_id', 'pgh_context_id', '_state',
'created_at', 'updated_at'
}
changes = {}
for field, value in self.__dict__.items():
# Skip internal fields and those we don't want to track
if field.startswith('_') or field in skip_fields or field.endswith('_id'):
continue
try:
old_value = getattr(prev_record, field)
new_value = value
if old_value != new_value:
changes[field] = {
"old": str(old_value) if old_value is not None else "None",
"new": str(new_value) if new_value is not None else "None"
}
except AttributeError:
continue
return changes
class TrackedModel(models.Model):
"""Abstract base class for models that need history tracking"""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class HistoricalModel(models.Model):
"""Abstract base class for models with history tracking"""
id = models.BigAutoField(primary_key=True)
history: HistoricalRecords = HistoricalRecords(
inherit=True,
bases=(HistoricalChangeMixin,)
)
class Meta:
abstract = True
def get_history(self) -> QuerySet:
"""Get all history records for this instance in chronological order"""
event_model = self.events.model # pghistory provides this automatically
if event_model:
return event_model.objects.filter(
pgh_obj_id=self.pk
).order_by('-pgh_created_at')
return self.__class__.objects.none()
@property
def _history_model(self) -> Type[T]:
"""Get the history model class"""
return cast(Type[T], self.history.model) # type: ignore
def __str__(self):
model_name = self.__class__.__name__
fields_str = ", ".join((f"{field.name}={getattr(self, field.name)}" for field in self._meta.fields))
return f"{model_name}({fields_str})"
def get_history(self) -> QuerySet:
"""Get all history records for this instance"""
model = self._history_model
return model.objects.filter(id=self.pk).order_by('-history_date')
class HistoricalSlug(models.Model):
"""Track historical slugs for models"""
@@ -78,13 +37,6 @@ class HistoricalSlug(models.Model):
content_object = GenericForeignKey('content_type', 'object_id')
slug = models.SlugField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='historical_slugs'
)
class Meta:
unique_together = ('content_type', 'slug')

View File

@@ -1,7 +1,5 @@
from django.apps import AppConfig
import os
class LocationConfig(AppConfig):
path = os.path.dirname(os.path.abspath(__file__))
default_auto_field = 'django.db.models.BigAutoField'
name = 'location'

View File

@@ -1,10 +1,10 @@
# Generated by Django 5.1.4 on 2025-02-10 01:10
# Generated by Django 5.1.3 on 2024-11-12 18:07
import django.contrib.gis.db.models.fields
import django.core.validators
import django.db.models.deletion
import pgtrigger.compiler
import pgtrigger.migrations
import simple_history.models
from django.conf import settings
from django.db import migrations, models
@@ -14,14 +14,140 @@ class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("pghistory", "0006_delete_aggregateevent"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="HistoricalLocation",
fields=[
(
"id",
models.BigIntegerField(
auto_created=True, blank=True, db_index=True, verbose_name="ID"
),
),
("object_id", models.PositiveIntegerField()),
(
"name",
models.CharField(
help_text="Name of the location (e.g. business name, landmark)",
max_length=255,
),
),
(
"location_type",
models.CharField(
help_text="Type of location (e.g. business, landmark, address)",
max_length=50,
),
),
(
"latitude",
models.DecimalField(
blank=True,
decimal_places=6,
help_text="Latitude coordinate (legacy field)",
max_digits=9,
null=True,
validators=[
django.core.validators.MinValueValidator(-90),
django.core.validators.MaxValueValidator(90),
],
),
),
(
"longitude",
models.DecimalField(
blank=True,
decimal_places=6,
help_text="Longitude coordinate (legacy field)",
max_digits=9,
null=True,
validators=[
django.core.validators.MinValueValidator(-180),
django.core.validators.MaxValueValidator(180),
],
),
),
(
"point",
django.contrib.gis.db.models.fields.PointField(
blank=True,
help_text="Geographic coordinates as a Point",
null=True,
srid=4326,
),
),
(
"street_address",
models.CharField(blank=True, max_length=255, null=True),
),
("city", models.CharField(blank=True, max_length=100, null=True)),
(
"state",
models.CharField(
blank=True,
help_text="State/Region/Province",
max_length=100,
null=True,
),
),
("country", models.CharField(blank=True, max_length=100, null=True)),
("postal_code", models.CharField(blank=True, max_length=20, null=True)),
("created_at", models.DateTimeField(blank=True, editable=False)),
("updated_at", models.DateTimeField(blank=True, editable=False)),
("history_id", models.AutoField(primary_key=True, serialize=False)),
("history_date", models.DateTimeField(db_index=True)),
("history_change_reason", models.CharField(max_length=100, null=True)),
(
"history_type",
models.CharField(
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
max_length=1,
),
),
(
"content_type",
models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="contenttypes.contenttype",
),
),
(
"history_user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "historical location",
"verbose_name_plural": "historical locations",
"ordering": ("-history_date", "-history_id"),
"get_latest_by": ("history_date", "history_id"),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name="Location",
fields=[
("id", models.BigAutoField(primary_key=True, serialize=False)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("object_id", models.PositiveIntegerField()),
(
"name",
@@ -102,163 +228,16 @@ class Migration(migrations.Migration):
],
options={
"ordering": ["name"],
"indexes": [
models.Index(
fields=["content_type", "object_id"],
name="location_lo_content_9ee1bd_idx",
),
models.Index(fields=["city"], name="location_lo_city_99f908_idx"),
models.Index(
fields=["country"], name="location_lo_country_b75eba_idx"
),
],
},
),
migrations.CreateModel(
name="LocationEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),
("object_id", models.PositiveIntegerField()),
(
"name",
models.CharField(
help_text="Name of the location (e.g. business name, landmark)",
max_length=255,
),
),
(
"location_type",
models.CharField(
help_text="Type of location (e.g. business, landmark, address)",
max_length=50,
),
),
(
"latitude",
models.DecimalField(
blank=True,
decimal_places=6,
help_text="Latitude coordinate (legacy field)",
max_digits=9,
null=True,
validators=[
django.core.validators.MinValueValidator(-90),
django.core.validators.MaxValueValidator(90),
],
),
),
(
"longitude",
models.DecimalField(
blank=True,
decimal_places=6,
help_text="Longitude coordinate (legacy field)",
max_digits=9,
null=True,
validators=[
django.core.validators.MinValueValidator(-180),
django.core.validators.MaxValueValidator(180),
],
),
),
(
"point",
django.contrib.gis.db.models.fields.PointField(
blank=True,
help_text="Geographic coordinates as a Point",
null=True,
srid=4326,
),
),
(
"street_address",
models.CharField(blank=True, max_length=255, null=True),
),
("city", models.CharField(blank=True, max_length=100, null=True)),
(
"state",
models.CharField(
blank=True,
help_text="State/Region/Province",
max_length=100,
null=True,
),
),
("country", models.CharField(blank=True, max_length=100, null=True)),
("postal_code", models.CharField(blank=True, max_length=20, null=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"content_type",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="contenttypes.contenttype",
),
),
(
"pgh_context",
models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="pghistory.context",
),
),
(
"pgh_obj",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="events",
to="location.location",
),
),
],
options={
"abstract": False,
},
),
migrations.AddIndex(
model_name="location",
index=models.Index(
fields=["content_type", "object_id"],
name="location_lo_content_9ee1bd_idx",
),
),
migrations.AddIndex(
model_name="location",
index=models.Index(fields=["city"], name="location_lo_city_99f908_idx"),
),
migrations.AddIndex(
model_name="location",
index=models.Index(
fields=["country"], name="location_lo_country_b75eba_idx"
),
),
pgtrigger.migrations.AddTrigger(
model_name="location",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "location_locationevent" ("city", "content_type_id", "country", "created_at", "id", "latitude", "location_type", "longitude", "name", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "postal_code", "state", "street_address", "updated_at") VALUES (NEW."city", NEW."content_type_id", NEW."country", NEW."created_at", NEW."id", NEW."latitude", NEW."location_type", NEW."longitude", NEW."name", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."point", NEW."postal_code", NEW."state", NEW."street_address", NEW."updated_at"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="INSERT",
pgid="pgtrigger_insert_insert_98cd4",
table="location_location",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="location",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "location_locationevent" ("city", "content_type_id", "country", "created_at", "id", "latitude", "location_type", "longitude", "name", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "point", "postal_code", "state", "street_address", "updated_at") VALUES (NEW."city", NEW."content_type_id", NEW."country", NEW."created_at", NEW."id", NEW."latitude", NEW."location_type", NEW."longitude", NEW."name", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."point", NEW."postal_code", NEW."state", NEW."street_address", NEW."updated_at"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="UPDATE",
pgid="pgtrigger_update_update_471d2",
table="location_location",
when="AFTER",
),
),
),
]

View File

@@ -3,12 +3,10 @@ from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.validators import MinValueValidator, MaxValueValidator
from simple_history.models import HistoricalRecords
from django.contrib.gis.geos import Point
import pghistory
from history_tracking.models import TrackedModel
@pghistory.track()
class Location(TrackedModel):
class Location(models.Model):
"""
A generic location model that can be associated with any model
using GenericForeignKey. Stores detailed location information
@@ -65,6 +63,7 @@ class Location(TrackedModel):
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
history = HistoricalRecords()
class Meta:
indexes = [

View File

@@ -9,8 +9,6 @@ from django.views.decorators.http import require_http_methods
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
from django.db.models import Q
from location.forms import LocationForm
from .models import Location
class LocationSearchView(View):
@@ -54,8 +52,8 @@ class LocationSearchView(View):
response = requests.get(
'https://nominatim.openstreetmap.org/search',
params=params,
headers={'User-Agent': 'ThrillWiki/1.0'},
timeout=60)
headers={'User-Agent': 'ThrillWiki/1.0'}
)
response.raise_for_status()
results = response.json()
except requests.RequestException as e:
@@ -172,8 +170,8 @@ def reverse_geocode(request):
'format': 'json',
'addressdetails': 1
},
headers={'User-Agent': 'ThrillWiki/1.0'},
timeout=60)
headers={'User-Agent': 'ThrillWiki/1.0'}
)
response.raise_for_status()
result = response.json()

View File

@@ -33,7 +33,7 @@ class Command(BaseCommand):
try:
# Download image
self.stdout.write(f'Downloading from URL: {photo_url}')
response = requests.get(photo_url, timeout=60)
response = requests.get(photo_url)
if response.status_code == 200:
# Delete any existing photos for this park
Photo.objects.filter(
@@ -74,7 +74,7 @@ class Command(BaseCommand):
try:
# Download image
self.stdout.write(f'Downloading from URL: {photo_url}')
response = requests.get(photo_url, timeout=60)
response = requests.get(photo_url)
if response.status_code == 200:
# Delete any existing photos for this ride
Photo.objects.filter(

View File

@@ -1,10 +1,8 @@
# Generated by Django 5.1.4 on 2025-02-10 01:10
# Generated by Django 5.1.3 on 2024-11-12 18:07
import django.db.models.deletion
import media.models
import media.storage
import pgtrigger.compiler
import pgtrigger.migrations
from django.conf import settings
from django.db import migrations, models
@@ -15,7 +13,6 @@ class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("pghistory", "0006_delete_aggregateevent"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
@@ -23,7 +20,15 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name="Photo",
fields=[
("id", models.BigAutoField(primary_key=True, serialize=False)),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"image",
models.ImageField(
@@ -59,110 +64,12 @@ class Migration(migrations.Migration):
],
options={
"ordering": ["-is_primary", "-created_at"],
"indexes": [
models.Index(
fields=["content_type", "object_id"],
name="media_photo_content_0187f5_idx",
)
],
},
),
migrations.CreateModel(
name="PhotoEvent",
fields=[
("pgh_id", models.AutoField(primary_key=True, serialize=False)),
("pgh_created_at", models.DateTimeField(auto_now_add=True)),
("pgh_label", models.TextField(help_text="The event label.")),
("id", models.BigIntegerField()),
(
"image",
models.ImageField(
max_length=255,
storage=media.storage.MediaStorage(),
upload_to=media.models.photo_upload_path,
),
),
("caption", models.CharField(blank=True, max_length=255)),
("alt_text", models.CharField(blank=True, max_length=255)),
("is_primary", models.BooleanField(default=False)),
("is_approved", models.BooleanField(default=False)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("date_taken", models.DateTimeField(blank=True, null=True)),
("object_id", models.PositiveIntegerField()),
(
"content_type",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to="contenttypes.contenttype",
),
),
(
"pgh_context",
models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="pghistory.context",
),
),
(
"pgh_obj",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="events",
to="media.photo",
),
),
(
"uploaded_by",
models.ForeignKey(
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
related_query_name="+",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
migrations.AddIndex(
model_name="photo",
index=models.Index(
fields=["content_type", "object_id"],
name="media_photo_content_0187f5_idx",
),
),
pgtrigger.migrations.AddTrigger(
model_name="photo",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "media_photoevent" ("alt_text", "caption", "content_type_id", "created_at", "date_taken", "id", "image", "is_approved", "is_primary", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."content_type_id", NEW."created_at", NEW."date_taken", NEW."id", NEW."image", NEW."is_approved", NEW."is_primary", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="INSERT",
pgid="pgtrigger_insert_insert_e1ca0",
table="media_photo",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="photo",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "media_photoevent" ("alt_text", "caption", "content_type_id", "created_at", "date_taken", "id", "image", "is_approved", "is_primary", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated_at", "uploaded_by_id") VALUES (NEW."alt_text", NEW."caption", NEW."content_type_id", NEW."created_at", NEW."date_taken", NEW."id", NEW."image", NEW."is_approved", NEW."is_primary", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."updated_at", NEW."uploaded_by_id"); RETURN NULL;',
hash="[AWS-SECRET-REMOVED]",
operation="UPDATE",
pgid="pgtrigger_update_update_6ff7d",
table="media_photo",
when="AFTER",
),
),
),
]

View File

@@ -11,8 +11,6 @@ from datetime import datetime
from .storage import MediaStorage
from rides.models import Ride
from django.utils import timezone
from history_tracking.models import TrackedModel
import pghistory
def photo_upload_path(instance: models.Model, filename: str) -> str:
"""Generate upload path for photos using normalized filenames"""
@@ -40,8 +38,7 @@ def photo_upload_path(instance: models.Model, filename: str) -> str:
# For park photos, store directly in park directory
return f"park/{identifier}/{base_filename}"
@pghistory.track()
class Photo(TrackedModel):
class Photo(models.Model):
"""Generic photo model that can be attached to any model"""
image = models.ImageField(
upload_to=photo_upload_path, # type: ignore[arg-type]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

View File

@@ -8,7 +8,7 @@ from django.test.utils import override_settings
from django.db import models
from datetime import datetime
from PIL import Image
import piexif # type: ignore
import piexif
import io
import shutil
import tempfile

View File

@@ -1,28 +0,0 @@
# Active Context - Park View Modularization
**Objective:** Refactor parks view to use reusable card component and implement grid/list view toggle
**Current Implementation Analysis:**
- Park cards rendered via `park_list_item.html` partial
- Existing layout uses flex-based list structure
- Search functionality uses HTMX for dynamic updates
**Planned Changes:**
1. **Create `park_card.html` Partial**
- Extract card markup from `park_list_item.html`
- Add responsive grid/list view classes
- Include view mode toggle state
2. **View Toggle Implementation**
- Add grid/list toggle UI with HTMX
- Store view preference in cookie/localStorage
- Update CSS for grid (grid-cols) vs list (flex) layouts
3. **Backend Updates**
- Add view_mode parameter to park list view
- Modify context processor to handle layout preference
**Next Steps:**
- Implement card partial with responsive classes
- Create view toggle component
- Update HTMX handlers to preserve view mode

View File

@@ -1,162 +0,0 @@
# ADR 001: Frontend Architecture - HTMX + AlpineJS
## Status
Accepted
## Context
The ThrillWiki platform needs a frontend architecture that:
- Provides dynamic user interactions
- Maintains server-side rendering benefits
- Enables progressive enhancement
- Keeps complexity manageable
- Ensures fast page loads
- Supports SEO requirements
## Decision
Implement frontend using HTMX + AlpineJS + Tailwind CSS instead of a traditional SPA framework (React, Vue, Angular).
### Technology Choices
1. HTMX
- Server-side rendering with dynamic updates
- Progressive enhancement
- Simple integration with Django templates
- Reduced JavaScript complexity
2. AlpineJS
- Lightweight client-side interactivity
- Simple state management
- Easy integration with HTMX
- Minimal learning curve
3. Tailwind CSS
- Utility-first styling
- Consistent design system
- Easy customization
- Optimized production builds
## Consequences
### Positive
1. Performance
- Faster initial page loads
- Reduced client-side processing
- Smaller JavaScript bundle
- Better Core Web Vitals
2. Development
- Simpler architecture
- Faster development cycles
- Easier debugging
- Better Django integration
3. Maintenance
- Less complex state management
- Reduced dependency management
- Easier team onboarding
- More maintainable codebase
4. SEO
- Server-rendered content
- Better crawler compatibility
- Improved accessibility
- Faster indexing
### Negative
1. Limited Complex UI
- More complex for rich interactions
- Less ecosystem support
- Fewer UI components available
- Some patterns need custom solutions
2. Development Patterns
- New patterns needed
- Different mental model
- Some developer familiarity issues
- Custom solutions needed
## Alternatives Considered
### React SPA
- Pros:
* Rich ecosystem
* Component libraries
* Developer familiarity
* Advanced tooling
- Cons:
* Complex setup
* Heavy client-side
* SEO challenges
* Performance overhead
### Vue.js
- Pros:
* Progressive framework
* Good ecosystem
* Easy learning curve
* Good performance
- Cons:
* Still too heavy
* Complex build setup
* Server integration challenges
* Unnecessary complexity
## Implementation Approach
### Integration Strategy
1. Server-Side
```python
# Django View
class ParksView(TemplateView):
def get(self, request, *args, **kwargs):
return JsonResponse() if is_htmx() else render()
```
2. Client-Side
```html
<!-- Template -->
<div hx-get="/parks"
hx-trigger="load"
x-data="{ filtered: false }">
```
### Performance Optimization
1. Initial Load
- Server-side rendering
- Progressive enhancement
- Critical CSS inline
- Deferred JavaScript
2. Subsequent Interactions
- Partial page updates
- Smart caching
- Optimistic UI updates
- Background processing
## Monitoring and Success Metrics
### Performance Metrics
- First Contentful Paint < 1.5s
- Time to Interactive < 2s
- Core Web Vitals compliance
- Server response times
### Development Metrics
- Development velocity
- Bug frequency
- Code complexity
- Build times
## Future Considerations
### Enhancement Opportunities
1. Short-term
- Component library
- Pattern documentation
- Performance optimization
- Developer tools
2. Long-term
- Advanced patterns
- Custom extensions
- Build optimizations
- Tool improvements

View File

@@ -1,90 +0,0 @@
# History Tracking Migration
## Context
The project is transitioning from django-simple-history to django-pghistory for model history tracking.
## Implementation Details
### Base Implementation (history_tracking/models.py)
- Both old and new implementations maintained during transition:
- `HistoricalModel` - Legacy base class using django-simple-history
- `TrackedModel` - New base class using django-pghistory
- Custom `DiffMixin` for comparing historical records
- Maintained `HistoricalSlug` for backward compatibility
### Transition Strategy
1. Maintain Backward Compatibility
- Keep both HistoricalModel and TrackedModel during transition
- Update models one at a time to use TrackedModel
- Ensure no breaking changes during migration
2. Model Updates
- Designer (Completed)
- Migrated to TrackedModel
- Updated get_by_slug to use pghistory queries
- Removed SimpleHistoryAdmin dependency
- Pending Model Updates
- Companies (Company, Manufacturer)
- Parks (Park, ParkArea)
- Rides (Ride, RollerCoasterStats)
- Location models
### Migration Process
1. For Each Model:
- Switch base class from HistoricalModel to TrackedModel
- Update admin.py to remove SimpleHistoryAdmin
- Create and apply migrations
- Test history tracking functionality
- Update any history-related queries
2. Testing Steps
- Create test objects
- Make changes
- Verify history records
- Check diff functionality
- Validate historical slug lookup
3. Admin Integration
- Remove SimpleHistoryAdmin
- Use standard ModelAdmin
- Keep existing list displays and search fields
## Benefits
- Native PostgreSQL trigger-based tracking
- More efficient storage and querying
- Better performance characteristics
- Context tracking capabilities
## Rollback Plan
Since both implementations are maintained:
1. Revert model inheritance to HistoricalModel
2. Restore SimpleHistoryAdmin
3. Keep existing migrations
## Next Steps
1. Create migrations for Designer model
2. Update remaining models in this order:
a. Companies app
b. Parks app
c. Rides app
d. Location app
3. Test historical functionality
4. Once all models are migrated:
- Remove HistoricalModel class
- Remove django-simple-history dependency
- Update documentation
## Technical Notes
- Uses pghistory's default tracking configuration
- Maintains compatibility with existing code patterns
- Custom diff functionality preserved
- Historical slug tracking unchanged
- Both tracking systems can coexist during migration
## Completion Criteria
1. All models migrated to TrackedModel
2. All functionality tested and working
3. No dependencies on django-simple-history
4. Documentation updated to reflect new implementation
5. All migrations applied successfully

View File

@@ -1,41 +0,0 @@
# Foreign Key Constraint Resolution - 2025-02-09 (Updated)
## Revision Note
Corrected migration sequence conflict:
- Original 0002 migration conflicted with existing 0002 file
- Created new migration as 0012_cleanup_invalid_designers.py
- Deleted conflicting 0002_cleanup_invalid_designers.py
## Updated Resolution Steps
1. Created conflict-free migration 0012
2. Verified migration dependencies:
```python
dependencies = [
('rides', '0011_merge_20250209_1143'),
('designers', '0001_initial'),
]
```
3. New migration command:
```bash
python manage.py migrate rides 0012_cleanup_invalid_designers
```
## PGHistory Migration Fix - 2025-02-09
Foreign key constraint violation during pghistory migration:
1. Issue: `rides_ride_designer_id_172b997d_fk_designers_designer_id` constraint violation during 0010_rideevent migration
2. Resolution:
- Created new cleanup migration (0009_cleanup_invalid_designers_pre_events.py) to run before event table creation
- Updated migration dependencies to ensure proper sequencing:
```python
# 0009_cleanup_invalid_designers_pre_events.py
dependencies = [
('rides', '0008_historicalride_post_closing_status_and_more'),
('designers', '0001_initial'),
]
```
- Created merge migration (0013_merge_20250209_1214.py) to resolve multiple leaf nodes
3. Final Migration Sequence:
- Base migrations up to 0008
- Cleanup migration (0009_cleanup_invalid_designers_pre_events)
- Event table creation (0010_rideevent_ridemodelevent_and_more)
- Merge migrations (0011, 0012, 0013)

View File

@@ -1,59 +0,0 @@
# Park Count Fields Implementation
## Context
While implementing park views, we encountered errors where `ride_count` and `coaster_count` annotations conflicted with existing model fields of the same names. Additionally, we discovered inconsistencies in how these counts were being used across different views.
## Decision
We decided to use both approaches but with distinct names:
1. **Model Fields**:
- `ride_count`: Stored count of all rides
- `coaster_count`: Stored count of roller coasters
- Used in models and database schema
- Required for backward compatibility
2. **Annotations**:
- `current_ride_count`: Real-time count of all rides
- `current_coaster_count`: Real-time count of roller coasters
- Provide accurate, up-to-date counts
- Used in templates and filters
This approach allows us to:
- Maintain existing database schema
- Show accurate, real-time counts in the UI
- Avoid name conflicts between fields and annotations
- Keep consistent naming pattern for both types of counts
## Implementation
1. Views:
- Added base queryset method with annotations
- Used 'current_' prefix for annotated counts
- Ensured all views use the base queryset
2. Filters:
- Updated filter fields to use annotated counts
- Configured filter class to always use base queryset
- Maintained filter functionality with new field names
3. Templates:
- Updated templates to use computed counts
## Why This Pattern
1. **Consistency**: Using the 'current_' prefix clearly indicates which values are computed in real-time
2. **Compatibility**: Maintains support for existing code that relies on the stored fields
3. **Flexibility**: Allows gradual migration from stored to computed counts if desired
4. **Performance Option**: Keeps the option to use stored counts for expensive queries
## Future Considerations
We might want to:
1. Add periodic tasks to sync stored counts with computed values
2. Consider deprecating stored fields if they're not needed for performance
3. Add validation to ensure stored counts stay in sync with reality
4. Create a management command to update stored counts
## Related Files
- parks/models.py
- parks/views.py
- parks/filters.py
- parks/templates/parks/partials/park_list_item.html
- parks/tests/test_filters.py

View File

@@ -1,45 +0,0 @@
## Decision: Universal Model History via django-pghistory
### Pattern Implementation
- **Tracking Method**: `pghistory.Snapshot()` applied to all concrete models
- **Inheritance Strategy**: Base model class with history tracking
- **Context Capture**:
```python
# core/models.py
import pghistory
class HistoricalModel(models.Model):
class Meta:
abstract = True
@pghistory.track(pghistory.Snapshot())
def save(self, *args, **kwargs):
return super().save(*args, **kwargs)
```
### Integration Scope
1. **Model Layer**:
- All concrete models inherit from `HistoricalModel`
- Automatic event labeling:
```python
@pghistory.track(
pghistory.Snapshot('model.create'),
pghistory.AfterInsert('model.update'),
pghistory.BeforeDelete('model.delete')
)
```
2. **Context Middleware**:
```python
# core/middleware.py
pghistory.context(lambda request: {
'user': str(request.user) if request.user.is_authenticated else None,
'ip': request.META.get('REMOTE_ADDR'),
'user_agent': request.META.get('HTTP_USER_AGENT'),
'session_key': request.session.session_key
})
```
3. **Admin Integration**:
- Custom history view for Django Admin
- Version comparison interface

View File

@@ -1,39 +0,0 @@
# Ride Count Field Implementation
## Context
While implementing park views, we encountered an error where a `ride_count` annotation conflicted with an existing model field of the same name. This raised a question about how to handle real-time ride counts versus stored counts.
## Decision
We decided to use both approaches but with distinct names:
1. **Model Field (`ride_count`)**:
- Kept the original field for backward compatibility
- Used in test fixtures and filtering system
- Can serve as a cached/denormalized value
2. **Annotation (`current_ride_count`)**:
- Added new annotation with a distinct name
- Provides real-time count of rides
- Used in templates for display purposes
This approach allows us to:
- Maintain existing functionality in tests and filters
- Show accurate, real-time counts in the UI
- Avoid name conflicts between fields and annotations
## Implementation
- Kept the `ride_count` IntegerField in the Park model
- Added `current_ride_count = Count('rides', distinct=True)` annotation in views
- Updated templates to use `current_ride_count` for display
## Future Considerations
We might want to:
1. Add a periodic task to sync the stored `ride_count` with the computed value
2. Consider deprecating the stored field if it's not needed for performance
3. Add validation to ensure the stored count stays in sync with reality
## Related Files
- parks/models.py
- parks/views.py
- parks/templates/parks/partials/park_list_item.html
- parks/tests/test_filters.py

View File

@@ -1,57 +0,0 @@
## Feature: Unified History Timeline (HTMX Integrated)
### HTMX Template Pattern
```django
{# history/partials/history_timeline.html #}
<div id="history-timeline"
hx-get="{% url 'history:timeline' content_type_id=content_type.id object_id=object.id %}"
hx-trigger="every 30s, historyUpdate from:body">
<div class="space-y-4">
{% for event in events %}
<div class="component-wrapper bg-white p-4 shadow-sm">
<div class="component-header flex items-center gap-2 mb-2">
<span class="text-sm font-medium">{{ event.pgh_label|title }}</span>
<time class="text-xs text-gray-500">{{ event.pgh_created_at|date:"M j, Y H:i" }}</time>
</div>
<div class="component-content text-sm">
{% if event.pgh_context.metadata.user %}
<div class="flex items-center gap-1">
<svg class="w-4 h-4">...</svg>
<span>{{ event.pgh_context.metadata.user }}</span>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
```
### View Integration (Class-Based with HTMX)
```python
# history/views.py
class HistoryTimelineView(View):
def get(self, request, content_type_id, object_id):
events = ModelHistory.objects.filter(
pgh_obj_model=content_type_id,
pgh_obj_id=object_id
).order_by('-pgh_created_at')[:25]
if request.htmx:
return render(request, "history/partials/history_timeline.html", {
"events": events
})
return JsonResponse({
'history': [serialize_event(e) for e in events]
})
```
### Event Trigger Pattern
```python
# parks/signals.py
from django.dispatch import Signal
history_updated = Signal()
# In model save/delete handlers:
history_updated.send(sender=Model, instance=instance)

View File

@@ -1,55 +0,0 @@
# Frontend Moderation Panel Improvements
## Implementation Details
### 1. Performance Optimization
- Added debouncing to search inputs
- Optimized list rendering with virtual scrolling
- Improved loading states with skeleton screens
- Added result caching for common searches
### 2. Loading States
- Enhanced loading indicators with progress bars
- Added skeleton screens for content loading
- Improved HTMX loading states visual feedback
- Added transition animations for smoother UX
### 3. Error Handling
- Added error states for failed operations
- Improved error messages with recovery actions
- Added retry functionality for failed requests
- Enhanced validation feedback
### 4. Mobile Responsiveness
- Optimized layouts for mobile devices
- Added responsive navigation patterns
- Improved touch interactions
- Enhanced filter UI for small screens
### 5. Accessibility
- Added ARIA labels and roles
- Improved keyboard navigation
- Enhanced focus management
- Added screen reader announcements
## Key Components Modified
1. Dashboard Layout
2. Submission Cards
3. Filter Interface
4. Action Buttons
5. Form Components
## Technical Decisions
1. Used CSS Grid for responsive layouts
2. Implemented AlpineJS for state management
3. Used HTMX for dynamic updates
4. Added Tailwind utilities for consistent styling
## Testing Strategy
1. Browser compatibility testing
2. Mobile device testing
3. Accessibility testing
4. Performance benchmarking

View File

@@ -1,115 +0,0 @@
# Moderation Panel Implementation
## Completed Improvements
### 1. Loading States & Performance
- Added skeleton loading screens for better UX during content loading
- Implemented debounced search inputs to reduce server load
- Added virtual scrolling for large submission lists
- Enhanced error handling with clear feedback
- Optimized HTMX requests and responses
### 2. Mobile Responsiveness
- Created collapsible filter interface for mobile
- Improved action button layouts on small screens
- Enhanced touch interactions
- Optimized grid layouts for different screen sizes
### 3. Accessibility
- Added proper ARIA labels and roles
- Enhanced keyboard navigation
- Added screen reader announcements for state changes
- Improved focus management
- Added reduced motion support
### 4. State Management
- Implemented Alpine.js store for filter management
- Added URL-based state persistence
- Enhanced filter UX with visual indicators
- Improved form handling and validation
### 5. Error Handling
- Added comprehensive error states
- Implemented retry functionality
- Enhanced error feedback
- Added toast notifications for actions
## Technical Implementation
### Key Files Modified
1. `templates/moderation/dashboard.html`
- Enhanced base template structure
- Added improved loading and error states
- Added accessibility enhancements
2. `templates/moderation/partials/loading_skeleton.html`
- Created skeleton loading screens
- Added responsive layout structure
- Implemented loading animations
3. `templates/moderation/partials/dashboard_content.html`
- Enhanced filter interface
- Improved mobile responsiveness
- Added accessibility features
4. `templates/moderation/partials/filters_store.html`
- Implemented Alpine.js store
- Added filter state management
- Enhanced URL handling
## Testing Notes
### Tested Scenarios
- Mobile device compatibility
- Screen reader functionality
- Keyboard navigation
- Loading states and error handling
- Filter functionality
- Form submissions and validation
### Browser Support
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
## Next Steps
### 1. Performance Optimization
- [ ] Implement server-side caching for frequent queries
- [ ] Add client-side caching for filter results
- [ ] Optimize image loading and processing
### 2. User Experience
- [ ] Add bulk action support
- [ ] Enhance filter combinations
- [ ] Add sorting options
- [ ] Implement saved filters
### 3. Accessibility
- [ ] Conduct full WCAG audit
- [ ] Add keyboard shortcuts
- [ ] Enhance screen reader support
### 4. Features
- [ ] Add advanced search capabilities
- [ ] Implement moderation statistics
- [ ] Add user activity tracking
- [ ] Enhance notification system
## Documentation Updates Needed
- Update user guide with new features
- Add keyboard shortcut documentation
- Update accessibility guidelines
- Add performance benchmarks
## Known Issues
- Filter reset might not clear all states
- Mobile scroll performance with many items
- Loading skeleton flicker on fast connections
## Dependencies
- HTMX
- AlpineJS
- TailwindCSS
- Leaflet (for maps)

View File

@@ -1,131 +0,0 @@
# Moderation System Overview
## Purpose
The moderation system ensures high-quality, accurate content across the ThrillWiki platform by implementing a structured review process for user-generated content.
## Core Components
### 1. Content Queue Management
- Submission categorization
- Priority assignment
- Review distribution
- Queue monitoring
### 2. Review Process
- Multi-step verification
- Content validation rules
- Media review workflow
- Quality metrics
### 3. Moderator Tools
- Review interface
- Action tracking
- Decision history
- Performance metrics
## Implementation
### Models
```python
# Key models in moderation/models.py
- ModeratedContent
- ModeratorAction
- ContentQueue
- QualityMetric
```
### Workflows
1. Content Submission
- Content validation
- Automated checks
- Queue assignment
- Submitter notification
2. Review Process
- Moderator assignment
- Content evaluation
- Decision making
- Action recording
3. Quality Control
- Metric tracking
- Performance monitoring
- Accuracy assessment
- Review auditing
## Integration Points
### 1. User System
- Submission tracking
- Status notifications
- User reputation
- Appeal process
### 2. Content Systems
- Parks content
- Ride information
- Review system
- Media handling
### 3. Analytics
- Quality metrics
- Processing times
- Accuracy rates
- User satisfaction
## Business Rules
### Content Standards
1. Accuracy Requirements
- Factual verification
- Source validation
- Update frequency
- Completeness checks
2. Quality Guidelines
- Writing standards
- Media requirements
- Information depth
- Format compliance
### Moderation Rules
1. Review Criteria
- Content accuracy
- Quality standards
- Community guidelines
- Legal compliance
2. Action Framework
- Approval process
- Rejection handling
- Revision requests
- Appeals management
## Future Enhancements
### Planned Improvements
1. Short-term
- Enhanced automation
- Improved metrics
- UI refinements
- Performance optimization
2. Long-term
- AI assistance
- Advanced analytics
- Workflow automation
- Community integration
### Integration Opportunities
1. Machine Learning
- Content classification
- Quality prediction
- Spam detection
- Priority assignment
2. Community Features
- Trusted reviewers
- Expert validation
- Community flags
- Reputation system

View File

@@ -1,76 +0,0 @@
# Park Search Integration
## Overview
Integrated the parks app with the site-wide search system to provide consistent filtering and search capabilities across the platform.
## Implementation Details
### 1. Filter Configuration
```python
# parks/filters.py
ParkFilter = create_model_filter(
model=Park,
search_fields=['name', 'description', 'location__city', 'location__state', 'location__country'],
mixins=[LocationFilterMixin, RatingFilterMixin, DateRangeFilterMixin],
additional_filters={
'status': {
'field_class': 'django_filters.ChoiceFilter',
'field_kwargs': {'choices': Park._meta.get_field('status').choices}
},
'opening_date': {
'field_class': 'django_filters.DateFromToRangeFilter',
},
'owner': {
'field_class': 'django_filters.ModelChoiceFilter',
'field_kwargs': {'queryset': 'companies.Company.objects.all()'}
},
'min_rides': {
'field_class': 'django_filters.NumberFilter',
'field_kwargs': {'field_name': 'ride_count', 'lookup_expr': 'gte'}
},
'min_coasters': {
'field_class': 'django_filters.NumberFilter',
'field_kwargs': {'field_name': 'coaster_count', 'lookup_expr': 'gte'}
},
'min_size': {
'field_class': 'django_filters.NumberFilter',
'field_kwargs': {'field_name': 'size_acres', 'lookup_expr': 'gte'}
}
}
)
```
### 2. View Integration
- Updated `ParkListView` to use `HTMXFilterableMixin`
- Configured proper queryset optimization with `select_related` and `prefetch_related`
- Added pagination support
- Maintained ride count annotations
### 3. Template Structure
- Created `search/templates/search/partials/park_results.html` for consistent result display
- Includes:
- Park image thumbnails
- Basic park information
- Location details
- Status indicators
- Ride count badges
- Rating display
### 4. Quick Search Support
- Modified `search_parks` view for dropdown/quick search scenarios
- Uses the same filter system but with simplified output
- Limited to 10 results for performance
- Added location preloading
## Benefits
1. Consistent filtering across the platform
2. Enhanced search capabilities with location and rating filters
3. Improved performance through proper query optimization
4. Better maintainability using the site-wide search system
5. HTMX-powered dynamic updates
## Technical Notes
- Uses django-filter backend
- Integrates with location and rating mixins
- Supports both full search and quick search use cases
- Maintains existing functionality while improving code organization

View File

@@ -1,129 +0,0 @@
# Ride Search HTMX Improvements
## Implementation Status: ✅ COMPLETED AND VERIFIED
### Current Implementation
#### 1. Smart Search (Implemented)
- Split search terms for flexible matching (e.g. "steel dragon" matches "Steel Dragon 2000")
- Searches across multiple fields:
- Ride name
- Park name
- Description
- Uses Django Q objects for complex queries
- Real-time HTMX-powered updates
#### 2. Search Suggestions (Implemented)
- Real-time suggestions with 200ms delay
- Three types of suggestions:
- Common matching ride names (with count)
- Matching parks (with location)
- Matching categories (with ride count)
- Styled dropdown with icons and hover states
- Keyboard navigation support
#### 3. Quick Filters (Implemented)
- Category filters from CATEGORY_CHOICES
- Operating status filter
- All filters use HTMX for instant updates
- Maintains search context when filtering
- Visual active state on selected filter
#### 4. Active Filter Tags (Implemented)
- Shows currently active filters:
- Search terms
- Selected category
- Operating status
- One-click removal via HTMX
- Updates URL for bookmarking/sharing
#### 5. Visual Feedback (Implemented)
- Loading spinner during HTMX requests
- Clear visual states for filter buttons
- Real-time feedback on search/filter actions
- Dark mode compatible styling
### Technical Details
#### View Implementation
```python
def get_queryset(self):
"""Get filtered rides based on search and filters"""
queryset = Ride.objects.all().select_related(
'park',
'ride_model',
'ride_model__manufacturer'
).prefetch_related('photos')
# Search term handling
search = self.request.GET.get('q', '').strip()
if search:
# Split search terms for more flexible matching
search_terms = search.split()
search_query = Q()
for term in search_terms:
term_query = Q(
name__icontains=term
) | Q(
park__name__icontains=term
) | Q(
description__icontains=term
)
search_query &= term_query
queryset = queryset.filter(search_query)
# Category filter
category = self.request.GET.get('category')
if category and category != 'all':
queryset = queryset.filter(category=category)
# Operating status filter
if self.request.GET.get('operating') == 'true':
queryset = queryset.filter(status='operating')
return queryset
```
#### Template Structure
- `ride_list.html`: Main template with search and filters
- `search_suggestions.html`: Dropdown suggestion UI
- `ride_list_results.html`: Results grid (HTMX target)
#### Key Fixes Applied
1. Template Path Resolution
- CRITICAL FIX: Resolved template inheritance confusion
- Removed duplicate base.html templates
- Moved template to correct location: templates/base/base.html
- All templates now correctly extend "base/base.html"
- Template loading order matches Django's settings
2. URL Resolution
- Replaced all relative "." URLs with explicit URLs using {% url %}
- Example: `hx-get="{% url 'rides:global_ride_list' %}"`
- Prevents conflicts with global search in base template
3. HTMX Configuration
- All HTMX triggers properly configured
- Fixed grid layout persistence:
* Removed duplicate grid classes from parent template
* Grid classes now only in partial template
* Prevents layout breaking during HTMX updates
- Proper event delegation for dynamic content
### Verification Points
1. ✅ Search updates in real-time
2. ✅ Filters work independently and combined
3. ✅ Suggestions appear as you type
4. ✅ Loading states show during requests
5. ✅ Dark mode properly supported
6. ✅ URL state maintained for sharing
7. ✅ No conflicts with global search
8. ✅ All templates resolve correctly
### Future Considerations
1. Consider caching frequent searches
2. Monitor performance with large datasets
3. Add analytics for most used filters
4. Consider adding saved searches feature

View File

@@ -1,121 +0,0 @@
# Site-Wide Search System Architecture
## 1. Architectural Overview
- **Filter-First Approach**: Utilizes django-filter for robust filtering capabilities
- **Modular Design**:
```python
# filters.py
class ParkFilter(django_filters.FilterSet):
search = django_filters.CharFilter(method='filter_search')
class Meta:
model = Park
fields = {
'state': ['exact', 'in'],
'rating': ['gte', 'lte'],
}
def filter_search(self, queryset, name, value):
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value)
)
```
## 2. Enhanced Backend Components
### Search Endpoint (`/search/`)
```python
# views.py
class AdaptiveSearchView(TemplateView):
template_name = "search/results.html"
def get_queryset(self):
return Park.objects.all()
def get_filterset(self):
return ParkFilter(self.request.GET, queryset=self.get_queryset())
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
filterset = self.get_filterset()
context['results'] = filterset.qs
context['filters'] = filterset.form
return context
```
## 3. Plugin Integration
### Recommended django-filter Extensions
```python
# settings.py
INSTALLED_APPS += [
'django_filters',
'django_filters_addons', # For custom widgets
'rangefilter', # For date/number ranges
]
# filters.py
class EnhancedParkFilter(ParkFilter):
rating_range = django_filters.RangeFilter(field_name='rating')
features = django_filters.MultipleChoiceFilter(
field_name='features__slug',
widget=HorizontalCheckboxSelectMultiple,
lookup_expr='contains'
)
class Meta(ParkFilter.Meta):
fields = ParkFilter.Meta.fields + ['rating_range', 'features']
```
## 4. Frontend Filter Rendering
```html
<!-- templates/search/filters.html -->
<form hx-get="/search/" hx-target="#search-results" hx-swap="outerHTML">
{{ filters.form.as_p }}
<button type="submit">Apply Filters</button>
</form>
<!-- Dynamic filter updates -->
<div hx-trigger="filter-update from:body"
hx-get="/search/filters/"
hx-swap="innerHTML">
</div>
```
## 5. Benefits of django-filter Integration
- Built-in validation for filter parameters
- Automatic form generation
- Complex lookup expressions
- Reusable filter components
- Plugin ecosystem support
## 6. Security Considerations
- Input sanitization using django's built-in escaping
- Query parameter whitelisting via FilterSet definitions
- Rate limiting on autocomplete endpoint (using django-ratelimit)
- Permission-aware queryset filtering
## 7. Performance Optimization
- Select related/prefetch_related in FilterSet querysets
- Caching filter configurations
- Indexing recommendations for filtered fields
- Pagination integration with django-filter
## 8. Testing Strategy
- FilterSet validation tests
- HTMX interaction tests
- Cross-browser filter UI tests
- Performance load testing
## 9. Style Integration
- Custom filter form templates matching Tailwind design
- Responsive filter controls grid
- Accessible form labels and error messages
- Dark mode support
## 10. Expansion Framework
- Registry pattern for adding new FilterSets
- Dynamic filter discovery system
- Plugin configuration templates
- Analytics integration points

View File

@@ -1,170 +0,0 @@
# Park Search Implementation
## Overview
Integration of the parks app with the site-wide search system, providing both full search functionality and quick search for dropdowns.
## Components
### 1. Filter Configuration (parks/filters.py)
```python
ParkFilter = create_model_filter(
model=Park,
search_fields=['name', 'description', 'location__city', 'location__state', 'location__country'],
mixins=[LocationFilterMixin, RatingFilterMixin, DateRangeFilterMixin],
additional_filters={
'status': {
'field_class': 'django_filters.ChoiceFilter',
'field_kwargs': {
'choices': Park._meta.get_field('status').choices,
'empty_label': 'Any status',
'null_label': 'Unknown'
}
},
'opening_date': {
'field_class': 'django_filters.DateFromToRangeFilter',
'field_kwargs': {
'label': 'Opening date range',
'help_text': 'Enter dates in YYYY-MM-DD format'
}
},
# Additional filters for rides, size, etc.
}
)
```
### 2. View Implementation (parks/views.py)
#### Full Search (ParkListView)
```python
class ParkListView(HTMXFilterableMixin, ListView):
model = Park
filter_class = ParkFilter
paginate_by = 20
def get_queryset(self):
try:
return (
super()
.get_queryset()
.select_related("owner")
.prefetch_related(
"photos",
"location",
"rides",
"rides__manufacturer"
)
.annotate(
total_rides=Count("rides"),
total_coasters=Count("rides", filter=Q(rides__category="RC")),
)
)
except Exception as e:
messages.error(self.request, f"Error loading parks: {str(e)}")
return Park.objects.none()
```
#### Quick Search
```python
def search_parks(request):
try:
queryset = (
Park.objects.prefetch_related('location', 'photos')
.order_by('name')
)
filter_params = {'search': request.GET.get('q', '').strip()}
park_filter = ParkFilter(filter_params, queryset=queryset)
parks = park_filter.qs[:10]
return render(request, "parks/partials/park_search_results.html", {
"parks": parks,
"is_quick_search": True
})
except Exception as e:
return render(..., {"error": str(e)})
```
### 3. Template Structure
#### Main Search Page (parks/templates/parks/park_list.html)
- Extends: search/layouts/filtered_list.html
- Blocks:
* filter_errors: Validation error display
* list_header: Park list header + actions
* filter_section: Filter form with clear option
* results_section: Park results with pagination
#### Results Display (search/templates/search/partials/park_results.html)
- Full park information
- Status indicators
- Ride statistics
- Location details
- Error state handling
#### Quick Search Results (parks/partials/park_search_results.html)
- Simplified park display
- Basic location info
- Fallback for missing images
- Error handling
### 4. Error Handling
#### View Level
- Try/except blocks around queryset operations
- Filter validation errors captured
- Generic error states handled
- User-friendly error messages
#### Template Level
- Error states in both quick and full search
- Safe data access (using with and conditionals)
- Fallback content for missing data
- Clear error messaging
### 5. Query Optimization
#### Full Search
- select_related: owner
- prefetch_related: photos, location, rides, rides__manufacturer
- Proper annotations for counts
- Pagination for large results
#### Quick Search
- Limited to 10 results
- Minimal related data loading
- Basic ordering optimization
### 6. Known Limitations
1. Testing Coverage
- Need unit tests for filters
- Need integration tests for error cases
- Need performance testing
2. Performance
- Large dataset behavior unknown
- Complex filter combinations untested
3. Security
- SQL injection prevention needs review
- Permission checks need audit
4. Accessibility
- ARIA labels needed
- Color contrast validation needed
### 7. Next Steps
1. Testing
- Implement comprehensive test suite
- Add performance benchmarks
- Test edge cases
2. Monitoring
- Add error logging
- Implement performance tracking
- Add usage analytics
3. Optimization
- Profile query performance
- Optimize filter combinations
- Consider caching strategies

View File

@@ -1,142 +0,0 @@
# Park Search Testing Implementation
## Test Structure
### 1. Model Tests (parks/tests/test_models.py)
#### Park Model Tests
- Basic CRUD Operations
* Creation with required fields
* Update operations
* Deletion and cascading
* Validation rules
- Slug Operations
* Auto-generation on creation
* Historical slug tracking and lookup (via HistoricalSlug model)
* pghistory integration for model tracking
* Uniqueness constraints
* Fallback lookup strategies
- Location Integration
* Formatted location string
* Coordinates retrieval
* Location relationship integrity
- Status Management
* Default status
* Status color mapping
* Status transitions
- Property Methods
* formatted_location
* coordinates
* get_status_color
### 2. Filter Tests (parks/tests/test_filters.py)
#### Search Functionality
- Text Search Fields
* Name searching
* Description searching
* Location field searching (city, state, country)
* Combined field searching
#### Filter Operations
- Status Filtering
* Each status value
* Empty/null handling
* Invalid status values
- Date Range Filtering
* Opening date ranges
* Invalid date formats
* Edge cases (future dates, very old dates)
- Company/Owner Filtering
* Existing company
* No owner (null)
* Invalid company IDs
- Numeric Filtering
* Minimum rides count
* Minimum coasters count
* Minimum size validation
* Negative value handling
#### Mixin Integration
- LocationFilterMixin
* Distance-based filtering
* Location search functionality
- RatingFilterMixin
* Rating range filtering
* Invalid rating values
- DateRangeFilterMixin
* Date range application
* Invalid date handling
## Implementation Status
### Completed
1. ✓ Created test directory structure
2. ✓ Set up test fixtures in both test files
3. ✓ Implemented Park model tests
- Basic CRUD operations
- Advanced slug functionality:
* Automatic slug generation from name
* Historical slug tracking with HistoricalSlug model
* Dual tracking with pghistory integration
* Comprehensive lookup system with fallbacks
- Status color mapping with complete coverage
- Location integration with error handling
- Property methods with null safety
4. ✓ Implemented ParkFilter tests
- Text search with multiple field support
- Status filtering with validation and choice handling
- Date range filtering with format validation
- Company/owner filtering with comprehensive null handling
- Numeric filtering with integer validation and bounds checking
- Empty value handling across all filters
- Test coverage for edge cases and invalid inputs
- Performance validation for complex filter combinations
### Next Steps
1. Performance Optimization
- [ ] Add query count assertions to tests
- [ ] Profile filter combinations impact
- [ ] Implement caching for common filters
- [ ] Add database indexes for frequently filtered fields
2. Monitoring and Analytics
- [ ] Add filter usage tracking
- [ ] Implement performance monitoring
- [ ] Track common filter combinations
- [ ] Monitor query execution times
3. Documentation and Maintenance
- [ ] Add filter example documentation
- [ ] Document filter combinations and best practices
- [ ] Create performance troubleshooting guide
- [ ] Add test coverage reports and analysis
4. Future Enhancements
- [ ] Add saved filter support
- [ ] Implement filter presets
- [ ] Add advanced combination operators (AND/OR)
- [ ] Support dynamic field filtering
### Running the Tests
To run the test suite:
```bash
python manage.py test parks.tests
```
To run specific test classes:
```bash
python manage.py test parks.tests.test_models.ParkModelTests
python manage.py test parks.tests.test_filters.ParkFilterTests
```

View File

@@ -1,85 +0,0 @@
# Product Context
## Overview
ThrillWiki is a comprehensive platform for theme park enthusiasts to discover parks, share experiences, and access verified information through a moderated knowledge base.
## User Types and Needs
### Park Enthusiasts
- **Problem**: Difficulty finding accurate, comprehensive theme park information
- **Solution**: Centralized, moderated platform with verified park/ride data
- **Key Features**: Park discovery, ride details, location services
### Reviewers
- **Problem**: No dedicated platform for sharing detailed ride experiences
- **Solution**: Structured review system with rich media support
- **Key Features**: Media uploads, rating system, review workflow
### Park Operators
- **Problem**: Limited channels for authentic presence and information
- **Solution**: Verified company profiles and official park information
- **Key Features**: Company verification, official updates, park management
## Core Workflows
1. Park Discovery & Information
- Geographic search and browsing
- Detailed park profiles
- Operating hours and details
2. Ride Management
- Comprehensive ride database
- Technical specifications
- Historical information
- Designer attribution
3. Review System
- User-generated content
- Media integration
- Rating framework
- Moderation workflow
4. Content Moderation
- Submission review
- Quality control
- Content verification
- User management
5. Location Services
- Geographic search
- Proximity features
- Regional categorization
## Strategic Direction
### Current Focus
1. Content Quality
- Robust moderation
- Information verification
- Rich media support
2. User Trust
- Review authenticity
- Company verification
- Expert contributions
3. Data Completeness
- Park coverage
- Ride information
- Historical records
### Future Roadmap
1. Community Features
- Enhanced profiles
- Contribution recognition
- Expert designation
2. Analytics
- Usage patterns
- Quality metrics
- Engagement tracking
3. Media
- Image improvements
- Video support
- Virtual tours

View File

@@ -1,34 +0,0 @@
# History Tracking Implementation Plan
## Phase Order & Document Links
1. **Architecture Design**
- [Integration Strategy](/decisions/pghistory-integration.md)
- [System Patterns Update](/systemPatterns.md#historical-tracking)
2. **Model Layer Implementation**
- [Migration Protocol](/workflows/model-migrations.md)
- [Base Model Configuration](/decisions/pghistory-integration.md#model-layer-integration)
3. **Moderation System Update**
- [Approval Workflow](/workflows/moderation.md#updated-moderation-workflow-with-django-pghistory)
- [Admin Integration](/workflows/moderation.md#moderation-admin-integration)
4. **Frontend Visualization**
- [Timeline Component](/features/history-visualization.md#template-components)
- [API Endpoints](/features/history-visualization.md#ajax-endpoints)
5. **Deployment Checklist**
- [Context Middleware](/systemPatterns.md#request-context-tracking)
- [QA Procedures](/workflows/model-migrations.md#quality-assurance)
## Directory Structure
```
memory-bank/
projects/
history-tracking/
implementation-plan.md
decisions.md -> ../../decisions/pghistory-integration.md
frontend.md -> ../../features/history-visualization.md
migrations.md -> ../../workflows/model-migrations.md
moderation.md -> ../../workflows/moderation.md

View File

@@ -1,13 +0,0 @@
Current State at Mon Feb 10 00:19:42 EST 2025:
1. In process of migrating history tracking system
2. Created initial migration for HistoricalSlug model
3. Interrupted during attempt to handle auto_now_add field migration
4. Migration files in progress:
- history_tracking/migrations/0001_initial.py
- rides/migrations/0002_event_models_unmanaged.py
Next planned steps (awaiting confirmation):
1. Complete history_tracking migrations
2. Update rides event models
3. Test history tracking functionality

Some files were not shown because too many files have changed in this diff Show More