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:
parent
a173ff5471
commit
2da568dcc1
4 changed files with 174 additions and 14 deletions
|
@ -19,7 +19,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||
|
||||
from adventures.models import (
|
||||
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
|
||||
|
||||
|
@ -111,18 +111,69 @@ class BackupViewSet(viewsets.ViewSet):
|
|||
'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],
|
||||
'visits': [],
|
||||
'trails': [],
|
||||
'images': [],
|
||||
'attachments': []
|
||||
}
|
||||
|
||||
# Add visits
|
||||
for visit in location.visits.all():
|
||||
location_data['visits'].append({
|
||||
for visit_idx, visit in enumerate(location.visits.all()):
|
||||
visit_data = {
|
||||
'export_id': visit_idx, # Add unique identifier for this visit
|
||||
'start_date': visit.start_date.isoformat() if visit.start_date else None,
|
||||
'end_date': visit.end_date.isoformat() if visit.end_date else None,
|
||||
'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
|
||||
for image in location.images.all():
|
||||
|
@ -242,7 +293,7 @@ class BackupViewSet(viewsets.ViewSet):
|
|||
# Add JSON data
|
||||
zip_file.writestr('data.json', json.dumps(export_data, indent=2))
|
||||
|
||||
# Add images and attachments
|
||||
# Add images, attachments, and GPX files
|
||||
files_added = set()
|
||||
|
||||
for location in user.location_set.all():
|
||||
|
@ -268,6 +319,18 @@ class BackupViewSet(viewsets.ViewSet):
|
|||
except Exception as 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
|
||||
with open(tmp_file.name, 'rb') as zip_file:
|
||||
response = HttpResponse(zip_file.read(), content_type='application/zip')
|
||||
|
@ -341,6 +404,8 @@ class BackupViewSet(viewsets.ViewSet):
|
|||
def _clear_user_data(self, user):
|
||||
"""Clear all existing user data before import"""
|
||||
# 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.checklist_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):
|
||||
"""Import backup data and return summary"""
|
||||
from datetime import timedelta
|
||||
|
||||
# Track mappings and counts
|
||||
category_map = {}
|
||||
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 = {
|
||||
'categories': 0, 'collections': 0, 'locations': 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
|
||||
|
@ -468,15 +539,29 @@ class BackupViewSet(viewsets.ViewSet):
|
|||
category=category_map.get(adv_data.get('category_name'))
|
||||
)
|
||||
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()
|
||||
for collection_export_id in adv_data.get('collection_export_ids', []):
|
||||
if collection_export_id in collection_map:
|
||||
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', []):
|
||||
Visit.objects.create(
|
||||
visit = Visit.objects.create(
|
||||
location=location,
|
||||
start_date=visit_data.get('start_date'),
|
||||
end_date=visit_data.get('end_date'),
|
||||
|
@ -484,6 +569,69 @@ class BackupViewSet(viewsets.ViewSet):
|
|||
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
|
||||
content_type = ContentType.objects.get(model='location')
|
||||
|
||||
|
|
|
@ -1141,9 +1141,11 @@
|
|||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each wandererFetchedTrails as trail (trail.id)}
|
||||
<WandererCard {trail} on:link={linkWandererTrail} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
|
@ -1154,7 +1156,7 @@
|
|||
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
class="btn btn-accent btn-sm"
|
||||
on:click={() => {
|
||||
showWandererForm = false;
|
||||
showAddTrailForm = false;
|
||||
|
|
|
@ -544,7 +544,9 @@
|
|||
"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.",
|
||||
"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_created": "Collection created successfully!",
|
||||
|
|
|
@ -1289,6 +1289,14 @@
|
|||
<span>🖼️ {$t('settings.media')}</span>
|
||||
<span>✅</span>
|
||||
</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">
|
||||
<span>🌍 {$t('settings.world_travel_visits')}</span>
|
||||
<span>✅</span>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue