API - add typing
This commit is contained in:
		@@ -33,6 +33,11 @@ lint:
 | 
			
		||||
  script:
 | 
			
		||||
    - pytest --flake8 --isort --black -m "flake8 or isort or black" fittrackee e2e --ignore=fittrackee/migrations
 | 
			
		||||
 | 
			
		||||
type-check:
 | 
			
		||||
  extends: .python
 | 
			
		||||
  script:
 | 
			
		||||
    - mypy fittrackee --disallow-untyped-defs --ignore-missing-imports
 | 
			
		||||
 | 
			
		||||
python-3.7:
 | 
			
		||||
  extends: .python
 | 
			
		||||
  image: python:3.7
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								Makefile
									
									
									
									
									
								
							@@ -9,10 +9,13 @@ make-p:
 | 
			
		||||
build-client: lint-client
 | 
			
		||||
	cd fittrackee_client && $(NPM) build
 | 
			
		||||
 | 
			
		||||
check-all: lint-all type-check test-python
 | 
			
		||||
 | 
			
		||||
clean-install:
 | 
			
		||||
	rm -fr $(NODE_MODULES)
 | 
			
		||||
	rm -fr $(VENV)
 | 
			
		||||
	rm -rf *.egg-info
 | 
			
		||||
	rm -rf .mypy_cache
 | 
			
		||||
	rm -rf .pytest_cache
 | 
			
		||||
	rm -rf dist/
 | 
			
		||||
 | 
			
		||||
@@ -61,18 +64,18 @@ lint-all: lint-python lint-client
 | 
			
		||||
 | 
			
		||||
lint-all-fix: lint-python-fix lint-client-fix
 | 
			
		||||
 | 
			
		||||
lint-python:
 | 
			
		||||
	$(PYTEST) --flake8 --isort --black -m "flake8 or isort or black" fittrackee e2e --ignore=fittrackee/migrations
 | 
			
		||||
 | 
			
		||||
lint-python-fix:
 | 
			
		||||
	$(BLACK) fittrackee e2e
 | 
			
		||||
 | 
			
		||||
lint-client:
 | 
			
		||||
	cd fittrackee_client && $(NPM) lint
 | 
			
		||||
 | 
			
		||||
lint-client-fix:
 | 
			
		||||
	cd fittrackee_client && $(NPM) lint-fix
 | 
			
		||||
 | 
			
		||||
lint-python:
 | 
			
		||||
	$(PYTEST) --flake8 --isort --black -m "flake8 or isort or black" fittrackee e2e --ignore=fittrackee/migrations
 | 
			
		||||
 | 
			
		||||
lint-python-fix:
 | 
			
		||||
	$(BLACK) fittrackee e2e
 | 
			
		||||
 | 
			
		||||
mail:
 | 
			
		||||
	docker run -d -e "MH_STORAGE=maildir" -v /tmp/maildir:/maildir -p 1025:1025 -p 8025:8025 mailhog/mailhog
 | 
			
		||||
 | 
			
		||||
@@ -91,21 +94,21 @@ run-server:
 | 
			
		||||
run-workers:
 | 
			
		||||
	$(FLASK) worker --processes=$(WORKERS_PROCESSES) >> dramatiq.log  2>&1
 | 
			
		||||
 | 
			
		||||
serve-python:
 | 
			
		||||
	$(FLASK) run --with-threads -h $(HOST) -p $(PORT)
 | 
			
		||||
 | 
			
		||||
serve-python-dev:
 | 
			
		||||
	$(FLASK) run --with-threads -h $(HOST) -p $(PORT) --cert=adhoc
 | 
			
		||||
 | 
			
		||||
serve-client:
 | 
			
		||||
	cd fittrackee_client && $(NPM) start
 | 
			
		||||
 | 
			
		||||
serve:
 | 
			
		||||
	$(MAKE) P="serve-client serve-python" make-p
 | 
			
		||||
 | 
			
		||||
serve-dev:
 | 
			
		||||
	$(MAKE) P="serve-client serve-python-dev" make-p
 | 
			
		||||
 | 
			
		||||
serve-client:
 | 
			
		||||
	cd fittrackee_client && $(NPM) start
 | 
			
		||||
 | 
			
		||||
serve-python:
 | 
			
		||||
	$(FLASK) run --with-threads -h $(HOST) -p $(PORT)
 | 
			
		||||
 | 
			
		||||
serve-python-dev:
 | 
			
		||||
	$(FLASK) run --with-threads -h $(HOST) -p $(PORT) --cert=adhoc
 | 
			
		||||
 | 
			
		||||
test-e2e: init-db
 | 
			
		||||
	$(PYTEST) e2e --driver firefox $(PYTEST_ARGS)
 | 
			
		||||
 | 
			
		||||
@@ -115,5 +118,9 @@ test-e2e-client: init-db
 | 
			
		||||
test-python:
 | 
			
		||||
	$(PYTEST) fittrackee --cov-config .coveragerc --cov=fittrackee --cov-report term-missing $(PYTEST_ARGS)
 | 
			
		||||
 | 
			
		||||
type-check:
 | 
			
		||||
	echo 'Running mypy...'
 | 
			
		||||
	$(MYPY) fittrackee --disallow-untyped-defs --ignore-missing-imports
 | 
			
		||||
 | 
			
		||||
upgrade-db:
 | 
			
		||||
	$(FLASK) db upgrade --directory $(MIGRATIONS)
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ FLASK = $(VENV)/bin/flask
 | 
			
		||||
PYTEST = $(VENV)/bin/py.test -c pyproject.toml -W ignore::DeprecationWarning
 | 
			
		||||
GUNICORN = $(VENV)/bin/gunicorn
 | 
			
		||||
BLACK = $(VENV)/bin/black
 | 
			
		||||
MYPY = $(VENV)/bin/mypy
 | 
			
		||||
 | 
			
		||||
# Node env
 | 
			
		||||
NODE_MODULES = $(PWD)/fittrackee_client/node_modules
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
from importlib import import_module, reload
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from flask import Flask, render_template, send_file
 | 
			
		||||
from flask import Flask, Response, render_template, send_file
 | 
			
		||||
from flask_bcrypt import Bcrypt
 | 
			
		||||
from flask_dramatiq import Dramatiq
 | 
			
		||||
from flask_migrate import Migrate
 | 
			
		||||
