1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-19 12:59:36 +02:00

Compare commits

...

758 commits
v0.7.0 ... main

Author SHA1 Message Date
Sean Morley
4e96e529f4
fix(adventure): enhance collection ownership validation in AdventureSerializer (#723) 2025-07-09 23:03:48 -04:00
Sean Morley
cadea118d3
Merge pull request #680 from seanmorley15/development
Date and Timezone fixes (lots of them!!)
2025-06-19 11:54:41 -04:00
Sean Morley
7a17e0e1d8 feat(calendar): add markdown rendering for event descriptions in modal 2025-06-19 11:53:24 -04:00
Sean Morley
6516bc56ef refactor(lodging): remove unused icon imports and add comment for check-out date logic 2025-06-19 11:40:04 -04:00
Sean Morley
a6b39f64d6 feat(calendar): add adventure detail link in event modal 2025-06-19 11:37:08 -04:00
Sean Morley
36f9022872 fix(lodging): remove console log and improve all-day event checks in lodging modal 2025-06-19 11:33:04 -04:00
Sean Morley
3b0ccdb6d3 feat(DateRangeCollapse): auto-detect all-day setting for transportation and lodging types 2025-06-18 22:29:37 -04:00
Sean Morley
9964398e25 feat(lodging): add check-in and check-out labels and enhance date handling for lodging events 2025-06-18 22:21:34 -04:00
Sean Morley
df24316837 feat(lodging): improve lodging date handling with all-day event support and timezone adjustments 2025-06-18 21:10:10 -04:00
Sean Morley
63e8e96d52 feat(collections): enhance lodging date handling with timezone support and all-day event formatting 2025-06-18 19:57:23 -04:00
Sean Morley
08cd3912c7 fix(config): correct appVersion string formatting 2025-06-18 19:24:51 -04:00
Sean Morley
eef8c92e82 feat(calendar): enhance event handling with timezone support and filtering capabilities 2025-06-18 19:03:32 -04:00
Sean Morley
3306b799df
Merge pull request #678 from seanmorley15/development
feat: add CollectionAllView component for unified display of adventur…
2025-06-18 14:29:01 -04:00
Sean Morley
8b108c5797 refactor(CollectionAllView): enhance mobile responsiveness and clean up unused imports 2025-06-18 14:28:03 -04:00
Sean Morley
93a489a778 feat: add CollectionAllView component for unified display of adventures, transportations, lodging, notes, and checklists with filtering and sorting capabilities
i18n: update translations for collection contents and sorting options in multiple languages

refactor: replace individual sections for adventures, transportations, lodging, notes, and checklists in the collection page with the new CollectionAllView component
2025-06-18 14:05:39 -04:00
Sean Morley
44ea7dff0c
Merge pull request #677 from seanmorley15/development
fix(adventure): add collection ID to adventure when creating a new ad…
2025-06-18 10:23:02 -04:00
Sean Morley
7ec4e5d0f5 fix(adventure): add collection ID to adventure when creating a new adventure 2025-06-18 10:20:30 -04:00
Sean Morley
380fe1364f
Merge pull request #676 from blitzdose/main
Fixed frontend returning corrupt binary data
2025-06-17 18:05:20 -04:00
Christian Zäske
69631848cf
Fixed frontend returning corrupt binary data 2025-06-18 00:00:51 +02:00
Sean Morley
7f285f2b1d
Merge pull request #675 from seanmorley15/development
fix(integration): update image entry retrieval to handle multiple col…
2025-06-17 16:11:12 -04:00
Sean Morley
4f7d408460
Merge pull request #674 from nordtektiger/patch-1
update readme to reflect username changes (sorry sean!)
2025-06-17 16:03:44 -04:00
Sean Morley
aed76a5689 fix(integration): update image entry retrieval to handle multiple collections and improve access control logic 2025-06-17 15:49:27 -04:00
Jacob
a556c49147
chore: update readme to reflect username changes (sorry sean!) 2025-06-17 18:59:13 +00:00
Sean Morley
ea4b6bd715
Add Adventures to Multiple Collections 2025-06-16 18:19:43 -04:00
Sean Morley
2fb1548f9f fix(recommendations): update Google Places API integration to new endpoint and response structure 2025-06-16 17:35:38 -04:00
Sean Morley
be8ac67161 fix(geocoding): update search_google function to use new Places API and improve response handling 2025-06-16 17:31:17 -04:00
Sean Morley
0636f0ec8c docs: update maintainer information and add testimonial from Open Source Daily 2025-06-16 14:00:58 -04:00
Sean Morley
09ffb6cdff fix(docker.md): update frontend configuration table for clarity and consistency 2025-06-16 12:12:37 -04:00
Sean Morley
930c98a607 fix(geocoding): improve error handling and response validation in search_google function 2025-06-16 11:47:36 -04:00
Sean Morley
cee9345bf1 feat: Enhance CollectionLink component with search functionality and statistics display
- Implemented search functionality to filter collections based on user input.
- Added statistics display for linked collections and total collections.
- Updated modal layout for better user experience, including a search bar and clear filters option.
- Improved accessibility and visual design of the modal and its components.

refactor: Update localization files for multiple languages

- Removed outdated delete collection warning messages.
- Added new keys for adventures available, collections linked, and other relevant phrases in various languages.
- Ensured consistency across localization files for better user experience.

fix: Adjust styles in worldtravel and collections pages

- Updated styles for quick stats section in worldtravel page for improved visibility.
- Ensured proper handling of sorting parameters in collections page navigation.
2025-06-15 18:28:48 -04:00
Sean Morley
ced1f94473 fix(adventure_view): restrict queryset to user-owned adventures only 2025-06-15 17:40:43 -04:00
Sean Morley
da65235277 Update screenshots for adventures, countries, dashboard, details, edit, itinerary, map, and regions 2025-06-15 13:24:18 -04:00
Sean Morley
ab5082317e Add "invalid_credentials" message to multiple language files
- Updated Spanish (es.json) to include "invalid_credentials": "Credenciales no válidas"
- Updated French (fr.json) to include "invalid_credentials": "Des références non valides"
- Updated Italian (it.json) to include "invalid_credentials": "Credenziali non valide"
- Updated Korean (ko.json) to include "invalid_credentials": "잘못된 자격 증명"
- Updated Dutch (nl.json) to include "invalid_credentials": "Ongeldige referenties"
- Updated Norwegian (no.json) to include "invalid_credentials": "Ugyldig legitimasjon"
- Updated Russian (ru.json) to include "invalid_credentials": "Неверные полномочия"
- Updated Swedish (sv.json) to include "invalid_credentials": "Ogiltiga referenser"
- Updated Chinese (zh.json) to include "invalid_credentials": "无效的凭据"
2025-06-15 12:56:53 -04:00
Sean Morley
c2074c1581 fix(collections): ensure linked collections are sorted correctly after fetching 2025-06-14 22:16:39 -04:00
Sean Morley
977843cbc6 fix(collections): remove debug log for API URL fetching 2025-06-14 22:01:56 -04:00
Sean Morley
b0e8000cf8 Refactor UI components for improved localization and styling
- Updated various headings and text elements to utilize localization functions for better internationalization support.
- Simplified gradient styles in headings to enhance readability.
- Adjusted adventure and travel statistics sections to reflect localized titles and descriptions.
- Enhanced filter options and buttons with localized text for clarity.
- Modified Tailwind CSS configuration to include a new color 'dim' for future styling needs.
2025-06-14 18:55:59 -04:00
Sean Morley
b5931c6c23 refactor(worldtravel): remove insert_id fields from city, country, and region models; update related migration
feat(search): enhance search results display with total results count and improved layout
fix(profile): update achievement levels based on adventure count; remove unused quick actions
refactor(shared): delete unused shared collections route and related components
feat(worldtravel): improve interactive map functionality and layout in world travel detail view
2025-06-14 14:05:30 -04:00
Sean Morley
151c76dbd1 Enhance user profile and world travel pages with improved UI and functionality
- Updated user profile page to include achievement calculations and enhanced styling for user information and statistics.
- Added icons for better visual representation of user stats and achievements.
- Improved layout for displaying adventures and collections with conditional rendering for empty states.
- Refactored world travel page to include search and filter functionality for cities, with a sidebar for progress and stats.
- Implemented completion percentage and progress bars for visited cities.
- Enhanced map integration with markers for visited and not visited cities, including toggle options for map labels.
2025-06-14 11:10:59 -04:00
Sean Morley
d4c76f8718 feat(map): implement dynamic basemap URL based on theme; update map styles across components 2025-06-13 23:49:14 -04:00
Sean Morley
badeac867d fix(worldtravel): remove unnecessary hover scale effect on country cards 2025-06-13 21:48:35 -04:00
Sean Morley
a99553ba0d refactor: remove archived collections page and related components; enhance world travel pages with improved UI and filtering options 2025-06-13 21:41:10 -04:00
Sean Morley
14eb4ca802 feat(collections): enhance collections page with sorting, filtering, and pagination features
- Updated the collections loading logic to include sorting and pagination parameters from the URL.
- Refactored the collections page to manage owned, shared, and archived collections with a tabbed interface.
- Added sorting functionality to allow users to sort collections by different attributes.
- Implemented a sidebar for filtering and sorting options.
- Improved the UI for better user experience, including a floating action button for creating new collections.
- Added a not found page for collections that do not exist, enhancing error handling.
2025-06-13 12:11:42 -04:00
Sean Morley
7eb96bcc2a
Merge pull request #663 from seanmorley15/dependabot/npm_and_yarn/frontend/npm_and_yarn-6ea9762674
chore(deps): bump brace-expansion from 1.1.11 to 1.1.12 in /frontend in the npm_and_yarn group across 1 directory
2025-06-13 09:50:39 -04:00
Sean Morley
24cf3f844d
Merge pull request #665 from DesarrolloAntonio/fix-spanish-translations
fix: correct Spanish translations
2025-06-13 09:49:35 -04:00
Sean Morley
c60ced09c4
Merge branch 'development' into fix-spanish-translations 2025-06-13 09:48:18 -04:00
Antonio Corrales
05db98f968 fix: correct Spanish translations
- Fix incorrect translations and typos
- Fix inconsistent capitalization
- Keep proper names untranslated (Immich)
- Improve consistency in technical terms translation
- Correct 'Campo de golf' to 'Enlaces' (links)
- Change 'Ahorrar' to 'Guardar' (save)
- Fix various grammatical errors
2025-06-13 09:14:28 +02:00
Sean Morley
3f9a6767bd feat: Enhance Adventure and Collection Management
- Added support for multiple collections in AdventureSerializer, allowing adventures to be linked to multiple collections.
- Implemented validation to ensure collections belong to the current user during adventure creation and updates.
- Introduced a signal to update adventure publicity based on the public status of linked collections.
- Updated file permission checks to consider multiple collections when determining access rights.
- Modified AdventureImageViewSet and AttachmentViewSet to check access against collections instead of a single collection.
- Enhanced AdventureViewSet to support filtering and sorting adventures based on collections.
- Updated frontend components to manage collections more effectively, including linking and unlinking adventures from collections.
- Adjusted API endpoints and data structures to accommodate the new collections feature.
- Improved user experience with appropriate notifications for collection actions.
2025-06-12 15:54:01 -04:00
dependabot[bot]
f6b9802ba7
chore(deps): bump brace-expansion
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [brace-expansion](https://github.com/juliangruber/brace-expansion).


Updates `brace-expansion` from 1.1.11 to 1.1.12
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-12 05:57:26 +00:00
Sean Morley
71ff217323
Delete documentation/public/funding.json 2025-06-11 19:45:30 -04:00
Sean Morley
ef5dca79e5
Create funding.json 2025-06-11 19:42:45 -04:00
Sean Morley
d9070e68bb
Merge pull request #660 from ShalunBdk/patch-1
Added Russian translation
2025-06-11 19:12:42 -04:00
Sean Morley
f72fb50eb0
Update ru.json 2025-06-11 19:11:23 -04:00
Sean Morley
b59dcade3b
Merge pull request #661 from jlcs-es/patch-1
Update es.json with more contextual translations
2025-06-11 12:53:43 -04:00
José Luis Cánovas
164627b35b
Update es.json
Modified some translations with more contextual ones.
2025-06-11 17:17:47 +02:00
Александр
3f89fad67f
Update +layout.svelte 2025-06-11 14:59:41 +07:00
Александр
871e265001
Update Navbar.svelte 2025-06-11 14:58:55 +07:00
Александр
2cab28e921
Create ru.json 2025-06-11 14:49:55 +07:00
Sean Morley
8a46bd7ed3
Bug fixes, UI enhancements, Google Maps, Immich Integration Improvements 2025-06-09 09:18:47 -04:00
Sean Morley
f36de76501 fix: clarify instruction in PUBLIC_SERVER_URL comment in .env.example 2025-06-08 21:05:31 -04:00
Sean Morley
55f501d939 fix: handle user_id correctly in ChecklistItem creation and updates to avoid constraint issues 2025-06-08 13:15:43 -04:00
Sean Morley
0037d037cf fix: add Authelia to the list of supported social authentication services 2025-06-07 11:16:17 -04:00
Sean Morley
19baf6ab35 chore: update version to v0.10.0 across Dockerfiles, package.json, and configuration files; add changelog for v0.10.0 release 2025-06-07 11:14:27 -04:00
Sean Morley
20cdc2405f feat: add alert message in AdventureModal for GPX file tips 2025-06-07 10:58:02 -04:00
Sean Morley
297eb2916a fix: improve Docker installation documentation for clarity and completeness 2025-06-06 19:07:47 -04:00
Sean Morley
27a27545ca fix: update button styles in CategoryModal for improved UI consistency 2025-06-06 18:39:46 -04:00
Sean Morley
06a5bb06b3 feat: enhance CategoryModal with add/edit functionality and improve localization support 2025-06-06 14:20:37 -04:00
Sean Morley
c0f2d060db fix: update marker colors based on visit status for countries, regions, and cities 2025-06-06 11:00:46 -04:00
Sean Morley
2d7b6c85c9 fix: enhance Navbar component with scroll effect and improve layout responsiveness 2025-06-06 10:21:53 -04:00
Sean Morley
5d0132a2a8 fix: pin setuptools version and update docker-compose to use images 2025-06-06 09:44:01 -04:00
Sean Morley
39c664ab1a fix: update Dockerfiles with metadata labels and improve build process 2025-06-05 23:29:39 -04:00
Sean Morley
d91a4fbe98 fix: update link to installation options in quick start guide 2025-06-05 16:40:37 -04:00
Sean Morley
90624664f4 fix: ensure interactive terminal requirement for installation script 2025-06-05 16:33:34 -04:00
Sean Morley
5f9d0cd207 chore: update dependencies and improve script execution condition
- Updated vitepress from version 1.5.0 to 1.6.3 in package.json and pnpm-lock.yaml.
- Updated various dependencies in pnpm-lock.yaml including @docsearch/css, @docsearch/js, @iconify-json/simple-icons, @shikijs/core, and others to their latest versions.
- Modified install_adventurelog.sh to improve script execution condition by allowing it to run when piped input is detected.
2025-06-05 16:24:43 -04:00
Sean Morley
36a2e59d52 fix: add logging import to integrations views 2025-06-05 15:12:40 -04:00
Sean Morley
af5be0bc8f
Potential fix for code scanning alert no. 21: Information exposure through an exception
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-06-05 15:10:41 -04:00
Sean Morley
16840a6040 fix: update support button link in Navbar component 2025-06-05 15:02:01 -04:00
Sean Morley
ebee6f52e8 fix: update request data handling in AdventureImageViewSet and improve error handling in RecommendationsViewSet 2025-06-05 14:53:08 -04:00
Sean Morley
9d817a5ce9 feat: add Immich server connection validation and error handling in integration settings 2025-06-05 14:28:37 -04:00
Sean Morley
6fdfb86297 fix: update Docker images to use beta tags for frontend and backend services 2025-06-04 17:08:38 -04:00
Sean Morley
c0c91c8472 fix: update environment configuration and improve installer output messages 2025-06-04 16:59:41 -04:00
Sean Morley
d6ab4e9f64 feat: add auto-generation for item name in LocationDropdown and improve theme handling in Navbar 2025-06-04 14:41:29 -04:00
Sean Morley
4f342425ab
Update FUNDING.yml 2025-06-04 10:12:40 -04:00
Sean Morley
6c338b8c0f chore: update screenshots for adventures, countries, dashboard, details, edit, itinerary, map, and regions 2025-06-03 19:50:37 -04:00
Sean Morley
2f7103f5f3 fix: clear album selection when switching search categories in ImmichSelect 2025-06-03 19:16:44 -04:00
Sean Morley
d0c1ecd394 feat: enhance ImmichIntegrationView to support date range filtering and improve error handling for invalid date formats 2025-06-03 19:12:38 -04:00
Sean Morley
cf108ecd3a fix: update CollectionCard to use adventures from collection instead of a separate prop 2025-06-03 19:03:36 -04:00
Sean Morley
50a80a8116 fix: improve error handling for Immich image fetching and processing 2025-06-03 18:11:29 -04:00
Sean Morley
442a7724a0 feat: enhance Immich integration to support image downloading for shared users and improve access control for adventure images 2025-06-03 17:59:29 -04:00
Sean Morley
b336a24401 feat: enhance AdventureImageSerializer to support Immich integration and improve image URL handling 2025-06-03 17:16:28 -04:00
Sean Morley
45e195a84e feat: update Immich integration to use dynamic image URLs and enhance image retrieval logic 2025-06-02 21:25:07 -04:00
Sean Morley
937db00226 feat: add Google Maps integration with description and display status in settings 2025-06-01 23:09:48 -04:00
Sean Morley
0838a41156 feat: add unique constraint for immich_id per user in AdventureImage model and enhance Immich integration image retrieval 2025-06-01 22:50:26 -04:00
Sean Morley
06787bccf6 feat: enhance Immich integration with local copy option and validation for image handling 2025-06-01 19:55:12 -04:00
Sean Morley
f95afdc35c fix: update location blocks for protected media in nginx configuration 2025-06-01 12:47:08 -04:00
Sean Morley
c159e176b3 refactor: improve formatting and organization in nginx configuration 2025-05-31 21:54:45 -04:00
Sean Morley
b50447b1a2 refactor: clean up comments and improve readability in nginx configuration 2025-05-31 21:36:23 -04:00
Sean Morley
92f9bf6908 fix: standardize quotes in language object and improve dropdown z-index 2025-05-31 21:10:42 -04:00
Sean Morley
48df9ee56d
Update backend/server/main/settings.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-31 21:08:33 -04:00
Sean Morley
64bfda6bf0
Update backend/nginx.conf
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-31 21:08:20 -04:00
Sean Morley
8f1c60a440
Update backend/server/users/backends.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-31 21:08:09 -04:00
Sean Morley
806efd71bf feat: secure CSRF cookie based on frontend URL protocol 2025-05-31 17:30:33 -04:00
Sean Morley
e89f2a947e feat: enhance image navigation in adventure view with improved controls and indicators 2025-05-31 16:20:24 -04:00
Sean Morley
724aec1f3a feat: update get_num_visits method to improve user visit count retrieval 2025-05-31 15:46:15 -04:00
Sean Morley
514ee85767 feat: add distance calculation to Transportation model and update TransportationCard to display distance in km and miles 2025-05-30 12:33:30 -04:00
Sean Morley
53d370297e feat: enhance adventure save method to support skipping geocode threading 2025-05-29 21:04:07 -04:00
Sean Morley
ae16c12251 feat: update Docker Compose and environment configuration for dynamic port handling 2025-05-29 20:21:13 -04:00
Sean Morley
4e8024051c feat: implement background geocoding for adventure locations on save 2025-05-29 18:17:15 -04:00
Sean Morley
9d69935f22
Update backend/nginx.conf
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-29 18:02:05 -04:00
Sean Morley
787ac4a8b3 feat: add new translations for 'joined' and 'view_profile' in German locale 2025-05-29 17:48:02 -04:00
Sean Morley
81006af027 refactor: enhance UI components with improved styling and layout
- Updated CollectionCard, CountryCard, LodgingCard, NoteCard, RegionCard, TransportationCard, UserCard, and ShareModal components for better visual consistency and responsiveness.
- Introduced hover effects and transitions for a more interactive experience.
- Improved accessibility by ensuring proper alt text for images and using semantic HTML elements.
- Refactored date formatting logic into a utility function for reuse across components.
- Added new translations for profile viewing and joined date in the localization files.
2025-05-29 17:47:58 -04:00
Sean Morley
3acfc9f228 Fix banner formatting in AdventureLog installer script for improved aesthetics 2025-05-29 09:52:41 -04:00
Sean Morley
8be723b9ad Refactor authentication backends to enhance NoPasswordAuthBackend functionality; integrate Allauth for email login and improve password handling logic. 2025-05-28 12:21:43 -04:00
Sean Morley
a7128756bd Reorder authentication backends to prioritize NoPasswordAuthBackend while retaining Allauth and ModelBackend 2025-05-28 11:54:21 -04:00
Sean Morley
d41d46f15d Remove commented-out logging statements in login handling for cleaner code 2025-05-28 11:52:53 -04:00
Sean Morley
0adfdfa62f Refactor login cookie handling to improve parsing and error handling; ensure multiple Set-Cookie headers are processed correctly. 2025-05-28 11:43:03 -04:00
Sean Morley
fa52af8ad1 Add detailed logging for login process and cookie handling 2025-05-28 11:31:38 -04:00
Sean Morley
6be4acb196 Add authentication backends for Allauth and Django's ModelBackend 2025-05-28 11:06:21 -04:00
Sean Morley
9df0338c3d Add new translations for distance, itinerary, and linked items in multiple languages; update UI to reflect new localization keys. 2025-05-28 10:51:26 -04:00
Sean Morley
069bcfb58a Enhance dynamic port handling in installer and configuration files; update .env.example and docker-compose.yml to use environment variables for frontend and backend ports. 2025-05-27 22:42:08 -04:00
Sean Morley
1551fba9ab Refactor migration to set end_date only if start_date is present; improve data integrity during migration process. 2025-05-27 12:43:29 -04:00
Sean Morley
56b8b55b84 Refactor deleteCollection function to use DELETE method for API call; update endpoint to match new API structure. 2025-05-27 12:39:38 -04:00
Sean Morley
57aa2c9916 Enhance download-countries command with temporary SQLite database for efficient data processing; add batch size argument for improved performance and memory management. 2025-05-27 10:12:33 -04:00
Sean Morley
37866660d3 Adjust batch size in download-countries command for improved processing efficiency; update installer banner for better visual alignment. 2025-05-27 09:59:15 -04:00
Sean Morley
d34a9001c0 Enhance AdventureLog installer with Docker container check; improve service readiness feedback and success message formatting. 2025-05-26 21:09:00 -04:00
Sean Morley
bcd1f02131 Enhance download-countries command with batch processing feedback; improve logging for countries, regions, and cities processing. 2025-05-26 20:43:11 -04:00
Sean Morley
f2246921d4 Refactor download-countries command for improved memory efficiency and batch processing; enhance data import logic for countries, regions, and cities. 2025-05-26 20:39:24 -04:00
Sean Morley
575669aedf Refactor batch processing in download-countries command for clarity and safety; enhance service wait function in installer script by removing unnecessary whitespace. 2025-05-26 18:06:06 -04:00
Sean Morley
9eaaadc0f2 Enhance entrypoint script with a helper function for environment variable retrieval; improve PostgreSQL connection logic. Update installer script to add spacing for readability in service wait function. 2025-05-26 17:36:22 -04:00
Sean Morley
3f6aa67b3f Refactor database configuration to use a helper function for environment variables, improving compatibility with legacy setups. 2025-05-26 17:18:30 -04:00
Sean Morley
5e6d5305cc Update docker-compose to use beta images for frontend and backend services; clean up install script 2025-05-26 17:14:12 -04:00
Sean Morley
4fb25f63fd Update .env.example with additional optional configurations and change docker-compose to use development images for frontend and backend services 2025-05-26 17:07:52 -04:00
Sean Morley
40f54529a4 Refactor environment variable names in .env.example and settings.py for consistency; add install script for streamlined setup 2025-05-26 16:55:00 -04:00
Sean Morley
7707ac7769 Add .env.example and update docker-compose to use env_file for configuration 2025-05-26 14:11:31 -04:00
Sean Morley
b25bf4af27 Add Google Maps integration documentation and update docker-compose for API key 2025-05-26 13:59:18 -04:00
Sean Morley
f355ba48e2 Fix condition for Google Maps integration check in IntegrationView 2025-05-25 22:25:36 -04:00
Sean Morley
e56335d30f Refactor geocoding and integration handling: remove debug print, streamline reverse geocoding logic, and enhance integration response structure 2025-05-25 22:13:18 -04:00
Sean Morley
c123231bab Optimize country data import process: increase batch size, implement memory management, and streamline record creation and updates 2025-05-24 19:05:16 -04:00
Sean Morley
9e304f81fe Fix date display logic to handle undefined visit dates in DateRangeCollapse and adventure page 2025-05-24 18:56:59 -04:00
Sean Morley
1997f164b8 Handle exceptions in search method with a generic error message 2025-05-24 18:08:58 -04:00
Sean Morley
c0b9013576 Remove OpenStreetMap button from recommendations in collections page 2025-05-24 18:06:25 -04:00
Sean Morley
ce9faa28f8 Refactor recommendations feature: add RecommendationsViewSet, update routing, and remove OverpassViewSet 2025-05-24 18:00:05 -04:00
Sean Morley
68ba3c4b4d Add Google Maps API integration for geocoding and reverse geocoding functionality 2025-05-24 14:59:58 -04:00
Sean Morley
042d034594 Implement reverse geocoding search functionality and update type definitions 2025-05-24 14:12:06 -04:00
Sean Morley
ec2b285d50 Remove logging for error handling in reverse_geocode function 2025-05-23 23:17:47 -04:00
Sean Morley
03c76adc6d Merge branch 'development' of github.com:seanmorley15/AdventureLog into development 2025-05-23 23:15:41 -04:00
Sean Morley
4404064263 Adjust heading margin for improved layout on adventures page 2025-05-23 23:15:39 -04:00
Sean Morley
7fddca6fb0
Potential fix for code scanning alert no. 16: Information exposure through an exception
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-05-23 23:13:31 -04:00
Sean Morley
809cf98169 Enhance AdventureCard component with activity type display and rating system; update CardCarousel gradient fallback for no images 2025-05-23 23:00:15 -04:00
Sean Morley
d3d74f9f35 Add localization support for adventure and settings pages; enhance UI elements
- Updated Chinese translations in zh.json for various UI components including coordinates, sun times, and authentication settings.
- Refactored adventure page to utilize localization for visit counts, descriptions, and other text elements.
- Improved settings page by integrating localization for profile, security, email management, and integration sections.
- Enhanced visual consistency by updating card backgrounds and adding localized text for buttons and labels.
2025-05-23 17:22:28 -04:00
Sean Morley
e856a57498 Refactor code structure for improved readability and maintainability 2025-05-23 12:15:44 -04:00
Sean Morley
0d5792a99a Refactor adventure page layout and enhance UI components
- Updated the layout of the adventure page to improve visual hierarchy and user experience.
- Reorganized the structure of the hero section, including image navigation and quick info cards.
- Enhanced the display of adventure details, including user info, visit history, and sun times.
- Improved the handling of attachments and external links with better styling and accessibility.
- Added functionality for copying coordinates and links to the clipboard.
- Refined the map section to provide clearer location information and improved marker display.
- Cleaned up unused imports and optimized the code for better readability.
2025-05-23 11:23:24 -04:00
Sean Morley
2ccb8f5e0b Enhance geocoding functionality with host resolution and improved error handling; update AdventureModal for loading state management; refine LocationDropdown toast display logic; adjust world travel page for better documentation link visibility. 2025-05-23 10:46:37 -04:00
Sean Morley
0d800e8986
Merge pull request #587 from lkiesow/bool-case-sensitivity
Make boolean settings case insensitive
2025-05-22 21:22:36 -04:00
Sean Morley
3d9f4545a1
Merge branch 'development' into bool-case-sensitivity 2025-05-22 21:22:27 -04:00
Sean Morley
0a7db8985d
Merge pull request #608 from seanmorley15/dependabot/npm_and_yarn/documentation/npm_and_yarn-1b547a5c31
chore(deps): bump the npm_and_yarn group across 2 directories with 1 update
2025-05-22 21:19:41 -04:00
Sean Morley
c828f86570
Change note preview to render markdown content 2025-05-22 21:17:21 -04:00
Sean Morley
a1062e72cf Enhance Adventure model and serializers with visited status logic and toast notifications for marking visits 2025-05-22 21:13:31 -04:00
Sean Morley
84cd136401 Add bulk geocoding command and trigger geocoding action in admin 2025-05-22 20:29:05 -04:00
Sean Morley
ac32f9ac5b Add country field to AdventureSerializer with country code retrieval 2025-05-22 20:13:36 -04:00
Sean Morley
d52e302e9b Add geocoding functionality and enhance Adventure model with location fields 2025-05-22 20:05:13 -04:00
Sean Morley
14e71626f6 Enhance Nginx configuration for protected media files with PDF handling 2025-05-22 16:26:50 -04:00
Sean Morley
f96b6f5f65
Merge pull request #556 from janausis/oicd_login_fix
Frontend OICD Login Fix
2025-05-22 11:07:43 -04:00
Sean Morley
5dfe39468a
Add link to Mastodon profile in VitePress config 2025-05-21 14:14:59 -04:00
Sean Morley
a991f54d0a Add link to Mastodon profile in VitePress config 2025-05-21 14:14:39 -04:00
Florian Meinicke
274dafc47d Change note preview to render markdown content
in the Collection/Itinerary view

Closes #627
2025-05-21 07:33:20 +00:00
Sean Morley
5f19670ed9
Fixes [BUG] Cannot change a adventure from Private to Public #617 2025-05-19 11:52:51 -04:00
Sean Morley
bd9f3fc494
Update backend/server/adventures/views/adventure_view.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-19 11:28:04 -04:00
Sean Morley
d87d0e807f Fixes [BUG] Cannot change a adventure from Private to Public #617 2025-05-19 11:21:40 -04:00
Sean Morley
8b41a0f5a4
Update date validation to use UTC comparison and enhance documentation 2025-05-13 12:55:06 -04:00
Sean Morley
9435ccfa5a Add arrival and departure date labels to localization files 2025-05-13 12:54:06 -04:00
Sean Morley
890332f4b6 Update date validation to use UTC comparison and enhance documentation 2025-05-13 12:50:43 -04:00
Sean Morley
049c229799
Merge pull request #614 from seanmorley15/development
Development
2025-05-12 10:46:02 -04:00
Sean Morley
f15d7bfd1e Remove debug print statements from DisableCSRFForMobileLoginSignup middleware 2025-05-12 10:42:50 -04:00
Sean Morley
7c3c139e61 Add DisableCSRFForMobileLoginSignup middleware to handle CSRF checks for mobile login/signup requests 2025-05-12 10:42:26 -04:00
Sean Morley
1ff116ed00
Merge pull request #611 from seanmorley15/development
Adjust CollectionCard styles: remove overflow-hidden and increase dro…
2025-05-11 12:44:05 -04:00
Sean Morley
b0e8c025fc Adjust CollectionCard styles: remove overflow-hidden and increase dropdown width 2025-05-11 12:43:31 -04:00
Sean Morley
feeb682e14
Merge pull request #610 from seanmorley15/development
Replace crypto.randomUUID with a unique ID generation method for visi…
2025-05-11 11:50:58 -04:00
Sean Morley
6de737bbf8 Replace crypto.randomUUID with a unique ID generation method for visit objects and timezone selector instance 2025-05-11 11:50:29 -04:00
dependabot[bot]
6ca674ff7e
chore(deps): bump the npm_and_yarn group across 2 directories with 1 update
Bumps the npm_and_yarn group with 1 update in the /documentation directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 5.4.14 to 5.4.19
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.19/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.19/packages/vite)

Updates `vite` from 5.4.18 to 5.4.19
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.19/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.19/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.19
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 5.4.19
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-11 02:21:09 +00:00
Sean Morley
908d31a4a3
Adventure Times, Collection Ordering, Trip Maps 2025-05-10 22:19:43 -04:00
Sean Morley
4af80eb584 Add arrival and departure timezone translations in multiple languages 2025-05-10 22:10:55 -04:00
Sean Morley
d9d754a87c Fix date handling in DateRangeCollapse and TransportationModal; improve hash change handling in +page.svelte 2025-05-10 22:03:31 -04:00
Sean Morley
abc2d86dcf Enhance visit date display with timezone support and improved formatting 2025-05-10 21:40:18 -04:00
Sean Morley
330fabb3e0 Fix validation in Lodging model to check check-in and check-out dates; update LodgingCard to conditionally display timezone 2025-05-10 13:17:01 -04:00
Sean Morley
8538aa0b7c Validate ISO date in toLocalDatetime function and return empty string for invalid dates 2025-05-10 13:02:44 -04:00
Sean Morley
c9fa1d55f7 Refactor toLocalDatetime function to improve ISO date handling and return format 2025-05-10 13:02:06 -04:00
Sean Morley
ab189c8aff Improve UTC date formatting by adding validation for ISO date strings in formatUTCDate function 2025-05-10 12:49:43 -04:00
Sean Morley
1323d91e32 Add timezone support for visits, transportation, and lodging
- Introduced TIMEZONES constant in models.py to store valid timezone options.
- Updated Visit, Transportation, and Lodging models to include timezone fields.
- Modified serializers to include timezone fields in VisitSerializer, TransportationSerializer, and LodgingSerializer.
- Enhanced DateRangeCollapse component to handle timezone selection and formatting.
- Implemented timezone formatting functions in LodgingCard and TransportationCard components.
- Updated LodgingModal and TransportationModal to bind timezone data.
- Added VALID_TIMEZONES to dateUtils for consistent timezone management across the application.
2025-05-10 11:59:56 -04:00
Sean Morley
b30d6df964 Enhance timezone handling in AdventureModal and DateRangeCollapse components; add support for departure and arrival timezones in the TimezoneSelector and update localization for new timezone labels. 2025-05-10 10:47:00 -04:00
Sean Morley
89c4f1058a Fix date formatting for constraint dates in DateRangeCollapse component 2025-05-09 23:33:58 -04:00
Sean Morley
13bc748d0d Merge branch 'development' of github.com:seanmorley15/AdventureLog into development 2025-05-09 23:27:54 -04:00
Sean Morley
c6177c5a05 Refactor DateRangeCollapse component layout and improve styling 2025-05-09 23:27:53 -04:00
Sean Morley
e9c333642f
Merge pull request #589 from lkiesow/sm-hide-al
Move hiding AdventureLog to first Tailwind breakpoint
2025-05-09 23:13:08 -04:00
Sean Morley
07c0c36ab8
Potential fix for code scanning alert no. 14: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-05-09 21:35:33 -04:00
Sean Morley
b712d10d7e
Potential fix for code scanning alert no. 13: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-05-09 21:35:16 -04:00
Sean Morley
a5e44d29e6
Merge pull request #594 from lkiesow/mobile-map-full-width
Full width map on mobile
2025-05-09 21:33:29 -04:00
Sean Morley
a91018d792
Merge pull request #572 from lkiesow/theme-picker
Harmonize language and theme picker interface
2025-05-09 21:32:47 -04:00
Sean Morley
9f86688fe9 Merge branch 'development' of github.com:seanmorley15/AdventureLog into development 2025-05-09 21:31:39 -04:00
Sean Morley
4ce7ed7045 Improve layout by wrapping map link text in a paragraph element 2025-05-09 21:31:37 -04:00
Sean Morley
b74fe90512
Merge pull request #586 from lkiesow/collection-dropdown
Collection view selection on mobile devices
2025-05-09 21:30:54 -04:00
Sean Morley
73c664e549
Merge pull request #569 from lkiesow/note-preview
Show Note Preview on Card
2025-05-09 21:29:51 -04:00
Sean Morley
5e1f17fe2e
Merge pull request #591 from lkiesow/menu-text-size
Adjust main menu font size
2025-05-09 21:27:44 -04:00
Sean Morley
f3f75368df
Merge pull request #593 from lkiesow/google-maps
Open in Apple Maps or Google Maps
2025-05-09 21:19:58 -04:00
Sean Morley
ab12a2e7d8
Merge pull request #588 from lkiesow/collection-card-width
Unify collection card width
2025-05-09 21:19:20 -04:00
Sean Morley
cbb8c6283b
Merge pull request #600 from theshaun/patch-1
Update en.json - Correct spelling of Search
2025-05-09 21:18:28 -04:00
Sean Morley
0af0218a34
Merge branch 'development' into patch-1 2025-05-09 21:18:13 -04:00
Sean Morley
04f9227ae6 Add default category icon and improve visit display:
- Set default icon for empty category in AdventureModal
- Enhance layout for visit buttons and validation messages in DateRangeCollapse
- Update localization files to include "no visits" strings in multiple languages
2025-05-09 21:17:11 -04:00
Sean Morley
3caebd37dd Add additional localization strings for itinerary features in Polish, Swedish, and Chinese
- Added "additional_info", "invalid_date_range", "sunrise_sunset", and "timezone" keys to pl.json, sv.json, and zh.json.
- Updated existing strings for consistency across languages.
2025-05-09 15:59:48 -04:00
Sean Morley
311e2847cb Enhance visit display: Improve layout and formatting of visit dates and notes 2025-05-09 15:19:17 -04:00
Sean Morley
2c50ca0b1a Refactor date handling components: Replace DateRangeDropdown with DateRangeCollapse
- Introduced DateRangeCollapse.svelte to manage date range selection with timezone support.
- Removed DateRangeDropdown.svelte as it was redundant.
- Updated LodgingModal and TransportationModal to utilize DateRangeCollapse for date selection.
- Enhanced date conversion utilities to handle all-day events correctly.
- Adjusted TimezoneSelector for improved accessibility and focus management.
- Updated date handling logic in dateUtils.ts to support all-day events.
- Modified test page to reflect changes in date range component usage.
2025-05-09 10:24:29 -04:00
Lars Kiesow
bbad7b890c
Add support for OpenStreetMap
This patch adds an option to open an item in OpenStreetMap as well as in
Google Maps and Apple Maps.
2025-05-07 21:11:42 +02:00
Sean Morley
5c109bbbaf
Update config.mts 2025-05-06 19:40:36 -04:00
Sean Morley
c93d4865ce
Update config.mts 2025-05-06 16:57:32 -04:00
Sean Morley
8d0490fd81 fix: Update pageData check to use 'index.md' for JSON-LD transformation 2025-05-06 14:50:39 -04:00
Sean Morley
77be046f19 feat: Add JSON-LD structured data for homepage SEO enhancement 2025-05-06 14:47:03 -04:00
Sean Morley
827b150965 Merge branch 'development' of github.com:seanmorley15/AdventureLog into development 2025-05-06 14:38:33 -04:00
Sean Morley
7442bd70cd Update version to 0.9.0, add DateRangeDropdown component, enhance LodgingModal with price step, and add invalid date range message 2025-05-06 14:38:31 -04:00
Shaun Walker
645cc9728b
Update en.json - Correct spelling of Search 2025-05-03 20:50:06 +10:00
Sean Morley
d99ca18e1c
Merge pull request #590 from lkiesow/remove-invisible-mapmarker
Remove Invisible MapMarker
2025-04-30 22:29:27 -04:00
Sean Morley
39104b3fef
Merge pull request #595 from lesensei/development
Update fr.json
2025-04-27 12:14:31 -04:00
lesensei
5bdadd0f88
Merge pull request #1 from lesensei/fr-translations-update-1
Update fr.json
2025-04-27 18:07:26 +02:00
lesensei
f1f1cda799
Update fr.json 2025-04-27 18:05:27 +02:00
Lars Kiesow
f31db982ce
Full width map on mobile
This patch lets the map in the adventure details use the full screen
width on mobile instead of having one sixth of the screen empty.
2025-04-27 17:49:31 +02:00
Lars Kiesow
f6097a2d60
Open in Apple Maps or Google Maps
This patch provides the additional option to open a location in Google
maps as an alternative to Apple Maps.
2025-04-27 17:42:51 +02:00
Lars Kiesow
5f7bf52758
Adjust main menu font size
This patch slightly adjusts the font size of list items in the main
menu. I probably wouldn't mind making it even a bit bigger, but that's
probably worth a separate discussion.

Reasons for this adjustment:

1. Don't use different font sizes in the same lement.
   While the buttons rendered at 14px, the search text rendered at 16px.
   They should have the same font-size.

2. The buttons were below the base text size controlled by Tailwind CSS.
   This means, it also puts the font size below the recommended
   font-size for mobile devices (Google/Apple guidelines). This patch
   puts them at the base level.
2025-04-27 16:08:03 +02:00
Lars Kiesow
911ce67d9f
Remove Invisible MapMarker
This patch removes the map marker from the adventures list item of the
main menu dropdown. It is not being rendered and given that all other
elements do not have an icon, it is probably a remnant of old code and
left by accident.
2025-04-27 16:03:16 +02:00
Lars Kiesow
932036bc8b
Move hiding AdventureLog to first Tailwind breakpoint
This is a slight improvement to pull request #576. I noticed that on a
tablet, the AdventureLog in the navigation bar would not render, even
though there would be enpugh free space. This patch moves the breakpoint
for hiding the text one step further towards smaller devices.
2025-04-27 15:41:43 +02:00
Lars Kiesow
cd494fefee Unify collection card width
The width of collection cards can vary quite a bit. Additionally, the
cards look different than the cards within a collection. It would be
nice if the design would be more or leyy the same for all of them.

This patch adjusts the collection card design by adapting the classes
from the cards within the collection (I literally just copied the
classes from ChecklistCard). This is another step in making the user
interface more homogeneous.
2025-04-27 15:30:12 +02:00
Lars Kiesow
5136122ed9
Make boolean settings case insensitive
Having to use the Python syntax when it comes to case sensitivity for
booleans in environment variables can be unexpected and doesn't really
provide any benefit.

This patch makes all boolean settings case-insensitive. This means that,
for example, both `True` and `true` evaluate to `True` in Python.

This fixes #559
2025-04-26 23:27:25 +02:00
Lars Kiesow
e40ea028d0
Collection view selection on mobile devices
The tab-based selection of views in a collection doesn't really work on
mobile devices since it needs too much horizontal space. This leads to
text overflowing buttons as well as half of the tab bar disappearing
behind the right edge of the phone screen.

This patch modifies the navigation by keeping the current tabs in
desktop mode, but switching to a very compact dropdown on mobile
devices.

The functionality of both elements is identical.
2025-04-26 22:53:45 +02:00
Sean Morley
56bbbb0ffb Implement code changes to enhance functionality and improve performance 2025-04-26 11:22:17 -04:00
Sean Morley
228f79dbc3
Merge pull request #585 from lkiesow/node22
Upgrade Node.js to Version 22
2025-04-25 10:02:52 -04:00
Sean Morley
e44c153f9b
Merge pull request #576 from lkiesow/mobile-navbar
Improve overlapping on navbar on mobile devices
2025-04-25 10:00:49 -04:00
Lars Kiesow
3a8776c000
Show AdventureLog icon instead of text in mobile mode
This patch makes AdventureLog hide the text but show the app icon in
the navigation bar when in mobile mode.
2025-04-25 15:56:30 +02:00
Lars Kiesow
b8aa96b5b3
Improve overlapping on navbar on mobile devices
This patch makes it less likely for elements of the navigation bar to
overlap each other on mobile devices. It also makes spacing a bit more
homogeneous.

The patch basically just adjust some spacing as and hides the map icon
on mobile devices.
2025-04-25 15:56:20 +02:00
Lars Kiesow
be0e56728a
Upgrade Node.js to Version 22
This patch updates the Dockerfile to use Node.js 22 for running the
frontend. Given that the security support of Node.js 18 ends in 6 days
(30 Apr 2025), upgrading definitely makes sense.

Additionally, this also seems to fix the broken JSON generated by the
recently upgrraded version of @sveltejs/kit, thus fixing #584.
2025-04-25 14:19:07 +02:00
Sean Morley
85b4db87ec refactor: Simplify date handling by replacing updateLocalDates and updateUTCDates with updateLocalDate and updateUTCDate functions 2025-04-19 21:53:17 -04:00
Sean Morley
c12f94855d feat: Refactor date handling in TransportationModal and add utility functions for date conversion and validation 2025-04-19 21:44:40 -04:00
Sean Morley
6942f5e1bb feat: Add TimezoneSelector component and integrate Luxon for date handling 2025-04-18 23:06:36 -04:00
Sean Morley
7499722867 feat: Add server error handling and SVG asset for 500 error page 2025-04-18 21:55:30 -04:00
Sean Morley
1ea4022e80 Merge branch 'development' of github.com:seanmorley15/AdventureLog into development 2025-04-18 15:04:10 -04:00
Sean Morley
a8502884dc feat: Add tutorial video section to usage documentation 2025-04-18 15:04:04 -04:00
Sean Morley
b29461f29d
Merge pull request #575 from lkiesow/frontend-build-test
Simple Frontend Build Test
2025-04-18 11:51:49 -04:00
Sean Morley
983a038420
Update frontend-test.yml 2025-04-18 11:50:33 -04:00
Sean Morley
233d2be63f
Merge pull request #574 from lkiesow/transportation-card-badges
Prevent UI overlaps in transportation card
2025-04-18 11:49:52 -04:00
Sean Morley
64377c0300
Merge pull request #566 from lkiesow/dropdown-size
Fix rendering issue in adventure dropdown
2025-04-18 11:48:39 -04:00
Lars Kiesow
0f36f34bfb
Simple Frontend Build Test
This patch adds a simple test for checking if the frontend builds
properly. This is similar to the recently added backend check and will
be run automatically on pushes and pull requests.
2025-04-18 00:48:53 +02:00
Lars Kiesow
ea85c5fc5a Prevent UI overlaps in transportation card
Similar to #552 (658764f) the card title and badges of the
transportation card can overlap. This patch adjusts the transportation
card to list the badges below the title similar to the other cards.
2025-04-18 00:22:50 +02:00
Lars Kiesow
9a825e56e4 Harmonize language and theme picker interface
This patch adjusts the theme picker to more look and feel like the
language picker right next to it.
2025-04-16 00:45:29 +02:00
Sean Morley
9043ee9565
Merge pull request #570 from seanmorley15/dependabot/npm_and_yarn/frontend/npm_and_yarn-d34ec6d50c
chore(deps-dev): bump @sveltejs/kit from 2.8.3 to 2.20.6 in /frontend in the npm_and_yarn group across 1 directory
2025-04-15 09:33:39 -04:00
dependabot[bot]
a11400fa98
chore(deps-dev): bump @sveltejs/kit
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit).


