API - workouts refactoring
This commit is contained in:
parent
0b2e2ed5dd
commit
1b4a477544
@ -10,7 +10,7 @@ from werkzeug.datastructures import FileStorage
|
|||||||
|
|
||||||
from fittrackee import db
|
from fittrackee import db
|
||||||
from fittrackee.workouts.models import Sport, Workout, WorkoutSegment
|
from fittrackee.workouts.models import Sport, Workout, WorkoutSegment
|
||||||
from fittrackee.workouts.utils import StaticMap
|
from fittrackee.workouts.utils.maps import StaticMap
|
||||||
|
|
||||||
byte_io = BytesIO()
|
byte_io = BytesIO()
|
||||||
Image.new('RGB', (256, 256)).save(byte_io, 'PNG')
|
Image.new('RGB', (256, 256)).save(byte_io, 'PNG')
|
||||||
|
0
fittrackee/tests/workouts/test_utils/__init__.py
Normal file
0
fittrackee/tests/workouts/test_utils/__init__.py
Normal file
@ -7,7 +7,7 @@ from werkzeug.datastructures import FileStorage
|
|||||||
|
|
||||||
from fittrackee.users.models import User, UserSportPreference
|
from fittrackee.users.models import User, UserSportPreference
|
||||||
from fittrackee.workouts.models import Sport
|
from fittrackee.workouts.models import Sport
|
||||||
from fittrackee.workouts.utils import process_files
|
from fittrackee.workouts.utils.workouts import process_files
|
||||||
|
|
||||||
folders = {
|
folders = {
|
||||||
'extract_dir': '/tmp/fitTrackee/uploads',
|
'extract_dir': '/tmp/fitTrackee/uploads',
|
||||||
@ -38,7 +38,7 @@ class TestStoppedSpeedThreshold:
|
|||||||
expected_threshold: float,
|
expected_threshold: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
with patch(
|
with patch(
|
||||||
'fittrackee.workouts.utils.get_new_file_path',
|
'fittrackee.workouts.utils.workouts.get_new_file_path',
|
||||||
return_value='/tmp/fitTrackee/uploads/test.png',
|
return_value='/tmp/fitTrackee/uploads/test.png',
|
||||||
), patch(
|
), patch(
|
||||||
'gpxpy.gpx.GPXTrackSegment.get_moving_data',
|
'gpxpy.gpx.GPXTrackSegment.get_moving_data',
|
||||||
@ -68,7 +68,7 @@ class TestStoppedSpeedThreshold:
|
|||||||
expected_threshold = 0.7
|
expected_threshold = 0.7
|
||||||
user_sport_1_preference.stopped_speed_threshold = expected_threshold
|
user_sport_1_preference.stopped_speed_threshold = expected_threshold
|
||||||
with patch(
|
with patch(
|
||||||
'fittrackee.workouts.utils.get_new_file_path',
|
'fittrackee.workouts.utils.workouts.get_new_file_path',
|
||||||
return_value='/tmp/fitTrackee/uploads/test.png',
|
return_value='/tmp/fitTrackee/uploads/test.png',
|
||||||
), patch(
|
), patch(
|
||||||
'gpxpy.gpx.GPXTrackSegment.get_moving_data',
|
'gpxpy.gpx.GPXTrackSegment.get_moving_data',
|
@ -3,7 +3,7 @@ from typing import List
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from fittrackee.workouts.utils import get_average_speed
|
from fittrackee.workouts.utils.workouts import get_average_speed
|
||||||
|
|
||||||
|
|
||||||
class TestWorkoutAverageSpeed:
|
class TestWorkoutAverageSpeed:
|
@ -11,7 +11,7 @@ from flask import Flask
|
|||||||
|
|
||||||
from fittrackee.users.models import User
|
from fittrackee.users.models import User
|
||||||
from fittrackee.workouts.models import Sport, Workout
|
from fittrackee.workouts.models import Sport, Workout
|
||||||
from fittrackee.workouts.utils_id import decode_short_id
|
from fittrackee.workouts.utils.short_id import decode_short_id
|
||||||
|
|
||||||
from ..api_test_case import ApiTestCaseMixin, CallArgsMixin
|
from ..api_test_case import ApiTestCaseMixin, CallArgsMixin
|
||||||
|
|
||||||
|
@ -3,9 +3,9 @@ import os
|
|||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
|
from fittrackee.files import get_absolute_file_path
|
||||||
from fittrackee.users.models import User
|
from fittrackee.users.models import User
|
||||||
from fittrackee.workouts.models import Sport, Workout
|
from fittrackee.workouts.models import Sport, Workout
|
||||||
from fittrackee.workouts.utils import get_absolute_file_path
|
|
||||||
|
|
||||||
from ..api_test_case import ApiTestCaseMixin
|
from ..api_test_case import ApiTestCaseMixin
|
||||||
from .utils import get_random_short_id, post_an_workout
|
from .utils import get_random_short_id, post_an_workout
|
||||||
|
@ -5,7 +5,7 @@ from flask import Flask
|
|||||||
from fittrackee import db
|
from fittrackee import db
|
||||||
from fittrackee.users.models import User
|
from fittrackee.users.models import User
|
||||||
from fittrackee.workouts.models import Sport, Workout
|
from fittrackee.workouts.models import Sport, Workout
|
||||||
from fittrackee.workouts.utils_id import decode_short_id
|
from fittrackee.workouts.utils.short_id import decode_short_id
|
||||||
|
|
||||||
|
|
||||||
class TestWorkoutModel:
|
class TestWorkoutModel:
|
||||||
|
@ -5,7 +5,7 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from fittrackee.workouts.utils_id import encode_uuid
|
from fittrackee.workouts.utils.short_id import encode_uuid
|
||||||
|
|
||||||
|
|
||||||
def get_random_short_id() -> str:
|
def get_random_short_id() -> str:
|
||||||
|
@ -1,109 +0,0 @@
|
|||||||
import re
|
|
||||||
from typing import Optional, Tuple
|
|
||||||
|
|
||||||
from flask import Request
|
|
||||||
|
|
||||||
from fittrackee import db
|
|
||||||
from fittrackee.responses import (
|
|
||||||
ForbiddenErrorResponse,
|
|
||||||
HttpResponse,
|
|
||||||
UnauthorizedErrorResponse,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .exceptions import UserNotFoundException
|
|
||||||
from .models import User
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_email(email: str) -> bool:
|
|
||||||
"""
|
|
||||||
Return if email format is valid
|
|
||||||
"""
|
|
||||||
mail_pattern = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
|
|
||||||
return re.match(mail_pattern, email) is not None
|
|
||||||
|
|
||||||
|
|
||||||
def check_passwords(password: str, password_conf: str) -> str:
|
|
||||||
"""
|
|
||||||
Verify if password and password confirmation are the same and have
|
|
||||||
more than 8 characters
|
|
||||||
|
|
||||||
If not, it returns not empty string
|
|
||||||
"""
|
|
||||||
ret = ''
|
|
||||||
if password_conf != password:
|
|
||||||
ret = 'password: password and password confirmation do not match\n'
|
|
||||||
if len(password) < 8:
|
|
||||||
ret += 'password: 8 characters required\n'
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def check_username(username: str) -> str:
|
|
||||||
"""
|
|
||||||
Return if username is valid
|
|
||||||
"""
|
|
||||||
ret = ''
|
|
||||||
if not 2 < len(username) < 13:
|
|
||||||
ret += 'username: 3 to 12 characters required\n'
|
|
||||||
if not re.match(r'^[a-zA-Z0-9_]+$', username):
|
|
||||||
ret += (
|
|
||||||
'username: only alphanumeric characters and the '
|
|
||||||
'underscore character "_" allowed\n'
|
|
||||||
)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def register_controls(
|
|
||||||
username: str, email: str, password: str, password_conf: str
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Verify if username, email and passwords are valid
|
|
||||||
|
|
||||||
If not, it returns not empty string
|
|
||||||
"""
|
|
||||||
ret = check_username(username)
|
|
||||||
if not is_valid_email(email):
|
|
||||||
ret += 'email: valid email must be provided\n'
|
|
||||||
ret += check_passwords(password, password_conf)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def verify_user(
|
|
||||||
current_request: Request, verify_admin: bool
|
|
||||||
) -> Tuple[Optional[HttpResponse], Optional[User]]:
|
|
||||||
"""
|
|
||||||
Return authenticated user, if the provided token is valid and user has
|
|
||||||
admin rights if 'verify_admin' is True
|
|
||||||
"""
|
|
||||||
default_message = 'provide a valid auth token'
|
|
||||||
auth_header = current_request.headers.get('Authorization')
|
|
||||||
if not auth_header:
|
|
||||||
return UnauthorizedErrorResponse(default_message), None
|
|
||||||
auth_token = auth_header.split(' ')[1]
|
|
||||||
resp = User.decode_auth_token(auth_token)
|
|
||||||
if isinstance(resp, str):
|
|
||||||
return UnauthorizedErrorResponse(resp), None
|
|
||||||
user = User.query.filter_by(id=resp).first()
|
|
||||||
if not user:
|
|
||||||
return UnauthorizedErrorResponse(default_message), None
|
|
||||||
if verify_admin and not user.admin:
|
|
||||||
return ForbiddenErrorResponse(), None
|
|
||||||
return None, user
|
|
||||||
|
|
||||||
|
|
||||||
def can_view_workout(
|
|
||||||
auth_user_id: int, workout_user_id: int
|
|
||||||
) -> Optional[HttpResponse]:
|
|
||||||
"""
|
|
||||||
Return error response if user has no right to view workout
|
|
||||||
"""
|
|
||||||
if auth_user_id != workout_user_id:
|
|
||||||
return ForbiddenErrorResponse()
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def set_admin_rights(username: str) -> None:
|
|
||||||
user = User.query.filter_by(username=username).first()
|
|
||||||
if not user:
|
|
||||||
raise UserNotFoundException()
|
|
||||||
user.admin = True
|
|
||||||
db.session.commit()
|
|
@ -15,8 +15,8 @@ from sqlalchemy.types import JSON, Enum
|
|||||||
from fittrackee import db
|
from fittrackee import db
|
||||||
from fittrackee.files import get_absolute_file_path
|
from fittrackee.files import get_absolute_file_path
|
||||||
|
|
||||||
from .utils_format import convert_in_duration, convert_value_to_integer
|
from .utils.convert import convert_in_duration, convert_value_to_integer
|
||||||
from .utils_id import encode_uuid
|
from .utils.short_id import encode_uuid
|
||||||
|
|
||||||
BaseModel: DeclarativeMeta = db.Model
|
BaseModel: DeclarativeMeta = db.Model
|
||||||
record_types = [
|
record_types = [
|
||||||
|
@ -16,12 +16,9 @@ from fittrackee.users.decorators import authenticate, authenticate_as_admin
|
|||||||
from fittrackee.users.models import User
|
from fittrackee.users.models import User
|
||||||
|
|
||||||
from .models import Sport, Workout
|
from .models import Sport, Workout
|
||||||
from .utils import (
|
from .utils.convert import convert_timedelta_to_integer
|
||||||
get_average_speed,
|
from .utils.uploads import get_upload_dir_size
|
||||||
get_datetime_from_request_args,
|
from .utils.workouts import get_average_speed, get_datetime_from_request_args
|
||||||
get_upload_dir_size,
|
|
||||||
)
|
|
||||||
from .utils_format import convert_timedelta_to_integer
|
|
||||||
|
|
||||||
stats_blueprint = Blueprint('stats', __name__)
|
stats_blueprint = Blueprint('stats', __name__)
|
||||||
|
|
||||||
|
0
fittrackee/workouts/utils/__init__.py
Normal file
0
fittrackee/workouts/utils/__init__.py
Normal file
@ -3,8 +3,8 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
|
|
||||||
import gpxpy.gpx
|
import gpxpy.gpx
|
||||||
|
|
||||||
from .exceptions import WorkoutGPXException
|
from ..exceptions import WorkoutGPXException
|
||||||
from .utils_weather import get_weather
|
from .weather import get_weather
|
||||||
|
|
||||||
|
|
||||||
def open_gpx_file(gpx_file: str) -> Optional[gpxpy.gpx.GPX]:
|
def open_gpx_file(gpx_file: str) -> Optional[gpxpy.gpx.GPX]:
|
35
fittrackee/workouts/utils/maps.py
Normal file
35
fittrackee/workouts/utils/maps.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import hashlib
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
from staticmap import Line, StaticMap
|
||||||
|
|
||||||
|
from fittrackee.files import get_absolute_file_path
|
||||||
|
|
||||||
|
|
||||||
|
def generate_map(map_filepath: str, map_data: List) -> None:
|
||||||
|
"""
|
||||||
|
Generate and save map image from map data
|
||||||
|
"""
|
||||||
|
m = StaticMap(400, 225, 10)
|
||||||
|
if not current_app.config['TILE_SERVER']['DEFAULT_STATICMAP']:
|
||||||
|
m.url_template = current_app.config['TILE_SERVER']['URL'].replace(
|
||||||
|
'{s}.', ''
|
||||||
|
)
|
||||||
|
line = Line(map_data, '#3388FF', 4)
|
||||||
|
m.add_line(line)
|
||||||
|
image = m.render()
|
||||||
|
image.save(map_filepath)
|
||||||
|
|
||||||
|
|
||||||
|
def get_map_hash(map_filepath: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate a md5 hash used as id instead of workout id, to retrieve map
|
||||||
|
image (maps are sensitive data)
|
||||||
|
"""
|
||||||
|
md5 = hashlib.md5()
|
||||||
|
absolute_map_filepath = get_absolute_file_path(map_filepath)
|
||||||
|
with open(absolute_map_filepath, 'rb') as f:
|
||||||
|
for chunk in iter(lambda: f.read(128 * md5.block_size), b''):
|
||||||
|
md5.update(chunk)
|
||||||
|
return md5.hexdigest()
|
@ -5,7 +5,7 @@ import shortuuid
|
|||||||
|
|
||||||
def encode_uuid(uuid_value: UUID) -> str:
|
def encode_uuid(uuid_value: UUID) -> str:
|
||||||
"""
|
"""
|
||||||
Return short id string from an UUID
|
Return short id string from a UUID
|
||||||
"""
|
"""
|
||||||
return shortuuid.encode(uuid_value)
|
return shortuuid.encode(uuid_value)
|
||||||
|
|
16
fittrackee/workouts/utils/uploads.py
Normal file
16
fittrackee/workouts/utils/uploads.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from fittrackee.files import get_absolute_file_path
|
||||||
|
|
||||||
|
|
||||||
|
def get_upload_dir_size() -> int:
|
||||||
|
"""
|
||||||
|
Return upload directory size
|
||||||
|
"""
|
||||||
|
upload_path = get_absolute_file_path('')
|
||||||
|
total_size = 0
|
||||||
|
for dir_path, _, filenames in os.walk(upload_path):
|
||||||
|
for f in filenames:
|
||||||
|
fp = os.path.join(dir_path, f)
|
||||||
|
total_size += os.path.getsize(fp)
|
||||||
|
return total_size
|
@ -1,4 +1,3 @@
|
|||||||
import hashlib
|
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
@ -10,7 +9,6 @@ import gpxpy.gpx
|
|||||||
import pytz
|
import pytz
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from sqlalchemy import exc
|
from sqlalchemy import exc
|
||||||
from staticmap import Line, StaticMap
|
|
||||||
from werkzeug.datastructures import FileStorage
|
from werkzeug.datastructures import FileStorage
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
@ -18,9 +16,10 @@ from fittrackee import db
|
|||||||
from fittrackee.files import get_absolute_file_path
|
from fittrackee.files import get_absolute_file_path
|
||||||
from fittrackee.users.models import User, UserSportPreference
|
from fittrackee.users.models import User, UserSportPreference
|
||||||
|
|
||||||
from .exceptions import WorkoutException
|
from ..exceptions import WorkoutException
|
||||||
from .models import Sport, Workout, WorkoutSegment
|
from ..models import Sport, Workout, WorkoutSegment
|
||||||
from .utils_gpx import get_gpx_info
|
from .gpx import get_gpx_info
|
||||||
|
from .maps import generate_map, get_map_hash
|
||||||
|
|
||||||
|
|
||||||
def get_datetime_with_tz(
|
def get_datetime_with_tz(
|
||||||
@ -258,39 +257,11 @@ def get_new_file_path(
|
|||||||
return file_path
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
def generate_map(map_filepath: str, map_data: List) -> None:
|
|
||||||
"""
|
|
||||||
Generate and save map image from map data
|
|
||||||
"""
|
|
||||||
m = StaticMap(400, 225, 10)
|
|
||||||
if not current_app.config['TILE_SERVER']['DEFAULT_STATICMAP']:
|
|
||||||
m.url_template = current_app.config['TILE_SERVER']['URL'].replace(
|
|
||||||
'{s}.', ''
|
|
||||||
)
|
|
||||||
line = Line(map_data, '#3388FF', 4)
|
|
||||||
m.add_line(line)
|
|
||||||
image = m.render()
|
|
||||||
image.save(map_filepath)
|
|
||||||
|
|
||||||
|
|
||||||
def get_map_hash(map_filepath: str) -> str:
|
|
||||||
"""
|
|
||||||
Generate a md5 hash used as id instead of workout id, to retrieve map
|
|
||||||
image (maps are sensitive data)
|
|
||||||
"""
|
|
||||||
md5 = hashlib.md5()
|
|
||||||
absolute_map_filepath = get_absolute_file_path(map_filepath)
|
|
||||||
with open(absolute_map_filepath, 'rb') as f:
|
|
||||||
for chunk in iter(lambda: f.read(128 * md5.block_size), b''):
|
|
||||||
md5.update(chunk)
|
|
||||||
return md5.hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def process_one_gpx_file(
|
def process_one_gpx_file(
|
||||||
params: Dict, filename: str, stopped_speed_threshold: float
|
params: Dict, filename: str, stopped_speed_threshold: float
|
||||||
) -> Workout:
|
) -> Workout:
|
||||||
"""
|
"""
|
||||||
Get all data from a gpx file to create an workout with map image
|
Get all data from a gpx file to create a workout with map image
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
gpx_data, map_data, weather_data = get_gpx_info(
|
gpx_data, map_data, weather_data = get_gpx_info(
|
||||||
@ -433,19 +404,6 @@ def process_files(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_upload_dir_size() -> int:
|
|
||||||
"""
|
|
||||||
Return upload directory size
|
|
||||||
"""
|
|
||||||
upload_path = get_absolute_file_path('')
|
|
||||||
total_size = 0
|
|
||||||
for dir_path, _, filenames in os.walk(upload_path):
|
|
||||||
for f in filenames:
|
|
||||||
fp = os.path.join(dir_path, f)
|
|
||||||
total_size += os.path.getsize(fp)
|
|
||||||
return total_size
|
|
||||||
|
|
||||||
|
|
||||||
def get_average_speed(
|
def get_average_speed(
|
||||||
nb_workouts: int, total_average_speed: float, workout_average_speed: float
|
nb_workouts: int, total_average_speed: float, workout_average_speed: float
|
||||||
) -> float:
|
) -> float:
|
@ -33,7 +33,14 @@ from fittrackee.users.models import User
|
|||||||
from fittrackee.users.utils import can_view_workout
|
from fittrackee.users.utils import can_view_workout
|
||||||
|
|
||||||
from .models import Workout
|
from .models import Workout
|
||||||
from .utils import (
|
from .utils.convert import convert_in_duration
|
||||||
|
from .utils.gpx import (
|
||||||
|
WorkoutGPXException,
|
||||||
|
extract_segment_from_gpx_file,
|
||||||
|
get_chart_data,
|
||||||
|
)
|
||||||
|
from .utils.short_id import decode_short_id
|
||||||
|
from .utils.workouts import (
|
||||||
WorkoutException,
|
WorkoutException,
|
||||||
create_workout,
|
create_workout,
|
||||||
edit_workout,
|
edit_workout,
|
||||||
@ -41,13 +48,6 @@ from .utils import (
|
|||||||
get_datetime_from_request_args,
|
get_datetime_from_request_args,
|
||||||
process_files,
|
process_files,
|
||||||
)
|
)
|
||||||
from .utils_format import convert_in_duration
|
|
||||||
from .utils_gpx import (
|
|
||||||
WorkoutGPXException,
|
|
||||||
extract_segment_from_gpx_file,
|
|
||||||
get_chart_data,
|
|
||||||
)
|
|
||||||
from .utils_id import decode_short_id
|
|
||||||
|
|
||||||
workouts_blueprint = Blueprint('workouts', __name__)
|
workouts_blueprint = Blueprint('workouts', __name__)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user