diff --git a/rowers/braintreestuff.py b/rowers/braintreestuff.py index df39ecd6..64ba63b9 100644 --- a/rowers/braintreestuff.py +++ b/rowers/braintreestuff.py @@ -328,6 +328,8 @@ def update_subscription(rower, data, method='up'): '%Y-%m-%d'), method) + _ = idoklad.create_invoice(rower, amount, result.subscription.id, dosend=True) + return True, amount else: # pragma: no cover errors = result.errors.for_object("subscription") diff --git a/rowers/idoklad.py b/rowers/idoklad.py index 2f74ac86..4c301573 100644 --- a/rowers/idoklad.py +++ b/rowers/idoklad.py @@ -14,6 +14,13 @@ contacts_url = 'https://api.idoklad.cz/v3/Contacts' invoice_url = 'https://api.idoklad.cz/v3/IssuedInvoices' email_url = 'https://api.idoklad.cz/v3/Mails/IssuedInvoice/Send' +from rowers.tasks import handle_send_email_noinvoice + +from rowers.utils import myqueue + +import django_rq +queuehigh = django_rq.get_queue('high') + from rowers.models import iDokladToken #idoklad_countries = json.loads(open('rowers/idoklad_countries.json').read())["Data"]["Items"] @@ -118,6 +125,14 @@ def create_contact(rower): res = requests.post(contacts_url, json=data, headers=headers) if res.status_code not in [200, 201]: + dologging('idoklad.log','Contact Created - reason '+str(res.reason)+'\n') + _ = myqueue.enqueue( + queuehigh, handle_send_email_noinvoice, + rower.user.email, + rower.user.first_name, + rower.user.last_name, + 'idoklad Contact creation failed for '+str(rower.user.email)+'\n'+str(res.text) + ) return 0 id = res.json()['Data']['Id'] @@ -199,6 +214,14 @@ def create_invoice(rower, amount, braintreeid, dosend=True, contact_id=None, nam dologging('idoklad.log','Invoice Sent - status code '+str(res.status_code)+'\n') if res.status_code not in [200, 201]: dologging('idoklad.log','Invoice Sent - reason '+str(res.text)+'\n') + _ = myqueue.enqueue( + queuehigh, handle_send_email_noinvoice, + rower.user.email, + rower.user.first_name, + rower.user.last_name, + 'idoklad Invoice sending failed for '+str(rower.user.email)+'\n'+str(res.text) + ) + return id diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py index 042b5eff..182a8863 100644 --- a/rowers/integrations/intervals.py +++ b/rowers/integrations/intervals.py @@ -1,6 +1,7 @@ from .integrations import SyncIntegration, NoTokenError, create_or_update_syncrecord, get_known_ids -from rowers.models import Rower, User, Workout, TombStone +from rowers.models import Rower, User, Workout, TombStone, PlannedSession from rowingdata import rowingdata +from rowers.rower_rules import user_is_not_basic, user_is_coachee from rowers import mytypes @@ -49,6 +50,8 @@ headers = { intervals_authorize_url = 'https://intervals.icu/oauth/authorize?' intervals_token_url = 'https://intervals.icu/api/oauth/token' +webhookverification = 'JA9Vt6RNH10' + class IntervalsIntegration(SyncIntegration): def __init__(self, *args, **kwargs): super(IntervalsIntegration, self).__init__(*args, **kwargs) @@ -335,6 +338,28 @@ class IntervalsIntegration(SyncIntegration): data = response.json() return data + + def update_plannedsession(self, ps, data, *args, **kwargs): + _ = self.open() + r = self.rower + + if data['category'] == 'WORKOUT': + url = self.oauth_data['base_url'] + 'athlete/0/events/' + str(ps.intervals_icu_id) + '/downloadfit' + headers = { + 'Authorization': 'Bearer ' + r.intervals_token, + } + response = requests.get(url, headers=headers) + if response.status_code != 200: + dologging('intervals.icu.log', response.text) + else: + filename = 'planned_' + str(ps.intervals_icu_id) + '.fit' + filename2 = 'media/planned_' + str(ps.intervals_icu_id) + '.fit' + with open(filename2, 'wb') as f: + f.write(response.content) + + data['fitfile'] = filename + + return data def get_plannedsession(self, id, *args, **kwargs): _ = self.open() @@ -396,6 +421,7 @@ class IntervalsIntegration(SyncIntegration): "name": ps.name, "description": stepstext, "indoor": ps.sessionsport in mytypes.ergtypes, + 'external_id': ps.id, } if ps.sessiontype == 'cycletarget': @@ -442,3 +468,119 @@ class IntervalsIntegration(SyncIntegration): ps.save() return 1 + + def update_calendar(self, r, event, *args, **kwargs): + try: + records = event["events"] + except KeyError: + records = [] + + for record in records: + id = record['id'] + data = {} + try: + pss = PlannedSession.objects.filter(intervals_icu_id=id) + if pss.count() > 0: + ps = pss[0] + data = self.update_plannedsession(ps, record) + else: + data = self.get_plannedsession(id) + ps = PlannedSession( + manager=r.user, + intervals_icu_id=id, + ) + ps.save() + ps.rower.add(r) + except PlannedSession.DoesNotExist: + continue + + # got data + if data: + ps.name = data['name'] + ps.comment = data['description'] + ps.startdate = arrow.get(data['start_date_local']).datetime + ps.enddate = arrow.get(data['end_date_local']).datetime + ps.preferreddate = arrow.get(data['start_date_local']).datetime + ps.sessionsport = mytypes.intervalsmappinginv[data['type']] + ps.sessiontype = 'session' + ps.save() + try: + timetarget = data['time_target'] + except KeyError: + timetarget = None + if timetarget is None: + try: + timetarget = data['moving_time'] + except KeyError: + timetarget = None + if timetarget is None: + timetarget = 3600 + timetarget = int(timetarget)/60. + ps.sessionvalue = timetarget + ps.save() + if data['category'].lower() == 'workout': + ps.fitfile = data['fitfile'] + ps.save() + ps.update_steps() + if data['category'].lower() == 'target': + ps.sessiontype = 'cycletarget' + ps.sessionvalue = int(data['time_target'])/60. + ps.enddate = ps.startdate + datetime.timedelta(days=6) + ps.save() + + try: + deleted_records = event["deleted_events"] + except KeyError: + deleted_records = [] + + for record in deleted_records: + id = record['id'] + try: + pss = PlannedSession.objects.filter(intervals_icu_id=id) + if r.intervals_delete_plannedsession and pss.count() > 0: + for ps in pss: + ps.delete() + except PlannedSession.DoesNotExist: + continue + + return 1 + + def import_activities(self, event, *args, **kwargs): + if not self.rower.intervals_auto_import: + return 0 + if user_is_not_basic(self.rower.user) or user_is_coachee(self.rower.user): + try: + record = event["activity"] + except KeyError: + records = [] + + try: + id = record['id'] + result = self.get_workout(id) + except KeyError: + pass + + return 1 + + return 0 + + def delete_activities(self, event, *args, **kwargs): + if not self.rower.intervals_auto_delete: + return 0 + try: + record = event["activity"] + except KeyError: + records = [] + + try: + id = record['id'] + try: + w = Workout.objects.get(uploadedtointervals=id) + if w.user == self.rower: + w.delete() + except Workout.DoesNotExist: + pass + except KeyError: + pass + + return 1 diff --git a/rowers/models.py b/rowers/models.py index 587de999..40195356 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1176,6 +1176,7 @@ class Rower(models.Model): intervals_auto_export = models.BooleanField(default=False) intervals_auto_import = models.BooleanField(default=False) intervals_delete_plannedsession = models.BooleanField(default=False, verbose_name="Deleting planned session deletes it on intervals.icu") + intervals_auto_delete = models.BooleanField(default=True, verbose_name="Deleting an activity on intervals.icu deletes it on both") 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) @@ -4676,6 +4677,7 @@ class RowerExportForm(ModelForm): 'intervals_auto_export', 'intervals_delete_plannedsession', 'intervals_resample_to_1s', + 'intervals_auto_delete', 'imports_are_private' ] @@ -4703,6 +4705,7 @@ class RowerExportFormIntervals(ModelForm): 'intervals_auto_import', 'intervals_auto_export', 'intervals_resample_to_1s', + 'intervals_auto_delete', 'intervals_delete_plannedsession', ] diff --git a/rowers/tasks.py b/rowers/tasks.py index 8b4aca87..0382f924 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -1796,6 +1796,22 @@ def sigdig(value, digits=3): fmtstr = "%.0f" return fmtstr % (round(value, places)) +@app.task +def handle_send_email_noinvoice( + useremail, userfirstname, userlastname, reason, **kwargs): + + subject = "Reason" + + from_email = 'Rowsandall ' + d = { + 'name': userfirstname+' '+userlastname, + 'siteurl': siteurl, + 'reason': reason, + } + + _ = send_template_email(from_email, support@rowsandall.com, subject, + 'paymentconfirmationemail_noinvoice.html', + d, **kwargs) @app.task def handle_send_email_alert( diff --git a/rowers/templates/paymentconfirmationemail_noinvoice.html b/rowers/templates/paymentconfirmationemail_noinvoice.html new file mode 100644 index 00000000..88609383 --- /dev/null +++ b/rowers/templates/paymentconfirmationemail_noinvoice.html @@ -0,0 +1,12 @@ +{% extends "emailbase.html" %} + +{% block body %} +

