from rowers.views.statements import * from rowers.tasks import handle_calctrimp from rowers.opaque import encoder from rowers.courses import coursetokml, coursestokml #from xml.etree import ElementTree as ET from defusedxml import ElementTree as ET import arrow import pendulum from pendulum.parsing.exceptions import ParserError from rowsandall_app.settings import UPLOAD_SERVICE_SECRET, UPLOAD_SERVICE_URL from rowers.dataroutines import get_workouttype_from_tcx, get_startdate_time_zone from rest_framework.decorators import parser_classes from rest_framework.parsers import BaseParser from rowers.utils import geo_distance from datetime import datetime as dt import rowingdata.tcxtools as tcxtools from rowingdata import TCXParser, rowingdata from rowingdata import FITParser as FP import arrow import base64 # create a FITParser which parses the application/octet-stream and creates a fit file class FITParser(BaseParser): media_type = "application/octet-stream" def parse(self, stream, media_type=None, parser_context=None): try: return stream.read() except Exception as e: dologging("apilog.log", "FIT Parser") dologging("apilog.log", e) raise ValueError(f"Failed to read FIT file: {str(e)}") return stream.read() class XMLParser(BaseParser): media_type = "application/xml" def parse(self, stream, media_type=None, parser_context=None): dologging("apilog.log", "XML Parser") try: s = ET.parse(stream).getroot() except ET.XMLSyntaxError: raise ValueError("XML Syntax Error") except Exception as e: # pragma: no cover dologging("apilog.log",e) raise ValueError(f"Failed to parse XML file: {str(e)}") return s # Stroke data form to test API upload @csrf_exempt def javascript_log(request): if request.method != 'POST': message = {'status': 'false', 'message': 'this view cannot be accessed through GET'} return JSONResponse(status=403, data=message) # test if JSON try: json_data = json.loads(request.body) secret = json_data['secret'] post_data = json_data except: q = request.POST post_data = {k: q.getlist(k) if len( q.getlist(k)) > 1 else v for k, v in q.items()} # only allow local host hostt = request.get_host().split(':') if hostt[0] not in ['localhost', '127.0.0.1', 'dev.rowsandall.com', 'rowsandall.com']: message = {'status': 'false', 'message': 'permission denied for host '+hostt[0]} return JSONResponse(status=403, data=message) # check credentials here try: secret = post_data['secret'] except KeyError: dologging('own_api.log','Missing credentials') message = {'status': 'false', 'message': 'missing credentials'} return JSONResponse(status=400, data=message) if secret != settings.LOG_SECRET: message = {'status': 'false', 'message': 'invalid credentials'} return JSONResponse(status=403, data=message) try: message = post_data['message'] except KeyError: dologging('javascript_log.log','no message received') message = {'status': 'false', 'message': 'no filename given'} return JSONResponse(status=400, data=message) dologging('javascript_log.log', message) return JSONResponse(status=200, data = {'status': 'true', 'message': message}) @login_required() def strokedataform(request, id=0): id = encoder.decode_hex(id) try: w = Workout.objects.get(id=id) except Workout.DoesNotExist: # pragma: no cover raise Http404("Workout doesn't exist") if request.method == 'GET': form = StrokeDataForm() return render(request, 'strokedata_form.html', { 'form': form, 'teams': get_my_teams(request.user), 'id': id, 'workout': w, }) elif request.method == 'POST': # pragma: no cover form = StrokeDataForm() return render(request, 'strokedata_form.html', { 'form': form, 'teams': get_my_teams(request.user), 'id': id, 'workout': w, }) # pragma: no cover def api_get_dataframe(startdatetime, df): try: time = df['time']/1.e3 except (KeyError, ColumnNotFoundError): # pragma: no cover try: time = df['t']/10. except (KeyError, ColumnNotFoundError): return 400, "Missing time", pl.DataFrame() try: spm = df['spm'] except (KeyError, ColumnNotFoundError): # pragma: no cover return 400, "Missing spm", pl.DataFrame() try: distance = df['distance'] except (KeyError, ColumnNotFoundError): # pragma: no cover try: distance = df['d']/10. except (KeyError, ColumnNotFoundError): return 400, "Missing distance", pl.DataFrame() try: pace = df['pace']/1.e3 except (KeyError, ColumnNotFoundError): # pragma: no cover try: pace = df['p']/10. except (KeyError, ColumnNotFoundError): return 400, "Missing pace", pl.DataFrame() try: power = df['power'] except (KeyError, ColumnNotFoundError): # pragma: no cover power = 0*time try: drivelength = df['drivelength'] except (KeyError, ColumnNotFoundError): drivelength = 0*time try: dragfactor = df['dragfactor'] except (KeyError, ColumnNotFoundError): dragfactor = 0*time try: drivetime = df['drivetime'] except (KeyError, ColumnNotFoundError): drivetime = 0*time try: strokerecoverytime = df['strokerecoverytime'] except (KeyError, ColumnNotFoundError): strokerecoverytime = 0*time try: averagedriveforce = df['averagedriveforce'] except (KeyError, ColumnNotFoundError): averagedriveforce = 0*time try: peakdriveforce = df['peakdriveforce'] except (KeyError, ColumnNotFoundError): peakdriveforce = 0*time try: wash = df['wash'] except (KeyError, ColumnNotFoundError): wash = 0*time try: catch = df['catch'] except (KeyError, ColumnNotFoundError): catch = 0*time try: finish = df['finish'] except (KeyError, ColumnNotFoundError): finish = 0*time try: peakforceangle = df['peakforceangle'] except (KeyError, ColumnNotFoundError): peakforceangle = 0*time try: driveenergy = df['driveenergy'] except (KeyError, ColumnNotFoundError): driveenergy = 60.*power/spm try: slip = df['slip'] except (KeyError, ColumnNotFoundError): slip = 0*time try: lapidx = df['lapidx'] except (KeyError, ColumnNotFoundError): lapidx = 0*time try: hr = df['hr'] except (KeyError, ColumnNotFoundError): # pragma: no cover hr = 0*df['time'] try: latitude = df['latitude'] except (KeyError, ColumnNotFoundError): latitude = 0*df['time'] try: longitude = df['longitude'] except (KeyError, ColumnNotFoundError): longitude = 0*df['time'] starttime = totimestamp(startdatetime)+time[0] unixtime = starttime+time dologging('apilog.log',"(strokedatajson_v2/3 POST - data parsed)") data = pl.DataFrame({'TimeStamp (sec)': unixtime, ' Horizontal (meters)': distance, ' Cadence (stokes/min)': spm, ' HRCur (bpm)': hr, ' DragFactor': dragfactor, ' Stroke500mPace (sec/500m)': pace, ' Power (watts)': power, ' DriveLength (meters)': drivelength, ' DriveTime (ms)': drivetime, ' StrokeRecoveryTime (ms)': strokerecoverytime, ' AverageDriveForce (lbs)': averagedriveforce, ' PeakDriveForce (lbs)': peakdriveforce, ' lapIdx': lapidx, ' ElapsedTime (sec)': time, 'catch': catch, 'slip': slip, 'finish': finish, 'wash': wash, 'driveenergy': driveenergy, 'peakforceangle': peakforceangle, ' latitude': latitude, ' longitude': longitude, }) return 200, "Success", data @login_required() def strokedataform_v2(request, id=0): id = encoder.decode_hex(id) try: w = Workout.objects.get(id=id) except Workout.DoesNotExist: # pragma: no cover raise Http404("Workout doesn't exist") if request.method == 'GET': form = StrokeDataForm() return render(request, 'strokedata_form_v2.html', { 'form': form, 'teams': get_my_teams(request.user), 'id': id, 'workout': w, }) elif request.method == 'POST': # pragma: no cover form = StrokeDataForm() return render(request, 'strokedata_form_v2.html', { 'form': form, 'teams': get_my_teams(request.user), 'id': id, 'workout': w, }) # pragma: no cover def part_of_day(hour): if 5 <= hour < 12: return "Morning" elif 12 <= hour < 18: # pragma: no cover return "Afternoon" elif 18 <= hour < 24: # pragma: no cover return "Evening" else: # pragma: no cover return "Night" # KML API views """ - Get a list of courses OK - Nearby a certain coordinate - Filtered by - Get a (KML) course (in response.content rather than as attachment) OK - Get multiple courses as one KML in response.content OK - GET with parameters? Optional, not for CN - Create one or more new courses from KML - Should check for duplicates (Placemark ID) - Update one or more new courses from KML """ @api_view(["GET"]) @permission_classes([IsAuthenticated]) def course_list(request): if request.method != 'GET': # pragma: no cover dologging('apilog.log','{m} request to KML endpoint'.format(m=request.method)) return HttpResponseNotAllowed("Method not supported") query_data = {} name = request.GET.get('name') distance = request.GET.get('course_distance') latitude = request.GET.get('latitude') longitude = request.GET.get('longitude') distance_from = request.GET.get('distance_from') if name is not None: query_data['name__contains'] = name if distance is not None: try: query_data['distance__lte'] = int(distance)+50 query_data['distance__gte'] = int(distance)-50 except: pass courses = GeoCourse.objects.filter(**query_data) if latitude is not None and longitude is not None and distance_from is not None: try: newlist = [] for c in courses: distance = geo_distance(float(latitude), float(longitude), c.coord[0], c.coord[1])[0] if distance < float(distance_from): # pragma: no cover newlist.append(c) courses = newlist except ValueError: pass courselist = [] for c in courses: d = { 'id': c.id, 'name':c.name, 'country':c.country, 'distance':c.distance, 'notes':c.notes, 'location':(c.coord[0], c.coord[1]), } courselist.append(d) response_dict = {'courses': courselist} return JsonResponse(response_dict, content_type='application/json; charset=utf8') @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_crewnerd_kml(request,id=0): if request.method != 'GET': # pragma: no cover dologging('apilog.log','{m} request to CrewNerd KML endpoint'.format(m=request.method)) return HttpResponseNotAllowed("Method not supported") try: c = GeoCourse.objects.get(id=id) except GeoCourse.DoesNotExist: # pragma: no cover raise Http404("This course does not exist") kml = coursetokml(c, cn=True) return HttpResponse(kml, content_type='text/xml') @csrf_exempt @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_crewnerd_multiple(request): if request.method != 'GET': # pragma: no cover dologging('apilog.log','{m} request to CrewNerd KML endpoint'.format(m=request.method)) return HttpResponseNotAllowed("Method not supported") ids = request.GET.get('id') if ids is not None: tdict = dict(request.GET.lists()) idsnew = [] for id in tdict['id']: try: idsnew.append(int(id)) except ValueError: pass ids = idsnew else: gcs = GeoCourse.objects.all() ids = [c.id for c in gcs] kml = coursestokml(ids, cn=True) return HttpResponse(kml, content_type='text/xml') @csrf_exempt @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_crewnerd_liked(request): r = getrower(request.user) if request.method != 'GET': # pragma: no cover dologging('apilog.log','{m} request to CrewNerd KML endpoint'.format(m=request.method)) return HttpResponseNotAllowed("Method not supported") ids = request.GET.get('id') if ids is not None: tdict = dict(request.GET.lists()) idsnew = [] for id in tdict['id']: try: idsnew.append(int(id)) except ValueError: pass ids = idsnew else: gcs = GeoCourse.objects.filter(followers=r) ids = [c.id for c in gcs] kml = coursestokml(ids, cn=True) return HttpResponse(kml, content_type='text/xml') # Stroke data views @csrf_exempt @logged_in_or_basicauth() #@api_view(["POST"]) #@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True) #@permission_classes([IsAuthenticated]) def strokedata_rowingdata(request): """ Upload a .csv file (rowingdata standard) through API, using Basic Auth """ r = getrower(request.user) if r.rowerplan == 'freecoach': return HttpResponseNotAllowed("This endpoint is for users, not for free coach accounts") if request.method != 'POST': return HttpResponseNotAllowed("Method not supported") # pragma: no cover form = DocumentsForm(request.POST, request.FILES) if not form.is_valid(): return HttpResponseBadRequest(json.dumps(form.errors)) f = form.cleaned_data['file'] if f is None: return HttpResponseBadRequest("Missing file") filename, completefilename = handle_uploaded_file(f) uploadoptions = { 'secret': settings.UPLOAD_SERVICE_SECRET, 'user': r.user.id, 'file': completefilename, 'workouttype': form.cleaned_data['workouttype'], 'boattype': form.cleaned_data['boattype'], 'title': form.cleaned_data['title'], 'rpe': form.cleaned_data['rpe'], 'notes': form.cleaned_data['notes'] } url = settings.UPLOAD_SERVICE_URL _ = myqueue(queuehigh, handle_request_post, url, uploadoptions) response = JsonResponse( { "status": "success", } ) response.status_code = 201 return response @csrf_exempt @api_view(["POST"]) @permission_classes([IsAuthenticated]) def strokedata_rowingdata_apikey(request): """ Upload a .csv file (rowingdata standard) through API, using """ r = getrower(request.user) if r.rowerplan == 'freecoach': return HttpResponseNotAllowed("This endpoint is for users, not for free coach accounts") if request.method != 'POST': return HttpResponseNotAllowed("Method not supported") form = DocumentsForm(request.POST, request.FILES) if not form.is_valid(): return HttpResponseBadRequest(json.dumps(form.errors)) f = form.cleaned_data['file'] if f is None: return HttpResponseBadRequest("Missing file") filename, completefilename = handle_uploaded_file(f) uploadoptions = { 'secret': settings.UPLOAD_SERVICE_SECRET, 'user': r.user.id, 'file': completefilename, 'workouttype': form.cleaned_data['workouttype'], 'boattype': form.cleaned_data['boattype'], 'title': form.cleaned_data['title'], 'rpe': form.cleaned_data['rpe'], 'notes': form.cleaned_data['notes'] } url = settings.UPLOAD_SERVICE_URL _ = myqueue(queuehigh, handle_request_post, url, uploadoptions) response = JsonResponse( { "status": "success", } ) response.status_code = 201 return response @csrf_exempt @api_view(["POST"]) @permission_classes([IsAuthenticated]) @parser_classes([FITParser]) def strokedata_fit(request): """ Handle a POST request to upload a binary FIT file and save it locally. """ if request.method != 'POST': return HttpResponseBadRequest("Only POST requests are allowed.") dologging('apilog.log', request.user.username+" (strokedata_fit POST)") try: fit_data = request.data if not fit_data: return HttpResponseBadRequest("No FIT data provided.") # Ensure the media directory exists media_dir = 'media' os.makedirs(media_dir, exist_ok=True) # Generate a unique filename for the FIT file fit_filename = os.path.join(media_dir, f'{uuid4().hex[:16]}.fit') # Save the FIT file locally with open(fit_filename, 'wb') as fit_file: fit_file.write(fit_data) except Exception as e: dologging('apilog.log','FIT') dologging('apilog.log',e) return JsonResponse({ "status": "error", "message": f"An error occurred while saving the FIT file: {str(e)}" }, status=400) try: # Parse the FIT file try: row = FP(fit_filename) dologging('apilog.log','FIT file parsed') except ValueError as e: dologging('apilog.log','FIT error') dologging('apilog.log',e) return JsonResponse({ "status": "error", "message": f"An error occurred while parsing the FIT file: {str(e)}" }, status=422) rowdata = rowingdata(df=row.df) duration = totaltime_sec_to_string(rowdata.duration) distance = rowdata.df[" Horizontal (meters)"].iloc[-1] title = "" try: startdatetime = rowdata.rowdatetime startdate = startdatetime.date() partofday = part_of_day(startdatetime.hour) title = '{partofday} water'.format(partofday=partofday) except Exception as e: dologging('apilog.log','FIT error to get time') dologging('apilog.log',e) _ = myqueue(queuehigh, handle_sendemail_unrecognized, fit_filename, "fit parser") return HttpResponse(status=422) w = Workout.objects.create(user=request.user.rower, duration=duration, distance=distance, name=title, date=startdate, ) uploadoptions = { 'secret': UPLOAD_SERVICE_SECRET, 'user': request.user.id, 'file': fit_filename, 'boattype': '1x', 'title': title, 'rpe': 0, 'notes': '', 'id': w.id, 'workouttype': 'water', 'offline': False, } url = UPLOAD_SERVICE_URL _ = myqueue(queuehigh, handle_request_post, url, uploadoptions) dologging('apilog.log','FIT file uploaded, returning response') returndict = { "status": "success", "workout public id": encoder.encode_hex(w.id), "workout id": w.id, } return JsonResponse(returndict, status=201) except Exception as e: dologging('apilog.log','FIT API endpoint') dologging('apilog.log',e) _ = myqueue(queuehigh, handle_sendemail_unrecognized, fit_filename, "fit parser") return JSONResponse({"error": "the uploaded file is corrupted or invalid and cannot be processed"}, status=422) @csrf_exempt #@login_required() @api_view(["POST"]) @permission_classes([IsAuthenticated]) @parser_classes([XMLParser]) def strokedata_tcx(request): """ Upload a TCX file through API """ if request.method != 'POST': # pragma: no cover dologging('apilog.log','GET request to TCX endpoint') return HttpResponseNotAllowed("Method not supported") if 'application/xml' not in request.content_type.lower(): # pragma: no cover dologging('apilog.log','POST data not application/xml, request to TCX endpoint') dologging('apilog.log', request.content_type.lower()) return HttpResponseNotAllowed("Need application/xml") dologging('apilog.log', request.user.username+" (strokedata_tcx POST)") try: tcxdata = request.data activity_node = tcxdata.find(".//{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}Activity") # Extract the activity start time start_time_node = activity_node.find(".//{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}Id") start_time_str = start_time_node.text # Calculate the total duration of the entire activity total_duration = 0 # Find all Lap nodes lap_nodes = activity_node.findall(".//{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}Lap") # Sum up the durations of all laps for lap_node in lap_nodes: lap_duration_node = lap_node.find(".//{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}TotalTimeSeconds") lap_duration_seconds = float(lap_duration_node.text) total_duration += lap_duration_seconds except Exception as e: # pragma: no cover dologging('apilog.log','TCX') dologging('apilog.log',e) if request.data: tcxdata = request.data tcxfilename = 'media/{code}.tcx'.format(code=uuid4().hex[:16]) try: xml_string = ET.tostring(tcxdata, encoding='utf-8', method='xml').decode('utf-8') except AttributeError: return HttpResponseNotAllowed("Could not parse TCX data") with open(tcxfilename, 'w', encoding='utf-8') as xml_file: xml_file.write(xml_string) _ = myqueue(queuehigh, handle_sendemail_unrecognized, tcxfilename, "tcx parser") return HttpResponseNotAllowed("Could not parse TCX data") try: tcxfilename = 'media/{code}.tcx'.format(code=uuid4().hex[:16]) xml_string = ET.tostring(tcxdata, encoding='utf-8', method='xml').decode('utf-8') with open(tcxfilename, 'w', encoding='utf-8') as xml_file: xml_file.write(xml_string) duration = totaltime_sec_to_string(total_duration) title = "CrewNerd water" try: startdatetime = arrow.get(start_time_str) #startdatetime = dt.strptime(start_time_str, "%Y-%m-%dT%H:%M:%S%z") startdate = startdatetime.date() partofday = part_of_day(startdatetime.hour) title = '{partofday} water'.format(partofday=partofday) except Exception as e: dologging('apilog.log','TCX error to get time') dologging('apilog.log',e) _ = myqueue(queuehigh, handle_sendemail_unrecognized, tcxfilename, "tcx parser") w = Workout(user=request.user.rower, date=startdate, name=title, duration=duration) w.save() # need workouttype, duration uploadoptions = { 'secret': UPLOAD_SERVICE_SECRET, 'user': request.user.id, 'file': tcxfilename, 'id': w.id, 'title': title, 'rpe': 0, 'workouttype': 'water', 'boattype': '1x', 'notes': '', 'offline': False, } _ = myqueue(queuehigh, handle_post_workout_api, uploadoptions) workoutid = w.id return JsonResponse( {"workout public id": encoder.encode_hex(workoutid), "workout id": workoutid, "status": "success", }) except Exception as e: # pragma: no cover dologging('apilog.log','TCX API endpoint') dologging('apilog.log',e) _ = myqueue(queuehigh, handle_sendemail_unrecognized, tcxfilename, "tcx parser") return HttpResponse(status=500) @csrf_exempt @api_view(["POST"]) #@login_required() @permission_classes([IsAuthenticated]) def strokedatajson_v3(request): """ POST: Add Stroke data to workout GET: Get stroke data of workout This v2 API works on stroke based data dict: { "distance": 2100, "elapsedTime": 592, "duration": "0:09:52", "name": "Test Workout (GO)", "startdatetime": "2023-01-16 17:54:35.588838+00:00", "workouttype": "water", "boattype": "1x", "notes": "some\nnotes", "strokes": {"data": [ {"distance":5, "power": 112, "hr": 132, "pace": 145800, "spm": 11, "time": 0, "latitude":52.2264097,"longitude":6.8493638}, {"distance":12, "power": 221, "hr": 131, "pace": 116400, "spm": 41, "time": 2200, "latitude":52.2263474,"longitude":6.8495814}, {"distance":19, "power": 511, "hr": 131, "pace": 88100, "spm": 56, "time": 4599, "latitude":52.2262715,"longitude":6.8496975}, {"distance":27, "power": 673, "hr": 132, "pace": 80400, "spm": 59, "time": 7000, "latitude":52.2262003,"longitude":6.8498095}, {"distance":35, "power": 744, "hr": 133, "pace": 77700, "spm": 55, "time": 9599, "latitude":52.2261312, "longitude":6.8499267}, {"distance":43, "power": 754, "hr": 136, "pace": 77400, "spm": 48, "time": 12000, "latitude":52.2260576,"longitude":6.8500366}, {"distance":51, "power": 754, "hr": 139, "pace": 77400, "spm": 48, "time": 14400, "latitude":52.2259872,"longitude":6.8501561}, {"distance":59, "power": 749, "hr": 142, "pace": 77600, "spm": 48, "time": 16799, "latitude":52.2259154,"longitude":6.8502769}, {"distance":67, "power": 729, "hr": 145, "pace": 78300, "spm": 48, "time": 19400, "latitude":52.2257749,"longitude":6.8503918}, {"distance":75, "power": 729, "hr": 147, "pace": 78300, "spm": 48, "time": 21799, "latitude":52.2259154,"longitude":6.8502769}, {"distance":82, "power": 726, "hr": 150, "pace": 78400, "spm": 48, "time": 24200, "latitude":52.2259872,"longitude":6.8501561}, {"distance":90, "power": 709, "hr": 152, "pace": 79000, "spm": 48, "time": 26599, "latitude":52.2260576,"longitude":6.8500366}, {"distance":100, "power": 707, "hr": 153, "pace": 79100, "spm": 49, "time": 29000, "latitude":52.2261312,"longitude":6.8499267}]} } """ if request.method != 'POST': return HttpResponseNotAllowed("Method not supported") # pragma: no cover dologging('apilog.log', request.user.username+" (strokedatajson_v3 POST)") title = request.data.get('name','') try: elapsedTime = request.data['elapsedTime'] except (KeyError, ColumnNotFoundError): # pragma: no cover try: duration = request.data['duration'] try: t = datetime.strptime(duration,"%H:%M:%S.%d") except ValueError: t = datetime.strptime(duration,"%H:%M:%S") elapsedTime = 3600*t.hour+60.*t.minute+t.second+t.microsecond/1.e6 except: return HttpResponse("Missing Elapsed Time", status=400) try: totalDistance = request.data['distance'] except (KeyError, ColumnNotFoundError): # pragma: no cover return HttpResponse("Missing Total Distance", status=400) timeZone = request.data.get('timezone','UTC') workouttype = request.data.get('workouttype','rower') boattype = request.data.get('boattype','1x') notes = request.data.get('notes','') rpe = request.data.get('rpe',0) startdatetime = request.data.get('startdatetime',"%s" % timezone.now()) try: startdatetime = pendulum.parse(startdatetime) except ParserError: startdatetime = timezone.now() dologging('apilog.log',workouttype) dologging('apilog.log',boattype) dologging('apilog.log',notes) dologging('apilog.log',title) dologging('apilog.log',totalDistance) dologging('apilog.log',elapsedTime) df = pl.DataFrame() try: strokes = request.data['strokes'] except (KeyError, ColumnNotFoundError): # pragma: no cover return HttpResponse("No Stroke Data in JSON", status=400) try: df = pl.DataFrame(strokes['data']) except (KeyError, ColumnNotFoundError): # pragma: no cover try: df = pl.DataFrame(request.data['strokedata']) except: return HttpResponse("No JSON Object could be decoded", status=400) try: df = df.sort("time") except ColumnNotFoundError: return HttpResponse("No time column", status=400) status, comment, data = api_get_dataframe(startdatetime, df) if status != 200: # pragma: no cover return HttpResponse(comment, status=status) csvfilename = 'media/{code}.csv.gz'.format(code=uuid4().hex[:16]) with gzip.open(csvfilename, 'w') as f: _ = data.write_csv(f) duration = datetime.time(0,0,1) w = Workout( user=request.user.rower, date=timezone.now().date(), duration=duration) w.save() uploadoptions = { 'secret': UPLOAD_SERVICE_SECRET, 'user': request.user.id, 'file': csvfilename, 'title': title, 'workouttype': workouttype, 'boattype': boattype, 'elapsedTime': elapsedTime/1000., # in seconds 'totalDistance': totalDistance, 'rpe': rpe, 'notes': notes, 'timezone': timeZone, 'id': w.id, } _ = myqueue(queuehigh, handle_post_workout_api, uploadoptions) workoutid = w.id return JsonResponse( {"workout public id": encoder.encode_hex(workoutid), "workout id": workoutid, "status": "success", }) # Process the POSTed stroke data according to the API definition # Return the GET stroke data according to the API definition @csrf_exempt #@login_required() @api_view(["GET", "POST"]) @permission_classes([IsAuthenticated]) def strokedatajson_v2(request, id): """ POST: Add Stroke data to workout GET: Get stroke data of workout This v2 API works on stroke based data dict: {"data": [ {"hr": 110, "p": 3600, "spm": 53, "d": 6, "t": 12}, {"hr": 111, "p": 3600, "spm": 53, "d": 6, "t": 12}, {"hr": 111, "p": 3600, "spm": 64, "d": 6, "t": 22}, {"hr": 110, "p": 3600, "spm": 16, "d": 14, "t": 55}, {"hr": 110, "p": 3600, "spm": 16, "d": 14, "t": 82}, {"hr": 107, "p": 3600, "spm": 12, "d": 22, "t": 109}, {"hr": 107, "p": 3600, "spm": 12, "d": 22, "t": 133}, {"hr": 108, "p": 3600, "spm": 12, "d": 32, "t": 157}, {"hr": 108, "p": 3577, "spm": 12, "d": 32, "t": 157}, {"hr": 108, "p": 3411, "spm": 12, "d": 32, "t": 157}, {"hr": 108, "p": 2649, "spm": 12, "d": 32, "t": 157}, {"hr": 108, "p": 3099, "spm": 12, "d": 32, "t": 157}, {"hr": 108, "p": 3600, "spm": 12, "d": 32, "t": 157}, {"hr": 100, "p": 3600, "spm": 44, "d": 115, "t": 292}, {"hr": 99, "p": 3600, "spm": 27, "d": 129, "t": 305}, {"hr": 97, "p": 3600, "spm": 34, "d": 161, "t": 330}, {"hr": 96, "p": 3600, "spm": 25, "d": 177, "t": 344}, {"hr": 96, "p": 3494, "spm": 43, "d": 196, "t": 357}, {"hr": 98, "p": 2927, "spm": 26, "d": 235, "t": 377}, {"hr": 102, "p": 2718, "spm": 27, "d": 380, "t": 455}, {"hr": 102, "p": 2753, "spm": 9, "d": 398, "t": 472}, {"hr": 102, "p": 2864, "spm": 61, "d": 406, "t": 477}, {"hr": 101, "p": 2780, "spm": 15, "d": 484, "t": 515}, {"hr": 101, "p": 2365, "spm": 16, "d": 583, "t": 554}, {"hr": 103, "p": 1965, "spm": 16, "d": 681, "t": 592}, ] } """ row = get_object_or_404(Workout, pk=id) if row.user != request.user.rower: # pragma: no cover return HttpResponse("You do not have permission to perform this action", status=403) try: id = int(id) except ValueError: # pragma: no cover return HttpResponse("Not a valid workout number", status=404) if request.method == 'GET': columns = ['spm', 'time', 'hr', 'pace', 'power', 'distance'] datadf = dataprep.read_data(columns, ids=[id]) datadf = dataprep.remove_nulls_pl(datadf) dologging('apilog.log',request.user.username+"(strokedatajson_v2 GET)") data = datadf.write_json(row_oriented=True) data2 = json.loads(data) data2 = {"data": data2} return JSONResponse(data2) if request.method == 'POST': dologging('apilog.log',request.user.username+" (strokedatajson_v2 POST)") try: for d in request.data['data']: dologging('apilog.log',json.dumps(d)) except (KeyError, ColumnNotFoundError): # pragma: no cover try: for d in request.data['strokedata']: dologging('apilog.log',json.dumps(d)) except (KeyError, ColumnNotFoundError): dologging('apilog.log','No data in request.data') checkdata, r = dataprep.getrowdata_db(id=row.id) if not checkdata.empty: # pragma: no cover return HttpResponse("Duplicate Error", status=409) df = pl.DataFrame() try: df = pl.DataFrame(request.data['data']) except (KeyError, ColumnNotFoundError): # pragma: no cover try: df = pl.DataFrame(request.data['strokedata']) except: return HttpResponse("No JSON object could be decoded", status=400) try: df = df.sort("time") except ColumnNotFoundError: # pragma: no cover return HttpResponse("No time column", status=400) status, comment, data = api_get_dataframe(row.startdatetime, df) if status != 200: # pragma: no cover return HttpResponse(comment, status=status) r = getrower(request.user) timestr = row.startdatetime.strftime("%Y%m%d-%H%M%S") csvfilename = 'media/Import_'+timestr+'.csv' workoutdate = row.date workoutstartdatetime = row.startdatetime workoutenddatetime = workoutstartdatetime + \ datetime.timedelta(seconds=data[' ElapsedTime (sec)'].max()) duplicate = dataprep.checkduplicates( r, workoutdate, workoutstartdatetime, workoutenddatetime) if duplicate: row.duplicate = True row.save() with gzip.open(csvfilename+'.gz', 'w') as f: _ = data.write_csv(f) row.csvfilename = csvfilename row.save() powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, r.pw_at, r.pw_tr, r.pw_an])/r.ftp ftp = float(r.ftp) if row.workouttype in mytypes.otwtypes: ftp = ftp*(100.-r.otwslack)/100. rr = rrower(hrmax=r.max, hrut2=r.ut2, hrut1=r.ut1, hrat=r.at, hrtr=r.tr, hran=r.an, ftp=ftp, powerperc=powerperc, powerzones=r.powerzones) rowdata = rdata(csvfile=row.csvfilename, rower=rr).df datadf = dataprep.dataplep( rowdata, id=row.id, bands=True, barchart=True, otwpower=True, empower=True) if row.workouttype in mytypes.otwtypes: wps_avg = r.median_wps elif row.workouttype in mytypes.ergtypes: wps_avg = r.median_wps_erg else: wps_avg = 0 _ = myqueue(queuehigh, handle_calctrimp, row.id, row.csvfilename, r.ftp, r.sex, r.hrftp, r.max, r.rest, wps_avg) isbreakthrough, ishard = dataprep.checkbreakthrough(row, r) if r.getemailnotifications and not r.emailbounced: # pragma: no cover link = settings.SITE_URL+reverse( r.defaultlandingpage, kwargs={ 'id': encoder.encode_hex(row.id), } ) _ = send_confirm(r.user, row.name, link, '') _ = uploads.do_sync(row, {}, quick=True) dologging('apilog.log'," (strokedatajson_v2 POST completed successfully)") return(JsonResponse( {"workout public id": encoder.encode_hex(row.id), "workout private id": row.id, "status": "success", })) # return(HttpResponse(encoder.encode_hex(row.id),status=201)) return HttpResponseNotAllowed("Method not supported") # pragma: no cover @csrf_exempt #@login_required() @api_view(['GET', 'POST']) @permission_classes([IsAuthenticated]) def strokedatajson(request, id=0): """ POST: Add Stroke data to workout GET: Get stroke data of workout """ row = get_object_or_404(Workout, pk=id) if row.user != request.user.rower: # pragma: no cover raise PermissionDenied("You have no access to this workout") try: id = int(id) except ValueError: # pragma: no cover return HttpResponse("Not a valid workout number", status=403) if request.method == 'GET': # currently only returns a subset. columns = ['spm', 'time', 'hr', 'pace', 'power', 'distance'] datadf = dataprep.read_data(columns, ids=[id]) datadf = dataprep.remove_nulls_pl(datadf) datadf = datadf.to_pandas() dologging("apilog.log",request.user.username+"(strokedatajson GET) ") return JSONResponse(datadf) if request.method == 'POST': dologging("apilog.log",request.user.username+"(strokedatajson POST)") checkdata, r = dataprep.getrowdata_db(id=row.id) if not checkdata.empty: # pragma: no cover return HttpResponse("Duplicate Error", status=409) # strokedata = request.POST['strokedata'] # checking/validating and cleaning try: strokedata = json.loads(request.data)['strokedata'] except: # pragma: no cover try: s = json.dumps(request.data) strokedata = json.loads(s)['strokedata'] except: # pragma: no cover return HttpResponse("No JSON object could be decoded", status=400) try: df = pl.DataFrame(strokedata) except ValueError: # pragma: no cover return HttpResponse("Arrays must all be same length", status=400) df = df.sort("time") try: time = df['time']/1.e3 except (KeyError, ColumnNotFoundError): # pragma: no cover return HttpResponse("There must be time values", status=400) aantal = len(time) pace = df['pace']/1.e3 if len(pace) != aantal: # pragma: no cover return HttpResponse("Pace array has incorrect length", status=400) distance = df['distance'] if len(distance) != aantal: # pragma: no cover return HttpResponse("Distance array has incorrect length", status=400) spm = df['spm'] if len(spm) != aantal: # pragma: no cover return HttpResponse("SPM array has incorrect length", status=400) res = dataprep.testdata(time, distance, pace, spm) if not res: # pragma: no cover return HttpResponse("Data are not numerical", status=400) power = trydf(df, aantal, 'power') drivelength = trydf(df, aantal, 'drivelength') dragfactor = trydf(df, aantal, 'dragfactor') drivetime = trydf(df, aantal, 'drivetime') strokerecoverytime = trydf(df, aantal, 'strokerecoverytime') averagedriveforce = trydf(df, aantal, 'averagedriveforce') peakdriveforce = trydf(df, aantal, 'peakdriveforce') wash = trydf(df, aantal, 'wash') catch = trydf(df, aantal, 'catch') finish = trydf(df, aantal, 'finish') peakforceangle = trydf(df, aantal, 'peakforceangle') driveenergy = trydf(df, aantal, 'driveenergy') slip = trydf(df, aantal, 'slip') lapidx = trydf(df, aantal, 'lapidx') hr = trydf(df, aantal, 'hr') starttime = totimestamp(row.startdatetime)+time[0] unixtime = starttime+time dologging("apilog.log",request.user.username+"(POST)") data = pl.DataFrame({'TimeStamp (sec)': unixtime, ' Horizontal (meters)': distance, ' Cadence (stokes/min)': spm, ' HRCur (bpm)': hr, ' DragFactor': dragfactor, ' Stroke500mPace (sec/500m)': pace, ' Power (watts)': power, ' DriveLength (meters)': drivelength, ' DriveTime (ms)': drivetime, ' StrokeRecoveryTime (ms)': strokerecoverytime, ' AverageDriveForce (lbs)': averagedriveforce, ' PeakDriveForce (lbs)': peakdriveforce, ' lapIdx': lapidx, ' ElapsedTime (sec)': time, 'catch': catch, 'slip': slip, 'finish': finish, 'wash': wash, 'driveenergy': driveenergy, 'peakforceangle': peakforceangle, }) r = getrower(request.user) timestr = row.startdatetime.strftime("%Y%m%d-%H%M%S") csvfilename = 'media/Import_'+timestr+'.csv' with gzip.open(csvfilename+'.gz','w') as f: res = data.write_csv(f) row.csvfilename = csvfilename row.save() powerperc = 100*np.array([r.pw_ut2, r.pw_ut1, r.pw_at, r.pw_tr, r.pw_an])/r.ftp ftp = float(r.ftp) if row.workouttype in mytypes.otwtypes: ftp = ftp*(100.-r.otwslack)/100. rr = rrower(hrmax=r.max, hrut2=r.ut2, hrut1=r.ut1, hrat=r.at, hrtr=r.tr, hran=r.an, ftp=ftp, powerperc=powerperc, powerzones=r.powerzones) rowdata = rdata(csvfile=row.csvfilename, rower=rr).df datadf = dataprep.dataplep( rowdata, id=row.id, bands=True, barchart=True, otwpower=True, empower=True) # mangling # return HttpResponse(encoder.encode_hex(row.id), status=201) # Method not supported return HttpResponseNotAllowed("Method not supported") # pragma: no cover