From e4a65f4c7900ac80554738a4605863ae8c838d94 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 14 May 2018 13:25:00 +0200 Subject: [PATCH] API: Activity segments table init --- mpwo_api/migrations/versions/caf0e0dc621a_.py | 5 +- mpwo_api/migrations/versions/dd73d23a7a3d_.py | 43 +++++++++++++++ mpwo_api/mpwo_api/activities/activities.py | 9 ++- mpwo_api/mpwo_api/activities/models.py | 55 ++++++++++++++++++- mpwo_api/mpwo_api/activities/utils.py | 35 +++++++++--- mpwo_api/mpwo_api/tests/conftest.py | 15 ++++- .../mpwo_api/tests/test_activities_model.py | 7 +++ 7 files changed, 152 insertions(+), 17 deletions(-) create mode 100644 mpwo_api/migrations/versions/dd73d23a7a3d_.py diff --git a/mpwo_api/migrations/versions/caf0e0dc621a_.py b/mpwo_api/migrations/versions/caf0e0dc621a_.py index b4872a30..b45a544f 100644 --- a/mpwo_api/migrations/versions/caf0e0dc621a_.py +++ b/mpwo_api/migrations/versions/caf0e0dc621a_.py @@ -23,10 +23,9 @@ def upgrade(): 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('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('value_interval', sa.Interval(), nullable=True), - sa.Column('value_float', sa.Numeric(precision=5, scale=3), nullable=True), + 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'], ), diff --git a/mpwo_api/migrations/versions/dd73d23a7a3d_.py b/mpwo_api/migrations/versions/dd73d23a7a3d_.py new file mode 100644 index 00000000..fd3355bf --- /dev/null +++ b/mpwo_api/migrations/versions/dd73d23a7a3d_.py @@ -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 ### diff --git a/mpwo_api/mpwo_api/activities/activities.py b/mpwo_api/mpwo_api/activities/activities.py index 2e40021c..0e60408a 100644 --- a/mpwo_api/mpwo_api/activities/activities.py +++ b/mpwo_api/mpwo_api/activities/activities.py @@ -8,8 +8,8 @@ from sqlalchemy import exc from ..users.utils import authenticate, verify_extension from .models import Activity, Sport from .utils import ( - create_activity, edit_activity, get_file_path, get_gpx_info, - get_new_file_path + create_activity, create_segment, edit_activity, get_file_path, + get_gpx_info, get_new_file_path ) activities_blueprint = Blueprint('activities', __name__) @@ -170,6 +170,10 @@ def post_activity(auth_user_id): new_activity = create_activity( auth_user_id, activity_data, gpx_data) 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() response_object = { 'status': 'created', @@ -178,7 +182,6 @@ def post_activity(auth_user_id): } } return jsonify(response_object), 201 - except (exc.IntegrityError, ValueError) as e: db.session.rollback() appLog.error(e) diff --git a/mpwo_api/mpwo_api/activities/models.py b/mpwo_api/mpwo_api/activities/models.py index b389f25d..c4df0ceb 100644 --- a/mpwo_api/mpwo_api/activities/models.py +++ b/mpwo_api/mpwo_api/activities/models.py @@ -68,6 +68,13 @@ class Activity(db.Model): 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 + segments = db.relationship('ActivitySegment', + lazy=True, + cascade='all, delete', + backref=db.backref( + 'activities', + lazy='joined', + single_parent=True)) records = db.relationship('Record', lazy=True, backref=db.backref('activities', lazy='joined')) @@ -102,7 +109,53 @@ class Activity(db.Model): "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 + "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 ''.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 } diff --git a/mpwo_api/mpwo_api/activities/utils.py b/mpwo_api/mpwo_api/activities/utils.py index f9a06de1..95e0b848 100644 --- a/mpwo_api/mpwo_api/activities/utils.py +++ b/mpwo_api/mpwo_api/activities/utils.py @@ -7,7 +7,20 @@ from flask import current_app from mpwo_api import appLog 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( @@ -39,14 +52,7 @@ def create_activity( if gpx_data: new_activity.gpx = gpx_data['filename'] - new_activity.pauses = gpx_data['stop_time'] - 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'] + update_activity_data(new_activity, gpx_data) else: new_activity.moving = duration new_activity.ave_speed = new_activity.distance / ( @@ -55,6 +61,17 @@ def create_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): activity.sport_id = activity_data.get('sport_id') if activity_data.get('title'): diff --git a/mpwo_api/mpwo_api/tests/conftest.py b/mpwo_api/mpwo_api/tests/conftest.py index fa0d50a6..d4d9a5dd 100644 --- a/mpwo_api/mpwo_api/tests/conftest.py +++ b/mpwo_api/mpwo_api/tests/conftest.py @@ -3,7 +3,7 @@ import os import pytest 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 os.environ["FLASK_ENV"] = 'testing' @@ -93,6 +93,19 @@ def activity_cycling_user_1(): 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() def activity_running_user_1(): activity = Activity( diff --git a/mpwo_api/mpwo_api/tests/test_activities_model.py b/mpwo_api/mpwo_api/tests/test_activities_model.py index 3835ac0d..d16dc92f 100644 --- a/mpwo_api/mpwo_api/tests/test_activities_model.py +++ b/mpwo_api/mpwo_api/tests/test_activities_model.py @@ -13,3 +13,10 @@ def test_add_activity( assert '0:17:04' == str(activity_cycling_user_1.duration) assert 'Test' == activity_cycling_user_1.title assert '' == 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 '' == str(activity_cycling_user_1_segment) # noqa