Dear Admin,

+ + +

{{ name }}: {{ reason }}.

+ +

+ Best Regards, the Rowsandall Team +

+{% endblock %} diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index 56c82066..21e91159 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -208,7 +208,12 @@
  • {% endif %} - + {% if user.is_staff %} +
  • +

    Admin

    +

    iDoklad authorize

    +
  • + {% endif %} diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 1385fa85..54f0625f 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 aa39292b..ccb49dc7 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -633,6 +633,7 @@ urlpatterns = [ name='workout_rojaboimport_view'), re_path(r'^session/intervalsimport/$', views.plannedsession_intervalsimport_view, name='plannedsession_intervalsimport_view'), + re_path(r'^session/intervals/webhook/$', views.intervals_webhook_view, name='intervals_webhook_view'), re_path(r'^workout/(?P\w+.*)import/$', views.workout_import_view, name='workout_import_view'), re_path(r'^workout/(?P\w+.*)import/(?P\d+)/$', diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index c1d05ef9..30cd7b7b 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -10,7 +10,9 @@ from rowers.utils import NoTokenError from rowers.models import PlannedSession import rowers.integrations.strava as strava +import rowers.integrations.intervals as intervals from rowers.integrations import importsources +from rowers.integrations import IntervalsIntegration from rowers.utils import NoTokenError import numpy @@ -913,9 +915,62 @@ def workout_rojaboimport_view(request, message="", userid=0): # pragma: no cover + +@csrf_exempt +def intervals_webhook_view(request): + if request.method == 'GET': + verificationtoken = request.GET.get('secret') + if verificationtoken != intervals.webhookverification: + return HttpResponse(status=403) + + dologging("intervals_webhooks.log","GET request") + dologging("intervals_webhooks.log",request.body) + + else: + data = json.loads(request.body) + try: + verificationtoken = data['secret'] + except KeyError: + return HttpResponse(status=403) + if verificationtoken != intervals.webhookverification: + return HttpResponse(status=403) + try: + events = data['events'] + except KeyError: + # return invalid request if no events + return HttpResponse(status=200) + + webhook_type = None + + + for event in events: + try: + athlete_id = event['athlete_id'] + webhook_type = event['type'] + r = Rower.objects.get(intervals_owner_id=athlete_id) + except Rower.DoesNotExist: + return HttpResponse(status=200) + except MultipleObjectsReturned: + rs = Rower.objects.filter(intervals_owner_id=athlete_id) + r = rs[0] + except KeyError: + return HttpResponse(status=200) + + integration = IntervalsIntegration(r.user) + + if webhook_type.lower() == 'calendar_updated': + integration.update_calendar(r, event) + + if webhook_type.lower() == 'activity_uploaded': + integration.import_activities(event) + + if webhook_type.lower() == 'activity_deleted': + integration.delete_activities(event) + + return HttpResponse(status=200) + + # for Strava webhook request validation - - @csrf_exempt def strava_webhook_view(request): if request.method == 'GET': diff --git a/rowers/views/paymentviews.py b/rowers/views/paymentviews.py index 241091c1..e2dbe64c 100644 --- a/rowers/views/paymentviews.py +++ b/rowers/views/paymentviews.py @@ -58,7 +58,10 @@ def process_idokladcallback(request): t.acces_token = result['access_token'], t.refresh_token = result['refresh_token'] t.expires_in = result['expires_in'] - t.id_token = result['id_token'] + try: + t.id_token = result['id_token'] + except KeyError: + pass t.save() except iDokladToken.DoesNotExist: t = iDokladToken( diff --git a/rowers/views/userviews.py b/rowers/views/userviews.py index 61f2a9c9..7378c1ac 100644 --- a/rowers/views/userviews.py +++ b/rowers/views/userviews.py @@ -1,4 +1,3 @@ - from rowers.views.statements import * from rowers.rower_rules import user_is_not_basic, user_is_coachee