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 os
|
||||||
import shutil
|
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 mpwo_api import appLog, db
|
||||||
from sqlalchemy import exc
|
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')
|
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'])
|
@activities_blueprint.route('/activities', methods=['POST'])
|
||||||
@authenticate
|
@authenticate
|
||||||
def post_activity(auth_user_id):
|
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
|
ave_speed = db.Column(db.Numeric(6, 2), nullable=True) # km/h
|
||||||
bounds = db.Column(postgresql.ARRAY(db.Float), nullable=True)
|
bounds = db.Column(postgresql.ARRAY(db.Float), nullable=True)
|
||||||
map = db.Column(db.String(255), nullable=True)
|
map = db.Column(db.String(255), nullable=True)
|
||||||
|
map_id = db.Column(db.String(50), nullable=True)
|
||||||
segments = db.relationship('ActivitySegment',
|
segments = db.relationship('ActivitySegment',
|
||||||
lazy=True,
|
lazy=True,
|
||||||
cascade='all, delete',
|
cascade='all, delete',
|
||||||
@ -199,7 +200,7 @@ class Activity(db.Model):
|
|||||||
"next_activity": next_activity.id if next_activity else None,
|
"next_activity": next_activity.id if next_activity else None,
|
||||||
"segments": [segment.serialize() for segment in self.segments],
|
"segments": [segment.serialize() for segment in self.segments],
|
||||||
"records": [record.serialize() for record in self.records],
|
"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
|
@classmethod
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
@ -260,6 +261,14 @@ def generate_map(map_filepath, map_data):
|
|||||||
image.save(map_filepath)
|
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):
|
def process_one_gpx_file(auth_user_id, activity_data, file_path, filename):
|
||||||
try:
|
try:
|
||||||
gpx_data, map_data = get_gpx_info(file_path)
|
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(
|
new_activity = create_activity(
|
||||||
auth_user_id, activity_data, gpx_data)
|
auth_user_id, activity_data, gpx_data)
|
||||||
new_activity.map = map_filepath
|
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.add(new_activity)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
|
@ -169,6 +169,33 @@ def test_get_activities_pagination(
|
|||||||
assert len(data['data']['activities']) == 0
|
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(
|
def test_get_an_activity(
|
||||||
app, user_1, sport_1_cycling, activity_cycling_user_1
|
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 'error' in data['status']
|
||||||
assert 'internal error' in data['message']
|
assert 'internal error' in data['message']
|
||||||
assert data['data']['chart_data'] == ''
|
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
|
import os
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
from mpwo_api.activities.models import Activity
|
||||||
|
|
||||||
|
|
||||||
def assert_activity_data_with_gpx(data):
|
def assert_activity_data_with_gpx(data):
|
||||||
assert 'creation_date' in data['data']['activities'][0]
|
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]['moving'] == '0:04:10'
|
||||||
assert data['data']['activities'][0]['pauses'] is None
|
assert data['data']['activities'][0]['pauses'] is None
|
||||||
assert data['data']['activities'][0]['with_gpx'] is True
|
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
|
assert len(data['data']['activities'][0]['segments']) == 1
|
||||||
|
|
||||||
segment = data['data']['activities'][0]['segments'][0]
|
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]['moving'] == '1:00:00'
|
||||||
assert data['data']['activities'][0]['pauses'] is None
|
assert data['data']['activities'][0]['pauses'] is None
|
||||||
assert data['data']['activities'][0]['with_gpx'] is False
|
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
|
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 'just an activity' == data['data']['activities'][0]['title']
|
||||||
assert_activity_data_with_gpx(data)
|
assert_activity_data_with_gpx(data)
|
||||||
|
|
||||||
|
map_id = data['data']['activities'][0]['map']
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
'/api/activities/1/gpx',
|
'/api/activities/1/gpx',
|
||||||
headers=dict(
|
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 '' in data['message']
|
||||||
assert len(data['data']['gpx']) != ''
|
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(
|
def test_get_chart_data_activty_with_gpx(
|
||||||
app, user_1, sport_1_cycling, gpx_file
|
app, user_1, sport_1_cycling, gpx_file
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { apiUrl } from '../../utils'
|
||||||
|
|
||||||
export default function ActivityCard (props) {
|
export default function ActivityCard (props) {
|
||||||
const { activity, sports } = props
|
const { activity, sports } = props
|
||||||
|
|
||||||
@ -14,16 +16,35 @@ export default function ActivityCard (props) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<p>
|
<div className="row">
|
||||||
<i className="fa fa-calendar" aria-hidden="true" />{' '}
|
{activity.map && (
|
||||||
Start at {activity.activity_date}
|
<div className="col">
|
||||||
</p>
|
<img
|
||||||
<p>
|
alt="Map"
|
||||||
<i className="fa fa-clock-o" aria-hidden="true" />{' '}
|
src={`${apiUrl}activities/map/${activity.map}` +
|
||||||
Duration: {activity.duration} -{' '}
|
`?${Date.now()}`}
|
||||||
<i className="fa fa-road" aria-hidden="true" />{' '}
|
className="img-fluid"
|
||||||
Distance: {activity.distance} km
|
/>
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user