Skip to content

Commit 9ddc466

Browse files
wanooclaude
andcommitted
feat: Entity system overhaul — visits, types, shared architecture, ratings, pricing
- Expand Visit model: support Transportation/Lodging parents, rating, pricing, collection link - Add admin-managed types: TransportationType, LodgingType, AdventureType, ActivityType with seed data - Convert Transportation/Lodging collections from FK to M2M, add tags support - Add country FK fields to Transportation (origin/dest) and Lodging - Add cached average_rating to Location, Transportation, Lodging - Default is_public=True on all entities - Add 52 shared frontend components (modal, form, cards, detail, list) - Refactor entity modals, details, and visits to use shared base components - Add entity list pages with filters, sorting, and public tab - Update cards with visit count, tags, rating, last visit date - Add dashboard stats (total km, nights, places) - Add collection enhancements (public tab, sharing, broken items, recommendations) - Sync all i18n translations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 529ea1c commit 9ddc466

File tree

168 files changed

+37772
-31553
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

168 files changed

+37772
-31553
lines changed

.gitignore

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,3 @@
33
.vscode/settings.json
44
.pnpm-store/
55
.env
6-
7-
# Cursor IDE configuration
8-
.cursor/
9-
.cursorrules
10-
11-
# Internal development documentation (not for public repo)
12-
docs/cursor-prompts/
13-
docs/FIX_PACKAGE_PLAN.md
14-
docs/GITHUB_COMMENTS.md
15-
BUGFIX_DOCUMENTATION.md

backend/server/adventures/admin.py

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
from django.contrib import admin
33
from django.utils.html import mark_safe, format_html
44
from django.urls import reverse
5-
from .models import Location, Checklist, ChecklistItem, Collection, Transportation, Note, ContentImage, Visit, Category, ContentAttachment, Lodging, CollectionInvite, Trail, Activity, CollectionItineraryItem, CollectionItineraryDay
6-
from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
5+
from .models import Location, Checklist, ChecklistItem, Collection, Transportation, Note, ContentImage, Visit, Category, ContentAttachment, Lodging, CollectionInvite, Trail, Activity, CollectionItineraryItem, CollectionItineraryDay, TransportationType, LodgingType, AdventureType, ActivityType
6+
# worldtravel models are now registered in worldtravel/admin.py
77
from allauth.account.decorators import secure_admin_login
88

99
admin.autodiscover()
@@ -45,38 +45,6 @@ def get_visit_count(self, obj):
4545
get_visit_count.short_description = 'Visit Count'
4646

4747

48-
class CountryAdmin(admin.ModelAdmin):
49-
list_display = ('name', 'country_code', 'number_of_regions')
50-
list_filter = ('subregion',)
51-
search_fields = ('name', 'country_code')
52-
53-
def number_of_regions(self, obj):
54-
return Region.objects.filter(country=obj).count()
55-
56-
number_of_regions.short_description = 'Number of Regions'
57-
58-
59-
class RegionAdmin(admin.ModelAdmin):
60-
list_display = ('name', 'country', 'number_of_visits')
61-
list_filter = ('country',)
62-
search_fields = ('name', 'country__name')
63-
# list_filter = ('country', 'number_of_visits')
64-
65-
def number_of_visits(self, obj):
66-
return VisitedRegion.objects.filter(region=obj).count()
67-
68-
number_of_visits.short_description = 'Number of Visits'
69-
70-
class CityAdmin(admin.ModelAdmin):
71-
list_display = ('name', 'region', 'country')
72-
list_filter = ('region', 'region__country')
73-
search_fields = ('name', 'region__name', 'region__country__name')
74-
75-
def country(self, obj):
76-
return obj.region.country.name
77-
78-
country.short_description = 'Country'
79-
8048
from django.contrib import admin
8149
from django.contrib.auth.admin import UserAdmin
8250
from users.models import CustomUser
@@ -193,28 +161,55 @@ def object_link(self, obj):
193161

194162
object_link.short_description = 'Item'
195163

