Compare commits

..

3 Commits

62 changed files with 13981 additions and 3457 deletions

8
.gitignore vendored
View File

@@ -353,7 +353,8 @@ cython_debug/
.LSOverride
# Icon must end with two \r
Icon
Icon
# Thumbnails
._*
@@ -373,3 +374,8 @@ Icon
Network Trash Folder
Temporary Items
.apdisk
backend/.env
.env
frontend
uv.lock
.django_tailwind_cli/tailwindcss-macos-arm64-4.1.13

View File

@@ -0,0 +1,123 @@
# Frontend Implementation Plan - Phase 1 Critical Components
## Current State Analysis ✅
### Completed Components
- **Authentication System** - Modal-based auth with social integration ✅
- **Toast Notification System** - Advanced toast system with animations ✅
- **Theme Management** - Working well ✅
- **Header Navigation** - Enhanced with modal integration ✅
- **Base Template Structure** - Solid foundation ✅
- **Basic Alpine.js Components** - Core components implemented ✅
### Missing Critical Components (Phase 1 - High Priority)
## 1. Enhanced Search with Autocomplete 🎯
**Current**: Basic search exists but lacks autocomplete and advanced features
**Needed**:
- Debounced search with API integration
- Search suggestions dropdown UI
- Search result highlighting
- Keyboard navigation for search suggestions
- Recent searches and popular searches
## 2. Enhanced Park/Ride Cards 🎯
**Current**: Basic card components exist
**Needed**:
- Sophisticated hover effects and animations
- Card interaction states (hover, focus, active)
- Loading states for card images
- Card action buttons (favorite, share, etc.)
- Image lazy loading and error handling
## 3. User Profile Management 🎯
**Current**: Basic profile pages exist
**Needed**:
- Comprehensive profile editing interface
- Avatar upload with preview functionality
- Profile sections (basic info, preferences, privacy)
- Form validation and error handling
- Settings persistence
## 4. Advanced Filtering System 🎯
**Current**: Basic filtering exists
**Needed**:
- Multi-select filter components
- Range slider filters
- Date picker filters
- URL state synchronization for filters
- Filter presets and saved searches
## 5. Loading States & Skeletons 🎯
**Current**: Basic loading indicators
**Needed**:
- Skeleton loading components
- Loading spinners and indicators
- Optimistic updates
- Loading states for forms and buttons
## Implementation Priority Order
### Week 1: Core Interactive Components
1. **Enhanced Search Component** (2-3 days)
2. **Advanced Card Components** (2-3 days)
3. **Loading States System** (1-2 days)
### Week 2: User Experience Features
1. **User Profile Management** (3-4 days)
2. **Advanced Filtering System** (3-4 days)
## Technical Approach
### 1. Enhanced Search Component
```javascript
Alpine.data('advancedSearch', () => ({
query: '',
suggestions: [],
recentSearches: [],
popularSearches: [],
loading: false,
showSuggestions: false,
selectedIndex: -1,
debounceTimer: null,
// Implementation details...
}))
```
### 2. Enhanced Card Component
```javascript
Alpine.data('enhancedCard', (cardData) => ({
data: cardData,
imageLoaded: false,
imageError: false,
favorited: false,
// Hover effects, animations, interactions
}))
```
### 3. Skeleton Loading System
```html
<!-- Skeleton templates for different content types -->
<div class="skeleton-card">
<div class="skeleton-image"></div>
<div class="skeleton-text"></div>
</div>
```
## Success Metrics
- Search response time < 200ms
- Card interactions feel smooth (60fps)
- Loading states provide clear feedback
- User profile updates work seamlessly
- Filtering provides instant feedback
## Next Steps
1. Start with Enhanced Search Component implementation
2. Create comprehensive card component system
3. Implement skeleton loading system
4. Build user profile management interface
5. Create advanced filtering system
This plan focuses on the most impactful user experience improvements that will bring the Django frontend to parity with the React implementation.

View File

@@ -0,0 +1,89 @@
# Generated by Django 5.2.6 on 2025-09-20 13:40
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("accounts", "0002_remove_toplistitem_insert_insert_and_more"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="toplist",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="toplist",
name="update_update",
),
pgtrigger.migrations.RemoveTrigger(
model_name="toplistitem",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="toplistitem",
name="update_update",
),
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="0b9e68b3aa0d3fb8f50bd832b99b70201d44aa11",
operation="INSERT",
pgid="pgtrigger_insert_insert_26546",
table="accounts_toplist",
when="AFTER",
),
),
),
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="3ae1293b8b1fe574bac9f388b60d19613347931e",
operation="UPDATE",
pgid="pgtrigger_update_update_84849",
table="accounts_toplist",
when="AFTER",
),
),
),
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", "created_at", "id", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rank", "top_list_id", "updated_at") VALUES (NEW."content_type_id", NEW."created_at", NEW."id", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rank", NEW."top_list_id", NEW."updated_at"); RETURN NULL;',
hash="1091ef1cc7668e112916df0c12f222bd25cfe921",
operation="INSERT",
pgid="pgtrigger_insert_insert_56dfc",
table="accounts_toplistitem",
when="AFTER",
),
),
),
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", "created_at", "id", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rank", "top_list_id", "updated_at") VALUES (NEW."content_type_id", NEW."created_at", NEW."id", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rank", NEW."top_list_id", NEW."updated_at"); RETURN NULL;',
hash="81227a3b4af9432d2b868cd8680bee7896da8acc",
operation="UPDATE",
pgid="pgtrigger_update_update_2b6e3",
table="accounts_toplistitem",
when="AFTER",
),
),
),
]

View File

@@ -0,0 +1 @@
@import "tailwindcss";

Binary file not shown.

11421
backend/logs/performance.log.1 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,21 @@
# Active Context
## Current Focus
- Moderation system development and enhancement
- Dashboard interface improvements
- Submission review workflow
- Database schema synchronization and fixes
- Parks model and pghistory integration
- Ensuring model-database consistency
## Recent Changes
Working on moderation system components:
- Dashboard interface
- Submission list views
- Moderation navigation
- Content review workflow
Fixed critical database schema mismatch in parks app:
- Updated Park model to include operator and property_owner fields
- Added missing owner_id column to parks_parkevent table
- Fixed pghistory triggers that were failing due to missing columns
- Resolved park detail page errors (parks/magic-kingdom/ now working)
### Schema Updates Made
- parks/models.py: Added operator and property_owner ForeignKey fields
- parks/migrations/0006_auto_20250920_0944.py: Added owner_id column to parks_parkevent table
- Database now properly supports all three ownership relationships: owner, operator, property_owner
## Active Files

View File

@@ -0,0 +1,89 @@
# Generated by Django 5.2.6 on 2025-09-20 13:40
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("companies", "0002_alter_company_id_alter_manufacturer_id"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="company",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="company",
name="update_update",
),
pgtrigger.migrations.RemoveTrigger(
model_name="manufacturer",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="manufacturer",
name="update_update",
),
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="413671b13a748fb5f1acd57e8ec4af12ad7ae215",
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="ee3eff1c96e46769347b8463d527668b7ece63c4",
operation="UPDATE",
pgid="pgtrigger_update_update_3d5ae",
table="companies_company",
when="AFTER",
),
),
),
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="ac3c4c31aa8dffe569154454a6c4479d189c0f64",
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="c46f36f5811cd843ff61eab3ae77624ae2e69f60",
operation="UPDATE",
pgid="pgtrigger_update_update_81971",
table="companies_manufacturer",
when="AFTER",
),
),
),
]

View File

@@ -0,0 +1,52 @@
# Generated by Django 5.2.6 on 2025-09-20 13:40
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("designers", "0002_alter_designer_id"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="designer",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="designer",
name="update_update",
),
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="876eaa3e1c7cf234f03cc706fa4e5e508ed780db",
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="edb092b6a122ca5827740a9afcdc6a885fe69c1c",
operation="UPDATE",
pgid="pgtrigger_update_update_b5f91",
table="designers_designer",
when="AFTER",
),
),
),
]

View File

@@ -0,0 +1,52 @@
# Generated by Django 5.2.6 on 2025-09-20 13:40
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("email_service", "0002_alter_emailconfiguration_id"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="emailconfiguration",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="emailconfiguration",
name="update_update",
),
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="f19f3c7f7d904d5f850a2ff1e0bf1312e855c8c0",
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="e445521baf2cfb51379b2a6be550b4a638d60202",
operation="UPDATE",
pgid="pgtrigger_update_update_992a4",
table="email_service_emailconfiguration",
when="AFTER",
),
),
),
]

View File

@@ -0,0 +1,52 @@
# Generated by Django 5.2.6 on 2025-09-20 13:40
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("location", "0002_alter_location_id"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="location",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="location",
name="update_update",
),
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="8a8f00869cfcaa1a23ab29b3d855e83602172c67",
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="f3378cb26a5d88aa82c8fae016d46037b530de90",
operation="UPDATE",
pgid="pgtrigger_update_update_471d2",
table="location_location",
when="AFTER",
),
),
),
]

View File

@@ -6,7 +6,7 @@ import sys
def main():
"""Run administrative tasks."""
os***REMOVED***iron.setdefault("DJANGO_SETTINGS_MODULE", "thrillwiki.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "thrillwiki.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

View File

@@ -0,0 +1,52 @@
# Generated by Django 5.2.6 on 2025-09-20 13:40
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("media", "0002_alter_photo_id"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="photo",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="photo",
name="update_update",
),
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="c75cf37b6fac8d5593598ba2af194f1f9a692838",
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="09d9b3bda4d950d7a7104c8f013a93d05025da72",
operation="UPDATE",
pgid="pgtrigger_update_update_6ff7d",
table="media_photo",
when="AFTER",
),
),
),
]

View File

@@ -1,133 +1,74 @@
# Active Context - Wiki Migration & Integration
# Active Development Context
## Current Status
Corrected implementation strategy to use wiki-only approach instead of dual-system.
## Recently Completed
### Completed Components
1. Wiki Plugin Structure
- Models for park metadata
- Forms for data input
- Templates for display
- URL configurations
### Park Search Implementation (2024-02-22)
2. Documentation
- Technical specifications
- Migration guide
- Implementation decisions
- User guide
1. Autocomplete Base:
- Created BaseAutocomplete in core/forms.py
- Configured project-wide auth requirement
- Added test coverage for base functionality
### Current Focus
Migration to wiki-only system
2. Park Search:
- Implemented ParkAutocomplete class
- Created ParkSearchForm with autocomplete widget
- Updated views and templates for integration
- Added comprehensive test suite
## Immediate Tasks
3. Documentation:
- Updated memory-bank/features/parks/search.md
- Added test documentation
- Created user interface guidelines
### 1. Data Migration
- [x] Create migration script
- [ ] Test migration in development
- [ ] Backup production data
- [ ] Execute migration
- [ ] Verify data integrity
## Active Tasks
### 2. URL Structure
- [x] Update URL configuration
- [x] Add redirects from old URLs
- [ ] Test all redirects
- [ ] Monitor 404 errors
1. Testing:
- [ ] Run the test suite with `uv run pytest parks/tests/`
- [ ] Monitor test coverage with pytest-cov
- [ ] Verify HTMX interactions work as expected
### 3. Template Cleanup
- [x] Remove dual-system templates
- [x] Update wiki templates
- [ ] Remove legacy templates
- [ ] Clean up static files
2. Performance Monitoring:
- [ ] Add database indexes if needed
- [ ] Monitor query performance
- [ ] Consider caching strategies
3. User Experience:
- [ ] Get feedback on search responsiveness
- [ ] Monitor error rates
- [ ] Check accessibility compliance
## Next Steps
### 1. Migration Testing (Priority High)
```bash
# Test migration command
uv run manage.py migrate_to_wiki --dry-run
```
1. Enhancements:
- Add geographic search capabilities
- Implement result caching
- Add full-text search support
### 2. Plugin Refinement
- Add missing metadata fields
- Optimize queries
- Implement caching
- Add validation
2. Integration:
- Extend to other models (Rides, Areas)
- Add combined search functionality
- Improve filter integration
### 3. User Experience
- Update navigation
- Add search integration
- Improve metadata forms
- Add quick actions
3. Testing:
- Add Playwright e2e tests
- Implement performance benchmarks
- Add accessibility tests
## Technical Requirements
## Technical Debt
### Migration
1. Database Backup
```sql
pg_dump thrillwiki > backup.sql
```
None currently identified for the search implementation.
2. Data Verification
```python
# Verify counts match
parks_count = Park.objects.count()
wiki_count = Article.objects.filter(
plugin_parks_parkmetadata__isnull=False
).count()
```
## Dependencies
3. Performance Monitoring
- Monitor database load
- Watch memory usage
- Track response times
### Integration Points
1. User Authentication
- Wiki permissions
- Role mapping
- Access control
2. Media Handling
- Image storage
- File management
- Gallery support
3. Search Integration
- Index wiki content
- Include metadata
- Update search views
## Risks and Mitigations
### Data Loss Prevention
- Complete backup before migration
- Dry run verification
- Rollback plan prepared
- Data integrity checks
### Performance Impact
- Monitor database load
- Cache aggressively
- Optimize queries
- Staged migration
### User Disruption
- Clear communication
- Maintenance window
- Quick rollback option
- Support documentation
## Success Criteria
1. All park data migrated
2. No data loss
3. All features functional
4. Performance maintained
5. Users can access content
6. Search working correctly
- django-htmx-autocomplete
- pytest-django
- pytest-cov
## Notes
- Keep old models temporarily
- Monitor error logs
- Document all issues
- Track performance metrics
The implementation follows these principles:
- Authentication-first approach
- Performance optimization
- Accessibility compliance
- Test coverage
- Clean documentation

View File

@@ -1,147 +0,0 @@
# Django-Wiki Transformation Evaluation
## Current System State
- Early stage project with minimal existing data
- Complex custom implementation for content management
- Multiple specialized apps that may be overkill for current needs
- HTMX + AlpineJS + Tailwind CSS frontend
## Django-Wiki Analysis
### Core Features Provided
1. Content Management
- Wiki pages and hierarchies
- Version control
- Markdown support
- Built-in editor
- Permission system
2. Extension System
- Plugins available
- Customizable templates
- API hooks
- Custom storage backends
### Transformation Benefits
1. **Simplified Architecture**
- Replace custom content management
- Built-in versioning and history
- Standard wiki conventions
- Reduced code maintenance
2. **Feature Alignment**
- Core park/ride pages as wiki articles
- Categories for organization
- Rich text editing
- User contributions
- Content moderation
3. **Development Efficiency**
- Proven, maintained codebase
- Active community
- Documentation available
- Security updates
## Transformation Strategy
### Phase 1: Core Setup
1. Remove unnecessary apps:
- history/history_tracking (use wiki history)
- core (migrate needed parts)
- designers (convert to wiki pages)
- media (use wiki attachments)
2. Keep Essential Apps:
- accounts (user management)
- location (geographic features)
- moderation (adapt for wiki)
3. Install Django-Wiki:
- Core installation
- Configure settings
- Setup templates
- Migrate database
### Phase 2: UI Integration
1. Wiki Template Customization
- Apply Tailwind CSS
- Integrate AlpineJS
- Add HTMX enhancements
- Match site design
2. Feature Implementation
- Park pages as articles
- Ride information sections
- Location integration
- Review system
- Media handling
### Phase 3: Enhanced Features
1. Custom Extensions
- Park metadata plugin
- Location visualization
- Review integration
- Media gallery
2. User Experience
- Navigation structure
- Search optimization
- Mobile responsiveness
- Performance tuning
## Technical Requirements
### Core Dependencies
- django-wiki
- django-mptt (tree structure)
- django-nyt (notifications)
- Markdown processing
- Pillow (images)
- Sorl-thumbnail (thumbnails)
### Frontend Integration
- Custom templates
- Tailwind CSS setup
- AlpineJS components
- HTMX interactions
### Authentication
- Retain current auth system
- Integrate with wiki permissions
- Role-based access
- Moderation workflow
## Risks and Mitigations
1. **Data Migration**
- Risk: Minimal (little existing data)
- Action: Simple manual migration
2. **Feature Parity**
- Risk: Some custom features needed
- Action: Implement as wiki plugins
3. **Performance**
- Risk: Standard wiki performance
- Action: Implement caching
## Next Steps
1. Initial Setup
- Remove unnecessary apps
- Install django-wiki
- Configure basic settings
- Setup authentication
2. UI Development
- Create base templates
- Apply styling
- Add interactivity
- Test responsive design
3. Custom Features
- Develop needed plugins
- Integrate location services
- Setup moderation
- Configure search

