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 (
|
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')
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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!",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue