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.workouts.models import Sport, Workout, WorkoutSegment
|
||||
from fittrackee.workouts.utils import StaticMap
|
||||
from fittrackee.workouts.utils.maps import StaticMap
|
||||
|
||||
byte_io = BytesIO()
|
||||
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.workouts.models import Sport
|
||||
from fittrackee.workouts.utils import process_files
|
||||
from fittrackee.workouts.utils.workouts import process_files
|
||||
|
||||
folders = {
|
||||
'extract_dir': '/tmp/fitTrackee/uploads',
|
||||
@ -38,7 +38,7 @@ class TestStoppedSpeedThreshold:
|
||||
expected_threshold: float,
|
||||
) -> None:
|
||||
with patch(
|
||||
'fittrackee.workouts.utils.get_new_file_path',
|
||||
'fittrackee.workouts.utils.workouts.get_new_file_path',
|
||||
return_value='/tmp/fitTrackee/uploads/test.png',
|
||||
), patch(
|
||||
'gpxpy.gpx.GPXTrackSegment.get_moving_data',
|
||||
@ -68,7 +68,7 @@ class TestStoppedSpeedThreshold:
|
||||
expected_threshold = 0.7
|
||||
user_sport_1_preference.stopped_speed_threshold = expected_threshold
|
||||
with patch(
|
||||
'fittrackee.workouts.utils.get_new_file_path',
|
||||
'fittrackee.workouts.utils.workouts.get_new_file_path',
|
||||
return_value='/tmp/fitTrackee/uploads/test.png',
|
||||
), patch(
|
||||
'gpxpy.gpx.GPXTrackSegment.get_moving_data',
|
@ -3,7 +3,7 @@ from typing import List
|
||||
|
||||
import pytest
|
||||
|
||||
from fittrackee.workouts.utils import get_average_speed
|
||||
from fittrackee.workouts.utils.workouts import get_average_speed
|
||||
|
||||
|
||||
class TestWorkoutAverageSpeed:
|
@ -11,7 +11,7 @@ from flask import Flask
|
||||
|
||||
from fittrackee.users.models import User
|
||||
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
|
||||
|
||||
|
@ -3,9 +3,9 @@ import os
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from fittrackee.files import get_absolute_file_path
|
||||
from fittrackee.users.models import User
|
||||
from fittrackee.workouts.models import Sport, Workout
|
||||
from fittrackee.workouts.utils import get_absolute_file_path
|
||||
|
||||
from ..api_test_case import ApiTestCaseMixin
|
||||
from .utils import get_random_short_id, post_an_workout
|
||||
|
@ -5,7 +5,7 @@ from flask import Flask
|
||||
from fittrackee import db
|
||||
from fittrackee.users.models import User
|
||||
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:
|
||||
|
@ -5,7 +5,7 @@ from uuid import uuid4
|
||||
|
||||
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:
|
||||
|
@ -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.files import get_absolute_file_path
|
||||
|
||||
from .utils_format import convert_in_duration, convert_value_to_integer
|
||||
from .utils_id import encode_uuid
|
||||
from .utils.convert import convert_in_duration, convert_value_to_integer
|
||||
from .utils.short_id import encode_uuid
|
||||
|
||||
BaseModel: DeclarativeMeta = db.Model
|
||||
record_types = [
|
||||
|
@ -16,12 +16,9 @@ from fittrackee.users.decorators import authenticate, authenticate_as_admin
|
||||
from fittrackee.users.models import User
|
||||
|
||||
from .models import Sport, Workout
|
||||
from .utils import (
|
||||
get_average_speed,
|
||||
get_datetime_from_request_args,
|
||||
get_upload_dir_size,
|
||||
)
|
||||
from .utils_format import convert_timedelta_to_integer
|
||||
from .utils.convert import convert_timedelta_to_integer
|
||||
from .utils.uploads import get_upload_dir_size
|
||||
from .utils.workouts import get_average_speed, get_datetime_from_request_args
|
||||
|
||||
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
|
||||
|
||||
from .exceptions import WorkoutGPXException
|
||||
from .utils_weather import get_weather
|
||||
from ..exceptions import WorkoutGPXException
|
||||
from .weather import get_weather
|
||||
|
||||
|
||||
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:
|
||||
"""
|
||||
Return short id string from an UUID
|
||||
Return short id string from a UUID
|
||||
"""
|
||||
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 tempfile
|
||||
import zipfile
|
||||
@ -10,7 +9,6 @@ import gpxpy.gpx
|
||||
import pytz
|
||||
from flask import current_app
|
||||
from sqlalchemy import exc
|
||||
from staticmap import Line, StaticMap
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
@ -18,9 +16,10 @@ from fittrackee import db
|
||||
from fittrackee.files import get_absolute_file_path
|
||||
from fittrackee.users.models import User, UserSportPreference
|
||||
|
||||
from .exceptions import WorkoutException
|
||||
from .models import Sport, Workout, WorkoutSegment
|
||||
from .utils_gpx import get_gpx_info
|
||||
from ..exceptions import WorkoutException
|
||||
from ..models import Sport, Workout, WorkoutSegment
|
||||
from .gpx import get_gpx_info
|
||||
from .maps import generate_map, get_map_hash
|
||||
|
||||
|
||||
def get_datetime_with_tz(
|
||||
@ -258,39 +257,11 @@ def get_new_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(
|
||||
params: Dict, filename: str, stopped_speed_threshold: float
|
||||
) -> 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:
|
||||
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(
|
||||
nb_workouts: int, total_average_speed: float, workout_average_speed: float
|
||||
) -> float:
|
@ -33,7 +33,14 @@ from fittrackee.users.models import User
|
||||
from fittrackee.users.utils import can_view_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,
|
||||
create_workout,
|
||||
edit_workout,
|
||||
@ -41,13 +48,6 @@ from .utils import (
|
||||
get_datetime_from_request_args,
|
||||
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__)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user