164+
class TransportationTypeAdmin(admin.ModelAdmin):
165+
list_display = ('key', 'name', 'icon', 'display_order', 'is_active')
166+
list_editable = ('name', 'icon', 'display_order', 'is_active')
167+
search_fields = ('key', 'name')
168+
ordering = ('display_order', 'name')
169+
170+
171+
class LodgingTypeAdmin(admin.ModelAdmin):
172+
list_display = ('key', 'name', 'icon', 'display_order', 'is_active')
173+
list_editable = ('name', 'icon', 'display_order', 'is_active')
174+
search_fields = ('key', 'name')
175+
ordering = ('display_order', 'name')
176+
177+
178+
class AdventureTypeAdmin(admin.ModelAdmin):
179+
list_display = ('key', 'name', 'icon', 'display_order', 'is_active')
180+
list_editable = ('name', 'icon', 'display_order', 'is_active')
181+
search_fields = ('key', 'name')
182+
ordering = ('display_order', 'name')
183+
184+
185+
class ActivityTypeAdmin(admin.ModelAdmin):
186+
list_display = ('key', 'name', 'icon', 'color', 'display_order', 'is_active')
187+
list_editable = ('name', 'icon', 'color', 'display_order', 'is_active')
188+
search_fields = ('key', 'name')
189+
ordering = ('display_order', 'name')
190+
191+
196192
admin.site.register(CustomUser, CustomUserAdmin)
197193
admin.site.register(Location, LocationAdmin)
198194
admin.site.register(Collection, CollectionAdmin)
199195
admin.site.register(Visit, VisitAdmin)
200-
admin.site.register(Country, CountryAdmin)
201-
admin.site.register(Region, RegionAdmin)
202-
admin.site.register(VisitedRegion)
203196
admin.site.register(Transportation)
204197
admin.site.register(Note)
205198
admin.site.register(Checklist)
206199
admin.site.register(ChecklistItem)
207200
admin.site.register(ContentImage, ContentImageImageAdmin)
208201
admin.site.register(Category, CategoryAdmin)
209-
admin.site.register(City, CityAdmin)
210-
admin.site.register(VisitedCity)
211202
admin.site.register(ContentAttachment)
212203
admin.site.register(Lodging)
213204
admin.site.register(CollectionInvite, CollectionInviteAdmin)
214205
admin.site.register(Trail)
215206
admin.site.register(Activity, ActivityAdmin)
216207
admin.site.register(CollectionItineraryItem, CollectionItineraryItemAdmin)
217208
admin.site.register(CollectionItineraryDay)
209+
admin.site.register(TransportationType, TransportationTypeAdmin)
210+
admin.site.register(LodgingType, LodgingTypeAdmin)
211+
admin.site.register(AdventureType, AdventureTypeAdmin)
212+
admin.site.register(ActivityType, ActivityTypeAdmin)
218213

