Merge branch 'dev' into alternate_weather_api
This commit is contained in:
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user