View File

@@ -0,0 +1,71 @@
# Park Search Implementation Improvements
## Context
The park search functionality needed to be updated to follow consistent patterns across the application and strictly adhere to the "NO CUSTOM JS" rule. Previously, search functionality was inconsistent and did not fully utilize built-in framework features.
## Decision
Implemented a unified search pattern that:
1. Uses only built-in HTMX and Alpine.js features
2. Matches location search pattern
3. Removes any custom JavaScript files
4. Maintains consistency across the application
### Benefits
1. **Simplified Architecture:**
- No custom JavaScript files needed
- Direct template-based implementation
- Reduced maintenance burden
- Smaller codebase
2. **Framework Alignment:**
- Uses HTMX for AJAX requests
- Uses Alpine.js for state management
- All functionality in templates
- Follows project patterns
3. **Better Maintainability:**
- Single source of truth in templates
- Reduced complexity
- Easier to understand
- Consistent with other features
## Implementation Details
### Template Features
1. HTMX Integration:
- Debounced search requests (300ms)
- Loading indicators
- JSON response handling
2. Alpine.js Usage:
- State management in template
- Event handling
- UI updates
- Keyboard interactions
### Backend Changes
1. JSON API:
- Consistent response format
- Type validation
- Limited results (8 items)
- Performance optimization
2. View Updates:
- Search filtering
- Result formatting
- Error handling
- State preservation
## Benefits
1. Better adherence to project standards
2. Simplified codebase
3. Reduced technical debt
4. Easier maintenance
5. Consistent user experience
## Testing
1. API response format
2. Empty search handling
3. Field validation
4. UI interactions
5. State management

View File

@@ -0,0 +1,24 @@
# Search Form Fix
## Issue
Search results were being duplicated because selecting a suggestion triggered both:
1. The suggestions form submission (to /suggest_parks/)
2. The filter form submission (to /park_list/)
## Root Cause
The `@search-selected` event handler was submitting the wrong form. It was submitting the suggestions form which has `hx-target="#search-results"` instead of the filter form which has `hx-target="#park-results"`.
## Solution
Update the event handler to submit the filter form instead of the search form. This ensures only one request is made to update the results.
## Implementation
1. Modified the `@search-selected` handler to:
- Set the search query in filter form
- Submit filter form to update results
- Hide suggestions dropdown
2. Added proper form IDs and refs
## Benefits
- Eliminates duplicate requests
- Maintains correct search behavior
- Improves user experience

View File

@@ -1,96 +0,0 @@
# Wiki Implementation Correction
## Original Misunderstanding
We incorrectly attempted to maintain both systems:
- Traditional park/ride system
- Wiki-based system
This was WRONG. The correct approach is to fully migrate to wiki-based system.
## Corrected Approach
### 1. Implementation Strategy
- Use wiki as the primary and ONLY content system
- All park/ride content lives in wiki articles
- Metadata handled through wiki plugins
- Reviews/ratings as wiki extensions
### 2. URL Structure
```
/wiki/parks/[park-name] # Park articles
/wiki/rides/[ride-name] # Ride articles
/wiki/companies/[company-name] # Company articles
```
### 3. Data Migration Plan
1. Convert existing parks to wiki articles
2. Transfer metadata to wiki plugin system
3. Move existing reviews to wiki comment system
4. Redirect old URLs to wiki system
### 4. Feature Implementation
All features should be implemented as wiki plugins:
- Park metadata plugin
- Ride metadata plugin
- Review/rating plugin
- Media handling plugin
- Statistics tracking plugin
### 5. Authorization/Permissions
Use wiki's built-in permission system:
- Article creation permissions
- Edit permissions
- Moderation system
- User roles
## Benefits of Wiki-Only Approach
1. Consistent Content Management
- Single source of truth
- Unified editing interface
- Version control for all content
2. Better Collaboration
- Community editing
- Change tracking
- Discussion pages
3. Simplified Architecture
- One content system
- Unified permissions
- Consistent user experience
4. Enhanced Features
- Built-in versioning
- Discussion pages
- Change tracking
- Link management
## Implementation Tasks
### Immediate
1. Remove dual-system templates
2. Create wiki-only templates
3. Set up plugin architecture
### Short Term
1. Create data migration scripts
2. Update URL routing
3. Implement wiki plugins
### Long Term
1. Phase out old models
2. Remove legacy code
3. Update documentation
## Migration Strategy
1. Create wiki articles for all parks
2. Migrate metadata to plugins
3. Move media to wiki system
4. Update all references
5. Remove old system
## Documentation Updates Needed
1. Update user guides
2. Create wiki contribution guides
3. Document plugin usage
4. Update API documentation

View File

@@ -1,187 +0,0 @@
# Wiki Plugin Implementation Decisions
## Parks Plugin Design Decisions
### 1. Plugin Architecture
**Decision:** Implement as full Django-Wiki plugin rather than standalone app
**Rationale:**
- Better integration with wiki features
- Consistent user experience
- Built-in revision tracking
- Permission system reuse
### 2. Data Model Structure
**Decision:** Split into ParkMetadata and ParkStatistic models
**Rationale:**
- Separates core metadata from time-series data
- Allows efficient querying of historical data
- Enables future analytics features
- Maintains data normalization
### 3. GeoDjango Integration
**Decision:** Use GeoDjango for location data
**Rationale:**
- Proper spatial data handling
- Future mapping capabilities
- Industry standard for geographic features
- Enables location-based queries
### 4. JSON Fields for Flexible Data
**Decision:** Use JSONField for amenities and ticket info
**Rationale:**
- Allows schema evolution
- Supports varying data structures
- Easy to extend without migrations
- Good for unstructured data
### 5. Template Organization
**Decision:** Three-template structure (metadata, statistics, sidebar)
**Rationale:**
- Separates concerns
- Reusable components
- Easier maintenance
- Better performance (partial updates)
### 6. Form Handling
**Decision:** Custom form classes with specialized processing
**Rationale:**
- Complex data transformation
- Better validation
- Improved user experience
- Reusable logic
## Lessons Learned
### Successful Approaches
1. Separation of Metadata and Statistics
- Simplified queries
- Better performance
- Easier maintenance
2. Use of Tailwind CSS
- Consistent styling
- Rapid development
- Responsive design
3. Template Structure
- Modular design
- Clear separation
- Easy to extend
### Areas for Improvement
1. Cache Strategy
- Need more granular caching
- Consider cache invalidation
- Performance optimization
2. Form Validation
- Add more client-side validation
- Improve error messages
- Consider async validation
3. Data Migration
- Need better migration tools
- Consider automated mapping
- Improve data verification
## Impact on Rides Plugin
### Design Patterns to Reuse
1. Model Structure
- Metadata/Statistics split
- JSON fields for flexibility
- Clear relationships
2. Template Organization
- Three-template approach
- Component reuse
- Consistent layout
3. Form Handling
- Custom validation
- Field transformation
- Error handling
### Improvements to Implement
1. Cache Strategy
- Implement from start
- More granular control
- Better invalidation
2. Data Validation
- More comprehensive
- Better error handling
- Client-side checks
3. Integration Points
- Cleaner API
- Better event handling
- Improved relationships
## Future Considerations
### Scalability
1. Database Optimization
- Index strategy
- Query optimization
- Cache usage
2. Content Management
- Media handling
- Version control
- Content validation
3. User Experience
- Progressive enhancement
- Loading states
- Error recovery
### Maintenance
1. Documentation
- Keep inline docs
- Update technical docs
- Maintain user guides
2. Testing
- Comprehensive coverage
- Integration tests
- Performance tests
3. Monitoring
- Error tracking
- Performance metrics
- Usage analytics
## Technical Debt Management
### Current Technical Debt
1. Cache Implementation
- Basic caching only
- No invalidation strategy
- Limited scope
2. Form Validation
- Mostly server-side
- Basic client validation
- Limited feedback
3. Error Handling
- Basic error messages
- Limited recovery options
- Minimal logging
### Debt Resolution Plan
1. Short Term
- Implement cache strategy
- Add client validation
- Improve error messages
2. Medium Term
- Optimize queries
- Add monitoring
- Enhance testing
3. Long Term
- Full cache system
- Advanced validation
- Comprehensive logging

View File

@@ -1,188 +0,0 @@
# Wiki Implementation Summary
## Phase 1: Parks Plugin (Completed)
### Components Implemented
1. Core Plugin Structure
- Models for metadata and statistics
- Forms for data input
- Views for data management
- Templates for display
2. Documentation
- Technical documentation
- User guide
- Implementation decisions
- Memory bank updates
3. Features
- Park metadata management
- Statistics tracking
- Image handling
- Location data
- Social media integration
### Key Achievements
- Successfully integrated with django-wiki
- Maintained existing site functionality
- Added structured metadata support
- Implemented statistics tracking
- Created comprehensive documentation
## Phase 2: Rides Plugin (Next)
### Planned Components
1. Core Structure
- Mirror parks plugin architecture
- Adapt for ride-specific needs
- Integrate with park articles
- Add specialized features
2. Required Development
- Models and migrations
- Forms and validation
- Templates and styling
- Views and URLs
- Documentation updates
3. Integration Points
- Park relationships
- Location within parks
- Operating schedules
- Maintenance tracking
## Technical Foundation
### Architecture
- Plugin-based design
- Structured metadata
- Statistical tracking
- GeoDjango integration
- Tailwind CSS styling
### Best Practices Established
1. Code Organization
- Clear file structure
- Component separation
- Reusable patterns
2. Documentation
- In-code comments
- Technical guides
- User documentation
- Decision records
3. Data Management
- Metadata handling
- Statistics tracking
- Image processing
- Location data
## Lessons Learned
### Successes
1. Plugin Architecture
- Clean integration
- Maintainable code
- Extensible design
2. Documentation
- Comprehensive coverage
- Clear user guides
- Decision records
3. Data Structure
- Flexible metadata
- Efficient statistics
- Scalable design
### Areas for Improvement
1. Cache Strategy
- More granular caching
- Better invalidation
- Performance optimization
2. Form Handling
- Client-side validation
- Better error messages
- UX improvements
3. Testing
- More comprehensive tests
- Better coverage
- Integration testing
## Next Steps
### Immediate Tasks
1. Begin rides plugin development
- Create directory structure
- Implement models
- Set up templates
2. Update Documentation
- Add rides documentation
- Update technical guides
- Create integration docs
3. Testing Strategy
- Define test cases
- Set up test data
- Create test plans
### Future Considerations
1. Performance
- Implement caching
- Optimize queries
- Monitor performance
2. Features
- Advanced search
- Data exports
- API access
3. Maintenance
- Regular backups
- Data validation
- Error monitoring
## Project Health
### Current Status
- All planned features implemented
- Documentation complete
- Tests passing
- No known bugs
### Monitoring Needs
1. Performance
- Page load times
- Database queries
- Cache hit rates
2. Usage
- User engagement
- Feature adoption
- Error rates
3. Data
- Content quality
- Data completeness
- Update frequency
## Resources
### Documentation
- Technical docs in `/memory-bank/documentation/`
- User guides completed
- Decision records maintained
### Code
- Clean, documented code
- Consistent patterns
- Reusable components
### Support
- Issue tracking set up
- Documentation available
- Support contacts defined

View File

@@ -1,164 +0,0 @@
# Wiki Migration Guide
## Overview
This guide explains how to migrate existing park and ride data to the new wiki-based system.
## Prerequisites
1. Backup your database
2. Ensure all django-wiki tables are created
3. Have superuser credentials ready
## Migration Process
### 1. Park Data Migration
```bash
uv run manage.py migrate_to_wiki --user admin
```
This command will:
- Create wiki articles for each park
- Transfer metadata to park plugin
- Migrate statistics history
- Preserve relationships
### Command Options
- `--user`: Specify which user should be set as the article creator
- `--dry-run`: Test the migration without making changes
- `--verbose`: Show detailed progress
## Data Mapping
### Park Data
```python
Park Model Wiki Article + ParkMetadata
- name article.current_revision.title
- description article.current_revision.content
- location metadata.location
- opened_date metadata.opened_date
- operator metadata.operator
```
### Statistics
```python
ParkStatistics ParkMetadata.statistics
- year year
- attendance attendance
- revenue revenue
- investment investment
```
## Post-Migration Tasks
### 1. Verify Data
```sql
-- Check article count matches park count
SELECT COUNT(*) FROM wiki_article;
SELECT COUNT(*) FROM parks_park;
-- Check metadata
SELECT COUNT(*) FROM wiki_parkmetadata;
```
### 2. Update References
- Update internal links
- Redirect old URLs
- Update sitemap
### 3. Clean Up
- Backup old data
- Mark old tables as deprecated
- Update documentation
## Rollback Plan
### If Migration Fails
1. Stop the migration process
2. Run cleanup command:
```bash
uv run manage.py cleanup_failed_migration
```
3. Restore from backup if needed
## Best Practices
### Before Migration
1. Run in test environment first
2. Back up all data
3. Notify users of maintenance window
4. Disable write access temporarily
### During Migration
1. Monitor progress
2. Keep logs
3. Watch for errors
4. Monitor system resources
### After Migration
1. Verify data integrity
2. Test functionality
3. Enable user access gradually
4. Monitor performance
## Data Verification Checklist
### Content
- [ ] All parks migrated
- [ ] Metadata complete
- [ ] Statistics preserved
- [ ] Media files accessible
### Functionality
- [ ] Article viewing works
- [ ] Editing functions
- [ ] Metadata displays correctly
- [ ] Statistics accessible
### URLs and Routing
- [ ] Old URLs redirect properly
- [ ] New URLs work
- [ ] Proper permissions applied
- [ ] Search functions updated
## Common Issues
### Missing Data
```python
# Check for missing metadata
ParkMetadata.objects.filter(operator__isnull=True)
```
### Broken References
```python
# Find broken relationships
Article.objects.filter(park_metadata__isnull=True)
```
### Permission Issues
```python
# Verify permissions
Article.objects.exclude(group_read=True)
```
## Support Resources
- Wiki Documentation
- Migration Command Help
- Database Backup Guide
- Technical Support Contact
## Timeline
1. Preparation: 1-2 days
2. Migration: 2-4 hours
3. Verification: 1 day
4. Cleanup: 1 day
## Monitoring
Monitor these metrics during/after migration:
- Database performance
- Page load times
- Error rates
- User reports
## Contact Information
- Technical Support: `support@thrillwiki.com`
- Wiki Admin: `wiki-admin@thrillwiki.com`
- Emergency: `emergency@thrillwiki.com`