219214
admin.site.site_header = 'AdventureLog Admin'
220215
admin.site.site_title = 'AdventureLog Admin Site'
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"""
2+
Django management command to synchronize visited regions/cities from Visit records.
3+
4+
This command processes all visits and marks their corresponding cities/regions as visited
5+
using reverse geocoding of the parent item's coordinates.
6+
7+
Usage:
8+
python manage.py sync_visits_to_worldtravel
9+
python manage.py sync_visits_to_worldtravel --dry-run
10+
python manage.py sync_visits_to_worldtravel --user-id 123
11+
"""
12+
13+
from django.core.management.base import BaseCommand, CommandError
14+
from django.contrib.auth import get_user_model
15+
from adventures.models import Visit
16+
from adventures.signals import mark_city_region_visited, _get_visit_coordinates
17+
import logging
18+
19+
logger = logging.getLogger(__name__)
20+
User = get_user_model()
21+
22+
23+
class Command(BaseCommand):
24+
help = 'Sync visited regions/cities from Visit records using reverse geocoding'
25+
26+
def add_arguments(self, parser):
27+
parser.add_argument(
28+
'--dry-run',
29+
action='store_true',
30+
help='Show what would be created without making changes',
31+
)
32+
parser.add_argument(
33+
'--user-id',
34+
type=int,
35+
help='Sync only for a specific user ID',
36+
)
37+
parser.add_argument(
38+
'--verbose',
39+
action='store_true',
40+
help='Show detailed output for each visit',
41+
)
42+
43+
def handle(self, *args, **options):
44+
dry_run = options['dry_run']
45+
user_id = options.get('user_id')
46+
verbose = options['verbose']
47+
48+
if dry_run:
49+
self.stdout.write(
50+
self.style.WARNING('DRY RUN MODE - No changes will be made')
51+
)
52+
53+
# Build visits queryset
54+
visits_queryset = Visit.objects.select_related(
55+
'user', 'location', 'transportation', 'lodging'
56+
).filter(user__isnull=False)
57+
58+
if user_id:
59+
visits_queryset = visits_queryset.filter(user_id=user_id)
60+
61+
try:
62+
total_visits = visits_queryset.count()
63+
except Exception as e:
64+
# May fail during migration if schema is changing
65+
self.stdout.write(self.style.WARNING(f'Could not count visits (migration in progress?): {e}'))
66+
return
67+
68+
if total_visits == 0:
69+
self.stdout.write(self.style.WARNING('No visits found'))
70+
return
71+
72+
self.stdout.write(f'Processing {total_visits} visit(s)...\n')
73+
74+
# Track statistics
75+
total_cities_marked = 0
76+
total_regions_marked = 0
77+
visits_processed = 0
78+
visits_with_coords = 0
79+
visits_with_changes = 0
80+
81+
try:
82+
visits_list = list(visits_queryset)
83+
except Exception as e:
84+
# May fail during migration if schema is changing
85+
self.stdout.write(self.style.WARNING(f'Could not load visits (migration in progress?): {e}'))
86+
return
87+
88+
for visit in visits_list:
89+
visits_processed += 1
90+
91+
lat, lon = _get_visit_coordinates(visit)
92+
93+
if lat is None or lon is None:
94+
if verbose:
95+
self.stdout.write(f' Visit {visit.id}: No coordinates')
96+
continue
97+
98+
visits_with_coords += 1
99+
100+
if dry_run:
101+
if verbose:
102+
self.stdout.write(
103+
f' Visit {visit.id}: Would geocode ({lat}, {lon}) for user {visit.user.username}'
104+
)
105+
continue
106+
107+
try:
108+
city_marked, region_marked = mark_city_region_visited(
109+
visit.user, lat, lon
110+
)
111+
112+
if city_marked:
113+
total_cities_marked += 1
114+
if region_marked:
115+
total_regions_marked += 1
116+
if city_marked or region_marked:
117+
visits_with_changes += 1
118+
119+
if verbose and (city_marked or region_marked):
120+
self.stdout.write(
121+
f' Visit {visit.id}: city={city_marked}, region={region_marked}'
122+
)
123+
124+
except Exception as e:
125+
self.stdout.write(
126+
self.style.ERROR(f'Error processing visit {visit.id}: {str(e)}')
127+
)
128+
logger.exception(f'Error processing visit {visit.id}')
129+
130+
# Progress indicator
131+
if visits_processed % 100 == 0:
132+
self.stdout.write(f'Processed {visits_processed}/{total_visits} visits...')
133+
134+
# Summary
135+
self.stdout.write('\n' + '=' * 60)
136+
if dry_run:
137+
self.stdout.write(
138+
self.style.SUCCESS(
139+
f'DRY RUN COMPLETE:\n'
140+
f' Visits processed: {visits_processed}\n'
141+
f' Visits with coordinates: {visits_with_coords}\n'
142+
f' Would geocode {visits_with_coords} visits'
143+
)
144+
)
145+
else:
146+
self.stdout.write(
147+
self.style.SUCCESS(
148+
f'SYNC COMPLETE:\n'
149+
f' Visits processed: {visits_processed}\n'
150+
f' Visits with coordinates: {visits_with_coords}\n'
151+
f' Visits with changes: {visits_with_changes}\n'
152+
f' New regions marked: {total_regions_marked}\n'
153+
f' New cities marked: {total_cities_marked}'
154+
)
155+
)

0 commit comments

Comments
 (0)