Updates `@sveltejs/kit` from 2.8.3 to 2.20.6
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.20.6/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-version: 2.20.6
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 23:21:27 +00:00
Lars Kiesow
44a260b5b6 Show Note preview on Card
Often, you end up having short notes or even just a simple link and it
is somewhat tedious to go into the details to retrieve the additional
information.

This patch displays a preview of the note content and a maximum of three
links on the notes card directly. This makes accessing information much
faster.

This fixes #562.
2025-04-15 01:05:02 +02:00
Lars Kiesow
49f7bf27e8 Fix rendering issue in adventure dropdown
This is a very simple patch fixing the rendering issue of the “Remove
from collection” option in the adventure dropdown.-

Together with #552, this should fix #539.
2025-04-14 18:44:15 +02:00
Sean Morley
dd01ada61e
Update docker.md
Fixes #560
2025-04-14 11:22:20 -04:00
Sean Morley
a974c8c275
Merge pull request #558 from lkiesow/test
Basic Integration Test for backend
2025-04-14 11:12:59 -04:00
Sean Morley
988cdc12e6
Merge pull request #557 from lkiesow/python-syntax-error
Fix Python Syntax Error
2025-04-14 11:11:48 -04:00
Lars Kiesow
548d43b563 Basic Integration Test for backend
This patch implements a very basic test for commits and pull requests to
be run on GitHub Actions. This does not yet check much, but would have
caught something like the recent syntax error.
2025-04-12 01:07:28 +02:00
Lars Kiesow
23426012af Fix Python Syntax Error
Commit 937c3c6a68 introduced a Python
syntax error, breaking the server. This fixes the issue by restoring the
probably accidental removal of one line of code.
2025-04-11 20:18:14 +02:00
Jannis Martensen
2b031f51ac fixed oicd not working on frontend login page 2025-04-11 12:35:05 +02:00
Sean Morley
d7496df100
Merge pull request #554 from andreatitolo/development
Adjusted Italian translation
2025-04-08 15:32:44 -04:00
Andrea Titolo
6f489b2734
Consistency in translating collections 2025-04-08 20:49:49 +02:00
Andrea Titolo
a09b3f379f
Update italian translation and terms 2025-04-08 19:26:11 +02:00
Sean Morley
937c3c6a68
Update backend/server/adventures/views/collection_view.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-07 19:17:26 -04:00
Sean Morley
43b8275fc1
Update documentation/docs/install/caddy.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-07 19:16:58 -04:00
Sean Morley
3b5240dffe
Update documentation/docs/troubleshooting/login_unresponsive.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-07 19:14:55 -04:00
Sean Morley
d2933854ff
Update documentation/docs/usage/usage.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-07 19:14:04 -04:00
Sean Morley
0ce101594f
Merge pull request #550 from lkiesow/collection-browser-navigation
Enable Browser Navigation in Collections
2025-04-06 20:27:25 -04:00
Sean Morley
b166058d36
Merge pull request #551 from larsl-net/dual-stack
fix: Reenable IPv6 for Backend
2025-04-06 20:26:36 -04:00
Sean Morley
15d34c88dc
Merge pull request #552 from lkiesow/lodging-card
Prevent UI overlaps in lodging card
2025-04-06 20:25:11 -04:00
Sean Morley
af2778ff61
Merge pull request #548 from lkiesow/users-reedirect
Prevent unnecessary redirect when requesting users
2025-04-06 20:23:53 -04:00
Sean Morley
2982a4044d
Merge pull request #547 from lkiesow/caddy
Document deployment using Caddy
2025-04-06 20:22:53 -04:00
Lars Kiesow
658764fb58
Prevent UI overlaps in lodging card
This patch prevents overlapping UI eelements in the ledging card. It
adjusts the UI to be more like the adventure cards when it comes to font
size and element placing.

This partly fixes #539
2025-04-07 01:42:12 +02:00
Lars Lehmann
847193a0ae
fix: Reenable IPv6 for Backend 2025-04-06 22:55:13 +02:00
Lars Kiesow
8531855f46 Enable Browser Navigation in Collections
If you are in a collection – e.g on “All Linked Items” – and go into an
adventure, going back will cause you to end up on the itinerary. This is
quite annoying if you have a number of options for a trip linked already
but whenever you go back, you have to scroll up again, click on “All
Linked Items”, and scroll down again to get to the next adventure in
line.

This patch makes AdventureLog remember the tab you were in and going
back and forth in the browser history will actually work.
2025-04-06 21:09:27 +02:00
Lars Kiesow
af8a9acbae
Prevent unnecessary redirect when requesting users
Opening the share dialog, the frontend is requesting `/auth/users/`
which is always redirected to `/auth/users`. That's an unnecessary extra
step.

This patch makes the front-end request `/auth/users` in the first place.
2025-04-06 14:37:37 +02:00
Lars Kiesow
8041f67ba1
Document deployment using Caddy
This patch adds documentation on how to run AdventureLog with Caddy as a
reverse proxy.