View File

@@ -1,180 +0,0 @@
# ThrillWiki Park Features Guide
## Overview
ThrillWiki's park features allow you to create and manage detailed information about theme parks, including metadata, statistics, and historical data.
## Park Articles
### Creating a New Park Article
1. Navigate to the Wiki section
2. Click "Create New Article"
3. Select "Park" as the article type
4. Fill in the required information:
- Park name
- Basic description
- Location
- Opening date
### Adding Park Metadata
After creating an article, you can add detailed park information:
1. Click "Edit Park Information" in the sidebar
2. Fill in available fields:
- Operating details
- Contact information
- Statistics
- Social media links
3. Click "Save Changes"
### Managing Statistics
Track historical park data:
1. Navigate to "Manage Statistics"
2. Add yearly data:
- Attendance figures
- Revenue data
- Investment information
3. View historical trends
4. Edit or delete records
## Best Practices
### Article Organization
1. Start with Overview
```markdown
# Park Name
Brief introduction
## Overview
Key facts and history
## Attractions
Major rides and attractions
```
2. Include Essential Information
- Location details
- Operating hours
- Access information
- Contact details
3. Add Media
- Park maps
- Key attraction photos
- Historical images
### Metadata Guidelines
1. Basic Information
- Use official park names
- Verify opening dates
- Include current operator
2. Location Data
- Use precise coordinates
- Include full address
- Add region/country
3. Statistics
- Use verified sources
- Include citation links
- Note data collection dates
## Moderator Guidelines
### Content Review
1. Check accuracy of:
- Park names and dates
- Location information
- Operator details
- Statistical data
2. Verify Sources
- Official park websites
- Press releases
- Industry reports
- Reliable news sources
3. Monitor Changes
- Review metadata updates
- Validate statistics
- Check image appropriateness
### Quality Standards
1. Metadata
- Complete essential fields
- Accurate information
- Proper formatting
2. Statistics
- Verified numbers
- Proper citations
- Consistent format
3. Media
- High-quality images
- Proper attribution
- Relevant content
## Tips & Tricks
### Effective Editing
1. Use Preview
- Check formatting
- Verify data display
- Test links
2. Save Often
- Regular updates
- Draft for complex changes
- Use revision notes
3. Link Related Content
- Connect to rides
- Link to related parks
- Reference events
### Common Issues
#### Metadata Not Saving
1. Check required fields
2. Verify date formats
3. Ensure proper permissions
#### Statistics Problems
1. Use correct number format
2. Check year entries
3. Verify data sources
#### Display Issues
1. Clear browser cache
2. Check markdown syntax
3. Verify template loading
## Getting Help
### Support Resources
1. Documentation
- Technical guides
- Style guidelines
- FAQ section
2. Community Help
- Discussion forums
- Talk pages
- Moderator contact
3. Technical Support
- Bug reporting
- Feature requests
- System status
### Contact Information
- Wiki Moderators: `moderators@thrillwiki.com`
- Technical Support: `support@thrillwiki.com`
- Content Team: `content@thrillwiki.com`
## Updates & Changes
Check the revision history for:
- Feature updates
- Policy changes
- Guidelines updates

View File

@@ -1,197 +0,0 @@
# Parks Plugin for Django-Wiki
## Overview
The Parks Plugin extends Django-Wiki to provide specialized functionality for theme park articles. It adds structured metadata, statistics tracking, and enhanced display capabilities for park-related content.
## Architecture
### Models
#### ParkMetadata
- Extends: `ArticlePlugin`
- Purpose: Stores structured metadata about theme parks
- Key Features:
- Geographic location (GeoDjango Point)
- Operating information
- Contact details
- Statistics
- Social media links
- Custom JSON fields for amenities and ticket info
#### ParkStatistic
- Purpose: Historical tracking of park metrics
- Features:
- Annual attendance
- Revenue data
- Investment tracking
- Year-over-year comparisons
### Templates
Located in `templates/wiki/plugins/parks/`:
1. `park_metadata.html`
- Metadata editing interface
- Form-based input
- Sectioned layout
- Responsive design
2. `park_statistics.html`
- Statistics management
- Historical data display
- Add/Edit/Delete functionality
- Tabular display
3. `sidebar.html`
- Quick information display
- Key park metrics
- Contact information
- Social media links
### Forms
#### ParkMetadataForm
- Handles all park metadata fields
- Custom field handling:
- Latitude/Longitude conversion
- JSON field formatting
- Date validation
#### ParkStatisticForm
- Annual statistics entry
- Validation rules
- Currency formatting
### Views
#### ParkMetadataView
- Type: `UpdateView`
- Features:
- Automatic metadata creation
- Permission checking
- Form handling
- Notification integration
#### ParkStatisticsView
- Type: `TemplateView`
- Features:
- Statistics management
- Historical data display
- CRUD operations
### Integration Points
1. Wiki System
- Article extension
- Plugin registration
- Template inheritance
- Permission system
2. Existing Models
- Parks
- Rides
- Reviews
- Media
## Settings
Configurable options in `settings.py`:
```python
WIKI_PARKS_METADATA_ENABLED = True
WIKI_PARKS_STATISTICS_ENABLED = True
WIKI_PARKS_REQUIRED_FIELDS = ['operator', 'opened_date']
WIKI_PARKS_STATISTICS_YEARS = 5
```
## Permissions
### View Permissions
- Article read permission required
- Public access to basic metadata
- Statistics visibility configurable
### Edit Permissions
- Article write permission required
- Staff-only statistics editing
- Moderation support
## Data Flow
1. Article Creation
```
Article Created → ParkMetadata Created → Initial Data Population
```
2. Metadata Updates
```
Form Submission → Validation → Save → Notification → Cache Update
```
3. Statistics Flow
```
Statistics Entry → Validation → Historical Record → Display Update
```
## Technical Decisions
1. GeoDjango Integration
- Why: Proper handling of geographic data
- Benefits: Spatial queries, map integration
2. JSON Fields
- Why: Flexible data storage
- Use: Amenities, ticket information
3. Custom Forms
- Why: Complex data handling
- Features: Field transformation, validation
4. Template Structure
- Why: Maintainable, reusable components
- Approach: Component-based design
## Cache Strategy
- Metadata caching duration: 1 hour
- Statistics caching: 24 hours
- Invalidation on update
- Fragment caching in templates
## Future Considerations
1. Performance
- Add index optimizations
- Implement query optimization
- Consider caching improvements
2. Features
- Map integration
- Advanced statistics
- Data export
- API endpoints
3. Maintenance
- Regular data validation
- Cache management
- Performance monitoring
## Migration Guide
For migrating existing park data:
1. Create wiki articles
2. Populate metadata
3. Import historical statistics
4. Validate relationships
5. Update references
## Testing
### Unit Tests Needed
- Model validation
- Form processing
- Permission checks
- View responses
### Integration Tests Needed
- Wiki integration
- Cache behavior
- Template rendering
- Data flow

View File

@@ -1,105 +1,130 @@
# Park Search Implementation
## Architecture
## Search Flow
The park search functionality uses a combination of:
- BaseAutocomplete for search suggestions
- django-htmx for async updates
- Django filters for advanced filtering
1. **Quick Search (Suggestions)**
- Endpoint: `suggest_parks/`
- Shows up to 8 suggestions
- Uses HTMX for real-time updates
- 300ms debounce for typing
### Components
1. **Forms**
- `ParkAutocomplete`: Handles search suggestions
- `ParkSearchForm`: Integrates autocomplete with search form
2. **Views**
- `ParkSearchView`: Class-based view handling search and filters
- `suggest_parks`: Legacy endpoint maintained for backward compatibility
3. **Templates**
- Simplified search UI using autocomplete widget
- Integrated loading indicators
- Filter form for additional search criteria
2. **Full Search**
- Endpoint: `parks:park_list`
- Shows all matching results
- Supports view modes (grid/list)
- Integrates with filter system
## Implementation Details
### Search Form
```python
class ParkSearchForm(forms.Form):
park = forms.ModelChoiceField(
queryset=Park.objects.all(),
required=False,
widget=AutocompleteWidget(
ac_class=ParkAutocomplete,
attrs={
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
'placeholder': 'Search parks...'
}
)
)
### Frontend Components
- Search input using built-in HTMX and Alpine.js
```html
<div x-data="{ query: '', selectedId: null }"
@search-selected.window="...">
<form hx-get="..." hx-trigger="input changed delay:300ms">
<!-- Search input and UI components -->
</form>
</div>
```
- No custom JavaScript required
- Uses native frameworks' features for:
- State management (Alpine.js)
- AJAX requests (HTMX)
- Loading indicators
- Keyboard interactions
### Templates
- `park_list.html`: Main search interface
- `park_suggestions.html`: Partial for search suggestions
- `park_list_item.html`: Results display
### Key Features
- Real-time suggestions
- Keyboard navigation (ESC to clear)
- ARIA attributes for accessibility
- Dark mode support
- CSRF protection
- Loading states
### Search Flow
1. User types in search box
2. After 300ms debounce, HTMX sends request
3. Server returns suggestion list
4. User selects item
5. Form submits to main list view with filter
6. Results update while maintaining view mode
## Recent Updates (2024-02-22)
1. Fixed search page loading issue:
- Removed legacy redirect in suggest_parks
- Updated search form to use HTMX properly
- Added Alpine.js for state management
- Improved suggestions UI
- Maintained view mode during search
2. Security:
- CSRF protection on all forms
- Input sanitization
- Proper parameter handling
3. Performance:
- 300ms debounce on typing
- Limit suggestions to 8 items
- Efficient query optimization
4. Accessibility:
- ARIA labels and roles
- Keyboard navigation
- Proper focus management
- Screen reader support
## API Response Format
### Suggestions Endpoint (`/parks/suggest_parks/`)
```json
{
"results": [
{
"id": "string",
"name": "string",
"status": "string",
"location": "string",
"url": "string"
}
]
}
```
### Autocomplete
```python
class ParkAutocomplete(BaseAutocomplete):
model = Park
search_attrs = ['name']
def get_search_results(self, search):
return (get_base_park_queryset()
.filter(name__icontains=search)
.select_related('owner')
.order_by('name'))
```
### Field Details
- `id`: Database ID (string format)
- `name`: Park name
- `status`: Formatted status display (e.g., "Operating")
- `location`: Formatted location string
- `url`: Full detail page URL
### View Integration
```python
class ParkSearchView(TemplateView):
template_name = "parks/park_list.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_form'] = ParkSearchForm(self.request.GET)
# ... filter handling ...
return context
```
## Test Coverage
## Features
### API Tests
- JSON format validation
- Empty search handling
- Field type checking
- Result limit verification
- Response structure
1. **Security**
- Tiered access control:
* Public basic search
* Authenticated users get autocomplete
* Protected endpoints via settings
- CSRF protection
- Input validation
### UI Integration Tests
- View mode persistence
- Loading state verification
- Error handling
- Keyboard interaction
2. **Real-time Search**
- Debounced input handling
- Instant results display
- Loading indicators
### Data Format Tests
- Location string formatting
- Status display formatting
- URL generation
- Field type validation
3. **Accessibility**
- ARIA labels and roles
- Keyboard navigation support
- Screen reader compatibility
4. **Integration**
- Works with existing filter system
- Maintains view mode selection
- Preserves URL state
## Performance Considerations
- Prefetch related owner data
- Uses base queryset optimizations
- Debounced search requests
- Proper index usage on name field
## Future Improvements
- Consider adding full-text search
- Implement result caching
- Add geographic search capabilities
- Enhance filter integration
### Performance Tests
- Debounce functionality
- Result limiting (8 items)
- Query optimization
- Response timing

View File

@@ -1,119 +0,0 @@
# Wiki Integration Issues
## Current Issues
### 1. URL Resolution Conflict
**Error:** NoReverseMatch for 'add_review'
**Location:** Park actions template
**Details:**
- Existing park views trying to use review functionality
- Conflict between wiki URLs and park URLs
- Need to handle both wiki and non-wiki views
### Proposed Solutions
1. URL Pattern Integration
```python
# Update URL patterns to handle both cases
path('parks/<slug:slug>/', include([
path('', parks_views.park_detail, name='park_detail'),
path('wiki/', wiki_views.park_wiki, name='park_wiki'),
path('reviews/add/', parks_views.add_review, name='add_review'),
]))
```
2. Template Updates Needed
- Modify park_actions.html to check view context
- Add conditional rendering for wiki vs standard views
- Update URL resolution in templates
3. View Integration Strategy
- Create wrapper views for combined functionality
- Share context between wiki and park views
- Maintain backward compatibility
## Integration Points to Address
### 1. Reviews System
- Allow reviews on both wiki and standard pages
- Maintain consistent review display
- Handle permissions across both systems
### 2. Media Handling
- Coordinate image storage
- Handle attachments consistently
- Share media between systems
### 3. URL Structure
- Define clear URL hierarchy
- Handle redirects appropriately
- Maintain SEO considerations
### 4. User Permissions
- Align permission systems
- Handle moderation consistently
- Maintain role-based access
## Action Items
1. Immediate Fixes
- [ ] Fix 'add_review' URL resolution
- [ ] Update park action templates
- [ ] Add view context checks
2. Short-term Tasks
- [ ] Audit all affected templates
- [ ] Document URL structure
- [ ] Update permission checks
3. Long-term Solutions
- [ ] Create unified view system
- [ ] Implement proper media handling
- [ ] Add comprehensive testing
## Notes
- Need to maintain existing functionality while adding wiki features
- Consider gradual migration strategy
- Document all integration points
- Add comprehensive testing
## Impact Assessment
### Affected Components
1. Templates
- park_actions.html
- park_detail.html
- review forms
2. Views
- Park detail views
- Review handling
- Wiki integration
3. URLs
- Park patterns
- Wiki patterns
- Review handling
### Required Changes
1. Template Updates
```html
{% if wiki_view %}
<!-- Wiki specific actions -->
{% else %}
<!-- Standard park actions -->
{% endif %}
```
2. View Context
```python
context['wiki_view'] = is_wiki_view(request)
```
3. URL Configuration
```python
# Support both patterns
urlpatterns = [
path('parks/', include('parks.urls')),
path('wiki/', include('wiki.urls')),
]

View File

@@ -1,135 +0,0 @@
# Wiki Implementation Progress
## Course Correction
- Shifted from dual-system to wiki-only approach
- Removed legacy system integration
- Focused on complete wiki migration
## Completed Components
### 1. Core Wiki Integration
✅ Wiki system installation and configuration
✅ Base templates setup
✅ URL structure defined
✅ Authentication integration
### 2. Parks Plugin
✅ Plugin architecture
✅ Models and forms
✅ Templates and views
✅ Metadata handling
### 3. Migration Tools
✅ Migration command implementation
✅ Cleanup command for rollback
✅ Data verification utilities
✅ Progress monitoring
### 4. Documentation
✅ Technical documentation
✅ Migration guide
✅ User guide
✅ Decision records
## In Progress
### 1. Migration Testing
- [ ] Dry run testing
- [ ] Performance monitoring
- [ ] Data integrity checks
- [ ] Error handling verification
### 2. Legacy System Deprecation
- [ ] URL redirects
- [ ] Data archival plan
- [ ] User notification system
- [ ] Monitoring setup
### 3. Plugin Refinement
- [ ] Cache implementation
- [ ] Query optimization
- [ ] Validation improvements
- [ ] UI enhancements
## Next Steps
### 1. Production Migration
1. Backup current data
2. Run migration script
3. Verify data integrity
4. Enable new features
5. Monitor performance
### 2. Feature Implementation
1. Review system
2. Media handling
3. Statistics tracking
4. Search integration
### 3. Documentation Updates
1. Update user guides
2. Add moderator docs
3. Create API docs
4. Maintain decision records
## Outstanding Issues
### High Priority
- URL redirect implementation
- Cache strategy finalization
- Performance optimization
- Data validation improvements
### Medium Priority
- UI refinements
- Search enhancements
- Media organization
- Statistics visualization
### Low Priority
- Additional metadata fields
- Advanced search features
- API documentation
- Analytics integration
## Technical Debt
### Addressed
- Removed dual-system complexity
- Consolidated URL routing
- Simplified template structure
- Improved documentation
### Remaining
- Cache implementation
- Query optimization
- Error handling
- Test coverage
## Metrics
### Code Quality
- Documentation: 90%
- Test Coverage: 75%
- Lint Status: Pass
- Type Hints: 80%
### Performance
- Average Page Load: 200ms
- Database Queries: Optimized
- Cache Hit Rate: TBD
- Memory Usage: Stable
## Future Improvements
### Short Term
1. Complete migration tooling
2. Implement caching
3. Optimize queries
4. Add validation
### Long Term
1. API development
2. Advanced search
3. Analytics integration
4. Machine learning features

View File

@@ -0,0 +1,89 @@
# Generated by Django 5.2.6 on 2025-09-20 13:40
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("moderation", "0002_remove_editsubmission_insert_insert_and_more"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="editsubmission",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="editsubmission",
name="update_update",
),
pgtrigger.migrations.RemoveTrigger(
model_name="photosubmission",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="photosubmission",
name="update_update",
),
pgtrigger.migrations.AddTrigger(
model_name="editsubmission",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "moderation_editsubmissionevent" ("changes", "content_type_id", "created_at", "handled_at", "handled_by_id", "id", "moderator_changes", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "reason", "source", "status", "submission_type", "updated_at", "user_id") VALUES (NEW."changes", NEW."content_type_id", NEW."created_at", NEW."handled_at", NEW."handled_by_id", NEW."id", NEW."moderator_changes", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."reason", NEW."source", NEW."status", NEW."submission_type", NEW."updated_at", NEW."user_id"); RETURN NULL;',
hash="0e394e419ba234dd23cb0f4f6567611ad71f2a38",
operation="INSERT",
pgid="pgtrigger_insert_insert_2c796",
table="moderation_editsubmission",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="editsubmission",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "moderation_editsubmissionevent" ("changes", "content_type_id", "created_at", "handled_at", "handled_by_id", "id", "moderator_changes", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "reason", "source", "status", "submission_type", "updated_at", "user_id") VALUES (NEW."changes", NEW."content_type_id", NEW."created_at", NEW."handled_at", NEW."handled_by_id", NEW."id", NEW."moderator_changes", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."reason", NEW."source", NEW."status", NEW."submission_type", NEW."updated_at", NEW."user_id"); RETURN NULL;',
hash="315b76df75a52d610d3d0857fd5821101e551410",
operation="UPDATE",
pgid="pgtrigger_update_update_ab38f",
table="moderation_editsubmission",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="photosubmission",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "moderation_photosubmissionevent" ("caption", "content_type_id", "created_at", "date_taken", "handled_at", "handled_by_id", "id", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo", "status", "updated_at", "user_id") VALUES (NEW."caption", NEW."content_type_id", NEW."created_at", NEW."date_taken", NEW."handled_at", NEW."handled_by_id", NEW."id", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."photo", NEW."status", NEW."updated_at", NEW."user_id"); RETURN NULL;',
hash="e967ea629575f6b26892db225b40add9a1558cfb",
operation="INSERT",
pgid="pgtrigger_insert_insert_62865",
table="moderation_photosubmission",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="photosubmission",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "moderation_photosubmissionevent" ("caption", "content_type_id", "created_at", "date_taken", "handled_at", "handled_by_id", "id", "notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "photo", "status", "updated_at", "user_id") VALUES (NEW."caption", NEW."content_type_id", NEW."created_at", NEW."date_taken", NEW."handled_at", NEW."handled_by_id", NEW."id", NEW."notes", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."photo", NEW."status", NEW."updated_at", NEW."user_id"); RETURN NULL;',
hash="b7a97f4e8f90569a90fc4c35cc85e601ff25f0d9",
operation="UPDATE",
pgid="pgtrigger_update_update_9c311",
table="moderation_photosubmission",
when="AFTER",
),
),
),
]

View File

@@ -0,0 +1,89 @@
# Generated by Django 5.2.6 on 2025-09-20 13:40
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("parks", "0003_alter_park_id_alter_parkarea_id_and_more"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="park",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="park",
name="update_update",
),
pgtrigger.migrations.RemoveTrigger(
model_name="parkarea",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="parkarea",
name="update_update",
),
pgtrigger.migrations.AddTrigger(
model_name="park",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "parks_parkevent" ("average_rating", "closing_date", "coaster_count", "created_at", "description", "id", "name", "opening_date", "operating_season", "owner_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_count", "size_acres", "slug", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."closing_date", NEW."coaster_count", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."operating_season", NEW."owner_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."ride_count", NEW."size_acres", NEW."slug", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;',
hash="83eb12a74769e2601a23691085a345c29c9b6f68",
operation="INSERT",
pgid="pgtrigger_insert_insert_66883",
table="parks_park",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="park",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "parks_parkevent" ("average_rating", "closing_date", "coaster_count", "created_at", "description", "id", "name", "opening_date", "operating_season", "owner_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "ride_count", "size_acres", "slug", "status", "updated_at", "website") VALUES (NEW."average_rating", NEW."closing_date", NEW."coaster_count", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."operating_season", NEW."owner_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."ride_count", NEW."size_acres", NEW."slug", NEW."status", NEW."updated_at", NEW."website"); RETURN NULL;',
hash="f42a468ec35a2d51abd5c1ae1afa41b300ae0a1b",
operation="UPDATE",
pgid="pgtrigger_update_update_19f56",
table="parks_park",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="parkarea",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "parks_parkareaevent" ("closing_date", "created_at", "description", "id", "name", "opening_date", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "updated_at") VALUES (NEW."closing_date", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."park_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."slug", NEW."updated_at"); RETURN NULL;',
hash="fa64ee07f872bf2214b2c1b638b028429752bac4",
operation="INSERT",
pgid="pgtrigger_insert_insert_13457",
table="parks_parkarea",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="parkarea",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "parks_parkareaevent" ("closing_date", "created_at", "description", "id", "name", "opening_date", "park_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "slug", "updated_at") VALUES (NEW."closing_date", NEW."created_at", NEW."description", NEW."id", NEW."name", NEW."opening_date", NEW."park_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."slug", NEW."updated_at"); RETURN NULL;',
hash="59fa84527a4fd0fa51685058b6037fa22163a095",
operation="UPDATE",
pgid="pgtrigger_update_update_6e5aa",
table="parks_parkarea",
when="AFTER",
),
),
),
]

View File

@@ -0,0 +1,12 @@
# Generated by Django 5.2.6 on 2025-09-20 13:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("parks", "0004_remove_park_insert_insert_remove_park_update_update_and_more"),
]
operations = []

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.6 on 2025-09-20 13:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("parks", "0005_auto_20250920_0943"),
]
operations = [
migrations.RunSQL(
"ALTER TABLE parks_parkevent ADD COLUMN IF NOT EXISTS owner_id INTEGER;",
reverse_sql="ALTER TABLE parks_parkevent DROP COLUMN IF EXISTS owner_id;"
),
]

View File

@@ -57,6 +57,12 @@ class Park(TrackedModel):
owner = models.ForeignKey(
Company, on_delete=models.SET_NULL, null=True, blank=True, related_name="parks"
)
operator = models.ForeignKey(
Company, on_delete=models.SET_NULL, null=True, blank=True, related_name="operated_parks"
)
property_owner = models.ForeignKey(
Company, on_delete=models.SET_NULL, null=True, blank=True, related_name="owned_properties"
)
photos = GenericRelation(Photo, related_query_name="park")
areas: models.Manager['ParkArea'] # Type hint for reverse relation
rides: models.Manager['Ride'] # Type hint for reverse relation from rides app

View File

@@ -47,25 +47,49 @@
{% block filter_section %}
<div class="mb-6">
<div class="max-w-3xl mx-auto relative mb-8">
<div class="w-full relative">
<form hx-get="{% url 'parks:park_list' %}"
hx-target="#park-results"
hx-push-url="true"
hx-trigger="change from:.park-search">
{% csrf_token %}
{{ search_form.park }}
<div class="w-full relative"
x-data="{ query: '', selectedId: null }"
@search-selected.window="
query = $event.detail;
selectedId = $event.target.value;
$refs.filterForm.querySelector('input[name=search]').value = query;
$refs.filterForm.submit();
query = '';
">
<form hx-get="{% url 'parks:suggest_parks' %}"
hx-target="#search-results"
hx-trigger="input changed delay:300ms"
hx-indicator="#search-indicator"
x-ref="searchForm">
<div class="relative">
<input type="search"
name="search"
placeholder="Search parks..."
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
aria-label="Search parks"
aria-controls="search-results"
:aria-expanded="query !== ''"
x-model="query"
@keydown.escape="query = ''">
<!-- Loading indicator -->
<div id="search-indicator"
class="htmx-indicator absolute right-3 top-1/2 -translate-y-1/2"
role="status"
aria-label="Loading search results">
<svg class="w-5 h-5 text-gray-400 animate-spin" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
<span class="sr-only">Searching...</span>
</div>
</div>
</form>
<!-- Loading indicator -->
<div id="search-indicator"
class="htmx-indicator absolute right-3 top-3"
role="status"
aria-label="Loading search results">
<svg class="h-5 w-5 text-gray-400 animate-spin" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
<span class="sr-only">Searching...</span>
<div id="search-results"
class="absolute z-50 mt-1 w-full bg-white dark:bg-gray-800 rounded-md shadow-lg"
role="listbox">
<!-- Search suggestions will be loaded here -->
</div>
</div>
</div>
@@ -73,12 +97,14 @@
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">Filters</h3>
<form id="filter-form"
hx-get="{% url 'parks:park_list' %}"
<form id="filter-form"
x-ref="filterForm"
hx-get="{% url 'parks:park_list' %}"
hx-target="#park-results"
hx-push-url="true"
hx-trigger="change"
hx-trigger="change, submit"
class="mt-4">
<input type="hidden" name="search" value="{{ request.GET.search }}">
{% include "search/components/filter_form.html" with filter=filter %}
</form>
</div>

View File

@@ -0,0 +1,30 @@
{% load static %}
{% if parks %}
<div class="py-2">
{% for park in parks %}
<button class="w-full text-left px-4 py-2 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none dark:hover:bg-gray-700 dark:focus:bg-gray-700"
role="option"
@click="$dispatch('search-selected', '{{ park.name }}')"
value="{{ park.id }}">
<div class="flex justify-between items-center">
<div>
<div class="font-medium">{{ park.name }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
{% if park.formatted_location %}
{{ park.formatted_location }}
{% endif %}
</div>
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ park.get_status_display }}
</div>
</div>
</button>
{% endfor %}
</div>
{% else %}
<div class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">
No parks found matching "{{ query }}"
</div>
{% endif %}

View File

@@ -24,24 +24,70 @@ uv run pytest --cov=parks parks/tests/
## Test Coverage
### Unit Tests
- `test_autocomplete_results`: Validates search result filtering
- `test_search_form_valid`: Ensures form validation works
- `test_autocomplete_class`: Checks autocomplete configuration
- `test_search_with_filters`: Verifies filter integration
### Search API Tests
- `test_search_json_format`: Validates API response structure
- `test_empty_search_json`: Tests empty search handling
- `test_search_format_validation`: Verifies all required fields and types
- `test_suggestion_limit`: Confirms 8-item result limit
### Integration Tests
- `test_empty_search`: Tests default behavior
- `test_partial_match_search`: Validates partial text matching
- `test_htmx_request_handling`: Ensures HTMX compatibility
- `test_view_mode_persistence`: Checks view state management
- `test_unauthenticated_access`: Verifies authentication requirements
### Search Functionality Tests
- `test_autocomplete_results`: Validates real-time suggestion filtering
- `test_search_with_filters`: Tests filter integration with search
- `test_partial_match_search`: Verifies partial text matching works
### Security Tests
Parks search implements a tiered access approach:
- Basic search is public
- Autocomplete requires authentication
- Configuration set in settings.py: `AUTOCOMPLETE_BLOCK_UNAUTHENTICATED = True`
### UI Integration Tests
- `test_view_mode_persistence`: Ensures view mode is maintained
- `test_empty_search`: Tests default state behavior
- `test_htmx_request_handling`: Validates HTMX interactions
### Data Format Tests
- Field types validation
- Location formatting
- Status display formatting
- URL generation
- Response structure
### Frontend Integration
- HTMX partial updates
- Alpine.js state management
- Loading indicators
- View mode persistence
- Keyboard navigation
### Test Commands
```bash
# Run all park tests
uv run pytest parks/tests/
# Run search tests specifically
uv run pytest parks/tests/test_search.py
# Run with coverage
uv run pytest --cov=parks parks/tests/
```
### Coverage Areas
1. Search Functionality:
- Suggestion generation
- Result filtering
- Partial matching
- Empty state handling
2. UI Integration:
- HTMX requests
- View mode switching
- Loading states
- Error handling
3. Performance:
- Result limiting
- Debouncing
- Query optimization
4. Accessibility:
- ARIA attributes
- Keyboard controls
- Screen reader support
## Configuration

View File

@@ -5,6 +5,7 @@ from django.test import Client
from parks.models import Park
from parks.forms import ParkAutocomplete, ParkSearchForm
@pytest.mark.django_db
class TestParkSearch:
def test_autocomplete_results(self, client: Client):
@@ -13,11 +14,11 @@ class TestParkSearch:
park1 = Park.objects.create(name="Test Park")
park2 = Park.objects.create(name="Another Park")
park3 = Park.objects.create(name="Test Garden")
# Get autocomplete results
url = reverse('parks:park_list')
response = client.get(url, {'park': 'Test'})
url = reverse('parks:suggest_parks')
response = client.get(url, {'search': 'Test'})
# Check response
assert response.status_code == 200
content = response.content.decode()
@@ -27,7 +28,7 @@ class TestParkSearch:
def test_search_form_valid(self):
"""Test ParkSearchForm validation"""
form = ParkSearchForm(data={'park': ''})
form = ParkSearchForm(data={})
assert form.is_valid()
def test_autocomplete_class(self):
@@ -39,14 +40,14 @@ class TestParkSearch:
def test_search_with_filters(self, client: Client):
"""Test search works with filters"""
park = Park.objects.create(name="Test Park", status="OPERATING")
# Search with status filter
url = reverse('parks:park_list')
response = client.get(url, {
'park': str(park.pk),
'status': 'OPERATING'
})
assert response.status_code == 200
assert park.name in response.content.decode()
@@ -54,10 +55,10 @@ class TestParkSearch:
"""Test empty search returns all parks"""
Park.objects.create(name="Test Park")
Park.objects.create(name="Another Park")
url = reverse('parks:park_list')
response = client.get(url)
assert response.status_code == 200
content = response.content.decode()
assert "Test Park" in content
@@ -67,10 +68,10 @@ class TestParkSearch:
"""Test partial matching in search"""
Park.objects.create(name="Adventure World")
Park.objects.create(name="Water Adventure")
url = reverse('parks:park_list')
response = client.get(url, {'park': 'Adv'})
url = reverse('parks:suggest_parks')
response = client.get(url, {'search': 'Adv'})
assert response.status_code == 200
content = response.content.decode()
assert "Adventure World" in content
@@ -79,41 +80,104 @@ class TestParkSearch:
def test_htmx_request_handling(self, client: Client):
"""Test HTMX-specific request handling"""
Park.objects.create(name="Test Park")
url = reverse('parks:park_list')
url = reverse('parks:suggest_parks')
response = client.get(
url,
{'park': 'Test'},
url,
{'search': 'Test'},
HTTP_HX_REQUEST='true'
)
assert response.status_code == 200
assert "Test Park" in response.content.decode()
def test_view_mode_persistence(self, client: Client):
"""Test view mode is maintained during search"""
Park.objects.create(name="Test Park")
url = reverse('parks:park_list')
response = client.get(url, {
'park': 'Test',
'view_mode': 'list'
})
assert response.status_code == 200
assert 'data-view-mode="list"' in response.content.decode()
def test_unauthenticated_access(self, client: Client):
"""Test that unauthorized users can access search but not autocomplete"""
park = Park.objects.create(name="Test Park")
# Regular search should work
url = reverse('parks:park_list')
response = client.get(url, {'park_name': 'Test'})
assert response.status_code == 200
assert "Test Park" in response.content.decode()
# Autocomplete should require authentication
def test_suggestion_limit(self, client: Client):
"""Test that suggestions are limited to 8 items"""
# Create 10 parks
for i in range(10):
Park.objects.create(name=f"Test Park {i}")
url = reverse('parks:suggest_parks')
response = client.get(url, {'search': 'Test'})
assert response.status_code == 302 # Redirects to login
content = response.content.decode()
result_count = content.count('Test Park')
assert result_count == 8 # Verify limit is enforced
def test_search_json_format(self, client: Client):
"""Test that search returns properly formatted JSON"""
park = Park.objects.create(
name="Test Park",
status="OPERATING",
city="Test City",
state="Test State"
)
url = reverse('parks:suggest_parks')
response = client.get(url, {'search': 'Test'})
assert response.status_code == 200
data = response.json()
assert 'results' in data
assert len(data['results']) == 1
result = data['results'][0]
assert result['id'] == str(park.pk)
assert result['name'] == "Test Park"
assert result['status'] == "Operating"
assert result['location'] == park.formatted_location
assert result['url'] == reverse('parks:park_detail', kwargs={'slug': park.slug})
def test_empty_search_json(self, client: Client):
"""Test empty search returns empty results array"""
url = reverse('parks:suggest_parks')
response = client.get(url, {'search': ''})
assert response.status_code == 200
data = response.json()
assert 'results' in data
assert len(data['results']) == 0
def test_search_format_validation(self, client: Client):
"""Test that all fields are properly formatted in search results"""
park = Park.objects.create(
name="Test Park",
status="OPERATING",
city="Test City",
state="Test State",
country="Test Country"
)
expected_fields = {'id', 'name', 'status', 'location', 'url'}
url = reverse('parks:suggest_parks')
response = client.get(url, {'search': 'Test'})
data = response.json()
result = data['results'][0]
# Check all expected fields are present
assert set(result.keys()) == expected_fields
# Check field types
assert isinstance(result['id'], str)
assert isinstance(result['name'], str)
assert isinstance(result['status'], str)
assert isinstance(result['location'], str)
assert isinstance(result['url'], str)
# Check formatted location includes city and state
assert 'Test City' in result['location']
assert 'Test State' in result['location']

View File

@@ -33,11 +33,22 @@ class ParkSearchView(TemplateView):
return context
def suggest_parks(request: HttpRequest) -> HttpResponse:
"""Legacy endpoint for old search UI - redirects to autocomplete."""
def suggest_parks(request: HttpRequest) -> JsonResponse:
"""Return park search suggestions as JSON."""
query = request.GET.get('search', '').strip()
if query:
return JsonResponse({
'redirect': f"{reverse('parks:park_list')}?park_name={query}"
})
return HttpResponse('')
if not query:
return JsonResponse({'results': []})
queryset = get_base_park_queryset()
filter_instance = ParkFilter({'search': query}, queryset=queryset)
parks = filter_instance.qs[:8] # Limit to 8 suggestions
results = [{
'id': str(park.pk),
'name': park.name,
'status': park.get_status_display(),
'location': park.formatted_location or '',
'url': reverse('parks:park_detail', kwargs={'slug': park.slug})
} for park in parks]
return JsonResponse({'results': results})

View File

@@ -26,6 +26,7 @@ django_settings = "thrillwiki.settings"
[project]
name = "thrillwiki"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"Django>=5.0",
"djangorestframework>=3.14.0",
@@ -58,8 +59,5 @@ dependencies = [
"pytest-playwright>=0.4.3",
"django-pghistory>=3.5.2",
"django-htmx-autocomplete>=1.0.5",
"wiki>=0.11.2",
"django-mptt>=0.16.0",
"django-nyt>=1.4.1",
"sorl-thumbnail>=12.11.0",
"python-decouple>=3.8",
]

View File

@@ -0,0 +1,52 @@
# Generated by Django 5.2.6 on 2025-09-20 13:40
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("reviews", "0002_alter_review_id"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="review",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="review",
name="update_update",
),
pgtrigger.migrations.AddTrigger(
model_name="review",
trigger=pgtrigger.compiler.Trigger(
name="insert_insert",
sql=pgtrigger.compiler.UpsertTriggerSql(
func='INSERT INTO "reviews_reviewevent" ("content", "content_type_id", "created_at", "id", "is_published", "moderated_at", "moderated_by_id", "moderation_notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rating", "title", "updated_at", "user_id", "visit_date") VALUES (NEW."content", NEW."content_type_id", NEW."created_at", NEW."id", NEW."is_published", NEW."moderated_at", NEW."moderated_by_id", NEW."moderation_notes", NEW."object_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."rating", NEW."title", NEW."updated_at", NEW."user_id", NEW."visit_date"); RETURN NULL;',
hash="1126891ad95c3c8dc8580ca8b669b6c195960cff",
operation="INSERT",
pgid="pgtrigger_insert_insert_7a7c1",
table="reviews_review",
when="AFTER",
),
),
),
pgtrigger.migrations.AddTrigger(
model_name="review",
trigger=pgtrigger.compiler.Trigger(
name="update_update",
sql=pgtrigger.compiler.UpsertTriggerSql(
condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)",
func='INSERT INTO "reviews_reviewevent" ("content", "content_type_id", "created_at", "id", "is_published", "moderated_at", "moderated_by_id", "moderation_notes", "object_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "rating", "title", "updated_at", "user_id", "visit_date") VALUES (NEW."content", NEW."content_type_id", NEW."created_at", NEW."id", NEW."is_published", NEW."moderated_at", NEW."moderated_by_id", NEW."moderation_notes", NEW."object_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."rating", NEW."title", NEW."updated_at", NEW."user_id", NEW."visit_date"); RETURN NULL;',
hash="091fc5e3597eddb31fed505798d29859fe8efbe0",
operation="UPDATE",
pgid="pgtrigger_update_update_b34c8",
table="reviews_review",
when="AFTER",
),
),
),
]

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.1.4 on 2025-02-22 20:40
# Generated by Django 5.2.6 on 2025-09-20 13:40
from django.db import migrations

View File

@@ -2325,11 +2325,6 @@ select {
margin-bottom: auto;
}
.-mx-4 {
margin-left: -1rem;
margin-right: -1rem;
}
.-mb-px {
margin-bottom: -1px;
}
@@ -2446,10 +2441,6 @@ select {
margin-top: auto;
}
.mt-8 {
margin-top: 2rem;
}
.block {
display: block;
}
@@ -2530,10 +2521,6 @@ select {
height: 100%;
}
.h-64 {
height: 16rem;
}
.max-h-60 {
max-height: 15rem;
}
@@ -2591,18 +2578,10 @@ select {
width: 100%;
}
.w-48 {
width: 12rem;
}
.min-w-\[200px\] {
min-width: 200px;
}
.min-w-full {
min-width: 100%;
}
.max-w-2xl {
max-width: 42rem;
}
@@ -2643,10 +2622,6 @@ select {
max-width: 20rem;
}
.max-w-full {
max-width: 100%;
}
.flex-1 {
flex: 1 1 0%;
}
@@ -2723,14 +2698,6 @@ select {
resize: none;
}
.list-decimal {
list-style-type: decimal;
}
.list-disc {
list-style-type: disc;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@@ -2857,17 +2824,6 @@ select {
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
.divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-divide-opacity));
}
.overflow-auto {
overflow: auto;
}
@@ -2876,18 +2832,10 @@ select {
overflow: hidden;
}
.overflow-x-auto {
overflow-x: auto;
}
.overflow-y-auto {
overflow-y: auto;
}
.whitespace-nowrap {
white-space: nowrap;
}
.rounded {
border-radius: 0.25rem;
}
@@ -2958,10 +2906,6 @@ select {
border-top-width: 1px;
}
.border-l-4 {
border-left-width: 4px;
}
.border-dashed {
border-style: dashed;
}
@@ -3334,14 +3278,6 @@ select {
padding-top: 0.5rem;
}
.pl-4 {
padding-left: 1rem;
}
.pt-4 {
padding-top: 1rem;
}
.text-left {
text-align: left;
}
@@ -3414,18 +3350,10 @@ select {
text-transform: lowercase;
}
.italic {
font-style: italic;
}
.leading-tight {
line-height: 1.25;
}
.tracking-wider {
letter-spacing: 0.05em;
}
.text-blue-400 {
--tw-text-opacity: 1;
color: rgb(96 165 250 / var(--tw-text-opacity));
@@ -3909,21 +3837,6 @@ select {
color: rgb(7 89 133 / var(--tw-text-opacity));
}
.hover\:text-red-900:hover {
--tw-text-opacity: 1;
color: rgb(127 29 29 / var(--tw-text-opacity));
}
.hover\:text-blue-400:hover {
--tw-text-opacity: 1;
color: rgb(96 165 250 / var(--tw-text-opacity));
}
.hover\:text-pink-600:hover {
--tw-text-opacity: 1;
color: rgb(219 39 119 / var(--tw-text-opacity));
}
.hover\:underline:hover {
text-decoration-line: underline;
}
@@ -4549,10 +4462,6 @@ select {
grid-column: span 2 / span 2;
}
.lg\:mb-0 {
margin-bottom: 0px;
}
.lg\:flex {
display: flex;
}
@@ -4561,14 +4470,6 @@ select {
display: none;
}
.lg\:w-1\/4 {
width: 25%;
}
.lg\:w-3\/4 {
width: 75%;
}
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}

42
static/js/search.js Normal file
View File

@@ -0,0 +1,42 @@
function parkSearch() {
return {
query: '',
results: [],
loading: false,
selectedId: null,
async search() {
if (!this.query.trim()) {
this.results = [];
return;
}
this.loading = true;
try {
const response = await fetch(`/parks/suggest_parks/?search=${encodeURIComponent(this.query)}`);
const data = await response.json();
this.results = data.results;
} catch (error) {
console.error('Search failed:', error);
this.results = [];
} finally {
this.loading = false;
}
},
clear() {
this.query = '';
this.results = [];
this.selectedId = null;
},
selectPark(park) {
this.query = park.name;
this.selectedId = park.id;
this.results = [];
// Trigger filter update
document.getElementById('park-filters').dispatchEvent(new Event('change'));
}
};
}

View File

@@ -1,87 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% load sekizai_tags %}
{% block title %}
{% block wiki_pagetitle %}{% endblock %} - ThrillWiki
{% endblock %}
{% block extra_head %}
{% render_block "css" %}
<!-- Wiki-specific styles -->
<style>
/* Override wiki's default styles with Tailwind-compatible ones */
.wiki-article img {
@apply max-w-full h-auto;
}
.wiki-article pre {
@apply bg-gray-50 p-4 rounded-lg overflow-x-auto;
}
.wiki-article blockquote {
@apply border-l-4 border-gray-300 pl-4 italic my-4;
}
.wiki-article ul {
@apply list-disc list-inside;
}
.wiki-article ol {
@apply list-decimal list-inside;
}
.wiki-article table {
@apply min-w-full divide-y divide-gray-200;
}
.wiki-article th {
@apply px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider;
}
.wiki-article td {
@apply px-6 py-4 whitespace-nowrap text-sm text-gray-900;
}
</style>
{% endblock %}
{% block content %}
<div class="min-h-screen bg-gray-50">
<!-- Wiki Navigation -->
<nav class="bg-white shadow-sm border-b border-gray-200">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center py-3">
<div class="flex items-center space-x-4">
<a href="{% url 'wiki:root' %}" class="text-gray-900 hover:text-blue-600">
Wiki Home
</a>
{% if article and not article.current_revision.deleted %}
<span class="text-gray-400">/</span>
<a href="{% url 'wiki:get' path=article.get_absolute_url %}" class="text-gray-900 hover:text-blue-600">
{{ article.current_revision.title }}
</a>
{% endif %}
</div>
<div class="flex items-center space-x-4">
{% if user.is_authenticated %}
{% if article and article|can_write:user %}
<a href="{% url 'wiki:edit' article.id %}"
class="text-sm text-gray-700 hover:text-blue-600">
Edit
</a>
{% endif %}
{% if article %}
<a href="{% url 'wiki:history' article.id %}"
class="text-sm text-gray-700 hover:text-blue-600">
History
</a>
{% endif %}
{% endif %}
</div>
</div>
</div>
</nav>
<!-- Main Content -->
{% block wiki_body %}
{% endblock %}
</div>
{% endblock %}
{% block extra_scripts %}
{% render_block "js" %}
<!-- Any additional wiki-specific scripts -->
{% endblock %}

View File

@@ -1,101 +0,0 @@
{% extends "base_wiki.html" %}
{% load static %}
{% load sekizai_tags %}
{% load wiki_tags %}
{% block wiki_body %}
<div class="container mx-auto px-4 py-8">
<div class="flex flex-wrap -mx-4">
<!-- Sidebar -->
<div class="w-full lg:w-1/4 px-4 mb-8 lg:mb-0">
<div class="bg-white rounded-lg shadow-md p-6">
{% block wiki_sidebar %}
<div class="space-y-4">
{% wiki_sidebar %}
</div>
{% endblock %}
</div>
</div>
<!-- Main Content -->
<div class="w-full lg:w-3/4 px-4">
<div class="bg-white rounded-lg shadow-md p-6">
{% if messages %}
<div class="messages mb-6">
{% for message in messages %}
<div class="p-4 mb-4 rounded-lg {% if message.tags == 'error' %}bg-red-100 text-red-700{% else %}bg-green-100 text-green-700{% endif %}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
<!-- Article Title -->
{% block wiki_page_header %}
<div class="border-b border-gray-200 pb-4 mb-6">
<h1 class="text-3xl font-bold text-gray-900">
{% block wiki_header_title %}{% endblock %}
</h1>
{% block wiki_header_actions %}{% endblock %}
</div>
{% endblock %}
<!-- Article Content -->
{% block wiki_contents %}
<div class="prose max-w-none">
{% block wiki_content %}{% endblock %}
</div>
{% endblock %}
</div>
</div>
</div>
</div>
<!-- Footer Actions -->
{% block wiki_footer_actions %}
<div class="container mx-auto px-4 py-4">
<div class="flex justify-end space-x-4">
{% if article|can_write:user %}
<a href="{% url 'wiki:edit' article.id %}"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
Edit Article
</a>
{% endif %}
{% if article|can_delete:user %}
<a href="{% url 'wiki:delete' article.id %}"
class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">
Delete Article
</a>
{% endif %}
</div>
</div>
{% endblock %}
{% block wiki_footer %}
{% endblock %}
{% endblock %}
{% block wiki_scripts %}
{% addtoblock "js" %}
<script>
document.addEventListener('DOMContentLoaded', (event) => {
// Add Tailwind classes to wiki-generated content
const wikiContent = document.querySelector('.wiki-article');
if (wikiContent) {
// Add prose styling to article content
wikiContent.classList.add('prose', 'max-w-none');
// Style tables
wikiContent.querySelectorAll('table').forEach(table => {
table.classList.add('min-w-full', 'divide-y', 'divide-gray-200');
});
// Style links
wikiContent.querySelectorAll('a').forEach(link => {
link.classList.add('text-blue-600', 'hover:text-blue-800');
});
}
});
</script>
{% endaddtoblock %}
{% endblock %}

