Merge branch 'dev' into alternate_weather_api

This commit is contained in:
Sam
2022-12-28 10:25:13 +01:00
252 changed files with 13980 additions and 4458 deletions

View File

@ -148,8 +148,8 @@ class Workout(BaseModel):
distance = db.Column(db.Numeric(6, 3), nullable=True) # kilometers
min_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters
max_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters
descent = db.Column(db.Numeric(7, 2), nullable=True) # meters
ascent = db.Column(db.Numeric(7, 2), nullable=True) # meters
descent = db.Column(db.Numeric(8, 3), nullable=True) # meters
ascent = db.Column(db.Numeric(8, 3), nullable=True) # meters
max_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
ave_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
bounds = db.Column(postgresql.ARRAY(db.Float), nullable=True)
@ -296,8 +296,10 @@ class Workout(BaseModel):
'distance': float(self.distance) if self.distance else None,
'min_alt': float(self.min_alt) if self.min_alt else None,
'max_alt': float(self.max_alt) if self.max_alt else None,
'descent': float(self.descent) if self.descent else None,
'ascent': float(self.ascent) if self.ascent else None,
'descent': float(self.descent)
if self.descent is not None
else None,
'ascent': float(self.ascent) if self.ascent is not None else None,
'max_speed': float(self.max_speed) if self.max_speed else None,
'ave_speed': float(self.ave_speed) if self.ave_speed else None,
'with_gpx': self.gpx is not None,
@ -413,8 +415,8 @@ class WorkoutSegment(BaseModel):
distance = db.Column(db.Numeric(6, 3), nullable=True) # kilometers
min_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters
max_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters
descent = db.Column(db.Numeric(7, 2), nullable=True) # meters
ascent = db.Column(db.Numeric(7, 2), nullable=True) # meters
descent = db.Column(db.Numeric(8, 3), nullable=True) # meters
ascent = db.Column(db.Numeric(8, 3), nullable=True) # meters
max_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
ave_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h

View File

