440 lines
13 KiB
Python
440 lines
13 KiB
Python
from datetime import datetime, timedelta
|
|
from typing import Dict, Union
|
|
|
|
from flask import Blueprint, request
|
|
from sqlalchemy import func
|
|
|
|
from fittrackee import db
|
|
from fittrackee.oauth2.server import require_auth
|
|
from fittrackee.responses import (
|
|
HttpResponse,
|
|
InvalidPayloadErrorResponse,
|
|
NotFoundErrorResponse,
|
|
UserNotFoundErrorResponse,
|
|
handle_error_and_return_response,
|
|
)
|
|
from fittrackee.users.models import User
|
|
|
|
from .models import Sport, Workout
|
|
from .utils.convert import convert_timedelta_to_integer
|
|
from .utils.uploads import get_upload_dir_size
|
|
from .utils.workouts import get_average_speed, get_datetime_from_request_args
|
|
|
|
stats_blueprint = Blueprint('stats', __name__)
|
|
|
|
|
|
def get_workouts(
|
|
user_name: str, filter_type: str
|
|
) -> Union[Dict, HttpResponse]:
|
|
"""
|
|
Return user workouts by sport or by time
|
|
"""
|
|
try:
|
|
user = User.query.filter_by(username=user_name).first()
|
|
if not user:
|
|
return UserNotFoundErrorResponse()
|
|
|
|
params = request.args.copy()
|
|
date_from, date_to = get_datetime_from_request_args(params, user)
|
|
sport_id = params.get('sport_id')
|
|
time = params.get('time')
|
|
|
|
if filter_type == 'by_sport':
|
|
if sport_id:
|
|
sport = Sport.query.filter_by(id=sport_id).first()
|
|
if not sport:
|
|
return NotFoundErrorResponse('sport does not exist')
|
|
|
|
workouts = (
|
|
Workout.query.filter(
|
|
Workout.user_id == user.id,
|
|
Workout.workout_date >= date_from if date_from else True,
|
|
Workout.workout_date < date_to + timedelta(seconds=1)
|
|
if date_to
|
|
else True,
|
|
Workout.sport_id == sport_id if sport_id else True,
|
|
)
|
|
.order_by(Workout.workout_date.asc())
|
|
.all()
|
|
)
|
|
|
|
workouts_list_by_sport = {}
|
|
workouts_list_by_time = {} # type: ignore
|
|
for workout in workouts:
|
|
if filter_type == 'by_sport':
|
|
sport_id = workout.sport_id
|
|
if sport_id not in workouts_list_by_sport:
|
|
workouts_list_by_sport[sport_id] = {
|
|
'average_speed': 0.0,
|
|
'nb_workouts': 0,
|
|
'total_distance': 0.0,
|
|
'total_duration': 0,
|
|
'total_ascent': 0.0,
|
|
'total_descent': 0.0,
|
|
}
|
|
workouts_list_by_sport[sport_id]['nb_workouts'] += 1
|
|
workouts_list_by_sport[sport_id][
|
|
'average_speed'
|
|
] = get_average_speed(
|
|
workouts_list_by_sport[sport_id]['nb_workouts'], # type: ignore # noqa
|
|
workouts_list_by_sport[sport_id]['average_speed'],
|
|
workout.ave_speed,
|
|
)
|
|
workouts_list_by_sport[sport_id]['total_distance'] += float(
|
|
workout.distance
|
|
)
|
|
workouts_list_by_sport[sport_id][
|
|
'total_duration'
|
|
] += convert_timedelta_to_integer(workout.moving)
|
|
if workout.ascent:
|
|
workouts_list_by_sport[sport_id]['total_ascent'] += float(
|
|
workout.ascent
|
|
)
|
|
if workout.descent:
|
|
workouts_list_by_sport[sport_id]['total_descent'] += float(
|
|
workout.descent
|
|
)
|
|
|
|
# filter_type == 'by_time'
|
|
else:
|
|
if time == 'week':
|
|
workout_date = workout.workout_date - timedelta(
|
|
days=(
|
|
workout.workout_date.isoweekday()
|
|
if workout.workout_date.isoweekday() < 7
|
|
else 0
|
|
)
|
|
)
|
|
time_period = datetime.strftime(workout_date, "%Y-%m-%d")
|
|
elif time == 'weekm': # week start Monday
|
|
workout_date = workout.workout_date - timedelta(
|
|
days=workout.workout_date.weekday()
|
|
)
|
|
time_period = datetime.strftime(workout_date, "%Y-%m-%d")
|
|
elif time == 'month':
|
|
time_period = datetime.strftime(
|
|
workout.workout_date, "%Y-%m"
|
|
)
|
|
elif time == 'year' or not time:
|
|
time_period = datetime.strftime(workout.workout_date, "%Y")
|
|
else:
|
|
return InvalidPayloadErrorResponse(
|
|
'Invalid time period.', 'fail'
|
|
)
|
|
sport_id = workout.sport_id
|
|
if time_period not in workouts_list_by_time:
|
|
workouts_list_by_time[time_period] = {}
|
|
if sport_id not in workouts_list_by_time[time_period]:
|
|
workouts_list_by_time[time_period][sport_id] = {
|
|
'average_speed': 0.0,
|
|
'nb_workouts': 0,
|
|
'total_distance': 0.0,
|
|
'total_duration': 0,
|
|
'total_ascent': 0.0,
|
|
'total_descent': 0.0,
|
|
}
|
|
workouts_list_by_time[time_period][sport_id][
|
|
'nb_workouts'
|
|
] += 1
|
|
workouts_list_by_time[time_period][sport_id][
|
|
'average_speed'
|
|
] = get_average_speed(
|
|
workouts_list_by_time[time_period][sport_id][
|
|
'nb_workouts'
|
|
],
|
|
workouts_list_by_time[time_period][sport_id][
|
|
'average_speed'
|
|
],
|
|
workout.ave_speed,
|
|
)
|
|
workouts_list_by_time[time_period][sport_id][
|
|
'total_distance'
|
|
] += float(workout.distance)
|
|
workouts_list_by_time[time_period][sport_id][
|
|
'total_duration'
|
|
] += convert_timedelta_to_integer(workout.moving)
|
|
if workout.ascent:
|
|
workouts_list_by_time[time_period][sport_id][
|
|
'total_ascent'
|
|
] += float(workout.ascent)
|
|
if workout.descent:
|
|
workouts_list_by_time[time_period][sport_id][
|
|
'total_descent'
|
|
] += float(workout.descent)
|
|
return {
|
|
'status': 'success',
|
|
'data': {
|
|
'statistics': workouts_list_by_sport
|
|
if filter_type == 'by_sport'
|
|
else workouts_list_by_time
|
|
},
|
|
}
|
|
except Exception as e:
|
|
return handle_error_and_return_response(e)
|
|
|
|
|
|
@stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET'])
|
|
@require_auth(scopes=['workouts:read'])
|
|
def get_workouts_by_time(
|
|
auth_user: User, user_name: str
|
|
) -> Union[Dict, HttpResponse]:
|
|
"""
|
|
Get workouts statistics for a user by time.
|
|
|
|
**Scope**: ``workouts:read``
|
|
|
|
**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": {
|
|
"average_speed": 4.48,
|
|
"nb_workouts": 2,
|
|
"total_ascent": 203.0,
|
|
"total_ascent": 156.0,
|
|
"total_distance": 15.282,
|
|
"total_duration": 12341
|
|
}
|
|
},
|
|
"2019": {
|
|
"1": {
|
|
"average_speed": 16.99,
|
|
"nb_workouts": 3,
|
|
"total_ascent": 150.0,
|
|
"total_ascent": 178.0,
|
|
"total_distance": 47,
|
|
"total_duration": 9960
|
|
},
|
|
"2": {
|
|
"average_speed": 15.95,
|
|
"nb_workouts": 1,
|
|
"total_ascent": 46.0,
|
|
"total_ascent": 78.0,
|
|
"total_distance": 5.613,
|
|
"total_duration": 1267
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"status": "success"
|
|
}
|
|
|
|
- no workouts:
|
|
|
|
.. sourcecode:: http
|
|
|
|
HTTP/1.1 200 OK
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"data": {
|
|
"statistics": {}
|
|
},
|
|
"status": "success"
|
|
}
|
|
|
|
:param integer user_name: username
|
|
|
|
: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_workouts(user_name, 'by_time')
|
|
|
|
|
|
@stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET'])
|
|
@require_auth(scopes=['workouts:read'])
|
|
def get_workouts_by_sport(
|
|
auth_user: User, user_name: str
|
|
) -> Union[Dict, HttpResponse]:
|
|
"""
|
|
Get workouts statistics for a user by sport.
|
|
|
|
**Scope**: ``workouts:read``
|
|
|
|
**Example requests**:
|
|
|
|
- without parameters (get stats for all sports with workouts):
|
|
|
|
.. 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": {
|
|
"average_speed": 16.99,
|
|
"nb_workouts": 3,
|
|
"total_ascent": 150.0,
|
|
"total_ascent": 178.0,
|
|
"total_distance": 47,
|
|
"total_duration": 9960
|
|
},
|
|
"2": {
|
|
"average_speed": 15.95,
|
|
"nb_workouts": 1,
|
|
"total_ascent": 46.0,
|
|
"total_ascent": 78.0,
|
|
"total_distance": 5.613,
|
|
"total_duration": 1267
|
|
},
|
|
"3": {
|
|
"average_speed": 4.46,
|
|
"nb_workouts": 2,
|
|
"total_ascent": 203.0,
|
|
"total_ascent": 156.0,
|
|
"total_distance": 15.282,
|
|
"total_duration": 12341
|
|
}
|
|
}
|
|
},
|
|
"status": "success"
|
|
}
|
|
|
|
- no workouts:
|
|
|
|
.. sourcecode:: http
|
|
|
|
HTTP/1.1 200 OK
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"data": {
|
|
"statistics": {}
|
|
},
|
|
"status": "success"
|
|
}
|
|
|
|
:param integer user_name: username
|
|
|
|
: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_workouts(user_name, 'by_sport')
|
|
|
|
|
|
@stats_blueprint.route('/stats/all', methods=['GET'])
|
|
@require_auth(scopes=['workouts:read'], as_admin=True)
|
|
def get_application_stats(auth_user: User) -> Dict:
|
|
"""
|
|
Get all application statistics.
|
|
|
|
**Scope**: ``workouts:read``
|
|
|
|
**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": {
|
|
"sports": 3,
|
|
"uploads_dir_size": 1000,
|
|
"users": 2,
|
|
"workouts": 3,
|
|
},
|
|
"status": "success"
|
|
}
|
|
|
|
: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_workouts = Workout.query.filter().count()
|
|
nb_users = User.query.filter().count()
|
|
nb_sports = (
|
|
db.session.query(func.count(Workout.sport_id))
|
|
.group_by(Workout.sport_id)
|
|
.count()
|
|
)
|
|
return {
|
|
'status': 'success',
|
|
'data': {
|
|
'workouts': nb_workouts,
|
|
'sports': nb_sports,
|
|
'users': nb_users,
|
|
'uploads_dir_size': get_upload_dir_size(),
|
|
},
|
|
}
|