Merge pull request #358 from jat255/max_speed_preference

Add user preference for filtering of GPX speed data
This commit is contained in:
Sam 2023-05-28 17:03:57 +02:00 committed by GitHub
commit a74c03d14a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 275 additions and 27 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.a33476ea.js"></script><script defer="defer" src="/static/js/app.464dd65e.js"></script><link href="/static/css/app.fa4567f8.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.a33476ea.js"></script><script defer="defer" src="/static/js/app.f72ca9c7.js"></script><link href="/static/css/app.3193986e.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

@ -0,0 +1,34 @@
"""Add user prefrence for gpx speed calculation
Revision ID: eff1c16c43eb
Revises: db58d195c5bf
Create Date: 2023-05-14 22:12:56.244291
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'eff1c16c43eb'
down_revision = 'db58d195c5bf'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('users', schema=None) as batch_op:
batch_op.add_column(sa.Column('use_raw_gpx_speed', sa.Boolean(), nullable=True))
op.execute("UPDATE users SET use_raw_gpx_speed = false")
op.alter_column('users', 'use_raw_gpx_speed', nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('users', schema=None) as batch_op:
batch_op.drop_column('use_raw_gpx_speed')
# ### end Alembic commands ###

View File

@ -59,6 +59,24 @@ def user_1_full() -> User:
return user
@pytest.fixture()
def user_1_raw_speed() -> User:
user = User(username='test', email='test@test.com', password='12345678')
user.first_name = 'John'
user.last_name = 'Doe'
user.bio = 'just a random guy'
user.location = 'somewhere'
user.language = 'en'
user.timezone = 'America/New_York'
user.birth_date = datetime.datetime.strptime('01/01/1980', '%d/%m/%Y')
user.is_active = True
user.use_raw_gpx_speed = True
user.accepted_policy = datetime.datetime.utcnow()
db.session.add(user)
db.session.commit()
return user
@pytest.fixture()
def user_1_paris() -> User:
user = User(username='test', email='test@test.com', password='12345678')

View File

@ -1459,6 +1459,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
imperial_units=True,
display_ascent=False,
start_elevation_at_zero=False,
use_raw_gpx_speed=True,
date_format='yyyy-MM-dd',
)
),
@ -1471,6 +1472,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
assert data['message'] == 'user preferences updated'
assert data['data']['display_ascent'] is False
assert data['data']['start_elevation_at_zero'] is False
assert data['data']['use_raw_gpx_speed'] is True
assert data['data']['imperial_units'] is True
assert data['data']['language'] == expected_language
assert data['data']['timezone'] == 'America/New_York'

View File

@ -54,7 +54,7 @@ class TestStoppedSpeedThreshold:
)
assert gpx_track_segment_mock.call_args_list[0] == call(
stopped_speed_threshold=expected_threshold
stopped_speed_threshold=expected_threshold, raw=False
)
gpx_track_segment_mock.assert_called_with(
expected_threshold, # stopped_speed_threshold
@ -88,7 +88,7 @@ class TestStoppedSpeedThreshold:
)
assert gpx_track_segment_mock.call_args_list[0] == call(
stopped_speed_threshold=expected_threshold
stopped_speed_threshold=expected_threshold, raw=False
)
gpx_track_segment_mock.assert_called_with(
expected_threshold, # stopped_speed_threshold
@ -98,6 +98,43 @@ class TestStoppedSpeedThreshold:
)
class TestUseRawGpxSpeed:
@pytest.mark.parametrize('input_use_raw_gpx_speed', [True, False])
def test_it_calls_get_moving_data_with_user_use_raw_gpx_speed_preference(
self,
app: Flask,
user_1: User,
gpx_file_storage: FileStorage,
sport_1_cycling: Sport,
input_use_raw_gpx_speed: bool,
) -> None:
user_1.use_raw_gpx_speed = input_use_raw_gpx_speed
with patch(
'fittrackee.workouts.utils.workouts.get_new_file_path',
return_value='/tmp/fitTrackee/uploads/test.png',
), patch(
'gpxpy.gpx.GPXTrackSegment.get_moving_data',
return_value=moving_data,
) as gpx_track_segment_mock:
process_files(
auth_user=user_1,
folders=folders,
workout_data={'sport_id': sport_1_cycling.id},
workout_file=gpx_file_storage,
)
assert gpx_track_segment_mock.call_args_list[0] == call(
stopped_speed_threshold=sport_1_cycling.stopped_speed_threshold,
raw=input_use_raw_gpx_speed,
)
gpx_track_segment_mock.assert_called_with(
sport_1_cycling.stopped_speed_threshold, # stopped_speed_threshold
False, # raw
IGNORE_TOP_SPEED_PERCENTILES, # speed_extreemes_percentiles
True, # ignore_nonstandard_distances
)
class TestGetGpxInfoStopTime:
def test_stop_time_equals_to_0_when_gpx_file_contains_one_segment(
self, gpx_file: str

View File

@ -279,6 +279,37 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
assert 'just a workout' == data['data']['workouts'][0]['title']
assert_workout_data_with_gpx(data)
def test_it_adds_a_workout_with_gpx_file_raw_speed(
self,
app: Flask,
user_1_raw_speed: User,
sport_1_cycling: Sport,
gpx_file: str,
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1_raw_speed.email
)
response = 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}',
),
)
data = json.loads(response.data.decode())
assert response.status_code == 201
assert 'created' in data['status']
assert len(data['data']['workouts']) == 1
# max speed should be slightly higher than that tested in
# assert_workout_data_with_gpx
assert data['data']['workouts'][0]['max_speed'] == pytest.approx(5.25)
def test_it_returns_ha_record_when_a_workout_without_gpx_exists(
self,
app: Flask,

View File

@ -879,6 +879,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
"total_ascent": 720.35,
"total_distance": 67.895,
"total_duration": "6:50:27",
"use_raw_gpx_speed": true,
"username": "sam"
"weekm": true,
},
@ -892,6 +893,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
:<json string language: language preferences
:<json boolean start_elevation_at_zero: do elevation plots start at zero?
:<json string timezone: user time zone
:<json boolean use_raw_gpx_speed: Use unfiltered gpx to calculate speeds
:<json boolean weekm: does week start on Monday?
:reqheader Authorization: OAuth 2.0 Bearer Token
@ -915,6 +917,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
'language',
'start_elevation_at_zero',
'timezone',
'use_raw_gpx_speed',
'weekm',
}
if not post_data or not post_data.keys() >= user_mandatory_data:
@ -925,6 +928,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
imperial_units = post_data.get('imperial_units')
language = get_language(post_data.get('language'))
start_elevation_at_zero = post_data.get('start_elevation_at_zero')
use_raw_gpx_speed = post_data.get('use_raw_gpx_speed')
timezone = post_data.get('timezone')
weekm = post_data.get('weekm')
@ -935,6 +939,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
auth_user.language = language
auth_user.start_elevation_at_zero = start_elevation_at_zero
auth_user.timezone = timezone
auth_user.use_raw_gpx_speed = use_raw_gpx_speed
auth_user.weekm = weekm
db.session.commit()

View File

@ -62,6 +62,7 @@ class User(BaseModel):
start_elevation_at_zero = db.Column(
db.Boolean, default=True, nullable=False
)
use_raw_gpx_speed = db.Column(db.Boolean, default=False, nullable=False)
def __repr__(self) -> str:
return f'<User {self.username!r}>'
@ -216,6 +217,7 @@ class User(BaseModel):
'language': self.language,
'start_elevation_at_zero': self.start_elevation_at_zero,
'timezone': self.timezone,
'use_raw_gpx_speed': self.use_raw_gpx_speed,
'weekm': self.weekm,
},
}

View File

@ -73,6 +73,7 @@ def get_gpx_info(
stopped_speed_threshold: float,
update_map_data: Optional[bool] = True,
update_weather_data: Optional[bool] = True,
use_raw_gpx_speed: bool = False,
) -> Tuple:
"""
Parse and return gpx, map and weather data from gpx file
@ -128,7 +129,8 @@ def get_gpx_info(
if update_map_data:
map_data.append([point.longitude, point.latitude])
moving_data = segment.get_moving_data(
stopped_speed_threshold=stopped_speed_threshold
stopped_speed_threshold=stopped_speed_threshold,
raw=use_raw_gpx_speed,
)
if moving_data:
calculated_max_speed = moving_data.max_speed

View File

@ -299,10 +299,12 @@ def process_one_gpx_file(
absolute_gpx_filepath = None
absolute_map_filepath = None
try:
gpx_data, map_data, weather_data = get_gpx_info(
params['file_path'], stopped_speed_threshold
)
auth_user = params['auth_user']
gpx_data, map_data, weather_data = get_gpx_info(
gpx_file=params['file_path'],
stopped_speed_threshold=stopped_speed_threshold,
use_raw_gpx_speed=auth_user.use_raw_gpx_speed,
)
workout_date, _ = get_workout_datetime(
workout_date=gpx_data['start'],
date_str_format=None if gpx_data else '%Y-%m-%d %H:%M',

View File

@ -20,7 +20,31 @@
<dt>{{ $t('user.PROFILE.ASCENT_DATA') }}:</dt>
<dd>{{ $t(`common.${display_ascent}`) }}</dd>
<dt>{{ $t('user.PROFILE.ELEVATION_CHART_START.LABEL') }}:</dt>
<dd>{{ $t(`user.PROFILE.ELEVATION_CHART_START.${user.start_elevation_at_zero ? 'ZERO' : 'MIN_ALT'}`) }}</dd>
<dd>
{{
$t(
`user.PROFILE.ELEVATION_CHART_START.${
user.start_elevation_at_zero ? 'ZERO' : 'MIN_ALT'
}`
)
}}
</dd>
<dt>{{ $t('user.PROFILE.USE_RAW_GPX_SPEED.LABEL') }}:</dt>
<dd>
{{
$t(
`user.PROFILE.USE_RAW_GPX_SPEED.${
user.use_raw_gpx_speed ? 'RAW_SPEED' : 'FILTERED_SPEED'
}`
)
}}
</dd>
<div class="info-box raw-speed-help">
<span>
<i class="fa fa-info-circle" aria-hidden="true" />
{{ $t('user.PROFILE.USE_RAW_GPX_SPEED.HELP') }}
</span>
</div>
</dl>
<div class="profile-buttons">
<button @click="$router.push('/profile/edit/preferences')">

View File

@ -104,7 +104,10 @@
{{ $t('user.PROFILE.ELEVATION_CHART_START.LABEL') }}
</span>
<div class="checkboxes">
<label v-for="status in startElevationAtZeroData" :key="status.label">
<label
v-for="status in startElevationAtZeroData"
:key="status.label"
>
<input
type="radio"
:id="status.label"
@ -119,6 +122,32 @@
</label>
</div>
</div>
<div class="form-items form-checkboxes">
<span class="checkboxes-label">
{{ $t('user.PROFILE.USE_RAW_GPX_SPEED.LABEL') }}
</span>
<div class="checkboxes">
<label v-for="status in useRawGpxSpeed" :key="status.label">
<input
type="radio"
:id="status.label"
:name="status.label"
:checked="status.value === userForm.use_raw_gpx_speed"
:disabled="loading"
@input="updateUseRawGpxSpeed(status.value)"
/>
<span class="checkbox-label">
{{ $t(`user.PROFILE.USE_RAW_GPX_SPEED.${status.label}`) }}
</span>
</label>
</div>
<div class="info-box raw-speed-help">
<span>
<i class="fa fa-info-circle" aria-hidden="true" />
{{ $t('user.PROFILE.USE_RAW_GPX_SPEED.HELP') }}
</span>
</div>
</div>
<div class="form-buttons">
<button class="confirm" type="submit">
{{ $t('buttons.SUBMIT') }}
@ -193,12 +222,22 @@
const startElevationAtZeroData = [
{
label: 'ZERO',
value: true
value: true,
},
{
label: 'MIN_ALT',
value: false
}
value: false,
},
]
const useRawGpxSpeed = [
{
label: 'FILTERED_SPEED',
value: false,
},
{
label: 'RAW_SPEED',
value: true,
},
]
const loading = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
@ -222,7 +261,12 @@
function updateUserForm(user: IAuthUserProfile) {
userForm.display_ascent = user.display_ascent
userForm.start_elevation_at_zero = user.start_elevation_at_zero ? user.start_elevation_at_zero : false
userForm.start_elevation_at_zero = user.start_elevation_at_zero
? user.start_elevation_at_zero
: false
userForm.use_raw_gpx_speed = user.use_raw_gpx_speed
? user.use_raw_gpx_speed
: false
userForm.imperial_units = user.imperial_units ? user.imperial_units : false
userForm.language = user.language ? user.language : 'en'
userForm.timezone = user.timezone ? user.timezone : 'Europe/Paris'
@ -238,6 +282,9 @@
function updateStartElevationAtZero(value: boolean) {
userForm.start_elevation_at_zero = value
}
function updateUseRawGpxSpeed(value: boolean) {
userForm.use_raw_gpx_speed = value
}
function updateAscentDisplay(value: boolean) {
userForm.display_ascent = value
}

View File

@ -116,6 +116,11 @@
"IMPERIAL": "Imperiales System (ft, mi, mph, °F)",
"LABEL": "Einheiten für die Distanz",
"METRIC": "Metrisches System (m, km, m/s, °C)"
},
"USE_RAW_GPX_SPEED": {
"FILTERED_SPEED": "Gefiltert",
"LABEL": "Höchstgeschwindigkeitsstrategie",
"RAW_SPEED": "Rau"
}
},
"READ_AND_ACCEPT_PRIVACY_POLICY": "Ich habe die {0} gelesen und stimme ihr zu.",

View File

@ -68,12 +68,12 @@
"EDIT": "Edit profile",
"EDIT_PREFERENCES": "Edit preferences",
"EDIT_SPORTS_PREFERENCES": "Edit sports preferences",
"ERRORED_EMAIL_UPDATE": "Please {0} to change your email address again or contact the administrator",
"ELEVATION_CHART_START": {
"LABEL": "Elevation chart starts at",
"ZERO": "Zero",
"MIN_ALT": "Minimum altitude"
"MIN_ALT": "Minimum altitude",
"ZERO": "Zero"
},
"ERRORED_EMAIL_UPDATE": "Please {0} to change your email address again or contact the administrator",
"FIRST_DAY_OF_WEEK": "First day of week",
"FIRST_NAME": "First name",
"LANGUAGE": "Language",
@ -116,6 +116,12 @@
"IMPERIAL": "Imperial system (ft, mi, mph, °F)",
"LABEL": "Units for distance",
"METRIC": "Metric system (m, km, m/s, °C)"
},
"USE_RAW_GPX_SPEED": {
"FILTERED_SPEED": "Filtered",
"HELP": "If filtered, it excludes extreme values (which may be GPS errors) when calculating the maximum speed.",
"LABEL": "GPX max speed strategy",
"RAW_SPEED": "Raw"
}
},
"READ_AND_ACCEPT_PRIVACY_POLICY": "I have read and agree to the {0}.",

View File

@ -116,6 +116,11 @@
"IMPERIAL": "Sistema Imperial (ft, mi, mph, ºF)",
"LABEL": "Unidades de distancia",
"METRIC": "Sistema Métrico (m, km, m/s, ºC)"
},
"USE_RAW_GPX_SPEED": {
"FILTERED_SPEED": "Filtrado",
"LABEL": "Estrategia de velocidad máxima",
"RAW_SPEED": "Crudo"
}
},
"READ_AND_ACCEPT_PRIVACY_POLICY": "He leído y aceptado la {0}.",

View File

@ -70,8 +70,8 @@
"EDIT_SPORTS_PREFERENCES": "Modifier les préférences des sports",
"ELEVATION_CHART_START": {
"LABEL": "Début de l'axe pour le graphe affichant l'altitude",
"ZERO": "0",
"MIN_ALT": "Altitude minimale"
"MIN_ALT": "Altitude minimale",
"ZERO": "0"
},
"ERRORED_EMAIL_UPDATE": "Veuillez vous {0} pour changer de nouveau votre adresse électronique ou contacter l'administrateur",
"FIRST_DAY_OF_WEEK": "Premier jour de la semaine",
@ -116,6 +116,12 @@
"IMPERIAL": "Système impérial (ft, mi, mph, °F)",
"LABEL": "Unités pour les distances",
"METRIC": "Système métrique (m, km, m/s, °C)"
},
"USE_RAW_GPX_SPEED": {
"FILTERED_SPEED": "Filtré",
"HELP": "Si filtré, les valeurs extrêmes (qui peuvent être des erreurs GPS) sont exclues lors du calcul de la vitesse maximale.",
"LABEL": "Stratégie de vitesse maximale",
"RAW_SPEED": "Brut"
}
},
"READ_AND_ACCEPT_PRIVACY_POLICY": "J'ai lu et accepte la {0}.",

View File

@ -116,6 +116,11 @@
"IMPERIAL": "Sistema Imperial (ft, mi, mph, ºF)",
"LABEL": "Unidades de distancia",
"METRIC": "Sistema Métrico (m, km, m/s, ºC)"
},
"USE_RAW_GPX_SPEED": {
"FILTERED_SPEED": "Filtrado",
"LABEL": "Estratexia de velocidade máxima",
"RAW_SPEED": "Cru"
}
},
"READ_AND_ACCEPT_PRIVACY_POLICY": "Lin e acepto a {0}.",

View File

@ -97,6 +97,11 @@
"IMPERIAL": "Sistema imperiale (ft, mi, mph, °F)",
"LABEL": "Unità per la distanza",
"METRIC": "Sistema metrico (m, km, m/s, °C)"
},
"USE_RAW_GPX_SPEED": {
"FILTERED_SPEED": "Filtrato",
"LABEL": "Strategia di massima velocità",
"RAW_SPEED": "Greggio"
}
},
"REGISTER": "Registra",

View File

@ -47,7 +47,12 @@
"TABS": {
"PROFILE": "profil"
},
"TIMEZONE": "Tidssone"
"TIMEZONE": "Tidssone",
"USE_RAW_GPX_SPEED": {
"FILTERED_SPEED": "Filtrert",
"LABEL": "Maks hastighet strategi",
"RAW_SPEED": "Rå"
}
},
"RESET_PASSWORD": "Tilbakestill passordet ditt",
"SHOW_PASSWORD": "vis passord",

View File

@ -116,6 +116,11 @@
"IMPERIAL": "Imperialistisch systeem (ft, mi, mph, °F)",
"LABEL": "Eenheid voor afstand",
"METRIC": "Metrisch systeem (m, km, m/s, °C)"
},
"USE_RAW_GPX_SPEED": {
"FILTERED_SPEED": "Gefilterd",
"LABEL": "Strategie voor maximale snelheid",
"RAW_SPEED": "Rauw"
}
},
"READ_AND_ACCEPT_PRIVACY_POLICY": "Ik heb het {0} gelezen en goedgekeurd.",

View File

@ -29,6 +29,7 @@ export interface IAuthUserProfile extends IUserProfile {
display_ascent: boolean
imperial_units: boolean
start_elevation_at_zero: boolean
use_raw_gpx_speed: boolean
language: string | null
timezone: string
date_format: string
@ -65,6 +66,7 @@ export interface IAdminUserPayload {
export interface IUserPreferencesPayload {
display_ascent: boolean
start_elevation_at_zero: boolean
use_raw_gpx_speed: boolean
imperial_units: boolean
language: string
timezone: string