API - add typing

This commit is contained in:
Sam 2021-01-02 19:28:03 +01:00
parent 4705393a08
commit 634d06b05a
53 changed files with 1884 additions and 1075 deletions

View File

@ -33,6 +33,11 @@ lint:
script: script:
- pytest --flake8 --isort --black -m "flake8 or isort or black" fittrackee e2e --ignore=fittrackee/migrations - 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: python-3.7:
extends: .python extends: .python
image: python:3.7 image: python:3.7

View File

@ -9,10 +9,13 @@ make-p:
build-client: lint-client build-client: lint-client
cd fittrackee_client && $(NPM) build cd fittrackee_client && $(NPM) build
check-all: lint-all type-check test-python
clean-install: clean-install:
rm -fr $(NODE_MODULES) rm -fr $(NODE_MODULES)
rm -fr $(VENV) rm -fr $(VENV)
rm -rf *.egg-info rm -rf *.egg-info
rm -rf .mypy_cache
rm -rf .pytest_cache rm -rf .pytest_cache
rm -rf dist/ rm -rf dist/
@ -61,18 +64,18 @@ lint-all: lint-python lint-client
lint-all-fix: lint-python-fix lint-client-fix 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: lint-client:
cd fittrackee_client && $(NPM) lint cd fittrackee_client && $(NPM) lint
lint-client-fix: lint-client-fix:
cd fittrackee_client && $(NPM) lint-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: mail:
docker run -d -e "MH_STORAGE=maildir" -v /tmp/maildir:/maildir -p 1025:1025 -p 8025:8025 mailhog/mailhog 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: run-workers:
$(FLASK) worker --processes=$(WORKERS_PROCESSES) >> dramatiq.log 2>&1 $(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: serve:
$(MAKE) P="serve-client serve-python" make-p $(MAKE) P="serve-client serve-python" make-p
serve-dev: serve-dev:
$(MAKE) P="serve-client serve-python-dev" make-p $(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 test-e2e: init-db
$(PYTEST) e2e --driver firefox $(PYTEST_ARGS) $(PYTEST) e2e --driver firefox $(PYTEST_ARGS)
@ -115,5 +118,9 @@ test-e2e-client: init-db
test-python: test-python:
$(PYTEST) fittrackee --cov-config .coveragerc --cov=fittrackee --cov-report term-missing $(PYTEST_ARGS) $(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: upgrade-db:
$(FLASK) db upgrade --directory $(MIGRATIONS) $(FLASK) db upgrade --directory $(MIGRATIONS)

View File

@ -25,6 +25,7 @@ FLASK = $(VENV)/bin/flask
PYTEST = $(VENV)/bin/py.test -c pyproject.toml -W ignore::DeprecationWarning PYTEST = $(VENV)/bin/py.test -c pyproject.toml -W ignore::DeprecationWarning
GUNICORN = $(VENV)/bin/gunicorn GUNICORN = $(VENV)/bin/gunicorn
BLACK = $(VENV)/bin/black BLACK = $(VENV)/bin/black
MYPY = $(VENV)/bin/mypy
# Node env # Node env
NODE_MODULES = $(PWD)/fittrackee_client/node_modules NODE_MODULES = $(PWD)/fittrackee_client/node_modules

View File

@ -1,8 +1,9 @@
import logging import logging
import os import os
from importlib import import_module, reload 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_bcrypt import Bcrypt
from flask_dramatiq import Dramatiq from flask_dramatiq import Dramatiq
from flask_migrate import Migrate from flask_migrate import Migrate
@ -24,7 +25,7 @@ logging.basicConfig(
appLog = logging.getLogger('fittrackee') appLog = logging.getLogger('fittrackee')
def create_app(): def create_app() -> Flask:
# instantiate the app # instantiate the app
app = Flask(__name__, static_folder='dist/static', template_folder='dist') app = Flask(__name__, static_folder='dist/static', template_folder='dist')
@ -88,7 +89,7 @@ def create_app():
# Enable CORS # Enable CORS
@app.after_request @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-Origin', '*')
response.headers.add( response.headers.add(
'Access-Control-Allow-Headers', 'Content-Type,Authorization' 'Access-Control-Allow-Headers', 'Content-Type,Authorization'
@ -100,15 +101,23 @@ def create_app():
return response return response
@app.route('/favicon.ico') @app.route('/favicon.ico')
def favicon(): def favicon() -> Any:
return send_file(os.path.join(app.root_path, 'dist/favicon.ico')) return send_file(
os.path.join(app.root_path, 'dist/favicon.ico') # type: ignore
)
@app.route('/', defaults={'path': ''}) @app.route('/', defaults={'path': ''})
@app.route('/<path:path>') @app.route('/<path:path>')
def catch_all(path): def catch_all(path: str) -> Any:
# workaround to serve images (not in static directory) # workaround to serve images (not in static directory)
if path.startswith('img/'): 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: else:
return render_template('index.html') return render_template('index.html')

View File

@ -2,6 +2,7 @@
# http://docs.gunicorn.org/en/stable/custom.html # http://docs.gunicorn.org/en/stable/custom.html
import os import os
import shutil import shutil
from typing import Dict, Optional
import gunicorn.app.base import gunicorn.app.base
from fittrackee import create_app, db 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.activities.utils import update_activity
from fittrackee.application.utils import init_config from fittrackee.application.utils import init_config
from fittrackee.database_utils import init_database from fittrackee.database_utils import init_database
from flask import Flask
from flask_dramatiq import worker from flask_dramatiq import worker
from flask_migrate import upgrade from flask_migrate import upgrade
from tqdm import tqdm from tqdm import tqdm
@ -22,12 +24,14 @@ dramatiq_worker = worker
class StandaloneApplication(gunicorn.app.base.BaseApplication): 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.options = options or {}
self.application = current_app self.application = current_app
super().__init__() super().__init__()
def load_config(self): def load_config(self) -> None:
config = { config = {
key: value key: value
for key, value in self.options.items() for key, value in self.options.items()
@ -36,17 +40,17 @@ class StandaloneApplication(gunicorn.app.base.BaseApplication):
for key, value in config.items(): for key, value in config.items():
self.cfg.set(key.lower(), value) self.cfg.set(key.lower(), value)
def load(self): def load(self) -> Flask:
return self.application return self.application
def upgrade_db(): def upgrade_db() -> None:
with app.app_context(): with app.app_context():
upgrade(directory=BASEDIR + '/migrations') upgrade(directory=BASEDIR + '/migrations')
@app.cli.command('drop-db') @app.cli.command('drop-db')
def drop_db(): def drop_db() -> None:
"""Empty database for dev environments.""" """Empty database for dev environments."""
db.engine.execute("DROP TABLE IF EXISTS alembic_version;") db.engine.execute("DROP TABLE IF EXISTS alembic_version;")
db.drop_all() db.drop_all()
@ -57,13 +61,13 @@ def drop_db():
@app.cli.command('init-data') @app.cli.command('init-data')
def init_data(): def init_data() -> None:
"""Init the database and application config.""" """Init the database and application config."""
init_database(app) init_database(app)
@app.cli.command() @app.cli.command()
def recalculate(): def recalculate() -> None:
print("Starting activities data refresh") print("Starting activities data refresh")
activities = ( activities = (
Activity.query.filter(Activity.gpx != None) # noqa Activity.query.filter(Activity.gpx != None) # noqa
@ -81,7 +85,7 @@ def recalculate():
@app.cli.command('init-app-config') @app.cli.command('init-app-config')
def init_app_config(): def init_app_config() -> None:
"""Init application configuration.""" """Init application configuration."""
print("Init application configuration") print("Init application configuration")
config_created, _ = init_config() config_created, _ = init_config()
@ -94,7 +98,7 @@ def init_app_config():
) )
def main(): def main() -> None:
options = {'bind': f'{HOST}:{PORT}', 'workers': WORKERS} options = {'bind': f'{HOST}:{PORT}', 'workers': WORKERS}
StandaloneApplication(app, options).run() StandaloneApplication(app, options).run()

View File

@ -2,12 +2,14 @@ import json
import os import os
import shutil import shutil
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple, Union
import requests import requests
from fittrackee import appLog, db from fittrackee import appLog, db
from fittrackee.responses import ( from fittrackee.responses import (
DataInvalidPayloadErrorResponse, DataInvalidPayloadErrorResponse,
DataNotFoundErrorResponse, DataNotFoundErrorResponse,
HttpResponse,
InternalServerErrorResponse, InternalServerErrorResponse,
InvalidPayloadErrorResponse, InvalidPayloadErrorResponse,
NotFoundErrorResponse, NotFoundErrorResponse,
@ -46,7 +48,7 @@ ACTIVITIES_PER_PAGE = 5
@activities_blueprint.route('/activities', methods=['GET']) @activities_blueprint.route('/activities', methods=['GET'])
@authenticate @authenticate
def get_activities(auth_user_id): def get_activities(auth_user_id: int) -> Union[Dict, HttpResponse]:
""" """
Get activities for the authenticated user. Get activities for the authenticated user.
@ -275,7 +277,9 @@ def get_activities(auth_user_id):
'/activities/<string:activity_short_id>', methods=['GET'] '/activities/<string:activity_short_id>', methods=['GET']
) )
@authenticate @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 Get an activity
@ -375,8 +379,11 @@ def get_activity(auth_user_id, activity_short_id):
def get_activity_data( 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""" """Get data from an activity gpx file"""
activity_uuid = decode_short_id(activity_short_id) activity_uuid = decode_short_id(activity_short_id)
activity = Activity.query.filter_by(uuid=activity_uuid).first() activity = Activity.query.filter_by(uuid=activity_uuid).first()
@ -396,14 +403,17 @@ def get_activity_data(
try: try:
absolute_gpx_filepath = get_absolute_file_path(activity.gpx) absolute_gpx_filepath = get_absolute_file_path(activity.gpx)
chart_data_content: Optional[List] = []
if data_type == 'chart_data': 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' else: # data_type == 'gpx'
with open(absolute_gpx_filepath, encoding='utf-8') as f: with open(absolute_gpx_filepath, encoding='utf-8') as f:
content = f.read() gpx_content = f.read()
if segment_id is not None: if segment_id is not None:
content = extract_segment_from_gpx_file( gpx_segment_content = extract_segment_from_gpx_file(
content, segment_id gpx_content, segment_id
) )
except ActivityGPXException as e: except ActivityGPXException as e:
appLog.error(e.message) appLog.error(e.message)
@ -416,7 +426,15 @@ def get_activity_data(
return { return {
'status': 'success', 'status': 'success',
'message': '', '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'] '/activities/<string:activity_short_id>/gpx', methods=['GET']
) )
@authenticate @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 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'] '/activities/<string:activity_short_id>/chart_data', methods=['GET']
) )
@authenticate @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 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'], methods=['GET'],
) )
@authenticate @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 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'], methods=['GET'],
) )
@authenticate @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 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']) @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 Get map image for activities with gpx
@ -704,7 +730,7 @@ def get_map(map_id):
@activities_blueprint.route( @activities_blueprint.route(
'/activities/map_tile/<s>/<z>/<x>/<y>.png', methods=['GET'] '/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. Get map tile from tile server.
@ -743,7 +769,7 @@ def get_map_tile(s, z, x, y):
@activities_blueprint.route('/activities', methods=['POST']) @activities_blueprint.route('/activities', methods=['POST'])
@authenticate @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 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']) @activities_blueprint.route('/activities/no_gpx', methods=['POST'])
@authenticate @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 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'] '/activities/<string:activity_short_id>', methods=['PATCH']
) )
@authenticate @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 Update an activity
@ -1199,7 +1229,9 @@ def update_activity(auth_user_id, activity_short_id):
'/activities/<string:activity_short_id>', methods=['DELETE'] '/activities/<string:activity_short_id>', methods=['DELETE']
) )
@authenticate @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 Delete an activity

View File

@ -1,18 +1,23 @@
import datetime import datetime
import os import os
from uuid import uuid4 from typing import Any, Dict, Optional, Union
from uuid import UUID, uuid4
from fittrackee import db from fittrackee import db
from sqlalchemy.dialects import postgresql from sqlalchemy.dialects import postgresql
from sqlalchemy.engine.base import Connection
from sqlalchemy.event import listens_for from sqlalchemy.event import listens_for
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.ext.hybrid import hybrid_property 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 sqlalchemy.types import JSON, Enum
from .utils_files import get_absolute_file_path from .utils_files import get_absolute_file_path
from .utils_format import convert_in_duration, convert_value_to_integer from .utils_format import convert_in_duration, convert_value_to_integer
from .utils_id import encode_uuid from .utils_id import encode_uuid
BaseModel: DeclarativeMeta = db.Model
record_types = [ record_types = [
'AS', # 'Best Average Speed' 'AS', # 'Best Average Speed'
'FD', # 'Farthest Distance' '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__ record_table = Record.__table__
new_records = Activity.get_user_activity_records(user_id, sport_id) new_records = Activity.get_user_activity_records(user_id, sport_id)
for record_type, record_data in new_records.items(): 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( new_record = Record(
activity=record_data['activity'], record_type=record_type 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) session.add(new_record)
else: else:
connection.execute( connection.execute(
@ -58,7 +65,7 @@ def update_records(user_id, sport_id, connection, session):
) )
class Sport(db.Model): class Sport(BaseModel):
__tablename__ = "sports" __tablename__ = "sports"
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
label = db.Column(db.String(50), unique=True, nullable=False) 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') 'Record', lazy=True, backref=db.backref('sports', lazy='joined')
) )
def __repr__(self): def __repr__(self) -> str:
return f'<Sport {self.label!r}>' return f'<Sport {self.label!r}>'
def __init__(self, label): def __init__(self, label: str) -> None:
self.label = label self.label = label
def serialize(self, is_admin=False): def serialize(self, is_admin: Optional[bool] = False) -> Dict:
serialized_sport = { serialized_sport = {
'id': self.id, 'id': self.id,
'label': self.label, 'label': self.label,
@ -89,7 +96,7 @@ class Sport(db.Model):
return serialized_sport return serialized_sport
class Activity(db.Model): class Activity(BaseModel):
__tablename__ = "activities" __tablename__ = "activities"
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
uuid = db.Column( uuid = db.Column(
@ -138,10 +145,17 @@ class Activity(db.Model):
backref=db.backref('activities', lazy='joined', single_parent=True), 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}>' 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.user_id = user_id
self.sport_id = sport_id self.sport_id = sport_id
self.activity_date = activity_date self.activity_date = activity_date
@ -149,10 +163,10 @@ class Activity(db.Model):
self.duration = duration self.duration = duration
@property @property
def short_id(self): def short_id(self) -> str:
return encode_uuid(self.uuid) 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_from = params.get('from') if params else None
date_to = params.get('to') if params else None date_to = params.get('to') if params else None
distance_from = params.get('distance_from') if params else None distance_from = params.get('distance_from') if params else None
@ -239,41 +253,43 @@ class Activity(db.Model):
.first() .first()
) )
return { return {
"id": self.short_id, # WARNING: client use uuid as id 'id': self.short_id, # WARNING: client use uuid as id
"user": self.user.username, 'user': self.user.username,
"sport_id": self.sport_id, 'sport_id': self.sport_id,
"title": self.title, 'title': self.title,
"creation_date": self.creation_date, 'creation_date': self.creation_date,
"modification_date": self.modification_date, 'modification_date': self.modification_date,
"activity_date": self.activity_date, 'activity_date': self.activity_date,
"duration": str(self.duration) if self.duration else None, 'duration': str(self.duration) if self.duration else None,
"pauses": str(self.pauses) if self.pauses else None, 'pauses': str(self.pauses) if self.pauses else None,
"moving": str(self.moving) if self.moving else None, 'moving': str(self.moving) if self.moving else None,
"distance": float(self.distance) if self.distance else None, 'distance': float(self.distance) if self.distance else None,
"min_alt": float(self.min_alt) if self.min_alt 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, 'max_alt': float(self.max_alt) if self.max_alt else None,
"descent": float(self.descent) if self.descent else None, 'descent': float(self.descent) if self.descent else None,
"ascent": float(self.ascent) if self.ascent else None, 'ascent': float(self.ascent) if self.ascent else None,
"max_speed": float(self.max_speed) if self.max_speed 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, 'ave_speed': float(self.ave_speed) if self.ave_speed else None,
"with_gpx": self.gpx is not None, 'with_gpx': self.gpx is not None,
"bounds": [float(bound) for bound in self.bounds] 'bounds': [float(bound) for bound in self.bounds]
if self.bounds if self.bounds
else [], # noqa else [], # noqa
"previous_activity": previous_activity.short_id 'previous_activity': previous_activity.short_id
if previous_activity if previous_activity
else None, # noqa else None, # noqa
"next_activity": next_activity.short_id if next_activity else None, 'next_activity': next_activity.short_id if next_activity else None,
"segments": [segment.serialize() for segment in self.segments], 'segments': [segment.serialize() for segment in self.segments],
"records": [record.serialize() for record in self.records], 'records': [record.serialize() for record in self.records],
"map": self.map_id if self.map else None, 'map': self.map_id if self.map else None,
"weather_start": self.weather_start, 'weather_start': self.weather_start,
"weather_end": self.weather_end, 'weather_end': self.weather_end,
"notes": self.notes, 'notes': self.notes,
} }
@classmethod @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 = { record_types_columns = {
'AS': 'ave_speed', # 'Average speed' 'AS': 'ave_speed', # 'Average speed'
'FD': 'distance', # 'Farthest Distance' 'FD': 'distance', # 'Farthest Distance'
@ -300,22 +316,26 @@ class Activity(db.Model):
@listens_for(Activity, 'after_insert') @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) @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( update_records(
activity.user_id, activity.sport_id, connection, session activity.user_id, activity.sport_id, connection, session
) # noqa ) # noqa
@listens_for(Activity, 'after_update') @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( if object_session(activity).is_modified(
activity, include_collections=True activity, include_collections=True
): # noqa ): # noqa
@listens_for(db.Session, 'after_flush', once=True) @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] sports_list = [activity.sport_id]
records = Record.query.filter_by(activity_id=activity.id).all() records = Record.query.filter_by(activity_id=activity.id).all()
for rec in records: for rec in records:
@ -326,16 +346,18 @@ def on_activity_update(mapper, connection, activity):
@listens_for(Activity, 'after_delete') @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) @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: if old_record.map:
os.remove(get_absolute_file_path(old_record.map)) os.remove(get_absolute_file_path(old_record.map))
if old_record.gpx: if old_record.gpx:
os.remove(get_absolute_file_path(old_record.gpx)) os.remove(get_absolute_file_path(old_record.gpx))
class ActivitySegment(db.Model): class ActivitySegment(BaseModel):
__tablename__ = "activity_segments" __tablename__ = "activity_segments"
activity_id = db.Column( activity_id = db.Column(
db.Integer, db.ForeignKey('activities.id'), primary_key=True 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 max_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
ave_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 ( return (
f'<Segment \'{self.segment_id}\' ' f'<Segment \'{self.segment_id}\' '
f'for activity \'{encode_uuid(self.activity_uuid)}\'>' 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.segment_id = segment_id
self.activity_id = activity_id self.activity_id = activity_id
self.activity_uuid = activity_uuid self.activity_uuid = activity_uuid
def serialize(self): def serialize(self) -> Dict:
return { return {
"activity_id": encode_uuid(self.activity_uuid), 'activity_id': encode_uuid(self.activity_uuid),
"segment_id": self.segment_id, 'segment_id': self.segment_id,
"duration": str(self.duration) if self.duration else None, 'duration': str(self.duration) if self.duration else None,
"pauses": str(self.pauses) if self.pauses else None, 'pauses': str(self.pauses) if self.pauses else None,
"moving": str(self.moving) if self.moving else None, 'moving': str(self.moving) if self.moving else None,
"distance": float(self.distance) if self.distance else None, 'distance': float(self.distance) if self.distance else None,
"min_alt": float(self.min_alt) if self.min_alt 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, 'max_alt': float(self.max_alt) if self.max_alt else None,
"descent": float(self.descent) if self.descent else None, 'descent': float(self.descent) if self.descent else None,
"ascent": float(self.ascent) if self.ascent else None, 'ascent': float(self.ascent) if self.ascent else None,
"max_speed": float(self.max_speed) if self.max_speed 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, 'ave_speed': float(self.ave_speed) if self.ave_speed else None,
} }
class Record(db.Model): class Record(BaseModel):
__tablename__ = "records" __tablename__ = "records"
__table_args__ = ( __table_args__ = (
db.UniqueConstraint( db.UniqueConstraint(
@ -401,14 +425,14 @@ class Record(db.Model):
activity_date = db.Column(db.DateTime, nullable=False) activity_date = db.Column(db.DateTime, nullable=False)
_value = db.Column("value", db.Integer, nullable=True) _value = db.Column("value", db.Integer, nullable=True)
def __str__(self): def __str__(self) -> str:
return ( return (
f'<Record {self.sports.label} - ' f'<Record {self.sports.label} - '
f'{self.record_type} - ' f'{self.record_type} - '
f"{self.activity_date.strftime('%Y-%m-%d')}>" 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.user_id = activity.user_id
self.sport_id = activity.sport_id self.sport_id = activity.sport_id
self.activity_id = activity.id self.activity_id = activity.id
@ -417,7 +441,7 @@ class Record(db.Model):
self.activity_date = activity.activity_date self.activity_date = activity.activity_date
@hybrid_property @hybrid_property
def value(self): def value(self) -> Optional[Union[datetime.timedelta, float]]:
if self._value is None: if self._value is None:
return None return None
if self.record_type == 'LD': if self.record_type == 'LD':
@ -427,33 +451,35 @@ class Record(db.Model):
else: # 'FD' else: # 'FD'
return float(self._value / 1000) return float(self._value / 1000)
@value.setter @value.setter # type: ignore
def value(self, val): def value(self, val: Union[str, float]) -> None:
self._value = convert_value_to_integer(self.record_type, val) self._value = convert_value_to_integer(self.record_type, val)
def serialize(self): def serialize(self) -> Dict:
if self.value is None: if self.value is None:
value = None value = None
elif self.record_type in ['AS', 'FD', 'MS']: elif self.record_type in ['AS', 'FD', 'MS']:
value = float(self.value) value = float(self.value) # type: ignore
else: # 'LD' else: # 'LD'
value = str(self.value) value = str(self.value) # type: ignore
return { return {
"id": self.id, 'id': self.id,
"user": self.user.username, 'user': self.user.username,
"sport_id": self.sport_id, 'sport_id': self.sport_id,
"activity_id": encode_uuid(self.activity_uuid), 'activity_id': encode_uuid(self.activity_uuid),
"record_type": self.record_type, 'record_type': self.record_type,
"activity_date": self.activity_date, 'activity_date': self.activity_date,
"value": value, 'value': value,
} }
@listens_for(Record, 'after_delete') @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) @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 activity = old_record.activities
new_records = Activity.get_user_activity_records( new_records = Activity.get_user_activity_records(
activity.user_id, activity.sport_id activity.user_id, activity.sport_id
@ -466,5 +492,5 @@ def on_record_delete(mapper, connection, old_record):
new_record = Record( new_record = Record(
activity=record_data['activity'], record_type=record_type 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) session.add(new_record)

View File

@ -1,3 +1,5 @@
from typing import Dict
from flask import Blueprint from flask import Blueprint
from ..users.utils import authenticate from ..users.utils import authenticate
@ -8,7 +10,7 @@ records_blueprint = Blueprint('records', __name__)
@records_blueprint.route('/records', methods=['GET']) @records_blueprint.route('/records', methods=['GET'])
@authenticate @authenticate
def get_records(auth_user_id): def get_records(auth_user_id: int) -> Dict:
""" """
Get all records for authenticated user. Get all records for authenticated user.

View File

@ -1,6 +1,9 @@
from typing import Dict, Union
from fittrackee import db from fittrackee import db
from fittrackee.responses import ( from fittrackee.responses import (
DataNotFoundErrorResponse, DataNotFoundErrorResponse,
HttpResponse,
InvalidPayloadErrorResponse, InvalidPayloadErrorResponse,
handle_error_and_return_response, handle_error_and_return_response,
) )
@ -16,7 +19,7 @@ sports_blueprint = Blueprint('sports', __name__)
@sports_blueprint.route('/sports', methods=['GET']) @sports_blueprint.route('/sports', methods=['GET'])
@authenticate @authenticate
def get_sports(auth_user_id): def get_sports(auth_user_id: int) -> Dict:
""" """
Get all sports Get all sports
@ -158,7 +161,7 @@ def get_sports(auth_user_id):
@sports_blueprint.route('/sports/<int:sport_id>', methods=['GET']) @sports_blueprint.route('/sports/<int:sport_id>', methods=['GET'])
@authenticate @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 Get a sport
@ -253,7 +256,9 @@ def get_sport(auth_user_id, sport_id):
@sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH']) @sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH'])
@authenticate_as_admin @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 Update a sport
Authenticated user must be an admin Authenticated user must be an admin

View File

@ -1,7 +1,9 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict, Union
from fittrackee import db from fittrackee import db
from fittrackee.responses import ( from fittrackee.responses import (
HttpResponse,
InvalidPayloadErrorResponse, InvalidPayloadErrorResponse,
NotFoundErrorResponse, NotFoundErrorResponse,
UserNotFoundErrorResponse, UserNotFoundErrorResponse,
@ -19,7 +21,12 @@ from .utils_format import convert_timedelta_to_integer
stats_blueprint = Blueprint('stats', __name__) 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: try:
user = User.query.filter_by(username=user_name).first() user = User.query.filter_by(username=user_name).first()
if not user: if not user:
@ -40,7 +47,6 @@ def get_activities(user_name, filter_type):
time = params.get('time') time = params.get('time')
if filter_type == 'by_sport': if filter_type == 'by_sport':
sport_id = params.get('sport_id')
if sport_id: if sport_id:
sport = Sport.query.filter_by(id=sport_id).first() sport = Sport.query.filter_by(id=sport_id).first()
if not sport: if not sport:
@ -59,24 +65,26 @@ def get_activities(user_name, filter_type):
.all() .all()
) )
activities_list = {} activities_list_by_sport = {}
activities_list_by_time = {} # type: ignore
for activity in activities: for activity in activities:
if filter_type == 'by_sport': if filter_type == 'by_sport':
sport_id = activity.sport_id sport_id = activity.sport_id
if sport_id not in activities_list: if sport_id not in activities_list_by_sport:
activities_list[sport_id] = { activities_list_by_sport[sport_id] = {
'nb_activities': 0, 'nb_activities': 0,
'total_distance': 0.0, 'total_distance': 0.0,
'total_duration': 0, 'total_duration': 0,
} }
activities_list[sport_id]['nb_activities'] += 1 activities_list_by_sport[sport_id]['nb_activities'] += 1
activities_list[sport_id]['total_distance'] += float( activities_list_by_sport[sport_id]['total_distance'] += float(
activity.distance activity.distance
) )
activities_list[sport_id][ activities_list_by_sport[sport_id][
'total_duration' 'total_duration'
] += convert_timedelta_to_integer(activity.moving) ] += convert_timedelta_to_integer(activity.moving)
# filter_type == 'by_time'
else: else:
if time == 'week': if time == 'week':
activity_date = activity.activity_date - timedelta( activity_date = activity.activity_date - timedelta(
@ -105,25 +113,31 @@ def get_activities(user_name, filter_type):
'Invalid time period.', 'fail' 'Invalid time period.', 'fail'
) )
sport_id = activity.sport_id sport_id = activity.sport_id
if time_period not in activities_list: if time_period not in activities_list_by_time:
activities_list[time_period] = {} activities_list_by_time[time_period] = {}
if sport_id not in activities_list[time_period]: if sport_id not in activities_list_by_time[time_period]:
activities_list[time_period][sport_id] = { activities_list_by_time[time_period][sport_id] = {
'nb_activities': 0, 'nb_activities': 0,
'total_distance': 0.0, 'total_distance': 0.0,
'total_duration': 0, 'total_duration': 0,
} }
activities_list[time_period][sport_id]['nb_activities'] += 1 activities_list_by_time[time_period][sport_id][
activities_list[time_period][sport_id][ 'nb_activities'
] += 1
activities_list_by_time[time_period][sport_id][
'total_distance' 'total_distance'
] += float(activity.distance) ] += float(activity.distance)
activities_list[time_period][sport_id][ activities_list_by_time[time_period][sport_id][
'total_duration' 'total_duration'
] += convert_timedelta_to_integer(activity.moving) ] += convert_timedelta_to_integer(activity.moving)
return { return {
'status': 'success', '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: except Exception as e:
return handle_error_and_return_response(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']) @stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET'])
@authenticate @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 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']) @stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET'])
@authenticate @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 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']) @stats_blueprint.route('/stats/all', methods=['GET'])
@authenticate_as_admin @authenticate_as_admin
def get_application_stats(auth_user_id): def get_application_stats(auth_user_id: int) -> Dict:
""" """
Get all application statistics Get all application statistics

View File

@ -3,6 +3,8 @@ import os
import tempfile import tempfile
import zipfile import zipfile
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Union
from uuid import UUID
import gpxpy.gpx import gpxpy.gpx
import pytz import pytz
@ -10,6 +12,7 @@ from fittrackee import appLog, db
from flask import current_app from flask import current_app
from sqlalchemy import exc from sqlalchemy import exc
from staticmap import Line, StaticMap from staticmap import Line, StaticMap
from werkzeug.datastructures import FileStorage
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from ..users.models import User from ..users.models import User
@ -19,13 +22,20 @@ from .utils_gpx import get_gpx_info
class ActivityException(Exception): 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.status = status
self.message = message self.message = message
self.e = e 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 activity_date_tz = None
if timezone: if timezone:
user_tz = pytz.timezone(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 return activity_date_tz, activity_date
def update_activity_data(activity, gpx_data): def update_activity_data(
"""activity could be a complete activity or an activity segment""" 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.pauses = gpx_data['stop_time']
activity.moving = gpx_data['moving_time'] activity.moving = gpx_data['moving_time']
activity.min_alt = gpx_data['elevation_min'] activity.min_alt = gpx_data['elevation_min']
@ -60,12 +74,18 @@ def update_activity_data(activity, gpx_data):
return activity 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 = ( activity_date = (
gpx_data['start'] gpx_data['start']
if gpx_data if gpx_data
else datetime.strptime( 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( activity_date_tz, activity_date = get_datetime_with_tz(
@ -75,16 +95,14 @@ def create_activity(user, activity_data, gpx_data=None):
duration = ( duration = (
gpx_data['duration'] gpx_data['duration']
if gpx_data if gpx_data
else timedelta(seconds=activity_data.get('duration')) else timedelta(seconds=activity_data['duration'])
) )
distance = ( distance = gpx_data['distance'] if gpx_data else activity_data['distance']
gpx_data['distance'] if gpx_data else activity_data.get('distance') title = gpx_data['name'] if gpx_data else activity_data.get('title', '')
)
title = gpx_data['name'] if gpx_data else activity_data.get('title')
new_activity = Activity( new_activity = Activity(
user_id=user.id, user_id=user.id,
sport_id=activity_data.get('sport_id'), sport_id=activity_data['sport_id'],
activity_date=activity_date, activity_date=activity_date,
distance=distance, distance=distance,
duration=duration, duration=duration,
@ -118,7 +136,12 @@ def create_activity(user, activity_data, gpx_data=None):
return new_activity 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( new_segment = ActivitySegment(
activity_id=activity_id, activity_id=activity_id,
activity_uuid=activity_uuid, activity_uuid=activity_uuid,
@ -130,12 +153,9 @@ def create_segment(activity_id, activity_uuid, segment_data):
return new_segment 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) Update activity data from gpx file
In a next version, map_data and weather_data will be updated
(case of a modified gpx file, see issue #7)
""" """
gpx_data, _, _ = get_gpx_info( gpx_data, _, _ = get_gpx_info(
get_absolute_file_path(activity.gpx), False, False get_absolute_file_path(activity.gpx), False, False
@ -155,7 +175,16 @@ def update_activity(activity):
return updated_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() user = User.query.filter_by(id=auth_user_id).first()
if activity_data.get('refresh'): if activity_data.get('refresh'):
activity = update_activity(activity) activity = update_activity(activity)
@ -168,20 +197,18 @@ def edit_activity(activity, activity_data, auth_user_id):
if not activity.gpx: if not activity.gpx:
if activity_data.get('activity_date'): if activity_data.get('activity_date'):
activity_date = datetime.strptime( 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( _, activity.activity_date = get_datetime_with_tz(
user.timezone, activity_date user.timezone, activity_date
) )
if activity_data.get('duration'): if activity_data.get('duration'):
activity.duration = timedelta( activity.duration = timedelta(seconds=activity_data['duration'])
seconds=activity_data.get('duration')
)
activity.moving = activity.duration activity.moving = activity.duration
if activity_data.get('distance'): if activity_data.get('distance'):
activity.distance = activity_data.get('distance') activity.distance = activity_data['distance']
activity.ave_speed = ( activity.ave_speed = (
None None
@ -192,7 +219,10 @@ def edit_activity(activity, activity_data, auth_user_id):
return activity 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): if not os.path.exists(dir_path):
os.makedirs(dir_path) os.makedirs(dir_path)
file_path = os.path.join(dir_path, filename) file_path = os.path.join(dir_path, filename)
@ -200,9 +230,16 @@ def get_file_path(dir_path, filename):
def get_new_file_path( def get_new_file_path(
auth_user_id, activity_date, sport, old_filename=None, extension=None auth_user_id: int,
): activity_date: str,
if not extension: 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()}" extension = f".{old_filename.rsplit('.', 1)[1].lower()}"
_, new_filename = tempfile.mkstemp( _, new_filename = tempfile.mkstemp(
prefix=f'{activity_date}_{sport}_', suffix=extension prefix=f'{activity_date}_{sport}_', suffix=extension
@ -214,7 +251,10 @@ def get_new_file_path(
return 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) m = StaticMap(400, 225, 10)
line = Line(map_data, '#3388FF', 4) line = Line(map_data, '#3388FF', 4)
m.add_line(line) m.add_line(line)
@ -222,10 +262,10 @@ def generate_map(map_filepath, map_data):
image.save(map_filepath) 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 Generate a md5 hash used as id instead of activity id, to retrieve map
(maps are sensitive data) image (maps are sensitive data)
""" """
md5 = hashlib.md5() md5 = hashlib.md5()
absolute_map_filepath = get_absolute_file_path(map_filepath) absolute_map_filepath = get_absolute_file_path(map_filepath)
@ -235,7 +275,10 @@ def get_map_hash(map_filepath):
return md5.hexdigest() 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: try:
gpx_data, map_data, weather_data = get_gpx_info(params['file_path']) gpx_data, map_data, weather_data = get_gpx_info(params['file_path'])
auth_user_id = params['user'].id 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) 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: with zipfile.ZipFile(common_params['file_path'], "r") as zip_ref:
zip_ref.extractall(extract_dir) zip_ref.extractall(extract_dir)
new_activities = [] new_activities = []
gpx_files_limit = os.getenv('REACT_APP_GPX_LIMIT_IMPORT', '10') gpx_files_limit = os.getenv('REACT_APP_GPX_LIMIT_IMPORT', 10)
if gpx_files_limit and gpx_files_limit.isdigit(): if (
gpx_files_limit
and isinstance(gpx_files_limit, str)
and gpx_files_limit.isdigit()
):
gpx_files_limit = int(gpx_files_limit) gpx_files_limit = int(gpx_files_limit)
else: else:
gpx_files_limit = 10 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 gpx_files_ok = 0
for gpx_file in os.listdir(extract_dir): for gpx_file in os.listdir(extract_dir):
if '.' in gpx_file and gpx_file.rsplit('.', 1)[ if (
1 '.' in gpx_file
].lower() in current_app.config.get('ACTIVITY_ALLOWED_EXTENSIONS'): and gpx_file.rsplit('.', 1)[1].lower()
in current_app.config['ACTIVITY_ALLOWED_EXTENSIONS']
):
gpx_files_ok += 1 gpx_files_ok += 1
if gpx_files_ok > gpx_files_limit: if gpx_files_ok > gpx_files_limit:
break break
@ -313,7 +366,17 @@ def process_zip_archive(common_params, extract_dir):
return new_activities 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) filename = secure_filename(activity_file.filename)
extension = f".{filename.rsplit('.', 1)[1].lower()}" extension = f".{filename.rsplit('.', 1)[1].lower()}"
file_path = get_file_path(folders['tmp_dir'], filename) 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( raise ActivityException(
'error', 'error',
f"Sport id: {activity_data.get('sport_id')} does not exist", f"Sport id: {activity_data.get('sport_id')} does not exist",
None,
) )
user = User.query.filter_by(id=auth_user_id).first() 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']) 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('') upload_path = get_absolute_file_path('')
total_size = 0 total_size = 0
for dir_path, _, filenames in os.walk(upload_path): for dir_path, _, filenames in os.walk(upload_path):

View File

@ -3,5 +3,5 @@ import os
from flask import current_app 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) return os.path.join(current_app.config['UPLOAD_FOLDER'], relative_path)

View File

@ -1,23 +1,26 @@
from datetime import timedelta 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]) hours = int(value.split(':')[0])
minutes = int(value.split(':')[1]) minutes = int(value.split(':')[1])
return timedelta(seconds=(hours * 3600 + minutes * 60)) 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(':') hours, minutes, seconds = str(value).split(':')
return int(hours) * 3600 + int(minutes) * 60 + int(seconds) 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: if val is None:
return None return None
if record_type == 'LD': if record_type == 'LD':
return convert_timedelta_to_integer(val) return convert_timedelta_to_integer(str(val))
elif record_type in ['AS', 'MS']: elif record_type in ['AS', 'MS']:
return int(val * 100) return int(val * 100)
else: # 'FD' else: # 'FD'

View File

@ -1,4 +1,5 @@
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict, List, Optional, Tuple
import gpxpy.gpx import gpxpy.gpx
@ -6,25 +7,40 @@ from .utils_weather import get_weather
class ActivityGPXException(Exception): 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.status = status
self.message = message self.message = message
self.e = e self.e = e
def open_gpx_file(gpx_file): def open_gpx_file(gpx_file: str) -> Optional[gpxpy.gpx.GPX]:
gpx_file = open(gpx_file, 'r') gpx_file = open(gpx_file, 'r') # type: ignore
gpx = gpxpy.parse(gpx_file) gpx = gpxpy.parse(gpx_file)
if len(gpx.tracks) == 0: if len(gpx.tracks) == 0:
return None return None
return gpx return gpx
def get_gpx_data(parsed_gpx, max_speed, start, stopped_time_btwn_seg): def get_gpx_data(
gpx_data = {'max_speed': (max_speed / 1000) * 3600, 'start': start} 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() 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() ele = parsed_gpx.get_elevation_extremes()
gpx_data['elevation_max'] = ele.maximum 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() mv = parsed_gpx.get_moving_data()
gpx_data['moving_time'] = timedelta(seconds=mv.moving_time) gpx_data['moving_time'] = timedelta(seconds=mv.moving_time)
gpx_data['stop_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 distance = mv.moving_distance + mv.stopped_distance
gpx_data['distance'] = distance / 1000 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 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) gpx = open_gpx_file(gpx_file)
if gpx is None: if gpx is None:
return None raise ActivityGPXException('not found', 'No gpx file')
gpx_data = {'name': gpx.tracks[0].name, 'segments': []} gpx_data = {'name': gpx.tracks[0].name, 'segments': []}
max_speed = 0 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) segments_nb = len(gpx.tracks[0].segments)
prev_seg_last_point = None prev_seg_last_point = None
no_stopped_time = timedelta(seconds=0) 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): for segment_idx, segment in enumerate(gpx.tracks[0].segments):
segment_start = 0 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 # if a previous segment exists, calculate stopped time between
# the two segments # the two segments
if prev_seg_last_point: 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 # last segment point
if point_idx == (segment_points_nb - 1): 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 segment_data['idx'] = segment_idx
gpx_data['segments'].append(segment_data) 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} gpx_data = {**gpx_data, **full_gpx_data}
if update_map_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 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: if segment_id is not None:
segment_index = segment_id - 1 segment_index = segment_id - 1
if segment_index > (len(track_segments) - 1): if segment_index > (len(track_segments) - 1):
@ -135,7 +165,12 @@ def get_gpx_segments(track_segments, segment_id=None):
return segments 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) gpx = open_gpx_file(gpx_file)
if gpx is None: if gpx is None:
return None return None
@ -193,7 +228,12 @@ def get_chart_data(gpx_file, segment_id=None):
return chart_data 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) gpx_content = gpxpy.parse(content)
if len(gpx_content.tracks) == 0: if len(gpx_content.tracks) == 0:
return None return None

View File

@ -1,9 +1,17 @@
from uuid import UUID
import shortuuid 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) 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) return shortuuid.decode(short_id)

View File

@ -1,13 +1,15 @@
import os import os
from typing import Dict, Optional
import forecastio import forecastio
import pytz import pytz
from fittrackee import appLog from fittrackee import appLog
from gpxpy.gpx import GPXRoutePoint
API_KEY = os.getenv('WEATHER_API_KEY') 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 == '': if not API_KEY or API_KEY == '':
return None return None
try: try:

View File

@ -1,5 +1,8 @@
from typing import Dict, Union
from fittrackee import db from fittrackee import db
from fittrackee.responses import ( from fittrackee.responses import (
HttpResponse,
InvalidPayloadErrorResponse, InvalidPayloadErrorResponse,
handle_error_and_return_response, handle_error_and_return_response,
) )
@ -14,7 +17,7 @@ config_blueprint = Blueprint('config', __name__)
@config_blueprint.route('/config', methods=['GET']) @config_blueprint.route('/config', methods=['GET'])
def get_application_config(): def get_application_config() -> Union[Dict, HttpResponse]:
""" """
Get Application config Get Application config
@ -59,7 +62,7 @@ def get_application_config():
@config_blueprint.route('/config', methods=['PATCH']) @config_blueprint.route('/config', methods=['PATCH'])
@authenticate_as_admin @authenticate_as_admin
def update_application_config(auth_user_id): def update_application_config(auth_user_id: int) -> Union[Dict, HttpResponse]:
""" """
Update Application config Update Application config
@ -137,7 +140,7 @@ def update_application_config(auth_user_id):
@config_blueprint.route('/ping', methods=['GET']) @config_blueprint.route('/ping', methods=['GET'])
def health_check(): def health_check() -> Union[Dict, HttpResponse]:
"""health check endpoint """health check endpoint
**Example request**: **Example request**:

View File

@ -1,11 +1,19 @@
from typing import Dict
from fittrackee import db from fittrackee import db
from flask import current_app from flask import current_app
from sqlalchemy.engine.base import Connection
from sqlalchemy.event import listens_for 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 from ..users.models import User
BaseModel: DeclarativeMeta = db.Model
class AppConfig(db.Model):
class AppConfig(BaseModel):
__tablename__ = 'app_config' __tablename__ = 'app_config'
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
max_users = db.Column(db.Integer, default=0, nullable=False) 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) max_zip_file_size = db.Column(db.Integer, default=10485760, nullable=False)
@property @property
def is_registration_enabled(self): def is_registration_enabled(self) -> bool:
nb_users = User.query.count() nb_users = User.query.count()
return self.max_users == 0 or nb_users < self.max_users return self.max_users == 0 or nb_users < self.max_users
@property @property
def map_attribution(self): def map_attribution(self) -> str:
return current_app.config['TILE_SERVER']['ATTRIBUTION'] return current_app.config['TILE_SERVER']['ATTRIBUTION']
def serialize(self): def serialize(self) -> Dict:
return { return {
"gpx_limit_import": self.gpx_limit_import, 'gpx_limit_import': self.gpx_limit_import,
"is_registration_enabled": self.is_registration_enabled, 'is_registration_enabled': self.is_registration_enabled,
"max_single_file_size": self.max_single_file_size, 'max_single_file_size': self.max_single_file_size,
"max_zip_file_size": self.max_zip_file_size, 'max_zip_file_size': self.max_zip_file_size,
"max_users": self.max_users, 'max_users': self.max_users,
"map_attribution": self.map_attribution, 'map_attribution': self.map_attribution,
} }
def update_app_config(): def update_app_config() -> None:
config = AppConfig.query.first() config = AppConfig.query.first()
if config: if config:
current_app.config[ current_app.config[
@ -44,14 +52,16 @@ def update_app_config():
@listens_for(User, 'after_insert') @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) @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() update_app_config()
@listens_for(User, 'after_delete') @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) @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() update_app_config()

View File

@ -1,14 +1,16 @@
import os import os
from typing import Tuple
from fittrackee import db from fittrackee import db
from fittrackee.users.models import User from fittrackee.users.models import User
from flask import Flask
from .models import AppConfig from .models import AppConfig
MAX_FILE_SIZE = 1 * 1024 * 1024 # 1MB MAX_FILE_SIZE = 1 * 1024 * 1024 # 1MB
def init_config(): def init_config() -> Tuple[bool, AppConfig]:
""" """
init application configuration if not existing in database init application configuration if not existing in database
@ -36,7 +38,9 @@ def init_config():
return False, existing_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['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_single_file_size'] = db_config.max_single_file_size
current_app.config['MAX_CONTENT_LENGTH'] = db_config.max_zip_file_size current_app.config['MAX_CONTENT_LENGTH'] = db_config.max_zip_file_size

View File

@ -5,9 +5,10 @@ from fittrackee.application.utils import (
update_app_config_from_database, update_app_config_from_database,
) )
from fittrackee.users.models import User from fittrackee.users.models import User
from flask import Flask
def init_database(app): def init_database(app: Flask) -> None:
"""Init the database.""" """Init the database."""
admin = User( admin = User(
username='admin', email='admin@example.com', password='mpwoadmin' username='admin', email='admin@example.com', password='mpwoadmin'

View File

@ -3,7 +3,9 @@ import smtplib
import ssl import ssl
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from typing import Dict, Optional, Type, Union
from flask import Flask
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from .utils_email import parse_email_url from .utils_email import parse_email_url
@ -13,14 +15,16 @@ email_log.setLevel(logging.DEBUG)
class EmailMessage: 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.sender = sender
self.recipient = recipient self.recipient = recipient
self.subject = subject self.subject = subject
self.html = html self.html = html
self.text = text self.text = text
def generate_message(self): def generate_message(self) -> MIMEMultipart:
message = MIMEMultipart('alternative') message = MIMEMultipart('alternative')
message['Subject'] = self.subject message['Subject'] = self.subject
message['From'] = self.sender message['From'] = self.sender
@ -33,20 +37,24 @@ class EmailMessage:
class EmailTemplate: class EmailTemplate:
def __init__(self, template_directory): def __init__(self, template_directory: str) -> None:
self._env = Environment(loader=FileSystemLoader(template_directory)) self._env = Environment(loader=FileSystemLoader(template_directory))
def get_content(self, template, lang, part, data): def get_content(
template = self._env.get_template(f'{template}/{lang}/{part}') 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) 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 = {} output = {}
for part in ['subject.txt', 'body.txt', 'body.html']: for part in ['subject.txt', 'body.txt', 'body.html']:
output[part] = self.get_content(template, lang, part, data) output[part] = self.get_content(template, lang, part, data)
return output 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) output = self.get_all_contents(template, lang, data)
message = EmailMessage( message = EmailMessage(
sender, sender,
@ -59,7 +67,7 @@ class EmailTemplate:
class Email: class Email:
def __init__(self, app=None): def __init__(self, app: Optional[Flask] = None) -> None:
self.host = 'localhost' self.host = 'localhost'
self.port = 1025 self.port = 1025
self.use_tls = False self.use_tls = False
@ -67,26 +75,30 @@ class Email:
self.username = None self.username = None
self.password = None self.password = None
self.sender_email = 'no-reply@example.com' self.sender_email = 'no-reply@example.com'
self.email_template = None self.email_template: Optional[EmailTemplate] = None
if app is not None: if app is not None:
self.init_email(app) self.init_email(app)
def init_email(self, app): def init_email(self, app: Flask) -> None:
parsed_url = parse_email_url(app.config.get('EMAIL_URL')) parsed_url = parse_email_url(app.config['EMAIL_URL'])
self.host = parsed_url['host'] self.host = parsed_url['host']
self.port = parsed_url['port'] self.port = parsed_url['port']
self.use_tls = parsed_url['use_tls'] self.use_tls = parsed_url['use_tls']
self.use_ssl = parsed_url['use_ssl'] self.use_ssl = parsed_url['use_ssl']
self.username = parsed_url['username'] self.username = parsed_url['username']
self.password = parsed_url['password'] self.password = parsed_url['password']
self.sender_email = app.config.get('SENDER_EMAIL') self.sender_email = app.config['SENDER_EMAIL']
self.email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER')) self.email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
@property @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 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( message = self.email_template.get_message(
template, lang, self.sender_email, recipient, data template, lang, self.sender_email, recipient, data
) )
@ -95,8 +107,10 @@ class Email:
context = ssl.create_default_context() context = ssl.create_default_context()
if self.use_ssl: if self.use_ssl:
connection_params.update({'context': context}) connection_params.update({'context': context})
with self.smtp(self.host, self.port, **connection_params) as smtp: with self.smtp(
smtp.login(self.username, self.password) self.host, self.port, **connection_params # type: ignore
) as smtp:
smtp.login(self.username, self.password) # type: ignore
if self.use_tls: if self.use_tls:
smtp.starttls(context=context) smtp.starttls(context=context)
smtp.sendmail(self.sender_email, recipient, message.as_string()) smtp.sendmail(self.sender_email, recipient, message.as_string())

View File

@ -1,3 +1,5 @@
from typing import Dict
from urllib3.util import parse_url 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) parsed_url = parse_url(email_url)
if parsed_url.scheme != 'smtp': if parsed_url.scheme != 'smtp':
raise InvalidEmailUrlScheme() raise InvalidEmailUrlScheme()

View File

@ -1,20 +1,22 @@
from json import dumps from json import dumps
from typing import Dict, List, Optional, Union
from fittrackee import appLog from fittrackee import appLog
from flask import Response 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 [] return '' if data_type in ['gpx', 'chart_data'] else []
class HttpResponse(Response): class HttpResponse(Response):
def __init__( def __init__(
self, self,
response=None, response: Optional[Union[str, Dict]] = None,
status_code=None, status_code: Optional[int] = None,
content_type=None, content_type: Optional[str] = None,
): ) -> None:
if isinstance(response, dict): if isinstance(response, dict):
response = dumps(response) response = dumps(response)
content_type = ( content_type = (
@ -28,7 +30,9 @@ class HttpResponse(Response):
class GenericErrorResponse(HttpResponse): 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 = { response = {
'status': 'error' if status is None else status, 'status': 'error' if status is None else status,
'message': message, 'message': message,
@ -40,13 +44,15 @@ class GenericErrorResponse(HttpResponse):
class InvalidPayloadErrorResponse(GenericErrorResponse): 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 message = 'Invalid payload.' if message is None else message
super().__init__(status_code=400, message=message, status=status) super().__init__(status_code=400, message=message, status=status)
class DataInvalidPayloadErrorResponse(HttpResponse): class DataInvalidPayloadErrorResponse(HttpResponse):
def __init__(self, data_type, status=None): def __init__(self, data_type: str, status: Optional[str] = None) -> None:
response = { response = {
'status': 'error' if status is None else status, 'status': 'error' if status is None else status,
'data': {data_type: get_empty_data_for_datatype(data_type)}, 'data': {data_type: get_empty_data_for_datatype(data_type)},
@ -55,7 +61,7 @@ class DataInvalidPayloadErrorResponse(HttpResponse):
class UnauthorizedErrorResponse(GenericErrorResponse): class UnauthorizedErrorResponse(GenericErrorResponse):
def __init__(self, message=None): def __init__(self, message: Optional[str] = None) -> None:
message = ( message = (
'Invalid token. Please request a new token.' 'Invalid token. Please request a new token.'
if message is None if message is None
@ -65,7 +71,7 @@ class UnauthorizedErrorResponse(GenericErrorResponse):
class ForbiddenErrorResponse(GenericErrorResponse): class ForbiddenErrorResponse(GenericErrorResponse):
def __init__(self, message=None): def __init__(self, message: Optional[str] = None) -> None:
message = ( message = (
'You do not have permissions.' if message is None else message 'You do not have permissions.' if message is None else message
) )
@ -73,17 +79,17 @@ class ForbiddenErrorResponse(GenericErrorResponse):
class NotFoundErrorResponse(GenericErrorResponse): class NotFoundErrorResponse(GenericErrorResponse):
def __init__(self, message): def __init__(self, message: str) -> None:
super().__init__(status_code=404, message=message, status='not found') super().__init__(status_code=404, message=message, status='not found')
class UserNotFoundErrorResponse(NotFoundErrorResponse): class UserNotFoundErrorResponse(NotFoundErrorResponse):
def __init__(self): def __init__(self) -> None:
super().__init__(message='User does not exist.') super().__init__(message='User does not exist.')
class DataNotFoundErrorResponse(HttpResponse): class DataNotFoundErrorResponse(HttpResponse):
def __init__(self, data_type, message=None): def __init__(self, data_type: str, message: Optional[str] = None) -> None:
response = { response = {
'status': 'not found', 'status': 'not found',
'data': {data_type: get_empty_data_for_datatype(data_type)}, 'data': {data_type: get_empty_data_for_datatype(data_type)},
@ -94,12 +100,14 @@ class DataNotFoundErrorResponse(HttpResponse):
class PayloadTooLargeErrorResponse(GenericErrorResponse): class PayloadTooLargeErrorResponse(GenericErrorResponse):
def __init__(self, message): def __init__(self, message: str) -> None:
super().__init__(status_code=413, message=message, status='fail') super().__init__(status_code=413, message=message, status='fail')
class InternalServerErrorResponse(GenericErrorResponse): class InternalServerErrorResponse(GenericErrorResponse):
def __init__(self, message=None, status=None): def __init__(
self, message: Optional[str] = None, status: Optional[str] = None
):
message = ( message = (
'Error. Please try again or contact the administrator.' 'Error. Please try again or contact the administrator.'
if message is None if message is None
@ -109,8 +117,11 @@ class InternalServerErrorResponse(GenericErrorResponse):
def handle_error_and_return_response( 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: if db is not None:
db.session.rollback() db.session.rollback()
appLog.error(error) appLog.error(error)

View File

@ -1,8 +1,10 @@
from typing import Dict
from fittrackee import dramatiq, email_service from fittrackee import dramatiq, email_service
@dramatiq.actor(queue_name='fittrackee_emails') @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( email_service.send(
template='password_reset_request', template='password_reset_request',
lang=user['language'], lang=user['language'],

View File

@ -1,21 +1,25 @@
import json import json
from uuid import uuid4 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 from .utils import get_random_short_id
class TestGetActivities: class TestGetActivities:
def test_it_gets_all_activities_for_authenticated_user( def test_it_gets_all_activities_for_authenticated_user(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
activity_cycling_user_1, activity_cycling_user_1: Activity,
activity_cycling_user_2, activity_cycling_user_2: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -57,14 +61,14 @@ class TestGetActivities:
def test_it_gets_no_activities_for_authenticated_user_with_no_activities( def test_it_gets_no_activities_for_authenticated_user_with_no_activities(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
activity_cycling_user_1, activity_cycling_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -85,7 +89,9 @@ class TestGetActivities:
assert 'success' in data['status'] assert 'success' in data['status']
assert len(data['data']['activities']) == 0 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() client = app.test_client()
response = client.get('/api/activities') response = client.get('/api/activities')
@ -98,8 +104,12 @@ class TestGetActivities:
class TestGetActivitiesWithPagination: class TestGetActivitiesWithPagination:
def test_it_gets_activities_with_default_pagination( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -133,8 +143,12 @@ class TestGetActivitiesWithPagination:
assert '0:17:04' == data['data']['activities'][4]['duration'] assert '0:17:04' == data['data']['activities'][4]['duration']
def test_it_gets_first_page( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -168,8 +182,12 @@ class TestGetActivitiesWithPagination:
assert '0:17:04' == data['data']['activities'][4]['duration'] assert '0:17:04' == data['data']['activities'][4]['duration']
def test_it_gets_second_page( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -203,8 +221,12 @@ class TestGetActivitiesWithPagination:
assert '0:17:04' == data['data']['activities'][1]['duration'] assert '0:17:04' == data['data']['activities'][1]['duration']
def test_it_gets_empty_third_page( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -226,8 +248,12 @@ class TestGetActivitiesWithPagination:
assert len(data['data']['activities']) == 0 assert len(data['data']['activities']) == 0
def test_it_returns_error_on_invalid_page_value( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -252,8 +278,12 @@ class TestGetActivitiesWithPagination:
) )
def test_it_gets_5_activities_per_page( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -283,8 +313,12 @@ class TestGetActivitiesWithPagination:
) )
def test_it_gets_3_activities_per_page( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -316,8 +350,12 @@ class TestGetActivitiesWithPagination:
class TestGetActivitiesWithFilters: class TestGetActivitiesWithFilters:
def test_it_gets_activities_with_date_filter( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -351,8 +389,12 @@ class TestGetActivitiesWithFilters:
assert '0:16:40' == data['data']['activities'][1]['duration'] assert '0:16:40' == data['data']['activities'][1]['duration']
def test_it_gets_no_activities_with_date_filter( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -374,8 +416,12 @@ class TestGetActivitiesWithFilters:
assert len(data['data']['activities']) == 0 assert len(data['data']['activities']) == 0
def test_if_gets_activities_with_date_filter_from( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -406,8 +452,12 @@ class TestGetActivitiesWithFilters:
) )
def test_it_gets_activities_with_date_filter_to( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -437,8 +487,12 @@ class TestGetActivitiesWithFilters:
) )
def test_it_gets_activities_with_ascending_order( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -468,8 +522,12 @@ class TestGetActivitiesWithFilters:
) )
def test_it_gets_activities_with_distance_filter( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -499,8 +557,12 @@ class TestGetActivitiesWithFilters:
) )
def test_it_gets_activities_with_duration_filter( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -526,8 +588,12 @@ class TestGetActivitiesWithFilters:
) )
def test_it_gets_activities_with_average_speed_filter( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -554,13 +620,13 @@ class TestGetActivitiesWithFilters:
def test_it_gets_activities_with_max_speed_filter( def test_it_gets_activities_with_max_speed_filter(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
activity_cycling_user_1, activity_cycling_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
activity_cycling_user_1.max_speed = 25 activity_cycling_user_1.max_speed = 25
activity_running_user_1.max_speed = 11 activity_running_user_1.max_speed = 11
client = app.test_client() client = app.test_client()
@ -589,13 +655,13 @@ class TestGetActivitiesWithFilters:
def test_it_gets_activities_with_sport_filter( def test_it_gets_activities_with_sport_filter(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
sport_2_running, sport_2_running: Sport,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -623,8 +689,12 @@ class TestGetActivitiesWithFilters:
class TestGetActivitiesWithFiltersAndPagination: class TestGetActivitiesWithFiltersAndPagination:
def test_it_gets_page_2_with_date_filter( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -654,8 +724,12 @@ class TestGetActivitiesWithFiltersAndPagination:
) )
def test_it_get_page_2_with_date_filter_and_ascending_order( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -687,8 +761,12 @@ class TestGetActivitiesWithFiltersAndPagination:
class TestGetActivity: class TestGetActivity:
def test_it_gets_an_activity( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -719,8 +797,13 @@ class TestGetActivity:
assert '1:00:00' == data['data']['activities'][0]['duration'] assert '1:00:00' == data['data']['activities'][0]['duration']
def test_it_returns_403_if_activity_belongs_to_a_different_user( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -741,7 +824,9 @@ class TestGetActivity:
assert 'error' in data['status'] assert 'error' in data['status']
assert 'You do not have permissions.' in data['message'] 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -763,8 +848,8 @@ class TestGetActivity:
assert len(data['data']['activities']) == 0 assert len(data['data']['activities']) == 0
def test_it_returns_404_on_getting_gpx_if_activity_does_not_exist( 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() random_short_id = get_random_short_id()
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -788,8 +873,8 @@ class TestGetActivity:
assert data['data']['gpx'] == '' assert data['data']['gpx'] == ''
def test_it_returns_404_on_getting_chart_data_if_activity_does_not_exist( 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() random_short_id = get_random_short_id()
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -813,8 +898,12 @@ class TestGetActivity:
assert data['data']['chart_data'] == '' assert data['data']['chart_data'] == ''
def test_it_returns_404_on_getting_gpx_if_activity_have_no_gpx( 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 activity_short_id = activity_cycling_user_1.short_id
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -840,8 +929,12 @@ class TestGetActivity:
) )
def test_it_returns_404_if_activity_have_no_chart_data( 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 activity_short_id = activity_cycling_user_1.short_id
client = app.test_client() client = app.test_client()
resp_login = client.post( 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 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" activity_cycling_user_1.gpx = "some path"
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -895,8 +992,12 @@ class TestGetActivity:
assert 'data' not in data assert 'data' not in data
def test_it_returns_500_on_getting_chart_data_if_an_activity_has_invalid_gpx_pathname( # noqa 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' activity_cycling_user_1.gpx = 'some path'
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -922,7 +1023,9 @@ class TestGetActivity:
) )
assert 'data' not in data 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',

View File

@ -2,12 +2,15 @@ import json
import os import os
from datetime import datetime from datetime import datetime
from io import BytesIO 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.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 'creation_date' in data['data']['activities'][0]
assert ( assert (
'Tue, 13 Mar 2018 12:44:45 GMT' '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 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 'creation_date' in data['data']['activities'][0]
assert ( assert (
'Tue, 13 Mar 2018 12:44:45 GMT' '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 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 'creation_date' in data['data']['activities'][0]
assert ( assert (
data['data']['activities'][0]['activity_date'] data['data']['activities'][0]['activity_date']
@ -200,8 +203,8 @@ def assert_activity_data_wo_gpx(data):
class TestPostActivityWithGpx: class TestPostActivityWithGpx:
def test_it_adds_an_activity_with_gpx_file( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -230,8 +233,12 @@ class TestPostActivityWithGpx:
assert_activity_data_with_gpx(data) assert_activity_data_with_gpx(data)
def test_it_adds_an_activity_with_gpx_without_name( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -263,8 +270,12 @@ class TestPostActivityWithGpx:
assert_activity_data_with_gpx(data) assert_activity_data_with_gpx(data)
def test_it_adds_an_activity_with_gpx_without_name_timezone( 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' user_1.timezone = 'Europe/Paris'
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -297,8 +308,8 @@ class TestPostActivityWithGpx:
assert_activity_data_with_gpx(data) assert_activity_data_with_gpx(data)
def test_it_adds_get_an_activity_with_gpx_notes( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -327,8 +338,12 @@ class TestPostActivityWithGpx:
assert 'test activity' == data['data']['activities'][0]['notes'] assert 'test activity' == data['data']['activities'][0]['notes']
def test_it_returns_500_if_gpx_file_has_not_tracks( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -352,12 +367,16 @@ class TestPostActivityWithGpx:
data = json.loads(response.data.decode()) data = json.loads(response.data.decode())
assert response.status_code == 500 assert response.status_code == 500
assert 'error' in data['status'] 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 assert 'data' not in data
def test_it_returns_500_if_gpx_has_invalid_xml( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -388,8 +407,8 @@ class TestPostActivityWithGpx:
assert 'data' not in data assert 'data' not in data
def test_it_returns_400_if_activity_gpx_has_invalid_extension( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -416,8 +435,8 @@ class TestPostActivityWithGpx:
assert data['message'] == 'File extension not allowed.' assert data['message'] == 'File extension not allowed.'
def test_it_returns_400_if_sport_id_is_not_provided( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -443,8 +462,8 @@ class TestPostActivityWithGpx:
assert data['message'] == 'Invalid payload.' assert data['message'] == 'Invalid payload.'
def test_it_returns_500_if_sport_id_does_not_exists( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -471,8 +490,8 @@ class TestPostActivityWithGpx:
assert data['message'] == 'Sport id: 2 does not exist' assert data['message'] == 'Sport id: 2 does not exist'
def test_returns_400_if_no_gpx_file_is_provided( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -498,8 +517,8 @@ class TestPostActivityWithGpx:
class TestPostActivityWithoutGpx: class TestPostActivityWithoutGpx:
def test_it_adds_an_activity_without_gpx( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -531,8 +550,8 @@ class TestPostActivityWithoutGpx:
assert_activity_data_wo_gpx(data) assert_activity_data_wo_gpx(data)
def test_it_returns_400_if_activity_date_is_missing( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -556,8 +575,8 @@ class TestPostActivityWithoutGpx:
assert 'Invalid payload.' in data['message'] assert 'Invalid payload.' in data['message']
def test_it_returns_500_if_activity_format_is_invalid( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -588,8 +607,12 @@ class TestPostActivityWithoutGpx:
assert 'Error during activity save.' in data['message'] assert 'Error during activity save.' in data['message']
def test_it_adds_activity_with_zero_value( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -645,8 +668,8 @@ class TestPostActivityWithoutGpx:
class TestPostActivityWithZipArchive: class TestPostActivityWithZipArchive:
def test_it_adds_activities_with_zip_archive( 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') 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 # 'gpx_test.zip' contains 3 gpx files (same data) and 1 non-gpx file
with open(file_path, 'rb') as zip_file: with open(file_path, 'rb') as zip_file:
@ -679,8 +702,8 @@ class TestPostActivityWithZipArchive:
assert_activity_data_with_gpx(data) assert_activity_data_with_gpx(data)
def test_it_returns_400_if_folder_is_present_in_zpi_archive( 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( file_path = os.path.join(
app.root_path, 'tests/files/gpx_test_folder.zip' app.root_path, 'tests/files/gpx_test_folder.zip'
) )
@ -715,8 +738,8 @@ class TestPostActivityWithZipArchive:
assert len(data['data']['activities']) == 0 assert len(data['data']['activities']) == 0
def test_it_returns_500_if_one_fle_in_zip_archive_is_invalid( 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( file_path = os.path.join(
app.root_path, 'tests/files/gpx_test_incorrect.zip' app.root_path, 'tests/files/gpx_test_incorrect.zip'
) )
@ -747,13 +770,15 @@ class TestPostActivityWithZipArchive:
data = json.loads(response.data.decode()) data = json.loads(response.data.decode())
assert response.status_code == 500 assert response.status_code == 500
assert 'error' in data['status'] 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 assert 'data' not in data
class TestPostAndGetActivityWithGpx: class TestPostAndGetActivityWithGpx:
@staticmethod @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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -849,18 +874,22 @@ class TestPostAndGetActivityWithGpx:
) )
def test_it_gets_an_activity_created_with_gpx( 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) return self.activity_assertion(app, gpx_file, False)
def test_it_gets_an_activity_created_with_gpx_with_segments( 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) return self.activity_assertion(app, gpx_file_with_segments, True)
def test_it_gets_chart_data_for_an_activity_created_with_gpx( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -897,8 +926,8 @@ class TestPostAndGetActivityWithGpx:
assert data['data']['chart_data'] != '' assert data['data']['chart_data'] != ''
def test_it_gets_segment_chart_data_for_an_activity_created_with_gpx( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -935,8 +964,13 @@ class TestPostAndGetActivityWithGpx:
assert data['data']['chart_data'] != '' assert data['data']['chart_data'] != ''
def test_it_returns_403_on_getting_chart_data_if_activity_belongs_to_another_user( # noqa 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -977,8 +1011,8 @@ class TestPostAndGetActivityWithGpx:
assert data['message'] == 'You do not have permissions.' assert data['message'] == 'You do not have permissions.'
def test_it_returns_500_on_invalid_segment_id( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1015,8 +1049,8 @@ class TestPostAndGetActivityWithGpx:
assert 'data' not in data assert 'data' not in data
def test_it_returns_404_if_segment_id_does_not_exist( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1055,8 +1089,8 @@ class TestPostAndGetActivityWithGpx:
class TestPostAndGetActivityWithoutGpx: class TestPostAndGetActivityWithoutGpx:
def test_it_add_and_gets_an_activity_wo_gpx( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1097,8 +1131,8 @@ class TestPostAndGetActivityWithoutGpx:
assert_activity_data_wo_gpx(data) assert_activity_data_wo_gpx(data)
def test_it_adds_and_gets_an_activity_wo_gpx_notes( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1142,8 +1176,8 @@ class TestPostAndGetActivityWithoutGpx:
class TestPostAndGetActivityUsingTimezones: class TestPostAndGetActivityUsingTimezones:
def test_it_add_and_gets_an_activity_wo_gpx_with_timezone( 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' user_1.timezone = 'Europe/Paris'
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -1192,8 +1226,8 @@ class TestPostAndGetActivityUsingTimezones:
) )
def test_it_adds_and_gets_activities_date_filter_with_timezone_new_york( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1239,8 +1273,12 @@ class TestPostAndGetActivityUsingTimezones:
) )
def test_it_adds_and_gets_activities_date_filter_with_timezone_paris( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',

View File

@ -1,12 +1,15 @@
import json 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.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 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 'creation_date' in data['data']['activities'][0]
assert ( assert (
'Tue, 13 Mar 2018 12:44:45 GMT' 'Tue, 13 Mar 2018 12:44:45 GMT'
@ -51,8 +54,13 @@ def assert_activity_data_with_gpx(data, sport_id):
class TestEditActivityWithGpx: class TestEditActivityWithGpx:
def test_it_updates_title_for_an_activity_with_gpx( 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) token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client() client = app.test_client()
@ -72,8 +80,13 @@ class TestEditActivityWithGpx:
assert_activity_data_with_gpx(data, sport_2_running.id) assert_activity_data_with_gpx(data, sport_2_running.id)
def test_it_adds_notes_for_an_activity_with_gpx( 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) token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client() client = app.test_client()
@ -92,8 +105,14 @@ class TestEditActivityWithGpx:
assert data['data']['activities'][0]['notes'] == 'test notes' assert data['data']['activities'][0]['notes'] == 'test notes'
def test_it_raises_403_when_editing_an_activity_from_different_user( 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) _, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -118,8 +137,13 @@ class TestEditActivityWithGpx:
assert 'You do not have permissions.' in data['message'] assert 'You do not have permissions.' in data['message']
def test_it_updates_sport( 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) token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client() client = app.test_client()
@ -139,8 +163,8 @@ class TestEditActivityWithGpx:
assert_activity_data_with_gpx(data, sport_2_running.id) assert_activity_data_with_gpx(data, sport_2_running.id)
def test_it_returns_400_if_payload_is_empty( 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) token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client() client = app.test_client()
@ -157,8 +181,8 @@ class TestEditActivityWithGpx:
assert 'Invalid payload.' in data['message'] assert 'Invalid payload.' in data['message']
def test_it_raises_500_if_sport_does_not_exists( 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) token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client() client = app.test_client()
@ -181,12 +205,12 @@ class TestEditActivityWithGpx:
class TestEditActivityWithoutGpx: class TestEditActivityWithoutGpx:
def test_it_updates_an_activity_wo_gpx( def test_it_updates_an_activity_wo_gpx(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
activity_cycling_user_1, activity_cycling_user_1: Activity,
): ) -> None:
activity_short_id = activity_cycling_user_1.short_id activity_short_id = activity_cycling_user_1.short_id
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -266,8 +290,12 @@ class TestEditActivityWithoutGpx:
assert records[3]['value'] == 8.0 assert records[3]['value'] == 8.0
def test_it_adds_notes_to_an_activity_wo_gpx( 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 activity_short_id = activity_cycling_user_1.short_id
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -338,8 +366,13 @@ class TestEditActivityWithoutGpx:
assert records[3]['value'] == 10.0 assert records[3]['value'] == 10.0
def test_returns_403_when_editing_an_activity_wo_gpx_from_different_user( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -372,12 +405,12 @@ class TestEditActivityWithoutGpx:
def test_it_updates_an_activity_wo_gpx_with_timezone( def test_it_updates_an_activity_wo_gpx_with_timezone(
self, self,
app, app: Flask,
user_1_paris, user_1_paris: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
activity_cycling_user_1, activity_cycling_user_1: Activity,
): ) -> None:
activity_short_id = activity_cycling_user_1.short_id activity_short_id = activity_cycling_user_1.short_id
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -453,12 +486,12 @@ class TestEditActivityWithoutGpx:
def test_it_updates_only_sport_and_distance_an_activity_wo_gpx( def test_it_updates_only_sport_and_distance_an_activity_wo_gpx(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
activity_cycling_user_1, activity_cycling_user_1: Activity,
): ) -> None:
activity_short_id = activity_cycling_user_1.short_id activity_short_id = activity_cycling_user_1.short_id
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -525,8 +558,12 @@ class TestEditActivityWithoutGpx:
assert records[3]['value'] == 20.0 assert records[3]['value'] == 20.0
def test_it_returns_400_if_payload_is_empty( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -550,8 +587,12 @@ class TestEditActivityWithoutGpx:
assert 'Invalid payload.' in data['message'] assert 'Invalid payload.' in data['message']
def test_it_returns_500_if_date_format_is_invalid( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -585,8 +626,8 @@ class TestEditActivityWithoutGpx:
) )
def test_it_returns_404_if_edited_activity_does_not_exists( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -618,8 +659,13 @@ class TestEditActivityWithoutGpx:
class TestRefreshActivityWithGpx: class TestRefreshActivityWithGpx:
def test_refresh_an_activity_with_gpx( 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) token, activity_short_id = post_an_activity(app, gpx_file)
activity_uuid = decode_short_id(activity_short_id) activity_uuid = decode_short_id(activity_short_id)
client = app.test_client() client = app.test_client()

View File

@ -1,21 +1,23 @@
import json import json
import os 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.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 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() activity = Activity.query.filter_by(id=activity_id).first()
return activity.gpx return activity.gpx
class TestDeleteActivityWithGpx: class TestDeleteActivityWithGpx:
def test_it_deletes_an_activity_with_gpx( 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) token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client() client = app.test_client()
@ -27,8 +29,13 @@ class TestDeleteActivityWithGpx:
assert response.status_code == 204 assert response.status_code == 204
def test_it_returns_403_when_deleting_an_activity_from_different_user( 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) _, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -51,7 +58,9 @@ class TestDeleteActivityWithGpx:
assert 'error' in data['status'] assert 'error' in data['status']
assert 'You do not have permissions.' in data['message'] 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -70,8 +79,8 @@ class TestDeleteActivityWithGpx:
assert 'not found' in data['status'] assert 'not found' in data['status']
def test_it_returns_500_when_deleting_an_activity_with_gpx_invalid_file( 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) token, activity_short_id = post_an_activity(app, gpx_file)
client = app.test_client() client = app.test_client()
gpx_filepath = get_gpx_filepath(1) gpx_filepath = get_gpx_filepath(1)
@ -95,8 +104,12 @@ class TestDeleteActivityWithGpx:
class TestDeleteActivityWithoutGpx: class TestDeleteActivityWithoutGpx:
def test_it_deletes_an_activity_wo_gpx( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -113,8 +126,13 @@ class TestDeleteActivityWithoutGpx:
assert response.status_code == 204 assert response.status_code == 204
def test_it_returns_403_when_deleting_an_activity_from_different_user( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',

View File

@ -1,12 +1,19 @@
from uuid import UUID from uuid import UUID
from fittrackee.activities.models import Activity, Sport
from fittrackee.activities.utils_id import decode_short_id from fittrackee.activities.utils_id import decode_short_id
from fittrackee.users.models import User
from flask import Flask
class TestActivityModel: class TestActivityModel:
def test_activity_model( 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' activity_cycling_user_1.title = 'Test'
assert 1 == activity_cycling_user_1.id assert 1 == activity_cycling_user_1.id
@ -55,12 +62,12 @@ class TestActivityModel:
def test_activity_segment_model( def test_activity_segment_model(
self, self,
app, app: Flask,
sport_1_cycling, sport_1_cycling: Sport,
user_1, user_1: User,
activity_cycling_user_1, activity_cycling_user_1: Activity,
activity_cycling_user_1_segment, activity_cycling_user_1_segment: Activity,
): ) -> None:
assert ( assert (
f'<Segment \'{activity_cycling_user_1_segment.segment_id}\' ' f'<Segment \'{activity_cycling_user_1_segment.segment_id}\' '
f'for activity \'{activity_cycling_user_1.short_id}\'>' f'for activity \'{activity_cycling_user_1.short_id}\'>'

View File

@ -1,17 +1,21 @@
import json import json
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from flask import Flask
class TestGetRecords: class TestGetRecords:
def test_it_gets_records_for_authenticated_user( def test_it_gets_records_for_authenticated_user(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
activity_cycling_user_1, activity_cycling_user_1: Activity,
activity_cycling_user_2, activity_cycling_user_2: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -85,13 +89,13 @@ class TestGetRecords:
def test_it_gets_no_records_if_user_has_no_activity( def test_it_gets_no_records_if_user_has_no_activity(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
activity_cycling_user_2, activity_cycling_user_2: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -112,8 +116,12 @@ class TestGetRecords:
assert len(data['data']['records']) == 0 assert len(data['data']['records']) == 0
def test_it_gets_no_records_if_activity_has_zero_value( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -151,8 +159,8 @@ class TestGetRecords:
assert len(data['data']['records']) == 0 assert len(data['data']['records']) == 0
def test_it_gets_updated_records_after_activities_post_and_patch( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -696,8 +704,12 @@ class TestGetRecords:
assert len(data['data']['records']) == 0 assert len(data['data']['records']) == 0
def test_it_gets_updated_records_after_sport_change( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',

View File

@ -1,12 +1,18 @@
import datetime 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: class TestRecordModel:
def test_record_model( 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( record_ld = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id, user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id, sport_id=activity_cycling_user_1.sport_id,
@ -29,8 +35,12 @@ class TestRecordModel:
assert 'value' in record_serialize assert 'value' in record_serialize
def test_record_model_with_none_value( 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( record_ld = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id, user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id, sport_id=activity_cycling_user_1.sport_id,
@ -49,8 +59,12 @@ class TestRecordModel:
assert record_serialize['value'] is None assert record_serialize['value'] is None
def test_average_speed_records( 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( record_as = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id, user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id, sport_id=activity_cycling_user_1.sport_id,
@ -66,8 +80,12 @@ class TestRecordModel:
assert isinstance(record_serialize.get('value'), float) assert isinstance(record_serialize.get('value'), float)
def test_add_farest_distance_records( 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( record_fd = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id, user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id, sport_id=activity_cycling_user_1.sport_id,
@ -83,8 +101,12 @@ class TestRecordModel:
assert isinstance(record_serialize.get('value'), float) assert isinstance(record_serialize.get('value'), float)
def test_add_longest_duration_records( 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( record_ld = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id, user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id, sport_id=activity_cycling_user_1.sport_id,
@ -100,8 +122,12 @@ class TestRecordModel:
assert isinstance(record_serialize.get('value'), str) assert isinstance(record_serialize.get('value'), str)
def test_add_longest_duration_records_with_zero( 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( record_ld = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id, user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id, sport_id=activity_cycling_user_1.sport_id,
@ -118,8 +144,12 @@ class TestRecordModel:
assert isinstance(record_serialize.get('value'), str) assert isinstance(record_serialize.get('value'), str)
def test_max_speed_records_no_value( 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( record_ms = Record.query.filter_by(
user_id=activity_cycling_user_1.user_id, user_id=activity_cycling_user_1.user_id,
sport_id=activity_cycling_user_1.sport_id, sport_id=activity_cycling_user_1.sport_id,

View File

@ -1,5 +1,9 @@
import json import json
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from flask import Flask
expected_sport_1_cycling_result = { expected_sport_1_cycling_result = {
'id': 1, 'id': 1,
'label': 'Cycling', 'label': 'Cycling',
@ -32,8 +36,12 @@ expected_sport_1_cycling_inactive_admin_result['has_activities'] = False
class TestGetSports: class TestGetSports:
def test_it_gets_all_sports( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -57,8 +65,12 @@ class TestGetSports:
assert data['data']['sports'][1] == expected_sport_2_running_result assert data['data']['sports'][1] == expected_sport_2_running_result
def test_it_gets_all_sports_with_inactive_one( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -85,8 +97,12 @@ class TestGetSports:
assert data['data']['sports'][1] == expected_sport_2_running_result assert data['data']['sports'][1] == expected_sport_2_running_result
def test_it_gets_all_sports_with_admin_rights( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -118,7 +134,9 @@ class TestGetSports:
class TestGetSport: 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -140,7 +158,9 @@ class TestGetSport:
assert len(data['data']['sports']) == 1 assert len(data['data']['sports']) == 1
assert data['data']['sports'][0] == expected_sport_1_cycling_result 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -162,8 +182,8 @@ class TestGetSport:
assert len(data['data']['sports']) == 0 assert len(data['data']['sports']) == 0
def test_it_gets_a_inactive_sport( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -189,8 +209,8 @@ class TestGetSport:
) )
def test_it_get_an_inactive_sport_with_admin_rights( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -219,7 +239,9 @@ class TestGetSport:
class TestUpdateSport: 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -246,7 +268,9 @@ class TestUpdateSport:
assert data['data']['sports'][0]['is_active'] is False assert data['data']['sports'][0]['is_active'] is False
assert data['data']['sports'][0]['has_activities'] 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 sport_1_cycling.is_active = False
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -275,8 +299,12 @@ class TestUpdateSport:
assert data['data']['sports'][0]['has_activities'] is False assert data['data']['sports'][0]['has_activities'] is False
def test_it_disables_a_sport_with_activities( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -304,8 +332,12 @@ class TestUpdateSport:
assert data['data']['sports'][0]['has_activities'] is True assert data['data']['sports'][0]['has_activities'] is True
def test_it_enables_a_sport_with_activities( 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 sport_1_cycling.is_active = False
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -334,8 +366,8 @@ class TestUpdateSport:
assert data['data']['sports'][0]['has_activities'] is True assert data['data']['sports'][0]['has_activities'] is True
def test_returns_error_if_user_has_no_admin_rights( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -358,7 +390,9 @@ class TestUpdateSport:
assert 'error' in data['status'] assert 'error' in data['status']
assert 'You do not have permissions.' in data['message'] 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -383,7 +417,9 @@ class TestUpdateSport:
assert 'error' in data['status'] assert 'error' in data['status']
assert 'Invalid payload.' in data['message'] 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',

View File

@ -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: class TestSportModel:
@staticmethod @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 1 == sport.id
assert 'Cycling' == sport.label assert 'Cycling' == sport.label
assert '<Sport \'Cycling\'>' == str(sport) assert '<Sport \'Cycling\'>' == str(sport)
@ -11,18 +20,26 @@ class TestSportModel:
assert serialized_sport['is_active'] is True assert serialized_sport['is_active'] is True
return serialized_sport 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) serialized_sport = self.assert_sport_model(sport_1_cycling)
assert 'has_activities' not in serialized_sport assert 'has_activities' not in serialized_sport
def test_sport_model_with_activity( 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) serialized_sport = self.assert_sport_model(sport_1_cycling)
assert 'has_activities' not in serialized_sport assert 'has_activities' not in serialized_sport
def test_sport_model_with_activity_as_admin( 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) serialized_sport = self.assert_sport_model(sport_1_cycling, True)
assert serialized_sport['has_activities'] is True assert serialized_sport['has_activities'] is True

View File

@ -1,8 +1,14 @@
import json import json
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from flask import Flask
class TestGetStatsByTime: 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -23,7 +29,9 @@ class TestGetStatsByTime:
assert 'success' in data['status'] assert 'success' in data['status']
assert data['data']['statistics'] == {} 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -46,13 +54,13 @@ class TestGetStatsByTime:
def test_it_returns_error_if_date_format_is_invalid( def test_it_returns_error_if_date_format_is_invalid(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -78,13 +86,13 @@ class TestGetStatsByTime:
def test_it_returns_error_if_period_is_invalid( def test_it_returns_error_if_period_is_invalid(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -107,13 +115,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_time_all_activities( def test_it_gets_stats_by_time_all_activities(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -156,13 +164,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_for_april_2018( def test_it_gets_stats_for_april_2018(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -198,13 +206,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_for_april_2018_with_paris_timezone( def test_it_gets_stats_for_april_2018_with_paris_timezone(
self, self,
app, app: Flask,
user_1_paris, user_1_paris: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -241,13 +249,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_year( def test_it_gets_stats_by_year(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -290,13 +298,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_year_for_april_2018( def test_it_gets_stats_by_year_for_april_2018(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -332,13 +340,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_year_for_april_2018_with_paris_timezone( def test_it_gets_stats_by_year_for_april_2018_with_paris_timezone(
self, self,
app, app: Flask,
user_1_paris, user_1_paris: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -374,13 +382,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_month( def test_it_gets_stats_by_month(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -451,13 +459,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_month_with_new_york_timezone( def test_it_gets_stats_by_month_with_new_york_timezone(
self, self,
app, app: Flask,
user_1_full, user_1_full: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -528,13 +536,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_month_for_april_2018( def test_it_gets_stats_by_month_for_april_2018(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -570,13 +578,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_week( def test_it_gets_stats_by_week(
self, self,
app, app: Flask,
user_1_full, user_1_full: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -647,13 +655,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_week_for_week_13( def test_it_gets_stats_by_week_for_week_13(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -689,13 +697,13 @@ class TestGetStatsByTime:
def test_if_get_stats_by_week_starting_with_monday( def test_if_get_stats_by_week_starting_with_monday(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -766,13 +774,13 @@ class TestGetStatsByTime:
def test_it_gets_stats_by_week_starting_with_monday_for_week_13( def test_it_gets_stats_by_week_starting_with_monday_for_week_13(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -810,13 +818,13 @@ class TestGetStatsByTime:
class TestGetStatsBySport: class TestGetStatsBySport:
def test_it_gets_stats_by_sport( def test_it_gets_stats_by_sport(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -850,13 +858,13 @@ class TestGetStatsBySport:
def test_it_get_stats_for_sport_1( def test_it_get_stats_for_sport_1(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -885,13 +893,13 @@ class TestGetStatsBySport:
def test_it_returns_errors_if_user_does_not_exist( def test_it_returns_errors_if_user_does_not_exist(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -914,13 +922,13 @@ class TestGetStatsBySport:
def test_it_returns_error_if_sport_does_not_exist( def test_it_returns_error_if_sport_does_not_exist(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -943,13 +951,13 @@ class TestGetStatsBySport:
def test_it_returns_error_if_sport_id_is_invalid( def test_it_returns_error_if_sport_id_is_invalid(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
seven_activities_user_1, seven_activities_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -976,8 +984,8 @@ class TestGetStatsBySport:
class TestGetAllStats: class TestGetAllStats:
def test_it_returns_all_stats_when_users_have_no_activities( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1005,16 +1013,16 @@ class TestGetAllStats:
def test_it_gets_app_all_stats_with_activities( def test_it_gets_app_all_stats_with_activities(
self, self,
app, app: Flask,
user_1_admin, user_1_admin: User,
user_2, user_2: User,
user_3, user_3: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
activity_cycling_user_1, activity_cycling_user_1: Activity,
activity_cycling_user_2, activity_cycling_user_2: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1042,16 +1050,16 @@ class TestGetAllStats:
def test_it_returns_error_if_user_has_no_admin_rights( def test_it_returns_error_if_user_has_no_admin_rights(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
user_3, user_3: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
activity_cycling_user_1, activity_cycling_user_1: Activity,
activity_cycling_user_2, activity_cycling_user_2: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',

View File

@ -1,15 +1,17 @@
import json import json
from io import BytesIO from io import BytesIO
from typing import Tuple
from uuid import uuid4 from uuid import uuid4
from fittrackee.activities.utils_id import encode_uuid 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()) 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',

View File

@ -1,8 +1,13 @@
import json import json
from fittrackee.users.models import User
from flask import Flask
class TestGetConfig: 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -33,8 +38,8 @@ class TestGetConfig:
) )
def test_it_returns_error_if_application_has_no_config( 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() client = app_no_config.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -59,8 +64,8 @@ class TestGetConfig:
assert 'Error on getting configuration.' in data['message'] assert 'Error on getting configuration.' in data['message']
def test_it_returns_error_if_application_has_several_config( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -86,7 +91,9 @@ class TestGetConfig:
class TestUpdateConfig: 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -114,7 +121,9 @@ class TestUpdateConfig:
assert data['data']['max_zip_file_size'] == 10485760 assert data['data']['max_zip_file_size'] == 10485760
assert data['data']['max_users'] == 10 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -150,7 +159,9 @@ class TestUpdateConfig:
assert data['data']['max_zip_file_size'] == 25000 assert data['data']['max_zip_file_size'] == 25000
assert data['data']['max_users'] == 50 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -174,7 +185,9 @@ class TestUpdateConfig:
assert 'error' in data['status'] assert 'error' in data['status']
assert 'You do not have permissions.' in data['message'] 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -200,8 +213,8 @@ class TestUpdateConfig:
assert 'Invalid payload.' in data['message'] assert 'Invalid payload.' in data['message']
def test_it_returns_error_on_update_if_application_has_no_config( 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() client = app_no_config.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',

View File

@ -1,8 +1,9 @@
from fittrackee.application.models import AppConfig from fittrackee.application.models import AppConfig
from flask import Flask
class TestConfigModel: class TestConfigModel:
def test_application_config(self, app): def test_application_config(self, app: Flask) -> None:
app_config = AppConfig.query.first() app_config = AppConfig.query.first()
assert 1 == app_config.id assert 1 == app_config.id

View File

@ -1,8 +1,10 @@
import os import os
from flask import Flask
class TestConfig: class TestConfig:
def test_development_config(self, app): def test_development_config(self, app: Flask) -> None:
app.config.from_object('fittrackee.config.DevelopmentConfig') app.config.from_object('fittrackee.config.DevelopmentConfig')
assert app.config['DEBUG'] assert app.config['DEBUG']
assert not app.config['TESTING'] assert not app.config['TESTING']
@ -10,7 +12,7 @@ class TestConfig:
'DATABASE_URL' 'DATABASE_URL'
) )
def test_testing_config(self, app): def test_testing_config(self, app: Flask) -> None:
app.config.from_object('fittrackee.config.TestingConfig') app.config.from_object('fittrackee.config.TestingConfig')
assert app.config['DEBUG'] assert app.config['DEBUG']
assert app.config['TESTING'] assert app.config['TESTING']

View File

@ -1,8 +1,10 @@
import json import json
from flask import Flask
class TestHealthCheck: 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.""" """ => Ensure the /health_check route behaves correctly."""
client = app.test_client() client = app.test_client()
response = client.get('/api/ping') response = client.get('/api/ping')

View File

@ -1,5 +1,6 @@
import datetime import datetime
import os import os
from typing import Generator, Optional
import pytest import pytest
from fittrackee import create_app, db from fittrackee import create_app, db
@ -11,10 +12,10 @@ from fittrackee.users.models import User
os.environ['FLASK_ENV'] = 'testing' os.environ['FLASK_ENV'] = 'testing'
os.environ['APP_SETTINGS'] = 'fittrackee.config.TestingConfig' os.environ['APP_SETTINGS'] = 'fittrackee.config.TestingConfig'
# to avoid resetting dev database during tests # 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: if with_config:
config = AppConfig() config = AppConfig()
config.gpx_limit_import = 10 config.gpx_limit_import = 10
@ -28,7 +29,7 @@ def get_app_config(with_config=False):
return None return None
def get_app(with_config=False): def get_app(with_config: Optional[bool] = False) -> Generator:
app = create_app() app = create_app()
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
@ -46,7 +47,7 @@ def get_app(with_config=False):
@pytest.fixture @pytest.fixture
def app(monkeypatch): def app(monkeypatch: pytest.MonkeyPatch) -> Generator:
monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025') monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025')
if os.getenv('TILE_SERVER_URL'): if os.getenv('TILE_SERVER_URL'):
monkeypatch.delenv('TILE_SERVER_URL') monkeypatch.delenv('TILE_SERVER_URL')
@ -56,24 +57,24 @@ def app(monkeypatch):
@pytest.fixture @pytest.fixture
def app_no_config(): def app_no_config() -> Generator:
yield from get_app(with_config=False) yield from get_app(with_config=False)
@pytest.fixture @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') monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025?ssl=True')
yield from get_app(with_config=True) yield from get_app(with_config=True)
@pytest.fixture @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') monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025?tls=True')
yield from get_app(with_config=True) yield from get_app(with_config=True)
@pytest.fixture() @pytest.fixture()
def app_config(): def app_config() -> AppConfig:
config = AppConfig() config = AppConfig()
config.gpx_limit_import = 10 config.gpx_limit_import = 10
config.max_single_file_size = 1048576 config.max_single_file_size = 1048576
@ -85,7 +86,7 @@ def app_config():
@pytest.fixture() @pytest.fixture()
def user_1(): def user_1() -> User:
user = User(username='test', email='test@test.com', password='12345678') user = User(username='test', email='test@test.com', password='12345678')
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
@ -93,7 +94,7 @@ def user_1():
@pytest.fixture() @pytest.fixture()
def user_1_admin(): def user_1_admin() -> User:
admin = User( admin = User(
username='admin', email='admin@example.com', password='12345678' username='admin', email='admin@example.com', password='12345678'
) )
@ -104,7 +105,7 @@ def user_1_admin():
@pytest.fixture() @pytest.fixture()
def user_1_full(): def user_1_full() -> User:
user = User(username='test', email='test@test.com', password='12345678') user = User(username='test', email='test@test.com', password='12345678')
user.first_name = 'John' user.first_name = 'John'
user.last_name = 'Doe' user.last_name = 'Doe'
@ -119,7 +120,7 @@ def user_1_full():
@pytest.fixture() @pytest.fixture()
def user_1_paris(): def user_1_paris() -> User:
user = User(username='test', email='test@test.com', password='12345678') user = User(username='test', email='test@test.com', password='12345678')
user.timezone = 'Europe/Paris' user.timezone = 'Europe/Paris'
db.session.add(user) db.session.add(user)
@ -128,7 +129,7 @@ def user_1_paris():
@pytest.fixture() @pytest.fixture()
def user_2(): def user_2() -> User:
user = User(username='toto', email='toto@toto.com', password='87654321') user = User(username='toto', email='toto@toto.com', password='87654321')
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
@ -136,7 +137,7 @@ def user_2():
@pytest.fixture() @pytest.fixture()
def user_2_admin(): def user_2_admin() -> User:
user = User(username='toto', email='toto@toto.com', password='87654321') user = User(username='toto', email='toto@toto.com', password='87654321')
user.admin = True user.admin = True
db.session.add(user) db.session.add(user)
@ -145,7 +146,7 @@ def user_2_admin():
@pytest.fixture() @pytest.fixture()
def user_3(): def user_3() -> User:
user = User(username='sam', email='sam@test.com', password='12345678') user = User(username='sam', email='sam@test.com', password='12345678')
user.weekm = True user.weekm = True
db.session.add(user) db.session.add(user)
@ -154,7 +155,7 @@ def user_3():
@pytest.fixture() @pytest.fixture()
def sport_1_cycling(): def sport_1_cycling() -> Sport:
sport = Sport(label='Cycling') sport = Sport(label='Cycling')
db.session.add(sport) db.session.add(sport)
db.session.commit() db.session.commit()
@ -162,7 +163,7 @@ def sport_1_cycling():
@pytest.fixture() @pytest.fixture()
def sport_1_cycling_inactive(): def sport_1_cycling_inactive() -> Sport:
sport = Sport(label='Cycling') sport = Sport(label='Cycling')
sport.is_active = False sport.is_active = False
db.session.add(sport) db.session.add(sport)
@ -171,7 +172,7 @@ def sport_1_cycling_inactive():
@pytest.fixture() @pytest.fixture()
def sport_2_running(): def sport_2_running() -> Sport:
sport = Sport(label='Running') sport = Sport(label='Running')
db.session.add(sport) db.session.add(sport)
db.session.commit() db.session.commit()
@ -179,7 +180,7 @@ def sport_2_running():
@pytest.fixture() @pytest.fixture()
def activity_cycling_user_1(): def activity_cycling_user_1() -> Activity:
activity = Activity( activity = Activity(
user_id=1, user_id=1,
sport_id=1, sport_id=1,
@ -196,7 +197,9 @@ def activity_cycling_user_1():
@pytest.fixture() @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_segment = ActivitySegment(
activity_id=activity_cycling_user_1.id, activity_id=activity_cycling_user_1.id,
activity_uuid=activity_cycling_user_1.uuid, activity_uuid=activity_cycling_user_1.uuid,
@ -211,7 +214,7 @@ def activity_cycling_user_1_segment(activity_cycling_user_1):
@pytest.fixture() @pytest.fixture()
def activity_running_user_1(): def activity_running_user_1() -> Activity:
activity = Activity( activity = Activity(
user_id=1, user_id=1,
sport_id=2, sport_id=2,
@ -226,7 +229,7 @@ def activity_running_user_1():
@pytest.fixture() @pytest.fixture()
def seven_activities_user_1(): def seven_activities_user_1() -> Activity:
activity = Activity( activity = Activity(
user_id=1, user_id=1,
sport_id=1, sport_id=1,
@ -308,7 +311,7 @@ def seven_activities_user_1():
@pytest.fixture() @pytest.fixture()
def activity_cycling_user_2(): def activity_cycling_user_2() -> Activity:
activity = Activity( activity = Activity(
user_id=2, user_id=2,
sport_id=1, sport_id=1,
@ -323,7 +326,7 @@ def activity_cycling_user_2():
@pytest.fixture() @pytest.fixture()
def gpx_file(): def gpx_file() -> str:
return ( return (
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>' '<?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 '<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() @pytest.fixture()
def gpx_file_wo_name(): def gpx_file_wo_name() -> str:
return ( return (
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>' '<?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 '<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() @pytest.fixture()
def gpx_file_wo_track(): def gpx_file_wo_track() -> str:
return ( return (
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>' '<?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 '<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() @pytest.fixture()
def gpx_file_invalid_xml(): def gpx_file_invalid_xml() -> str:
return ( return (
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>' '<?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 '<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() @pytest.fixture()
def gpx_file_with_segments(): def gpx_file_with_segments() -> str:
return ( return (
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>' '<?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 '<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

View File

@ -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 import email_service
from fittrackee.email.email import EmailMessage from fittrackee.email.email import EmailMessage
from flask import Flask
from ..template_results.password_reset_request import expected_en_text_body from ..template_results.password_reset_request import expected_en_text_body
class TestEmailMessage: class TestEmailMessage:
def test_it_generate_email_data(self): def test_it_generate_email_data(self) -> None:
message = EmailMessage( message = EmailMessage(
sender='fittrackee@example.com', sender='fittrackee@example.com',
recipient='test@test.com', recipient='test@test.com',
@ -40,14 +42,14 @@ class TestEmailSending:
} }
@staticmethod @staticmethod
def get_args(call_args): def get_args(call_args: Any) -> Any:
if len(call_args) == 2: if len(call_args) == 2:
args, _ = call_args args, _ = call_args
else: else:
_, args, _ = call_args _, args, _ = call_args
return args return args
def assert_smtp(self, smtp): def assert_smtp(self, smtp: Mock) -> None:
assert smtp.sendmail.call_count == 1 assert smtp.sendmail.call_count == 1
call_args = self.get_args(smtp.sendmail.call_args) call_args = self.get_args(smtp.sendmail.call_args)
assert call_args[0] == 'fittrackee@example.com' assert call_args[0] == 'fittrackee@example.com'
@ -56,7 +58,9 @@ class TestEmailSending:
@patch('smtplib.SMTP_SSL') @patch('smtplib.SMTP_SSL')
@patch('smtplib.SMTP') @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( email_service.send(
template='password_reset_request', template='password_reset_request',
@ -72,8 +76,8 @@ class TestEmailSending:
@patch('smtplib.SMTP_SSL') @patch('smtplib.SMTP_SSL')
@patch('smtplib.SMTP') @patch('smtplib.SMTP')
def test_it_sends_message_with_ssl( 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( email_service.send(
template='password_reset_request', template='password_reset_request',
lang='en', lang='en',
@ -88,8 +92,8 @@ class TestEmailSending:
@patch('smtplib.SMTP_SSL') @patch('smtplib.SMTP_SSL')
@patch('smtplib.SMTP') @patch('smtplib.SMTP')
def test_it_sends_message_with_tls( 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( email_service.send(
template='password_reset_request', template='password_reset_request',
lang='en', lang='en',

View File

@ -1,5 +1,6 @@
import pytest import pytest
from fittrackee.email.email import EmailTemplate from fittrackee.email.email import EmailTemplate
from flask import Flask
from ..template_results.password_reset_request import ( from ..template_results.password_reset_request import (
expected_en_html_body, expected_en_html_body,
@ -17,8 +18,10 @@ class TestEmailTemplateForPasswordRequest:
('fr', 'FitTrackee - Réinitialiser votre mot de passe'), ('fr', 'FitTrackee - Réinitialiser votre mot de passe'),
], ],
) )
def test_it_gets_subject(self, app, lang, expected_subject): def test_it_gets_subject(
email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER')) self, app: Flask, lang: str, expected_subject: str
) -> None:
email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
subject = email_template.get_content( subject = email_template.get_content(
'password_reset_request', lang, 'subject.txt', {} 'password_reset_request', lang, 'subject.txt', {}
@ -30,8 +33,10 @@ class TestEmailTemplateForPasswordRequest:
'lang, expected_text_body', 'lang, expected_text_body',
[('en', expected_en_text_body), ('fr', expected_fr_text_body)], [('en', expected_en_text_body), ('fr', expected_fr_text_body)],
) )
def test_it_gets_text_body(self, app, lang, expected_text_body): def test_it_gets_text_body(
email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER')) self, app: Flask, lang: str, expected_text_body: str
) -> None:
email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
email_data = { email_data = {
'expiration_delay': '3 seconds' if lang == 'en' else '3 secondes', 'expiration_delay': '3 seconds' if lang == 'en' else '3 secondes',
'username': 'test', 'username': 'test',
@ -46,8 +51,8 @@ class TestEmailTemplateForPasswordRequest:
assert text_body == expected_text_body assert text_body == expected_text_body
def test_it_gets_en_html_body(self, app): def test_it_gets_en_html_body(self, app: Flask) -> None:
email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER')) email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
email_data = { email_data = {
'expiration_delay': '3 seconds', 'expiration_delay': '3 seconds',
'username': 'test', 'username': 'test',
@ -62,8 +67,8 @@ class TestEmailTemplateForPasswordRequest:
assert expected_en_html_body in text_body assert expected_en_html_body in text_body
def test_it_gets_fr_html_body(self, app): def test_it_gets_fr_html_body(self, app: Flask) -> None:
email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER')) email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
email_data = { email_data = {
'expiration_delay': '3 secondes', 'expiration_delay': '3 secondes',
'username': 'test', 'username': 'test',

View File

@ -3,12 +3,12 @@ from fittrackee.email.utils_email import InvalidEmailUrlScheme, parse_email_url
class TestEmailUrlParser: 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' url = 'stmp://username:password@localhost:587'
with pytest.raises(InvalidEmailUrlScheme): with pytest.raises(InvalidEmailUrlScheme):
parse_email_url(url) 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' url = 'smtp://test@example.com:12345678@localhost:25'
parsed_email = parse_email_url(url) parsed_email = parse_email_url(url)
assert parsed_email['username'] == 'test@example.com' assert parsed_email['username'] == 'test@example.com'
@ -18,7 +18,7 @@ class TestEmailUrlParser:
assert parsed_email['use_tls'] is False assert parsed_email['use_tls'] is False
assert parsed_email['use_ssl'] 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' url = 'smtp://test@example.com:12345678@localhost:587?tls=True'
parsed_email = parse_email_url(url) parsed_email = parse_email_url(url)
assert parsed_email['username'] == 'test@example.com' assert parsed_email['username'] == 'test@example.com'
@ -28,7 +28,7 @@ class TestEmailUrlParser:
assert parsed_email['use_tls'] is True assert parsed_email['use_tls'] is True
assert parsed_email['use_ssl'] is False 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' url = 'smtp://test@example.com:12345678@localhost:465?ssl=True'
parsed_email = parse_email_url(url) parsed_email = parse_email_url(url)
assert parsed_email['username'] == 'test@example.com' assert parsed_email['username'] == 'test@example.com'

View File

@ -1,14 +1,17 @@
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from io import BytesIO 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 fittrackee.users.utils_token import get_user_token
from flask import Flask
from freezegun import freeze_time from freezegun import freeze_time
class TestUserRegistration: class TestUserRegistration:
def test_user_can_register(self, app): def test_user_can_register(self, app: Flask) -> None:
client = app.test_client() client = app.test_client()
response = client.post( response = client.post(
@ -31,7 +34,9 @@ class TestUserRegistration:
assert response.content_type == 'application/json' assert response.content_type == 'application/json'
assert response.status_code == 201 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() client = app.test_client()
response = client.post( response = client.post(
'/api/auth/register', '/api/auth/register',
@ -51,7 +56,9 @@ class TestUserRegistration:
assert response.content_type == 'application/json' assert response.content_type == 'application/json'
assert response.status_code == 400 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() client = app.test_client()
response = client.post( response = client.post(
@ -73,7 +80,9 @@ class TestUserRegistration:
assert response.content_type == 'application/json' assert response.content_type == 'application/json'
assert response.status_code == 400 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() client = app.test_client()
response = client.post( response = client.post(
'/api/auth/register', '/api/auth/register',
@ -93,7 +102,7 @@ class TestUserRegistration:
assert response.content_type == 'application/json' assert response.content_type == 'application/json'
assert response.status_code == 400 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() client = app.test_client()
response = client.post( response = client.post(
@ -115,7 +124,9 @@ class TestUserRegistration:
assert response.content_type == 'application/json' assert response.content_type == 'application/json'
assert response.status_code == 400 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() client = app.test_client()
response = client.post( response = client.post(
@ -137,7 +148,7 @@ class TestUserRegistration:
assert response.content_type == 'application/json' assert response.content_type == 'application/json'
assert response.status_code == 400 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() client = app.test_client()
response = client.post( response = client.post(
@ -162,7 +173,7 @@ class TestUserRegistration:
assert response.content_type == 'application/json' assert response.content_type == 'application/json'
assert response.status_code == 400 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() client = app.test_client()
response = client.post( response = client.post(
'/api/auth/register', '/api/auth/register',
@ -174,7 +185,7 @@ class TestUserRegistration:
assert 'Invalid payload.', data['message'] assert 'Invalid payload.', data['message']
assert 'error', data['status'] 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() client = app.test_client()
response = client.post( response = client.post(
@ -194,7 +205,7 @@ class TestUserRegistration:
assert 'Invalid payload.' in data['message'] assert 'Invalid payload.' in data['message']
assert 'error' in data['status'] 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() client = app.test_client()
response = client.post( response = client.post(
@ -214,7 +225,7 @@ class TestUserRegistration:
assert 'Invalid payload.' in data['message'] assert 'Invalid payload.' in data['message']
assert 'error' in data['status'] 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() client = app.test_client()
response = client.post( response = client.post(
@ -234,7 +245,9 @@ class TestUserRegistration:
assert 'Invalid payload.', data['message'] assert 'Invalid payload.', data['message']
assert 'error', data['status'] 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() client = app.test_client()
response = client.post( response = client.post(
'/api/auth/register', '/api/auth/register',
@ -250,7 +263,7 @@ class TestUserRegistration:
assert 'Invalid payload.' in data['message'] assert 'Invalid payload.' in data['message']
assert 'error' in data['status'] 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() client = app.test_client()
response = client.post( response = client.post(
@ -276,7 +289,7 @@ class TestUserRegistration:
class TestUserLogin: 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() client = app.test_client()
response = client.post( response = client.post(
@ -292,7 +305,9 @@ class TestUserLogin:
assert data['message'] == 'Successfully logged in.' assert data['message'] == 'Successfully logged in.'
assert data['auth_token'] 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() client = app.test_client()
response = client.post( response = client.post(
@ -307,7 +322,7 @@ class TestUserLogin:
assert data['status'] == 'error' assert data['status'] == 'error'
assert data['message'] == 'Invalid credentials.' 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() client = app.test_client()
response = client.post( response = client.post(
@ -322,7 +337,9 @@ class TestUserLogin:
assert data['status'] == 'error' assert data['status'] == 'error'
assert data['message'] == 'Invalid payload.' 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() client = app.test_client()
response = client.post( response = client.post(
@ -339,7 +356,7 @@ class TestUserLogin:
class TestUserLogout: 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -360,7 +377,9 @@ class TestUserLogout:
assert data['message'] == 'Successfully logged out.' assert data['message'] == 'Successfully logged out.'
assert response.status_code == 200 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() client = app.test_client()
now = datetime.utcnow() now = datetime.utcnow()
resp_login = client.post( resp_login = client.post(
@ -381,7 +400,7 @@ class TestUserLogout:
assert data['message'] == 'Signature expired. Please log in again.' assert data['message'] == 'Signature expired. Please log in again.'
assert response.status_code == 401 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() client = app.test_client()
response = client.get( response = client.get(
'/api/auth/logout', headers=dict(Authorization='Bearer invalid') '/api/auth/logout', headers=dict(Authorization='Bearer invalid')
@ -391,7 +410,7 @@ class TestUserLogout:
assert data['message'] == 'Invalid token. Please log in again.' assert data['message'] == 'Invalid token. Please log in again.'
assert response.status_code == 401 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() client = app.test_client()
response = client.get('/api/auth/logout', headers=dict()) response = client.get('/api/auth/logout', headers=dict())
data = json.loads(response.data.decode()) data = json.loads(response.data.decode())
@ -401,7 +420,9 @@ class TestUserLogout:
class TestUserProfile: 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -432,7 +453,9 @@ class TestUserProfile:
assert data['data']['total_duration'] == '0:00:00' assert data['data']['total_duration'] == '0:00:00'
assert response.status_code == 200 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -470,13 +493,13 @@ class TestUserProfile:
def test_it_returns_user_profile_with_activities( def test_it_returns_user_profile_with_activities(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
activity_cycling_user_1, activity_cycling_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -505,7 +528,7 @@ class TestUserProfile:
assert data['data']['total_duration'] == '2:40:00' assert data['data']['total_duration'] == '2:40:00'
assert response.status_code == 200 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() client = app.test_client()
response = client.get( response = client.get(
'/api/auth/profile', headers=dict(Authorization='Bearer invalid') '/api/auth/profile', headers=dict(Authorization='Bearer invalid')
@ -517,7 +540,7 @@ class TestUserProfile:
class TestUserProfileUpdate: 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -568,7 +591,9 @@ class TestUserProfileUpdate:
assert data['data']['total_distance'] == 0 assert data['data']['total_distance'] == 0
assert data['data']['total_duration'] == '0:00:00' 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -617,7 +642,9 @@ class TestUserProfileUpdate:
assert data['data']['total_distance'] == 0 assert data['data']['total_distance'] == 0
assert data['data']['total_duration'] == '0:00:00' 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -638,7 +665,9 @@ class TestUserProfileUpdate:
assert data['message'] == 'Invalid payload.' assert data['message'] == 'Invalid payload.'
assert response.status_code == 400 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -659,7 +688,9 @@ class TestUserProfileUpdate:
assert 'Invalid payload.' in data['message'] assert 'Invalid payload.' in data['message']
assert 'error' in data['status'] 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -697,8 +728,8 @@ class TestUserProfileUpdate:
assert response.status_code == 400 assert response.status_code == 400
def test_it_returns_error_if_password_confirmation_is_missing( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -736,7 +767,7 @@ class TestUserProfileUpdate:
class TestUserPicture: 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -774,7 +805,9 @@ class TestUserPicture:
assert 'avatar.png' not in user_1.picture assert 'avatar.png' not in user_1.picture
assert 'avatar2.png' 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -794,7 +827,9 @@ class TestUserPicture:
assert data['message'] == 'No file part.' assert data['message'] == 'No file part.'
assert response.status_code == 400 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -818,8 +853,8 @@ class TestUserPicture:
class TestRegistrationConfiguration: class TestRegistrationConfiguration:
def test_it_returns_error_if_it_exceeds_max_users( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
@ -859,8 +894,12 @@ class TestRegistrationConfiguration:
assert data['message'] == 'Error. Registration is disabled.' assert data['message'] == 'Error. Registration is disabled.'
def test_it_disables_registration_on_user_registration( 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 app_config.max_users = 3
client = app_no_config.test_client() client = app_no_config.test_client()
client.post( client.post(
@ -894,11 +933,11 @@ class TestRegistrationConfiguration:
def test_it_does_not_disable_registration_on_user_registration( def test_it_does_not_disable_registration_on_user_registration(
self, self,
app_no_config, app_no_config: Flask,
app_config, app_config: Flask,
user_1_admin, user_1_admin: User,
user_2, user_2: User,
): ) -> None:
app_config.max_users = 4 app_config.max_users = 4
client = app_no_config.test_client() client = app_no_config.test_client()
client.post( client.post(
@ -932,8 +971,8 @@ class TestPasswordResetRequest:
@patch('smtplib.SMTP_SSL') @patch('smtplib.SMTP_SSL')
@patch('smtplib.SMTP') @patch('smtplib.SMTP')
def test_it_requests_password_reset_when_user_exists( 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() client = app.test_client()
response = client.post( response = client.post(
'/api/auth/password/reset-request', '/api/auth/password/reset-request',
@ -946,7 +985,9 @@ class TestPasswordResetRequest:
assert data['status'] == 'success' assert data['status'] == 'success'
assert data['message'] == 'Password reset request processed.' 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() client = app.test_client()
response = client.post( response = client.post(
@ -960,7 +1001,7 @@ class TestPasswordResetRequest:
assert data['status'] == 'success' assert data['status'] == 'success'
assert data['message'] == 'Password reset request processed.' 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() client = app.test_client()
response = client.post( response = client.post(
@ -974,7 +1015,7 @@ class TestPasswordResetRequest:
assert data['message'] == 'Invalid payload.' assert data['message'] == 'Invalid payload.'
assert data['status'] == 'error' 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() client = app.test_client()
response = client.post( response = client.post(
@ -990,7 +1031,7 @@ class TestPasswordResetRequest:
class TestPasswordUpdate: 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() client = app.test_client()
response = client.post( response = client.post(
@ -1009,7 +1050,7 @@ class TestPasswordUpdate:
assert data['status'] == 'error' assert data['status'] == 'error'
assert data['message'] == 'Invalid payload.' 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() client = app.test_client()
response = client.post( response = client.post(
@ -1028,7 +1069,7 @@ class TestPasswordUpdate:
assert data['status'] == 'error' assert data['status'] == 'error'
assert data['message'] == 'Invalid payload.' 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() client = app.test_client()
response = client.post( response = client.post(
@ -1047,7 +1088,9 @@ class TestPasswordUpdate:
assert data['status'] == 'error' assert data['status'] == 'error'
assert data['message'] == 'Invalid payload.' 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() client = app.test_client()
response = client.post( response = client.post(
@ -1066,7 +1109,7 @@ class TestPasswordUpdate:
assert data['status'] == 'error' assert data['status'] == 'error'
assert data['message'] == 'Invalid payload.' 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) token = get_user_token(1)
client = app.test_client() client = app.test_client()
@ -1087,7 +1130,9 @@ class TestPasswordUpdate:
assert data['status'] == 'error' assert data['status'] == 'error'
assert data['message'] == 'Invalid token. Please request a new token.' 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() now = datetime.utcnow()
token = get_user_token(user_1.id, password_reset=True) token = get_user_token(user_1.id, password_reset=True)
client = app.test_client() client = app.test_client()
@ -1112,7 +1157,9 @@ class TestPasswordUpdate:
data['message'] == 'Invalid token. Please request a new token.' 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) token = get_user_token(user_1.id, password_reset=True)
client = app.test_client() client = app.test_client()
@ -1133,7 +1180,7 @@ class TestPasswordUpdate:
assert data['status'] == 'error' assert data['status'] == 'error'
assert data['message'] == 'Password: 8 characters required.\n' 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) token = get_user_token(user_1.id, password_reset=True)
client = app.test_client() client = app.test_client()

View File

@ -3,9 +3,15 @@ from datetime import datetime, timedelta
from io import BytesIO from io import BytesIO
from unittest.mock import patch from unittest.mock import patch
from fittrackee.activities.models import Activity, Sport
from fittrackee.users.models import User
from flask import Flask
class TestGetUser: 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -47,13 +53,13 @@ class TestGetUser:
def test_it_gets_single_user_with_activities( def test_it_gets_single_user_with_activities(
self, self,
app, app: Flask,
user_1, user_1: User,
sport_1_cycling, sport_1_cycling: Sport,
sport_2_running, sport_2_running: Sport,
activity_cycling_user_1, activity_cycling_user_1: Activity,
activity_running_user_1, activity_running_user_1: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -93,7 +99,9 @@ class TestGetUser:
assert user['total_distance'] == 22 assert user['total_distance'] == 22
assert user['total_duration'] == '2:40:00' 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -116,7 +124,9 @@ class TestGetUser:
class TestGetUsers: 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -179,16 +189,16 @@ class TestGetUsers:
def test_it_gets_users_list_with_activities( def test_it_gets_users_list_with_activities(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
user_3, user_3: User,
sport_1_cycling, sport_1_cycling: Sport,
activity_cycling_user_1, activity_cycling_user_1: Activity,
sport_2_running, sport_2_running: Sport,
activity_running_user_1, activity_running_user_1: Activity,
activity_cycling_user_2, activity_cycling_user_2: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -249,11 +259,11 @@ class TestGetUsers:
@patch('fittrackee.users.users.USER_PER_PAGE', 2) @patch('fittrackee.users.users.USER_PER_PAGE', 2)
def test_it_gets_first_page_on_users_list( def test_it_gets_first_page_on_users_list(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
user_3, user_3: User,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -284,11 +294,11 @@ class TestGetUsers:
@patch('fittrackee.users.users.USER_PER_PAGE', 2) @patch('fittrackee.users.users.USER_PER_PAGE', 2)
def test_it_gets_next_page_on_users_list( def test_it_gets_next_page_on_users_list(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
user_3, user_3: User,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -318,11 +328,11 @@ class TestGetUsers:
def test_it_gets_empty_next_page_on_users_list( def test_it_gets_empty_next_page_on_users_list(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
user_3, user_3: User,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -352,11 +362,11 @@ class TestGetUsers:
def test_it_gets_user_list_with_2_per_page( def test_it_gets_user_list_with_2_per_page(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
user_3, user_3: User,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -386,11 +396,11 @@ class TestGetUsers:
def test_it_gets_next_page_on_user_list_with_2_per_page( def test_it_gets_next_page_on_user_list_with_2_per_page(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
user_3, user_3: User,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -419,8 +429,8 @@ class TestGetUsers:
} }
def test_it_gets_users_list_ordered_by_username( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -451,8 +461,8 @@ class TestGetUsers:
} }
def test_it_gets_users_list_ordered_by_username_ascending( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -484,8 +494,8 @@ class TestGetUsers:
} }
def test_it_gets_users_list_ordered_by_username_descending( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -517,8 +527,8 @@ class TestGetUsers:
} }
def test_it_gets_users_list_ordered_by_creation_date( 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_2.created_at = datetime.utcnow() - timedelta(days=1)
user_3.created_at = datetime.utcnow() - timedelta(hours=1) user_3.created_at = datetime.utcnow() - timedelta(hours=1)
user_1_admin.created_at = datetime.utcnow() user_1_admin.created_at = datetime.utcnow()
@ -555,8 +565,8 @@ class TestGetUsers:
} }
def test_it_gets_users_list_ordered_by_creation_date_ascending( 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_2.created_at = datetime.utcnow() - timedelta(days=1)
user_3.created_at = datetime.utcnow() - timedelta(hours=1) user_3.created_at = datetime.utcnow() - timedelta(hours=1)
user_1_admin.created_at = datetime.utcnow() user_1_admin.created_at = datetime.utcnow()
@ -593,8 +603,8 @@ class TestGetUsers:
} }
def test_it_gets_users_list_ordered_by_creation_date_descending( 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_2.created_at = datetime.utcnow() - timedelta(days=1)
user_3.created_at = datetime.utcnow() - timedelta(hours=1) user_3.created_at = datetime.utcnow() - timedelta(hours=1)
user_1_admin.created_at = datetime.utcnow() user_1_admin.created_at = datetime.utcnow()
@ -631,8 +641,8 @@ class TestGetUsers:
} }
def test_it_gets_users_list_ordered_by_admin_rights( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -666,8 +676,8 @@ class TestGetUsers:
} }
def test_it_gets_users_list_ordered_by_admin_rights_ascending( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -701,8 +711,8 @@ class TestGetUsers:
} }
def test_it_gets_users_list_ordered_by_admin_rights_descending( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -737,13 +747,13 @@ class TestGetUsers:
def test_it_gets_users_list_ordered_by_activities_count( def test_it_gets_users_list_ordered_by_activities_count(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
user_3, user_3: User,
sport_1_cycling, sport_1_cycling: Sport,
activity_cycling_user_2, activity_cycling_user_2: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -779,13 +789,13 @@ class TestGetUsers:
def test_it_gets_users_list_ordered_by_activities_count_ascending( def test_it_gets_users_list_ordered_by_activities_count_ascending(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
user_3, user_3: User,
sport_1_cycling, sport_1_cycling: Sport,
activity_cycling_user_2, activity_cycling_user_2: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -821,13 +831,13 @@ class TestGetUsers:
def test_it_gets_users_list_ordered_by_activities_count_descending( def test_it_gets_users_list_ordered_by_activities_count_descending(
self, self,
app, app: Flask,
user_1, user_1: User,
user_2, user_2: User,
user_3, user_3: User,
sport_1_cycling, sport_1_cycling: Sport,
activity_cycling_user_2, activity_cycling_user_2: Activity,
): ) -> None:
client = app.test_client() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -862,8 +872,8 @@ class TestGetUsers:
} }
def test_it_gets_users_list_filtering_on_username( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -893,8 +903,8 @@ class TestGetUsers:
} }
def test_it_returns_empty_users_list_filtering_on_username( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -923,8 +933,8 @@ class TestGetUsers:
} }
def test_it_users_list_with_complex_query( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -955,7 +965,9 @@ class TestGetUsers:
class TestGetUserPicture: 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() client = app.test_client()
response = client.get(f'/api/users/{user_1.username}/picture') response = client.get(f'/api/users/{user_1.username}/picture')
@ -965,7 +977,9 @@ class TestGetUserPicture:
assert 'not found' in data['status'] assert 'not found' in data['status']
assert 'No picture.' in data['message'] 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() client = app.test_client()
response = client.get('/api/users/not_existing/picture') response = client.get('/api/users/not_existing/picture')
@ -977,7 +991,9 @@ class TestGetUserPicture:
class TestUpdateUser: 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1006,8 +1022,8 @@ class TestUpdateUser:
assert user['admin'] is True assert user['admin'] is True
def test_it_removes_admin_rights_to_a_user( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1037,8 +1053,8 @@ class TestUpdateUser:
assert user['admin'] is False assert user['admin'] is False
def test_it_returns_error_if_payload_for_admin_rights_is_empty( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1064,8 +1080,8 @@ class TestUpdateUser:
assert 'Invalid payload.' in data['message'] assert 'Invalid payload.' in data['message']
def test_it_returns_error_if_payload_for_admin_rights_is_invalid( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1094,8 +1110,8 @@ class TestUpdateUser:
) )
def test_it_returns_error_if_user_can_not_change_admin_rights( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1120,7 +1136,9 @@ class TestUpdateUser:
class TestDeleteUser: 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1139,8 +1157,8 @@ class TestDeleteUser:
assert response.status_code == 204 assert response.status_code == 204
def test_user_with_activity_can_delete_its_own_account( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1171,8 +1189,8 @@ class TestDeleteUser:
assert response.status_code == 204 assert response.status_code == 204
def test_user_with_picture_can_delete_its_own_account( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1200,8 +1218,8 @@ class TestDeleteUser:
assert response.status_code == 204 assert response.status_code == 204
def test_user_can_not_delete_another_user_account( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1223,8 +1241,8 @@ class TestDeleteUser:
assert 'You do not have permissions.' in data['message'] assert 'You do not have permissions.' in data['message']
def test_it_returns_error_when_deleting_non_existing_user( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1246,8 +1264,8 @@ class TestDeleteUser:
assert 'User does not exist.' in data['message'] assert 'User does not exist.' in data['message']
def test_admin_can_delete_another_user_account( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1268,8 +1286,8 @@ class TestDeleteUser:
assert response.status_code == 204 assert response.status_code == 204
def test_admin_can_delete_its_own_account( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1290,8 +1308,8 @@ class TestDeleteUser:
assert response.status_code == 204 assert response.status_code == 204
def test_admin_can_not_delete_its_own_account_if_no_other_admin( 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() client = app.test_client()
resp_login = client.post( resp_login = client.post(
'/api/auth/login', '/api/auth/login',
@ -1317,8 +1335,13 @@ class TestDeleteUser:
) )
def test_it_enables_registration_on_user_delete( 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 app_config.max_users = 3
client = app_no_config.test_client() client = app_no_config.test_client()
resp_login = client.post( resp_login = client.post(
@ -1351,8 +1374,13 @@ class TestDeleteUser:
assert response.status_code == 201 assert response.status_code == 201
def test_it_does_not_enable_registration_on_user_delete( 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 app_config.max_users = 2
client = app_no_config.test_client() client = app_no_config.test_client()
resp_login = client.post( resp_login = client.post(

View File

@ -1,8 +1,9 @@
from fittrackee.users.models import User from fittrackee.users.models import User
from flask import Flask
class TestUserModel: 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) assert '<User \'test\'>' == str(user_1)
serialized_user = user_1.serialize() serialized_user = user_1.serialize()
@ -23,15 +24,15 @@ class TestUserModel:
assert serialized_user['total_distance'] == 0 assert serialized_user['total_distance'] == 0
assert serialized_user['total_duration'] == '0:00:00' 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) auth_token = user_1.encode_auth_token(user_1.id)
assert isinstance(auth_token, str) 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) password_token = user_1.encode_password_reset_token(user_1.id)
assert isinstance(password_token, str) 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) auth_token = user_1.encode_auth_token(user_1.id)
assert isinstance(auth_token, str) assert isinstance(auth_token, str)
assert User.decode_auth_token(auth_token) == user_1.id assert User.decode_auth_token(auth_token) == user_1.id

View File

@ -1,10 +1,12 @@
import datetime import datetime
import os import os
from typing import Dict, Tuple, Union
import jwt import jwt
from fittrackee import appLog, bcrypt, db from fittrackee import appLog, bcrypt, db
from fittrackee.responses import ( from fittrackee.responses import (
ForbiddenErrorResponse, ForbiddenErrorResponse,
HttpResponse,
InvalidPayloadErrorResponse, InvalidPayloadErrorResponse,
PayloadTooLargeErrorResponse, PayloadTooLargeErrorResponse,
UnauthorizedErrorResponse, UnauthorizedErrorResponse,
@ -32,7 +34,7 @@ auth_blueprint = Blueprint('auth', __name__)
@auth_blueprint.route('/auth/register', methods=['POST']) @auth_blueprint.route('/auth/register', methods=['POST'])
def register_user(): def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
""" """
register a user register a user
@ -144,7 +146,7 @@ def register_user():
@auth_blueprint.route('/auth/login', methods=['POST']) @auth_blueprint.route('/auth/login', methods=['POST'])
def login_user(): def login_user() -> Union[Dict, HttpResponse]:
""" """
user login user login
@ -216,7 +218,7 @@ def login_user():
@auth_blueprint.route('/auth/logout', methods=['GET']) @auth_blueprint.route('/auth/logout', methods=['GET'])
@authenticate @authenticate
def logout_user(auth_user_id): def logout_user(auth_user_id: int) -> Union[Dict, HttpResponse]:
""" """
user logout user logout
@ -277,7 +279,9 @@ def logout_user(auth_user_id):
@auth_blueprint.route('/auth/profile', methods=['GET']) @auth_blueprint.route('/auth/profile', methods=['GET'])
@authenticate @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 get authenticated user info
@ -338,7 +342,7 @@ def get_authenticated_user_profile(auth_user_id):
@auth_blueprint.route('/auth/profile/edit', methods=['POST']) @auth_blueprint.route('/auth/profile/edit', methods=['POST'])
@authenticate @authenticate
def edit_user(auth_user_id): def edit_user(auth_user_id: int) -> Union[Dict, HttpResponse]:
""" """
edit authenticated user edit authenticated user
@ -474,7 +478,7 @@ def edit_user(auth_user_id):
@auth_blueprint.route('/auth/picture', methods=['POST']) @auth_blueprint.route('/auth/picture', methods=['POST'])
@authenticate @authenticate
def edit_picture(auth_user_id): def edit_picture(auth_user_id: int) -> Union[Dict, HttpResponse]:
""" """
update authenticated user picture update authenticated user picture
@ -561,7 +565,7 @@ def edit_picture(auth_user_id):
@auth_blueprint.route('/auth/picture', methods=['DELETE']) @auth_blueprint.route('/auth/picture', methods=['DELETE'])
@authenticate @authenticate
def del_picture(auth_user_id): def del_picture(auth_user_id: int) -> Union[Tuple[Dict, int], HttpResponse]:
""" """
delete authenticated user picture delete authenticated user picture
@ -604,7 +608,7 @@ def del_picture(auth_user_id):
@auth_blueprint.route('/auth/password/reset-request', methods=['POST']) @auth_blueprint.route('/auth/password/reset-request', methods=['POST'])
def request_password_reset(): def request_password_reset() -> Union[Dict, HttpResponse]:
""" """
handle password reset request handle password reset request
@ -644,15 +648,15 @@ def request_password_reset():
ui_url = current_app.config['UI_URL'] ui_url = current_app.config['UI_URL']
email_data = { email_data = {
'expiration_delay': get_readable_duration( '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, 'en' if user.language is None else user.language,
), ),
'username': user.username, 'username': user.username,
'password_reset_url': ( 'password_reset_url': (
f'{ui_url}/password-reset?token={password_reset_token}' # noqa f'{ui_url}/password-reset?token={password_reset_token}' # noqa
), ),
'operating_system': request.user_agent.platform, 'operating_system': request.user_agent.platform, # type: ignore
'browser_name': request.user_agent.browser, 'browser_name': request.user_agent.browser, # type: ignore
} }
user_data = { user_data = {
'language': user.language if user.language else 'en', 'language': user.language if user.language else 'en',
@ -666,7 +670,7 @@ def request_password_reset():
@auth_blueprint.route('/auth/password/update', methods=['POST']) @auth_blueprint.route('/auth/password/update', methods=['POST'])
def update_password(): def update_password() -> Union[Dict, HttpResponse]:
""" """
update user password update user password

View File

@ -1,18 +1,22 @@
from datetime import datetime from datetime import datetime
from typing import Dict, Optional, Union
import jwt import jwt
from fittrackee import bcrypt, db from fittrackee import bcrypt, db
from flask import current_app from flask import current_app
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.sql.expression import select from sqlalchemy.sql.expression import select
from ..activities.models import Activity from ..activities.models import Activity
from .utils_token import decode_user_token, get_user_token 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) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(20), unique=True, nullable=False) username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(120), 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) language = db.Column(db.String(50), nullable=True)
def __repr__(self): def __repr__(self) -> str:
return f'<User {self.username!r}>' return f'<User {self.username!r}>'
def __init__( 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.username = username
self.email = email self.email = email
self.password = bcrypt.generate_password_hash( self.password = bcrypt.generate_password_hash(
@ -50,31 +58,25 @@ class User(db.Model):
self.created_at = created_at self.created_at = created_at
@staticmethod @staticmethod
def encode_auth_token(user_id): def encode_auth_token(user_id: int) -> str:
""" """
Generates the auth token Generates the auth token
:param user_id: - :param user_id: -
:return: JWToken :return: JWToken
""" """
try:
return get_user_token(user_id) return get_user_token(user_id)
except Exception as e:
return e
@staticmethod @staticmethod
def encode_password_reset_token(user_id): def encode_password_reset_token(user_id: int) -> str:
""" """
Generates the auth token Generates the auth token
:param user_id: - :param user_id: -
:return: JWToken :return: JWToken
""" """
try:
return get_user_token(user_id, password_reset=True) return get_user_token(user_id, password_reset=True)
except Exception as e:
return e
@staticmethod @staticmethod
def decode_auth_token(auth_token): def decode_auth_token(auth_token: str) -> Union[int, str]:
""" """
Decodes the auth token Decodes the auth token
:param auth_token: - :param auth_token: -
@ -88,21 +90,21 @@ class User(db.Model):
return 'Invalid token. Please log in again.' return 'Invalid token. Please log in again.'
@hybrid_property @hybrid_property
def activities_count(self): def activities_count(self) -> int:
return Activity.query.filter(Activity.user_id == self.id).count() return Activity.query.filter(Activity.user_id == self.id).count()
@activities_count.expression @activities_count.expression # type: ignore
def activities_count(self): def activities_count(self) -> int:
return ( return (
select([func.count(Activity.id)]) select([func.count(Activity.id)])
.where(Activity.user_id == self.id) .where(Activity.user_id == self.id)
.label("activities_count") .label('activities_count')
) )
def serialize(self): def serialize(self) -> Dict:
sports = [] sports = []
total = (None, None) total = (0, '0:00:00')
if self.activities_count > 0: if self.activities_count > 0: # type: ignore
sports = ( sports = (
db.session.query(Activity.sport_id) db.session.query(Activity.sport_id)
.filter(Activity.user_id == self.id) .filter(Activity.user_id == self.id)
@ -136,6 +138,6 @@ class User(db.Model):
'sports_list': [ 'sports_list': [
sport for sportslist in sports for sport in sportslist sport for sportslist in sports for sport in sportslist
], ],
'total_distance': float(total[0]) if total[0] else 0, 'total_distance': float(total[0]),
'total_duration': str(total[1]) if total[1] else "0:00:00", 'total_duration': str(total[1]),
} }

View File

@ -1,9 +1,11 @@
import os import os
import shutil import shutil
from typing import Any, Dict, Tuple, Union
from fittrackee import db from fittrackee import db
from fittrackee.responses import ( from fittrackee.responses import (
ForbiddenErrorResponse, ForbiddenErrorResponse,
HttpResponse,
InvalidPayloadErrorResponse, InvalidPayloadErrorResponse,
NotFoundErrorResponse, NotFoundErrorResponse,
UserNotFoundErrorResponse, UserNotFoundErrorResponse,
@ -23,7 +25,7 @@ USER_PER_PAGE = 10
@users_blueprint.route('/users', methods=['GET']) @users_blueprint.route('/users', methods=['GET'])
@authenticate @authenticate
def get_users(auth_user_id): def get_users(auth_user_id: int) -> Dict:
""" """
Get all users Get all users
@ -135,10 +137,10 @@ def get_users(auth_user_id):
User.username.like('%' + query + '%') if query else True, User.username.like('%' + query + '%') if query else True,
) )
.order_by( .order_by(
User.activities_count.asc() User.activities_count.asc() # type: ignore
if order_by == 'activities_count' and order == 'asc' if order_by == 'activities_count' and order == 'asc'
else True, else True,
User.activities_count.desc() User.activities_count.desc() # type: ignore
if order_by == 'activities_count' and order == 'desc' if order_by == 'activities_count' and order == 'desc'
else True, else True,
User.username.asc() User.username.asc()
@ -178,7 +180,9 @@ def get_users(auth_user_id):
@users_blueprint.route('/users/<user_name>', methods=['GET']) @users_blueprint.route('/users/<user_name>', methods=['GET'])
@authenticate @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 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']) @users_blueprint.route('/users/<user_name>/picture', methods=['GET'])
def get_picture(user_name): def get_picture(user_name: str) -> Any:
"""get user picture """get user picture
**Example request**: **Example request**:
@ -290,7 +294,9 @@ def get_picture(user_name):
@users_blueprint.route('/users/<user_name>', methods=['PATCH']) @users_blueprint.route('/users/<user_name>', methods=['PATCH'])
@authenticate_as_admin @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 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']) @users_blueprint.route('/users/<user_name>', methods=['DELETE'])
@authenticate @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 Delete a user account

View File

@ -1,30 +1,44 @@
import re import re
from datetime import timedelta from datetime import timedelta
from functools import wraps from functools import wraps
from typing import Any, Callable, Optional, Tuple, Union
import humanize import humanize
from fittrackee.responses import ( from fittrackee.responses import (
ForbiddenErrorResponse, ForbiddenErrorResponse,
HttpResponse,
InvalidPayloadErrorResponse, InvalidPayloadErrorResponse,
PayloadTooLargeErrorResponse, PayloadTooLargeErrorResponse,
UnauthorizedErrorResponse, UnauthorizedErrorResponse,
) )
from flask import current_app, request from flask import Request, current_app, request
from .models import User 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() user = User.query.filter_by(id=user_id).first()
return user.admin 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-.]+$)" mail_pattern = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
return re.match(mail_pattern, email) is not None 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 = '' ret = ''
if password_conf != password: if password_conf != password:
ret = 'Password and password confirmation don\'t match.\n' ret = 'Password and password confirmation don\'t match.\n'
@ -33,7 +47,14 @@ def check_passwords(password, password_conf):
return ret 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 = '' ret = ''
if not 2 < len(username) < 13: if not 2 < len(username) < 13:
ret += 'Username: 3 to 12 characters required.\n' ret += 'Username: 3 to 12 characters required.\n'
@ -43,7 +64,12 @@ def register_controls(username, email, password, password_conf):
return ret 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: if 'file' not in req.files:
return InvalidPayloadErrorResponse('No file part.', 'fail') return InvalidPayloadErrorResponse('No file part.', 'fail')
@ -66,7 +92,7 @@ def verify_extension_and_size(file_type, req):
if not ( if not (
file_extension file_extension
and file_extension in current_app.config.get(allowed_extensions) and file_extension in current_app.config[allowed_extensions]
): ):
return InvalidPayloadErrorResponse( return InvalidPayloadErrorResponse(
'File extension not allowed.', 'fail' 'File extension not allowed.', 'fail'
@ -81,7 +107,13 @@ def verify_extension_and_size(file_type, req):
return None 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.' default_message = 'Provide a valid auth token.'
auth_header = current_request.headers.get('Authorization') auth_header = current_request.headers.get('Authorization')
if not auth_header: if not auth_header:
@ -98,9 +130,11 @@ def verify_user(current_request, verify_admin):
return None, resp return None, resp
def authenticate(f): def authenticate(f: Callable) -> Callable:
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(
*args: Any, **kwargs: Any
) -> Union[Callable, HttpResponse]:
verify_admin = False verify_admin = False
response_object, resp = verify_user(request, verify_admin) response_object, resp = verify_user(request, verify_admin)
if response_object: if response_object:
@ -110,9 +144,11 @@ def authenticate(f):
return decorated_function return decorated_function
def authenticate_as_admin(f): def authenticate_as_admin(f: Callable) -> Callable:
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(
*args: Any, **kwargs: Any
) -> Union[Callable, HttpResponse]:
verify_admin = True verify_admin = True
response_object, resp = verify_user(request, verify_admin) response_object, resp = verify_user(request, verify_admin)
if response_object: if response_object:
@ -122,25 +158,36 @@ def authenticate_as_admin(f):
return decorated_function 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: if auth_user_id != activity_user_id:
return ForbiddenErrorResponse() return ForbiddenErrorResponse()
return None 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: if size_in_bytes == 0:
return '0 bytes' return '0 bytes'
if size_in_bytes == 1: if size_in_bytes == 1:
return '1 byte' return '1 byte'
for unit in [' bytes', 'KB', 'MB', 'GB', 'TB']: for unit in [' bytes', 'KB', 'MB', 'GB', 'TB']:
if abs(size_in_bytes) < 1024.0: 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 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': if locale is not None and locale != 'en':
_t = humanize.i18n.activate(locale) # noqa _t = humanize.i18n.activate(locale) # noqa
readable_duration = humanize.naturaldelta(timedelta(seconds=duration)) readable_duration = humanize.naturaldelta(timedelta(seconds=duration))

View File

@ -1,19 +1,25 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional
import jwt import jwt
from flask import current_app from flask import current_app
def get_user_token(user_id, password_reset=False): def get_user_token(
expiration_days = ( user_id: int, password_reset: Optional[bool] = False
0 ) -> str:
if password_reset """
else current_app.config.get('TOKEN_EXPIRATION_DAYS') 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 = ( expiration_seconds: float = (
current_app.config.get('PASSWORD_TOKEN_EXPIRATION_SECONDS') current_app.config['PASSWORD_TOKEN_EXPIRATION_SECONDS']
if password_reset if password_reset
else current_app.config.get('TOKEN_EXPIRATION_SECONDS') else current_app.config['TOKEN_EXPIRATION_SECONDS']
) )
payload = { payload = {
'exp': datetime.utcnow() 'exp': datetime.utcnow()
@ -23,15 +29,18 @@ def get_user_token(user_id, password_reset=False):
} }
return jwt.encode( return jwt.encode(
payload, payload,
current_app.config.get('SECRET_KEY'), current_app.config['SECRET_KEY'],
algorithm='HS256', algorithm='HS256',
) )
def decode_user_token(auth_token): def decode_user_token(auth_token: str) -> int:
"""
Return user id from token
"""
payload = jwt.decode( payload = jwt.decode(
auth_token, auth_token,
current_app.config.get('SECRET_KEY'), current_app.config['SECRET_KEY'],
algorithms=['HS256'], algorithms=['HS256'],
) )
return payload['sub'] return payload['sub']

99
poetry.lock generated
View File

@ -396,7 +396,7 @@ python-versions = "*"
[[package]] [[package]]
name = "isort" name = "isort"
version = "5.6.4" version = "5.7.0"
description = "A Python utility / library to sort Python imports." description = "A Python utility / library to sort Python imports."
category = "dev" category = "dev"
optional = false optional = false
@ -460,6 +460,22 @@ category = "dev"
optional = false optional = false
python-versions = "*" 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]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "0.4.3" version = "0.4.3"
@ -1095,7 +1111,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "4.55.0" version = "4.55.1"
description = "Fast, Extensible Progress Meter" description = "Fast, Extensible Progress Meter"
category = "main" category = "main"
optional = false optional = false
@ -1107,7 +1123,7 @@ telegram = ["requests"]
[[package]] [[package]]
name = "typed-ast" 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" description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev" category = "dev"
optional = false optional = false
@ -1162,7 +1178,7 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pyt
[metadata] [metadata]
lock-version = "1.0" lock-version = "1.0"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "b5c7cbb6e449c8c6cc94a7413ae2fc0b8889d738e3fc802566efc45cc5096287" content-hash = "11c3a22ee800d71d1d9804e33e756be81c5b7c56bba85d7de03a0f27acb35148"
[metadata.files] [metadata.files]
alabaster = [ alabaster = [
@ -1388,8 +1404,8 @@ iniconfig = [
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
] ]
isort = [ isort = [
{file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"}, {file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"},
{file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"}, {file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"},
] ]
itsdangerous = [ itsdangerous = [
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, {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-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, {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 = [ mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {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"}, {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"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
] ]
tqdm = [ tqdm = [
{file = "tqdm-4.55.0-py2.py3-none-any.whl", hash = "sha256:0cd81710de29754bf17b6fee07bdb86f956b4fa20d3078f02040f83e64309416"}, {file = "tqdm-4.55.1-py2.py3-none-any.whl", hash = "sha256:b8b46036fd00176d0870307123ef06bb851096964fa7fc578d789f90ce82c3e4"},
{file = "tqdm-4.55.0.tar.gz", hash = "sha256:f4f80b96e2ceafea69add7bf971b8403b9cba8fb4451c1220f91c79be4ebd208"}, {file = "tqdm-4.55.1.tar.gz", hash = "sha256:556c55b081bd9aa746d34125d024b73f0e2a0e62d5927ff0e400e20ee0a03b9a"},
] ]
typed-ast = [ typed-ast = [
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
{file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"},
{file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"},
{file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"},
{file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"},
{file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"},
{file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"},
{file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"},
{file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"},
{file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"},
{file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"},
{file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"},
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"},
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, {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 = [ typing-extensions = [
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},

View File

@ -42,6 +42,7 @@ tqdm = "^4.55"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
black = "^20.8b1" black = "^20.8b1"
freezegun = "^1.0.0" freezegun = "^1.0.0"
mypy = "^0.790"
pyopenssl = "^20.0" pyopenssl = "^20.0"
pytest = "^6.2" pytest = "^6.2"
pytest-black = "^0.3.12" pytest-black = "^0.3.12"