Merge pull request #195 from SamR1/workouts-fixes

workouts fixes
This commit is contained in:
Sam 2022-06-22 17:31:05 +02:00 committed by GitHub
commit e3ba0259f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 158 additions and 64 deletions

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"/><link rel="stylesheet" href="/static/css/leaflet.css"/><title>FitTrackee</title><script defer="defer" src="/static/js/chunk-vendors.e335684a.js"></script><script defer="defer" src="/static/js/app.8517c25d.js"></script><link href="/static/css/app.e8b7692c.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/static/css/fork-awesome.min.css"/><link rel="stylesheet" href="/static/css/leaflet.css"/><title>FitTrackee</title><script defer="defer" src="/static/js/chunk-vendors.e335684a.js"></script><script defer="defer" src="/static/js/app.69114670.js"></script><link href="/static/css/app.e8b7692c.css" rel="stylesheet"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="fittrackee_client"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but FitTrackee doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[193],{7749:function(e,t,s){s.r(t),s.d(t,{default:function(){return A}});s(6699);var a=s(6252),r=s(2262),l=s(3577),o=s(3324),n=s(7402);const c={class:"chart-menu"},i={class:"chart-arrow"},u={class:"time-frames custom-checkboxes-group"},d={class:"time-frames-checkboxes custom-checkboxes"},p=["id","name","checked","onInput"],m={class:"chart-arrow"};var v=(0,a.aZ)({name:"StatsMenu",emits:["arrowClick","timeFrameUpdate"],setup(e,{emit:t}){let s=(0,r.iH)("month");const o=["week","month","year"];function n(e){s.value=e,t("timeFrameUpdate",e)}return(e,v)=>((0,a.wg)(),(0,a.iD)("div",c,[(0,a._)("div",i,[(0,a._)("i",{class:"fa fa-chevron-left","aria-hidden":"true",onClick:v[0]||(v[0]=e=>t("arrowClick",!0))})]),(0,a._)("div",u,[(0,a._)("div",d,[((0,a.wg)(),(0,a.iD)(a.HY,null,(0,a.Ko)(o,(t=>(0,a._)("div",{class:"time-frame custom-checkbox",key:t},[(0,a._)("label",null,[(0,a._)("input",{type:"radio",id:t,name:t,checked:(0,r.SU)(s)===t,onInput:e=>n(t)},null,40,p),(0,a._)("span",null,(0,l.zw)(e.$t(`statistics.TIME_FRAMES.${t}`)),1)])]))),64))])]),(0,a._)("div",m,[(0,a._)("i",{class:"fa fa-chevron-right","aria-hidden":"true",onClick:v[1]||(v[1]=e=>t("arrowClick",!1))})])]))}}),k=s(3744);const S=(0,k.Z)(v,[["__scopeId","data-v-af15954c"]]);var w=S,f=s(631);const _={class:"sports-menu"},h=["id","name","checked","onInput"],U={class:"sport-label"};var b=(0,a.aZ)({name:"StatsSportsMenu",props:{userSports:null,selectedSportIds:{default:()=>[]}},emits:["selectedSportIdsUpdate"],setup(e,{emit:t}){const s=e,{t:n}=(0,o.QT)(),c=(0,a.f3)("sportColors"),{selectedSportIds:i}=(0,r.BK)(s),u=(0,a.Fl)((()=>(0,f.xH)(s.userSports,n)));function d(e){t("selectedSportIdsUpdate",e)}return(e,t)=>{const s=(0,a.up)("SportImage");return(0,a.wg)(),(0,a.iD)("div",_,[((0,a.wg)(!0),(0,a.iD)(a.HY,null,(0,a.Ko)((0,r.SU)(u),(e=>((0,a.wg)(),(0,a.iD)("label",{type:"checkbox",key:e.id,style:(0,l.j5)({color:e.color?e.color:(0,r.SU)(c)[e.label]})},[(0,a._)("input",{type:"checkbox",id:e.id,name:e.label,checked:(0,r.SU)(i).includes(e.id),onInput:t=>d(e.id)},null,40,h),(0,a.Wm)(s,{"sport-label":e.label,color:e.color},null,8,["sport-label","color"]),(0,a._)("span",U,(0,l.zw)(e.translatedLabel),1)],4)))),128))])}}});const I=b;var g=I,T=s(9318);const y={key:0,id:"user-statistics"};var C=(0,a.aZ)({name:"index",props:{sports:null,user:null},setup(e){const t=e,{t:s}=(0,o.QT)(),{sports:l,user:c}=(0,r.BK)(t);let i=(0,r.iH)("month");const u=(0,r.iH)(v(i.value)),d=(0,a.Fl)((()=>(0,f.xH)(t.sports,s))),p=(0,r.iH)(S(t.sports));function m(e){i.value=e,u.value=v(i.value)}function v(e){return(0,T.aZ)(new Date,e,t.user.weekm)}function k(e){u.value=(0,T.FN)(u.value,e,t.user.weekm)}function S(e){return e.map((e=>e.id))}function _(e){p.value.includes(e)?p.value=p.value.filter((t=>t!==e)):p.value.push(e)}return(0,a.YP)((()=>t.sports),(e=>{p.value=S(e)})),(e,t)=>(0,r.SU)(d)?((0,a.wg)(),(0,a.iD)("div",y,[(0,a.Wm)(w,{onTimeFrameUpdate:m,onArrowClick:k}),(0,a.Wm)(n.Z,{sports:(0,r.SU)(l),user:(0,r.SU)(c),chartParams:u.value,"displayed-sport-ids":p.value,fullStats:!0},null,8,["sports","user","chartParams","displayed-sport-ids"]),(0,a.Wm)(g,{"selected-sport-ids":p.value,"user-sports":(0,r.SU)(l),onSelectedSportIdsUpdate:_},null,8,["selected-sport-ids","user-sports"])])):(0,a.kq)("",!0)}});const F=(0,k.Z)(C,[["__scopeId","data-v-7d54529b"]]);var Z=F,x=s(5630),D=s(8602),H=s(9917);const E={id:"statistics",class:"view"},R={key:0,class:"container"};var W=(0,a.aZ)({name:"StatisticsView",setup(e){const t=(0,H.o)(),s=(0,a.Fl)((()=>t.getters[D.YN.GETTERS.AUTH_USER_PROFILE])),o=(0,a.Fl)((()=>t.getters[D.O8.GETTERS.SPORTS].filter((e=>s.value.sports_list.includes(e.id)))));return(e,t)=>{const n=(0,a.up)("Card");return(0,a.wg)(),(0,a.iD)("div",E,[(0,r.SU)(s).username?((0,a.wg)(),(0,a.iD)("div",R,[(0,a.Wm)(n,null,{title:(0,a.w5)((()=>[(0,a.Uk)((0,l.zw)(e.$t("statistics.STATISTICS")),1)])),content:(0,a.w5)((()=>[(0,a.Wm)(Z,{class:(0,l.C_)({"stats-disabled":0===(0,r.SU)(s).nb_workouts}),user:(0,r.SU)(s),sports:(0,r.SU)(o)},null,8,["class","user","sports"])])),_:1}),0===(0,r.SU)(s).nb_workouts?((0,a.wg)(),(0,a.j4)(x.Z,{key:0})):(0,a.kq)("",!0)])):(0,a.kq)("",!0)])}}});const P=(0,k.Z)(W,[["__scopeId","data-v-2e341d4e"]]);var A=P}}]);
//# sourceMappingURL=statistics.ea7ff674.js.map
//# sourceMappingURL=statistics.7aabfecc.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1158,6 +1158,27 @@ class TestGetWorkout(ApiTestCaseMixin):
self.assert_404_with_message(response, 'Map does not exist')
def test_it_returns_404_if_map_file_not_found(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
workout_cycling_user_1: Workout,
) -> None:
map_ip = self.random_string()
workout_cycling_user_1.map = self.random_string()
workout_cycling_user_1.map_id = map_ip
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.get(
f'/api/workouts/map/{map_ip}',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
self.assert_404_with_message(response, 'Map file does not exist')
class TestDownloadWorkoutGpx(ApiTestCaseMixin):
def test_it_returns_404_if_workout_does_not_exist(

View File

@ -1,5 +1,6 @@
import json
import os
import re
from datetime import datetime
from io import BytesIO
from typing import Dict, Optional
@ -10,7 +11,6 @@ from flask import Flask
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout
from fittrackee.workouts.utils.short_id import decode_short_id
from ..mixins import ApiTestCaseMixin, CallArgsMixin
@ -251,6 +251,56 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
assert 'just a workout' == data['data']['workouts'][0]['title']
assert_workout_data_with_gpx(data)
def test_it_creates_workout_with_expecting_gpx_path(
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
client.post(
'/api/workouts',
data=dict(
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
data='{"sport_id": 1}',
),
headers=dict(
content_type='multipart/form-data',
Authorization=f'Bearer {auth_token}',
),
)
workout = Workout.query.first()
assert re.match(
r'^workouts/1/2018-03-13_12-44-45_1_([\w\d_-]*).gpx$',
workout.gpx,
)
def test_it_creates_workout_with_expecting_map_path(
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
client.post(
'/api/workouts',
data=dict(
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
data='{"sport_id": 1}',
),
headers=dict(
content_type='multipart/form-data',
Authorization=f'Bearer {auth_token}',
),
)
workout = Workout.query.first()
assert re.match(
r'^workouts/1/2018-03-13_12-44-45_1_([\w\d_-]*).png$',
workout.map,
)
def test_it_adds_a_workout_with_gpx_without_name(
self,
app: Flask,
@ -974,23 +1024,6 @@ class TestPostAndGetWorkoutWithGpx(ApiTestCaseMixin):
)
assert response.status_code == 200
# error case in the same test to avoid generate a new map file
workout_uuid = decode_short_id(workout_short_id)
workout = Workout.query.filter_by(uuid=workout_uuid).first()
workout.map = 'incorrect path'
assert response.status_code == 200
assert 'success' in data['status']
assert '' in data['message']
assert len(data['data']['gpx']) != ''
response = client.get(
f'/api/workouts/map/{map_id}',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
self.assert_500(response)
def test_it_gets_a_workout_created_with_gpx(
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None:

View File

@ -1,8 +1,5 @@
import os
from flask import Flask
from fittrackee.files import get_absolute_file_path
from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout
@ -64,21 +61,45 @@ class TestDeleteWorkoutWithGpx(ApiTestCaseMixin):
data = self.assert_404(response)
assert 'not found' in data['status']
def test_it_returns_500_when_deleting_a_workout_with_gpx_invalid_file(
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
def test_a_workout_with_gpx_can_be_deleted_if_gpx_file_is_invalid(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
workout_cycling_user_1: Workout,
) -> None:
token, workout_short_id = post_a_workout(app, gpx_file)
client = app.test_client()
gpx_filepath = get_gpx_filepath(1)
gpx_filepath = get_absolute_file_path(gpx_filepath)
os.remove(gpx_filepath)
response = client.delete(
f'/api/workouts/{workout_short_id}',
headers=dict(Authorization=f'Bearer {token}'),
workout_cycling_user_1.gpx = self.random_string()
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
self.assert_500(response)
response = client.delete(
f'/api/workouts/{workout_cycling_user_1.short_id}',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
assert response.status_code == 204
def test_a_workout_with_gpx_can_be_deleted_if_map_file_is_invalid(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
workout_cycling_user_1: Workout,
) -> None:
map_ip = self.random_string()
workout_cycling_user_1.map = self.random_string()
workout_cycling_user_1.map_id = map_ip
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.delete(
f'/api/workouts/{workout_cycling_user_1.short_id}',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
assert response.status_code == 204
class TestDeleteWorkoutWithoutGpx(ApiTestCaseMixin):

View File

@ -12,7 +12,7 @@ from sqlalchemy.orm.mapper import Mapper
from sqlalchemy.orm.session import Session, object_session
from sqlalchemy.types import JSON, Enum
from fittrackee import db
from fittrackee import appLog, db
from fittrackee.files import get_absolute_file_path
from .utils.convert import convert_in_duration, convert_value_to_integer
@ -379,9 +379,15 @@ def on_workout_delete(
@listens_for(db.Session, 'after_flush', once=True)
def receive_after_flush(session: Session, context: Any) -> None:
if old_record.map:
try:
os.remove(get_absolute_file_path(old_record.map))
except OSError:
appLog.error('map file not found when deleting workout')
if old_record.gpx:
try:
os.remove(get_absolute_file_path(old_record.gpx))
except OSError:
appLog.error('gpx file not found when deleting workout')
class WorkoutSegment(BaseModel):

View File

@ -255,7 +255,7 @@ def get_file_path(dir_path: str, filename: str) -> str:
def get_new_file_path(
auth_user_id: int,
workout_date: str,
sport: str,
sport_id: int,
old_filename: Optional[str] = None,
extension: Optional[str] = None,
) -> str:
@ -265,11 +265,9 @@ def get_new_file_path(
if not extension and old_filename:
extension = f".{old_filename.rsplit('.', 1)[1].lower()}"
_, new_filename = tempfile.mkstemp(
prefix=f'{workout_date}_{sport}_', suffix=extension
prefix=f'{workout_date}_{sport_id}_', suffix=extension
)
dir_path = os.path.join('workouts', str(auth_user_id))
if not os.path.exists(dir_path):
os.makedirs(dir_path)
file_path = os.path.join(dir_path, new_filename.split('/')[-1])
return file_path
@ -285,11 +283,16 @@ def process_one_gpx_file(
params['file_path'], stopped_speed_threshold
)
auth_user = params['auth_user']
workout_date, _ = get_workout_datetime(
workout_date=gpx_data['start'],
date_str_format=None if gpx_data else '%Y-%m-%d %H:%M',
user_timezone=None,
)
new_filepath = get_new_file_path(
auth_user_id=auth_user.id,
workout_date=gpx_data['start'],
workout_date=workout_date.strftime('%Y-%m-%d_%H-%M-%S'),
old_filename=filename,
sport=params['sport_label'],
sport_id=params['sport_id'],
)
absolute_gpx_filepath = get_absolute_file_path(new_filepath)
os.rename(params['file_path'], absolute_gpx_filepath)
@ -297,9 +300,9 @@ def process_one_gpx_file(
map_filepath = get_new_file_path(
auth_user_id=auth_user.id,
workout_date=gpx_data['start'],
workout_date=workout_date.strftime('%Y-%m-%d_%H-%M-%S'),
extension='.png',
sport=params['sport_label'],
sport_id=params['sport_id'],
)
absolute_map_filepath = get_absolute_file_path(map_filepath)
generate_map(absolute_map_filepath, map_data)
@ -397,7 +400,7 @@ def process_files(
'auth_user': auth_user,
'workout_data': workout_data,
'file_path': file_path,
'sport_label': sport.label,
'sport_id': sport.id,
}
try:

View File

@ -13,7 +13,7 @@ from flask import (
send_from_directory,
)
from sqlalchemy import exc
from werkzeug.exceptions import RequestEntityTooLarge
from werkzeug.exceptions import NotFound, RequestEntityTooLarge
from werkzeug.utils import secure_filename
from fittrackee import appLog, db
@ -798,6 +798,8 @@ def get_map(map_id: int) -> Union[HttpResponse, Response]:
current_app.config['UPLOAD_FOLDER'],
workout.map,
)
except NotFound:
return NotFoundErrorResponse('Map file does not exist.')
except Exception as e:
return handle_error_and_return_response(e)

View File

@ -71,7 +71,7 @@
class="fa fa-map-o"
aria-hidden="true"
/>
{{ workout.title }}
<span class="title">{{ workout.title }}</span>
</router-link>
<StaticMap
v-if="workout.with_gpx && hoverWorkoutId === workout.id"
@ -182,7 +182,7 @@
import { WORKOUTS_STORE } from '@/store/constants'
import { IPagination } from '@/types/api'
import { ITranslatedSport } from '@/types/sports'
import { IUserProfile } from '@/types/user'
import { IAuthUserProfile } from '@/types/user'
import { IWorkout, TWorkoutsPayload } from '@/types/workouts'
import { useStore } from '@/use/useStore'
import { getQuery, sortList, workoutsPayloadKeys } from '@/utils/api'
@ -192,7 +192,7 @@
import { defaultOrder } from '@/utils/workouts'
interface Props {
user: IUserProfile
user: IAuthUserProfile
sports: ITranslatedSport[]
}
const props = defineProps<Props>()
@ -258,7 +258,7 @@
...payload,
}
Object.entries(convertedPayload).map((entry) => {
if (entry[0].match('speed|distance')) {
if (entry[0].match('speed|distance') && entry[1]) {
convertedPayload[entry[0]] = convertDistance(+entry[1], 'mi', 'km')
}
})
@ -326,6 +326,14 @@
position: relative;
.fa-map-o {
font-size: 0.75em;
padding-right: $default-padding * 0.5;
}
.nav-item {
white-space: nowrap;
.title {
word-break: break-word;
white-space: normal;
}
}
.static-map {
display: none;