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 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>
 | 
			
		||||
  )
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user