Private
Public Access
1
0

export improvements

This commit is contained in:
2026-03-16 14:11:25 +01:00
parent db826f27e9
commit 41b0262681
8 changed files with 189 additions and 68 deletions

View File

@@ -828,6 +828,15 @@ class DateRangeWorkoutTypeForm(DateRangeForm):
typeselectchoices.append((wtype, verbose))
workouttype = forms.ChoiceField(initial='All', choices=typeselectchoices)
# add a radio button to select how in-stroke data be treated
instrokedatachoices = (
('off', 'Do not export in-stroke data'),
('summary', 'Export summary per stroke'),
('downsampled', 'Export downsampled time series (16 points per stroke)'),
('companion', 'Export as companion .instroke.json file with full curve data per stroke'))
instrokedata = forms.ChoiceField(initial='off', choices=instrokedatachoices, label='In-stroke data export')

View File

@@ -360,9 +360,46 @@ def correct_intensity(workout):
import io
import zipfile
@app.task
def email_user_workouts_zip_chunk(rower, workout_ids, filename, instrokedata,
part, total_parts, debug=False, **kwargs):
zip_file_path = os.path.join(settings.MEDIA_ROOT, filename)
with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for workout_id in workout_ids:
try:
workout = Workout.objects.get(id=workout_id)
sport = mytypes.fitmapping.get(workout.workouttype, 'generic')
fit_filename = f"workout_{sport}_{workout.id}_{workout.date.strftime('%Y%m%d')}.fit"
rowdata = rdata(csvfile=workout.csvfilename)
res = rowdata.exporttofit(fit_filename, sport=sport, notes=workout.name,
instroke_export=instrokedata)
zip_file.write(fit_filename, arcname=fit_filename)
os.remove(fit_filename)
if res.get('companion_file'):
companion_filename = res['companion_file']
zip_file.write(companion_filename, arcname=os.path.basename(companion_filename))
os.remove(companion_filename)
except Exception as e:
dologging('export_all_workouts.log',
f"Error exporting workout {workout_id}: {e}")
continue
download_url = f"{SITE_URL}/rowers/workouts/download/?file={filename}"
subject = f"Rowsandall Workouts Export (part {part} of {total_parts})"
send_template_email(
'Rowsandall <info@rowsandall.com>',
[rower.user.email],
subject,
'workouts_export_email.html',
{'download_url': download_url, 'filename': filename,
'part': part, 'total_parts': total_parts},
)
return 1
@app.task
def email_all_user_workouts_zip(rower, start_date, end_date,
workouttype, debug=False, **kwargs):
workouttype, instrokedata, debug=False, **kwargs):
# Get all workouts for this user, optionally filtered by date range
workouts = Workout.objects.filter(user=rower).order_by('-date')
@@ -379,10 +416,11 @@ def email_all_user_workouts_zip(rower, start_date, end_date,
dologging('export_all_workouts.log', f"No workouts found for user {rower.user.id} in date range {start_date} to {end_date}")
return 0
# Create ZIP file in memory
zip_buffer = io.BytesIO()
export_date = datetime.datetime.now().strftime('%Y%m%d')
filename = f"{rower.user.username}_workouts_{export_date}_from_{start_date}_to_{end_date}_{uuid4().hex[:8]}.zip"
zip_file_path = os.path.join(settings.MEDIA_ROOT, filename)
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for workout in workouts:
try:
rowdata = rdata(csvfile=workout.csvfilename)
@@ -390,24 +428,22 @@ def email_all_user_workouts_zip(rower, start_date, end_date,
fit_filename = f"workout_{workouttype}_{workout.id}_{workout.date.strftime('%Y%m%d')}.fit"
# exporttofit creates a file, we need to add it to the zip_file
rowdata.exporttofit(fit_filename, sport=workouttype, notes=workout.name)
res = rowdata.exporttofit(fit_filename, sport=workouttype, notes=workout.name,
instroke_export=instrokedata)
zip_file.write(fit_filename, arcname=fit_filename)
os.remove(fit_filename)
# res is a dict. If res[companion_file] is not None,
# it contains the filename of the companion file that was
# created (e.g. for instroke data) which also needs to be added to the zip
if res.get('companion_file'):
companion_filename = res['companion_file']
zip_file.write(companion_filename, arcname=os.path.basename(companion_filename))
os.remove(companion_filename)
except Exception as e: # pragma: no cover
dologging('export_all_workouts.log', f"Error exporting workout {workout.id}: {str(e)}")
continue
# Save ZIP file to disk
export_date = datetime.datetime.now().strftime('%Y%m%d')
filename = f"{rower.user.username}_workouts_{export_date}_from_{start_date}_to_{end_date}_{uuid4().hex[:8]}.zip"
zip_file_path = os.path.join(settings.MEDIA_ROOT, filename)
try:
with open(zip_file_path, 'wb') as f:
f.write(zip_buffer.getvalue())
except Exception as e: # pragma: no cover
dologging('export_all_workouts.log', f"Error saving ZIP file: {str(e)}")
return 0
# Send email with download link
subject = "Rowsandall Workouts Export"

View File

@@ -9,7 +9,7 @@
<div class="row">
<div class="col-md-8 offset-md-2">
<h2>Export All Your Workouts</h2>
<p>Select a date range to export your workouts as a ZIP file containing individual CSV files.</p>
<p>Select a date range to export your workouts as a ZIP file containing individual CSV files. Please be considerate and download only the workout types you need, and do downloads in batches (e.g. one year at a time).</p>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}

Binary file not shown.

View File

@@ -416,7 +416,6 @@ def myqueue(queue, function, *args, **kwargs):
job_id = str(uuid.uuid4())
kwargs['job_id'] = job_id
kwargs['jobkey'] = job_id
kwargs['timeout'] = 3600
dologging('queue.log',function.__name__)

View File

@@ -280,7 +280,6 @@ import io
def export_all_workouts_zip_view(request):
from datetime import datetime
r = getrower(request.user)
if request.method == 'GET':
form = DateRangeWorkoutTypeForm()
elif request.method == 'POST':
@@ -292,19 +291,39 @@ def export_all_workouts_zip_view(request):
startdate = form.cleaned_data['startdate']
enddate = form.cleaned_data['enddate']
workouttype = form.cleaned_data['workouttype']
instrokedata = form.cleaned_data['instrokedata']
myqueue(queuehigh, email_all_user_workouts_zip, r, startdate, enddate, workouttype)
workouts = (Workout.objects.filter(user=r)
.order_by('-date')
.filter(date__gte=startdate, date__lte=enddate))
if workouttype != 'All':
workouts = workouts.filter(workouttype=workouttype)
successmessage = "A download link will be sent to you per email"
messages.info(request, successmessage)
workout_ids = list(workouts.values_list('id', flat=True))
if not workout_ids:
messages.warning(request, "No workouts found for the selected date range.")
return render(request, "export_workouts_daterange.html", {'form': form})
chunks = [workout_ids[i:i+100] for i in range(0, len(workout_ids), 100)]
total_parts = len(chunks)
export_date = datetime.now().strftime('%Y%m%d')
for i, chunk in enumerate(chunks, start=1):
filename = (f"{r.user.username}_workouts_{export_date}"
f"_part{i}of{total_parts}_{uuid4().hex[:8]}.zip")
myqueue(queuelow, email_user_workouts_zip_chunk,
r, chunk, filename, instrokedata, i, total_parts,
job_timeout=600)
messages.info(request, "A download link (or multiple download links) will be "
"sent to you by email. This may take up to one hour.")
# return to export settings view
return render(request, "export_workouts_daterange.html", {'form': form})
def download_zip_file_view(request):
# This view would be called when the user clicks the download link in the email
zip_file_path = request.GET.get('file')
print("Requested ZIP file path:", zip_file_path) # Debugging statement
# add media folder
zip_file_path = os.path.join(settings.MEDIA_ROOT, zip_file_path)

View File

@@ -255,6 +255,7 @@ from rowers.rows import handle_uploaded_file, handle_uploaded_image
from rowers.plannedsessions import *
from rowers.tasks import handle_makeplot, handle_otwsetpower, handle_sendemailtcx, handle_sendemailcsv
from rowers.tasks import (
email_user_workouts_zip_chunk,
email_all_user_workouts_zip,
handle_intervals_updateworkout,
handle_post_workout_api,