Compare commits

...

150 Commits

Author SHA1 Message Date
pacnpal
8900716215 Refactor migrations across multiple apps to remove and add triggers for event logging, ensuring consistency in data handling. Updated park and ride event tables, added new columns, and optimized user-generated content workflows. Enhanced moderation processes and improved overall database integrity. 2025-09-20 09:46:09 -04:00
pacnpal
401449201c Fix search form duplication by updating event handler to submit the correct filter form and return JSON responses for park suggestions 2025-02-23 12:05:26 -05:00
pacnpal
1ca1362fee Implement park search suggestions with HTMX integration: replace legacy redirect with real-time suggestions and enhance UI for better user experience 2025-02-23 10:50:25 -05:00
pacnpal
02e4b82beb Allow unauthenticated access for autocomplete functionality 2025-02-22 15:17:49 -05:00
pacnpal
4339c5c5e0 Add autocomplete functionality for parks: implement BaseAutocomplete class and integrate with forms 2025-02-22 13:36:24 -05:00
pacnpal
5278ad39d0 Refactor imports and improve code organization: streamline import statements and enhance readability in parks/views.py 2025-02-21 20:37:03 -05:00
pacnpal
4d145ebabe Implement park search suggestions: add autocomplete functionality and improve search input handling 2025-02-21 20:36:12 -05:00
pacnpal
e4959b7a04 Improve address formatting in location widget: enhance address display logic and ensure fallback for missing fields 2025-02-21 20:20:00 -05:00
pacnpal
ef2437b7f4 2025-02-21 19:14:26 -05:00
pacnpal
3523274cbd Refactor error message handling: centralize required fields error message and improve park list template accessibility 2025-02-21 18:55:41 -05:00
pacnpal
d7951756dc Enhance park search functionality: update view mode handling and improve park list item layout 2025-02-21 18:52:01 -05:00
pacnpal
518fcbee22 Add custom development modes and guidelines for ThrillWiki project 2025-02-21 18:28:36 -05:00
pacnpal
41f1738cc1 Add migrations to alter primary key fields to BigAutoField for multiple models 2025-02-21 12:55:21 -05:00
pacnpal
645a74a4c3 Implement search functionality improvements: optimize database queries, enhance service layer, and update frontend interactions 2025-02-21 10:31:49 -05:00
pacnpal
8c85b2afd4 Update .clinerules: add guidelines for using UV with Django management commands 2025-02-19 11:13:21 -05:00
pacnpal
063398d220 Refactor development server startup instructions for clarity and conciseness 2025-02-19 09:59:39 -05:00
pacnpal
20ae4862e4 Add development server and package management guidelines to documentation 2025-02-19 09:56:23 -05:00
pacnpal
5541a5f02d Refactor park queryset logic: move base queryset to a dedicated module for improved organization and maintainability 2025-02-19 09:30:17 -05:00
pacnpal
78f465b273 Analyze feasibility of migrating from Django to Laravel; recommend maintaining current implementation due to high risks and costs 2025-02-18 10:43:13 -05:00
pacnpal
0b51ee123a Add comprehensive system architecture and feature documentation for ThrillWiki 2025-02-18 10:08:46 -05:00
pacnpal
c19aaf2f4b Implement ride count fields with real-time annotations; update filters and templates for consistency and accuracy 2025-02-13 16:44:30 -05:00
pacnpal
9d6f6dab2c Refactor park listing templates: implement grid and list view modes, enhance search results rendering, and improve error handling in search functionality 2025-02-13 12:19:23 -05:00
pacnpal
bba707fa98 Refactor search functionality: remove obsolete JavaScript and HTML templates; enhance error handling and response rendering for park search results 2025-02-13 10:29:57 -05:00
pacnpal
c197051b25 Enhance search functionality with loading indicators, dark mode support, and improved UI; implement event handling for search results and refine park filter tests for better coverage 2025-02-13 09:42:58 -05:00
pacnpal
1fe299fb4b Integrate parks app with site-wide search system; add filter configuration, error handling, and search interfaces 2025-02-12 16:59:20 -05:00
pacnpal
af57592496 Add search app configuration, views, and templates for advanced filtering functionality 2025-02-12 12:58:04 -05:00
pacnpal
62723d0e33 Implement site-wide search system architecture with modular design and enhanced backend components; integrate django-filter for improved filtering capabilities and security considerations 2025-02-12 12:41:35 -05:00
pacnpal
f5c063b76f Refactor ride list and search suggestion templates for improved structure and styling; update view logic to support dynamic template rendering based on request type 2025-02-11 15:24:14 -05:00
pacnpal
59efc39143 Add search suggestions feature with category filtering and display; enhance ride list view with improved search functionality 2025-02-11 13:41:05 -05:00
pacnpal
2e0d32819a Add views for ride management including detail, creation, and listing; implement search functionality for manufacturers, designers, and ride models 2025-02-11 10:41:40 -05:00
pacnpal
6034227796 Implement RideUpdateView and refactor search suggestion handling for improved modularity and error management 2025-02-10 23:35:58 -05:00
pacnpal
fb6c6ec37b Refactor cleanup logic in search script for improved readability and maintainability 2025-02-10 23:30:34 -05:00
pacnpal
99b935da19 Enhance search suggestions with request tracking and cleanup handlers 2025-02-10 22:10:11 -05:00
pacnpal
2756079010 Add search suggestions feature with improved filtering and UI 2025-02-10 21:27:59 -05:00
pacnpal
5195c234c6 Merge pull request #27 from pacnpal/dependabot/pip/whitenoise-6.9.0
[DEPENDABOT] Update: Bump whitenoise from 6.8.2 to 6.9.0
2025-02-10 17:29:22 -05:00
pacnpal
c861d4f6ae Merge branch 'main' into dependabot/pip/whitenoise-6.9.0 2025-02-10 17:19:54 -05:00
pacnpal
ac71e5f047 Merge pull request #28 from pacnpal/dependabot/pip/django-htmx-1.22.0
[DEPENDABOT] Update: Bump django-htmx from 1.21.0 to 1.22.0
2025-02-10 17:19:15 -05:00
pacnpal
bdbb864cef Merge pull request #29 from pacnpal/dependabot/pip/django-allauth-65.4.1
[DEPENDABOT] Update: Bump django-allauth from 65.4.0 to 65.4.1
2025-02-10 17:19:04 -05:00
pacnpal
b46e13426a Merge pull request #26 from pacnpal/dependabot/github_actions/actions/setup-python-5
[DEPENDABOT] Update Actions: Bump actions/setup-python from 4 to 5
2025-02-10 17:18:53 -05:00
pacnpal
39c8fe2c57 Merge pull request #30 from pacnpal/dependabot/pip/django-tailwind-cli-4.0.1
[DEPENDABOT] Update: Bump django-tailwind-cli from 2.21.1 to 4.0.1
2025-02-10 17:18:27 -05:00
pacnpal
c27f320a49 Merge pull request #31 from pacnpal/dependabot/pip/pytest-django-4.10.0
[DEPENDABOT] Update: Bump pytest-django from 4.9.0 to 4.10.0
2025-02-10 17:16:37 -05:00
pacnpal
f4c6cd99db Merge pull request #32 from pacnpal/dependabot/pip/django-pghistory-3.5.2
[DEPENDABOT] Update: Bump django-pghistory from 2.9.0 to 3.5.2
2025-02-10 17:16:13 -05:00
pacnpal
467f7ba3f8 Merge pull request #37 from pacnpal/pacnpal-patch-3
Update README.md
2025-02-10 17:11:34 -05:00
pacnpal
d4a1f88644 Update README.md 2025-02-10 17:10:13 -05:00
pacnpal
369c5e698e Merge pull request #35 from pacnpal/pacnpal-patch-2
Create README.md
2025-02-10 16:31:52 -05:00
pacnpal
19b7aee707 Create README.md 2025-02-10 16:20:31 -05:00
pacnpal
a09fd66d70 Merge pull request #33 from pacnpal/dependabot/pip/django-cors-headers-4.7.0
[DEPENDABOT] Update: Bump django-cors-headers from 4.6.0 to 4.7.0
2025-02-10 16:12:25 -05:00
pacnpal
910762722e Update review.yml 2025-02-10 15:03:59 -05:00
pacnpal
79e34473a4 Update review.yml 2025-02-10 15:02:47 -05:00
pacnpal
872f3378a1 Update review.yml 2025-02-10 14:59:00 -05:00
pacnpal
df91eb97b8 Add ParkContextRequired mixin to enforce park context in ride views; update URLs and templates for global ride listing 2025-02-10 14:48:29 -05:00
pacnpal
ad33332506 Update review.yml 2025-02-10 13:37:13 -05:00
pacnpal
69cdb0a554 Update review.yml 2025-02-10 13:36:58 -05:00
pacnpal
d2b6b712bf Update review.yml 2025-02-10 13:36:34 -05:00
pacnpal
1784644a52 Update review.yml 2025-02-10 13:36:19 -05:00
pacnpal
3c40a32925 Update review.yml 2025-02-10 13:36:06 -05:00
pacnpal
29392f0de1 Update review.yml 2025-02-10 13:35:29 -05:00
pacnpal
d0bd0e1bf9 Update review.yml 2025-02-10 13:35:03 -05:00
pacnpal
11e643a47a Update review.yml 2025-02-10 13:32:21 -05:00
pacnpal
db78de4cfe Update review.yml 2025-02-10 13:31:16 -05:00
pacnpal
4a495182bd Update review.yml 2025-02-10 13:28:58 -05:00
pacnpal
2add4c7fc2 Add links to ride names in ride list template for improved navigation 2025-02-10 12:45:45 -05:00
dependabot[bot]
f1c37f2bc1 [DEPENDABOT] Update: Bump django-cors-headers from 4.6.0 to 4.7.0
Bumps [django-cors-headers](https://github.com/adamchainz/django-cors-headers) from 4.6.0 to 4.7.0.
- [Changelog](https://github.com/adamchainz/django-cors-headers/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/adamchainz/django-cors-headers/compare/4.6.0...4.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 16:50:51 +00:00
dependabot[bot]
1c71ad9b6b [DEPENDABOT] Update: Bump django-pghistory from 2.9.0 to 3.5.2
Bumps [django-pghistory](https://github.com/AmbitionEng/django-pghistory) from 2.9.0 to 3.5.2.
- [Release notes](https://github.com/AmbitionEng/django-pghistory/releases)
- [Changelog](https://github.com/AmbitionEng/django-pghistory/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AmbitionEng/django-pghistory/compare/2.9.0...3.5.2)

---
updated-dependencies:
- dependency-name: django-pghistory
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 16:50:38 +00:00
dependabot[bot]
7e8c40db0d [DEPENDABOT] Update: Bump pytest-django from 4.9.0 to 4.10.0
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.9.0 to 4.10.0.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v4.9.0...v4.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 16:50:33 +00:00
dependabot[bot]
7211c17aae [DEPENDABOT] Update: Bump django-tailwind-cli from 2.21.1 to 4.0.1
Bumps [django-tailwind-cli](https://github.com/django-commons/django-tailwind-cli) from 2.21.1 to 4.0.1.
- [Release notes](https://github.com/django-commons/django-tailwind-cli/releases)
- [Changelog](https://github.com/django-commons/django-tailwind-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/django-commons/django-tailwind-cli/compare/2.21.1...4.0.1)

---
updated-dependencies:
- dependency-name: django-tailwind-cli
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 16:50:31 +00:00
dependabot[bot]
a16b0444d4 [DEPENDABOT] Update: Bump django-allauth from 65.4.0 to 65.4.1
Bumps [django-allauth](https://github.com/sponsors/pennersr) from 65.4.0 to 65.4.1.
- [Commits](https://github.com/sponsors/pennersr/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 16:50:27 +00:00
dependabot[bot]
a01cda306e [DEPENDABOT] Update: Bump django-htmx from 1.21.0 to 1.22.0
Bumps [django-htmx](https://github.com/adamchainz/django-htmx) from 1.21.0 to 1.22.0.
- [Changelog](https://github.com/adamchainz/django-htmx/blob/main/docs/changelog.rst)
- [Commits](https://github.com/adamchainz/django-htmx/compare/1.21.0...1.22.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 16:50:22 +00:00
dependabot[bot]
808deb82e2 [DEPENDABOT] Update: Bump whitenoise from 6.8.2 to 6.9.0
Bumps [whitenoise](https://github.com/evansd/whitenoise) from 6.8.2 to 6.9.0.
- [Changelog](https://github.com/evansd/whitenoise/blob/main/docs/changelog.rst)
- [Commits](https://github.com/evansd/whitenoise/compare/6.8.2...6.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 16:50:19 +00:00
dependabot[bot]
2d2d832e07 [DEPENDABOT] Update Actions: Bump actions/setup-python from 4 to 5
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 16:41:58 +00:00
pacnpal
b4c474c496 Refactor URL patterns for parks and rides; update context fields in models to use ForeignKey for pghistory.Context 2025-02-10 10:54:34 -05:00
pacnpal
9ed28b15b4 Create initial migration for HistoricalSlug model; update foreign key references to use settings.AUTH_USER_MODEL and define unique constraints 2025-02-10 00:20:27 -05:00
pacnpal
4b32580b13 Update migration files for Django 5.1.4; remove obsolete merge migrations and adjust history tracking context in templates 2025-02-10 00:11:29 -05:00
pacnpal
228eeeb3c8 Add merge migrations for parks, companies, and moderation apps; update EmailConfiguration, Review, Photo, TopList, and TopListItem models to use pghistory for historical tracking 2025-02-09 16:00:10 -05:00
pacnpal
b7f6c60682 Refactor model imports and update admin classes to use pghistory for historical tracking; replace HistoricalModel with TrackedModel in relevant models 2025-02-09 11:20:40 -05:00
pacnpal
7ecf43f1a4 Add history tracking functionality using django-pghistory; implement views, templates, and middleware for event serialization and context management 2025-02-09 09:52:19 -05:00
pacnpal
a148d34cf9 Implement historical tracking using django-pghistory; add middleware for context capture and update model architecture 2025-02-08 21:18:44 -05:00
pacnpal
71b73522ae Revert "Add version control system functionality with branch management, history tracking, and merge operations"
This reverts commit f3d28817a5.
2025-02-08 17:37:30 -05:00
pacnpal
03f9df4bab Refactor comments app to use mixins for comment functionality; update admin interfaces and add historical model fixes 2025-02-08 16:33:55 -05:00
pacnpal
75f5b07129 Add comments app with models, views, and tests; integrate comments into existing models 2025-02-07 21:58:02 -05:00
pacnpal
86ae24bbac Add comprehensive evaluation and recommendations for version control system 2025-02-07 13:57:07 -05:00
pacnpal
0e0ed01cee Add comment and reply functionality with preview and notification templates 2025-02-07 13:13:49 -05:00
pacnpal
2c4d2daf34 Add OWASP compliance mapping and security test case templates, and document version control implementation phases 2025-02-07 10:51:11 -05:00
pacnpal
d353f24f9d Add timezone utility and current branch signal to enhance park detail functionality 2025-02-06 20:38:38 -05:00
pacnpal
9c65df12bb Enhance type safety in version control system by adding UserModel TypeVar, improving type hints in managers.py and utils.py, and ensuring consistent type imports. 2025-02-06 20:35:30 -05:00
pacnpal
ecf94bf84e Add version control context processor and integrate map functionality with dedicated JavaScript 2025-02-06 20:06:10 -05:00
pacnpal
f3d28817a5 Add version control system functionality with branch management, history tracking, and merge operations 2025-02-06 19:29:23 -05:00
pacnpal
6fa807f4b6 Add photo gallery functionality with file upload, sharing, and fullscreen display features 2025-02-06 14:20:48 -05:00
pacnpal
323aa561a5 Refactor authentication settings and enhance frontend moderation panel with performance optimizations, loading states, error handling, mobile responsiveness, and accessibility improvements 2025-02-06 14:20:12 -05:00
pacnpal
7d25d6f992 Update Django and dependencies, add app path configuration 2025-02-06 13:58:57 -05:00
pacnpal
19852207f6 Refactor CI workflow to use Homebrew for GDAL installation and update Python version to 3.13.1 2025-02-06 11:55:43 -05:00
pacnpal
185af7fd17 Refactor CI workflow to install pip using get-pip.py and ensure system package compatibility 2025-02-06 11:47:17 -05:00
pacnpal
768f05b783 Improve CI workflow by setting up Python and upgrading pip, setuptools, and wheel before installing dependencies 2025-02-06 11:45:29 -05:00
pacnpal
411c6f6f68 Enhance CI workflow by configuring Django GDAL settings and adding debug steps for GDAL installation verification 2025-02-06 11:26:36 -05:00
pacnpal
789a6386a5 Update GDAL installation steps in CI workflow for improved compatibility 2025-02-06 10:45:04 -05:00
pacnpal
dbd76785b5 Add GDAL installation and verification steps to CI workflow 2025-02-06 10:35:56 -05:00
pacnpal
4215e14b5e Merge pull request #25 from pacnpal/pixeebot/drip-2025-02-06-pixee-python/remove-unnecessary-f-str
Remove Unnecessary F-strings
2025-02-05 23:04:48 -05:00
pixeebot[bot]
dee7c61320 Remove Unnecessary F-strings 2025-02-06 03:53:13 +00:00
pacnpal
2f26061170 Add four GIF files to media submissions 2025-02-05 18:48:36 -05:00
pacnpal
bc68eaf4d9 Add auto-approval for moderator submissions and update submission handling 2025-02-05 18:47:23 -05:00
pacnpal
1ef38f4a96 Add three GIF files to media submissions 2025-02-05 18:23:45 -05:00
pacnpal
1f3f94702e Add LocationForm import to LocationSearchView and ignore type check for piexif import 2025-02-05 18:10:07 -05:00
pacnpal
63b484b724 Merge pull request #24 from pacnpal/dependabot/pip/black-25.1.0
[DEPENDABOT] Update: Bump black from 24.10.0 to 25.1.0
2025-02-05 18:03:44 -05:00
pacnpal
1182e894e3 Merge branch 'main' into dependabot/pip/black-25.1.0 2025-02-05 18:01:31 -05:00
pacnpal
cda755ea59 Merge pull request #23 from pacnpal/dependabot/pip/isort-tw-6.0.0
[DEPENDABOT] Update: Update isort requirement from ^5.13.0 to ^6.0.0
2025-02-05 18:00:24 -05:00
pacnpal
8bbfce3f2a Merge pull request #22 from pacnpal/pixeebot/drip-2025-01-28-pixee-python/add-requests-timeouts
Add timeout to `requests` calls
2025-02-05 18:00:10 -05:00
pacnpal
0e97fdc96b Merge pull request #21 from pacnpal/dependabot/pip/django-simple-history-3.8.0
[DEPENDABOT] Update: Bump django-simple-history from 3.7.0 to 3.8.0
2025-02-05 17:59:59 -05:00
pacnpal
ebc38228e6 Fix filter query in cleanup_test_data command to use correct field for reviews 2025-02-05 17:58:40 -05:00
dependabot[bot]
45c40c720d [DEPENDABOT] Update: Bump black from 24.10.0 to 25.1.0
Bumps [black](https://github.com/psf/black) from 24.10.0 to 25.1.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/24.10.0...25.1.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 16:57:28 +00:00
dependabot[bot]
696d26acdd [DEPENDABOT] Update: Update isort requirement from ^5.13.0 to ^6.0.0
Updates the requirements on [isort](https://github.com/pycqa/isort) to permit the latest version.
- [Release notes](https://github.com/pycqa/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pycqa/isort/compare/5.13.0...6.0.0)

---
updated-dependencies:
- dependency-name: isort
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 16:57:15 +00:00
pixeebot[bot]
96857ad1d4 Add timeout to requests calls 2025-01-28 03:09:49 +00:00
dependabot[bot]
ef40184e07 [DEPENDABOT] Update: Bump django-simple-history from 3.7.0 to 3.8.0
Bumps [django-simple-history](https://github.com/jazzband/django-simple-history) from 3.7.0 to 3.8.0.
- [Release notes](https://github.com/jazzband/django-simple-history/releases)
- [Changelog](https://github.com/jazzband/django-simple-history/blob/master/CHANGES.rst)
- [Commits](https://github.com/jazzband/django-simple-history/compare/3.7.0...3.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 16:46:08 +00:00
pacnpal
7aa706d12a Merge pull request #16 from pacnpal/dependabot/pip/dj-rest-auth-7.0.1 2025-01-21 21:59:57 -05:00
pacnpal
de6146f812 Merge pull request #17 from pacnpal/dependabot/pip/pillow-11.1.0 2025-01-21 21:59:46 -05:00
pacnpal
1bfbe4a8b4 Merge pull request #19 from pacnpal/dependabot/pip/django-5.1.5 2025-01-21 21:59:37 -05:00
pacnpal
209c3e4d21 Merge pull request #20 from pacnpal/dependabot/pip/reactivated-tw-0.46.0 2025-01-21 21:58:30 -05:00
dependabot[bot]
886b275f65 [DEPENDABOT] Update: Update reactivated requirement
Updates the requirements on [reactivated](https://github.com/silviogutierrez/django-react) to permit the latest version.
- [Commits](https://github.com/silviogutierrez/django-react/compare/v0.45.3...v0.46.0)

---
updated-dependencies:
- dependency-name: reactivated
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 16:28:36 +00:00
dependabot[bot]
c9ebf4c833 [DEPENDABOT] Update: Bump django from 5.1.4 to 5.1.5
Bumps [django](https://github.com/django/django) from 5.1.4 to 5.1.5.
- [Commits](https://github.com/django/django/compare/5.1.4...5.1.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 16:28:25 +00:00
dependabot[bot]
672749d109 [DEPENDABOT] Update: Bump pillow from 11.0.0 to 11.1.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.0.0 to 11.1.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/11.0.0...11.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-06 16:31:53 +00:00
dependabot[bot]
a5c3e56046 [DEPENDABOT] Update: Bump dj-rest-auth from 7.0.0 to 7.0.1
Bumps [dj-rest-auth](https://github.com/iMerica/dj-rest-auth) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/iMerica/dj-rest-auth/releases)
- [Commits](https://github.com/iMerica/dj-rest-auth/compare/7.0.0...7.0.1)

---
updated-dependencies:
- dependency-name: dj-rest-auth
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-06 16:31:42 +00:00
pacnpal
d728ba6e9c Merge pull request #14 from pacnpal/dependabot/pip/django-allauth-65.3.1 2025-01-02 22:31:24 -05:00
pacnpal
f819a1f07c Merge pull request #13 from pacnpal/pixeebot/drip-2024-12-26-pixee-python/secure-random 2025-01-02 22:30:39 -05:00
dependabot[bot]
d91f79e29c [DEPENDABOT] Update: Bump django-allauth from 65.3.0 to 65.3.1
Bumps [django-allauth](https://github.com/sponsors/pennersr) from 65.3.0 to 65.3.1.
- [Commits](https://github.com/sponsors/pennersr/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 16:59:20 +00:00
pixeebot[bot]
304812d43f Secure Source of Randomness 2024-12-26 03:11:52 +00:00
pacnpal
3f7296d7a5 ygeah 2024-12-24 14:32:26 -05:00
pacnpal
e60f73de9d Add test setup, cleanup, and fixtures 2024-12-24 14:32:16 -05:00
pacnpal
af7ea6b4ce Add comprehensive e2e tests with Playwright 2024-12-24 14:29:32 -05:00
pacnpal
36478c7a1b Fix dependencies and GeoDjango configuration 2024-12-24 14:24:18 -05:00
pacnpal
c8628984e0 Merge pull request #1 from pacnpal/dependabot/github_actions/actions/setup-python-5
[DEPENDABOT] Update Actions: Bump actions/setup-python from 3 to 5
2024-12-24 11:18:44 -05:00
pacnpal
1bfe08a0a7 Merge pull request #2 from pacnpal/dependabot/pip/django-allauth-65.3.0
[DEPENDABOT] Update: Bump django-allauth from 65.2.0 to 65.3.0
2024-12-24 11:18:32 -05:00
pacnpal
901a1c421d Merge pull request #3 from pacnpal/dependabot/pip/channels-4.2.0
[DEPENDABOT] Update: Bump channels from 4.1.0 to 4.2.0
2024-12-24 11:18:21 -05:00
dependabot[bot]
280ad4d6da [DEPENDABOT] Update: Bump channels from 4.1.0 to 4.2.0
Bumps [channels](https://github.com/django/channels) from 4.1.0 to 4.2.0.
- [Changelog](https://github.com/django/channels/blob/main/CHANGELOG.txt)
- [Commits](https://github.com/django/channels/compare/4.1.0...4.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-24 16:17:52 +00:00
dependabot[bot]
9634bac155 [DEPENDABOT] Update: Bump django-allauth from 65.2.0 to 65.3.0
Bumps [django-allauth](https://github.com/sponsors/pennersr) from 65.2.0 to 65.3.0.
- [Commits](https://github.com/sponsors/pennersr/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-24 16:17:15 +00:00
pacnpal
69094a9af8 Merge pull request #4 from pacnpal/dependabot/pip/django-5.1.4
[DEPENDABOT] Update: Bump django from 5.1.3 to 5.1.4
2024-12-24 11:17:01 -05:00
pacnpal
d338917ca1 Merge pull request #5 from pacnpal/dependabot/pip/pytest-8.3.4
[DEPENDABOT] Update: Bump pytest from 8.3.3 to 8.3.4
2024-12-24 11:16:52 -05:00
pacnpal
5acc74d34c Merge pull request #6 from pacnpal/dependabot/pip/channels-redis-4.2.1
[DEPENDABOT] Update: Bump channels-redis from 4.2.0 to 4.2.1
2024-12-24 11:16:41 -05:00
pacnpal
8b7ad53cbd Merge pull request #7 from pacnpal/dependabot/pip/pyjwt-2.10.1
[DEPENDABOT] Update: Bump pyjwt from 2.9.0 to 2.10.1
2024-12-24 11:16:07 -05:00
pacnpal
7553752f0d Update review.yml 2024-12-24 11:14:13 -05:00
dependabot[bot]
1ca84208ef [DEPENDABOT] Update: Bump django from 5.1.3 to 5.1.4
Bumps [django](https://github.com/django/django) from 5.1.3 to 5.1.4.
- [Commits](https://github.com/django/django/compare/5.1.3...5.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-24 16:03:05 +00:00
pacnpal
fdd7a4fcf1 Merge pull request #8 from pacnpal/dependabot/pip/django-tailwind-cli-2.21.1
[DEPENDABOT] Update: Bump django-tailwind-cli from 2.19.0 to 2.21.1
2024-12-24 11:02:08 -05:00
pacnpal
ae8710c157 Merge pull request #12 from pacnpal/dependabot/pip/reactivated-tw-0.45.3
[DEPENDABOT] Update: Update reactivated requirement from ^0.41.1 to ^0.45.3
2024-12-24 11:00:31 -05:00
pacnpal
56d9174bb5 Update and rename main.yml to review.yml 2024-12-24 10:59:19 -05:00
pacnpal
5c62b41070 Update main.yml 2024-12-24 10:51:01 -05:00
pacnpal
8014bcc368 Create main.yml 2024-12-24 10:49:55 -05:00
dependabot[bot]
1a60658f17 [DEPENDABOT] Update: Update reactivated requirement
Updates the requirements on [reactivated](https://github.com/silviogutierrez/django-react) to permit the latest version.
- [Commits](https://github.com/silviogutierrez/django-react/compare/v0.41.1...v0.45.3)

---
updated-dependencies:
- dependency-name: reactivated
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 16:44:37 +00:00
dependabot[bot]
311c0e999c [DEPENDABOT] Update: Bump django-tailwind-cli from 2.19.0 to 2.21.1
Bumps [django-tailwind-cli](https://github.com/django-commons/django-tailwind-cli) from 2.19.0 to 2.21.1.
- [Release notes](https://github.com/django-commons/django-tailwind-cli/releases)
- [Changelog](https://github.com/django-commons/django-tailwind-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/django-commons/django-tailwind-cli/compare/2.19.0...2.21.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 11:45:12 +00:00
dependabot[bot]
6af9f7332a [DEPENDABOT] Update: Bump pyjwt from 2.9.0 to 2.10.1
Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.9.0 to 2.10.1.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.9.0...2.10.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 11:44:58 +00:00
dependabot[bot]
7c81d8e8eb [DEPENDABOT] Update: Bump channels-redis from 4.2.0 to 4.2.1
Bumps [channels-redis](https://github.com/django/channels_redis) from 4.2.0 to 4.2.1.
- [Changelog](https://github.com/django/channels_redis/blob/main/CHANGELOG.txt)
- [Commits](https://github.com/django/channels_redis/compare/4.2.0...4.2.1)

---
updated-dependencies:
- dependency-name: channels-redis
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 11:44:55 +00:00
dependabot[bot]
04daf9573b [DEPENDABOT] Update: Bump pytest from 8.3.3 to 8.3.4
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.3 to 8.3.4.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.3...8.3.4)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 11:44:52 +00:00
dependabot[bot]
96e2f097cd [DEPENDABOT] Update Actions: Bump actions/setup-python from 3 to 5
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v3...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 11:44:15 +00:00
271 changed files with 30500 additions and 2964 deletions

30
.clinerules Normal file
View File

@@ -0,0 +1,30 @@
# Project Startup Rules
## Development Server
IMPORTANT: Always follow these instructions exactly when starting the development server:
```bash
lsof -ti :8000 | xargs kill -9; find . -type d -name "__pycache__" -exec rm -r {} +; uv run manage.py tailwind runserver
```
Note: These steps must be executed in this exact order as a single command to ensure consistent behavior.
## Package Management
IMPORTANT: When a Python package is needed, only use UV to add it:
```bash
uv add <package>
```
Do not attempt to install packages using any other method.
## Django Management Commands
IMPORTANT: When running any Django manage.py commands (migrations, shell, etc.), always use UV:
```bash
uv run manage.py <command>
```
This applies to all management commands including but not limited to:
- Making migrations: `uv run manage.py makemigrations`
- Applying migrations: `uv run manage.py migrate`
- Creating superuser: `uv run manage.py createsuperuser`
- Starting shell: `uv run manage.py shell`
NEVER use `python manage.py` or `uv run python manage.py`. Always use `uv run manage.py` directly.

View File

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

34
.github/workflows/review.yml vendored Normal file
View File

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

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

20
.roomodes Normal file

File diff suppressed because one or more lines are too long

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.

1
README.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,93 @@
# Generated by Django 5.1.4 on 2025-02-21 17:55
import django.utils.timezone
import pgtrigger.compiler
import pgtrigger.migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0001_initial"),
]
operations = [
pgtrigger.migrations.RemoveTrigger(
model_name="toplistitem",
name="insert_insert",
),
pgtrigger.migrations.RemoveTrigger(
model_name="toplistitem",
name="update_update",
),
migrations.AddField(
model_name="toplistitem",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="toplistitem",
name="updated_at",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="toplistitemevent",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="toplistitemevent",
name="updated_at",
field=models.DateTimeField(auto_now=True),
),
migrations.AlterField(
model_name="toplist",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.AlterField(
model_name="toplistitem",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
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="[AWS-SECRET-REMOVED]",
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="[AWS-SECRET-REMOVED]",
operation="UPDATE",
pgid="pgtrigger_update_update_2b6e3",
table="accounts_toplistitem",
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 = [
("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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.1.4 on 2025-02-21 17:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("companies", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="company",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.AlterField(
model_name="manufacturer",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
]

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

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

View File

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

39
core/forms.py Normal file
View File

@@ -0,0 +1,39 @@
"""Core forms and form components."""
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.utils.translation import gettext_lazy as _
from autocomplete import Autocomplete
class BaseAutocomplete(Autocomplete):
"""Base autocomplete class for consistent autocomplete behavior across the project.
This class extends django-htmx-autocomplete's base Autocomplete class to provide:
- Project-wide defaults for autocomplete behavior
- Translation strings
- Authentication enforcement
- Sensible search configuration
"""
# Search configuration
minimum_search_length = 2 # More responsive than default 3
max_results = 10 # Reasonable limit for performance
# UI text configuration using gettext for i18n
no_result_text = _("No matches found")
narrow_search_text = _("Showing %(page_size)s of %(total)s matches. Please refine your search.")
type_at_least_n_characters = _("Type at least %(n)s characters...")
# Project-wide component settings
placeholder = _("Search...")
@staticmethod
def auth_check(request):
"""Enforce authentication by default.
This can be overridden in subclasses if public access is needed.
Configure AUTOCOMPLETE_BLOCK_UNAUTHENTICATED in settings to disable.
"""
block_unauth = getattr(settings, 'AUTOCOMPLETE_BLOCK_UNAUTHENTICATED', True)
if block_unauth and not request.user.is_authenticated:
raise PermissionDenied(_("Authentication required"))

27
core/middleware.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.1.4 on 2025-02-21 17:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("designers", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="designer",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
]

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

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.1.4 on 2025-02-21 17:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("email_service", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="emailconfiguration",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
]

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

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

View File

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

3
globalLocators.js Normal file
View File

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

12
history/apps.py Normal file
View File

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

View File

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

View File

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

10
history/urls.py Normal file
View File

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

41
history/views.py Normal file
View File

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

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.1.4 on 2025-02-21 17:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("history_tracking", "0001_initial"),
]
operations = [
migrations.RenameIndex(
model_name="historicalslug",
new_name="history_tra_content_63013c_idx",
old_name="history_tra_content_1234ab_idx",
),
migrations.RenameIndex(
model_name="historicalslug",
new_name="history_tra_slug_f843aa_idx",
old_name="history_tra_slug_1234ab_idx",
),
migrations.AlterField(
model_name="historicalslug",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
]

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.1.4 on 2025-02-21 17:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("location", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="location",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
]

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

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

View File

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

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

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

View File

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

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.1.4 on 2025-02-21 17:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("media", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="photo",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
]

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

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