Merge branch 'release/v22.6.0'
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
|
||||
@@ -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 <admin@rowsandall.com>'
|
||||
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(
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends "emailbase.html" %}
|
||||
|
||||
{% block body %}
|
||||
<p>Dear Admin,</p>
|
||||
|
||||
|
||||
<p><strong>{{ name }}</strong>: {{ reason }}.</p>
|
||||
|
||||
<p>
|
||||
Best Regards, the Rowsandall Team
|
||||
</p>
|
||||
{% endblock %}
|
||||
@@ -208,7 +208,12 @@
|
||||
</table>
|
||||
<li>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_staff %}
|
||||
<li class="rounder">
|
||||
<h2>Admin</h2>
|
||||
<p><a href="/rowers/me/idokladauthorize/">iDoklad authorize</a></p>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
|
||||
BIN
Binary file not shown.
@@ -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<source>\w+.*)import/$',
|
||||
views.workout_import_view, name='workout_import_view'),
|
||||
re_path(r'^workout/(?P<source>\w+.*)import/(?P<externalid>\d+)/$',
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
from rowers.views.statements import *
|
||||
from rowers.rower_rules import user_is_not_basic, user_is_coachee
|
||||
|
||||
|
||||
Reference in New Issue
Block a user