@@ -24,7 +25,7 @@ logging.basicConfig(
 | 
			
		||||
appLog = logging.getLogger('fittrackee')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_app():
 | 
			
		||||
def create_app() -> Flask:
 | 
			
		||||
    # instantiate the app
 | 
			
		||||
    app = Flask(__name__, static_folder='dist/static', template_folder='dist')
 | 
			
		||||
 | 
			
		||||
@@ -88,7 +89,7 @@ def create_app():
 | 
			
		||||
 | 
			
		||||
        # Enable CORS
 | 
			
		||||
        @app.after_request
 | 
			
		||||
        def after_request(response):
 | 
			
		||||
        def after_request(response: Response) -> Response:
 | 
			
		||||
            response.headers.add('Access-Control-Allow-Origin', '*')
 | 
			
		||||
            response.headers.add(
 | 
			
		||||
                'Access-Control-Allow-Headers', 'Content-Type,Authorization'
 | 
			
		||||
@@ -100,15 +101,23 @@ def create_app():
 | 
			
		||||
            return response
 | 
			
		||||
 | 
			
		||||
    @app.route('/favicon.ico')
 | 
			
		||||
    def favicon():
 | 
			
		||||
        return send_file(os.path.join(app.root_path, 'dist/favicon.ico'))
 | 
			
		||||
    def favicon() -> Any:
 | 
			
		||||
        return send_file(
 | 
			
		||||
            os.path.join(app.root_path, 'dist/favicon.ico')  # type: ignore
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @app.route('/', defaults={'path': ''})
 | 
			
		||||
    @app.route('/<path:path>')
 | 
			
		||||
    def catch_all(path):
 | 
			
		||||
    def catch_all(path: str) -> Any:
 | 
			
		||||
        # workaround to serve images (not in static directory)
 | 
			
		||||
        if path.startswith('img/'):
 | 
			
		||||
            return send_file(os.path.join(app.root_path, 'dist', path))
 | 
			
		||||
            return send_file(
 | 
			
		||||
                os.path.join(
 | 
			
		||||
                    app.root_path,  # type: ignore
 | 
			
		||||
                    'dist',
 | 
			
		||||
                    path,
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            return render_template('index.html')
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
# http://docs.gunicorn.org/en/stable/custom.html
 | 
			
		||||
import os
 | 
			
		||||
import shutil
 | 
			
		||||
from typing import Dict, Optional
 | 
			
		||||
 | 
			
		||||
import gunicorn.app.base
 | 
			
		||||
from fittrackee import create_app, db
 | 
			
		||||
@@ -9,6 +10,7 @@ from fittrackee.activities.models import Activity
 | 
			
		||||
from fittrackee.activities.utils import update_activity
 | 
			
		||||
from fittrackee.application.utils import init_config
 | 
			
		||||
from fittrackee.database_utils import init_database
 | 
			
		||||
from flask import Flask
 | 
			
		||||
from flask_dramatiq import worker
 | 
			
		||||
from flask_migrate import upgrade
 | 
			
		||||
from tqdm import tqdm
 | 
			
		||||
@@ -22,12 +24,14 @@ dramatiq_worker = worker
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StandaloneApplication(gunicorn.app.base.BaseApplication):
 | 
			
		||||
    def __init__(self, current_app, options=None):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, current_app: Flask, options: Optional[Dict] = None
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        self.options = options or {}
 | 
			
		||||
        self.application = current_app
 | 
			
		||||
        super().__init__()
 | 
			
		||||
 | 
			
		||||
    def load_config(self):
 | 
			
		||||
    def load_config(self) -> None:
 | 
			
		||||
        config = {
 | 
			
		||||
            key: value
 | 
			
		||||
            for key, value in self.options.items()
 | 
			
		||||
@@ -36,17 +40,17 @@ class StandaloneApplication(gunicorn.app.base.BaseApplication):
 | 
			
		||||
        for key, value in config.items():
 | 
			
		||||
            self.cfg.set(key.lower(), value)
 | 
			
		||||
 | 
			
		||||
    def load(self):
 | 
			
		||||
    def load(self) -> Flask:
 | 
			
		||||
        return self.application
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade_db():
 | 
			
		||||
def upgrade_db() -> None:
 | 
			
		||||
    with app.app_context():
 | 
			
		||||
        upgrade(directory=BASEDIR + '/migrations')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.cli.command('drop-db')
 | 
			
		||||
def drop_db():
 | 
			
		||||
def drop_db() -> None:
 | 
			
		||||
    """Empty database for dev environments."""
 | 
			
		||||
    db.engine.execute("DROP TABLE IF EXISTS alembic_version;")
 | 
			
		||||
    db.drop_all()
 | 
			
		||||
@@ -57,13 +61,13 @@ def drop_db():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.cli.command('init-data')
 | 
			
		||||
def init_data():
 | 
			
		||||
def init_data() -> None:
 | 
			
		||||
    """Init the database and application config."""
 | 
			
		||||
    init_database(app)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.cli.command()
 | 
			
		||||
def recalculate():
 | 
			
		||||
def recalculate() -> None:
 | 
			
		||||
    print("Starting activities data refresh")
 | 
			
		||||
    activities = (
 | 
			
		||||
        Activity.query.filter(Activity.gpx != None)  # noqa
 | 
			
		||||
@@ -81,7 +85,7 @@ def recalculate():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.cli.command('init-app-config')
 | 
			
		||||
def init_app_config():
 | 
			
		||||
def init_app_config() -> None:
 | 
			
		||||
    """Init application configuration."""
 | 
			
		||||
    print("Init application configuration")
 | 
			
		||||
    config_created, _ = init_config()
 | 
			
		||||
@@ -94,7 +98,7 @@ def init_app_config():
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
def main() -> None:
 | 
			
		||||
    options = {'bind': f'{HOST}:{PORT}', 'workers': WORKERS}
 | 
			
		||||
    StandaloneApplication(app, options).run()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,14 @@ import json
 | 
			
		||||
import os
 | 
			
		||||
import shutil
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Any, Dict, List, Optional, Tuple, Union
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
from fittrackee import appLog, db
 | 
			
		||||
from fittrackee.responses import (
 | 
			
		||||
    DataInvalidPayloadErrorResponse,
 | 
			
		||||
    DataNotFoundErrorResponse,
 | 
			
		||||
    HttpResponse,
 | 
			
		||||
    InternalServerErrorResponse,
 | 
			
		||||
    InvalidPayloadErrorResponse,
 | 
			
		||||
    NotFoundErrorResponse,
 | 
			
		||||
@@ -46,7 +48,7 @@ ACTIVITIES_PER_PAGE = 5
 | 
			
		||||
 | 
			
		||||
@activities_blueprint.route('/activities', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_activities(auth_user_id):
 | 
			
		||||
def get_activities(auth_user_id: int) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get activities for the authenticated user.
 | 
			
		||||
 | 
			
		||||
@@ -275,7 +277,9 @@ def get_activities(auth_user_id):
 | 
			
		||||
    '/activities/<string:activity_short_id>', methods=['GET']
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_activity(auth_user_id, activity_short_id):
 | 
			
		||||
def get_activity(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get an activity
 | 
			
		||||
 | 
			
		||||
@@ -375,8 +379,11 @@ def get_activity(auth_user_id, activity_short_id):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_activity_data(
 | 
			
		||||
    auth_user_id, activity_short_id, data_type, segment_id=None
 | 
			
		||||
):
 | 
			
		||||
    auth_user_id: int,
 | 
			
		||||
    activity_short_id: str,
 | 
			
		||||
    data_type: str,
 | 
			
		||||
    segment_id: Optional[int] = None,
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """Get data from an activity gpx file"""
 | 
			
		||||
    activity_uuid = decode_short_id(activity_short_id)
 | 
			
		||||
    activity = Activity.query.filter_by(uuid=activity_uuid).first()
 | 
			
		||||
@@ -396,14 +403,17 @@ def get_activity_data(
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        absolute_gpx_filepath = get_absolute_file_path(activity.gpx)
 | 
			
		||||
        chart_data_content: Optional[List] = []
 | 
			
		||||
        if data_type == 'chart_data':
 | 
			
		||||
            content = get_chart_data(absolute_gpx_filepath, segment_id)
 | 
			
		||||
            chart_data_content = get_chart_data(
 | 
			
		||||
                absolute_gpx_filepath, segment_id
 | 
			
		||||
            )
 | 
			
		||||
        else:  # data_type == 'gpx'
 | 
			
		||||
            with open(absolute_gpx_filepath, encoding='utf-8') as f:
 | 
			
		||||
                content = f.read()
 | 
			
		||||
                gpx_content = f.read()
 | 
			
		||||
                if segment_id is not None:
 | 
			
		||||
                    content = extract_segment_from_gpx_file(
 | 
			
		||||
                        content, segment_id
 | 
			
		||||
                    gpx_segment_content = extract_segment_from_gpx_file(
 | 
			
		||||
                        gpx_content, segment_id
 | 
			
		||||
                    )
 | 
			
		||||
    except ActivityGPXException as e:
 | 
			
		||||
        appLog.error(e.message)
 | 
			
		||||
@@ -416,7 +426,15 @@ def get_activity_data(
 | 
			
		||||
    return {
 | 
			
		||||
        'status': 'success',
 | 
			
		||||
        'message': '',
 | 
			
		||||
        'data': ({data_type: content}),
 | 
			
		||||
        'data': (
 | 
			
		||||
            {
 | 
			
		||||
                data_type: chart_data_content
 | 
			
		||||
                if data_type == 'chart_data'
 | 
			
		||||
                else gpx_content
 | 
			
		||||
                if segment_id is None
 | 
			
		||||
                else gpx_segment_content
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -424,7 +442,9 @@ def get_activity_data(
 | 
			
		||||
    '/activities/<string:activity_short_id>/gpx', methods=['GET']
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_activity_gpx(auth_user_id, activity_short_id):
 | 
			
		||||
def get_activity_gpx(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get gpx file for an activity displayed on map with Leaflet
 | 
			
		||||
 | 
			
		||||
@@ -473,7 +493,9 @@ def get_activity_gpx(auth_user_id, activity_short_id):
 | 
			
		||||
    '/activities/<string:activity_short_id>/chart_data', methods=['GET']
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_activity_chart_data(auth_user_id, activity_short_id):
 | 
			
		||||
def get_activity_chart_data(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get chart data from an activity gpx file, to display it with Recharts
 | 
			
		||||
 | 
			
		||||
@@ -542,7 +564,9 @@ def get_activity_chart_data(auth_user_id, activity_short_id):
 | 
			
		||||
    methods=['GET'],
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_segment_gpx(auth_user_id, activity_short_id, segment_id):
 | 
			
		||||
def get_segment_gpx(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str, segment_id: int
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get gpx file for an activity segment displayed on map with Leaflet
 | 
			
		||||
 | 
			
		||||
@@ -595,7 +619,9 @@ def get_segment_gpx(auth_user_id, activity_short_id, segment_id):
 | 
			
		||||
    methods=['GET'],
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_segment_chart_data(auth_user_id, activity_short_id, segment_id):
 | 
			
		||||
def get_segment_chart_data(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str, segment_id: int
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get chart data from an activity gpx file, to display it with Recharts
 | 
			
		||||
 | 
			
		||||
@@ -662,7 +688,7 @@ def get_segment_chart_data(auth_user_id, activity_short_id, segment_id):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@activities_blueprint.route('/activities/map/<map_id>', methods=['GET'])
 | 
			
		||||
def get_map(map_id):
 | 
			
		||||
def get_map(map_id: int) -> Any:
 | 
			
		||||
    """
 | 
			
		||||
    Get map image for activities with gpx
 | 
			
		||||
 | 
			
		||||
@@ -704,7 +730,7 @@ def get_map(map_id):
 | 
			
		||||
@activities_blueprint.route(
 | 
			
		||||
    '/activities/map_tile/<s>/<z>/<x>/<y>.png', methods=['GET']
 | 
			
		||||
)
 | 
			
		||||
def get_map_tile(s, z, x, y):
 | 
			
		||||
def get_map_tile(s: str, z: str, x: str, y: str) -> Tuple[Response, int]:
 | 
			
		||||
    """
 | 
			
		||||
    Get map tile from tile server.
 | 
			
		||||
 | 
			
		||||
@@ -743,7 +769,7 @@ def get_map_tile(s, z, x, y):
 | 
			
		||||
 | 
			
		||||
@activities_blueprint.route('/activities', methods=['POST'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def post_activity(auth_user_id):
 | 
			
		||||
def post_activity(auth_user_id: int) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Post an activity with a gpx file
 | 
			
		||||
 | 
			
		||||
@@ -904,7 +930,9 @@ def post_activity(auth_user_id):
 | 
			
		||||
 | 
			
		||||
@activities_blueprint.route('/activities/no_gpx', methods=['POST'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def post_activity_no_gpx(auth_user_id):
 | 
			
		||||
def post_activity_no_gpx(
 | 
			
		||||
    auth_user_id: int,
 | 
			
		||||
) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Post an activity without gpx file
 | 
			
		||||
 | 
			
		||||
@@ -1053,7 +1081,9 @@ def post_activity_no_gpx(auth_user_id):
 | 
			
		||||
    '/activities/<string:activity_short_id>', methods=['PATCH']
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def update_activity(auth_user_id, activity_short_id):
 | 
			
		||||
def update_activity(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Update an activity
 | 
			
		||||
 | 
			
		||||
@@ -1199,7 +1229,9 @@ def update_activity(auth_user_id, activity_short_id):
 | 
			
		||||
    '/activities/<string:activity_short_id>', methods=['DELETE']
 | 
			
		||||
)
 | 
			
		||||
@authenticate
 | 
			
		||||
def delete_activity(auth_user_id, activity_short_id):
 | 
			
		||||
def delete_activity(
 | 
			
		||||
    auth_user_id: int, activity_short_id: str
 | 
			
		||||
) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Delete an activity
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,23 @@
 | 
			
		||||
import datetime
 | 
			
		||||
import os
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
from typing import Any, Dict, Optional, Union
 | 
			
		||||
from uuid import UUID, uuid4
 | 
			
		||||
 | 
			
		||||
from fittrackee import db
 | 
			
		||||
from sqlalchemy.dialects import postgresql
 | 
			
		||||
from sqlalchemy.engine.base import Connection
 | 
			
		||||
from sqlalchemy.event import listens_for
 | 
			
		||||
from sqlalchemy.ext.declarative import DeclarativeMeta
 | 
			
		||||
from sqlalchemy.ext.hybrid import hybrid_property
 | 
			
		||||
from sqlalchemy.orm.session import object_session
 | 
			
		||||
from sqlalchemy.orm.mapper import Mapper
 | 
			
		||||
from sqlalchemy.orm.session import Session, object_session
 | 
			
		||||
from sqlalchemy.types import JSON, Enum
 | 
			
		||||
 | 
			
		||||
from .utils_files import get_absolute_file_path
 | 
			
		||||
from .utils_format import convert_in_duration, convert_value_to_integer
 | 
			
		||||
from .utils_id import encode_uuid
 | 
			
		||||
 | 
			
		||||
BaseModel: DeclarativeMeta = db.Model
 | 
			
		||||
record_types = [
 | 
			
		||||
    'AS',  # 'Best Average Speed'
 | 
			
		||||
    'FD',  # 'Farthest Distance'
 | 
			
		||||
@@ -21,7 +26,9 @@ record_types = [
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_records(user_id, sport_id, connection, session):
 | 
			
		||||
def update_records(
 | 
			
		||||
    user_id: int, sport_id: int, connection: Connection, session: Session
 | 
			
		||||
) -> None:
 | 
			
		||||
    record_table = Record.__table__
 | 
			
		||||
    new_records = Activity.get_user_activity_records(user_id, sport_id)
 | 
			
		||||
    for record_type, record_data in new_records.items():
 | 
			
		||||
@@ -47,7 +54,7 @@ def update_records(user_id, sport_id, connection, session):
 | 
			
		||||
                new_record = Record(
 | 
			
		||||
                    activity=record_data['activity'], record_type=record_type
 | 
			
		||||
                )
 | 
			
		||||
                new_record.value = record_data['record_value']
 | 
			
		||||
                new_record.value = record_data['record_value']  # type: ignore
 | 
			
		||||
                session.add(new_record)
 | 
			
		||||
        else:
 | 
			
		||||
            connection.execute(
 | 
			
		||||
@@ -58,7 +65,7 @@ def update_records(user_id, sport_id, connection, session):
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Sport(db.Model):
 | 
			
		||||
class Sport(BaseModel):
 | 
			
		||||
    __tablename__ = "sports"
 | 
			
		||||
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
 | 
			
		||||
    label = db.Column(db.String(50), unique=True, nullable=False)
 | 
			
		||||
@@ -71,13 +78,13 @@ class Sport(db.Model):
 | 
			
		||||
        'Record', lazy=True, backref=db.backref('sports', lazy='joined')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
    def __repr__(self) -> str:
 | 
			
		||||
        return f'<Sport {self.label!r}>'
 | 
			
		||||
 | 
			
		||||
    def __init__(self, label):
 | 
			
		||||
    def __init__(self, label: str) -> None:
 | 
			
		||||
        self.label = label
 | 
			
		||||
 | 
			
		||||
    def serialize(self, is_admin=False):
 | 
			
		||||
    def serialize(self, is_admin: Optional[bool] = False) -> Dict:
 | 
			
		||||
        serialized_sport = {
 | 
			
		||||
            'id': self.id,
 | 
			
		||||
            'label': self.label,
 | 
			
		||||
@@ -89,7 +96,7 @@ class Sport(db.Model):
 | 
			
		||||
        return serialized_sport
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Activity(db.Model):
 | 
			
		||||
class Activity(BaseModel):
 | 
			
		||||
    __tablename__ = "activities"
 | 
			
		||||
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
 | 
			
		||||
    uuid = db.Column(
 | 
			
		||||
@@ -138,10 +145,17 @@ class Activity(db.Model):
 | 
			
		||||
        backref=db.backref('activities', lazy='joined', single_parent=True),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return f'<Activity \'{self.sports.label}\' - {self.activity_date}>'
 | 
			
		||||
 | 
			
		||||
    def __init__(self, user_id, sport_id, activity_date, distance, duration):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        user_id: int,
 | 
			
		||||
        sport_id: int,
 | 
			
		||||
        activity_date: datetime.datetime,
 | 
			
		||||
        distance: float,
 | 
			
		||||
        duration: datetime.timedelta,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        self.user_id = user_id
 | 
			
		||||
        self.sport_id = sport_id
 | 
			
		||||
        self.activity_date = activity_date
 | 
			
		||||
@@ -149,10 +163,10 @@ class Activity(db.Model):
 | 
			
		||||
        self.duration = duration
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def short_id(self):
 | 
			
		||||
    def short_id(self) -> str:
 | 
			
		||||
        return encode_uuid(self.uuid)
 | 
			
		||||
 | 
			
		||||
    def serialize(self, params=None):
 | 
			
		||||
    def serialize(self, params: Optional[Dict] = None) -> Dict:
 | 
			
		||||
        date_from = params.get('from') if params else None
 | 
			
		||||
        date_to = params.get('to') if params else None
 | 
			
		||||
        distance_from = params.get('distance_from') if params else None
 | 
			
		||||
@@ -239,41 +253,43 @@ class Activity(db.Model):
 | 
			
		||||
            .first()
 | 
			
		||||
        )
 | 
			
		||||
        return {
 | 
			
		||||
            "id": self.short_id,  # WARNING: client use uuid as id
 | 
			
		||||
            "user": self.user.username,
 | 
			
		||||
            "sport_id": self.sport_id,
 | 
			
		||||
            "title": self.title,
 | 
			
		||||
            "creation_date": self.creation_date,
 | 
			
		||||
            "modification_date": self.modification_date,
 | 
			
		||||
            "activity_date": self.activity_date,
 | 
			
		||||
            "duration": str(self.duration) if self.duration else None,
 | 
			
		||||
            "pauses": str(self.pauses) if self.pauses else None,
 | 
			
		||||
            "moving": str(self.moving) if self.moving else None,
 | 
			
		||||
            "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,
 | 
			
		||||
            "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,
 | 
			
		||||
            "bounds": [float(bound) for bound in self.bounds]
 | 
			
		||||
            'id': self.short_id,  # WARNING: client use uuid as id
 | 
			
		||||
            'user': self.user.username,
 | 
			
		||||
            'sport_id': self.sport_id,
 | 
			
		||||
            'title': self.title,
 | 
			
		||||
            'creation_date': self.creation_date,
 | 
			
		||||
            'modification_date': self.modification_date,
 | 
			
		||||
            'activity_date': self.activity_date,
 | 
			
		||||
            'duration': str(self.duration) if self.duration else None,
 | 
			
		||||
            'pauses': str(self.pauses) if self.pauses else None,
 | 
			
		||||
            'moving': str(self.moving) if self.moving else None,
 | 
			
		||||
            '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,
 | 
			
		||||
            '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,
 | 
			
		||||
            'bounds': [float(bound) for bound in self.bounds]
 | 
			
		||||
            if self.bounds
 | 
			
		||||
            else [],  # noqa
 | 
			
		||||
            "previous_activity": previous_activity.short_id
 | 
			
		||||
            'previous_activity': previous_activity.short_id
 | 
			
		||||
            if previous_activity
 | 
			
		||||
            else None,  # noqa
 | 
			
		||||
            "next_activity": next_activity.short_id if next_activity else None,
 | 
			
		||||
            "segments": [segment.serialize() for segment in self.segments],
 | 
			
		||||
            "records": [record.serialize() for record in self.records],
 | 
			
		||||
            "map": self.map_id if self.map else None,
 | 
			
		||||
            "weather_start": self.weather_start,
 | 
			
		||||
            "weather_end": self.weather_end,
 | 
			
		||||
            "notes": self.notes,
 | 
			
		||||
            'next_activity': next_activity.short_id if next_activity else None,
 | 
			
		||||
            'segments': [segment.serialize() for segment in self.segments],
 | 
			
		||||
            'records': [record.serialize() for record in self.records],
 | 
			
		||||
            'map': self.map_id if self.map else None,
 | 
			
		||||
            'weather_start': self.weather_start,
 | 
			
		||||
            'weather_end': self.weather_end,
 | 
			
		||||
            'notes': self.notes,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_user_activity_records(cls, user_id, sport_id, as_integer=False):
 | 
			
		||||
    def get_user_activity_records(
 | 
			
		||||
        cls, user_id: int, sport_id: int, as_integer: Optional[bool] = False
 | 
			
		||||
    ) -> Dict:
 | 
			
		||||
        record_types_columns = {
 | 
			
		||||
            'AS': 'ave_speed',  # 'Average speed'
 | 
			
		||||
            'FD': 'distance',  # 'Farthest Distance'
 | 
			
		||||
@@ -300,22 +316,26 @@ class Activity(db.Model):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@listens_for(Activity, 'after_insert')
 | 
			
		||||
def on_activity_insert(mapper, connection, activity):
 | 
			
		||||
def on_activity_insert(
 | 
			
		||||
    mapper: Mapper, connection: Connection, activity: Activity
 | 
			
		||||
) -> None:
 | 
			
		||||
    @listens_for(db.Session, 'after_flush', once=True)
 | 
			
		||||
    def receive_after_flush(session, context):
 | 
			
		||||
    def receive_after_flush(session: Session, context: Any) -> None:
 | 
			
		||||
        update_records(
 | 
			
		||||
            activity.user_id, activity.sport_id, connection, session
 | 
			
		||||
        )  # noqa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@listens_for(Activity, 'after_update')
 | 
			
		||||
def on_activity_update(mapper, connection, activity):
 | 
			
		||||
def on_activity_update(
 | 
			
		||||
    mapper: Mapper, connection: Connection, activity: Activity
 | 
			
		||||
) -> None:
 | 
			
		||||
    if object_session(activity).is_modified(
 | 
			
		||||
        activity, include_collections=True
 | 
			
		||||
    ):  # noqa
 | 
			
		||||
 | 
			
		||||
        @listens_for(db.Session, 'after_flush', once=True)
 | 
			
		||||
        def receive_after_flush(session, context):
 | 
			
		||||
        def receive_after_flush(session: Session, context: Any) -> None:
 | 
			
		||||
            sports_list = [activity.sport_id]
 | 
			
		||||
            records = Record.query.filter_by(activity_id=activity.id).all()
 | 
			
		||||
            for rec in records:
 | 
			
		||||
@@ -326,16 +346,18 @@ def on_activity_update(mapper, connection, activity):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@listens_for(Activity, 'after_delete')
 | 
			
		||||
def on_activity_delete(mapper, connection, old_record):
 | 
			
		||||
def on_activity_delete(
 | 
			
		||||
    mapper: Mapper, connection: Connection, old_record: 'Record'
 | 
			
		||||
) -> None:
 | 
			
		||||
    @listens_for(db.Session, 'after_flush', once=True)
 | 
			
		||||
    def receive_after_flush(session, context):
 | 
			
		||||
    def receive_after_flush(session: Session, context: Any) -> None:
 | 
			
		||||
        if old_record.map:
 | 
			
		||||
            os.remove(get_absolute_file_path(old_record.map))
 | 
			
		||||
        if old_record.gpx:
 | 
			
		||||
            os.remove(get_absolute_file_path(old_record.gpx))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ActivitySegment(db.Model):
 | 
			
		||||
class ActivitySegment(BaseModel):
 | 
			
		||||
    __tablename__ = "activity_segments"
 | 
			
		||||
    activity_id = db.Column(
 | 
			
		||||
        db.Integer, db.ForeignKey('activities.id'), primary_key=True
 | 
			
		||||
@@ -353,35 +375,37 @@ class ActivitySegment(db.Model):
 | 
			
		||||
    max_speed = db.Column(db.Numeric(6, 2), nullable=True)  # km/h
 | 
			
		||||
    ave_speed = db.Column(db.Numeric(6, 2), nullable=True)  # km/h
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return (
 | 
			
		||||
            f'<Segment \'{self.segment_id}\' '
 | 
			
		||||
            f'for activity \'{encode_uuid(self.activity_uuid)}\'>'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def __init__(self, segment_id, activity_id, activity_uuid):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, segment_id: int, activity_id: int, activity_uuid: UUID
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        self.segment_id = segment_id
 | 
			
		||||
        self.activity_id = activity_id
 | 
			
		||||
        self.activity_uuid = activity_uuid
 | 
			
		||||
 | 
			
		||||
    def serialize(self):
 | 
			
		||||
    def serialize(self) -> Dict:
 | 
			
		||||
        return {
 | 
			
		||||
            "activity_id": encode_uuid(self.activity_uuid),
 | 
			
		||||
            "segment_id": self.segment_id,
 | 
			
		||||
            "duration": str(self.duration) if self.duration else None,
 | 
			
		||||
            "pauses": str(self.pauses) if self.pauses else None,
 | 
			
		||||
            "moving": str(self.moving) if self.moving else None,
 | 
			
		||||
            "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,
 | 
			
		||||
            "max_speed": float(self.max_speed) if self.max_speed else None,
 | 
			
		||||
            "ave_speed": float(self.ave_speed) if self.ave_speed else None,
 | 
			
		||||
            'activity_id': encode_uuid(self.activity_uuid),
 | 
			
		||||
            'segment_id': self.segment_id,
 | 
			
		||||
            'duration': str(self.duration) if self.duration else None,
 | 
			
		||||
            'pauses': str(self.pauses) if self.pauses else None,
 | 
			
		||||
            'moving': str(self.moving) if self.moving else None,
 | 
			
		||||
            '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,
 | 
			
		||||
            'max_speed': float(self.max_speed) if self.max_speed else None,
 | 
			
		||||
            'ave_speed': float(self.ave_speed) if self.ave_speed else None,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Record(db.Model):
 | 
			
		||||
class Record(BaseModel):
 | 
			
		||||
    __tablename__ = "records"
 | 
			
		||||
    __table_args__ = (
 | 
			
		||||
        db.UniqueConstraint(
 | 
			
		||||
@@ -401,14 +425,14 @@ class Record(db.Model):
 | 
			
		||||
    activity_date = db.Column(db.DateTime, nullable=False)
 | 
			
		||||
    _value = db.Column("value", db.Integer, nullable=True)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return (
 | 
			
		||||
            f'<Record {self.sports.label} - '
 | 
			
		||||
            f'{self.record_type} - '
 | 
			
		||||
            f"{self.activity_date.strftime('%Y-%m-%d')}>"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def __init__(self, activity, record_type):
 | 
			
		||||
    def __init__(self, activity: Activity, record_type: str) -> None:
 | 
			
		||||
        self.user_id = activity.user_id
 | 
			
		||||
        self.sport_id = activity.sport_id
 | 
			
		||||
        self.activity_id = activity.id
 | 
			
		||||
@@ -417,7 +441,7 @@ class Record(db.Model):
 | 
			
		||||
        self.activity_date = activity.activity_date
 | 
			
		||||
 | 
			
		||||
    @hybrid_property
 | 
			
		||||
    def value(self):
 | 
			
		||||
    def value(self) -> Optional[Union[datetime.timedelta, float]]:
 | 
			
		||||
        if self._value is None:
 | 
			
		||||
            return None
 | 
			
		||||
        if self.record_type == 'LD':
 | 
			
		||||
@@ -427,33 +451,35 @@ class Record(db.Model):
 | 
			
		||||
        else:  # 'FD'
 | 
			
		||||
            return float(self._value / 1000)
 | 
			
		||||
 | 
			
		||||
    @value.setter
 | 
			
		||||
    def value(self, val):
 | 
			
		||||
    @value.setter  # type: ignore
 | 
			
		||||
    def value(self, val: Union[str, float]) -> None:
 | 
			
		||||
        self._value = convert_value_to_integer(self.record_type, val)
 | 
			
		||||
 | 
			
		||||
    def serialize(self):
 | 
			
		||||
    def serialize(self) -> Dict:
 | 
			
		||||
        if self.value is None:
 | 
			
		||||
            value = None
 | 
			
		||||
        elif self.record_type in ['AS', 'FD', 'MS']:
 | 
			
		||||
            value = float(self.value)
 | 
			
		||||
            value = float(self.value)  # type: ignore
 | 
			
		||||
        else:  # 'LD'
 | 
			
		||||
            value = str(self.value)
 | 
			
		||||
            value = str(self.value)  # type: ignore
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            "id": self.id,
 | 
			
		||||
            "user": self.user.username,
 | 
			
		||||
            "sport_id": self.sport_id,
 | 
			
		||||
            "activity_id": encode_uuid(self.activity_uuid),
 | 
			
		||||
            "record_type": self.record_type,
 | 
			
		||||
            "activity_date": self.activity_date,
 | 
			
		||||
            "value": value,
 | 
			
		||||
            'id': self.id,
 | 
			
		||||
            'user': self.user.username,
 | 
			
		||||
            'sport_id': self.sport_id,
 | 
			
		||||
            'activity_id': encode_uuid(self.activity_uuid),
 | 
			
		||||
            'record_type': self.record_type,
 | 
			
		||||
            'activity_date': self.activity_date,
 | 
			
		||||
            'value': value,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@listens_for(Record, 'after_delete')
 | 
			
		||||
def on_record_delete(mapper, connection, old_record):
 | 
			
		||||
def on_record_delete(
 | 
			
		||||
    mapper: Mapper, connection: Connection, old_record: Record
 | 
			
		||||
) -> None:
 | 
			
		||||
    @listens_for(db.Session, 'after_flush', once=True)
 | 
			
		||||
    def receive_after_flush(session, context):
 | 
			
		||||
    def receive_after_flush(session: Session, context: Any) -> None:
 | 
			
		||||
        activity = old_record.activities
 | 
			
		||||
        new_records = Activity.get_user_activity_records(
 | 
			
		||||
            activity.user_id, activity.sport_id
 | 
			
		||||
@@ -466,5 +492,5 @@ def on_record_delete(mapper, connection, old_record):
 | 
			
		||||
                new_record = Record(
 | 
			
		||||
                    activity=record_data['activity'], record_type=record_type
 | 
			
		||||
                )
 | 
			
		||||
                new_record.value = record_data['record_value']
 | 
			
		||||
                new_record.value = record_data['record_value']  # type: ignore
 | 
			
		||||
                session.add(new_record)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
from typing import Dict
 | 
			
		||||
 | 
			
		||||
from flask import Blueprint
 | 
			
		||||
 | 
			
		||||
from ..users.utils import authenticate
 | 
			
		||||
@@ -8,7 +10,7 @@ records_blueprint = Blueprint('records', __name__)
 | 
			
		||||
 | 
			
		||||
@records_blueprint.route('/records', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_records(auth_user_id):
 | 
			
		||||
def get_records(auth_user_id: int) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Get all records for authenticated user.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,9 @@
 | 
			
		||||
from typing import Dict, Union
 | 
			
		||||
 | 
			
		||||
from fittrackee import db
 | 
			
		||||
from fittrackee.responses import (
 | 
			
		||||
    DataNotFoundErrorResponse,
 | 
			
		||||
    HttpResponse,
 | 
			
		||||
    InvalidPayloadErrorResponse,
 | 
			
		||||
    handle_error_and_return_response,
 | 
			
		||||
)
 | 
			
		||||
@@ -16,7 +19,7 @@ sports_blueprint = Blueprint('sports', __name__)
 | 
			
		||||
 | 
			
		||||
@sports_blueprint.route('/sports', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_sports(auth_user_id):
 | 
			
		||||
def get_sports(auth_user_id: int) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Get all sports
 | 
			
		||||
 | 
			
		||||
@@ -158,7 +161,7 @@ def get_sports(auth_user_id):
 | 
			
		||||
 | 
			
		||||
@sports_blueprint.route('/sports/<int:sport_id>', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_sport(auth_user_id, sport_id):
 | 
			
		||||
def get_sport(auth_user_id: int, sport_id: int) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get a sport
 | 
			
		||||
 | 
			
		||||
@@ -253,7 +256,9 @@ def get_sport(auth_user_id, sport_id):
 | 
			
		||||
 | 
			
		||||
@sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH'])
 | 
			
		||||
@authenticate_as_admin
 | 
			
		||||
def update_sport(auth_user_id, sport_id):
 | 
			
		||||
def update_sport(
 | 
			
		||||
    auth_user_id: int, sport_id: int
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Update a sport
 | 
			
		||||
    Authenticated user must be an admin
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Dict, Union
 | 
			
		||||
 | 
			
		||||
from fittrackee import db
 | 
			
		||||
from fittrackee.responses import (
 | 
			
		||||
    HttpResponse,
 | 
			
		||||
    InvalidPayloadErrorResponse,
 | 
			
		||||
    NotFoundErrorResponse,
 | 
			
		||||
    UserNotFoundErrorResponse,
 | 
			
		||||
@@ -19,7 +21,12 @@ from .utils_format import convert_timedelta_to_integer
 | 
			
		||||
stats_blueprint = Blueprint('stats', __name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_activities(user_name, filter_type):
 | 
			
		||||
def get_activities(
 | 
			
		||||
    user_name: str, filter_type: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Return user activities by sport or by time
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        user = User.query.filter_by(username=user_name).first()
 | 
			
		||||
        if not user:
 | 
			
		||||
@@ -40,7 +47,6 @@ def get_activities(user_name, filter_type):
 | 
			
		||||
        time = params.get('time')
 | 
			
		||||
 | 
			
		||||
        if filter_type == 'by_sport':
 | 
			
		||||
            sport_id = params.get('sport_id')
 | 
			
		||||
            if sport_id:
 | 
			
		||||
                sport = Sport.query.filter_by(id=sport_id).first()
 | 
			
		||||
                if not sport:
 | 
			
		||||
@@ -59,24 +65,26 @@ def get_activities(user_name, filter_type):
 | 
			
		||||
            .all()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        activities_list = {}
 | 
			
		||||
        activities_list_by_sport = {}
 | 
			
		||||
        activities_list_by_time = {}  # type: ignore
 | 
			
		||||
        for activity in activities:
 | 
			
		||||
            if filter_type == 'by_sport':
 | 
			
		||||
                sport_id = activity.sport_id
 | 
			
		||||
                if sport_id not in activities_list:
 | 
			
		||||
                    activities_list[sport_id] = {
 | 
			
		||||
                if sport_id not in activities_list_by_sport:
 | 
			
		||||
                    activities_list_by_sport[sport_id] = {
 | 
			
		||||
                        'nb_activities': 0,
 | 
			
		||||
                        'total_distance': 0.0,
 | 
			
		||||
                        'total_duration': 0,
 | 
			
		||||
                    }
 | 
			
		||||
                activities_list[sport_id]['nb_activities'] += 1
 | 
			
		||||
                activities_list[sport_id]['total_distance'] += float(
 | 
			
		||||
                activities_list_by_sport[sport_id]['nb_activities'] += 1
 | 
			
		||||
                activities_list_by_sport[sport_id]['total_distance'] += float(
 | 
			
		||||
                    activity.distance
 | 
			
		||||
                )
 | 
			
		||||
                activities_list[sport_id][
 | 
			
		||||
                activities_list_by_sport[sport_id][
 | 
			
		||||
                    'total_duration'
 | 
			
		||||
                ] += convert_timedelta_to_integer(activity.moving)
 | 
			
		||||
 | 
			
		||||
            # filter_type == 'by_time'
 | 
			
		||||
            else:
 | 
			
		||||
                if time == 'week':
 | 
			
		||||
                    activity_date = activity.activity_date - timedelta(
 | 
			
		||||
@@ -105,25 +113,31 @@ def get_activities(user_name, filter_type):
 | 
			
		||||
                        'Invalid time period.', 'fail'
 | 
			
		||||
                    )
 | 
			
		||||
                sport_id = activity.sport_id
 | 
			
		||||
                if time_period not in activities_list:
 | 
			
		||||
                    activities_list[time_period] = {}
 | 
			
		||||
                if sport_id not in activities_list[time_period]:
 | 
			
		||||
                    activities_list[time_period][sport_id] = {
 | 
			
		||||
                if time_period not in activities_list_by_time:
 | 
			
		||||
                    activities_list_by_time[time_period] = {}
 | 
			
		||||
                if sport_id not in activities_list_by_time[time_period]:
 | 
			
		||||
                    activities_list_by_time[time_period][sport_id] = {
 | 
			
		||||
                        'nb_activities': 0,
 | 
			
		||||
                        'total_distance': 0.0,
 | 
			
		||||
                        'total_duration': 0,
 | 
			
		||||
                    }
 | 
			
		||||
                activities_list[time_period][sport_id]['nb_activities'] += 1
 | 
			
		||||
                activities_list[time_period][sport_id][
 | 
			
		||||
                activities_list_by_time[time_period][sport_id][
 | 
			
		||||
                    'nb_activities'
 | 
			
		||||
                ] += 1
 | 
			
		||||
                activities_list_by_time[time_period][sport_id][
 | 
			
		||||
                    'total_distance'
 | 
			
		||||
                ] += float(activity.distance)
 | 
			
		||||
                activities_list[time_period][sport_id][
 | 
			
		||||
                activities_list_by_time[time_period][sport_id][
 | 
			
		||||
                    'total_duration'
 | 
			
		||||
                ] += convert_timedelta_to_integer(activity.moving)
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            'status': 'success',
 | 
			
		||||
            'data': {'statistics': activities_list},
 | 
			
		||||
            'data': {
 | 
			
		||||
                'statistics': activities_list_by_sport
 | 
			
		||||
                if filter_type == 'by_sport'
 | 
			
		||||
                else activities_list_by_time
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        return handle_error_and_return_response(e)
 | 
			
		||||
@@ -131,7 +145,9 @@ def get_activities(user_name, filter_type):
 | 
			
		||||
 | 
			
		||||
@stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_activities_by_time(auth_user_id, user_name):
 | 
			
		||||
def get_activities_by_time(
 | 
			
		||||
    auth_user_id: int, user_name: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get activities statistics for a user by time
 | 
			
		||||
 | 
			
		||||
@@ -227,7 +243,9 @@ def get_activities_by_time(auth_user_id, user_name):
 | 
			
		||||
 | 
			
		||||
@stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_activities_by_sport(auth_user_id, user_name):
 | 
			
		||||
def get_activities_by_sport(
 | 
			
		||||
    auth_user_id: int, user_name: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get activities statistics for a user by sport
 | 
			
		||||
 | 
			
		||||
@@ -313,7 +331,7 @@ def get_activities_by_sport(auth_user_id, user_name):
 | 
			
		||||
 | 
			
		||||
@stats_blueprint.route('/stats/all', methods=['GET'])
 | 
			
		||||
@authenticate_as_admin
 | 
			
		||||
def get_application_stats(auth_user_id):
 | 
			
		||||
def get_application_stats(auth_user_id: int) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Get all application statistics
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@ import os
 | 
			
		||||
import tempfile
 | 
			
		||||
import zipfile
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Dict, List, Optional, Tuple, Union
 | 
			
		||||
from uuid import UUID
 | 
			
		||||
 | 
			
		||||
import gpxpy.gpx
 | 
			
		||||
import pytz
 | 
			
		||||
@@ -10,6 +12,7 @@ from fittrackee import appLog, db
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
from ..users.models import User
 | 
			
		||||
@@ -19,13 +22,20 @@ from .utils_gpx import get_gpx_info
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ActivityException(Exception):
 | 
			
		||||
    def __init__(self, status, message, e):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, status: str, message: str, e: Optional[Exception] = None
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        self.status = status
 | 
			
		||||
        self.message = message
 | 
			
		||||
        self.e = e
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_datetime_with_tz(timezone, activity_date, gpx_data=None):
 | 
			
		||||
def get_datetime_with_tz(
 | 
			
		||||
    timezone: str, activity_date: datetime, gpx_data: Optional[Dict] = None
 | 
			
		||||
) -> Tuple[Optional[datetime], datetime]:
 | 
			
		||||
    """
 | 
			
		||||
    Return naive datetime and datetime with user timezone
 | 
			
		||||
    """
 | 
			
		||||
    activity_date_tz = None
 | 
			
		||||
    if timezone:
 | 
			
		||||
        user_tz = pytz.timezone(timezone)
 | 
			
		||||
@@ -47,8 +57,12 @@ def get_datetime_with_tz(timezone, activity_date, gpx_data=None):
 | 
			
		||||
    return activity_date_tz, activity_date
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_activity_data(activity, gpx_data):
 | 
			
		||||
    """activity could be a complete activity or an activity segment"""
 | 
			
		||||
def update_activity_data(
 | 
			
		||||
    activity: Union[Activity, ActivitySegment], gpx_data: Dict
 | 
			
		||||
) -> Union[Activity, ActivitySegment]:
 | 
			
		||||
    """
 | 
			
		||||
    Update activity or activity segment with data from gpx file
 | 
			
		||||
    """
 | 
			
		||||
    activity.pauses = gpx_data['stop_time']
 | 
			
		||||
    activity.moving = gpx_data['moving_time']
 | 
			
		||||
    activity.min_alt = gpx_data['elevation_min']
 | 
			
		||||
@@ -60,12 +74,18 @@ def update_activity_data(activity, gpx_data):
 | 
			
		||||
    return activity
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_activity(user, activity_data, gpx_data=None):
 | 
			
		||||
def create_activity(
 | 
			
		||||
    user: User, activity_data: Dict, gpx_data: Optional[Dict] = None
 | 
			
		||||
) -> Activity:
 | 
			
		||||
    """
 | 
			
		||||
    Create Activity from data entered by user and from gpx if a gpx file is
 | 
			
		||||
    provided
 | 
			
		||||
    """
 | 
			
		||||
    activity_date = (
 | 
			
		||||
        gpx_data['start']
 | 
			
		||||
        if gpx_data
 | 
			
		||||
        else datetime.strptime(
 | 
			
		||||
            activity_data.get('activity_date'), '%Y-%m-%d %H:%M'
 | 
			
		||||
            activity_data['activity_date'], '%Y-%m-%d %H:%M'
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    activity_date_tz, activity_date = get_datetime_with_tz(
 | 
			
		||||
@@ -75,16 +95,14 @@ def create_activity(user, activity_data, gpx_data=None):
 | 
			
		||||
    duration = (
 | 
			
		||||
        gpx_data['duration']
 | 
			
		||||
        if gpx_data
 | 
			
		||||
        else timedelta(seconds=activity_data.get('duration'))
 | 
			
		||||
        else timedelta(seconds=activity_data['duration'])
 | 
			
		||||
    )
 | 
			
		||||
    distance = (
 | 
			
		||||
        gpx_data['distance'] if gpx_data else activity_data.get('distance')
 | 
			
		||||
    )
 | 
			
		||||
    title = gpx_data['name'] if gpx_data else activity_data.get('title')
 | 
			
		||||
    distance = gpx_data['distance'] if gpx_data else activity_data['distance']
 | 
			
		||||
    title = gpx_data['name'] if gpx_data else activity_data.get('title', '')
 | 
			
		||||
 | 
			
		||||
    new_activity = Activity(
 | 
			
		||||
        user_id=user.id,
 | 
			
		||||
        sport_id=activity_data.get('sport_id'),
 | 
			
		||||
        sport_id=activity_data['sport_id'],
 | 
			
		||||
        activity_date=activity_date,
 | 
			
		||||
        distance=distance,
 | 
			
		||||
        duration=duration,
 | 
			
		||||
@@ -118,7 +136,12 @@ def create_activity(user, activity_data, gpx_data=None):
 | 
			
		||||
    return new_activity
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_segment(activity_id, activity_uuid, segment_data):
 | 
			
		||||
def create_segment(
 | 
			
		||||
    activity_id: int, activity_uuid: UUID, segment_data: Dict
 | 
			
		||||
) -> ActivitySegment:
 | 
			
		||||
    """
 | 
			
		||||
    Create Activity Segment from gpx data
 | 
			
		||||
    """
 | 
			
		||||
    new_segment = ActivitySegment(
 | 
			
		||||
        activity_id=activity_id,
 | 
			
		||||
        activity_uuid=activity_uuid,
 | 
			
		||||
@@ -130,12 +153,9 @@ def create_segment(activity_id, activity_uuid, segment_data):
 | 
			
		||||
    return new_segment
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_activity(activity):
 | 
			
		||||
def update_activity(activity: Activity) -> Activity:
 | 
			
		||||
    """
 | 
			
		||||
    Note: only gpx_data is be updated for now (the gpx file is NOT modified)
 | 
			
		||||
 | 
			
		||||
    In a next version, map_data and weather_data will be updated
 | 
			
		||||
    (case of a modified gpx file, see issue #7)
 | 
			
		||||
    Update activity data from gpx file
 | 
			
		||||
    """
 | 
			
		||||
    gpx_data, _, _ = get_gpx_info(
 | 
			
		||||
        get_absolute_file_path(activity.gpx), False, False
 | 
			
		||||
@@ -155,7 +175,16 @@ def update_activity(activity):
 | 
			
		||||
    return updated_activity
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def edit_activity(activity, activity_data, auth_user_id):
 | 
			
		||||
def edit_activity(
 | 
			
		||||
    activity: Activity, activity_data: Dict, auth_user_id: int
 | 
			
		||||
) -> Activity:
 | 
			
		||||
    """
 | 
			
		||||
    Edit an activity
 | 
			
		||||
    Note: the gpx file is NOT modified
 | 
			
		||||
 | 
			
		||||
    In a next version, map_data and weather_data will be updated
 | 
			
		||||
    (case of a modified gpx file, see issue #7)
 | 
			
		||||
    """
 | 
			
		||||
    user = User.query.filter_by(id=auth_user_id).first()
 | 
			
		||||
    if activity_data.get('refresh'):
 | 
			
		||||
        activity = update_activity(activity)
 | 
			
		||||
@@ -168,20 +197,18 @@ def edit_activity(activity, activity_data, auth_user_id):
 | 
			
		||||
    if not activity.gpx:
 | 
			
		||||
        if activity_data.get('activity_date'):
 | 
			
		||||
            activity_date = datetime.strptime(
 | 
			
		||||
                activity_data.get('activity_date'), '%Y-%m-%d %H:%M'
 | 
			
		||||
                activity_data['activity_date'], '%Y-%m-%d %H:%M'
 | 
			
		||||
            )
 | 
			
		||||
            _, activity.activity_date = get_datetime_with_tz(
 | 
			
		||||
                user.timezone, activity_date
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if activity_data.get('duration'):
 | 
			
		||||
            activity.duration = timedelta(
 | 
			
		||||
                seconds=activity_data.get('duration')
 | 
			
		||||
            )
 | 
			
		||||
            activity.duration = timedelta(seconds=activity_data['duration'])
 | 
			
		||||
            activity.moving = activity.duration
 | 
			
		||||
 | 
			
		||||
        if activity_data.get('distance'):
 | 
			
		||||
            activity.distance = activity_data.get('distance')
 | 
			
		||||
            activity.distance = activity_data['distance']
 | 
			
		||||
 | 
			
		||||
        activity.ave_speed = (
 | 
			
		||||
            None
 | 
			
		||||
@@ -192,7 +219,10 @@ def edit_activity(activity, activity_data, auth_user_id):
 | 
			
		||||
    return activity
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_file_path(dir_path, filename):
 | 
			
		||||
def get_file_path(dir_path: str, filename: str) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Get full path for a file
 | 
			
		||||
    """
 | 
			
		||||
    if not os.path.exists(dir_path):
 | 
			
		||||
        os.makedirs(dir_path)
 | 
			
		||||
    file_path = os.path.join(dir_path, filename)
 | 
			
		||||
@@ -200,9 +230,16 @@ def get_file_path(dir_path, filename):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_new_file_path(
 | 
			
		||||
    auth_user_id, activity_date, sport, old_filename=None, extension=None
 | 
			
		||||
):
 | 
			
		||||
    if not extension:
 | 
			
		||||
    auth_user_id: int,
 | 
			
		||||
    activity_date: str,
 | 
			
		||||
    sport: str,
 | 
			
		||||
    old_filename: Optional[str] = None,
 | 
			
		||||
    extension: Optional[str] = None,
 | 
			
		||||
) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Generate a file path from user and activity data
 | 
			
		||||
    """
 | 
			
		||||
    if not extension and old_filename:
 | 
			
		||||
        extension = f".{old_filename.rsplit('.', 1)[1].lower()}"
 | 
			
		||||
    _, new_filename = tempfile.mkstemp(
 | 
			
		||||
        prefix=f'{activity_date}_{sport}_', suffix=extension
 | 
			
		||||
@@ -214,7 +251,10 @@ def get_new_file_path(
 | 
			
		||||
    return file_path
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_map(map_filepath, map_data):
 | 
			
		||||
def generate_map(map_filepath: str, map_data: List) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Generate and save map image from map data
 | 
			
		||||
    """
 | 
			
		||||
    m = StaticMap(400, 225, 10)
 | 
			
		||||
    line = Line(map_data, '#3388FF', 4)
 | 
			
		||||
    m.add_line(line)
 | 
			
		||||
@@ -222,10 +262,10 @@ def generate_map(map_filepath, map_data):
 | 
			
		||||
    image.save(map_filepath)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_map_hash(map_filepath):
 | 
			
		||||
def get_map_hash(map_filepath: str) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    md5 hash is used as id instead of activity id, to retrieve map image
 | 
			
		||||
    (maps are sensitive data)
 | 
			
		||||
    Generate a md5 hash used as id instead of activity id, to retrieve map
 | 
			
		||||
    image (maps are sensitive data)
 | 
			
		||||
    """
 | 
			
		||||
    md5 = hashlib.md5()
 | 
			
		||||
    absolute_map_filepath = get_absolute_file_path(map_filepath)
 | 
			
		||||
@@ -235,7 +275,10 @@ def get_map_hash(map_filepath):
 | 
			
		||||
    return md5.hexdigest()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def process_one_gpx_file(params, filename):
 | 
			
		||||
def process_one_gpx_file(params: Dict, filename: str) -> Activity:
 | 
			
		||||
    """
 | 
			
		||||
    Get all data from a gpx file to create an activity with map image
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        gpx_data, map_data, weather_data = get_gpx_info(params['file_path'])
 | 
			
		||||
        auth_user_id = params['user'].id
 | 
			
		||||
@@ -284,23 +327,33 @@ def process_one_gpx_file(params, filename):
 | 
			
		||||
        raise ActivityException('fail', 'Error during activity save.', e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def process_zip_archive(common_params, extract_dir):
 | 
			
		||||
def process_zip_archive(common_params: Dict, extract_dir: str) -> List:
 | 
			
		||||
    """
 | 
			
		||||
    Get files from a zip archive and create activities, if number of files
 | 
			
		||||
    does not exceed defined limit.
 | 
			
		||||
    """
 | 
			
		||||
    with zipfile.ZipFile(common_params['file_path'], "r") as zip_ref:
 | 
			
		||||
        zip_ref.extractall(extract_dir)
 | 
			
		||||
 | 
			
		||||
    new_activities = []
 | 
			
		||||
    gpx_files_limit = os.getenv('REACT_APP_GPX_LIMIT_IMPORT', '10')
 | 
			
		||||
    if gpx_files_limit and gpx_files_limit.isdigit():
 | 
			
		||||
    gpx_files_limit = os.getenv('REACT_APP_GPX_LIMIT_IMPORT', 10)
 | 
			
		||||
    if (
 | 
			
		||||
        gpx_files_limit
 | 
			
		||||
        and isinstance(gpx_files_limit, str)
 | 
			
		||||
        and gpx_files_limit.isdigit()
 | 
			
		||||
    ):
 | 
			
		||||
        gpx_files_limit = int(gpx_files_limit)
 | 
			
		||||
    else:
 | 
			
		||||
        gpx_files_limit = 10
 | 
			
		||||
        appLog.error('GPX limit not configured, set to 10.')
 | 
			
		||||
        appLog.warning('GPX limit not configured, set to 10.')
 | 
			
		||||
    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.get('ACTIVITY_ALLOWED_EXTENSIONS'):
 | 
			
		||||
        if (
 | 
			
		||||
            '.' in gpx_file
 | 
			
		||||
            and gpx_file.rsplit('.', 1)[1].lower()
 | 
			
		||||
            in current_app.config['ACTIVITY_ALLOWED_EXTENSIONS']
 | 
			
		||||
        ):
 | 
			
		||||
            gpx_files_ok += 1
 | 
			
		||||
            if gpx_files_ok > gpx_files_limit:
 | 
			
		||||
                break
 | 
			
		||||
@@ -313,7 +366,17 @@ def process_zip_archive(common_params, extract_dir):
 | 
			
		||||
    return new_activities
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def process_files(auth_user_id, activity_data, activity_file, folders):
 | 
			
		||||
def process_files(
 | 
			
		||||
    auth_user_id: int,
 | 
			
		||||
    activity_data: Dict,
 | 
			
		||||
    activity_file: FileStorage,
 | 
			
		||||
    folders: Dict,
 | 
			
		||||
) -> List:
 | 
			
		||||
    """
 | 
			
		||||
    Store gpx file or zip archive and create activities
 | 
			
		||||
    """
 | 
			
		||||
    if activity_file.filename is None:
 | 
			
		||||
        raise ActivityException('error', 'File has no filename.')
 | 
			
		||||
    filename = secure_filename(activity_file.filename)
 | 
			
		||||
    extension = f".{filename.rsplit('.', 1)[1].lower()}"
 | 
			
		||||
    file_path = get_file_path(folders['tmp_dir'], filename)
 | 
			
		||||
@@ -322,7 +385,6 @@ def process_files(auth_user_id, activity_data, activity_file, folders):
 | 
			
		||||
        raise ActivityException(
 | 
			
		||||
            'error',
 | 
			
		||||
            f"Sport id: {activity_data.get('sport_id')} does not exist",
 | 
			
		||||
            None,
 | 
			
		||||
        )
 | 
			
		||||
    user = User.query.filter_by(id=auth_user_id).first()
 | 
			
		||||
 | 
			
		||||
@@ -344,7 +406,10 @@ def process_files(auth_user_id, activity_data, activity_file, folders):
 | 
			
		||||
        return process_zip_archive(common_params, folders['extract_dir'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_upload_dir_size():
 | 
			
		||||
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):
 | 
			
		||||
 
 | 
			
		||||
@@ -3,5 +3,5 @@ import os
 | 
			
		||||
from flask import current_app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_absolute_file_path(relative_path):
 | 
			
		||||
def get_absolute_file_path(relative_path: str) -> str:
 | 
			
		||||
    return os.path.join(current_app.config['UPLOAD_FOLDER'], relative_path)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,26 @@
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from typing import Optional, Union
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def convert_in_duration(value):
 | 
			
		||||
def convert_in_duration(value: str) -> timedelta:
 | 
			
		||||
    hours = int(value.split(':')[0])
 | 
			
		||||
    minutes = int(value.split(':')[1])
 | 
			
		||||
    return timedelta(seconds=(hours * 3600 + minutes * 60))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def convert_timedelta_to_integer(value):
 | 
			
		||||
def convert_timedelta_to_integer(value: str) -> int:
 | 
			
		||||
    hours, minutes, seconds = str(value).split(':')
 | 
			
		||||
    return int(hours) * 3600 + int(minutes) * 60 + int(seconds)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def convert_value_to_integer(record_type, val):
 | 
			
		||||
def convert_value_to_integer(
 | 
			
		||||
    record_type: str, val: Union[str, float]
 | 
			
		||||
) -> Optional[int]:
 | 
			
		||||
    if val is None:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    if record_type == 'LD':
 | 
			
		||||
        return convert_timedelta_to_integer(val)
 | 
			
		||||
        return convert_timedelta_to_integer(str(val))
 | 
			
		||||
    elif record_type in ['AS', 'MS']:
 | 
			
		||||
        return int(val * 100)
 | 
			
		||||
    else:  # 'FD'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from typing import Any, Dict, List, Optional, Tuple
 | 
			
		||||
 | 
			
		||||
import gpxpy.gpx
 | 
			
		||||
 | 
			
		||||
@@ -6,25 +7,40 @@ from .utils_weather import get_weather
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ActivityGPXException(Exception):
 | 
			
		||||
    def __init__(self, status, message, e):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, status: str, message: str, e: Optional[Exception] = None
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        self.status = status
 | 
			
		||||
        self.message = message
 | 
			
		||||
        self.e = e
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def open_gpx_file(gpx_file):
 | 
			
		||||
    gpx_file = open(gpx_file, 'r')
 | 
			
		||||
def open_gpx_file(gpx_file: str) -> Optional[gpxpy.gpx.GPX]:
 | 
			
		||||
    gpx_file = open(gpx_file, 'r')  # type: ignore
 | 
			
		||||
    gpx = gpxpy.parse(gpx_file)
 | 
			
		||||
    if len(gpx.tracks) == 0:
 | 
			
		||||
        return None
 | 
			
		||||
    return gpx
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_gpx_data(parsed_gpx, max_speed, start, stopped_time_btwn_seg):
 | 
			
		||||
    gpx_data = {'max_speed': (max_speed / 1000) * 3600, 'start': start}
 | 
			
		||||
def get_gpx_data(
 | 
			
		||||
    parsed_gpx: gpxpy.gpx,
 | 
			
		||||
    max_speed: float,
 | 
			
		||||
    start: int,
 | 
			
		||||
    stopped_time_between_seg: timedelta,
 | 
			
		||||
) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Returns data from parsed gpx file
 | 
			
		||||
    """
 | 
			
		||||
    gpx_data: Dict[str, Any] = {
 | 
			
		||||
        'max_speed': (max_speed / 1000) * 3600,
 | 
			
		||||
        'start': start,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    duration = parsed_gpx.get_duration()
 | 
			
		||||
    gpx_data['duration'] = timedelta(seconds=duration) + stopped_time_btwn_seg
 | 
			
		||||
    gpx_data['duration'] = (
 | 
			
		||||
        timedelta(seconds=duration) + stopped_time_between_seg
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    ele = parsed_gpx.get_elevation_extremes()
 | 
			
		||||
    gpx_data['elevation_max'] = ele.maximum
 | 
			
		||||
@@ -37,7 +53,7 @@ def get_gpx_data(parsed_gpx, max_speed, start, stopped_time_btwn_seg):
 | 
			
		||||
    mv = parsed_gpx.get_moving_data()
 | 
			
		||||
    gpx_data['moving_time'] = timedelta(seconds=mv.moving_time)
 | 
			
		||||
    gpx_data['stop_time'] = (
 | 
			
		||||
        timedelta(seconds=mv.stopped_time) + stopped_time_btwn_seg
 | 
			
		||||
        timedelta(seconds=mv.stopped_time) + stopped_time_between_seg
 | 
			
		||||
    )
 | 
			
		||||
    distance = mv.moving_distance + mv.stopped_distance
 | 
			
		||||
    gpx_data['distance'] = distance / 1000
 | 
			
		||||
@@ -48,10 +64,17 @@ def get_gpx_data(parsed_gpx, max_speed, start, stopped_time_btwn_seg):
 | 
			
		||||
    return gpx_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
 | 
			
		||||
def get_gpx_info(
 | 
			
		||||
    gpx_file: str,
 | 
			
		||||
    update_map_data: Optional[bool] = True,
 | 
			
		||||
    update_weather_data: Optional[bool] = True,
 | 
			
		||||
) -> Tuple:
 | 
			
		||||
    """
 | 
			
		||||
    Parse and return gpx, map and weather data from gpx file
 | 
			
		||||
    """
 | 
			
		||||
    gpx = open_gpx_file(gpx_file)
 | 
			
		||||
    if gpx is None:
 | 
			
		||||
        return None
 | 
			
		||||
        raise ActivityGPXException('not found', 'No gpx file')
 | 
			
		||||
 | 
			
		||||
    gpx_data = {'name': gpx.tracks[0].name, 'segments': []}
 | 
			
		||||
    max_speed = 0
 | 
			
		||||
@@ -61,7 +84,7 @@ def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
 | 
			
		||||
    segments_nb = len(gpx.tracks[0].segments)
 | 
			
		||||
    prev_seg_last_point = None
 | 
			
		||||
    no_stopped_time = timedelta(seconds=0)
 | 
			
		||||
    stopped_time_btwn_seg = no_stopped_time
 | 
			
		||||
    stopped_time_between_seg = no_stopped_time
 | 
			
		||||
 | 
			
		||||
    for segment_idx, segment in enumerate(gpx.tracks[0].segments):
 | 
			
		||||
        segment_start = 0
 | 
			
		||||
@@ -77,7 +100,7 @@ def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
 | 
			
		||||
                # if a previous segment exists, calculate stopped time between
 | 
			
		||||
                # the two segments
 | 
			
		||||
                if prev_seg_last_point:
 | 
			
		||||
                    stopped_time_btwn_seg = point.time - prev_seg_last_point
 | 
			
		||||
                    stopped_time_between_seg = point.time - prev_seg_last_point
 | 
			
		||||
 | 
			
		||||
            # last segment point
 | 
			
		||||
            if point_idx == (segment_points_nb - 1):
 | 
			
		||||
@@ -104,7 +127,9 @@ def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
 | 
			
		||||
        segment_data['idx'] = segment_idx
 | 
			
		||||
        gpx_data['segments'].append(segment_data)
 | 
			
		||||
 | 
			
		||||
    full_gpx_data = get_gpx_data(gpx, max_speed, start, stopped_time_btwn_seg)
 | 
			
		||||
    full_gpx_data = get_gpx_data(
 | 
			
		||||
        gpx, max_speed, start, stopped_time_between_seg
 | 
			
		||||
    )
 | 
			
		||||
    gpx_data = {**gpx_data, **full_gpx_data}
 | 
			
		||||
 | 
			
		||||
    if update_map_data:
 | 
			
		||||
@@ -119,7 +144,12 @@ def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
 | 
			
		||||
    return gpx_data, map_data, weather_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_gpx_segments(track_segments, segment_id=None):
 | 
			
		||||
def get_gpx_segments(
 | 
			
		||||
    track_segments: List, segment_id: Optional[int] = None
 | 
			
		||||
) -> List:
 | 
			
		||||
    """
 | 
			
		||||
    Return list of segments, filtered on segment id if provided
 | 
			
		||||
    """
 | 
			
		||||
    if segment_id is not None:
 | 
			
		||||
        segment_index = segment_id - 1
 | 
			
		||||
        if segment_index > (len(track_segments) - 1):
 | 
			
		||||
@@ -135,7 +165,12 @@ def get_gpx_segments(track_segments, segment_id=None):
 | 
			
		||||
    return segments
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_chart_data(gpx_file, segment_id=None):
 | 
			
		||||
def get_chart_data(
 | 
			
		||||
    gpx_file: str, segment_id: Optional[int] = None
 | 
			
		||||
) -> Optional[List]:
 | 
			
		||||
    """
 | 
			
		||||
    Return data needed to generate chart with speed and elevation
 | 
			
		||||
    """
 | 
			
		||||
    gpx = open_gpx_file(gpx_file)
 | 
			
		||||
    if gpx is None:
 | 
			
		||||
        return None
 | 
			
		||||
@@ -193,7 +228,12 @@ def get_chart_data(gpx_file, segment_id=None):
 | 
			
		||||
    return chart_data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def extract_segment_from_gpx_file(content, segment_id):
 | 
			
		||||
def extract_segment_from_gpx_file(
 | 
			
		||||
    content: str, segment_id: int
 | 
			
		||||
) -> Optional[str]:
 | 
			
		||||
    """
 | 
			
		||||
    Returns segments in xml format from a gpx file content
 | 
			
		||||
    """
 | 
			
		||||
    gpx_content = gpxpy.parse(content)
 | 
			
		||||
    if len(gpx_content.tracks) == 0:
 | 
			
		||||
        return None
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,17 @@
 | 
			
		||||
from uuid import UUID
 | 
			
		||||
 | 
			
		||||
import shortuuid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def encode_uuid(uuid_value):
 | 
			
		||||
def encode_uuid(uuid_value: UUID) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Return short id string from an UUID
 | 
			
		||||
    """
 | 
			
		||||
    return shortuuid.encode(uuid_value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def decode_short_id(short_id):
 | 
			
		||||
def decode_short_id(short_id: str) -> UUID:
 | 
			
		||||
    """
 | 
			
		||||
    Return UUID from a short id string
 | 
			
		||||
    """
 | 
			
		||||
    return shortuuid.decode(short_id)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,15 @@
 | 
			
		||||
import os
 | 
			
		||||
from typing import Dict, Optional
 | 
			
		||||
 | 
			
		||||
import forecastio
 | 
			
		||||
import pytz
 | 
			
		||||
from fittrackee import appLog
 | 
			
		||||
from gpxpy.gpx import GPXRoutePoint
 | 
			
		||||
 | 
			
		||||
API_KEY = os.getenv('WEATHER_API_KEY')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_weather(point):
 | 
			
		||||
def get_weather(point: GPXRoutePoint) -> Optional[Dict]:
 | 
			
		||||
    if not API_KEY or API_KEY == '':
 | 
			
		||||
        return None
 | 
			
		||||
    try:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
from typing import Dict, Union
 | 
			
		||||
 | 
			
		||||
from fittrackee import db
 | 
			
		||||
from fittrackee.responses import (
 | 
			
		||||
    HttpResponse,
 | 
			
		||||
    InvalidPayloadErrorResponse,
 | 
			
		||||
    handle_error_and_return_response,
 | 
			
		||||
)
 | 
			
		||||
@@ -14,7 +17,7 @@ config_blueprint = Blueprint('config', __name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@config_blueprint.route('/config', methods=['GET'])
 | 
			
		||||
def get_application_config():
 | 
			
		||||
def get_application_config() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get Application config
 | 
			
		||||
 | 
			
		||||
@@ -59,7 +62,7 @@ def get_application_config():
 | 
			
		||||
 | 
			
		||||
@config_blueprint.route('/config', methods=['PATCH'])
 | 
			
		||||
@authenticate_as_admin
 | 
			
		||||
def update_application_config(auth_user_id):
 | 
			
		||||
def update_application_config(auth_user_id: int) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Update Application config
 | 
			
		||||
 | 
			
		||||
@@ -137,7 +140,7 @@ def update_application_config(auth_user_id):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@config_blueprint.route('/ping', methods=['GET'])
 | 
			
		||||
def health_check():
 | 
			
		||||
def health_check() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """health check endpoint
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,19 @@
 | 
			
		||||
from typing import Dict
 | 
			
		||||
 | 
			
		||||
from fittrackee import db
 | 
			
		||||
from flask import current_app
 | 
			
		||||
from sqlalchemy.engine.base import Connection
 | 
			
		||||
from sqlalchemy.event import listens_for
 | 
			
		||||
from sqlalchemy.ext.declarative import DeclarativeMeta
 | 
			
		||||
from sqlalchemy.orm.mapper import Mapper
 | 
			
		||||
from sqlalchemy.orm.session import Session
 | 
			
		||||
 | 
			
		||||
from ..users.models import User
 | 
			
		||||
 | 
			
		||||
BaseModel: DeclarativeMeta = db.Model
 | 
			
		||||
 | 
			
		||||
class AppConfig(db.Model):
 | 
			
		||||
 | 
			
		||||
class AppConfig(BaseModel):
 | 
			
		||||
    __tablename__ = 'app_config'
 | 
			
		||||
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
 | 
			
		||||
    max_users = db.Column(db.Integer, default=0, nullable=False)
 | 
			
		||||
@@ -16,26 +24,26 @@ class AppConfig(db.Model):
 | 
			
		||||
    max_zip_file_size = db.Column(db.Integer, default=10485760, nullable=False)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_registration_enabled(self):
 | 
			
		||||
    def is_registration_enabled(self) -> bool:
 | 
			
		||||
        nb_users = User.query.count()
 | 
			
		||||
        return self.max_users == 0 or nb_users < self.max_users
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def map_attribution(self):
 | 
			
		||||
    def map_attribution(self) -> str:
 | 
			
		||||
        return current_app.config['TILE_SERVER']['ATTRIBUTION']
 | 
			
		||||
 | 
			
		||||
    def serialize(self):
 | 
			
		||||
    def serialize(self) -> Dict:
 | 
			
		||||
        return {
 | 
			
		||||
            "gpx_limit_import": self.gpx_limit_import,
 | 
			
		||||
            "is_registration_enabled": self.is_registration_enabled,
 | 
			
		||||
            "max_single_file_size": self.max_single_file_size,
 | 
			
		||||
            "max_zip_file_size": self.max_zip_file_size,
 | 
			
		||||
            "max_users": self.max_users,
 | 
			
		||||
            "map_attribution": self.map_attribution,
 | 
			
		||||
            'gpx_limit_import': self.gpx_limit_import,
 | 
			
		||||
            'is_registration_enabled': self.is_registration_enabled,
 | 
			
		||||
            'max_single_file_size': self.max_single_file_size,
 | 
			
		||||
            'max_zip_file_size': self.max_zip_file_size,
 | 
			
		||||
            'max_users': self.max_users,
 | 
			
		||||
            'map_attribution': self.map_attribution,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_app_config():
 | 
			
		||||
def update_app_config() -> None:
 | 
			
		||||
    config = AppConfig.query.first()
 | 
			
		||||
    if config:
 | 
			
		||||
        current_app.config[
 | 
			
		||||
@@ -44,14 +52,16 @@ def update_app_config():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@listens_for(User, 'after_insert')
 | 
			
		||||
def on_user_insert(mapper, connection, user):
 | 
			
		||||
def on_user_insert(mapper: Mapper, connection: Connection, user: User) -> None:
 | 
			
		||||
    @listens_for(db.Session, 'after_flush', once=True)
 | 
			
		||||
    def receive_after_flush(session, context):
 | 
			
		||||
    def receive_after_flush(session: Session, context: Connection) -> None:
 | 
			
		||||
        update_app_config()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@listens_for(User, 'after_delete')
 | 
			
		||||
def on_user_delete(mapper, connection, old_user):
 | 
			
		||||
def on_user_delete(
 | 
			
		||||
    mapper: Mapper, connection: Connection, old_user: User
 | 
			
		||||
) -> None:
 | 
			
		||||
    @listens_for(db.Session, 'after_flush', once=True)
 | 
			
		||||
    def receive_after_flush(session, context):
 | 
			
		||||
    def receive_after_flush(session: Session, context: Connection) -> None:
 | 
			
		||||
        update_app_config()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,16 @@
 | 
			
		||||
import os
 | 
			
		||||
from typing import Tuple
 | 
			
		||||
 | 
			
		||||
from fittrackee import db
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
from .models import AppConfig
 | 
			
		||||
 | 
			
		||||
MAX_FILE_SIZE = 1 * 1024 * 1024  # 1MB
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def init_config():
 | 
			
		||||
def init_config() -> Tuple[bool, AppConfig]:
 | 
			
		||||
    """
 | 
			
		||||
    init application configuration if not existing in database
 | 
			
		||||
 | 
			
		||||
@@ -36,7 +38,9 @@ def init_config():
 | 
			
		||||
    return False, existing_config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_app_config_from_database(current_app, db_config):
 | 
			
		||||
def update_app_config_from_database(
 | 
			
		||||
    current_app: Flask, db_config: AppConfig
 | 
			
		||||
) -> None:
 | 
			
		||||
    current_app.config['gpx_limit_import'] = db_config.gpx_limit_import
 | 
			
		||||
    current_app.config['max_single_file_size'] = db_config.max_single_file_size
 | 
			
		||||
    current_app.config['MAX_CONTENT_LENGTH'] = db_config.max_zip_file_size
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,10 @@ from fittrackee.application.utils import (
 | 
			
		||||
    update_app_config_from_database,
 | 
			
		||||
)
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def init_database(app):
 | 
			
		||||
def init_database(app: Flask) -> None:
 | 
			
		||||
    """Init the database."""
 | 
			
		||||
    admin = User(
 | 
			
		||||
        username='admin', email='admin@example.com', password='mpwoadmin'
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,9 @@ import smtplib
 | 
			
		||||
import ssl
 | 
			
		||||
from email.mime.multipart import MIMEMultipart
 | 
			
		||||
from email.mime.text import MIMEText
 | 
			
		||||
from typing import Dict, Optional, Type, Union
 | 
			
		||||
 | 
			
		||||
from flask import Flask
 | 
			
		||||
from jinja2 import Environment, FileSystemLoader
 | 
			
		||||
 | 
			
		||||
from .utils_email import parse_email_url
 | 
			
		||||
@@ -13,14 +15,16 @@ email_log.setLevel(logging.DEBUG)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EmailMessage:
 | 
			
		||||
    def __init__(self, sender, recipient, subject, html, text):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, sender: str, recipient: str, subject: str, html: str, text: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        self.sender = sender
 | 
			
		||||
        self.recipient = recipient
 | 
			
		||||
        self.subject = subject
 | 
			
		||||
        self.html = html
 | 
			
		||||
        self.text = text
 | 
			
		||||
 | 
			
		||||
    def generate_message(self):
 | 
			
		||||
    def generate_message(self) -> MIMEMultipart:
 | 
			
		||||
        message = MIMEMultipart('alternative')
 | 
			
		||||
        message['Subject'] = self.subject
 | 
			
		||||
        message['From'] = self.sender
 | 
			
		||||
@@ -33,20 +37,24 @@ class EmailMessage:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EmailTemplate:
 | 
			
		||||
    def __init__(self, template_directory):
 | 
			
		||||
    def __init__(self, template_directory: str) -> None:
 | 
			
		||||
        self._env = Environment(loader=FileSystemLoader(template_directory))
 | 
			
		||||
 | 
			
		||||
    def get_content(self, template, lang, part, data):
 | 
			
		||||
        template = self._env.get_template(f'{template}/{lang}/{part}')
 | 
			
		||||
    def get_content(
 | 
			
		||||
        self, template_name: str, lang: str, part: str, data: Dict
 | 
			
		||||
    ) -> str:
 | 
			
		||||
        template = self._env.get_template(f'{template_name}/{lang}/{part}')
 | 
			
		||||
        return template.render(data)
 | 
			
		||||
 | 
			
		||||
    def get_all_contents(self, template, lang, data):
 | 
			
		||||
    def get_all_contents(self, template: str, lang: str, data: Dict) -> Dict:
 | 
			
		||||
        output = {}
 | 
			
		||||
        for part in ['subject.txt', 'body.txt', 'body.html']:
 | 
			
		||||
            output[part] = self.get_content(template, lang, part, data)
 | 
			
		||||
        return output
 | 
			
		||||
 | 
			
		||||
    def get_message(self, template, lang, sender, recipient, data):
 | 
			
		||||
    def get_message(
 | 
			
		||||
        self, template: str, lang: str, sender: str, recipient: str, data: Dict
 | 
			
		||||
    ) -> MIMEMultipart:
 | 
			
		||||
        output = self.get_all_contents(template, lang, data)
 | 
			
		||||
        message = EmailMessage(
 | 
			
		||||
            sender,
 | 
			
		||||
@@ -59,7 +67,7 @@ class EmailTemplate:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Email:
 | 
			
		||||
    def __init__(self, app=None):
 | 
			
		||||
    def __init__(self, app: Optional[Flask] = None) -> None:
 | 
			
		||||
        self.host = 'localhost'
 | 
			
		||||
        self.port = 1025
 | 
			
		||||
        self.use_tls = False
 | 
			
		||||
@@ -67,26 +75,30 @@ class Email:
 | 
			
		||||
        self.username = None
 | 
			
		||||
        self.password = None
 | 
			
		||||
        self.sender_email = 'no-reply@example.com'
 | 
			
		||||
        self.email_template = None
 | 
			
		||||
        self.email_template: Optional[EmailTemplate] = None
 | 
			
		||||
        if app is not None:
 | 
			
		||||
            self.init_email(app)
 | 
			
		||||
 | 
			
		||||
    def init_email(self, app):
 | 
			
		||||
        parsed_url = parse_email_url(app.config.get('EMAIL_URL'))
 | 
			
		||||
    def init_email(self, app: Flask) -> None:
 | 
			
		||||
        parsed_url = parse_email_url(app.config['EMAIL_URL'])
 | 
			
		||||
        self.host = parsed_url['host']
 | 
			
		||||
        self.port = parsed_url['port']
 | 
			
		||||
        self.use_tls = parsed_url['use_tls']
 | 
			
		||||
        self.use_ssl = parsed_url['use_ssl']
 | 
			
		||||
        self.username = parsed_url['username']
 | 
			
		||||
        self.password = parsed_url['password']
 | 
			
		||||
        self.sender_email = app.config.get('SENDER_EMAIL')
 | 
			
		||||
        self.email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
 | 
			
		||||
        self.sender_email = app.config['SENDER_EMAIL']
 | 
			
		||||
        self.email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def smtp(self):
 | 
			
		||||
    def smtp(self) -> Type[Union[smtplib.SMTP_SSL, smtplib.SMTP]]:
 | 
			
		||||
        return smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP
 | 
			
		||||
 | 
			
		||||
    def send(self, template, lang, recipient, data):
 | 
			
		||||
    def send(
 | 
			
		||||
        self, template: str, lang: str, recipient: str, data: Dict
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        if not self.email_template:
 | 
			
		||||
            raise Exception('No email template defined.')
 | 
			
		||||
        message = self.email_template.get_message(
 | 
			
		||||
            template, lang, self.sender_email, recipient, data
 | 
			
		||||
        )
 | 
			
		||||
@@ -95,8 +107,10 @@ class Email:
 | 
			
		||||
            context = ssl.create_default_context()
 | 
			
		||||
        if self.use_ssl:
 | 
			
		||||
            connection_params.update({'context': context})
 | 
			
		||||
        with self.smtp(self.host, self.port, **connection_params) as smtp:
 | 
			
		||||
            smtp.login(self.username, self.password)
 | 
			
		||||
        with self.smtp(
 | 
			
		||||
            self.host, self.port, **connection_params  # type: ignore
 | 
			
		||||
        ) as smtp:
 | 
			
		||||
            smtp.login(self.username, self.password)  # type: ignore
 | 
			
		||||
            if self.use_tls:
 | 
			
		||||
                smtp.starttls(context=context)
 | 
			
		||||
            smtp.sendmail(self.sender_email, recipient, message.as_string())
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
from typing import Dict
 | 
			
		||||
 | 
			
		||||
from urllib3.util import parse_url
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -5,7 +7,7 @@ class InvalidEmailUrlScheme(Exception):
 | 
			
		||||
    ...
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_email_url(email_url):
 | 
			
		||||
def parse_email_url(email_url: str) -> Dict:
 | 
			
		||||
    parsed_url = parse_url(email_url)
 | 
			
		||||
    if parsed_url.scheme != 'smtp':
 | 
			
		||||
        raise InvalidEmailUrlScheme()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,22 @@
 | 
			
		||||
from json import dumps
 | 
			
		||||
from typing import Dict, List, Optional, Union
 | 
			
		||||
 | 
			
		||||
from fittrackee import appLog
 | 
			
		||||
from flask import Response
 | 
			
		||||
from flask_sqlalchemy import SQLAlchemy
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_empty_data_for_datatype(data_type):
 | 
			
		||||
def get_empty_data_for_datatype(data_type: str) -> Union[str, List]:
 | 
			
		||||
    return '' if data_type in ['gpx', 'chart_data'] else []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HttpResponse(Response):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        response=None,
 | 
			
		||||
        status_code=None,
 | 
			
		||||
        content_type=None,
 | 
			
		||||
    ):
 | 
			
		||||
        response: Optional[Union[str, Dict]] = None,
 | 
			
		||||
        status_code: Optional[int] = None,
 | 
			
		||||
        content_type: Optional[str] = None,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        if isinstance(response, dict):
 | 
			
		||||
            response = dumps(response)
 | 
			
		||||
            content_type = (
 | 
			
		||||
@@ -28,7 +30,9 @@ class HttpResponse(Response):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GenericErrorResponse(HttpResponse):
 | 
			
		||||
    def __init__(self, status_code, message, status=None):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, status_code: int, message: str, status: Optional[str] = None
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        response = {
 | 
			
		||||
            'status': 'error' if status is None else status,
 | 
			
		||||
            'message': message,
 | 
			
		||||
@@ -40,13 +44,15 @@ class GenericErrorResponse(HttpResponse):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidPayloadErrorResponse(GenericErrorResponse):
 | 
			
		||||
    def __init__(self, message=None, status=None):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, message: Optional[str] = None, status: Optional[str] = None
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        message = 'Invalid payload.' if message is None else message
 | 
			
		||||
        super().__init__(status_code=400, message=message, status=status)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DataInvalidPayloadErrorResponse(HttpResponse):
 | 
			
		||||
    def __init__(self, data_type, status=None):
 | 
			
		||||
    def __init__(self, data_type: str, status: Optional[str] = None) -> None:
 | 
			
		||||
        response = {
 | 
			
		||||
            'status': 'error' if status is None else status,
 | 
			
		||||
            'data': {data_type: get_empty_data_for_datatype(data_type)},
 | 
			
		||||
@@ -55,7 +61,7 @@ class DataInvalidPayloadErrorResponse(HttpResponse):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UnauthorizedErrorResponse(GenericErrorResponse):
 | 
			
		||||
    def __init__(self, message=None):
 | 
			
		||||
    def __init__(self, message: Optional[str] = None) -> None:
 | 
			
		||||
        message = (
 | 
			
		||||
            'Invalid token. Please request a new token.'
 | 
			
		||||
            if message is None
 | 
			
		||||
@@ -65,7 +71,7 @@ class UnauthorizedErrorResponse(GenericErrorResponse):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ForbiddenErrorResponse(GenericErrorResponse):
 | 
			
		||||
    def __init__(self, message=None):
 | 
			
		||||
    def __init__(self, message: Optional[str] = None) -> None:
 | 
			
		||||
        message = (
 | 
			
		||||
            'You do not have permissions.' if message is None else message
 | 
			
		||||
        )
 | 
			
		||||
@@ -73,17 +79,17 @@ class ForbiddenErrorResponse(GenericErrorResponse):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NotFoundErrorResponse(GenericErrorResponse):
 | 
			
		||||
    def __init__(self, message):
 | 
			
		||||
    def __init__(self, message: str) -> None:
 | 
			
		||||
        super().__init__(status_code=404, message=message, status='not found')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserNotFoundErrorResponse(NotFoundErrorResponse):
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        super().__init__(message='User does not exist.')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DataNotFoundErrorResponse(HttpResponse):
 | 
			
		||||
    def __init__(self, data_type, message=None):
 | 
			
		||||
    def __init__(self, data_type: str, message: Optional[str] = None) -> None:
 | 
			
		||||
        response = {
 | 
			
		||||
            'status': 'not found',
 | 
			
		||||
            'data': {data_type: get_empty_data_for_datatype(data_type)},
 | 
			
		||||
@@ -94,12 +100,14 @@ class DataNotFoundErrorResponse(HttpResponse):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PayloadTooLargeErrorResponse(GenericErrorResponse):
 | 
			
		||||
    def __init__(self, message):
 | 
			
		||||
    def __init__(self, message: str) -> None:
 | 
			
		||||
        super().__init__(status_code=413, message=message, status='fail')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InternalServerErrorResponse(GenericErrorResponse):
 | 
			
		||||
    def __init__(self, message=None, status=None):
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, message: Optional[str] = None, status: Optional[str] = None
 | 
			
		||||
    ):
 | 
			
		||||
        message = (
 | 
			
		||||
            'Error. Please try again or contact the administrator.'
 | 
			
		||||
            if message is None
 | 
			
		||||
@@ -109,8 +117,11 @@ class InternalServerErrorResponse(GenericErrorResponse):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def handle_error_and_return_response(
 | 
			
		||||
    error, message=None, status=None, db=None
 | 
			
		||||
):
 | 
			
		||||
    error: Exception,
 | 
			
		||||
    message: Optional[str] = None,
 | 
			
		||||
    status: Optional[str] = None,
 | 
			
		||||
    db: Optional[SQLAlchemy] = None,
 | 
			
		||||
) -> HttpResponse:
 | 
			
		||||
    if db is not None:
 | 
			
		||||
        db.session.rollback()
 | 
			
		||||
    appLog.error(error)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
from typing import Dict
 | 
			
		||||
 | 
			
		||||
from fittrackee import dramatiq, email_service
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dramatiq.actor(queue_name='fittrackee_emails')
 | 
			
		||||
def reset_password_email(user, email_data):
 | 
			
		||||
def reset_password_email(user: Dict, email_data: Dict) -> None:
 | 
			
		||||
    email_service.send(
 | 
			
		||||
        template='password_reset_request',
 | 
			
		||||
        lang=user['language'],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,25 @@
 | 
			
		||||
import json
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from fittrackee.activities.models import Activity, Sport
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
from .utils import get_random_short_id
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestGetActivities:
 | 
			
		||||
    def test_it_gets_all_activities_for_authenticated_user(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_cycling_user_1,
 | 
			
		||||
        activity_cycling_user_2,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
        activity_cycling_user_2: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -57,14 +61,14 @@ class TestGetActivities:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_no_activities_for_authenticated_user_with_no_activities(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_cycling_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -85,7 +89,9 @@ class TestGetActivities:
 | 
			
		||||
        assert 'success' in data['status']
 | 
			
		||||
        assert len(data['data']['activities']) == 0
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_401_if_user_is_not_authenticated(self, app):
 | 
			
		||||
    def test_it_returns_401_if_user_is_not_authenticated(
 | 
			
		||||
        self, app: Flask
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.get('/api/activities')
 | 
			
		||||
@@ -98,8 +104,12 @@ class TestGetActivities:
 | 
			
		||||
 | 
			
		||||
class TestGetActivitiesWithPagination:
 | 
			
		||||
    def test_it_gets_activities_with_default_pagination(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -133,8 +143,12 @@ class TestGetActivitiesWithPagination:
 | 
			
		||||
        assert '0:17:04' == data['data']['activities'][4]['duration']
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_first_page(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -168,8 +182,12 @@ class TestGetActivitiesWithPagination:
 | 
			
		||||
        assert '0:17:04' == data['data']['activities'][4]['duration']
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_second_page(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -203,8 +221,12 @@ class TestGetActivitiesWithPagination:
 | 
			
		||||
        assert '0:17:04' == data['data']['activities'][1]['duration']
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_empty_third_page(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -226,8 +248,12 @@ class TestGetActivitiesWithPagination:
 | 
			
		||||
        assert len(data['data']['activities']) == 0
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_on_invalid_page_value(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -252,8 +278,12 @@ class TestGetActivitiesWithPagination:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_5_activities_per_page(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -283,8 +313,12 @@ class TestGetActivitiesWithPagination:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_3_activities_per_page(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -316,8 +350,12 @@ class TestGetActivitiesWithPagination:
 | 
			
		||||
 | 
			
		||||
class TestGetActivitiesWithFilters:
 | 
			
		||||
    def test_it_gets_activities_with_date_filter(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -351,8 +389,12 @@ class TestGetActivitiesWithFilters:
 | 
			
		||||
        assert '0:16:40' == data['data']['activities'][1]['duration']
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_no_activities_with_date_filter(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -374,8 +416,12 @@ class TestGetActivitiesWithFilters:
 | 
			
		||||
        assert len(data['data']['activities']) == 0
 | 
			
		||||
 | 
			
		||||
    def test_if_gets_activities_with_date_filter_from(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -406,8 +452,12 @@ class TestGetActivitiesWithFilters:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_activities_with_date_filter_to(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -437,8 +487,12 @@ class TestGetActivitiesWithFilters:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_activities_with_ascending_order(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -468,8 +522,12 @@ class TestGetActivitiesWithFilters:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_activities_with_distance_filter(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -499,8 +557,12 @@ class TestGetActivitiesWithFilters:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_activities_with_duration_filter(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -526,8 +588,12 @@ class TestGetActivitiesWithFilters:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_activities_with_average_speed_filter(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -554,13 +620,13 @@ class TestGetActivitiesWithFilters:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_activities_with_max_speed_filter(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_cycling_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        activity_cycling_user_1.max_speed = 25
 | 
			
		||||
        activity_running_user_1.max_speed = 11
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
@@ -589,13 +655,13 @@ class TestGetActivitiesWithFilters:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_activities_with_sport_filter(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -623,8 +689,12 @@ class TestGetActivitiesWithFilters:
 | 
			
		||||
 | 
			
		||||
class TestGetActivitiesWithFiltersAndPagination:
 | 
			
		||||
    def test_it_gets_page_2_with_date_filter(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -654,8 +724,12 @@ class TestGetActivitiesWithFiltersAndPagination:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_get_page_2_with_date_filter_and_ascending_order(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, seven_activities_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -687,8 +761,12 @@ class TestGetActivitiesWithFiltersAndPagination:
 | 
			
		||||
 | 
			
		||||
class TestGetActivity:
 | 
			
		||||
    def test_it_gets_an_activity(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -719,8 +797,13 @@ class TestGetActivity:
 | 
			
		||||
        assert '1:00:00' == data['data']['activities'][0]['duration']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_403_if_activity_belongs_to_a_different_user(
 | 
			
		||||
        self, app, user_1, user_2, sport_1_cycling, activity_cycling_user_2
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_2: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -741,7 +824,9 @@ class TestGetActivity:
 | 
			
		||||
        assert 'error' in data['status']
 | 
			
		||||
        assert 'You do not have permissions.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_404_if_activity_does_not_exist(self, app, user_1):
 | 
			
		||||
    def test_it_returns_404_if_activity_does_not_exist(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -763,8 +848,8 @@ class TestGetActivity:
 | 
			
		||||
        assert len(data['data']['activities']) == 0
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_404_on_getting_gpx_if_activity_does_not_exist(
 | 
			
		||||
        self, app, user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        random_short_id = get_random_short_id()
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -788,8 +873,8 @@ class TestGetActivity:
 | 
			
		||||
        assert data['data']['gpx'] == ''
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_404_on_getting_chart_data_if_activity_does_not_exist(
 | 
			
		||||
        self, app, user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        random_short_id = get_random_short_id()
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -813,8 +898,12 @@ class TestGetActivity:
 | 
			
		||||
        assert data['data']['chart_data'] == ''
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_404_on_getting_gpx_if_activity_have_no_gpx(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        activity_short_id = activity_cycling_user_1.short_id
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -840,8 +929,12 @@ class TestGetActivity:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_404_if_activity_have_no_chart_data(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        activity_short_id = activity_cycling_user_1.short_id
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -867,8 +960,12 @@ class TestGetActivity:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_500_on_getting_gpx_if_an_activity_has_invalid_gpx_pathname(  # noqa
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        activity_cycling_user_1.gpx = "some path"
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -895,8 +992,12 @@ class TestGetActivity:
 | 
			
		||||
        assert 'data' not in data
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_500_on_getting_chart_data_if_an_activity_has_invalid_gpx_pathname(  # noqa
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        activity_cycling_user_1.gpx = 'some path'
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -922,7 +1023,9 @@ class TestGetActivity:
 | 
			
		||||
        )
 | 
			
		||||
        assert 'data' not in data
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_404_if_activity_has_no_map(self, app, user_1):
 | 
			
		||||
    def test_it_returns_404_if_activity_has_no_map(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,15 @@ import json
 | 
			
		||||
import os
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from io import BytesIO
 | 
			
		||||
from typing import Dict
 | 
			
		||||
 | 
			
		||||
from fittrackee.activities.models import Activity
 | 
			
		||||
from fittrackee.activities.models import Activity, Sport
 | 
			
		||||
from fittrackee.activities.utils_id import decode_short_id
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def assert_activity_data_with_gpx(data):
 | 
			
		||||
def assert_activity_data_with_gpx(data: Dict) -> None:
 | 
			
		||||
    assert 'creation_date' in data['data']['activities'][0]
 | 
			
		||||
    assert (
 | 
			
		||||
        'Tue, 13 Mar 2018 12:44:45 GMT'
 | 
			
		||||
@@ -70,7 +73,7 @@ def assert_activity_data_with_gpx(data):
 | 
			
		||||
    assert records[3]['value'] == 4.61
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def assert_activity_data_with_gpx_segments(data):
 | 
			
		||||
def assert_activity_data_with_gpx_segments(data: Dict) -> None:
 | 
			
		||||
    assert 'creation_date' in data['data']['activities'][0]
 | 
			
		||||
    assert (
 | 
			
		||||
        'Tue, 13 Mar 2018 12:44:45 GMT'
 | 
			
		||||
@@ -144,7 +147,7 @@ def assert_activity_data_with_gpx_segments(data):
 | 
			
		||||
    assert records[2]['value'] == 4.59
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def assert_activity_data_wo_gpx(data):
 | 
			
		||||
def assert_activity_data_wo_gpx(data: Dict) -> None:
 | 
			
		||||
    assert 'creation_date' in data['data']['activities'][0]
 | 
			
		||||
    assert (
 | 
			
		||||
        data['data']['activities'][0]['activity_date']
 | 
			
		||||
@@ -200,8 +203,8 @@ def assert_activity_data_wo_gpx(data):
 | 
			
		||||
 | 
			
		||||
class TestPostActivityWithGpx:
 | 
			
		||||
    def test_it_adds_an_activity_with_gpx_file(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -230,8 +233,12 @@ class TestPostActivityWithGpx:
 | 
			
		||||
        assert_activity_data_with_gpx(data)
 | 
			
		||||
 | 
			
		||||
    def test_it_adds_an_activity_with_gpx_without_name(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file_wo_name
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        gpx_file_wo_name: str,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -263,8 +270,12 @@ class TestPostActivityWithGpx:
 | 
			
		||||
        assert_activity_data_with_gpx(data)
 | 
			
		||||
 | 
			
		||||
    def test_it_adds_an_activity_with_gpx_without_name_timezone(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file_wo_name
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        gpx_file_wo_name: str,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        user_1.timezone = 'Europe/Paris'
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -297,8 +308,8 @@ class TestPostActivityWithGpx:
 | 
			
		||||
        assert_activity_data_with_gpx(data)
 | 
			
		||||
 | 
			
		||||
    def test_it_adds_get_an_activity_with_gpx_notes(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -327,8 +338,12 @@ class TestPostActivityWithGpx:
 | 
			
		||||
        assert 'test activity' == data['data']['activities'][0]['notes']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_500_if_gpx_file_has_not_tracks(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file_wo_track
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        gpx_file_wo_track: str,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -352,12 +367,16 @@ class TestPostActivityWithGpx:
 | 
			
		||||
        data = json.loads(response.data.decode())
 | 
			
		||||
        assert response.status_code == 500
 | 
			
		||||
        assert 'error' in data['status']
 | 
			
		||||
        assert 'Error during gpx file parsing.' in data['message']
 | 
			
		||||
        assert 'Error during gpx processing.' in data['message']
 | 
			
		||||
        assert 'data' not in data
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_500_if_gpx_has_invalid_xml(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file_invalid_xml
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        gpx_file_invalid_xml: str,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -388,8 +407,8 @@ class TestPostActivityWithGpx:
 | 
			
		||||
        assert 'data' not in data
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_400_if_activity_gpx_has_invalid_extension(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -416,8 +435,8 @@ class TestPostActivityWithGpx:
 | 
			
		||||
        assert data['message'] == 'File extension not allowed.'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_400_if_sport_id_is_not_provided(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -443,8 +462,8 @@ class TestPostActivityWithGpx:
 | 
			
		||||
        assert data['message'] == 'Invalid payload.'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_500_if_sport_id_does_not_exists(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -471,8 +490,8 @@ class TestPostActivityWithGpx:
 | 
			
		||||
        assert data['message'] == 'Sport id: 2 does not exist'
 | 
			
		||||
 | 
			
		||||
    def test_returns_400_if_no_gpx_file_is_provided(
 | 
			
		||||
        self, app, user_1, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -498,8 +517,8 @@ class TestPostActivityWithGpx:
 | 
			
		||||
 | 
			
		||||
class TestPostActivityWithoutGpx:
 | 
			
		||||
    def test_it_adds_an_activity_without_gpx(
 | 
			
		||||
        self, app, user_1, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -531,8 +550,8 @@ class TestPostActivityWithoutGpx:
 | 
			
		||||
        assert_activity_data_wo_gpx(data)
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_400_if_activity_date_is_missing(
 | 
			
		||||
        self, app, user_1, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -556,8 +575,8 @@ class TestPostActivityWithoutGpx:
 | 
			
		||||
        assert 'Invalid payload.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_500_if_activity_format_is_invalid(
 | 
			
		||||
        self, app, user_1, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -588,8 +607,12 @@ class TestPostActivityWithoutGpx:
 | 
			
		||||
        assert 'Error during activity save.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_adds_activity_with_zero_value(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, sport_2_running
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -645,8 +668,8 @@ class TestPostActivityWithoutGpx:
 | 
			
		||||
 | 
			
		||||
class TestPostActivityWithZipArchive:
 | 
			
		||||
    def test_it_adds_activities_with_zip_archive(
 | 
			
		||||
        self, app, user_1, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        file_path = os.path.join(app.root_path, 'tests/files/gpx_test.zip')
 | 
			
		||||
        # 'gpx_test.zip' contains 3 gpx files (same data) and 1 non-gpx file
 | 
			
		||||
        with open(file_path, 'rb') as zip_file:
 | 
			
		||||
@@ -679,8 +702,8 @@ class TestPostActivityWithZipArchive:
 | 
			
		||||
            assert_activity_data_with_gpx(data)
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_400_if_folder_is_present_in_zpi_archive(
 | 
			
		||||
        self, app, user_1, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        file_path = os.path.join(
 | 
			
		||||
            app.root_path, 'tests/files/gpx_test_folder.zip'
 | 
			
		||||
        )
 | 
			
		||||
@@ -715,8 +738,8 @@ class TestPostActivityWithZipArchive:
 | 
			
		||||
            assert len(data['data']['activities']) == 0
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_500_if_one_fle_in_zip_archive_is_invalid(
 | 
			
		||||
        self, app, user_1, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        file_path = os.path.join(
 | 
			
		||||
            app.root_path, 'tests/files/gpx_test_incorrect.zip'
 | 
			
		||||
        )
 | 
			
		||||
@@ -747,13 +770,15 @@ class TestPostActivityWithZipArchive:
 | 
			
		||||
            data = json.loads(response.data.decode())
 | 
			
		||||
            assert response.status_code == 500
 | 
			
		||||
            assert 'error' in data['status']
 | 
			
		||||
            assert 'Error during gpx file parsing.' in data['message']
 | 
			
		||||
            assert 'Error during gpx processing.' in data['message']
 | 
			
		||||
            assert 'data' not in data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestPostAndGetActivityWithGpx:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def activity_assertion(app, gpx_file, with_segments):
 | 
			
		||||
    def activity_assertion(
 | 
			
		||||
        app: Flask, gpx_file: str, with_segments: bool
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -849,18 +874,22 @@ class TestPostAndGetActivityWithGpx:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_an_activity_created_with_gpx(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        return self.activity_assertion(app, gpx_file, False)
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_an_activity_created_with_gpx_with_segments(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file_with_segments
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        gpx_file_with_segments: str,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        return self.activity_assertion(app, gpx_file_with_segments, True)
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_chart_data_for_an_activity_created_with_gpx(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -897,8 +926,8 @@ class TestPostAndGetActivityWithGpx:
 | 
			
		||||
        assert data['data']['chart_data'] != ''
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_segment_chart_data_for_an_activity_created_with_gpx(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -935,8 +964,13 @@ class TestPostAndGetActivityWithGpx:
 | 
			
		||||
        assert data['data']['chart_data'] != ''
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_403_on_getting_chart_data_if_activity_belongs_to_another_user(  # noqa
 | 
			
		||||
        self, app, user_1, user_2, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        gpx_file: str,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -977,8 +1011,8 @@ class TestPostAndGetActivityWithGpx:
 | 
			
		||||
        assert data['message'] == 'You do not have permissions.'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_500_on_invalid_segment_id(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1015,8 +1049,8 @@ class TestPostAndGetActivityWithGpx:
 | 
			
		||||
        assert 'data' not in data
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_404_if_segment_id_does_not_exist(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1055,8 +1089,8 @@ class TestPostAndGetActivityWithGpx:
 | 
			
		||||
 | 
			
		||||
class TestPostAndGetActivityWithoutGpx:
 | 
			
		||||
    def test_it_add_and_gets_an_activity_wo_gpx(
 | 
			
		||||
        self, app, user_1, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1097,8 +1131,8 @@ class TestPostAndGetActivityWithoutGpx:
 | 
			
		||||
        assert_activity_data_wo_gpx(data)
 | 
			
		||||
 | 
			
		||||
    def test_it_adds_and_gets_an_activity_wo_gpx_notes(
 | 
			
		||||
        self, app, user_1, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1142,8 +1176,8 @@ class TestPostAndGetActivityWithoutGpx:
 | 
			
		||||
 | 
			
		||||
class TestPostAndGetActivityUsingTimezones:
 | 
			
		||||
    def test_it_add_and_gets_an_activity_wo_gpx_with_timezone(
 | 
			
		||||
        self, app, user_1, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        user_1.timezone = 'Europe/Paris'
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -1192,8 +1226,8 @@ class TestPostAndGetActivityUsingTimezones:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_adds_and_gets_activities_date_filter_with_timezone_new_york(
 | 
			
		||||
        self, app, user_1_full, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1_full: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1239,8 +1273,12 @@ class TestPostAndGetActivityUsingTimezones:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_adds_and_gets_activities_date_filter_with_timezone_paris(
 | 
			
		||||
        self, app, user_1_paris, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1_paris: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,15 @@
 | 
			
		||||
import json
 | 
			
		||||
from typing import Dict
 | 
			
		||||
 | 
			
		||||
from fittrackee.activities.models import Activity
 | 
			
		||||
from fittrackee.activities.models import Activity, Sport
 | 
			
		||||
from fittrackee.activities.utils_id import decode_short_id
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
from .utils import get_random_short_id, post_an_activity
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def assert_activity_data_with_gpx(data, sport_id):
 | 
			
		||||
def assert_activity_data_with_gpx(data: Dict, sport_id: int) -> None:
 | 
			
		||||
    assert 'creation_date' in data['data']['activities'][0]
 | 
			
		||||
    assert (
 | 
			
		||||
        'Tue, 13 Mar 2018 12:44:45 GMT'
 | 
			
		||||
@@ -51,8 +54,13 @@ def assert_activity_data_with_gpx(data, sport_id):
 | 
			
		||||
 | 
			
		||||
class TestEditActivityWithGpx:
 | 
			
		||||
    def test_it_updates_title_for_an_activity_with_gpx(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, sport_2_running, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        gpx_file: str,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        token, activity_short_id = post_an_activity(app, gpx_file)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
@@ -72,8 +80,13 @@ class TestEditActivityWithGpx:
 | 
			
		||||
        assert_activity_data_with_gpx(data, sport_2_running.id)
 | 
			
		||||
 | 
			
		||||
    def test_it_adds_notes_for_an_activity_with_gpx(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, sport_2_running, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        gpx_file: str,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        token, activity_short_id = post_an_activity(app, gpx_file)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
@@ -92,8 +105,14 @@ class TestEditActivityWithGpx:
 | 
			
		||||
        assert data['data']['activities'][0]['notes'] == 'test notes'
 | 
			
		||||
 | 
			
		||||
    def test_it_raises_403_when_editing_an_activity_from_different_user(
 | 
			
		||||
        self, app, user_1, user_2, sport_1_cycling, sport_2_running, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        gpx_file: str,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        _, activity_short_id = post_an_activity(app, gpx_file)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -118,8 +137,13 @@ class TestEditActivityWithGpx:
 | 
			
		||||
        assert 'You do not have permissions.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_updates_sport(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, sport_2_running, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        gpx_file: str,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        token, activity_short_id = post_an_activity(app, gpx_file)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
@@ -139,8 +163,8 @@ class TestEditActivityWithGpx:
 | 
			
		||||
        assert_activity_data_with_gpx(data, sport_2_running.id)
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_400_if_payload_is_empty(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        token, activity_short_id = post_an_activity(app, gpx_file)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
@@ -157,8 +181,8 @@ class TestEditActivityWithGpx:
 | 
			
		||||
        assert 'Invalid payload.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_raises_500_if_sport_does_not_exists(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        token, activity_short_id = post_an_activity(app, gpx_file)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
@@ -181,12 +205,12 @@ class TestEditActivityWithGpx:
 | 
			
		||||
class TestEditActivityWithoutGpx:
 | 
			
		||||
    def test_it_updates_an_activity_wo_gpx(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_cycling_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        activity_short_id = activity_cycling_user_1.short_id
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -266,8 +290,12 @@ class TestEditActivityWithoutGpx:
 | 
			
		||||
        assert records[3]['value'] == 8.0
 | 
			
		||||
 | 
			
		||||
    def test_it_adds_notes_to_an_activity_wo_gpx(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        activity_short_id = activity_cycling_user_1.short_id
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -338,8 +366,13 @@ class TestEditActivityWithoutGpx:
 | 
			
		||||
        assert records[3]['value'] == 10.0
 | 
			
		||||
 | 
			
		||||
    def test_returns_403_when_editing_an_activity_wo_gpx_from_different_user(
 | 
			
		||||
        self, app, user_1, user_2, sport_1_cycling, activity_cycling_user_2
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_2: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -372,12 +405,12 @@ class TestEditActivityWithoutGpx:
 | 
			
		||||
 | 
			
		||||
    def test_it_updates_an_activity_wo_gpx_with_timezone(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1_paris,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_cycling_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1_paris: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        activity_short_id = activity_cycling_user_1.short_id
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -453,12 +486,12 @@ class TestEditActivityWithoutGpx:
 | 
			
		||||
 | 
			
		||||
    def test_it_updates_only_sport_and_distance_an_activity_wo_gpx(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_cycling_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        activity_short_id = activity_cycling_user_1.short_id
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -525,8 +558,12 @@ class TestEditActivityWithoutGpx:
 | 
			
		||||
        assert records[3]['value'] == 20.0
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_400_if_payload_is_empty(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -550,8 +587,12 @@ class TestEditActivityWithoutGpx:
 | 
			
		||||
        assert 'Invalid payload.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_500_if_date_format_is_invalid(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -585,8 +626,8 @@ class TestEditActivityWithoutGpx:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_404_if_edited_activity_does_not_exists(
 | 
			
		||||
        self, app, user_1, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -618,8 +659,13 @@ class TestEditActivityWithoutGpx:
 | 
			
		||||
 | 
			
		||||
class TestRefreshActivityWithGpx:
 | 
			
		||||
    def test_refresh_an_activity_with_gpx(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, sport_2_running, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        gpx_file: str,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        token, activity_short_id = post_an_activity(app, gpx_file)
 | 
			
		||||
        activity_uuid = decode_short_id(activity_short_id)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,23 @@
 | 
			
		||||
import json
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from fittrackee.activities.models import Activity
 | 
			
		||||
from fittrackee.activities.models import Activity, Sport
 | 
			
		||||
from fittrackee.activities.utils import get_absolute_file_path
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
from .utils import get_random_short_id, post_an_activity
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_gpx_filepath(activity_id):
 | 
			
		||||
def get_gpx_filepath(activity_id: int) -> str:
 | 
			
		||||
    activity = Activity.query.filter_by(id=activity_id).first()
 | 
			
		||||
    return activity.gpx
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestDeleteActivityWithGpx:
 | 
			
		||||
    def test_it_deletes_an_activity_with_gpx(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        token, activity_short_id = post_an_activity(app, gpx_file)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
@@ -27,8 +29,13 @@ class TestDeleteActivityWithGpx:
 | 
			
		||||
        assert response.status_code == 204
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_403_when_deleting_an_activity_from_different_user(
 | 
			
		||||
        self, app, user_1, user_2, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        gpx_file: str,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        _, activity_short_id = post_an_activity(app, gpx_file)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -51,7 +58,9 @@ class TestDeleteActivityWithGpx:
 | 
			
		||||
        assert 'error' in data['status']
 | 
			
		||||
        assert 'You do not have permissions.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_404_if_activity_does_not_exist(self, app, user_1):
 | 
			
		||||
    def test_it_returns_404_if_activity_does_not_exist(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -70,8 +79,8 @@ class TestDeleteActivityWithGpx:
 | 
			
		||||
        assert 'not found' in data['status']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_500_when_deleting_an_activity_with_gpx_invalid_file(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        token, activity_short_id = post_an_activity(app, gpx_file)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        gpx_filepath = get_gpx_filepath(1)
 | 
			
		||||
@@ -95,8 +104,12 @@ class TestDeleteActivityWithGpx:
 | 
			
		||||
 | 
			
		||||
class TestDeleteActivityWithoutGpx:
 | 
			
		||||
    def test_it_deletes_an_activity_wo_gpx(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -113,8 +126,13 @@ class TestDeleteActivityWithoutGpx:
 | 
			
		||||
        assert response.status_code == 204
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_403_when_deleting_an_activity_from_different_user(
 | 
			
		||||
        self, app, user_1, user_2, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,19 @@
 | 
			
		||||
from uuid import UUID
 | 
			
		||||
 | 
			
		||||
from fittrackee.activities.models import Activity, Sport
 | 
			
		||||
from fittrackee.activities.utils_id import decode_short_id
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestActivityModel:
 | 
			
		||||
    def test_activity_model(
 | 
			
		||||
        self, app, sport_1_cycling, user_1, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        activity_cycling_user_1.title = 'Test'
 | 
			
		||||
 | 
			
		||||
        assert 1 == activity_cycling_user_1.id
 | 
			
		||||
@@ -55,12 +62,12 @@ class TestActivityModel:
 | 
			
		||||
 | 
			
		||||
    def test_activity_segment_model(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        user_1,
 | 
			
		||||
        activity_cycling_user_1,
 | 
			
		||||
        activity_cycling_user_1_segment,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
        activity_cycling_user_1_segment: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        assert (
 | 
			
		||||
            f'<Segment \'{activity_cycling_user_1_segment.segment_id}\' '
 | 
			
		||||
            f'for activity \'{activity_cycling_user_1.short_id}\'>'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,21 @@
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from fittrackee.activities.models import Activity, Sport
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestGetRecords:
 | 
			
		||||
    def test_it_gets_records_for_authenticated_user(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_cycling_user_1,
 | 
			
		||||
        activity_cycling_user_2,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
        activity_cycling_user_2: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -85,13 +89,13 @@ class TestGetRecords:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_no_records_if_user_has_no_activity(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_cycling_user_2,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_cycling_user_2: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -112,8 +116,12 @@ class TestGetRecords:
 | 
			
		||||
        assert len(data['data']['records']) == 0
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_no_records_if_activity_has_zero_value(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, sport_2_running
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -151,8 +159,8 @@ class TestGetRecords:
 | 
			
		||||
        assert len(data['data']['records']) == 0
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_updated_records_after_activities_post_and_patch(
 | 
			
		||||
        self, app, user_1, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -696,8 +704,12 @@ class TestGetRecords:
 | 
			
		||||
        assert len(data['data']['records']) == 0
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_updated_records_after_sport_change(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, sport_2_running
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,18 @@
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
from fittrackee.activities.models import Record
 | 
			
		||||
from fittrackee.activities.models import Activity, Record, Sport
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestRecordModel:
 | 
			
		||||
    def test_record_model(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        record_ld = Record.query.filter_by(
 | 
			
		||||
            user_id=activity_cycling_user_1.user_id,
 | 
			
		||||
            sport_id=activity_cycling_user_1.sport_id,
 | 
			
		||||
@@ -29,8 +35,12 @@ class TestRecordModel:
 | 
			
		||||
        assert 'value' in record_serialize
 | 
			
		||||
 | 
			
		||||
    def test_record_model_with_none_value(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        record_ld = Record.query.filter_by(
 | 
			
		||||
            user_id=activity_cycling_user_1.user_id,
 | 
			
		||||
            sport_id=activity_cycling_user_1.sport_id,
 | 
			
		||||
@@ -49,8 +59,12 @@ class TestRecordModel:
 | 
			
		||||
        assert record_serialize['value'] is None
 | 
			
		||||
 | 
			
		||||
    def test_average_speed_records(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        record_as = Record.query.filter_by(
 | 
			
		||||
            user_id=activity_cycling_user_1.user_id,
 | 
			
		||||
            sport_id=activity_cycling_user_1.sport_id,
 | 
			
		||||
@@ -66,8 +80,12 @@ class TestRecordModel:
 | 
			
		||||
        assert isinstance(record_serialize.get('value'), float)
 | 
			
		||||
 | 
			
		||||
    def test_add_farest_distance_records(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        record_fd = Record.query.filter_by(
 | 
			
		||||
            user_id=activity_cycling_user_1.user_id,
 | 
			
		||||
            sport_id=activity_cycling_user_1.sport_id,
 | 
			
		||||
@@ -83,8 +101,12 @@ class TestRecordModel:
 | 
			
		||||
        assert isinstance(record_serialize.get('value'), float)
 | 
			
		||||
 | 
			
		||||
    def test_add_longest_duration_records(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        record_ld = Record.query.filter_by(
 | 
			
		||||
            user_id=activity_cycling_user_1.user_id,
 | 
			
		||||
            sport_id=activity_cycling_user_1.sport_id,
 | 
			
		||||
@@ -100,8 +122,12 @@ class TestRecordModel:
 | 
			
		||||
        assert isinstance(record_serialize.get('value'), str)
 | 
			
		||||
 | 
			
		||||
    def test_add_longest_duration_records_with_zero(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        record_ld = Record.query.filter_by(
 | 
			
		||||
            user_id=activity_cycling_user_1.user_id,
 | 
			
		||||
            sport_id=activity_cycling_user_1.sport_id,
 | 
			
		||||
@@ -118,8 +144,12 @@ class TestRecordModel:
 | 
			
		||||
        assert isinstance(record_serialize.get('value'), str)
 | 
			
		||||
 | 
			
		||||
    def test_max_speed_records_no_value(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        record_ms = Record.query.filter_by(
 | 
			
		||||
            user_id=activity_cycling_user_1.user_id,
 | 
			
		||||
            sport_id=activity_cycling_user_1.sport_id,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,9 @@
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from fittrackee.activities.models import Activity, Sport
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
expected_sport_1_cycling_result = {
 | 
			
		||||
    'id': 1,
 | 
			
		||||
    'label': 'Cycling',
 | 
			
		||||
@@ -32,8 +36,12 @@ expected_sport_1_cycling_inactive_admin_result['has_activities'] = False
 | 
			
		||||
 | 
			
		||||
class TestGetSports:
 | 
			
		||||
    def test_it_gets_all_sports(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, sport_2_running
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -57,8 +65,12 @@ class TestGetSports:
 | 
			
		||||
        assert data['data']['sports'][1] == expected_sport_2_running_result
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_all_sports_with_inactive_one(
 | 
			
		||||
        self, app, user_1, sport_1_cycling_inactive, sport_2_running
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling_inactive: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -85,8 +97,12 @@ class TestGetSports:
 | 
			
		||||
        assert data['data']['sports'][1] == expected_sport_2_running_result
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_all_sports_with_admin_rights(
 | 
			
		||||
        self, app, user_1_admin, sport_1_cycling_inactive, sport_2_running
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1_admin: User,
 | 
			
		||||
        sport_1_cycling_inactive: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -118,7 +134,9 @@ class TestGetSports:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestGetSport:
 | 
			
		||||
    def test_it_gets_a_sport(self, app, user_1, sport_1_cycling):
 | 
			
		||||
    def test_it_gets_a_sport(
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -140,7 +158,9 @@ class TestGetSport:
 | 
			
		||||
        assert len(data['data']['sports']) == 1
 | 
			
		||||
        assert data['data']['sports'][0] == expected_sport_1_cycling_result
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_404_if_sport_does_not_exist(self, app, user_1):
 | 
			
		||||
    def test_it_returns_404_if_sport_does_not_exist(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -162,8 +182,8 @@ class TestGetSport:
 | 
			
		||||
        assert len(data['data']['sports']) == 0
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_a_inactive_sport(
 | 
			
		||||
        self, app, user_1, sport_1_cycling_inactive
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling_inactive: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -189,8 +209,8 @@ class TestGetSport:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_get_an_inactive_sport_with_admin_rights(
 | 
			
		||||
        self, app, user_1_admin, sport_1_cycling_inactive
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1_admin: User, sport_1_cycling_inactive: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -219,7 +239,9 @@ class TestGetSport:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestUpdateSport:
 | 
			
		||||
    def test_it_disables_a_sport(self, app, user_1_admin, sport_1_cycling):
 | 
			
		||||
    def test_it_disables_a_sport(
 | 
			
		||||
        self, app: Flask, user_1_admin: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -246,7 +268,9 @@ class TestUpdateSport:
 | 
			
		||||
        assert data['data']['sports'][0]['is_active'] is False
 | 
			
		||||
        assert data['data']['sports'][0]['has_activities'] is False
 | 
			
		||||
 | 
			
		||||
    def test_it_enables_a_sport(self, app, user_1_admin, sport_1_cycling):
 | 
			
		||||
    def test_it_enables_a_sport(
 | 
			
		||||
        self, app: Flask, user_1_admin: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        sport_1_cycling.is_active = False
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -275,8 +299,12 @@ class TestUpdateSport:
 | 
			
		||||
        assert data['data']['sports'][0]['has_activities'] is False
 | 
			
		||||
 | 
			
		||||
    def test_it_disables_a_sport_with_activities(
 | 
			
		||||
        self, app, user_1_admin, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1_admin: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -304,8 +332,12 @@ class TestUpdateSport:
 | 
			
		||||
        assert data['data']['sports'][0]['has_activities'] is True
 | 
			
		||||
 | 
			
		||||
    def test_it_enables_a_sport_with_activities(
 | 
			
		||||
        self, app, user_1_admin, sport_1_cycling, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1_admin: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        sport_1_cycling.is_active = False
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -334,8 +366,8 @@ class TestUpdateSport:
 | 
			
		||||
        assert data['data']['sports'][0]['has_activities'] is True
 | 
			
		||||
 | 
			
		||||
    def test_returns_error_if_user_has_no_admin_rights(
 | 
			
		||||
        self, app, user_1, sport_1_cycling
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -358,7 +390,9 @@ class TestUpdateSport:
 | 
			
		||||
        assert 'error' in data['status']
 | 
			
		||||
        assert 'You do not have permissions.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_returns_error_if_payload_is_invalid(self, app, user_1_admin):
 | 
			
		||||
    def test_returns_error_if_payload_is_invalid(
 | 
			
		||||
        self, app: Flask, user_1_admin: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -383,7 +417,9 @@ class TestUpdateSport:
 | 
			
		||||
        assert 'error' in data['status']
 | 
			
		||||
        assert 'Invalid payload.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_sport_does_not_exist(self, app, user_1_admin):
 | 
			
		||||
    def test_it_returns_error_if_sport_does_not_exist(
 | 
			
		||||
        self, app: Flask, user_1_admin: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,15 @@
 | 
			
		||||
from typing import Dict, Optional
 | 
			
		||||
 | 
			
		||||
from fittrackee.activities.models import Activity, Sport
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestSportModel:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def assert_sport_model(sport, is_admin=False):
 | 
			
		||||
    def assert_sport_model(
 | 
			
		||||
        sport: Sport, is_admin: Optional[bool] = False
 | 
			
		||||
    ) -> Dict:
 | 
			
		||||
        assert 1 == sport.id
 | 
			
		||||
        assert 'Cycling' == sport.label
 | 
			
		||||
        assert '<Sport \'Cycling\'>' == str(sport)
 | 
			
		||||
@@ -11,18 +20,26 @@ class TestSportModel:
 | 
			
		||||
        assert serialized_sport['is_active'] is True
 | 
			
		||||
        return serialized_sport
 | 
			
		||||
 | 
			
		||||
    def test_sport_model(self, app, sport_1_cycling):
 | 
			
		||||
    def test_sport_model(self, app: Flask, sport_1_cycling: Sport) -> None:
 | 
			
		||||
        serialized_sport = self.assert_sport_model(sport_1_cycling)
 | 
			
		||||
        assert 'has_activities' not in serialized_sport
 | 
			
		||||
 | 
			
		||||
    def test_sport_model_with_activity(
 | 
			
		||||
        self, app, sport_1_cycling, user_1, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        serialized_sport = self.assert_sport_model(sport_1_cycling)
 | 
			
		||||
        assert 'has_activities' not in serialized_sport
 | 
			
		||||
 | 
			
		||||
    def test_sport_model_with_activity_as_admin(
 | 
			
		||||
        self, app, sport_1_cycling, user_1, activity_cycling_user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        serialized_sport = self.assert_sport_model(sport_1_cycling, True)
 | 
			
		||||
        assert serialized_sport['has_activities'] is True
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,14 @@
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from fittrackee.activities.models import Activity, Sport
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestGetStatsByTime:
 | 
			
		||||
    def test_it_gets_no_stats_when_user_has_no_activities(self, app, user_1):
 | 
			
		||||
    def test_it_gets_no_stats_when_user_has_no_activities(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -23,7 +29,9 @@ class TestGetStatsByTime:
 | 
			
		||||
        assert 'success' in data['status']
 | 
			
		||||
        assert data['data']['statistics'] == {}
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_when_user_does_not_exists(self, app, user_1):
 | 
			
		||||
    def test_it_returns_error_when_user_does_not_exists(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -46,13 +54,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_date_format_is_invalid(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -78,13 +86,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_period_is_invalid(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -107,13 +115,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_stats_by_time_all_activities(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -156,13 +164,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_stats_for_april_2018(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -198,13 +206,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_stats_for_april_2018_with_paris_timezone(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1_paris,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1_paris: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -241,13 +249,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_stats_by_year(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -290,13 +298,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_stats_by_year_for_april_2018(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -332,13 +340,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_stats_by_year_for_april_2018_with_paris_timezone(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1_paris,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1_paris: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -374,13 +382,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_stats_by_month(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -451,13 +459,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_stats_by_month_with_new_york_timezone(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1_full,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1_full: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -528,13 +536,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_stats_by_month_for_april_2018(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -570,13 +578,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_stats_by_week(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1_full,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1_full: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -647,13 +655,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_stats_by_week_for_week_13(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -689,13 +697,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_if_get_stats_by_week_starting_with_monday(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -766,13 +774,13 @@ class TestGetStatsByTime:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_stats_by_week_starting_with_monday_for_week_13(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -810,13 +818,13 @@ class TestGetStatsByTime:
 | 
			
		||||
class TestGetStatsBySport:
 | 
			
		||||
    def test_it_gets_stats_by_sport(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -850,13 +858,13 @@ class TestGetStatsBySport:
 | 
			
		||||
 | 
			
		||||
    def test_it_get_stats_for_sport_1(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -885,13 +893,13 @@ class TestGetStatsBySport:
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_errors_if_user_does_not_exist(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -914,13 +922,13 @@ class TestGetStatsBySport:
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_sport_does_not_exist(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -943,13 +951,13 @@ class TestGetStatsBySport:
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_sport_id_is_invalid(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        seven_activities_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        seven_activities_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -976,8 +984,8 @@ class TestGetStatsBySport:
 | 
			
		||||
 | 
			
		||||
class TestGetAllStats:
 | 
			
		||||
    def test_it_returns_all_stats_when_users_have_no_activities(
 | 
			
		||||
        self, app, user_1_admin, user_2
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1_admin: User, user_2: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1005,16 +1013,16 @@ class TestGetAllStats:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_app_all_stats_with_activities(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1_admin,
 | 
			
		||||
        user_2,
 | 
			
		||||
        user_3,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_cycling_user_1,
 | 
			
		||||
        activity_cycling_user_2,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1_admin: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        user_3: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
        activity_cycling_user_2: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1042,16 +1050,16 @@ class TestGetAllStats:
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_user_has_no_admin_rights(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        user_3,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_cycling_user_1,
 | 
			
		||||
        activity_cycling_user_2,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        user_3: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
        activity_cycling_user_2: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,17 @@
 | 
			
		||||
import json
 | 
			
		||||
from io import BytesIO
 | 
			
		||||
from typing import Tuple
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from fittrackee.activities.utils_id import encode_uuid
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_random_short_id():
 | 
			
		||||
def get_random_short_id() -> str:
 | 
			
		||||
    return encode_uuid(uuid4())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def post_an_activity(app, gpx_file):
 | 
			
		||||
def post_an_activity(app: Flask, gpx_file: str) -> Tuple[str, str]:
 | 
			
		||||
    client = app.test_client()
 | 
			
		||||
    resp_login = client.post(
 | 
			
		||||
        '/api/auth/login',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,13 @@
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestGetConfig:
 | 
			
		||||
    def test_it_gets_application_config(self, app, user_1):
 | 
			
		||||
    def test_it_gets_application_config(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -33,8 +38,8 @@ class TestGetConfig:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_application_has_no_config(
 | 
			
		||||
        self, app_no_config, user_1_admin
 | 
			
		||||
    ):
 | 
			
		||||
        self, app_no_config: Flask, user_1_admin: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app_no_config.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -59,8 +64,8 @@ class TestGetConfig:
 | 
			
		||||
        assert 'Error on getting configuration.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_application_has_several_config(
 | 
			
		||||
        self, app, app_config, user_1_admin
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, app_config: Flask, user_1_admin: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -86,7 +91,9 @@ class TestGetConfig:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestUpdateConfig:
 | 
			
		||||
    def test_it_updates_config_when_user_is_admin(self, app, user_1_admin):
 | 
			
		||||
    def test_it_updates_config_when_user_is_admin(
 | 
			
		||||
        self, app: Flask, user_1_admin: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -114,7 +121,9 @@ class TestUpdateConfig:
 | 
			
		||||
        assert data['data']['max_zip_file_size'] == 10485760
 | 
			
		||||
        assert data['data']['max_users'] == 10
 | 
			
		||||
 | 
			
		||||
    def test_it_updates_all_config(self, app, user_1_admin):
 | 
			
		||||
    def test_it_updates_all_config(
 | 
			
		||||
        self, app: Flask, user_1_admin: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -150,7 +159,9 @@ class TestUpdateConfig:
 | 
			
		||||
        assert data['data']['max_zip_file_size'] == 25000
 | 
			
		||||
        assert data['data']['max_users'] == 50
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_403_when_user_is_not_an_admin(self, app, user_1):
 | 
			
		||||
    def test_it_returns_403_when_user_is_not_an_admin(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -174,7 +185,9 @@ class TestUpdateConfig:
 | 
			
		||||
        assert 'error' in data['status']
 | 
			
		||||
        assert 'You do not have permissions.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_400_if_invalid_is_payload(self, app, user_1_admin):
 | 
			
		||||
    def test_it_returns_400_if_invalid_is_payload(
 | 
			
		||||
        self, app: Flask, user_1_admin: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -200,8 +213,8 @@ class TestUpdateConfig:
 | 
			
		||||
        assert 'Invalid payload.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_on_update_if_application_has_no_config(
 | 
			
		||||
        self, app_no_config, user_1_admin
 | 
			
		||||
    ):
 | 
			
		||||
        self, app_no_config: Flask, user_1_admin: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app_no_config.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
from fittrackee.application.models import AppConfig
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestConfigModel:
 | 
			
		||||
    def test_application_config(self, app):
 | 
			
		||||
    def test_application_config(self, app: Flask) -> None:
 | 
			
		||||
        app_config = AppConfig.query.first()
 | 
			
		||||
        assert 1 == app_config.id
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestConfig:
 | 
			
		||||
    def test_development_config(self, app):
 | 
			
		||||
    def test_development_config(self, app: Flask) -> None:
 | 
			
		||||
        app.config.from_object('fittrackee.config.DevelopmentConfig')
 | 
			
		||||
        assert app.config['DEBUG']
 | 
			
		||||
        assert not app.config['TESTING']
 | 
			
		||||
@@ -10,7 +12,7 @@ class TestConfig:
 | 
			
		||||
            'DATABASE_URL'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_testing_config(self, app):
 | 
			
		||||
    def test_testing_config(self, app: Flask) -> None:
 | 
			
		||||
        app.config.from_object('fittrackee.config.TestingConfig')
 | 
			
		||||
        assert app.config['DEBUG']
 | 
			
		||||
        assert app.config['TESTING']
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestHealthCheck:
 | 
			
		||||
    def test_it_returns_pong_on_health_check(self, app):
 | 
			
		||||
    def test_it_returns_pong_on_health_check(self, app: Flask) -> None:
 | 
			
		||||
        """ => Ensure the /health_check route behaves correctly."""
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        response = client.get('/api/ping')
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import datetime
 | 
			
		||||
import os
 | 
			
		||||
from typing import Generator, Optional
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
from fittrackee import create_app, db
 | 
			
		||||
@@ -11,10 +12,10 @@ from fittrackee.users.models import User
 | 
			
		||||
os.environ['FLASK_ENV'] = 'testing'
 | 
			
		||||
os.environ['APP_SETTINGS'] = 'fittrackee.config.TestingConfig'
 | 
			
		||||
# to avoid resetting dev database during tests
 | 
			
		||||
os.environ['DATABASE_URL'] = os.getenv('DATABASE_TEST_URL')
 | 
			
		||||
os.environ['DATABASE_URL'] = os.environ['DATABASE_TEST_URL']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_app_config(with_config=False):
 | 
			
		||||
def get_app_config(with_config: Optional[bool] = False) -> Optional[AppConfig]:
 | 
			
		||||
    if with_config:
 | 
			
		||||
        config = AppConfig()
 | 
			
		||||
        config.gpx_limit_import = 10
 | 
			
		||||
@@ -28,7 +29,7 @@ def get_app_config(with_config=False):
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_app(with_config=False):
 | 
			
		||||
def get_app(with_config: Optional[bool] = False) -> Generator:
 | 
			
		||||
    app = create_app()
 | 
			
		||||
    with app.app_context():
 | 
			
		||||
        db.create_all()
 | 
			
		||||
@@ -46,7 +47,7 @@ def get_app(with_config=False):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def app(monkeypatch):
 | 
			
		||||
def app(monkeypatch: pytest.MonkeyPatch) -> Generator:
 | 
			
		||||
    monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025')
 | 
			
		||||
    if os.getenv('TILE_SERVER_URL'):
 | 
			
		||||
        monkeypatch.delenv('TILE_SERVER_URL')
 | 
			
		||||
@@ -56,24 +57,24 @@ def app(monkeypatch):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def app_no_config():
 | 
			
		||||
def app_no_config() -> Generator:
 | 
			
		||||
    yield from get_app(with_config=False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def app_ssl(monkeypatch):
 | 
			
		||||
def app_ssl(monkeypatch: pytest.MonkeyPatch) -> Generator:
 | 
			
		||||
    monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025?ssl=True')
 | 
			
		||||
    yield from get_app(with_config=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def app_tls(monkeypatch):
 | 
			
		||||
def app_tls(monkeypatch: pytest.MonkeyPatch) -> Generator:
 | 
			
		||||
    monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025?tls=True')
 | 
			
		||||
    yield from get_app(with_config=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def app_config():
 | 
			
		||||
def app_config() -> AppConfig:
 | 
			
		||||
    config = AppConfig()
 | 
			
		||||
    config.gpx_limit_import = 10
 | 
			
		||||
    config.max_single_file_size = 1048576
 | 
			
		||||
@@ -85,7 +86,7 @@ def app_config():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def user_1():
 | 
			
		||||
def user_1() -> User:
 | 
			
		||||
    user = User(username='test', email='test@test.com', password='12345678')
 | 
			
		||||
    db.session.add(user)
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
@@ -93,7 +94,7 @@ def user_1():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def user_1_admin():
 | 
			
		||||
def user_1_admin() -> User:
 | 
			
		||||
    admin = User(
 | 
			
		||||
        username='admin', email='admin@example.com', password='12345678'
 | 
			
		||||
    )
 | 
			
		||||
@@ -104,7 +105,7 @@ def user_1_admin():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def user_1_full():
 | 
			
		||||
def user_1_full() -> User:
 | 
			
		||||
    user = User(username='test', email='test@test.com', password='12345678')
 | 
			
		||||
    user.first_name = 'John'
 | 
			
		||||
    user.last_name = 'Doe'
 | 
			
		||||
@@ -119,7 +120,7 @@ def user_1_full():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def user_1_paris():
 | 
			
		||||
def user_1_paris() -> User:
 | 
			
		||||
    user = User(username='test', email='test@test.com', password='12345678')
 | 
			
		||||
    user.timezone = 'Europe/Paris'
 | 
			
		||||
    db.session.add(user)
 | 
			
		||||
@@ -128,7 +129,7 @@ def user_1_paris():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def user_2():
 | 
			
		||||
def user_2() -> User:
 | 
			
		||||
    user = User(username='toto', email='toto@toto.com', password='87654321')
 | 
			
		||||
    db.session.add(user)
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
@@ -136,7 +137,7 @@ def user_2():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def user_2_admin():
 | 
			
		||||
def user_2_admin() -> User:
 | 
			
		||||
    user = User(username='toto', email='toto@toto.com', password='87654321')
 | 
			
		||||
    user.admin = True
 | 
			
		||||
    db.session.add(user)
 | 
			
		||||
@@ -145,7 +146,7 @@ def user_2_admin():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def user_3():
 | 
			
		||||
def user_3() -> User:
 | 
			
		||||
    user = User(username='sam', email='sam@test.com', password='12345678')
 | 
			
		||||
    user.weekm = True
 | 
			
		||||
    db.session.add(user)
 | 
			
		||||
@@ -154,7 +155,7 @@ def user_3():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def sport_1_cycling():
 | 
			
		||||
def sport_1_cycling() -> Sport:
 | 
			
		||||
    sport = Sport(label='Cycling')
 | 
			
		||||
    db.session.add(sport)
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
@@ -162,7 +163,7 @@ def sport_1_cycling():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def sport_1_cycling_inactive():
 | 
			
		||||
def sport_1_cycling_inactive() -> Sport:
 | 
			
		||||
    sport = Sport(label='Cycling')
 | 
			
		||||
    sport.is_active = False
 | 
			
		||||
    db.session.add(sport)
 | 
			
		||||
@@ -171,7 +172,7 @@ def sport_1_cycling_inactive():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def sport_2_running():
 | 
			
		||||
def sport_2_running() -> Sport:
 | 
			
		||||
    sport = Sport(label='Running')
 | 
			
		||||
    db.session.add(sport)
 | 
			
		||||
    db.session.commit()
 | 
			
		||||
@@ -179,7 +180,7 @@ def sport_2_running():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def activity_cycling_user_1():
 | 
			
		||||
def activity_cycling_user_1() -> Activity:
 | 
			
		||||
    activity = Activity(
 | 
			
		||||
        user_id=1,
 | 
			
		||||
        sport_id=1,
 | 
			
		||||
@@ -196,7 +197,9 @@ def activity_cycling_user_1():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def activity_cycling_user_1_segment(activity_cycling_user_1):
 | 
			
		||||
def activity_cycling_user_1_segment(
 | 
			
		||||
    activity_cycling_user_1: Activity,
 | 
			
		||||
) -> ActivitySegment:
 | 
			
		||||
    activity_segment = ActivitySegment(
 | 
			
		||||
        activity_id=activity_cycling_user_1.id,
 | 
			
		||||
        activity_uuid=activity_cycling_user_1.uuid,
 | 
			
		||||
@@ -211,7 +214,7 @@ def activity_cycling_user_1_segment(activity_cycling_user_1):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def activity_running_user_1():
 | 
			
		||||
def activity_running_user_1() -> Activity:
 | 
			
		||||
    activity = Activity(
 | 
			
		||||
        user_id=1,
 | 
			
		||||
        sport_id=2,
 | 
			
		||||
@@ -226,7 +229,7 @@ def activity_running_user_1():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def seven_activities_user_1():
 | 
			
		||||
def seven_activities_user_1() -> Activity:
 | 
			
		||||
    activity = Activity(
 | 
			
		||||
        user_id=1,
 | 
			
		||||
        sport_id=1,
 | 
			
		||||
@@ -308,7 +311,7 @@ def seven_activities_user_1():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def activity_cycling_user_2():
 | 
			
		||||
def activity_cycling_user_2() -> Activity:
 | 
			
		||||
    activity = Activity(
 | 
			
		||||
        user_id=2,
 | 
			
		||||
        sport_id=1,
 | 
			
		||||
@@ -323,7 +326,7 @@ def activity_cycling_user_2():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def gpx_file():
 | 
			
		||||
def gpx_file() -> str:
 | 
			
		||||
    return (
 | 
			
		||||
        '<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
 | 
			
		||||
        '<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">'  # noqa
 | 
			
		||||
@@ -438,7 +441,7 @@ def gpx_file():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def gpx_file_wo_name():
 | 
			
		||||
def gpx_file_wo_name() -> str:
 | 
			
		||||
    return (
 | 
			
		||||
        '<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
 | 
			
		||||
        '<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">'  # noqa
 | 
			
		||||
@@ -552,7 +555,7 @@ def gpx_file_wo_name():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def gpx_file_wo_track():
 | 
			
		||||
def gpx_file_wo_track() -> str:
 | 
			
		||||
    return (
 | 
			
		||||
        '<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
 | 
			
		||||
        '<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">'  # noqa
 | 
			
		||||
@@ -562,7 +565,7 @@ def gpx_file_wo_track():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def gpx_file_invalid_xml():
 | 
			
		||||
def gpx_file_invalid_xml() -> str:
 | 
			
		||||
    return (
 | 
			
		||||
        '<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
 | 
			
		||||
        '<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">'  # noqa
 | 
			
		||||
@@ -571,7 +574,7 @@ def gpx_file_invalid_xml():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture()
 | 
			
		||||
def gpx_file_with_segments():
 | 
			
		||||
def gpx_file_with_segments() -> str:
 | 
			
		||||
    return (
 | 
			
		||||
        '<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
 | 
			
		||||
        '<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">'  # noqa
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,15 @@
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
from typing import Any
 | 
			
		||||
from unittest.mock import Mock, patch
 | 
			
		||||
 | 
			
		||||
from fittrackee import email_service
 | 
			
		||||
from fittrackee.email.email import EmailMessage
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
from ..template_results.password_reset_request import expected_en_text_body
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestEmailMessage:
 | 
			
		||||
    def test_it_generate_email_data(self):
 | 
			
		||||
    def test_it_generate_email_data(self) -> None:
 | 
			
		||||
        message = EmailMessage(
 | 
			
		||||
            sender='fittrackee@example.com',
 | 
			
		||||
            recipient='test@test.com',
 | 
			
		||||
@@ -40,14 +42,14 @@ class TestEmailSending:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_args(call_args):
 | 
			
		||||
    def get_args(call_args: Any) -> Any:
 | 
			
		||||
        if len(call_args) == 2:
 | 
			
		||||
            args, _ = call_args
 | 
			
		||||
        else:
 | 
			
		||||
            _, args, _ = call_args
 | 
			
		||||
        return args
 | 
			
		||||
 | 
			
		||||
    def assert_smtp(self, smtp):
 | 
			
		||||
    def assert_smtp(self, smtp: Mock) -> None:
 | 
			
		||||
        assert smtp.sendmail.call_count == 1
 | 
			
		||||
        call_args = self.get_args(smtp.sendmail.call_args)
 | 
			
		||||
        assert call_args[0] == 'fittrackee@example.com'
 | 
			
		||||
@@ -56,7 +58,9 @@ class TestEmailSending:
 | 
			
		||||
 | 
			
		||||
    @patch('smtplib.SMTP_SSL')
 | 
			
		||||
    @patch('smtplib.SMTP')
 | 
			
		||||
    def test_it_sends_message(self, mock_smtp, mock_smtp_ssl, app):
 | 
			
		||||
    def test_it_sends_message(
 | 
			
		||||
        self, mock_smtp: Mock, mock_smtp_ssl: Mock, app: Flask
 | 
			
		||||
    ) -> None:
 | 
			
		||||
 | 
			
		||||
        email_service.send(
 | 
			
		||||
            template='password_reset_request',
 | 
			
		||||
@@ -72,8 +76,8 @@ class TestEmailSending:
 | 
			
		||||
    @patch('smtplib.SMTP_SSL')
 | 
			
		||||
    @patch('smtplib.SMTP')
 | 
			
		||||
    def test_it_sends_message_with_ssl(
 | 
			
		||||
        self, mock_smtp, mock_smtp_ssl, app_ssl
 | 
			
		||||
    ):
 | 
			
		||||
        self, mock_smtp: Mock, mock_smtp_ssl: Mock, app_ssl: Flask
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        email_service.send(
 | 
			
		||||
            template='password_reset_request',
 | 
			
		||||
            lang='en',
 | 
			
		||||
@@ -88,8 +92,8 @@ class TestEmailSending:
 | 
			
		||||
    @patch('smtplib.SMTP_SSL')
 | 
			
		||||
    @patch('smtplib.SMTP')
 | 
			
		||||
    def test_it_sends_message_with_tls(
 | 
			
		||||
        self, mock_smtp, mock_smtp_ssl, app_tls
 | 
			
		||||
    ):
 | 
			
		||||
        self, mock_smtp: Mock, mock_smtp_ssl: Mock, app_tls: Flask
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        email_service.send(
 | 
			
		||||
            template='password_reset_request',
 | 
			
		||||
            lang='en',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import pytest
 | 
			
		||||
from fittrackee.email.email import EmailTemplate
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
from ..template_results.password_reset_request import (
 | 
			
		||||
    expected_en_html_body,
 | 
			
		||||
@@ -17,8 +18,10 @@ class TestEmailTemplateForPasswordRequest:
 | 
			
		||||
            ('fr', 'FitTrackee - Réinitialiser votre mot de passe'),
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
    def test_it_gets_subject(self, app, lang, expected_subject):
 | 
			
		||||
        email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
 | 
			
		||||
    def test_it_gets_subject(
 | 
			
		||||
        self, app: Flask, lang: str, expected_subject: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
 | 
			
		||||
 | 
			
		||||
        subject = email_template.get_content(
 | 
			
		||||
            'password_reset_request', lang, 'subject.txt', {}
 | 
			
		||||
@@ -30,8 +33,10 @@ class TestEmailTemplateForPasswordRequest:
 | 
			
		||||
        'lang, expected_text_body',
 | 
			
		||||
        [('en', expected_en_text_body), ('fr', expected_fr_text_body)],
 | 
			
		||||
    )
 | 
			
		||||
    def test_it_gets_text_body(self, app, lang, expected_text_body):
 | 
			
		||||
        email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
 | 
			
		||||
    def test_it_gets_text_body(
 | 
			
		||||
        self, app: Flask, lang: str, expected_text_body: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
 | 
			
		||||
        email_data = {
 | 
			
		||||
            'expiration_delay': '3 seconds' if lang == 'en' else '3 secondes',
 | 
			
		||||
            'username': 'test',
 | 
			
		||||
@@ -46,8 +51,8 @@ class TestEmailTemplateForPasswordRequest:
 | 
			
		||||
 | 
			
		||||
        assert text_body == expected_text_body
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_en_html_body(self, app):
 | 
			
		||||
        email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
 | 
			
		||||
    def test_it_gets_en_html_body(self, app: Flask) -> None:
 | 
			
		||||
        email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
 | 
			
		||||
        email_data = {
 | 
			
		||||
            'expiration_delay': '3 seconds',
 | 
			
		||||
            'username': 'test',
 | 
			
		||||
@@ -62,8 +67,8 @@ class TestEmailTemplateForPasswordRequest:
 | 
			
		||||
 | 
			
		||||
        assert expected_en_html_body in text_body
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_fr_html_body(self, app):
 | 
			
		||||
        email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
 | 
			
		||||
    def test_it_gets_fr_html_body(self, app: Flask) -> None:
 | 
			
		||||
        email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
 | 
			
		||||
        email_data = {
 | 
			
		||||
            'expiration_delay': '3 secondes',
 | 
			
		||||
            'username': 'test',
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,12 @@ from fittrackee.email.utils_email import InvalidEmailUrlScheme, parse_email_url
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestEmailUrlParser:
 | 
			
		||||
    def test_it_raises_error_if_url_scheme_is_invalid(self):
 | 
			
		||||
    def test_it_raises_error_if_url_scheme_is_invalid(self) -> None:
 | 
			
		||||
        url = 'stmp://username:password@localhost:587'
 | 
			
		||||
        with pytest.raises(InvalidEmailUrlScheme):
 | 
			
		||||
            parse_email_url(url)
 | 
			
		||||
 | 
			
		||||
    def test_it_parses_email_url(self):
 | 
			
		||||
    def test_it_parses_email_url(self) -> None:
 | 
			
		||||
        url = 'smtp://test@example.com:12345678@localhost:25'
 | 
			
		||||
        parsed_email = parse_email_url(url)
 | 
			
		||||
        assert parsed_email['username'] == 'test@example.com'
 | 
			
		||||
@@ -18,7 +18,7 @@ class TestEmailUrlParser:
 | 
			
		||||
        assert parsed_email['use_tls'] is False
 | 
			
		||||
        assert parsed_email['use_ssl'] is False
 | 
			
		||||
 | 
			
		||||
    def test_it_parses_email_url_with_tls(self):
 | 
			
		||||
    def test_it_parses_email_url_with_tls(self) -> None:
 | 
			
		||||
        url = 'smtp://test@example.com:12345678@localhost:587?tls=True'
 | 
			
		||||
        parsed_email = parse_email_url(url)
 | 
			
		||||
        assert parsed_email['username'] == 'test@example.com'
 | 
			
		||||
@@ -28,7 +28,7 @@ class TestEmailUrlParser:
 | 
			
		||||
        assert parsed_email['use_tls'] is True
 | 
			
		||||
        assert parsed_email['use_ssl'] is False
 | 
			
		||||
 | 
			
		||||
    def test_it_parses_email_url_with_ssl(self):
 | 
			
		||||
    def test_it_parses_email_url_with_ssl(self) -> None:
 | 
			
		||||
        url = 'smtp://test@example.com:12345678@localhost:465?ssl=True'
 | 
			
		||||
        parsed_email = parse_email_url(url)
 | 
			
		||||
        assert parsed_email['username'] == 'test@example.com'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,17 @@
 | 
			
		||||
import json
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from io import BytesIO
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
from unittest.mock import Mock, patch
 | 
			
		||||
 | 
			
		||||
from fittrackee.activities.models import Activity, Sport
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from fittrackee.users.utils_token import get_user_token
 | 
			
		||||
from flask import Flask
 | 
			
		||||
from freezegun import freeze_time
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestUserRegistration:
 | 
			
		||||
    def test_user_can_register(self, app):
 | 
			
		||||
    def test_user_can_register(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -31,7 +34,9 @@ class TestUserRegistration:
 | 
			
		||||
        assert response.content_type == 'application/json'
 | 
			
		||||
        assert response.status_code == 201
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_user_already_exists(self, app, user_1):
 | 
			
		||||
    def test_it_returns_error_if_user_already_exists(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        response = client.post(
 | 
			
		||||
            '/api/auth/register',
 | 
			
		||||
@@ -51,7 +56,9 @@ class TestUserRegistration:
 | 
			
		||||
        assert response.content_type == 'application/json'
 | 
			
		||||
        assert response.status_code == 400
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_username_is_too_short(self, app):
 | 
			
		||||
    def test_it_returns_error_if_username_is_too_short(
 | 
			
		||||
        self, app: Flask
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -73,7 +80,9 @@ class TestUserRegistration:
 | 
			
		||||
        assert response.content_type == 'application/json'
 | 
			
		||||
        assert response.status_code == 400
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_username_is_too_long(self, app):
 | 
			
		||||
    def test_it_returns_error_if_username_is_too_long(
 | 
			
		||||
        self, app: Flask
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        response = client.post(
 | 
			
		||||
            '/api/auth/register',
 | 
			
		||||
@@ -93,7 +102,7 @@ class TestUserRegistration:
 | 
			
		||||
        assert response.content_type == 'application/json'
 | 
			
		||||
        assert response.status_code == 400
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_email_is_invalid(self, app):
 | 
			
		||||
    def test_it_returns_error_if_email_is_invalid(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -115,7 +124,9 @@ class TestUserRegistration:
 | 
			
		||||
        assert response.content_type == 'application/json'
 | 
			
		||||
        assert response.status_code == 400
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_password_is_too_short(self, app):
 | 
			
		||||
    def test_it_returns_error_if_password_is_too_short(
 | 
			
		||||
        self, app: Flask
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -137,7 +148,7 @@ class TestUserRegistration:
 | 
			
		||||
        assert response.content_type == 'application/json'
 | 
			
		||||
        assert response.status_code == 400
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_passwords_mismatch(self, app):
 | 
			
		||||
    def test_it_returns_error_if_passwords_mismatch(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -162,7 +173,7 @@ class TestUserRegistration:
 | 
			
		||||
        assert response.content_type == 'application/json'
 | 
			
		||||
        assert response.status_code == 400
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_payload_is_invalid(self, app):
 | 
			
		||||
    def test_it_returns_error_if_payload_is_invalid(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        response = client.post(
 | 
			
		||||
            '/api/auth/register',
 | 
			
		||||
@@ -174,7 +185,7 @@ class TestUserRegistration:
 | 
			
		||||
        assert 'Invalid payload.', data['message']
 | 
			
		||||
        assert 'error', data['status']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_username_is_missing(self, app):
 | 
			
		||||
    def test_it_returns_error_if_username_is_missing(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -194,7 +205,7 @@ class TestUserRegistration:
 | 
			
		||||
        assert 'Invalid payload.' in data['message']
 | 
			
		||||
        assert 'error' in data['status']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_email_is_missing(self, app):
 | 
			
		||||
    def test_it_returns_error_if_email_is_missing(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -214,7 +225,7 @@ class TestUserRegistration:
 | 
			
		||||
        assert 'Invalid payload.' in data['message']
 | 
			
		||||
        assert 'error' in data['status']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_password_is_missing(self, app):
 | 
			
		||||
    def test_it_returns_error_if_password_is_missing(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -234,7 +245,9 @@ class TestUserRegistration:
 | 
			
		||||
        assert 'Invalid payload.', data['message']
 | 
			
		||||
        assert 'error', data['status']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_password_confirmation_is_missing(self, app):
 | 
			
		||||
    def test_it_returns_error_if_password_confirmation_is_missing(
 | 
			
		||||
        self, app: Flask
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        response = client.post(
 | 
			
		||||
            '/api/auth/register',
 | 
			
		||||
@@ -250,7 +263,7 @@ class TestUserRegistration:
 | 
			
		||||
        assert 'Invalid payload.' in data['message']
 | 
			
		||||
        assert 'error' in data['status']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_username_is_invalid(self, app):
 | 
			
		||||
    def test_it_returns_error_if_username_is_invalid(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -276,7 +289,7 @@ class TestUserRegistration:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestUserLogin:
 | 
			
		||||
    def test_user_can_register(self, app, user_1):
 | 
			
		||||
    def test_user_can_register(self, app: Flask, user_1: User) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -292,7 +305,9 @@ class TestUserLogin:
 | 
			
		||||
        assert data['message'] == 'Successfully logged in.'
 | 
			
		||||
        assert data['auth_token']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_user_does_not_exists(self, app):
 | 
			
		||||
    def test_it_returns_error_if_user_does_not_exists(
 | 
			
		||||
        self, app: Flask
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -307,7 +322,7 @@ class TestUserLogin:
 | 
			
		||||
        assert data['status'] == 'error'
 | 
			
		||||
        assert data['message'] == 'Invalid credentials.'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_on_invalid_payload(self, app):
 | 
			
		||||
    def test_it_returns_error_on_invalid_payload(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -322,7 +337,9 @@ class TestUserLogin:
 | 
			
		||||
        assert data['status'] == 'error'
 | 
			
		||||
        assert data['message'] == 'Invalid payload.'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_password_is_invalid(self, app, user_1):
 | 
			
		||||
    def test_it_returns_error_if_password_is_invalid(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -339,7 +356,7 @@ class TestUserLogin:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestUserLogout:
 | 
			
		||||
    def test_user_can_logout(self, app, user_1):
 | 
			
		||||
    def test_user_can_logout(self, app: Flask, user_1: User) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -360,7 +377,9 @@ class TestUserLogout:
 | 
			
		||||
        assert data['message'] == 'Successfully logged out.'
 | 
			
		||||
        assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_with_expired_token(self, app, user_1):
 | 
			
		||||
    def test_it_returns_error_with_expired_token(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        now = datetime.utcnow()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -381,7 +400,7 @@ class TestUserLogout:
 | 
			
		||||
            assert data['message'] == 'Signature expired. Please log in again.'
 | 
			
		||||
            assert response.status_code == 401
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_with_invalid_token(self, app):
 | 
			
		||||
    def test_it_returns_error_with_invalid_token(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        response = client.get(
 | 
			
		||||
            '/api/auth/logout', headers=dict(Authorization='Bearer invalid')
 | 
			
		||||
@@ -391,7 +410,7 @@ class TestUserLogout:
 | 
			
		||||
        assert data['message'] == 'Invalid token. Please log in again.'
 | 
			
		||||
        assert response.status_code == 401
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_with_invalid_headers(self, app):
 | 
			
		||||
    def test_it_returns_error_with_invalid_headers(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        response = client.get('/api/auth/logout', headers=dict())
 | 
			
		||||
        data = json.loads(response.data.decode())
 | 
			
		||||
@@ -401,7 +420,9 @@ class TestUserLogout:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestUserProfile:
 | 
			
		||||
    def test_it_returns_user_minimal_profile(self, app, user_1):
 | 
			
		||||
    def test_it_returns_user_minimal_profile(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -432,7 +453,9 @@ class TestUserProfile:
 | 
			
		||||
        assert data['data']['total_duration'] == '0:00:00'
 | 
			
		||||
        assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_user_full_profile(self, app, user_1_full):
 | 
			
		||||
    def test_it_returns_user_full_profile(
 | 
			
		||||
        self, app: Flask, user_1_full: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -470,13 +493,13 @@ class TestUserProfile:
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_user_profile_with_activities(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_cycling_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -505,7 +528,7 @@ class TestUserProfile:
 | 
			
		||||
        assert data['data']['total_duration'] == '2:40:00'
 | 
			
		||||
        assert response.status_code == 200
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_headers_are_invalid(self, app):
 | 
			
		||||
    def test_it_returns_error_if_headers_are_invalid(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        response = client.get(
 | 
			
		||||
            '/api/auth/profile', headers=dict(Authorization='Bearer invalid')
 | 
			
		||||
@@ -517,7 +540,7 @@ class TestUserProfile:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestUserProfileUpdate:
 | 
			
		||||
    def test_it_updates_user_profile(self, app, user_1):
 | 
			
		||||
    def test_it_updates_user_profile(self, app: Flask, user_1: User) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -568,7 +591,9 @@ class TestUserProfileUpdate:
 | 
			
		||||
        assert data['data']['total_distance'] == 0
 | 
			
		||||
        assert data['data']['total_duration'] == '0:00:00'
 | 
			
		||||
 | 
			
		||||
    def test_it_updates_user_profile_without_password(self, app, user_1):
 | 
			
		||||
    def test_it_updates_user_profile_without_password(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -617,7 +642,9 @@ class TestUserProfileUpdate:
 | 
			
		||||
        assert data['data']['total_distance'] == 0
 | 
			
		||||
        assert data['data']['total_duration'] == '0:00:00'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_fields_are_missing(self, app, user_1):
 | 
			
		||||
    def test_it_returns_error_if_fields_are_missing(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -638,7 +665,9 @@ class TestUserProfileUpdate:
 | 
			
		||||
        assert data['message'] == 'Invalid payload.'
 | 
			
		||||
        assert response.status_code == 400
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_payload_is_empty(self, app, user_1):
 | 
			
		||||
    def test_it_returns_error_if_payload_is_empty(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -659,7 +688,9 @@ class TestUserProfileUpdate:
 | 
			
		||||
        assert 'Invalid payload.' in data['message']
 | 
			
		||||
        assert 'error' in data['status']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_passwords_mismatch(self, app, user_1):
 | 
			
		||||
    def test_it_returns_error_if_passwords_mismatch(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -697,8 +728,8 @@ class TestUserProfileUpdate:
 | 
			
		||||
        assert response.status_code == 400
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_password_confirmation_is_missing(
 | 
			
		||||
        self, app, user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -736,7 +767,7 @@ class TestUserProfileUpdate:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestUserPicture:
 | 
			
		||||
    def test_it_updates_user_picture(self, app, user_1):
 | 
			
		||||
    def test_it_updates_user_picture(self, app: Flask, user_1: User) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -774,7 +805,9 @@ class TestUserPicture:
 | 
			
		||||
        assert 'avatar.png' not in user_1.picture
 | 
			
		||||
        assert 'avatar2.png' in user_1.picture
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_file_is_missing(self, app, user_1):
 | 
			
		||||
    def test_it_returns_error_if_file_is_missing(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -794,7 +827,9 @@ class TestUserPicture:
 | 
			
		||||
        assert data['message'] == 'No file part.'
 | 
			
		||||
        assert response.status_code == 400
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_file_is_invalid(self, app, user_1):
 | 
			
		||||
    def test_it_returns_error_if_file_is_invalid(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -818,8 +853,8 @@ class TestUserPicture:
 | 
			
		||||
 | 
			
		||||
class TestRegistrationConfiguration:
 | 
			
		||||
    def test_it_returns_error_if_it_exceeds_max_users(
 | 
			
		||||
        self, app, user_1_admin, user_2, user_3
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1_admin: User, user_2: User, user_3: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -859,8 +894,12 @@ class TestRegistrationConfiguration:
 | 
			
		||||
        assert data['message'] == 'Error. Registration is disabled.'
 | 
			
		||||
 | 
			
		||||
    def test_it_disables_registration_on_user_registration(
 | 
			
		||||
        self, app_no_config, app_config, user_1_admin, user_2
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app_no_config: Flask,
 | 
			
		||||
        app_config: Flask,
 | 
			
		||||
        user_1_admin: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        app_config.max_users = 3
 | 
			
		||||
        client = app_no_config.test_client()
 | 
			
		||||
        client.post(
 | 
			
		||||
@@ -894,11 +933,11 @@ class TestRegistrationConfiguration:
 | 
			
		||||
 | 
			
		||||
    def test_it_does_not_disable_registration_on_user_registration(
 | 
			
		||||
        self,
 | 
			
		||||
        app_no_config,
 | 
			
		||||
        app_config,
 | 
			
		||||
        user_1_admin,
 | 
			
		||||
        user_2,
 | 
			
		||||
    ):
 | 
			
		||||
        app_no_config: Flask,
 | 
			
		||||
        app_config: Flask,
 | 
			
		||||
        user_1_admin: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        app_config.max_users = 4
 | 
			
		||||
        client = app_no_config.test_client()
 | 
			
		||||
        client.post(
 | 
			
		||||
@@ -932,8 +971,8 @@ class TestPasswordResetRequest:
 | 
			
		||||
    @patch('smtplib.SMTP_SSL')
 | 
			
		||||
    @patch('smtplib.SMTP')
 | 
			
		||||
    def test_it_requests_password_reset_when_user_exists(
 | 
			
		||||
        self, mock_smtp, mock_smtp_ssl, app, user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self, mock_smtp: Mock, mock_smtp_ssl: Mock, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        response = client.post(
 | 
			
		||||
            '/api/auth/password/reset-request',
 | 
			
		||||
@@ -946,7 +985,9 @@ class TestPasswordResetRequest:
 | 
			
		||||
        assert data['status'] == 'success'
 | 
			
		||||
        assert data['message'] == 'Password reset request processed.'
 | 
			
		||||
 | 
			
		||||
    def test_it_does_not_return_error_when_user_does_not_exist(self, app):
 | 
			
		||||
    def test_it_does_not_return_error_when_user_does_not_exist(
 | 
			
		||||
        self, app: Flask
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -960,7 +1001,7 @@ class TestPasswordResetRequest:
 | 
			
		||||
        assert data['status'] == 'success'
 | 
			
		||||
        assert data['message'] == 'Password reset request processed.'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_on_invalid_payload(self, app):
 | 
			
		||||
    def test_it_returns_error_on_invalid_payload(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -974,7 +1015,7 @@ class TestPasswordResetRequest:
 | 
			
		||||
        assert data['message'] == 'Invalid payload.'
 | 
			
		||||
        assert data['status'] == 'error'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_on_empty_payload(self, app):
 | 
			
		||||
    def test_it_returns_error_on_empty_payload(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -990,7 +1031,7 @@ class TestPasswordResetRequest:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestPasswordUpdate:
 | 
			
		||||
    def test_it_returns_error_if_payload_is_empty(self, app):
 | 
			
		||||
    def test_it_returns_error_if_payload_is_empty(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -1009,7 +1050,7 @@ class TestPasswordUpdate:
 | 
			
		||||
        assert data['status'] == 'error'
 | 
			
		||||
        assert data['message'] == 'Invalid payload.'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_token_is_missing(self, app):
 | 
			
		||||
    def test_it_returns_error_if_token_is_missing(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -1028,7 +1069,7 @@ class TestPasswordUpdate:
 | 
			
		||||
        assert data['status'] == 'error'
 | 
			
		||||
        assert data['message'] == 'Invalid payload.'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_password_is_missing(self, app):
 | 
			
		||||
    def test_it_returns_error_if_password_is_missing(self, app: Flask) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -1047,7 +1088,9 @@ class TestPasswordUpdate:
 | 
			
		||||
        assert data['status'] == 'error'
 | 
			
		||||
        assert data['message'] == 'Invalid payload.'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_password_confirmation_is_missing(self, app):
 | 
			
		||||
    def test_it_returns_error_if_password_confirmation_is_missing(
 | 
			
		||||
        self, app: Flask
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.post(
 | 
			
		||||
@@ -1066,7 +1109,7 @@ class TestPasswordUpdate:
 | 
			
		||||
        assert data['status'] == 'error'
 | 
			
		||||
        assert data['message'] == 'Invalid payload.'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_token_is_invalid(self, app):
 | 
			
		||||
    def test_it_returns_error_if_token_is_invalid(self, app: Flask) -> None:
 | 
			
		||||
        token = get_user_token(1)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
@@ -1087,7 +1130,9 @@ class TestPasswordUpdate:
 | 
			
		||||
        assert data['status'] == 'error'
 | 
			
		||||
        assert data['message'] == 'Invalid token. Please request a new token.'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_token_is_expired(self, app, user_1):
 | 
			
		||||
    def test_it_returns_error_if_token_is_expired(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        now = datetime.utcnow()
 | 
			
		||||
        token = get_user_token(user_1.id, password_reset=True)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
@@ -1112,7 +1157,9 @@ class TestPasswordUpdate:
 | 
			
		||||
                data['message'] == 'Invalid token. Please request a new token.'
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_password_is_invalid(self, app, user_1):
 | 
			
		||||
    def test_it_returns_error_if_password_is_invalid(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        token = get_user_token(user_1.id, password_reset=True)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
@@ -1133,7 +1180,7 @@ class TestPasswordUpdate:
 | 
			
		||||
        assert data['status'] == 'error'
 | 
			
		||||
        assert data['message'] == 'Password: 8 characters required.\n'
 | 
			
		||||
 | 
			
		||||
    def test_it_update_password(self, app, user_1):
 | 
			
		||||
    def test_it_update_password(self, app: Flask, user_1: User) -> None:
 | 
			
		||||
        token = get_user_token(user_1.id, password_reset=True)
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,15 @@ from datetime import datetime, timedelta
 | 
			
		||||
from io import BytesIO
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
from fittrackee.activities.models import Activity, Sport
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestGetUser:
 | 
			
		||||
    def test_it_gets_single_user_without_activities(self, app, user_1, user_2):
 | 
			
		||||
    def test_it_gets_single_user_without_activities(
 | 
			
		||||
        self, app: Flask, user_1: User, user_2: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -47,13 +53,13 @@ class TestGetUser:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_single_user_with_activities(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_cycling_user_1,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -93,7 +99,9 @@ class TestGetUser:
 | 
			
		||||
        assert user['total_distance'] == 22
 | 
			
		||||
        assert user['total_duration'] == '2:40:00'
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_user_does_not_exist(self, app, user_1):
 | 
			
		||||
    def test_it_returns_error_if_user_does_not_exist(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -116,7 +124,9 @@ class TestGetUser:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestGetUsers:
 | 
			
		||||
    def test_it_get_users_list(self, app, user_1, user_2, user_3):
 | 
			
		||||
    def test_it_get_users_list(
 | 
			
		||||
        self, app: Flask, user_1: User, user_2: User, user_3: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -179,16 +189,16 @@ class TestGetUsers:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_with_activities(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        user_3,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        activity_cycling_user_1,
 | 
			
		||||
        sport_2_running,
 | 
			
		||||
        activity_running_user_1,
 | 
			
		||||
        activity_cycling_user_2,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        user_3: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_1: Activity,
 | 
			
		||||
        sport_2_running: Sport,
 | 
			
		||||
        activity_running_user_1: Activity,
 | 
			
		||||
        activity_cycling_user_2: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -249,11 +259,11 @@ class TestGetUsers:
 | 
			
		||||
    @patch('fittrackee.users.users.USER_PER_PAGE', 2)
 | 
			
		||||
    def test_it_gets_first_page_on_users_list(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        user_3,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        user_3: User,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -284,11 +294,11 @@ class TestGetUsers:
 | 
			
		||||
    @patch('fittrackee.users.users.USER_PER_PAGE', 2)
 | 
			
		||||
    def test_it_gets_next_page_on_users_list(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        user_3,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        user_3: User,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -318,11 +328,11 @@ class TestGetUsers:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_empty_next_page_on_users_list(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        user_3,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        user_3: User,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -352,11 +362,11 @@ class TestGetUsers:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_user_list_with_2_per_page(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        user_3,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        user_3: User,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -386,11 +396,11 @@ class TestGetUsers:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_next_page_on_user_list_with_2_per_page(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        user_3,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        user_3: User,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -419,8 +429,8 @@ class TestGetUsers:
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_ordered_by_username(
 | 
			
		||||
        self, app, user_1, user_2, user_3
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, user_2: User, user_3: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -451,8 +461,8 @@ class TestGetUsers:
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_ordered_by_username_ascending(
 | 
			
		||||
        self, app, user_1, user_2, user_3
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, user_2: User, user_3: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -484,8 +494,8 @@ class TestGetUsers:
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_ordered_by_username_descending(
 | 
			
		||||
        self, app, user_1, user_2, user_3
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, user_2: User, user_3: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -517,8 +527,8 @@ class TestGetUsers:
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_ordered_by_creation_date(
 | 
			
		||||
        self, app, user_2, user_3, user_1_admin
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_2: User, user_3: User, user_1_admin: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        user_2.created_at = datetime.utcnow() - timedelta(days=1)
 | 
			
		||||
        user_3.created_at = datetime.utcnow() - timedelta(hours=1)
 | 
			
		||||
        user_1_admin.created_at = datetime.utcnow()
 | 
			
		||||
@@ -555,8 +565,8 @@ class TestGetUsers:
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_ordered_by_creation_date_ascending(
 | 
			
		||||
        self, app, user_2, user_3, user_1_admin
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_2: User, user_3: User, user_1_admin: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        user_2.created_at = datetime.utcnow() - timedelta(days=1)
 | 
			
		||||
        user_3.created_at = datetime.utcnow() - timedelta(hours=1)
 | 
			
		||||
        user_1_admin.created_at = datetime.utcnow()
 | 
			
		||||
@@ -593,8 +603,8 @@ class TestGetUsers:
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_ordered_by_creation_date_descending(
 | 
			
		||||
        self, app, user_2, user_3, user_1_admin
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_2: User, user_3: User, user_1_admin: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        user_2.created_at = datetime.utcnow() - timedelta(days=1)
 | 
			
		||||
        user_3.created_at = datetime.utcnow() - timedelta(hours=1)
 | 
			
		||||
        user_1_admin.created_at = datetime.utcnow()
 | 
			
		||||
@@ -631,8 +641,8 @@ class TestGetUsers:
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_ordered_by_admin_rights(
 | 
			
		||||
        self, app, user_2, user_1_admin, user_3
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_2: User, user_1_admin: User, user_3: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -666,8 +676,8 @@ class TestGetUsers:
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_ordered_by_admin_rights_ascending(
 | 
			
		||||
        self, app, user_2, user_1_admin, user_3
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_2: User, user_1_admin: User, user_3: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -701,8 +711,8 @@ class TestGetUsers:
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_ordered_by_admin_rights_descending(
 | 
			
		||||
        self, app, user_2, user_3, user_1_admin
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_2: User, user_3: User, user_1_admin: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -737,13 +747,13 @@ class TestGetUsers:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_ordered_by_activities_count(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        user_3,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        activity_cycling_user_2,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        user_3: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_2: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -779,13 +789,13 @@ class TestGetUsers:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_ordered_by_activities_count_ascending(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        user_3,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        activity_cycling_user_2,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        user_3: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_2: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -821,13 +831,13 @@ class TestGetUsers:
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_ordered_by_activities_count_descending(
 | 
			
		||||
        self,
 | 
			
		||||
        app,
 | 
			
		||||
        user_1,
 | 
			
		||||
        user_2,
 | 
			
		||||
        user_3,
 | 
			
		||||
        sport_1_cycling,
 | 
			
		||||
        activity_cycling_user_2,
 | 
			
		||||
    ):
 | 
			
		||||
        app: Flask,
 | 
			
		||||
        user_1: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        user_3: User,
 | 
			
		||||
        sport_1_cycling: Sport,
 | 
			
		||||
        activity_cycling_user_2: Activity,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -862,8 +872,8 @@ class TestGetUsers:
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_it_gets_users_list_filtering_on_username(
 | 
			
		||||
        self, app, user_1, user_2, user_3
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, user_2: User, user_3: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -893,8 +903,8 @@ class TestGetUsers:
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_empty_users_list_filtering_on_username(
 | 
			
		||||
        self, app, user_1, user_2, user_3
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, user_2: User, user_3: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -923,8 +933,8 @@ class TestGetUsers:
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def test_it_users_list_with_complex_query(
 | 
			
		||||
        self, app, user_1, user_2, user_3
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, user_2: User, user_3: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -955,7 +965,9 @@ class TestGetUsers:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestGetUserPicture:
 | 
			
		||||
    def test_it_return_error_if_user_has_no_picture(self, app, user_1):
 | 
			
		||||
    def test_it_return_error_if_user_has_no_picture(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.get(f'/api/users/{user_1.username}/picture')
 | 
			
		||||
@@ -965,7 +977,9 @@ class TestGetUserPicture:
 | 
			
		||||
        assert 'not found' in data['status']
 | 
			
		||||
        assert 'No picture.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_user_does_not_exist(self, app, user_1):
 | 
			
		||||
    def test_it_returns_error_if_user_does_not_exist(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
 | 
			
		||||
        response = client.get('/api/users/not_existing/picture')
 | 
			
		||||
@@ -977,7 +991,9 @@ class TestGetUserPicture:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestUpdateUser:
 | 
			
		||||
    def test_it_adds_admin_rights_to_a_user(self, app, user_1_admin, user_2):
 | 
			
		||||
    def test_it_adds_admin_rights_to_a_user(
 | 
			
		||||
        self, app: Flask, user_1_admin: User, user_2: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1006,8 +1022,8 @@ class TestUpdateUser:
 | 
			
		||||
        assert user['admin'] is True
 | 
			
		||||
 | 
			
		||||
    def test_it_removes_admin_rights_to_a_user(
 | 
			
		||||
        self, app, user_1_admin, user_2
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1_admin: User, user_2: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1037,8 +1053,8 @@ class TestUpdateUser:
 | 
			
		||||
        assert user['admin'] is False
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_payload_for_admin_rights_is_empty(
 | 
			
		||||
        self, app, user_1_admin, user_2
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1_admin: User, user_2: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1064,8 +1080,8 @@ class TestUpdateUser:
 | 
			
		||||
        assert 'Invalid payload.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_payload_for_admin_rights_is_invalid(
 | 
			
		||||
        self, app, user_1_admin, user_2
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1_admin: User, user_2: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1094,8 +1110,8 @@ class TestUpdateUser:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_if_user_can_not_change_admin_rights(
 | 
			
		||||
        self, app, user_1, user_2
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, user_2: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1120,7 +1136,9 @@ class TestUpdateUser:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestDeleteUser:
 | 
			
		||||
    def test_user_can_delete_its_own_account(self, app, user_1):
 | 
			
		||||
    def test_user_can_delete_its_own_account(
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1139,8 +1157,8 @@ class TestDeleteUser:
 | 
			
		||||
        assert response.status_code == 204
 | 
			
		||||
 | 
			
		||||
    def test_user_with_activity_can_delete_its_own_account(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1171,8 +1189,8 @@ class TestDeleteUser:
 | 
			
		||||
        assert response.status_code == 204
 | 
			
		||||
 | 
			
		||||
    def test_user_with_picture_can_delete_its_own_account(
 | 
			
		||||
        self, app, user_1, sport_1_cycling, gpx_file
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1200,8 +1218,8 @@ class TestDeleteUser:
 | 
			
		||||
        assert response.status_code == 204
 | 
			
		||||
 | 
			
		||||
    def test_user_can_not_delete_another_user_account(
 | 
			
		||||
        self, app, user_1, user_2
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User, user_2: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1223,8 +1241,8 @@ class TestDeleteUser:
 | 
			
		||||
        assert 'You do not have permissions.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_it_returns_error_when_deleting_non_existing_user(
 | 
			
		||||
        self, app, user_1
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1246,8 +1264,8 @@ class TestDeleteUser:
 | 
			
		||||
        assert 'User does not exist.' in data['message']
 | 
			
		||||
 | 
			
		||||
    def test_admin_can_delete_another_user_account(
 | 
			
		||||
        self, app, user_1_admin, user_2
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1_admin: User, user_2: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1268,8 +1286,8 @@ class TestDeleteUser:
 | 
			
		||||
        assert response.status_code == 204
 | 
			
		||||
 | 
			
		||||
    def test_admin_can_delete_its_own_account(
 | 
			
		||||
        self, app, user_1_admin, user_2_admin
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1_admin: User, user_2_admin: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1290,8 +1308,8 @@ class TestDeleteUser:
 | 
			
		||||
        assert response.status_code == 204
 | 
			
		||||
 | 
			
		||||
    def test_admin_can_not_delete_its_own_account_if_no_other_admin(
 | 
			
		||||
        self, app, user_1_admin, user_2
 | 
			
		||||
    ):
 | 
			
		||||
        self, app: Flask, user_1_admin: User, user_2: User
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        client = app.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
            '/api/auth/login',
 | 
			
		||||
@@ -1317,8 +1335,13 @@ class TestDeleteUser:
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_it_enables_registration_on_user_delete(
 | 
			
		||||
        self, app_no_config, app_config, user_1_admin, user_2, user_3
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app_no_config: Flask,
 | 
			
		||||
        app_config: Flask,
 | 
			
		||||
        user_1_admin: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        user_3: User,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        app_config.max_users = 3
 | 
			
		||||
        client = app_no_config.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
@@ -1351,8 +1374,13 @@ class TestDeleteUser:
 | 
			
		||||
        assert response.status_code == 201
 | 
			
		||||
 | 
			
		||||
    def test_it_does_not_enable_registration_on_user_delete(
 | 
			
		||||
        self, app_no_config, app_config, user_1_admin, user_2, user_3
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        app_no_config: Flask,
 | 
			
		||||
        app_config: Flask,
 | 
			
		||||
        user_1_admin: User,
 | 
			
		||||
        user_2: User,
 | 
			
		||||
        user_3: User,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        app_config.max_users = 2
 | 
			
		||||
        client = app_no_config.test_client()
 | 
			
		||||
        resp_login = client.post(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
from fittrackee.users.models import User
 | 
			
		||||
from flask import Flask
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestUserModel:
 | 
			
		||||
    def test_user_model(self, app, user_1):
 | 
			
		||||
    def test_user_model(self, app: Flask, user_1: User) -> None:
 | 
			
		||||
        assert '<User \'test\'>' == str(user_1)
 | 
			
		||||
 | 
			
		||||
        serialized_user = user_1.serialize()
 | 
			
		||||
@@ -23,15 +24,15 @@ class TestUserModel:
 | 
			
		||||
        assert serialized_user['total_distance'] == 0
 | 
			
		||||
        assert serialized_user['total_duration'] == '0:00:00'
 | 
			
		||||
 | 
			
		||||
    def test_encode_auth_token(self, app, user_1):
 | 
			
		||||
    def test_encode_auth_token(self, app: Flask, user_1: User) -> None:
 | 
			
		||||
        auth_token = user_1.encode_auth_token(user_1.id)
 | 
			
		||||
        assert isinstance(auth_token, str)
 | 
			
		||||
 | 
			
		||||
    def test_encode_password_token(self, app, user_1):
 | 
			
		||||
    def test_encode_password_token(self, app: Flask, user_1: User) -> None:
 | 
			
		||||
        password_token = user_1.encode_password_reset_token(user_1.id)
 | 
			
		||||
        assert isinstance(password_token, str)
 | 
			
		||||
 | 
			
		||||
    def test_decode_auth_token(self, app, user_1):
 | 
			
		||||
    def test_decode_auth_token(self, app: Flask, user_1: User) -> None:
 | 
			
		||||
        auth_token = user_1.encode_auth_token(user_1.id)
 | 
			
		||||
        assert isinstance(auth_token, str)
 | 
			
		||||
        assert User.decode_auth_token(auth_token) == user_1.id
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,12 @@
 | 
			
		||||
import datetime
 | 
			
		||||
import os
 | 
			
		||||
from typing import Dict, Tuple, Union
 | 
			
		||||
 | 
			
		||||
import jwt
 | 
			
		||||
from fittrackee import appLog, bcrypt, db
 | 
			
		||||
from fittrackee.responses import (
 | 
			
		||||
    ForbiddenErrorResponse,
 | 
			
		||||
    HttpResponse,
 | 
			
		||||
    InvalidPayloadErrorResponse,
 | 
			
		||||
    PayloadTooLargeErrorResponse,
 | 
			
		||||
    UnauthorizedErrorResponse,
 | 
			
		||||
@@ -32,7 +34,7 @@ auth_blueprint = Blueprint('auth', __name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@auth_blueprint.route('/auth/register', methods=['POST'])
 | 
			
		||||
def register_user():
 | 
			
		||||
def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    register a user
 | 
			
		||||
 | 
			
		||||
@@ -144,7 +146,7 @@ def register_user():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@auth_blueprint.route('/auth/login', methods=['POST'])
 | 
			
		||||
def login_user():
 | 
			
		||||
def login_user() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    user login
 | 
			
		||||
 | 
			
		||||
@@ -216,7 +218,7 @@ def login_user():
 | 
			
		||||
 | 
			
		||||
@auth_blueprint.route('/auth/logout', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def logout_user(auth_user_id):
 | 
			
		||||
def logout_user(auth_user_id: int) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    user logout
 | 
			
		||||
 | 
			
		||||
@@ -277,7 +279,9 @@ def logout_user(auth_user_id):
 | 
			
		||||
 | 
			
		||||
@auth_blueprint.route('/auth/profile', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_authenticated_user_profile(auth_user_id):
 | 
			
		||||
def get_authenticated_user_profile(
 | 
			
		||||
    auth_user_id: int,
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    get authenticated user info
 | 
			
		||||
 | 
			
		||||
@@ -338,7 +342,7 @@ def get_authenticated_user_profile(auth_user_id):
 | 
			
		||||
 | 
			
		||||
@auth_blueprint.route('/auth/profile/edit', methods=['POST'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def edit_user(auth_user_id):
 | 
			
		||||
def edit_user(auth_user_id: int) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    edit authenticated user
 | 
			
		||||
 | 
			
		||||
@@ -474,7 +478,7 @@ def edit_user(auth_user_id):
 | 
			
		||||
 | 
			
		||||
@auth_blueprint.route('/auth/picture', methods=['POST'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def edit_picture(auth_user_id):
 | 
			
		||||
def edit_picture(auth_user_id: int) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    update authenticated user picture
 | 
			
		||||
 | 
			
		||||
@@ -561,7 +565,7 @@ def edit_picture(auth_user_id):
 | 
			
		||||
 | 
			
		||||
@auth_blueprint.route('/auth/picture', methods=['DELETE'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def del_picture(auth_user_id):
 | 
			
		||||
def del_picture(auth_user_id: int) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    delete authenticated user picture
 | 
			
		||||
 | 
			
		||||
@@ -604,7 +608,7 @@ def del_picture(auth_user_id):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@auth_blueprint.route('/auth/password/reset-request', methods=['POST'])
 | 
			
		||||
def request_password_reset():
 | 
			
		||||
def request_password_reset() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    handle password reset request
 | 
			
		||||
 | 
			
		||||
@@ -644,15 +648,15 @@ def request_password_reset():
 | 
			
		||||
        ui_url = current_app.config['UI_URL']
 | 
			
		||||
        email_data = {
 | 
			
		||||
            'expiration_delay': get_readable_duration(
 | 
			
		||||
                current_app.config.get('PASSWORD_TOKEN_EXPIRATION_SECONDS'),
 | 
			
		||||
                current_app.config['PASSWORD_TOKEN_EXPIRATION_SECONDS'],
 | 
			
		||||
                'en' if user.language is None else user.language,
 | 
			
		||||
            ),
 | 
			
		||||
            'username': user.username,
 | 
			
		||||
            'password_reset_url': (
 | 
			
		||||
                f'{ui_url}/password-reset?token={password_reset_token}'  # noqa
 | 
			
		||||
            ),
 | 
			
		||||
            'operating_system': request.user_agent.platform,
 | 
			
		||||
            'browser_name': request.user_agent.browser,
 | 
			
		||||
            'operating_system': request.user_agent.platform,  # type: ignore
 | 
			
		||||
            'browser_name': request.user_agent.browser,  # type: ignore
 | 
			
		||||
        }
 | 
			
		||||
        user_data = {
 | 
			
		||||
            'language': user.language if user.language else 'en',
 | 
			
		||||
@@ -666,7 +670,7 @@ def request_password_reset():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@auth_blueprint.route('/auth/password/update', methods=['POST'])
 | 
			
		||||
def update_password():
 | 
			
		||||
def update_password() -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    update user password
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,22 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from typing import Dict, Optional, Union
 | 
			
		||||
 | 
			
		||||
import jwt
 | 
			
		||||
from fittrackee import bcrypt, db
 | 
			
		||||
from flask import current_app
 | 
			
		||||
from sqlalchemy import func
 | 
			
		||||
from sqlalchemy.ext.declarative import DeclarativeMeta
 | 
			
		||||
from sqlalchemy.ext.hybrid import hybrid_property
 | 
			
		||||
from sqlalchemy.sql.expression import select
 | 
			
		||||
 | 
			
		||||
from ..activities.models import Activity
 | 
			
		||||
from .utils_token import decode_user_token, get_user_token
 | 
			
		||||
 | 
			
		||||
BaseModel: DeclarativeMeta = db.Model
 | 
			
		||||
 | 
			
		||||
class User(db.Model):
 | 
			
		||||
    __tablename__ = "users"
 | 
			
		||||
 | 
			
		||||
class User(BaseModel):
 | 
			
		||||
    __tablename__ = 'users'
 | 
			
		||||
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
 | 
			
		||||
    username = db.Column(db.String(20), unique=True, nullable=False)
 | 
			
		||||
    email = db.Column(db.String(120), unique=True, nullable=False)
 | 
			
		||||
@@ -36,12 +40,16 @@ class User(db.Model):
 | 
			
		||||
    )
 | 
			
		||||
    language = db.Column(db.String(50), nullable=True)
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
    def __repr__(self) -> str:
 | 
			
		||||
        return f'<User {self.username!r}>'
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, username, email, password, created_at=datetime.utcnow()
 | 
			
		||||
    ):
 | 
			
		||||
        self,
 | 
			
		||||
        username: str,
 | 
			
		||||
        email: str,
 | 
			
		||||
        password: str,
 | 
			
		||||
        created_at: Optional[datetime] = datetime.utcnow(),
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        self.username = username
 | 
			
		||||
        self.email = email
 | 
			
		||||
        self.password = bcrypt.generate_password_hash(
 | 
			
		||||
@@ -50,31 +58,25 @@ class User(db.Model):
 | 
			
		||||
        self.created_at = created_at
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def encode_auth_token(user_id):
 | 
			
		||||
    def encode_auth_token(user_id: int) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Generates the auth token
 | 
			
		||||
        :param user_id: -
 | 
			
		||||
        :return: JWToken
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            return get_user_token(user_id)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            return e
 | 
			
		||||
        return get_user_token(user_id)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def encode_password_reset_token(user_id):
 | 
			
		||||
    def encode_password_reset_token(user_id: int) -> str:
 | 
			
		||||
        """
 | 
			
		||||
        Generates the auth token
 | 
			
		||||
        :param user_id: -
 | 
			
		||||
        :return: JWToken
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            return get_user_token(user_id, password_reset=True)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            return e
 | 
			
		||||
        return get_user_token(user_id, password_reset=True)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def decode_auth_token(auth_token):
 | 
			
		||||
    def decode_auth_token(auth_token: str) -> Union[int, str]:
 | 
			
		||||
        """
 | 
			
		||||
        Decodes the auth token
 | 
			
		||||
        :param auth_token: -
 | 
			
		||||
@@ -88,21 +90,21 @@ class User(db.Model):
 | 
			
		||||
            return 'Invalid token. Please log in again.'
 | 
			
		||||
 | 
			
		||||
    @hybrid_property
 | 
			
		||||
    def activities_count(self):
 | 
			
		||||
    def activities_count(self) -> int:
 | 
			
		||||
        return Activity.query.filter(Activity.user_id == self.id).count()
 | 
			
		||||
 | 
			
		||||
    @activities_count.expression
 | 
			
		||||
    def activities_count(self):
 | 
			
		||||
    @activities_count.expression  # type: ignore
 | 
			
		||||
    def activities_count(self) -> int:
 | 
			
		||||
        return (
 | 
			
		||||
            select([func.count(Activity.id)])
 | 
			
		||||
            .where(Activity.user_id == self.id)
 | 
			
		||||
            .label("activities_count")
 | 
			
		||||
            .label('activities_count')
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def serialize(self):
 | 
			
		||||
    def serialize(self) -> Dict:
 | 
			
		||||
        sports = []
 | 
			
		||||
        total = (None, None)
 | 
			
		||||
        if self.activities_count > 0:
 | 
			
		||||
        total = (0, '0:00:00')
 | 
			
		||||
        if self.activities_count > 0:  # type: ignore
 | 
			
		||||
            sports = (
 | 
			
		||||
                db.session.query(Activity.sport_id)
 | 
			
		||||
                .filter(Activity.user_id == self.id)
 | 
			
		||||
@@ -136,6 +138,6 @@ class User(db.Model):
 | 
			
		||||
            'sports_list': [
 | 
			
		||||
                sport for sportslist in sports for sport in sportslist
 | 
			
		||||
            ],
 | 
			
		||||
            'total_distance': float(total[0]) if total[0] else 0,
 | 
			
		||||
            'total_duration': str(total[1]) if total[1] else "0:00:00",
 | 
			
		||||
            'total_distance': float(total[0]),
 | 
			
		||||
            'total_duration': str(total[1]),
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
import os
 | 
			
		||||
import shutil
 | 
			
		||||
from typing import Any, Dict, Tuple, Union
 | 
			
		||||
 | 
			
		||||
from fittrackee import db
 | 
			
		||||
from fittrackee.responses import (
 | 
			
		||||
    ForbiddenErrorResponse,
 | 
			
		||||
    HttpResponse,
 | 
			
		||||
    InvalidPayloadErrorResponse,
 | 
			
		||||
    NotFoundErrorResponse,
 | 
			
		||||
    UserNotFoundErrorResponse,
 | 
			
		||||
@@ -23,7 +25,7 @@ USER_PER_PAGE = 10
 | 
			
		||||
 | 
			
		||||
@users_blueprint.route('/users', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_users(auth_user_id):
 | 
			
		||||
def get_users(auth_user_id: int) -> Dict:
 | 
			
		||||
    """
 | 
			
		||||
    Get all users
 | 
			
		||||
 | 
			
		||||
@@ -135,10 +137,10 @@ def get_users(auth_user_id):
 | 
			
		||||
            User.username.like('%' + query + '%') if query else True,
 | 
			
		||||
        )
 | 
			
		||||
        .order_by(
 | 
			
		||||
            User.activities_count.asc()
 | 
			
		||||
            User.activities_count.asc()  # type: ignore
 | 
			
		||||
            if order_by == 'activities_count' and order == 'asc'
 | 
			
		||||
            else True,
 | 
			
		||||
            User.activities_count.desc()
 | 
			
		||||
            User.activities_count.desc()  # type: ignore
 | 
			
		||||
            if order_by == 'activities_count' and order == 'desc'
 | 
			
		||||
            else True,
 | 
			
		||||
            User.username.asc()
 | 
			
		||||
@@ -178,7 +180,9 @@ def get_users(auth_user_id):
 | 
			
		||||
 | 
			
		||||
@users_blueprint.route('/users/<user_name>', methods=['GET'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def get_single_user(auth_user_id, user_name):
 | 
			
		||||
def get_single_user(
 | 
			
		||||
    auth_user_id: int, user_name: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Get single user details
 | 
			
		||||
 | 
			
		||||
@@ -251,7 +255,7 @@ def get_single_user(auth_user_id, user_name):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@users_blueprint.route('/users/<user_name>/picture', methods=['GET'])
 | 
			
		||||
def get_picture(user_name):
 | 
			
		||||
def get_picture(user_name: str) -> Any:
 | 
			
		||||
    """get user picture
 | 
			
		||||
 | 
			
		||||
    **Example request**:
 | 
			
		||||
@@ -290,7 +294,9 @@ def get_picture(user_name):
 | 
			
		||||
 | 
			
		||||
@users_blueprint.route('/users/<user_name>', methods=['PATCH'])
 | 
			
		||||
@authenticate_as_admin
 | 
			
		||||
def update_user(auth_user_id, user_name):
 | 
			
		||||
def update_user(
 | 
			
		||||
    auth_user_id: int, user_name: str
 | 
			
		||||
) -> Union[Dict, HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Update user to add admin rights
 | 
			
		||||
 | 
			
		||||
@@ -377,7 +383,9 @@ def update_user(auth_user_id, user_name):
 | 
			
		||||
 | 
			
		||||
@users_blueprint.route('/users/<user_name>', methods=['DELETE'])
 | 
			
		||||
@authenticate
 | 
			
		||||
def delete_user(auth_user_id, user_name):
 | 
			
		||||
def delete_user(
 | 
			
		||||
    auth_user_id: int, user_name: str
 | 
			
		||||
) -> Union[Tuple[Dict, int], HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Delete a user account
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,44 @@
 | 
			
		||||
import re
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from functools import wraps
 | 
			
		||||
from typing import Any, Callable, Optional, Tuple, Union
 | 
			
		||||
 | 
			
		||||
import humanize
 | 
			
		||||
from fittrackee.responses import (
 | 
			
		||||
    ForbiddenErrorResponse,
 | 
			
		||||
    HttpResponse,
 | 
			
		||||
    InvalidPayloadErrorResponse,
 | 
			
		||||
    PayloadTooLargeErrorResponse,
 | 
			
		||||
    UnauthorizedErrorResponse,
 | 
			
		||||
)
 | 
			
		||||
from flask import current_app, request
 | 
			
		||||
from flask import Request, current_app, request
 | 
			
		||||
 | 
			
		||||
from .models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_admin(user_id):
 | 
			
		||||
def is_admin(user_id: int) -> bool:
 | 
			
		||||
    """
 | 
			
		||||
    Return if user has admin rights
 | 
			
		||||
    """
 | 
			
		||||
    user = User.query.filter_by(id=user_id).first()
 | 
			
		||||
    return user.admin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_valid_email(email):
 | 
			
		||||
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, password_conf):
 | 
			
		||||
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 and password confirmation don\'t match.\n'
 | 
			
		||||
@@ -33,7 +47,14 @@ def check_passwords(password, password_conf):
 | 
			
		||||
    return ret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register_controls(username, email, password, password_conf):
 | 
			
		||||
def register_controls(
 | 
			
		||||
    username: str, email: str, password: str, password_conf: str
 | 
			
		||||
) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Verify if user name, email and passwords are valid
 | 
			
		||||
 | 
			
		||||
    If not, it returns not empty string
 | 
			
		||||
    """
 | 
			
		||||
    ret = ''
 | 
			
		||||
    if not 2 < len(username) < 13:
 | 
			
		||||
        ret += 'Username: 3 to 12 characters required.\n'
 | 
			
		||||
@@ -43,7 +64,12 @@ def register_controls(username, email, password, password_conf):
 | 
			
		||||
    return ret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def verify_extension_and_size(file_type, req):
 | 
			
		||||
def verify_extension_and_size(
 | 
			
		||||
    file_type: str, req: Request
 | 
			
		||||
) -> Optional[HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Return error Response if file is invalid
 | 
			
		||||
    """
 | 
			
		||||
    if 'file' not in req.files:
 | 
			
		||||
        return InvalidPayloadErrorResponse('No file part.', 'fail')
 | 
			
		||||
 | 
			
		||||
@@ -66,7 +92,7 @@ def verify_extension_and_size(file_type, req):
 | 
			
		||||
 | 
			
		||||
    if not (
 | 
			
		||||
        file_extension
 | 
			
		||||
        and file_extension in current_app.config.get(allowed_extensions)
 | 
			
		||||
        and file_extension in current_app.config[allowed_extensions]
 | 
			
		||||
    ):
 | 
			
		||||
        return InvalidPayloadErrorResponse(
 | 
			
		||||
            'File extension not allowed.', 'fail'
 | 
			
		||||
@@ -81,7 +107,13 @@ def verify_extension_and_size(file_type, req):
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def verify_user(current_request, verify_admin):
 | 
			
		||||
def verify_user(
 | 
			
		||||
    current_request: Request, verify_admin: bool
 | 
			
		||||
) -> Tuple[Optional[HttpResponse], Optional[int]]:
 | 
			
		||||
    """
 | 
			
		||||
    Return user id, if the provided token is valid and if 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:
 | 
			
		||||
@@ -98,9 +130,11 @@ def verify_user(current_request, verify_admin):
 | 
			
		||||
    return None, resp
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def authenticate(f):
 | 
			
		||||
def authenticate(f: Callable) -> Callable:
 | 
			
		||||
    @wraps(f)
 | 
			
		||||
    def decorated_function(*args, **kwargs):
 | 
			
		||||
    def decorated_function(
 | 
			
		||||
        *args: Any, **kwargs: Any
 | 
			
		||||
    ) -> Union[Callable, HttpResponse]:
 | 
			
		||||
        verify_admin = False
 | 
			
		||||
        response_object, resp = verify_user(request, verify_admin)
 | 
			
		||||
        if response_object:
 | 
			
		||||
@@ -110,9 +144,11 @@ def authenticate(f):
 | 
			
		||||
    return decorated_function
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def authenticate_as_admin(f):
 | 
			
		||||
def authenticate_as_admin(f: Callable) -> Callable:
 | 
			
		||||
    @wraps(f)
 | 
			
		||||
    def decorated_function(*args, **kwargs):
 | 
			
		||||
    def decorated_function(
 | 
			
		||||
        *args: Any, **kwargs: Any
 | 
			
		||||
    ) -> Union[Callable, HttpResponse]:
 | 
			
		||||
        verify_admin = True
 | 
			
		||||
        response_object, resp = verify_user(request, verify_admin)
 | 
			
		||||
        if response_object:
 | 
			
		||||
@@ -122,25 +158,36 @@ def authenticate_as_admin(f):
 | 
			
		||||
    return decorated_function
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def can_view_activity(auth_user_id, activity_user_id):
 | 
			
		||||
def can_view_activity(
 | 
			
		||||
    auth_user_id: int, activity_user_id: int
 | 
			
		||||
) -> Optional[HttpResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Return error response if user has no right to view activity
 | 
			
		||||
    """
 | 
			
		||||
    if auth_user_id != activity_user_id:
 | 
			
		||||
        return ForbiddenErrorResponse()
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def display_readable_file_size(size_in_bytes):
 | 
			
		||||
def display_readable_file_size(size_in_bytes: Union[float, int]) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Return readable file size from size in bytes
 | 
			
		||||
    """
 | 
			
		||||
    if size_in_bytes == 0:
 | 
			
		||||
        return '0 bytes'
 | 
			
		||||
    if size_in_bytes == 1:
 | 
			
		||||
        return '1 byte'
 | 
			
		||||
    for unit in [' bytes', 'KB', 'MB', 'GB', 'TB']:
 | 
			
		||||
        if abs(size_in_bytes) < 1024.0:
 | 
			
		||||
            return f"{size_in_bytes:3.1f}{unit}"
 | 
			
		||||
            return f'{size_in_bytes:3.1f}{unit}'
 | 
			
		||||
        size_in_bytes /= 1024.0
 | 
			
		||||
    return f"{size_in_bytes} bytes"
 | 
			
		||||
    return f'{size_in_bytes} bytes'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_readable_duration(duration, locale='en'):
 | 
			
		||||
def get_readable_duration(duration: int, locale: Optional[str] = 'en') -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Return readable and localized duration from duration in seconds
 | 
			
		||||
    """
 | 
			
		||||
    if locale is not None and locale != 'en':
 | 
			
		||||
        _t = humanize.i18n.activate(locale)  # noqa
 | 
			
		||||
    readable_duration = humanize.naturaldelta(timedelta(seconds=duration))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,25 @@
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
import jwt
 | 
			
		||||
from flask import current_app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_user_token(user_id, password_reset=False):
 | 
			
		||||
    expiration_days = (
 | 
			
		||||
        0
 | 
			
		||||
        if password_reset
 | 
			
		||||
        else current_app.config.get('TOKEN_EXPIRATION_DAYS')
 | 
			
		||||
def get_user_token(
 | 
			
		||||
    user_id: int, password_reset: Optional[bool] = False
 | 
			
		||||
) -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Return authentication token for a given user.
 | 
			
		||||
    Token expiration time depends on token type (authentication or password
 | 
			
		||||
    reset)
 | 
			
		||||
    """
 | 
			
		||||
    expiration_days: float = (
 | 
			
		||||
        0.0 if password_reset else current_app.config['TOKEN_EXPIRATION_DAYS']
 | 
			
		||||
    )
 | 
			
		||||
    expiration_seconds = (
 | 
			
		||||
        current_app.config.get('PASSWORD_TOKEN_EXPIRATION_SECONDS')
 | 
			
		||||
    expiration_seconds: float = (
 | 
			
		||||
        current_app.config['PASSWORD_TOKEN_EXPIRATION_SECONDS']
 | 
			
		||||
        if password_reset
 | 
			
		||||
        else current_app.config.get('TOKEN_EXPIRATION_SECONDS')
 | 
			
		||||
        else current_app.config['TOKEN_EXPIRATION_SECONDS']
 | 
			
		||||
    )
 | 
			
		||||
    payload = {
 | 
			
		||||
        'exp': datetime.utcnow()
 | 
			
		||||
@@ -23,15 +29,18 @@ def get_user_token(user_id, password_reset=False):
 | 
			
		||||
    }
 | 
			
		||||
    return jwt.encode(
 | 
			
		||||
        payload,
 | 
			
		||||
        current_app.config.get('SECRET_KEY'),
 | 
			
		||||
        current_app.config['SECRET_KEY'],
 | 
			
		||||
        algorithm='HS256',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def decode_user_token(auth_token):
 | 
			
		||||
def decode_user_token(auth_token: str) -> int:
 | 
			
		||||
    """
 | 
			
		||||
    Return user id from token
 | 
			
		||||
    """
 | 
			
		||||
    payload = jwt.decode(
 | 
			
		||||
        auth_token,
 | 
			
		||||
        current_app.config.get('SECRET_KEY'),
 | 
			
		||||
        current_app.config['SECRET_KEY'],
 | 
			
		||||
        algorithms=['HS256'],
 | 
			
		||||
    )
 | 
			
		||||
    return payload['sub']
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										99
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										99
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							@@ -396,7 +396,7 @@ python-versions = "*"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "isort"
 | 
			
		||||
version = "5.6.4"
 | 
			
		||||
version = "5.7.0"
 | 
			
		||||
description = "A Python utility / library to sort Python imports."
 | 
			
		||||
category = "dev"
 | 
			
		||||
optional = false
 | 
			
		||||
@@ -460,6 +460,22 @@ category = "dev"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = "*"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "mypy"
 | 
			
		||||
version = "0.790"
 | 
			
		||||
description = "Optional static typing for Python"
 | 
			
		||||
category = "dev"
 | 
			
		||||
optional = false
 | 
			
		||||
python-versions = ">=3.5"
 | 
			
		||||
 | 
			
		||||
[package.extras]
 | 
			
		||||
dmypy = ["psutil (>=4.0)"]
 | 
			
		||||
 | 
			
		||||
[package.dependencies]
 | 
			
		||||
mypy-extensions = ">=0.4.3,<0.5.0"
 | 
			
		||||
typed-ast = ">=1.4.0,<1.5.0"
 | 
			
		||||
typing-extensions = ">=3.7.4"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "mypy-extensions"
 | 
			
		||||
version = "0.4.3"
 | 
			
		||||
@@ -1095,7 +1111,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "tqdm"
 | 
			
		||||
version = "4.55.0"
 | 
			
		||||
version = "4.55.1"
 | 
			
		||||
description = "Fast, Extensible Progress Meter"
 | 
			
		||||
category = "main"
 | 
			
		||||
optional = false
 | 
			
		||||
@@ -1107,7 +1123,7 @@ telegram = ["requests"]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "typed-ast"
 | 
			
		||||
version = "1.4.1"
 | 
			
		||||
version = "1.4.2"
 | 
			
		||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
 | 
			
		||||
category = "dev"
 | 
			
		||||
optional = false
 | 
			
		||||
@@ -1162,7 +1178,7 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pyt
 | 
			
		||||
[metadata]
 | 
			
		||||
lock-version = "1.0"
 | 
			
		||||
python-versions = "^3.7"
 | 
			
		||||
content-hash = "b5c7cbb6e449c8c6cc94a7413ae2fc0b8889d738e3fc802566efc45cc5096287"
 | 
			
		||||
content-hash = "11c3a22ee800d71d1d9804e33e756be81c5b7c56bba85d7de03a0f27acb35148"
 | 
			
		||||
 | 
			
		||||
[metadata.files]
 | 
			
		||||
alabaster = [
 | 
			
		||||
@@ -1388,8 +1404,8 @@ iniconfig = [
 | 
			
		||||
    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
 | 
			
		||||
]
 | 
			
		||||
isort = [
 | 
			
		||||
    {file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"},
 | 
			
		||||
    {file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"},
 | 
			
		||||
    {file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"},
 | 
			
		||||
    {file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"},
 | 
			
		||||
]
 | 
			
		||||
itsdangerous = [
 | 
			
		||||
    {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
 | 
			
		||||
@@ -1442,6 +1458,22 @@ mccabe = [
 | 
			
		||||
    {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
 | 
			
		||||
    {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
 | 
			
		||||
]
 | 
			
		||||
mypy = [
 | 
			
		||||
    {file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"},
 | 
			
		||||
    {file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"},
 | 
			
		||||
    {file = "mypy-0.790-cp35-cp35m-win_amd64.whl", hash = "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de"},
 | 
			
		||||
    {file = "mypy-0.790-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1"},
 | 
			
		||||
    {file = "mypy-0.790-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc"},
 | 
			
		||||
    {file = "mypy-0.790-cp36-cp36m-win_amd64.whl", hash = "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7"},
 | 
			
		||||
    {file = "mypy-0.790-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c"},
 | 
			
		||||
    {file = "mypy-0.790-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178"},
 | 
			
		||||
    {file = "mypy-0.790-cp37-cp37m-win_amd64.whl", hash = "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324"},
 | 
			
		||||
    {file = "mypy-0.790-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01"},
 | 
			
		||||
    {file = "mypy-0.790-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666"},
 | 
			
		||||
    {file = "mypy-0.790-cp38-cp38-win_amd64.whl", hash = "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea"},
 | 
			
		||||
    {file = "mypy-0.790-py3-none-any.whl", hash = "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122"},
 | 
			
		||||
    {file = "mypy-0.790.tar.gz", hash = "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975"},
 | 
			
		||||
]
 | 
			
		||||
mypy-extensions = [
 | 
			
		||||
    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
 | 
			
		||||
    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
 | 
			
		||||
@@ -1781,31 +1813,40 @@ toml = [
 | 
			
		||||
    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
 | 
			
		||||
]
 | 
			
		||||
tqdm = [
 | 
			
		||||
    {file = "tqdm-4.55.0-py2.py3-none-any.whl", hash = "sha256:0cd81710de29754bf17b6fee07bdb86f956b4fa20d3078f02040f83e64309416"},
 | 
			
		||||
    {file = "tqdm-4.55.0.tar.gz", hash = "sha256:f4f80b96e2ceafea69add7bf971b8403b9cba8fb4451c1220f91c79be4ebd208"},
 | 
			
		||||
    {file = "tqdm-4.55.1-py2.py3-none-any.whl", hash = "sha256:b8b46036fd00176d0870307123ef06bb851096964fa7fc578d789f90ce82c3e4"},
 | 
			
		||||
    {file = "tqdm-4.55.1.tar.gz", hash = "sha256:556c55b081bd9aa746d34125d024b73f0e2a0e62d5927ff0e400e20ee0a03b9a"},
 | 
			
		||||
]
 | 
			
		||||
typed-ast = [
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
 | 
			
		||||
    {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
 | 
			
		||||
    {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"},
 | 
			
		||||
    {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"},
 | 
			
		||||
    {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"},
 | 
			
		||||
]
 | 
			
		||||
typing-extensions = [
 | 
			
		||||
    {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,7 @@ tqdm = "^4.55"
 | 
			
		||||
[tool.poetry.dev-dependencies]
 | 
			
		||||
black = "^20.8b1"
 | 
			
		||||
freezegun = "^1.0.0"
 | 
			
		||||
mypy = "^0.790"
 | 
			
		||||
pyopenssl = "^20.0"
 | 
			
		||||
pytest = "^6.2"
 | 
			
		||||
pytest-black = "^0.3.12"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user