From 51b085001d2b92a2aae2c0350522d7cb078544bd Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 31 Mar 2021 08:52:55 +0200 Subject: [PATCH 1/3] email confirmation for new user registration - still need coach --- rowers/nkstuff.py | 142 +++++++++++++++++++++++ rowers/templates/acc_activate_email.html | 7 ++ rowers/templates/nk_list_import.html | 58 +++++++++ rowers/tests/testdata/nkworkouts.txt | 84 ++++++++++++++ rowers/tokens.py | 12 ++ rowers/urls.py | 1 + rowers/views/paymentviews.py | 84 +++++++++++--- 7 files changed, 370 insertions(+), 18 deletions(-) create mode 100644 rowers/nkstuff.py create mode 100644 rowers/templates/acc_activate_email.html create mode 100644 rowers/templates/nk_list_import.html create mode 100644 rowers/tests/testdata/nkworkouts.txt create mode 100644 rowers/tokens.py diff --git a/rowers/nkstuff.py b/rowers/nkstuff.py new file mode 100644 index 00000000..37d91aba --- /dev/null +++ b/rowers/nkstuff.py @@ -0,0 +1,142 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import unicode_literals, absolute_import + +import time +from time import strftime + +import requests + +#https://oauth-stage.nkrowlink.com/oauth/authorizegrant_type=authorization_code&response_type=code&client_id=rowsandall-staging&scope=read&state=fc8fc3d8-ce0a-443e-838a-1c06fb5317c6&redirect_uri=https%3A%2F%2Fdunav.ngrok.io%2Fnk_callback%2F +#https://oauth-stage.nkrowlink.com/oauth/authorize?grant_type=authorization_code&response_type=code&client_id=rowsandall-staging&scope=read&state=1234&redirect_uri=https%3A%2F%2Fdev.rowsandall.com%2Fnk_callback + +from requests_oauthlib import OAuth2Session + +import django_rq +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('low') + +from rowers.rower_rules import is_workout_user, ispromember + +from iso8601 import ParseError +from rowers.utils import myqueue + +import rowers.mytypes as mytypes +import gzip + +from rowsandall_app.settings import ( + NK_CLIENT_ID, NK_REDIRECT_URI, NK_CLIENT_SECRET, + SITE_URL, NK_API_LOCATION + ) + +try: + from json.decoder import JSONDecodeError +except ImportError: + JSONDecodeError = ValueError + +from rowers.imports import * + +oauth_data = { + 'client_id': NK_CLIENT_ID, + 'client_secret': NK_CLIENT_SECRET, + 'redirect_uri': NK_REDIRECT_URI, + 'autorization_uri': "https://oauth-stage.nkrowlink.com/oauth/authorize", + 'content_type': 'application/json', + 'tokenname': 'nktoken', + 'refreshtokenname': 'nkrefreshtoken', + 'expirydatename': 'nktokenexpirydate', + 'bearer_auth': True, + 'base_url': "https://oauth-stage.nkrowlink.com/oauth/token", + 'scope':'read', + } + +def get_token(code): + #client_id = oauth_data['client_id'] + #client_secret = oauth_data['client_secret'] + #base_uri = oauth_data['base_url'] + + #callbackuri = 'https://'+callbackuri[9:] + #print(callbackuri) + + #nk = OAuth2Session(client_id) + #token = nk.fetch_token(base_uri,client_secret=client_secret,authorization_response=callbackuri) + + #print(token) + #return [0,0,0] + return imports_get_token(code, oauth_data) + +def nk_open(user): + t = time.localtime() + timestamp = time.strftime('%b-%d-%Y_%H%M', t) + token = imports_open(user,oauth_data) + return token + +def do_refresh_token(refreshtoken): + return imports_do_refresh_token(refreshtoken, oauth_data) + +def rower_nk_token_refresh(user): + r = Rower.objects.get(user=user) + res = do_refresh_token(r.nkrefreshtoken) + access_token = res[0] + expires_in = res[1] + refresh_token = res[2] + expirydatetime = timezone.now()+timedelta(seconds=expires_in) + + r.nktoken = access_token + r.nktokenexpirydate = expirydatetime + r.nkrefreshtoken = refresh_token + r.save() + + return r.nktoken + +def make_authorization_url(request): + return imports_make_authorization_url(oauth_data) + +def get_nk_workout_list(user,fake=False): + r = Rower.objects.get(user=user) + + if (r.nktoken == '') or (r.nktoken is None): + s = "Token doesn't exist. Need to authorize" + return custom_exception_handler(401,s) + elif (r.nktokenexpirydate is None or timezone.now()+timedelta(seconds=3599)>r.nktokenexpirydate): + s = "Token expired. Needs to refresh." + return custom_exception_handler(401,s) + else: + # ready to fetch. Hurray + authorizationstring = str('Bearer ' + r.nktoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + + url = NK_API_LOCATION+"api/v1/sessions" + + params = {} # start / end time + + s = requests.get(url,headers=headers,params=params) + + return s + +def get_nk_workout_strokes(user,nkid): + r = Rower.objects.get(user=user) + + if (r.nktoken == '') or (r.nktoken is None): + return custom_exception_handler(401,s) + s = "Token doesn't exist. Need to authorize" + elif (timezone.now()>r.tokenexpirydate): + s = "Token expired. Needs to refresh." + return custom_exception_handler(401,s) + else: + # ready to fetch. Hurray + authorizationstring = str('Bearer ' + r.nktoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://log.concept2.com/api/users/me/results/"+str(nkid)+"/strokes" + s = requests.get(url,headers=headers) + + return s +# +#def get_workout(user,nkid): diff --git a/rowers/templates/acc_activate_email.html b/rowers/templates/acc_activate_email.html new file mode 100644 index 00000000..2c4f4e1e --- /dev/null +++ b/rowers/templates/acc_activate_email.html @@ -0,0 +1,7 @@ +{% autoescape off %} +Hi {{ user.username }}, +Please click on the link to confirm your registration, + +http://{{ domain }}{% url 'useractivate' uidb64=uid token=token %} +If you think, it's not you, then just ignore this email. +{% endautoescape %} diff --git a/rowers/templates/nk_list_import.html b/rowers/templates/nk_list_import.html new file mode 100644 index 00000000..44ae0554 --- /dev/null +++ b/rowers/templates/nk_list_import.html @@ -0,0 +1,58 @@ +{% extends "newbase.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block main %} +