@ -160,6 +160,8 @@ def create_workout(
else float(new_workout.distance) / (duration.seconds / 3600)
)
new_workout.max_speed = new_workout.ave_speed
new_workout.ascent = workout_data.get('ascent')
new_workout.descent = workout_data.get('descent')
return new_workout
@ -239,6 +241,12 @@ def edit_workout(
else float(workout.distance) / (workout.duration.seconds / 3600)
)
workout.max_speed = workout.ave_speed
if 'ascent' in workout_data:
workout.ascent = workout_data.get('ascent')
if 'descent' in workout_data:
workout.descent = workout_data.get('descent')
return workout
@ -333,6 +341,14 @@ def process_one_gpx_file(
raise WorkoutException('fail', 'Error during workout save.', e)
def is_gpx_file(filename: str) -> bool:
return (
'.' in filename
and filename.rsplit('.', 1)[1].lower()
in current_app.config['WORKOUT_ALLOWED_EXTENSIONS']
)
def process_zip_archive(
common_params: Dict, extract_dir: str, stopped_speed_threshold: float
) -> List:
@ -341,21 +357,33 @@ def process_zip_archive(
does not exceed defined limit.
"""
with zipfile.ZipFile(common_params['file_path'], "r") as zip_ref:
max_file_size = current_app.config['max_single_file_size']
gpx_files_count = 0
files_with_invalid_size_count = 0
for zip_info in zip_ref.infolist():
if is_gpx_file(zip_info.filename):
gpx_files_count += 1
if zip_info.file_size > max_file_size:
files_with_invalid_size_count += 1
if gpx_files_count > current_app.config['gpx_limit_import']:
raise WorkoutException(
'fail', 'the number of files in the archive exceeds the limit'
)
if files_with_invalid_size_count > 0:
raise WorkoutException(
'fail',
'at least one file in zip archive exceeds size limit, '
'please check the archive',
)
zip_ref.extractall(extract_dir)
new_workouts = []
gpx_files_limit = current_app.config['gpx_limit_import']
gpx_files_ok = 0
for gpx_file in os.listdir(extract_dir):
if (
'.' in gpx_file
and gpx_file.rsplit('.', 1)[1].lower()
in current_app.config['WORKOUT_ALLOWED_EXTENSIONS']
):
gpx_files_ok += 1
if gpx_files_ok > gpx_files_limit:
break
if is_gpx_file(gpx_file):
file_path = os.path.join(extract_dir, gpx_file)
params = common_params
params['file_path'] = file_path

View File

@ -271,7 +271,7 @@ def get_workouts(auth_user: User) -> Union[Dict, HttpResponse]:
if order == 'asc'
else desc(workout_column),
)
.paginate(page, per_page, False)
.paginate(page=page, per_page=per_page, error_out=False)
)
workouts = workouts_pagination.items
return {
@ -956,7 +956,8 @@ def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
}
:form file: gpx file (allowed extensions: .gpx, .zip)
:form data: sport id and notes (example: ``{"sport_id": 1, "notes": ""}``)
:form data: sport id and notes (example: ``{"sport_id": 1, "notes": ""}``).
Double quotes in notes must be escaped.
:reqheader Authorization: OAuth 2.0 Bearer Token
@ -988,7 +989,11 @@ def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
if error_response:
return error_response
workout_data = json.loads(request.form['data'], strict=False)
try:
workout_data = json.loads(request.form['data'], strict=False)
except json.decoder.JSONDecodeError:
return InvalidPayloadErrorResponse()
if not workout_data or workout_data.get('sport_id') is None:
return InvalidPayloadErrorResponse()
@ -1022,7 +1027,7 @@ def post_workout(auth_user: User) -> Union[Tuple[Dict, int], HttpResponse]:
appLog.error(e.e)
if e.status == 'error':
return InternalServerErrorResponse(e.message)
return InvalidPayloadErrorResponse(e.message)
return InvalidPayloadErrorResponse(e.message, e.status)
shutil.rmtree(folders['extract_dir'], ignore_errors=True)
shutil.rmtree(folders['tmp_dir'], ignore_errors=True)
@ -1127,13 +1132,17 @@ def post_workout_no_gpx(
"status": "success"
}
:<json string workout_date: workout date, in user timezone
(format: ``%Y-%m-%d %H:%M``)
:<json float ascent: workout ascent (not mandatory,
must be provided with descent)
:<json float descent: workout descent (not mandatory,
must be provided with ascent)
:<json float distance: workout distance in km
:<json integer duration: workout duration in seconds
:<json string notes: notes (not mandatory)
:<json integer sport_id: workout sport id
:<json string title: workout title
:<json string title: workout title (not mandatory)
:<json string workout_date: workout date, in user timezone
(format: ``%Y-%m-%d %H:%M``)
:reqheader Authorization: OAuth 2.0 Bearer Token
@ -1149,13 +1158,27 @@ def post_workout_no_gpx(
workout_data = request.get_json()
if (
not workout_data
or workout_data.get('sport_id') is None
or workout_data.get('duration') is None
or workout_data.get('distance') is None
or workout_data.get('workout_date') is None
or not workout_data.get('sport_id')
or not workout_data.get('duration')
or not workout_data.get('distance')
or not workout_data.get('workout_date')
):
return InvalidPayloadErrorResponse()
ascent = workout_data.get('ascent')
descent = workout_data.get('descent')
try:
if (
(ascent is None and descent is not None)
or (ascent is not None and descent is None)
or (
(ascent is not None and descent is not None)
and (float(ascent) < 0 or float(descent) < 0)
)
):
return InvalidPayloadErrorResponse()
except ValueError:
return InvalidPayloadErrorResponse()
try:
new_workout = create_workout(auth_user, workout_data)
db.session.add(new_workout)
@ -1280,9 +1303,10 @@ def update_workout(
:param string workout_short_id: workout short id
:<json string workout_date: workout date in user timezone
(format: ``%Y-%m-%d %H:%M``)
(only for workout without gpx)
:<json float ascent: workout ascent
(only for workout without gpx, must be provided with descent)
:<json float descent: workout descent
(only for workout without gpx, must be provided with ascent)
:<json float distance: workout distance in km
(only for workout without gpx)
:<json integer duration: workout duration in seconds
@ -1290,6 +1314,9 @@ def update_workout(
:<json string notes: notes
:<json integer sport_id: workout sport id
:<json string title: workout title
:<json string workout_date: workout date in user timezone
(format: ``%Y-%m-%d %H:%M``)
(only for workout without gpx)
:reqheader Authorization: OAuth 2.0 Bearer Token
@ -1317,6 +1344,33 @@ def update_workout(
if response_object:
return response_object
if not workout.gpx:
try:
# for workout without gpx file, both elevation values must be
# provided.
if (
(
'ascent' in workout_data
and 'descent' not in workout_data
)
or (
'ascent' not in workout_data
and 'descent' in workout_data
)
) or (
not (
workout_data.get('ascent') is None
and workout_data.get('descent') is None
)
and (
float(workout_data.get('ascent')) < 0
or float(workout_data.get('descent')) < 0
)
):
return InvalidPayloadErrorResponse()
except (TypeError, ValueError):
return InvalidPayloadErrorResponse()
workout = edit_workout(workout, workout_data, auth_user)
db.session.commit()
return {