Merge pull request #162 from Fmstrat/elevation
Added total elevation to dashboard
This commit is contained in:
commit
856ec210e5
@ -72,7 +72,7 @@ Account & preferences
|
||||
- A user can reset his password (*new in 0.3.0*)
|
||||
- A user can change his email address (*new in 0.6.0*)
|
||||
- A user can choose between metric system and imperial system for distance, elevation and speed display (*new in 0.5.0*)
|
||||
- A user can choose to display or hide ascent records (*new in 0.6.11*)
|
||||
- A user can choose to display or hide ascent records and total on Dashboard (*new in 0.6.11*)
|
||||
- A user can set sport preferences (*new in 0.5.0*):
|
||||
- change sport color (used for sport image and charts)
|
||||
- can override stopped speed threshold (for next uploaded gpx files)
|
||||
|
@ -663,7 +663,7 @@ character “_” allowed</p></li>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Request JSON Object<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>display_ascent</strong> (<em>boolean</em>) – display highest ascent records</p></li>
|
||||
<li><p><strong>display_ascent</strong> (<em>boolean</em>) – display highest ascent records and total</p></li>
|
||||
<li><p><strong>imperial_units</strong> (<em>boolean</em>) – display distance in imperial units</p></li>
|
||||
<li><p><strong>language</strong> (<em>string</em>) – language preferences</p></li>
|
||||
<li><p><strong>timezone</strong> (<em>string</em>) – user time zone</p></li>
|
||||
|
@ -249,7 +249,7 @@ A user with an inactive account cannot log in. (<em>new in 0.6.0</em>)</p></li>
|
||||
<li><p>A user can reset his password (<em>new in 0.3.0</em>)</p></li>
|
||||
<li><p>A user can change his email address (<em>new in 0.6.0</em>)</p></li>
|
||||
<li><p>A user can choose between metric system and imperial system for distance, elevation and speed display (<em>new in 0.5.0</em>)</p></li>
|
||||
<li><p>A user can choose to display or hide ascent records (<em>new in 0.6.11</em>)</p></li>
|
||||
<li><p>A user can choose to display or hide ascent records and total on Dashboard (<em>new in 0.6.11</em>)</p></li>
|
||||
<li><dl class="simple">
|
||||
<dt>A user can set sport preferences (<em>new in 0.5.0</em>):</dt><dd><ul>
|
||||
<li><p>change sport color (used for sport image and charts)</p></li>
|
||||
|
File diff suppressed because one or more lines are too long
@ -72,7 +72,7 @@ Account & preferences
|
||||
- A user can reset his password (*new in 0.3.0*)
|
||||
- A user can change his email address (*new in 0.6.0*)
|
||||
- A user can choose between metric system and imperial system for distance, elevation and speed display (*new in 0.5.0*)
|
||||
- A user can choose to display or hide ascent records (*new in 0.6.11*)
|
||||
- A user can choose to display or hide ascent records and total on Dashboard (*new in 0.6.11*)
|
||||
- A user can set sport preferences (*new in 0.5.0*):
|
||||
- change sport color (used for sport image and charts)
|
||||
- can override stopped speed threshold (for next uploaded gpx files)
|
||||
|
2
fittrackee/dist/index.html
vendored
2
fittrackee/dist/index.html
vendored
@ -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.7132edc6.js"></script><script defer="defer" src="/static/js/app.ac1e5052.js"></script><link href="/static/css/app.f768a44b.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.7132edc6.js"></script><script defer="defer" src="/static/js/app.5447516d.js"></script><link href="/static/css/app.f768a44b.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>
|
2
fittrackee/dist/service-worker.js
vendored
2
fittrackee/dist/service-worker.js
vendored
File diff suppressed because one or more lines are too long
2
fittrackee/dist/service-worker.js.map
vendored
2
fittrackee/dist/service-worker.js.map
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/app.5447516d.js.map
vendored
Normal file
1
fittrackee/dist/static/js/app.5447516d.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,2 +1,2 @@
|
||||
"use strict";(self["webpackChunkfittrackee_client"]=self["webpackChunkfittrackee_client"]||[]).push([[193],{9161:function(e,s,t){t.r(s),t.d(s,{default:function(){return A}});t(6699);var a=t(6252),r=t(2262),l=t(3577),o=t(3324),n=t(9996);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:s}){const t=(0,r.iH)("month"),o=["week","month","year"];function n(e){t.value=e,s("timeFrameUpdate",e)}return(e,r)=>((0,a.wg)(),(0,a.iD)("div",c,[(0,a._)("div",i,[(0,a._)("i",{class:"fa fa-chevron-left","aria-hidden":"true",onClick:r[0]||(r[0]=e=>s("arrowClick",!0))})]),(0,a._)("div",u,[(0,a._)("div",d,[((0,a.wg)(),(0,a.iD)(a.HY,null,(0,a.Ko)(o,(s=>(0,a._)("div",{class:"time-frame custom-checkbox",key:s},[(0,a._)("label",null,[(0,a._)("input",{type:"radio",id:s,name:s,checked:t.value===s,onInput:e=>n(s)},null,40,p),(0,a._)("span",null,(0,l.zw)(e.$t(`statistics.TIME_FRAMES.${s}`)),1)])]))),64))])]),(0,a._)("div",m,[(0,a._)("i",{class:"fa fa-chevron-right","aria-hidden":"true",onClick:r[1]||(r[1]=e=>s("arrowClick",!1))})])]))}}),k=t(3744);const _=(0,k.Z)(v,[["__scopeId","data-v-22d55de2"]]);var S=_,w=t(631);const f={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:s}){const t=e,{t:n}=(0,o.QT)(),c=(0,a.f3)("sportColors"),{selectedSportIds:i}=(0,r.BK)(t),u=(0,a.Fl)((()=>(0,w.xH)(t.userSports,n)));function d(e){s("selectedSportIdsUpdate",e)}return(e,s)=>{const t=(0,a.up)("SportImage");return(0,a.wg)(),(0,a.iD)("div",f,[((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:s=>d(e.id)},null,40,h),(0,a.Wm)(t,{"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=t(9318);const y={key:0,id:"user-statistics"};var C=(0,a.aZ)({__name:"index",props:{sports:null,user:null},setup(e){const s=e,{t:t}=(0,o.QT)(),{sports:l,user:c}=(0,r.BK)(s),i=(0,r.iH)("month"),u=(0,r.iH)(v(i.value)),d=(0,a.Fl)((()=>(0,w.xH)(s.sports,t))),p=(0,r.iH)(_(s.sports));function m(e){i.value=e,u.value=v(i.value)}function v(e){return(0,T.aZ)(new Date,e,s.user.weekm)}function k(e){u.value=(0,T.FN)(u.value,e,s.user.weekm)}function _(e){return e.map((e=>e.id))}function f(e){p.value.includes(e)?p.value=p.value.filter((s=>s!==e)):p.value.push(e)}return(0,a.YP)((()=>s.sports),(e=>{p.value=_(e)})),(e,s)=>(0,r.SU)(d)?((0,a.wg)(),(0,a.iD)("div",y,[(0,a.Wm)(S,{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:f},null,8,["selected-sport-ids","user-sports"])])):(0,a.kq)("",!0)}});const F=(0,k.Z)(C,[["__scopeId","data-v-d693c7da"]]);var Z=F,x=t(5630),D=t(8602),H=t(9917);const E={id:"statistics",class:"view"},R={key:0,class:"container"};var W=(0,a.aZ)({__name:"StatisticsView",setup(e){const s=(0,H.o)(),t=(0,a.Fl)((()=>s.getters[D.YN.GETTERS.AUTH_USER_PROFILE])),o=(0,a.Fl)((()=>s.getters[D.O8.GETTERS.SPORTS].filter((e=>t.value.sports_list.includes(e.id)))));return(e,s)=>{const n=(0,a.up)("Card");return(0,a.wg)(),(0,a.iD)("div",E,[(0,r.SU)(t).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)(t).nb_workouts}),user:(0,r.SU)(t),sports:(0,r.SU)(o)},null,8,["class","user","sports"])])),_:1}),0===(0,r.SU)(t).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.440cd8b2.js.map
|
||||
//# sourceMappingURL=statistics.ef50f3c2.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
@ -38,6 +38,7 @@ class UserModelAssertMixin:
|
||||
assert 'nb_workouts' in serialized_user
|
||||
assert 'records' in serialized_user
|
||||
assert 'sports_list' in serialized_user
|
||||
assert 'total_ascent' in serialized_user
|
||||
assert 'total_distance' in serialized_user
|
||||
assert 'total_duration' in serialized_user
|
||||
|
||||
@ -169,6 +170,46 @@ class TestUserRecords(UserModelAssertMixin):
|
||||
)
|
||||
assert serialized_user['records'][0]['workout_date']
|
||||
|
||||
def test_it_returns_totals_when_user_has_workout_without_ascent(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
workout_cycling_user_1: Workout,
|
||||
) -> None:
|
||||
serialized_user = user_1.serialize(user_1)
|
||||
assert serialized_user['total_ascent'] == 0
|
||||
assert serialized_user['total_distance'] == 10
|
||||
assert serialized_user['total_duration'] == '1:00:00'
|
||||
|
||||
def test_it_returns_totals_when_user_has_workout_with_ascent(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
workout_cycling_user_1: Workout,
|
||||
) -> None:
|
||||
workout_cycling_user_1.ascent = 100
|
||||
serialized_user = user_1.serialize(user_1)
|
||||
assert serialized_user['total_ascent'] == 100
|
||||
assert serialized_user['total_distance'] == 10
|
||||
assert serialized_user['total_duration'] == '1:00:00'
|
||||
|
||||
def test_it_returns_totals_when_user_has_mutiple_workouts(
|
||||
self,
|
||||
app: Flask,
|
||||
user_1: User,
|
||||
sport_1_cycling: Sport,
|
||||
sport_2_running: Sport,
|
||||
workout_cycling_user_1: Workout,
|
||||
workout_running_user_1: Workout,
|
||||
) -> None:
|
||||
workout_cycling_user_1.ascent = 100
|
||||
serialized_user = user_1.serialize(user_1)
|
||||
assert serialized_user['total_ascent'] == 100
|
||||
assert serialized_user['total_distance'] == 22
|
||||
assert serialized_user['total_duration'] == '2:40:00'
|
||||
|
||||
|
||||
class TestUserWorkouts(UserModelAssertMixin):
|
||||
def test_it_returns_infos_when_no_workouts(
|
||||
|
@ -849,7 +849,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:<json boolean display_ascent: display highest ascent records
|
||||
:<json boolean display_ascent: display highest ascent records and total
|
||||
:<json boolean imperial_units: display distance in imperial units
|
||||
:<json string language: language preferences
|
||||
:<json string timezone: user time zone
|
||||
|
@ -128,7 +128,7 @@ class User(BaseModel):
|
||||
raise UserNotFoundException()
|
||||
|
||||
sports = []
|
||||
total = (0, '0:00:00')
|
||||
total = (0, '0:00:00', 0)
|
||||
if self.workouts_count > 0: # type: ignore
|
||||
sports = (
|
||||
db.session.query(Workout.sport_id)
|
||||
@ -139,7 +139,9 @@ class User(BaseModel):
|
||||
)
|
||||
total = (
|
||||
db.session.query(
|
||||
func.sum(Workout.distance), func.sum(Workout.duration)
|
||||
func.sum(Workout.distance),
|
||||
func.sum(Workout.duration),
|
||||
func.sum(Workout.ascent),
|
||||
)
|
||||
.filter(Workout.user_id == self.id)
|
||||
.first()
|
||||
@ -163,6 +165,7 @@ class User(BaseModel):
|
||||
'sports_list': [
|
||||
sport for sportslist in sports for sport in sportslist
|
||||
],
|
||||
'total_ascent': float(total[2]) if total[2] else 0.0,
|
||||
'total_distance': float(total[0]),
|
||||
'total_duration': str(total[1]),
|
||||
'username': self.username,
|
||||
|
@ -8,7 +8,13 @@
|
||||
<StatCard
|
||||
icon="road"
|
||||
:value="totalDistance"
|
||||
:text="unitTo === 'mi' ? 'miles' : unitTo"
|
||||
:text="distanceUnitTo === 'mi' ? 'miles' : distanceUnitTo"
|
||||
/>
|
||||
<StatCard
|
||||
v-if="user.display_ascent"
|
||||
icon="location-arrow"
|
||||
:value="totalAscent"
|
||||
:text="ascentUnitTo === 'ft' ? 'feet' : ascentUnitTo"
|
||||
/>
|
||||
<StatCard
|
||||
icon="clock-o"
|
||||
@ -16,6 +22,7 @@
|
||||
:text="totalDuration.duration"
|
||||
/>
|
||||
<StatCard
|
||||
v-if="!user.display_ascent"
|
||||
icon="tags"
|
||||
:value="user.nb_sports"
|
||||
:text="$t('workouts.SPORT', user.nb_sports)"
|
||||
@ -43,15 +50,23 @@
|
||||
() => props.user.total_duration
|
||||
)
|
||||
const totalDuration = computed(() => get_duration(userTotalDuration))
|
||||
const defaultUnitFrom: TUnit = 'km'
|
||||
const unitTo: TUnit = user.value.imperial_units
|
||||
? units[defaultUnitFrom].defaultTarget
|
||||
: defaultUnitFrom
|
||||
const distanceUnitFrom: TUnit = 'km'
|
||||
const distanceUnitTo: TUnit = user.value.imperial_units
|
||||
? units[distanceUnitFrom].defaultTarget
|
||||
: distanceUnitFrom
|
||||
const totalDistance: ComputedRef<number> = computed(() =>
|
||||
user.value.imperial_units
|
||||
? convertDistance(user.value.total_distance, defaultUnitFrom, unitTo, 2)
|
||||
: parseFloat(user.value.total_distance.toFixed(2))
|
||||
)
|
||||
? convertDistance(user.value.total_distance, distanceUnitFrom, distanceUnitTo, 2)
|
||||
: parseFloat(user.value.total_distance.toFixed(2)))
|
||||
const ascentUnitFrom: TUnit = 'm'
|
||||
const ascentUnitTo: TUnit = user.value.imperial_units
|
||||
? units[ascentUnitFrom].defaultTarget
|
||||
: ascentUnitFrom
|
||||
const totalAscent: ComputedRef<number> = computed(() =>
|
||||
user.value.imperial_units
|
||||
? convertDistance(user.value.total_ascent, ascentUnitFrom, ascentUnitTo, 2)
|
||||
: parseFloat(user.value.total_ascent.toFixed(2)))
|
||||
|
||||
|
||||
function get_duration(total_duration: ComputedRef<string>) {
|
||||
const duration = total_duration.value.match(/day/g)
|
||||
|
Loading…
Reference in New Issue
Block a user