From 7d410956b5e3b8e81453d9026ac22928ef725d7b Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 30 May 2018 13:35:27 +0200 Subject: [PATCH] API & Client: display map image for activities w/ gpx on dashboard --- mpwo_api/migrations/versions/9f8c9c37da44_.py | 28 ++++++++++ mpwo_api/mpwo_api/activities/activities.py | 23 +++++++- mpwo_api/mpwo_api/activities/models.py | 3 +- mpwo_api/mpwo_api/activities/utils.py | 11 ++++ .../tests/test_activities_api_0_get.py | 54 +++++++++++++++++++ .../tests/test_activities_api_1_post.py | 41 +++++++++++++- .../src/components/Dashboard/ActivityCard.jsx | 41 ++++++++++---- 7 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 mpwo_api/migrations/versions/9f8c9c37da44_.py diff --git a/mpwo_api/migrations/versions/9f8c9c37da44_.py b/mpwo_api/migrations/versions/9f8c9c37da44_.py new file mode 100644 index 00000000..7af78c9c --- /dev/null +++ b/mpwo_api/migrations/versions/9f8c9c37da44_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 9f8c9c37da44 +Revises: 5a42db64e872 +Create Date: 2018-05-30 12:48:11.714627 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9f8c9c37da44' +down_revision = '5a42db64e872' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('activities', sa.Column('map_id', sa.String(length=50), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('activities', 'map_id') + # ### end Alembic commands ### diff --git a/mpwo_api/mpwo_api/activities/activities.py b/mpwo_api/mpwo_api/activities/activities.py index 7e2ce9e4..236c1096 100644 --- a/mpwo_api/mpwo_api/activities/activities.py +++ b/mpwo_api/mpwo_api/activities/activities.py @@ -2,7 +2,7 @@ import json import os import shutil -from flask import Blueprint, current_app, jsonify, request +from flask import Blueprint, current_app, jsonify, request, send_file from mpwo_api import appLog, db from sqlalchemy import exc @@ -128,6 +128,27 @@ def get_activity_chart_data(auth_user_id, activity_id): return get_activity_data(auth_user_id, activity_id, 'chart') +@activities_blueprint.route('/activities/map/', methods=['GET']) +def get_map(map_id): + try: + activity = Activity.query.filter_by(map_id=map_id).first() + if not activity: + response_object = { + 'status': 'fail', + 'message': 'Map does not exist' + } + return jsonify(response_object), 404 + else: + return send_file(activity.map) + except Exception as e: + appLog.error(e) + response_object = { + 'status': 'error', + 'message': 'internal error.' + } + return jsonify(response_object), 500 + + @activities_blueprint.route('/activities', methods=['POST']) @authenticate def post_activity(auth_user_id): diff --git a/mpwo_api/mpwo_api/activities/models.py b/mpwo_api/mpwo_api/activities/models.py index 7b41e871..10fe179d 100644 --- a/mpwo_api/mpwo_api/activities/models.py +++ b/mpwo_api/mpwo_api/activities/models.py @@ -135,6 +135,7 @@ class Activity(db.Model): 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) segments = db.relationship('ActivitySegment', lazy=True, cascade='all, delete', @@ -199,7 +200,7 @@ class Activity(db.Model): "next_activity": next_activity.id if next_activity else None, "segments": [segment.serialize() for segment in self.segments], "records": [record.serialize() for record in self.records], - "with_map": self.map is not None + "map": self.map_id if self.map else None } @classmethod diff --git a/mpwo_api/mpwo_api/activities/utils.py b/mpwo_api/mpwo_api/activities/utils.py index 8637cde7..18b22eae 100644 --- a/mpwo_api/mpwo_api/activities/utils.py +++ b/mpwo_api/mpwo_api/activities/utils.py @@ -1,3 +1,4 @@ +import hashlib import os import tempfile import zipfile @@ -260,6 +261,14 @@ def generate_map(map_filepath, map_data): image.save(map_filepath) +def get_map_hash(map_filepath): + md5 = hashlib.md5() + with open(map_filepath, 'rb') as f: + for chunk in iter(lambda: f.read(128 * md5.block_size), b''): + md5.update(chunk) + return md5.hexdigest() + + def process_one_gpx_file(auth_user_id, activity_data, file_path, filename): try: gpx_data, map_data = get_gpx_info(file_path) @@ -290,6 +299,8 @@ def process_one_gpx_file(auth_user_id, activity_data, file_path, filename): new_activity = create_activity( auth_user_id, activity_data, gpx_data) new_activity.map = map_filepath + new_activity.map_id = get_map_hash(map_filepath) + print(new_activity.map_id) db.session.add(new_activity) db.session.flush() diff --git a/mpwo_api/mpwo_api/tests/test_activities_api_0_get.py b/mpwo_api/mpwo_api/tests/test_activities_api_0_get.py index 76f7cb11..295c131b 100644 --- a/mpwo_api/mpwo_api/tests/test_activities_api_0_get.py +++ b/mpwo_api/mpwo_api/tests/test_activities_api_0_get.py @@ -169,6 +169,33 @@ def test_get_activities_pagination( assert len(data['data']['activities']) == 0 +def test_get_activities_pagination_error( + app, user_1, sport_1_cycling, seven_activities_user_1 +): + 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/activities?page=A', + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 500 + assert 'error' in data['status'] + assert 'Error. Please try again or contact the administrator.' in data['message'] # noqa + + def test_get_an_activity( app, user_1, sport_1_cycling, activity_cycling_user_1 ): @@ -353,3 +380,30 @@ def test_get_an_activity_activity_invalid_gpx( assert 'error' in data['status'] assert 'internal error' in data['message'] assert data['data']['chart_data'] == '' + + +def test_get_map_no_activity( + app, user_1 +): + 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/activities/map/123', + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 404 + assert 'fail' in data['status'] + assert 'Map does not exist' in data['message'] diff --git a/mpwo_api/mpwo_api/tests/test_activities_api_1_post.py b/mpwo_api/mpwo_api/tests/test_activities_api_1_post.py index c6e79b47..93c47cc6 100644 --- a/mpwo_api/mpwo_api/tests/test_activities_api_1_post.py +++ b/mpwo_api/mpwo_api/tests/test_activities_api_1_post.py @@ -2,6 +2,8 @@ import json import os from io import BytesIO +from mpwo_api.activities.models import Activity + def assert_activity_data_with_gpx(data): assert 'creation_date' in data['data']['activities'][0] @@ -19,7 +21,7 @@ def assert_activity_data_with_gpx(data): assert data['data']['activities'][0]['moving'] == '0:04:10' assert data['data']['activities'][0]['pauses'] is None assert data['data']['activities'][0]['with_gpx'] is True - assert data['data']['activities'][0]['with_map'] is True + assert data['data']['activities'][0]['map'] is not None assert len(data['data']['activities'][0]['segments']) == 1 segment = data['data']['activities'][0]['segments'][0] @@ -77,7 +79,7 @@ def assert_activity_data_wo_gpx(data): assert data['data']['activities'][0]['moving'] == '1:00:00' assert data['data']['activities'][0]['pauses'] is None assert data['data']['activities'][0]['with_gpx'] is False - assert data['data']['activities'][0]['with_map'] is False + assert data['data']['activities'][0]['map'] is None assert len(data['data']['activities'][0]['segments']) == 0 @@ -176,6 +178,8 @@ def test_get_an_activity_with_gpx(app, user_1, sport_1_cycling, gpx_file): assert 'just an activity' == data['data']['activities'][0]['title'] assert_activity_data_with_gpx(data) + map_id = data['data']['activities'][0]['map'] + response = client.get( '/api/activities/1/gpx', headers=dict( @@ -191,6 +195,39 @@ def test_get_an_activity_with_gpx(app, user_1, sport_1_cycling, gpx_file): assert '' in data['message'] assert len(data['data']['gpx']) != '' + response = client.get( + f'/api/activities/map/{map_id}', + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + assert response.status_code == 200 + + # error case in the same test to avoid generate a new map file + activity = Activity.query.filter_by(id=1).first() + activity.map = 'incorrect path' + + assert response.status_code == 200 + assert 'success' in data['status'] + assert '' in data['message'] + assert len(data['data']['gpx']) != '' + + response = client.get( + f'/api/activities/map/{map_id}', + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 500 + assert data['status'] == 'error' + assert data['message'] == 'internal error.' + def test_get_chart_data_activty_with_gpx( app, user_1, sport_1_cycling, gpx_file diff --git a/mpwo_client/src/components/Dashboard/ActivityCard.jsx b/mpwo_client/src/components/Dashboard/ActivityCard.jsx index e0ee9c15..2e3ab17a 100644 --- a/mpwo_client/src/components/Dashboard/ActivityCard.jsx +++ b/mpwo_client/src/components/Dashboard/ActivityCard.jsx @@ -1,6 +1,8 @@ import React from 'react' import { Link } from 'react-router-dom' +import { apiUrl } from '../../utils' + export default function ActivityCard (props) { const { activity, sports } = props @@ -14,16 +16,35 @@ export default function ActivityCard (props) {
-

-