export improvements
This commit is contained in:
@@ -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')
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
BIN
rowers/tests/testdata/testdata.tcx.gz
vendored
BIN
rowers/tests/testdata/testdata.tcx.gz
vendored
Binary file not shown.
@@ -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__)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user