API - remove intermediate directory and rename api directory
99
fittrackee/__init__.py
Normal file
@ -0,0 +1,99 @@
|
||||
import logging
|
||||
import os
|
||||
from importlib import import_module, reload
|
||||
|
||||
from flask import Flask, render_template
|
||||
from flask_bcrypt import Bcrypt
|
||||
from flask_dramatiq import Dramatiq
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
from .email.email import Email
|
||||
|
||||
db = SQLAlchemy()
|
||||
bcrypt = Bcrypt()
|
||||
migrate = Migrate()
|
||||
email_service = Email()
|
||||
dramatiq = Dramatiq()
|
||||
appLog = logging.getLogger('fittrackee')
|
||||
|
||||
|
||||
def create_app():
|
||||
# instantiate the app
|
||||
app = Flask(__name__, static_folder='dist/static', template_folder='dist')
|
||||
|
||||
# set config
|
||||
with app.app_context():
|
||||
app_settings = os.getenv('APP_SETTINGS')
|
||||
if app_settings == 'fittrackee.config.TestingConfig':
|
||||
# reload config on tests
|
||||
config = import_module('fittrackee.config')
|
||||
reload(config)
|
||||
app.config.from_object(app_settings)
|
||||
|
||||
# set up extensions
|
||||
db.init_app(app)
|
||||
bcrypt.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
dramatiq.init_app(app)
|
||||
|
||||
# set up email
|
||||
email_service.init_email(app)
|
||||
|
||||
# get configuration from database
|
||||
from .application.models import AppConfig
|
||||
from .application.utils import init_config, update_app_config_from_database
|
||||
|
||||
with app.app_context():
|
||||
# Note: check if "app_config" table exist to avoid errors when
|
||||
# dropping tables on dev environments
|
||||
if db.engine.dialect.has_table(db.engine, 'app_config'):
|
||||
db_app_config = AppConfig.query.one_or_none()
|
||||
if not db_app_config:
|
||||
_, db_app_config = init_config()
|
||||
update_app_config_from_database(app, db_app_config)
|
||||
|
||||
from .activities.activities import activities_blueprint # noqa
|
||||
from .activities.records import records_blueprint # noqa
|
||||
from .activities.sports import sports_blueprint # noqa
|
||||
from .activities.stats import stats_blueprint # noqa
|
||||
from .application.app_config import config_blueprint # noqa
|
||||
from .users.auth import auth_blueprint # noqa
|
||||
from .users.users import users_blueprint # noqa
|
||||
|
||||
app.register_blueprint(users_blueprint, url_prefix='/api')
|
||||
app.register_blueprint(auth_blueprint, url_prefix='/api')
|
||||
app.register_blueprint(activities_blueprint, url_prefix='/api')
|
||||
app.register_blueprint(records_blueprint, url_prefix='/api')
|
||||
app.register_blueprint(sports_blueprint, url_prefix='/api')
|
||||
app.register_blueprint(stats_blueprint, url_prefix='/api')
|
||||
app.register_blueprint(config_blueprint, url_prefix='/api')
|
||||
|
||||
if app.debug:
|
||||
logging.getLogger('sqlalchemy').setLevel(logging.WARNING)
|
||||
logging.getLogger('sqlalchemy').handlers = logging.getLogger(
|
||||
'werkzeug'
|
||||
).handlers
|
||||
logging.getLogger('sqlalchemy.orm').setLevel(logging.WARNING)
|
||||
logging.getLogger('flake8').propagate = False
|
||||
appLog.setLevel(logging.DEBUG)
|
||||
|
||||
# Enable CORS
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
response.headers.add('Access-Control-Allow-Origin', '*')
|
||||
response.headers.add(
|
||||
'Access-Control-Allow-Headers', 'Content-Type,Authorization'
|
||||
)
|
||||
response.headers.add(
|
||||
'Access-Control-Allow-Methods',
|
||||
'GET,PUT,POST,DELETE,PATCH,OPTIONS',
|
||||
)
|
||||
return response
|
||||
|
||||
@app.route('/', defaults={'path': ''})
|
||||
@app.route('/<path:path>')
|
||||
def catch_all(path):
|
||||
return render_template('index.html')
|
||||
|
||||
return app
|
0
fittrackee/activities/__init__.py
Normal file
1299
fittrackee/activities/activities.py
Normal file
453
fittrackee/activities/models.py
Normal file
@ -0,0 +1,453 @@
|
||||
import datetime
|
||||
import os
|
||||
|
||||
from fittrackee import db
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy.event import listens_for
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm.session import object_session
|
||||
from sqlalchemy.types import JSON, Enum
|
||||
|
||||
from .utils_files import get_absolute_file_path
|
||||
from .utils_format import convert_in_duration, convert_value_to_integer
|
||||
|
||||
record_types = [
|
||||
'AS', # 'Best Average Speed'
|
||||
'FD', # 'Farthest Distance'
|
||||
'LD', # 'Longest Duration'
|
||||
'MS', # 'Max speed'
|
||||
]
|
||||
|
||||
|
||||
def update_records(user_id, sport_id, connection, session):
|
||||
record_table = Record.__table__
|
||||
new_records = Activity.get_user_activity_records(user_id, sport_id)
|
||||
for record_type, record_data in new_records.items():
|
||||
if record_data['record_value']:
|
||||
record = Record.query.filter_by(
|
||||
user_id=user_id, sport_id=sport_id, record_type=record_type
|
||||
).first()
|
||||
if record:
|
||||
value = convert_value_to_integer(
|
||||
record_type, record_data['record_value']
|
||||
)
|
||||
connection.execute(
|
||||
record_table.update()
|
||||
.where(record_table.c.id == record.id)
|
||||
.values(
|
||||
value=value,
|
||||
activity_id=record_data['activity'].id,
|
||||
activity_date=record_data['activity'].activity_date,
|
||||
)
|
||||
)
|
||||
else:
|
||||
new_record = Record(
|
||||
activity=record_data['activity'], record_type=record_type
|
||||
)
|
||||
new_record.value = record_data['record_value']
|
||||
session.add(new_record)
|
||||
else:
|
||||
connection.execute(
|
||||
record_table.delete()
|
||||
.where(record_table.c.user_id == user_id)
|
||||
.where(record_table.c.sport_id == sport_id)
|
||||
.where(record_table.c.record_type == record_type)
|
||||
)
|
||||
|
||||
|
||||
class Sport(db.Model):
|
||||
__tablename__ = "sports"
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
label = db.Column(db.String(50), unique=True, nullable=False)
|
||||
img = db.Column(db.String(255), unique=True, nullable=True)
|
||||
is_active = db.Column(db.Boolean, default=True, nullable=False)
|
||||
activities = db.relationship(
|
||||
'Activity', lazy=True, backref=db.backref('sports', lazy='joined')
|
||||
)
|
||||
records = db.relationship(
|
||||
'Record', lazy=True, backref=db.backref('sports', lazy='joined')
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Sport {self.label!r}>'
|
||||
|
||||
def __init__(self, label):
|
||||
self.label = label
|
||||
|
||||
def serialize(self, is_admin=False):
|
||||
serialized_sport = {
|
||||
'id': self.id,
|
||||
'label': self.label,
|
||||
'img': self.img,
|
||||
'is_active': self.is_active,
|
||||
}
|
||||
if is_admin:
|
||||
serialized_sport['has_activities'] = len(self.activities) > 0
|
||||
return serialized_sport
|
||||
|
||||
|
||||
class Activity(db.Model):
|
||||
__tablename__ = "activities"
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
sport_id = db.Column(
|
||||
db.Integer, db.ForeignKey('sports.id'), nullable=False
|
||||
)
|
||||
title = db.Column(db.String(255), nullable=True)
|
||||
gpx = db.Column(db.String(255), nullable=True)
|
||||
creation_date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
modification_date = db.Column(
|
||||
db.DateTime, onupdate=datetime.datetime.utcnow
|
||||
)
|
||||
activity_date = db.Column(db.DateTime, nullable=False)
|
||||
duration = db.Column(db.Interval, nullable=False)
|
||||
pauses = db.Column(db.Interval, nullable=True)
|
||||
moving = db.Column(db.Interval, nullable=True)
|
||||
distance = db.Column(db.Numeric(6, 3), nullable=True) # kilometers
|
||||
min_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters
|
||||
max_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters
|
||||
descent = db.Column(db.Numeric(7, 2), nullable=True) # meters
|
||||
ascent = db.Column(db.Numeric(7, 2), nullable=True) # meters
|
||||
max_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
|
||||
ave_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
|
||||
bounds = db.Column(postgresql.ARRAY(db.Float), nullable=True)
|
||||
map = db.Column(db.String(255), nullable=True)
|
||||
map_id = db.Column(db.String(50), nullable=True)
|
||||
weather_start = db.Column(JSON, nullable=True)
|
||||
weather_end = db.Column(JSON, nullable=True)
|
||||
notes = db.Column(db.String(500), nullable=True)
|
||||
segments = db.relationship(
|
||||
'ActivitySegment',
|
||||
lazy=True,
|
||||
cascade='all, delete',
|
||||
backref=db.backref('activities', lazy='joined', single_parent=True),
|
||||
)
|
||||
records = db.relationship(
|
||||
'Record',
|
||||
lazy=True,
|
||||
cascade='all, delete',
|
||||
backref=db.backref('activities', lazy='joined', single_parent=True),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'<Activity \'{self.sports.label}\' - {self.activity_date}>'
|
||||
|
||||
def __init__(self, user_id, sport_id, activity_date, distance, duration):
|
||||
self.user_id = user_id
|
||||
self.sport_id = sport_id
|
||||
self.activity_date = activity_date
|
||||
self.distance = distance
|
||||
self.duration = duration
|
||||
|
||||
def serialize(self, params=None):
|
||||
date_from = params.get('from') if params else None
|
||||
date_to = params.get('to') if params else None
|
||||
distance_from = params.get('distance_from') if params else None
|
||||
distance_to = params.get('distance_to') if params else None
|
||||
duration_from = params.get('duration_from') if params else None
|
||||
duration_to = params.get('duration_to') if params else None
|
||||
ave_speed_from = params.get('ave_speed_from') if params else None
|
||||
ave_speed_to = params.get('ave_speed_to') if params else None
|
||||
max_speed_from = params.get('max_speed_from') if params else None
|
||||
max_speed_to = params.get('max_speed_to') if params else None
|
||||
sport_id = params.get('sport_id') if params else None
|
||||
previous_activity = (
|
||||
Activity.query.filter(
|
||||
Activity.id != self.id,
|
||||
Activity.user_id == self.user_id,
|
||||
Activity.sport_id == sport_id if sport_id else True,
|
||||
Activity.activity_date <= self.activity_date,
|
||||
Activity.activity_date
|
||||
>= datetime.datetime.strptime(date_from, '%Y-%m-%d')
|
||||
if date_from
|
||||
else True,
|
||||
Activity.activity_date
|
||||
<= datetime.datetime.strptime(date_to, '%Y-%m-%d')
|
||||
if date_to
|
||||
else True,
|
||||
Activity.distance >= int(distance_from)
|
||||
if distance_from
|
||||
else True,
|
||||
Activity.distance <= int(distance_to) if distance_to else True,
|
||||
Activity.duration >= convert_in_duration(duration_from)
|
||||
if duration_from
|
||||
else True,
|
||||
Activity.duration <= convert_in_duration(duration_to)
|
||||
if duration_to
|
||||
else True,
|
||||
Activity.ave_speed >= float(ave_speed_from)
|
||||
if ave_speed_from
|
||||
else True,
|
||||
Activity.ave_speed <= float(ave_speed_to)
|
||||
if ave_speed_to
|
||||
else True,
|
||||
Activity.max_speed >= float(max_speed_from)
|
||||
if max_speed_from
|
||||
else True,
|
||||
Activity.max_speed <= float(max_speed_to)
|
||||
if max_speed_to
|
||||
else True,
|
||||
)
|
||||
.order_by(Activity.activity_date.desc())
|
||||
.first()
|
||||
)
|
||||
next_activity = (
|
||||
Activity.query.filter(
|
||||
Activity.id != self.id,
|
||||
Activity.user_id == self.user_id,
|
||||
Activity.sport_id == sport_id if sport_id else True,
|
||||
Activity.activity_date >= self.activity_date,
|
||||
Activity.activity_date
|
||||
>= datetime.datetime.strptime(date_from, '%Y-%m-%d')
|
||||
if date_from
|
||||
else True,
|
||||
Activity.activity_date
|
||||
<= datetime.datetime.strptime(date_to, '%Y-%m-%d')
|
||||
if date_to
|
||||
else True,
|
||||
Activity.distance >= int(distance_from)
|
||||
if distance_from
|
||||
else True,
|
||||
Activity.distance <= int(distance_to) if distance_to else True,
|
||||
Activity.duration >= convert_in_duration(duration_from)
|
||||
if duration_from
|
||||
else True,
|
||||
Activity.duration <= convert_in_duration(duration_to)
|
||||
if duration_to
|
||||
else True,
|
||||
Activity.ave_speed >= float(ave_speed_from)
|
||||
if ave_speed_from
|
||||
else True,
|
||||
Activity.ave_speed <= float(ave_speed_to)
|
||||
if ave_speed_to
|
||||
else True,
|
||||
)
|
||||
.order_by(Activity.activity_date.asc())
|
||||
.first()
|
||||
)
|
||||
return {
|
||||
"id": self.id,
|
||||
"user": self.user.username,
|
||||
"sport_id": self.sport_id,
|
||||
"title": self.title,
|
||||
"creation_date": self.creation_date,
|
||||
"modification_date": self.modification_date,
|
||||
"activity_date": self.activity_date,
|
||||
"duration": str(self.duration) if self.duration else None,
|
||||
"pauses": str(self.pauses) if self.pauses else None,
|
||||
"moving": str(self.moving) if self.moving else None,
|
||||
"distance": float(self.distance) if self.distance else None,
|
||||
"min_alt": float(self.min_alt) if self.min_alt else None,
|
||||
"max_alt": float(self.max_alt) if self.max_alt else None,
|
||||
"descent": float(self.descent) if self.descent else None,
|
||||
"ascent": float(self.ascent) if self.ascent else None,
|
||||
"max_speed": float(self.max_speed) if self.max_speed else None,
|
||||
"ave_speed": float(self.ave_speed) if self.ave_speed else None,
|
||||
"with_gpx": self.gpx is not None,
|
||||
"bounds": [float(bound) for bound in self.bounds]
|
||||
if self.bounds
|
||||
else [], # noqa
|
||||
"previous_activity": previous_activity.id
|
||||
if previous_activity
|
||||
else None, # noqa
|
||||
"next_activity": next_activity.id if next_activity else None,
|
||||
"segments": [segment.serialize() for segment in self.segments],
|
||||
"records": [record.serialize() for record in self.records],
|
||||
"map": self.map_id if self.map else None,
|
||||
"weather_start": self.weather_start,
|
||||
"weather_end": self.weather_end,
|
||||
"notes": self.notes,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_user_activity_records(cls, user_id, sport_id, as_integer=False):
|
||||
record_types_columns = {
|
||||
'AS': 'ave_speed', # 'Average speed'
|
||||
'FD': 'distance', # 'Farthest Distance'
|
||||
'LD': 'moving', # 'Longest Duration'
|
||||
'MS': 'max_speed', # 'Max speed'
|
||||
}
|
||||
records = {}
|
||||
for record_type, column in record_types_columns.items():
|
||||
column_sorted = getattr(getattr(Activity, column), 'desc')()
|
||||
record_activity = (
|
||||
Activity.query.filter_by(user_id=user_id, sport_id=sport_id)
|
||||
.order_by(column_sorted, Activity.activity_date)
|
||||
.first()
|
||||
)
|
||||
records[record_type] = dict(
|
||||
record_value=(
|
||||
getattr(record_activity, column)
|
||||
if record_activity
|
||||
else None
|
||||
),
|
||||
activity=record_activity,
|
||||
)
|
||||
return records
|
||||
|
||||
|
||||
@listens_for(Activity, 'after_insert')
|
||||
def on_activity_insert(mapper, connection, activity):
|
||||
@listens_for(db.Session, 'after_flush', once=True)
|
||||
def receive_after_flush(session, context):
|
||||
update_records(
|
||||
activity.user_id, activity.sport_id, connection, session
|
||||
) # noqa
|
||||
|
||||
|
||||
@listens_for(Activity, 'after_update')
|
||||
def on_activity_update(mapper, connection, activity):
|
||||
if object_session(activity).is_modified(
|
||||
activity, include_collections=True
|
||||
): # noqa
|
||||
|
||||
@listens_for(db.Session, 'after_flush', once=True)
|
||||
def receive_after_flush(session, context):
|
||||
sports_list = [activity.sport_id]
|
||||
records = Record.query.filter_by(activity_id=activity.id).all()
|
||||
for rec in records:
|
||||
if rec.sport_id not in sports_list:
|
||||
sports_list.append(rec.sport_id)
|
||||
for sport_id in sports_list:
|
||||
update_records(activity.user_id, sport_id, connection, session)
|
||||
|
||||
|
||||
@listens_for(Activity, 'after_delete')
|
||||
def on_activity_delete(mapper, connection, old_record):
|
||||
@listens_for(db.Session, 'after_flush', once=True)
|
||||
def receive_after_flush(session, context):
|
||||
if old_record.map:
|
||||
os.remove(get_absolute_file_path(old_record.map))
|
||||
if old_record.gpx:
|
||||
os.remove(get_absolute_file_path(old_record.gpx))
|
||||
|
||||
|
||||
class ActivitySegment(db.Model):
|
||||
__tablename__ = "activity_segments"
|
||||
activity_id = db.Column(
|
||||
db.Integer, db.ForeignKey('activities.id'), primary_key=True
|
||||
)
|
||||
segment_id = db.Column(db.Integer, primary_key=True)
|
||||
duration = db.Column(db.Interval, nullable=False)
|
||||
pauses = db.Column(db.Interval, nullable=True)
|
||||
moving = db.Column(db.Interval, nullable=True)
|
||||
distance = db.Column(db.Numeric(6, 3), nullable=True) # kilometers
|
||||
min_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters
|
||||
max_alt = db.Column(db.Numeric(6, 2), nullable=True) # meters
|
||||
descent = db.Column(db.Numeric(7, 2), nullable=True) # meters
|
||||
ascent = db.Column(db.Numeric(7, 2), nullable=True) # meters
|
||||
max_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
|
||||
ave_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f'<Segment \'{self.segment_id}\' '
|
||||
f'for activity \'{self.activity_id}\'>'
|
||||
)
|
||||
|
||||
def __init__(self, segment_id, activity_id):
|
||||
self.segment_id = segment_id
|
||||
self.activity_id = activity_id
|
||||
|
||||
def serialize(self):
|
||||
return {
|
||||
"activity_id": self.activity_id,
|
||||
"segment_id": self.segment_id,
|
||||
"duration": str(self.duration) if self.duration else None,
|
||||
"pauses": str(self.pauses) if self.pauses else None,
|
||||
"moving": str(self.moving) if self.moving else None,
|
||||
"distance": float(self.distance) if self.distance else None,
|
||||
"min_alt": float(self.min_alt) if self.min_alt else None,
|
||||
"max_alt": float(self.max_alt) if self.max_alt else None,
|
||||
"descent": float(self.descent) if self.descent else None,
|
||||
"ascent": float(self.ascent) if self.ascent else None,
|
||||
"max_speed": float(self.max_speed) if self.max_speed else None,
|
||||
"ave_speed": float(self.ave_speed) if self.ave_speed else None,
|
||||
}
|
||||
|
||||
|
||||
class Record(db.Model):
|
||||
__tablename__ = "records"
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint(
|
||||
'user_id', 'sport_id', 'record_type', name='user_sports_records'
|
||||
),
|
||||
)
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
sport_id = db.Column(
|
||||
db.Integer, db.ForeignKey('sports.id'), nullable=False
|
||||
)
|
||||
activity_id = db.Column(
|
||||
db.Integer, db.ForeignKey('activities.id'), nullable=False
|
||||
)
|
||||
record_type = db.Column(Enum(*record_types, name="record_types"))
|
||||
activity_date = db.Column(db.DateTime, nullable=False)
|
||||
_value = db.Column("value", db.Integer, nullable=True)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f'<Record {self.sports.label} - '
|
||||
f'{self.record_type} - '
|
||||
f"{self.activity_date.strftime('%Y-%m-%d')}>"
|
||||
)
|
||||
|
||||
def __init__(self, activity, record_type):
|
||||
self.user_id = activity.user_id
|
||||
self.sport_id = activity.sport_id
|
||||
self.activity_id = activity.id
|
||||
self.record_type = record_type
|
||||
self.activity_date = activity.activity_date
|
||||
|
||||
@hybrid_property
|
||||
def value(self):
|
||||
if self._value is None:
|
||||
return None
|
||||
if self.record_type == 'LD':
|
||||
return datetime.timedelta(seconds=self._value)
|
||||
elif self.record_type in ['AS', 'MS']:
|
||||
return float(self._value / 100)
|
||||
else: # 'FD'
|
||||
return float(self._value / 1000)
|
||||
|
||||
@value.setter
|
||||
def value(self, val):
|
||||
self._value = convert_value_to_integer(self.record_type, val)
|
||||
|
||||
def serialize(self):
|
||||
if self.value is None:
|
||||
value = None
|
||||
elif self.record_type in ['AS', 'FD', 'MS']:
|
||||
value = float(self.value)
|
||||
else: # 'LD'
|
||||
value = str(self.value)
|
||||
|
||||
return {
|
||||
"id": self.id,
|
||||
"user": self.user.username,
|
||||
"sport_id": self.sport_id,
|
||||
"activity_id": self.activity_id,
|
||||
"record_type": self.record_type,
|
||||
"activity_date": self.activity_date,
|
||||
"value": value,
|
||||
}
|
||||
|
||||
|
||||
@listens_for(Record, 'after_delete')
|
||||
def on_record_delete(mapper, connection, old_record):
|
||||
@listens_for(db.Session, 'after_flush', once=True)
|
||||
def receive_after_flush(session, context):
|
||||
activity = old_record.activities
|
||||
new_records = Activity.get_user_activity_records(
|
||||
activity.user_id, activity.sport_id
|
||||
)
|
||||
for record_type, record_data in new_records.items():
|
||||
if (
|
||||
record_data['record_value']
|
||||
and record_type == old_record.record_type
|
||||
):
|
||||
new_record = Record(
|
||||
activity=record_data['activity'], record_type=record_type
|
||||
)
|
||||
new_record.value = record_data['record_value']
|
||||
session.add(new_record)
|
116
fittrackee/activities/records.py
Normal file
@ -0,0 +1,116 @@
|
||||
from flask import Blueprint, jsonify
|
||||
|
||||
from ..users.utils import authenticate
|
||||
from .models import Record
|
||||
|
||||
records_blueprint = Blueprint('records', __name__)
|
||||
|
||||
|
||||
@records_blueprint.route('/records', methods=['GET'])
|
||||
@authenticate
|
||||
def get_records(auth_user_id):
|
||||
"""
|
||||
Get all records for authenticated user.
|
||||
|
||||
Following types of records are available:
|
||||
- average speed (record_type: 'AS')
|
||||
- farest distance (record_type: 'FD')
|
||||
- longest duration (record_type: 'LD')
|
||||
- maximum speed (record_type: 'MS')
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/records HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example responses**:
|
||||
|
||||
- returning records
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"activity_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
||||
"activity_id": 4,
|
||||
"id": 9,
|
||||
"record_type": "AS",
|
||||
"sport_id": 1,
|
||||
"user": "admin",
|
||||
"value": 18
|
||||
},
|
||||
{
|
||||
"activity_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
||||
"activity_id": 4,
|
||||
"id": 10,
|
||||
"record_type": "FD",
|
||||
"sport_id": 1,
|
||||
"user": "admin",
|
||||
"value": 18
|
||||
},
|
||||
{
|
||||
"activity_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
||||
"activity_id": 7,
|
||||
"id": 11,
|
||||
"record_type": "LD",
|
||||
"sport_id": 1,
|
||||
"user": "admin",
|
||||
"value": "1:01:00"
|
||||
},
|
||||
{
|
||||
"activity_date": "Sun, 07 Jul 2019 08:00:00 GMT",
|
||||
"activity_id": 4,
|
||||
"id": 12,
|
||||
"record_type": "MS",
|
||||
"sport_id": 1,
|
||||
"user": "admin",
|
||||
"value": 18
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- no records
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"records": []
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 401:
|
||||
- Provide a valid auth token.
|
||||
- Signature expired. Please log in again.
|
||||
- Invalid token. Please log in again.
|
||||
|
||||
"""
|
||||
|
||||
records = (
|
||||
Record.query.filter_by(user_id=auth_user_id)
|
||||
.order_by(Record.sport_id.asc(), Record.record_type.asc())
|
||||
.all()
|
||||
)
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'data': {'records': [record.serialize() for record in records]},
|
||||
}
|
||||
return jsonify(response_object), 200
|
352
fittrackee/activities/sports.py
Normal file
@ -0,0 +1,352 @@
|
||||
from fittrackee import appLog, db
|
||||
from flask import Blueprint, jsonify, request
|
||||
from sqlalchemy import exc
|
||||
|
||||
from ..users.models import User
|
||||
from ..users.utils import authenticate, authenticate_as_admin
|
||||
from .models import Sport
|
||||
|
||||
sports_blueprint = Blueprint('sports', __name__)
|
||||
|
||||
|
||||
@sports_blueprint.route('/sports', methods=['GET'])
|
||||
@authenticate
|
||||
def get_sports(auth_user_id):
|
||||
"""
|
||||
Get all sports
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/sports HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
- for non admin user :
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Sport)"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"img": "/img/sports/cycling-transport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Transport)"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"img": "/img/sports/hiking.png",
|
||||
"is_active": true,
|
||||
"label": "Hiking"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"img": "/img/sports/mountain-biking.png",
|
||||
"is_active": true,
|
||||
"label": "Mountain Biking"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"img": "/img/sports/running.png",
|
||||
"is_active": true,
|
||||
"label": "Running"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"img": "/img/sports/walking.png",
|
||||
"is_active": true,
|
||||
"label": "Walking"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- for admin user :
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"has_activities": true,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Sport)"
|
||||
},
|
||||
{
|
||||
"has_activities": false,
|
||||
"id": 2,
|
||||
"img": "/img/sports/cycling-transport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Transport)"
|
||||
},
|
||||
{
|
||||
"has_activities": false,
|
||||
"id": 3,
|
||||
"img": "/img/sports/hiking.png",
|
||||
"is_active": true,
|
||||
"label": "Hiking"
|
||||
},
|
||||
{
|
||||
"has_activities": false,
|
||||
"id": 4,
|
||||
"img": "/img/sports/mountain-biking.png",
|
||||
"is_active": true,
|
||||
"label": "Mountain Biking"
|
||||
},
|
||||
{
|
||||
"has_activities": false,
|
||||
"id": 5,
|
||||
"img": "/img/sports/running.png",
|
||||
"is_active": true,
|
||||
"label": "Running"
|
||||
},
|
||||
{
|
||||
"has_activities": false,
|
||||
"id": 6,
|
||||
"img": "/img/sports/walking.png",
|
||||
"is_active": true,
|
||||
"label": "Walking"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 401:
|
||||
- Provide a valid auth token.
|
||||
- Signature expired. Please log in again.
|
||||
- Invalid token. Please log in again.
|
||||
|
||||
"""
|
||||
|
||||
user = User.query.filter_by(id=int(auth_user_id)).first()
|
||||
sports = Sport.query.order_by(Sport.id).all()
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'data': {'sports': [sport.serialize(user.admin) for sport in sports]},
|
||||
}
|
||||
return jsonify(response_object), 200
|
||||
|
||||
|
||||
@sports_blueprint.route('/sports/<int:sport_id>', methods=['GET'])
|
||||
@authenticate
|
||||
def get_sport(auth_user_id, sport_id):
|
||||
"""
|
||||
Get a sport
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/sports/1 HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
- success for non admin user :
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Sport)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- success for admin user :
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"has_activities": false,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Sport)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- sport not found
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 404 NOT FOUND
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"sports": []
|
||||
},
|
||||
"status": "not found"
|
||||
}
|
||||
|
||||
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
||||
:param integer sport_id: sport id
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 401:
|
||||
- Provide a valid auth token.
|
||||
- Signature expired. Please log in again.
|
||||
- Invalid token. Please log in again.
|
||||
:statuscode 404: sport not found
|
||||
|
||||
"""
|
||||
|
||||
user = User.query.filter_by(id=int(auth_user_id)).first()
|
||||
sport = Sport.query.filter_by(id=sport_id).first()
|
||||
if sport:
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'data': {'sports': [sport.serialize(user.admin)]},
|
||||
}
|
||||
code = 200
|
||||
else:
|
||||
response_object = {'status': 'not found', 'data': {'sports': []}}
|
||||
code = 404
|
||||
return jsonify(response_object), code
|
||||
|
||||
|
||||
@sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH'])
|
||||
@authenticate_as_admin
|
||||
def update_sport(auth_user_id, sport_id):
|
||||
"""
|
||||
Update a sport
|
||||
Authenticated user must be an admin
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/sports/1 HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
- success
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"has_activities": false,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": false,
|
||||
"label": "Cycling (Sport)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- sport not found
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 404 NOT FOUND
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"sports": []
|
||||
},
|
||||
"status": "not found"
|
||||
}
|
||||
|
||||
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
||||
:param integer sport_id: sport id
|
||||
|
||||
:<json string is_active: sport active status
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: sport updated
|
||||
:statuscode 400: invalid payload
|
||||
:statuscode 401:
|
||||
- Provide a valid auth token.
|
||||
- Signature expired. Please log in again.
|
||||
- Invalid token. Please log in again.
|
||||
:statuscode 403: You do not have permissions.
|
||||
:statuscode 404: sport not found
|
||||
:statuscode 500:
|
||||
|
||||
"""
|
||||
sport_data = request.get_json()
|
||||
if not sport_data or sport_data.get('is_active') is None:
|
||||
response_object = {'status': 'error', 'message': 'Invalid payload.'}
|
||||
return jsonify(response_object), 400
|
||||
|
||||
try:
|
||||
sport = Sport.query.filter_by(id=sport_id).first()
|
||||
if sport:
|
||||
sport.is_active = sport_data.get('is_active')
|
||||
db.session.commit()
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'data': {'sports': [sport.serialize(True)]},
|
||||
}
|
||||
code = 200
|
||||
else:
|
||||
response_object = {'status': 'not found', 'data': {'sports': []}}
|
||||
code = 404
|
||||
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||
db.session.rollback()
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error. Please try again or contact the administrator.',
|
||||
}
|
||||
code = 500
|
||||
return jsonify(response_object), code
|
383
fittrackee/activities/stats.py
Normal file
@ -0,0 +1,383 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fittrackee import appLog, db
|
||||
from flask import Blueprint, jsonify, request
|
||||
from sqlalchemy import func
|
||||
|
||||
from ..users.models import User
|
||||
from ..users.utils import authenticate, authenticate_as_admin
|
||||
from .models import Activity, Sport
|
||||
from .utils import get_datetime_with_tz, get_upload_dir_size
|
||||
from .utils_format import convert_timedelta_to_integer
|
||||
|
||||
stats_blueprint = Blueprint('stats', __name__)
|
||||
|
||||
|
||||
def get_activities(user_name, filter_type):
|
||||
try:
|
||||
user = User.query.filter_by(username=user_name).first()
|
||||
if not user:
|
||||
response_object = {
|
||||
'status': 'not found',
|
||||
'message': 'User does not exist.',
|
||||
}
|
||||
return jsonify(response_object), 404
|
||||
|
||||
params = request.args.copy()
|
||||
date_from = params.get('from')
|
||||
if date_from:
|
||||
date_from = datetime.strptime(date_from, '%Y-%m-%d')
|
||||
_, date_from = get_datetime_with_tz(user.timezone, date_from)
|
||||
date_to = params.get('to')
|
||||
if date_to:
|
||||
date_to = datetime.strptime(
|
||||
f'{date_to} 23:59:59', '%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
_, date_to = get_datetime_with_tz(user.timezone, date_to)
|
||||
sport_id = params.get('sport_id')
|
||||
time = params.get('time')
|
||||
|
||||
if filter_type == 'by_sport':
|
||||
sport_id = params.get('sport_id')
|
||||
if sport_id:
|
||||
sport = Sport.query.filter_by(id=sport_id).first()
|
||||
if not sport:
|
||||
response_object = {
|
||||
'status': 'not found',
|
||||
'message': 'Sport does not exist.',
|
||||
}
|
||||
return jsonify(response_object), 404
|
||||
|
||||
activities = (
|
||||
Activity.query.filter(
|
||||
Activity.user_id == user.id,
|
||||
Activity.activity_date >= date_from if date_from else True,
|
||||
Activity.activity_date < date_to + timedelta(seconds=1)
|
||||
if date_to
|
||||
else True,
|
||||
Activity.sport_id == sport_id if sport_id else True,
|
||||
)
|
||||
.order_by(Activity.activity_date.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
activities_list = {}
|
||||
for activity in activities:
|
||||
if filter_type == 'by_sport':
|
||||
sport_id = activity.sport_id
|
||||
if sport_id not in activities_list:
|
||||
activities_list[sport_id] = {
|
||||
'nb_activities': 0,
|
||||
'total_distance': 0.0,
|
||||
'total_duration': 0,
|
||||
}
|
||||
activities_list[sport_id]['nb_activities'] += 1
|
||||
activities_list[sport_id]['total_distance'] += float(
|
||||
activity.distance
|
||||
)
|
||||
activities_list[sport_id][
|
||||
'total_duration'
|
||||
] += convert_timedelta_to_integer(activity.moving)
|
||||
|
||||
else:
|
||||
if time == 'week':
|
||||
activity_date = activity.activity_date - timedelta(
|
||||
days=(
|
||||
activity.activity_date.isoweekday()
|
||||
if activity.activity_date.isoweekday() < 7
|
||||
else 0
|
||||
)
|
||||
)
|
||||
time_period = datetime.strftime(activity_date, "%Y-%m-%d")
|
||||
elif time == 'weekm': # week start Monday
|
||||
activity_date = activity.activity_date - timedelta(
|
||||
days=activity.activity_date.weekday()
|
||||
)
|
||||
time_period = datetime.strftime(activity_date, "%Y-%m-%d")
|
||||
elif time == 'month':
|
||||
time_period = datetime.strftime(
|
||||
activity.activity_date, "%Y-%m"
|
||||
)
|
||||
elif time == 'year' or not time:
|
||||
time_period = datetime.strftime(
|
||||
activity.activity_date, "%Y"
|
||||
)
|
||||
else:
|
||||
response_object = {
|
||||
'status': 'fail',
|
||||
'message': 'Invalid time period.',
|
||||
}
|
||||
return jsonify(response_object), 400
|
||||
sport_id = activity.sport_id
|
||||
if time_period not in activities_list:
|
||||
activities_list[time_period] = {}
|
||||
if sport_id not in activities_list[time_period]:
|
||||
activities_list[time_period][sport_id] = {
|
||||
'nb_activities': 0,
|
||||
'total_distance': 0.0,
|
||||
'total_duration': 0,
|
||||
}
|
||||
activities_list[time_period][sport_id]['nb_activities'] += 1
|
||||
activities_list[time_period][sport_id][
|
||||
'total_distance'
|
||||
] += float(activity.distance)
|
||||
activities_list[time_period][sport_id][
|
||||
'total_duration'
|
||||
] += convert_timedelta_to_integer(activity.moving)
|
||||
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'data': {'statistics': activities_list},
|
||||
}
|
||||
code = 200
|
||||
except Exception as e:
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error. Please try again or contact the administrator.',
|
||||
}
|
||||
code = 500
|
||||
return jsonify(response_object), code
|
||||
|
||||
|
||||
@stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET'])
|
||||
@authenticate
|
||||
def get_activities_by_time(auth_user_id, user_name):
|
||||
"""
|
||||
Get activities statistics for a user by time
|
||||
|
||||
**Example requests**:
|
||||
|
||||
- without parameters
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/stats/admin/by_time HTTP/1.1
|
||||
|
||||
- with parameters
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/stats/admin/by_time?from=2018-01-01&to=2018-06-30&time=week HTTP/1.1
|
||||
|
||||
**Example responses**:
|
||||
|
||||
- success
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"statistics": {
|
||||
"2017": {
|
||||
"3": {
|
||||
"nb_activities": 2,
|
||||
"total_distance": 15.282,
|
||||
"total_duration": 12341
|
||||
}
|
||||
},
|
||||
"2019": {
|
||||
"1": {
|
||||
"nb_activities": 3,
|
||||
"total_distance": 47,
|
||||
"total_duration": 9960
|
||||
},
|
||||
"2": {
|
||||
"nb_activities": 1,
|
||||
"total_distance": 5.613,
|
||||
"total_duration": 1267
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- no activities
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"statistics": {}
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
||||
:param integer user_name: user name
|
||||
|
||||
:query string from: start date (format: ``%Y-%m-%d``)
|
||||
:query string to: end date (format: ``%Y-%m-%d``)
|
||||
:query string time: time frame:
|
||||
|
||||
- ``week``: week starting Sunday
|
||||
- ``weekm``: week starting Monday
|
||||
- ``month``: month
|
||||
- ``year``: year (default)
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 401:
|
||||
- Provide a valid auth token.
|
||||
- Signature expired. Please log in again.
|
||||
- Invalid token. Please log in again.
|
||||
:statuscode 404:
|
||||
- User does not exist.
|
||||
|
||||
"""
|
||||
return get_activities(user_name, 'by_time')
|
||||
|
||||
|
||||
@stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET'])
|
||||
@authenticate
|
||||
def get_activities_by_sport(auth_user_id, user_name):
|
||||
"""
|
||||
Get activities statistics for a user by sport
|
||||
|
||||
**Example requests**:
|
||||
|
||||
- without parameters (get stats for all sports with activities)
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/stats/admin/by_sport HTTP/1.1
|
||||
|
||||
- with sport id
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/stats/admin/by_sport?sport_id=1 HTTP/1.1
|
||||
|
||||
**Example responses**:
|
||||
|
||||
- success
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"statistics": {
|
||||
"1": {
|
||||
"nb_activities": 3,
|
||||
"total_distance": 47,
|
||||
"total_duration": 9960
|
||||
},
|
||||
"2": {
|
||||
"nb_activities": 1,
|
||||
"total_distance": 5.613,
|
||||
"total_duration": 1267
|
||||
},
|
||||
"3": {
|
||||
"nb_activities": 2,
|
||||
"total_distance": 15.282,
|
||||
"total_duration": 12341
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- no activities
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"statistics": {}
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
||||
:param integer user_name: user name
|
||||
|
||||
:query integer sport_id: sport id
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 401:
|
||||
- Provide a valid auth token.
|
||||
- Signature expired. Please log in again.
|
||||
- Invalid token. Please log in again.
|
||||
:statuscode 404:
|
||||
- User does not exist.
|
||||
- Sport does not exist.
|
||||
|
||||
"""
|
||||
return get_activities(user_name, 'by_sport')
|
||||
|
||||
|
||||
@stats_blueprint.route('/stats/all', methods=['GET'])
|
||||
@authenticate_as_admin
|
||||
def get_application_stats(auth_user_id):
|
||||
"""
|
||||
Get all application statistics
|
||||
|
||||
**Example requests**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/stats/all HTTP/1.1
|
||||
|
||||
|
||||
**Example responses**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"activities": 3,
|
||||
"sports": 3,
|
||||
"users": 2,
|
||||
"uploads_dir_size": 1000
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 401:
|
||||
- Provide a valid auth token.
|
||||
- Signature expired. Please log in again.
|
||||
- Invalid token. Please log in again.
|
||||
:statuscode 403: You do not have permissions.
|
||||
"""
|
||||
|
||||
nb_activities = Activity.query.filter().count()
|
||||
nb_users = User.query.filter().count()
|
||||
nb_sports = (
|
||||
db.session.query(func.count(Activity.sport_id))
|
||||
.group_by(Activity.sport_id)
|
||||
.count()
|
||||
)
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'activities': nb_activities,
|
||||
'sports': nb_sports,
|
||||
'users': nb_users,
|
||||
'uploads_dir_size': get_upload_dir_size(),
|
||||
},
|
||||
}
|
||||
return jsonify(response_object), 200
|
350
fittrackee/activities/utils.py
Normal file
@ -0,0 +1,350 @@
|
||||
import hashlib
|
||||
import os
|
||||
import tempfile
|
||||
import zipfile
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import gpxpy.gpx
|
||||
import pytz
|
||||
from fittrackee import appLog, db
|
||||
from flask import current_app
|
||||
from sqlalchemy import exc
|
||||
from staticmap import Line, StaticMap
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from ..users.models import User
|
||||
from .models import Activity, ActivitySegment, Sport
|
||||
from .utils_files import get_absolute_file_path
|
||||
from .utils_gpx import get_gpx_info
|
||||
|
||||
|
||||
class ActivityException(Exception):
|
||||
def __init__(self, status, message, e):
|
||||
self.status = status
|
||||
self.message = message
|
||||
self.e = e
|
||||
|
||||
|
||||
def get_datetime_with_tz(timezone, activity_date, gpx_data=None):
|
||||
activity_date_tz = None
|
||||
if timezone:
|
||||
user_tz = pytz.timezone(timezone)
|
||||
utc_tz = pytz.utc
|
||||
if gpx_data:
|
||||
# activity date in gpx is in UTC, but in naive datetime
|
||||
fmt = '%Y-%m-%d %H:%M:%S'
|
||||
activity_date_string = activity_date.strftime(fmt)
|
||||
activity_date_tmp = utc_tz.localize(
|
||||
datetime.strptime(activity_date_string, fmt)
|
||||
)
|
||||
activity_date_tz = activity_date_tmp.astimezone(user_tz)
|
||||
else:
|
||||
activity_date_tz = user_tz.localize(activity_date)
|
||||
activity_date = activity_date_tz.astimezone(utc_tz)
|
||||
# make datetime 'naive' like in gpx file
|
||||
activity_date = activity_date.replace(tzinfo=None)
|
||||
|
||||
return activity_date_tz, activity_date
|
||||
|
||||
|
||||
def update_activity_data(activity, gpx_data):
|
||||
"""activity could be a complete activity or an activity segment"""
|
||||
activity.pauses = gpx_data['stop_time']
|
||||
activity.moving = gpx_data['moving_time']
|
||||
activity.min_alt = gpx_data['elevation_min']
|
||||
activity.max_alt = gpx_data['elevation_max']
|
||||
activity.descent = gpx_data['downhill']
|
||||
activity.ascent = gpx_data['uphill']
|
||||
activity.max_speed = gpx_data['max_speed']
|
||||
activity.ave_speed = gpx_data['average_speed']
|
||||
return activity
|
||||
|
||||
|
||||
def create_activity(user, activity_data, gpx_data=None):
|
||||
activity_date = (
|
||||
gpx_data['start']
|
||||
if gpx_data
|
||||
else datetime.strptime(
|
||||
activity_data.get('activity_date'), '%Y-%m-%d %H:%M'
|
||||
)
|
||||
)
|
||||
activity_date_tz, activity_date = get_datetime_with_tz(
|
||||
user.timezone, activity_date, gpx_data
|
||||
)
|
||||
|
||||
duration = (
|
||||
gpx_data['duration']
|
||||
if gpx_data
|
||||
else timedelta(seconds=activity_data.get('duration'))
|
||||
)
|
||||
distance = (
|
||||
gpx_data['distance'] if gpx_data else activity_data.get('distance')
|
||||
)
|
||||
title = gpx_data['name'] if gpx_data else activity_data.get('title')
|
||||
|
||||
new_activity = Activity(
|
||||
user_id=user.id,
|
||||
sport_id=activity_data.get('sport_id'),
|
||||
activity_date=activity_date,
|
||||
distance=distance,
|
||||
duration=duration,
|
||||
)
|
||||
new_activity.notes = activity_data.get('notes')
|
||||
|
||||
if title is not None and title != '':
|
||||
new_activity.title = title
|
||||
else:
|
||||
sport = Sport.query.filter_by(id=new_activity.sport_id).first()
|
||||
fmt = "%Y-%m-%d %H:%M:%S"
|
||||
activity_datetime = (
|
||||
activity_date_tz.strftime(fmt)
|
||||
if activity_date_tz
|
||||
else new_activity.activity_date.strftime(fmt)
|
||||
)
|
||||
new_activity.title = f'{sport.label} - {activity_datetime}'
|
||||
|
||||
if gpx_data:
|
||||
new_activity.gpx = gpx_data['filename']
|
||||
new_activity.bounds = gpx_data['bounds']
|
||||
update_activity_data(new_activity, gpx_data)
|
||||
else:
|
||||
new_activity.moving = duration
|
||||
new_activity.ave_speed = (
|
||||
None
|
||||
if duration.seconds == 0
|
||||
else float(new_activity.distance) / (duration.seconds / 3600)
|
||||
)
|
||||
new_activity.max_speed = new_activity.ave_speed
|
||||
return new_activity
|
||||
|
||||
|
||||
def create_segment(activity_id, segment_data):
|
||||
new_segment = ActivitySegment(
|
||||
activity_id=activity_id, segment_id=segment_data['idx']
|
||||
)
|
||||
new_segment.duration = segment_data['duration']
|
||||
new_segment.distance = segment_data['distance']
|
||||
update_activity_data(new_segment, segment_data)
|
||||
return new_segment
|
||||
|
||||
|
||||
def update_activity(activity):
|
||||
"""
|
||||
Note: only gpx_data is be updated for now (the gpx file is NOT modified)
|
||||
|
||||
In a next version, map_data and weather_data will be updated
|
||||
(case of a modified gpx file, see issue #7)
|
||||
"""
|
||||
gpx_data, _, _ = get_gpx_info(
|
||||
get_absolute_file_path(activity.gpx), False, False
|
||||
)
|
||||
updated_activity = update_activity_data(activity, gpx_data)
|
||||
updated_activity.duration = gpx_data['duration']
|
||||
updated_activity.distance = gpx_data['distance']
|
||||
db.session.flush()
|
||||
|
||||
for segment_idx, segment in enumerate(updated_activity.segments):
|
||||
segment_data = gpx_data['segments'][segment_idx]
|
||||
updated_segment = update_activity_data(segment, segment_data)
|
||||
updated_segment.duration = segment_data['duration']
|
||||
updated_segment.distance = segment_data['distance']
|
||||
db.session.flush()
|
||||
|
||||
return updated_activity
|
||||
|
||||
|
||||
def edit_activity(activity, activity_data, auth_user_id):
|
||||
user = User.query.filter_by(id=auth_user_id).first()
|
||||
if activity_data.get('refresh'):
|
||||
activity = update_activity(activity)
|
||||
if activity_data.get('sport_id'):
|
||||
activity.sport_id = activity_data.get('sport_id')
|
||||
if activity_data.get('title'):
|
||||
activity.title = activity_data.get('title')
|
||||
if activity_data.get('notes'):
|
||||
activity.notes = activity_data.get('notes')
|
||||
if not activity.gpx:
|
||||
if activity_data.get('activity_date'):
|
||||
activity_date = datetime.strptime(
|
||||
activity_data.get('activity_date'), '%Y-%m-%d %H:%M'
|
||||
)
|
||||
_, activity.activity_date = get_datetime_with_tz(
|
||||
user.timezone, activity_date
|
||||
)
|
||||
|
||||
if activity_data.get('duration'):
|
||||
activity.duration = timedelta(
|
||||
seconds=activity_data.get('duration')
|
||||
)
|
||||
activity.moving = activity.duration
|
||||
|
||||
if activity_data.get('distance'):
|
||||
activity.distance = activity_data.get('distance')
|
||||
|
||||
activity.ave_speed = (
|
||||
None
|
||||
if activity.duration.seconds == 0
|
||||
else float(activity.distance) / (activity.duration.seconds / 3600)
|
||||
)
|
||||
activity.max_speed = activity.ave_speed
|
||||
return activity
|
||||
|
||||
|
||||
def get_file_path(dir_path, filename):
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
file_path = os.path.join(dir_path, filename)
|
||||
return file_path
|
||||
|
||||
|
||||
def get_new_file_path(
|
||||
auth_user_id, activity_date, sport, old_filename=None, extension=None
|
||||
):
|
||||
if not extension:
|
||||
extension = f".{old_filename.rsplit('.', 1)[1].lower()}"
|
||||
_, new_filename = tempfile.mkstemp(
|
||||
prefix=f'{activity_date}_{sport}_', suffix=extension
|
||||
)
|
||||
dir_path = os.path.join('activities', str(auth_user_id))
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
file_path = os.path.join(dir_path, new_filename.split('/')[-1])
|
||||
return file_path
|
||||
|
||||
|
||||
def generate_map(map_filepath, map_data):
|
||||
m = StaticMap(400, 225, 10)
|
||||
line = Line(map_data, '#3388FF', 4)
|
||||
m.add_line(line)
|
||||
image = m.render()
|
||||
image.save(map_filepath)
|
||||
|
||||
|
||||
def get_map_hash(map_filepath):
|
||||
"""
|
||||
md5 hash is used as id instead of activity id, to retrieve map image
|
||||
(maps are sensitive data)
|
||||
"""
|
||||
md5 = hashlib.md5()
|
||||
absolute_map_filepath = get_absolute_file_path(map_filepath)
|
||||
with open(absolute_map_filepath, 'rb') as f:
|
||||
for chunk in iter(lambda: f.read(128 * md5.block_size), b''):
|
||||
md5.update(chunk)
|
||||
return md5.hexdigest()
|
||||
|
||||
|
||||
def process_one_gpx_file(params, filename):
|
||||
try:
|
||||
gpx_data, map_data, weather_data = get_gpx_info(params['file_path'])
|
||||
auth_user_id = params['user'].id
|
||||
new_filepath = get_new_file_path(
|
||||
auth_user_id=auth_user_id,
|
||||
activity_date=gpx_data['start'],
|
||||
old_filename=filename,
|
||||
sport=params['sport_label'],
|
||||
)
|
||||
absolute_gpx_filepath = get_absolute_file_path(new_filepath)
|
||||
os.rename(params['file_path'], absolute_gpx_filepath)
|
||||
gpx_data['filename'] = new_filepath
|
||||
|
||||
map_filepath = get_new_file_path(
|
||||
auth_user_id=auth_user_id,
|
||||
activity_date=gpx_data['start'],
|
||||
extension='.png',
|
||||
sport=params['sport_label'],
|
||||
)
|
||||
absolute_map_filepath = get_absolute_file_path(map_filepath)
|
||||
generate_map(absolute_map_filepath, map_data)
|
||||
except (gpxpy.gpx.GPXXMLSyntaxException, TypeError) as e:
|
||||
raise ActivityException('error', 'Error during gpx file parsing.', e)
|
||||
except Exception as e:
|
||||
raise ActivityException('error', 'Error during gpx processing.', e)
|
||||
|
||||
try:
|
||||
new_activity = create_activity(
|
||||
params['user'], params['activity_data'], gpx_data
|
||||
)
|
||||
new_activity.map = map_filepath
|
||||
new_activity.map_id = get_map_hash(map_filepath)
|
||||
new_activity.weather_start = weather_data[0]
|
||||
new_activity.weather_end = weather_data[1]
|
||||
db.session.add(new_activity)
|
||||
db.session.flush()
|
||||
|
||||
for segment_data in gpx_data['segments']:
|
||||
new_segment = create_segment(new_activity.id, segment_data)
|
||||
db.session.add(new_segment)
|
||||
db.session.commit()
|
||||
return new_activity
|
||||
except (exc.IntegrityError, ValueError) as e:
|
||||
raise ActivityException('fail', 'Error during activity save.', e)
|
||||
|
||||
|
||||
def process_zip_archive(common_params, extract_dir):
|
||||
with zipfile.ZipFile(common_params['file_path'], "r") as zip_ref:
|
||||
zip_ref.extractall(extract_dir)
|
||||
|
||||
new_activities = []
|
||||
gpx_files_limit = os.getenv('REACT_APP_GPX_LIMIT_IMPORT', '10')
|
||||
if gpx_files_limit and gpx_files_limit.isdigit():
|
||||
gpx_files_limit = int(gpx_files_limit)
|
||||
else:
|
||||
gpx_files_limit = 10
|
||||
appLog.error('GPX limit not configured, set to 10.')
|
||||
gpx_files_ok = 0
|
||||
|
||||
for gpx_file in os.listdir(extract_dir):
|
||||
if '.' in gpx_file and gpx_file.rsplit('.', 1)[
|
||||
1
|
||||
].lower() in current_app.config.get('ACTIVITY_ALLOWED_EXTENSIONS'):
|
||||
gpx_files_ok += 1
|
||||
if gpx_files_ok > gpx_files_limit:
|
||||
break
|
||||
file_path = os.path.join(extract_dir, gpx_file)
|
||||
params = common_params
|
||||
params['file_path'] = file_path
|
||||
new_activity = process_one_gpx_file(params, gpx_file)
|
||||
new_activities.append(new_activity)
|
||||
|
||||
return new_activities
|
||||
|
||||
|
||||
def process_files(auth_user_id, activity_data, activity_file, folders):
|
||||
filename = secure_filename(activity_file.filename)
|
||||
extension = f".{filename.rsplit('.', 1)[1].lower()}"
|
||||
file_path = get_file_path(folders['tmp_dir'], filename)
|
||||
sport = Sport.query.filter_by(id=activity_data.get('sport_id')).first()
|
||||
if not sport:
|
||||
raise ActivityException(
|
||||
'error',
|
||||
f"Sport id: {activity_data.get('sport_id')} does not exist",
|
||||
None,
|
||||
)
|
||||
user = User.query.filter_by(id=auth_user_id).first()
|
||||
|
||||
common_params = {
|
||||
'user': user,
|
||||
'activity_data': activity_data,
|
||||
'file_path': file_path,
|
||||
'sport_label': sport.label,
|
||||
}
|
||||
|
||||
try:
|
||||
activity_file.save(file_path)
|
||||
except Exception as e:
|
||||
raise ActivityException('error', 'Error during activity file save.', e)
|
||||
|
||||
if extension == ".gpx":
|
||||
return [process_one_gpx_file(common_params, filename)]
|
||||
else:
|
||||
return process_zip_archive(common_params, folders['extract_dir'])
|
||||
|
||||
|
||||
def get_upload_dir_size():
|
||||
upload_path = get_absolute_file_path('')
|
||||
total_size = 0
|
||||
for dir_path, _, filenames in os.walk(upload_path):
|
||||
for f in filenames:
|
||||
fp = os.path.join(dir_path, f)
|
||||
total_size += os.path.getsize(fp)
|
||||
return total_size
|
7
fittrackee/activities/utils_files.py
Normal file
@ -0,0 +1,7 @@
|
||||
import os
|
||||
|
||||
from flask import current_app
|
||||
|
||||
|
||||
def get_absolute_file_path(relative_path):
|
||||
return os.path.join(current_app.config['UPLOAD_FOLDER'], relative_path)
|
24
fittrackee/activities/utils_format.py
Normal file
@ -0,0 +1,24 @@
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
def convert_in_duration(value):
|
||||
hours = int(value.split(':')[0])
|
||||
minutes = int(value.split(':')[1])
|
||||
return timedelta(seconds=(hours * 3600 + minutes * 60))
|
||||
|
||||
|
||||
def convert_timedelta_to_integer(value):
|
||||
hours, minutes, seconds = str(value).split(':')
|
||||
return int(hours) * 3600 + int(minutes) * 60 + int(seconds)
|
||||
|
||||
|
||||
def convert_value_to_integer(record_type, val):
|
||||
if val is None:
|
||||
return None
|
||||
|
||||
if record_type == 'LD':
|
||||
return convert_timedelta_to_integer(val)
|
||||
elif record_type in ['AS', 'MS']:
|
||||
return int(val * 100)
|
||||
else: # 'FD'
|
||||
return int(val * 1000)
|
218
fittrackee/activities/utils_gpx.py
Normal file
@ -0,0 +1,218 @@
|
||||
from datetime import timedelta
|
||||
|
||||
import gpxpy.gpx
|
||||
|
||||
from .utils_weather import get_weather
|
||||
|
||||
|
||||
class ActivityGPXException(Exception):
|
||||
def __init__(self, status, message, e):
|
||||
self.status = status
|
||||
self.message = message
|
||||
self.e = e
|
||||
|
||||
|
||||
def open_gpx_file(gpx_file):
|
||||
gpx_file = open(gpx_file, 'r')
|
||||
gpx = gpxpy.parse(gpx_file)
|
||||
if len(gpx.tracks) == 0:
|
||||
return None
|
||||
return gpx
|
||||
|
||||
|
||||
def get_gpx_data(parsed_gpx, max_speed, start, stopped_time_btwn_seg):
|
||||
gpx_data = {'max_speed': (max_speed / 1000) * 3600, 'start': start}
|
||||
|
||||
duration = parsed_gpx.get_duration()
|
||||
gpx_data['duration'] = timedelta(seconds=duration) + stopped_time_btwn_seg
|
||||
|
||||
ele = parsed_gpx.get_elevation_extremes()
|
||||
gpx_data['elevation_max'] = ele.maximum
|
||||
gpx_data['elevation_min'] = ele.minimum
|
||||
|
||||
hill = parsed_gpx.get_uphill_downhill()
|
||||
gpx_data['uphill'] = hill.uphill
|
||||
gpx_data['downhill'] = hill.downhill
|
||||
|
||||
mv = parsed_gpx.get_moving_data()
|
||||
gpx_data['moving_time'] = timedelta(seconds=mv.moving_time)
|
||||
gpx_data['stop_time'] = (
|
||||
timedelta(seconds=mv.stopped_time) + stopped_time_btwn_seg
|
||||
)
|
||||
distance = mv.moving_distance + mv.stopped_distance
|
||||
gpx_data['distance'] = distance / 1000
|
||||
|
||||
average_speed = distance / mv.moving_time if mv.moving_time > 0 else 0
|
||||
gpx_data['average_speed'] = (average_speed / 1000) * 3600
|
||||
|
||||
return gpx_data
|
||||
|
||||
|
||||
def get_gpx_info(gpx_file, update_map_data=True, update_weather_data=True):
|
||||
gpx = open_gpx_file(gpx_file)
|
||||
if gpx is None:
|
||||
return None
|
||||
|
||||
gpx_data = {'name': gpx.tracks[0].name, 'segments': []}
|
||||
max_speed = 0
|
||||
start = 0
|
||||
map_data = []
|
||||
weather_data = []
|
||||
segments_nb = len(gpx.tracks[0].segments)
|
||||
prev_seg_last_point = None
|
||||
no_stopped_time = timedelta(seconds=0)
|
||||
stopped_time_btwn_seg = no_stopped_time
|
||||
|
||||
for segment_idx, segment in enumerate(gpx.tracks[0].segments):
|
||||
segment_start = 0
|
||||
segment_points_nb = len(segment.points)
|
||||
for point_idx, point in enumerate(segment.points):
|
||||
if point_idx == 0:
|
||||
# first gpx point => get weather
|
||||
if start == 0:
|
||||
start = point.time
|
||||
if update_weather_data:
|
||||
weather_data.append(get_weather(point))
|
||||
|
||||
# if a previous segment exists, calculate stopped time between
|
||||
# the two segments
|
||||
if prev_seg_last_point:
|
||||
stopped_time_btwn_seg = point.time - prev_seg_last_point
|
||||
|
||||
# last segment point
|
||||
if point_idx == (segment_points_nb - 1):
|
||||
prev_seg_last_point = point.time
|
||||
|
||||
# last gpx point => get weather
|
||||
if segment_idx == (segments_nb - 1) and update_weather_data:
|
||||
weather_data.append(get_weather(point))
|
||||
|
||||
if update_map_data:
|
||||
map_data.append([point.longitude, point.latitude])
|
||||
segment_max_speed = (
|
||||
segment.get_moving_data().max_speed
|
||||
if segment.get_moving_data().max_speed
|
||||
else 0
|
||||
)
|
||||
|
||||
if segment_max_speed > max_speed:
|
||||
max_speed = segment_max_speed
|
||||
|
||||
segment_data = get_gpx_data(
|
||||
segment, segment_max_speed, segment_start, no_stopped_time
|
||||
)
|
||||
segment_data['idx'] = segment_idx
|
||||
gpx_data['segments'].append(segment_data)
|
||||
|
||||
full_gpx_data = get_gpx_data(gpx, max_speed, start, stopped_time_btwn_seg)
|
||||
gpx_data = {**gpx_data, **full_gpx_data}
|
||||
|
||||
if update_map_data:
|
||||
bounds = gpx.get_bounds()
|
||||
gpx_data['bounds'] = [
|
||||
bounds.min_latitude,
|
||||
bounds.min_longitude,
|
||||
bounds.max_latitude,
|
||||
bounds.max_longitude,
|
||||
]
|
||||
|
||||
return gpx_data, map_data, weather_data
|
||||
|
||||
|
||||
def get_gpx_segments(track_segments, segment_id=None):
|
||||
if segment_id is not None:
|
||||
segment_index = segment_id - 1
|
||||
if segment_index > (len(track_segments) - 1):
|
||||
raise ActivityGPXException(
|
||||
'not found', f'No segment with id \'{segment_id}\'', None
|
||||
)
|
||||
if segment_index < 0:
|
||||
raise ActivityGPXException('error', 'Incorrect segment id', None)
|
||||
segments = [track_segments[segment_index]]
|
||||
else:
|
||||
segments = track_segments
|
||||
|
||||
return segments
|
||||
|
||||
|
||||
def get_chart_data(gpx_file, segment_id=None):
|
||||
gpx = open_gpx_file(gpx_file)
|
||||
if gpx is None:
|
||||
return None
|
||||
|
||||
chart_data = []
|
||||
first_point = None
|
||||
previous_point = None
|
||||
previous_distance = 0
|
||||
|
||||
track_segments = gpx.tracks[0].segments
|
||||
segments = get_gpx_segments(track_segments, segment_id)
|
||||
|
||||
for segment_idx, segment in enumerate(segments):
|
||||
for point_idx, point in enumerate(segment.points):
|
||||
if segment_idx == 0 and point_idx == 0:
|
||||
first_point = point
|
||||
distance = (
|
||||
point.distance_3d(previous_point)
|
||||
if (
|
||||
point.elevation
|
||||
and previous_point
|
||||
and previous_point.elevation
|
||||
)
|
||||
else point.distance_2d(previous_point)
|
||||
)
|
||||
distance = 0 if distance is None else distance
|
||||
distance += previous_distance
|
||||
speed = (
|
||||
round((segment.get_speed(point_idx) / 1000) * 3600, 2)
|
||||
if segment.get_speed(point_idx) is not None
|
||||
else 0
|
||||
)
|
||||
chart_data.append(
|
||||
{
|
||||
'distance': (
|
||||
round(distance / 1000, 2)
|
||||
if distance is not None
|
||||
else 0
|
||||
),
|
||||
'duration': point.time_difference(first_point),
|
||||
'elevation': (
|
||||
round(point.elevation, 1)
|
||||
if point.elevation is not None
|
||||
else 0
|
||||
),
|
||||
'latitude': point.latitude,
|
||||
'longitude': point.longitude,
|
||||
'speed': speed,
|
||||
'time': point.time,
|
||||
}
|
||||
)
|
||||
previous_point = point
|
||||
previous_distance = distance
|
||||
|
||||
return chart_data
|
||||
|
||||
|
||||
def extract_segment_from_gpx_file(content, segment_id):
|
||||
gpx_content = gpxpy.parse(content)
|
||||
if len(gpx_content.tracks) == 0:
|
||||
return None
|
||||
|
||||
track_segment = get_gpx_segments(
|
||||
gpx_content.tracks[0].segments, segment_id
|
||||
)
|
||||
|
||||
gpx = gpxpy.gpx.GPX()
|
||||
gpx_track = gpxpy.gpx.GPXTrack()
|
||||
gpx.tracks.append(gpx_track)
|
||||
gpx_segment = gpxpy.gpx.GPXTrackSegment()
|
||||
gpx_track.segments.append(gpx_segment)
|
||||
|
||||
for point_idx, point in enumerate(track_segment[0].points):
|
||||
gpx_segment.points.append(
|
||||
gpxpy.gpx.GPXTrackPoint(
|
||||
point.latitude, point.longitude, elevation=point.elevation
|
||||
)
|
||||
)
|
||||
|
||||
return gpx.to_xml()
|
32
fittrackee/activities/utils_weather.py
Normal file
@ -0,0 +1,32 @@
|
||||
import os
|
||||
|
||||
import forecastio
|
||||
import pytz
|
||||
from fittrackee import appLog
|
||||
|
||||
API_KEY = os.getenv('WEATHER_API')
|
||||
|
||||
|
||||
def get_weather(point):
|
||||
if not API_KEY or API_KEY == '':
|
||||
return None
|
||||
try:
|
||||
point_time = pytz.utc.localize(point.time)
|
||||
forecast = forecastio.load_forecast(
|
||||
API_KEY,
|
||||
point.latitude,
|
||||
point.longitude,
|
||||
time=point_time,
|
||||
units='si',
|
||||
)
|
||||
weather = forecast.currently()
|
||||
return {
|
||||
'summary': weather.summary,
|
||||
'icon': weather.icon,
|
||||
'temperature': weather.temperature,
|
||||
'humidity': weather.humidity,
|
||||
'wind': weather.windSpeed,
|
||||
}
|
||||
except Exception as e:
|
||||
appLog.error(e)
|
||||
return None
|
0
fittrackee/application/__init__.py
Normal file
171
fittrackee/application/app_config.py
Normal file
@ -0,0 +1,171 @@
|
||||
from fittrackee import appLog, db
|
||||
from flask import Blueprint, current_app, jsonify, request
|
||||
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
|
||||
|
||||
from ..users.utils import authenticate_as_admin
|
||||
from .models import AppConfig
|
||||
from .utils import update_app_config_from_database
|
||||
|
||||
config_blueprint = Blueprint('config', __name__)
|
||||
|
||||
|
||||
@config_blueprint.route('/config', methods=['GET'])
|
||||
def get_application_config():
|
||||
"""
|
||||
Get Application config
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/config HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"gpx_limit_import": 10,
|
||||
"is_registration_enabled": false,
|
||||
"max_single_file_size": 1048576,
|
||||
"max_zip_file_size": 10485760,
|
||||
"max_users": 0
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 500: Error on getting configuration.
|
||||
"""
|
||||
|
||||
try:
|
||||
config = AppConfig.query.one()
|
||||
response_object = {'status': 'success', 'data': config.serialize()}
|
||||
return jsonify(response_object), 200
|
||||
except (MultipleResultsFound, NoResultFound) as e:
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error on getting configuration.',
|
||||
}
|
||||
return jsonify(response_object), 500
|
||||
|
||||
|
||||
@config_blueprint.route('/config', methods=['PATCH'])
|
||||
@authenticate_as_admin
|
||||
def update_application_config(auth_user_id):
|
||||
"""
|
||||
Update Application config
|
||||
|
||||
Authenticated user must be an admin
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/config HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"gpx_limit_import": 10,
|
||||
"is_registration_enabled": true,
|
||||
"max_single_file_size": 1048576,
|
||||
"max_zip_file_size": 10485760,
|
||||
"max_users": 10
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
||||
|
||||
:<json integrer gpx_limit_import: max number of files in zip archive
|
||||
:<json boolean is_registration_enabled: is registration enabled ?
|
||||
:<json integrer max_single_file_size: max size of a single file
|
||||
:<json integrer max_zip_file_size: max size of a zip archive
|
||||
:<json integrer max_users: max users allowed to register on instance
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: success
|
||||
:statuscode 400: invalid payload
|
||||
:statuscode 401:
|
||||
- Provide a valid auth token.
|
||||
- Signature expired. Please log in again.
|
||||
- Invalid token. Please log in again.
|
||||
:statuscode 403: You do not have permissions.
|
||||
:statuscode 500: Error on updating configuration.
|
||||
"""
|
||||
config_data = request.get_json()
|
||||
if not config_data:
|
||||
response_object = {'status': 'error', 'message': 'Invalid payload.'}
|
||||
return jsonify(response_object), 400
|
||||
|
||||
try:
|
||||
config = AppConfig.query.one()
|
||||
if 'gpx_limit_import' in config_data:
|
||||
config.gpx_limit_import = config_data.get('gpx_limit_import')
|
||||
if 'max_single_file_size' in config_data:
|
||||
config.max_single_file_size = config_data.get(
|
||||
'max_single_file_size'
|
||||
)
|
||||
if 'max_zip_file_size' in config_data:
|
||||
config.max_zip_file_size = config_data.get('max_zip_file_size')
|
||||
if 'max_users' in config_data:
|
||||
config.max_users = config_data.get('max_users')
|
||||
|
||||
db.session.commit()
|
||||
update_app_config_from_database(current_app, config)
|
||||
|
||||
response_object = {'status': 'success', 'data': config.serialize()}
|
||||
code = 200
|
||||
|
||||
except Exception as e:
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error on updating configuration.',
|
||||
}
|
||||
code = 500
|
||||
return jsonify(response_object), code
|
||||
|
||||
|
||||
@config_blueprint.route('/ping', methods=['GET'])
|
||||
def health_check():
|
||||
"""health check endpoint
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/ping HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "pong!",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
:statuscode 200: success
|
||||
|
||||
"""
|
||||
return jsonify({'status': 'success', 'message': 'pong!'})
|
57
fittrackee/application/models.py
Normal file
@ -0,0 +1,57 @@
|
||||
from fittrackee import db
|
||||
from flask import current_app
|
||||
from sqlalchemy.event import listens_for
|
||||
|
||||
from ..users.models import User
|
||||
|
||||
|
||||
class AppConfig(db.Model):
|
||||
__tablename__ = 'app_config'
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
max_users = db.Column(db.Integer, default=0, nullable=False)
|
||||
gpx_limit_import = db.Column(db.Integer, default=10, nullable=False)
|
||||
max_single_file_size = db.Column(
|
||||
db.Integer, default=1048576, nullable=False
|
||||
)
|
||||
max_zip_file_size = db.Column(db.Integer, default=10485760, nullable=False)
|
||||
|
||||
@property
|
||||
def is_registration_enabled(self):
|
||||
nb_users = User.query.count()
|
||||
return self.max_users == 0 or nb_users < self.max_users
|
||||
|
||||
@property
|
||||
def map_attribution(self):
|
||||
return current_app.config['TILE_SERVER']['ATTRIBUTION']
|
||||
|
||||
def serialize(self):
|
||||
return {
|
||||
"gpx_limit_import": self.gpx_limit_import,
|
||||
"is_registration_enabled": self.is_registration_enabled,
|
||||
"max_single_file_size": self.max_single_file_size,
|
||||
"max_zip_file_size": self.max_zip_file_size,
|
||||
"max_users": self.max_users,
|
||||
"map_attribution": self.map_attribution,
|
||||
}
|
||||
|
||||
|
||||
def update_app_config():
|
||||
config = AppConfig.query.first()
|
||||
if config:
|
||||
current_app.config[
|
||||
'is_registration_enabled'
|
||||
] = config.is_registration_enabled
|
||||
|
||||
|
||||
@listens_for(User, 'after_insert')
|
||||
def on_user_insert(mapper, connection, user):
|
||||
@listens_for(db.Session, 'after_flush', once=True)
|
||||
def receive_after_flush(session, context):
|
||||
update_app_config()
|
||||
|
||||
|
||||
@listens_for(User, 'after_delete')
|
||||
def on_user_delete(mapper, connection, old_user):
|
||||
@listens_for(db.Session, 'after_flush', once=True)
|
||||
def receive_after_flush(session, context):
|
||||
update_app_config()
|
46
fittrackee/application/utils.py
Normal file
@ -0,0 +1,46 @@
|
||||
import os
|
||||
|
||||
from fittrackee import db
|
||||
from fittrackee.users.models import User
|
||||
|
||||
from .models import AppConfig
|
||||
|
||||
MAX_FILE_SIZE = 1 * 1024 * 1024 # 1MB
|
||||
|
||||
|
||||
def init_config():
|
||||
"""
|
||||
init application configuration if not existing in database
|
||||
|
||||
Note: get some configuration values from env variables
|
||||
(for FitTrackee versions prior to v0.3.0)
|
||||
"""
|
||||
existing_config = AppConfig.query.one_or_none()
|
||||
nb_users = User.query.count()
|
||||
if not existing_config:
|
||||
config = AppConfig()
|
||||
config.max_users = (
|
||||
nb_users
|
||||
if os.getenv('REACT_APP_ALLOW_REGISTRATION') == "false"
|
||||
else 0
|
||||
)
|
||||
config.max_single_file_size = os.environ.get(
|
||||
'REACT_APP_MAX_SINGLE_FILE_SIZE', MAX_FILE_SIZE
|
||||
)
|
||||
config.max_zip_file_size = os.environ.get(
|
||||
'REACT_APP_MAX_ZIP_FILE_SIZE', MAX_FILE_SIZE * 10
|
||||
)
|
||||
db.session.add(config)
|
||||
db.session.commit()
|
||||
return True, config
|
||||
return False, existing_config
|
||||
|
||||
|
||||
def update_app_config_from_database(current_app, db_config):
|
||||
current_app.config['gpx_limit_import'] = db_config.gpx_limit_import
|
||||
current_app.config['max_single_file_size'] = db_config.max_single_file_size
|
||||
current_app.config['MAX_CONTENT_LENGTH'] = db_config.max_zip_file_size
|
||||
current_app.config['max_users'] = db_config.max_users
|
||||
current_app.config[
|
||||
'is_registration_enabled'
|
||||
] = db_config.is_registration_enabled
|
70
fittrackee/config.py
Normal file
@ -0,0 +1,70 @@
|
||||
import os
|
||||
|
||||
from dramatiq.brokers.redis import RedisBroker
|
||||
from dramatiq.brokers.stub import StubBroker
|
||||
from flask import current_app
|
||||
|
||||
if os.getenv('APP_SETTINGS') == 'fittrackee.config.TestingConfig':
|
||||
broker = StubBroker
|
||||
else:
|
||||
broker = RedisBroker
|
||||
|
||||
|
||||
class BaseConfig:
|
||||
"""Base configuration"""
|
||||
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
BCRYPT_LOG_ROUNDS = 13
|
||||
TOKEN_EXPIRATION_DAYS = 30
|
||||
TOKEN_EXPIRATION_SECONDS = 0
|
||||
PASSWORD_TOKEN_EXPIRATION_SECONDS = 3600
|
||||
UPLOAD_FOLDER = os.path.join(current_app.root_path, 'uploads')
|
||||
PICTURE_ALLOWED_EXTENSIONS = {'jpg', 'png', 'gif'}
|
||||
ACTIVITY_ALLOWED_EXTENSIONS = {'gpx', 'zip'}
|
||||
TEMPLATES_FOLDER = os.path.join(current_app.root_path, 'email/templates')
|
||||
UI_URL = os.environ.get('UI_URL')
|
||||
EMAIL_URL = os.environ.get('EMAIL_URL')
|
||||
SENDER_EMAIL = os.environ.get('SENDER_EMAIL')
|
||||
DRAMATIQ_BROKER = broker
|
||||
TILE_SERVER = {
|
||||
'URL': os.environ.get(
|
||||
'TILE_SERVER_URL',
|
||||
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
),
|
||||
'ATTRIBUTION': os.environ.get(
|
||||
'MAP_ATTRIBUTION',
|
||||
'© <a href="http://www.openstreetmap.org/copyright" '
|
||||
'target="_blank" rel="noopener noreferrer">OpenStreetMap</a>'
|
||||
' contributors',
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class DevelopmentConfig(BaseConfig):
|
||||
"""Development configuration"""
|
||||
|
||||
DEBUG = True
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
|
||||
SECRET_KEY = 'development key'
|
||||
USERNAME = 'admin'
|
||||
PASSWORD = 'default'
|
||||
BCRYPT_LOG_ROUNDS = 4
|
||||
DRAMATIQ_BROKER_URL = os.getenv('REDIS_URL', 'redis://')
|
||||
|
||||
|
||||
class TestingConfig(BaseConfig):
|
||||
"""Testing configuration"""
|
||||
|
||||
DEBUG = True
|
||||
TESTING = True
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_TEST_URL')
|
||||
SECRET_KEY = 'test key'
|
||||
USERNAME = 'admin'
|
||||
PASSWORD = 'default'
|
||||
BCRYPT_LOG_ROUNDS = 4
|
||||
TOKEN_EXPIRATION_DAYS = 0
|
||||
TOKEN_EXPIRATION_SECONDS = 3
|
||||
PASSWORD_TOKEN_EXPIRATION_SECONDS = 3
|
||||
UPLOAD_FOLDER = '/tmp/fitTrackee/uploads'
|
26
fittrackee/dist/asset-manifest.json
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.9eb63bc2.chunk.css",
|
||||
"main.js": "/static/js/main.e589eaf8.chunk.js",
|
||||
"main.js.map": "/static/js/main.e589eaf8.chunk.js.map",
|
||||
"runtime-main.js": "/static/js/runtime-main.2d7c76f9.js",
|
||||
"runtime-main.js.map": "/static/js/runtime-main.2d7c76f9.js.map",
|
||||
"static/js/2.8ad7236a.chunk.js": "/static/js/2.8ad7236a.chunk.js",
|
||||
"static/js/2.8ad7236a.chunk.js.map": "/static/js/2.8ad7236a.chunk.js.map",
|
||||
"index.html": "/index.html",
|
||||
"precache-manifest.5c6aeed76c2cb8cfc6f5407698470a91.js": "/precache-manifest.5c6aeed76c2cb8cfc6f5407698470a91.js",
|
||||
"service-worker.js": "/service-worker.js",
|
||||
"static/css/main.9eb63bc2.chunk.css.map": "/static/css/main.9eb63bc2.chunk.css.map",
|
||||
"static/js/2.8ad7236a.chunk.js.LICENSE.txt": "/static/js/2.8ad7236a.chunk.js.LICENSE.txt",
|
||||
"static/media/en.501888db.svg": "/static/media/en.501888db.svg",
|
||||
"static/media/fr.b75cd962.svg": "/static/media/fr.b75cd962.svg",
|
||||
"static/media/mail-send.66b8d739.svg": "/static/media/mail-send.66b8d739.svg",
|
||||
"static/media/password.488f5f4c.svg": "/static/media/password.488f5f4c.svg"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/js/runtime-main.2d7c76f9.js",
|
||||
"static/js/2.8ad7236a.chunk.js",
|
||||
"static/css/main.9eb63bc2.chunk.css",
|
||||
"static/js/main.e589eaf8.chunk.js"
|
||||
]
|
||||
}
|
BIN
fittrackee/dist/favicon.ico
vendored
Normal file
After Width: | Height: | Size: 318 B |
BIN
fittrackee/dist/img/photo.png
vendored
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
fittrackee/dist/img/sports/cycling-sport.png
vendored
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
fittrackee/dist/img/sports/cycling-transport.png
vendored
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
fittrackee/dist/img/sports/hiking.png
vendored
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
fittrackee/dist/img/sports/mountain-biking.png
vendored
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
fittrackee/dist/img/sports/running.png
vendored
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
fittrackee/dist/img/sports/walking.png
vendored
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
fittrackee/dist/img/weather/breeze.png
vendored
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
fittrackee/dist/img/weather/clear-day.png
vendored
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
fittrackee/dist/img/weather/clear-night.png
vendored
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
fittrackee/dist/img/weather/cloudy.png
vendored
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
fittrackee/dist/img/weather/fog.png
vendored
Normal file
After Width: | Height: | Size: 1012 B |
BIN
fittrackee/dist/img/weather/partly-cloudy-day.png
vendored
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
fittrackee/dist/img/weather/partly-cloudy-night.png
vendored
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
fittrackee/dist/img/weather/pour-rain.png
vendored
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
fittrackee/dist/img/weather/rain.png
vendored
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
fittrackee/dist/img/weather/sleet.png
vendored
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
fittrackee/dist/img/weather/snow.png
vendored
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
fittrackee/dist/img/weather/temperature.png
vendored
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
fittrackee/dist/img/weather/wind.png
vendored
Normal file
After Width: | Height: | Size: 2.5 KiB |
1
fittrackee/dist/index.html
vendored
Normal file
@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="/manifest.json"><link rel="shortcut icon" href="/favicon.ico"><link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.7/css/fork-awesome.min.css" integrity="sha256-gsmEoJAws/Kd3CjuOQzLie5Q3yshhvmo7YNtBG7aaEY=" crossorigin="anonymous"><link rel="stylesheet" href="https://cdn.jsdelivr.net/foundation-icons/3.0/foundation-icons.min.css"><link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""><title>FitTrackee</title><link href="/static/css/main.9eb63bc2.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script><script type="text/javascript">$(document).ready((function(){$("li.nav-item").click((function(){$("button.navbar-toggler").toggleClass("collapsed"),$("#navbarSupportedContent").toggleClass("show")}))}))</script><script>!function(e){function t(t){for(var n,i,l=t[0],f=t[1],a=t[2],p=0,s=[];p<l.length;p++)i=l[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(t);s.length;)s.shift()();return u.push.apply(u,a||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,l=1;l<r.length;l++){var f=r[l];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/";var l=this.webpackJsonpfittrackee_client=this.webpackJsonpfittrackee_client||[],f=l.push.bind(l);l.push=t,l=l.slice();for(var a=0;a<l.length;a++)t(l[a]);var c=f;r()}([])</script><script src="/static/js/2.8ad7236a.chunk.js"></script><script src="/static/js/main.e589eaf8.chunk.js"></script></body></html>
|
16
fittrackee/dist/manifest.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"short_name": "FitTrackee",
|
||||
"name": "Self hosted workout/activity tracker",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff",
|
||||
"version": "0.2.0-beta"
|
||||
}
|
42
fittrackee/dist/precache-manifest.5c6aeed76c2cb8cfc6f5407698470a91.js
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
self.__precacheManifest = (self.__precacheManifest || []).concat([
|
||||
{
|
||||
"revision": "ce27ec4197f51699274b624e1dd24806",
|
||||
"url": "/index.html"
|
||||
},
|
||||
{
|
||||
"revision": "18e9a261449a30132350",
|
||||
"url": "/static/css/main.9eb63bc2.chunk.css"
|
||||
},
|
||||
{
|
||||
"revision": "4fea69143e8c947ba6e0",
|
||||
"url": "/static/js/2.8ad7236a.chunk.js"
|
||||
},
|
||||
{
|
||||
"revision": "92427c76d8e49274b27c017747c41b82",
|
||||
"url": "/static/js/2.8ad7236a.chunk.js.LICENSE.txt"
|
||||
},
|
||||
{
|
||||
"revision": "18e9a261449a30132350",
|
||||
"url": "/static/js/main.e589eaf8.chunk.js"
|
||||
},
|
||||
{
|
||||
"revision": "c25e4b04867fdd1a7bb1",
|
||||
"url": "/static/js/runtime-main.2d7c76f9.js"
|
||||
},
|
||||
{
|
||||
"revision": "501888db26f05ed93528eae579c09ce9",
|
||||
"url": "/static/media/en.501888db.svg"
|
||||
},
|
||||
{
|
||||
"revision": "b75cd9624fb6c184c40c24f5bc035610",
|
||||
"url": "/static/media/fr.b75cd962.svg"
|
||||
},
|
||||
{
|
||||
"revision": "66b8d739b7190f68ea99314d55e1f53d",
|
||||
"url": "/static/media/mail-send.66b8d739.svg"
|
||||
},
|
||||
{
|
||||
"revision": "488f5f4cd9648db1147b97d8f1357e24",
|
||||
"url": "/static/media/password.488f5f4c.svg"
|
||||
}
|
||||
]);
|
39
fittrackee/dist/service-worker.js
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Welcome to your Workbox-powered service worker!
|
||||
*
|
||||
* You'll need to register this file in your web app and you should
|
||||
* disable HTTP caching for this file too.
|
||||
* See https://goo.gl/nhQhGp
|
||||
*
|
||||
* The rest of the code is auto-generated. Please don't update this file
|
||||
* directly; instead, make changes to your Workbox build configuration
|
||||
* and re-run your build process.
|
||||
* See https://goo.gl/2aRDsh
|
||||
*/
|
||||
|
||||
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
|
||||
|
||||
importScripts(
|
||||
"/precache-manifest.5c6aeed76c2cb8cfc6f5407698470a91.js"
|
||||
);
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
workbox.core.clientsClaim();
|
||||
|
||||
/**
|
||||
* The workboxSW.precacheAndRoute() method efficiently caches and responds to
|
||||
* requests for URLs in the manifest.
|
||||
* See https://goo.gl/S9QRab
|
||||
*/
|
||||
self.__precacheManifest = [].concat(self.__precacheManifest || []);
|
||||
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
|
||||
|
||||
workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), {
|
||||
|
||||
blacklist: [/^\/_/,/\/[^/?]+\.[^/]+$/],
|
||||
});
|
2
fittrackee/dist/static/css/main.9eb63bc2.chunk.css
vendored
Normal file
1
fittrackee/dist/static/css/main.9eb63bc2.chunk.css.map
vendored
Normal file
3
fittrackee/dist/static/js/2.8ad7236a.chunk.js
vendored
Normal file
54
fittrackee/dist/static/js/2.8ad7236a.chunk.js.LICENSE.txt
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
object-assign
|
||||
(c) Sindre Sorhus
|
||||
@license MIT
|
||||
*/
|
||||
|
||||
/* @preserve
|
||||
* Leaflet 1.7.1, a JS library for interactive maps. http://leafletjs.com
|
||||
* (c) 2010-2019 Vladimir Agafonkin, (c) 2010-2011 CloudMade
|
||||
*/
|
||||
|
||||
/*!
|
||||
Copyright (c) 2017 Jed Watson.
|
||||
Licensed under the MIT License (MIT), see
|
||||
http://jedwatson.github.io/classnames
|
||||
*/
|
||||
|
||||
/*! decimal.js-light v2.5.0 https://github.com/MikeMcl/decimal.js-light/LICENCE */
|
||||
|
||||
/** @license React v0.19.1
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.13.1
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.13.1
|
||||
* react-is.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.13.1
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
1
fittrackee/dist/static/js/2.8ad7236a.chunk.js.map
vendored
Normal file
2
fittrackee/dist/static/js/main.e589eaf8.chunk.js
vendored
Normal file
1
fittrackee/dist/static/js/main.e589eaf8.chunk.js.map
vendored
Normal file
2
fittrackee/dist/static/js/runtime-main.2d7c76f9.js
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
!function(e){function t(t){for(var n,i,l=t[0],f=t[1],a=t[2],p=0,s=[];p<l.length;p++)i=l[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(t);s.length;)s.shift()();return u.push.apply(u,a||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,l=1;l<r.length;l++){var f=r[l];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/";var l=this.webpackJsonpfittrackee_client=this.webpackJsonpfittrackee_client||[],f=l.push.bind(l);l.push=t,l=l.slice();for(var a=0;a<l.length;a++)t(l[a]);var c=f;r()}([]);
|
||||
//# sourceMappingURL=runtime-main.2d7c76f9.js.map
|
1
fittrackee/dist/static/js/runtime-main.2d7c76f9.js.map
vendored
Normal file
27
fittrackee/dist/static/media/en.501888db.svg
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512"
|
||||
viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m466.916 27.803h-421.832c-24.859 0-45.084 20.225-45.084 45.084v366.226c0 24.859 20.225 45.084 45.084 45.084h421.832c24.859 0 45.084-20.225 45.084-45.084v-366.226c0-24.859-20.225-45.084-45.084-45.084z"
|
||||
fill="#f0f9ff"/>
|
||||
<path d="m198.58 188.334-181.344-150.862c-7.75 6.107-13.456 14.691-15.905 24.554l164.142 136.551h33.102z"
|
||||
fill="#f40055"/>
|
||||
<path d="m313.425 198.576h33.93l163.447-135.973c-2.325-9.923-7.93-18.592-15.613-24.796l-181.764 151.211z"
|
||||
fill="#c20044"/>
|
||||
<path d="m165.472 313.425-164.141 136.549c2.449 9.863 8.155 18.447 15.905 24.553l181.344-150.861-.005-10.241z"
|
||||
fill="#f40055"/>
|
||||
<path d="m313.425 313.425v9.557l181.765 151.211c7.683-6.204 13.288-14.874 15.613-24.796l-163.446-135.971z"
|
||||
fill="#c20044"/>
|
||||
<path d="m53.273 27.803 145.302 120.879v-120.879z" fill="#406bd4"/>
|
||||
<path d="m313.425 150.571v-122.768h148.082z" fill="#3257b0"/>
|
||||
<path d="m394.732 198.575 117.268-97.556v97.556z" fill="#3257b0"/>
|
||||
<g fill="#406bd4">
|
||||
<path d="m0 99.317v99.258h119.313z"/>
|
||||
<path d="m0 313.425v97.699l117.44-97.699z"/>
|
||||
<path d="m50.49 484.197 148.085-122.676v122.676z"/>
|
||||
</g>
|
||||
<path d="m313.425 484.197v-124.139l149.221 124.139z" fill="#3257b0"/>
|
||||
<path d="m512 409.423-115.395-95.998h115.395z" fill="#3257b0"/>
|
||||
<path d="m512 222.142h-222.142v-194.339h-67.716v194.339h-222.142v67.716h222.142v194.339h67.716v-194.339h222.142z"
|
||||
fill="#f40055"/>
|
||||
<path d="m289.858 222.142v-194.339h-33.858v456.394h33.858v-194.339h222.142v-67.716z"
|
||||
fill="#c20044"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
1
fittrackee/dist/static/media/fr.b75cd962.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m173.899 31.804h-8.707l-4.397-4h-115.711c-24.859-.001-45.084 20.224-45.084 45.083v366.226c0 24.859 20.225 45.084 45.084 45.084h115.711l6.348-4h6.755v-448.393z" fill="#406bd4"/><path d="m466.916 27.803h-115.711l-4.523 4h-5.141v448.393h4.141l5.523 4h115.711c24.859 0 45.084-20.225 45.084-45.084v-366.225c0-24.859-20.225-45.084-45.084-45.084z" fill="#c20044"/><path d="m160.795 27.803h190.409v456.394h-190.409z" fill="#f0f9ff"/><path d="m256 27.803h95.205v456.394h-95.205z" fill="#cee5f5"/></svg>
|
After Width: | Height: | Size: 637 B |
42
fittrackee/dist/static/media/mail-send.66b8d739.svg
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 345.834 345.834" style="enable-background:new 0 0 345.834 345.834;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M339.798,260.429c0.13-0.026,0.257-0.061,0.385-0.094c0.109-0.028,0.219-0.051,0.326-0.084
|
||||
c0.125-0.038,0.247-0.085,0.369-0.129c0.108-0.039,0.217-0.074,0.324-0.119c0.115-0.048,0.226-0.104,0.338-0.157
|
||||
c0.109-0.052,0.22-0.1,0.327-0.158c0.107-0.057,0.208-0.122,0.312-0.184c0.107-0.064,0.215-0.124,0.319-0.194
|
||||
c0.111-0.074,0.214-0.156,0.321-0.236c0.09-0.067,0.182-0.13,0.27-0.202c0.162-0.133,0.316-0.275,0.466-0.421
|
||||
c0.027-0.026,0.056-0.048,0.083-0.075c0.028-0.028,0.052-0.059,0.079-0.088c0.144-0.148,0.284-0.3,0.416-0.46
|
||||
c0.077-0.094,0.144-0.192,0.216-0.289c0.074-0.1,0.152-0.197,0.221-0.301c0.074-0.111,0.139-0.226,0.207-0.34
|
||||
c0.057-0.096,0.118-0.19,0.171-0.289c0.062-0.115,0.114-0.234,0.169-0.351c0.049-0.104,0.101-0.207,0.146-0.314
|
||||
c0.048-0.115,0.086-0.232,0.128-0.349c0.041-0.114,0.085-0.227,0.12-0.343c0.036-0.118,0.062-0.238,0.092-0.358
|
||||
c0.029-0.118,0.063-0.234,0.086-0.353c0.028-0.141,0.045-0.283,0.065-0.425c0.014-0.1,0.033-0.199,0.043-0.3
|
||||
c0.025-0.249,0.038-0.498,0.038-0.748V92.76c0-4.143-3.357-7.5-7.5-7.5h-236.25c-0.066,0-0.13,0.008-0.196,0.01
|
||||
c-0.143,0.004-0.285,0.01-0.427,0.022c-0.113,0.009-0.225,0.022-0.337,0.037c-0.128,0.016-0.255,0.035-0.382,0.058
|
||||
c-0.119,0.021-0.237,0.046-0.354,0.073c-0.119,0.028-0.238,0.058-0.356,0.092c-0.117,0.033-0.232,0.069-0.346,0.107
|
||||
c-0.117,0.04-0.234,0.082-0.349,0.128c-0.109,0.043-0.216,0.087-0.322,0.135c-0.118,0.053-0.235,0.11-0.351,0.169
|
||||
c-0.099,0.051-0.196,0.103-0.292,0.158c-0.116,0.066-0.23,0.136-0.343,0.208c-0.093,0.06-0.184,0.122-0.274,0.185
|
||||
c-0.106,0.075-0.211,0.153-0.314,0.235c-0.094,0.075-0.186,0.152-0.277,0.231c-0.09,0.079-0.179,0.158-0.266,0.242
|
||||
c-0.099,0.095-0.194,0.194-0.288,0.294c-0.047,0.05-0.097,0.094-0.142,0.145c-0.027,0.03-0.048,0.063-0.074,0.093
|
||||
c-0.094,0.109-0.182,0.223-0.27,0.338c-0.064,0.084-0.13,0.168-0.19,0.254c-0.078,0.112-0.15,0.227-0.222,0.343
|
||||
c-0.059,0.095-0.12,0.189-0.174,0.286c-0.063,0.112-0.118,0.227-0.175,0.342c-0.052,0.105-0.106,0.21-0.153,0.317
|
||||
c-0.049,0.113-0.092,0.23-0.135,0.345c-0.043,0.113-0.087,0.225-0.124,0.339c-0.037,0.115-0.067,0.232-0.099,0.349
|
||||
c-0.032,0.12-0.066,0.239-0.093,0.36c-0.025,0.113-0.042,0.228-0.062,0.342c-0.022,0.13-0.044,0.26-0.06,0.39
|
||||
c-0.013,0.108-0.019,0.218-0.027,0.328c-0.01,0.14-0.019,0.28-0.021,0.421c-0.001,0.041-0.006,0.081-0.006,0.122v46.252
|
||||
c0,4.143,3.357,7.5,7.5,7.5s7.5-3.357,7.5-7.5v-29.595l66.681,59.037c-0.348,0.245-0.683,0.516-0.995,0.827l-65.687,65.687v-49.288
|
||||
c0-4.143-3.357-7.5-7.5-7.5s-7.5,3.357-7.5,7.5v9.164h-38.75c-4.143,0-7.5,3.357-7.5,7.5s3.357,7.5,7.5,7.5h38.75v43.231
|
||||
c0,4.143,3.357,7.5,7.5,7.5h236.25c0.247,0,0.494-0.013,0.74-0.037c0.115-0.011,0.226-0.033,0.339-0.049
|
||||
C339.542,260.469,339.67,260.454,339.798,260.429z M330.834,234.967l-65.688-65.687c-0.042-0.042-0.087-0.077-0.13-0.117
|
||||
l49.383-41.897c3.158-2.68,3.546-7.412,0.866-10.571c-2.678-3.157-7.41-3.547-10.571-0.866l-84.381,71.59l-98.444-87.158h208.965
|
||||
V234.967z M185.878,179.888c0.535-0.535,0.969-1.131,1.308-1.765l28.051,24.835c1.418,1.255,3.194,1.885,4.972,1.885
|
||||
c1.726,0,3.451-0.593,4.853-1.781l28.587-24.254c0.26,0.38,0.553,0.743,0.89,1.08l65.687,65.687H120.191L185.878,179.888z"/>
|
||||
<path d="M7.5,170.676h126.667c4.143,0,7.5-3.357,7.5-7.5s-3.357-7.5-7.5-7.5H7.5c-4.143,0-7.5,3.357-7.5,7.5
|
||||
S3.357,170.676,7.5,170.676z"/>
|
||||
<path d="M20.625,129.345H77.5c4.143,0,7.5-3.357,7.5-7.5s-3.357-7.5-7.5-7.5H20.625c-4.143,0-7.5,3.357-7.5,7.5
|
||||
S16.482,129.345,20.625,129.345z"/>
|
||||
<path d="M62.5,226.51h-55c-4.143,0-7.5,3.357-7.5,7.5s3.357,7.5,7.5,7.5h55c4.143,0,7.5-3.357,7.5-7.5S66.643,226.51,62.5,226.51z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.0 KiB |
65
fittrackee/dist/static/media/password.488f5f4c.svg
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512.001 512.001" style="enable-background:new 0 0 512.001 512.001;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M468.683,287.265h-69.07c-4.147,0-7.508,3.361-7.508,7.508c0,4.147,3.361,7.508,7.508,7.508h69.07
|
||||
c4.147,0,7.508-3.361,7.508-7.508C476.191,290.626,472.83,287.265,468.683,287.265z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M105.012,268.377L85.781,256l19.231-12.376c3.487-2.243,4.495-6.888,2.251-10.376c-2.244-3.486-6.888-4.497-10.376-2.25
|
||||
l-17.471,11.243v-20.776c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.775l-17.47-11.243
|
||||
c-3.486-2.245-8.132-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L58.034,256l-19.231,12.376
|
||||
c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.47-11.243v20.775
|
||||
c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.471,11.243c1.257,0.809,2.664,1.196,4.056,1.196
|
||||
c2.467,0,4.885-1.216,6.32-3.446C109.507,275.266,108.499,270.62,105.012,268.377z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M194.441,268.377L175.21,256l19.231-12.376c3.487-2.244,4.495-6.889,2.25-10.376c-2.245-3.486-6.888-4.497-10.376-2.25
|
||||
l-17.47,11.243v-20.775c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.776l-17.471-11.243
|
||||
c-3.487-2.245-8.133-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L147.463,256l-19.231,12.376
|
||||
c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.471-11.243v20.776
|
||||
c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.47,11.243c1.257,0.809,2.664,1.196,4.056,1.196
|
||||
c2.467,0,4.885-1.216,6.32-3.446C198.936,275.266,197.928,270.62,194.441,268.377z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M283.871,268.377L264.64,256l19.231-12.376c3.487-2.243,4.495-6.888,2.251-10.376c-2.245-3.486-6.888-4.497-10.376-2.25
|
||||
l-17.471,11.243v-20.775c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.775l-17.471-11.243
|
||||
c-3.486-2.245-8.134-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L236.892,256l-19.231,12.376
|
||||
c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.471-11.243v20.775
|
||||
c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.471,11.243c1.257,0.809,2.664,1.196,4.056,1.196
|
||||
c2.467,0,4.886-1.216,6.32-3.446C288.366,275.266,287.358,270.62,283.871,268.377z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M373.3,268.377L354.069,256l19.231-12.376c3.487-2.244,4.495-6.889,2.25-10.376c-2.244-3.486-6.888-4.497-10.376-2.25
|
||||
l-17.471,11.243v-20.776c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.775l-17.47-11.243
|
||||
c-3.486-2.245-8.132-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L326.322,256l-19.231,12.376
|
||||
c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.47-11.243v20.776
|
||||
c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.471,11.243c1.257,0.809,2.664,1.196,4.056,1.196
|
||||
c2.467,0,4.885-1.216,6.32-3.446C377.795,275.266,376.787,270.62,373.3,268.377z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M271.792,330.359H15.016V181.642h93.1c4.147,0,7.508-3.361,7.508-7.508c0-4.147-3.361-7.508-7.508-7.508H12.513
|
||||
C5.613,166.626,0,172.24,0,179.14v153.722c0,6.9,5.613,12.513,12.513,12.513h259.278c4.147,0,7.508-3.361,7.508-7.508
|
||||
C279.299,333.72,275.939,330.359,271.792,330.359z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M499.487,166.626H162.174c-4.147,0-7.508,3.361-7.508,7.508c0,4.147,3.361,7.508,7.508,7.508h334.811v148.716H323.848
|
||||
c-4.147,0-7.508,3.361-7.508,7.508c0,4.147,3.361,7.508,7.508,7.508h175.64c6.9,0,12.513-5.613,12.513-12.513V179.14
|
||||
C512.001,172.24,506.387,166.626,499.487,166.626z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.0 KiB |
0
fittrackee/email/__init__.py
Normal file
103
fittrackee/email/email.py
Normal file
@ -0,0 +1,103 @@
|
||||
import logging
|
||||
import smtplib
|
||||
import ssl
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from .utils_email import parse_email_url
|
||||
|
||||
email_log = logging.getLogger('fittrackee_api_email')
|
||||
email_log.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class EmailMessage:
|
||||
def __init__(self, sender, recipient, subject, html, text):
|
||||
self.sender = sender
|
||||
self.recipient = recipient
|
||||
self.subject = subject
|
||||
self.html = html
|
||||
self.text = text
|
||||
|
||||
def generate_message(self):
|
||||
message = MIMEMultipart('alternative')
|
||||
message['Subject'] = self.subject
|
||||
message['From'] = self.sender
|
||||
message['To'] = self.recipient
|
||||
part1 = MIMEText(self.text, 'plain')
|
||||
part2 = MIMEText(self.html, 'html')
|
||||
message.attach(part1)
|
||||
message.attach(part2)
|
||||
return message
|
||||
|
||||
|
||||
class EmailTemplate:
|
||||
def __init__(self, template_directory):
|
||||
self._env = Environment(loader=FileSystemLoader(template_directory))
|
||||
|
||||
def get_content(self, template, lang, part, data):
|
||||
template = self._env.get_template(f'{template}/{lang}/{part}')
|
||||
return template.render(data)
|
||||
|
||||
def get_all_contents(self, template, lang, data):
|
||||
output = {}
|
||||
for part in ['subject.txt', 'body.txt', 'body.html']:
|
||||
output[part] = self.get_content(template, lang, part, data)
|
||||
return output
|
||||
|
||||
def get_message(self, template, lang, sender, recipient, data):
|
||||
output = self.get_all_contents(template, lang, data)
|
||||
message = EmailMessage(
|
||||
sender,
|
||||
recipient,
|
||||
output['subject.txt'],
|
||||
output['body.html'],
|
||||
output['body.txt'],
|
||||
)
|
||||
return message.generate_message()
|
||||
|
||||
|
||||
class Email:
|
||||
def __init__(self, app=None):
|
||||
self.host = 'localhost'
|
||||
self.port = 1025
|
||||
self.use_tls = False
|
||||
self.use_ssl = False
|
||||
self.username = None
|
||||
self.password = None
|
||||
self.sender_email = 'no-reply@example.com'
|
||||
self.email_template = None
|
||||
if app is not None:
|
||||
self.init_email(app)
|
||||
|
||||
def init_email(self, app):
|
||||
parsed_url = parse_email_url(app.config.get('EMAIL_URL'))
|
||||
self.host = parsed_url['host']
|
||||
self.port = parsed_url['port']
|
||||
self.use_tls = parsed_url['use_tls']
|
||||
self.use_ssl = parsed_url['use_ssl']
|
||||
self.username = parsed_url['username']
|
||||
self.password = parsed_url['password']
|
||||
self.sender_email = app.config.get('SENDER_EMAIL')
|
||||
self.email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
|
||||
|
||||
@property
|
||||
def smtp(self):
|
||||
return smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP
|
||||
|
||||
def send(self, template, lang, recipient, data):
|
||||
message = self.email_template.get_message(
|
||||
template, lang, self.sender_email, recipient, data
|
||||
)
|
||||
connection_params = {}
|
||||
if self.use_ssl or self.use_tls:
|
||||
context = ssl.create_default_context()
|
||||
if self.use_ssl:
|
||||
connection_params.update({'context': context})
|
||||
with self.smtp(self.host, self.port, **connection_params) as smtp:
|
||||
smtp.login(self.username, self.password)
|
||||
if self.use_tls:
|
||||
smtp.starttls(context=context)
|
||||
smtp.sendmail(self.sender_email, recipient, message.as_string())
|
||||
smtp.quit()
|
268
fittrackee/email/templates/password_reset_request/en/body.html
Normal file
@ -0,0 +1,268 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="x-apple-disable-message-reformatting" content=""/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Fittrackee - Password reset request</title>
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
|
||||
body {
|
||||
background-color: #F4F4F7;
|
||||
color: #51545E;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #51545E;
|
||||
margin: .4em 0 1.1875em;
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
p.sub {
|
||||
color: #6B6E76;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #3869D4;
|
||||
border-top: 10px solid #3869D4;
|
||||
border-right: 18px solid #3869D4;
|
||||
border-bottom: 10px solid #3869D4;
|
||||
border-left: 18px solid #3869D4;
|
||||
display: inline-block;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
border-right: 18px solid #22BC66;
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #F4F4F7;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.email-masthead {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.email-masthead-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #A8AAAF;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.email-body-inner {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body-sub {
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.content-cell {
|
||||
padding: 35px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-body-inner,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body,
|
||||
.email-body,
|
||||
.email-body-inner,
|
||||
.email-content,
|
||||
.email-wrapper,
|
||||
.email-masthead,
|
||||
.email-footer {
|
||||
background-color: #333333 !important;
|
||||
color: #FFF !important;
|
||||
}
|
||||
p,
|
||||
h1 {
|
||||
color: #FFF !important;
|
||||
}
|
||||
.email-masthead-name {
|
||||
text-shadow: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
.f-fallback {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body>
|
||||
<span class="preheader">Use this link to reset your password. The link is only valid for {{ expiration_delay }}.</span>
|
||||
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="email-masthead">
|
||||
<a href="https://example.com" class="f-fallback email-masthead-name">
|
||||
FitTrackee
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
|
||||
<table class="email-body-inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="content-cell">
|
||||
<div class="f-fallback">
|
||||
<h1>Hi {{username}},</h1>
|
||||
<p>You recently requested to reset your password for your account. Use the button below to reset it.
|
||||
<strong>This password reset link is only valid for {{ expiration_delay }}.</strong>
|
||||
</p>
|
||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="{{password_reset_url}}" class="f-fallback button button--green" target="_blank">Reset your password</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>
|
||||
For security, this request was received from a {{operating_system}} device using {{browser_name}}.
|
||||
If you did not request a password reset, please ignore this email.
|
||||
</p>
|
||||
<p>Thanks,
|
||||
<br>The FitTrackee Team</p>
|
||||
<table class="body-sub" role="presentation">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="f-fallback sub">If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
|
||||
<p class="f-fallback sub">{{password_reset_url}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="content-cell" align="center">
|
||||
<p class="f-fallback sub align-center">© FitTrackee.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,10 @@
|
||||
Hi {{username}},
|
||||
|
||||
You recently requested to reset your password for your FitTrackee account. Use the button below to reset it. This password reset link is only valid for {{ expiration_delay }}.
|
||||
|
||||
Reset your password ( {{ password_reset_url }} )
|
||||
|
||||
For security, this request was received from a {{operating_system}} device using {{browser_name}}. If you did not request a password reset, please ignore this email.
|
||||
|
||||
Thanks,
|
||||
The FitTrackee Team
|
@ -0,0 +1 @@
|
||||
FitTrackee - Password reset request
|
269
fittrackee/email/templates/password_reset_request/fr/body.html
Normal file
@ -0,0 +1,269 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="x-apple-disable-message-reformatting" content=""/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>FitTrackee - Réinitialiser le mot de passe</title>
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
|
||||
body {
|
||||
background-color: #F4F4F7;
|
||||
color: #51545E;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #51545E;
|
||||
margin: .4em 0 1.1875em;
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
p.sub {
|
||||
color: #6B6E76;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #3869D4;
|
||||
border-top: 10px solid #3869D4;
|
||||
border-right: 18px solid #3869D4;
|
||||
border-bottom: 10px solid #3869D4;
|
||||
border-left: 18px solid #3869D4;
|
||||
display: inline-block;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
border-right: 18px solid #22BC66;
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #F4F4F7;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.email-masthead {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.email-masthead-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #A8AAAF;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.email-body-inner {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body-sub {
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.content-cell {
|
||||
padding: 35px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-body-inner,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body,
|
||||
.email-body,
|
||||
.email-body-inner,
|
||||
.email-content,
|
||||
.email-wrapper,
|
||||
.email-masthead,
|
||||
.email-footer {
|
||||
background-color: #333333 !important;
|
||||
color: #FFF !important;
|
||||
}
|
||||
p,
|
||||
h1 {
|
||||
color: #FFF !important;
|
||||
}
|
||||
.email-masthead-name {
|
||||
text-shadow: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
.f-fallback {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body>
|
||||
<span class="preheader">Utiliser ce lien pour réinitialiser le mot de passe. Ce lien n'est valide que pendant {{ expiration_delay }}.</span>
|
||||
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="email-masthead">
|
||||
<a href="https://example.com" class="f-fallback email-masthead-name">
|
||||
FitTrackee
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
|
||||
<table class="email-body-inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="content-cell">
|
||||
<div class="f-fallback">
|
||||
<h1>Bonjour {{username}},</h1>
|
||||
<p>Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
|
||||
Cliquez sur le bouton ci-dessous pour le réinitialiser.
|
||||
<strong>Cette réinitialisation n'est valide que pendant {{ expiration_delay }}.</strong>
|
||||
</p>
|
||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="{{password_reset_url}}" class="f-fallback button button--green" target="_blank">Réinitialiser le mot de passe</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>
|
||||
Pour vérification, cette demande a été reçue à partir d'un appareil sous {{operating_system}}, utilisant le navigateur {{browser_name}}.
|
||||
Si vous n'avez pas demandé de réinitalisation, vous pouvez ignorer cet e-mail.
|
||||
</p>
|
||||
<p>Merci,
|
||||
<br>L'équipe FitTrackee</p>
|
||||
<table class="body-sub" role="presentation">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="f-fallback sub">Si vous avez des problèmes avec le bouton, vous pouvez copier et coller le lien suivant dans votre navigateur</p>
|
||||
<p class="f-fallback sub">{{password_reset_url}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="content-cell" align="center">
|
||||
<p class="f-fallback sub align-center">© FitTrackee.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,12 @@
|
||||
Bonjour {{username}},
|
||||
|
||||
Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
|
||||
Cliquez sur le lien ci-dessous pour le réinitialiser. Ce lien n'est valide que pendant {{ expiration_delay }}.
|
||||
|
||||
Réinitialiser le mot de passe: ( {{ password_reset_url }} )
|
||||
|
||||
Pour vérification, cette demande a été reçue à partir d'un appareil sous {{operating_system}}, utilisant le navigateur {{browser_name}}.
|
||||
Si vous n'avez pas demandé de réinitalisation, vous pouvez ignorer cet e-mail.
|
||||
|
||||
Merci,
|
||||
L'équipe FitTrackee
|
@ -0,0 +1 @@
|
||||
FitTrackee - Réinitialiser votre mot de passe
|
20
fittrackee/email/utils_email.py
Normal file
@ -0,0 +1,20 @@
|
||||
from urllib3.util import parse_url
|
||||
|
||||
|
||||
class InvalidEmailUrlScheme(Exception):
|
||||
...
|
||||
|
||||
|
||||
def parse_email_url(email_url):
|
||||
parsed_url = parse_url(email_url)
|
||||
if parsed_url.scheme != 'smtp':
|
||||
raise InvalidEmailUrlScheme()
|
||||
credentials = parsed_url.auth.split(':')
|
||||
return {
|
||||
'host': parsed_url.host,
|
||||
'port': parsed_url.port,
|
||||
'use_tls': True if parsed_url.query == 'tls=True' else False,
|
||||
'use_ssl': True if parsed_url.query == 'ssl=True' else False,
|
||||
'username': credentials[0],
|
||||
'password': credentials[1],
|
||||
}
|
1
fittrackee/migrations/README
Executable file
@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
45
fittrackee/migrations/alembic.ini
Normal file
@ -0,0 +1,45 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
88
fittrackee/migrations/env.py
Executable file
@ -0,0 +1,88 @@
|
||||
from __future__ import with_statement
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
import logging
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from flask import current_app
|
||||
config.set_main_option('sqlalchemy.url',
|
||||
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(url=url)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
engine = engine_from_config(config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
|
||||
connection = engine.connect()
|
||||
context.configure(connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args)
|
||||
|
||||
try:
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
24
fittrackee/migrations/script.py.mako
Executable file
@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
@ -0,0 +1,44 @@
|
||||
"""create User table
|
||||
|
||||
Revision ID: 9741fc7834da
|
||||
Revises:
|
||||
Create Date: 2018-01-20 18:59:49.200032
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '9741fc7834da'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('users',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.String(length=20), nullable=False),
|
||||
sa.Column('email', sa.String(length=120), nullable=False),
|
||||
sa.Column('password', sa.String(length=255), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('admin', sa.Boolean(), nullable=False),
|
||||
sa.Column('first_name', sa.String(length=80), nullable=True),
|
||||
sa.Column('last_name', sa.String(length=80), nullable=True),
|
||||
sa.Column('birth_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('location', sa.String(length=80), nullable=True),
|
||||
sa.Column('bio', sa.String(length=200), nullable=True),
|
||||
sa.Column('picture', sa.String(length=255), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('email'),
|
||||
sa.UniqueConstraint('username')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('users')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,59 @@
|
||||
"""create Activity & Sport tables
|
||||
|
||||
Revision ID: b7cfe0c17708
|
||||
Revises: 9741fc7834da
|
||||
Create Date: 2018-01-21 17:24:52.587814
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b7cfe0c17708'
|
||||
down_revision = '9741fc7834da'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('sports',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('label', sa.String(length=50), nullable=False),
|
||||
sa.Column('img', sa.String(length=255), nullable=True),
|
||||
sa.Column('is_default', sa.Boolean(), default=False, nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('label')
|
||||
)
|
||||
op.create_table('activities',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('sport_id', sa.Integer(), nullable=False),
|
||||
sa.Column('title', sa.String(length=255), nullable=True),
|
||||
sa.Column('gpx', sa.String(length=255), nullable=True),
|
||||
sa.Column('creation_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('modification_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('activity_date', sa.DateTime(), nullable=False),
|
||||
sa.Column('duration', sa.Interval(), nullable=False),
|
||||
sa.Column('pauses', sa.Interval(), nullable=True),
|
||||
sa.Column('moving', sa.Interval(), nullable=True),
|
||||
sa.Column('distance', sa.Numeric(precision=6, scale=3), nullable=True),
|
||||
sa.Column('min_alt', sa.Numeric(precision=6, scale=2), nullable=True),
|
||||
sa.Column('max_alt', sa.Numeric(precision=6, scale=2), nullable=True),
|
||||
sa.Column('descent', sa.Numeric(precision=7, scale=2), nullable=True),
|
||||
sa.Column('ascent', sa.Numeric(precision=7, scale=2), nullable=True),
|
||||
sa.Column('max_speed', sa.Numeric(precision=6, scale=2), nullable=True),
|
||||
sa.Column('ave_speed', sa.Numeric(precision=6, scale=2), nullable=True),
|
||||
sa.ForeignKeyConstraint(['sport_id'], ['sports.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('activities')
|
||||
op.drop_table('sports')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,41 @@
|
||||
"""create Record table
|
||||
|
||||
Revision ID: caf0e0dc621a
|
||||
Revises: b7cfe0c17708
|
||||
Create Date: 2018-05-11 22:33:08.267488
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'caf0e0dc621a'
|
||||
down_revision = 'b7cfe0c17708'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('records',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('sport_id', sa.Integer(), nullable=False),
|
||||
sa.Column('activity_id', sa.Integer(), nullable=False),
|
||||
sa.Column('record_type', sa.Enum('AS', 'FD', 'LD', 'MS', name='record_types'), nullable=True),
|
||||
sa.Column('activity_date', sa.DateTime(), nullable=False),
|
||||
sa.Column('value', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['activity_id'], ['activities.id'], ),
|
||||
sa.ForeignKeyConstraint(['sport_id'], ['sports.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('user_id', 'sport_id', 'record_type', name='user_sports_records')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('records')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,43 @@
|
||||
"""create Activities Segments table
|
||||
|
||||
Revision ID: dd73d23a7a3d
|
||||
Revises: caf0e0dc621a
|
||||
Create Date: 2018-05-14 12:12:57.299200
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'dd73d23a7a3d'
|
||||
down_revision = 'caf0e0dc621a'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('activity_segments',
|
||||
sa.Column('activity_id', sa.Integer(), nullable=False),
|
||||
sa.Column('segment_id', sa.Integer(), nullable=False),
|
||||
sa.Column('duration', sa.Interval(), nullable=False),
|
||||
sa.Column('pauses', sa.Interval(), nullable=True),
|
||||
sa.Column('moving', sa.Interval(), nullable=True),
|
||||
sa.Column('distance', sa.Numeric(precision=6, scale=3), nullable=True),
|
||||
sa.Column('min_alt', sa.Numeric(precision=6, scale=2), nullable=True),
|
||||
sa.Column('max_alt', sa.Numeric(precision=6, scale=2), nullable=True),
|
||||
sa.Column('descent', sa.Numeric(precision=7, scale=2), nullable=True),
|
||||
sa.Column('ascent', sa.Numeric(precision=7, scale=2), nullable=True),
|
||||
sa.Column('max_speed', sa.Numeric(precision=6, scale=2), nullable=True),
|
||||
sa.Column('ave_speed', sa.Numeric(precision=6, scale=2), nullable=True),
|
||||
sa.ForeignKeyConstraint(['activity_id'], ['activities.id'], ),
|
||||
sa.PrimaryKeyConstraint('activity_id', 'segment_id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('activity_segments')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,28 @@
|
||||
"""add 'bounds' column to 'Activity' table
|
||||
|
||||
Revision ID: 92adde6ac0d0
|
||||
Revises: dd73d23a7a3d
|
||||
Create Date: 2018-05-14 14:25:04.889189
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '92adde6ac0d0'
|
||||
down_revision = 'dd73d23a7a3d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('activities', sa.Column('bounds', postgresql.ARRAY(sa.Float()), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('activities', 'bounds')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,30 @@
|
||||
"""add static map url to 'Activity' table
|
||||
|
||||
Revision ID: 5a42db64e872
|
||||
Revises: 92adde6ac0d0
|
||||
Create Date: 2018-05-30 10:52:33.433687
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5a42db64e872'
|
||||
down_revision = '92adde6ac0d0'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('activities', sa.Column('map', sa.String(length=255), nullable=True))
|
||||
op.create_unique_constraint(None, 'sports', ['img'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'sports', type_='unique')
|
||||
op.drop_column('activities', 'map')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,28 @@
|
||||
"""add static map id to 'Activity' table
|
||||
|
||||
Revision ID: 9f8c9c37da44
|
||||
Revises: 5a42db64e872
|
||||
Create Date: 2018-05-30 12:48:11.714627
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '9f8c9c37da44'
|
||||
down_revision = '5a42db64e872'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('activities', sa.Column('map_id', sa.String(length=50), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('activities', 'map_id')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,28 @@
|
||||
"""add 'timezone' to 'User' table
|
||||
|
||||
Revision ID: e82e5e9447de
|
||||
Revises: 9f8c9c37da44
|
||||
Create Date: 2018-06-08 16:01:52.684935
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e82e5e9447de'
|
||||
down_revision = '9f8c9c37da44'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('timezone', sa.String(length=50), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'timezone')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,30 @@
|
||||
"""add weather infos in 'Activity' table
|
||||
|
||||
Revision ID: 71093ac9ca44
|
||||
Revises: e82e5e9447de
|
||||
Create Date: 2018-06-13 15:29:11.715377
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '71093ac9ca44'
|
||||
down_revision = 'e82e5e9447de'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('activities', sa.Column('weather_end', sa.JSON(), nullable=True))
|
||||
op.add_column('activities', sa.Column('weather_start', sa.JSON(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('activities', 'weather_start')
|
||||
op.drop_column('activities', 'weather_end')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,28 @@
|
||||
"""Add 'notes' column to 'Activity' table
|
||||
|
||||
Revision ID: 096dd0b43beb
|
||||
Revises: 71093ac9ca44
|
||||
Create Date: 2018-06-13 17:56:20.359884
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '096dd0b43beb'
|
||||
down_revision = '71093ac9ca44'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('activities', sa.Column('notes', sa.String(length=500), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('activities', 'notes')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,28 @@
|
||||
"""add 'weekm' in 'User' table
|
||||
|
||||
Revision ID: 27425324c9e3
|
||||
Revises: 096dd0b43beb
|
||||
Create Date: 2019-08-31 15:25:09.412426
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '27425324c9e3'
|
||||
down_revision = '096dd0b43beb'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('users', sa.Column('weekm', sa.Boolean(create_constraint=50), nullable=True))
|
||||
op.execute("UPDATE users SET weekm = false")
|
||||
op.alter_column('users', 'weekm', nullable=False)
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'weekm')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,28 @@
|
||||
"""add 'language' to 'User' table
|
||||
|
||||
Revision ID: f69f1e413bde
|
||||
Revises: 27425324c9e3
|
||||
Create Date: 2019-09-16 13:18:39.198777
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f69f1e413bde'
|
||||
down_revision = '27425324c9e3'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('language', sa.String(length=50), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'language')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,36 @@
|
||||
"""replace 'is_default' with 'is_active' in 'Sports' table
|
||||
|
||||
Revision ID: 1345afe3b11d
|
||||
Revises: f69f1e413bde
|
||||
Create Date: 2019-09-22 17:57:00.595775
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1345afe3b11d'
|
||||
down_revision = 'f69f1e413bde'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('sports',
|
||||
sa.Column('is_active',
|
||||
sa.Boolean(create_constraint=50),
|
||||
nullable=True))
|
||||
op.execute("UPDATE sports SET is_active = true")
|
||||
op.alter_column('sports', 'is_active', nullable=False)
|
||||
op.drop_column('sports', 'is_default')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.add_column('sports',
|
||||
sa.Column('is_default',
|
||||
sa.Boolean(create_constraint=50),
|
||||
nullable=True))
|
||||
op.execute("UPDATE sports SET is_default = true")
|
||||
op.alter_column('sports', 'is_default', nullable=False)
|
||||
op.drop_column('sports', 'is_active')
|
@ -0,0 +1,35 @@
|
||||
"""add application config in database
|
||||
|
||||
Revision ID: 8a0aad4c838c
|
||||
Revises: 1345afe3b11d
|
||||
Create Date: 2019-11-13 13:14:20.147296
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8a0aad4c838c'
|
||||
down_revision = '1345afe3b11d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('app_config',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('max_users', sa.Integer(), nullable=False),
|
||||
sa.Column('gpx_limit_import', sa.Integer(), nullable=False),
|
||||
sa.Column('max_single_file_size', sa.Integer(), nullable=False),
|
||||
sa.Column('max_zip_file_size', sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('app_config')
|
||||
# ### end Alembic commands ###
|
101
fittrackee/server.py
Normal file
@ -0,0 +1,101 @@
|
||||
import shutil
|
||||
|
||||
from fittrackee import create_app, db
|
||||
from fittrackee.activities.models import Activity, Sport
|
||||
from fittrackee.activities.utils import update_activity
|
||||
from fittrackee.application.utils import (
|
||||
init_config,
|
||||
update_app_config_from_database,
|
||||
)
|
||||
from fittrackee.users.models import User
|
||||
from tqdm import tqdm
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
@app.cli.command()
|
||||
def drop_db():
|
||||
"""Empty database for dev environments."""
|
||||
db.engine.execute("DROP TABLE IF EXISTS alembic_version;")
|
||||
db.drop_all()
|
||||
db.session.commit()
|
||||
print('Database dropped.')
|
||||
shutil.rmtree(app.config['UPLOAD_FOLDER'], ignore_errors=True)
|
||||
print('Uploaded files deleted.')
|
||||
|
||||
|
||||
@app.cli.command()
|
||||
def initdata():
|
||||
"""Init the database."""
|
||||
admin = User(
|
||||
username='admin', email='admin@example.com', password='mpwoadmin'
|
||||
)
|
||||
admin.admin = True
|
||||
admin.timezone = 'Europe/Paris'
|
||||
db.session.add(admin)
|
||||
sport = Sport(label='Cycling (Sport)')
|
||||
sport.img = '/img/sports/cycling-sport.png'
|
||||
sport.is_default = True
|
||||
db.session.add(sport)
|
||||
sport = Sport(label='Cycling (Transport)')
|
||||
sport.img = '/img/sports/cycling-transport.png'
|
||||
sport.is_default = True
|
||||
db.session.add(sport)
|
||||
sport = Sport(label='Hiking')
|
||||
sport.img = '/img/sports/hiking.png'
|
||||
sport.is_default = True
|
||||
db.session.add(sport)
|
||||
sport = Sport(label='Mountain Biking')
|
||||
sport.img = '/img/sports/mountain-biking.png'
|
||||
sport.is_default = True
|
||||
db.session.add(sport)
|
||||
sport = Sport(label='Running')
|
||||
sport.img = '/img/sports/running.png'
|
||||
sport.is_default = True
|
||||
db.session.add(sport)
|
||||
sport = Sport(label='Walking')
|
||||
sport.img = '/img/sports/walking.png'
|
||||
sport.is_default = True
|
||||
db.session.add(sport)
|
||||
db.session.commit()
|
||||
# update app config
|
||||
_, db_app_config = init_config()
|
||||
update_app_config_from_database(app, db_app_config)
|
||||
|
||||
print('Initial data stored in database.')
|
||||
|
||||
|
||||
@app.cli.command()
|
||||
def recalculate():
|
||||
print("Starting activities data refresh")
|
||||
activities = (
|
||||
Activity.query.filter(Activity.gpx != None) # noqa
|
||||
.order_by(Activity.activity_date.asc()) # noqa
|
||||
.all()
|
||||
)
|
||||
if len(activities) == 0:
|
||||
print('➡️ no activities to upgrade.')
|
||||
return None
|
||||
pbar = tqdm(activities)
|
||||
for activity in pbar:
|
||||
update_activity(activity)
|
||||
pbar.set_postfix(activitiy_id=activity.id)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@app.cli.command('init-app-config')
|
||||
def init_app_config():
|
||||
"""Init application configuration."""
|
||||
print("Init application configuration")
|
||||
config_created, _ = init_config()
|
||||
if config_created:
|
||||
print("Creation done!")
|
||||
else:
|
||||
print(
|
||||
"Application configuration already existing in database. "
|
||||
"Please use web application to update it."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
11
fittrackee/tasks.py
Normal file
@ -0,0 +1,11 @@
|
||||
from fittrackee import dramatiq, email_service
|
||||
|
||||
|
||||
@dramatiq.actor(queue_name='fittrackee_emails')
|
||||
def reset_password_email(user, email_data):
|
||||
email_service.send(
|
||||
template='password_reset_request',
|
||||
lang=user['language'],
|
||||
recipient=user['email'],
|
||||
data=email_data,
|
||||
)
|
0
fittrackee/tests/__init__.py
Normal file
683
fittrackee/tests/conftest.py
Normal file
@ -0,0 +1,683 @@
|
||||
import datetime
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from fittrackee import create_app, db
|
||||
from fittrackee.activities.models import Activity, ActivitySegment, Sport
|
||||
from fittrackee.application.models import AppConfig
|
||||
from fittrackee.application.utils import update_app_config_from_database
|
||||
from fittrackee.users.models import User
|
||||
|
||||
os.environ['FLASK_ENV'] = 'testing'
|
||||
os.environ['APP_SETTINGS'] = 'fittrackee.config.TestingConfig'
|
||||
# to avoid resetting dev database during tests
|
||||
os.environ['DATABASE_URL'] = os.getenv('DATABASE_TEST_URL')
|
||||
|
||||
|
||||
def get_app_config(with_config=False):
|
||||
if with_config:
|
||||
config = AppConfig()
|
||||
config.gpx_limit_import = 10
|
||||
config.max_single_file_size = 1 * 1024 * 1024
|
||||
config.max_zip_file_size = 1 * 1024 * 1024 * 10
|
||||
config.max_users = 100
|
||||
db.session.add(config)
|
||||
db.session.commit()
|
||||
return config
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_app(with_config=False):
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
app_db_config = get_app_config(with_config)
|
||||
if app_db_config:
|
||||
update_app_config_from_database(app, app_db_config)
|
||||
yield app
|
||||
db.session.remove()
|
||||
db.drop_all()
|
||||
# close unused idle connections => avoid the following error:
|
||||
# FATAL: remaining connection slots are reserved for non-replication
|
||||
# superuser connections
|
||||
db.engine.dispose()
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(monkeypatch):
|
||||
monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025')
|
||||
if os.getenv('TILE_SERVER_URL'):
|
||||
monkeypatch.delenv('TILE_SERVER_URL')
|
||||
if os.getenv('MAP_ATTRIBUTION'):
|
||||
monkeypatch.delenv('MAP_ATTRIBUTION')
|
||||
yield from get_app(with_config=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_no_config():
|
||||
yield from get_app(with_config=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_ssl(monkeypatch):
|
||||
monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025?ssl=True')
|
||||
yield from get_app(with_config=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_tls(monkeypatch):
|
||||
monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025?tls=True')
|
||||
yield from get_app(with_config=True)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def app_config():
|
||||
config = AppConfig()
|
||||
config.gpx_limit_import = 10
|
||||
config.max_single_file_size = 1048576
|
||||
config.max_zip_file_size = 10485760
|
||||
config.max_users = 0
|
||||
db.session.add(config)
|
||||
db.session.commit()
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user_1():
|
||||
user = User(username='test', email='test@test.com', password='12345678')
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user_1_admin():
|
||||
admin = User(
|
||||
username='admin', email='admin@example.com', password='12345678'
|
||||
)
|
||||
admin.admin = True
|
||||
db.session.add(admin)
|
||||
db.session.commit()
|
||||
return admin
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user_1_full():
|
||||
user = User(username='test', email='test@test.com', password='12345678')
|
||||
user.first_name = 'John'
|
||||
user.last_name = 'Doe'
|
||||
user.bio = 'just a random guy'
|
||||
user.location = 'somewhere'
|
||||
user.language = 'en'
|
||||
user.timezone = 'America/New_York'
|
||||
user.birth_date = datetime.datetime.strptime('01/01/1980', '%d/%m/%Y')
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user_1_paris():
|
||||
user = User(username='test', email='test@test.com', password='12345678')
|
||||
user.timezone = 'Europe/Paris'
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user_2():
|
||||
user = User(username='toto', email='toto@toto.com', password='87654321')
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user_2_admin():
|
||||
user = User(username='toto', email='toto@toto.com', password='87654321')
|
||||
user.admin = True
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user_3():
|
||||
user = User(username='sam', email='sam@test.com', password='12345678')
|
||||
user.weekm = True
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def sport_1_cycling():
|
||||
sport = Sport(label='Cycling')
|
||||
db.session.add(sport)
|
||||
db.session.commit()
|
||||
return sport
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def sport_1_cycling_inactive():
|
||||
sport = Sport(label='Cycling')
|
||||
sport.is_active = False
|
||||
db.session.add(sport)
|
||||
db.session.commit()
|
||||
return sport
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def sport_2_running():
|
||||
sport = Sport(label='Running')
|
||||
db.session.add(sport)
|
||||
db.session.commit()
|
||||
return sport
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def activity_cycling_user_1():
|
||||
activity = Activity(
|
||||
user_id=1,
|
||||
sport_id=1,
|
||||
activity_date=datetime.datetime.strptime('01/01/2018', '%d/%m/%Y'),
|
||||
distance=10,
|
||||
duration=datetime.timedelta(seconds=3600),
|
||||
)
|
||||
activity.max_speed = 10
|
||||
activity.ave_speed = 10
|
||||
activity.moving = activity.duration
|
||||
db.session.add(activity)
|
||||
db.session.commit()
|
||||
return activity
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def activity_cycling_user_1_segment():
|
||||
activity_segment = ActivitySegment(activity_id=1, segment_id=0)
|
||||
activity_segment.duration = datetime.timedelta(seconds=6000)
|
||||
activity_segment.moving = activity_segment.duration
|
||||
activity_segment.distance = 5
|
||||
db.session.add(activity_segment)
|
||||
db.session.commit()
|
||||
return activity_segment
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def activity_running_user_1():
|
||||
activity = Activity(
|
||||
user_id=1,
|
||||
sport_id=2,
|
||||
activity_date=datetime.datetime.strptime('01/04/2018', '%d/%m/%Y'),
|
||||
distance=12,
|
||||
duration=datetime.timedelta(seconds=6000),
|
||||
)
|
||||
activity.moving = activity.duration
|
||||
db.session.add(activity)
|
||||
db.session.commit()
|
||||
return activity
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def seven_activities_user_1():
|
||||
activity = Activity(
|
||||
user_id=1,
|
||||
sport_id=1,
|
||||
activity_date=datetime.datetime.strptime('20/03/2017', '%d/%m/%Y'),
|
||||
distance=5,
|
||||
duration=datetime.timedelta(seconds=1024),
|
||||
)
|
||||
activity.ave_speed = float(activity.distance) / (1024 / 3600)
|
||||
activity.moving = activity.duration
|
||||
db.session.add(activity)
|
||||
db.session.flush()
|
||||
activity = Activity(
|
||||
user_id=1,
|
||||
sport_id=1,
|
||||
activity_date=datetime.datetime.strptime('01/06/2017', '%d/%m/%Y'),
|
||||
distance=10,
|
||||
duration=datetime.timedelta(seconds=3456),
|
||||
)
|
||||
activity.ave_speed = float(activity.distance) / (3456 / 3600)
|
||||
activity.moving = activity.duration
|
||||
db.session.add(activity)
|
||||
db.session.flush()
|
||||
activity = Activity(
|
||||
user_id=1,
|
||||
sport_id=1,
|
||||
activity_date=datetime.datetime.strptime('01/01/2018', '%d/%m/%Y'),
|
||||
distance=10,
|
||||
duration=datetime.timedelta(seconds=1024),
|
||||
)
|
||||
activity.ave_speed = float(activity.distance) / (1024 / 3600)
|
||||
activity.moving = activity.duration
|
||||
db.session.add(activity)
|
||||
db.session.flush()
|
||||
activity = Activity(
|
||||
user_id=1,
|
||||
sport_id=1,
|
||||
activity_date=datetime.datetime.strptime('23/02/2018', '%d/%m/%Y'),
|
||||
distance=1,
|
||||
duration=datetime.timedelta(seconds=600),
|
||||
)
|
||||
activity.ave_speed = float(activity.distance) / (600 / 3600)
|
||||
activity.moving = activity.duration
|
||||
db.session.add(activity)
|
||||
db.session.flush()
|
||||
activity = Activity(
|
||||
user_id=1,
|
||||
sport_id=1,
|
||||
activity_date=datetime.datetime.strptime('23/02/2018', '%d/%m/%Y'),
|
||||
distance=10,
|
||||
duration=datetime.timedelta(seconds=1000),
|
||||
)
|
||||
activity.ave_speed = float(activity.distance) / (1000 / 3600)
|
||||
activity.moving = activity.duration
|
||||
db.session.add(activity)
|
||||
db.session.flush()
|
||||
activity = Activity(
|
||||
user_id=1,
|
||||
sport_id=1,
|
||||
activity_date=datetime.datetime.strptime('01/04/2018', '%d/%m/%Y'),
|
||||
distance=8,
|
||||
duration=datetime.timedelta(seconds=6000),
|
||||
)
|
||||
activity.ave_speed = float(activity.distance) / (6000 / 3600)
|
||||
activity.moving = activity.duration
|
||||
db.session.add(activity)
|
||||
db.session.flush()
|
||||
activity = Activity(
|
||||
user_id=1,
|
||||
sport_id=1,
|
||||
activity_date=datetime.datetime.strptime('09/05/2018', '%d/%m/%Y'),
|
||||
distance=10,
|
||||
duration=datetime.timedelta(seconds=3000),
|
||||
)
|
||||
activity.ave_speed = float(activity.distance) / (3000 / 3600)
|
||||
activity.moving = activity.duration
|
||||
db.session.add(activity)
|
||||
db.session.commit()
|
||||
return activity
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def activity_cycling_user_2():
|
||||
activity = Activity(
|
||||
user_id=2,
|
||||
sport_id=1,
|
||||
activity_date=datetime.datetime.strptime('23/01/2018', '%d/%m/%Y'),
|
||||
distance=15,
|
||||
duration=datetime.timedelta(seconds=3600),
|
||||
)
|
||||
activity.moving = activity.duration
|
||||
db.session.add(activity)
|
||||
db.session.commit()
|
||||
return activity
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def gpx_file():
|
||||
return (
|
||||
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
|
||||
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
|
||||
' <metadata/>'
|
||||
' <trk>'
|
||||
' <name>just an activity</name>'
|
||||
' <trkseg>'
|
||||
' <trkpt lat="44.68095" lon="6.07367">'
|
||||
' <ele>998</ele>'
|
||||
' <time>2018-03-13T12:44:45Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68091" lon="6.07367">'
|
||||
' <ele>998</ele>'
|
||||
' <time>2018-03-13T12:44:50Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.6808" lon="6.07364">'
|
||||
' <ele>994</ele>'
|
||||
' <time>2018-03-13T12:45:00Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68075" lon="6.07364">'
|
||||
' <ele>994</ele>'
|
||||
' <time>2018-03-13T12:45:05Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68071" lon="6.07364">'
|
||||
' <ele>994</ele>'
|
||||
' <time>2018-03-13T12:45:10Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68049" lon="6.07361">'
|
||||
' <ele>993</ele>'
|
||||
' <time>2018-03-13T12:45:30Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68019" lon="6.07356">'
|
||||
' <ele>992</ele>'
|
||||
' <time>2018-03-13T12:45:55Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68014" lon="6.07355">'
|
||||
' <ele>992</ele>'
|
||||
' <time>2018-03-13T12:46:00Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67995" lon="6.07358">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T12:46:15Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67977" lon="6.07364">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T12:46:30Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67972" lon="6.07367">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T12:46:35Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67966" lon="6.07368">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T12:46:40Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67961" lon="6.0737">'
|
||||
' <ele>986</ele>'
|
||||
' <time>2018-03-13T12:46:45Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67938" lon="6.07377">'
|
||||
' <ele>986</ele>'
|
||||
' <time>2018-03-13T12:47:05Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67933" lon="6.07381">'
|
||||
' <ele>986</ele>'
|
||||
' <time>2018-03-13T12:47:10Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67922" lon="6.07385">'
|
||||
' <ele>985</ele>'
|
||||
' <time>2018-03-13T12:47:20Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67911" lon="6.0739">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T12:47:30Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.679" lon="6.07399">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T12:47:40Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67896" lon="6.07402">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T12:47:45Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67884" lon="6.07408">'
|
||||
' <ele>979</ele>'
|
||||
' <time>2018-03-13T12:47:55Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67863" lon="6.07423">'
|
||||
' <ele>981</ele>'
|
||||
' <time>2018-03-13T12:48:15Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67858" lon="6.07425">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T12:48:20Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67842" lon="6.07434">'
|
||||
' <ele>979</ele>'
|
||||
' <time>2018-03-13T12:48:35Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67837" lon="6.07435">'
|
||||
' <ele>979</ele>'
|
||||
' <time>2018-03-13T12:48:40Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67822" lon="6.07442">'
|
||||
' <ele>975</ele>'
|
||||
' <time>2018-03-13T12:48:55Z</time>'
|
||||
' </trkpt>'
|
||||
' </trkseg>'
|
||||
' </trk>'
|
||||
'</gpx>'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def gpx_file_wo_name():
|
||||
return (
|
||||
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
|
||||
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
|
||||
' <metadata/>'
|
||||
' <trk>'
|
||||
' <trkseg>'
|
||||
' <trkpt lat="44.68095" lon="6.07367">'
|
||||
' <ele>998</ele>'
|
||||
' <time>2018-03-13T12:44:45Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68091" lon="6.07367">'
|
||||
' <ele>998</ele>'
|
||||
' <time>2018-03-13T12:44:50Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.6808" lon="6.07364">'
|
||||
' <ele>994</ele>'
|
||||
' <time>2018-03-13T12:45:00Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68075" lon="6.07364">'
|
||||
' <ele>994</ele>'
|
||||
' <time>2018-03-13T12:45:05Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68071" lon="6.07364">'
|
||||
' <ele>994</ele>'
|
||||
' <time>2018-03-13T12:45:10Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68049" lon="6.07361">'
|
||||
' <ele>993</ele>'
|
||||
' <time>2018-03-13T12:45:30Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68019" lon="6.07356">'
|
||||
' <ele>992</ele>'
|
||||
' <time>2018-03-13T12:45:55Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68014" lon="6.07355">'
|
||||
' <ele>992</ele>'
|
||||
' <time>2018-03-13T12:46:00Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67995" lon="6.07358">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T12:46:15Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67977" lon="6.07364">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T12:46:30Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67972" lon="6.07367">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T12:46:35Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67966" lon="6.07368">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T12:46:40Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67961" lon="6.0737">'
|
||||
' <ele>986</ele>'
|
||||
' <time>2018-03-13T12:46:45Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67938" lon="6.07377">'
|
||||
' <ele>986</ele>'
|
||||
' <time>2018-03-13T12:47:05Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67933" lon="6.07381">'
|
||||
' <ele>986</ele>'
|
||||
' <time>2018-03-13T12:47:10Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67922" lon="6.07385">'
|
||||
' <ele>985</ele>'
|
||||
' <time>2018-03-13T12:47:20Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67911" lon="6.0739">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T12:47:30Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.679" lon="6.07399">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T12:47:40Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67896" lon="6.07402">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T12:47:45Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67884" lon="6.07408">'
|
||||
' <ele>979</ele>'
|
||||
' <time>2018-03-13T12:47:55Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67863" lon="6.07423">'
|
||||
' <ele>981</ele>'
|
||||
' <time>2018-03-13T12:48:15Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67858" lon="6.07425">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T12:48:20Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67842" lon="6.07434">'
|
||||
' <ele>979</ele>'
|
||||
' <time>2018-03-13T12:48:35Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67837" lon="6.07435">'
|
||||
' <ele>979</ele>'
|
||||
' <time>2018-03-13T12:48:40Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67822" lon="6.07442">'
|
||||
' <ele>975</ele>'
|
||||
' <time>2018-03-13T12:48:55Z</time>'
|
||||
' </trkpt>'
|
||||
' </trkseg>'
|
||||
' </trk>'
|
||||
'</gpx>'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def gpx_file_wo_track():
|
||||
return (
|
||||
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
|
||||
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
|
||||
' <metadata/>'
|
||||
'</gpx>'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def gpx_file_invalid_xml():
|
||||
return (
|
||||
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
|
||||
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
|
||||
' <metadata/>'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def gpx_file_with_segments():
|
||||
return (
|
||||
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
|
||||
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
|
||||
' <metadata/>'
|
||||
' <trk>'
|
||||
' <name>just an activity</name>'
|
||||
' <trkseg>'
|
||||
' <trkpt lat="44.68095" lon="6.07367">'
|
||||
' <ele>998</ele>'
|
||||
' <time>2018-03-13T12:44:45Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68091" lon="6.07367">'
|
||||
' <ele>998</ele>'
|
||||
' <time>2018-03-13T12:44:50Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.6808" lon="6.07364">'
|
||||
' <ele>994</ele>'
|
||||
' <time>2018-03-13T12:45:00Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68075" lon="6.07364">'
|
||||
' <ele>994</ele>'
|
||||
' <time>2018-03-13T12:45:05Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68071" lon="6.07364">'
|
||||
' <ele>994</ele>'
|
||||
' <time>2018-03-13T12:45:10Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68049" lon="6.07361">'
|
||||
' <ele>993</ele>'
|
||||
' <time>2018-03-13T12:45:30Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68019" lon="6.07356">'
|
||||
' <ele>992</ele>'
|
||||
' <time>2018-03-13T12:45:55Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.68014" lon="6.07355">'
|
||||
' <ele>992</ele>'
|
||||
' <time>2018-03-13T12:46:00Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67995" lon="6.07358">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T12:46:15Z</time>'
|
||||
' </trkpt>'
|
||||
' </trkseg>'
|
||||
' <trkseg>'
|
||||
' <trkpt lat="44.67977" lon="6.07364">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T12:46:30Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67972" lon="6.07367">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T12:46:35Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67966" lon="6.07368">'
|
||||
' <ele>987</ele>'
|
||||
' <time>2018-03-13T12:46:40Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67961" lon="6.0737">'
|
||||
' <ele>986</ele>'
|
||||
' <time>2018-03-13T12:46:45Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67938" lon="6.07377">'
|
||||
' <ele>986</ele>'
|
||||
' <time>2018-03-13T12:47:05Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67933" lon="6.07381">'
|
||||
' <ele>986</ele>'
|
||||
' <time>2018-03-13T12:47:10Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67922" lon="6.07385">'
|
||||
' <ele>985</ele>'
|
||||
' <time>2018-03-13T12:47:20Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67911" lon="6.0739">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T12:47:30Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.679" lon="6.07399">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T12:47:40Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67896" lon="6.07402">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T12:47:45Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67884" lon="6.07408">'
|
||||
' <ele>979</ele>'
|
||||
' <time>2018-03-13T12:47:55Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67863" lon="6.07423">'
|
||||
' <ele>981</ele>'
|
||||
' <time>2018-03-13T12:48:15Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67858" lon="6.07425">'
|
||||
' <ele>980</ele>'
|
||||
' <time>2018-03-13T12:48:20Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67842" lon="6.07434">'
|
||||
' <ele>979</ele>'
|
||||
' <time>2018-03-13T12:48:35Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67837" lon="6.07435">'
|
||||
' <ele>979</ele>'
|
||||
' <time>2018-03-13T12:48:40Z</time>'
|
||||
' </trkpt>'
|
||||
' <trkpt lat="44.67822" lon="6.07442">'
|
||||
' <ele>975</ele>'
|
||||
' <time>2018-03-13T12:48:55Z</time>'
|
||||
' </trkpt>'
|
||||
' </trkseg>'
|
||||
' </trk>'
|
||||
'</gpx>'
|
||||
)
|
BIN
fittrackee/tests/files/gpx_test.zip
Normal file
BIN
fittrackee/tests/files/gpx_test_folder.zip
Normal file
BIN
fittrackee/tests/files/gpx_test_incorrect.zip
Normal file
0
fittrackee/tests/template_results/__init__.py
Normal file
174
fittrackee/tests/template_results/password_reset_request.py
Normal file
@ -0,0 +1,174 @@
|
||||
# flake8: noqa
|
||||
|
||||
expected_en_text_body = """Hi test,
|
||||
|
||||
You recently requested to reset your password for your FitTrackee account. Use the button below to reset it. This password reset link is only valid for 3 seconds.
|
||||
|
||||
Reset your password ( http://localhost/password-reset?token=xxx )
|
||||
|
||||
For security, this request was received from a Linux device using Firefox. If you did not request a password reset, please ignore this email.
|
||||
|
||||
Thanks,
|
||||
The FitTrackee Team"""
|
||||
|
||||
expected_fr_text_body = """Bonjour test,
|
||||
|
||||
Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
|
||||
Cliquez sur le lien ci-dessous pour le réinitialiser. Ce lien n'est valide que pendant 3 secondes.
|
||||
|
||||
Réinitialiser le mot de passe: ( http://localhost/password-reset?token=xxx )
|
||||
|
||||
Pour vérification, cette demande a été reçue à partir d'un appareil sous Linux, utilisant le navigateur Firefox.
|
||||
Si vous n'avez pas demandé de réinitalisation, vous pouvez ignorer cet e-mail.
|
||||
|
||||
Merci,
|
||||
L'équipe FitTrackee"""
|
||||
|
||||
expected_en_html_body = """ <body>
|
||||
<span class="preheader">Use this link to reset your password. The link is only valid for 3 seconds.</span>
|
||||
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="email-masthead">
|
||||
<a href="https://example.com" class="f-fallback email-masthead-name">
|
||||
FitTrackee
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
|
||||
<table class="email-body-inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="content-cell">
|
||||
<div class="f-fallback">
|
||||
<h1>Hi test,</h1>
|
||||
<p>You recently requested to reset your password for your account. Use the button below to reset it.
|
||||
<strong>This password reset link is only valid for 3 seconds.</strong>
|
||||
</p>
|
||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="http://localhost/password-reset?token=xxx" class="f-fallback button button--green" target="_blank">Reset your password</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>
|
||||
For security, this request was received from a Linux device using Firefox.
|
||||
If you did not request a password reset, please ignore this email.
|
||||
</p>
|
||||
<p>Thanks,
|
||||
<br>The FitTrackee Team</p>
|
||||
<table class="body-sub" role="presentation">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="f-fallback sub">If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
|
||||
<p class="f-fallback sub">http://localhost/password-reset?token=xxx</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="content-cell" align="center">
|
||||
<p class="f-fallback sub align-center">© FitTrackee.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
expected_fr_html_body = """ <body>
|
||||
<span class="preheader">Utiliser ce lien pour réinitialiser le mot de passe. Ce lien n'est valide que pendant 3 secondes.</span>
|
||||
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="email-masthead">
|
||||
<a href="https://example.com" class="f-fallback email-masthead-name">
|
||||
FitTrackee
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
|
||||
<table class="email-body-inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="content-cell">
|
||||
<div class="f-fallback">
|
||||
<h1>Bonjour test,</h1>
|
||||
<p>Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
|
||||
Cliquez sur le bouton ci-dessous pour le réinitialiser.
|
||||
<strong>Cette réinitialisation n'est valide que pendant 3 secondes.</strong>
|
||||
</p>
|
||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="http://localhost/password-reset?token=xxx" class="f-fallback button button--green" target="_blank">Réinitialiser le mot de passe</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>
|
||||
Pour vérification, cette demande a été reçue à partir d'un appareil sous Linux, utilisant le navigateur Firefox.
|
||||
Si vous n'avez pas demandé de réinitalisation, vous pouvez ignorer cet e-mail.
|
||||
</p>
|
||||
<p>Merci,
|
||||
<br>L'équipe FitTrackee</p>
|
||||
<table class="body-sub" role="presentation">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="f-fallback sub">Si vous avez des problèmes avec le bouton, vous pouvez copier et coller le lien suivant dans votre navigateur</p>
|
||||
<p class="f-fallback sub">http://localhost/password-reset?token=xxx</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="content-cell" align="center">
|
||||
<p class="f-fallback sub align-center">© FitTrackee.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>"""
|
924
fittrackee/tests/test_activities_api_0_get.py
Normal file
@ -0,0 +1,924 @@
|
||||
import json
|
||||
|
||||
|
||||
class TestGetActivities:
|
||||
def test_it_gets_all_activities_for_authenticated_user(
|
||||
self,
|
||||
app,
|
||||
user_1,
|
||||
user_2,
|
||||
sport_1_cycling,
|
||||
sport_2_running,
|
||||
activity_cycling_user_1,
|
||||
activity_cycling_user_2,
|
||||
activity_running_user_1,
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 2
|
||||
assert 'creation_date' in data['data']['activities'][0]
|
||||
assert (
|
||||
'Sun, 01 Apr 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert 'test' == data['data']['activities'][0]['user']
|
||||
assert 2 == data['data']['activities'][0]['sport_id']
|
||||
assert 12.0 == data['data']['activities'][0]['distance']
|
||||
assert '1:40:00' == data['data']['activities'][0]['duration']
|
||||
|
||||
assert 'creation_date' in data['data']['activities'][1]
|
||||
assert (
|
||||
'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][1]['activity_date']
|
||||
)
|
||||
assert 'test' == data['data']['activities'][1]['user']
|
||||
assert 1 == data['data']['activities'][1]['sport_id']
|
||||
assert 10.0 == data['data']['activities'][1]['distance']
|
||||
assert '1:00:00' == data['data']['activities'][1]['duration']
|
||||
|
||||
def test_it_gets_no_activities_for_authenticated_user_with_no_activities(
|
||||
self,
|
||||
app,
|
||||
user_1,
|
||||
user_2,
|
||||
sport_1_cycling,
|
||||
sport_2_running,
|
||||
activity_cycling_user_1,
|
||||
activity_running_user_1,
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='toto@toto.com', password='87654321')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 0
|
||||
|
||||
def test_it_returns_401_if_user_is_not_authenticated(self, app):
|
||||
client = app.test_client()
|
||||
|
||||
response = client.get('/api/activities')
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 401
|
||||
assert 'error' in data['status']
|
||||
assert 'Provide a valid auth token.' in data['message']
|
||||
|
||||
|
||||
class TestGetActivitiesWithPagination:
|
||||
def test_it_gets_activities_with_default_pagination(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 5
|
||||
assert 'creation_date' in data['data']['activities'][0]
|
||||
assert (
|
||||
'Wed, 09 May 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert '0:50:00' == data['data']['activities'][0]['duration']
|
||||
assert 'creation_date' in data['data']['activities'][4]
|
||||
assert (
|
||||
'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][4]['activity_date']
|
||||
)
|
||||
assert '0:17:04' == data['data']['activities'][4]['duration']
|
||||
|
||||
def test_it_gets_first_page(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?page=1',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 5
|
||||
assert 'creation_date' in data['data']['activities'][0]
|
||||
assert (
|
||||
'Wed, 09 May 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert '0:50:00' == data['data']['activities'][0]['duration']
|
||||
assert 'creation_date' in data['data']['activities'][4]
|
||||
assert (
|
||||
'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][4]['activity_date']
|
||||
)
|
||||
assert '0:17:04' == data['data']['activities'][4]['duration']
|
||||
|
||||
def test_it_gets_second_page(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?page=2',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 2
|
||||
assert 'creation_date' in data['data']['activities'][0]
|
||||
assert (
|
||||
'Thu, 01 Jun 2017 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert '0:57:36' == data['data']['activities'][0]['duration']
|
||||
assert 'creation_date' in data['data']['activities'][1]
|
||||
assert (
|
||||
'Mon, 20 Mar 2017 00:00:00 GMT'
|
||||
== data['data']['activities'][1]['activity_date']
|
||||
)
|
||||
assert '0:17:04' == data['data']['activities'][1]['duration']
|
||||
|
||||
def test_it_gets_empty_third_page(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?page=3',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 0
|
||||
|
||||
def test_it_returns_error_on_invalid_page_value(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?page=A',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 500
|
||||
assert 'error' in data['status']
|
||||
assert (
|
||||
'Error. Please try again or contact the administrator.'
|
||||
in data['message']
|
||||
)
|
||||
|
||||
def test_it_gets_5_activities_per_page(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?per_page=10',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 7
|
||||
assert (
|
||||
'Wed, 09 May 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert (
|
||||
'Mon, 20 Mar 2017 00:00:00 GMT'
|
||||
== data['data']['activities'][6]['activity_date']
|
||||
)
|
||||
|
||||
def test_it_gets_3_activities_per_page(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?per_page=3',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 3
|
||||
assert (
|
||||
'Wed, 09 May 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert (
|
||||
'Fri, 23 Feb 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][2]['activity_date']
|
||||
)
|
||||
|
||||
|
||||
class TestGetActivitiesWithFilters:
|
||||
def test_it_gets_activities_with_date_filter(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?from=2018-02-01&to=2018-02-28',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 2
|
||||
assert 'creation_date' in data['data']['activities'][0]
|
||||
assert (
|
||||
'Fri, 23 Feb 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert '0:10:00' == data['data']['activities'][0]['duration']
|
||||
assert 'creation_date' in data['data']['activities'][1]
|
||||
assert (
|
||||
'Fri, 23 Feb 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][1]['activity_date']
|
||||
)
|
||||
assert '0:16:40' == data['data']['activities'][1]['duration']
|
||||
|
||||
def test_it_gets_no_activities_with_date_filter(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?from=2018-03-01&to=2018-03-30',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 0
|
||||
|
||||
def test_if_gets_activities_with_date_filter_from(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?from=2018-04-01',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 2
|
||||
assert 'creation_date' in data['data']['activities'][0]
|
||||
assert (
|
||||
'Wed, 09 May 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert (
|
||||
'Sun, 01 Apr 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][1]['activity_date']
|
||||
)
|
||||
|
||||
def test_it_gets_activities_with_date_filter_to(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?to=2017-12-31',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 2
|
||||
assert (
|
||||
'Thu, 01 Jun 2017 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert (
|
||||
'Mon, 20 Mar 2017 00:00:00 GMT'
|
||||
== data['data']['activities'][1]['activity_date']
|
||||
)
|
||||
|
||||
def test_it_gets_activities_with_ascending_order(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?order=asc',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 5
|
||||
assert (
|
||||
'Mon, 20 Mar 2017 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert (
|
||||
'Fri, 23 Feb 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][4]['activity_date']
|
||||
)
|
||||
|
||||
def test_it_gets_activities_with_distance_filter(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?distance_from=5&distance_to=8',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 2
|
||||
assert (
|
||||
'Sun, 01 Apr 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert (
|
||||
'Mon, 20 Mar 2017 00:00:00 GMT'
|
||||
== data['data']['activities'][1]['activity_date']
|
||||
)
|
||||
|
||||
def test_it_gets_activities_with_duration_filter(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?duration_from=00:52&duration_to=01:20',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 1
|
||||
assert (
|
||||
'Thu, 01 Jun 2017 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
|
||||
def test_it_gets_activities_with_average_speed_filter(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?ave_speed_from=5&ave_speed_to=10',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 1
|
||||
assert (
|
||||
'Fri, 23 Feb 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
|
||||
def test_it_gets_activities_with_max_speed_filter(
|
||||
self,
|
||||
app,
|
||||
user_1,
|
||||
sport_1_cycling,
|
||||
sport_2_running,
|
||||
activity_cycling_user_1,
|
||||
activity_running_user_1,
|
||||
):
|
||||
activity_cycling_user_1.max_speed = 25
|
||||
activity_running_user_1.max_speed = 11
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?max_speed_from=10&max_speed_to=20',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 1
|
||||
assert (
|
||||
'Sun, 01 Apr 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
|
||||
def test_it_gets_activities_with_sport_filter(
|
||||
self,
|
||||
app,
|
||||
user_1,
|
||||
sport_1_cycling,
|
||||
seven_activities_user_1,
|
||||
sport_2_running,
|
||||
activity_running_user_1,
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?sport_id=2',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 1
|
||||
assert (
|
||||
'Sun, 01 Apr 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
|
||||
|
||||
class TestGetActivitiesWithFiltersAndPagination:
|
||||
def test_it_gets_page_2_with_date_filter(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?from=2017-01-01&page=2',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 2
|
||||
assert (
|
||||
'Thu, 01 Jun 2017 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert (
|
||||
'Mon, 20 Mar 2017 00:00:00 GMT'
|
||||
== data['data']['activities'][1]['activity_date']
|
||||
)
|
||||
|
||||
def test_it_get_page_2_with_date_filter_and_ascending_order(
|
||||
self, app, user_1, sport_1_cycling, seven_activities_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities?from=2017-01-01&page=2&order=asc',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 2
|
||||
assert (
|
||||
'Sun, 01 Apr 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert (
|
||||
'Wed, 09 May 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][1]['activity_date']
|
||||
)
|
||||
|
||||
|
||||
class TestGetActivity:
|
||||
def test_it_gets_an_activity(
|
||||
self, app, user_1, sport_1_cycling, activity_cycling_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities/1',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 1
|
||||
assert 'creation_date' in data['data']['activities'][0]
|
||||
assert (
|
||||
'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert 'test' == data['data']['activities'][0]['user']
|
||||
assert 1 == data['data']['activities'][0]['sport_id']
|
||||
assert 10.0 == data['data']['activities'][0]['distance']
|
||||
assert '1:00:00' == data['data']['activities'][0]['duration']
|
||||
|
||||
def test_it_returns_403_if_activity_belongs_to_a_different_user(
|
||||
self, app, user_1, user_2, sport_1_cycling, activity_cycling_user_2
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities/1',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 403
|
||||
assert 'error' in data['status']
|
||||
assert 'You do not have permissions.' in data['message']
|
||||
|
||||
def test_it_returns_404_if_activity_does_not_exist(self, app, user_1):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities/11',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 404
|
||||
assert 'not found' in data['status']
|
||||
assert len(data['data']['activities']) == 0
|
||||
|
||||
def test_it_returns_404_on_getting_gpx_if_activity_does_not_exist(
|
||||
self, app, user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities/11/gpx',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 404
|
||||
assert 'not found' in data['status']
|
||||
assert 'Activity not found (id: 11)' in data['message']
|
||||
assert data['data']['gpx'] == ''
|
||||
|
||||
def test_it_returns_404_on_getting_chart_data_if_activity_does_not_exist(
|
||||
self, app, user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities/11/chart_data',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 404
|
||||
assert 'not found' in data['status']
|
||||
assert 'Activity not found (id: 11)' in data['message']
|
||||
assert data['data']['chart_data'] == ''
|
||||
|
||||
def test_it_returns_404_on_getting_gpx_if_activity_have_no_gpx(
|
||||
self, app, user_1, sport_1_cycling, activity_cycling_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities/1/gpx',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 404
|
||||
assert 'error' in data['status']
|
||||
assert 'No gpx file for this activity (id: 1)' in data['message']
|
||||
|
||||
def test_it_returns_404_if_activity_have_no_chart_data(
|
||||
self, app, user_1, sport_1_cycling, activity_cycling_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities/1/chart_data',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 404
|
||||
assert 'error' in data['status']
|
||||
assert 'No gpx file for this activity (id: 1)' in data['message']
|
||||
|
||||
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
|
||||
):
|
||||
activity_cycling_user_1.gpx = "some path"
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities/1/gpx',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 500
|
||||
assert 'error' in data['status']
|
||||
assert 'internal error' in data['message']
|
||||
assert 'data' not in data
|
||||
|
||||
def test_it_returns_500_on_getting_chart_data_if_an_activity_has_invalid_gpx_pathname( # noqa
|
||||
self, app, user_1, sport_1_cycling, activity_cycling_user_1
|
||||
):
|
||||
activity_cycling_user_1.gpx = "some path"
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/activities/1/chart_data',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 500
|
||||
assert 'error' in data['status']
|
||||
assert 'internal error' in data['message']
|
||||
assert 'data' not in data
|
||||
|
||||
def test_it_returns_404_if_activity_has_no_map(self, app, user_1):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.get(
|
||||
'/api/activities/map/123',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 404
|
||||
assert 'error' in data['status']
|
||||
assert 'Map does not exist' in data['message']
|
1322
fittrackee/tests/test_activities_api_1_post.py
Normal file
768
fittrackee/tests/test_activities_api_2_patch.py
Normal file
@ -0,0 +1,768 @@
|
||||
import json
|
||||
from io import BytesIO
|
||||
|
||||
from fittrackee.activities.models import Activity
|
||||
|
||||
|
||||
def assert_activity_data_with_gpx(data):
|
||||
assert 'creation_date' in data['data']['activities'][0]
|
||||
assert (
|
||||
'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
== data['data']['activities'][0]['activity_date']
|
||||
)
|
||||
assert 'test' == data['data']['activities'][0]['user']
|
||||
assert '0:04:10' == data['data']['activities'][0]['duration']
|
||||
assert data['data']['activities'][0]['ascent'] == 0.4
|
||||
assert data['data']['activities'][0]['ave_speed'] == 4.61
|
||||
assert data['data']['activities'][0]['descent'] == 23.4
|
||||
assert data['data']['activities'][0]['distance'] == 0.32
|
||||
assert data['data']['activities'][0]['max_alt'] == 998.0
|
||||
assert data['data']['activities'][0]['max_speed'] == 5.12
|
||||
assert data['data']['activities'][0]['min_alt'] == 975.0
|
||||
assert data['data']['activities'][0]['moving'] == '0:04:10'
|
||||
assert data['data']['activities'][0]['pauses'] is None
|
||||
assert data['data']['activities'][0]['with_gpx'] is True
|
||||
|
||||
records = data['data']['activities'][0]['records']
|
||||
assert len(records) == 4
|
||||
assert records[0]['sport_id'] == 2
|
||||
assert records[0]['activity_id'] == 1
|
||||
assert records[0]['record_type'] == 'MS'
|
||||
assert records[0]['activity_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[0]['value'] == 5.12
|
||||
assert records[1]['sport_id'] == 2
|
||||
assert records[1]['activity_id'] == 1
|
||||
assert records[1]['record_type'] == 'LD'
|
||||
assert records[1]['activity_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[1]['value'] == '0:04:10'
|
||||
assert records[2]['sport_id'] == 2
|
||||
assert records[2]['activity_id'] == 1
|
||||
assert records[2]['record_type'] == 'FD'
|
||||
assert records[2]['activity_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[2]['value'] == 0.32
|
||||
assert records[3]['sport_id'] == 2
|
||||
assert records[3]['activity_id'] == 1
|
||||
assert records[3]['record_type'] == 'AS'
|
||||
assert records[3]['activity_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||
assert records[3]['value'] == 4.61
|
||||
|
||||
|
||||
class TestEditActivityWithGpx:
|
||||
def test_it_updates_title_for_an_activity_with_gpx(
|
||||
self, app, user_1, sport_1_cycling, sport_2_running, gpx_file
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
client.post(
|
||||
'/api/activities',
|
||||
data=dict(
|
||||
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token'],
|
||||
),
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(sport_id=2, title="Activity test")),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 1
|
||||
assert 2 == data['data']['activities'][0]['sport_id']
|
||||
assert data['data']['activities'][0]['title'] == 'Activity test'
|
||||
assert_activity_data_with_gpx(data)
|
||||
|
||||
def test_it_adds_notes_for_an_activity_with_gpx(
|
||||
self, app, user_1, sport_1_cycling, sport_2_running, gpx_file
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
client.post(
|
||||
'/api/activities',
|
||||
data=dict(
|
||||
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token'],
|
||||
),
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(notes="test notes")),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 1
|
||||
assert data['data']['activities'][0]['title'] == 'just an activity'
|
||||
assert data['data']['activities'][0]['notes'] == 'test notes'
|
||||
|
||||
def test_it_raises_403_when_editing_an_activity_from_different_user(
|
||||
self, app, user_1, user_2, sport_1_cycling, sport_2_running, gpx_file
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
client.post(
|
||||
'/api/activities',
|
||||
data=dict(
|
||||
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token'],
|
||||
),
|
||||
)
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='toto@toto.com', password='87654321')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(sport_id=2, title="Activity test")),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 403
|
||||
assert 'error' in data['status']
|
||||
assert 'You do not have permissions.' in data['message']
|
||||
|
||||
def test_it_updates_sport(
|
||||
self, app, user_1, sport_1_cycling, sport_2_running, gpx_file
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
client.post(
|
||||
'/api/activities',
|
||||
data=dict(
|
||||
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token'],
|
||||
),
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(sport_id=2)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 1
|
||||
assert 2 == data['data']['activities'][0]['sport_id']
|
||||
assert data['data']['activities'][0]['title'] == 'just an activity'
|
||||
assert_activity_data_with_gpx(data)
|
||||
|
||||
def test_it_returns_400_if_payload_is_empty(
|
||||
self, app, user_1, sport_1_cycling, gpx_file
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
client.post(
|
||||
'/api/activities',
|
||||
data=dict(
|
||||
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token'],
|
||||
),
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict()),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 400
|
||||
assert 'error' in data['status']
|
||||
assert 'Invalid payload.' in data['message']
|
||||
|
||||
def test_it_raises_500_if_sport_does_not_exists(
|
||||
self, app, user_1, sport_1_cycling, gpx_file
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
client.post(
|
||||
'/api/activities',
|
||||
data=dict(
|
||||
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token'],
|
||||
),
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(sport_id=2)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 500
|
||||
assert 'error' in data['status']
|
||||
assert (
|
||||
'Error. Please try again or contact the administrator.'
|
||||
in data['message']
|
||||
)
|
||||
|
||||
|
||||
class TestEditActivityWithoutGpx:
|
||||
def test_it_updates_an_activity_wo_gpx(
|
||||
self,
|
||||
app,
|
||||
user_1,
|
||||
sport_1_cycling,
|
||||
sport_2_running,
|
||||
activity_cycling_user_1,
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
sport_id=2,
|
||||
duration=3600,
|
||||
activity_date='2018-05-15 15:05',
|
||||
distance=8,
|
||||
title='Activity test',
|
||||
)
|
||||
),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 1
|
||||
assert 'creation_date' in data['data']['activities'][0]
|
||||
assert (
|
||||
data['data']['activities'][0]['activity_date']
|
||||
== 'Tue, 15 May 2018 15:05:00 GMT'
|
||||
)
|
||||
assert data['data']['activities'][0]['user'] == 'test'
|
||||
assert data['data']['activities'][0]['sport_id'] == 2
|
||||
assert data['data']['activities'][0]['duration'] == '1:00:00'
|
||||
assert data['data']['activities'][0]['title'] == 'Activity test'
|
||||
assert data['data']['activities'][0]['ascent'] is None
|
||||
assert data['data']['activities'][0]['ave_speed'] == 8.0
|
||||
assert data['data']['activities'][0]['descent'] is None
|
||||
assert data['data']['activities'][0]['distance'] == 8.0
|
||||
assert data['data']['activities'][0]['max_alt'] is None
|
||||
assert data['data']['activities'][0]['max_speed'] == 8.0
|
||||
assert data['data']['activities'][0]['min_alt'] is None
|
||||
assert data['data']['activities'][0]['moving'] == '1:00:00'
|
||||
assert data['data']['activities'][0]['pauses'] is None
|
||||
assert data['data']['activities'][0]['with_gpx'] is False
|
||||
assert data['data']['activities'][0]['map'] is None
|
||||
assert data['data']['activities'][0]['weather_start'] is None
|
||||
assert data['data']['activities'][0]['weather_end'] is None
|
||||
assert data['data']['activities'][0]['notes'] is None
|
||||
|
||||
records = data['data']['activities'][0]['records']
|
||||
assert len(records) == 4
|
||||
assert records[0]['sport_id'] == 2
|
||||
assert records[0]['activity_id'] == 1
|
||||
assert records[0]['record_type'] == 'MS'
|
||||
assert records[0]['activity_date'] == 'Tue, 15 May 2018 15:05:00 GMT'
|
||||
assert records[0]['value'] == 8.0
|
||||
assert records[1]['sport_id'] == 2
|
||||
assert records[1]['activity_id'] == 1
|
||||
assert records[1]['record_type'] == 'LD'
|
||||
assert records[1]['activity_date'] == 'Tue, 15 May 2018 15:05:00 GMT'
|
||||
assert records[1]['value'] == '1:00:00'
|
||||
assert records[2]['sport_id'] == 2
|
||||
assert records[2]['activity_id'] == 1
|
||||
assert records[2]['record_type'] == 'FD'
|
||||
assert records[2]['activity_date'] == 'Tue, 15 May 2018 15:05:00 GMT'
|
||||
assert records[2]['value'] == 8.0
|
||||
assert records[3]['sport_id'] == 2
|
||||
assert records[3]['activity_id'] == 1
|
||||
assert records[3]['record_type'] == 'AS'
|
||||
assert records[3]['activity_date'] == 'Tue, 15 May 2018 15:05:00 GMT'
|
||||
assert records[3]['value'] == 8.0
|
||||
|
||||
def test_it_adds_notes_to_an_activity_wo_gpx(
|
||||
self, app, user_1, sport_1_cycling, activity_cycling_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(notes='test notes')),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 1
|
||||
assert 'creation_date' in data['data']['activities'][0]
|
||||
assert (
|
||||
data['data']['activities'][0]['activity_date']
|
||||
== 'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
)
|
||||
assert data['data']['activities'][0]['user'] == 'test'
|
||||
assert data['data']['activities'][0]['sport_id'] == 1
|
||||
assert data['data']['activities'][0]['duration'] == '1:00:00'
|
||||
assert data['data']['activities'][0]['title'] is None
|
||||
assert data['data']['activities'][0]['ascent'] is None
|
||||
assert data['data']['activities'][0]['ave_speed'] == 10.0
|
||||
assert data['data']['activities'][0]['descent'] is None
|
||||
assert data['data']['activities'][0]['distance'] == 10.0
|
||||
assert data['data']['activities'][0]['max_alt'] is None
|
||||
assert data['data']['activities'][0]['max_speed'] == 10.0
|
||||
assert data['data']['activities'][0]['min_alt'] is None
|
||||
assert data['data']['activities'][0]['moving'] == '1:00:00'
|
||||
assert data['data']['activities'][0]['pauses'] is None
|
||||
assert data['data']['activities'][0]['with_gpx'] is False
|
||||
assert data['data']['activities'][0]['map'] is None
|
||||
assert data['data']['activities'][0]['weather_start'] is None
|
||||
assert data['data']['activities'][0]['weather_end'] is None
|
||||
assert data['data']['activities'][0]['notes'] == 'test notes'
|
||||
|
||||
records = data['data']['activities'][0]['records']
|
||||
assert len(records) == 4
|
||||
assert records[0]['sport_id'] == 1
|
||||
assert records[0]['activity_id'] == 1
|
||||
assert records[0]['record_type'] == 'MS'
|
||||
assert records[0]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
assert records[0]['value'] == 10.0
|
||||
assert records[1]['sport_id'] == 1
|
||||
assert records[1]['activity_id'] == 1
|
||||
assert records[1]['record_type'] == 'LD'
|
||||
assert records[1]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
assert records[1]['value'] == '1:00:00'
|
||||
assert records[2]['sport_id'] == 1
|
||||
assert records[2]['activity_id'] == 1
|
||||
assert records[2]['record_type'] == 'FD'
|
||||
assert records[2]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
assert records[2]['value'] == 10.0
|
||||
assert records[3]['sport_id'] == 1
|
||||
assert records[3]['activity_id'] == 1
|
||||
assert records[3]['record_type'] == 'AS'
|
||||
assert records[3]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
assert records[3]['value'] == 10.0
|
||||
|
||||
def test_returns_403_when_editing_an_activity_wo_gpx_from_different_user(
|
||||
self, app, user_1, user_2, sport_1_cycling, activity_cycling_user_2
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
sport_id=2,
|
||||
duration=3600,
|
||||
activity_date='2018-05-15 15:05',
|
||||
distance=8,
|
||||
title='Activity test',
|
||||
)
|
||||
),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 403
|
||||
assert 'error' in data['status']
|
||||
assert 'You do not have permissions.' in data['message']
|
||||
|
||||
def test_it_updates_an_activity_wo_gpx_with_timezone(
|
||||
self,
|
||||
app,
|
||||
user_1_paris,
|
||||
sport_1_cycling,
|
||||
sport_2_running,
|
||||
activity_cycling_user_1,
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
sport_id=2,
|
||||
duration=3600,
|
||||
activity_date='2018-05-15 15:05',
|
||||
distance=8,
|
||||
title='Activity test',
|
||||
)
|
||||
),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 1
|
||||
assert 'creation_date' in data['data']['activities'][0]
|
||||
assert (
|
||||
data['data']['activities'][0]['activity_date']
|
||||
== 'Tue, 15 May 2018 13:05:00 GMT'
|
||||
)
|
||||
assert data['data']['activities'][0]['user'] == 'test'
|
||||
assert data['data']['activities'][0]['sport_id'] == 2
|
||||
assert data['data']['activities'][0]['duration'] == '1:00:00'
|
||||
assert data['data']['activities'][0]['title'] == 'Activity test'
|
||||
assert data['data']['activities'][0]['ascent'] is None
|
||||
assert data['data']['activities'][0]['ave_speed'] == 8.0
|
||||
assert data['data']['activities'][0]['descent'] is None
|
||||
assert data['data']['activities'][0]['distance'] == 8.0
|
||||
assert data['data']['activities'][0]['max_alt'] is None
|
||||
assert data['data']['activities'][0]['max_speed'] == 8.0
|
||||
assert data['data']['activities'][0]['min_alt'] is None
|
||||
assert data['data']['activities'][0]['moving'] == '1:00:00'
|
||||
assert data['data']['activities'][0]['pauses'] is None
|
||||
assert data['data']['activities'][0]['with_gpx'] is False
|
||||
|
||||
records = data['data']['activities'][0]['records']
|
||||
assert len(records) == 4
|
||||
assert records[0]['sport_id'] == 2
|
||||
assert records[0]['activity_id'] == 1
|
||||
assert records[0]['record_type'] == 'MS'
|
||||
assert records[0]['activity_date'] == 'Tue, 15 May 2018 13:05:00 GMT'
|
||||
assert records[0]['value'] == 8.0
|
||||
assert records[1]['sport_id'] == 2
|
||||
assert records[1]['activity_id'] == 1
|
||||
assert records[1]['record_type'] == 'LD'
|
||||
assert records[1]['activity_date'] == 'Tue, 15 May 2018 13:05:00 GMT'
|
||||
assert records[1]['value'] == '1:00:00'
|
||||
assert records[2]['sport_id'] == 2
|
||||
assert records[2]['activity_id'] == 1
|
||||
assert records[2]['record_type'] == 'FD'
|
||||
assert records[2]['activity_date'] == 'Tue, 15 May 2018 13:05:00 GMT'
|
||||
assert records[2]['value'] == 8.0
|
||||
assert records[3]['sport_id'] == 2
|
||||
assert records[3]['activity_id'] == 1
|
||||
assert records[3]['record_type'] == 'AS'
|
||||
assert records[3]['activity_date'] == 'Tue, 15 May 2018 13:05:00 GMT'
|
||||
assert records[3]['value'] == 8.0
|
||||
|
||||
def test_it_updates_only_sport_and_distance_an_activity_wo_gpx(
|
||||
self,
|
||||
app,
|
||||
user_1,
|
||||
sport_1_cycling,
|
||||
sport_2_running,
|
||||
activity_cycling_user_1,
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(sport_id=2, distance=20)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 1
|
||||
assert 'creation_date' in data['data']['activities'][0]
|
||||
assert (
|
||||
data['data']['activities'][0]['activity_date']
|
||||
== 'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
)
|
||||
assert data['data']['activities'][0]['user'] == 'test'
|
||||
assert data['data']['activities'][0]['sport_id'] == 2
|
||||
assert data['data']['activities'][0]['duration'] == '1:00:00'
|
||||
assert data['data']['activities'][0]['title'] is None
|
||||
assert data['data']['activities'][0]['ascent'] is None
|
||||
assert data['data']['activities'][0]['ave_speed'] == 20.0
|
||||
assert data['data']['activities'][0]['descent'] is None
|
||||
assert data['data']['activities'][0]['distance'] == 20.0
|
||||
assert data['data']['activities'][0]['max_alt'] is None
|
||||
assert data['data']['activities'][0]['max_speed'] == 20.0
|
||||
assert data['data']['activities'][0]['min_alt'] is None
|
||||
assert data['data']['activities'][0]['moving'] == '1:00:00'
|
||||
assert data['data']['activities'][0]['pauses'] is None
|
||||
assert data['data']['activities'][0]['with_gpx'] is False
|
||||
|
||||
records = data['data']['activities'][0]['records']
|
||||
assert len(records) == 4
|
||||
assert records[0]['sport_id'] == 2
|
||||
assert records[0]['activity_id'] == 1
|
||||
assert records[0]['record_type'] == 'MS'
|
||||
assert records[0]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
assert records[0]['value'] == 20.0
|
||||
assert records[1]['sport_id'] == 2
|
||||
assert records[1]['activity_id'] == 1
|
||||
assert records[1]['record_type'] == 'LD'
|
||||
assert records[1]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
assert records[1]['value'] == '1:00:00'
|
||||
assert records[2]['sport_id'] == 2
|
||||
assert records[2]['activity_id'] == 1
|
||||
assert records[2]['record_type'] == 'FD'
|
||||
assert records[2]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
assert records[2]['value'] == 20.0
|
||||
assert records[3]['sport_id'] == 2
|
||||
assert records[3]['activity_id'] == 1
|
||||
assert records[3]['record_type'] == 'AS'
|
||||
assert records[3]['activity_date'] == 'Mon, 01 Jan 2018 00:00:00 GMT'
|
||||
assert records[3]['value'] == 20.0
|
||||
|
||||
def test_it_returns_400_if_payload_is_empty(
|
||||
self, app, user_1, sport_1_cycling, activity_cycling_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict()),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 400
|
||||
assert 'error' in data['status']
|
||||
assert 'Invalid payload.' in data['message']
|
||||
|
||||
def test_it_returns_500_if_date_format_is_invalid(
|
||||
self, app, user_1, sport_1_cycling, activity_cycling_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
sport_id=1,
|
||||
duration=3600,
|
||||
activity_date='15/2018',
|
||||
distance=10,
|
||||
)
|
||||
),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 500
|
||||
assert 'error' in data['status']
|
||||
assert (
|
||||
'Error. Please try again or contact the administrator.'
|
||||
in data['message']
|
||||
)
|
||||
|
||||
def test_it_returns_404_if_edited_activity_doens_not_exists(
|
||||
self, app, user_1, sport_1_cycling
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
sport_id=1,
|
||||
duration=3600,
|
||||
activity_date='2018-05-15 14:05',
|
||||
distance=10,
|
||||
)
|
||||
),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 404
|
||||
assert 'not found' in data['status']
|
||||
assert len(data['data']['activities']) == 0
|
||||
|
||||
|
||||
class TestRefreshActivityWithGpx:
|
||||
def test_refresh_an_activity_with_gpx(
|
||||
self, app, user_1, sport_1_cycling, sport_2_running, gpx_file
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
client.post(
|
||||
'/api/activities',
|
||||
data=dict(
|
||||
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token'],
|
||||
),
|
||||
)
|
||||
|
||||
# Edit some activity data
|
||||
activity = Activity.query.filter_by(id=1).first()
|
||||
activity.ascent = 1000
|
||||
activity.min_alt = -100
|
||||
|
||||
response = client.patch(
|
||||
'/api/activities/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(refresh=True)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert len(data['data']['activities']) == 1
|
||||
assert 1 == data['data']['activities'][0]['sport_id']
|
||||
assert 0.4 == data['data']['activities'][0]['ascent']
|
||||
assert 975.0 == data['data']['activities'][0]['min_alt']
|
188
fittrackee/tests/test_activities_api_3_delete.py
Normal file
@ -0,0 +1,188 @@
|
||||
import json
|
||||
import os
|
||||
from io import BytesIO
|
||||
|
||||
from fittrackee.activities.models import Activity
|
||||
from fittrackee.activities.utils import get_absolute_file_path
|
||||
|
||||
|
||||
def get_gpx_filepath(activity_id):
|
||||
activity = Activity.query.filter_by(id=activity_id).first()
|
||||
return activity.gpx
|
||||
|
||||
|
||||
class TestDeleteActivityWithGpx:
|
||||
def test_it_deletes_an_activity_with_gpx(
|
||||
self, app, user_1, sport_1_cycling, gpx_file
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
client.post(
|
||||
'/api/activities',
|
||||
data=dict(
|
||||
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token'],
|
||||
),
|
||||
)
|
||||
response = client.delete(
|
||||
'/api/activities/1',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
def test_it_returns_403_when_deleting_an_activity_from_different_user(
|
||||
self, app, user_1, user_2, sport_1_cycling, gpx_file
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
client.post(
|
||||
'/api/activities',
|
||||
data=dict(
|
||||
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token'],
|
||||
),
|
||||
)
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='toto@toto.com', password='87654321')),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.delete(
|
||||
'/api/activities/1',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 403
|
||||
assert 'error' in data['status']
|
||||
assert 'You do not have permissions.' in data['message']
|
||||
|
||||
def test_it_returns_404_if_activity_does_not_exist(self, app, user_1):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.delete(
|
||||
'/api/activities/9999',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 404
|
||||
assert 'not found' in data['status']
|
||||
|
||||
def test_it_returns_500_when_deleting_an_activity_with_gpx_invalid_file(
|
||||
self, app, user_1, sport_1_cycling, gpx_file
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
client.post(
|
||||
'/api/activities',
|
||||
data=dict(
|
||||
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||
data='{"sport_id": 1}',
|
||||
),
|
||||
headers=dict(
|
||||
content_type='multipart/form-data',
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token'],
|
||||
),
|
||||
)
|
||||
|
||||
gpx_filepath = get_gpx_filepath(1)
|
||||
gpx_filepath = get_absolute_file_path(gpx_filepath)
|
||||
os.remove(gpx_filepath)
|
||||
|
||||
response = client.delete(
|
||||
'/api/activities/1',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 500
|
||||
assert 'error' in data['status']
|
||||
assert (
|
||||
'Error. Please try again or contact the administrator.'
|
||||
in data['message']
|
||||
)
|
||||
|
||||
|
||||
class TestDeleteActivityWithoutGpx:
|
||||
def test_it_deletes_an_activity_wo_gpx(
|
||||
self, app, user_1, sport_1_cycling, activity_cycling_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.delete(
|
||||
'/api/activities/1',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
def test_it_returns_403_when_deleting_an_activity_from_different_user(
|
||||
self, app, user_1, user_2, sport_1_cycling, activity_cycling_user_1
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='toto@toto.com', password='87654321')),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.delete(
|
||||
'/api/activities/1',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 403
|
||||
assert 'error' in data['status']
|
||||
assert 'You do not have permissions.' in data['message']
|
60
fittrackee/tests/test_activities_model.py
Normal file
@ -0,0 +1,60 @@
|
||||
class TestActivityModel:
|
||||
def test_activity_model(
|
||||
self, app, sport_1_cycling, user_1, activity_cycling_user_1
|
||||
):
|
||||
activity_cycling_user_1.title = 'Test'
|
||||
|
||||
assert 1 == activity_cycling_user_1.id
|
||||
assert 1 == activity_cycling_user_1.user_id
|
||||
assert 1 == activity_cycling_user_1.sport_id
|
||||
assert '2018-01-01 00:00:00' == str(
|
||||
activity_cycling_user_1.activity_date
|
||||
)
|
||||
assert 10.0 == float(activity_cycling_user_1.distance)
|
||||
assert '1:00:00' == str(activity_cycling_user_1.duration)
|
||||
assert 'Test' == activity_cycling_user_1.title
|
||||
assert '<Activity \'Cycling\' - 2018-01-01 00:00:00>' == str(
|
||||
activity_cycling_user_1
|
||||
)
|
||||
|
||||
serialized_activity = activity_cycling_user_1.serialize()
|
||||
assert 1 == serialized_activity['id']
|
||||
assert 'test' == serialized_activity['user']
|
||||
assert 1 == serialized_activity['sport_id']
|
||||
assert serialized_activity['title'] == 'Test'
|
||||
assert 'creation_date' in serialized_activity
|
||||
assert serialized_activity['modification_date'] is not None
|
||||
assert (
|
||||
str(serialized_activity['activity_date']) == '2018-01-01 00:00:00'
|
||||
)
|
||||
assert serialized_activity['duration'] == '1:00:00'
|
||||
assert serialized_activity['pauses'] is None
|
||||
assert serialized_activity['moving'] == '1:00:00'
|
||||
assert serialized_activity['distance'] == 10.0
|
||||
assert serialized_activity['max_alt'] is None
|
||||
assert serialized_activity['descent'] is None
|
||||
assert serialized_activity['ascent'] is None
|
||||
assert serialized_activity['max_speed'] == 10.0
|
||||
assert serialized_activity['ave_speed'] == 10.0
|
||||
assert serialized_activity['with_gpx'] is False
|
||||
assert serialized_activity['bounds'] == []
|
||||
assert serialized_activity['previous_activity'] is None
|
||||
assert serialized_activity['next_activity'] is None
|
||||
assert serialized_activity['segments'] == []
|
||||
assert serialized_activity['records'] != []
|
||||
assert serialized_activity['map'] is None
|
||||
assert serialized_activity['weather_start'] is None
|
||||
assert serialized_activity['weather_end'] is None
|
||||
assert serialized_activity['notes'] is None
|
||||
|
||||
def test_activity_segment_model(
|
||||
self,
|
||||
app,
|
||||
sport_1_cycling,
|
||||
user_1,
|
||||
activity_cycling_user_1,
|
||||
activity_cycling_user_1_segment,
|
||||
):
|
||||
assert '<Segment \'0\' for activity \'1\'>' == str(
|
||||
activity_cycling_user_1_segment
|
||||
)
|
227
fittrackee/tests/test_app_config_api.py
Normal file
@ -0,0 +1,227 @@
|
||||
import json
|
||||
|
||||
|
||||
class TestGetConfig:
|
||||
def test_it_gets_application_config(self, app, user_1):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/config',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert data['data']['gpx_limit_import'] == 10
|
||||
assert data['data']['is_registration_enabled'] is True
|
||||
assert data['data']['max_single_file_size'] == 1048576
|
||||
assert data['data']['max_zip_file_size'] == 10485760
|
||||
assert data['data']['max_users'] == 100
|
||||
assert data['data']['map_attribution'] == (
|
||||
'© <a href="http://www.openstreetmap.org/copyright" '
|
||||
'target="_blank" rel="noopener noreferrer">OpenStreetMap</a> '
|
||||
'contributors'
|
||||
)
|
||||
|
||||
def test_it_returns_error_if_application_has_no_config(
|
||||
self, app_no_config, user_1_admin
|
||||
):
|
||||
client = app_no_config.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(
|
||||
dict(email='admin@example.com', password='12345678')
|
||||
),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/config',
|
||||
content_type='application/json',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 500
|
||||
assert 'error' in data['status']
|
||||
assert 'Error on getting configuration.' in data['message']
|
||||
|
||||
def test_it_returns_error_if_application_has_several_config(
|
||||
self, app, app_config, user_1_admin
|
||||
):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(
|
||||
dict(email='admin@example.com', password='12345678')
|
||||
),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
'/api/config',
|
||||
content_type='application/json',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 500
|
||||
assert 'error' in data['status']
|
||||
assert 'Error on getting configuration.' in data['message']
|
||||
|
||||
|
||||
class TestUpdateConfig:
|
||||
def test_it_updates_config_when_user_is_admin(self, app, user_1_admin):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(
|
||||
dict(email='admin@example.com', password='12345678')
|
||||
),
|
||||
content_type='application/json',
|
||||
)
|
||||
response = client.patch(
|
||||
'/api/config',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(gpx_limit_import=100, max_users=10)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert data['data']['gpx_limit_import'] == 100
|
||||
assert data['data']['is_registration_enabled'] is True
|
||||
assert data['data']['max_single_file_size'] == 1048576
|
||||
assert data['data']['max_zip_file_size'] == 10485760
|
||||
assert data['data']['max_users'] == 10
|
||||
|
||||
def test_it_updates_all_config(self, app, user_1_admin):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(
|
||||
dict(email='admin@example.com', password='12345678')
|
||||
),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/config',
|
||||
content_type='application/json',
|
||||
data=json.dumps(
|
||||
dict(
|
||||
gpx_limit_import=20,
|
||||
max_single_file_size=10000,
|
||||
max_zip_file_size=25000,
|
||||
max_users=50,
|
||||
)
|
||||
),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 200
|
||||
assert 'success' in data['status']
|
||||
assert data['data']['gpx_limit_import'] == 20
|
||||
assert data['data']['is_registration_enabled'] is True
|
||||
assert data['data']['max_single_file_size'] == 10000
|
||||
assert data['data']['max_zip_file_size'] == 25000
|
||||
assert data['data']['max_users'] == 50
|
||||
|
||||
def test_it_returns_403_when_user_is_not_an_admin(self, app, user_1):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/config',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(gpx_limit_import=100, max_users=10)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 403
|
||||
assert 'success' not in data['status']
|
||||
assert 'error' in data['status']
|
||||
assert 'You do not have permissions.' in data['message']
|
||||
|
||||
def test_it_returns_400_if_invalid_is_payload(self, app, user_1_admin):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(
|
||||
dict(email='admin@example.com', password='12345678')
|
||||
),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/config',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict()),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 400
|
||||
assert 'error' in data['status']
|
||||
assert 'Invalid payload.' in data['message']
|
||||
|
||||
def test_it_returns_error_on_update_if_application_has_no_config(
|
||||
self, app_no_config, user_1_admin
|
||||
):
|
||||
client = app_no_config.test_client()
|
||||
resp_login = client.post(
|
||||
'/api/auth/login',
|
||||
data=json.dumps(
|
||||
dict(email='admin@example.com', password='12345678')
|
||||
),
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
response = client.patch(
|
||||
'/api/config',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(gpx_limit_import=100, max_users=10)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 500
|
||||
assert 'error' in data['status']
|
||||
assert 'Error on updating configuration.' in data['message']
|
19
fittrackee/tests/test_app_config_model.py
Normal file
@ -0,0 +1,19 @@
|
||||
from fittrackee.application.models import AppConfig
|
||||
|
||||
|
||||
class TestConfigModel:
|
||||
def test_application_config(self, app):
|
||||
app_config = AppConfig.query.first()
|
||||
assert 1 == app_config.id
|
||||
|
||||
serialized_app_config = app_config.serialize()
|
||||
assert serialized_app_config['gpx_limit_import'] == 10
|
||||
assert serialized_app_config['is_registration_enabled'] is True
|
||||
assert serialized_app_config['max_single_file_size'] == 1048576
|
||||
assert serialized_app_config['max_zip_file_size'] == 10485760
|
||||
assert serialized_app_config['max_users'] == 100
|
||||
assert serialized_app_config['map_attribution'] == (
|
||||
'© <a href="http://www.openstreetmap.org/copyright" '
|
||||
'target="_blank" rel="noopener noreferrer">OpenStreetMap</a> '
|
||||
'contributors'
|
||||
)
|