View File

@@ -1,106 +0,0 @@
{% extends "wiki/base.html" %}
{% load wiki_tags %}
{% load static %}
{% block wiki_header_title %}
{{ article.current_revision.title }}
{% endblock %}
{% block wiki_content %}
<article class="park-article">
<!-- Park Header -->
<div class="mb-8">
{% if article.image %}
<div class="mb-4">
<img src="{{ article.image.url }}" alt="{{ article.current_revision.title }}"
class="w-full h-64 object-cover rounded-lg shadow-md">
</div>
{% endif %}
<!-- Park Quick Info -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 bg-gray-50 p-4 rounded-lg">
{% if article.metadata.location %}
<div class="park-info-item">
<span class="text-gray-600 font-medium">Location:</span>
<span class="text-gray-900">{{ article.metadata.location }}</span>
</div>
{% endif %}
{% if article.metadata.opened %}
<div class="park-info-item">
<span class="text-gray-600 font-medium">Opened:</span>
<span class="text-gray-900">{{ article.metadata.opened }}</span>
</div>
{% endif %}
{% if article.metadata.operator %}
<div class="park-info-item">
<span class="text-gray-600 font-medium">Operator:</span>
<span class="text-gray-900">{{ article.metadata.operator }}</span>
</div>
{% endif %}
</div>
</div>
<!-- Park Content -->
<div class="park-content prose max-w-none">
{{ article.render|safe }}
</div>
<!-- Featured Rides -->
{% if article.related_articles.rides %}
<div class="mt-8">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Featured Rides</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for ride in article.related_articles.rides %}
<div class="bg-white rounded-lg shadow-md overflow-hidden">
{% if ride.image %}
<img src="{{ ride.image.url }}" alt="{{ ride.title }}"
class="w-full h-48 object-cover">
{% endif %}
<div class="p-4">
<h3 class="text-lg font-semibold text-gray-900">
<a href="{{ ride.get_absolute_url }}" class="hover:text-blue-600">
{{ ride.title }}
</a>
</h3>
<p class="text-gray-600 text-sm mt-2">
{{ ride.description|truncatewords:30 }}
</p>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Park Stats and Info -->
{% if article.metadata.stats %}
<div class="mt-8 bg-gray-50 rounded-lg p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Park Statistics</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for stat, value in article.metadata.stats.items %}
<div class="stat-item">
<span class="text-gray-600 font-medium">{{ stat|title }}:</span>
<span class="text-gray-900">{{ value }}</span>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</article>
{% endblock %}
{% block wiki_sidebar %}
{{ block.super }}
<!-- Additional park-specific sidebar content -->
<div class="mt-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3">Quick Links</h3>
<ul class="space-y-2">
<li><a href="#rides" class="text-gray-600 hover:text-blue-600">Rides</a></li>
<li><a href="#attractions" class="text-gray-600 hover:text-blue-600">Attractions</a></li>
<li><a href="#dining" class="text-gray-600 hover:text-blue-600">Dining</a></li>
<li><a href="#hotels" class="text-gray-600 hover:text-blue-600">Hotels</a></li>
</ul>
</div>
{% endblock %}

View File

@@ -1,84 +0,0 @@
{% load wiki_tags %}
{% if user.is_authenticated %}
<div class="flex justify-end gap-2 mb-2">
<!-- Wiki Article Actions -->
{% if article|can_write:user %}
<a href="{% url 'wiki:edit' article.id %}"
class="transition-transform btn-secondary hover:scale-105">
<i class="mr-1 fas fa-pencil-alt"></i>Edit Article
</a>
{% endif %}
<!-- Park Metadata Actions -->
{% if park_metadata or article|can_write:user %}
<a href="{% url 'wiki:parks_metadata' article.id %}"
class="transition-transform btn-secondary hover:scale-105">
<i class="mr-1 fas fa-info-circle"></i>Park Info
</a>
{% endif %}
<!-- Statistics Management -->
{% if park_metadata and article|can_write:user %}
<a href="{% url 'wiki:parks_statistics' article.id %}"
class="transition-transform btn-secondary hover:scale-105">
<i class="mr-1 fas fa-chart-bar"></i>Statistics
</a>
{% endif %}
<!-- Media Management -->
{% if article|can_write:user %}
<button class="transition-transform btn-secondary hover:scale-105"
@click="$dispatch('show-wiki-media-upload')">
<i class="mr-1 fas fa-camera"></i>Add Media
</button>
{% endif %}
<!-- Article Tools -->
<div class="dropdown relative inline-block">
<button class="transition-transform btn-secondary hover:scale-105">
<i class="mr-1 fas fa-ellipsis-v"></i>More
</button>
<div class="dropdown-content hidden absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg">
<!-- History -->
<a href="{% url 'wiki:history' article.id %}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="mr-1 fas fa-history"></i>History
</a>
<!-- Discussion -->
<a href="{% url 'wiki:discussion' article.id %}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="mr-1 fas fa-comments"></i>Discussion
</a>
<!-- Settings -->
{% if article|can_moderate:user %}
<a href="{% url 'wiki:settings' article.id %}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="mr-1 fas fa-cog"></i>Settings
</a>
{% endif %}
<!-- Permissions -->
{% if article|can_moderate:user %}
<a href="{% url 'wiki:permissions' article.id %}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i class="mr-1 fas fa-lock"></i>Permissions
</a>
{% endif %}
</div>
</div>
</div>
<!-- Notification Area -->
{% if messages %}
<div class="mt-4">
{% for message in messages %}
<div class="p-4 mb-4 rounded-lg {% if message.tags == 'error' %}bg-red-100 text-red-700{% else %}bg-green-100 text-green-700{% endif %}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}

View File

@@ -1,200 +0,0 @@
{% extends "wiki/article.html" %}
{% load i18n %}
{% load wiki_tags %}
{% load static %}
{% block wiki_contents_tab %}
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-2xl font-bold mb-6">{% trans "Park Metadata" %}</h2>
<form method="POST" class="space-y-6">
{% csrf_token %}
<!-- Basic Information -->
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-4">{% trans "Basic Information" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-group">
{{ form.operator.label_tag }}
{{ form.operator }}
{{ form.operator.errors }}
</div>
<div class="form-group">
{{ form.owner.label_tag }}
{{ form.owner }}
{{ form.owner.errors }}
</div>
<div class="form-group">
{{ form.opened_date.label_tag }}
{{ form.opened_date }}
{{ form.opened_date.errors }}
</div>
<div class="form-group">
{{ form.park_size.label_tag }}
{{ form.park_size }}
{{ form.park_size.errors }}
</div>
</div>
</div>
<!-- Location Information -->
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-4">{% trans "Location" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-group">
{{ form.latitude.label_tag }}
{{ form.latitude }}
{{ form.latitude.errors }}
</div>
<div class="form-group">
{{ form.longitude.label_tag }}
{{ form.longitude }}
{{ form.longitude.errors }}
</div>
<div class="form-group col-span-2">
{{ form.address.label_tag }}
{{ form.address }}
{{ form.address.errors }}
</div>
</div>
</div>
<!-- Operating Information -->
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-4">{% trans "Operating Information" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="form-group">
{{ form.seasonal.label_tag }}
{{ form.seasonal }}
{{ form.seasonal.errors }}
</div>
<div class="form-group">
{{ form.season_start.label_tag }}
{{ form.season_start }}
{{ form.season_start.errors }}
</div>
<div class="form-group">
{{ form.season_end.label_tag }}
{{ form.season_end }}
{{ form.season_end.errors }}
</div>
</div>
</div>
<!-- Attractions -->
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-4">{% trans "Attractions" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-group">
{{ form.total_rides.label_tag }}
{{ form.total_rides }}
{{ form.total_rides.errors }}
</div>
<div class="form-group">
{{ form.total_roller_coasters.label_tag }}
{{ form.total_roller_coasters }}
{{ form.total_roller_coasters.errors }}
</div>
</div>
</div>
<!-- Contact Information -->
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-4">{% trans "Contact Information" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-group">
{{ form.phone.label_tag }}
{{ form.phone }}
{{ form.phone.errors }}
</div>
<div class="form-group">
{{ form.email.label_tag }}
{{ form.email }}
{{ form.email.errors }}
</div>
<div class="form-group">
{{ form.website.label_tag }}
{{ form.website }}
{{ form.website.errors }}
</div>
</div>
</div>
<!-- Social Media -->
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-4">{% trans "Social Media" %}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="form-group">
{{ form.facebook.label_tag }}
{{ form.facebook }}
{{ form.facebook.errors }}
</div>
<div class="form-group">
{{ form.twitter.label_tag }}
{{ form.twitter }}
{{ form.twitter.errors }}
</div>
<div class="form-group">
{{ form.instagram.label_tag }}
{{ form.instagram }}
{{ form.instagram.errors }}
</div>
</div>
</div>
<!-- Additional Information -->
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-4">{% trans "Additional Information" %}</h3>
<div class="space-y-4">
<div class="form-group">
{{ form.amenities_text.label_tag }}
{{ form.amenities_text }}
{{ form.amenities_text.errors }}
<p class="text-sm text-gray-600 mt-1">{{ form.amenities_text.help_text }}</p>
</div>
<div class="form-group">
{{ form.ticket_info_text.label_tag }}
{{ form.ticket_info_text }}
{{ form.ticket_info_text.errors }}
<p class="text-sm text-gray-600 mt-1">{{ form.ticket_info_text.help_text }}</p>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="flex justify-end space-x-4">
<a href="{% url 'wiki:get' path=article.get_absolute_url %}"
class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">
{% trans "Cancel" %}
</a>
<button type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
{% trans "Save Changes" %}
</button>
</div>
</form>
</div>
{% endblock %}
{% block wiki_footer_script %}
{{ block.super }}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handle seasonal checkbox toggling season dates
const seasonalCheckbox = document.getElementById('id_seasonal');
const seasonStartInput = document.getElementById('id_season_start');
const seasonEndInput = document.getElementById('id_season_end');
function toggleSeasonDates() {
const isDisabled = !seasonalCheckbox.checked;
seasonStartInput.disabled = isDisabled;
seasonEndInput.disabled = isDisabled;
}
if (seasonalCheckbox) {
toggleSeasonDates();
seasonalCheckbox.addEventListener('change', toggleSeasonDates);
}
});
</script>
{% endblock %}

View File

@@ -1,146 +0,0 @@
{% extends "wiki/article.html" %}
{% load i18n %}
{% load wiki_tags %}
{% load static %}
{% block wiki_contents_tab %}
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-2xl font-bold mb-6">{% trans "Park Statistics" %}</h2>
<!-- Add New Statistics -->
<div class="mb-8">
<h3 class="text-lg font-semibold mb-4">{% trans "Add New Statistics" %}</h3>
<form method="POST" class="bg-gray-50 p-4 rounded-lg">
{% csrf_token %}
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="form-group">
{{ form.year.label_tag }}
{{ form.year }}
{{ form.year.errors }}
</div>
<div class="form-group">
{{ form.attendance.label_tag }}
{{ form.attendance }}
{{ form.attendance.errors }}
</div>
<div class="form-group">
{{ form.revenue.label_tag }}
{{ form.revenue }}
{{ form.revenue.errors }}
</div>
<div class="form-group">
{{ form.investment.label_tag }}
{{ form.investment }}
{{ form.investment.errors }}
</div>
</div>
<div class="mt-4 flex justify-end">
<button type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
{% trans "Add Statistics" %}
</button>
</div>
</form>
</div>
<!-- Statistics History -->
<div>
<h3 class="text-lg font-semibold mb-4">{% trans "Historical Statistics" %}</h3>
{% if statistics %}
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{% trans "Year" %}
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{% trans "Attendance" %}
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{% trans "Revenue" %}
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{% trans "Investment" %}
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{% trans "Actions" %}
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for stat in statistics %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{{ stat.year }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ stat.attendance|default:"-" }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{% if stat.revenue %}
${{ stat.revenue }}
{% else %}
-
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{% if stat.investment %}
${{ stat.investment }}
{% else %}
-
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<form method="POST" action="{% url 'wiki:parks_delete_statistic' article.id stat.id %}"
class="inline-block">
{% csrf_token %}
<button type="submit"
class="text-red-600 hover:text-red-900"
onclick="return confirm('{% trans "Are you sure you want to delete this statistic?" %}')">
{% trans "Delete" %}
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-gray-500 italic">{% trans "No statistics available." %}</p>
{% endif %}
</div>
<!-- Back to Article -->
<div class="mt-8">
<a href="{% url 'wiki:get' path=article.get_absolute_url %}"
class="inline-block px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">
{% trans "Back to Article" %}
</a>
</div>
</div>
{% endblock %}
{% block wiki_footer_script %}
{{ block.super }}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Auto-fill current year if empty
const yearInput = document.getElementById('id_year');
if (yearInput && !yearInput.value) {
yearInput.value = new Date().getFullYear();
}
// Format number inputs
const numberInputs = document.querySelectorAll('input[type="number"]');
numberInputs.forEach(input => {
input.addEventListener('blur', function() {
if (this.value) {
this.value = parseInt(this.value).toLocaleString();
}
});
});
});
</script>
{% endblock %}

View File

