API & Client: display map image for activities w/ gpx on dashboard
This commit is contained in:
parent
2c5c7f609a
commit
7d410956b5
28
mpwo_api/migrations/versions/9f8c9c37da44_.py
Normal file
28
mpwo_api/migrations/versions/9f8c9c37da44_.py
Normal 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 ###
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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']
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
</Link>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<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} -{' '}
|
||||
<i className="fa fa-road" aria-hidden="true" />{' '}
|
||||
Distance: {activity.distance} km
|
||||
</p>
|
||||
<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}
|
||||
{activity.map ? (
|
||||
<span><br /><br /></span>
|
||||
) : (
|
||||
' - '
|
||||
)}
|
||||
<i className="fa fa-road" aria-hidden="true" />{' '}
|
||||
Distance: {activity.distance} km
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user