API & Client: display map image for activities w/ gpx on dashboard

This commit is contained in:
Sam 2018-05-30 13:35:27 +02:00
parent 2c5c7f609a
commit 7d410956b5
7 changed files with 187 additions and 14 deletions

View File

@ -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 ###

View File

@ -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/<map_id>', 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):

View File

@ -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

View File

@ -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()

View File

@ -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']

View File

@ -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

View File

@ -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,17 +16,36 @@ export default function ActivityCard (props) {
</Link>
</div>
<div className="card-body">
<div className="row">
{activity.map && (
<div className="col">
<img
alt="Map"
src={`${apiUrl}activities/map/${activity.map}` +
`?${Date.now()}`}
className="img-fluid"
/>
</div>
)}
<div className="col">
<p>
<i className="fa fa-calendar" aria-hidden="true" />{' '}
Start at {activity.activity_date}
</p>
<p>
<i className="fa fa-clock-o" aria-hidden="true" />{' '}
Duration: {activity.duration} -{' '}
Duration: {activity.duration}
{activity.map ? (
<span><br /><br /></span>
) : (
' - '
)}
<i className="fa fa-road" aria-hidden="true" />{' '}
Distance: {activity.distance} km
</p>
</div>
</div>
</div>
</div>
)
}