1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-07 06:05:19 +02:00

feat: enhance import/export functionality to include trails and activities; update UI components and localization

This commit is contained in:
Sean Morley 2025-08-05 20:59:37 -04:00
parent a173ff5471
commit 2da568dcc1
4 changed files with 174 additions and 14 deletions

View file

@ -19,7 +19,7 @@ from django.contrib.contenttypes.models import ContentType
from adventures.models import ( from adventures.models import (
Location, Collection, Transportation, Note, Checklist, ChecklistItem, Location, Collection, Transportation, Note, Checklist, ChecklistItem,
ContentImage, ContentAttachment, Category, Lodging, Visit ContentImage, ContentAttachment, Category, Lodging, Visit, Trail, Activity
) )
from worldtravel.models import VisitedCity, VisitedRegion, City, Region, Country from worldtravel.models import VisitedCity, VisitedRegion, City, Region, Country
@ -111,18 +111,69 @@ class BackupViewSet(viewsets.ViewSet):
'category_name': location.category.name if location.category else None, 'category_name': location.category.name if location.category else None,
'collection_export_ids': [collection_name_to_id[col_name] for col_name in location.collections.values_list('name', flat=True) if col_name in collection_name_to_id], 'collection_export_ids': [collection_name_to_id[col_name] for col_name in location.collections.values_list('name', flat=True) if col_name in collection_name_to_id],
'visits': [], 'visits': [],
'trails': [],
'images': [], 'images': [],
'attachments': [] 'attachments': []
} }
# Add visits # Add visits
for visit in location.visits.all(): for visit_idx, visit in enumerate(location.visits.all()):
location_data['visits'].append({ visit_data = {
'export_id': visit_idx, # Add unique identifier for this visit
'start_date': visit.start_date.isoformat() if visit.start_date else None, 'start_date': visit.start_date.isoformat() if visit.start_date else None,
'end_date': visit.end_date.isoformat() if visit.end_date else None, 'end_date': visit.end_date.isoformat() if visit.end_date else None,
'timezone': visit.timezone, 'timezone': visit.timezone,
'notes': visit.notes 'notes': visit.notes,
}) 'activities': []
}
# Add activities for this visit
for activity in visit.activities.all():
activity_data = {
'name': activity.name,
'type': activity.type,
'sport_type': activity.sport_type,
'distance': float(activity.distance) if activity.distance else None,
'moving_time': activity.moving_time.total_seconds() if activity.moving_time else None,
'elapsed_time': activity.elapsed_time.total_seconds() if activity.elapsed_time else None,
'rest_time': activity.rest_time.total_seconds() if activity.rest_time else None,
'elevation_gain': float(activity.elevation_gain) if activity.elevation_gain else None,
'elevation_loss': float(activity.elevation_loss) if activity.elevation_loss else None,
'elev_high': float(activity.elev_high) if activity.elev_high else None,
'elev_low': float(activity.elev_low) if activity.elev_low else None,
'start_date': activity.start_date.isoformat() if activity.start_date else None,
'start_date_local': activity.start_date_local.isoformat() if activity.start_date_local else None,
'timezone': activity.timezone,
'average_speed': float(activity.average_speed) if activity.average_speed else None,
'max_speed': float(activity.max_speed) if activity.max_speed else None,
'average_cadence': float(activity.average_cadence) if activity.average_cadence else None,
'calories': float(activity.calories) if activity.calories else None,
'start_lat': float(activity.start_lat) if activity.start_lat else None,
'start_lng': float(activity.start_lng) if activity.start_lng else None,
'end_lat': float(activity.end_lat) if activity.end_lat else None,
'end_lng': float(activity.end_lng) if activity.end_lng else None,
'external_service_id': activity.external_service_id,
'trail_name': activity.trail.name if activity.trail else None, # Link by trail name
'gpx_filename': None
}
# Handle GPX file
if activity.gpx_file:
activity_data['gpx_filename'] = activity.gpx_file.name.split('/')[-1]
visit_data['activities'].append(activity_data)
location_data['visits'].append(visit_data)
# Add trails for this location
for trail in location.trails.all():
trail_data = {
'name': trail.name,
'link': trail.link,
'wanderer_id': trail.wanderer_id,
'created_at': trail.created_at.isoformat() if trail.created_at else None
}
location_data['trails'].append(trail_data)
# Add images # Add images
for image in location.images.all(): for image in location.images.all():
@ -242,7 +293,7 @@ class BackupViewSet(viewsets.ViewSet):
# Add JSON data # Add JSON data
zip_file.writestr('data.json', json.dumps(export_data, indent=2)) zip_file.writestr('data.json', json.dumps(export_data, indent=2))
# Add images and attachments # Add images, attachments, and GPX files
files_added = set() files_added = set()
for location in user.location_set.all(): for location in user.location_set.all():
@ -267,6 +318,18 @@ class BackupViewSet(viewsets.ViewSet):
files_added.add(attachment.file.name) files_added.add(attachment.file.name)
except Exception as e: except Exception as e:
print(f"Error adding attachment {attachment.file.name}: {e}") print(f"Error adding attachment {attachment.file.name}: {e}")
# Add GPX files from activities
for visit in location.visits.all():
for activity in visit.activities.all():
if activity.gpx_file and activity.gpx_file.name not in files_added:
try:
gpx_content = default_storage.open(activity.gpx_file.name).read()
filename = activity.gpx_file.name.split('/')[-1]
zip_file.writestr(f'gpx/{filename}', gpx_content)
files_added.add(activity.gpx_file.name)
except Exception as e:
print(f"Error adding GPX file {activity.gpx_file.name}: {e}")
# Return ZIP file as response # Return ZIP file as response
with open(tmp_file.name, 'rb') as zip_file: with open(tmp_file.name, 'rb') as zip_file:
@ -341,6 +404,8 @@ class BackupViewSet(viewsets.ViewSet):
def _clear_user_data(self, user): def _clear_user_data(self, user):
"""Clear all existing user data before import""" """Clear all existing user data before import"""
# Delete in reverse order of dependencies # Delete in reverse order of dependencies
user.activity_set.all().delete() # Delete activities first
user.trail_set.all().delete() # Delete trails
user.checklistitem_set.all().delete() user.checklistitem_set.all().delete()
user.checklist_set.all().delete() user.checklist_set.all().delete()
user.note_set.all().delete() user.note_set.all().delete()
@ -363,13 +428,19 @@ class BackupViewSet(viewsets.ViewSet):
def _import_data(self, backup_data, zip_file, user): def _import_data(self, backup_data, zip_file, user):
"""Import backup data and return summary""" """Import backup data and return summary"""
from datetime import timedelta
# Track mappings and counts # Track mappings and counts
category_map = {} category_map = {}
collection_map = {} # Map export_id to actual collection object collection_map = {} # Map export_id to actual collection object
location_map = {} # Map location export_id to actual location object
trail_name_map = {} # Map (location_id, trail_name) to trail object
summary = { summary = {
'categories': 0, 'collections': 0, 'locations': 0, 'categories': 0, 'collections': 0, 'locations': 0,
'transportation': 0, 'notes': 0, 'checklists': 0, 'transportation': 0, 'notes': 0, 'checklists': 0,
'checklist_items': 0, 'lodging': 0, 'images': 0, 'attachments': 0, 'visited_cities': 0, 'visited_regions': 0 'checklist_items': 0, 'lodging': 0, 'images': 0,
'attachments': 0, 'visited_cities': 0, 'visited_regions': 0,
'trails': 0, 'activities': 0, 'gpx_files': 0
} }
# Import Visited Cities # Import Visited Cities
@ -468,21 +539,98 @@ class BackupViewSet(viewsets.ViewSet):
category=category_map.get(adv_data.get('category_name')) category=category_map.get(adv_data.get('category_name'))
) )
location.save(_skip_geocode=True) # Skip geocoding for now location.save(_skip_geocode=True) # Skip geocoding for now
location_map[adv_data['export_id']] = location
# Add to collections using export_ids - MUST be done after save() # Add to collections using export_ids - MUST be done after save()
for collection_export_id in adv_data.get('collection_export_ids', []): for collection_export_id in adv_data.get('collection_export_ids', []):
if collection_export_id in collection_map: if collection_export_id in collection_map:
location.collections.add(collection_map[collection_export_id]) location.collections.add(collection_map[collection_export_id])
# Import visits # Import trails for this location first
for trail_data in adv_data.get('trails', []):
trail = Trail.objects.create(
user=user,
location=location,
name=trail_data['name'],
link=trail_data.get('link'),
wanderer_id=trail_data.get('wanderer_id'),
created_at=trail_data.get('created_at')
)
trail_name_map[(location.id, trail_data['name'])] = trail
summary['trails'] += 1
# Import visits and their activities
for visit_data in adv_data.get('visits', []): for visit_data in adv_data.get('visits', []):
Visit.objects.create( visit = Visit.objects.create(
location=location, location=location,
start_date=visit_data.get('start_date'), start_date=visit_data.get('start_date'),
end_date=visit_data.get('end_date'), end_date=visit_data.get('end_date'),
timezone=visit_data.get('timezone'), timezone=visit_data.get('timezone'),
notes=visit_data.get('notes') notes=visit_data.get('notes')
) )
# Import activities for this visit
for activity_data in visit_data.get('activities', []):
# Find the trail if specified
trail = None
if activity_data.get('trail_name'):
trail = trail_name_map.get((location.id, activity_data['trail_name']))
# Convert time durations back from seconds
moving_time = None
if activity_data.get('moving_time') is not None:
moving_time = timedelta(seconds=activity_data['moving_time'])
elapsed_time = None
if activity_data.get('elapsed_time') is not None:
elapsed_time = timedelta(seconds=activity_data['elapsed_time'])
rest_time = None
if activity_data.get('rest_time') is not None:
rest_time = timedelta(seconds=activity_data['rest_time'])
activity = Activity(
user=user,
visit=visit,
trail=trail,
name=activity_data['name'],
type=activity_data.get('type', 'general'),
sport_type=activity_data.get('sport_type'),
distance=activity_data.get('distance'),
moving_time=moving_time,
elapsed_time=elapsed_time,
rest_time=rest_time,
elevation_gain=activity_data.get('elevation_gain'),
elevation_loss=activity_data.get('elevation_loss'),
elev_high=activity_data.get('elev_high'),
elev_low=activity_data.get('elev_low'),
start_date=activity_data.get('start_date'),
start_date_local=activity_data.get('start_date_local'),
timezone=activity_data.get('timezone'),
average_speed=activity_data.get('average_speed'),
max_speed=activity_data.get('max_speed'),
average_cadence=activity_data.get('average_cadence'),
calories=activity_data.get('calories'),
start_lat=activity_data.get('start_lat'),
start_lng=activity_data.get('start_lng'),
end_lat=activity_data.get('end_lat'),
end_lng=activity_data.get('end_lng'),
external_service_id=activity_data.get('external_service_id')
)
# Handle GPX file
gpx_filename = activity_data.get('gpx_filename')
if gpx_filename:
try:
gpx_content = zip_file.read(f'gpx/{gpx_filename}')
gpx_file = ContentFile(gpx_content, name=gpx_filename)
activity.gpx_file = gpx_file
summary['gpx_files'] += 1
except KeyError:
pass # GPX file not found in backup
activity.save()
summary['activities'] += 1
# Import images # Import images
content_type = ContentType.objects.get(model='location') content_type = ContentType.objects.get(model='location')

