diff --git a/rowers/admin.py b/rowers/admin.py index f89d9710..8318dec7 100644 --- a/rowers/admin.py +++ b/rowers/admin.py @@ -49,7 +49,9 @@ class RowerInline(admin.StackedInline): 'stravatoken', 'stravatokenexpirydate', 'stravarefreshtoken', 'stravaexportas', 'strava_auto_export', 'strava_auto_import', - 'garmintoken', 'garminrefreshtoken')}), + 'garmintoken', 'garminrefreshtoken', + 'nktoken','nkrefreshtoken','nktokenexpirydate', + 'rojabo_token','rojabo_refreshtoken','rojabo_tokenexpirydate')}), ('Team', {'fields': ('friends', 'privacy', 'team')}), ) diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py index bddc3bbe..c0d53ec2 100644 --- a/rowers/c2stuff.py +++ b/rowers/c2stuff.py @@ -845,16 +845,16 @@ def get_c2_workout_list(user, page=1): s = "Token expired. Needs to refresh." return custom_exception_handler(401, s) - else: - # ready to fetch. Hurray - authorizationstring = str('Bearer ' + r.c2token) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - url = "https://log.concept2.com/api/users/me/results" - url += "?page={page}".format(page=page) - s = requests.get(url, headers=headers) + # ready to fetch. Hurray + authorizationstring = str('Bearer ' + r.c2token) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://log.concept2.com/api/users/me/results" + url += "?page={page}".format(page=page) + + s = requests.get(url, headers=headers) return s diff --git a/rowers/models.py b/rowers/models.py index e1996516..8225aa16 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1048,6 +1048,12 @@ class Rower(models.Model): rp3_auto_import = models.BooleanField(default=False) + rojabo_token = models.CharField( + default='', max_length=200, blank=True, null=True) + rojabo_refreshtoken = models.CharField( + default='', max_length=200, blank=True, null=True) + rojabo_tokenexpirydate = models.DateTimeField(blank=True, null=True) + nktoken = models.TextField( default='', max_length=1000, blank=True, null=True) nktokenexpirydate = models.DateTimeField(blank=True, null=True) @@ -2568,6 +2574,7 @@ class PlannedSession(models.Model): ) manager = models.ForeignKey(User, on_delete=models.PROTECT) + rojabo_id = models.BigIntegerField(default=0,blank=True) course = models.ForeignKey(GeoCourse, blank=True, null=True, verbose_name='OTW Course', on_delete=models.SET_NULL) diff --git a/rowers/rojabo_stuff.py b/rowers/rojabo_stuff.py new file mode 100644 index 00000000..af3029b6 --- /dev/null +++ b/rowers/rojabo_stuff.py @@ -0,0 +1,197 @@ +from rowers.models import Rower, Workout, TombStone +from rowers import utils +import datetime + +from rowers.imports import * +from rowsandall_app.settings import ( + ROJABO_CLIENT_ID, ROJABO_REDIRECT_URI, ROJABO_CLIENT_SECRET, + SITE_URL, ROJABO_OAUTH_LOCATION, + UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET, +) +import gzip +import rowers.mytypes as mytypes +from rowers.utils import myqueue + +import requests +import base64 + +from rowers.utils import dologging +from json.decoder import JSONDecodeError + +import django_rq +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('low') + +requests.packages.urllib3.disable_warnings() + +oauth_data = { + 'client_id': ROJABO_CLIENT_ID, + 'client_secret': ROJABO_CLIENT_SECRET, + 'redirect_uri': ROJABO_REDIRECT_URI, + 'autorization_uri': ROJABO_OAUTH_LOCATION+"oauth/authorize", + 'content_type': 'application/json', + 'tokenname': 'nktoken', + 'refreshtokenname': 'nkrefreshtoken', + 'expirydatename': 'nktokenexpirydate', + 'bearer_auth': True, + 'base_url': ROJABO_OAUTH_LOCATION+"oauth/token", + 'scope': 'read', +} + +def get_token(code): # pragma: no cover + + post_data = { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": oauth_data['redirect_uri'], + } + + + auth_string = '{id}:{secret}'.format( + id=ROJABO_CLIENT_ID, + secret=ROJABO_CLIENT_SECRET + ) + + + + try: + headers = {'Authorization': 'Basic %s' % base64.b64encode(auth_string), + 'Content-Type': 'application/x-www-form-urlencoded'} + except TypeError: + headers = {'Authorization': 'Basic %s' % base64.b64encode( + bytes(auth_string, 'utf-8')).decode('utf-8'), + 'Content-Type': 'application/x-www-form-urlencoded'} + + + + response = requests.post(oauth_data['base_url'], + data=post_data, + headers=headers) + + if response.status_code != 200: + return (0,response.reason) + + try: + token_json = response.json() + thetoken = token_json['access_token'] + expires_in = token_json['expires_in'] + refresh_token = token_json['refresh_token'] + except (KeyError, JSONDecodeError) as e: # pragma: no cover + thetoken = 0 + expires_in = 0 + refresh_token = 0 + + return [thetoken, expires_in, refresh_token] + +def rojabo_open(user): + r = Rower.objects.get(user=user) + if (r.rojabo_token == '') or (r.rojabo_token is None): + raise NoTokenError("User has no token") + else: + if (timezone.now() > r.rojabo_tokenexpirydate): + res = rower_rojabo_token_refresh(user) + if res is None: # pragma: no cover + raise NoTokenError("User has no token") + if res[0] is not None: + thetoken = res[0] + else: # pragma: no cover + raise NoTokenError("User has no token") + else: + thetoken = r.rojabo_token + + return thetoken + +def rower_rojabo_token_refresh(user): + r = Rower.objects.get(user=user) + res = do_refresh_token(r.rojabo_refreshtoken) + if res[0]: + access_token = res[0] + expires_in = res[1] + refresh_token = res[2] + expirydatetime = timezone.now()+timedelta(seconds=expires_in) + + r = Rower.objects.get(user=user) + r.rojabo_token = access_token + r.rojabo_tokenexpirydate = expirydatetime + r.rojabo_refreshtoken = refresh_token + + r.save() + return r.rojabo_token + else: # pragma: no cover + return None + +def do_refresh_token(refreshtoken): + post_data = { + "grant_type": "refresh_token", + "refresh_token": refreshtoken, + "redirect_uri": oauth_data['redirect_uri'], + } + + + auth_string = '{id}:{secret}'.format( + id=ROJABO_CLIENT_ID, + secret=ROJABO_CLIENT_SECRET + ) + + + + try: + headers = {'Authorization': 'Basic %s' % base64.b64encode(auth_string), + 'Content-Type': 'application/x-www-form-urlencoded'} + except TypeError: + headers = {'Authorization': 'Basic %s' % base64.b64encode( + bytes(auth_string, 'utf-8')).decode('utf-8'), + 'Content-Type': 'application/x-www-form-urlencoded'} + + + response = requests.post(oauth_data['base_url'], + data=post_data, + headers=headers, + verify=False) + + if response.status_code != 200: + return (0,response.reason) + + try: + token_json = response.json() + thetoken = token_json['access_token'] + expires_in = token_json['expires_in'] + refresh_token = token_json['refresh_token'] + except (KeyError, JSONDecodeError) as e: # pragma: no cover + thetoken = 0 + expires_in = 0 + refresh_token = 0 + + return [thetoken, expires_in, refresh_token] + +aweekago = timezone.now()-timedelta(days=7) +today = timezone.now() +a_week_from_now = timezone.now()+timedelta(days=7) + +def get_rojabo_workout_list(user,startdate=aweekago,enddate=a_week_from_now): + r = Rower.objects.get(user=user) + if (r.rojabo_token == '') or (r.rojabo_token is None): # pragma: no cover + s = "Token doesn't exist. Need to authorize" + return custom_exception_handler(401, s) + elif (timezone.now() > r.rojabo_tokenexpirydate): # pragma: no cover + s = "Token expired. Needs to refresh." + + return custom_exception_handler(401, s) + + _ = rojabo_open(user) + + authorizationstring = str('Bearer ' + r.rojabo_token) + + headers = {'Authorization': authorizationstring, + 'Content-Type': 'application/json'} + + date1 = startdate.strftime('%Y-%m-%d') + date2 = enddate.strftime('%Y-%m-%d') + + + url = ROJABO_OAUTH_LOCATION+'api/v1/training_sessions?from={date1}&to={date2}'.format(date1=date1,date2=date2) + + response = requests.get(url, headers=headers) + + return response diff --git a/rowers/tasks.py b/rowers/tasks.py index 7886c1e3..a7bc701c 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -3377,6 +3377,16 @@ def handle_c2_async_workout(alldata, userid, c2token, c2id, delaysec, defaulttim return workoutid +@app.task +def fetch_rojabo_session(id,alldata,userid,rowerid,debug=False, **kwargs): + try: + item = alldata[id] + except KeyError: + return 0 + + + return 1 + @app.task def fetch_strava_workout(stravatoken, oauth_data, stravaid, csvfilename, userid, debug=False, **kwargs): @@ -3390,7 +3400,7 @@ def fetch_strava_workout(stravatoken, oauth_data, stravaid, csvfilename, userid, if response.status_code != 200: # pragma: no cover tstamp = time.localtime() timestamp = time.strftime('%b-%d-%Y_%H%M', tstamp) - with open('strava_webhooks.log', 'a') as f: + with open('stravalog.log', 'a') as f: f.write('\n') f.write(timestamp) f.write(' ') diff --git a/rowers/templates/menu_plan.html b/rowers/templates/menu_plan.html index da163066..4ff3f5bf 100644 --- a/rowers/templates/menu_plan.html +++ b/rowers/templates/menu_plan.html @@ -66,6 +66,11 @@  Add Session +
  • + +  Import from Rojabo + +
  • Plan Microcycle diff --git a/rowers/templates/rojabo_list_import.html b/rowers/templates/rojabo_list_import.html new file mode 100644 index 00000000..50cd88e7 --- /dev/null +++ b/rowers/templates/rojabo_list_import.html @@ -0,0 +1,59 @@ +{% extends "newbase.html" %} +{% load static %} +{% load rowerfilters %} + +{% block title %}sessions{% endblock %} + +{% block main %} +

    Available on Rojabo

    +{% if sessions %} +
      +
    • +
      + {% csrf_token %} + + Select All New + + + + + + + + + + + + + {% for session in sessions %} + + + + + + + + + + {% endfor %} + +
      Import Session SPM Points Planned Date New
      + {% if session|lookuplong:'new' == 'NEW' and checknew == 'true' %} + + {% else %} + + {% endif %} + {{ session|lookuplong:'name' }}{{ session|lookuplong:'spm' }}{{ session|lookuplong:'points' }}{{ session|lookuplong:'date' }} {{ session|lookuplong:'new' }}
      +
      +
    • +
    +{% else %} +

    + No sessions found +

    + {% endif %} + {% endblock %} + +{% block sidebar %} +{% include 'menu_plan.html' %} +{% endblock %} diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index 7f77f1d2..a62ad6a2 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -32,6 +32,10 @@ {% if rower.rp3token is not None and rower.rp3token != '' %} RP3 {% endif %} + {% if rower.rojabo_token is not None and rower.rojabo_token != '' %} + Rojabo + {% endif %} +

    {% if form.errors %} @@ -80,8 +84,10 @@

    connect with Garmin

    -

    connect with RP3

    +

    connect with RP3

    +

    connect with Rojabo

    {% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index 49cfa865..65836ba7 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -598,6 +598,8 @@ urlpatterns = [ views.workout_rp3import_view, name='workout_rp3import_view'), re_path(r'^workout/stravaimport/$', views.workout_stravaimport_view, name='workout_stravaimport_view'), + re_path(r'^session/rojaboimport/$', views.workout_rojaboimport_view, + name='workout_rojaboimport_view'), re_path(r'^workout/stravaimport/user/(?P\d+)/$', views.workout_stravaimport_view, name='workout_stravaimport_view'), re_path(r'^workout/c2import/all/$', views.workout_getc2workout_all, @@ -757,6 +759,8 @@ urlpatterns = [ name='rower_c2_authorize'), re_path(r'^me/nkauthorize/$', views.rower_nk_authorize, name='rower_nk_authorize'), + re_path(r'^me/rojaboauthorize/$', views.rower_rojabo_authorize, + name='rower_rojabo_authorize'), re_path(r'^me/polarauthorize/$', views.rower_polar_authorize, name='rower_polar_authorize'), re_path(r'^me/revokeapp/(?P\d+)/$', diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index b80126d5..35191735 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -1,4 +1,4 @@ -from rowsandall_app.settings import NK_OAUTH_LOCATION +from rowsandall_app.settings import NK_OAUTH_LOCATION, ROJABO_OAUTH_LOCATION from rowers.views.statements import * from rowers.plannedsessions import get_dates_timeperiod @@ -139,6 +139,23 @@ def workout_sporttracks_upload_view(request, id=0): return HttpResponseRedirect(url) # pragma: no cover +# ROJABO authorization +def rower_rojabo_authorize(request): # pragma: no cover + state = str(uuid4()) + scope = "read" + params = { + # "grant_type": "authorization_code", + # "response_type": "code", + "client_id": ROJABO_CLIENT_ID, + #"client_secret": ROJABO_CLIENT_SECRET, + # "scope": scope, + #"state": state, + "redirect_uri": ROJABO_REDIRECT_URI, + } + + url = ROJABO_OAUTH_LOCATION+'oauth/authorize?'+urllib.parse.urlencode(params) + + return HttpResponseRedirect(url) # NK Logbook authorization @login_required() @@ -496,9 +513,47 @@ def rower_process_garmincallback(request): # pragma: no cover url = reverse('rower_exportsettings_view') return HttpResponseRedirect(url) +# Process Rojabo callback +@login_required() +def rower_process_rojabocallback(request): # prgrma: no cover + # do stuff + try: + code = request.GET.get('code', None) + res = rojabo_stuff.get_token(code) + except MultiValueDictKeyError: + message = "The resource owner or authorization server denied the request" + messages.error(request, message) + + url = reverse('rower_exportsettings_view') + return HttpResponseRedirect(url) + + access_token = res[0] + if access_token == 0: + message = res[1] + message += ' Contact support@rowsandall.com if this behavior persists' + messages.error(request, message) + + url = reverse('rower_exportsettings_view') + + return HttpResponseRedirect(url) + + expires_in = res[1] + refresh_token = res[2] + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + + r = getrower(request.user) + r.rojabo_token = access_token + r.rojabo_tokenexpirydate = expirydatetime + r.rojabo_refreshtoken = refresh_token + + 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 NK Callback - - @login_required() def rower_process_nkcallback(request): # pragma: no cover # do stuff @@ -974,6 +1029,133 @@ def workout_rp3import_view(request, userid=0): 'teams': get_my_teams(request.user) }) +# The page where you select which Strava workout to import +@login_required() +@user_passes_test(ispromember, login_url="/rowers/paidplans/", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) +def workout_rojaboimport_view(request, message="", userid=0): + r = getrequestrower(request, userid=userid) + if r.user != request.user: + messages.error( + request, 'You can only access your own workouts on Rojabo, not those of your athletes') + url = reverse('workout_rojaboimport_view', + kwargs={'userid': request.user.id}) + return HttpResponseRedirect(url) + + try: + _ = rojabo_open(request.user) + except NoTokenError: # pragma: no cover + return HttpResponseRedirect("/rowers/me/rojaboauthorize/") + + res = rojabo_stuff.get_rojabo_workout_list(request.user) + + if (res.status_code != 200): # pragma: no cover + if (res.status_code == 401): + r = getrower(request.user) + if (r.stravatoken == '') or (r.stravatoken is None): + s = "Token doesn't exist. Need to authorize" + return HttpResponseRedirect("/rowers/me/rojaboauthorize/") + message = "Something went wrong in workout_rojaboimport_view" + messages.error(request, message) + url = reverse('workouts_view') + return HttpResponseRedirect(url) + + sessions = [] + r = getrower(request.user) + + if request.method == "POST": + try: + tdict = dict(request.POST.lists()) + ids = tdict['sessionid'] + rojaboids = [int(id) for id in ids] + alldata = {} + for item in res.json(): + alldata[item['training_session']['id']] = item['training_session'] + for rojaboid in rojaboids: + try: + item = alldata[rojaboid] + name = item['workout'] + spm = item['stroke'] + points = item['points'] + manager = userid + comment = 'ROJABO {name}, SPM: {spm}. Points: {points}'.format( + name = name, + points = points, + spm = spm, + ) + + preferreddate = datetime.datetime.strptime(item['training_date'],"%Y-%m-%d") + startdate = preferreddate + enddate = preferreddate + ps = PlannedSession( + name = item['workout'], + comment = comment, + startdate = startdate, + enddate = enddate, + preferreddate = preferreddate, + sessionsport = 'rower', + manager = request.user, + rojabo_id = rojaboid, + ) + ps.save() + ps.rower.add(r) + ps.tags.add('ROJABO') + ps.save() + messages.info(request,'Saved planned session {id}'.format(id=ps.id)) + except KeyError: + pass + except KeyError: + pass + + rojabo_ids = [int(item['training_session']['id']) for item in res.json()] + + knownrojaboids = uniqify([ + ps.rojabo_id for ps in PlannedSession.objects.filter(manager=request.user) + ]) + + + for item in res.json(): + i = item['training_session']['id'] + if i in knownrojaboids: + nnn = '' + else: + nnn = 'NEW' + n = item['training_session']['workout'] + spm = item['training_session']['stroke'] + d = item['training_session']['training_date'] + p = item['training_session']['points'] + sessions.append({ + 'id':i, + 'name':n, + 'spm':spm, + 'new':nnn, + 'date': d, + 'points': p, + }) + + breadcrumbs = [ + { + 'url': '/rowers/list-workouts/', + 'name': 'Workouts' + }, + { + 'url': reverse('workout_rojaboimport_view'), + 'name': 'Strava' + }, + ] + + checknew = request.GET.get('selectallnew', False) + + return render(request, 'rojabo_list_import.html', + {'sessions': sessions, + 'rower': r, + 'active': 'nav-plans', + 'breadcrumbs': breadcrumbs, + 'teams': get_my_teams(request.user), + 'checknew': checknew, + }) # The page where you select which Strava workout to import @login_required() @@ -1566,8 +1748,6 @@ def workout_getrp3workout_all(request): # pragma: no cover return HttpResponseRedirect(url) # List of workouts available on Concept2 logbook - for import - - @login_required() @permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) @permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True) diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 257391fe..b03447e6 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -190,6 +190,7 @@ import datetime import iso8601 import rowers.c2stuff as c2stuff import rowers.nkstuff as nkstuff +import rowers.rojabo_stuff as rojabo_stuff from rowers.c2stuff import c2_open from rowers.nkstuff import nk_open from rowers.rp3stuff import rp3_open @@ -197,8 +198,10 @@ from rowers.sporttracksstuff import sporttracks_open from rowers.tpstuff import tp_open from iso8601 import ParseError import rowers.stravastuff as stravastuff +import rowers.rojabo_stuff as rojabo_stuff import rowers.garmin_stuff as garmin_stuff from rowers.stravastuff import strava_open +from rowers.rojabo_stuff import rojabo_open import rowers.polarstuff as polarstuff import rowers.sporttracksstuff as sporttracksstuff @@ -218,7 +221,8 @@ from rowsandall_app.settings import ( BRAINTREE_MERCHANT_ID, BRAINTREE_PUBLIC_KEY, BRAINTREE_PRIVATE_KEY, PAYMENT_PROCESSING_ON, RECAPTCHA_SITE_KEY, RECAPTCHA_SITE_SECRET, - NK_REDIRECT_URI, NK_CLIENT_ID, NK_CLIENT_SECRET + NK_REDIRECT_URI, NK_CLIENT_ID, NK_CLIENT_SECRET, + ROJABO_REDIRECT_URI, ROJABO_CLIENT_ID, ROJABO_CLIENT_SECRET, ) from django.contrib import messages @@ -235,6 +239,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 ( + fetch_rojabo_session, handle_sendemail_unrecognized, handle_sendemailnewcomment, handle_request_post, handle_sendemailsummary, diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index f95454a1..caa32dfc 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -281,6 +281,13 @@ STRAVA_CLIENT_ID = CFG['strava_client_id'] STRAVA_CLIENT_SECRET = CFG['strava_client_secret'] STRAVA_REDIRECT_URI = CFG['strava_callback'] +# ROJABO + +ROJABO_CLIENT_ID = CFG['rojabo_client_id'] +ROJABO_CLIENT_SECRET = CFG['rojabo_client_secret'] +ROJABO_REDIRECT_URI = CFG['rojabo_callback'] +ROJABO_OAUTH_LOCATION = CFG['rojabo_oauth_location'] + # Garmin GARMIN_CLIENT_KEY = CFG["garmin_client_key"] GARMIN_CLIENT_SECRET = CFG['garmin_client_secret'] diff --git a/rowsandall_app/urls.py b/rowsandall_app/urls.py index 5692ba76..e7fdbfba 100644 --- a/rowsandall_app/urls.py +++ b/rowsandall_app/urls.py @@ -84,6 +84,7 @@ urlpatterns += [ # re_path(r'^admin/rq/',include('django_rq_dashboard.urls')), re_path(r'^call\_back', rowersviews.rower_process_callback), 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'^sporttracks\_callback', rowersviews.rower_process_sporttrackscallback), diff --git a/static/img/rojabo.png b/static/img/rojabo.png new file mode 100644 index 00000000..843e0174 Binary files /dev/null and b/static/img/rojabo.png differ