Merge pull request #334 from jat255/filter_workouts_by_name

Add filter for workouts by title
This commit is contained in:
Sam 2023-04-08 12:55:33 +02:00 committed by GitHub
commit 89fd4e27f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 362 additions and 213 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.01cb48d3.js"></script><script defer="defer" src="/static/js/app.fbd9f0c5.js"></script><link href="/static/css/app.5f8309dc.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.01cb48d3.js"></script><script defer="defer" src="/static/js/app.d68a5506.js"></script><link href="/static/css/app.5f8309dc.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

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],{7885:function(e,s,t){t.r(s),t.d(s,{default:function(){return A}});var a=t(6252),r=t(2262),l=t(3577),o=(t(7658),t(9150)),n=t(436);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-30799d13"]]);var Z=F,x=t(5630),D=t(5801),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.3b4abef2.js.map
//# sourceMappingURL=statistics.1a300331.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

@ -116,6 +116,7 @@ def seven_workouts_user_1() -> List[Workout]:
distance=5,
duration=datetime.timedelta(seconds=1024),
)
workout_1.title = "Workout 1 of 7"
update_workout(workout_1)
workout_1.ascent = 120
workout_1.descent = 200
@ -130,6 +131,7 @@ def seven_workouts_user_1() -> List[Workout]:
distance=10,
duration=datetime.timedelta(seconds=3456),
)
workout_2.title = "Workout 2 of 7"
update_workout(workout_2)
workout_2.ascent = 100
workout_2.descent = 80
@ -144,6 +146,7 @@ def seven_workouts_user_1() -> List[Workout]:
distance=10,
duration=datetime.timedelta(seconds=1024),
)
workout_3.title = "Workout 3 of 7"
update_workout(workout_3)
workout_3.ascent = 80
workout_3.descent = 100
@ -160,6 +163,7 @@ def seven_workouts_user_1() -> List[Workout]:
distance=1,
duration=datetime.timedelta(seconds=600),
)
workout_4.title = "Workout 4 of 7"
update_workout(workout_4)
workout_4.ascent = 120
workout_4.descent = 180
@ -174,6 +178,7 @@ def seven_workouts_user_1() -> List[Workout]:
distance=10,
duration=datetime.timedelta(seconds=1000),
)
workout_5.title = "Workout 5 of 7"
update_workout(workout_5)
workout_5.ascent = 100
workout_5.descent = 200
@ -188,6 +193,7 @@ def seven_workouts_user_1() -> List[Workout]:
distance=8,
duration=datetime.timedelta(seconds=6000),
)
workout_6.title = "Workout 6 of 7"
update_workout(workout_6)
workout_6.ascent = 40
workout_6.descent = 20
@ -202,6 +208,7 @@ def seven_workouts_user_1() -> List[Workout]:
distance=10,
duration=datetime.timedelta(seconds=3000),
)
workout_7.title = "Workout 7 of 7"
update_workout(workout_7)
db.session.add(workout_7)
db.session.commit()

View File

