FitTrackee/fittrackee/workouts/stats.py

381 lines
10 KiB
Python
Raw Normal View History

2018-06-06 13:45:39 +02:00
from datetime import datetime, timedelta
2021-01-02 19:28:03 +01:00
from typing import Dict, Union
2018-06-06 00:22:24 +02:00
2021-01-20 16:47:00 +01:00
from flask import Blueprint, request
from sqlalchemy import func
2021-01-01 16:39:25 +01:00
from fittrackee import db
from fittrackee.responses import (
2021-01-02 19:28:03 +01:00
HttpResponse,
2021-01-01 16:39:25 +01:00
InvalidPayloadErrorResponse,
NotFoundErrorResponse,
UserNotFoundErrorResponse,
handle_error_and_return_response,
)
2021-01-20 16:47:00 +01:00
from fittrackee.users.decorators import authenticate, authenticate_as_admin
from fittrackee.users.models import User
2018-06-06 00:22:24 +02:00
from .models import Sport, Workout
2021-05-22 17:14:24 +02:00
from .utils import get_datetime_from_request_args, get_upload_dir_size
from .utils_format import convert_timedelta_to_integer
2018-06-06 00:22:24 +02:00
stats_blueprint = Blueprint('stats', __name__)
def get_workouts(
2021-01-02 19:28:03 +01:00
user_name: str, filter_type: str
) -> Union[Dict, HttpResponse]:
"""
Return user workouts by sport or by time
2021-01-02 19:28:03 +01:00
"""
2018-06-06 00:22:24 +02:00
try:
user = User.query.filter_by(username=user_name).first()
2018-06-06 00:22:24 +02:00
if not user:
2021-01-01 16:39:25 +01:00
return UserNotFoundErrorResponse()
2018-06-06 00:22:24 +02:00
params = request.args.copy()
2021-05-22 17:14:24 +02:00
date_from, date_to = get_datetime_from_request_args(params, user)
2018-06-06 12:16:52 +02:00
sport_id = params.get('sport_id')
2018-06-06 12:09:09 +02:00
time = params.get('time')
2018-06-06 12:16:52 +02:00
2018-06-12 11:47:01 +02:00
if filter_type == 'by_sport':
2018-06-06 12:16:52 +02:00
if sport_id:
sport = Sport.query.filter_by(id=sport_id).first()
if not sport:
2021-01-01 16:39:25 +01:00
return NotFoundErrorResponse('Sport does not exist.')
2018-06-06 00:22:24 +02:00
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)
2019-08-28 13:25:39 +02:00
if date_to
else True,
Workout.sport_id == sport_id if sport_id else True,
2019-08-28 13:25:39 +02:00
)
.order_by(Workout.workout_date.asc())
2019-08-28 13:25:39 +02:00
.all()
)
2018-06-06 00:22:24 +02:00
workouts_list_by_sport = {}
workouts_list_by_time = {} # type: ignore
for workout in workouts:
2018-06-12 11:47:01 +02:00
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] = {
'nb_workouts': 0,
2019-08-28 13:25:39 +02:00
'total_distance': 0.0,
2018-06-06 12:16:52 +02:00
'total_duration': 0,
}
workouts_list_by_sport[sport_id]['nb_workouts'] += 1
workouts_list_by_sport[sport_id]['total_distance'] += float(
workout.distance
2019-08-28 13:25:39 +02:00
)
workouts_list_by_sport[sport_id][
2019-08-28 13:25:39 +02:00
'total_duration'
] += convert_timedelta_to_integer(workout.moving)
2018-06-06 12:16:52 +02:00
2021-01-02 19:28:03 +01:00
# filter_type == 'by_time'
2018-06-06 12:09:09 +02:00
else:
2018-06-06 12:16:52 +02:00
if time == 'week':
workout_date = workout.workout_date - timedelta(
2019-08-28 13:25:39 +02:00
days=(
workout.workout_date.isoweekday()
if workout.workout_date.isoweekday() < 7
2019-08-28 13:25:39 +02:00
else 0
)
2018-06-06 13:45:39 +02:00
)
time_period = datetime.strftime(workout_date, "%Y-%m-%d")
2018-06-06 12:16:52 +02:00
elif time == 'weekm': # week start Monday
workout_date = workout.workout_date - timedelta(
days=workout.workout_date.weekday()
2018-06-06 13:45:39 +02:00
)
time_period = datetime.strftime(workout_date, "%Y-%m-%d")
2018-06-06 12:16:52 +02:00
elif time == 'month':
2019-08-28 13:25:39 +02:00
time_period = datetime.strftime(
workout.workout_date, "%Y-%m"
)
2018-06-06 12:16:52 +02:00
elif time == 'year' or not time:
time_period = datetime.strftime(workout.workout_date, "%Y")
2018-06-06 12:16:52 +02:00
else:
2021-01-01 16:39:25 +01:00
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] = {
'nb_workouts': 0,
2019-08-28 13:25:39 +02:00
'total_distance': 0.0,
2018-06-06 12:16:52 +02:00
'total_duration': 0,
}
workouts_list_by_time[time_period][sport_id][
'nb_workouts'
2021-01-02 19:28:03 +01:00
] += 1
workouts_list_by_time[time_period][sport_id][
2019-08-28 13:25:39 +02:00
'total_distance'
] += float(workout.distance)
workouts_list_by_time[time_period][sport_id][
2019-08-28 13:25:39 +02:00
'total_duration'
] += convert_timedelta_to_integer(workout.moving)
2018-06-06 12:09:09 +02:00
2021-01-01 16:39:25 +01:00
return {
2018-06-06 12:09:09 +02:00
'status': 'success',
2021-01-02 19:28:03 +01:00
'data': {
'statistics': workouts_list_by_sport
2021-01-02 19:28:03 +01:00
if filter_type == 'by_sport'
else workouts_list_by_time
2021-01-02 19:28:03 +01:00
},
2018-06-06 12:09:09 +02:00
}
except Exception as e:
2021-01-01 16:39:25 +01:00
return handle_error_and_return_response(e)
2018-06-06 12:09:09 +02:00
@stats_blueprint.route('/stats/<user_name>/by_time', methods=['GET'])
2018-06-06 12:09:09 +02:00
@authenticate
def get_workouts_by_time(
2021-01-02 19:28:03 +01:00
auth_user_id: int, user_name: str
) -> Union[Dict, HttpResponse]:
"""
Get workouts 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_workouts": 2,
"total_distance": 15.282,
"total_duration": 12341
}
},
"2019": {
"1": {
"nb_workouts": 3,
"total_distance": 47,
"total_duration": 9960
},
"2": {
"nb_workouts": 1,
"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 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_workouts(user_name, 'by_time')
2018-06-06 12:09:09 +02:00
@stats_blueprint.route('/stats/<user_name>/by_sport', methods=['GET'])
2018-06-06 12:16:52 +02:00
@authenticate
def get_workouts_by_sport(
2021-01-02 19:28:03 +01:00
auth_user_id: int, user_name: str
) -> Union[Dict, HttpResponse]:
"""
Get workouts statistics for a user by sport
**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": {
"nb_workouts": 3,
"total_distance": 47,
"total_duration": 9960
},
"2": {
"nb_workouts": 1,
"total_distance": 5.613,
"total_duration": 1267
},
"3": {
"nb_workouts": 2,
"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 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_workouts(user_name, 'by_sport')
@stats_blueprint.route('/stats/all', methods=['GET'])
@authenticate_as_admin
2021-01-02 19:28:03 +01:00
def get_application_stats(auth_user_id: int) -> Dict:
"""
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": {
"sports": 3,
"uploads_dir_size": 1000,
"users": 2,
"workouts": 3,
},
"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_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()
)
2021-01-01 16:39:25 +01:00
return {
'status': 'success',
'data': {
'workouts': nb_workouts,
'sports': nb_sports,
'users': nb_users,
'uploads_dir_size': get_upload_dir_size(),
},
}