2025-03-15 12:29:12 -04:00
from django . core . exceptions import ValidationError
2025-01-01 19:27:33 -05:00
import os
2024-11-22 17:03:02 -05:00
from typing import Iterable
2024-08-10 22:40:57 -04:00
import uuid
2024-07-08 11:44:39 -04:00
from django . db import models
2025-01-01 19:27:33 -05:00
from django . utils . deconstruct import deconstructible
2025-01-18 12:28:14 -05:00
from adventures . managers import AdventureManager
2025-05-29 18:17:15 -04:00
import threading
2024-07-08 11:44:39 -04:00
from django . contrib . auth import get_user_model
from django . contrib . postgres . fields import ArrayField
2024-07-09 13:01:56 -04:00
from django . forms import ValidationError
2024-07-18 11:07:24 -04:00
from django_resized import ResizedImageField
2025-05-22 21:13:31 -04:00
from worldtravel . models import City , Country , Region , VisitedCity , VisitedRegion
2025-05-22 20:05:13 -04:00
from adventures . geocoding import reverse_geocode
2025-05-22 21:13:31 -04:00
from django . utils import timezone
2024-07-09 13:01:56 -04:00
2025-05-29 18:17:15 -04:00
def background_geocode_and_assign ( adventure_id : str ) :
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
adventure . save ( update_fields = [ " region " , " city " , " country " ] )
except Exception as e :
# Optional: log or print the error
print ( f " [Adventure Geocode Thread] Error processing { adventure_id } : { e } " )
2025-03-15 12:29:12 -04:00
def validate_file_extension ( value ) :
import os
from django . core . exceptions import ValidationError
ext = os . path . splitext ( value . name ) [ 1 ] # [0] returns path+filename
2025-03-23 16:40:55 -04:00
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 ' ]
2025-03-15 12:29:12 -04:00
if not ext . lower ( ) in valid_extensions :
raise ValidationError ( ' Unsupported file extension. ' )
2025-01-18 12:28:14 -05:00
2024-07-09 13:01:56 -04:00
ADVENTURE_TYPES = [
2024-09-22 10:29:23 -04:00
( ' general ' , ' General 🌍 ' ) ,
2024-09-24 17:05:16 -04:00
( ' outdoor ' , ' Outdoor 🏞️ ' ) ,
2024-09-22 10:29:23 -04:00
( ' 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 ' )
2024-07-09 13:01:56 -04:00
]
2024-07-08 11:44:39 -04:00
2025-05-10 11:59:56 -04:00
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 "
]
2025-02-08 16:10:01 -05:00
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 ' )
]
2024-07-27 19:04:55 -04:00
TRANSPORTATION_TYPES = [
( ' car ' , ' Car ' ) ,
( ' plane ' , ' Plane ' ) ,
( ' train ' , ' Train ' ) ,
( ' bus ' , ' Bus ' ) ,
( ' boat ' , ' Boat ' ) ,
( ' bike ' , ' Bike ' ) ,
( ' walking ' , ' Walking ' ) ,
( ' other ' , ' Other ' )
]
2024-07-08 11:44:39 -04:00
# Assuming you have a default user ID you want to use
default_user_id = 1 # Replace with an actual user ID
User = get_user_model ( )
2024-09-22 00:07:45 -04:00
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 ' )
2025-03-18 14:04:31 -04:00
start_date = models . DateTimeField ( null = True , blank = True )
end_date = models . DateTimeField ( null = True , blank = True )
2025-05-10 11:59:56 -04:00
timezone = models . CharField ( max_length = 50 , choices = [ ( tz , tz ) for tz in TIMEZONES ] , null = True , blank = True )
2024-09-22 00:07:45 -04:00
notes = models . TextField ( blank = True , null = True )
created_at = models . DateTimeField ( auto_now_add = True )
updated_at = models . DateTimeField ( auto_now = True )
def clean ( self ) :
if self . start_date > self . end_date :
raise ValidationError ( ' The start date must be before or equal to the end date. ' )
def __str__ ( self ) :
return f " { self . adventure . name } - { self . start_date } to { self . end_date } "
2024-07-08 11:44:39 -04:00
class Adventure ( models . Model ) :
2024-09-22 10:26:55 -04:00
#id = models.AutoField(primary_key=True)
2024-08-11 09:37:12 -04:00
id = models . UUIDField ( default = uuid . uuid4 , editable = False , unique = True , primary_key = True )
2024-07-08 11:44:39 -04:00
user_id = models . ForeignKey (
User , on_delete = models . CASCADE , default = default_user_id )
2024-11-14 09:37:35 -05:00
category = models . ForeignKey ( ' Category ' , on_delete = models . SET_NULL , blank = True , null = True )
2024-07-08 11:44:39 -04:00
name = models . CharField ( max_length = 200 )
location = models . CharField ( max_length = 200 , blank = True , null = True )
activity_types = ArrayField ( models . CharField (
max_length = 100 ) , blank = True , null = True )
description = models . TextField ( blank = True , null = True )
rating = models . FloatField ( blank = True , null = True )
2024-09-17 10:34:31 -04:00
link = models . URLField ( blank = True , null = True , max_length = 2083 )
2024-07-08 11:44:39 -04:00
is_public = models . BooleanField ( default = False )
2025-05-22 20:05:13 -04:00
2024-07-08 11:44:39 -04:00
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 )
2025-05-22 20:05:13 -04:00
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 )
2024-07-15 09:36:07 -04:00
collection = models . ForeignKey ( ' Collection ' , on_delete = models . CASCADE , blank = True , null = True )
2025-05-22 20:05:13 -04:00
2024-07-18 15:48:14 -04:00
created_at = models . DateTimeField ( auto_now_add = True )
2024-07-19 09:05:47 -04:00
updated_at = models . DateTimeField ( auto_now = True )
2024-07-09 13:01:56 -04:00
2025-01-18 12:28:14 -05:00
objects = AdventureManager ( )
2024-09-22 14:12:41 -04:00
# 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)
2024-11-14 09:37:35 -05:00
# type = models.CharField(max_length=100, choices=ADVENTURE_TYPES, default='general')
2024-09-22 14:12:41 -04:00
2025-05-22 21:13:31 -04:00
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
2024-07-09 13:01:56 -04:00
def clean ( self ) :
2024-07-15 09:36:07 -04:00
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 )
2024-11-14 09:37:35 -05:00
if self . category :
if self . user_id != self . category . user_id :
raise ValidationError ( ' Adventures must be associated with categories owned by the same user. Category owner: ' + self . category . user_id . username + ' Adventure owner: ' + self . user_id . username )
2024-11-22 17:03:02 -05:00
2025-05-29 18:17:15 -04:00
def save ( self , force_insert = False , force_update = False , using = None , update_fields = None ) :
2024-11-22 17:03:02 -05:00
if force_insert and force_update :
raise ValueError ( " Cannot force both insert and updating in model saving. " )
if not self . category :
2025-05-29 18:17:15 -04:00
category , _ = Category . objects . get_or_create (
user_id = self . user_id ,
name = ' general ' ,
defaults = { ' display_name ' : ' General ' , ' icon ' : ' 🌍 ' }
)
2024-11-26 17:39:10 -05:00
self . category = category
2025-05-22 20:05:13 -04:00
2025-05-29 18:17:15 -04:00
# First save the adventure quickly
result = super ( ) . save ( force_insert , force_update , using , update_fields )
# Then fire off a thread to geocode + update region/city/country
2025-05-22 20:05:13 -04:00
if self . latitude and self . longitude :
2025-05-29 18:17:15 -04:00
threading . Thread ( target = background_geocode_and_assign , args = ( str ( self . id ) , ) ) . start ( )
return result
2024-07-18 11:26:14 -04:00
2024-07-09 13:26:39 -04:00
def __str__ ( self ) :
return self . name
2024-07-09 13:01:56 -04:00
2024-07-15 09:36:07 -04:00
class Collection ( models . Model ) :
2024-09-22 10:26:55 -04:00
#id = models.AutoField(primary_key=True)
2024-08-11 09:37:12 -04:00
id = models . UUIDField ( default = uuid . uuid4 , editable = False , unique = True , primary_key = True )
2024-07-09 13:01:56 -04:00
user_id = models . ForeignKey (
User , on_delete = models . CASCADE , default = default_user_id )
name = models . CharField ( max_length = 200 )
2024-07-15 09:36:07 -04:00
description = models . TextField ( blank = True , null = True )
2024-07-09 13:01:56 -04:00
is_public = models . BooleanField ( default = False )
2024-07-18 15:48:14 -04:00
created_at = models . DateTimeField ( auto_now_add = True )
2024-07-27 14:26:15 -04:00
start_date = models . DateField ( blank = True , null = True )
end_date = models . DateField ( blank = True , null = True )
2024-08-05 14:17:41 -04:00
updated_at = models . DateTimeField ( auto_now = True )
2024-08-07 13:01:12 -04:00
is_archived = models . BooleanField ( default = False )
2024-09-02 10:29:51 -04:00
shared_with = models . ManyToManyField ( User , related_name = ' shared_with ' , blank = True )
2024-10-07 23:16:27 -04:00
link = models . URLField ( blank = True , null = True , max_length = 2083 )
2024-08-05 14:17:41 -04:00
2024-07-09 13:01:56 -04:00
2024-07-15 09:36:07 -04:00
# if connected adventures are private and collection is public, raise an error
2024-07-09 13:01:56 -04:00
def clean ( self ) :
2024-07-09 13:43:32 -04:00
if self . is_public and self . pk : # Only check if the instance has a primary key
2024-07-09 13:26:39 -04:00
for adventure in self . adventure_set . all ( ) :
if not adventure . is_public :
2024-07-15 09:36:07 -04:00
raise ValidationError ( ' Public collections cannot be associated with private adventures. Collection: ' + self . name + ' Adventure: ' + adventure . name )
2024-07-08 11:44:39 -04:00
def __str__ ( self ) :
2024-07-20 21:39:33 -04:00
return self . name
2024-07-27 19:04:55 -04:00
class Transportation ( models . Model ) :
2024-09-22 10:26:55 -04:00
#id = models.AutoField(primary_key=True)
2024-08-11 09:37:12 -04:00
id = models . UUIDField ( default = uuid . uuid4 , editable = False , unique = True , primary_key = True )
2024-07-27 19:04:55 -04:00
user_id = models . ForeignKey (
User , on_delete = models . CASCADE , default = default_user_id )
type = models . CharField ( max_length = 100 , choices = TRANSPORTATION_TYPES )
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 , null = True )
date = models . DateTimeField ( blank = True , null = True )
2024-08-19 16:32:08 -04:00
end_date = models . DateTimeField ( blank = True , null = True )
2025-05-10 11:59:56 -04:00
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 )
2024-07-27 19:04:55 -04:00
flight_number = models . CharField ( max_length = 100 , blank = True , null = True )
from_location = models . CharField ( max_length = 200 , blank = True , null = True )
2024-12-19 18:46:52 -05:00
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 )
2024-07-27 19:04:55 -04:00
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 )
created_at = models . DateTimeField ( auto_now_add = True )
updated_at = models . DateTimeField ( auto_now = True )
def clean ( self ) :
2024-08-19 16:32:08 -04:00
print ( self . date )
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 ) )
2024-08-19 18:41:05 -04:00
2024-07-27 19:04:55 -04:00
if self . collection :
if self . collection . is_public and not self . is_public :
2024-07-27 21:18:15 -04:00
raise ValidationError ( ' Transportations associated with a public collection must be public. Collection: ' + self . collection . name + ' Transportation: ' + self . name )
2024-07-27 19:04:55 -04:00
if self . user_id != self . collection . user_id :
raise ValidationError ( ' Transportations must be associated with collections owned by the same user. Collection owner: ' + self . collection . user_id . username + ' Transportation owner: ' + self . user_id . username )
def __str__ ( self ) :
return self . name
2024-08-03 21:09:49 -04:00
class Note ( models . Model ) :
2024-09-22 10:26:55 -04:00
#id = models.AutoField(primary_key=True)
2024-08-11 09:37:12 -04:00
id = models . UUIDField ( default = uuid . uuid4 , editable = False , unique = True , primary_key = True )
2024-08-03 21:09:49 -04:00
user_id = models . ForeignKey (
User , on_delete = models . CASCADE , default = default_user_id )
name = models . CharField ( max_length = 200 )
content = models . TextField ( blank = True , null = True )
2024-08-04 09:56:58 -04:00
links = ArrayField ( models . URLField ( ) , blank = True , null = True )
2024-08-03 21:09:49 -04:00
date = models . DateField ( 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 . collection :
if self . collection . is_public and not self . is_public :
raise ValidationError ( ' Notes associated with a public collection must be public. Collection: ' + self . collection . name + ' Transportation: ' + self . name )
if self . user_id != self . collection . user_id :
raise ValidationError ( ' Notes must be associated with collections owned by the same user. Collection owner: ' + self . collection . user_id . username + ' Transportation owner: ' + self . user_id . username )
def __str__ ( self ) :
return self . name
2024-08-05 16:09:57 -04:00
class Checklist ( models . Model ) :
2024-09-22 10:26:55 -04:00
# id = models.AutoField(primary_key=True)
2024-08-11 09:37:12 -04:00
id = models . UUIDField ( default = uuid . uuid4 , editable = False , unique = True , primary_key = True )
2024-08-05 16:09:57 -04:00
user_id = models . ForeignKey (
User , on_delete = models . CASCADE , default = default_user_id )
name = models . CharField ( max_length = 200 )
date = models . DateField ( 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 . collection :
if self . collection . is_public and not self . is_public :
raise ValidationError ( ' Checklists associated with a public collection must be public. Collection: ' + self . collection . name + ' Checklist: ' + self . name )
if self . user_id != self . collection . user_id :
raise ValidationError ( ' Checklists must be associated with collections owned by the same user. Collection owner: ' + self . collection . user_id . username + ' Checklist owner: ' + self . user_id . username )
def __str__ ( self ) :
return self . name
class ChecklistItem ( models . Model ) :
2024-09-22 10:26:55 -04:00
#id = models.AutoField(primary_key=True)
2024-08-11 09:37:12 -04:00
id = models . UUIDField ( default = uuid . uuid4 , editable = False , unique = True , primary_key = True )
2024-08-05 16:09:57 -04:00
user_id = models . ForeignKey (
User , on_delete = models . CASCADE , default = default_user_id )
name = models . CharField ( max_length = 200 )
is_checked = models . BooleanField ( default = False )
checklist = models . ForeignKey ( ' Checklist ' , on_delete = models . CASCADE )
created_at = models . DateTimeField ( auto_now_add = True )
updated_at = models . DateTimeField ( auto_now = True )
def clean ( self ) :
if self . checklist . is_public and not self . checklist . is_public :
raise ValidationError ( ' Checklist items associated with a public checklist must be public. Checklist: ' + self . checklist . name + ' Checklist item: ' + self . name )
if self . user_id != self . checklist . user_id :
raise ValidationError ( ' Checklist items must be associated with checklists owned by the same user. Checklist owner: ' + self . checklist . user_id . username + ' Checklist item owner: ' + self . user_id . username )
def __str__ ( self ) :
return self . name
2024-08-15 19:36:42 -04:00
2025-01-01 19:27:33 -05:00
@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 )
2024-08-15 19:36:42 -04:00
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 )
2025-01-01 19:27:33 -05:00
image = ResizedImageField (
force_format = " WEBP " ,
quality = 75 ,
upload_to = PathAndRename ( ' images/ ' ) # Use the callable class here
)
2024-08-15 19:36:42 -04:00
adventure = models . ForeignKey ( Adventure , related_name = ' images ' , on_delete = models . CASCADE )
2025-01-02 23:25:58 -05:00
is_primary = models . BooleanField ( default = False )
2024-08-15 19:36:42 -04:00
def __str__ ( self ) :
2024-11-14 09:37:35 -05:00
return self . image . url
2025-01-18 20:06:12 -05:00
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 )
2025-03-15 12:29:12 -04:00
file = models . FileField ( upload_to = PathAndRename ( ' attachments/ ' ) , validators = [ validate_file_extension ] )
2025-01-18 20:06:12 -05:00
adventure = models . ForeignKey ( Adventure , related_name = ' attachments ' , on_delete = models . CASCADE )
2025-01-19 22:22:03 -05:00
name = models . CharField ( max_length = 200 , null = True , blank = True )
2025-01-18 20:06:12 -05:00
def __str__ ( self ) :
return self . file . url
2024-11-14 09:37:35 -05:00
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 )
2024-11-26 15:10:17 -05:00
icon = models . CharField ( max_length = 200 , default = ' 🌍 ' )
2024-11-14 09:37:35 -05:00
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 ) :
2025-02-02 10:36:47 -05:00
return self . name + ' - ' + self . display_name + ' - ' + self . icon
2025-02-08 16:10:01 -05:00
class Lodging ( models . Model ) :
2025-02-02 10:36:47 -05:00
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 )
2025-02-08 16:10:01 -05:00
type = models . CharField ( max_length = 100 , choices = LODGING_TYPES , default = ' other ' )
2025-02-02 10:36:47 -05:00
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 )
2025-05-10 11:59:56 -04:00
timezone = models . CharField ( max_length = 50 , choices = [ ( tz , tz ) for tz in TIMEZONES ] , null = True , blank = True )
2025-02-02 10:36:47 -05:00
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 ) :
2025-05-10 13:17:01 -04:00
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 ) )
2025-02-02 10:36:47 -05:00
if self . collection :
if self . collection . is_public and not self . is_public :
2025-02-08 16:10:01 -05:00
raise ValidationError ( ' Lodging associated with a public collection must be public. Collection: ' + self . collection . name + ' Loging: ' + self . name )
2025-02-02 10:36:47 -05:00
if self . user_id != self . collection . user_id :
2025-02-08 16:10:01 -05:00
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 )
2025-02-02 10:36:47 -05:00
def __str__ ( self ) :
return self . name