From b24f5c2988dfee39f8843845b67960ce7f42a52c Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 11 May 2018 23:12:25 +0200 Subject: [PATCH] API: Activities records init + minor db change for Activity --- mpwo_api/migrations/versions/9741fc7834da_.py | 2 +- mpwo_api/migrations/versions/b7cfe0c17708_.py | 2 +- mpwo_api/migrations/versions/caf0e0dc621a_.py | 42 ++++++++++ mpwo_api/mpwo_api/__init__.py | 2 + mpwo_api/mpwo_api/activities/models.py | 66 ++++++++++++++++ mpwo_api/mpwo_api/activities/records.py | 24 ++++++ mpwo_api/mpwo_api/tests/test_records.py | 77 +++++++++++++++++++ mpwo_api/mpwo_api/tests/utils.py | 13 +++- mpwo_api/mpwo_api/users/models.py | 3 + 9 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 mpwo_api/migrations/versions/caf0e0dc621a_.py create mode 100644 mpwo_api/mpwo_api/activities/records.py create mode 100644 mpwo_api/mpwo_api/tests/test_records.py diff --git a/mpwo_api/migrations/versions/9741fc7834da_.py b/mpwo_api/migrations/versions/9741fc7834da_.py index 845eab26..c24f08fc 100644 --- a/mpwo_api/migrations/versions/9741fc7834da_.py +++ b/mpwo_api/migrations/versions/9741fc7834da_.py @@ -1,4 +1,4 @@ -"""empty message +"""create User table Revision ID: 9741fc7834da Revises: diff --git a/mpwo_api/migrations/versions/b7cfe0c17708_.py b/mpwo_api/migrations/versions/b7cfe0c17708_.py index 0aa01e96..d2e3d618 100644 --- a/mpwo_api/migrations/versions/b7cfe0c17708_.py +++ b/mpwo_api/migrations/versions/b7cfe0c17708_.py @@ -1,4 +1,4 @@ -"""empty message +"""create Activity & Sport tables Revision ID: b7cfe0c17708 Revises: 9741fc7834da diff --git a/mpwo_api/migrations/versions/caf0e0dc621a_.py b/mpwo_api/migrations/versions/caf0e0dc621a_.py new file mode 100644 index 00000000..b4872a30 --- /dev/null +++ b/mpwo_api/migrations/versions/caf0e0dc621a_.py @@ -0,0 +1,42 @@ +"""create Record table + +Revision ID: caf0e0dc621a +Revises: b7cfe0c17708 +Create Date: 2018-05-11 22:33:08.267488 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'caf0e0dc621a' +down_revision = 'b7cfe0c17708' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('records', + sa.Column('id', sa.Integer(), nullable=False), + 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('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.ForeignKeyConstraint(['activity_id'], ['activities.id'], ), + sa.ForeignKeyConstraint(['sport_id'], ['sports.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'sport_id', 'record_type', name='user_sports_records') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('records') + # ### end Alembic commands ### diff --git a/mpwo_api/mpwo_api/__init__.py b/mpwo_api/mpwo_api/__init__.py index 0bcd69ff..0763729e 100644 --- a/mpwo_api/mpwo_api/__init__.py +++ b/mpwo_api/mpwo_api/__init__.py @@ -29,11 +29,13 @@ def create_app(): from .users.auth import auth_blueprint # noqa from .users.users import users_blueprint # noqa from .activities.activities import activities_blueprint # noqa + from .activities.records import records_blueprint # noqa from .activities.sports import sports_blueprint # noqa app.register_blueprint(users_blueprint, url_prefix='/api') app.register_blueprint(auth_blueprint, url_prefix='/api') app.register_blueprint(activities_blueprint, url_prefix='/api') + app.register_blueprint(records_blueprint, url_prefix='/api') app.register_blueprint(sports_blueprint, url_prefix='/api') if app.debug: diff --git a/mpwo_api/mpwo_api/activities/models.py b/mpwo_api/mpwo_api/activities/models.py index 7d60f2b5..670b3949 100644 --- a/mpwo_api/mpwo_api/activities/models.py +++ b/mpwo_api/mpwo_api/activities/models.py @@ -1,6 +1,14 @@ import datetime from mpwo_api import db +from sqlalchemy.types import Enum + +record_types = [ + 'AS', # 'Best Average Speed' + 'FD', # 'Farthest Distance' + 'LD', # 'Longest Duration' + 'MS', # 'Max speed' +] class Sport(db.Model): @@ -10,6 +18,9 @@ class Sport(db.Model): activities = db.relationship('Activity', lazy=True, backref=db.backref('sports', lazy='joined')) + records = db.relationship('Record', + lazy=True, + backref=db.backref('sports', lazy='joined')) def __repr__(self): return self.label @@ -55,6 +66,9 @@ class Activity(db.Model): ascent = db.Column(db.Numeric(5, 2), nullable=True) # meters max_speed = db.Column(db.Numeric(5, 3), nullable=True) # km/h ave_speed = db.Column(db.Numeric(5, 3), nullable=True) # km/h + records = db.relationship('Record', + lazy=True, + backref=db.backref('activities', lazy='joined')) def __str__(self): return str(self.sports.label) + \ @@ -87,3 +101,55 @@ class Activity(db.Model): "ave_speed": float(self.ave_speed) if self.ave_speed else None, "with_gpx": self.gpx is not None } + + +class Record(db.Model): + __tablename__ = "records" + __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( + db.Integer, + db.ForeignKey('sports.id'), + nullable=False) + activity_id = db.Column( + db.Integer, + db.ForeignKey('activities.id'), + nullable=False) + record_type = db.Column(Enum(*record_types, name="record_types")) + activity_date = db.Column(db.DateTime, nullable=False) + value_interval = db.Column(db.Interval, nullable=True) + value_float = db.Column(db.Numeric(5, 3), nullable=True) + + def __str__(self): + return str(self.sports.label) + \ + " - " + self.record_type + \ + " - " + self.activity_date.strftime('%Y-%m-%d') + + def __init__(self, user_id, sport_id, activity, record_type): + self.user_id = user_id + self.sport_id = sport_id + self.activity_id = activity.id + self.record_type = record_type + self.activity_date = activity.activity_date + + def serialize(self): + return { + "id": self.id, + "user_id": self.user_id, + "sport_id": self.sport_id, + "activity_id": self.activity_id, + "record_type": self.record_type, + "activity_date": self.activity_date, + "value_interval": str( + self.value_interval) if self.value_interval else None, + "value_float": str( + self.value_float) if self.value_float else None, + } diff --git a/mpwo_api/mpwo_api/activities/records.py b/mpwo_api/mpwo_api/activities/records.py new file mode 100644 index 00000000..61462e28 --- /dev/null +++ b/mpwo_api/mpwo_api/activities/records.py @@ -0,0 +1,24 @@ +from flask import Blueprint, jsonify + +from ..users.utils import authenticate +from .models import Record + +records_blueprint = Blueprint('records', __name__) + + +@records_blueprint.route('/records', methods=['GET']) +@authenticate +def get_sports(auth_user_id): + """Get all records for authenticated user""" + records = Record.query.filter_by(user_id=auth_user_id)\ + .order_by(Record.record_type).all() + records_list = [] + for record in records: + records_list.append(record.serialize()) + response_object = { + 'status': 'success', + 'data': { + 'records': records_list + } + } + return jsonify(response_object), 200 diff --git a/mpwo_api/mpwo_api/tests/test_records.py b/mpwo_api/mpwo_api/tests/test_records.py new file mode 100644 index 00000000..155f3103 --- /dev/null +++ b/mpwo_api/mpwo_api/tests/test_records.py @@ -0,0 +1,77 @@ +import datetime +import json + +from mpwo_api.tests.utils import add_activity, add_record, add_sport, add_user + + +def test_get_all_activities_for_authenticated_user(app): + add_user('test', 'test@test.com', '12345678') + add_user('toto', 'toto@toto.com', '12345678') + add_sport('cycling') + add_sport('running') + + activity = add_activity( + user_id=1, + sport_id=2, + activity_date=datetime.datetime.strptime('01/01/2018', '%d/%m/%Y'), + distance=10, + duration=datetime.timedelta(seconds=1024) + ) + add_record(1, 2, activity, 'LD') + + activity = add_activity( + user_id=2, + sport_id=1, + activity_date=datetime.datetime.strptime('23/01/2018', '%d/%m/%Y'), + distance=15, + duration=datetime.timedelta(seconds=3600), + ) + add_record(2, 1, activity, 'MS') + + activity = add_activity( + user_id=1, + sport_id=1, + activity_date=datetime.datetime.strptime('01/04/2018', '%d/%m/%Y'), + distance=12, + duration=datetime.timedelta(seconds=6000) + ) + add_record(1, 1, activity, 'FD') + + client = app.test_client() + resp_login = client.post( + '/api/auth/login', + data=json.dumps(dict( + email='test@test.com', + password='12345678' + )), + content_type='application/json' + ) + response = client.get( + '/api/records', + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 200 + assert 'success' in data['status'] + assert len(data['data']['records']) == 2 + + assert 'Sun, 01 Apr 2018 00:00:00 GMT' == data['data']['records'][0]['activity_date'] # noqa + assert 1 == data['data']['records'][0]['user_id'] + assert 1 == data['data']['records'][0]['sport_id'] + assert 3 == data['data']['records'][0]['activity_id'] + assert 'FD' == data['data']['records'][0]['record_type'] + assert 'value_interval' in data['data']['records'][0] + assert 'value_float' in data['data']['records'][0] + + assert 'Mon, 01 Jan 2018 00:00:00 GMT' == data['data']['records'][1]['activity_date'] # noqa + assert 1 == data['data']['records'][1]['user_id'] + assert 2 == data['data']['records'][1]['sport_id'] + assert 1 == data['data']['records'][1]['activity_id'] + assert 'LD' == data['data']['records'][1]['record_type'] + assert 'value_interval' in data['data']['records'][1] + assert 'value_float' in data['data']['records'][1] diff --git a/mpwo_api/mpwo_api/tests/utils.py b/mpwo_api/mpwo_api/tests/utils.py index ec8d530c..b7c43379 100644 --- a/mpwo_api/mpwo_api/tests/utils.py +++ b/mpwo_api/mpwo_api/tests/utils.py @@ -1,7 +1,7 @@ import datetime from mpwo_api import db -from mpwo_api.activities.models import Activity, Sport +from mpwo_api.activities.models import Activity, Record, Sport from mpwo_api.users.models import User @@ -58,3 +58,14 @@ def add_activity(user_id, sport_id, activity_date, distance, duration): def get_gpx_filepath(activity_id): activity = Activity.query.filter_by(id=activity_id).first() return activity.gpx + + +def add_record(user_id, sport_id, activity, record_type): + record = Record( + user_id=user_id, + sport_id=sport_id, + activity=activity, + record_type=record_type) + db.session.add(record) + db.session.commit() + return record diff --git a/mpwo_api/mpwo_api/users/models.py b/mpwo_api/mpwo_api/users/models.py index fb991f42..31beecd6 100644 --- a/mpwo_api/mpwo_api/users/models.py +++ b/mpwo_api/mpwo_api/users/models.py @@ -22,6 +22,9 @@ class User(db.Model): activities = db.relationship('Activity', lazy=True, backref=db.backref('users', lazy='joined')) + records = db.relationship('Record', + lazy=True, + backref=db.backref('users', lazy='joined')) def __repr__(self): return '' % self.username