diff --git a/fittrackee/tests/fixtures/fixtures_workouts.py b/fittrackee/tests/fixtures/fixtures_workouts.py index 0f19b5f1..4db4609a 100644 --- a/fittrackee/tests/fixtures/fixtures_workouts.py +++ b/fittrackee/tests/fixtures/fixtures_workouts.py @@ -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') diff --git a/fittrackee/tests/workouts/test_utils/__init__.py b/fittrackee/tests/workouts/test_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fittrackee/tests/workouts/test_gpx_utils.py b/fittrackee/tests/workouts/test_utils/test_gpx.py similarity index 93% rename from fittrackee/tests/workouts/test_gpx_utils.py rename to fittrackee/tests/workouts/test_utils/test_gpx.py index df5da68c..06d655a0 100644 --- a/fittrackee/tests/workouts/test_gpx_utils.py +++ b/fittrackee/tests/workouts/test_utils/test_gpx.py @@ -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', diff --git a/fittrackee/tests/workouts/test_workouts_utils.py b/fittrackee/tests/workouts/test_utils/test_workouts.py similarity index 92% rename from fittrackee/tests/workouts/test_workouts_utils.py rename to fittrackee/tests/workouts/test_utils/test_workouts.py index ba824198..0e325878 100644 --- a/fittrackee/tests/workouts/test_workouts_utils.py +++ b/fittrackee/tests/workouts/test_utils/test_workouts.py @@ -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: diff --git a/fittrackee/tests/workouts/test_workouts_api_1_post.py b/fittrackee/tests/workouts/test_workouts_api_1_post.py index a8ad9386..740581f8 100644 --- a/fittrackee/tests/workouts/test_workouts_api_1_post.py +++ b/fittrackee/tests/workouts/test_workouts_api_1_post.py @@ -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 diff --git a/fittrackee/tests/workouts/test_workouts_api_3_delete.py b/fittrackee/tests/workouts/test_workouts_api_3_delete.py index 25369e30..4b1aba91 100644 --- a/fittrackee/tests/workouts/test_workouts_api_3_delete.py +++ b/fittrackee/tests/workouts/test_workouts_api_3_delete.py @@ -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 diff --git a/fittrackee/tests/workouts/test_workouts_model.py b/fittrackee/tests/workouts/test_workouts_model.py index 2f4c5859..6c7167af 100644 --- a/fittrackee/tests/workouts/test_workouts_model.py +++ b/fittrackee/tests/workouts/test_workouts_model.py @@ -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: diff --git a/fittrackee/tests/workouts/utils.py b/fittrackee/tests/workouts/utils.py index 8b8d101d..7aaea2f6 100644 --- a/fittrackee/tests/workouts/utils.py +++ b/fittrackee/tests/workouts/utils.py @@ -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: diff --git a/fittrackee/users/utils.py b/fittrackee/users/utils.py deleted file mode 100644 index f6374198..00000000 --- a/fittrackee/users/utils.py +++ /dev/null @@ -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() diff --git a/fittrackee/workouts/models.py b/fittrackee/workouts/models.py index 083c88a4..ac5e2273 100644 --- a/fittrackee/workouts/models.py +++ b/fittrackee/workouts/models.py @@ -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 = [ diff --git a/fittrackee/workouts/stats.py b/fittrackee/workouts/stats.py index bccfafe9..9c3da148 100644 --- a/fittrackee/workouts/stats.py +++ b/fittrackee/workouts/stats.py @@ -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__) diff --git a/fittrackee/workouts/utils/__init__.py b/fittrackee/workouts/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fittrackee/workouts/utils_format.py b/fittrackee/workouts/utils/convert.py similarity index 100% rename from fittrackee/workouts/utils_format.py rename to fittrackee/workouts/utils/convert.py diff --git a/fittrackee/workouts/utils_gpx.py b/fittrackee/workouts/utils/gpx.py similarity index 98% rename from fittrackee/workouts/utils_gpx.py rename to fittrackee/workouts/utils/gpx.py index 02777b58..f549014f 100644 --- a/fittrackee/workouts/utils_gpx.py +++ b/fittrackee/workouts/utils/gpx.py @@ -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]: diff --git a/fittrackee/workouts/utils/maps.py b/fittrackee/workouts/utils/maps.py new file mode 100644 index 00000000..fda84ac6 --- /dev/null +++ b/fittrackee/workouts/utils/maps.py @@ -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() diff --git a/fittrackee/workouts/utils_id.py b/fittrackee/workouts/utils/short_id.py similarity index 87% rename from fittrackee/workouts/utils_id.py rename to fittrackee/workouts/utils/short_id.py index bf81bab4..86ec5126 100644 --- a/fittrackee/workouts/utils_id.py +++ b/fittrackee/workouts/utils/short_id.py @@ -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) diff --git a/fittrackee/workouts/utils/uploads.py b/fittrackee/workouts/utils/uploads.py new file mode 100644 index 00000000..60e7ebe7 --- /dev/null +++ b/fittrackee/workouts/utils/uploads.py @@ -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 diff --git a/fittrackee/workouts/utils_weather.py b/fittrackee/workouts/utils/weather.py similarity index 100% rename from fittrackee/workouts/utils_weather.py rename to fittrackee/workouts/utils/weather.py diff --git a/fittrackee/workouts/utils.py b/fittrackee/workouts/utils/workouts.py similarity index 89% rename from fittrackee/workouts/utils.py rename to fittrackee/workouts/utils/workouts.py index 03243263..e9159423 100644 --- a/fittrackee/workouts/utils.py +++ b/fittrackee/workouts/utils/workouts.py @@ -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: diff --git a/fittrackee/workouts/workouts.py b/fittrackee/workouts/workouts.py index 7b9beff4..41f8d8b1 100644 --- a/fittrackee/workouts/workouts.py +++ b/fittrackee/workouts/workouts.py @@ -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__)