Skip to content

Commit e6a79c6

Browse files
author
wanoo
committed
feat: Enhance Transportation & Lodging with visits, tags, and multi-collection support
This adds significant new functionality to Transportation and Lodging models: ## Visits Support - Add visits to Transportation and Lodging (like Locations) - Track visit dates, notes, and user who created each visit - Visits step in modal for creating/editing ## Tags Support - Add tags field to Transportation and Lodging - Display tags on detail pages - Full-text search by tags ## Multi-Collection Support - Change collection field from ForeignKey to ManyToMany - Items can now belong to multiple collections - Link existing items to collections from collection detail page ## Standalone List Pages - New /transportations and /lodging pages with filtering - Filter by type, visited status, public/private - Search by name and location - Consistent with /locations page design ## UI Enhancements - Visited badges on Transportation and Lodging cards - Dashboard displays Transportation and Lodging - Navigation links to new pages
1 parent c008f0c commit e6a79c6

Some content is hidden

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

54 files changed

+6178
-305
lines changed

backend/server/adventures/admin.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,29 @@ def image_display(self, obj):
110110

111111

112112
class VisitAdmin(admin.ModelAdmin):
113-
list_display = ('location', 'start_date', 'end_date', 'notes')
113+
list_display = ('get_parent', 'get_parent_type', 'start_date', 'end_date', 'user', 'notes')
114114
list_filter = ('start_date', 'end_date')
115-
search_fields = ('notes',)
115+
search_fields = ('notes', 'location__name', 'transportation__name', 'lodging__name')
116+
117+
def get_parent(self, obj):
118+
if obj.location:
119+
return obj.location.name
120+
elif obj.transportation:
121+
return obj.transportation.name
122+
elif obj.lodging:
123+
return obj.lodging.name
124+
return 'Unknown'
125+
get_parent.short_description = 'Parent'
126+
127+
def get_parent_type(self, obj):
128+
if obj.location:
129+
return 'Location'
130+
elif obj.transportation:
131+
return 'Transportation'
132+
elif obj.lodging:
133+
return 'Lodging'
134+
return 'Unknown'
135+
get_parent_type.short_description = 'Type'
116136

117137

