diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 827a9717..28bf84be 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -369,7 +369,7 @@ def workout_summary_to_df( return df -def resample(id, r, parent, overwrite='copy'): +def resample(id, r, parent, overwrite=False): data, row = getrowdata_db(id=id) messages = [] @@ -393,7 +393,7 @@ def resample(id, r, parent, overwrite='copy'): data['pace'] = data['pace'] / 1000. data['time'] = data['time'] / 1000. - if overwrite == 'overwrite': + if overwrite == True: # remove CP data try: cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=parent.id) diff --git a/rowers/forms.py b/rowers/forms.py index 0ebc5fb3..506f900f 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -67,13 +67,12 @@ class FlexibleDecimalField(forms.DecimalField): class ResampleForm(forms.Form): - resamplechoices = ( - ('overwrite', 'Overwrite Workout'), - ('copy', 'Create a Duplicate Workout') - ) - + # add resamplechoice field, the result is a True or False boolean, labels are "overwrite" and "create copy" resamplechoice = forms.ChoiceField( - initial='copy', choices=resamplechoices, label='Copy behavior') + required=True, + choices=((True, 'overwrite'), (False, 'create copy')), + label='Resample choice', + widget=forms.RadioSelect) class TrainingZonesForm(forms.Form): @@ -582,6 +581,11 @@ class UploadOptionsForm(forms.Form): races = VirtualRace.objects.filter( registration_closure__gt=timezone.now()) + # set upload_to_X based on r.X_auto_export + for field in ['C2', 'Strava', 'SportTracks', 'TrainingPeaks', 'Intervals']: + if getattr(r, field.lower()+'_auto_export') and r.rowerplan in ['pro', 'plan','coach']: + self.fields['upload_to_'+field].initial = True + registrations = IndoorVirtualRaceResult.objects.filter( race__in=races, userid=r.id) @@ -665,6 +669,10 @@ class TeamUploadOptionsForm(forms.Form): upload_to_TrainingPeaks = forms.BooleanField(initial=False, required=False, label='Export to TrainingPeaks') + + upload_to_Intervals = forms.BooleanField(initial=False, + required=False, + label='Export to TrainingPeaks') # do_physics = forms.BooleanField(initial=False,required=False,label='Power Estimate (OTW)') makeprivate = forms.BooleanField(initial=False, required=False, label='Make Workout Private') diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py index 4e0ac680..7db883ae 100644 --- a/rowers/integrations/intervals.py +++ b/rowers/integrations/intervals.py @@ -17,6 +17,7 @@ import os from uuid import uuid4 from django.utils import timezone from datetime import timedelta +import rowers.dataprep as dataprep from rowsandall_app.settings import ( INTERVALS_CLIENT_ID, INTERVALS_REDIRECT_URI, INTERVALS_CLIENT_SECRET, SITE_URL @@ -101,7 +102,16 @@ class IntervalsIntegration(SyncIntegration): def createworkoutdata(self, w, *args, **kwargs) -> str: dozip = kwargs.get('dozip', True) - filename = w.csvfilename + # resample if wanted by user, not tested + if w.user.intervals_resample_to_1s: + datadf, id, msgs = dataprep.resample( + w.id, w.user, w, overwrite=False + ) + w_resampled = Workout.objects.get(id=id) + filename = w_resampled.csvfilename + else: + w_resampled = None + filename = w.csvfilename try: row = rowingdata(csvfile=filename) except IOError: # pragma: no cover @@ -128,6 +138,8 @@ class IntervalsIntegration(SyncIntegration): except TypeError: newnotes = 'from'+w.workoutsource+' via rowsandall.com' + if w.user.intervals_resample_to_1s and w_resampled: + w_resampled.delete() row.exporttotcx(tcxfilename, notes=newnotes, sport=mytypes.intervalsmapping[w.workouttype]) if dozip: gzfilename = tcxfilename + '.gz' diff --git a/rowers/models.py b/rowers/models.py index f4715081..047d8d0a 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1174,6 +1174,7 @@ class Rower(models.Model): c2_auto_import = models.BooleanField(default=False) intervals_auto_export = models.BooleanField(default=False) intervals_auto_import = models.BooleanField(default=False) + intervals_resample_to_1s = models.BooleanField(default=False, verbose_name='Resample to 1s on export') sporttrackstoken = models.CharField( default='', max_length=200, blank=True, null=True) sporttrackstokenexpirydate = models.DateTimeField(blank=True, null=True) @@ -4560,7 +4561,78 @@ class RowerExportForm(ModelForm): 'rp3_auto_import', 'intervals_auto_import', 'intervals_auto_export', + 'intervals_resample_to_1s' ] + +class RowerExportFormStrava(ModelForm): + class Meta: + model = Rower + fields = [ + 'stravaexportas', + 'strava_auto_export', + 'strava_auto_import', + 'strava_auto_delete', + ] + +class RowerExportFormIntervals(ModelForm): + class Meta: + model = Rower + fields = [ + 'intervals_auto_import', + 'intervals_auto_export', + 'intervals_resample_to_1s', + ] + +class RowerExportFormGarmin(ModelForm): + class Meta: + model = Rower + fields = [ + 'garminactivity', + ] + +class RowerExportFormPolar(ModelForm): + class Meta: + model = Rower + fields = [ + 'polar_auto_import', + ] + +class RowerExportFormConcept2(ModelForm): + class Meta: + model = Rower + fields = [ + 'c2_auto_export', + 'c2_auto_import', + ] + +class RowerExportFormSportTracks(ModelForm): + class Meta: + model = Rower + fields = [ + 'sporttracks_auto_export', + ] + +class RowerExportFormTrainingPeaks(ModelForm): + class Meta: + model = Rower + fields = [ + 'trainingpeaks_auto_export', + ] + +class RowerExportFormRP3(ModelForm): + class Meta: + model = Rower + fields = [ + 'rp3_auto_import', + ] + +class RowerExportFormNK(ModelForm): + class Meta: + model = Rower + fields = [ + 'nk_auto_import' + ] + # Simple form to set rower's Functional Threshold Power class SimpleRowerPowerForm(ModelForm): diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index b6aae035..13418d15 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -7,8 +7,10 @@ {% block main %}

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

+
+ {% csrf_token %}
    -
  • +
  • You are currently connected to: {% if rower.c2token is not None and rower.c2token != '' %} Concept2 Logbook, @@ -41,44 +43,133 @@ Intervals.icu {% endif %}

    - -{% if form.errors %} -

    - Please correct the error{{ form.errors|pluralize }} below. -

    -{% endif %} -

    - - - {{ form.as_table }} -
    - {% csrf_token %} - -

  • -

    -{% if rower.garmintoken and rower.garmintoken != '' %} -

    - You are connected to Garmin. Switching off Garmin Connect sync is on the - Account settings - page. Look for the "Rowsandall" app. -

    -{% endif %} -

    - Garmin Connnect has no manual sync, so connecting your account to your Garmin account will - automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you - want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity" - to a activity type that is supported by your watch. Not all watches support "Custom" activities, so - you may have to set your activity to Run or Ride while rowing. -

    -

    - Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete - a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete - check box. -

    +

    + Click on the icons to establish the connection or to renew the authorization. +

    + +
  • +

    API Key

    +

    {{ apikey }}

    +

    + Regenerate +

    +

    This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.

    +
  • + + {% if form.errors %} +
  • +

    + Please correct the error{{ form.errors|pluralize }} below. +

    +
  • + {% endif %} +
  • +

    NK

    + + {{ forms.nk.as_table }} + +
    +

    connect with NK Logbook

    +
  • +
  • +

    Concept2

    + + {{ forms.c2.as_table }} + +
    +

    connect with Concept2

    +
  • +
  • +

    RP3

    + + {{ forms.rp3.as_table }} + +
    +

    connect with RP3

    +
  • +
  • +

    Rojabo

    +

    connect with Rojabo

    +
  • +
  • +

    Intervals.icu

    + + {{ forms.intervals.as_table }} + +
    +

    connect with intervals.icu

    +
  • +
  • +

    SportTracks

    + + {{ forms.sporttracks.as_table }} + +
    +

    connect with SportTracks

    +
  • +
  • +

    TrainingPeaks

    + + {{ forms.trainingpeaks.as_table }} + +
    +

    connect with Polar

    +
  • +
  • +

    Polar

    + + {{ forms.polar.as_table }} + +
    +

    connect with Polar

    +
  • +
  • +

    Garmin Connect

    + + {{ forms.garmin.as_table }} + +
    +

    connect with Garmin

    + +

    + Garmin Connnect has no manual sync, so connecting your account to your Garmin account will + automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you + want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity" + to a activity type that is supported by your watch. Not all watches support "Custom" activities, so + you may have to set your activity to Run or Ride while rowing. +

    + {% if rower.garmintoken and rower.garmintoken != '' %} +

    + You are connected to Garmin. Switching off Garmin Connect sync is on the + Account settings + page. Look for the "Rowsandall" app. +

    + {% endif %} +
  • +
  • +

    Strava

    +

    + {{ forms.strava.as_p }} +

    connect with strava

    +

    + Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete + a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete + check box. +

    +
  • -
  • {% if grants %} +
  • Applications

    +

    + These applications have access to your Rowsandall data. +

    @@ -99,35 +190,12 @@ {% endfor %}
    +
  • {% endif %} -

    API Key

    -

    {{ apikey }}

    -

    - Regenerate -

    - This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret. -
-

Click on one of the icons below to connect to the service of your - choice or to renew the authorization.

-

connect with strava

-

connect with Concept2

-

connect with NK Logbook

-

connect with SportTracks

-

connect with Polar

-

connect with Polar

- -

connect with Garmin

-

connect with RP3

-

connect with Rojabo

-

connect with intervals.icu

+ + {% endblock %} diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index f15cf389..dd7306f8 100644 Binary files a/rowers/tests/testdata/testdata.tcx.gz and b/rowers/tests/testdata/testdata.tcx.gz differ diff --git a/rowers/uploads.py b/rowers/uploads.py index 71f07553..fae902d5 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -245,23 +245,6 @@ def do_sync(w, options, quick=False): dologging('c2_log.log','Error C2') pass - if do_strava_export: # pragma: no cover - strava_integration = StravaIntegration(w.user.user) - try: - id = strava_integration.workout_export(w) - dologging( - 'strava_export_log.log', - 'exporting workout {id} as {type}'.format( - id=w.id, - type=w.workouttype, - ) - ) - except NoTokenError: # pragma: no cover - id = 0 - message = "Please connect to Strava first" - except Exception as e: - dologging('stravalog.log', e) - if do_icu_export: intervals_integration = IntervalsIntegration(w.user.user) try: @@ -334,4 +317,23 @@ def do_sync(w, options, quick=False): dologging('tp_export.log','No Token Error') return 0 + # we do Strava last. + if do_strava_export: # pragma: no cover + strava_integration = StravaIntegration(w.user.user) + try: + id = strava_integration.workout_export(w) + dologging( + 'strava_export_log.log', + 'exporting workout {id} as {type}'.format( + id=w.id, + type=w.workouttype, + ) + ) + except NoTokenError: # pragma: no cover + id = 0 + message = "Please connect to Strava first" + except Exception as e: + dologging('stravalog.log', e) + + return 1 diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 01771ce6..09ac2fa1 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -179,7 +179,13 @@ from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerFo IndoorVirtualRaceForm, PlannedSessionCommentForm, Alert, Condition, StaticChartRowerForm, FollowerForm, VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm, - StepEditorForm, iDokladToken ) + StepEditorForm, iDokladToken, + RowerExportFormStrava, RowerExportFormPolar, + RowerExportFormSportTracks, RowerExportFormTrainingPeaks, + RowerExportFormConcept2, RowerExportFormGarmin, + RowerExportFormIntervals, RowerExportFormRP3, + RowerExportFormNK, + ) from rowers.models import ( FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet, get_course_timezone, BaseConditionFormSet, diff --git a/rowers/views/userviews.py b/rowers/views/userviews.py index ddd4fe73..cf004032 100644 --- a/rowers/views/userviews.py +++ b/rowers/views/userviews.py @@ -457,19 +457,43 @@ def rower_exportsettings_view(request, userid=0): 'polar_auto_import': 'polartoken', 'c2_auto_export': 'c2token', 'c2_auto_import': 'c2token', - 'runkeeper_auto_export': 'runkeepertoken', 'sporttracks_auto_export': 'sporttrackstoken', 'strava_auto_export': 'stravatoken', 'strava_auto_import': 'stravatoken', 'strava_auto_delete': 'stravatoken', 'trainingpeaks_auto_export': 'tptoken', 'rp3_auto_import': 'rp3token', - 'nk_auto_import': 'nktoken' + 'nk_auto_import': 'nktoken', + 'intervals_auto_export': 'intervals_token', + 'intervals_resample_to_1s': 'intervals_token', } r = getrequestrowercoachee(request, userid=userid) + + forms = { + 'polar': RowerExportFormPolar(instance=r), + 'c2': RowerExportFormConcept2(instance=r), + 'sporttracks': RowerExportFormSportTracks(instance=r), + 'strava': RowerExportFormStrava(instance=r), + 'trainingpeaks': RowerExportFormTrainingPeaks(instance=r), + 'rp3': RowerExportFormRP3(instance=r), + 'intervals': RowerExportFormIntervals(instance=r), + 'nk': RowerExportFormNK(instance=r), + 'garmin': RowerExportFormGarmin(instance=r), + } if request.method == 'POST': form = RowerExportForm(request.POST) + forms = { + 'polar': RowerExportFormPolar(request.POST, instance=r), + 'c2': RowerExportFormConcept2(request.POST, instance=r), + 'sporttracks': RowerExportFormSportTracks(request.POST, instance=r), + 'strava': RowerExportFormStrava(request.POST, instance=r), + 'trainingpeaks': RowerExportFormTrainingPeaks(request.POST, instance=r), + 'rp3': RowerExportFormRP3(request.POST, instance=r), + 'intervals': RowerExportFormIntervals(request.POST, instance=r), + 'nk': RowerExportFormNK(request.POST, instance=r), + 'garmin': RowerExportFormGarmin(request.POST, instance=r), + } if form.is_valid(): cd = form.cleaned_data if r.rowerplan == 'basic': # pragma: no cover @@ -528,6 +552,7 @@ def rower_exportsettings_view(request, userid=0): return render(request, 'rower_exportsettings.html', {'form': form, + 'forms': forms, 'rower': r, 'breadcrumbs': breadcrumbs, 'grants': grants, diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 14112726..0412c7ff 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -5609,17 +5609,6 @@ def workout_upload_view(request, return response else: if not is_ajax: - if r.c2_auto_export and ispromember(r.user): # pragma: no cover - uploadoptions['upload_to_C2'] = True - - if r.strava_auto_export and ispromember(r.user): # pragma: no cover - uploadoptions['upload_to_Strava'] = True - - if r.sporttracks_auto_export and ispromember(r.user): # pragma: no cover - uploadoptions['upload_to_SportTracks'] = True - - if r.trainingpeaks_auto_export and ispromember(r.user): # pragma: no cover - uploadoptions['upload_to_TrainingPeaks'] = True form = DocumentsForm(initial=docformoptions) optionsform = UploadOptionsForm(initial=uploadoptions, diff --git a/static/img/intervals_logo_with_name.png b/static/img/intervals_logo_with_name.png new file mode 100644 index 00000000..b448dd4e Binary files /dev/null and b/static/img/intervals_logo_with_name.png differ