1313 lines
40 KiB
Python
1313 lines
40 KiB
Python
|
||
import time
|
||
import colorsys
|
||
|
||
import zipfile
|
||
import bleach
|
||
import arrow
|
||
import pytz
|
||
from pytz import UnknownTimeZoneError
|
||
import operator
|
||
import warnings
|
||
import urllib
|
||
import yaml
|
||
from PIL import Image
|
||
from numbers import Number
|
||
from django.views.generic.base import TemplateView
|
||
from django.contrib.auth import views as auth_views
|
||
from django.db.models import Q
|
||
from django import template
|
||
from django.db import IntegrityError, transaction
|
||
from django.views.decorators.csrf import csrf_exempt
|
||
from django.shortcuts import get_object_or_404
|
||
from matplotlib.backends.backend_agg import FigureCanvas
|
||
import gc
|
||
from pyparsing import ParseException
|
||
from uuid import uuid4
|
||
import codecs
|
||
import isodate
|
||
import re
|
||
import cgi
|
||
from icalendar import Calendar, Event
|
||
|
||
from functools import reduce
|
||
|
||
from rules.contrib.views import PermissionRequiredMixin
|
||
|
||
import rowers.braintreestuff as braintreestuff
|
||
import rowers.payments as payments
|
||
from rowers.opaque import encoder
|
||
|
||
from rowers.rower_rules import (
|
||
ispromember,is_coach_user,is_team_member,is_rower_team_member,
|
||
is_workout_user,isplanmember,can_delete_session,
|
||
can_view_target,can_change_target,can_delete_target,
|
||
can_view_plan,can_change_plan,can_delete_plan,
|
||
can_view_cycle,can_change_cycle,can_delete_cycle,
|
||
can_add_workout_member,can_plan_user,is_paid_coach,
|
||
can_start_trial, can_start_plantrial,can_plan,is_workout_team,
|
||
is_promember,
|
||
)
|
||
|
||
from django.shortcuts import render
|
||
from django.template.loader import render_to_string
|
||
|
||
from django.views.generic.edit import UpdateView,DeleteView
|
||
|
||
from django.http import (
|
||
HttpResponse, HttpResponseRedirect,
|
||
JsonResponse,
|
||
HttpResponseForbidden, HttpResponseNotAllowed,
|
||
HttpResponseNotFound,Http404
|
||
)
|
||
from django.contrib.auth import authenticate, login, logout
|
||
from rowers.forms import (
|
||
ForceCurveOptionsForm,HistoForm,TeamMessageForm,
|
||
LoginForm,DocumentsForm,UploadOptionsForm,ImageForm,CourseForm,
|
||
CourseConfirmForm,
|
||
TeamUploadOptionsForm,WorkFlowLeftPanelForm,WorkFlowMiddlePanelForm,
|
||
WorkFlowLeftPanelElement,WorkFlowMiddlePanelElement,
|
||
LandingPageForm,PlannedSessionSelectForm,WorkoutSessionSelectForm,
|
||
PlannedSessionTeamForm,PlannedSessionTeamMemberForm,
|
||
VirtualRaceSelectForm,WorkoutRaceSelectForm,CourseSelectForm,
|
||
RaceResultFilterForm,PowerIntervalUpdateForm,FlexAxesForm,
|
||
FlexOptionsForm,DataFrameColumnsForm,OteWorkoutTypeForm,
|
||
MetricsForm,DisqualificationForm,disqualificationreasons,
|
||
disqualifiers,SearchForm,BillingForm,PlanSelectForm,
|
||
VideoAnalysisCreateForm,WorkoutSingleSelectForm,
|
||
VideoAnalysisMetricsForm,SurveyForm,HistorySelectForm,
|
||
StravaChartForm,FitnessFitForm,PerformanceManagerForm,
|
||
TrainingPlanBillingForm,InstantPlanSelectForm,
|
||
TrainingZonesForm,
|
||
)
|
||
|
||
from django.urls import reverse, reverse_lazy
|
||
|
||
from django.core.exceptions import PermissionDenied, MultipleObjectsReturned
|
||
from django.template import RequestContext
|
||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||
from django.conf import settings
|
||
from django.urls import resolve
|
||
from django.utils.datastructures import MultiValueDictKeyError
|
||
from django.utils import timezone,translation
|
||
from django.core.mail import send_mail, BadHeaderError
|
||
from rowers.forms import (
|
||
SummaryStringForm,StrokeDataForm,
|
||
StatsOptionsForm,PredictedPieceForm,DateRangeForm,DeltaDaysForm,
|
||
FitnessMetricForm,PredictedPieceFormNoDistance,
|
||
EmailForm, RegistrationForm, RegistrationFormTermsOfService,
|
||
RegistrationFormUniqueEmail,RegistrationFormSex,
|
||
CNsummaryForm,UpdateWindForm,
|
||
StandardsForm,
|
||
UpdateStreamForm,WorkoutMultipleCompareForm,ChartParamChoiceForm,
|
||
FusionMetricChoiceForm,BoxPlotChoiceForm,MultiFlexChoiceForm,
|
||
TrendFlexModalForm,WorkoutSplitForm,WorkoutJoinParamForm,
|
||
AnalysisOptionsForm, AnalysisChoiceForm,
|
||
PlannedSessionMultipleCloneForm,SessionDateShiftForm,RowerTeamForm,
|
||
)
|
||
from rowers.models import (
|
||
Workout, User, Rower, WorkoutForm,FavoriteChart,
|
||
PlannedSession, DeactivateUserForm,DeleteUserForm,
|
||
TrainingPlan,TrainingPlanForm,TrainingTarget,TrainingTargetForm,
|
||
TrainingMacroCycle,TrainingMesoCycle,TrainingMicroCycle,
|
||
TrainingTarget,TrainingTargetForm,
|
||
TrainingMacroCycleForm,createmacrofillers,
|
||
createmicrofillers, createmesofillers,
|
||
microcyclecheckdates,mesocyclecheckdates,macrocyclecheckdates,
|
||
TrainingMesoCycleForm, TrainingMicroCycleForm,
|
||
RaceLogo,RowerBillingAddressForm,PaidPlan,
|
||
AlertEditForm, ConditionEditForm,
|
||
PlannedSessionComment,CoachRequest,CoachOffer,
|
||
VideoAnalysis,ShareKey,
|
||
StandardCollection,CourseStandard,
|
||
VirtualRaceFollower,TombStone, InstantPlan
|
||
)
|
||
from rowers.models import (
|
||
RowerPowerForm,RowerHRZonesForm,RowerForm,RowerCPForm,GraphImage,AdvancedWorkoutForm,
|
||
RowerPowerZonesForm,AccountRowerForm,UserForm,
|
||
Team,TeamForm,TeamInviteForm,TeamInvite,TeamRequest,
|
||
WorkoutComment,WorkoutCommentForm,RowerExportForm,
|
||
CalcAgePerformance,
|
||
PowerTimeFitnessMetric,BlogPost,
|
||
PlannedSessionForm,PlannedSessionTemplateForm,
|
||
PlannedSessionFormSmall,GeoCourseEditForm,VirtualRace,
|
||
VirtualRaceForm,VirtualRaceResultForm,RowerImportExportForm,
|
||
IndoorVirtualRaceResultForm,IndoorVirtualRaceResult,
|
||
IndoorVirtualRaceForm,PlannedSessionCommentForm,
|
||
Alert, Condition, StaticChartRowerForm,
|
||
FollowerForm,VirtualRaceAthleteForm,InstantPlanForm,DataRowerForm
|
||
)
|
||
from rowers.models import (
|
||
FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement,BasePlannedSessionFormSet,
|
||
get_course_timezone,BaseConditionFormSet,
|
||
)
|
||
from rowers.metrics import rowingmetrics,defaultfavoritecharts,nometrics,metricsgroups
|
||
from rowers import metrics as metrics
|
||
from rowers import courses as courses
|
||
import rowers.uploads as uploads
|
||
from django.forms.formsets import formset_factory
|
||
from django.forms import modelformset_factory
|
||
|
||
from django.contrib.auth.decorators import login_required #,user_passes_test
|
||
from rowers.decorators import user_passes_test
|
||
from time import strftime,strptime,mktime,time,daylight
|
||
import os,sys
|
||
import datetime
|
||
import iso8601
|
||
import rowers.c2stuff as c2stuff
|
||
import rowers.nkstuff as nkstuff
|
||
from rowers.c2stuff import c2_open
|
||
from rowers.nkstuff import nk_open
|
||
from rowers.rp3stuff import rp3_open
|
||
from rowers.sporttracksstuff import sporttracks_open
|
||
from rowers.tpstuff import tp_open
|
||
from iso8601 import ParseError
|
||
import rowers.stravastuff as stravastuff
|
||
import rowers.garmin_stuff as garmin_stuff
|
||
from rowers.stravastuff import strava_open
|
||
import rowers.polarstuff as polarstuff
|
||
import rowers.sporttracksstuff as sporttracksstuff
|
||
|
||
|
||
import rowers.tpstuff as tpstuff
|
||
import rowers.rp3stuff as rp3stuff
|
||
import rowers.ownapistuff as ownapistuff
|
||
from rowers.ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI
|
||
from rowsandall_app.settings import (
|
||
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
|
||
STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET,
|
||
POLAR_CLIENT_ID, POLAR_REDIRECT_URI, POLAR_CLIENT_SECRET,
|
||
SPORTTRACKS_CLIENT_ID, SPORTTRACKS_REDIRECT_URI,
|
||
SPORTTRACKS_CLIENT_SECRET,
|
||
TP_CLIENT_ID,TP_REDIRECT_URI,TP_CLIENT_KEY,TP_CLIENT_SECRET,
|
||
RP3_CLIENT_ID,RP3_REDIRECT_URI,RP3_CLIENT_KEY,RP3_CLIENT_SECRET,
|
||
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
|
||
)
|
||
|
||
#from rowers.tasks_standalone import addcomment2
|
||
from django.contrib import messages
|
||
from async_messages import messages as a_messages
|
||
|
||
from django.contrib.admin.widgets import AdminDateWidget,AdminTimeWidget,AdminSplitDateTime
|
||
|
||
|
||
import requests
|
||
import json
|
||
from rest_framework.renderers import JSONRenderer
|
||
from rest_framework.parsers import JSONParser
|
||
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 (
|
||
handle_sendemail_unrecognized,handle_sendemailnewcomment,
|
||
handle_sendemailsummary,
|
||
handle_rp3_async_workout,
|
||
handle_send_template_email,
|
||
handle_send_disqualification_email,
|
||
handle_send_withdraw_email,
|
||
handle_sendemailfile,
|
||
handle_sendemailkml,
|
||
handle_sendemailnewresponse, handle_updatedps,
|
||
handle_updatecp,long_test_task,long_test_task2,
|
||
handle_zip_file,handle_getagegrouprecords,
|
||
handle_update_empower,
|
||
handle_sendemailics,
|
||
handle_sendemail_userdeleted,
|
||
handle_sendemail_raceregistration,
|
||
handle_sendemail_racesubmission,
|
||
handle_sendemail_optout,
|
||
handle_sendemail_ical,
|
||
handle_c2_async_workout,
|
||
handle_send_email_instantplan_notification,
|
||
handle_nk_async_workout,
|
||
)
|
||
|
||
from scipy.signal import savgol_filter
|
||
#from django.shortcuts import render_to_response
|
||
try:
|
||
from Cookie import SimpleCookie
|
||
except ModuleNotFoundError:
|
||
from http.cookies import SimpleCookie
|
||
from shutil import copyfile,move
|
||
import rowers.mytypes as mytypes
|
||
from rowingdata import rower as rrower
|
||
from rowingdata import main as rmain
|
||
from rowingdata import rowingdata as rrdata
|
||
from rowingdata import make_cumvalues
|
||
from rowingdata import summarydata
|
||
import pandas as pd
|
||
import numpy as np
|
||
import matplotlib.pyplot as plt
|
||
|
||
from rowers.emails import send_template_email,htmlstrip
|
||
from rowers.alerts import *
|
||
|
||
from pytz import timezone as tz,utc
|
||
from timezonefinder import TimezoneFinder
|
||
import dateutil
|
||
import mpld3
|
||
from mpld3 import plugins
|
||
import stravalib
|
||
from stravalib.exc import ActivityUploadFailed,TimeoutExceeded
|
||
from rowers.weather import get_wind_data,get_airport_code,get_metar_data
|
||
|
||
from oauth2_provider.models import Application,Grant,AccessToken
|
||
|
||
import django_rq
|
||
queue = django_rq.get_queue('default')
|
||
queuelow = django_rq.get_queue('low')
|
||
queuehigh = django_rq.get_queue('low')
|
||
|
||
import redis
|
||
import threading
|
||
from redis import StrictRedis,Redis
|
||
from rq.exceptions import NoSuchJobError
|
||
from rq.registry import StartedJobRegistry
|
||
from rq import Queue,cancel_job
|
||
|
||
from django.utils.crypto import get_random_string
|
||
|
||
from django.core.cache import cache
|
||
from django_mailbox.models import Message,Mailbox,MessageAttachment
|
||
|
||
from rules.contrib.views import permission_required, objectgetter
|
||
|
||
def get_totals(workouts):
|
||
totalseconds = 0
|
||
totalmeters = 0
|
||
|
||
for w in workouts:
|
||
totalmeters += w.distance
|
||
totalseconds += 60*(w.duration.hour*60+w.duration.minute)+w.duration.second
|
||
|
||
hours, remainder = divmod(totalseconds,3600)
|
||
minutes, seconds = divmod(remainder,60)
|
||
|
||
return totalmeters,hours, minutes, seconds
|
||
|
||
# creating shareable views
|
||
def allow_shares(view_func):
|
||
def sharify(request, *args, **kwargs):
|
||
shared = kwargs.get('__shared', None)
|
||
if shared is not None: # pragma: no cover
|
||
del kwargs["__shared"]
|
||
request.session['shared'] = True
|
||
return view_func(request, *args, **kwargs)
|
||
else: return login_required(view_func)(request, *args, **kwargs)
|
||
return sharify
|
||
|
||
class SharifyError(Exception):
|
||
pass
|
||
|
||
def sharedPage(request, key):
|
||
try:
|
||
try:
|
||
shareKey = ShareKey.objects.get(pk=key)
|
||
except: # pragma: no cover
|
||
raise SharifyError
|
||
if shareKey.expired: # pragma: no cover
|
||
raise SharifyError
|
||
func, args, kwargs = resolve(shareKey.location)
|
||
kwargs["__shared"] = True
|
||
return func(request, *args, **kwargs)
|
||
except SharifyError: # pragma: no cover
|
||
raise Http404 # or add a more detailed error page. This either means that the key doesn’t exist or is expired.
|
||
|
||
def createShareURL(request):
|
||
if request.method == 'POST':
|
||
url = request.POST['url']
|
||
ndays = int(request.POST['ndays'])
|
||
key = ShareKey.objects.create(pk=get_random_string(40),
|
||
expiration_seconds=60*60*24*ndays,
|
||
location = url)
|
||
key.save()
|
||
return render(request, 'share.html', {"key":key})
|
||
else: # pragma: no cover
|
||
raise Http404
|
||
|
||
def createShareModel(request, model_id): # pragma: no cover
|
||
task = MyModel.objects.get(pk=model_id)
|
||
key = ShareKey.objects.create(pk=get_random_string(40),
|
||
expiration_seconds=60*60*24, # 1 day
|
||
location = task.get_absolute_url(),
|
||
)
|
||
key.save()
|
||
return render(request, 'share.html', {"key":key})
|
||
|
||
# Utility to get stroke data in a JSON response
|
||
class JSONResponse(HttpResponse):
|
||
def __init__(self, data, **kwargs):
|
||
content = JSONRenderer().render(data)
|
||
kwargs['content_type'] = 'application/json'
|
||
super(JSONResponse, self).__init__(content, **kwargs)
|
||
|
||
def getfavorites(r,row):
|
||
workouttype = 'ote'
|
||
if row.workouttype in mytypes.otwtypes:
|
||
workouttype = 'otw'
|
||
|
||
matchworkouttypes = [workouttype,'all']
|
||
|
||
workoutsource = row.workoutsource
|
||
if 'speedcoach2' in row.workoutsource: # pragma: no cover
|
||
workoutsource = 'speedcoach2'
|
||
|
||
favorites = FavoriteChart.objects.filter(user=r,
|
||
workouttype__in=matchworkouttypes).order_by("id")
|
||
favorites2 = FavoriteChart.objects.filter(user=r,
|
||
workouttype__in=[workoutsource]).order_by("id")
|
||
|
||
|
||
|
||
favorites = favorites | favorites2
|
||
|
||
maxfav = len(favorites)-1
|
||
|
||
|
||
return favorites,maxfav
|
||
|
||
def get_logo_by_pk(request,*args,**kwargs): # pragma: no cover
|
||
id = kwargs['id']
|
||
return get_object_or_404(RaceLogo,pk=id)
|
||
|
||
def get_virtualevent_by_pk(request,*args,**kwargs):
|
||
id = kwargs['id']
|
||
return get_object_or_404(VirtualRace,pk=id)
|
||
|
||
def get_promember(request,*args,**kwargs): # pragma: no cover
|
||
return request.user
|
||
|
||
def get_course_by_pk(request,*args,**kwargs):
|
||
id = kwargs['id']
|
||
return get_object_or_404(GeoCourse,pk=id)
|
||
|
||
def get_workout_by_opaqueid(request,id,**kwargs):
|
||
pk = encoder.decode_hex(id)
|
||
return get_object_or_404(Workout,pk=pk)
|
||
|
||
def get_session_by_pk(request,*args,**kwargs):
|
||
id = kwargs['id']
|
||
return get_object_or_404(PlannedSession,pk=id)
|
||
|
||
def get_target_by_pk(request,*args,**kwargs):
|
||
id = kwargs['id']
|
||
return get_object_or_404(TrainingTarget,pk=id)
|
||
|
||
def get_plan_by_pk(request,*args,**kwargs):
|
||
id = kwargs['id']
|
||
return get_object_or_404(TrainingPlan,pk=id)
|
||
|
||
def get_macro_by_pk(request,*args,**kwargs): # pragma: no cover
|
||
id = kwargs['id']
|
||
return get_object_or_404(TrainingMacroCycle,pk=id)
|
||
|
||
def get_meso_by_pk(request,*args,**kwargs): # pragma: no cover
|
||
id = kwargs['id']
|
||
return get_object_or_404(TrainingMesoCycle,pk=id)
|
||
|
||
def get_micro_by_pk(request,*args,**kwargs): # pragma: no cover
|
||
id = kwargs['id']
|
||
return get_object_or_404(TrainingMicroCycle,pk=id)
|
||
|
||
def get_workout_default_page(request,id):
|
||
if request.user.is_anonymous:
|
||
return reverse('workout_view',kwargs={'id':id})
|
||
else:
|
||
r = Rower.objects.get(user=request.user)
|
||
if r.defaultlandingpage == 'workout_edit_view':
|
||
return reverse('workout_edit_view',kwargs={'id':id})
|
||
else: # pragma: no cover
|
||
return reverse('workout_workflow_view',kwargs={'id':id})
|
||
|
||
def get_user_by_userid(*args,**kwargs):
|
||
request = args[0]
|
||
try:
|
||
id = kwargs['userid']
|
||
except KeyError:
|
||
id = request.user.id
|
||
|
||
if id is not None and int(id) == 0:
|
||
id = request.user.id
|
||
|
||
u = get_object_or_404(User,pk=id)
|
||
return u
|
||
|
||
def get_user_by_id(*args,**kwargs): # pragma: no cover
|
||
request = args[0]
|
||
try:
|
||
id = args[1]
|
||
except IndexError:
|
||
try:
|
||
id = kwargs['id']
|
||
except KeyError:
|
||
id = request.user.id
|
||
|
||
return get_object_or_404(User,pk=id)
|
||
|
||
def get_rower_by_id(request,id): # pragma: no cover
|
||
u = User.objects.get(id=id)
|
||
return u.rower
|
||
|
||
def getrequestrower(request,rowerid=0,userid=0,notpermanent=False):
|
||
userid = int(userid)
|
||
rowerid = int(rowerid)
|
||
|
||
#if userid == 0:
|
||
# userid = request.user.id
|
||
|
||
if notpermanent == False:
|
||
if rowerid == 0 and 'rowerid' in request.session:
|
||
rowerid = request.session['rowerid']
|
||
|
||
if userid != 0:
|
||
rowerid = 0
|
||
|
||
try:
|
||
|
||
if rowerid != 0:
|
||
r = Rower.objects.get(id=rowerid)
|
||
u = r.user
|
||
elif userid != 0:
|
||
u = User.objects.get(id=userid)
|
||
r = getrower(u)
|
||
elif request.user.is_anonymous: # pragma: no cover
|
||
return None
|
||
else:
|
||
r = getrower(request.user)
|
||
u = r.user
|
||
|
||
|
||
except Rower.DoesNotExist: # pragma: no cover: # pragma: no cover
|
||
raise Http404("Rower doesn't exist")
|
||
|
||
if r.user == request.user:
|
||
request.session['rowerid'] = r.id
|
||
return r
|
||
|
||
if userid != 0 and not is_rower_team_member(request.user,u.rower):
|
||
request.session['rowerid'] = request.user.rower.id
|
||
raise PermissionDenied("You have no access to this user")
|
||
|
||
if notpermanent == False:
|
||
request.session['rowerid'] = r.id
|
||
|
||
request.session['rowerid'] = r.id
|
||
return r
|
||
|
||
def getrequestrowercoachee(request,rowerid=0,userid=0,notpermanent=False):
|
||
userid = int(userid)
|
||
rowerid = int(rowerid)
|
||
|
||
#if userid == 0:
|
||
# userid = request.user.id
|
||
|
||
if notpermanent == False:
|
||
if rowerid == 0 and 'rowerid' in request.session:
|
||
rowerid = request.session['rowerid']
|
||
|
||
if userid != 0:
|
||
rowerid = 0
|
||
|
||
try:
|
||
|
||
if rowerid != 0:
|
||
r = Rower.objects.get(id=rowerid)
|
||
u = r.user
|
||
elif userid != 0:
|
||
u = User.objects.get(id=userid)
|
||
r = getrower(u)
|
||
elif request.user.is_anonymous: # pragma: no cover
|
||
return None
|
||
else:
|
||
r = getrower(request.user)
|
||
u = r.user
|
||
|
||
|
||
except Rower.DoesNotExist: # pragma: no cover: # pragma: no cover
|
||
raise Http404("Rower doesn't exist")
|
||
|
||
if r.user == request.user:
|
||
request.session['rowerid'] = r.id
|
||
return r
|
||
|
||
if userid != 0 and not is_coach_user(request.user,u): # pragma: no cover
|
||
request.session['rowerid'] = request.user.rower.id
|
||
raise PermissionDenied("You have no access to this user")
|
||
|
||
if notpermanent == False:
|
||
request.session['rowerid'] = r.id
|
||
|
||
request.session['rowerid'] = r.id
|
||
return r
|
||
|
||
def getrequestplanrower(request,rowerid=0,userid=0,notpermanent=False):
|
||
|
||
userid = int(userid)
|
||
rowerid = int(rowerid)
|
||
|
||
if notpermanent == False:
|
||
if rowerid == 0 and 'rowerid' in request.session:
|
||
rowerid = request.session['rowerid']
|
||
|
||
if userid != 0:
|
||
rowerid = 0
|
||
|
||
try:
|
||
|
||
if rowerid != 0:
|
||
r = Rower.objects.get(id=rowerid)
|
||
elif userid != 0:
|
||
try:
|
||
u = User.objects.get(id=userid)
|
||
except User.DoesNotExist: # pragma: no cover: # pragma: no cover
|
||
raise Http404("User does not exist")
|
||
r = getrower(u)
|
||
else:
|
||
r = getrower(request.user)
|
||
|
||
except Rower.DoesNotExist: # pragma: no cover: # pragma: no cover
|
||
raise Http404("Rower doesn't exist")
|
||
|
||
if 'shared' in request.session and request.session['shared']: # pragma: no cover
|
||
return r
|
||
|
||
if r.user != request.user and not can_plan_user(request.user,r ):
|
||
request.session['rowerid'] = r.id
|
||
raise PermissionDenied("You have no access to this user")
|
||
|
||
if notpermanent == False:
|
||
request.session['rowerid'] = r.id
|
||
|
||
return r
|
||
|
||
|
||
def getrower(user):
|
||
try:
|
||
if user is None or user.is_anonymous:
|
||
return None
|
||
except AttributeError: # pragma: no cover
|
||
if User.objects.get(id=user).is_anonymous:
|
||
return None
|
||
try:
|
||
r = Rower.objects.get(user=user)
|
||
except Rower.DoesNotExist: # pragma: no cover:
|
||
r = Rower(user=user)
|
||
r.save()
|
||
|
||
return r
|
||
|
||
|
||
def get_workout(id):
|
||
try:
|
||
id = encoder.decode_hex(id)
|
||
w = Workout.objects.get(id=id)
|
||
except Workout.DoesNotExist: # pragma: no cover:
|
||
raise Http404("Workout doesn't exist")
|
||
|
||
return w
|
||
|
||
def get_workoutuser(id,request):
|
||
try:
|
||
id = encoder.decode_hex(id)
|
||
w = Workout.objects.get(id=id)
|
||
except Workout.DoesNotExist: # pragma: no cover:
|
||
raise Http404("Workout doesn't exist")
|
||
|
||
if not is_workout_user(request.user,w): # pragma: no cover
|
||
raise PermissionDenied
|
||
|
||
return w
|
||
|
||
def getvalue(data): # pragma: no cover
|
||
perc = 0
|
||
total = 1
|
||
done = 0
|
||
id = 0
|
||
session_key = 'noot'
|
||
for i in data.iteritems():
|
||
if i[0] == 'total':
|
||
total = float(i[1])
|
||
if i[0] == 'done':
|
||
done = float(i[1])
|
||
if i[0] == 'id':
|
||
id = i[1]
|
||
if i[0] == 'session_key':
|
||
session_key = i[1]
|
||
|
||
return total,done,id,session_key
|
||
|
||
class SessionTaskListener(threading.Thread): # pragma: no cover
|
||
def __init__(self, r, channels):
|
||
threading.Thread.__init__(self)
|
||
self.redis = r
|
||
self.pubsub = self.redis.pubsub()
|
||
self.pubsub.subscribe(channels)
|
||
|
||
def work(self, item):
|
||
|
||
try:
|
||
data = json.loads(item['data'])
|
||
total,done,id,session_key = getvalue(data)
|
||
perc = int(100.*done/total)
|
||
cache.set(id,perc,3600)
|
||
|
||
except TypeError:
|
||
pass
|
||
|
||
def run(self):
|
||
for item in self.pubsub.listen():
|
||
if item['data'] == "KILL":
|
||
self.pubsub.unsubscribe()
|
||
break
|
||
else:
|
||
self.work(item)
|
||
|
||
|
||
queuefailed = Queue("failed",connection=Redis())
|
||
redis_connection = StrictRedis()
|
||
r = Redis()
|
||
|
||
# this doesn't yet work on production
|
||
#if settings.DEBUG:
|
||
# client = SessionTaskListener(r,['tasks'])
|
||
# client.start()
|
||
|
||
|
||
rq_registry = StartedJobRegistry(queue.name,connection=redis_connection)
|
||
rq_registryhigh = StartedJobRegistry(queuehigh.name,connection=redis_connection)
|
||
rq_registrylow = StartedJobRegistry(queuelow.name,connection=redis_connection)
|
||
|
||
from rq.job import Job
|
||
|
||
try:
|
||
from rest_framework_swagger.views import get_swagger_view
|
||
except ImportError: # pragma: no cover
|
||
pass
|
||
|
||
from rest_framework.renderers import JSONRenderer
|
||
from rest_framework.parsers import JSONParser
|
||
from rest_framework.response import Response
|
||
from rowers.serializers import RowerSerializer,WorkoutSerializer
|
||
try:
|
||
from rest_framework import status,permissions,generics
|
||
except ImportError: # pragma: no cover
|
||
pass
|
||
|
||
from rest_framework.decorators import api_view, renderer_classes, permission_classes
|
||
from rest_framework.permissions import IsAuthenticated
|
||
|
||
|
||
from rowers.permissions import IsOwnerOrNot, IsCompetitorOrNot
|
||
|
||
import rowers.plots as plots
|
||
import rowers.mailprocessing as mailprocessing
|
||
|
||
from io import BytesIO
|
||
from scipy.special import lambertw
|
||
|
||
from rowers.dataprep import timedeltaconv
|
||
from rowers.dataprep import getsmallrowdata_db
|
||
|
||
from scipy.interpolate import griddata
|
||
|
||
#LOCALTIMEZONE = tz('Etc/UTC')
|
||
USER_LANGUAGE = 'en-US'
|
||
|
||
from rowers.interactiveplots import *
|
||
from rowers.celery import result as celery_result
|
||
|
||
# Define the API documentation
|
||
try:
|
||
schema_view = get_swagger_view(title='Rowsandall API')
|
||
except NameError: # pragma: no cover
|
||
pass
|
||
|
||
def remove_asynctask(request,id): # pragma: no cover
|
||
try:
|
||
oldtasks = request.session['async_tasks']
|
||
except KeyError:
|
||
oldtasks = []
|
||
|
||
newtasks = []
|
||
for task in oldtasks:
|
||
if id not in task[0]:
|
||
newtasks += [(task[0],task[1])]
|
||
|
||
request.session['async_tasks'] = newtasks
|
||
|
||
def get_job_result(jobid): # pragma: no cover
|
||
if settings.TESTING:
|
||
return None
|
||
elif settings.CELERY:
|
||
result = celery_result.AsyncResult(jobid).result
|
||
else:
|
||
running_job_ids = rq_registry.get_job_ids()
|
||
if len(running_job_ids) and jobid in running_job_ids:
|
||
# job is running
|
||
return None
|
||
else:
|
||
# job is ready
|
||
try:
|
||
job = Job.fetch(jobid,connection=redis_connection)
|
||
result = job.result
|
||
except NoSuchJobError:
|
||
return None
|
||
|
||
return result
|
||
|
||
verbose_job_status = {
|
||
'updatecp': 'Critical Power Calculation for Ergometer Workouts',
|
||
'updatecpwater': 'Critical Power Calculation for OTW Workouts',
|
||
'otwsetpower': 'Rowing Physics OTW Power Calculation',
|
||
'agegrouprecords': 'Calculate age group records',
|
||
'make_plot': 'Create static chart',
|
||
'long_test_task': 'Long Test Task',
|
||
'long_test_task2': 'Long Test Task 2',
|
||
'update_empower': 'Correct Empower Inflated Power Bug',
|
||
'submit_race': 'Checking Race Course Result',
|
||
'check_race_course': 'Checking Course Result',
|
||
}
|
||
|
||
def get_job_status(jobid): # pragma: no cover
|
||
if settings.TESTING:
|
||
summary = {
|
||
'status': 'failed',
|
||
'result': 0,
|
||
'finished': True,
|
||
'failed': True,
|
||
'started_at':None,
|
||
}
|
||
return summary
|
||
elif settings.CELERY:
|
||
job = celery_result.AsyncResult(jobid)
|
||
jobresult = job.result
|
||
|
||
|
||
if 'fail' in job.status.lower():
|
||
jobresult = '0'
|
||
summary = {
|
||
'status': job.status,
|
||
'result': jobresult,
|
||
'started_at': None
|
||
}
|
||
else:
|
||
try:
|
||
job = Job.fetch(jobid,connection=redis_connection)
|
||
try:
|
||
status = job.status
|
||
except AttributeError:
|
||
status = job.get_status()
|
||
|
||
summary = {
|
||
'status':status,
|
||
'result':job.result,
|
||
'started_at':job.started_at
|
||
}
|
||
except NoSuchJobError:
|
||
summary = {
|
||
'status': 'success',
|
||
'result': 1,
|
||
'started_at':None,
|
||
}
|
||
|
||
try:
|
||
if 'fail' in summary['status'].lower():
|
||
summary['failed'] = True
|
||
else:
|
||
summary['failed'] = False
|
||
|
||
if 'success' in summary['status'].lower():
|
||
summary['finished'] = True
|
||
elif 'finished' in summary['status'].lower():
|
||
summary['finished'] = True
|
||
else:
|
||
summary['finished'] = False
|
||
except AttributeError:
|
||
summary = {
|
||
'status': 'failed',
|
||
'result': 0,
|
||
'finished': True,
|
||
'failed': True,
|
||
'started_at':None,
|
||
}
|
||
|
||
return summary
|
||
|
||
def kill_async_job(request,id='aap'): # pragma: no cover
|
||
if settings.CELERY:
|
||
job = celery_result.AsyncResult(id)
|
||
job.revoke()
|
||
else:
|
||
try:
|
||
cancel_job(id,connection=redis_connection)
|
||
except NoSuchJobError:
|
||
pass
|
||
|
||
remove_asynctask(request,id)
|
||
cache.delete(id)
|
||
url = reverse(session_jobs_status)
|
||
|
||
return HttpResponseRedirect(url)
|
||
|
||
@login_required()
|
||
def raise_500(request): # pragma: no cover
|
||
if request.user.is_superuser:
|
||
raise ValueError
|
||
else:
|
||
return HttpResponse("invalid")
|
||
|
||
#@login_required()
|
||
#def test_job_view(request,aantal=100):
|
||
#
|
||
# session_key = request.session._session_key
|
||
#
|
||
# job = myqueue(queuehigh,long_test_task,int(aantal),
|
||
# session_key=session_key)
|
||
#
|
||
#
|
||
# try:
|
||
# request.session['async_tasks'] += [(job.id,'long_test_task')]
|
||
# except KeyError:
|
||
# request.session['async_tasks'] = [(job.id,'long_test_task')]
|
||
#
|
||
# url = reverse(session_jobs_status)
|
||
#
|
||
# return HttpResponseRedirect(url)
|
||
|
||
#@login_required()
|
||
#def test_job_view2(request,aantal=100):
|
||
#
|
||
#
|
||
# job = myqueue(queuehigh,long_test_task2,int(aantal),
|
||
# secret=settings.PROGRESS_CACHE_SECRET)#
|
||
#
|
||
#
|
||
# try:
|
||
# request.session['async_tasks'] += [(job.id,'long_test_task2')]
|
||
# except KeyError:
|
||
# request.session['async_tasks'] = [(job.id,'long_test_task2')]
|
||
#
|
||
# url = reverse(session_jobs_status)
|
||
#
|
||
# return HttpResponseRedirect(url)
|
||
|
||
@csrf_exempt
|
||
def post_progress(request,id=None,value=0): # pragma: no cover
|
||
if request.method == 'POST':
|
||
try:
|
||
secret = request.POST['secret']
|
||
except KeyError:
|
||
return HttpResponse('Access Denied',status=401)
|
||
if secret == settings.PROGRESS_CACHE_SECRET:
|
||
if not id:
|
||
try:
|
||
id = request.POST['id']
|
||
except KeyError:
|
||
return HttpResponse('Invalid request',400)
|
||
try:
|
||
value = request.POST['value']
|
||
except KeyError:
|
||
pass
|
||
|
||
cache.set(id,value,3600)
|
||
# test
|
||
result = cache.get(id)
|
||
|
||
return HttpResponse('progress cached '+str(result),
|
||
status=201)
|
||
else: # secret not given
|
||
return HttpResponse('access denied',status=401)
|
||
|
||
else: # request method is not POST
|
||
return HttpResponse('GET method not allowed',status=405)
|
||
|
||
def get_all_queued_jobs(userid=0): # pragma: no cover
|
||
r = StrictRedis()
|
||
|
||
jobs = []
|
||
|
||
celerykeys = r.keys('celery*')
|
||
for key in celerykeys:
|
||
id= key[17:]
|
||
job = celery_result.AsyncResult(id)
|
||
jobresult = job.result
|
||
if 'fail' in job.status.lower():
|
||
jobresult = '0'
|
||
jobs.append(
|
||
(id,{
|
||
'status':job.status,
|
||
'result':jobresult,
|
||
'function':'',
|
||
'meta':job.info,
|
||
}))
|
||
|
||
ids = [j.id for j in queue.jobs]
|
||
ids += [j.id for j in queuehigh.jobs]
|
||
ids += [j.id for j in queuelow.jobs]
|
||
ids += [j.id for j in queuefailed.jobs]
|
||
|
||
|
||
for id in ids:
|
||
job = Job.fetch(id,connection=redis_connection)
|
||
jobs.append(
|
||
(id,{
|
||
'status':job.get_status(),
|
||
'result':job.result,
|
||
'function':job.func_name,
|
||
'meta':job.meta,
|
||
}))
|
||
|
||
return jobs
|
||
|
||
def get_stored_tasks_status(request):
|
||
try:
|
||
taskids = request.session['async_tasks']
|
||
except KeyError:
|
||
taskids = []
|
||
|
||
taskstatus = []
|
||
for id,func_name in reversed(taskids): # pragma: no cover
|
||
progress = 0
|
||
try:
|
||
cached_progress = cache.get(id)
|
||
if cached_progress is not None:
|
||
cached_progress = int(cached_progress)
|
||
else:
|
||
cached_progress = 0
|
||
except ValueError:
|
||
cached_progress = 0
|
||
finished = get_job_status(id)['finished']
|
||
if finished:
|
||
cache.set(id,100)
|
||
progress = 100
|
||
elif cached_progress is not None and cached_progress>0:
|
||
progress = cached_progress
|
||
else:
|
||
progress = 0
|
||
|
||
this_task_status = {
|
||
'id':id,
|
||
'status':get_job_status(id)['status'],
|
||
'failed':get_job_status(id)['failed'],
|
||
'finished':get_job_status(id)['finished'],
|
||
'func_name':func_name,
|
||
'verbose': verbose_job_status[func_name],
|
||
'progress': progress,
|
||
}
|
||
|
||
taskstatus.append(this_task_status)
|
||
|
||
|
||
return taskstatus
|
||
|
||
@permission_required('workout.change_workout',fn=get_workout_by_opaqueid,raise_exception=True)
|
||
def get_thumbnails(request,id):
|
||
row = get_workout_by_opaqueid(request,id)
|
||
|
||
|
||
r = getrower(request.user)
|
||
result = request.user.is_authenticated and ispromember(request.user)
|
||
if result:
|
||
promember=1
|
||
if request.user == row.user.user:
|
||
mayedit=1
|
||
|
||
comments = WorkoutComment.objects.filter(workout=row)
|
||
|
||
aantalcomments = len(comments)
|
||
|
||
favorites,maxfav = getfavorites(r,row)
|
||
|
||
charts = []
|
||
|
||
charts = thumbnails_set(r,encoder.decode_hex(id),favorites)
|
||
try:
|
||
if charts[0]['script'] == '':
|
||
charts = []
|
||
except IndexError: # pragma: no cover
|
||
charts = []
|
||
|
||
|
||
return JSONResponse(charts)
|
||
|
||
def get_blog_posts(request): # pragma: no cover
|
||
blogposts = BlogPost.objects.all().order_by("-date")
|
||
|
||
jsondata = []
|
||
|
||
if blogposts:
|
||
for blogpost in blogposts[0:3]:
|
||
thedict = {
|
||
'title':blogpost.title,
|
||
'link':blogpost.link,
|
||
}
|
||
|
||
jsondata.append(thedict)
|
||
|
||
return JSONResponse(jsondata)
|
||
|
||
def get_blog_posts_old(request): # pragma: no cover
|
||
try:
|
||
response = requests.get(
|
||
'https://analytics.rowsandall.com/wp-json/wp/v2/posts?per_page=3')
|
||
if response.status_code == 200:
|
||
blogs_json = response.json()
|
||
# with open('blogs.txt','w') as o:
|
||
# o.write(json.dumps(blogs_json,indent=2,sort_keys=True))
|
||
else:
|
||
blogs_json = []
|
||
except ConnectionError:
|
||
pass
|
||
|
||
blogposts = []
|
||
|
||
|
||
for postdata in blogs_json[0:3]:
|
||
|
||
try:
|
||
title = postdata['title']['rendered'].encode(
|
||
'ascii','xmlcharrefreplace')
|
||
|
||
except TypeError:
|
||
title = postdata['title']['rendered'].encode(
|
||
'ascii','xmlcharrefreplace').decode('utf-8')
|
||
|
||
|
||
thedict = {
|
||
'title': title,
|
||
# 'author': '',
|
||
# 'image': image_url,
|
||
# 'excerpt': excerpt_first,
|
||
'link': postdata['link'],
|
||
}
|
||
|
||
blogposts.append(thedict)
|
||
|
||
return JSONResponse(blogposts)
|
||
|
||
|
||
|
||
@login_required()
|
||
def session_jobs_view(request):
|
||
taskstatus = get_stored_tasks_status(request)
|
||
|
||
return HttpResponse(json.dumps(taskstatus))
|
||
|
||
@login_required()
|
||
def session_jobs_status(request):
|
||
taskstatus = get_stored_tasks_status(request)
|
||
|
||
return render(request,
|
||
'async_tasks.html',
|
||
{'taskstatus':taskstatus})
|
||
|
||
# Test if row data include candidates
|
||
def rowhascoordinates(row):
|
||
# create interactive plot
|
||
f1 = row.csvfilename
|
||
u = row.user.user
|
||
r = getrower(u)
|
||
rowdata = rdata(csvfile=f1)
|
||
hascoordinates = 1
|
||
if rowdata != 0:
|
||
try:
|
||
latitude = rowdata.df[' latitude']
|
||
|
||
if not latitude.std(): # pragma: no cover
|
||
hascoordinates = 0
|
||
except (KeyError,AttributeError):
|
||
hascoordinates = 0
|
||
|
||
else: # pragma: no cover
|
||
hascoordinates = 0
|
||
|
||
return hascoordinates
|
||
|
||
|
||
# Wrapper around the rowingdata call to catch some exceptions
|
||
# Checks for CSV file, then for gzipped CSV file, and if all fails, returns 0
|
||
def rdata(csvfile=None,rower=rrower()):
|
||
if csvfile is None: # pragma: no cover
|
||
return 0
|
||
try:
|
||
res = rrdata(csvfile=csvfile,rower=rower)
|
||
except pd.errors.EmptyDataError: # pragma: no cover
|
||
res = 0
|
||
except (IOError, IndexError, EOFError,FileNotFoundError): # pragma: no cover
|
||
try:
|
||
res = rrdata(csvfile=file+'.gz',rower=rower)
|
||
except (IOError, IndexError, EOFError,FileNotFoundError):
|
||
res = 0
|
||
|
||
return res
|
||
|
||
# Query to get teams managed and member of
|
||
def get_my_teams(user):
|
||
try:
|
||
therower = Rower.objects.get(user=user)
|
||
try:
|
||
teams1 = therower.team.all()
|
||
except AttributeError: # pragma: no cover
|
||
teams1 = []
|
||
|
||
teams2 = Team.objects.filter(manager=user)
|
||
teams = list(set(teams1).union(set(teams2)))
|
||
except TypeError:
|
||
teams = []
|
||
|
||
return teams
|
||
|
||
# Used for the interval editor - translates seconds to a time object
|
||
def get_time(second): # pragma: no cover
|
||
if (second<=0) or (second>1e9):
|
||
hours = 0
|
||
minutes=0
|
||
sec=0
|
||
microsecond = 0
|
||
elif math.isnan(second): # pragma: no cover
|
||
hours = 0
|
||
minutes=0
|
||
sec=0
|
||
microsecond = 0
|
||
else:
|
||
days = int(second/(24.*3600.)) % (24*3600)
|
||
hours = int((second-24.*3600.*days)/3600.) % 24
|
||
minutes = int((second-3600.*(hours+24.*days))/60.) % 60
|
||
sec = int(second-3600.*(hours+24.*days)-60.*minutes) % 60
|
||
microsecond = int(1.0e6*(second-3600.*(hours+24.*days)-60.*minutes-sec))
|
||
return datetime.time(hours,minutes,sec,microsecond)
|
||
|
||
|
||
# get the workout ID from the SportTracks URI
|
||
def getidfromsturi(uri,length=8): # pragma: no cover
|
||
return uri[len(uri)-length:]
|
||
|
||
import re
|
||
|
||
def getidfromuri(uri): # pragma: no cover
|
||
m = re.search('/(\w.*)\/(\d+)',uri)
|
||
return m.group(2)
|
||
|
||
|
||
|
||
from rowers.utils import (
|
||
geo_distance,serialize_list,deserialize_list,uniqify,
|
||
str2bool,range_to_color_hex,absolute,myqueue,get_call,
|
||
calculate_age,rankingdistances,rankingdurations,
|
||
my_dict_from_instance,wavg,NoTokenError,
|
||
request_is_ajax,dologging
|
||
)
|
||
|
||
import rowers.datautils as datautils
|
||
|
||
# Check if a user is a Coach member
|
||
def iscoachmember(user):
|
||
if not user.is_anonymous:
|
||
try:
|
||
r = Rower.objects.get(user=user)
|
||
except Rower.DoesNotExist: # pragma: no cover:
|
||
r = Rower(user=user)
|
||
r.save()
|
||
|
||
result = user.is_authenticated and ('coach' in r.rowerplan)
|
||
else: # pragma: no cover
|
||
result = False
|
||
|
||
return result
|
||
|
||
|
||
from rowers.utils import ProcessorCustomerError
|
||
|
||
|
||
# More User/Rower utils
|
||
def add_defaultfavorites(r):
|
||
for c in defaultfavoritecharts:
|
||
f = FavoriteChart(user=r,
|
||
yparam1=c['yparam1'],
|
||
yparam2=c['yparam2'],
|
||
xparam=c['xparam'],
|
||
plottype=c['plottype'],
|
||
workouttype=c['workouttype'],
|
||
reststrokes=c['reststrokes'],
|
||
notes=c['notes'])
|
||
|
||
f.save()
|
||
return 1
|
||
|
||
|
||
# Shows email form and sends it if submitted
|
||
def sendmail(request):
|
||
if request.method == 'POST':
|
||
# test recaptcha
|
||
response_string = request.POST.get('g-recaptcha-response')
|
||
# replace below with settings
|
||
recaptcha_secret = RECAPTCHA_SITE_SECRET
|
||
url = 'https://www.google.com/recaptcha/api/siteverify'
|
||
data = {
|
||
'secret':recaptcha_secret,
|
||
'response': response_string,
|
||
}
|
||
response = requests.post(url,data=data,verify=True)
|
||
success = False
|
||
if response.status_code == 200:
|
||
success = response.json().get('success')
|
||
|
||
form = EmailForm(request.POST)
|
||
if form.is_valid() and success: # pragma: no cover
|
||
firstname = form.cleaned_data['firstname']
|
||
lastname = form.cleaned_data['lastname']
|
||
email = form.cleaned_data['email']
|
||
subject = 'Rowsandall Contact Form:'+form.cleaned_data['subject']
|
||
message = form.cleaned_data['message']
|
||
fullemail = firstname + " " + lastname + " " + "<" + email + ">"
|
||
try:
|
||
send_mail(subject, message, fullemail, ['info@rowsandall.com'])
|
||
except:
|
||
messages.error(request,"Something went wrong trying to send the email")
|
||
return HttpResponseRedirect('/rowers/email/thankyou/')
|
||
else:
|
||
if not success:
|
||
messages.error(request,'Bots are not welcome')
|
||
else: # pragma: no cover
|
||
messages.error(request,'Something went wrong. Please try again')
|
||
return HttpResponseRedirect('/rowers/email/')
|
||
else:
|
||
return HttpResponseRedirect('/rowers/email/')
|
||
|
||
|
||
def keyvalue_get_default(key,options,def_options): # pragma: no cover
|
||
|
||
try:
|
||
return options[key]
|
||
except KeyError:
|
||
return def_options[key]
|
||
|
||
|
||
|
||
|
||
# Creates unix time stamp from a datetime object
|
||
def totimestamp(dt, epoch=datetime.datetime(1970,1,1,tzinfo=tz('UTC'))): # pragma: no cover
|
||
td = dt - epoch
|
||
# return td.total_seconds()
|
||
return (td.microseconds + (td.seconds + td.days * 86400) * 10**6) / 10**6
|
||
# Check if a column of a dataframe has the required (aantal)
|
||
# number of elements. Also checks if the column is a numerical type
|
||
# Replaces any faulty columns with zeros
|
||
def trydf(df,aantal,column): # pragma: no cover
|
||
try:
|
||
s = df[column]
|
||
if len(s) != aantal:
|
||
return np.zeros(aantal)
|
||
if not np.issubdtype(s,np.number):
|
||
return np.zeros(aantal)
|
||
except KeyError:
|
||
s = np.zeros(aantal)
|
||
|
||
return s
|
||
|
||
import rowers.teams as teams
|
||
from rowers.models import C2WorldClassAgePerformance
|