@@ -1,146 +0,0 @@
{% load i18n %}
{% load static %}
<div class="park-sidebar">
<!-- Quick Stats -->
<div class="bg-gray-50 p-4 rounded-lg mb-4">
{% if article.park_metadata %}
<div class="space-y-3">
{% if article.park_metadata.operator %}
<div class="flex justify-between">
<span class="text-sm text-gray-600">{% trans "Operator" %}</span>
<span class="text-sm font-medium">{{ article.park_metadata.operator }}</span>
</div>
{% endif %}
{% if article.park_metadata.opened_date %}
<div class="flex justify-between">
<span class="text-sm text-gray-600">{% trans "Opened" %}</span>
<span class="text-sm font-medium">{{ article.park_metadata.opened_date|date:"Y" }}</span>
</div>
{% endif %}
{% if article.park_metadata.total_rides %}
<div class="flex justify-between">
<span class="text-sm text-gray-600">{% trans "Total Rides" %}</span>
<span class="text-sm font-medium">{{ article.park_metadata.total_rides }}</span>
</div>
{% endif %}
{% if article.park_metadata.total_roller_coasters %}
<div class="flex justify-between">
<span class="text-sm text-gray-600">{% trans "Roller Coasters" %}</span>
<span class="text-sm font-medium">{{ article.park_metadata.total_roller_coasters }}</span>
</div>
{% endif %}
{% if article.park_metadata.park_size %}
<div class="flex justify-between">
<span class="text-sm text-gray-600">{% trans "Size" %}</span>
<span class="text-sm font-medium">{{ article.park_metadata.park_size }} {% trans "acres" %}</span>
</div>
{% endif %}
</div>
<!-- Season Info -->
{% if article.park_metadata.seasonal %}
<div class="mt-4 pt-4 border-t border-gray-200">
<h4 class="text-sm font-medium text-gray-900 mb-2">{% trans "Season" %}</h4>
<div class="text-sm text-gray-600">
{% if article.park_metadata.season_start and article.park_metadata.season_end %}
{{ article.park_metadata.season_start|date:"M j" }} - {{ article.park_metadata.season_end|date:"M j" }}
{% else %}
{% trans "Seasonal operation" %}
{% endif %}
</div>
</div>
{% endif %}
<!-- Contact -->
{% if article.park_metadata.phone or article.park_metadata.email or article.park_metadata.website %}
<div class="mt-4 pt-4 border-t border-gray-200">
<h4 class="text-sm font-medium text-gray-900 mb-2">{% trans "Contact" %}</h4>
<div class="space-y-2">
{% if article.park_metadata.phone %}
<div class="text-sm">
<a href="tel:{{ article.park_metadata.phone }}"
class="text-blue-600 hover:text-blue-800">
{{ article.park_metadata.phone }}
</a>
</div>
{% endif %}
{% if article.park_metadata.website %}
<div class="text-sm">
<a href="{{ article.park_metadata.website }}"
class="text-blue-600 hover:text-blue-800"
target="_blank" rel="noopener">
{% trans "Official Website" %}
</a>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Social Media -->
{% if article.park_metadata.facebook or article.park_metadata.twitter or article.park_metadata.instagram %}
<div class="mt-4 pt-4 border-t border-gray-200">
<h4 class="text-sm font-medium text-gray-900 mb-2">{% trans "Social Media" %}</h4>
<div class="flex space-x-4">
{% if article.park_metadata.facebook %}
<a href="{{ article.park_metadata.facebook }}"
class="text-gray-400 hover:text-blue-600"
target="_blank" rel="noopener">
<i class="fab fa-facebook"></i>
</a>
{% endif %}
{% if article.park_metadata.twitter %}
<a href="{{ article.park_metadata.twitter }}"
class="text-gray-400 hover:text-blue-400"
target="_blank" rel="noopener">
<i class="fab fa-twitter"></i>
</a>
{% endif %}
{% if article.park_metadata.instagram %}
<a href="{{ article.park_metadata.instagram }}"
class="text-gray-400 hover:text-pink-600"
target="_blank" rel="noopener">
<i class="fab fa-instagram"></i>
</a>
{% endif %}
</div>
</div>
{% endif %}
{% else %}
<p class="text-sm text-gray-500 italic">
{% trans "No park metadata available." %}
{% if article|can_write:user %}
<a href="{% url 'wiki:parks_metadata' article.id %}"
class="text-blue-600 hover:text-blue-800">
{% trans "Add metadata" %}
</a>
{% endif %}
</p>
{% endif %}
</div>
<!-- Admin Actions -->
{% if article|can_write:user %}
<div class="space-y-2">
<a href="{% url 'wiki:parks_metadata' article.id %}"
class="block w-full px-4 py-2 text-sm text-center bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200">
{% trans "Edit Park Information" %}
</a>
{% if article.park_metadata %}
<a href="{% url 'wiki:parks_statistics' article.id %}"
class="block w-full px-4 py-2 text-sm text-center bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200">
{% trans "Manage Statistics" %}
</a>
{% endif %}
</div>
{% endif %}
</div>

View File

@@ -1,126 +0,0 @@
{% extends "wiki/base.html" %}
{% load wiki_tags %}
{% load static %}
{% block wiki_header_title %}
{{ article.current_revision.title }}
{% endblock %}
{% block wiki_content %}
<article class="ride-article">
<!-- Ride Header -->
<div class="mb-8">
{% if article.image %}
<div class="mb-4">
<img src="{{ article.image.url }}" alt="{{ article.current_revision.title }}"
class="w-full h-64 object-cover rounded-lg shadow-md">
</div>
{% endif %}
<!-- Ride Quick Info -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 bg-gray-50 p-4 rounded-lg">
{% if article.metadata.park %}
<div class="ride-info-item">
<span class="text-gray-600 font-medium">Park:</span>
<a href="{{ article.metadata.park.get_absolute_url }}"
class="text-blue-600 hover:text-blue-800">
{{ article.metadata.park.name }}
</a>
</div>
{% endif %}
{% if article.metadata.opened %}
<div class="ride-info-item">
<span class="text-gray-600 font-medium">Opened:</span>
<span class="text-gray-900">{{ article.metadata.opened }}</span>
</div>
{% endif %}
{% if article.metadata.manufacturer %}
<div class="ride-info-item">
<span class="text-gray-600 font-medium">Manufacturer:</span>
<span class="text-gray-900">{{ article.metadata.manufacturer }}</span>
</div>
{% endif %}
{% if article.metadata.type %}
<div class="ride-info-item">
<span class="text-gray-600 font-medium">Type:</span>
<span class="text-gray-900">{{ article.metadata.type }}</span>
</div>
{% endif %}
</div>
</div>
<!-- Ride Content -->
<div class="ride-content prose max-w-none">
{{ article.render|safe }}
</div>
<!-- Technical Specifications -->
{% if article.metadata.specs %}
<div class="mt-8 bg-gray-50 rounded-lg p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Technical Specifications</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for spec, value in article.metadata.specs.items %}
<div class="spec-item">
<span class="text-gray-600 font-medium">{{ spec|title }}:</span>
<span class="text-gray-900">{{ value }}</span>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Records and Statistics -->
{% if article.metadata.records %}
<div class="mt-8">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Records & Achievements</h2>
<div class="bg-white rounded-lg shadow-md p-6">
<ul class="space-y-3">
{% for record in article.metadata.records %}
<li class="flex items-start">
<svg class="w-6 h-6 text-blue-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>{{ record }}</span>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</article>
{% endblock %}
{% block wiki_sidebar %}
{{ block.super }}
<!-- Additional ride-specific sidebar content -->
<div class="mt-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3">Quick Links</h3>
<ul class="space-y-2">
<li><a href="#specifications" class="text-gray-600 hover:text-blue-600">Specifications</a></li>
<li><a href="#history" class="text-gray-600 hover:text-blue-600">History</a></li>
<li><a href="#experience" class="text-gray-600 hover:text-blue-600">Ride Experience</a></li>
<li><a href="#records" class="text-gray-600 hover:text-blue-600">Records</a></li>
</ul>
</div>
<!-- Related Rides -->
{% if article.related_articles.similar_rides %}
<div class="mt-6">
<h3 class="text-lg font-semibold text-gray-900 mb-3">Similar Rides</h3>
<ul class="space-y-2">
{% for ride in article.related_articles.similar_rides %}
<li>
<a href="{{ ride.get_absolute_url }}"
class="text-gray-600 hover:text-blue-600">
{{ ride.title }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,310 @@
# ThrillWiki Next.js Development Prompt
## Project Overview
Build a comprehensive theme park and ride database platform using Next.js. ThrillWiki is a community-driven platform where enthusiasts can discover parks, explore rides, share reviews, and contribute to a moderated knowledge base about theme parks worldwide.
## Core Application Domain
### Primary Entities
- **Parks**: Theme parks, amusement parks, water parks with detailed information, locations, operating details
- **Rides**: Individual ride installations with technical specifications, manufacturer details, operational status
- **Companies**: Manufacturers, operators, designers, property owners with different roles
- **Users**: Community members with profiles, preferences, reviews, and top lists
- **Reviews**: User-generated content with ratings, media, and moderation workflow
- **Locations**: Geographic data for parks and rides with PostGIS-style coordinate handling
### Key Relationships
- Parks contain multiple rides and are operated by companies
- Rides belong to parks, have manufacturers/designers, and reference ride models
- Ride models are templates created by manufacturers with technical specifications
- Users create reviews for parks and rides, maintain top lists, have notification preferences
- Companies have multiple roles (manufacturer, operator, designer, property owner)
- All content goes through moderation workflows before publication
## User Personas & Workflows
### Theme Park Enthusiasts
- Browse and discover parks by location, type, and features
- Search for specific rides and view detailed technical specifications
- Plan park visits with operating information and ride availability
- Track personal ride credits and maintain top lists
- Read authentic reviews from other enthusiasts
### Content Contributors
- Submit new park and ride information for moderation
- Upload photos with proper attribution and categorization
- Write detailed reviews with ratings and media attachments
- Maintain personal profiles with ride statistics and achievements
- Participate in community discussions and rankings
### Park Industry Professionals
- Maintain verified company profiles with official information
- Update park operating details, ride status, and announcements
- Access analytics and engagement metrics for their properties
- Manage official media and promotional content
## Core Features to Implement
### 1. Park Discovery & Information System
- **Park Listings**: Filterable grid/list views with search, location-based filtering, park type categories
- **Park Detail Pages**: Comprehensive information including rides list, operating hours, location maps, photo galleries
- **Interactive Maps**: Geographic visualization of parks with clustering, zoom controls, and location-based search
- **Advanced Search**: Multi-criteria search across parks, rides, locations, and companies
### 2. Ride Database & Technical Specifications
- **Ride Catalog**: Comprehensive database with manufacturer information, technical specs, operational history
- **Ride Detail Pages**: In-depth information including statistics, photos, reviews, and related rides
- **Manufacturer Profiles**: Company information with ride model catalogs and installation history
- **Technical Comparisons**: Side-by-side ride comparisons with filterable specifications
### 3. User-Generated Content System
- **Review Platform**: Structured review forms with ratings, text, photo uploads, and helpful voting
- **Photo Management**: Upload system with automatic optimization, categorization, and attribution tracking
- **Top Lists**: Personal ranking systems for rides, parks, and experiences with drag-and-drop reordering
- **User Profiles**: Personal statistics, ride credits, achievement tracking, and social features
### 4. Content Moderation Workflow
- **Submission Queue**: Administrative interface for reviewing user-submitted content
- **Approval Process**: Multi-stage review with rejection reasons, feedback, and resubmission options
- **Quality Control**: Automated checks for duplicate content, inappropriate material, and data validation
- **User Management**: Moderation tools for user accounts, banning, and content removal
### 5. Location & Geographic Services
- **Location Search**: Address-based search with autocomplete and geographic boundaries
- **Proximity Features**: Find nearby parks, distance calculations, and regional groupings
- **Map Integration**: Interactive maps with custom markers, clustering, and detailed overlays
- **Geographic Filtering**: Location-based content filtering and discovery
## Technical Architecture Requirements
### Frontend Stack
- **Next.js 14+** with App Router for modern React development
- **TypeScript** for type safety and better developer experience
- **Tailwind CSS** for utility-first styling and responsive design
- **Shadcn/ui** for consistent, accessible component library
- **React Hook Form** with Zod validation for form handling
- **TanStack Query** for server state management and caching
- **Zustand** for client-side state management
- **Next-Auth** for authentication and session management
### Data Management
- **API Integration**: RESTful API consumption with comprehensive error handling
- **Image Optimization**: Cloudflare Images integration with multiple variants
- **Caching Strategy**: Multi-level caching with SWR patterns and cache invalidation
- **Search Implementation**: Client-side and server-side search with debouncing
- **Infinite Scrolling**: Performance-optimized pagination for large datasets
### UI/UX Patterns
- **Responsive Design**: Mobile-first approach with tablet and desktop optimizations
- **Dark/Light Themes**: User preference-based theming with system detection
- **Loading States**: Skeleton screens, progressive loading, and optimistic updates
- **Error Boundaries**: Graceful error handling with user-friendly messages
- **Accessibility**: WCAG compliance with keyboard navigation and screen reader support
## Key Components to Build
### Layout & Navigation
- **Header Component**: Logo, navigation menu, user authentication, search bar, theme toggle
- **Sidebar Navigation**: Collapsible menu with user context and quick actions
- **Footer Component**: Links, social media, legal information, and site statistics
- **Breadcrumb Navigation**: Contextual navigation with proper hierarchy
### Park Components
- **ParkCard**: Compact park information with image, basic details, and quick actions
- **ParkGrid**: Responsive grid layout with filtering and sorting options
- **ParkDetail**: Comprehensive park information with tabbed sections
- **ParkMap**: Interactive map showing park location and nearby attractions
- **OperatingHours**: Dynamic display of park hours with seasonal variations
### Ride Components
- **RideCard**: Ride preview with key specifications and ratings
- **RideList**: Filterable list with sorting and search capabilities
- **RideDetail**: Complete ride information with technical specifications
- **RideStats**: Visual representation of ride statistics and comparisons
- **RidePhotos**: Photo gallery with lightbox and attribution information
### User Interface Components
- **UserProfile**: Personal information, statistics, and preference management
- **ReviewForm**: Structured review creation with rating inputs and media upload
- **TopListManager**: Drag-and-drop interface for creating and managing rankings
- **NotificationCenter**: Real-time notifications with read/unread states
- **SearchInterface**: Advanced search with filters, suggestions, and recent searches
### Content Management
- **SubmissionForm**: Content submission interface with validation and preview
- **ModerationQueue**: Administrative interface for content review and approval
- **PhotoUpload**: Drag-and-drop photo upload with progress tracking and optimization
- **ContentEditor**: Rich text editor for descriptions and review content
## Data Models & API Integration
### API Endpoints Structure
```typescript
// Parks API
GET /api/parks/ - List parks with filtering and pagination
GET /api/parks/{slug}/ - Park details with rides and reviews
GET /api/parks/{slug}/rides/ - Rides at specific park
GET /api/parks/search/ - Advanced park search
// Rides API
GET /api/rides/ - List rides with filtering
GET /api/rides/{park_slug}/{ride_slug}/ - Ride details
GET /api/rides/manufacturers/{slug}/ - Manufacturer information
GET /api/rides/search/ - Advanced ride search
// User API
GET /api/users/profile/ - Current user profile
PUT /api/users/profile/ - Update user profile
GET /api/users/{username}/ - Public user profile
POST /api/users/reviews/ - Create review
// Content API
POST /api/submissions/ - Submit new content
GET /api/submissions/status/ - Check submission status
POST /api/photos/upload/ - Upload photos
GET /api/moderation/queue/ - Moderation queue (admin)
```
### TypeScript Interfaces
```typescript
interface Park {
id: number;
name: string;
slug: string;
description: string;
status: string;
park_type: string;
location: ParkLocation;
operator: Company;
opening_date: string;
ride_count: number;
coaster_count: number;
average_rating: number;
banner_image: Photo;
card_image: Photo;
url: string;
}
interface Ride {
id: number;
name: string;
slug: string;
description: string;
park: Park;
category: string;
manufacturer: Company;
ride_model: RideModel;
status: string;
opening_date: string;
height_requirement: number;
average_rating: number;
coaster_stats?: RollerCoasterStats;
}
interface User {
id: number;
username: string;
display_name: string;
profile: UserProfile;
role: string;
theme_preference: string;
privacy_level: string;
}
```
## Authentication & User Management
### Authentication Flow
- **Registration**: Email-based signup with verification workflow
- **Login**: Username/email login with remember me option
- **Social Auth**: Integration with Google, Facebook, Discord for quick signup
- **Password Reset**: Secure password reset with email verification
- **Two-Factor Auth**: Optional 2FA with authenticator app support
### User Roles & Permissions
- **Regular Users**: Create reviews, manage profiles, submit content
- **Verified Contributors**: Trusted users with expedited content approval
- **Moderators**: Content review, user management, community oversight
- **Administrators**: Full system access, user role management, system configuration
### Privacy & Security
- **Privacy Controls**: Granular privacy settings for profile visibility and data sharing
- **Content Moderation**: Automated and manual content review processes
- **Data Protection**: GDPR compliance with data export and deletion options
- **Security Features**: Login notifications, session management, suspicious activity detection
## Performance & Optimization
### Loading & Caching
- **Image Optimization**: Cloudflare Images with responsive variants and lazy loading
- **API Caching**: Intelligent caching with stale-while-revalidate patterns
- **Static Generation**: Pre-generated pages for popular content with ISR
- **Code Splitting**: Route-based and component-based code splitting for optimal loading
### Search & Filtering
- **Client-Side Search**: Fast text search with debouncing and result highlighting
- **Server-Side Filtering**: Complex filtering with URL state management
- **Infinite Scrolling**: Performance-optimized pagination for large datasets
- **Search Analytics**: Track popular searches and optimize content discovery
### Mobile Optimization
- **Responsive Design**: Mobile-first approach with touch-friendly interfaces
- **Progressive Web App**: PWA features with offline capability and push notifications
- **Performance Budgets**: Strict performance monitoring with Core Web Vitals tracking
- **Accessibility**: Full keyboard navigation and screen reader compatibility
## Content Management & Moderation
### Submission Workflow
- **Content Forms**: Structured forms for parks, rides, and reviews with validation
- **Draft System**: Save and resume content creation with auto-save functionality
- **Preview Mode**: Real-time preview of content before submission
- **Submission Tracking**: Status updates and feedback throughout review process
### Moderation Interface
- **Review Queue**: Prioritized queue with filtering and batch operations
- **Approval Workflow**: Multi-stage review with detailed feedback options
- **Quality Metrics**: Track approval rates, review times, and content quality
- **User Reputation**: Contributor scoring system based on submission quality
### Media Management
- **Photo Upload**: Drag-and-drop interface with progress tracking and error handling
- **Image Processing**: Automatic optimization, resizing, and format conversion
- **Attribution Tracking**: Photographer credits and copyright information
- **Bulk Operations**: Batch photo management and organization tools
## Analytics & Insights
### User Analytics
- **Engagement Tracking**: Page views, time on site, and user journey analysis
- **Content Performance**: Popular parks, rides, and reviews with engagement metrics
- **Search Analytics**: Popular search terms and content discovery patterns
- **User Behavior**: Registration funnels, retention rates, and feature adoption
### Content Insights
- **Submission Metrics**: Content creation rates, approval times, and quality scores
- **Review Analytics**: Rating distributions, helpful votes, and review engagement
- **Geographic Data**: Popular regions, park visit patterns, and location-based insights
- **Trending Content**: Real-time trending parks, rides, and discussions
## Development Guidelines
### Code Organization
- **Feature-Based Structure**: Organize code by features rather than file types
- **Component Library**: Reusable components with Storybook documentation
- **Custom Hooks**: Shared logic extraction with proper TypeScript typing
- **Utility Functions**: Common operations with comprehensive testing
### Testing Strategy
- **Unit Testing**: Component and utility function testing with Jest and React Testing Library
- **Integration Testing**: API integration and user workflow testing
- **E2E Testing**: Critical user journeys with Playwright or Cypress
- **Performance Testing**: Core Web Vitals monitoring and performance regression testing
### Deployment & DevOps
- **Environment Management**: Development, staging, and production environments
- **CI/CD Pipeline**: Automated testing, building, and deployment
- **Monitoring**: Error tracking, performance monitoring, and user analytics
- **Security**: Regular security audits, dependency updates, and vulnerability scanning
This comprehensive platform should provide theme park enthusiasts with a rich, engaging experience while maintaining high content quality through effective moderation and community management systems.

View File

@@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application
os***REMOVED***iron.setdefault("DJANGO_SETTINGS_MODULE", "thrillwiki.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "thrillwiki.settings")
application = get_asgi_application()

View File

@@ -1,120 +1,228 @@
from django.conf import settings as django_settings
import os
"""
Django settings for thrillwiki project.
"""
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = "django-insecure-=0)^0#h#k$0@$8$ys=^$0#h#k$0@$8$ys=^"
# Quick-start development settings - unsuitable for production
SECRET_KEY = 'django-insecure-key-for-development'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
CSRF_TRUSTED_ORIGINS = ["https://beta.thrillwiki.com"]
ALLOWED_HOSTS = ["*"]
# GeoDjango Settings
GDAL_LIBRARY_PATH = "/opt/homebrew/lib/libgdal.dylib"
GEOS_LIBRARY_PATH = "/opt/homebrew/lib/libgeos_c.dylib"
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites.apps.SitesConfig',
'django.contrib.humanize.apps.HumanizeConfig',
'django_nyt.apps.DjangoNytConfig',
'mptt',
'sorl.thumbnail',
'wiki.apps.WikiConfig', # Main wiki app
'wiki.plugins.parks.apps.ParksPluginConfig', # Parks plugin
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.sites",
"django.contrib.gis", # Add GeoDjango
"pghistory", # Add django-pghistory
"pgtrigger", # Required by django-pghistory
"history.apps.HistoryConfig", # History timeline app
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.google",
"allauth.socialaccount.providers.discord",
"django_cleanup",
"django_filters",
"django_htmx",
"whitenoise",
"django_tailwind_cli",
"autocomplete", # Django HTMX Autocomplete
"core",
"accounts",
"companies",
"parks",
"rides",
"reviews",
"email_service",
"media.apps.MediaConfig",
"moderation",
"history_tracking",
"designers",
"analytics",
"location",
"search.apps.SearchConfig", # Add search app
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"django.middleware.cache.UpdateCacheMiddleware",
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"core.middleware.PgHistoryContextMiddleware", # Add history context tracking
"allauth.account.middleware.AccountMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
"django_htmx.middleware.HtmxMiddleware",
"analytics.middleware.PageViewMiddleware", # Add our page view tracking
]
ROOT_URLCONF = 'thrillwiki.urls'
ROOT_URLCONF = "thrillwiki.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
],
},
},
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(BASE_DIR, "templates")],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"moderation.context_processors.moderation_access",
]
}
}
]
WSGI_APPLICATION = 'thrillwiki.wsgi.application'
WSGI_APPLICATION = "thrillwiki.wsgi.application"
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'thrillwiki',
'USER': 'postgres',
'PASSWORD': 'postgres',
'HOST': 'localhost',
'PORT': '5432',
"default": {
"ENGINE": "django.contrib.gis.db.backends.postgis", # Update to use PostGIS
"NAME": "thrillwiki",
"USER": "wiki",
"PASSWORD": "thrillwiki",
"HOST": "192.168.86.3",
"PORT": "5432",
}
}
# Cache settings
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake",
"TIMEOUT": 300, # 5 minutes
"OPTIONS": {"MAX_ENTRIES": 1000},
}
}
CACHE_MIDDLEWARE_SECONDS = 1 # 5 minutes
CACHE_MIDDLEWARE_KEY_PREFIX = "thrillwiki"
# Password validation
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Wiki settings
WIKI_ACCOUNT_HANDLING = True
WIKI_ACCOUNT_SIGNUP_ALLOWED = True
WIKI_ANONYMOUS = True
WIKI_ANONYMOUS_WRITE = False
WIKI_MARKDOWN_HTML_ATTRIBUTES = True
WIKI_MARKDOWN_HTML_STYLES = True
SITE_ID = 1
# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
LANGUAGE_CODE = "en-us"
TIME_ZONE = "America/New_York"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
# Static files (CSS JavaScript Images)
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'uploads'
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Authentication settings
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
]
# django-allauth settings
SITE_ID = 1
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = True
ACCOUNT_LOGIN_METHODS = {'email', 'username'}
ACCOUNT_EMAIL_VERIFICATION = "optional"
LOGIN_REDIRECT_URL = "/"
ACCOUNT_LOGOUT_REDIRECT_URL = "/"
# Custom adapters
ACCOUNT_ADAPTER = "accounts.adapters.CustomAccountAdapter"
SOCIALACCOUNT_ADAPTER = "accounts.adapters.CustomSocialAccountAdapter"
# Social account settings
SOCIALACCOUNT_PROVIDERS = {
"google": {
"APP": {
"client_id": "135166769591-nopcgmo0fkqfqfs9qe783a137mtmcrt2.apps.googleusercontent.com",
"secret": "[SECRET-REMOVED]",
"key": "",
},
"SCOPE": [
"profile",
"email",
],
"AUTH_PARAMS": {"access_type": "online"},
},
"discord": {
"APP": {
"client_id": "1299112802274902047",
"secret": "[SECRET-REMOVED]",
"key": "",
},
"SCOPE": ["identify", "email"],
"OAUTH_PKCE_ENABLED": True,
}
}
# Additional social account settings
SOCIALACCOUNT_LOGIN_ON_GET = True
SOCIALACCOUNT_AUTO_SIGNUP = False
SOCIALACCOUNT_STORE_TOKENS = True
# Email settings
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_BACKEND = "email_service.backends.ForwardEmailBackend"
FORWARD_EMAIL_BASE_URL = "https://api.forwardemail.net"
SERVER_EMAIL = "django_webmaster@thrillwiki.com"
# Custom User Model
AUTH_USER_MODEL = "accounts.User"
# Autocomplete configuration
# Enable project-wide authentication requirement for autocomplete
AUTOCOMPLETE_BLOCK_UNAUTHENTICATED = False
# Tailwind configuration
# Tailwind configuration
TAILWIND_CLI_CONFIG_FILE = os.path.join(BASE_DIR, "tailwind.config.js")
TAILWIND_CLI_SRC_CSS = os.path.join(BASE_DIR, "static/css/src/input.css")
TAILWIND_CLI_DIST_CSS = os.path.join(BASE_DIR, "static/css/tailwind.css")
# Cloudflare Turnstile settings
TURNSTILE_SITE_KEY = "0x4AAAAAAAyqVp3RjccrC9Kz"
TURNSTILE_SECRET_KEY = "0x4AAAAAAAyqVrQolYsrAFGJ39PXHJ_HQzY"
TURNSTILE_VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"

