API & Client: display map image for activities w/ gpx on dashboard
This commit is contained in:
		
							
								
								
									
										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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user