118138
def image_display(self, obj):
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add user field to Visit model for tracking who created each visit
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
('adventures', '0071_alter_collectionitineraryitem_unique_together_and_more'),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name='visit',
18+
name='user',
19+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='visits', to=settings.AUTH_USER_MODEL),
20+
),
21+
]
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Data migration to populate user field on existing visits
2+
3+
from django.db import migrations
4+
5+
6+
def populate_visit_users(apps, schema_editor):
7+
"""
8+
Assign existing visits to the location's owner.
9+
This ensures existing visits have a user for display purposes.
10+
"""
11+
Visit = apps.get_model('adventures', 'Visit')
12+
13+
for visit in Visit.objects.filter(user__isnull=True).select_related('location'):
14+
if visit.location and visit.location.user:
15+
visit.user = visit.location.user
16+
visit.save(update_fields=['user'])
17+
18+
19+
def reverse_populate(apps, schema_editor):
20+
"""Reverse migration - clear user from visits."""
21+
Visit = apps.get_model('adventures', 'Visit')
22+
Visit.objects.update(user=None)
23+
24+
25+
class Migration(migrations.Migration):
26+
27+
dependencies = [
28+
('adventures', '0072_visit_user'),
29+
]
30+
31+
operations = [
32+
migrations.RunPython(populate_visit_users, reverse_populate),
33+
]
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Generated for adding Transportation and Lodging to Visit model
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('adventures', '0073_populate_visit_users'),
11+
]
12+
13+
operations = [
14+
# Make location nullable since visits can now be for transportation/lodging
15+
migrations.AlterField(
16+
model_name='visit',
17+
name='location',
18+
field=models.ForeignKey(
19+
blank=True,
20+
null=True,
21+
on_delete=django.db.models.deletion.CASCADE,
22+
related_name='visits',
23+
to='adventures.location'
24+
),
25+
),
26+
# Add transportation FK
27+
migrations.AddField(
28+
model_name='visit',
29+
name='transportation',
30+
field=models.ForeignKey(
31+
blank=True,
32+
null=True,
33+
on_delete=django.db.models.deletion.CASCADE,
34+
related_name='visits',
35+
to='adventures.transportation'
36+
),
37+
),
38+
# Add lodging FK
39+
migrations.AddField(
40+
model_name='visit',
41+
name='lodging',
42+
field=models.ForeignKey(
43+
blank=True,
44+
null=True,
45+
on_delete=django.db.models.deletion.CASCADE,
46+
related_name='visits',
47+
to='adventures.lodging'
48+
),
49+
),
50+
]
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Safe migration for adding Transportation and Lodging to Visit model
2+
# This migration uses raw SQL to add columns if they don't exist
3+
4+
from django.db import migrations, connection
5+
6+
7+
def add_columns_if_not_exist(apps, schema_editor):
8+
"""Add transportation_id and lodging_id columns to adventures_visit if they don't exist."""
9+
with connection.cursor() as cursor:
10+
# Check if transportation_id column exists
11+
cursor.execute("""
12+
SELECT EXISTS (
13+
SELECT FROM information_schema.columns
14+
WHERE table_name = 'adventures_visit'
15+
AND column_name = 'transportation_id'
16+
);
17+
""")
18+
transportation_exists = cursor.fetchone()[0]
19+
20+
if not transportation_exists:
21+
cursor.execute("""
22+
ALTER TABLE adventures_visit
23+
ADD COLUMN transportation_id UUID NULL
24+
REFERENCES adventures_transportation(id) ON DELETE CASCADE;
25+
""")
26+
cursor.execute("""
27+
CREATE INDEX IF NOT EXISTS adventures_visit_transportation_id_idx
28+
ON adventures_visit (transportation_id);
29+
""")
30+
31+
# Check if lodging_id column exists
32+
cursor.execute("""
33+
SELECT EXISTS (
34+
SELECT FROM information_schema.columns
35+
WHERE table_name = 'adventures_visit'
36+
AND column_name = 'lodging_id'
37+
);
38+
""")
39+
lodging_exists = cursor.fetchone()[0]
40+
41+
if not lodging_exists:
42+
cursor.execute("""
43+
ALTER TABLE adventures_visit
44+
ADD COLUMN lodging_id UUID NULL
45+
REFERENCES adventures_lodging(id) ON DELETE CASCADE;
46+
""")
47+
cursor.execute("""
48+
CREATE INDEX IF NOT EXISTS adventures_visit_lodging_id_idx
49+
ON adventures_visit (lodging_id);
50+
""")
51+
52+
# Make location_id nullable if it isn't already
53+
cursor.execute("""
54+
ALTER TABLE adventures_visit
55+
ALTER COLUMN location_id DROP NOT NULL;
56+
""")
57+
58+
59+
def reverse_migration(apps, schema_editor):
60+
"""Remove the columns (for rollback)."""
61+
with connection.cursor() as cursor:
62+
cursor.execute("ALTER TABLE adventures_visit DROP COLUMN IF EXISTS transportation_id;")
63+
cursor.execute("ALTER TABLE adventures_visit DROP COLUMN IF EXISTS lodging_id;")
64+
65+
66+
class Migration(migrations.Migration):
67+
68+
dependencies = [
69+
('adventures', '0074_visit_transportation_lodging'),
70+
]
71+
72+
operations = [
73+
migrations.RunPython(add_columns_if_not_exist, reverse_migration),
74+
]
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Safe migration for adding tags field to Transportation and Lodging models
2+
# This migration uses raw SQL to add columns if they don't exist
3+
4+
from django.db import migrations, connection
5+
6+
7+
def add_tags_columns_if_not_exist(apps, schema_editor):
8+
"""Add tags column to adventures_transportation and adventures_lodging if they don't exist."""
9+
with connection.cursor() as cursor:
10+
# Check if tags column exists on transportation
11+
cursor.execute("""
12+
SELECT EXISTS (
13+
SELECT FROM information_schema.columns
14+
WHERE table_name = 'adventures_transportation'
15+
AND column_name = 'tags'
16+
);
17+
""")
18+
transportation_tags_exists = cursor.fetchone()[0]
19+
20+
if not transportation_tags_exists:
21+
cursor.execute("""
22+
ALTER TABLE adventures_transportation
23+
ADD COLUMN tags VARCHAR(100)[] NULL;
24+
""")
25+
26+
# Check if tags column exists on lodging
27+
cursor.execute("""
28+
SELECT EXISTS (
29+
SELECT FROM information_schema.columns
30+
WHERE table_name = 'adventures_lodging'
31+
AND column_name = 'tags'
32+
);
33+
""")
34+
lodging_tags_exists = cursor.fetchone()[0]
35+
36+
if not lodging_tags_exists:
37+
cursor.execute("""
38+
ALTER TABLE adventures_lodging
39+
ADD COLUMN tags VARCHAR(100)[] NULL;
40+
""")
41+
42+
43+
def reverse_migration(apps, schema_editor):
44+
"""Remove the columns (for rollback)."""
45+
with connection.cursor() as cursor:
46+
cursor.execute("ALTER TABLE adventures_transportation DROP COLUMN IF EXISTS tags;")
47+
cursor.execute("ALTER TABLE adventures_lodging DROP COLUMN IF EXISTS tags;")
48+
49+
50+
class Migration(migrations.Migration):
51+
52+
dependencies = [
53+
('adventures', '0075_visit_transportation_lodging_safe'),
54+
]
55+
56+
operations = [
57+
migrations.RunPython(add_tags_columns_if_not_exist, reverse_migration),
58+
]

0 commit comments

Comments
 (0)