Private
Public Access
1
0

Merge branch 'release/v23.8.1'

This commit is contained in:
2025-12-11 17:02:26 +01:00
9 changed files with 6 additions and 316 deletions

View File

@@ -348,7 +348,6 @@ def step_to_garmin(step, order=0):
def ps_to_garmin(ps, r):
payload = {
'workoutName': ps.name,
'sport': r.garminactivity,
'description': ps_dict_get_description(ps.steps),
'estimatedDurationInSecs': 60*ps.approximate_duration,
'estimatedDistanceInMeters': ps.approximate_distance,

View File

@@ -27,9 +27,6 @@
{% if rower.polartoken is not None and rower.polartoken != '' %}
Polar,
{% endif %}
{% if rower.garmintoken is not None and rower.garmintoken != '' %}
Garmin Connect,
{% endif %}
{% if rower.stravatoken is not None and rower.stravatoken != '' %}
Strava,
{% endif %}
@@ -136,30 +133,6 @@
<p><a href="/rowers/me/polarauthorize/"><img src="/static/img/Polar_connectwith_btn_white.png"
alt="connect with Polar" width="130"></a></p>
</li>
<li class="rounder">
<h2>Garmin Connect</h2>
<table>
{{ forms.garmin.as_table }}
<input type="submit" value="Save">
</table>
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
alt="connect with Garmin" width="130"></a></p>
<p>
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.
</p>
{% if rower.garmintoken and rower.garmintoken != '' %}
<p>
<em>You are connected to Garmin.</em> Switching off Garmin Connect sync is on the
<a href="https://connect.garmin.com/modern/settings/accountInformation">Account settings</a>
page. Look for the "Rowsandall" app.
</p>
{% endif %}
</li>
<li class="rounder">
<h2>Strava</h2>
<p><em>Warning: API restrictions!</em></p>

View File

@@ -23,7 +23,7 @@ import rowers.rojabo_stuff as rojabo_stuff
from rowers.integrations import *
from django.db import transaction
import rowers.garmin_stuff as gs
import rowers.integrations.strava as strava
from rowers.nkimportutils import *
@@ -193,179 +193,6 @@ class RojaboObjects(DjangoTestCase):
@pytest.mark.django_db
@override_settings(TESTING=True)
class GarminObjects(DjangoTestCase):
def setUp(self):
self.c = Client()
self.u = User.objects.create_user('john',
'sander@ds.ds',
'koeinsloot')
self.u.first_name = 'John'
self.u.last_name = 'Sander'
self.u.save()
self.r = Rower.objects.create(user=self.u,gdproptin=True, ftpset=True,surveydone=True,
gdproptindate=timezone.now()
)
self.r.garmintoken = 'dfdzf'
self.r.garminrefreshtoken = 'fsls'
self.r.rowerplan = 'pro'
self.r.save()
self.c.login(username='john',password='koeinsloot')
self.nu = datetime.datetime.now()
startdate = nu.date()
enddate = (nu+datetime.timedelta(days=3)).date()
preferreddate = startdate
self.ps_trimp = SessionFactory(
startdate=startdate,enddate=enddate,
sessiontype='test',
sessionmode = 'TRIMP',
criterium = 'none',
sessionvalue = 77,
sessionunit='none',
preferreddate=preferreddate,
manager=self.u,
)
self.ps_trimp.interval_string = '10min+4x1000m@200W/20sec+2000m@24spm+10min'
self.ps_trimp.save()
def tearDown(self):
ws = Workout.objects.filter(user=self.r)
for w in ws:
w.delete()
def test_garmin_push_summaries(self):
with open('rowers/tests/testdata/garminsummarydata.txt','r') as f:
data = json.load(f)
response = self.c.post('/rowers/garmin/summaries/',json.dumps(data),
content_type="application/json")
self.assertEqual(response.status_code, 200)
#response = self.c.get('/rowers/workout/'+encoded1+'/', follow=True)
#self.assertEqual(response.status_code, 200)
ws = Workout.objects.filter(user=self.r)
self.assertEqual(ws.count(),3)
def test_garmin_push_details3(self):
with open('rowers/tests/testdata/garmindetail3.txt','r') as f:
data = json.load(f)
response = self.c.post('/rowers/garmin/activities/',json.dumps(data),
content_type='application/json')
self.assertEqual(response.status_code, 200)
ws = Workout.objects.filter(user=self.r)
self.assertEqual(ws.count(),1)
data,w = dataprep.getrowdata_db(id=ws[0].id)
self.assertEqual(len(data),515)
def test_garmin_push_details4(self):
with open('rowers/tests/testdata/garmindetail4.txt','r') as f:
data = json.load(f)
response = self.c.post('/rowers/garmin/activities/',json.dumps(data),
content_type='application/json')
self.assertEqual(response.status_code, 200)
ws = Workout.objects.filter(user=self.r)
self.assertEqual(ws.count(),1)
data,w = dataprep.getrowdata_db(id=ws[0].id)
self.assertEqual(len(data),18)
def test_garmin_push_details2(self):
with open('rowers/tests/testdata/garmindetail2.txt','r') as f:
data = json.load(f)
response = self.c.post('/rowers/garmin/activities/',json.dumps(data),
content_type='application/json')
self.assertEqual(response.status_code, 200)
ws = Workout.objects.filter(user=self.r)
self.assertEqual(ws.count(),3)
data,w = dataprep.getrowdata_db(id=ws[0].id)
self.assertEqual(len(data),451)
def test_garmin_push_details1(self):
with open('rowers/tests/testdata/garmindetail1.txt','r') as f:
data = json.load(f)
response = self.c.post('/rowers/garmin/activities/',json.dumps(data),
content_type='application/json')
self.assertEqual(response.status_code, 200)
response = self.c.get('/rowers/workout/'+encoded1+'/', follow=True)
self.assertEqual(response.status_code, 200)
ws = Workout.objects.filter(user=self.r)
self.assertEqual(ws.count(),2)
data,w = dataprep.getrowdata_db(id=ws[0].id)
self.assertEqual(len(data),2)
def test_garmin_deregistration(self):
data = {"deregistrations":[{"userAccessToken":"dfdzf"}]}
response = self.c.post('/rowers/garmin/deregistration/',json.dumps(data),
content_type='application/json')
self.assertEqual(response.status_code,200)
@patch('rowers.tasks.OAuth1Session',side_effect=mocked_requests)
@patch('rowers.tasks.requests.session', side_effect=mocked_requests)
def test_handle_get_garmin_file(self, MockSession, MockOAuth1Session):
client_id = 'garmin'
client_secret = 'noot'
garmintoken = 'mies'
garminrefreshtoken = 'jet'
userid = self.r.user.id
url = 'fake_url'
filetype = 'fit'
res = tasks.handle_get_garmin_file(
client_id,client_secret,garmintoken,garminrefreshtoken,userid,url,filetype
)
self.assertEqual(res,1)
@patch('rowers.garmin_stuff.OAuth1Session')
def notest_garmin_callback(self,MockOAuth1Session):
with transaction.atomic():
response = self.c.get('/garmin_callback/?oauth_token=528ea5d9-1163-434d-b172-f428c5d9f522&oauth_verifier=LW33ZMBP8H')
self.assertEqual(response.status_code, 200)
@patch('rowers.garmin_stuff.requests.get',side_effect=mocked_requests)
def test_garmin_can_export_session(self,mock_get):
result = gs.garmin_can_export_session(self.u)
self.assertTrue(result)
def test_ps_to_garmin(self):
res = gs.ps_to_garmin(self.ps_trimp,self.r)
self.assertTrue(len(json.dumps(res))>500)
@patch('rowers.garmin_stuff.requests.get',side_effect=mocked_requests)
@patch('rowers.garmin_stuff.requests.post',side_effect=mocked_requests)
def test_garmin_session_create(self,mock_get,mock_post):
res = gs.garmin_session_create(self.ps_trimp,self.u)
self.assertEqual(res,1212)
@patch('rowers.garmin_stuff.requests.get',side_effect=mocked_requests)
@patch('rowers.garmin_stuff.requests.post',side_effect=mocked_requests)
def test_togarmin_view(self,mock_get,mock_post):
url = reverse('plannedsession_togarmin_view',kwargs={'id':self.ps_trimp.id})
response = self.c.get(url,follow=True)
self.assertEqual(response.status_code,200)
@pytest.mark.django_db

View File

@@ -2134,8 +2134,6 @@ description: ""
stepsdict = self.ps_trimp.steps['steps']
self.assertEqual(len(stepsdict),2)
response = garmin_stuff.ps_to_garmin(self.ps_trimp,self.r)
self.assertTrue(len(json.dumps(response))>800)
url = '0'
request = self.factory.get(url)

Binary file not shown.

View File

@@ -763,8 +763,8 @@ urlpatterns = [
name='rower_polar_authorize'),
re_path(r'^me/revokeapp/(?P<id>\d+)/$',
views.rower_revokeapp_view, name='rower_revokeapp_view'),
re_path(r'^me/garminauthorize/$', views.rower_garmin_authorize,
name='rower_garmin_authorize'),
# re_path(r'^me/garminauthorize/$', views.rower_garmin_authorize,
# name='rower_garmin_authorize'),
re_path(r'^me/edit/(.+.*)/$', views.rower_edit_view, name='rower_edit_view'),
re_path(r'^me/(?P<source>\w+.*)authorize', views.rower_integration_authorize,
name='rower_integration_authorize'),
@@ -1002,8 +1002,8 @@ urlpatterns = [
name='plannedsession_templateedit_view'),
re_path(r'^sessions/(?P<id>\d+)/maketemplate/$', views.plannedsession_totemplate_view,
name='plannedsession_totemplate_view'),
re_path(r'^sessions/(?P<id>\d+)/togarmin/$', views.plannedsession_togarmin_view,
name='plannedsession_togarmin_view'),
# re_path(r'^sessions/(?P<id>\d+)/togarmin/$', views.plannedsession_togarmin_view,
# name='plannedsession_togarmin_view'),
re_path(r'^sessions/(?P<id>\d+)/tointervals/$', views.plannedsession_tointervals_view,
name='plannedsession_tointervals_view'),
re_path(r'^sessions/(?P<id>\d+)/compare/$',

View File

@@ -26,7 +26,6 @@ importauthorizeviews = {
'trainingpeaks': 'rower_integration_authorize',
'nk': 'rower_integration_authorize',
'rp3': 'rower_integration_authorize',
'garmin': 'rower_garmin_authorize',
'intervals': 'rower_integration_authorize',
}
@@ -85,8 +84,6 @@ def rower_integration_authorize(request, source='c2'): # pragma: no cover
try:
integration = importsources[source](request.user)
except KeyError:
if source == 'garmin':
return rower_garmin_authorize(request)
if source == 'rojabo':
return rower_rojabo_authorize(request)
if source == 'polar':
@@ -96,12 +93,6 @@ def rower_integration_authorize(request, source='c2'): # pragma: no cover
@login_required()
def rower_garmin_authorize(request): # pragma: no cover
authorization_url, token, secret = garmin_stuff.garmin_authorize()
request.session['garmin_owner_key'] = token
request.session['garmin_owner_secret'] = secret
return HttpResponseRedirect(authorization_url)
# Polar Authorization
@@ -288,27 +279,6 @@ def rower_process_polarcallback(request):
return HttpResponseRedirect(url)
# process Garmin callback
@login_required()
def rower_process_garmincallback(request): # pragma: no cover
r = getrower(request.user)
absoluteurl = request.build_absolute_uri()
try:
key = request.session['garmin_owner_key']
secret = request.session['garmin_owner_secret']
except KeyError:
authorization_url, key, secret = garmin_stuff.garmin_authorize()
garmintoken, garminrefreshtoken = garmin_stuff.garmin_processcallback(
absoluteurl, key, secret)
r.garmintoken = garmintoken
r.garminrefreshtoken = garminrefreshtoken
r.save()
successmessage = "Tokens stored. Good to go. Please check your import/export settings"
messages.info(request, successmessage)
url = reverse('rower_exportsettings_view')
return HttpResponseRedirect(url)
# Process Rojabo callback
@login_required()
@@ -1108,81 +1078,6 @@ def strava_webhook_view(request):
# For push notifications from Garmin
@csrf_exempt
def garmin_summaries_view(request): # pragma: no cover
if request.method != 'POST':
return HttpResponse(status=200)
t = time.localtime()
timestamp = time.strftime('%b-%d-%Y_%H%M', t)
dologging("garminlog.log",request.body)
# POST request
data = json.loads(request.body)
activities = data['activities']
result = garmin_stuff.garmin_workouts_from_summaries(activities)
if result:
return HttpResponse(status=200)
return HttpResponse(status=200)
@csrf_exempt
def garmin_newfiles_ping(request): # pragma: no cover
if request.method != 'POST':
return HttpResponse(status=200)
data = json.loads(request.body)
for file in data['activityFiles']:
try:
garmintoken = file['userAccessToken']
try:
r = Rower.objects.get(garmintoken=garmintoken)
callbackURL = file['callbackURL']
starttime = file['startTimeInSeconds']
fileType = file['fileType']
_ = garmin_stuff.get_garmin_file(r, callbackURL, starttime, fileType)
except Rower.DoesNotExist:
pass
except KeyError:
pass
return HttpResponse(status=200) # pragma: no cover
@csrf_exempt
def garmin_deregistration_view(request):
if request.method != 'POST': # pragma: no cover
return HttpResponse(status=200)
data = json.loads(request.body)
deregistrations = data['deregistrations']
for deregistration in deregistrations:
try:
garmintoken = deregistration['userAccessToken']
try:
r = Rower.objects.get(garmintoken=garmintoken)
r.garmintoken = ''
r.save()
except Rower.DoesNotExist: # pragma: no cover
pass
except KeyError: # pragma: no cover
pass
return HttpResponse(status=200)
@csrf_exempt
def garmin_details_view(request):
if request.method != 'POST': # pragma: no cover
return HttpResponse(status=200)
# POST request
data = json.loads(request.body)
_ = garmin_stuff.garmin_workouts_from_details(data)
return HttpResponse(status=200)
@login_required()

View File

@@ -490,7 +490,6 @@ def rower_exportsettings_view(request, userid=0):
'rp3': RowerExportFormRP3(instance=r),
'intervals': RowerExportFormIntervals(instance=r),
'nk': RowerExportFormNK(instance=r),
'garmin': RowerExportFormGarmin(instance=r),
'imports_are_private': RowerPrivateImportForm(instance=r)
}
@@ -505,7 +504,6 @@ def rower_exportsettings_view(request, userid=0):
'rp3': RowerExportFormRP3(request.POST, instance=r),
'intervals': RowerExportFormIntervals(request.POST, instance=r),
'nk': RowerExportFormNK(request.POST, instance=r),
'garmin': RowerExportFormGarmin(request.POST, instance=r),
'imports_are_private': RowerPrivateImportForm(request.POST, instance=r),
}
if form.is_valid():

View File

@@ -87,7 +87,7 @@ urlpatterns += [
re_path(r'^nk\_callback', rowersviews.rower_process_nkcallback),
re_path(r'^rojabo\_callback', rowersviews.rower_process_rojabocallback),
re_path(r'^stravacall\_back', rowersviews.rower_process_stravacallback),
re_path(r'^garmin\_callback', rowersviews.rower_process_garmincallback),
# re_path(r'^garmin\_callback', rowersviews.rower_process_garmincallback),
re_path(r'^sporttracks\_callback', rowersviews.rower_process_sporttrackscallback),
re_path(r'^polarflowcallback', rowersviews.rower_process_polarcallback),
re_path(r'^tp\_callback', rowersviews.rower_process_tpcallback),