FitTrackee/fittrackee/activities/models.py

471 lines
18 KiB
Python
Raw Normal View History

2018-01-21 17:43:13 +01:00
import datetime
import os
from uuid import uuid4
2018-01-21 17:43:13 +01:00
from fittrackee import db
from sqlalchemy.dialects import postgresql
from sqlalchemy.event import listens_for
2018-05-15 22:22:04 +02:00
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
2020-12-30 22:07:43 +01:00
from .utils_id import encode_uuid
record_types = [
'AS', # 'Best Average Speed'
'FD', # 'Farthest Distance'
'LD', # 'Longest Duration'
'MS', # 'Max speed'
]
2018-01-21 17:43:13 +01:00
2018-05-18 15:21:11 +02:00
def update_records(user_id, sport_id, connection, session):
record_table = Record.__table__
2019-08-28 13:25:39 +02:00
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(
2019-08-28 13:25:39 +02:00
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']
)
2019-08-28 13:25:39 +02:00
connection.execute(
record_table.update()
.where(record_table.c.id == record.id)
.values(
value=value,
activity_id=record_data['activity'].id,
activity_uuid=record_data['activity'].uuid,
2019-08-28 13:25:39 +02:00
activity_date=record_data['activity'].activity_date,
)
)
else:
new_record = Record(
2019-08-28 13:25:39 +02:00
activity=record_data['activity'], record_type=record_type
)
new_record.value = record_data['record_value']
session.add(new_record)
2018-05-15 22:22:04 +02:00
else:
2019-08-28 13:25:39 +02:00
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)
)
2018-01-21 17:43:13 +01:00
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)
2018-05-16 23:52:55 +02:00
img = db.Column(db.String(255), unique=True, nullable=True)
is_active = db.Column(db.Boolean, default=True, nullable=False)
2019-08-28 13:25:39 +02:00
activities = db.relationship(
'Activity', lazy=True, backref=db.backref('sports', lazy='joined')
)
records = db.relationship(
'Record', lazy=True, backref=db.backref('sports', lazy='joined')
)
2018-01-21 17:43:13 +01:00
def __repr__(self):
2018-05-23 17:30:22 +02:00
return f'<Sport {self.label!r}>'
2018-01-21 17:43:13 +01:00
def __init__(self, label):
self.label = label
def serialize(self, is_admin=False):
serialized_sport = {
2018-05-09 16:50:30 +02:00
'id': self.id,
'label': self.label,
2018-05-16 23:52:55 +02:00
'img': self.img,
'is_active': self.is_active,
2018-05-09 16:50:30 +02:00
}
if is_admin:
serialized_sport['has_activities'] = len(self.activities) > 0
return serialized_sport
2018-05-09 16:50:30 +02:00
2018-01-21 17:43:13 +01:00
class Activity(db.Model):
__tablename__ = "activities"
2019-08-28 13:25:39 +02:00
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
uuid = db.Column(
postgresql.UUID(as_uuid=True),
default=uuid4,
unique=True,
nullable=False,
)
2019-08-28 13:25:39 +02:00
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
2018-01-21 17:43:13 +01:00
sport_id = db.Column(
2019-08-28 13:25:39 +02:00
db.Integer, db.ForeignKey('sports.id'), nullable=False
)
2018-05-13 12:52:22 +02:00
title = db.Column(db.String(255), nullable=True)
2018-01-21 17:43:13 +01:00
gpx = db.Column(db.String(255), nullable=True)
2019-08-28 13:25:39 +02:00
creation_date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
2018-01-28 11:28:47 +01:00
modification_date = db.Column(
2019-08-28 13:25:39 +02:00
db.DateTime, onupdate=datetime.datetime.utcnow
)
2018-01-21 17:43:13 +01:00
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)
2019-08-28 13:25:39 +02:00
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)
2018-06-13 18:17:28 +02:00
notes = db.Column(db.String(500), nullable=True)
2019-08-28 13:25:39 +02:00
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),
)
2018-01-21 17:43:13 +01:00
def __str__(self):
2018-05-23 17:30:22 +02:00
return f'<Activity \'{self.sports.label}\' - {self.activity_date}>'
2018-01-21 17:43:13 +01:00
def __init__(self, user_id, sport_id, activity_date, distance, duration):
2018-01-21 17:43:13 +01:00
self.user_id = user_id
self.sport_id = sport_id
self.activity_date = activity_date
self.distance = distance
self.duration = duration
2018-05-01 21:26:17 +02:00
2020-12-30 22:07:43 +01:00
@property
def short_id(self):
return encode_uuid(self.uuid)
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
2018-06-11 22:42:04 +02:00
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
2019-08-28 13:25:39 +02:00
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()
)
2018-05-01 21:26:17 +02:00
return {
2020-12-30 22:07:43 +01:00
"id": self.short_id, # WARNING: client use uuid as id
"user": self.user.username,
2018-05-01 21:26:17 +02:00
"sport_id": self.sport_id,
2018-05-13 12:52:22 +02:00
"title": self.title,
2018-05-01 21:26:17 +02:00
"creation_date": self.creation_date,
"modification_date": self.modification_date,
"activity_date": self.activity_date,
2018-05-02 20:47:42 +02:00
"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,
2018-05-01 21:40:43 +02:00
"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,
2018-05-08 18:20:41 +02:00
"ave_speed": float(self.ave_speed) if self.ave_speed else None,
2018-05-14 13:25:00 +02:00
"with_gpx": self.gpx is not None,
2019-08-28 13:25:39 +02:00
"bounds": [float(bound) for bound in self.bounds]
if self.bounds
else [], # noqa
2020-12-30 22:07:43 +01:00
"previous_activity": previous_activity.short_id
2019-08-28 13:25:39 +02:00
if previous_activity
else None, # noqa
2020-12-30 22:07:43 +01:00
"next_activity": next_activity.short_id if next_activity else None,
2018-05-15 22:22:04 +02:00
"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,
2018-06-13 18:17:28 +02:00
"weather_end": self.weather_end,
2019-08-28 13:25:39 +02:00
"notes": self.notes,
2018-05-14 13:25:00 +02:00
}
@classmethod
def get_user_activity_records(cls, user_id, sport_id, as_integer=False):
record_types_columns = {
'AS': 'ave_speed', # 'Average speed'
2019-08-28 13:25:39 +02:00
'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')()
2019-08-28 13:25:39 +02:00
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(
2019-08-28 13:25:39 +02:00
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):
2019-08-28 13:25:39 +02:00
update_records(
activity.user_id, activity.sport_id, connection, session
) # noqa
@listens_for(Activity, 'after_update')
def on_activity_update(mapper, connection, activity):
2019-08-28 13:25:39 +02:00
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):
2018-05-15 22:22:04 +02:00
sports_list = [activity.sport_id]
2019-08-28 13:25:39 +02:00
records = Record.query.filter_by(activity_id=activity.id).all()
2018-05-15 22:22:04 +02:00
for rec in records:
if rec.sport_id not in sports_list:
sports_list.append(rec.sport_id)
for sport_id in sports_list:
2018-05-18 15:21:11 +02:00
update_records(activity.user_id, sport_id, connection, session)
2018-05-14 13:25:00 +02:00
@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))
2018-05-14 13:25:00 +02:00
class ActivitySegment(db.Model):
__tablename__ = "activity_segments"
activity_id = db.Column(
2019-08-28 13:25:39 +02:00
db.Integer, db.ForeignKey('activities.id'), primary_key=True
)
activity_uuid = db.Column(postgresql.UUID(as_uuid=True), nullable=False)
2019-08-28 13:25:39 +02:00
segment_id = db.Column(db.Integer, primary_key=True)
2018-05-14 13:25:00 +02:00
duration = db.Column(db.Interval, nullable=False)
pauses = db.Column(db.Interval, nullable=True)
moving = db.Column(db.Interval, nullable=True)
2019-08-28 13:25:39 +02:00
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
2018-05-14 13:25:00 +02:00
def __str__(self):
2019-08-28 13:25:39 +02:00
return (
f'<Segment \'{self.segment_id}\' '
2020-12-30 22:07:43 +01:00
f'for activity \'{encode_uuid(self.activity_uuid)}\'>'
2019-08-28 13:25:39 +02:00
)
2018-05-14 13:25:00 +02:00
def __init__(self, segment_id, activity_id, activity_uuid):
2018-05-14 13:25:00 +02:00
self.segment_id = segment_id
self.activity_id = activity_id
self.activity_uuid = activity_uuid
2018-05-14 13:25:00 +02:00
def serialize(self):
return {
2020-12-30 22:07:43 +01:00
"activity_id": encode_uuid(self.activity_uuid),
2018-05-14 13:25:00 +02:00
"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,
2019-08-28 13:25:39 +02:00
"ave_speed": float(self.ave_speed) if self.ave_speed else None,
2018-05-01 21:26:17 +02:00
}
class Record(db.Model):
__tablename__ = "records"
2019-08-28 13:25:39 +02:00
__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(
2019-08-28 13:25:39 +02:00
db.Integer, db.ForeignKey('sports.id'), nullable=False
)
activity_id = db.Column(
2019-08-28 13:25:39 +02:00
db.Integer, db.ForeignKey('activities.id'), nullable=False
)
activity_uuid = db.Column(postgresql.UUID(as_uuid=True), 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):
2019-08-28 13:25:39 +02:00
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.activity_uuid = activity.uuid
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,
2020-12-30 22:07:43 +01:00
"activity_id": encode_uuid(self.activity_uuid),
"record_type": self.record_type,
"activity_date": self.activity_date,
"value": value,
}
@listens_for(Record, 'after_delete')
def on_record_delete(mapper, connection, old_record):
@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(
2019-08-28 13:25:39 +02:00
activity.user_id, activity.sport_id
)
for record_type, record_data in new_records.items():
2019-08-28 13:25:39 +02:00
if (
record_data['record_value']
and record_type == old_record.record_type
):
new_record = Record(
2019-08-28 13:25:39 +02:00
activity=record_data['activity'], record_type=record_type
)
new_record.value = record_data['record_value']
session.add(new_record)