@ -950,6 +950,52 @@ class TestGetWorkoutsWithFilters(ApiTestCaseMixin):
'total': 1,
}
def test_it_gets_one_workout_with_title_filter(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_workouts_user_1: List[Workout],
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.get(
'/api/workouts?title=3 of 7',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
workouts = data['data']['workouts']
assert len(workouts) == 1
assert 'Workout 3 of 7' == workouts[0]['title']
assert 'Mon, 01 Jan 2018 00:00:00 GMT' == workouts[0]['workout_date']
def test_it_gets_no_workouts_with_title_filter(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_workouts_user_1: List[Workout],
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.get(
'/api/workouts?title=no_such_title',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
workouts = data['data']['workouts']
assert len(workouts) == 0
class TestGetWorkoutsWithFiltersAndPagination(ApiTestCaseMixin):
def test_it_gets_page_2_with_date_filter(
@ -1024,6 +1070,38 @@ class TestGetWorkoutsWithFiltersAndPagination(ApiTestCaseMixin):
'total': 7,
}
def test_it_gets_all_workouts_with_title_filter(
self,
app: Flask,
user_1: User,
sport_1_cycling: Sport,
seven_workouts_user_1: List[Workout],
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.get(
'/api/workouts?title=of 7',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
data = json.loads(response.data.decode())
assert response.status_code == 200
assert 'success' in data['status']
assert len(data['data']['workouts']) == 5
assert (
'Wed, 09 May 2018 00:00:00 GMT'
== data['data']['workouts'][0]['workout_date']
)
assert data['pagination'] == {
'has_next': True,
'has_prev': False,
'page': 1,
'pages': 2,
'total': 7,
}
class TestGetWorkout(ApiTestCaseMixin):
def test_it_gets_a_workout(

View File

@ -187,6 +187,8 @@ def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]:
:query integer per_page: number of workouts per page
(default: 5, max: 100)
:query integer sport_id: sport id
:quert string title: any part (or all) of the workout title;
title matching is case-insensitive
:query string from: start date (format: ``%Y-%m-%d``)
:query string to: end date (format: ``%Y-%m-%d``)
:query float distance_from: minimal distance
@ -230,6 +232,7 @@ def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]:
)
order = params.get('order', 'desc')
sport_id = params.get('sport_id')
title = params.get('title')
per_page = int(params.get('per_page', DEFAULT_WORKOUTS_PER_PAGE))
if per_page > MAX_WORKOUTS_PER_PAGE:
per_page = MAX_WORKOUTS_PER_PAGE
@ -237,6 +240,7 @@ def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]:
Workout.query.filter(
Workout.user_id == auth_user.id,
Workout.sport_id == sport_id if sport_id else True,
Workout.title.ilike(f"%{title}%") if title else True,
Workout.workout_date >= date_from if date_from else True,
Workout.workout_date < date_to + timedelta(seconds=1)
if date_to

View File

@ -1,7 +1,8 @@
<template>
<div class="workouts-filters">
<div class="box">
<div class="form">
<form v-on:submit.prevent="onSubmit" class="form">
<div class="form-all-items">
<div class="form-items-group">
<div class="form-item">
<label> {{ $t('workouts.FROM') }}: </label>
@ -43,6 +44,20 @@
</option>
</select>
</div>
<div class="form-item form-item-title">
<label> {{ $t('workouts.TITLE', 1) }}:</label>
<div class="form-inputs-group">
<input
class="title"
name="title"
:value="$route.query.title"
@change="handleFilterChange"
placeholder=""
type="text"
@keyup.enter="submit"
/>
</div>
</div>
</div>
<div class="form-items-group">
@ -56,6 +71,7 @@
step="0.1"
:value="$route.query.distance_from"
@change="handleFilterChange"
@keyup.enter="submit"
/>
<span>{{ $t('workouts.TO') }}</span>
<input
@ -65,6 +81,7 @@
step="0.1"
:value="$route.query.distance_to"
@change="handleFilterChange"
@keyup.enter="submit"
/>
</div>
</div>
@ -81,6 +98,7 @@
pattern="^([0-9]*[0-9]):([0-5][0-9])$"
placeholder="hh:mm"
type="text"
@keyup.enter="submit"
/>
<span>{{ $t('workouts.TO') }}</span>
<input
@ -90,6 +108,7 @@
pattern="^([0-9]*[0-9]):([0-5][0-9])$"
placeholder="hh:mm"
type="text"
@keyup.enter="submit"
/>
</div>
</div>
@ -106,6 +125,7 @@
@change="handleFilterChange"
step="0.1"
type="number"
@keyup.enter="submit"
/>
<span>{{ $t('workouts.TO') }}</span>
<input
@ -115,6 +135,7 @@
@change="handleFilterChange"
step="0.1"
type="number"
@keyup.enter="submit"
/>
</div>
</div>
@ -132,6 +153,7 @@
@change="handleFilterChange"
step="0.1"
type="number"
@keyup.enter="submit"
/>
<span>{{ $t('workouts.TO') }}</span>
<input
@ -141,6 +163,7 @@
@change="handleFilterChange"
step="0.1"
type="number"
@keyup.enter="submit"
/>
</div>
</div>
@ -148,13 +171,14 @@
</div>
<div class="form-button">
<button class="confirm" @click="onFilter">
<button type="submit" class="confirm" @click="onFilter">
{{ $t('buttons.FILTER') }}
</button>
<button class="confirm" @click="onClearFilter">
{{ $t('buttons.CLEAR_FILTER') }}
</button>
</div>
</form>
</div>
</div>
</template>
@ -223,6 +247,7 @@
.workouts-filters {
.form {
.form-all-items {
display: flex;
flex-direction: column;
padding-top: 0;
@ -255,10 +280,17 @@
}
select {
height: 36px;
height: 38px;
padding: 0 $default-padding * 0.5;
}
}
.form-item-title {
padding-top: $default-padding;
input.title {
width: 100%;
}
}
}
}
}
@ -276,6 +308,7 @@
@media screen and (max-width: $medium-limit) {
.form {
.form-all-items {
flex-direction: row;
padding-top: $default-padding * 0.5;
@ -284,7 +317,7 @@
height: 100%;
.form-item {
label {
label, span {
font-size: 0.9em;
}
@ -294,8 +327,16 @@
padding: 0;
input {
width: 75%;
width: 85%;
}
span {
padding: 0;
}
}
}
.form-item-title {
padding-top: 0;
}
}
}
@ -311,6 +352,7 @@
}
@media screen and (max-width: $small-limit) {
.form {
.form-all-items {
flex-direction: column;
padding-top: 0;
@ -326,9 +368,11 @@
flex-direction: row;
justify-content: space-around;
align-items: center;
input {
width: 50%;
}
span {
padding: $default-padding * 0.5;
}
@ -336,6 +380,7 @@
}
}
}
}
.form-button {
flex-wrap: initial;
button {
@ -348,6 +393,20 @@
.form-button {
flex-wrap: wrap;
}
.form {
.form-all-items {
.form-items-group {
.form-item-title {
padding-top: $default-padding;
input.title {
width: 100%;
}
}
}
}
}
}
}
</style>

View File

@ -66,6 +66,7 @@ export const workoutsPayloadKeys = [
'duration_from',
'duration_to',
'sport_id',
'title'
]
const getRange = (stop: number, start = 1): number[] => {