Private
Public Access
1
0

Merge branch 'release/v22.6.0'

This commit is contained in:
2024-12-26 13:31:57 +01:00
12 changed files with 267 additions and 6 deletions
+2
View File
@@ -328,6 +328,8 @@ def update_subscription(rower, data, method='up'):
'%Y-%m-%d'), '%Y-%m-%d'),
method) method)
_ = idoklad.create_invoice(rower, amount, result.subscription.id, dosend=True)
return True, amount return True, amount
else: # pragma: no cover else: # pragma: no cover
errors = result.errors.for_object("subscription") errors = result.errors.for_object("subscription")
+23
View File
@@ -14,6 +14,13 @@ contacts_url = 'https://api.idoklad.cz/v3/Contacts'
invoice_url = 'https://api.idoklad.cz/v3/IssuedInvoices' invoice_url = 'https://api.idoklad.cz/v3/IssuedInvoices'
email_url = 'https://api.idoklad.cz/v3/Mails/IssuedInvoice/Send' 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 from rowers.models import iDokladToken
#idoklad_countries = json.loads(open('rowers/idoklad_countries.json').read())["Data"]["Items"] #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) res = requests.post(contacts_url, json=data, headers=headers)
if res.status_code not in [200, 201]: 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 return 0
id = res.json()['Data']['Id'] 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') dologging('idoklad.log','Invoice Sent - status code '+str(res.status_code)+'\n')
if res.status_code not in [200, 201]: if res.status_code not in [200, 201]:
dologging('idoklad.log','Invoice Sent - reason '+str(res.text)+'\n') 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 return id
+143 -1
View File
@@ -1,6 +1,7 @@
from .integrations import SyncIntegration, NoTokenError, create_or_update_syncrecord, get_known_ids 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 rowingdata import rowingdata
from rowers.rower_rules import user_is_not_basic, user_is_coachee
from rowers import mytypes from rowers import mytypes
@@ -49,6 +50,8 @@ headers = {
intervals_authorize_url = 'https://intervals.icu/oauth/authorize?' intervals_authorize_url = 'https://intervals.icu/oauth/authorize?'
intervals_token_url = 'https://intervals.icu/api/oauth/token' intervals_token_url = 'https://intervals.icu/api/oauth/token'
webhookverification = 'JA9Vt6RNH10'
class IntervalsIntegration(SyncIntegration): class IntervalsIntegration(SyncIntegration):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(IntervalsIntegration, self).__init__(*args, **kwargs) super(IntervalsIntegration, self).__init__(*args, **kwargs)
@@ -336,6 +339,28 @@ class IntervalsIntegration(SyncIntegration):
return data 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): def get_plannedsession(self, id, *args, **kwargs):
_ = self.open() _ = self.open()
r = self.rower r = self.rower
@@ -396,6 +421,7 @@ class IntervalsIntegration(SyncIntegration):
"name": ps.name, "name": ps.name,
"description": stepstext, "description": stepstext,
"indoor": ps.sessionsport in mytypes.ergtypes, "indoor": ps.sessionsport in mytypes.ergtypes,
'external_id': ps.id,
} }
if ps.sessiontype == 'cycletarget': if ps.sessiontype == 'cycletarget':
@@ -442,3 +468,119 @@ class IntervalsIntegration(SyncIntegration):
ps.save() ps.save()
return 1 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
+3
View File
@@ -1176,6 +1176,7 @@ class Rower(models.Model):
intervals_auto_export = models.BooleanField(default=False) intervals_auto_export = models.BooleanField(default=False)
intervals_auto_import = 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_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') intervals_resample_to_1s = models.BooleanField(default=False, verbose_name='Resample to 1s on export')
sporttrackstoken = models.CharField( sporttrackstoken = models.CharField(
default='', max_length=200, blank=True, null=True) default='', max_length=200, blank=True, null=True)
@@ -4676,6 +4677,7 @@ class RowerExportForm(ModelForm):
'intervals_auto_export', 'intervals_auto_export',
'intervals_delete_plannedsession', 'intervals_delete_plannedsession',
'intervals_resample_to_1s', 'intervals_resample_to_1s',
'intervals_auto_delete',
'imports_are_private' 'imports_are_private'
] ]
@@ -4703,6 +4705,7 @@ class RowerExportFormIntervals(ModelForm):
'intervals_auto_import', 'intervals_auto_import',
'intervals_auto_export', 'intervals_auto_export',
'intervals_resample_to_1s', 'intervals_resample_to_1s',
'intervals_auto_delete',
'intervals_delete_plannedsession', 'intervals_delete_plannedsession',
] ]
+16
View File
@@ -1796,6 +1796,22 @@ def sigdig(value, digits=3):
fmtstr = "%.0f" fmtstr = "%.0f"
return fmtstr % (round(value, places)) 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 @app.task
def handle_send_email_alert( 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 %}
+6 -1
View File
@@ -208,7 +208,12 @@
</table> </table>
<li> <li>
{% endif %} {% endif %}
{% if user.is_staff %}
<li class="rounder">
<h2>Admin</h2>
<p><a href="/rowers/me/idokladauthorize/">iDoklad authorize</a></p>
</li>
{% endif %}
</ul> </ul>
</form> </form>
Binary file not shown.
+1
View File
@@ -633,6 +633,7 @@ urlpatterns = [
name='workout_rojaboimport_view'), name='workout_rojaboimport_view'),
re_path(r'^session/intervalsimport/$', views.plannedsession_intervalsimport_view, re_path(r'^session/intervalsimport/$', views.plannedsession_intervalsimport_view,
name='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/$', re_path(r'^workout/(?P<source>\w+.*)import/$',
views.workout_import_view, name='workout_import_view'), views.workout_import_view, name='workout_import_view'),
re_path(r'^workout/(?P<source>\w+.*)import/(?P<externalid>\d+)/$', re_path(r'^workout/(?P<source>\w+.*)import/(?P<externalid>\d+)/$',
+57 -2
View File
@@ -10,7 +10,9 @@ from rowers.utils import NoTokenError
from rowers.models import PlannedSession from rowers.models import PlannedSession
import rowers.integrations.strava as strava import rowers.integrations.strava as strava
import rowers.integrations.intervals as intervals
from rowers.integrations import importsources from rowers.integrations import importsources
from rowers.integrations import IntervalsIntegration
from rowers.utils import NoTokenError from rowers.utils import NoTokenError
import numpy 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 # for Strava webhook request validation
@csrf_exempt @csrf_exempt
def strava_webhook_view(request): def strava_webhook_view(request):
if request.method == 'GET': if request.method == 'GET':
+3
View File
@@ -58,7 +58,10 @@ def process_idokladcallback(request):
t.acces_token = result['access_token'], t.acces_token = result['access_token'],
t.refresh_token = result['refresh_token'] t.refresh_token = result['refresh_token']
t.expires_in = result['expires_in'] t.expires_in = result['expires_in']
try:
t.id_token = result['id_token'] t.id_token = result['id_token']
except KeyError:
pass
t.save() t.save()
except iDokladToken.DoesNotExist: except iDokladToken.DoesNotExist:
t = iDokladToken( t = iDokladToken(
-1
View File
@@ -1,4 +1,3 @@
from rowers.views.statements import * from rowers.views.statements import *
from rowers.rower_rules import user_is_not_basic, user_is_coachee from rowers.rower_rules import user_is_not_basic, user_is_coachee