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}}]); "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') 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): class TestDownloadWorkoutGpx(ApiTestCaseMixin):
def test_it_returns_404_if_workout_does_not_exist( def test_it_returns_404_if_workout_does_not_exist(

View File

@ -1,5 +1,6 @@
import json import json
import os import os
import re
from datetime import datetime from datetime import datetime
from io import BytesIO from io import BytesIO
from typing import Dict, Optional from typing import Dict, Optional
@ -10,7 +11,6 @@ from flask import Flask
from fittrackee.users.models import User from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout from fittrackee.workouts.models import Sport, Workout
from fittrackee.workouts.utils.short_id import decode_short_id
from ..mixins import ApiTestCaseMixin, CallArgsMixin from ..mixins import ApiTestCaseMixin, CallArgsMixin
@ -251,6 +251,56 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
assert 'just a workout' == data['data']['workouts'][0]['title'] assert 'just a workout' == data['data']['workouts'][0]['title']
assert_workout_data_with_gpx(data) 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( def test_it_adds_a_workout_with_gpx_without_name(
self, self,
app: Flask, app: Flask,
@ -974,23 +1024,6 @@ class TestPostAndGetWorkoutWithGpx(ApiTestCaseMixin):
) )
assert response.status_code == 200 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( def test_it_gets_a_workout_created_with_gpx(
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
) -> None: ) -> None:

View File

@ -1,8 +1,5 @@
import os
from flask import Flask from flask import Flask
from fittrackee.files import get_absolute_file_path
from fittrackee.users.models import User from fittrackee.users.models import User
from fittrackee.workouts.models import Sport, Workout from fittrackee.workouts.models import Sport, Workout
@ -64,21 +61,45 @@ class TestDeleteWorkoutWithGpx(ApiTestCaseMixin):
data = self.assert_404(response) data = self.assert_404(response)
assert 'not found' in data['status'] assert 'not found' in data['status']
def test_it_returns_500_when_deleting_a_workout_with_gpx_invalid_file( def test_a_workout_with_gpx_can_be_deleted_if_gpx_file_is_invalid(
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
workout_cycling_user_1: Workout,
) -> None: ) -> None:
token, workout_short_id = post_a_workout(app, gpx_file) workout_cycling_user_1.gpx = self.random_string()
client = app.test_client() client, auth_token = self.get_test_client_and_auth_token(
gpx_filepath = get_gpx_filepath(1) app, user_1.email
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}'),
) )
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): 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.orm.session import Session, object_session
from sqlalchemy.types import JSON, Enum from sqlalchemy.types import JSON, Enum
from fittrackee import db from fittrackee import appLog, db
from fittrackee.files import get_absolute_file_path from fittrackee.files import get_absolute_file_path
from .utils.convert import convert_in_duration, convert_value_to_integer 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) @listens_for(db.Session, 'after_flush', once=True)
def receive_after_flush(session: Session, context: Any) -> None: def receive_after_flush(session: Session, context: Any) -> None:
if old_record.map: if old_record.map:
os.remove(get_absolute_file_path(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: if old_record.gpx:
os.remove(get_absolute_file_path(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): class WorkoutSegment(BaseModel):

View File

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

View File

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

View File

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