API: Activity segments table init

This commit is contained in:
Sam 2018-05-14 13:25:00 +02:00
parent 6d4574332b
commit e4a65f4c79
7 changed files with 152 additions and 17 deletions

View File

@ -23,10 +23,9 @@ def upgrade():
sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('sport_id', sa.Integer(), nullable=False), sa.Column('sport_id', sa.Integer(), nullable=False),
sa.Column('activity_id', sa.Integer(), nullable=False), sa.Column('activity_id', sa.Integer(), nullable=False),
sa.Column('record_type', sa.Enum('FD', 'LD', 'MS', name='record_types'), nullable=True), 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('activity_date', sa.DateTime(), nullable=False),
sa.Column('value_interval', sa.Interval(), nullable=True), sa.Column('value', sa.Integer(), nullable=True),
sa.Column('value_float', sa.Numeric(precision=5, scale=3), nullable=True),
sa.ForeignKeyConstraint(['activity_id'], ['activities.id'], ), sa.ForeignKeyConstraint(['activity_id'], ['activities.id'], ),
sa.ForeignKeyConstraint(['sport_id'], ['sports.id'], ), sa.ForeignKeyConstraint(['sport_id'], ['sports.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),

View File

@ -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=5, scale=3), nullable=True),
sa.Column('min_alt', sa.Numeric(precision=5, scale=2), nullable=True),
sa.Column('max_alt', sa.Numeric(precision=5, scale=2), nullable=True),
sa.Column('descent', sa.Numeric(precision=5, scale=2), nullable=True),
sa.Column('ascent', sa.Numeric(precision=5, scale=2), nullable=True),
sa.Column('max_speed', sa.Numeric(precision=5, scale=2), nullable=True),
sa.Column('ave_speed', sa.Numeric(precision=5, 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 ###

View File

@ -8,8 +8,8 @@ from sqlalchemy import exc
from ..users.utils import authenticate, verify_extension from ..users.utils import authenticate, verify_extension
from .models import Activity, Sport from .models import Activity, Sport
from .utils import ( from .utils import (
create_activity, edit_activity, get_file_path, get_gpx_info, create_activity, create_segment, edit_activity, get_file_path,
get_new_file_path get_gpx_info, get_new_file_path
) )
activities_blueprint = Blueprint('activities', __name__) activities_blueprint = Blueprint('activities', __name__)
@ -170,6 +170,10 @@ def post_activity(auth_user_id):
new_activity = create_activity( new_activity = create_activity(
auth_user_id, activity_data, gpx_data) auth_user_id, activity_data, gpx_data)
db.session.add(new_activity) 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() db.session.commit()
response_object = { response_object = {
'status': 'created', 'status': 'created',
@ -178,7 +182,6 @@ def post_activity(auth_user_id):
} }
} }
return jsonify(response_object), 201 return jsonify(response_object), 201
except (exc.IntegrityError, ValueError) as e: except (exc.IntegrityError, ValueError) as e:
db.session.rollback() db.session.rollback()
appLog.error(e) appLog.error(e)

View File

@ -68,6 +68,13 @@ class Activity(db.Model):
ascent = db.Column(db.Numeric(5, 2), nullable=True) # meters ascent = db.Column(db.Numeric(5, 2), nullable=True) # meters
max_speed = db.Column(db.Numeric(5, 2), nullable=True) # km/h max_speed = db.Column(db.Numeric(5, 2), nullable=True) # km/h
ave_speed = db.Column(db.Numeric(5, 2), nullable=True) # km/h ave_speed = db.Column(db.Numeric(5, 2), nullable=True) # km/h
segments = db.relationship('ActivitySegment',
lazy=True,
cascade='all, delete',
backref=db.backref(
'activities',
lazy='joined',
single_parent=True))
records = db.relationship('Record', records = db.relationship('Record',
lazy=True, lazy=True,
backref=db.backref('activities', lazy='joined')) backref=db.backref('activities', lazy='joined'))
@ -102,7 +109,53 @@ class Activity(db.Model):
"ascent": float(self.ascent) if self.ascent else None, "ascent": float(self.ascent) if self.ascent else None,
"max_speed": float(self.max_speed) if self.max_speed else None, "max_speed": float(self.max_speed) if self.max_speed else None,
"ave_speed": float(self.ave_speed) if self.ave_speed else None, "ave_speed": float(self.ave_speed) if self.ave_speed else None,
"with_gpx": self.gpx is not None "with_gpx": self.gpx is not None,
"segments": [segment.serialize() for segment in self.segments]
}
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(5, 3), nullable=True) # kilometers
min_alt = db.Column(db.Numeric(5, 2), nullable=True) # meters
max_alt = db.Column(db.Numeric(5, 2), nullable=True) # meters
descent = db.Column(db.Numeric(5, 2), nullable=True) # meters
ascent = db.Column(db.Numeric(5, 2), nullable=True) # meters
max_speed = db.Column(db.Numeric(5, 2), nullable=True) # km/h
ave_speed = db.Column(db.Numeric(5, 2), nullable=True) # km/h
def __str__(self):
return '<Segment \'{}\' for activity \'{}\'>'.format(
self.segment_id, 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
} }

View File

@ -7,7 +7,20 @@ from flask import current_app
from mpwo_api import appLog from mpwo_api import appLog
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from .models import Activity, Sport from .models import Activity, ActivitySegment, Sport
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( def create_activity(
@ -39,14 +52,7 @@ def create_activity(
if gpx_data: if gpx_data:
new_activity.gpx = gpx_data['filename'] new_activity.gpx = gpx_data['filename']
new_activity.pauses = gpx_data['stop_time'] update_activity_data(new_activity, gpx_data)
new_activity.moving = gpx_data['moving_time']
new_activity.min_alt = gpx_data['elevation_min']
new_activity.max_alt = gpx_data['elevation_max']
new_activity.descent = gpx_data['downhill']
new_activity.ascent = gpx_data['uphill']
new_activity.max_speed = gpx_data['max_speed']
new_activity.ave_speed = gpx_data['average_speed']
else: else:
new_activity.moving = duration new_activity.moving = duration
new_activity.ave_speed = new_activity.distance / ( new_activity.ave_speed = new_activity.distance / (
@ -55,6 +61,17 @@ def create_activity(
return new_activity 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 edit_activity(activity, activity_data): def edit_activity(activity, activity_data):
activity.sport_id = activity_data.get('sport_id') activity.sport_id = activity_data.get('sport_id')
if activity_data.get('title'): if activity_data.get('title'):

View File

@ -3,7 +3,7 @@ import os
import pytest import pytest
from mpwo_api import create_app, db from mpwo_api import create_app, db
from mpwo_api.activities.models import Activity, Record, Sport from mpwo_api.activities.models import Activity, ActivitySegment, Record, Sport
from mpwo_api.users.models import User from mpwo_api.users.models import User
os.environ["FLASK_ENV"] = 'testing' os.environ["FLASK_ENV"] = 'testing'
@ -93,6 +93,19 @@ def activity_cycling_user_1():
return activity 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.distance = 5
db.session.add(activity_segment)
db.session.commit()
return activity_segment
@pytest.fixture() @pytest.fixture()
def activity_running_user_1(): def activity_running_user_1():
activity = Activity( activity = Activity(

View File

@ -13,3 +13,10 @@ def test_add_activity(
assert '0:17:04' == str(activity_cycling_user_1.duration) assert '0:17:04' == str(activity_cycling_user_1.duration)
assert 'Test' == activity_cycling_user_1.title assert 'Test' == activity_cycling_user_1.title
assert '<Activity \'Cycling\' - 2018-01-01 00:00:00>' == str(activity_cycling_user_1) # noqa assert '<Activity \'Cycling\' - 2018-01-01 00:00:00>' == str(activity_cycling_user_1) # noqa
def test_add_segment(
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) # noqa