diff --git a/rowers/tasks.py b/rowers/tasks.py index 4e469477..2e1db4e7 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -357,6 +357,80 @@ def correct_intensity(workout): return workout +import io +import zipfile + +@app.task +def email_all_user_workouts_zip(rower, start_date=None, end_date=None, debug=False, **kwargs): + # Get all workouts for this user, optionally filtered by date range + workouts = Workout.objects.filter(user=rower).order_by('-date') + + # Apply date filters if provided + if start_date: + workouts = workouts.filter(date__gte=start_date) + if end_date: + workouts = workouts.filter(date__lte=end_date) + + # for debug, limit to 5 workouts + if settings.DEBUG: + workouts = workouts[:5] + + if not workouts.exists(): + 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() + + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: + for workout in workouts: + try: + rowdata = rdata(csvfile=workout.csvfilename) + rowdate = rowdata.rowdatetime + starttimeunix = arrow.get(rowdate).timestamp() + df = rowdata.df + + try: + df[' ElapsedTime (sec)'] = df['TimeStamp (sec)'] + df['TimeStamp (sec)'] = df['TimeStamp (sec)'] + starttimeunix + except KeyError: + pass + + csv_filename = f"workout_{workout.id}_{workout.date.strftime('%Y%m%d')}.csv" + zip_file.writestr(csv_filename, df.to_csv(index=False)) + 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" + from_email = 'Rowsandall ' + useremail = rower.user.email + + # Generate download URL + download_url = f"{SITE_URL}/rowers/workouts/download/?file={filename}" + + _ = send_template_email( + from_email, [useremail], + subject, + 'workouts_export_email.html', + {'download_url': download_url, 'filename': filename}, + ) + + return 1 + @app.task def handle_loadnextweek(rower, debug=False, **kwargs): @@ -4522,3 +4596,4 @@ def fetch_strava_workout(stravatoken, oauth_data, stravaid, csvfilename, userid, stravaid=stravaid, userid=userid)) return 1 + diff --git a/rowers/templates/export_workouts_daterange.html b/rowers/templates/export_workouts_daterange.html new file mode 100644 index 00000000..178c22a2 --- /dev/null +++ b/rowers/templates/export_workouts_daterange.html @@ -0,0 +1,24 @@ +{% extends "newbase.html" %} +{% load static %} +{% load rowerfilters %} + +{% block title %}Export Workouts{% endblock %} + +{% block main %} +
+
+
+

Export All Your Workouts

+

Select a date range to export your workouts as a ZIP file containing individual CSV files.

+ +
+ {% csrf_token %} + + {{ form.as_table }} +
+ +
+
+
+
+{% endblock %} diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index ea9f09b1..539199ef 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -7,6 +7,10 @@ {% block main %}

Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}

+

+ Export all workouts +

+
{% csrf_token %}
    diff --git a/rowers/templates/workouts_export_email.html b/rowers/templates/workouts_export_email.html new file mode 100644 index 00000000..42ced93d --- /dev/null +++ b/rowers/templates/workouts_export_email.html @@ -0,0 +1,11 @@ +{% extends "emailbase.html" %} +{% block body %} +

    + You can download the file {{ filename }} from the following link: {{ download_url }}. The file will be deleted after downloading, so please make sure to download it as soon as possible. +

    + + +

    + Best Regards, the Rowsandall Team +

    +{% endblock %} diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index a3ba1af5..badd3b34 100644 Binary files a/rowers/tests/testdata/testdata.tcx.gz and b/rowers/tests/testdata/testdata.tcx.gz differ diff --git a/rowers/urls.py b/rowers/urls.py index 3289b01d..b6e01143 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -432,6 +432,10 @@ urlpatterns = [ views.graphs_view, name='graphs_view'), re_path(r'^createmarkerworkouts/user/(?P\d+)/$', views.create_marker_workouts_view, name='create_marker_workouts_view'), + re_path(r'^workouts/alluserworkouts/$', views.export_all_workouts_zip_view, + name='workout_export_all_workouts_zip_view'), + re_path(r'^workouts/download/$', views.download_zip_file_view, + name='download_zip_file_view'), re_path(r'^createmarkerworkouts/$', views.create_marker_workouts_view, name='create_marker_workouts_view'), re_path(r'^goldmedalscores/$', views.goldmedalscores_view, diff --git a/rowers/views/exportviews.py b/rowers/views/exportviews.py index 0a4a0151..e2aa4a00 100644 --- a/rowers/views/exportviews.py +++ b/rowers/views/exportviews.py @@ -252,6 +252,53 @@ def workout_csvemail_view(request, id=0): return response +import io + +# Export all workouts as ZIP file with individual CSV files +@login_required() +def export_all_workouts_zip_view(request): + from datetime import datetime + r = getrower(request.user) + + if request.method == 'GET': + form = DateRangeForm() + elif request.method == 'POST': + form = DateRangeForm(request.POST) + if not form.is_valid(): + messages.error(request, "Invalid date range. Please try again.") + return render(request, "export_workouts_daterange.html", {'form': form}) + + startdate = form.cleaned_data['startdate'] + enddate = form.cleaned_data['enddate'] + + myqueue(queuehigh, email_all_user_workouts_zip, r, startdate, enddate) + + successmessage = "A download link will be sent to you per email" + messages.info(request, successmessage) + + # 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) + + if not zip_file_path or not os.path.exists(zip_file_path): + messages.error(request, "The requested file does not exist.") + return HttpResponseRedirect(reverse('workouts_view')) + + with open(zip_file_path, 'rb') as f: + response = HttpResponse(f.read(), content_type='application/zip') + response['Content-Disposition'] = f'attachment; filename="{os.path.basename(zip_file_path)}"' + + # remove the file after sending + os.remove(zip_file_path) + + return response + # Get Workout CSV file and send it to user's email address @login_required() @@ -276,3 +323,5 @@ def workout_csvtoadmin_view(request, id=0): # pragma: no cover response = HttpResponseRedirect(url) return response + + diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 84a62fdb..b0e8528e 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -254,6 +254,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_all_user_workouts_zip, handle_intervals_updateworkout, handle_post_workout_api, handle_sendemail_newftp,