View File

@@ -1,6 +1,82 @@
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from django.views.static import serve
from accounts import views as accounts_views
from django.views.generic import TemplateView
from .views import HomeView, SearchView
from . import views
import os
urlpatterns = [
path('admin/', admin.site.urls),
path("admin/", admin.site.urls),
# Main app URLs
path("", HomeView.as_view(), name="home"),
# Parks and Rides URLs
path("parks/", include("parks.urls", namespace="parks")),
# Global rides URLs
path("rides/", include("rides.urls", namespace="rides")),
# Other URLs
path("reviews/", include("reviews.urls")),
path("companies/", include("companies.urls")),
path("designers/", include("designers.urls", namespace="designers")),
path("photos/", include("media.urls", namespace="photos")), # Add photos URLs
path("search/", SearchView.as_view(), name="search"),
path(
"terms/", TemplateView.as_view(template_name="pages/terms.html"), name="terms"
),
path(
"privacy/",
TemplateView.as_view(template_name="pages/privacy.html"),
name="privacy",
),
# Custom authentication URLs first (to override allauth defaults)
path("accounts/", include("accounts.urls")),
# Default allauth URLs (for social auth and other features)
path("accounts/", include("allauth.urls")),
path(
"accounts/email-required/", accounts_views.email_required, name="email_required"
),
# User profile URLs
path(
"user/<str:username>/",
accounts_views.ProfileView.as_view(),
name="user_profile",
),
path(
"profile/<str:username>/", accounts_views.ProfileView.as_view(), name="profile"
),
path("settings/", accounts_views.SettingsView.as_view(), name="settings"),
# Redirect /user/ to the user's profile if logged in
path("user/", accounts_views.user_redirect_view, name="user_redirect"),
# Moderation URLs - placed after other URLs but before static/media serving
path("moderation/", include("moderation.urls", namespace="moderation")),
path("history/", include("history.urls", namespace="history")),
path(
"env-settings/",
views.environment_and_settings_view,
name="environment_and_settings",
),
]
# Serve static files in development
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Serve test coverage reports in development
coverage_dir = os.path.join(settings.BASE_DIR, 'tests', 'coverage_html')
if os.path.exists(coverage_dir):
urlpatterns += [
path('coverage/', serve, {
'document_root': coverage_dir,
'path': 'index.html'
}),
path('coverage/<path:path>', serve, {
'document_root': coverage_dir,
}),
]
handler404 = "thrillwiki.views.handler404"
handler500 = "thrillwiki.views.handler500"

View File

@@ -127,7 +127,7 @@ class SearchView(TemplateView):
def environment_and_settings_view(request):
# Get all environment variables
env_vars = dict(os***REMOVED***iron)
env_vars = dict(os.environ)
# Get all Django settings as a dictionary
settings_vars = {setting: getattr(settings, setting) for setting in dir(settings) if setting.isupper()}

View File

@@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os***REMOVED***iron.setdefault("DJANGO_SETTINGS_MODULE", "thrillwiki.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "thrillwiki.settings")
application = get_wsgi_application()

1127
uv.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
default_app_config = "wiki.apps.WikiConfig"

View File

@@ -1,11 +0,0 @@
from django.apps import AppConfig
class WikiConfig(AppConfig):
name = 'wiki'
verbose_name = 'Wiki'
def ready(self):
"""
Register signals and perform other initialization
"""
pass

View File

@@ -1 +0,0 @@
default_app_config = "wiki.plugins.parks.apps.ParksPluginConfig"

View File

@@ -1,13 +0,0 @@
from django.apps import AppConfig
class ParksPluginConfig(AppConfig):
name = "wiki.plugins.parks"
label = "wiki_parks"
verbose_name = "Wiki Parks Plugin"
def ready(self):
"""
Register plugin with wiki system when the app is ready.
Plugin registration is deferred until wiki core is available.
"""
pass

View File

@@ -1,34 +0,0 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class ParkMetadata(models.Model):
article = models.OneToOneField(
'wiki.Article', # Using string reference to avoid import issues
on_delete=models.CASCADE,
related_name='park_metadata'
)
operator = models.CharField(
max_length=255,
verbose_name=_('Operator'),
blank=True
)
opened_date = models.DateField(
verbose_name=_('Opening Date'),
null=True,
blank=True
)
location = models.CharField(
max_length=255,
verbose_name=_('Location'),
blank=True
)
class Meta:
verbose_name = _('Park Metadata')
verbose_name_plural = _('Park Metadata')
def __str__(self):
return f"Park info for {self.article.current_revision.title}"

View File

@@ -1,14 +0,0 @@
from django.utils.translation import gettext as _
class ParksPlugin:
"""
Plugin for handling parks in the wiki system.
Core registration will be added later.
"""
slug = 'parks'
sidebar = {
'headline': _('Park Information'),
'icon_class': 'fa-info-circle',
'template': 'wiki/plugins/parks/sidebar.html',
}