Signed-off-by: Lars Kiesow <lkiesow@uos.de>
2025-04-06 12:32:19 +02:00
Sean Morley
cd833884e8
Merge pull request #546 from eidsheim98/nb-locale
Norwegian translations
2025-04-01 16:41:26 -04:00
Nikolai Eidsheim
47c219affd Added Norwegian translation 2025-04-01 22:35:03 +02:00
Nikolai Eidsheim
cde293c4bd Merge remote-tracking branch 'origin/main' into nb-locale
Updating nb-locale branch
2025-04-01 21:03:47 +02:00
Sean Morley
04c3402e14 fix: Remove duplicate file extension validation for md and pdf formats 2025-03-23 16:40:55 -04:00
Sean Morley
b4c5e22662 fix: Correct file extension validation for gpx, md, and pdf formats 2025-03-23 16:40:08 -04:00
Sean Morley
7d5750049b Merge branch 'development' of github.com:seanmorley15/AdventureLog into development 2025-03-22 12:25:55 -04:00
Sean Morley
16a7772003 feat: Add additional adventure type and endpoint for sunrise/sunset information 2025-03-22 12:25:53 -04:00
Sean Morley
731b7e9db1
chore(docker): Fix supervisor logging to stdout 2025-03-22 10:21:26 -04:00
ClumsyAdmin
ca4ef79837 fix supervisor logging 2025-03-21 20:51:58 -04:00
Sean Morley
13d3b24ec2 chore: Reduce Gunicorn worker count from 4 to 2 for optimized resource usage 2025-03-21 20:27:46 -04:00
ClumsyAdmin
113d41ca30 Merge branch 'main' of github.com:ClumsyAdmin/AdventureLog 2025-03-21 20:25:40 -04:00
ClumsyAdmin
b71109fd09 potential fix: set Supervisor priorities to ensure Gunicorn starts before nginx to prevent 502 errors 2025-03-21 20:25:25 -04:00
Sean Morley
a3cd940065 feat: Pass collection data to adventure, transportation, lodging, and checklist components 2025-03-21 17:35:58 -04:00
Sean Morley
6eebd5b70a
chore(docker): Refactor service startup to use supervisord and foreground processes 2025-03-21 17:34:23 -04:00
Sean Morley
44ede92b92
Merge branch 'development' into main 2025-03-21 17:34:03 -04:00
Sean Morley
fe25f8e2c8 feat: Add start_date to collection ordering and enhance localization for itinerary features 2025-03-21 17:31:33 -04:00
Sean Morley
794df82ec6 feat: Enhance AdventureModal date handling for all-day events and improve localization in collections page 2025-03-21 16:30:03 -04:00
Sean Morley
db63b6e7d8 feat: Add usage guide for AdventureLog and enhance overview with personal message from the maintainer 2025-03-21 13:35:29 -04:00
ClumsyAdmin
4ccfa6e42c refactor docker startup to use supervisord 2025-03-21 12:02:23 -04:00
Sean Morley
f79b06f6b3 feat: Add troubleshooting guide for unresponsive login and registration, enhance collection modal alerts, and improve localization for itinerary features 2025-03-20 22:28:23 -04:00
Sean Morley
1042a3edcc refactor: Remove debug print statement from NoPasswordAuthBackend authentication method 2025-03-20 22:08:22 -04:00
Sean Morley
771579ef3d feat: Improve adventure date grouping to handle all-day events and enhance date formatting 2025-03-20 10:26:41 -04:00
Sean Morley
dbd417ca87
fix: Pass collection prop to various card components in the collections page 2025-03-19 13:50:32 -04:00
Sean Morley
6e1fbbfc3a feat: Pass collection prop to various card components in the collections page 2025-03-19 13:49:46 -04:00
Sean Morley
76ee4c73e0 feat: Add changelog for v0.9.0 and update configuration to include it in the documentation 2025-03-19 09:58:53 -04:00
Sean Morley
c402dff9a7 chore: Bump app version to v0.9.0 and update changelog link 2025-03-19 09:29:35 -04:00
Sean Morley
f554bb8777 feat: Enhance date handling in AdventureModal and related components for improved localization and all-day event support 2025-03-18 21:07:34 -04:00
Sean Morley
876c5e83b4 feat: Implement chronological itinerary path visualization with GeoJSON for adventures, transportation, and lodging 2025-03-18 18:16:25 -04:00
Sean Morley
1dc8e10758 feat: Add "all day" localization strings for multiple languages and enhance transportation date handling 2025-03-18 17:40:32 -04:00
Sean Morley
9fd2a142cb feat: Update Visit model to use DateTimeField for start and end dates, and enhance AdventureModal with datetime-local inputs 2025-03-18 14:04:31 -04:00
Sean Morley
6eb57d1d6e
Add Lodging to Map, Sanitize MD, Optional Password Disable 2025-03-17 15:20:44 -04:00
Sean Morley
6f720a154f feat: Add password disable functionality and update localization files 2025-03-17 14:40:33 -04:00
Sean Morley
585f050a30 feat: Add disable_password field to CustomUserDetailsSerializer and update PublicUserListView 2025-03-17 14:38:10 -04:00
Sean Morley
3b6d437f29 feat: Add search functionality for regions and cities in World Travel pages 2025-03-17 14:31:22 -04:00
Sean Morley
b82e4b6f0d feat: Add country name to Region serializer and update RegionCard component 2025-03-17 14:23:10 -04:00
Sean Morley
4e543fad55 feat: Enhance City and Lodging components with region and country names, and improve password disable functionality 2025-03-17 10:38:41 -04:00
Sean Morley
eb541225bd Merge branch 'development' of github.com:seanmorley15/AdventureLog into development 2025-03-16 21:49:02 -04:00
Sean Morley
a38828eb45 feat: Implement disable password authentication for users with social accounts 2025-03-16 21:49:00 -04:00
Sean Morley
9adf1d158a
Improved German translation 2025-03-16 13:30:42 -04:00
Marc Schumacher
ea61b5e67a
Update translation to German 2025-03-16 18:17:30 +01:00
Sean Morley
189cd0ee69 feat: Add origin and destination markers with connecting line for transportation routes 2025-03-16 12:35:42 -04:00
Sean Morley
c2fc249c92 feat: Add lodging types icons and update related components 2025-03-15 23:55:53 -04:00
Sean Morley
a640934370 [REQUEST] Add Lodgings to Map #509 2025-03-15 23:44:22 -04:00
Sean Morley
7fbcf170d0 feat: Add file type validation and sanitize markdown input in adventure components 2025-03-15 12:29:12 -04:00
Sean Morley
d26d3a2128
GPX file fixes, improved translations, and fixed itinerary view elements 2025-03-13 17:37:02 -04:00
Sean Morley
50a732b4d7 refactor: Optimize GPX file processing with concurrent fetching and improved error handling 2025-03-13 17:26:42 -04:00
Sean Morley
d9a88b644f
Merge pull request #514 from jyyyeung/fix/zh-translation
Improve Simplified Chinese Translations
2025-03-03 23:43:53 -05:00
Sean Morley
b7d6d6af56
Merge branch 'development' into fix/zh-translation 2025-03-03 23:37:56 -05:00
Sean Morley
4db72d9719 refactor: Remove duplicated notes display from collection page 2025-03-03 08:50:09 -05:00
Sean Morley
eaab1f8a7c refactor: Remove duplicate checklist display in collection page 2025-03-01 23:18:31 -05:00
Sean Morley
ed76c1426e
Open Locations in Maps and new API Routes 2025-02-28 15:50:49 -05:00
Sean Morley
a16df1640d Merge branch 'development' of github.com:seanmorley15/AdventureLog into development 2025-02-28 15:50:26 -05:00
Sean Morley
fee6c66eab feat: Add "Open in Maps" translations for multiple languages 2025-02-28 15:50:25 -05:00
Sean Morley
6d3b4c1e8e
Merge branch 'main' into development 2025-02-28 15:47:56 -05:00
Sean Morley
4759ff71a1 fix: Update URL path for user stats counts and remove email from response 2025-02-28 14:50:32 -05:00
JYYYeung
7a63e1227f fix: fixing Chinese translation with context 2025-02-26 09:28:30 +00:00
JYYYeung
689d2af21c fix: updated zh translations 2025-02-26 08:44:17 +00:00
Sean Morley
3e0639e6f7 fix: Update URL path regex in StatsViewSet for username counts action 2025-02-25 10:55:08 -05:00
Sean Morley
3c9aec4bfb refactor: Simplify location name assignment logic in LocationDropdown component 2025-02-23 17:15:37 -05:00
Sean Morley
b1068d27b0 refactor: Update API endpoint paths from "_allauth" to "auth" for consistency 2025-02-23 17:04:20 -05:00
Sean Morley
d5311bb71e feat: Add "Open in Maps" button for adventures with location coordinates 2025-02-22 20:36:25 -05:00
Sean Morley
00e4ec64ae
Hotel tracking, new profile system, UI improvements and lots of bug fixes 2025-02-22 17:22:31 -05:00
Sean Morley
b61083f7df refactor: Remove initial and key achievement migrations for cleanup 2025-02-22 17:06:40 -05:00
Sean Morley
d80805a181 feat: Add current timezone and airport description fields to localization files 2025-02-22 17:00:40 -05:00
Sean Morley
ea36b104b6 feat: Add Transportation and Lodging models to AdventureViewSet; update Avatar and TransportationModal components for improved user experience 2025-02-22 10:37:22 -05:00
Sean Morley
232e01bf8f fix: Correct button closing tags in Navbar component for proper rendering 2025-02-20 14:53:05 -05:00
Sean Morley
1c15e85986 feat: Enhance session cookie domain handling for IP addresses and single-label hostnames 2025-02-20 10:21:48 -05:00
Sean Morley
7cea432353
Merge pull request #493 from Cathnan/Modified-german-translation
Modified translation to make it more natural und understandable
2025-02-17 10:35:08 -05:00
Sean Morley
a7513d6c06
Merge pull request #495 from Cathnan/main
Modified german translation
2025-02-17 10:34:26 -05:00
Sean Morley
b4496508dd
Korean i18n (#503)
* Update email.md

Removing the single quotes allow SMTP to work -- it did not work otherwise. Also, [Google recommends TLS over SSL](https://support.google.com/a/answer/2520500?sjid=7564827219172741277-NA)

* Update immich_integration.md

Added the /api to the example URL for the Immich server.

* Add Korean language support to Navbar and layout

* chore: Korean translation (#502)

* correct ko translation at Navbar.svelte

* correct ko.json

correct ko.json::adventures

auto commit

auto commit

auto commit

auto commit

auto commit

adventures

collection

dashboard

home

immich

immich

map

navbar

auto commit

notes

profile

recommendations

settings

share

transportations

auto commit

worldtravel

polishing

auto commit

auto commit

auto commit

auto commit

auto commit

auto commit

auto commit

auto commit

auto commit

auto commit

auto commit

auto commit

---------

Co-authored-by: Patrick McGuire <60325264+mcguirepr89@users.noreply.github.com>
Co-authored-by: motox986 <49492476+motox986@users.noreply.github.com>
Co-authored-by: Seongjun Ji <metalg0su@gmail.com>
2025-02-17 10:32:18 -05:00
Sean Morley
2713b3640f feat: Set SESSION_COOKIE_SAMESITE to 'Lax' for improved security 2025-02-16 15:35:10 -05:00
Sean Morley
670e499d8f feat: Localize text in calendar and adventure pages, and update CountryCard component 2025-02-16 13:57:46 -05:00
Sean Morley
16fc9bc70e feat: Fetch categories and check for Immich integration on modal mount 2025-02-16 13:45:08 -05:00
Sean Morley
577eb9cceb feat: Improve is_visited parameter handling in AdventureViewSet 2025-02-16 13:11:35 -05:00
Sean Morley
af33acec66 feat: Add session cookie to API requests in profile page server load 2025-02-16 12:53:58 -05:00
Sean Morley
00f0fc9acf feat: Remove locations API call and implement random background image in stats view 2025-02-16 12:27:49 -05:00
Sean Morley
d31b95289d feat: Add reservation number and price fields to lodging information across multiple languages 2025-02-16 09:35:07 -05:00
Sean Morley
b5d6788c11 feat: Add location_name to ReverseGeocode type and implement location fetching in stats view 2025-02-15 19:44:11 -05:00
Cathnan
9e5e2456fa
Modified german translation 2025-02-10 13:31:54 +01:00
Sean Morley
60b5bbb3c8 feat: Enhance Navbar with new icons and improved button layout 2025-02-09 15:42:46 -05:00
Cathnan
299b190f69
Modified translation to make it more natural und understandable 2025-02-09 18:47:16 +01:00
Sean Morley
cf4691beaf Merge branch 'development' of github.com:seanmorley15/AdventureLog into development 2025-02-08 16:10:03 -05:00
Sean Morley
68924d7ecc feat: Refactor hotel terminology to lodging and update related components 2025-02-08 16:10:01 -05:00
Sean Morley
1351f26ba0
Merge pull request #489 from ThomasDetemmerman/main
Improved translation for Dutch
2025-02-08 10:15:24 -05:00
Sean Morley
57db1e088f
Merge branch 'development' into main 2025-02-08 10:14:11 -05:00
Thomas
cbcf2a70d7
improved locals for dutch 2025-02-08 08:04:01 +01:00
Thomas
d4f9abd158
Improved dutch language 2025-02-08 07:58:25 +01:00
Nikolai Eidsheim
a899579a02 Started work on norwegian translations 2025-02-06 21:33:17 +00:00
Sean Morley
d2cb862103 feat: Add favicon link to CDN index.html 2025-02-06 10:49:58 -05:00
Sean Morley
d1f50dfa17 feat: Add hotel management functionality with serializer and UI integration 2025-02-05 19:38:04 -05:00
Sean Morley
271cba9abc feat: Add GitHub Actions workflows for uploading CDN images to GHCR and Docker Hub 2025-02-05 16:55:07 -05:00
Sean Morley
921e756aef feat: Enhance CDN management with entrypoint script and update country data handling 2025-02-05 16:50:05 -05:00
Sean Morley
f7ff8f30b0 feat: Add CODEOWNERS file to define repository ownership 2025-02-04 18:06:03 -05:00
Sean Morley
9b54af8d40 feat: Update country data management to use AdventureLog CDN for fetching flags and JSON files 2025-02-04 17:43:41 -05:00
Sean Morley
5f344cad83 feat: Implement CDN update process with country data download and flag management 2025-02-04 17:19:58 -05:00
Sean Morley
a00d2abe0d feat: Add achievements app with models, admin, and management command for seeding data 2025-02-04 10:37:15 -05:00
Sean Morley
e93909e7ea
Merge pull request #487 from seanmorley15/cdn
feat: Add CDN generation scripts and Docker setup; include Nginx conf…
2025-02-04 10:36:59 -05:00
Sean Morley
8ea8043beb feat: Update stats API endpoint to include username for personalized user statistics 2025-02-03 19:57:22 -05:00
Sean Morley
9c3a52ae85 feat: Refactor user detail view and enhance localization strings for multiple languages 2025-02-03 19:56:25 -05:00
Sean Morley
ad5fb02ebb feat: Enhance AdventureViewSet to filter adventures based on visit status with improved boolean handling 2025-02-03 19:38:24 -05:00
Sean Morley
ed1e24252c feat: Enhance HotelModal with geocoding, custom location handling, and improved hotel initialization 2025-02-03 19:18:46 -05:00
Sean Morley
df60184f23 feat: Implement Hotel model with CRUD operations and integrate into views and serializers 2025-02-03 12:28:42 -05:00
Sean Morley
da9c2ba80a feat: Add linking instructions for existing accounts in Authentik and GitHub documentation 2025-02-03 11:10:14 -05:00
Sean Morley
5b42e8b372
Merge pull request #469 from mcguirepr89/patch-1
Update email.md
2025-02-03 09:23:40 -05:00
Sean Morley
28b2a918e8
Merge pull request #486 from motox986/patch-1
Update immich_integration.md
2025-02-03 09:05:25 -05:00
motox986
4da34032e9
Update immich_integration.md
Added the /api to the example URL for the Immich server.
2025-02-02 21:42:23 -08:00
Sean Morley
dc169d1046 feat: Add CDN generation scripts and Docker setup; include Nginx configuration 2025-02-02 20:31:28 -05:00
Sean Morley
659c56f02d feat: Add Hotel model and integrate into admin panel; update related components 2025-02-02 10:36:47 -05:00
Sean Morley
bdb17a3177
Merge pull request #478 from Thiesjoo/immich-improvements
Immich image selection improvements
2025-02-01 15:28:15 -05:00
Sean Morley
7d71c84fd2 feat: Remove email field from user details response and enhance frontend user display 2025-02-01 15:10:25 -05:00
Sean Morley
c1807826d0 feat: Add server-side redirection to admin page based on public URL fetch 2025-01-31 16:43:09 -05:00
Sean Morley
341de6daea
Merge pull request #480 from evertyang/patch-1
Update unraid.md
2025-01-31 07:25:51 -05:00
evertyang
9cdd502264
Update unraid.md
Variable appears to be in bytes not kilobytes. I got the following error assuming kilobytes:

SvelteKitError: Content-length of 4870507 exceeds limit of 30000 bytes.
    at Object.start (file:///app/build/handler.js:984:19)
    at setupReadableStreamDefaultController (node:internal/webstreams/readablestream:2333:23)
    at setupReadableStreamDefaultControllerFromSource (node:internal/webstreams/readablestream:2366:3)
    at new ReadableStream (node:internal/webstreams/readablestream:289:7)
    at get_raw_body (file:///app/build/handler.js:973:9)
    at getRequest (file:///app/build/handler.js:1054:7)
    at Array.ssr (file:///app/build/handler.js:1248:19)
    at handle (file:///app/build/handler.js:1318:23)
    at file:///app/build/handler.js:1318:40
    at Array.<anonymous> (file:///app/build/handler.js:1237:4) {
  status: 413,
  text: 'Payload Too Large'
}
2025-01-31 05:02:00 -06:00
Thies
cc6a98d23b
Convert indentation to tabs, as my vscode did not want to cooperate 2025-01-30 13:07:08 +01:00
Thies
94fca45af0
immich: Show dates of images next to the results 2025-01-30 13:06:17 +01:00
Thies
c7d34f6c48
immich: Add a loading indicator 2025-01-30 13:06:17 +01:00
Thies
08bfeac837
immich: Automatically select the date of the last visit for the photos 2025-01-30 13:06:10 +01:00
Thies
aaacacbd60
immich: Now able to show images by date! 2025-01-30 12:57:38 +01:00
Thies
bc08362a4c
navbar: Allow the typing of slash in input/textarea fields 2025-01-30 12:16:19 +01:00
Sean Morley
85b55660f9 feat: Update user profile handling and enhance public user details response 2025-01-29 22:50:53 -05:00
Sean Morley
6d80969fdc
Merge pull request #476 from ThunderLord956/main
Documentation Update/Spell-Checks
2025-01-29 08:04:41 -05:00
Thunder
defd97fc02 Mass documentation spelling check, as well as improving the unraid installation guide, and some small changes to the docker installation guide. 2025-01-28 23:23:47 -06:00
Sean Morley
afdc894f23
Merge pull request #472 from ThunderLord956/patch-1
Update unraid.md
2025-01-28 17:20:38 -05:00
ThunderLord956
41cc47d631
Update unraid.md
Separate 2 lines in Installation Configuration
2025-01-28 15:13:10 -06:00
ThunderLord956
145627b1eb
Update unraid.md
Try to fix tables
2025-01-28 15:11:33 -06:00
ThunderLord956
896c537791
Update unraid.md
Update the unraid wiki page to be more precise and easier to understand
2025-01-28 14:04:41 -06:00
Sean Morley
b68e2dedaa feat: Add new background image entry for adventurelog showcase 2025-01-27 20:19:33 -05:00
Sean Morley
db077b5fd7
Merge pull request #460 from seanmorley15/development
Attachments, GPX Maps, Global Search, Security
2025-01-27 09:50:00 -05:00
Patrick McGuire
697851e210
Update email.md
Removing the single quotes allow SMTP to work -- it did not work otherwise. Also, [Google recommends TLS over SSL](https://support.google.com/a/answer/2520500?sjid=7564827219172741277-NA)
2025-01-27 09:38:59 -05:00
Sean Morley
3468e82b04 feat: Sanitize FRONTEND_URL by removing quotes for improved URL parsing 2025-01-27 09:23:48 -05:00
Sean Morley
680a2378d9
Merge pull request #458 from larsl-net/basic-pwa
Basic PWA support
2025-01-26 20:20:47 -05:00
Sean Morley
f5dc0ceb0a feat: Refactor session cookie domain handling to use psl for improved domain parsing 2025-01-26 20:18:50 -05:00
Sean Morley
d326d38329 feat: Update session cookie domain handling using publicsuffix2 and psl libraries 2025-01-26 20:06:47 -05:00
Sean Morley
6a5bfbda2d feat: Enhance adventure marker popup with image carousel and visit details 2025-01-23 17:51:56 -05:00
Sean Morley
37fca9ba1d Merge branch 'development' of github.com:seanmorley15/AdventureLog into development 2025-01-23 17:49:03 -05:00
Sean Morley
abaee8c9c4 feat: Add attachments array to recommendation object and update options with collection start date 2025-01-23 17:48:55 -05:00
Sean Morley
6b090096d6
Merge pull request #465 from larsl-net/collection-map-icons
Display adventure category icon also on collection maps
2025-01-23 17:46:05 -05:00
Sean Morley
8fef53fd24
Merge pull request #466 from larsl-net/calendar-collection-range
Calendar collection range
2025-01-23 17:44:00 -05:00
Lars Lehmann
8fee537d8b
feat: Open collection calendar on start date 2025-01-23 21:56:19 +01:00
Lars Lehmann
69967b759f
fix: Error loading calender when transport has no date 2025-01-23 21:29:49 +01:00
Lars Lehmann
f04a6e30a7
Remove package-lock.json 2025-01-23 20:14:10 +01:00
Lars Lehmann
9e66c67c62
fix: Display adventure category icon also on collection maps 2025-01-23 20:07:16 +01:00
Sean Morley
12a6429812 Merge branch 'development' of github.com:seanmorley15/AdventureLog into development 2025-01-22 08:36:03 -05:00
Sean Morley
0eb4bc706a feat: enhance adventure handling with user ID support in serializers and attachment view; refactor saveEdit function and clean up Avatar component 2025-01-22 08:36:02 -05:00
Sean Morley
6ea0e07775
Merge pull request #462 from seanmorley15/dependabot/npm_and_yarn/documentation/npm_and_yarn-1d5806dca3
build(deps): bump the npm_and_yarn group across 2 directories with 1 update
2025-01-21 23:53:13 -05:00
dependabot[bot]
d5fe7eeff8
build(deps): bump the npm_and_yarn group across 2 directories with 1 update
Bumps the npm_and_yarn group with 1 update in the /documentation directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 5.4.11 to 5.4.14
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.14/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.14/packages/vite)

Updates `vite` from 5.3.6 to 5.4.12
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.14/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.14/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 04:51:56 +00:00
Sean Morley
1447a948c2
Merge pull request #461 from seanmorley15/dependabot/npm_and_yarn/frontend/npm_and_yarn-f25c717a0f
build(deps-dev): bump vite from 5.3.6 to 5.4.12 in /frontend in the npm_and_yarn group across 1 directory
2025-01-21 22:51:01 -05:00
dependabot[bot]
10230e926c
build(deps-dev): bump vite
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 5.3.6 to 5.4.12
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 03:28:49 +00:00
Sean Morley
3f30819d25 feat: add GPX file support in AdventureModal and improve map marker handling 2025-01-21 19:40:56 -05:00
Sean Morley
6e28e5234e feat: enhance attachment handling with new localization strings and GPX upload tips 2025-01-21 19:33:48 -05:00
Sean Morley
1b3cf6ab39 feat: add keyboard shortcut to focus search input in Navbar and clean up dashboard animations 2025-01-21 19:17:51 -05:00
Sean Morley
f9cf92041d feat: enhance CategoryModal with loading state and remove unused activities route 2025-01-21 19:01:45 -05:00
Sean Morley
64d2bdebce feat: add GPX file handling and GeoJSON integration in adventure page; update dependencies and AttachmentCard component 2025-01-20 20:03:00 -05:00
Sean Morley
30c58ca118 feat: refactor AttachmentCard component to handle delete action locally and simplify adventure page logic 2025-01-20 09:01:11 -05:00
Sean Morley
25edec460b feat: enhance search page with reactive data updates and simplify component usage 2025-01-19 23:09:28 -05:00
Sean Morley
1f3abf7f32 feat: replace placeholder image with gradient background and text for no image available state in CardCarousel component 2025-01-19 22:33:35 -05:00
Sean Morley
94c3e3d363 feat: implement attachment management with upload, delete, and permission checks; update serializers and models 2025-01-19 22:22:03 -05:00
Lars Lehmann
a88e59869e
Fix service worker 2025-01-19 22:43:31 +01:00
Lars Lehmann
3ccd078508
Add Service Worker 2025-01-19 11:51:04 +01:00
Lars Lehmann
e9538b707f
Add basic PWA 2025-01-19 11:36:28 +01:00
Sean Morley
e0fa62c1ea feat: add GSAP animations to signup, login, and dashboard pages; include Attachment serializer in backend 2025-01-19 00:05:08 -05:00
Sean Morley
aa216f5688 feat: add Attachment model and implement file permission checks for media access 2025-01-18 20:06:12 -05:00
Sean Morley
433599dc20 feat: implement protected media serving and permission checks for adventure images 2025-01-18 17:03:03 -05:00
Sean Morley
f7e183f9a3
Update README.md 2025-01-18 14:30:05 -05:00
Sean Morley
f10e171a8e fix: update RegionCard component to handle undefined visited state and refactor global search API to return structured results 2025-01-18 12:57:56 -05:00
Sean Morley
d60945d5b7 feat: implement global search functionality for adventures, collections, users, and locations 2025-01-18 12:28:14 -05:00
Sean Morley
9132ef39ec fix: add 'finding_recommendations' key and update 'no_adventures_to_recommendations' in multiple locale files 2025-01-18 09:19:21 -05:00
Sean Morley
8f6e655378
AdventureLog Smart Recommendations 2025-01-17 21:09:04 -05:00
Sean Morley
1a7643b8a7 fix: include Referer header in user-related API requests 2025-01-17 20:20:56 -05:00
Sean Morley
fcec4a420c
Update README.md 2025-01-17 19:38:18 -05:00
Sean Morley
75162bbf7b fix: improve error handling for Overpass API connection failures 2025-01-17 18:22:03 -05:00
Sean Morley
ae8617a9fb fix: update formatting in Authentik documentation for clarity 2025-01-17 18:15:26 -05:00
Sean Morley
8af53f8f01
Merge pull request #455 from UndyingSoul/main
Fixes gap in docker-compose example/docs with working oidc implementation
2025-01-17 18:13:53 -05:00
Corbin Whitton
a508dd8d96 update documentation for authentik 2025-01-17 15:55:31 -07:00
Corbin Whitton
8f1f2e139a updated traefik config to work with oidc 2025-01-17 15:47:48 -07:00
Sean Morley
e9084db832 fix: remove default adventure type and streamline image upload form 2025-01-17 17:33:16 -05:00
Sean Morley
6289c7e305
Merge pull request #454 from seanmorley15/csrf_error
Csrf error
2025-01-17 17:15:42 -05:00
Sean Morley
0cee4c386d fix: update deleteAdventure function to use DELETE method for API call 2025-01-17 17:15:18 -05:00
Sean Morley
f4450b6a38 fix: include Referer header in API requests 2025-01-17 16:58:08 -05:00
Sean Morley
5b4092ba6c feat: implement pagination, add activity types and stats views; update category management and localization 2025-01-17 16:50:01 -05:00
Sean Morley
2b78021155 feat: update localization files and add adventure creation messages; refine adventure filtering logic 2025-01-16 21:15:22 -05:00
Sean Morley
9ceee13edc
Merge pull request #453 from bucherfa/main
fix: container relations
2025-01-16 19:38:51 -05:00
Fabian Bucher
f56abd2312
fix: container relations 2025-01-16 22:24:31 +01:00
Sean Morley
62efa2478e feat: add OverpassViewSet and implement osmTagToEmoji function; update requirements and aria-labels 2025-01-15 15:39:21 -05:00
Sean Morley
0588555707 feat: update toast messages and add city updates; enhance geocoding checks and localizations 2025-01-15 11:53:04 -05:00
Sean Morley
bcbe7657ad
Merge pull request #450 from larsl-net/city-stats-api
fix: add cities to /stats/countes
2025-01-15 11:34:43 -05:00
Lars Lehmann
5f1a611671
fix: add cities to /stats/countes 2025-01-15 14:46:12 +01:00
Sean Morley
21345e0971 build(deps): upgrade Django from 5.0.10 to 5.0.11; remove debug print statements in user tests 2025-01-14 22:05:43 -05:00
Sean Morley
b4c9cd1f27
Merge pull request #449 from seanmorley15/dependabot/pip/backend/server/pip-607b783c7d
build(deps): bump django from 5.0.8 to 5.0.11 in /backend/server in the pip group across 1 directory
2025-01-14 22:02:38 -05:00
Sean Morley
92a52d65f2
Merge branch 'development' into dependabot/pip/backend/server/pip-607b783c7d 2025-01-14 22:02:30 -05:00
Sean Morley
2c1b540c20 refactor: change visit ordering to use latest visit date; remove unused signup form 2025-01-14 20:46:33 -05:00
dependabot[bot]
743774dbca
build(deps): bump django
Bumps the pip group with 1 update in the /backend/server directory: [django](https://github.com/django/django).


Updates `django` from 5.0.8 to 5.0.11
- [Commits](https://github.com/django/django/compare/5.0.8...5.0.11)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-14 22:37:24 +00:00
Sean Morley
5cce76274a
Merge pull request #448 from larsl-net/visited-cities
fix: mark visited cities based on locations
2025-01-14 16:54:49 -05:00
Sean Morley
16cdda2b82
Merge branch 'development' into visited-cities 2025-01-14 16:54:42 -05:00
Sean Morley
611509a289
Merge pull request #447 from larsl-net/development
fix small python errors
2025-01-14 16:53:50 -05:00
Lars Lehmann
af4026300b
fix: mark visited cities based on locations 2025-01-14 21:44:10 +01:00
Lars Lehmann
ab2ad829f2
fix: visited_city not assigned 2025-01-14 20:58:25 +01:00
Lars Lehmann
ce87afeeb9
fix: key error 2025-01-14 20:33:25 +01:00
Sean Morley
5965b82780
Merge pull request #444 from seanmorley15/dependabot/pip/backend/server/pip-59524fe9f2
build(deps): bump django from 5.0.8 to 5.0.10 in /backend/server in the pip group across 1 directory
2025-01-14 13:12:40 -05:00
Sean Morley
4a1e51e12a fix: update TOTP input type and attributes for better user experience 2025-01-14 13:10:14 -05:00
Sean Morley
7be21fcace fix: update image paths in Unraid installation documentation 2025-01-14 12:23:36 -05:00
Sean Morley
c084a40cab
Dual stack Backend 2025-01-14 12:19:03 -05:00
Sean Morley
b16b1a1e44
Merge pull request #445 from larsl-net/development
fix: make backend dual-stack ready
2025-01-14 11:52:36 -05:00
Lars Lehmann
69673fb5ef
fix: make backend dual stack ready 2025-01-14 17:38:31 +01:00
dependabot[bot]
b023078e04
build(deps): bump django
Bumps the pip group with 1 update in the /backend/server directory: [django](https://github.com/django/django).


Updates `django` from 5.0.8 to 5.0.10
- [Commits](https://github.com/django/django/compare/5.0.8...5.0.10)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-14 16:36:26 +00:00
Sean Morley
f6605bc1bf
fix: update session cookie domain handling for IP addresses 2025-01-14 09:49:24 -05:00
Sean Morley
e8f2c7ea81 fix: update session cookie domain handling for IP addresses and improve frontend URL configuration 2025-01-14 09:38:38 -05:00
Sean Morley
187f4c0a4f
Merge pull request #442 from seanmorley15/development
Development
2025-01-13 19:55:15 -05:00
Sean Morley
ef44836328 fix: remove unused console log import from page server file 2025-01-13 19:55:00 -05:00
Sean Morley
96ff727b57 fix: include Referer header and ensure CSRF token is set in request headers 2025-01-13 19:47:19 -05:00
Sean Morley
a8e84be28e docs: update known issues for Redirect URI and authorization callback URL in Authentik and GitHub configurations
fix: enhance session cookie deletion logic with dynamic domain handling
2025-01-13 19:34:19 -05:00
Sean Morley
4c84ac0979 fix: enhance middleware to set HTTP_X_FORWARDED_PROTO and secure proxy SSL header 2025-01-13 19:21:35 -05:00
Sean Morley
62d2fd7c6a docs: add warnings for known issues with Redirect URI and authorization callback URL in Authentik and GitHub configurations 2025-01-13 18:50:54 -05:00
Sean Morley
609f68b10d
Merge pull request #441 from seanmorley15/development
fix: dynamically set session cookie domain based on request hostname
2025-01-13 18:15:56 -05:00
Sean Morley
4a36fbb4c1 fix: dynamically set session cookie domain based on request hostname 2025-01-13 18:06:16 -05:00
Sean Morley
145a3f00c5
Merge pull request #440 from seanmorley15/development
fix: configure secure session cookies and dynamic domain for frontend…
2025-01-13 17:53:07 -05:00
Sean Morley
062111d7fe fix: configure secure session cookies and dynamic domain for frontend URL 2025-01-13 17:52:25 -05:00
Sean Morley
ec3ada986d
OIDC Auth and City Visits Data 2025-01-13 17:21:32 -05:00
Sean Morley
a010afcc43 fix: replace psutil with tqdm for progress tracking in country download command 2025-01-13 17:07:52 -05:00
Sean Morley
563373dd54 fix: implement memory limit and adjust batch size for country downloads 2025-01-13 14:42:48 -05:00
Sean Morley
a766a07ea1 fix: add warning for interrupted download-countries command due to memory issues 2025-01-13 14:12:05 -05:00
Sean Morley
f27453824a fix: add psutil dependency for system resource management 2025-01-13 13:03:19 -05:00
Sean Morley
ac37bc98c4 fix: add insert_id field to City, Country, and Region models 2025-01-13 13:01:36 -05:00
Sean Morley
53d500b7d5 fix: add ijson dependency for improved data processing 2025-01-13 11:34:56 -05:00
Sean Morley
9a7667fd62 fix: increase batch size for country downloads from 10 to 250 2025-01-13 11:24:25 -05:00
Sean Morley
7f2c3c33a5 fix: switch from json to ijson for efficient country data processing 2025-01-13 11:23:57 -05:00
Sean Morley
6252f3cf14 fix: reduce batch size for country downloads from 100 to 10 2025-01-13 11:19:45 -05:00
Sean Morley
6413b9a985 fix: reduce batch size for country downloads from 250 to 100 2025-01-13 11:07:59 -05:00
Sean Morley
3b5b5d83b9 docs: update download-countries command in documentation to include --force option 2025-01-13 10:54:46 -05:00
Sean Morley
0f828dd0bf fix: update conditionals in AdventureModal and CategoryDropdown for improved logic 2025-01-13 10:51:33 -05:00
Sean Morley
32125efed4 feat: adjust batch size for country updates and include longitude/latitude in bulk update 2025-01-12 22:09:55 -05:00
Sean Morley
1f7d75acfc feat: implement batch processing for country, region, and city creation and updates in download command 2025-01-12 21:53:16 -05:00
Sean Morley
30f1eaef1c feat: enhance download command with success and warning messages; update Authentik documentation for clarity 2025-01-12 20:28:27 -05:00
Sean Morley
af1d43ddf3
Update docker.md 2025-01-12 09:32:08 -05:00
Sean Morley
b04a8b8a9e feat: implement multiple image upload functionality in AdventureModal 2025-01-11 21:25:29 -05:00
Sean Morley
fa60db1d93
Update unraid.md 2025-01-11 16:11:37 -05:00
Sean Morley
e8f3674101
Update unraid.md 2025-01-11 15:49:25 -05:00
Sean Morley
cd06f838b0
Update unraid.md 2025-01-11 15:37:38 -05:00
Sean Morley
377420226e
Update unraid.md 2025-01-11 15:36:39 -05:00
Sean Morley
5cccadad15 fix: correct typo in Unraid installation documentation 2025-01-11 10:53:01 -05:00
Sean Morley
3bde752234 fix: update documentation link in README for accuracy 2025-01-11 10:28:01 -05:00
Sean Morley
2cd60adc8c docs: update Discord link in README for improved accessibility 2025-01-11 10:27:27 -05:00
Sean Morley
2221b1dad9 docs: new README add ARM compatibility note for PostGIS image in Docker installation guide 2025-01-11 10:26:22 -05:00
Sean Morley
88d62eb869 feat: add GitHub and OIDC social authentication documentation, update Authentik references for clarity 2025-01-11 09:51:31 -05:00
Sean Morley
76ee4a7290
feat: add Unraid installation documentation and update links 2025-01-10 20:17:50 -05:00
Sean Morley
a50f051079 feat: add Unraid installation documentation and update links 2025-01-10 20:16:09 -05:00
Sean Morley
ef996cd302 Merge branch 'main' of github.com:seanmorley15/AdventureLog 2025-01-10 19:41:06 -05:00
Sean Morley
ae3c43f7c8 Update NGINX configuration and docker-compose for improved service references 2025-01-10 19:41:03 -05:00
Sean Morley
b1a3379604 fix: update NGINX configuration and docker-compose for improved service referencing and clarity 2025-01-10 19:40:06 -05:00
Sean Morley
1e48152e1a fix: update NGINX configuration for improved backend service referencing and enhance docker-compose environment variable clarity 2025-01-10 19:36:38 -05:00
Sean Morley
de8764499b feat: enhance city and region visit tracking, update AdventureModal and serializers for improved data handling 2025-01-09 19:40:23 -05:00
Sean Morley
013a2cc751 feat: add password validation and error messages, enhance CityCard styling, and update localization for email and password prompts 2025-01-09 18:25:51 -05:00
Sean Morley
22790ae7c0 feat: add num_cities field to RegionSerializer, update RegionCard to display city count, and enhance CSRF token handling 2025-01-09 14:58:45 -05:00
Sean Morley
abe870506f feat: enhance region visit tracking with improved toast messages, update localization, and modify page titles 2025-01-09 13:53:16 -05:00
Sean Morley
80cec30fda feat: add VisitedCity model and serializer, update admin interface, and enhance city visit tracking functionality 2025-01-09 12:38:29 -05:00
Sean Morley
44810e6343 feat: add City model and serializer, update RegionCard and create CityCard component, enhance admin interface for City management 2025-01-09 11:11:02 -05:00
Sean Morley
a883d4104d feat: enhance adventure sorting by adding date and rating filters, update admin settings localization for OIDC 2025-01-08 17:08:53 -05:00
Sean Morley
f670fbc93a feat: enhance superuser creation with email verification and update settings for two-factor authentication 2025-01-07 10:27:11 -05:00
Sean Morley
548702890d feat: update NGINX configuration for improved proxy handling and enable social account login on GET requests 2025-01-07 09:58:39 -05:00
Sean Morley
a5aa09ed7b feat: add OverrideHostMiddleware to set HTTP_HOST from PUBLIC_URL environment variable and update nginx configuration 2025-01-06 20:44:37 -05:00
Sean Morley
e19781d7ac feat: add public URL endpoint and update user type to include password status 2025-01-06 18:53:08 -05:00
Sean Morley
59b41c01df feat: enhance CSRF token handling and add format=json to API requests 2025-01-06 14:49:23 -05:00
Sean Morley
128c33d9a1 feat: add social authentication support with enabled providers endpoint and UI integration 2025-01-06 14:25:57 -05:00
Sean Morley
a9c2af9649
Immich Integration 2025-01-05 19:08:37 -05:00
Sean Morley
4fdc16da58 feat: update locale info for immich integration text 2025-01-03 19:28:18 -05:00
Sean Morley
604cfa05b3 feat: adds banner image to brand folder 2025-01-03 19:24:10 -05:00
Sean Morley
3d22621a26 fix: update screenshot paths and descriptions in README.md 2025-01-03 19:04:17 -05:00
Sean Morley
9e4846a66a feat: enhance Immich integration documentation and add warnings for localhost usage 2025-01-03 18:46:45 -05:00
Sean Morley
a4df852744 feat: update .env.example for demo database setup and add image search functionality in AdventureModal 2025-01-03 16:42:27 -05:00
Sean Morley
56244329f5 feat: configure REST framework renderers based on DEBUG setting 2025-01-03 16:29:39 -05:00
Sean Morley
230a786d6e docs: add instructions for customizing email subject in admin site 2025-01-03 15:58:52 -05:00
Sean Morley
259f2e75c2 feat: version bump for release v0.8.0 2025-01-03 12:24:17 -05:00
Sean Morley
82a1134019 fix: prevent marking adventure as visited if the place is already visited 2025-01-03 12:15:39 -05:00
Sean Morley
50e0d4a34e feat: add localization for adventure visit confirmation message in multiple languages 2025-01-03 12:12:09 -05:00
Sean Morley
3a024e1e18 feat: implement logic to determine if an adventure will be marked as visited based on visit dates 2025-01-03 12:05:02 -05:00
Sean Morley
57e367d112 refactor: update AdventureSerializer to handle visits data more gracefully and remove visits from request body in AdventureCard 2025-01-03 09:53:23 -05:00
Sean Morley
6651557738 feat: include adventure visits in collection update requests 2025-01-03 09:47:05 -05:00
Sean Morley
c6fa603a93 feat: add primary image functionality to AdventureImage model and update related components 2025-01-02 23:25:58 -05:00
Sean Morley
991efa8d08 docs: add Immich integration documentation and update configuration menu 2025-01-02 19:00:11 -05:00
Sean Morley
73036dcef8 fix: put languages in locale selection dropdown into native language 2025-01-02 18:38:29 -05:00
Sean Morley
eea87e59a5 feat: update Immich integration migrations and enhance localization strings for user feedback 2025-01-02 18:34:13 -05:00
Sean Morley
386014db92 feat: add Immich album retrieval functionality and implement album selection component 2025-01-02 17:56:47 -05:00
Sean Morley
81b60d6021 feat: enhance settings page with text-neutral styling for labels and messages 2025-01-02 13:40:02 -05:00
Sean Morley
cee9f16cf5 feat: implement Immich integration with CRUD API, add serializer, and enhance frontend components 2025-01-02 13:34:51 -05:00
Sean Morley
5d12d103fc feat: enhance AdventureImage model with custom upload path and add latitude/longitude fields to Country model 2025-01-01 19:27:33 -05:00
Sean Morley
67f6af8ca3 feat: add Immich integration view and API documentation, enhance error handling, and include SVG asset 2025-01-01 16:24:44 -05:00
Sean Morley
15fd15ba40 feat: update Immich integration to use OneToOneField for user and enhance image retrieval functionality 2025-01-01 11:00:11 -05:00
Sean Morley
dc89743569 feat: add Immich integration module with API endpoints and admin interface 2024-12-31 10:38:15 -05:00
Sean Morley
66ce80226e
Merge pull request #422 from seanmorley15/development
Fix 'secure' cookie on http
2024-12-29 20:47:50 -05:00
Sean Morley
79db20a903
Merge pull request #418 from nordtechtiger/development
Improve swedish locale
2024-12-29 20:46:55 -05:00
RedTechTiger
d5ff2fc6b7 fix: slightly improve swedish locale 2024-12-29 15:50:21 -05:00
Sean Morley
fd7f285c57 Update session cookie deletion to conditionally use secure flag based on HTTPS protocol 2024-12-29 12:55:45 -05:00
Sean Morley
b5ac66a1cc Update secure cookie setting to conditionally use HTTPS protocol in authentication flows 2024-12-29 12:38:54 -05:00
Sean Morley
bc6cf42b8e
Markdown, New Transportation Modals/Card and Collection Improvements 2024-12-28 15:10:50 -05:00
Sean Morley
8716efb613 Increase Gunicorn timeout and worker count in entrypoint script for improved performance 2024-12-28 15:06:29 -05:00
Sean Morley
7cf634def8 Fix date handling in CollectionModal to set null values for missing start and end dates; update localization files to add new confirmation messages and labels. 2024-12-28 14:48:30 -05:00
Sean Morley
697c40fca0 Enhance TransportationCard unlinked state logic and improve date handling; update TransportationModal for conditional rendering and add new background image in JSON. 2024-12-28 10:56:25 -05:00
Sean Morley
5d3ec181a0 Fix date validation logic in CollectionModal to ensure start date is before end date and handle missing start date 2024-12-27 22:27:17 -05:00
Sean Morley
46e3e91679 Enhance unlinked state logic in TransportationCard to account for missing dates 2024-12-26 22:10:03 -05:00
Sean Morley
8f7551f4be Enhance unlinked state logic for checklists and notes; update button layout in modals 2024-12-26 22:09:17 -05:00
Sean Morley
df2ce01910 Update note and checklist modals as well as add an unlinked state in the collection page for better organization 2024-12-26 21:56:14 -05:00
Sean Morley
f7c998ab58 UI changes and updates, collection page refresh 2024-12-26 11:07:59 -05:00
Sean Morley
c3782cc9d7
Merge pull request #413 from PascalBru/feature_traefik
fix Django Admin with traefik
2024-12-23 20:16:42 -05:00
Pascal Bruckert
c11395cbd6
fix Django Admin with traefik 2024-12-23 22:06:35 +01:00
Sean Morley
c9dd5fe33d Add region map to worldtravel region page 2024-12-20 15:55:30 -05:00
Sean Morley
d6eb4edddd Refactor map component styles and update transportation location formatting 2024-12-19 21:44:31 -05:00
Sean Morley
c3fddb1889 Add delete confirmation modals for notes, checklists, and transportation 2024-12-19 19:07:23 -05:00
Sean Morley
7c68dc839a Add geographic coordinates to Transportation model and update related components 2024-12-19 18:46:52 -05:00
Sean Morley
57f2bdb8ba
Merge pull request #407 from seanmorley15/dependabot/npm_and_yarn/documentation/npm_and_yarn-36c5dcd58e
Bump the npm_and_yarn group across 2 directories with 2 updates
2024-12-17 19:03:51 -05:00
Sean Morley
dd08a6fe24 Add Markdown editor component and integrate it into AdventureModal 2024-12-17 18:58:38 -05:00
Sean Morley
4de6fa675f
Add check for missing start dates in calendar event loading 2024-12-15 13:29:08 -05:00
Sean Morley
7d609d01ea Add check for missing start dates in calendar event loading 2024-12-15 13:28:33 -05:00
Sean Morley
e0296543b6
Improve ICS calendar generation by handling missing and invalid dates 2024-12-15 13:05:40 -05:00
Sean Morley
edc5fdf1cc Improve ICS calendar generation by handling missing and invalid start dates 2024-12-15 13:04:52 -05:00
Sean Morley
f3c3e19d51
Enhance styling for no recent adventures message in dashboard 2024-12-15 12:34:58 -05:00
Sean Morley
54c859b2a8 Enhance styling for no recent adventures message in dashboard 2024-12-15 12:34:35 -05:00
Sean Morley
6cf17ce56e
Replace SVG icons with corresponding icon components in dashboard stats 2024-12-15 12:32:35 -05:00
Sean Morley
c77b3573d4 Replace SVG icons with corresponding icon components in dashboard stats 2024-12-15 12:31:55 -05:00
dependabot[bot]
febbdfa252
Bump the npm_and_yarn group across 2 directories with 2 updates
Bumps the npm_and_yarn group with 1 update in the /documentation directory: [nanoid](https://github.com/ai/nanoid).
Bumps the npm_and_yarn group with 2 updates in the /frontend directory: [nanoid](https://github.com/ai/nanoid) and [cross-spawn](https://github.com/moxystudio/node-cross-spawn).


Updates `nanoid` from 3.3.7 to 3.3.8
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

Updates `nanoid` from 3.3.7 to 3.3.8
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

Updates `cross-spawn` from 7.0.3 to 7.0.6
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: nanoid
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: cross-spawn
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-15 17:15:18 +00:00
Sean Morley
148568fca4
Auth Migration, Calendar and Other Misc. Fixes 2024-12-15 12:13:56 -05:00
Sean Morley
10dbafd1ee Update button label in NewTransportation component to Create 2024-12-14 17:27:10 -05:00
Sean Morley
c966b73eba Fix formatting in README.md for better readability 2024-12-14 17:18:38 -05:00
Sean Morley
0c27f4b8a4 Add download adventures as ICS calendar 2024-12-14 14:37:16 -05:00
Sean Morley
4839edde7b Update password reset form and add more localization for error messages 2024-12-14 13:36:15 -05:00
Sean Morley
c0fd91ef8d Add emoji picker to category dropdown; implement toggle functionality and handle emoji selection 2024-12-14 11:30:34 -05:00
Sean Morley
0376709325 Enhance localization support; update error messages and add translations for adventure calendar 2024-12-14 09:56:12 -05:00
Sean Morley
9bf0849b92 Add multi-factor authentication (MFA) support; update localization and error handling 2024-12-13 20:21:44 -05:00
Sean Morley
1b54f8ed69 Implement TOTP 2FA modal; add QR code generation and recovery codes management 2024-12-13 10:48:18 -05:00
Sean Morley
7b7db1c530 Refactor email management and localization; update requirements and settings for MFA support 2024-12-12 19:20:58 -05:00
Sean Morley
673a56c6a0 Add MFA to login screen 2024-12-12 15:45:19 -05:00
Sean Morley
54d7a1a229 Update forgot password link to point to user reset-password route 2024-12-12 15:22:23 -05:00
Sean Morley
2ccbf4be83 Update email verification and password reset flows; refactor Docker Compose and enhance email management 2024-12-12 11:01:09 -05:00
Sean Morley
0272c6b076 Refactor user admin settings and enhance email management functionality 2024-12-11 20:46:20 -05:00
Sean Morley
f7742ca37d Filter visited regions by user ID in ReverseGeocodeViewSet 2024-12-07 20:10:24 -05:00
Sean Morley
2f321517a6
Update getting_started.md 2024-12-07 17:45:26 -05:00
Sean Morley
57f3be0938
Merge pull request #398 from MaximUltimatum/adding-kustomize-documentation
Adding kustomize documentation
2024-12-07 16:27:46 -05:00
Sean Morley
914028ea2f Update Kubernetes and Kustomize documentation 2024-12-07 16:25:31 -05:00
Sean Morley
6a00a2ed55 Refactor Docker Compose configuration and enhance email management in settings 2024-12-07 16:15:41 -05:00
MaximUltimatum
91a51515dd
Removing extraneous env variable info that is externally linked 2024-12-07 14:31:13 -05:00
MaximUltimatum
56a4292f4f
Adding kustomize documentation 2024-12-07 14:26:18 -05:00
Sean Morley
64105808b5 Add calendar route and integrate calendar component in Navbar 2024-12-04 18:11:57 -05:00
Sean Morley
ca66b85838 Add Admin Panel guide to documentation 2024-12-04 12:43:26 -05:00
Sean Morley
d44cb06e31 Refactor AdventureCard usage and integrate event calendar components 2024-12-04 12:38:01 -05:00
Sean Morley
f0603f7dbd
Update README.md 2024-12-03 15:44:38 -05:00
Sean Morley
0616fe19f3 Remove printing in backend 2024-12-03 14:02:01 -05:00
Sean Morley
a39e22b0a8 Add new dashboard 2024-12-03 14:00:48 -05:00
Sean Morley
82040d5c85
Update README.md 2024-12-03 13:23:17 -05:00
Sean Morley
88dbd41b92
Merge pull request #375 from seanmorley15/dependabot/npm_and_yarn/frontend/npm_and_yarn-330f789f30
Bump @sveltejs/kit from 2.5.17 to 2.8.3 in /frontend in the npm_and_yarn group across 1 directory
2024-12-02 19:50:29 -05:00
Sean Morley
21d8b376b3
Merge pull request #395 from seanmorley15/update-nl
Update nl.json
2024-12-02 15:47:37 -05:00
Sean Morley
dc29f48ab2 Add 'Northern Lights' theme and update localization and Tailwind configuration 2024-12-02 15:47:09 -05:00
Sean Morley
4ecad7f46d Update nl.json 2024-12-02 13:16:48 -05:00
Sean Morley
0a2b172405
Merge pull request #393 from seanmorley15/fix-nginx-upload
Increase client_max_body_size to 100M in nginx configuration
2024-12-01 09:53:04 -05:00
Sean Morley
6ba047d2a5 Increase client_max_body_size to 100M in nginx configuration 2024-12-01 09:52:34 -05:00
Sean Morley
50dc0424a9 Refactor user serializers, update Docker configurations, and remove unused Nginx files 2024-12-01 09:52:04 -05:00
Sean Morley
84566b8ec1 User profile settings API and remove old Dj-Rest-Auth code 2024-11-30 10:24:27 -05:00
Sean Morley
c65fcc2558 Migrate to session based auth 2024-11-29 18:20:51 -05:00
Sean Morley
b86c7258e7 Enhance admin security by integrating secure_admin_login from AllAuth and updating settings for new dependencies 2024-11-29 17:51:32 -05:00
Sean Morley
1f4a02467b
Merge pull request #392 from seanmorley15/fix-390
Update adventure display to show category name and icon
2024-11-29 17:12:49 -05:00
Sean Morley
aecb3ca70e Update adventure display to show category name and icon 2024-11-29 17:08:52 -05:00
Sean Morley
2d24cb8aba Merge branch 'main' of github.com:seanmorley15/AdventureLog 2024-11-29 17:03:23 -05:00
Sean Morley
0d9c063c58 Update .gitignore to exclude .vscode/settings.json 2024-11-29 17:03:20 -05:00
Sean Morley
f119e6fdc2 Refactor settings.py to remove unused authentication configurations and clean up middleware 2024-11-29 16:55:28 -05:00
Sean Morley
53dc57453c
Update README.md 2024-11-29 15:29:50 -05:00
Sean Morley
9bc20be70e Initial migration to new session based auth system with AllAuth 2024-11-29 14:41:13 -05:00
Sean Morley
bde2e6a82d
Merge pull request #389 from seanmorley15/development
Add Polish Localization and Update Docs
2024-11-27 18:07:18 -05:00
Sean Morley
7defdac3a8 Update documentation and add Polish locale support 2024-11-27 18:06:16 -05:00
Sean Morley
0878008a05
Merge pull request #378 from dymek37/patch-11
Update de.json
2024-11-27 18:01:58 -05:00
Sean Morley
0b08dfbab9
Merge pull request #379 from dymek37/patch-12
Update en.json
2024-11-27 18:01:49 -05:00
Sean Morley
888bde042e
Merge pull request #380 from dymek37/patch-13
Update es.json
2024-11-27 18:01:35 -05:00
Sean Morley
335b5c2498
Merge pull request #381 from dymek37/patch-14
Update fr.json
2024-11-27 18:01:21 -05:00
Sean Morley
9f9e4fd12e
Merge pull request #382 from dymek37/patch-15
Update it.json
2024-11-27 18:01:13 -05:00
Sean Morley
30e2606a2f
Merge pull request #383 from dymek37/patch-16
Update nl.json
2024-11-27 18:01:05 -05:00
Sean Morley
df0d49298f
Merge pull request #384 from dymek37/patch-17
Update sv.json
2024-11-27 18:00:56 -05:00
Sean Morley
2ea3637c4b
Merge pull request #386 from dymek37/patch-18
Update zh.json
2024-11-27 18:00:45 -05:00
Sean Morley
84c013fa11
Merge pull request #377 from dymek37/patch-10
Create pl.json
2024-11-27 18:00:35 -05:00
Sean Morley
f618c24a7b
Update README.md 2024-11-27 12:34:56 -05:00
Sean Morley
39991c1877
Merge pull request #388 from seanmorley15/development
Add Umami analytics script to VitePress
2024-11-27 12:19:24 -05:00
Sean Morley
483c297af8 Add Umami analytics script to VitePress config for enhanced tracking 2024-11-27 12:18:29 -05:00
Sean Morley
66713ea76d
Merge pull request #387 from seanmorley15/development
Enhance documentation: update VitePress config to ignore localhost li…
2024-11-27 12:07:50 -05:00
Sean Morley
ec4935ecdd Update VitePress config: set ignoreDeadLinks to 'localhostLinks' for improved link management 2024-11-27 12:01:53 -05:00
Sean Morley
173c6183b3 Simplify VitePress config: set ignoreDeadLinks to true for streamlined link management 2024-11-27 11:59:24 -05:00
Sean Morley
f5a0f388b7 Enhance documentation: update VitePress config to ignore localhost links with any port and correct docker-compose.yml formatting for Nginx Proxy Manager 2024-11-27 11:58:25 -05:00
Sean Morley
9d2f83d4b6
Implement Custom Categories
Implement Custom Categories and other bug fixes
2024-11-27 11:28:02 -05:00
Sean Morley
7a3ec33fa7 Fix locale fallback logic: ensure valid fallback locale from navigator language or default to 'en' 2024-11-27 11:27:18 -05:00
Sean Morley
958e9de84e Enhance cookie management: set SameSite attribute for locale and theme cookies, and add comments for clarity 2024-11-27 11:25:33 -05:00
dymek37
06f0b93dd5
Update zh.json 2024-11-27 17:09:05 +01:00
dymek37
734208d869
Update sv.json 2024-11-27 17:06:12 +01:00
dymek37
fd3c30f540
Update nl.json 2024-11-27 17:04:04 +01:00
dymek37
6963fd858e
Update it.json 2024-11-27 17:00:11 +01:00
dymek37
74ff1b0b7f
Update fr.json 2024-11-27 16:58:22 +01:00
dymek37
4dbcd96581
Update es.json 2024-11-27 16:55:06 +01:00
dymek37
def642960c
Update en.json 2024-11-27 16:54:03 +01:00
dymek37
d20de4aeed
Update de.json 2024-11-27 16:52:03 +01:00
dymek37
a0aae8c237
Create pl.json
Addition of Polish language.
2024-11-27 16:50:15 +01:00
Sean Morley
02f534662b
Merge branch 'main' into development 2024-11-26 22:11:40 -05:00
Sean Morley
477e76a0af Fix user identification in adventure filtering: update user primary key reference from 'pk' to 'uuid' in search page logic 2024-11-26 21:59:15 -05:00
Sean Morley
f878167a36 Enhance category management: update adventure category assignment logic, improve category display in UI components, and add dynamic sorting for category dropdown 2024-11-26 20:06:52 -05:00
Sean Morley
adf45ff557 Fix general category handling 2024-11-26 17:39:10 -05:00
Sean Morley
ce0b82acb7 Fix custom default category 2024-11-26 15:10:17 -05:00
Sean Morley
17d8784d8c Migration to new documentation and information site 2024-11-26 10:31:25 -05:00
dependabot[bot]
d7baf6961e
Bump @sveltejs/kit
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit).


Updates `@sveltejs/kit` from 2.5.17 to 2.8.3
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.8.3/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 16:06:57 +00:00
Sean Morley
b1ec7dc815
Update docker-compose-traefik.yaml
As described in #345
2024-11-23 13:55:48 -05:00
Sean Morley
8e5a20ec62 Refactor adventure category handling: update type definitions, enhance category management in UI components, and implement user-specific category deletion logic in the backend 2024-11-23 13:42:41 -05:00
Sean Morley
736ede2417 Enhance adventure management: add error handling for category fetch, implement unique email constraint in user model, and update adventure save logic to ensure category assignment 2024-11-22 17:03:02 -05:00
Sean Morley
a6630b7de2
Update issue templates 2024-11-22 10:02:43 -05:00
Sean Morley
9b28d9a315
Update issue templates 2024-11-22 10:01:53 -05:00
Sean Morley
86d213bb8b Refactor user ID handling to use UUIDs; update related components and serializers for consistency 2024-11-17 16:34:46 -05:00
Sean Morley
129c76078e Fetch and display categories in CategoryFilterDropdown; update adventure details to include category information 2024-11-16 23:32:23 -05:00
Sean Morley
42f07dc2fb Implement user-specific category filtering in AdventureViewSet and update AdventureCard to display category details 2024-11-16 22:31:39 -05:00
Sean Morley
ae92fc2027 Fix bug where num_visits was not user specific 2024-11-16 16:47:21 -05:00
Sean Morley
4a7f720773 Initial framework for custom categories 2024-11-14 09:37:35 -05:00
Sean Morley
0ea948c62c
Merge pull request #356 from seanmorley15/development
AdventureLog v0.7.1
2024-11-13 09:06:32 -05:00
Sean Morley
c3f37b66d0 Fix detection of custom locations in AdventureModal 2024-11-12 23:12:08 -05:00
Sean Morley
954c154add Add Umami Analytics documentation for configuration 2024-11-12 09:23:23 -05:00
Sean Morley
10a0963a1e Update motivational message in adventurelog.txt 2024-11-12 00:02:49 -05:00
Sean Morley
f16a7f1e94 Add adventurelog.txt with motivational message and display it on entrypoint 2024-11-11 23:56:16 -05:00
Sean Morley
39fce5ace7 Add guides category to documentation and rename nginx migration guide 2024-11-11 23:46:22 -05:00
Sean Morley
0a7704aade Add support link to documentation and format sidebar configuration 2024-11-11 23:38:35 -05:00
Sean Morley
adb92a1d41 Remove redundant validation checks in Adventure model 2024-11-11 09:38:22 -05:00
Sean Morley
bde33260fa Show tabs in description box 2024-11-09 22:25:47 -05:00
Sean Morley
3dc193d098 Update nginx migration documentation and change package name to adventurelog-docs 2024-11-09 22:09:58 -05:00
Sean Morley
742cb9dd94 Fix formatting in README.md for backend container configuration options 2024-11-08 17:53:46 -05:00
Sean Morley
6132b1c0d6 Update nginx migration documentation and enhance AdventureModal component functionality 2024-11-08 17:15:53 -05:00
Sean Morley
efd19da10b Add custom location handling in AdventureModal component 2024-11-08 09:39:03 -05:00
Sean Morley
98e545be6f Add 'set_to_pin' translation to multiple language files 2024-11-07 20:03:54 -05:00
Sean Morley
add691b678 Update outline style for adventure markers on map 2024-11-07 19:58:36 -05:00
Sean Morley
b32c780266 Fix issues determined from testing series 2024-11-07 19:48:44 -05:00
Sean Morley
c7207ccf52 Add internationalization support for profile and shared collections; update translations in multiple languages 2024-11-05 14:40:32 -05:00
Sean Morley
bcba4f4425 Remove unused command from Dockerfile 2024-11-05 14:07:12 -05:00
Sean Morley
2c3fb41e5c Add staticfiles directory to Dockerfile 2024-11-05 11:24:42 -05:00
Sean Morley
04b332e031 Add creation command to dockerfile 2024-11-05 11:15:57 -05:00
Sean Morley
de45296dc1 Refactor media and static file paths; update countries JSON download URL to use COUNTRY_REGION_JSON_VERSION 2024-11-05 10:55:06 -05:00
Sean Morley
c27bf4baba Update COUNTRY_REGION_JSON_VERSION to v2.4 and adjust download URL in management command 2024-11-05 10:41:01 -05:00
Sean Morley
87928932c2 Update download URL for countries JSON to use tagged version 2024-11-05 10:27:06 -05:00
Sean Morley
6eff9f6456 Update STATIC_ROOT path to use 'staticfiles' directory 2024-11-05 10:15:38 -05:00
Sean Morley
44c0ffc1d7 Refactor static file path and update theme selection text in Navbar; change localization keys for login and signup buttons 2024-11-05 10:13:30 -05:00
Sean Morley
40cb5daa07 Add i18n support for ImageInfoModal and update localization strings 2024-11-04 19:56:54 -05:00
Sean Morley
9c6e11b16d Add language selection and share localization for multiple languages 2024-11-04 19:41:25 -05:00
Sean Morley
9ac4a8f4e9 Update localization for activities and adventures; change links and placeholders 2024-11-04 19:25:07 -05:00
Sean Morley
7cd869a507
Merge pull request #354 from seanmorley15/dependabot/npm_and_yarn/documentation/npm_and_yarn-b2e09fc846
Bump http-proxy-middleware from 2.0.6 to 2.0.7 in /documentation in the npm_and_yarn group across 1 directory
2024-11-03 23:12:13 -05:00
Sean Morley
7988ba4d68 Remove unused GeoJSONView and related URL path; clean up README links 2024-11-03 22:57:07 -05:00
Sean Morley
3df124b250 Add i18n support for transportation, notes, checklist, and collection components 2024-11-03 22:55:38 -05:00
Sean Morley
c0aaec1436 More settings localization 2024-11-03 15:45:29 -05:00
Sean Morley
4bbbc10097 Bump version to 0.7.1, update COUNTRY_REGION_JSON_VERSION, and add nginx migration guide 2024-11-03 15:12:28 -05:00
Sean Morley
172f07acca Add popup toggle and card carousel for adventure details in map view 2024-11-02 21:45:23 -04:00
Sean Morley
6137411b84 Update adventure type label handling and improve map marker display 2024-11-02 21:29:42 -04:00
Sean Morley
07263c5697 Refactor map page 2024-11-02 21:18:52 -04:00
Sean Morley
e6c5bc9ca8 Add localization strings for visited regions feature and update UI components 2024-11-01 20:12:43 -04:00
Sean Morley
b60455b50a Add mark_visited_region action to ReverseGeocodeViewSet and update AdventureModal for region tracking 2024-11-01 20:08:23 -04:00
Sean Morley
a7a49227c4 Add display_name to ReverseGeocode response and update AdventureModal 2024-10-31 23:47:02 -04:00
Sean Morley
25ed72afbc Add is_visited filtering to AdventureViewSet and update frontend components 2024-10-31 09:59:56 -04:00
Sean Morley
727daf0cfd is visited 2024-10-31 09:51:04 -04:00
Sean Morley
9d42dbac98 Add functionality to mark regions as visited in AdventureModal 2024-10-30 18:40:49 -04:00
Sean Morley
83d06fc0a4 Remove AUTHORS and MANIFEST.in files; add ReverseGeocodeViewSet and localization updates 2024-10-30 15:11:00 -04:00
Sean Morley
05076a6732 Refactor localization strings and add missing translations 2024-10-29 10:29:03 -04:00
Sean Morley
fcd2d27221 more localization 2024-10-28 19:59:44 -04:00
Sean Morley
5011829e6e Refactor frontend configuration and add optional Umami Analytics integration 2024-10-28 19:10:40 -04:00
Sean Morley
4f40d0de63 Add Italian and Dutch localization files 2024-10-28 15:28:49 -04:00
Sean Morley
8068fe93f7 more localization 2024-10-28 15:10:14 -04:00
Sean Morley
91c0ec8c07 localization v2 2024-10-28 13:56:57 -04:00
Sean Morley
6cf62cfb82 localization v1 2024-10-26 23:03:35 -04:00
Sean Morley
6f8864a13d Refactor Docker Compose configuration and add email backend documentation 2024-10-26 19:43:27 -04:00
Sean Morley
6daacabc1b Refactor Docker Compose configuration and update environment variables 2024-10-26 10:57:52 -04:00
Sean Morley
ae70c96ddc Trigger workflows 2024-10-26 09:47:46 -04:00
dependabot[bot]
5435208d3e
Bump http-proxy-middleware
Bumps the npm_and_yarn group with 1 update in the /documentation directory: [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware).


Updates `http-proxy-middleware` from 2.0.6 to 2.0.7
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.6...v2.0.7)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-26 13:47:41 +00:00
Sean Morley
f03c74cd74 Add workflows for uploading beta backend and frontend images to GHCR and Docker Hub 2024-10-26 09:46:14 -04:00
Sean Morley
de873c9eb5 Add backend and frontend beta deployment workflows 2024-10-26 09:44:36 -04:00
Sean Morley
ef3f2aee10 Fast. Easy. Fun. Deployment 🚀 2024-10-25 22:57:10 -04:00
Sean Morley
78ab387055 Refactor Docusaurus configuration and frontend components
- Update the title and logo in Docusaurus configuration
- Add Discord and Support links to the navbar
- Update the label and href for the GitHub link in the footer
- Remove redundant code and comments in ImageInfoModal component
- Update the text and link in ImageInfoModal component
- Update the conditional statement in +page.svelte file
- Comment out the unused code in settings page
2024-10-25 14:20:51 -04:00
Sean Morley
874a384477 Migrate Django server to guicorn 2024-10-21 19:59:01 -04:00
Sean Morley
075257c846 Refactor login and signup pages to improve code readability and remove unused imports 2024-10-21 19:42:46 -04:00
Sean Morley
1e67de6e14 Add type to map tag 2024-10-21 15:58:39 -04:00
Sean Morley
0a8ff18560
Merge pull request #348 from seanmorley15/fix-sorting-by-date
Update `apply_sorting` and `get_queryset` methods in `adventures/view…
2024-10-21 15:55:30 -04:00
Sean Morley
e22e175ae5
Merge pull request #347 from seanmorley15/ghcw-session-58ae
Add dates to map
2024-10-21 15:55:05 -04:00
Sean Morley
53cfd276f2 Refactor adventure and map pages to display visit dates 2024-10-21 15:52:57 -04:00
Sean Morley
fdf4f07156 Update apply_sorting and get_queryset methods in adventures/views.py
* Change `apply_sorting` method to reference `start_date` field instead of `date` field
* Remove references to `date` field in `get_queryset` method
2024-10-21 15:28:45 -04:00
Sean Morley
f34d533dc6 Load background from server page 2024-10-20 21:56:16 -04:00
Sean Morley
c084f348d9 New login screen with featured images 2024-10-17 21:35:55 -04:00
Sean Morley
792d9bcda2 Change over to Voyager map styles 2024-10-17 15:14:15 -04:00
Sean Morley
a9b42439cc Filter worldtravel by subregion 2024-10-16 22:37:37 -04:00
Sean Morley
8175daf773 Filter countries by visit status 2024-10-16 22:23:59 -04:00
Sean Morley
3a66a433ca Conditional rendering to the country card to show visited status as a badge 2024-10-16 20:02:28 -04:00
417 changed files with 39930 additions and 272672 deletions

47
.env.example Normal file
View file

@ -0,0 +1,47 @@
# 🌐 Frontend
PUBLIC_SERVER_URL=http://server:8000 # PLEASE DON'T CHANGE :) - Should be the service name of the backend with port 8000, even if you change the port in the backend service. Only change if you are using a custom more complex setup.
ORIGIN=http://localhost:8015
BODY_SIZE_LIMIT=Infinity
FRONTEND_PORT=8015
# 🐘 PostgreSQL Database
PGHOST=db
POSTGRES_DB=database
POSTGRES_USER=adventure
POSTGRES_PASSWORD=changeme123
# 🔒 Django Backend
SECRET_KEY=changeme123
DJANGO_ADMIN_USERNAME=admin
DJANGO_ADMIN_PASSWORD=admin
DJANGO_ADMIN_EMAIL=admin@example.com
PUBLIC_URL=http://localhost:8016 # Match the outward port, used for the creation of image urls
CSRF_TRUSTED_ORIGINS=http://localhost:8016,http://localhost:8015
DEBUG=False
FRONTEND_URL=http://localhost:8015 # Used for email generation. This should be the url of the frontend
BACKEND_PORT=8016
# Optional: use Google Maps integration
# https://adventurelog.app/docs/configuration/google_maps_integration.html
# GOOGLE_MAPS_API_KEY=your_google_maps_api_key
# Optional: disable registration
# https://adventurelog.app/docs/configuration/disable_registration.html
DISABLE_REGISTRATION=False
# DISABLE_REGISTRATION_MESSAGE=Registration is disabled for this instance of AdventureLog.
# Optional: Use email
# https://adventurelog.app/docs/configuration/email.html
# EMAIL_BACKEND=email
# EMAIL_HOST=smtp.gmail.com
# EMAIL_USE_TLS=True
# EMAIL_PORT=587
# EMAIL_USE_SSL=False
# EMAIL_HOST_USER=user
# EMAIL_HOST_PASSWORD=password
# DEFAULT_FROM_EMAIL=user@example.com
# Optional: Use Umami for analytics
# https://adventurelog.app/docs/configuration/analytics.html
# PUBLIC_UMAMI_SRC=https://cloud.umami.is/script.js # If you are using the hosted version of Umami
# PUBLIC_UMAMI_WEBSITE_ID=

16
.github/.docker-compose-database.yml vendored Normal file
View file

@ -0,0 +1,16 @@
services:
db:
image: postgis/postgis:15-3.3
container_name: adventurelog-db
restart: unless-stopped
ports:
- "127.0.0.1:5432:5432"
environment:
POSTGRES_DB: database
POSTGRES_USER: adventure
POSTGRES_PASSWORD: changeme123
volumes:
- postgres_data:/var/lib/postgresql/data/
volumes:
postgres_data:

1
.github/CODEOWNERS vendored Normal file
View file

@ -0,0 +1 @@
* @seanmorley15

1
.github/FUNDING.yml vendored
View file

@ -1 +1,2 @@
github: seanmorley15
buy_me_a_coffee: seanmorley15

View file

@ -1,6 +1,6 @@
---
name: Bug report
about: "Detailed bug reports help me diagnose and fix bugs quicker! Thanks!"
about: Detailed bug reports help me diagnose and fix bugs quicker! Thanks!
title: "[BUG]"
labels: bug
assignees: ''

View file

@ -0,0 +1,15 @@
---
name: Deployment Issue
about: Request help deploying AdventureLog on your machine. The more details, the
better I can help!
title: "[DEPLOYMENT]"
labels: deployment
assignees: ''
---
## Explain your issue
## Provide an **obfuscated** `docker-compose.yml`
## Provide any necessary logs from the containers and browser

46
.github/workflows/backend-beta.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: Upload beta backend image to GHCR and Docker Hub
on:
push:
branches:
- development
paths:
- "backend/**"
env:
IMAGE_NAME: "adventurelog-backend"
jobs:
upload:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.ACCESS_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: set lower case owner name
run: |
echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV}
env:
OWNER: "${{ github.repository_owner }}"
- name: Build Docker images
run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:beta -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:beta ./backend

64
.github/workflows/backend-test.yml vendored Normal file
View file

@ -0,0 +1,64 @@
name: Test Backend
permissions:
contents: read
on:
pull_request:
paths:
- 'backend/server/**'
- '.github/workflows/backend-test.yml'
push:
paths:
- 'backend/server/**'
- '.github/workflows/backend-test.yml'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: set up python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: install dependencies
run: |
sudo apt update -q
sudo apt install -y -q \
python3-gdal
- name: start database
run: |
docker compose -f .github/.docker-compose-database.yml up -d
- name: install python libreries
working-directory: backend/server
run: |
pip install -r requirements.txt
- name: run server
working-directory: backend/server
env:
PGHOST: "127.0.0.1"
PGDATABASE: "database"
PGUSER: "adventure"
PGPASSWORD: "changeme123"
SECRET_KEY: "changeme123"
DJANGO_ADMIN_USERNAME: "admin"
DJANGO_ADMIN_PASSWORD: "admin"
DJANGO_ADMIN_EMAIL: "admin@example.com"
PUBLIC_URL: "http://localhost:8000"
CSRF_TRUSTED_ORIGINS: "http://localhost:5173,http://localhost:8000"
DEBUG: "True"
FRONTEND_URL: "http://localhost:5173"
run: |
python manage.py migrate
python manage.py runserver &
- name: wait for backend to boot
run: >
curl -fisS --retry 60 --retry-delay 1 --retry-all-errors
http://localhost:8000/

46
.github/workflows/cdn-beta.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: Upload beta CDN image to GHCR and Docker Hub
on:
push:
branches:
- development
paths:
- "cdn/**"
env:
IMAGE_NAME: "adventurelog-cdn"
jobs:
upload:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.ACCESS_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: set lower case owner name
run: |
echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV}
env:
OWNER: "${{ github.repository_owner }}"
- name: Build Docker images
run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:beta -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:beta ./cdn

46
.github/workflows/cdn-latest.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: Upload latest CDN image to GHCR and Docker Hub
on:
push:
branches:
- main
paths:
- "cdn/**"
env:
IMAGE_NAME: "adventurelog-cdn"
jobs:
upload:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.ACCESS_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: set lower case owner name
run: |
echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV}
env:
OWNER: "${{ github.repository_owner }}"
- name: Build Docker images
run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:latest -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:latest ./cdn

43
.github/workflows/cdn-release.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: Upload the tagged release CDN image to GHCR and Docker Hub
on:
release:
types: [released]
env:
IMAGE_NAME: "adventurelog-cdn"
jobs:
upload:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.ACCESS_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: set lower case owner name
run: |
echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV}
env:
OWNER: "${{ github.repository_owner }}"
- name: Build Docker images
run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:${{ github.event.release.tag_name }} -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:${{ github.event.release.tag_name }} ./cdn

46
.github/workflows/frontend-beta.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: Upload beta frontend image to GHCR and Docker Hub
on:
push:
branches:
- development
paths:
- "frontend/**"
env:
IMAGE_NAME: "adventurelog-frontend"
jobs:
upload:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.ACCESS_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: set lower case owner name
run: |
echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV}
env:
OWNER: "${{ github.repository_owner }}"
- name: Build Docker images
run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:beta -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:beta ./frontend

32
.github/workflows/frontend-test.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Test Frontend
permissions:
contents: read
on:
pull_request:
paths:
- "frontend/**"
- ".github/workflows/frontend-test.yml"
push:
paths:
- "frontend/**"
- ".github/workflows/frontend-test.yml"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: install dependencies
working-directory: frontend
run: npm i
- name: build frontend
working-directory: frontend
run: npm run build

3
.gitignore vendored
View file

@ -1,2 +1,5 @@
# Ignore everything in the .venv folder
.venv/
.vscode/settings.json
.pnpm-store/
.env

6
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"recommendations": [
"lokalise.i18n-ally",
"svelte.svelte-vscode"
]
}

39
.vscode/settings.json vendored
View file

@ -1,3 +1,40 @@
{
"git.ignoreLimitWarning": true
"git.ignoreLimitWarning": true,
"i18n-ally.localesPaths": [
"frontend/src/locales",
"backend/server/backend/lib/python3.12/site-packages/allauth/locale",
"backend/server/backend/lib/python3.12/site-packages/dj_rest_auth/locale",
"backend/server/backend/lib/python3.12/site-packages/rest_framework/locale",
"backend/server/backend/lib/python3.12/site-packages/rest_framework_simplejwt/locale",
"backend/server/backend/lib/python3.12/site-packages/django/conf/locale",
"backend/server/backend/lib/python3.12/site-packages/django/contrib/messages",
"backend/server/backend/lib/python3.12/site-packages/allauth/templates/account/messages",
"backend/server/backend/lib/python3.12/site-packages/allauth/templates/mfa/messages",
"backend/server/backend/lib/python3.12/site-packages/allauth/templates/socialaccount/messages",
"backend/server/backend/lib/python3.12/site-packages/allauth/templates/usersessions/messages",
"backend/server/backend/lib/python3.12/site-packages/django/contrib/admindocs/locale",
"backend/server/backend/lib/python3.12/site-packages/django/contrib/auth/locale",
"backend/server/backend/lib/python3.12/site-packages/django/contrib/admin/locale",
"backend/server/backend/lib/python3.12/site-packages/django/contrib/contenttypes/locale",
"backend/server/backend/lib/python3.12/site-packages/django/contrib/flatpages/locale",
"backend/server/backend/lib/python3.12/site-packages/django/contrib/humanize/locale",
"backend/server/backend/lib/python3.12/site-packages/django/contrib/gis/locale",
"backend/server/backend/lib/python3.12/site-packages/django/contrib/redirects/locale",
"backend/server/backend/lib/python3.12/site-packages/django/contrib/postgres/locale",
"backend/server/backend/lib/python3.12/site-packages/django/contrib/sessions/locale",
"backend/server/backend/lib/python3.12/site-packages/django/contrib/sites/locale",
"backend/server/backend/lib/python3.12/site-packages/rest_framework/templates/rest_framework/docs/langs"
],
"i18n-ally.keystyle": "nested",
"i18n-ally.keysInUse": [
"navbar.themes.dim",
"navbar.themes.northernLights",
"navbar.themes.aqua",
"navbar.themes.aestheticDark",
"navbar.themes.aestheticLight",
"navbar.themes.forest",
"navbar.themes.night",
"navbar.themes.dark",
"navbar.themes.light"
]
}

View file

@ -1,91 +1,50 @@
# Contributing to AdventureLog
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
Please note we have a code of conduct, please follow it in all your interactions with the project.
Were excited to have you contribute to AdventureLog! To ensure that this community remains welcoming and productive for all users and developers, please follow this simple Code of Conduct.
## Pull Request Process
1. Please make sure you create an issue first for your change so you can link any pull requests to this issue. There should be a clear relationship between pull requests and issues.
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. Increase the version numbers in any examples files and the README.md to the new version that this
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
do not have permission to do that, you may request the second reviewer to merge it for you.
1. **Open an Issue First**: Discuss any changes or features you plan to implement by opening an issue. This helps to clarify your idea and ensures theres a shared understanding.
2. **Document Changes**: If your changes impact the user interface, add new environment variables, or introduce new container configurations, make sure to update the documentation accordingly. The documentation is located in the `documentation` folder.
3. **Pull Request**: Submit a pull request with your changes. Make sure to reference the issue you opened in the description.
## Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
At AdventureLog, we are committed to creating a community that fosters adventure, exploration, and innovation. We encourage diverse participation and strive to maintain a space where everyone feels welcome to contribute, regardless of their background or experience level. We ask that you contribute with respect and kindness, making sure to prioritize collaboration and mutual growth.
### Our Standards
Examples of behavior that contributes to creating a positive environment
include:
In order to maintain a positive environment, we encourage the following behaviors:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
- **Inclusivity**: Use welcoming and inclusive language that fosters collaboration across all perspectives and experiences.
- **Respect**: Respect differing opinions and engage with empathy, understanding that each persons perspective is valuable.
- **Constructive Feedback**: Offer feedback that helps improve the project and allows contributors to grow from it.
- **Adventure Spirit**: Bring the same sense of curiosity, discovery, and positivity that drives AdventureLog into all interactions with the community.
Examples of unacceptable behavior by participants include:
Examples of unacceptable behavior include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
- Personal attacks, trolling, or any form of harassment.
- Insensitive or discriminatory language, including sexualized comments or imagery.
- Spamming or misusing project spaces for personal gain.
- Publishing or using others private information without permission.
- Anything else that could be seen as disrespectful or unprofessional in a collaborative environment.
### Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
As maintainers of AdventureLog, we are committed to enforcing this Code of Conduct and taking corrective action when necessary. This may involve moderating comments, pulling code, or banning users who engage in harmful behaviors.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
We strive to foster a community that balances open collaboration with respect for all contributors.
### Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
This Code of Conduct applies in all spaces related to AdventureLog. This includes our GitHub repository, discussions, documentation, social media accounts, and events—both online and in person.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
If you experience or witness unacceptable behavior, please report it to the project team at `contact@adventurelog.app`. All reports will be confidential and handled swiftly. The maintainers will investigate the issue and take appropriate action as needed.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
This Code of Conduct is inspired by the [Contributor Covenant](http://contributor-covenant.org), version 1.4, and adapted to fit the unique spirit of AdventureLog.

View file

@ -1,5 +1,5 @@
AdventureLog: Self-hostable travel tracker and trip planner.
Copyright (C) 2024 Sean Morley
Copyright (C) 2023-2025 Sean Morley
Contact: contact@seanmorley.com
This program is free software: you can redistribute it and/or modify

233
README.md
View file

@ -1,161 +1,150 @@
# AdventureLog: Embark, Explore, Remember. 🌍
<div align="center">
### _"Never forget an adventure with AdventureLog - Your ultimate travel companion!"_
<img src="brand/adventurelog.png" alt="logo" width="200" height="auto" />
<h1>AdventureLog</h1>
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/seanmorley15)
<p>
The ultimate travel companion for the modern-day explorer.
</p>
**Documentation can be found [here](https://docs.adventurelog.app).**
<h4>
<a href="https://demo.adventurelog.app">View Demo</a>
<span> · </span>
<a href="https://adventurelog.app">Documentation</a>
<span> · </span>
<a href="https://discord.gg/wRbQ9Egr8C">Discord</a>
<span> · </span>
<a href="https://buymeacoffee.com/seanmorley15">Support 💖</a>
</h4>
</div>
**Join the AdventureLog Community Discord Server [here](https://discord.gg/wRbQ9Egr8C).**
<br />
<!-- Table of Contents -->
# Table of Contents
- [Installation](#installation)
- [Docker 🐋](#docker-)
- [Prerequisites](#prerequisites)
- [Getting Started](#getting-started)
- [Configuration](#configuration)
- [Frontend Container (web)](#frontend-container-web)
- [Backend Container (server)](#backend-container-server)
- [Proxy Container (nginx) Configuration](#proxy-container-nginx-configuration)
- [Running the Containers](#running-the-containers)
- [Screenshots 🖼️](#screenshots)
- [About AdventureLog](#about-adventurelog)
- [Attribution](#attribution)
- [About the Project](#-about-the-project)
- [Screenshots](#-screenshots)
- [Tech Stack](#-tech-stack)
- [Features](#-features)
- [Roadmap](#-roadmap)
- [Contributing](#-contributing)
- [License](#-license)
- [Contact](#-contact)
- [Acknowledgements](#-acknowledgements)
# Installation
<!-- About the Project -->
# Docker 🐋
## ⭐ About the Project
Docker is the preferred way to run AdventureLog on your local machine. It is a lightweight containerization technology that allows you to run applications in isolated environments called containers.
**Note**: This guide mainly focuses on installation with a linux based host machine, but the steps are similar for other operating systems.
Starting from a simple idea of tracking travel locations (called adventures), AdventureLog has grown into a full-fledged travel companion. With AdventureLog, you can log your adventures, keep track of where you've been on the world map, plan your next trip collaboratively, and share your experiences with friends and family.
## Prerequisites
AdventureLog was created to solve a problem: the lack of a modern, open-source, user-friendly travel companion. Many existing travel apps are either too complex, too expensive, or too closed-off to be useful for the average traveler. AdventureLog aims to be the opposite: simple, beautiful, and open to everyone.
- Docker installed on your machine/server. You can learn how to download it [here](https://docs.docker.com/engine/install/).
<!-- Screenshots -->
## Getting Started
### 📷 Screenshots
Get the `docker-compose.yml` file from the AdventureLog repository. You can download it from [here](https://github.com/seanmorley15/AdventureLog/blob/main/docker-compose.yml) or run this command to download it directly to your machine:
<div align="center">
<img src="./brand/screenshots/adventures.png" alt="Adventures" />
<p>Displays the adventures you have visited and the ones you plan to embark on. You can also filter and sort the adventures.</p>
<img src="./brand/screenshots/details.png" alt="Adventure Details" />
<p>Shows specific details about an adventure, including the name, date, location, description, and rating.</p>
<img src="./brand/screenshots/edit.png" alt="Edit Modal" />
<img src="./brand/screenshots/map.png" alt="Adventure Details" />
<p>View all of your adventures on a map, with the ability to filter by visit status and add new ones by click on the map</p>
<img src="./brand/screenshots/dashboard.png" alt="Dashboard" />
<p>Displays a summary of your adventures, including your world travel stats.</p>
<img src="./brand/screenshots/itinerary.png" alt="Itinerary" />
<p>Plan your adventures and travel itinerary with a list of activities and a map view. View your trip in a variety of ways, including an itinerary list, a map view, and a calendar view.</p>
<img src="./brand/screenshots/countries.png" alt="Countries" />
<p>Lists all the countries you have visited and plan to visit, with the ability to filter by visit status.</p>
<img src="./brand/screenshots/regions.png" alt="Regions" />
<p>Displays the regions for a specific country, includes a map view to visually select regions.</p>
</div>
```bash
wget https://raw.githubusercontent.com/seanmorley15/AdventureLog/main/docker-compose.yml
```
<!-- TechStack -->
## Configuration
### 🚀 Tech Stack
Here is a summary of the configuration options available in the `docker-compose.yml` file:
<details>
<summary>Client</summary>
<ul>
<li><a href="https://svelte.dev/">SvelteKit</a></li>
<li><a href="https://tailwindcss.com/">TailwindCSS</a></li>
<li><a href="https://daisyui.com/">DaisyUI</a></li>
<li><a href="https://github.com/dimfeld/svelte-maplibre/">Svelte MapLibre</a></li>
</ul>
</details>
<!-- make a table with colum name, is required, other -->
<details>
<summary>Server</summary>
<ul>
<li><a href="https://www.djangoproject.com/">Django</a></li>
<li><a href="https://postgis.net/">PostGIS</a></li>
<li><a href="https://www.django-rest-framework.org/">Django REST Framework</a></li>
<li><a href="https://allauth.org/">AllAuth</a></li>
</ul>
</details>
<!-- Features -->
### Frontend Container (web)
### 🎯 Features
| Name | Required | Description | Default Value |
| ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| `PUBLIC_SERVER_URL` | Yes | What the frontend SSR server uses to connect to the backend. | http://server:8000 |
| `ORIGIN` | Sometimes | Not needed if using HTTPS. If not, set it to the domain of what you will acess the app from. | http://localhost:8080 |
| `BODY_SIZE_LIMIT` | Yes | Used to set the maximum upload size to the server. Should be changed to prevent someone from uploading too much! Custom values must be set in **kiliobytes**. | Infinity |
- **Track Your Adventures** 🌍: Log your adventures and keep track of where you've been on the world map.
- Adventures can store a variety of information, including the location, date, and description.
- Adventures can be sorted into custom categories for easy organization.
- Adventures can be marked as private or public, allowing you to share your adventures with friends and family.
- Keep track of the countries and regions you've visited with the world travel book.
- **Plan Your Next Trip** 📃: Take the guesswork out of planning your next adventure with an easy-to-use itinerary planner.
- Itineraries can be created for any number of days and can include multiple destinations.
- Itineraries include many planning features like flight information, notes, checklists, and links to external resources.
- Itineraries can be shared with friends and family for collaborative planning.
- **Share Your Experiences** 📸: Share your adventures with friends and family and collaborate on trips together.
- Adventures and itineraries can be shared via a public link or directly with other AdventureLog users.
- Collaborators can view and edit shared itineraries (collections), making planning a breeze.
### Backend Container (server)
<!-- Roadmap -->
| Name | Required | Description | Default Value |
| ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| `PGHOST` | Yes | Databse host. | db |
| `PGDATABASE` | Yes | Database. | database |
| `PGUSER` | Yes | Database user. | adventure |
| `PGPASSWORD` | Yes | Database password. | changeme123 |
| `DJANGO_ADMIN_USERNAME` | Yes | Default username. | admin |
| `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after inital login. | admin |
| `DJANGO_ADMIN_EMAIL` | Yes | Default user's email. | admin@example.com |
| `PUBLIC_URL` | Yes | This is the publically accessible url to the **nginx** container. You should be able to acess nginx from this url where you access your app. | http://127.0.0.1:81 |
| `CSRF_TRUSTED_ORIGINS` | Yes | Need to be changed to the orgins where you use your backend server and frontend. These values are comma seperated. | Needs to be changed. |
| `FRONTEND_URL` | Yes | This is the publically accessible url to the **frontend** container. This link should be accessable for all users. Used for email generation. | http://localhost:3000 |
## 🧭 Roadmap
### Proxy Container (nginx) Configuration
The AdventureLog Roadmap can be found [here](https://github.com/users/seanmorley15/projects/5)
In order to use media files in a production environment, you need to configure the `nginx` container to serve the media files. The container is already in the docker compose file but you need to do a few things to make it work.
<!-- Contributing -->
1. Create a directory called `proxy` in the same directory as the `docker-compose.yml` file.
2. Create a file called `nginx.conf` in the `proxy` directory.
3. Add the following configuration to the `nginx.conf` file:
## 👋 Contributing
```nginx
server {
listen 80;
server_name localhost;
<a href="https://github.com/seanmorley15/AdventureLog/graphs/contributors">
<img src="https://contrib.rocks/image?repo=seanmorley15/AdventureLog" />
</a>
location /media/ {
alias /app/media/;
}
}
```
Contributions are always welcome!
## Running the Containers
See `contributing.md` for ways to get started.
To start the containers, run the following command:
<!-- License -->
```bash
docker compose up -d
```
## 📃 License
Enjoy AdventureLog! 🎉
Distributed under the GNU General Public License v3.0. See `LICENSE` for more information.
# Screenshots
<!-- Contact -->
![Adventure Page](screenshots/adventures.png)
Displaying the adventures you have visited and the ones you plan to embark on. You can also filter and sort the adventures.
## 🤝 Contact
![Detail Page](screenshots/details.png)
Shows specific details about an adventure, including the name, date, location, description, and rating.
Sean Morley - [website](https://seanmorley.com)
![Edit](screenshots/edit.png)
Hi! I'm Sean, the creator of AdventureLog. I'm a college student and software developer with a passion for travel and adventure. I created AdventureLog to help people like me document their adventures and plan new ones effortlessly. As a student, I am always looking for more opportunities to learn and grow, so feel free to reach out via the contact on my website if you would like to collaborate or chat!
![Map Page](screenshots/map.png)
View all of your adventures on a map, with the ability to filter by visit status and add new ones by click on the map.
<!-- Acknowledgments -->
![Itinerary Page](screenshots/itinerary.png)
## 💎 Acknowledgements
![Country Page](screenshots/countries.png)
![Region Page](screenshots/regions.png)
# About AdventureLog
AdventureLog is a Svelte Kit and Django application that utilizes a PostgreSQL database. Users can log the adventures they have experienced, as well as plan future ones. Key features include:
- Logging past adventures with fields like name, date, location, description, and rating.
- Planning future adventures with similar fields.
- Tagging different activity types for better organization.
- Viewing countries, regions, and marking visited regions.
AdventureLog aims to be your ultimate travel companion, helping you document your adventures and plan new ones effortlessly.
AdventureLog is licensed under the GNU General Public License v3.0.
<!-- ## Screenshots 🖼️
![Visited Log](https://github.com/seanmorley15/AdventureLog/blob/main/brand/screenshots/visited.png?raw=true)
![Planner Log](https://github.com/seanmorley15/AdventureLog/blob/main/brand/screenshots/ideas.png?raw=true)
![Country List](https://github.com/seanmorley15/AdventureLog/blob/main/brand/screenshots/countrylist.png?raw=true)
![Region List for the United States](https://github.com/seanmorley15/AdventureLog/blob/main/brand/screenshots/regions.png?raw=true)
## Roadmap 🛣️
- Improved mobile device support
- Password reset functionality
- Improved error handling
- Handling of adventure cards with variable width -->
# Attribution
- Logo Design by [redtechtiger](https://github.com/redtechtiger)
- Logo Design by [nordtektiger](https://github.com/nordtektiger)
- WorldTravel Dataset [dr5hn/countries-states-cities-database](https://github.com/dr5hn/countries-states-cities-database)
- [Mexico GEOJSON](https://cartographyvectors.com/map/784-mexico-with-states)
- [Japan GEOJSON](https://cartographyvectors.com/map/361-japan)
- [Ireland GEOJSON](https://cartographyvectors.com/map/1399-ireland-provinces)
- [Sweden GEOJSON](https://cartographyvectors.com/map/1521-sweden-with-regions)
- [Switzerland GEOJSON](https://cartographyvectors.com/map/1522-switzerland-with-regions)
- [Iceland GEOJSON](https://cartographyvectors.com/map/1453-iceland-with-regions)
- [Austria GEOJSON](https://github.com/codeforgermany/click_that_hood/blob/main/public/data/austria-states.geojson)
### Top Supporters 💖
- [Veymax](https://x.com/veymax)
- [nebriv](https://github.com/nebriv)
- [Victor Butler](https://x.com/victor_butler)

View file

@ -1 +0,0 @@
http://github.com/iMerica/dj-rest-auth/contributors

View file

@ -1,33 +1,60 @@
# Dockerfile
# Use the official Python slim image as the base image
FROM python:3.13-slim
FROM python:3.10-slim
LABEL Developers="Sean Morley"
# Metadata labels for the AdventureLog image
LABEL maintainer="Sean Morley" \
version="v0.10.0" \
description="AdventureLog — the ultimate self-hosted travel companion." \
org.opencontainers.image.title="AdventureLog" \
org.opencontainers.image.description="AdventureLog is a self-hosted travel companion that helps you plan, track, and share your adventures." \
org.opencontainers.image.version="v0.10.0" \
org.opencontainers.image.authors="Sean Morley" \
org.opencontainers.image.url="https://raw.githubusercontent.com/seanmorley15/AdventureLog/refs/heads/main/brand/banner.png" \
org.opencontainers.image.source="https://github.com/seanmorley15/AdventureLog" \
org.opencontainers.image.vendor="Sean Morley" \
org.opencontainers.image.created="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
org.opencontainers.image.licenses="GPL-3.0"
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Set the working directory
WORKDIR /code
# Install system dependencies
# Install system dependencies (Nginx included)
RUN apt-get update \
&& apt-get install -y git postgresql-client gdal-bin libgdal-dev \
&& apt-get clean
&& apt-get install -y git postgresql-client gdal-bin libgdal-dev nginx supervisor \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY ./server/requirements.txt /code/
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
RUN pip install --upgrade pip \
&& pip install -r requirements.txt
# Create necessary directories
RUN mkdir -p /code/static /code/media
# RUN mkdir -p /code/staticfiles /code/media
# Copy the Django project code into the Docker image
COPY ./server /code/
# Copy Nginx configuration
COPY ./nginx.conf /etc/nginx/nginx.conf
# Copy Supervisor configuration
COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Collect static files
RUN python3 manage.py collectstatic --noinput --verbosity 2
# Set the entrypoint script
COPY ./entrypoint.sh /code/entrypoint.sh
RUN chmod +x /code/entrypoint.sh
ENTRYPOINT ["/code/entrypoint.sh"]
# Expose ports for NGINX and Gunicorn
EXPOSE 80 8000
# Command to start Supervisor (which starts Nginx and Gunicorn)
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View file

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 iMerica https://github.com/iMerica/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,5 +0,0 @@
include AUTHORS
include LICENSE
include MANIFEST.in
include README.md
graft dj_rest_auth

View file

@ -1,4 +0,0 @@
# AdventureLog Django Backend
A demo of a possible AdventureLog 2.0 version using Django as the backend with a REST API.
Based of django-rest-framework and dj-rest-auth.

View file

@ -1,10 +1,32 @@
#!/bin/bash
# Function to check PostgreSQL availability
check_postgres() {
PGPASSWORD=$PGPASSWORD psql -h "$PGHOST" -U "$PGUSER" -d "$PGDATABASE" -c '\q' >/dev/null 2>&1
# Helper to get the first non-empty environment variable
get_env() {
for var in "$@"; do
value="${!var}"
if [ -n "$value" ]; then
echo "$value"
return
fi
done
}
check_postgres() {
local db_host
local db_user
local db_name
local db_pass
db_host=$(get_env PGHOST)
db_user=$(get_env PGUSER POSTGRES_USER)
db_name=$(get_env PGDATABASE POSTGRES_DB)
db_pass=$(get_env PGPASSWORD POSTGRES_PASSWORD)
PGPASSWORD="$db_pass" psql -h "$db_host" -U "$db_user" -d "$db_name" -c '\q' >/dev/null 2>&1
}
# Wait for PostgreSQL to become available
until check_postgres; do
>&2 echo "PostgreSQL is unavailable - sleeping"
@ -20,21 +42,50 @@ done
python manage.py migrate
# Create superuser if environment variables are set and there are no users present at all.
if [ -n "$DJANGO_ADMIN_USERNAME" ] && [ -n "$DJANGO_ADMIN_PASSWORD" ]; then
if [ -n "$DJANGO_ADMIN_USERNAME" ] && [ -n "$DJANGO_ADMIN_PASSWORD" ] && [ -n "$DJANGO_ADMIN_EMAIL" ]; then
echo "Creating superuser..."
python manage.py shell << EOF
from django.contrib.auth import get_user_model
from allauth.account.models import EmailAddress
User = get_user_model()
if User.objects.count() == 0:
User.objects.create_superuser('$DJANGO_ADMIN_USERNAME', '$DJANGO_ADMIN_EMAIL', '$DJANGO_ADMIN_PASSWORD')
# Check if the user already exists
if not User.objects.filter(username='$DJANGO_ADMIN_USERNAME').exists():
# Create the superuser
superuser = User.objects.create_superuser(
username='$DJANGO_ADMIN_USERNAME',
email='$DJANGO_ADMIN_EMAIL',
password='$DJANGO_ADMIN_PASSWORD'
)
print("Superuser created successfully.")
# Create the EmailAddress object for AllAuth
EmailAddress.objects.create(
user=superuser,
email='$DJANGO_ADMIN_EMAIL',
verified=True,
primary=True
)
print("EmailAddress object created successfully for AllAuth.")
else:
print("Superuser already exists.")
EOF
fi
# Sync the countries and world travel regions
# Sync the countries and world travel regions
python manage.py download-countries
if [ $? -eq 137 ]; then
>&2 echo "WARNING: The download-countries command was interrupted. This is likely due to lack of memory allocated to the container or the host. Please try again with more memory."
exit 1
fi
# Start Django server
python manage.py runserver 0.0.0.0:8000
cat /code/adventurelog.txt
# Start Gunicorn in foreground
exec gunicorn main.wsgi:application \
--bind [::]:8000 \
--workers 2 \
--timeout 120

42
backend/nginx.conf Normal file
View file

@ -0,0 +1,42 @@
worker_processes 1;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
client_max_body_size 100M;
# The backend is running in the same container, so reference localhost
upstream django {
server 127.0.0.1:8000; # Use localhost to point to Gunicorn running internally
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://django; # Forward to the upstream block
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static/ {
alias /code/staticfiles/; # Serve static files directly
}
# Serve protected media files with X-Accel-Redirect
location /protectedMedia/ {
internal; # Only internal requests are allowed
alias /code/media/; # This should match Django MEDIA_ROOT
try_files $uri =404; # Return a 404 if the file doesn't exist
# Security headers for all protected files
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; object-src 'none'; base-uri 'none'" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
}
}

View file

@ -21,3 +21,16 @@ EMAIL_BACKEND='console'
# EMAIL_HOST_USER='user'
# EMAIL_HOST_PASSWORD='password'
# DEFAULT_FROM_EMAIL='user@example.com'
# GOOGLE_MAPS_API_KEY='key'
# ------------------- #
# For Developers to start a Demo Database
# docker run --name adventurelog-development -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=adventurelog -p 5432:5432 -d postgis/postgis:15-3.3
# PGHOST='localhost'
# PGDATABASE='adventurelog'
# PGUSER='admin'
# PGPASSWORD='admin'
# ------------------- #

View file

@ -0,0 +1,9 @@
from django.contrib import admin
from allauth.account.decorators import secure_admin_login
from achievements.models import Achievement, UserAchievement
admin.autodiscover()
admin.site.login = secure_admin_login(admin.site.login)
admin.site.register(Achievement)
admin.site.register(UserAchievement)

View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AchievementsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'achievements'

View file

@ -0,0 +1,66 @@
import json
from django.core.management.base import BaseCommand
from achievements.models import Achievement
US_STATE_CODES = [
'US-AL', 'US-AK', 'US-AZ', 'US-AR', 'US-CA', 'US-CO', 'US-CT', 'US-DE',
'US-FL', 'US-GA', 'US-HI', 'US-ID', 'US-IL', 'US-IN', 'US-IA', 'US-KS',
'US-KY', 'US-LA', 'US-ME', 'US-MD', 'US-MA', 'US-MI', 'US-MN', 'US-MS',
'US-MO', 'US-MT', 'US-NE', 'US-NV', 'US-NH', 'US-NJ', 'US-NM', 'US-NY',
'US-NC', 'US-ND', 'US-OH', 'US-OK', 'US-OR', 'US-PA', 'US-RI', 'US-SC',
'US-SD', 'US-TN', 'US-TX', 'US-UT', 'US-VT', 'US-VA', 'US-WA', 'US-WV',
'US-WI', 'US-WY'
]
ACHIEVEMENTS = [
{
"name": "First Adventure",
"key": "achievements.first_adventure",
"type": "adventure_count",
"description": "Log your first adventure!",
"condition": {"type": "adventure_count", "value": 1},
},
{
"name": "Explorer",
"key": "achievements.explorer",
"type": "adventure_count",
"description": "Log 10 adventures.",
"condition": {"type": "adventure_count", "value": 10},
},
{
"name": "Globetrotter",
"key": "achievements.globetrotter",
"type": "country_count",
"description": "Visit 5 different countries.",
"condition": {"type": "country_count", "value": 5},
},
{
"name": "American Dream",
"key": "achievements.american_dream",
"type": "country_count",
"description": "Visit all 50 states in the USA.",
"condition": {"type": "country_count", "items": US_STATE_CODES},
}
]
class Command(BaseCommand):
help = "Seeds the database with predefined achievements"
def handle(self, *args, **kwargs):
for achievement_data in ACHIEVEMENTS:
achievement, created = Achievement.objects.update_or_create(
name=achievement_data["name"],
defaults={
"description": achievement_data["description"],
"condition": json.dumps(achievement_data["condition"]),
"type": achievement_data["type"],
"key": achievement_data["key"],
},
)
if created:
self.stdout.write(self.style.SUCCESS(f"✅ Created: {achievement.name}"))
else:
self.stdout.write(self.style.WARNING(f"🔄 Updated: {achievement.name}"))

View file

@ -0,0 +1,34 @@
import uuid
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
VALID_ACHIEVEMENT_TYPES = [
"adventure_count",
"country_count",
]
class Achievement(models.Model):
"""Stores all possible achievements"""
name = models.CharField(max_length=255, unique=True)
key = models.CharField(max_length=255, unique=True, default='achievements.other') # Used for frontend lookups, e.g. "achievements.first_adventure"
type = models.CharField(max_length=255, choices=[(tag, tag) for tag in VALID_ACHIEVEMENT_TYPES], default='adventure_count') # adventure_count, country_count, etc.
description = models.TextField()
icon = models.ImageField(upload_to="achievements/", null=True, blank=True)
condition = models.JSONField() # Stores rules like {"type": "adventure_count", "value": 10}
def __str__(self):
return self.name
class UserAchievement(models.Model):
"""Tracks which achievements a user has earned"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
achievement = models.ForeignKey(Achievement, on_delete=models.CASCADE)
earned_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("user", "achievement") # Prevent duplicates
def __str__(self):
return f"{self.user.username} - {self.achievement.name}"

View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View file

@ -0,0 +1,7 @@
█████╗ ██████╗ ██╗ ██╗███████╗███╗ ██╗████████╗██╗ ██╗██████╗ ███████╗██╗ ██████╗ ██████╗
██╔══██╗██╔══██╗██║ ██║██╔════╝████╗ ██║╚══██╔══╝██║ ██║██╔══██╗██╔════╝██║ ██╔═══██╗██╔════╝
███████║██║ ██║██║ ██║█████╗ ██╔██╗ ██║ ██║ ██║ ██║██████╔╝█████╗ ██║ ██║ ██║██║ ███╗
██╔══██║██║ ██║╚██╗ ██╔╝██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██╔══██╗██╔══╝ ██║ ██║ ██║██║ ██║
██║ ██║██████╔╝ ╚████╔╝ ███████╗██║ ╚████║ ██║ ╚██████╔╝██║ ██║███████╗███████╗╚██████╔╝╚██████╔╝
╚═╝ ╚═╝╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═════╝ ╚═════╝
“The world is full of wonderful things you haven't seen yet. Don't ever give up on the chance of seeing them.” - J.K. Rowling

View file

@ -1,14 +1,47 @@
import os
from django.contrib import admin
from django.utils.html import mark_safe
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit
from worldtravel.models import Country, Region, VisitedRegion
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category, Attachment, Lodging
from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
from allauth.account.decorators import secure_admin_login
admin.autodiscover()
admin.site.login = secure_admin_login(admin.site.login)
@admin.action(description="Trigger geocoding")
def trigger_geocoding(modeladmin, request, queryset):
count = 0
for adventure in queryset:
try:
adventure.save() # Triggers geocoding logic in your model
count += 1
except Exception as e:
modeladmin.message_user(request, f"Error geocoding {adventure}: {e}", level='error')
modeladmin.message_user(request, f"Geocoding triggered for {count} adventures.", level='success')
class AdventureAdmin(admin.ModelAdmin):
list_display = ('name', 'type', 'user_id', 'is_public')
list_filter = ('type', 'user_id', 'is_public')
list_display = ('name', 'get_category', 'get_visit_count', 'user_id', 'is_public')
list_filter = ( 'user_id', 'is_public')
search_fields = ('name',)
readonly_fields = ('city', 'region', 'country')
actions = [trigger_geocoding]
def get_category(self, obj):
if obj.category and obj.category.display_name and obj.category.icon:
return obj.category.display_name + ' ' + obj.category.icon
elif obj.category and obj.category.name:
return obj.category.name
else:
return 'No Category'
get_category.short_description = 'Category'
def get_visit_count(self, obj):
return obj.visits.count()
get_visit_count.short_description = 'Visit Count'
class CountryAdmin(admin.ModelAdmin):
@ -33,17 +66,27 @@ class RegionAdmin(admin.ModelAdmin):
number_of_visits.short_description = 'Number of Visits'
class CityAdmin(admin.ModelAdmin):
list_display = ('name', 'region', 'country')
list_filter = ('region', 'region__country')
search_fields = ('name', 'region__name', 'region__country__name')
def country(self, obj):
return obj.region.country.name
country.short_description = 'Country'
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from users.models import CustomUser
class CustomUserAdmin(UserAdmin):
model = CustomUser
list_display = ['username', 'email', 'is_staff', 'is_active', 'image_display']
list_display = ['username', 'is_staff', 'is_active', 'image_display']
readonly_fields = ('uuid',)
search_fields = ('username', 'email')
search_fields = ('username',)
fieldsets = UserAdmin.fieldsets + (
(None, {'fields': ('profile_pic', 'uuid', 'public_profile')}),
(None, {'fields': ('profile_pic', 'uuid', 'public_profile', 'disable_password')}),
)
def image_display(self, obj):
if obj.profile_pic:
@ -81,14 +124,14 @@ class VisitAdmin(admin.ModelAdmin):
image_display.short_description = 'Image Preview'
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'user_id', 'display_name', 'icon')
search_fields = ('name', 'display_name')
class CollectionAdmin(admin.ModelAdmin):
def adventure_count(self, obj):
return obj.adventure_set.count()
adventure_count.short_description = 'Adventure Count'
list_display = ('name', 'user_id', 'adventure_count', 'is_public')
list_display = ('name', 'user_id', 'is_public')
admin.site.register(CustomUser, CustomUserAdmin)
@ -105,6 +148,11 @@ admin.site.register(Note)
admin.site.register(Checklist)
admin.site.register(ChecklistItem)
admin.site.register(AdventureImage, AdventureImageAdmin)
admin.site.register(Category, CategoryAdmin)
admin.site.register(City, CityAdmin)
admin.site.register(VisitedCity)
admin.site.register(Attachment)
admin.site.register(Lodging)
admin.site.site_header = 'AdventureLog Admin'
admin.site.site_title = 'AdventureLog Admin Site'

View file

@ -1,6 +1,8 @@
from django.apps import AppConfig
from django.conf import settings
class AdventuresConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'adventures'
def ready(self):
import adventures.signals # Import signals when the app is ready

View file

@ -0,0 +1,273 @@
import requests
import time
import socket
from worldtravel.models import Region, City, VisitedRegion, VisitedCity
from django.conf import settings
# -----------------
# SEARCHING
def search_google(query):
try:
api_key = settings.GOOGLE_MAPS_API_KEY
if not api_key:
return {"error": "Missing Google Maps API key"}
# Updated to use the new Places API (New) endpoint
url = "https://places.googleapis.com/v1/places:searchText"
headers = {
'Content-Type': 'application/json',
'X-Goog-Api-Key': api_key,
'X-Goog-FieldMask': 'places.displayName.text,places.formattedAddress,places.location,places.types,places.rating,places.userRatingCount'
}
payload = {
"textQuery": query,
"maxResultCount": 20 # Adjust as needed
}
response = requests.post(url, json=payload, headers=headers, timeout=(2, 5))
response.raise_for_status()
data = response.json()
# Check if we have places in the response
places = data.get("places", [])
if not places:
return {"error": "No results found"}
results = []
for place in places:
location = place.get("location", {})
types = place.get("types", [])
primary_type = types[0] if types else None
category = _extract_google_category(types)
addresstype = _infer_addresstype(primary_type)
importance = None
rating = place.get("rating")
ratings_total = place.get("userRatingCount")
if rating is not None and ratings_total:
importance = round(float(rating) * ratings_total / 100, 2)
# Extract display name from the new API structure
display_name_obj = place.get("displayName", {})
name = display_name_obj.get("text") if display_name_obj else None
results.append({
"lat": location.get("latitude"),
"lon": location.get("longitude"),
"name": name,
"display_name": place.get("formattedAddress"),
"type": primary_type,
"category": category,
"importance": importance,
"addresstype": addresstype,
"powered_by": "google",
})
if results:
results.sort(key=lambda r: r["importance"] if r["importance"] is not None else 0, reverse=True)
return results
except requests.exceptions.RequestException as e:
return {"error": "Network error while contacting Google Maps", "details": str(e)}
except Exception as e:
return {"error": "Unexpected error during Google search", "details": str(e)}
def _extract_google_category(types):
# Basic category inference based on common place types
if not types:
return None
if "restaurant" in types:
return "food"
if "lodging" in types:
return "accommodation"
if "park" in types or "natural_feature" in types:
return "nature"
if "museum" in types or "tourist_attraction" in types:
return "attraction"
if "locality" in types or "administrative_area_level_1" in types:
return "region"
return types[0] # fallback to first type
def _infer_addresstype(type_):
# Rough mapping of Google place types to OSM-style addresstypes
mapping = {
"locality": "city",
"sublocality": "neighborhood",
"administrative_area_level_1": "region",
"administrative_area_level_2": "county",
"country": "country",
"premise": "building",
"point_of_interest": "poi",
"route": "road",
"street_address": "address",
}
return mapping.get(type_, None)
def search_osm(query):
url = f"https://nominatim.openstreetmap.org/search?q={query}&format=jsonv2"
headers = {'User-Agent': 'AdventureLog Server'}
response = requests.get(url, headers=headers)
data = response.json()
return [{
"lat": item.get("lat"),
"lon": item.get("lon"),
"name": item.get("name"),
"display_name": item.get("display_name"),
"type": item.get("type"),
"category": item.get("category"),
"importance": item.get("importance"),
"addresstype": item.get("addresstype"),
"powered_by": "nominatim",
} for item in data]
# -----------------
# REVERSE GEOCODING
# -----------------
def extractIsoCode(user, data):
"""
Extract the ISO code from the response data.
Returns a dictionary containing the region name, country name, and ISO code if found.
"""
iso_code = None
town_city_or_county = None
display_name = None
country_code = None
city = None
visited_city = None
location_name = None
# town = None
# city = None
# county = None
if 'name' in data.keys():
location_name = data['name']
if 'address' in data.keys():
keys = data['address'].keys()
for key in keys:
if key.find("ISO") != -1:
iso_code = data['address'][key]
if 'town' in keys:
town_city_or_county = data['address']['town']
if 'county' in keys:
town_city_or_county = data['address']['county']
if 'city' in keys:
town_city_or_county = data['address']['city']
if not iso_code:
return {"error": "No region found"}
region = Region.objects.filter(id=iso_code).first()
visited_region = VisitedRegion.objects.filter(region=region, user_id=user).first()
region_visited = False
city_visited = False
country_code = iso_code[:2]
if region:
if town_city_or_county:
display_name = f"{town_city_or_county}, {region.name}, {country_code}"
city = City.objects.filter(name__contains=town_city_or_county, region=region).first()
visited_city = VisitedCity.objects.filter(city=city, user_id=user).first()
if visited_region:
region_visited = True
if visited_city:
city_visited = True
if region:
return {"region_id": iso_code, "region": region.name, "country": region.country.name, "country_id": region.country.country_code, "region_visited": region_visited, "display_name": display_name, "city": city.name if city else None, "city_id": city.id if city else None, "city_visited": city_visited, 'location_name': location_name}
return {"error": "No region found"}
def is_host_resolvable(hostname: str) -> bool:
try:
socket.gethostbyname(hostname)
return True
except socket.error:
return False
def reverse_geocode(lat, lon, user):
if getattr(settings, 'GOOGLE_MAPS_API_KEY', None):
return reverse_geocode_google(lat, lon, user)
return reverse_geocode_osm(lat, lon, user)
def reverse_geocode_osm(lat, lon, user):
url = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}"
headers = {'User-Agent': 'AdventureLog Server'}
connect_timeout = 1
read_timeout = 5
if not is_host_resolvable("nominatim.openstreetmap.org"):
return {"error": "DNS resolution failed"}
try:
response = requests.get(url, headers=headers, timeout=(connect_timeout, read_timeout))
response.raise_for_status()
data = response.json()
return extractIsoCode(user, data)
except Exception:
return {"error": "An internal error occurred while processing the request"}
def reverse_geocode_google(lat, lon, user):
api_key = settings.GOOGLE_MAPS_API_KEY
# Updated to use the new Geocoding API endpoint (this one is still supported)
# The Geocoding API is separate from Places API and still uses the old format
url = "https://maps.googleapis.com/maps/api/geocode/json"
params = {"latlng": f"{lat},{lon}", "key": api_key}
try:
response = requests.get(url, params=params)
response.raise_for_status()
data = response.json()
if data.get("status") != "OK":
return {"error": "Geocoding failed"}
# Convert Google schema to Nominatim-style for extractIsoCode
first_result = data.get("results", [])[0]
result_data = {
"name": first_result.get("formatted_address"),
"address": _parse_google_address_components(first_result.get("address_components", []))
}
return extractIsoCode(user, result_data)
except Exception:
return {"error": "An internal error occurred while processing the request"}
def _parse_google_address_components(components):
parsed = {}
country_code = None
state_code = None
for comp in components:
types = comp.get("types", [])
long_name = comp.get("long_name")
short_name = comp.get("short_name")
if "country" in types:
parsed["country"] = long_name
country_code = short_name
parsed["ISO3166-1"] = short_name
if "administrative_area_level_1" in types:
parsed["state"] = long_name
state_code = short_name
if "administrative_area_level_2" in types:
parsed["county"] = long_name
if "locality" in types:
parsed["city"] = long_name
if "sublocality" in types:
parsed["town"] = long_name
# Build composite ISO 3166-2 code like US-ME
if country_code and state_code:
parsed["ISO3166-2-lvl1"] = f"{country_code}-{state_code}"
return parsed

View file

@ -0,0 +1,17 @@
from django.db import models
from django.db.models import Q
class AdventureManager(models.Manager):
def retrieve_adventures(self, user, include_owned=False, include_shared=False, include_public=False):
query = Q()
if include_owned:
query |= Q(user_id=user)
if include_shared:
query |= Q(collections__shared_with=user)
if include_public:
query |= Q(is_public=True)
return self.filter(query).distinct()

View file

@ -1,13 +1,40 @@
class AppVersionMiddleware:
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
import os
class OverrideHostMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Process request (if needed)
public_url = os.getenv('PUBLIC_URL', None)
if public_url:
# Extract host and scheme
scheme, host = public_url.split("://")
request.META['HTTP_HOST'] = host
request.META['wsgi.url_scheme'] = scheme
# Set X-Forwarded-Proto for Django
request.META['HTTP_X_FORWARDED_PROTO'] = scheme
response = self.get_response(request)
# Add custom header to response
# Replace with your app version
response['X-AdventureLog-Version'] = '1.0.0'
return response
class XSessionTokenMiddleware(MiddlewareMixin):
def process_request(self, request):
session_token = request.headers.get('X-Session-Token')
if session_token:
request.COOKIES[settings.SESSION_COOKIE_NAME] = session_token
class DisableCSRFForSessionTokenMiddleware(MiddlewareMixin):
def process_request(self, request):
if 'X-Session-Token' in request.headers:
setattr(request, '_dont_enforce_csrf_checks', True)
class DisableCSRFForMobileLoginSignup(MiddlewareMixin):
def process_request(self, request):
is_mobile = request.headers.get('X-Is-Mobile', '').lower() == 'true'
is_login_or_signup = request.path in ['/auth/browser/v1/auth/login', '/auth/browser/v1/auth/signup']
if is_mobile and is_login_or_signup:
setattr(request, '_dont_enforce_csrf_checks', True)

View file

@ -0,0 +1,34 @@
# Generated by Django 5.0.8 on 2024-11-14 04:30
from django.conf import settings
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0010_collection_link'),
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=200)),
('display_name', models.CharField(max_length=200)),
('icon', models.CharField(default='🌍', max_length=200)),
('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Categories',
},
),
migrations.AddField(
model_name='adventure',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='adventures.category'),
),
]

View file

@ -0,0 +1,59 @@
from django.db import migrations
def migrate_categories(apps, schema_editor):
# Use the historical models
Adventure = apps.get_model('adventures', 'Adventure')
Category = apps.get_model('adventures', 'Category')
ADVENTURE_TYPES = {
'general': ('General', '🌍'),
'outdoor': ('Outdoor', '🏞️'),
'lodging': ('Lodging', '🛌'),
'dining': ('Dining', '🍽️'),
'activity': ('Activity', '🏄'),
'attraction': ('Attraction', '🎢'),
'shopping': ('Shopping', '🛍️'),
'nightlife': ('Nightlife', '🌃'),
'event': ('Event', '🎉'),
'transportation': ('Transportation', '🚗'),
'culture': ('Culture', '🎭'),
'water_sports': ('Water Sports', '🚤'),
'hiking': ('Hiking', '🥾'),
'wildlife': ('Wildlife', '🦒'),
'historical_sites': ('Historical Sites', '🏛️'),
'music_concerts': ('Music & Concerts', '🎶'),
'fitness': ('Fitness', '🏋️'),
'art_museums': ('Art & Museums', '🎨'),
'festivals': ('Festivals', '🎪'),
'spiritual_journeys': ('Spiritual Journeys', '🧘‍♀️'),
'volunteer_work': ('Volunteer Work', '🤝'),
'other': ('Other', ''),
}
adventures = Adventure.objects.all()
for adventure in adventures:
# Access the old 'type' field using __dict__ because it's not in the model anymore
old_type = adventure.__dict__.get('type')
if old_type in ADVENTURE_TYPES:
category, created = Category.objects.get_or_create(
name=old_type,
user_id=adventure.user_id,
defaults={
'display_name': ADVENTURE_TYPES[old_type][0],
'icon': ADVENTURE_TYPES[old_type][1],
}
)
adventure.category = category
adventure.save()
else:
print(f"Unknown type: {old_type}")
class Migration(migrations.Migration):
dependencies = [
('adventures', '0011_category_adventure_category'),
]
operations = [
migrations.RunPython(migrate_categories),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 5.0.8 on 2024-11-14 04:51
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0012_migrate_types_to_categories'),
]
operations = [
migrations.RemoveField(
model_name='adventure',
name='type',
),
migrations.AlterField(
model_name='adventure',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='adventures.category'),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 5.0.8 on 2024-11-17 21:43
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('adventures', '0013_remove_adventure_type_alter_adventure_category'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterUniqueTogether(
name='category',
unique_together={('name', 'user_id')},
),
]

View file

@ -0,0 +1,33 @@
# Generated by Django 5.0.8 on 2024-12-19 17:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0014_alter_category_unique_together'),
]
operations = [
migrations.AddField(
model_name='transportation',
name='destination_latitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
migrations.AddField(
model_name='transportation',
name='destination_longitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
migrations.AddField(
model_name='transportation',
name='origin_latitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
migrations.AddField(
model_name='transportation',
name='origin_longitude',
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 5.0.8 on 2025-01-01 21:40
import adventures.models
import django_resized.forms
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('adventures', '0015_transportation_destination_latitude_and_more'),
]
operations = [
migrations.AlterField(
model_name='adventureimage',
name='image',
field=django_resized.forms.ResizedImageField(crop=None, force_format='WEBP', keep_meta=True, quality=75, scale=None, size=[1920, 1080], upload_to=adventures.models.PathAndRename('images/')),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.8 on 2025-01-03 04:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0016_alter_adventureimage_image'),
]
operations = [
migrations.AddField(
model_name='adventureimage',
name='is_primary',
field=models.BooleanField(default=False),
),
]

View file

@ -0,0 +1,26 @@
# Generated by Django 5.0.8 on 2025-01-19 00:39
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0017_adventureimage_is_primary'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Attachment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('file', models.FileField(upload_to='attachments/')),
('adventure', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='adventures.adventure')),
('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 5.0.8 on 2025-01-19 22:17
import adventures.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0018_attachment'),
]
operations = [
migrations.AlterField(
model_name='attachment',
name='file',
field=models.FileField(upload_to=adventures.models.PathAndRename('attachments/')),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 5.0.8 on 2025-01-19 22:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0019_alter_attachment_file'),
]
operations = [
migrations.AddField(
model_name='attachment',
name='name',
field=models.CharField(default='', max_length=200),
preserve_default=False,
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.8 on 2025-01-19 22:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0020_attachment_name'),
]
operations = [
migrations.AlterField(
model_name='attachment',
name='name',
field=models.CharField(blank=True, max_length=200, null=True),
),
]

View file

@ -0,0 +1,39 @@
# Generated by Django 5.0.8 on 2025-02-02 15:36
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0021_alter_attachment_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Hotel',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=200)),
('description', models.TextField(blank=True, null=True)),
('rating', models.FloatField(blank=True, null=True)),
('link', models.URLField(blank=True, max_length=2083, null=True)),
('check_in', models.DateTimeField(blank=True, null=True)),
('check_out', models.DateTimeField(blank=True, null=True)),
('reservation_number', models.CharField(blank=True, max_length=100, null=True)),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True)),
('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('location', models.CharField(blank=True, max_length=200, null=True)),
('is_public', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='adventures.collection')),
('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -0,0 +1,43 @@
# Generated by Django 5.0.8 on 2025-02-08 01:50
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0022_hotel'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Lodging',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=200)),
('type', models.CharField(choices=[('hotel', 'Hotel'), ('hostel', 'Hostel'), ('resort', 'Resort'), ('bnb', 'Bed & Breakfast'), ('campground', 'Campground'), ('cabin', 'Cabin'), ('apartment', 'Apartment'), ('house', 'House'), ('villa', 'Villa'), ('motel', 'Motel'), ('other', 'Other')], default='other', max_length=100)),
('description', models.TextField(blank=True, null=True)),
('rating', models.FloatField(blank=True, null=True)),
('link', models.URLField(blank=True, max_length=2083, null=True)),
('check_in', models.DateTimeField(blank=True, null=True)),
('check_out', models.DateTimeField(blank=True, null=True)),
('reservation_number', models.CharField(blank=True, max_length=100, null=True)),
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=9, null=True)),
('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
('location', models.CharField(blank=True, max_length=200, null=True)),
('is_public', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='adventures.collection')),
('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.DeleteModel(
name='Hotel',
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 5.0.8 on 2025-03-17 01:15
import adventures.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0023_lodging_delete_hotel'),
]
operations = [
migrations.AlterField(
model_name='attachment',
name='file',
field=models.FileField(upload_to=adventures.models.PathAndRename('attachments/'), validators=[adventures.models.validate_file_extension]),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 5.0.8 on 2025-03-17 21:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0024_alter_attachment_file'),
]
operations = [
migrations.AlterField(
model_name='visit',
name='end_date',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='visit',
name='start_date',
field=models.DateTimeField(blank=True, null=True),
),
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,30 @@
# Generated by Django 5.0.11 on 2025-05-22 22:48
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0028_lodging_timezone'),
('worldtravel', '0015_city_insert_id_country_insert_id_region_insert_id'),
]
operations = [
migrations.AddField(
model_name='adventure',
name='city',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='worldtravel.city'),
),
migrations.AddField(
model_name='adventure',
name='country',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='worldtravel.country'),
),
migrations.AddField(
model_name='adventure',
name='region',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='worldtravel.region'),
),
]

View file

@ -0,0 +1,18 @@
from django.db import migrations
def set_end_date_equal_to_start(apps, schema_editor):
Visit = apps.get_model('adventures', 'Visit')
for visit in Visit.objects.filter(end_date__isnull=True):
if visit.start_date:
visit.end_date = visit.start_date
visit.save()
class Migration(migrations.Migration):
dependencies = [
('adventures', '0029_adventure_city_adventure_country_adventure_region'),
]
operations = [
migrations.RunPython(set_end_date_equal_to_start),
]

View file

@ -0,0 +1,31 @@
# Generated by Django 5.2.1 on 2025-06-01 16:57
import adventures.models
import django_resized.forms
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0030_set_end_date_equal_start'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='adventureimage',
name='immich_id',
field=models.CharField(blank=True, max_length=200, null=True),
),
migrations.AlterField(
model_name='adventureimage',
name='image',
field=django_resized.forms.ResizedImageField(blank=True, crop=None, force_format='WEBP', keep_meta=True, null=True, quality=75, scale=None, size=[1920, 1080], upload_to=adventures.models.PathAndRename('images/')),
),
migrations.AddConstraint(
model_name='adventureimage',
constraint=models.CheckConstraint(condition=models.Q(models.Q(('image__isnull', False), ('immich_id__isnull', True)), models.Q(('image__isnull', True), ('immich_id__isnull', False)), _connector='OR'), name='image_xor_immich_id'),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 5.2.1 on 2025-06-01 17:18
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('adventures', '0031_adventureimage_immich_id_alter_adventureimage_image_and_more'),
]
operations = [
migrations.RemoveConstraint(
model_name='adventureimage',
name='image_xor_immich_id',
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 5.2.1 on 2025-06-02 02:31
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0032_remove_adventureimage_image_xor_immich_id'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddConstraint(
model_name='adventureimage',
constraint=models.UniqueConstraint(fields=('immich_id', 'user_id'), name='unique_immich_id_per_user'),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 5.2.1 on 2025-06-02 02:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('adventures', '0033_adventureimage_unique_immich_id_per_user'),
]
operations = [
migrations.RemoveConstraint(
model_name='adventureimage',
name='unique_immich_id_per_user',
),
]

View file

@ -0,0 +1,59 @@
# Generated by Django 5.2.1 on 2025-06-10 03:04
from django.db import migrations, models
def migrate_collection_relationships(apps, schema_editor):
"""
Migrate existing ForeignKey relationships to ManyToMany relationships
"""
Adventure = apps.get_model('adventures', 'Adventure')
# Get all adventures that have a collection assigned
adventures_with_collections = Adventure.objects.filter(collection__isnull=False)
for adventure in adventures_with_collections:
# Add the existing collection to the new many-to-many field
adventure.collections.add(adventure.collection_id)
def reverse_migrate_collection_relationships(apps, schema_editor):
"""
Reverse migration - convert first collection back to ForeignKey
Note: This will only preserve the first collection if an adventure has multiple
"""
Adventure = apps.get_model('adventures', 'Adventure')
for adventure in Adventure.objects.all():
first_collection = adventure.collections.first()
if first_collection:
adventure.collection = first_collection
adventure.save()
class Migration(migrations.Migration):
dependencies = [
('adventures', '0034_remove_adventureimage_unique_immich_id_per_user'),
]
operations = [
# First, add the new ManyToMany field
migrations.AddField(
model_name='adventure',
name='collections',
field=models.ManyToManyField(blank=True, related_name='adventures', to='adventures.collection'),
),
# Migrate existing data from old field to new field
migrations.RunPython(
migrate_collection_relationships,
reverse_migrate_collection_relationships
),
# Finally, remove the old ForeignKey field
migrations.RemoveField(
model_name='adventure',
name='collection',
),
]

View file

@ -1,10 +1,66 @@
from django.core.exceptions import ValidationError
import os
from typing import Iterable
import uuid
from django.db import models
from django.utils.deconstruct import deconstructible
from adventures.managers import AdventureManager
import threading
from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import ArrayField
from django.forms import ValidationError
from django_resized import ResizedImageField
from worldtravel.models import City, Country, Region, VisitedCity, VisitedRegion
from django.core.exceptions import ValidationError
from django.utils import timezone
def background_geocode_and_assign(adventure_id: str):
print(f"[Adventure Geocode Thread] Starting geocode for adventure {adventure_id}")
try:
adventure = Adventure.objects.get(id=adventure_id)
if not (adventure.latitude and adventure.longitude):
return
from adventures.geocoding import reverse_geocode # or wherever you defined it
is_visited = adventure.is_visited_status()
result = reverse_geocode(adventure.latitude, adventure.longitude, adventure.user_id)
if 'region_id' in result:
region = Region.objects.filter(id=result['region_id']).first()
if region:
adventure.region = region
if is_visited:
VisitedRegion.objects.get_or_create(user_id=adventure.user_id, region=region)
if 'city_id' in result:
city = City.objects.filter(id=result['city_id']).first()
if city:
adventure.city = city
if is_visited:
VisitedCity.objects.get_or_create(user_id=adventure.user_id, city=city)
if 'country_id' in result:
country = Country.objects.filter(country_code=result['country_id']).first()
if country:
adventure.country = country
# Save updated location info
# Save updated location info, skip geocode threading
adventure.save(update_fields=["region", "city", "country"], _skip_geocode=True)
# print(f"[Adventure Geocode Thread] Successfully processed {adventure_id}: {adventure.name} - {adventure.latitude}, {adventure.longitude}")
except Exception as e:
# Optional: log or print the error
print(f"[Adventure Geocode Thread] Error processing {adventure_id}: {e}")
def validate_file_extension(value):
import os
from django.core.exceptions import ValidationError
ext = os.path.splitext(value.name)[1] # [0] returns path+filename
valid_extensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.mp4', '.mov', '.avi', '.mkv', '.mp3', '.wav', '.flac', '.ogg', '.m4a', '.wma', '.aac', '.opus', '.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz', '.zst', '.lz4', '.lzma', '.lzo', '.z', '.tar.gz', '.tar.bz2', '.tar.xz', '.tar.zst', '.tar.lz4', '.tar.lzma', '.tar.lzo', '.tar.z', '.gpx', '.md']
if not ext.lower() in valid_extensions:
raise ValidationError('Unsupported file extension.')
ADVENTURE_TYPES = [
('general', 'General 🌍'),
@ -31,6 +87,440 @@ ADVENTURE_TYPES = [
('other', 'Other')
]
TIMEZONES = [
"Africa/Abidjan",
"Africa/Accra",
"Africa/Addis_Ababa",
"Africa/Algiers",
"Africa/Asmera",
"Africa/Bamako",
"Africa/Bangui",
"Africa/Banjul",
"Africa/Bissau",
"Africa/Blantyre",
"Africa/Brazzaville",
"Africa/Bujumbura",
"Africa/Cairo",
"Africa/Casablanca",
"Africa/Ceuta",
"Africa/Conakry",
"Africa/Dakar",
"Africa/Dar_es_Salaam",
"Africa/Djibouti",
"Africa/Douala",
"Africa/El_Aaiun",
"Africa/Freetown",
"Africa/Gaborone",
"Africa/Harare",
"Africa/Johannesburg",
"Africa/Juba",
"Africa/Kampala",
"Africa/Khartoum",
"Africa/Kigali",
"Africa/Kinshasa",
"Africa/Lagos",
"Africa/Libreville",
"Africa/Lome",
"Africa/Luanda",
"Africa/Lubumbashi",
"Africa/Lusaka",
"Africa/Malabo",
"Africa/Maputo",
"Africa/Maseru",
"Africa/Mbabane",
"Africa/Mogadishu",
"Africa/Monrovia",
"Africa/Nairobi",
"Africa/Ndjamena",
"Africa/Niamey",
"Africa/Nouakchott",
"Africa/Ouagadougou",
"Africa/Porto-Novo",
"Africa/Sao_Tome",
"Africa/Tripoli",
"Africa/Tunis",
"Africa/Windhoek",
"America/Adak",
"America/Anchorage",
"America/Anguilla",
"America/Antigua",
"America/Araguaina",
"America/Argentina/La_Rioja",
"America/Argentina/Rio_Gallegos",
"America/Argentina/Salta",
"America/Argentina/San_Juan",
"America/Argentina/San_Luis",
"America/Argentina/Tucuman",
"America/Argentina/Ushuaia",
"America/Aruba",
"America/Asuncion",
"America/Bahia",
"America/Bahia_Banderas",
"America/Barbados",
"America/Belem",
"America/Belize",
"America/Blanc-Sablon",
"America/Boa_Vista",
"America/Bogota",
"America/Boise",
"America/Buenos_Aires",
"America/Cambridge_Bay",
"America/Campo_Grande",
"America/Cancun",
"America/Caracas",
"America/Catamarca",
"America/Cayenne",
"America/Cayman",
"America/Chicago",
"America/Chihuahua",
"America/Ciudad_Juarez",
"America/Coral_Harbour",
"America/Cordoba",
"America/Costa_Rica",
"America/Creston",
"America/Cuiaba",
"America/Curacao",
"America/Danmarkshavn",
"America/Dawson",
"America/Dawson_Creek",
"America/Denver",
"America/Detroit",
"America/Dominica",
"America/Edmonton",
"America/Eirunepe",
"America/El_Salvador",
"America/Fort_Nelson",
"America/Fortaleza",
"America/Glace_Bay",
"America/Godthab",
"America/Goose_Bay",
"America/Grand_Turk",
"America/Grenada",
"America/Guadeloupe",
"America/Guatemala",
"America/Guayaquil",
"America/Guyana",
"America/Halifax",
"America/Havana",
"America/Hermosillo",
"America/Indiana/Knox",
"America/Indiana/Marengo",
"America/Indiana/Petersburg",
"America/Indiana/Tell_City",
"America/Indiana/Vevay",
"America/Indiana/Vincennes",
"America/Indiana/Winamac",
"America/Indianapolis",
"America/Inuvik",
"America/Iqaluit",
"America/Jamaica",
"America/Jujuy",
"America/Juneau",
"America/Kentucky/Monticello",
"America/Kralendijk",
"America/La_Paz",
"America/Lima",
"America/Los_Angeles",
"America/Louisville",
"America/Lower_Princes",
"America/Maceio",
"America/Managua",
"America/Manaus",
"America/Marigot",
"America/Martinique",
"America/Matamoros",
"America/Mazatlan",
"America/Mendoza",
"America/Menominee",
"America/Merida",
"America/Metlakatla",
"America/Mexico_City",
"America/Miquelon",
"America/Moncton",
"America/Monterrey",
"America/Montevideo",
"America/Montserrat",
"America/Nassau",
"America/New_York",
"America/Nome",
"America/Noronha",
"America/North_Dakota/Beulah",
"America/North_Dakota/Center",
"America/North_Dakota/New_Salem",
"America/Ojinaga",
"America/Panama",
"America/Paramaribo",
"America/Phoenix",
"America/Port-au-Prince",
"America/Port_of_Spain",
"America/Porto_Velho",
"America/Puerto_Rico",
"America/Punta_Arenas",
"America/Rankin_Inlet",
"America/Recife",
"America/Regina",
"America/Resolute",
"America/Rio_Branco",
"America/Santarem",
"America/Santiago",
"America/Santo_Domingo",
"America/Sao_Paulo",
"America/Scoresbysund",
"America/Sitka",
"America/St_Barthelemy",
"America/St_Johns",
"America/St_Kitts",
"America/St_Lucia",
"America/St_Thomas",
"America/St_Vincent",
"America/Swift_Current",
"America/Tegucigalpa",
"America/Thule",
"America/Tijuana",
"America/Toronto",
"America/Tortola",
"America/Vancouver",
"America/Whitehorse",
"America/Winnipeg",
"America/Yakutat",
"Antarctica/Casey",
"Antarctica/Davis",
"Antarctica/DumontDUrville",
"Antarctica/Macquarie",
"Antarctica/Mawson",
"Antarctica/McMurdo",
"Antarctica/Palmer",
"Antarctica/Rothera",
"Antarctica/Syowa",
"Antarctica/Troll",
"Antarctica/Vostok",
"Arctic/Longyearbyen",
"Asia/Aden",
"Asia/Almaty",
"Asia/Amman",
"Asia/Anadyr",
"Asia/Aqtau",
"Asia/Aqtobe",
"Asia/Ashgabat",
"Asia/Atyrau",
"Asia/Baghdad",
"Asia/Bahrain",
"Asia/Baku",
"Asia/Bangkok",
"Asia/Barnaul",
"Asia/Beirut",
"Asia/Bishkek",
"Asia/Brunei",
"Asia/Calcutta",
"Asia/Chita",
"Asia/Colombo",
"Asia/Damascus",
"Asia/Dhaka",
"Asia/Dili",
"Asia/Dubai",
"Asia/Dushanbe",
"Asia/Famagusta",
"Asia/Gaza",
"Asia/Hebron",
"Asia/Hong_Kong",
"Asia/Hovd",
"Asia/Irkutsk",
"Asia/Jakarta",
"Asia/Jayapura",
"Asia/Jerusalem",
"Asia/Kabul",
"Asia/Kamchatka",
"Asia/Karachi",
"Asia/Katmandu",
"Asia/Khandyga",
"Asia/Krasnoyarsk",
"Asia/Kuala_Lumpur",
"Asia/Kuching",
"Asia/Kuwait",
"Asia/Macau",
"Asia/Magadan",
"Asia/Makassar",
"Asia/Manila",
"Asia/Muscat",
"Asia/Nicosia",
"Asia/Novokuznetsk",
"Asia/Novosibirsk",
"Asia/Omsk",
"Asia/Oral",
"Asia/Phnom_Penh",
"Asia/Pontianak",
"Asia/Pyongyang",
"Asia/Qatar",
"Asia/Qostanay",
"Asia/Qyzylorda",
"Asia/Rangoon",
"Asia/Riyadh",
"Asia/Saigon",
"Asia/Sakhalin",
"Asia/Samarkand",
"Asia/Seoul",
"Asia/Shanghai",
"Asia/Singapore",
"Asia/Srednekolymsk",
"Asia/Taipei",
"Asia/Tashkent",
"Asia/Tbilisi",
"Asia/Tehran",
"Asia/Thimphu",
"Asia/Tokyo",
"Asia/Tomsk",
"Asia/Ulaanbaatar",
"Asia/Urumqi",
"Asia/Ust-Nera",
"Asia/Vientiane",
"Asia/Vladivostok",
"Asia/Yakutsk",
"Asia/Yekaterinburg",
"Asia/Yerevan",
"Atlantic/Azores",
"Atlantic/Bermuda",
"Atlantic/Canary",
"Atlantic/Cape_Verde",
"Atlantic/Faeroe",
"Atlantic/Madeira",
"Atlantic/Reykjavik",
"Atlantic/South_Georgia",
"Atlantic/St_Helena",
"Atlantic/Stanley",
"Australia/Adelaide",
"Australia/Brisbane",
"Australia/Broken_Hill",
"Australia/Darwin",
"Australia/Eucla",
"Australia/Hobart",
"Australia/Lindeman",
"Australia/Lord_Howe",
"Australia/Melbourne",
"Australia/Perth",
"Australia/Sydney",
"Europe/Amsterdam",
"Europe/Andorra",
"Europe/Astrakhan",
"Europe/Athens",
"Europe/Belgrade",
"Europe/Berlin",
"Europe/Bratislava",
"Europe/Brussels",
"Europe/Bucharest",
"Europe/Budapest",
"Europe/Busingen",
"Europe/Chisinau",
"Europe/Copenhagen",
"Europe/Dublin",
"Europe/Gibraltar",
"Europe/Guernsey",
"Europe/Helsinki",
"Europe/Isle_of_Man",
"Europe/Istanbul",
"Europe/Jersey",
"Europe/Kaliningrad",
"Europe/Kiev",
"Europe/Kirov",
"Europe/Lisbon",
"Europe/Ljubljana",
"Europe/London",
"Europe/Luxembourg",
"Europe/Madrid",
"Europe/Malta",
"Europe/Mariehamn",
"Europe/Minsk",
"Europe/Monaco",
"Europe/Moscow",
"Europe/Oslo",
"Europe/Paris",
"Europe/Podgorica",
"Europe/Prague",
"Europe/Riga",
"Europe/Rome",
"Europe/Samara",
"Europe/San_Marino",
"Europe/Sarajevo",
"Europe/Saratov",
"Europe/Simferopol",
"Europe/Skopje",
"Europe/Sofia",
"Europe/Stockholm",
"Europe/Tallinn",
"Europe/Tirane",
"Europe/Ulyanovsk",
"Europe/Vaduz",
"Europe/Vatican",
"Europe/Vienna",
"Europe/Vilnius",
"Europe/Volgograd",
"Europe/Warsaw",
"Europe/Zagreb",
"Europe/Zurich",
"Indian/Antananarivo",
"Indian/Chagos",
"Indian/Christmas",
"Indian/Cocos",
"Indian/Comoro",
"Indian/Kerguelen",
"Indian/Mahe",
"Indian/Maldives",
"Indian/Mauritius",
"Indian/Mayotte",
"Indian/Reunion",
"Pacific/Apia",
"Pacific/Auckland",
"Pacific/Bougainville",
"Pacific/Chatham",
"Pacific/Easter",
"Pacific/Efate",
"Pacific/Enderbury",
"Pacific/Fakaofo",
"Pacific/Fiji",
"Pacific/Funafuti",
"Pacific/Galapagos",
"Pacific/Gambier",
"Pacific/Guadalcanal",
"Pacific/Guam",
"Pacific/Honolulu",
"Pacific/Kiritimati",
"Pacific/Kosrae",
"Pacific/Kwajalein",
"Pacific/Majuro",
"Pacific/Marquesas",
"Pacific/Midway",
"Pacific/Nauru",
"Pacific/Niue",
"Pacific/Norfolk",
"Pacific/Noumea",
"Pacific/Pago_Pago",
"Pacific/Palau",
"Pacific/Pitcairn",
"Pacific/Ponape",
"Pacific/Port_Moresby",
"Pacific/Rarotonga",
"Pacific/Saipan",
"Pacific/Tahiti",
"Pacific/Tarawa",
"Pacific/Tongatapu",
"Pacific/Truk",
"Pacific/Wake",
"Pacific/Wallis"
]
LODGING_TYPES = [
('hotel', 'Hotel'),
('hostel', 'Hostel'),
('resort', 'Resort'),
('bnb', 'Bed & Breakfast'),
('campground', 'Campground'),
('cabin', 'Cabin'),
('apartment', 'Apartment'),
('house', 'House'),
('villa', 'Villa'),
('motel', 'Motel'),
('other', 'Other')
]
TRANSPORTATION_TYPES = [
('car', 'Car'),
('plane', 'Plane'),
@ -50,8 +540,9 @@ User = get_user_model()
class Visit(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
adventure = models.ForeignKey('Adventure', on_delete=models.CASCADE, related_name='visits')
start_date = models.DateField(null=True, blank=True)
end_date = models.DateField(null=True, blank=True)
start_date = models.DateTimeField(null=True, blank=True)
end_date = models.DateTimeField(null=True, blank=True)
timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True)
notes = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@ -68,7 +559,8 @@ class Adventure(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
type = models.CharField(max_length=100, choices=ADVENTURE_TYPES, default='general')
category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=True, null=True)
name = models.CharField(max_length=200)
location = models.CharField(max_length=200, blank=True, null=True)
activity_types = ArrayField(models.CharField(
@ -77,28 +569,92 @@ class Adventure(models.Model):
rating = models.FloatField(blank=True, null=True)
link = models.URLField(blank=True, null=True, max_length=2083)
is_public = models.BooleanField(default=False)
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True)
city = models.ForeignKey(City, on_delete=models.SET_NULL, blank=True, null=True)
region = models.ForeignKey(Region, on_delete=models.SET_NULL, blank=True, null=True)
country = models.ForeignKey(Country, on_delete=models.SET_NULL, blank=True, null=True)
# Changed from ForeignKey to ManyToManyField
collections = models.ManyToManyField('Collection', blank=True, related_name='adventures')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# DEPRECATED FIELDS - TO BE REMOVED IN FUTURE VERSIONS
# Migrations performed in this version will remove these fields
# image = ResizedImageField(force_format="WEBP", quality=75, null=True, blank=True, upload_to='images/')
# date = models.DateField(blank=True, null=True)
# end_date = models.DateField(blank=True, null=True)
objects = AdventureManager()
def clean(self):
if self.date and self.end_date and self.date > self.end_date:
raise ValidationError('The start date must be before the end date. Start date: ' + str(self.date) + ' End date: ' + str(self.end_date))
if self.end_date and not self.date:
raise ValidationError('Adventures must have an end date. Adventure: ' + self.name)
if self.collection:
if self.collection.is_public and not self.is_public:
raise ValidationError('Adventures associated with a public collection must be public. Collection: ' + self.trip.name + ' Adventure: ' + self.name)
if self.user_id != self.collection.user_id:
raise ValidationError('Adventures must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Adventure owner: ' + self.user_id.username)
def is_visited_status(self):
current_date = timezone.now().date()
for visit in self.visits.all():
start_date = visit.start_date.date() if isinstance(visit.start_date, timezone.datetime) else visit.start_date
end_date = visit.end_date.date() if isinstance(visit.end_date, timezone.datetime) else visit.end_date
if start_date and end_date and (start_date <= current_date):
return True
elif start_date and not end_date and (start_date <= current_date):
return True
return False
def clean(self, skip_shared_validation=False):
"""
Validate model constraints.
skip_shared_validation: Skip validation when called by shared users
"""
# Skip validation if this is a shared user update
if skip_shared_validation:
return
# Check collections after the instance is saved (in save method or separate validation)
if self.pk: # Only check if the instance has been saved
for collection in self.collections.all():
if collection.is_public and not self.is_public:
raise ValidationError(f'Adventures associated with a public collection must be public. Collection: {collection.name} Adventure: {self.name}')
# Only enforce same-user constraint for non-shared collections
if self.user_id != collection.user_id:
# Check if this is a shared collection scenario
# Allow if the adventure owner has access to the collection through sharing
if not collection.shared_with.filter(uuid=self.user_id.uuid).exists():
raise ValidationError(f'Adventures must be associated with collections owned by the same user or shared collections. Collection owner: {collection.user_id.username} Adventure owner: {self.user_id.username}')
if self.category:
if self.user_id != self.category.user_id:
raise ValidationError(f'Adventures must be associated with categories owned by the same user. Category owner: {self.category.user_id.username} Adventure owner: {self.user_id.username}')
def save(self, force_insert=False, force_update=False, using=None, update_fields=None, _skip_geocode=False, _skip_shared_validation=False):
if force_insert and force_update:
raise ValueError("Cannot force both insert and updating in model saving.")
if not self.category:
category, _ = Category.objects.get_or_create(
user_id=self.user_id,
name='general',
defaults={'display_name': 'General', 'icon': '🌍'}
)
self.category = category
result = super().save(force_insert, force_update, using, update_fields)
# Validate collections after saving (since M2M relationships require saved instance)
if self.pk:
try:
self.clean(skip_shared_validation=_skip_shared_validation)
except ValidationError as e:
# If validation fails, you might want to handle this differently
# For now, we'll re-raise the error
raise e
# ⛔ Skip threading if called from geocode background thread
if _skip_geocode:
return result
if self.latitude and self.longitude:
thread = threading.Thread(target=background_geocode_and_assign, args=(str(self.id),))
thread.daemon = True # Allows the thread to exit when the main program ends
thread.start()
return result
def __str__(self):
return self.name
@ -119,13 +675,13 @@ class Collection(models.Model):
shared_with = models.ManyToManyField(User, related_name='shared_with', blank=True)
link = models.URLField(blank=True, null=True, max_length=2083)
# if connected adventures are private and collection is public, raise an error
def clean(self):
if self.is_public and self.pk: # Only check if the instance has a primary key
for adventure in self.adventure_set.all():
# Updated to use the new related_name 'adventures'
for adventure in self.adventures.all():
if not adventure.is_public:
raise ValidationError('Public collections cannot be associated with private adventures. Collection: ' + self.name + ' Adventure: ' + adventure.name)
raise ValidationError(f'Public collections cannot be associated with private adventures. Collection: {self.name} Adventure: {adventure.name}')
def __str__(self):
return self.name
@ -142,8 +698,14 @@ class Transportation(models.Model):
link = models.URLField(blank=True, null=True)
date = models.DateTimeField(blank=True, null=True)
end_date = models.DateTimeField(blank=True, null=True)
start_timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True)
end_timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True)
flight_number = models.CharField(max_length=100, blank=True, null=True)
from_location = models.CharField(max_length=200, blank=True, null=True)
origin_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
origin_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
destination_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
destination_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
to_location = models.CharField(max_length=200, blank=True, null=True)
is_public = models.BooleanField(default=False)
collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True)
@ -230,12 +792,118 @@ class ChecklistItem(models.Model):
def __str__(self):
return self.name
@deconstructible
class PathAndRename:
def __init__(self, path):
self.path = path
def __call__(self, instance, filename):
ext = filename.split('.')[-1]
# Generate a new UUID for the filename
filename = f"{uuid.uuid4()}.{ext}"
return os.path.join(self.path, filename)
class AdventureImage(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user_id)
image = ResizedImageField(
force_format="WEBP",
quality=75,
upload_to=PathAndRename('images/'),
blank=True,
null=True,
)
immich_id = models.CharField(max_length=200, null=True, blank=True)
adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE)
is_primary = models.BooleanField(default=False)
def clean(self):
# One of image or immich_id must be set, but not both
has_image = bool(self.image and str(self.image).strip())
has_immich_id = bool(self.immich_id and str(self.immich_id).strip())
if has_image and has_immich_id:
raise ValidationError("Cannot have both image file and Immich ID. Please provide only one.")
if not has_image and not has_immich_id:
raise ValidationError("Must provide either an image file or an Immich ID.")
def save(self, *args, **kwargs):
# Clean empty strings to None for proper database storage
if not self.image:
self.image = None
if not self.immich_id or not str(self.immich_id).strip():
self.immich_id = None
self.full_clean() # This calls clean() method
super().save(*args, **kwargs)
def __str__(self):
return self.image.url if self.image else f"Immich ID: {self.immich_id or 'No image'}"
class Attachment(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
image = ResizedImageField(force_format="WEBP", quality=75, upload_to='images/')
adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE)
file = models.FileField(upload_to=PathAndRename('attachments/'),validators=[validate_file_extension])
adventure = models.ForeignKey(Adventure, related_name='attachments', on_delete=models.CASCADE)
name = models.CharField(max_length=200, null=True, blank=True)
def __str__(self):
return self.image.url
return self.file.url
class Category(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
name = models.CharField(max_length=200)
display_name = models.CharField(max_length=200)
icon = models.CharField(max_length=200, default='🌍')
class Meta:
verbose_name_plural = 'Categories'
unique_together = ['name', 'user_id']
def clean(self) -> None:
self.name = self.name.lower().strip()
return super().clean()
def __str__(self):
return self.name + ' - ' + self.display_name + ' - ' + self.icon
class Lodging(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
name = models.CharField(max_length=200)
type = models.CharField(max_length=100, choices=LODGING_TYPES, default='other')
description = models.TextField(blank=True, null=True)
rating = models.FloatField(blank=True, null=True)
link = models.URLField(blank=True, null=True, max_length=2083)
check_in = models.DateTimeField(blank=True, null=True)
check_out = models.DateTimeField(blank=True, null=True)
timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True)
reservation_number = models.CharField(max_length=100, blank=True, null=True)
price = models.DecimalField(max_digits=9, decimal_places=2, blank=True, null=True)
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
location = models.CharField(max_length=200, blank=True, null=True)
is_public = models.BooleanField(default=False)
collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def clean(self):
if self.check_in and self.check_out and self.check_in > self.check_out:
raise ValidationError('The start date must be before the end date. Start date: ' + str(self.check_in) + ' End date: ' + str(self.check_out))
if self.collection:
if self.collection.is_public and not self.is_public:
raise ValidationError('Lodging associated with a public collection must be public. Collection: ' + self.collection.name + ' Loging: ' + self.name)
if self.user_id != self.collection.user_id:
raise ValidationError('Lodging must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Lodging owner: ' + self.user_id.username)
def __str__(self):
return self.name

View file

@ -2,96 +2,99 @@ from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
Owners can edit, others have read-only access.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to the owner of the object.
# obj.user_id is FK to User, compare with request.user
return obj.user_id == request.user
class IsPublicReadOnly(permissions.BasePermission):
"""
Custom permission to only allow read-only access to public objects,
and write access to the owner of the object.
Read-only if public or owner, write only for owner.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed if the object is public
if request.method in permissions.SAFE_METHODS:
return obj.is_public or obj.user_id == request.user
# Write permissions are only allowed to the owner of the object
return obj.user_id == request.user
class CollectionShared(permissions.BasePermission):
"""
Custom permission to only allow read-only access to public objects,
and write access to the owner of the object.
Allow full access if user is in shared_with of collection(s) or owner,
read-only if public or shared_with,
write only if owner or shared_with.
"""
def has_object_permission(self, request, view, obj):
user = request.user
if not user or not user.is_authenticated:
# Anonymous: only read public
return request.method in permissions.SAFE_METHODS and obj.is_public
# Read permissions are allowed if the object is shared with the user
if obj.shared_with and obj.shared_with.filter(id=request.user.id).exists():
# Check if user is in shared_with of any collections related to the obj
# If obj is a Collection itself:
if hasattr(obj, 'shared_with'):
if obj.shared_with.filter(id=user.id).exists():
return True
# Write permissions are allowed if the object is shared with the user
if request.method not in permissions.SAFE_METHODS and obj.shared_with.filter(id=request.user.id).exists():
# If obj is an Adventure (has collections M2M)
if hasattr(obj, 'collections'):
# Check if user is in shared_with of any related collection
shared_collections = obj.collections.filter(shared_with=user)
if shared_collections.exists():
return True
# Read permissions are allowed if the object is public
# Read permission if public or owner
if request.method in permissions.SAFE_METHODS:
return obj.is_public or obj.user_id == request.user
return obj.is_public or obj.user_id == user
# Write permission only if owner or shared user via collections
if obj.user_id == user:
return True
if hasattr(obj, 'collections'):
if obj.collections.filter(shared_with=user).exists():
return True
# Default deny
return False
# Write permissions are only allowed to the owner of the object
return obj.user_id == request.user
class IsOwnerOrSharedWithFullAccess(permissions.BasePermission):
"""
Custom permission to allow:
- Full access for shared users
- Full access for owners
- Read-only access for others on safe methods
Full access for owners and users shared via collections,
read-only for others if public.
"""
def has_object_permission(self, request, view, obj):
# Check if the object has a collection
if hasattr(obj, 'collection') and obj.collection:
# Allow all actions for shared users
if request.user in obj.collection.shared_with.all():
return True
user = request.user
if not user or not user.is_authenticated:
return request.method in permissions.SAFE_METHODS and obj.is_public
# Always allow GET, HEAD, or OPTIONS requests (safe methods)
# If safe method (read), allow if:
if request.method in permissions.SAFE_METHODS:
if obj.is_public:
return True
if obj.user_id == user:
return True
# If user in shared_with of any collection related to obj
if hasattr(obj, 'collections') and obj.collections.filter(shared_with=user).exists():
return True
if hasattr(obj, 'collection') and obj.collection and obj.collection.shared_with.filter(id=user.id).exists():
return True
if hasattr(obj, 'shared_with') and obj.shared_with.filter(id=user.id).exists():
return True
return False
# For write methods, allow if owner or shared user
if obj.user_id == user:
return True
if hasattr(obj, 'collections') and obj.collections.filter(shared_with=user).exists():
return True
if hasattr(obj, 'collection') and obj.collection and obj.collection.shared_with.filter(id=user.id).exists():
return True
if hasattr(obj, 'shared_with') and obj.shared_with.filter(id=user.id).exists():
return True
# Allow all actions for the owner
return obj.user_id == request.user
class IsPublicOrOwnerOrSharedWithFullAccess(permissions.BasePermission):
"""
Custom permission to allow:
- Read-only access for public objects
- Full access for shared users
- Full access for owners
"""
def has_object_permission(self, request, view, obj):
# Allow read-only access for public objects
if obj.is_public and request.method in permissions.SAFE_METHODS:
return True
# Check if the object has a collection
if hasattr(obj, 'collection') and obj.collection:
# Allow all actions for shared users
if request.user in obj.collection.shared_with.all():
return True
# Allow all actions for the owner
return obj.user_id == request.user
return False

View file

@ -1,61 +1,236 @@
from django.utils import timezone
import os
from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit
from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment, Lodging
from rest_framework import serializers
from main.utils import CustomModelSerializer
from users.serializers import CustomUserDetailsSerializer
from worldtravel.serializers import CountrySerializer, RegionSerializer, CitySerializer
from geopy.distance import geodesic
from integrations.models import ImmichIntegration
class AdventureImageSerializer(serializers.ModelSerializer):
class AdventureImageSerializer(CustomModelSerializer):
class Meta:
model = AdventureImage
fields = ['id', 'image', 'adventure']
read_only_fields = ['id']
fields = ['id', 'image', 'adventure', 'is_primary', 'user_id', 'immich_id']
read_only_fields = ['id', 'user_id']
def to_representation(self, instance):
# If immich_id is set, check for user integration once
integration = None
if instance.immich_id:
integration = ImmichIntegration.objects.filter(user=instance.user_id).first()
if not integration:
return None # Skip if Immich image but no integration
# Base representation
representation = super().to_representation(instance)
# Prepare public URL once
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/').replace("'", "")
if instance.immich_id:
# Use Immich integration URL
representation['image'] = f"{public_url}/api/integrations/immich/{integration.id}/get/{instance.immich_id}"
elif instance.image:
# Use local image URL
representation['image'] = f"{public_url}/media/{instance.image.name}"
return representation
class AttachmentSerializer(CustomModelSerializer):
extension = serializers.SerializerMethodField()
class Meta:
model = Attachment
fields = ['id', 'file', 'adventure', 'extension', 'name', 'user_id']
read_only_fields = ['id', 'user_id']
def get_extension(self, obj):
return obj.file.name.split('.')[-1]
def to_representation(self, instance):
representation = super().to_representation(instance)
if instance.image:
if instance.file:
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
#print(public_url)
# remove any ' from the url
public_url = public_url.replace("'", "")
representation['image'] = f"{public_url}/media/{instance.image.name}"
representation['file'] = f"{public_url}/media/{instance.file.name}"
return representation
class VisitSerializer(serializers.ModelSerializer):
class CategorySerializer(serializers.ModelSerializer):
num_adventures = serializers.SerializerMethodField()
class Meta:
model = Visit
fields = ['id', 'start_date', 'end_date', 'notes']
read_only_fields = ['id']
model = Category
fields = ['id', 'name', 'display_name', 'icon', 'num_adventures']
read_only_fields = ['id', 'num_adventures']
class AdventureSerializer(serializers.ModelSerializer):
images = AdventureImageSerializer(many=True, read_only=True)
visits = VisitSerializer(many=True, read_only=False)
class Meta:
model = Adventure
fields = ['id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location', 'is_public', 'collection', 'created_at', 'updated_at', 'images', 'link', 'type', 'longitude', 'latitude', 'visits']
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
def to_representation(self, instance):
representation = super().to_representation(instance)
return representation
def validate_name(self, value):
return value.lower()
def create(self, validated_data):
visits_data = validated_data.pop('visits', [])
user = self.context['request'].user
validated_data['name'] = validated_data['name'].lower()
return Category.objects.create(user_id=user, **validated_data)
def update(self, instance, validated_data):
for attr, value in validated_data.items():
setattr(instance, attr, value)
if 'name' in validated_data:
instance.name = validated_data['name'].lower()
instance.save()
return instance
def get_num_adventures(self, obj):
return Adventure.objects.filter(category=obj, user_id=obj.user_id).count()
class VisitSerializer(serializers.ModelSerializer):
class Meta:
model = Visit
fields = ['id', 'start_date', 'end_date', 'timezone', 'notes']
read_only_fields = ['id']
class AdventureSerializer(CustomModelSerializer):
images = serializers.SerializerMethodField()
visits = VisitSerializer(many=True, read_only=False, required=False)
attachments = AttachmentSerializer(many=True, read_only=True)
category = CategorySerializer(read_only=False, required=False)
is_visited = serializers.SerializerMethodField()
user = serializers.SerializerMethodField()
country = CountrySerializer(read_only=True)
region = RegionSerializer(read_only=True)
city = CitySerializer(read_only=True)
collections = serializers.PrimaryKeyRelatedField(
many=True,
queryset=Collection.objects.all(),
required=False
)
class Meta:
model = Adventure
fields = [
'id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location',
'is_public', 'collections', 'created_at', 'updated_at', 'images', 'link', 'longitude',
'latitude', 'visits', 'is_visited', 'category', 'attachments', 'user', 'city', 'country', 'region'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'is_visited', 'user']
def get_images(self, obj):
serializer = AdventureImageSerializer(obj.images.all(), many=True, context=self.context)
# Filter out None values from the serialized data
return [image for image in serializer.data if image is not None]
def validate_collections(self, collections):
"""Validate that collections belong to the same user"""
if not collections:
return collections
user = self.context['request'].user
for collection in collections:
if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists():
raise serializers.ValidationError(
f"Collection '{collection.name}' does not belong to the current user."
)
return collections
def validate_category(self, category_data):
if isinstance(category_data, Category):
return category_data
if category_data:
user = self.context['request'].user
name = category_data.get('name', '').lower()
existing_category = Category.objects.filter(user_id=user, name=name).first()
if existing_category:
return existing_category
category_data['name'] = name
return category_data
def get_or_create_category(self, category_data):
user = self.context['request'].user
if isinstance(category_data, Category):
return category_data
if isinstance(category_data, dict):
name = category_data.get('name', '').lower()
display_name = category_data.get('display_name', name)
icon = category_data.get('icon', '🌍')
else:
name = category_data.name.lower()
display_name = category_data.display_name
icon = category_data.icon
category, created = Category.objects.get_or_create(
user_id=user,
name=name,
defaults={
'display_name': display_name,
'icon': icon
}
)
return category
def get_user(self, obj):
user = obj.user_id
return CustomUserDetailsSerializer(user).data
def get_is_visited(self, obj):
return obj.is_visited_status()
def create(self, validated_data):
visits_data = validated_data.pop('visits', None)
category_data = validated_data.pop('category', None)
collections_data = validated_data.pop('collections', [])
print(category_data)
adventure = Adventure.objects.create(**validated_data)
# Handle visits
for visit_data in visits_data:
Visit.objects.create(adventure=adventure, **visit_data)
# Handle category
if category_data:
category = self.get_or_create_category(category_data)
adventure.category = category
# Handle collections - set after adventure is saved
if collections_data:
adventure.collections.set(collections_data)
adventure.save()
return adventure
def update(self, instance, validated_data):
has_visits = 'visits' in validated_data
visits_data = validated_data.pop('visits', [])
category_data = validated_data.pop('category', None)
# Update Adventure fields
collections_data = validated_data.pop('collections', None)
# Update regular fields
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
# Get current visits
# Handle category - ONLY allow the adventure owner to change categories
user = self.context['request'].user
if category_data and instance.user_id == user:
# Only the owner can set categories
category = self.get_or_create_category(category_data)
instance.category = category
# If not the owner, ignore category changes
# Handle collections - only update if collections were provided
if collections_data is not None:
instance.collections.set(collections_data)
# Handle visits
if has_visits:
current_visits = instance.visits.all()
current_visit_ids = set(current_visits.values_list('id', flat=True))
# Update or create visits
updated_visit_ids = set()
for visit_data in visits_data:
visit_id = visit_data.get('id')
@ -66,28 +241,56 @@ class AdventureSerializer(serializers.ModelSerializer):
visit.save()
updated_visit_ids.add(visit_id)
else:
# If no ID is provided or ID doesn't exist, create new visit
new_visit = Visit.objects.create(adventure=instance, **visit_data)
updated_visit_ids.add(new_visit.id)
# Delete visits that are not in the updated data
visits_to_delete = current_visit_ids - updated_visit_ids
instance.visits.filter(id__in=visits_to_delete).delete()
# call save on the adventure to update the updated_at field and trigger any geocoding
instance.save()
return instance
class TransportationSerializer(serializers.ModelSerializer):
class TransportationSerializer(CustomModelSerializer):
distance = serializers.SerializerMethodField()
class Meta:
model = Transportation
fields = [
'id', 'user_id', 'type', 'name', 'description', 'rating',
'link', 'date', 'flight_number', 'from_location', 'to_location',
'is_public', 'collection', 'created_at', 'updated_at', 'end_date'
'is_public', 'collection', 'created_at', 'updated_at', 'end_date',
'origin_latitude', 'origin_longitude', 'destination_latitude', 'destination_longitude',
'start_timezone', 'end_timezone', 'distance' # ✅ Add distance here
]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'distance']
def get_distance(self, obj):
if (
obj.origin_latitude and obj.origin_longitude and
obj.destination_latitude and obj.destination_longitude
):
try:
origin = (float(obj.origin_latitude), float(obj.origin_longitude))
destination = (float(obj.destination_latitude), float(obj.destination_longitude))
return round(geodesic(origin, destination).km, 2)
except ValueError:
return None
return None
class LodgingSerializer(CustomModelSerializer):
class Meta:
model = Lodging
fields = [
'id', 'user_id', 'name', 'description', 'rating', 'link', 'check_in', 'check_out',
'reservation_number', 'price', 'latitude', 'longitude', 'location', 'is_public',
'collection', 'created_at', 'updated_at', 'type', 'timezone'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
class NoteSerializer(serializers.ModelSerializer):
class NoteSerializer(CustomModelSerializer):
class Meta:
model = Note
@ -97,7 +300,7 @@ class NoteSerializer(serializers.ModelSerializer):
]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
class ChecklistItemSerializer(serializers.ModelSerializer):
class ChecklistItemSerializer(CustomModelSerializer):
class Meta:
model = ChecklistItem
fields = [
@ -105,8 +308,9 @@ class ChecklistItemSerializer(serializers.ModelSerializer):
]
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id', 'checklist']
class ChecklistSerializer(serializers.ModelSerializer):
class ChecklistSerializer(CustomModelSerializer):
items = ChecklistItemSerializer(many=True, source='checklistitem_set')
class Meta:
model = Checklist
fields = [
@ -117,8 +321,16 @@ class ChecklistSerializer(serializers.ModelSerializer):
def create(self, validated_data):
items_data = validated_data.pop('checklistitem_set')
checklist = Checklist.objects.create(**validated_data)
for item_data in items_data:
ChecklistItem.objects.create(checklist=checklist, **item_data)
# Remove user_id from item_data to avoid constraint issues
item_data.pop('user_id', None)
# Set user_id from the parent checklist
ChecklistItem.objects.create(
checklist=checklist,
user_id=checklist.user_id,
**item_data
)
return checklist
def update(self, instance, validated_data):
@ -136,6 +348,9 @@ class ChecklistSerializer(serializers.ModelSerializer):
# Update or create items
updated_item_ids = set()
for item_data in items_data:
# Remove user_id from item_data to avoid constraint issues
item_data.pop('user_id', None)
item_id = item_data.get('id')
if item_id:
if item_id in current_item_ids:
@ -146,10 +361,18 @@ class ChecklistSerializer(serializers.ModelSerializer):
updated_item_ids.add(item_id)
else:
# If ID is provided but doesn't exist, create new item
ChecklistItem.objects.create(checklist=instance, **item_data)
ChecklistItem.objects.create(
checklist=instance,
user_id=instance.user_id,
**item_data
)
else:
# If no ID is provided, create new item
ChecklistItem.objects.create(checklist=instance, **item_data)
ChecklistItem.objects.create(
checklist=instance,
user_id=instance.user_id,
**item_data
)
# Delete items that are not in the updated data
items_to_delete = current_item_ids - updated_item_ids
@ -165,18 +388,18 @@ class ChecklistSerializer(serializers.ModelSerializer):
raise serializers.ValidationError(
'Checklists associated with a public collection must be public.'
)
return data
class CollectionSerializer(serializers.ModelSerializer):
adventures = AdventureSerializer(many=True, read_only=True, source='adventure_set')
class CollectionSerializer(CustomModelSerializer):
adventures = AdventureSerializer(many=True, read_only=True)
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
notes = NoteSerializer(many=True, read_only=True, source='note_set')
checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set')
lodging = LodgingSerializer(many=True, read_only=True, source='lodging_set')
class Meta:
model = Collection
fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link']
fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link', 'lodging']
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
def to_representation(self, instance):

View file

@ -0,0 +1,23 @@
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from adventures.models import Adventure
@receiver(m2m_changed, sender=Adventure.collections.through)
def update_adventure_publicity(sender, instance, action, **kwargs):
"""
Signal handler to update adventure publicity when collections are added/removed
"""
# Only process when collections are added or removed
if action in ('post_add', 'post_remove', 'post_clear'):
collections = instance.collections.all()
if collections.exists():
# If any collection is public, make the adventure public
has_public_collection = collections.filter(is_public=True).exists()
if has_public_collection and not instance.is_public:
instance.is_public = True
instance.save(update_fields=['is_public'])
elif not has_public_collection and instance.is_public:
instance.is_public = False
instance.save(update_fields=['is_public'])

View file

@ -1,6 +1,6 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet
from adventures.views import *
router = DefaultRouter()
router.register(r'adventures', AdventureViewSet, basename='adventures')
@ -12,7 +12,13 @@ router.register(r'transportations', TransportationViewSet, basename='transportat
router.register(r'notes', NoteViewSet, basename='notes')
router.register(r'checklists', ChecklistViewSet, basename='checklists')
router.register(r'images', AdventureImageViewSet, basename='images')
router.register(r'reverse-geocode', ReverseGeocodeViewSet, basename='reverse-geocode')
router.register(r'categories', CategoryViewSet, basename='categories')
router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-calendar')
router.register(r'search', GlobalSearchView, basename='search')
router.register(r'attachments', AttachmentViewSet, basename='attachments')
router.register(r'lodging', LodgingViewSet, basename='lodging')
router.register(r'recommendations', RecommendationsViewSet, basename='recommendations')
urlpatterns = [
# Include the router under the 'api/' prefix

View file

@ -0,0 +1,48 @@
from adventures.models import AdventureImage, Attachment
protected_paths = ['images/', 'attachments/']
def checkFilePermission(fileId, user, mediaType):
if mediaType not in protected_paths:
return True
if mediaType == 'images/':
try:
# Construct the full relative path to match the database field
image_path = f"images/{fileId}"
# Fetch the AdventureImage object
adventure = AdventureImage.objects.get(image=image_path).adventure
if adventure.is_public:
return True
elif adventure.user_id == user:
return True
elif adventure.collections.exists():
# Check if the user is in any collection's shared_with list
for collection in adventure.collections.all():
if collection.shared_with.filter(id=user.id).exists():
return True
return False
else:
return False
except AdventureImage.DoesNotExist:
return False
elif mediaType == 'attachments/':
try:
# Construct the full relative path to match the database field
attachment_path = f"attachments/{fileId}"
# Fetch the Attachment object
attachment = Attachment.objects.get(file=attachment_path)
adventure = attachment.adventure
if adventure.is_public:
return True
elif adventure.user_id == user:
return True
elif adventure.collections.exists():
# Check if the user is in any collection's shared_with list
for collection in adventure.collections.all():
if collection.shared_with.filter(id=user.id).exists():
return True
return False
else:
return False
except Attachment.DoesNotExist:
return False

View file

@ -0,0 +1,6 @@
from rest_framework.pagination import PageNumberPagination
class StandardResultsSetPagination(PageNumberPagination):
page_size = 25
page_size_query_param = 'page_size'
max_page_size = 1000

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
from .activity_types_view import *
from .adventure_image_view import *
from .adventure_view import *
from .category_view import *
from .checklist_view import *
from .collection_view import *
from .generate_description_view import *
from .ics_calendar_view import *
from .note_view import *
from .reverse_geocode_view import *
from .stats_view import *
from .transportation_view import *
from .global_search_view import *
from .attachment_view import *
from .lodging_view import *
from .recommendations_view import *

View file

@ -0,0 +1,32 @@
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from adventures.models import Adventure
class ActivityTypesView(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def types(self, request):
"""
Retrieve a list of distinct activity types for adventures associated with the current user.
Args:
request (HttpRequest): The HTTP request object.
Returns:
Response: A response containing a list of distinct activity types.
"""
types = Adventure.objects.filter(user_id=request.user.id).values_list('activity_types', flat=True).distinct()
allTypes = []
for i in types:
if not i:
continue
for x in i:
if x and x not in allTypes:
allTypes.append(x)
return Response(allTypes)

View file

@ -0,0 +1,215 @@
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.db.models import Q
from django.core.files.base import ContentFile
from adventures.models import Adventure, AdventureImage
from adventures.serializers import AdventureImageSerializer
from integrations.models import ImmichIntegration
import uuid
import requests
class AdventureImageViewSet(viewsets.ModelViewSet):
serializer_class = AdventureImageSerializer
permission_classes = [IsAuthenticated]
@action(detail=True, methods=['post'])
def image_delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
@action(detail=True, methods=['post'])
def toggle_primary(self, request, *args, **kwargs):
# Makes the image the primary image for the adventure, if there is already a primary image linked to the adventure, it is set to false and the new image is set to true. make sure that the permission is set to the owner of the adventure
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
instance = self.get_object()
adventure = instance.adventure
if adventure.user_id != request.user:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
# Check if the image is already the primary image
if instance.is_primary:
return Response({"error": "Image is already the primary image"}, status=status.HTTP_400_BAD_REQUEST)
# Set the current primary image to false
AdventureImage.objects.filter(adventure=adventure, is_primary=True).update(is_primary=False)
# Set the new image to true
instance.is_primary = True
instance.save()
return Response({"success": "Image set as primary image"})
def create(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
adventure_id = request.data.get('adventure')
try:
adventure = Adventure.objects.get(id=adventure_id)
except Adventure.DoesNotExist:
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)
if adventure.user_id != request.user:
# Check if the adventure has any collections
if adventure.collections.exists():
# Check if the user is in the shared_with list of any of the adventure's collections
user_has_access = False
for collection in adventure.collections.all():
if collection.shared_with.filter(id=request.user.id).exists():
user_has_access = True
break
if not user_has_access:
return Response({"error": "User does not have permission to access this adventure"}, status=status.HTTP_403_FORBIDDEN)
else:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
# Handle Immich ID for shared users by downloading the image
if (request.user != adventure.user_id and
'immich_id' in request.data and
request.data.get('immich_id')):
immich_id = request.data.get('immich_id')
# Get the shared user's Immich integration
try:
user_integration = ImmichIntegration.objects.get(user_id=request.user)
except ImmichIntegration.DoesNotExist:
return Response({
"error": "No Immich integration found for your account. Please set up Immich integration first.",
"code": "immich_integration_not_found"
}, status=status.HTTP_400_BAD_REQUEST)
# Download the image from the shared user's Immich server
try:
immich_response = requests.get(
f'{user_integration.server_url}/assets/{immich_id}/thumbnail?size=preview',
headers={'x-api-key': user_integration.api_key},
timeout=10
)
immich_response.raise_for_status()
# Create a temporary file with the downloaded content
content_type = immich_response.headers.get('Content-Type', 'image/jpeg')
if not content_type.startswith('image/'):
return Response({
"error": "Invalid content type returned from Immich server.",
"code": "invalid_content_type"
}, status=status.HTTP_400_BAD_REQUEST)
# Determine file extension from content type
ext_map = {
'image/jpeg': '.jpg',
'image/png': '.png',
'image/webp': '.webp',
'image/gif': '.gif'
}
file_ext = ext_map.get(content_type, '.jpg')
filename = f"immich_{immich_id}{file_ext}"
# Create a Django ContentFile from the downloaded image
image_file = ContentFile(immich_response.content, name=filename)
# Modify request data to use the downloaded image instead of immich_id
request_data = request.data.copy()
request_data.pop('immich_id', None) # Remove immich_id
request_data['image'] = image_file # Add the image file
# Create the serializer with the modified data
serializer = self.get_serializer(data=request_data)
serializer.is_valid(raise_exception=True)
# Save with the downloaded image
adventure = serializer.validated_data['adventure']
serializer.save(user_id=adventure.user_id, image=image_file)
return Response(serializer.data, status=status.HTTP_201_CREATED)
except requests.exceptions.RequestException:
return Response({
"error": f"Failed to fetch image from Immich server",
"code": "immich_fetch_failed"
}, status=status.HTTP_502_BAD_GATEWAY)
except Exception:
return Response({
"error": f"Unexpected error processing Immich image",
"code": "immich_processing_error"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return super().create(request, *args, **kwargs)
def update(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
adventure_id = request.data.get('adventure')
try:
adventure = Adventure.objects.get(id=adventure_id)
except Adventure.DoesNotExist:
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)
if adventure.user_id != request.user:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
return super().update(request, *args, **kwargs)
def perform_destroy(self, instance):
print("perform_destroy")
return super().perform_destroy(instance)
def destroy(self, request, *args, **kwargs):
print("destroy")
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
instance = self.get_object()
adventure = instance.adventure
if adventure.user_id != request.user:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
return super().destroy(request, *args, **kwargs)
def partial_update(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
instance = self.get_object()
adventure = instance.adventure
if adventure.user_id != request.user:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
return super().partial_update(request, *args, **kwargs)
@action(detail=False, methods=['GET'], url_path='(?P<adventure_id>[0-9a-f-]+)')
def adventure_images(self, request, adventure_id=None, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
try:
adventure_uuid = uuid.UUID(adventure_id)
except ValueError:
return Response({"error": "Invalid adventure ID"}, status=status.HTTP_400_BAD_REQUEST)
# Updated queryset to include images from adventures the user owns OR has shared access to
queryset = AdventureImage.objects.filter(
Q(adventure__id=adventure_uuid) & (
Q(adventure__user_id=request.user) | # User owns the adventure
Q(adventure__collections__shared_with=request.user) # User has shared access via collection
)
).distinct()
serializer = self.get_serializer(queryset, many=True, context={'request': request})
return Response(serializer.data)
def get_queryset(self):
# Updated to include images from adventures the user owns OR has shared access to
return AdventureImage.objects.filter(
Q(adventure__user_id=self.request.user) | # User owns the adventure
Q(adventure__collections__shared_with=self.request.user) # User has shared access via collection
).distinct()
def perform_create(self, serializer):
# Always set the image owner to the adventure owner, not the current user
adventure = serializer.validated_data['adventure']
serializer.save(user_id=adventure.user_id)

View file

@ -0,0 +1,341 @@
from django.utils import timezone
from django.db import transaction
from django.core.exceptions import PermissionDenied
from django.db.models import Q, Max
from django.db.models.functions import Lower
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
import requests
from adventures.models import Adventure, Category, Transportation, Lodging
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from adventures.serializers import AdventureSerializer, TransportationSerializer, LodgingSerializer
from adventures.utils import pagination
class AdventureViewSet(viewsets.ModelViewSet):
"""
ViewSet for managing Adventure objects with support for filtering, sorting,
and sharing functionality.
"""
serializer_class = AdventureSerializer
permission_classes = [IsOwnerOrSharedWithFullAccess]
pagination_class = pagination.StandardResultsSetPagination
# ==================== QUERYSET & PERMISSIONS ====================
def get_queryset(self):
"""
Returns queryset based on user authentication and action type.
Public actions allow unauthenticated access to public adventures.
"""
user = self.request.user
public_allowed_actions = {'retrieve', 'additional_info'}
if not user.is_authenticated:
if self.action in public_allowed_actions:
return Adventure.objects.retrieve_adventures(
user, include_public=True
).order_by('-updated_at')
return Adventure.objects.none()
include_public = self.action in public_allowed_actions
return Adventure.objects.retrieve_adventures(
user,
include_public=include_public,
include_owned=True,
include_shared=True
).order_by('-updated_at')
# ==================== SORTING & FILTERING ====================
def apply_sorting(self, queryset):
"""Apply sorting and collection filtering to queryset."""
order_by = self.request.query_params.get('order_by', 'updated_at')
order_direction = self.request.query_params.get('order_direction', 'asc')
include_collections = self.request.query_params.get('include_collections', 'true')
# Validate parameters
valid_order_by = ['name', 'type', 'date', 'rating', 'updated_at']
if order_by not in valid_order_by:
order_by = 'name'
if order_direction not in ['asc', 'desc']:
order_direction = 'asc'
# Apply sorting logic
queryset = self._apply_ordering(queryset, order_by, order_direction)
# Filter adventures without collections if requested
if include_collections == 'false':
queryset = queryset.filter(collections__isnull=True)
return queryset
def _apply_ordering(self, queryset, order_by, order_direction):
"""Apply ordering to queryset based on field type."""
if order_by == 'date':
queryset = queryset.annotate(
latest_visit=Max('visits__start_date')
).filter(latest_visit__isnull=False)
ordering = 'latest_visit'
elif order_by == 'name':
queryset = queryset.annotate(lower_name=Lower('name'))
ordering = 'lower_name'
elif order_by == 'rating':
queryset = queryset.filter(rating__isnull=False)
ordering = 'rating'
elif order_by == 'updated_at':
# Special handling for updated_at (reverse default order)
ordering = '-updated_at' if order_direction == 'asc' else 'updated_at'
return queryset.order_by(ordering)
else:
ordering = order_by
# Apply direction
if order_direction == 'desc':
ordering = f'-{ordering}'
return queryset.order_by(ordering)
# ==================== CRUD OPERATIONS ====================
@transaction.atomic
def perform_create(self, serializer):
"""Create adventure with collection validation and ownership logic."""
collections = serializer.validated_data.get('collections', [])
# Validate permissions for all collections
self._validate_collection_permissions(collections)
# Determine what user to assign as owner
user_to_assign = self.request.user
if collections:
# Use the current user as owner since ManyToMany allows multiple collection owners
user_to_assign = self.request.user
serializer.save(user_id=user_to_assign)
def perform_update(self, serializer):
"""Update adventure."""
# Just save the adventure - the signal will handle publicity updates
serializer.save()
def update(self, request, *args, **kwargs):
"""Handle adventure updates with collection permission validation."""
instance = self.get_object()
partial = kwargs.pop('partial', False)
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
# Validate collection permissions if collections are being updated
if 'collections' in serializer.validated_data:
self._validate_collection_update_permissions(
instance, serializer.validated_data['collections']
)
else:
# Remove collections from validated_data if not provided
serializer.validated_data.pop('collections', None)
self.perform_update(serializer)
return Response(serializer.data)
# ==================== CUSTOM ACTIONS ====================
@action(detail=False, methods=['get'])
def filtered(self, request):
"""Filter adventures by category types and visit status."""
types = request.query_params.get('types', '').split(',')
# Handle 'all' types
if 'all' in types:
types = Category.objects.filter(
user_id=request.user
).values_list('name', flat=True)
else:
# Validate provided types
if not types or not all(
Category.objects.filter(user_id=request.user, name=type_name).exists()
for type_name in types
):
return Response(
{"error": "Invalid category or no types provided"},
status=400
)
# Build base queryset
queryset = Adventure.objects.filter(
category__in=Category.objects.filter(name__in=types, user_id=request.user),
user_id=request.user.id
)
# Apply visit status filtering
queryset = self._apply_visit_filtering(queryset, request)
queryset = self.apply_sorting(queryset)
return self.paginate_and_respond(queryset, request)
@action(detail=False, methods=['get'])
def all(self, request):
"""Get all adventures (public and owned) with optional collection filtering."""
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
include_collections = request.query_params.get('include_collections', 'false') == 'true'
# Build queryset with collection filtering
base_filter = Q(user_id=request.user.id)
if include_collections:
queryset = Adventure.objects.filter(base_filter)
else:
queryset = Adventure.objects.filter(base_filter, collections__isnull=True)
queryset = self.apply_sorting(queryset)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get'], url_path='additional-info')
def additional_info(self, request, pk=None):
"""Get adventure with additional sunrise/sunset information."""
adventure = self.get_object()
user = request.user
# Validate access permissions
if not self._has_adventure_access(adventure, user):
return Response(
{"error": "User does not have permission to access this adventure"},
status=status.HTTP_403_FORBIDDEN
)
# Get base adventure data
serializer = self.get_serializer(adventure)
response_data = serializer.data
# Add sunrise/sunset data
response_data['sun_times'] = self._get_sun_times(adventure, response_data.get('visits', []))
return Response(response_data)
# ==================== HELPER METHODS ====================
def _validate_collection_permissions(self, collections):
"""Validate user has permission to use all provided collections. Only the owner or shared users can use collections."""
for collection in collections:
if not (collection.user_id == self.request.user or
collection.shared_with.filter(uuid=self.request.user.uuid).exists()):
raise PermissionDenied(
f"You do not have permission to use collection '{collection.name}'."
)
def _validate_collection_update_permissions(self, instance, new_collections):
"""Validate permissions for collection updates (add/remove)."""
# Check permissions for new collections being added
for collection in new_collections:
if (collection.user_id != self.request.user and
not collection.shared_with.filter(uuid=self.request.user.uuid).exists()):
raise PermissionDenied(
f"You do not have permission to use collection '{collection.name}'."
)
# Check permissions for collections being removed
current_collections = set(instance.collections.all())
new_collections_set = set(new_collections)
collections_to_remove = current_collections - new_collections_set
for collection in collections_to_remove:
if (collection.user_id != self.request.user and
not collection.shared_with.filter(uuid=self.request.user.uuid).exists()):
raise PermissionDenied(
f"You cannot remove the adventure from collection '{collection.name}' "
f"as you don't have permission."
)
def _apply_visit_filtering(self, queryset, request):
"""Apply visit status filtering to queryset."""
is_visited_param = request.query_params.get('is_visited')
if is_visited_param is None:
return queryset
# Convert parameter to boolean
if is_visited_param.lower() == 'true':
is_visited_bool = True
elif is_visited_param.lower() == 'false':
is_visited_bool = False
else:
return queryset
# Apply visit filtering
now = timezone.now().date()
if is_visited_bool:
queryset = queryset.filter(visits__start_date__lte=now).distinct()
else:
queryset = queryset.exclude(visits__start_date__lte=now).distinct()
return queryset
def _has_adventure_access(self, adventure, user):
"""Check if user has access to adventure."""
# Allow if public
if adventure.is_public:
return True
# Check ownership
if user.is_authenticated and adventure.user_id == user:
return True
# Check shared collection access
if user.is_authenticated:
for collection in adventure.collections.all():
if collection.shared_with.filter(uuid=user.uuid).exists():
return True
return False
def _get_sun_times(self, adventure, visits):
"""Get sunrise/sunset times for adventure visits."""
sun_times = []
for visit in visits:
date = visit.get('start_date')
if not (date and adventure.longitude and adventure.latitude):
continue
api_url = (
f'https://api.sunrisesunset.io/json?'
f'lat={adventure.latitude}&lng={adventure.longitude}&date={date}'
)
try:
response = requests.get(api_url)
if response.status_code == 200:
data = response.json()
results = data.get('results', {})
if results.get('sunrise') and results.get('sunset'):
sun_times.append({
"date": date,
"visit_id": visit.get('id'),
"sunrise": results.get('sunrise'),
"sunset": results.get('sunset')
})
except requests.RequestException:
# Skip this visit if API call fails
continue
return sun_times
def paginate_and_respond(self, queryset, request):
"""Paginate queryset and return response."""
paginator = self.pagination_class()
page = paginator.paginate_queryset(queryset, request)
if page is not None:
serializer = self.get_serializer(page, many=True)
return paginator.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

View file

@ -0,0 +1,56 @@
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from adventures.models import Adventure, Attachment
from adventures.serializers import AttachmentSerializer
class AttachmentViewSet(viewsets.ModelViewSet):
serializer_class = AttachmentSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Attachment.objects.filter(user_id=self.request.user)
@action(detail=True, methods=['post'])
def attachment_delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
def create(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
adventure_id = request.data.get('adventure')
try:
adventure = Adventure.objects.get(id=adventure_id)
except Adventure.DoesNotExist:
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)
if adventure.user_id != request.user:
# Check if the adventure has any collections
if adventure.collections.exists():
# Check if the user is in the shared_with list of any of the adventure's collections
user_has_access = False
for collection in adventure.collections.all():
if collection.shared_with.filter(id=request.user.id).exists():
user_has_access = True
break
if not user_has_access:
return Response({"error": "User does not have permission to access this adventure"}, status=status.HTTP_403_FORBIDDEN)
else:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
return super().create(request, *args, **kwargs)
def perform_create(self, serializer):
adventure_id = self.request.data.get('adventure')
adventure = Adventure.objects.get(id=adventure_id)
# If the adventure belongs to collections, set the owner to the collection owner
if adventure.collections.exists():
# Get the first collection's owner (assuming all collections have the same owner)
collection = adventure.collections.first()
serializer.save(user_id=collection.user_id)
else:
# Otherwise, set the owner to the request user
serializer.save(user_id=self.request.user)

View file

@ -0,0 +1,42 @@
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from adventures.models import Category, Adventure
from adventures.serializers import CategorySerializer
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Category.objects.filter(user_id=self.request.user)
@action(detail=False, methods=['get'])
def categories(self, request):
"""
Retrieve a list of distinct categories for adventures associated with the current user.
"""
categories = self.get_queryset().distinct()
serializer = self.get_serializer(categories, many=True)
return Response(serializer.data)
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
if instance.user_id != request.user:
return Response({"error": "User does not own this category"}, status
=400)
if instance.name == 'general':
return Response({"error": "Cannot delete the general category"}, status=400)
# set any adventures with this category to a default category called general before deleting the category, if general does not exist create it for the user
general_category = Category.objects.filter(user_id=request.user, name='general').first()
if not general_category:
general_category = Category.objects.create(user_id=request.user, name='general', icon='🌍', display_name='General')
Adventure.objects.filter(category=instance).update(category=general_category)
return super().destroy(request, *args, **kwargs)

View file

@ -0,0 +1,130 @@
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Q
from adventures.models import Checklist
from adventures.serializers import ChecklistSerializer
from rest_framework.exceptions import PermissionDenied
from adventures.permissions import IsOwnerOrSharedWithFullAccess
class ChecklistViewSet(viewsets.ModelViewSet):
queryset = Checklist.objects.all()
serializer_class = ChecklistSerializer
permission_classes = [IsOwnerOrSharedWithFullAccess]
filterset_fields = ['is_public', 'collection']
# return error message if user is not authenticated on the root endpoint
def list(self, request, *args, **kwargs):
# Prevent listing all adventures
return Response({"detail": "Listing all checklists is not allowed."},
status=status.HTTP_403_FORBIDDEN)
@action(detail=False, methods=['get'])
def all(self, request):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
queryset = Checklist.objects.filter(
Q(user_id=request.user.id)
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def get_queryset(self):
# if the user is not authenticated return only public transportations for retrieve action
if not self.request.user.is_authenticated:
if self.action == 'retrieve':
return Checklist.objects.filter(is_public=True).distinct().order_by('-updated_at')
return Checklist.objects.none()
if self.action == 'retrieve':
# For individual adventure retrieval, include public adventures
return Checklist.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user)
).distinct().order_by('-updated_at')
else:
# For other actions, include user's own adventures and shared adventures
return Checklist.objects.filter(
Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user)
).distinct().order_by('-updated_at')
def partial_update(self, request, *args, **kwargs):
# Retrieve the current object
instance = self.get_object()
# Partially update the instance with the request data
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
# Retrieve the collection from the validated data
new_collection = serializer.validated_data.get('collection')
user = request.user
print(new_collection)
if new_collection is not None and new_collection!=instance.collection:
# Check if the user is the owner of the new collection
if new_collection.user_id != user or instance.user_id != user:
raise PermissionDenied("You do not have permission to use this collection.")
elif new_collection is None:
# Handle the case where the user is trying to set the collection to None
if instance.collection is not None and instance.collection.user_id != user:
raise PermissionDenied("You cannot remove the collection as you are not the owner.")
# Perform the update
self.perform_update(serializer)
# Return the updated instance
return Response(serializer.data)
def partial_update(self, request, *args, **kwargs):
# Retrieve the current object
instance = self.get_object()
# Partially update the instance with the request data
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
# Retrieve the collection from the validated data
new_collection = serializer.validated_data.get('collection')
user = request.user
print(new_collection)
if new_collection is not None and new_collection!=instance.collection:
# Check if the user is the owner of the new collection
if new_collection.user_id != user or instance.user_id != user:
raise PermissionDenied("You do not have permission to use this collection.")
elif new_collection is None:
# Handle the case where the user is trying to set the collection to None
if instance.collection is not None and instance.collection.user_id != user:
raise PermissionDenied("You cannot remove the collection as you are not the owner.")
# Perform the update
self.perform_update(serializer)
# Return the updated instance
return Response(serializer.data)
def perform_update(self, serializer):
serializer.save()
# when creating an adventure, make sure the user is the owner of the collection or shared with the collection
def perform_create(self, serializer):
# Retrieve the collection from the validated data
collection = serializer.validated_data.get('collection')
# Check if a collection is provided
if collection:
user = self.request.user
# Check if the user is the owner or is in the shared_with list
if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists():
# Return an error response if the user does not have permission
raise PermissionDenied("You do not have permission to use this collection.")
# if collection the owner of the adventure is the owner of the collection
serializer.save(user_id=collection.user_id)
return
# Save the adventure with the current user as the owner
serializer.save(user_id=self.request.user)

View file

@ -0,0 +1,240 @@
from django.db.models import Q
from django.db.models.functions import Lower
from django.db import transaction
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from adventures.models import Collection, Adventure, Transportation, Note, Checklist
from adventures.permissions import CollectionShared
from adventures.serializers import CollectionSerializer
from users.models import CustomUser as User
from adventures.utils import pagination
class CollectionViewSet(viewsets.ModelViewSet):
serializer_class = CollectionSerializer
permission_classes = [CollectionShared]
pagination_class = pagination.StandardResultsSetPagination
# def get_queryset(self):
# return Collection.objects.filter(Q(user_id=self.request.user.id) & Q(is_archived=False))
def apply_sorting(self, queryset):
order_by = self.request.query_params.get('order_by', 'name')
order_direction = self.request.query_params.get('order_direction', 'asc')
valid_order_by = ['name', 'updated_at', 'start_date']
if order_by not in valid_order_by:
order_by = 'updated_at'
if order_direction not in ['asc', 'desc']:
order_direction = 'asc'
# Apply case-insensitive sorting for the 'name' field
if order_by == 'name':
queryset = queryset.annotate(lower_name=Lower('name'))
ordering = 'lower_name'
if order_direction == 'desc':
ordering = f'-{ordering}'
elif order_by == 'start_date':
ordering = 'start_date'
if order_direction == 'asc':
ordering = 'start_date'
else:
ordering = '-start_date'
else:
order_by == 'updated_at'
ordering = 'updated_at'
if order_direction == 'asc':
ordering = '-updated_at'
#print(f"Ordering by: {ordering}") # For debugging
return queryset.order_by(ordering)
def list(self, request, *args, **kwargs):
# make sure the user is authenticated
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
queryset = Collection.objects.filter(user_id=request.user.id, is_archived=False)
queryset = self.apply_sorting(queryset)
collections = self.paginate_and_respond(queryset, request)
return collections
@action(detail=False, methods=['get'])
def all(self, request):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
queryset = Collection.objects.filter(
Q(user_id=request.user.id)
)
queryset = self.apply_sorting(queryset)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def archived(self, request):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
queryset = Collection.objects.filter(
Q(user_id=request.user.id) & Q(is_archived=True)
)
queryset = self.apply_sorting(queryset)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
# this make the is_public field of the collection cascade to the adventures
@transaction.atomic
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
if 'collection' in serializer.validated_data:
new_collection = serializer.validated_data['collection']
# if the new collection is different from the old one and the user making the request is not the owner of the new collection return an error
if new_collection != instance.collection and new_collection.user_id != request.user:
return Response({"error": "User does not own the new collection"}, status=400)
# Check if the 'is_public' field is present in the update data
if 'is_public' in serializer.validated_data:
new_public_status = serializer.validated_data['is_public']
# if is_public has changed and the user is not the owner of the collection return an error
if new_public_status != instance.is_public and instance.user_id != request.user:
print(f"User {request.user.id} does not own the collection {instance.id} that is owned by {instance.user_id}")
return Response({"error": "User does not own the collection"}, status=400)
# Get all adventures in this collection
adventures_in_collection = Adventure.objects.filter(collections=instance)
if new_public_status:
# If collection becomes public, make all adventures public
adventures_in_collection.update(is_public=True)
else:
# If collection becomes private, check each adventure
# Only set an adventure to private if ALL of its collections are private
# Collect adventures that do NOT belong to any other public collection (excluding the current one)
adventure_ids_to_set_private = []
for adventure in adventures_in_collection:
has_public_collection = adventure.collections.filter(is_public=True).exclude(id=instance.id).exists()
if not has_public_collection:
adventure_ids_to_set_private.append(adventure.id)
# Bulk update those adventures
Adventure.objects.filter(id__in=adventure_ids_to_set_private).update(is_public=False)
# Update transportations, notes, and checklists related to this collection
# These still use direct ForeignKey relationships
Transportation.objects.filter(collection=instance).update(is_public=new_public_status)
Note.objects.filter(collection=instance).update(is_public=new_public_status)
Checklist.objects.filter(collection=instance).update(is_public=new_public_status)
# Log the action (optional)
action = "public" if new_public_status else "private"
print(f"Collection {instance.id} and its related objects were set to {action}")
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data)
# make an action to retreive all adventures that are shared with the user
@action(detail=False, methods=['get'])
def shared(self, request):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
queryset = Collection.objects.filter(
shared_with=request.user
)
queryset = self.apply_sorting(queryset)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
# Adds a new user to the shared_with field of an adventure
@action(detail=True, methods=['post'], url_path='share/(?P<uuid>[^/.]+)')
def share(self, request, pk=None, uuid=None):
collection = self.get_object()
if not uuid:
return Response({"error": "User UUID is required"}, status=400)
try:
user = User.objects.get(uuid=uuid, public_profile=True)
except User.DoesNotExist:
return Response({"error": "User not found"}, status=404)
if user == request.user:
return Response({"error": "Cannot share with yourself"}, status=400)
if collection.shared_with.filter(id=user.id).exists():
return Response({"error": "Adventure is already shared with this user"}, status=400)
collection.shared_with.add(user)
collection.save()
return Response({"success": f"Shared with {user.username}"})
@action(detail=True, methods=['post'], url_path='unshare/(?P<uuid>[^/.]+)')
def unshare(self, request, pk=None, uuid=None):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
collection = self.get_object()
if not uuid:
return Response({"error": "User UUID is required"}, status=400)
try:
user = User.objects.get(uuid=uuid, public_profile=True)
except User.DoesNotExist:
return Response({"error": "User not found"}, status=404)
if user == request.user:
return Response({"error": "Cannot unshare with yourself"}, status=400)
if not collection.shared_with.filter(id=user.id).exists():
return Response({"error": "Collection is not shared with this user"}, status=400)
collection.shared_with.remove(user)
collection.save()
return Response({"success": f"Unshared with {user.username}"})
def get_queryset(self):
if self.action == 'destroy':
return Collection.objects.filter(user_id=self.request.user.id)
if self.action in ['update', 'partial_update']:
return Collection.objects.filter(
Q(user_id=self.request.user.id) | Q(shared_with=self.request.user)
).distinct()
if self.action == 'retrieve':
if not self.request.user.is_authenticated:
return Collection.objects.filter(is_public=True)
return Collection.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id) | Q(shared_with=self.request.user)
).distinct()
# For list action, include collections owned by the user or shared with the user, that are not archived
return Collection.objects.filter(
(Q(user_id=self.request.user.id) | Q(shared_with=self.request.user)) & Q(is_archived=False)
).distinct()
def perform_create(self, serializer):
# This is ok because you cannot share a collection when creating it
serializer.save(user_id=self.request.user)
def paginate_and_respond(self, queryset, request):
paginator = self.pagination_class()
page = paginator.paginate_queryset(queryset, request)
if page is not None:
serializer = self.get_serializer(page, many=True)
return paginator.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

View file

@ -0,0 +1,44 @@
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
import requests
class GenerateDescription(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'],)
def desc(self, request):
name = self.request.query_params.get('name', '')
# un url encode the name
name = name.replace('%20', ' ')
name = self.get_search_term(name)
url = 'https://en.wikipedia.org/w/api.php?origin=*&action=query&prop=extracts&exintro&explaintext&format=json&titles=%s' % name
response = requests.get(url)
data = response.json()
data = response.json()
page_id = next(iter(data["query"]["pages"]))
extract = data["query"]["pages"][page_id]
if extract.get('extract') is None:
return Response({"error": "No description found"}, status=400)
return Response(extract)
@action(detail=False, methods=['get'],)
def img(self, request):
name = self.request.query_params.get('name', '')
# un url encode the name
name = name.replace('%20', ' ')
name = self.get_search_term(name)
url = 'https://en.wikipedia.org/w/api.php?origin=*&action=query&prop=pageimages&format=json&piprop=original&titles=%s' % name
response = requests.get(url)
data = response.json()
page_id = next(iter(data["query"]["pages"]))
extract = data["query"]["pages"][page_id]
if extract.get('original') is None:
return Response({"error": "No image found"}, status=400)
return Response(extract["original"])
def get_search_term(self, term):
response = requests.get(f'https://en.wikipedia.org/w/api.php?action=opensearch&search={term}&limit=10&namespace=0&format=json')
data = response.json()
if data[1] and len(data[1]) > 0:
return data[1][0]

View file

@ -0,0 +1,73 @@
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.db.models import Q
from django.contrib.postgres.search import SearchVector, SearchQuery
from adventures.models import Adventure, Collection
from adventures.serializers import AdventureSerializer, CollectionSerializer
from worldtravel.models import Country, Region, City, VisitedCity, VisitedRegion
from worldtravel.serializers import CountrySerializer, RegionSerializer, CitySerializer, VisitedCitySerializer, VisitedRegionSerializer
from users.models import CustomUser as User
from users.serializers import CustomUserDetailsSerializer as UserSerializer
class GlobalSearchView(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
def list(self, request):
search_term = request.query_params.get('query', '').strip()
if not search_term:
return Response({"error": "Search query is required"}, status=400)
# Initialize empty results
results = {
"adventures": [],
"collections": [],
"users": [],
"countries": [],
"regions": [],
"cities": [],
"visited_regions": [],
"visited_cities": []
}
# Adventures: Full-Text Search
adventures = Adventure.objects.annotate(
search=SearchVector('name', 'description', 'location')
).filter(search=SearchQuery(search_term), user_id=request.user)
results["adventures"] = AdventureSerializer(adventures, many=True).data
# Collections: Partial Match Search
collections = Collection.objects.filter(
Q(name__icontains=search_term) & Q(user_id=request.user)
)
results["collections"] = CollectionSerializer(collections, many=True).data
# Users: Public Profiles Only
users = User.objects.filter(
(Q(username__icontains=search_term) |
Q(first_name__icontains=search_term) |
Q(last_name__icontains=search_term)) & Q(public_profile=True)
)
results["users"] = UserSerializer(users, many=True).data
# Countries: Full-Text Search
countries = Country.objects.annotate(
search=SearchVector('name', 'country_code')
).filter(search=SearchQuery(search_term))
results["countries"] = CountrySerializer(countries, many=True).data
# Regions and Cities: Partial Match Search
regions = Region.objects.filter(Q(name__icontains=search_term))
results["regions"] = RegionSerializer(regions, many=True).data
cities = City.objects.filter(Q(name__icontains=search_term))
results["cities"] = CitySerializer(cities, many=True).data
# Visited Regions and Cities
visited_regions = VisitedRegion.objects.filter(user_id=request.user)
results["visited_regions"] = VisitedRegionSerializer(visited_regions, many=True).data
visited_cities = VisitedCity.objects.filter(user_id=request.user)
results["visited_cities"] = VisitedCitySerializer(visited_cities, many=True).data
return Response(results)

View file

@ -0,0 +1,67 @@
from django.http import HttpResponse
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from icalendar import Calendar, Event, vText, vCalAddress
from datetime import datetime, timedelta
from adventures.models import Adventure
from adventures.serializers import AdventureSerializer
class IcsCalendarGeneratorViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def generate(self, request):
adventures = Adventure.objects.filter(user_id=request.user)
serializer = AdventureSerializer(adventures, many=True)
user = request.user
name = f"{user.first_name} {user.last_name}"
print(serializer.data)
cal = Calendar()
cal.add('prodid', '-//My Adventure Calendar//example.com//')
cal.add('version', '2.0')
for adventure in serializer.data:
if adventure['visits']:
for visit in adventure['visits']:
# Skip if start_date is missing
if not visit.get('start_date'):
continue
# Parse start_date and handle end_date
try:
start_date = datetime.strptime(visit['start_date'], '%Y-%m-%d').date()
except ValueError:
continue # Skip if the start_date is invalid
end_date = (
datetime.strptime(visit['end_date'], '%Y-%m-%d').date() + timedelta(days=1)
if visit.get('end_date') else start_date + timedelta(days=1)
)
# Create event
event = Event()
event.add('summary', adventure['name'])
event.add('dtstart', start_date)
event.add('dtend', end_date)
event.add('dtstamp', datetime.now())
event.add('transp', 'TRANSPARENT')
event.add('class', 'PUBLIC')
event.add('created', datetime.now())
event.add('last-modified', datetime.now())
event.add('description', adventure['description'])
if adventure.get('location'):
event.add('location', adventure['location'])
if adventure.get('link'):
event.add('url', adventure['link'])
organizer = vCalAddress(f'MAILTO:{user.email}')
organizer.params['cn'] = vText(name)
event.add('organizer', organizer)
cal.add_component(event)
response = HttpResponse(cal.to_ical(), content_type='text/calendar')
response['Content-Disposition'] = 'attachment; filename=adventures.ics'
return response

View file

@ -0,0 +1,84 @@
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Q
from adventures.models import Lodging
from adventures.serializers import LodgingSerializer
from rest_framework.exceptions import PermissionDenied
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.permissions import IsAuthenticated
class LodgingViewSet(viewsets.ModelViewSet):
queryset = Lodging.objects.all()
serializer_class = LodgingSerializer
permission_classes = [IsOwnerOrSharedWithFullAccess]
def list(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response(status=status.HTTP_403_FORBIDDEN)
queryset = Lodging.objects.filter(
Q(user_id=request.user.id)
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def get_queryset(self):
user = self.request.user
if self.action == 'retrieve':
# For individual adventure retrieval, include public adventures, user's own adventures and shared adventures
return Lodging.objects.filter(
Q(is_public=True) | Q(user_id=user.id) | Q(collection__shared_with=user.id)
).distinct().order_by('-updated_at')
# For other actions, include user's own adventures and shared adventures
return Lodging.objects.filter(
Q(user_id=user.id) | Q(collection__shared_with=user.id)
).distinct().order_by('-updated_at')
def partial_update(self, request, *args, **kwargs):
# Retrieve the current object
instance = self.get_object()
user = request.user
# Partially update the instance with the request data
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
# Retrieve the collection from the validated data
new_collection = serializer.validated_data.get('collection')
if new_collection is not None and new_collection != instance.collection:
# Check if the user is the owner of the new collection
if new_collection.user_id != user or instance.user_id != user:
raise PermissionDenied("You do not have permission to use this collection.")
elif new_collection is None:
# Handle the case where the user is trying to set the collection to None
if instance.collection is not None and instance.collection.user_id != user:
raise PermissionDenied("You cannot remove the collection as you are not the owner.")
# Perform the update
self.perform_update(serializer)
# Return the updated instance
return Response(serializer.data)
def perform_update(self, serializer):
serializer.save()
# when creating an adventure, make sure the user is the owner of the collection or shared with the collection
def perform_create(self, serializer):
# Retrieve the collection from the validated data
collection = serializer.validated_data.get('collection')
# Check if a collection is provided
if collection:
user = self.request.user
# Check if the user is the owner or is in the shared_with list
if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists():
# Return an error response if the user does not have permission
raise PermissionDenied("You do not have permission to use this collection.")
# if collection the owner of the adventure is the owner of the collection
serializer.save(user_id=collection.user_id)
return
# Save the adventure with the current user as the owner
serializer.save(user_id=self.request.user)

View file

@ -0,0 +1,130 @@
from rest_framework import viewsets, status
from rest_framework.response import Response
from django.db.models import Q
from adventures.models import Note
from adventures.serializers import NoteSerializer
from rest_framework.exceptions import PermissionDenied
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.decorators import action
class NoteViewSet(viewsets.ModelViewSet):
queryset = Note.objects.all()
serializer_class = NoteSerializer
permission_classes = [IsOwnerOrSharedWithFullAccess]
filterset_fields = ['is_public', 'collection']
# return error message if user is not authenticated on the root endpoint
def list(self, request, *args, **kwargs):
# Prevent listing all adventures
return Response({"detail": "Listing all notes is not allowed."},
status=status.HTTP_403_FORBIDDEN)
@action(detail=False, methods=['get'])
def all(self, request):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
queryset = Note.objects.filter(
Q(user_id=request.user.id)
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def get_queryset(self):
# if the user is not authenticated return only public transportations for retrieve action
if not self.request.user.is_authenticated:
if self.action == 'retrieve':
return Note.objects.filter(is_public=True).distinct().order_by('-updated_at')
return Note.objects.none()
if self.action == 'retrieve':
# For individual adventure retrieval, include public adventures
return Note.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user)
).distinct().order_by('-updated_at')
else:
# For other actions, include user's own adventures and shared adventures
return Note.objects.filter(
Q(user_id=self.request.user.id) | Q(collection__shared_with=self.request.user)
).distinct().order_by('-updated_at')
def partial_update(self, request, *args, **kwargs):
# Retrieve the current object
instance = self.get_object()
# Partially update the instance with the request data
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
# Retrieve the collection from the validated data
new_collection = serializer.validated_data.get('collection')
user = request.user
print(new_collection)
if new_collection is not None and new_collection!=instance.collection:
# Check if the user is the owner of the new collection
if new_collection.user_id != user or instance.user_id != user:
raise PermissionDenied("You do not have permission to use this collection.")
elif new_collection is None:
# Handle the case where the user is trying to set the collection to None
if instance.collection is not None and instance.collection.user_id != user:
raise PermissionDenied("You cannot remove the collection as you are not the owner.")
# Perform the update
self.perform_update(serializer)
# Return the updated instance
return Response(serializer.data)
def partial_update(self, request, *args, **kwargs):
# Retrieve the current object
instance = self.get_object()
# Partially update the instance with the request data
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
# Retrieve the collection from the validated data
new_collection = serializer.validated_data.get('collection')
user = request.user
print(new_collection)
if new_collection is not None and new_collection!=instance.collection:
# Check if the user is the owner of the new collection
if new_collection.user_id != user or instance.user_id != user:
raise PermissionDenied("You do not have permission to use this collection.")
elif new_collection is None:
# Handle the case where the user is trying to set the collection to None
if instance.collection is not None and instance.collection.user_id != user:
raise PermissionDenied("You cannot remove the collection as you are not the owner.")
# Perform the update
self.perform_update(serializer)
# Return the updated instance
return Response(serializer.data)
def perform_update(self, serializer):
serializer.save()
# when creating an adventure, make sure the user is the owner of the collection or shared with the collection
def perform_create(self, serializer):
# Retrieve the collection from the validated data
collection = serializer.validated_data.get('collection')
# Check if a collection is provided
if collection:
user = self.request.user
# Check if the user is the owner or is in the shared_with list
if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists():
# Return an error response if the user does not have permission
raise PermissionDenied("You do not have permission to use this collection.")
# if collection the owner of the adventure is the owner of the collection
serializer.save(user_id=collection.user_id)
return
# Save the adventure with the current user as the owner
serializer.save(user_id=self.request.user)

View file

@ -0,0 +1,258 @@
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.conf import settings
import requests
from geopy.distance import geodesic
import time
class RecommendationsViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
BASE_URL = "https://overpass-api.de/api/interpreter"
HEADERS = {'User-Agent': 'AdventureLog Server'}
def parse_google_places(self, places, origin):
adventures = []
for place in places:
location = place.get('location', {})
types = place.get('types', [])
# Updated for new API response structure
formatted_address = place.get("formattedAddress") or place.get("shortFormattedAddress")
display_name = place.get("displayName", {})
name = display_name.get("text") if isinstance(display_name, dict) else display_name
lat = location.get('latitude')
lon = location.get('longitude')
if not name or not lat or not lon:
continue
distance_km = geodesic(origin, (lat, lon)).km
adventure = {
"id": place.get('id'),
"type": 'place',
"name": name,
"description": place.get('businessStatus', None),
"latitude": lat,
"longitude": lon,
"address": formatted_address,
"tag": types[0] if types else None,
"distance_km": round(distance_km, 2),
}
adventures.append(adventure)
# Sort by distance ascending
adventures.sort(key=lambda x: x["distance_km"])
return adventures
def parse_overpass_response(self, data, request):
nodes = data.get('elements', [])
adventures = []
all = request.query_params.get('all', False)
origin = None
try:
origin = (
float(request.query_params.get('lat')),
float(request.query_params.get('lon'))
)
except(ValueError, TypeError):
origin = None
for node in nodes:
if node.get('type') not in ['node', 'way', 'relation']:
continue
tags = node.get('tags', {})
lat = node.get('lat')
lon = node.get('lon')
name = tags.get('name', tags.get('official_name', ''))
if not name or lat is None or lon is None:
if not all:
continue
# Flatten address
address_parts = [tags.get(f'addr:{k}') for k in ['housenumber', 'street', 'suburb', 'city', 'state', 'postcode', 'country']]
formatted_address = ", ".join(filter(None, address_parts)) or name
# Calculate distance if possible
distance_km = None
if origin:
distance_km = round(geodesic(origin, (lat, lon)).km, 2)
# Unified format
adventure = {
"id": f"osm:{node.get('id')}",
"type": "place",
"name": name,
"description": tags.get('description'),
"latitude": lat,
"longitude": lon,
"address": formatted_address,
"tag": next((tags.get(key) for key in ['leisure', 'tourism', 'natural', 'historic', 'amenity'] if key in tags), None),
"distance_km": distance_km,
"powered_by": "osm"
}
adventures.append(adventure)
# Sort by distance if available
if origin:
adventures.sort(key=lambda x: x.get("distance_km") or float("inf"))
return adventures
def query_overpass(self, lat, lon, radius, category, request):
if category == 'tourism':
query = f"""
[out:json];
(
node(around:{radius},{lat},{lon})["tourism"];
node(around:{radius},{lat},{lon})["leisure"];
node(around:{radius},{lat},{lon})["historic"];
node(around:{radius},{lat},{lon})["sport"];
node(around:{radius},{lat},{lon})["natural"];
node(around:{radius},{lat},{lon})["attraction"];
node(around:{radius},{lat},{lon})["museum"];
node(around:{radius},{lat},{lon})["zoo"];
node(around:{radius},{lat},{lon})["aquarium"];
);
out;
"""
elif category == 'lodging':
query = f"""
[out:json];
(
node(around:{radius},{lat},{lon})["tourism"="hotel"];
node(around:{radius},{lat},{lon})["tourism"="motel"];
node(around:{radius},{lat},{lon})["tourism"="guest_house"];
node(around:{radius},{lat},{lon})["tourism"="hostel"];
node(around:{radius},{lat},{lon})["tourism"="camp_site"];
node(around:{radius},{lat},{lon})["tourism"="caravan_site"];
node(around:{radius},{lat},{lon})["tourism"="chalet"];
node(around:{radius},{lat},{lon})["tourism"="alpine_hut"];
node(around:{radius},{lat},{lon})["tourism"="apartment"];
);
out;
"""
elif category == 'food':
query = f"""
[out:json];
(
node(around:{radius},{lat},{lon})["amenity"="restaurant"];
node(around:{radius},{lat},{lon})["amenity"="cafe"];
node(around:{radius},{lat},{lon})["amenity"="fast_food"];
node(around:{radius},{lat},{lon})["amenity"="pub"];
node(around:{radius},{lat},{lon})["amenity"="bar"];
node(around:{radius},{lat},{lon})["amenity"="food_court"];
node(around:{radius},{lat},{lon})["amenity"="ice_cream"];
node(around:{radius},{lat},{lon})["amenity"="bakery"];
node(around:{radius},{lat},{lon})["amenity"="confectionery"];
);
out;
"""
else:
return Response({"error": "Invalid category."}, status=400)
overpass_url = f"{self.BASE_URL}?data={query}"
try:
response = requests.get(overpass_url, headers=self.HEADERS)
response.raise_for_status()
data = response.json()
except Exception as e:
print("Overpass API error:", e)
return Response({"error": "Failed to retrieve data from Overpass API."}, status=500)
adventures = self.parse_overpass_response(data, request)
return Response(adventures)
def query_google_nearby(self, lat, lon, radius, category, request):
"""Query Google Places API (New) for nearby places"""
api_key = settings.GOOGLE_MAPS_API_KEY
# Updated to use new Places API endpoint
url = "https://places.googleapis.com/v1/places:searchNearby"
headers = {
'Content-Type': 'application/json',
'X-Goog-Api-Key': api_key,
'X-Goog-FieldMask': 'places.displayName.text,places.formattedAddress,places.location,places.types,places.rating,places.userRatingCount,places.businessStatus,places.id'
}
# Map categories to place types for the new API
type_mapping = {
'lodging': 'lodging',
'food': 'restaurant',
'tourism': 'tourist_attraction',
}
payload = {
"includedTypes": [type_mapping[category]],
"maxResultCount": 20,
"locationRestriction": {
"circle": {
"center": {
"latitude": float(lat),
"longitude": float(lon)
},
"radius": float(radius)
}
}
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
places = data.get('places', [])
origin = (float(lat), float(lon))
adventures = self.parse_google_places(places, origin)
return Response(adventures)
except requests.exceptions.RequestException as e:
print(f"Google Places API error: {e}")
# Fallback to Overpass API
return self.query_overpass(lat, lon, radius, category, request)
except Exception as e:
print(f"Unexpected error with Google Places API: {e}")
# Fallback to Overpass API
return self.query_overpass(lat, lon, radius, category, request)
@action(detail=False, methods=['get'])
def query(self, request):
lat = request.query_params.get('lat')
lon = request.query_params.get('lon')
radius = request.query_params.get('radius', '1000')
category = request.query_params.get('category', 'all')
if not lat or not lon:
return Response({"error": "Latitude and longitude parameters are required."}, status=400)
valid_categories = {
'lodging': 'lodging',
'food': 'restaurant',
'tourism': 'tourist_attraction',
}
if category not in valid_categories:
return Response({"error": f"Invalid category. Valid categories: {', '.join(valid_categories)}"}, status=400)
api_key = getattr(settings, 'GOOGLE_MAPS_API_KEY', None)
# Fallback to Overpass if no API key configured
if not api_key:
return self.query_overpass(lat, lon, radius, category, request)
# Use the new Google Places API
return self.query_google_nearby(lat, lon, radius, category, request)

View file

@ -0,0 +1,87 @@
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from worldtravel.models import Region, City, VisitedRegion, VisitedCity
from adventures.models import Adventure
from adventures.serializers import AdventureSerializer
import requests
from adventures.geocoding import reverse_geocode
from adventures.geocoding import extractIsoCode
from django.conf import settings
from adventures.geocoding import search_google, search_osm
class ReverseGeocodeViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def reverse_geocode(self, request):
lat = request.query_params.get('lat', '')
lon = request.query_params.get('lon', '')
if not lat or not lon:
return Response({"error": "Latitude and longitude are required"}, status=400)
try:
lat = float(lat)
lon = float(lon)
except ValueError:
return Response({"error": "Invalid latitude or longitude"}, status=400)
data = reverse_geocode(lat, lon, self.request.user)
if 'error' in data:
return Response({"error": "An internal error occurred while processing the request"}, status=400)
return Response(data)
@action(detail=False, methods=['get'])
def search(self, request):
query = request.query_params.get('query', '')
if not query:
return Response({"error": "Query parameter is required"}, status=400)
try:
if getattr(settings, 'GOOGLE_MAPS_API_KEY', None):
results = search_google(query)
else:
results = search_osm(query)
return Response(results)
except Exception:
return Response({"error": "An internal error occurred while processing the request"}, status=500)
@action(detail=False, methods=['post'])
def mark_visited_region(self, request):
# searches through all of the users adventures, if the serialized data is_visited, is true, runs reverse geocode on the adventures and if a region is found, marks it as visited. Use the extractIsoCode function to get the region
new_region_count = 0
new_regions = {}
new_city_count = 0
new_cities = {}
adventures = Adventure.objects.filter(user_id=self.request.user)
serializer = AdventureSerializer(adventures, many=True)
for adventure, serialized_adventure in zip(adventures, serializer.data):
if serialized_adventure['is_visited'] == True:
lat = adventure.latitude
lon = adventure.longitude
if not lat or not lon:
continue
# Use the existing reverse_geocode function which handles both Google and OSM
data = reverse_geocode(lat, lon, self.request.user)
if 'error' in data:
continue
# data already contains region_id and city_id
if 'region_id' in data and data['region_id'] is not None:
region = Region.objects.filter(id=data['region_id']).first()
visited_region = VisitedRegion.objects.filter(region=region, user_id=self.request.user).first()
if not visited_region:
visited_region = VisitedRegion(region=region, user_id=self.request.user)
visited_region.save()
new_region_count += 1
new_regions[region.id] = region.name
if 'city_id' in data and data['city_id'] is not None:
city = City.objects.filter(id=data['city_id']).first()
visited_city = VisitedCity.objects.filter(city=city, user_id=self.request.user).first()
if not visited_city:
visited_city = VisitedCity(city=city, user_id=self.request.user)
visited_city.save()
new_city_count += 1
new_cities[city.id] = city.name
return Response({"new_regions": new_region_count, "regions": new_regions, "new_cities": new_city_count, "cities": new_cities})

View file

@ -0,0 +1,51 @@
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.decorators import action
from django.shortcuts import get_object_or_404
from worldtravel.models import City, Region, Country, VisitedCity, VisitedRegion
from adventures.models import Adventure, Collection
from users.serializers import CustomUserDetailsSerializer as PublicUserSerializer
from django.contrib.auth import get_user_model
User = get_user_model()
class StatsViewSet(viewsets.ViewSet):
"""
A simple ViewSet for listing the stats of a user.
"""
@action(detail=False, methods=['get'], url_path=r'counts/(?P<username>[\w.@+-]+)')
def counts(self, request, username):
if request.user.username == username:
user = get_object_or_404(User, username=username)
else:
user = get_object_or_404(User, username=username, public_profile=True)
# serializer = PublicUserSerializer(user)
# remove the email address from the response
user.email = None
# get the counts for the user
adventure_count = Adventure.objects.filter(
user_id=user.id).count()
trips_count = Collection.objects.filter(
user_id=user.id).count()
visited_city_count = VisitedCity.objects.filter(
user_id=user.id).count()
total_cities = City.objects.count()
visited_region_count = VisitedRegion.objects.filter(
user_id=user.id).count()
total_regions = Region.objects.count()
visited_country_count = VisitedRegion.objects.filter(
user_id=user.id).values('region__country').distinct().count()
total_countries = Country.objects.count()
return Response({
'adventure_count': adventure_count,
'trips_count': trips_count,
'visited_city_count': visited_city_count,
'total_cities': total_cities,
'visited_region_count': visited_region_count,
'total_regions': total_regions,
'visited_country_count': visited_country_count,
'total_countries': total_countries
})

View file

@ -0,0 +1,84 @@
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db.models import Q
from adventures.models import Transportation
from adventures.serializers import TransportationSerializer
from rest_framework.exceptions import PermissionDenied
from adventures.permissions import IsOwnerOrSharedWithFullAccess
from rest_framework.permissions import IsAuthenticated
class TransportationViewSet(viewsets.ModelViewSet):
queryset = Transportation.objects.all()
serializer_class = TransportationSerializer
permission_classes = [IsOwnerOrSharedWithFullAccess]
def list(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response(status=status.HTTP_403_FORBIDDEN)
queryset = Transportation.objects.filter(
Q(user_id=request.user.id)
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def get_queryset(self):
user = self.request.user
if self.action == 'retrieve':
# For individual adventure retrieval, include public adventures, user's own adventures and shared adventures
return Transportation.objects.filter(
Q(is_public=True) | Q(user_id=user.id) | Q(collection__shared_with=user.id)
).distinct().order_by('-updated_at')
# For other actions, include user's own adventures and shared adventures
return Transportation.objects.filter(
Q(user_id=user.id) | Q(collection__shared_with=user.id)
).distinct().order_by('-updated_at')
def partial_update(self, request, *args, **kwargs):
# Retrieve the current object
instance = self.get_object()
user = request.user
# Partially update the instance with the request data
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
# Retrieve the collection from the validated data
new_collection = serializer.validated_data.get('collection')
if new_collection is not None and new_collection != instance.collection:
# Check if the user is the owner of the new collection
if new_collection.user_id != user or instance.user_id != user:
raise PermissionDenied("You do not have permission to use this collection.")
elif new_collection is None:
# Handle the case where the user is trying to set the collection to None
if instance.collection is not None and instance.collection.user_id != user:
raise PermissionDenied("You cannot remove the collection as you are not the owner.")
# Perform the update
self.perform_update(serializer)
# Return the updated instance
return Response(serializer.data)
def perform_update(self, serializer):
serializer.save()
# when creating an adventure, make sure the user is the owner of the collection or shared with the collection
def perform_create(self, serializer):
# Retrieve the collection from the validated data
collection = serializer.validated_data.get('collection')
# Check if a collection is provided
if collection:
user = self.request.user
# Check if the user is the owner or is in the shared_with list
if collection.user_id != user and not collection.shared_with.filter(id=user.id).exists():
# Return an error response if the user does not have permission
raise PermissionDenied("You do not have permission to use this collection.")
# if collection the owner of the adventure is the owner of the collection
serializer.save(user_id=collection.user_id)
return
# Save the adventure with the current user as the owner
serializer.save(user_id=self.request.user)

View file

View file

@ -0,0 +1,9 @@
from django.contrib import admin
from allauth.account.decorators import secure_admin_login
from .models import ImmichIntegration
admin.autodiscover()
admin.site.login = secure_admin_login(admin.site.login)
admin.site.register(ImmichIntegration)

View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class IntegrationsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'integrations'

View file

@ -0,0 +1,27 @@
# Generated by Django 5.0.8 on 2025-01-02 23:16
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ImmichIntegration',
fields=[
('server_url', models.CharField(max_length=255)),
('api_key', models.CharField(max_length=255)),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-06-01 21:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('integrations', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='immichintegration',
name='copy_locally',
field=models.BooleanField(default=True, help_text='Copy image to local storage, instead of just linking to the remote URL.'),
),
]

View file

@ -0,0 +1,16 @@
from django.db import models
from django.contrib.auth import get_user_model
import uuid
User = get_user_model()
class ImmichIntegration(models.Model):
server_url = models.CharField(max_length=255)
api_key = models.CharField(max_length=255)
user = models.ForeignKey(
User, on_delete=models.CASCADE)
copy_locally = models.BooleanField(default=True, help_text="Copy image to local storage, instead of just linking to the remote URL.")
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
def __str__(self):
return self.user.username + ' - ' + self.server_url

View file

@ -0,0 +1,13 @@
from .models import ImmichIntegration
from rest_framework import serializers
class ImmichIntegrationSerializer(serializers.ModelSerializer):
class Meta:
model = ImmichIntegration
fields = '__all__'
read_only_fields = ['id', 'user']
def to_representation(self, instance):
representation = super().to_representation(instance)
representation.pop('user', None)
return representation

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