View file

@ -1141,9 +1141,11 @@
{/if} {/if}
</div> </div>
{:else} {:else}
{#each wandererFetchedTrails as trail (trail.id)} <div class="space-y-3">
<WandererCard {trail} on:link={linkWandererTrail} /> {#each wandererFetchedTrails as trail (trail.id)}
{/each} <WandererCard {trail} on:link={linkWandererTrail} />
{/each}
</div>
{/if} {/if}
</div> </div>
{:else} {:else}
@ -1154,7 +1156,7 @@
<div class="flex gap-2 justify-end"> <div class="flex gap-2 justify-end">
<button <button
class="btn btn-ghost btn-sm" class="btn btn-accent btn-sm"
on:click={() => { on:click={() => {
showWandererForm = false; showWandererForm = false;
showAddTrailForm = false; showAddTrailForm = false;

View file

@ -544,7 +544,9 @@
"data_override_acknowledge": "I acknowledge that this will override all my existing data", "data_override_acknowledge": "I acknowledge that this will override all my existing data",
"data_override_acknowledge_desc": "This action is irreversible and will replace all locations, collections, \t\t\t\t\t\t\t\t\t\t\t\t\t\tand visits in your account.", "data_override_acknowledge_desc": "This action is irreversible and will replace all locations, collections, \t\t\t\t\t\t\t\t\t\t\t\t\t\tand visits in your account.",
"use_imperial": "Use Imperial Units", "use_imperial": "Use Imperial Units",
"use_imperial_desc": "Use imperial units (feet, inches, pounds) instead of metric units" "use_imperial_desc": "Use imperial units (feet, inches, pounds) instead of metric units",
"trails": "Trails",
"activities": "Activities"
}, },
"collection": { "collection": {
"collection_created": "Collection created successfully!", "collection_created": "Collection created successfully!",

View file

@ -1289,6 +1289,14 @@
<span>🖼️ {$t('settings.media')}</span> <span>🖼️ {$t('settings.media')}</span>
<span></span> <span></span>
</div> </div>
<div class="flex items-center justify-between">
<span>🥾 {$t('settings.trails')}</span>
<span></span>
</div>
<div class="flex items-center justify-between">
<span>⏱️ {$t('settings.activities')}</span>
<span></span>
</div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span>🌍 {$t('settings.world_travel_visits')}</span> <span>🌍 {$t('settings.world_travel_visits')}</span>
<span></span> <span></span>