Available on nk

+{% if workouts %} +
    +
  • + Import all NEW +
  • +
  • +

    This imports all workouts that have not been imported to rowsandall.com. + The action may take a longer time to process, so please be patient. Click on Import in the list below to import an individual workout. +

    +
  • + +
  • + + + + + + + + + + + + + {% for workout in workouts %} + + + + + + + + + + {% endfor %} + +
    Import Name Date Duration Distance New
    + Import{{ workout|lookup:'name' }}{{ workout|lookup:'starttime' }}{{ workout|lookup:'duration' }} {{ workout|lookup:'distance' }} m{{ workout|lookup:'new' }}
    +
  • +
+{% else %} +

+ No workouts found +

+ {% endif %} + {% endblock %} + +{% block sidebar %} +{% include 'menu_workouts.html' %} +{% endblock %} diff --git a/rowers/tests/testdata/nkworkouts.txt b/rowers/tests/testdata/nkworkouts.txt new file mode 100644 index 00000000..320a47cc --- /dev/null +++ b/rowers/tests/testdata/nkworkouts.txt @@ -0,0 +1,84 @@ +[ + { + "id": 1, + "name": "JustGo-7189M", + "type": 0, + "speedInput": 0, + "startTime": 1614264826, + "endTime": 1614269826, + "location": "Some Location", + "deviceId": 11, + "elapsedTime": 1140000, + "totalDistanceImp": 2920.69, + "totalDistanceGps": 2286.5, + "avgPaceImp": 195159.36302723, + "avgPaceGps": 250466.812537141, + "avgStrokeRate": 19.5, + "distStrokeImp": 8.51, + "distStrokeGps": 6.77, + "avgHeartRate": 158, + "totalStrokeCount": 343, + "totalCalories": 4959900, + "avgCalHour": 4482062.35252774, + "avgSpeedGps": 2, + "avgSpeedImp": 2.56, + "avgPower": 68.6979270660324, + "avgCatch": -46.7961491475831, + "avgSlip": 16.1616225246003, + "avgFinish": 48.1512290049444, + "avgWash": 22.9514686031976, + "avgForceAvg": 114.502194258813, + "avgWork": 210.785493336831, + "avgForceMax": 249.481436977143, + "avgMaxForceAngle": 4.75378911974861, + "startGpsLat": 39.7356346, + "startGpsLon": -75.5581928, + "intervals": [ + { + "id": 1, + "sessionId": 1, + "startTime": 1614264826, + "intervalNumber": 1, + "sessionStrokeStartIndex": 473, + "sessionStrokeEndIndex": 674, + "sessionStrokeCount": 91, + "elapsedTime": 1140000, + "totalDistanceImp": 2920.69, + "totalDistanceGps": 2286.5, + "avgPaceImp": 195159.36302723, + "avgPaceGps": 250466.812537141, + "avgStrokeRate": 19.5, + "distStrokeImp": 8.51, + "distStrokeGps": 6.77, + "avgHeartRate": 158, + "totalStrokeCount": 343, + "totalCalories": 4959900, + "avgCalHour": 4482062.35252774, + "avgSpeedGps": 2, + "avgSpeedImp": 2.56, + "avgPower": 68.6979270660324, + "avgCatch": -46.7961491475831, + "avgSlip": 16.1616225246003, + "avgFinish": 48.1512290049444, + "avgWash": 22.9514686031976, + "avgForceAvg": 114.502194258813, + "avgWork": 210.785493336831, + "avgForceMax": 249.481436977143, + "avgMaxForceAngle": 4.75378911974861, + "startGpsLat": 39.7356346, + "startGpsLon": -75.5581928 + } + ], + "oarlockSessions": [ + { + "id": 1, + "sessionId": 1, + "boatName": "Fast Boat", + "seatNumber": 3, + "portStarboard": 0, + "oarLength": 284, + "oarInboardLength": 85 + } + ] + } +] diff --git a/rowers/tokens.py b/rowers/tokens.py new file mode 100644 index 00000000..e4d10f6e --- /dev/null +++ b/rowers/tokens.py @@ -0,0 +1,12 @@ +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.utils import six + + +class AccountActivationTokenGenerator(PasswordResetTokenGenerator): + def _make_hash_value(self, user, timestamp): + return ( + six.text_type(user.pk) + six.text_type(timestamp) + + six.text_type(user.is_active) + ) + +account_activation_token = AccountActivationTokenGenerator() diff --git a/rowers/urls.py b/rowers/urls.py index 99959d6c..434c291d 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -729,6 +729,7 @@ urlpatterns = [ re_path(r'^legal', TemplateView.as_view(template_name='legal.html'),name='legal'), re_path(r'^register/$',views.rower_register_view,name='rower_register_view'), re_path(r'^coachregister/$',views.freecoach_register_view,name='freecoach_register_view'), + path('activate///',views.useractivate, name='useractivate'), re_path(r'^register/thankyou/$', TemplateView.as_view(template_name='registerthankyou.html'), name='registerthankyou'), re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/workflow/$',views.workout_workflow_view, name='workout_workflow_view'), diff --git a/rowers/views/paymentviews.py b/rowers/views/paymentviews.py index 30be9c80..d4b431eb 100644 --- a/rowers/views/paymentviews.py +++ b/rowers/views/paymentviews.py @@ -4,6 +4,7 @@ from __future__ import print_function from __future__ import unicode_literals from rowers.views.statements import * +from django.core.mail import EmailMessage @csrf_exempt def braintree_webhook_view(request): @@ -711,6 +712,49 @@ def downgrade_completed_view(request): 'rower':r }) +from django.utils.encoding import force_bytes, force_text +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.contrib.sites.shortcuts import get_current_site +from rowers.tokens import account_activation_token +# Email activation +def useractivate(request, uidb64, token): + try: + uid = force_text(urlsafe_base64_decode(uidb64)) + user = User.objects.get(id=uid) + except(TypeError, ValueError, OverflowError, User.DoesNotExist): + user = None + if user is not None and account_activation_token.check_token(user, token): + user.is_active = True + user.save() + # below is old + fullemail = user.first_name + " " + user.last_name + " " + "<" + user.email + ">" + subject = "Thank you for registering on rowsandall.com" + from_address = 'Sander Roosendaal ' + + d = {'first_name':user.first_name} + + send_template_email(from_address,[fullemail], + subject,'registeremail.html',d) + + + subject2 = "New User" + message2 = "New user registered.\n" + message2 += fullemail + "\n" + message2 += "User name: "+user.username + + send_mail(subject2, message2, + 'Rowsandall Server ', + ['roosendaalsander@gmail.com']) + + + messages.info(request,'Thank you for your email confirmation. Now you can login your account.') + url = '/login/' + return HttpResponseRedirect(url) + else: + return HttpResponse('Activation link is invalid!') + + + # User registration def rower_register_view(request): @@ -736,6 +780,7 @@ def rower_register_view(request): theuser.first_name = first_name theuser.last_name = last_name theuser.email = email + theuser.is_active = False theuser.save() birthdate = birthdate.replace(tzinfo=None) @@ -766,26 +811,29 @@ def rower_register_view(request): w.save() # Create and send email - fullemail = first_name + " " + last_name + " " + "<" + email + ">" - subject = "Thank you for registering on rowsandall.com" - from_address = 'Sander Roosendaal ' - - d = {'first_name':theuser.first_name} - - send_template_email(from_address,[fullemail], - subject,'registeremail.html',d) + current_site = get_current_site(request) + mail_subject = 'Activate your account.' + d = { + 'user': theuser, + 'domain': current_site.domain, + 'uid': urlsafe_base64_encode(force_bytes(theuser.id)).decode(), + 'token': account_activation_token.make_token(theuser), + } + to_email = form.cleaned_data.get('email') + message = render_to_string('acc_activate_email.html', { + 'user': theuser, + 'domain': current_site.domain, + 'uid': urlsafe_base64_encode(force_bytes(theuser.id)).decode(), + 'token': account_activation_token.make_token(theuser), + }) + to_email = form.cleaned_data.get('email') + email = EmailMessage( + mail_subject, message, to=[to_email] + ) + email.send() + return HttpResponse('Please confirm your email address to complete the registration') - subject2 = "New User" - message2 = "New user registered.\n" - message2 += fullemail + "\n" - message2 += "User name: "+username - - send_mail(subject2, message2, - 'Rowsandall Server ', - ['roosendaalsander@gmail.com']) - - theuser = authenticate(username=username,password=password) login(request,theuser) return HttpResponseRedirect(nextpage) From ce22f51207f81fba30deea63458fef6f1b8ec49f Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 31 Mar 2021 10:26:11 +0200 Subject: [PATCH 2/3] email confirmation for new coach users --- rowers/views/paymentviews.py | 55 +++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/rowers/views/paymentviews.py b/rowers/views/paymentviews.py index d4b431eb..8dd72317 100644 --- a/rowers/views/paymentviews.py +++ b/rowers/views/paymentviews.py @@ -733,6 +733,10 @@ def useractivate(request, uidb64, token): d = {'first_name':user.first_name} + template = 'registeremail.html' + if user.rower.rowerplan == 'freecoach': + template = 'coachregisteremail.html' + send_template_email(from_address,[fullemail], subject,'registeremail.html',d) @@ -742,13 +746,19 @@ def useractivate(request, uidb64, token): message2 += fullemail + "\n" message2 += "User name: "+user.username + if user.rower.rowerplan == 'freecoach': + subject2 = "New Free Coach User" + + send_mail(subject2, message2, 'Rowsandall Server ', ['roosendaalsander@gmail.com']) - messages.info(request,'Thank you for your email confirmation. Now you can login your account.') + messages.info(request,'Thank you for your email confirmation. Now you can login to your account.') url = '/login/' + if user.rower.rowerplan == 'freecoach': + url+='?next=/rowers/me/teams' return HttpResponseRedirect(url) else: return HttpResponse('Activation link is invalid!') @@ -890,29 +900,28 @@ def freecoach_register_view(request): # create default favorite charts add_defaultfavorites(therower) - # Create and send email - fullemail = first_name + " " + last_name + " " + "<" + email + ">" - subject = "Thank you for registering on rowsandall.com" - from_address = 'Sander Roosendaal ' - - d = {'first_name':theuser.first_name} - - send_template_email(from_address,[fullemail], - subject,'coachregisteremail.html',d) - - - subject2 = "New Free Coach" - message2 = "New Free Coach registered.\n" - message2 += fullemail + "\n" - message2 += "User name: "+username - - send_mail(subject2, message2, - 'Rowsandall Server ', - ['roosendaalsander@gmail.com']) - - theuser = authenticate(username=username,password=password) - login(request,theuser) + current_site = get_current_site(request) + mail_subject = 'Activate your account.' + d = { + 'user': theuser, + 'domain': current_site.domain, + 'uid': urlsafe_base64_encode(force_bytes(theuser.id)).decode(), + 'token': account_activation_token.make_token(theuser), + } + to_email = form.cleaned_data.get('email') + message = render_to_string('acc_activate_email.html', { + 'user': theuser, + 'domain': current_site.domain, + 'uid': urlsafe_base64_encode(force_bytes(theuser.id)).decode(), + 'token': account_activation_token.make_token(theuser), + }) + to_email = form.cleaned_data.get('email') + email = EmailMessage( + mail_subject, message, to=[to_email] + ) + email.send() + return HttpResponse('Please confirm your email address to complete the registration') return HttpResponseRedirect(nextpage) From 0eec7bd70716b1d61d0117db437435087ad4404e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 31 Mar 2021 11:01:26 +0200 Subject: [PATCH 3/3] updating new user registration test --- rowers/tests/test_newusers.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/rowers/tests/test_newusers.py b/rowers/tests/test_newusers.py index bdfae610..3c53115a 100644 --- a/rowers/tests/test_newusers.py +++ b/rowers/tests/test_newusers.py @@ -42,20 +42,38 @@ class NewUserRegistrationTest(TestCase): self.assertTrue(form.is_valid()) response = self.c.post('/rowers/register/', form_data, follow=True) + self.assertEqual(response.status_code,200) - self.assertRedirects(response, - expected_url='/rowers/me/gdpr-optin/?next=/rowers/list-workouts/', - status_code=302,target_status_code=200) + # set opt-in + user = User.objects.get(username='janderoeiert') + user.rower.gdpr_optin = True + user.set_password('aapindewei2') + user.is_active = True + user.save() + user.rower.save() + + login = self.c.login(username=user.username,password='aapindewei2') + self.assertTrue(login) - url = '/rowers/me/gdpr-optin-confirm/?next=/rowers/list-workouts/' + url = '/rowers/list-workouts/' response = self.c.get(url) + expected = '/rowers/me/gdpr-optin/?next=/rowers/list-workouts/' + self.assertRedirects(response, - expected_url='/rowers/list-workouts/', - status_code=302,target_status_code=200, - ) + expected_url=expected, + status_code=302,target_status_code=200) + + url = '/rowers/me/gdpr-optin-confirm/?next=/rowers/list-workouts/' + response = self.c.get(url) + + expected = '/rowers/list-workouts/' + self.assertRedirects(response, + expected_url=expected, + status_code=302,target_status_code=200) + url = '/rowers/exportallworkouts/' @@ -74,6 +92,8 @@ class NewUserRegistrationTest(TestCase): self.assertTrue(response.status_code,200) + + url = '/rowers/me/delete/' form_data = {