Changeset c6219ed in OpenWorkouts-current


Ignore:
Timestamp:
Jan 22, 2019, 12:21:51 AM (5 years ago)
Author:
Borja Lopez <borja@…>
Branches:
current, feature/docs, master
Children:
26220ba, 2d2eb0d
Parents:
02048a6 (diff), be40b02 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Merged patches from darcs

Files:
5 added
11 edited

Legend:

Unmodified
Added
Removed
  • .boring

    r02048a6 rc6219ed  
    138138ow.egg-info/*
    139139ow/static/components
     140ow/static/maps
  • ow/models/workout.py

    r02048a6 rc6219ed  
    1 
     1import os
    22from datetime import datetime, timedelta, timezone
    33from decimal import Decimal
     
    1212    copy_blob,
    1313    create_blob,
     14    mps_to_kmph,
     15    save_map_screenshot
    1416)
    1517
     
    5052        self.duration = kw.get('duration', None)  # a timedelta object
    5153        self.distance = kw.get('distance', None)  # kilometers, Decimal
     54        self.speed = kw.get('speed', {})
    5255        self.hr_min = kw.get('hr_min', None)  # bpm, Decimal
    5356        self.hr_max = kw.get('hr_max', None)  # bpm, Decimal
     
    211214        path = None
    212215        if self.tracking_file:
    213             path = self.tracking_file._p_blob_uncommitted
     216            path = self.tracking_file._uncommitted()
    214217            if path is None:
    215                 path = self.tracking_file._p_blob_committed
     218                path = self.tracking_file.committed()
    216219        return path
    217220
     
    228231        path = None
    229232        if self.fit_file:
    230             path = self.fit_file._p_blob_uncommitted
     233            path = self.fit_file._uncommitted()
    231234            if path is None:
    232                 path = self.fit_file._p_blob_committed
     235                path = self.fit_file.committed()
    233236        return path
    234237
     
    348351        4. Grab some basic info from the fit file and store it in the Workout
    349352        """
    350         # backup the fit file
    351         self.fit_file = copy_blob(self.tracking_file)
     353
     354        # we can call load_from_fit afterwards for updates. In such case, check
     355        # if the tracking file is a fit file uploaded to override the previous
     356        # one. If not, just reuse the existing fit file
     357        if self.tracking_filetype == 'fit':
     358            # backup the fit file
     359            self.fit_file = copy_blob(self.tracking_file)
    352360
    353361        # create an instance of our Fit class
     
    378386            self.title = fit.name
    379387
     388        if fit.data['max_speed']:
     389            self.speed['max'] = mps_to_kmph(fit.data['max_speed'])
     390
     391        if fit.data['avg_speed']:
     392            self.speed['avg'] = mps_to_kmph(fit.data['avg_speed'])
     393
    380394        if fit.data['avg_hr']:
    381395            self.hr_avg = Decimal(fit.data['avg_hr'])
     
    406420    def has_fit(self):
    407421        return self.fit_file is not None
     422
     423    @property
     424    def map_screenshot(self):
     425        """
     426        Return the static path to the screenshot image of the map for
     427        this workout (works only for workouts with gps tracking)
     428        """
     429        if not self.has_gpx:
     430            return None
     431
     432        current_path = os.path.abspath(os.path.dirname(__file__))
     433        screenshot_path = os.path.join(
     434            current_path, '../static/maps',
     435            str(self.owner.uid), str(self.workout_id)) + '.png'
     436
     437        if not os.path.exists(screenshot_path):
     438            # screenshot does not exist, generate it
     439            save_map_screenshot(self)
     440
     441        # the value returned is relative to the static files served
     442        # by the app, so we can use request.static_url() with it
     443        static_path = os.path.join('static/maps', str(self.owner.uid),
     444                                   str(self.workout_id))
     445        return 'ow:' + static_path + '.png'
  • ow/static/css/main.css

    r02048a6 rc6219ed  
    752752}
    753753
     754.workout-map img {
     755    width: 100%;
     756}
     757
    754758.owo-del a:hover {
    755759    color:red
  • ow/static/js/ow.js

    r02048a6 rc6219ed  
    1616
    1717    // parameters provided when creating an "instance" of a map
     18    var map_id = spec.map_id;
    1819    var latitude = spec.latitude;
    1920    var longitude = spec.longitude;
     
    2324    var end_icon = spec.end_icon;
    2425    var shadow = spec.shadow;
     26    var elevation = spec.elevation;
     27    var zoom_control = spec.zoom_control;
    2528
    2629    // OpenStreetMap urls and references
    27     var openstreetmap_url = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
    28     var openstreetmap_attr = 'Map data &copy; <a href="http://www.osm.org">OpenStreetMap</a>'
     30    var openstreetmap_url = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
     31    var openstreetmap_attr = 'Map data &copy; <a href="http://www.osm.org">OpenStreetMap</a>';
    2932
    3033    // Some constants reused through the code
     
    3639    var create_map = function create_map(latitude, longitude, zoom) {
    3740        /* Create a Leaflet map, set center point and add tiles */
    38         map = L.map('map');
     41        map = L.map(map_id, {zoomControl: zoom_control});
    3942        map.setView([latitude, longitude], zoom);
    4043        var tile_layer = L.tileLayer(openstreetmap_url, {
     
    9093            },
    9194        });
    92         gpx.on("addline",function(e){
    93             elevation.addData(e.line);
    94             // ow_charts.addData(e.line);
    95         });
     95
     96        gpx.on('loaded', function(e) {
     97            map.fitBounds(e.target.getBounds());
     98        });
     99
     100        if (elevation) {
     101            gpx.on("addline",function(e){
     102                elevation.addData(e.line);
     103                // ow_charts.addData(e.line);
     104            });
     105        };
     106
    96107        gpx.addTo(map);
    97108    };
     
    100111        // create the map, add elevation, load gpx
    101112        create_map(latitude, longitude, zoom);
    102         add_elevation_chart();
     113        if (elevation) {
     114            add_elevation_chart();
     115        }
    103116        // add_ow_charts();
    104117        load_gpx(gpx_url);
  • ow/templates/dashboard.pt

    r02048a6 rc6219ed  
    9090            <div class="workout-intro" tal:content="workout.notes"></div>
    9191
     92            <div class="workout-map" tal:condition="workout.has_gpx">
     93                <a href="" tal:attributes="href request.resource_url(workout)">
     94                    <img src="" tal:attributes="src request.static_url(workout.map_screenshot);
     95                              alt workout.title; title workout.title">
     96                </a>
     97            </div>
     98
    9299            <ul class="workout-options">
    93100              <li class="owo-edit"><a href="" i18n:translate="" tal:attributes="href request.resource_url(workout, 'edit')"><span>edit</span></a></li>
  • ow/templates/workout.pt

    r02048a6 rc6219ed  
    8787          <tal:c tal:content="context.rounded_distance"></tal:c> km
    8888        </li>
     89        <li tal:condition="context.speed">
     90          <tal:t i18n:translate="">Speed:</tal:t>
     91          <tal:t i18n:translate="">Avg.</tal:t>
     92          <tal:c tal:content="round(context.speed['avg'], 1)"></tal:c> km/h |
     93          <tal:t i18n:translate="">Max.</tal:t>
     94          <tal:c tal:content="round(context.speed['max'], 1)"></tal:c> km/h
     95        </li>
    8996        <li tal:condition="hr">
    9097          <tal:t i18n:translate="">Heart Rate:</tal:t>
     
    145152    <script type="text/javascript" tal:condition="context.has_gpx">
    146153     var workout_map = owjs.map({
     154         map_id: 'map',
    147155         latitude: ${start_point['latitude']},
    148156         longitude: ${start_point['longitude']},
     
    152160         end_icon: '${request.static_url('ow:static/components/leaflet-gpx/pin-icon-end.png')}',
    153161         shadow: '${request.static_url('ow:static/components/leaflet-gpx/pin-shadow.png')}',
     162         elevation: true,
     163         zoom_control: true
    154164     });
    155165     workout_map.render();
  • ow/tests/models/test_workout.py

    r02048a6 rc6219ed  
    1212from ow.models.root import OpenWorkouts
    1313from ow.utilities import create_blob
     14
     15from ow.tests.helpers import join
    1416
    1517
     
    210212        # workout still not saved to the db
    211213        workout.tracking_file = Mock()
    212         workout.tracking_file._p_blob_uncommitted = '/tmp/blobtempfile'
    213         workout.tracking_file._p_blob_committed = None
     214        workout.tracking_file._uncommitted.return_value = '/tmp/blobtempfile'
     215        workout.tracking_file.committed.return_value = None
    214216        assert workout.tracking_file_path == '/tmp/blobtempfile'
    215         workout.tracking_file._p_blob_uncommitted = None
    216         workout.tracking_file._p_blob_committed = '/var/db/blobs/blobfile'
     217        workout.tracking_file._uncommitted.return_value = None
     218        workout.tracking_file.committed.return_value = '/var/db/blobs/blobfile'
    217219        assert workout.tracking_file_path == '/var/db/blobs/blobfile'
    218220
     
    223225        # workout still not saved to the db
    224226        workout.fit_file = Mock()
    225         workout.fit_file._p_blob_uncommitted = '/tmp/blobtempfile'
    226         workout.fit_file._p_blob_committed = None
     227        workout.fit_file._uncommitted.return_value = '/tmp/blobtempfile'
     228        workout.fit_file.committed.return_value = None
    227229        assert workout.fit_file_path == '/tmp/blobtempfile'
    228         workout.fit_file._p_blob_uncommitted = None
    229         workout.fit_file._p_blob_committed = '/var/db/blobs/blobfile'
     230        workout.fit_file._uncommitted.return_value = None
     231        workout.fit_file.committed.return_value = '/var/db/blobs/blobfile'
    230232        assert workout.fit_file_path == '/var/db/blobs/blobfile'
    231233
     
    514516        workout = root['john']['1']
    515517        # without tracking file
    516         assert workout.has_tracking_file is False
     518        assert not workout.has_tracking_file
    517519        # with tracking file
    518520        workout.tracking_file = 'faked tracking file'
    519         assert workout.has_tracking_file is True
     521        assert workout.has_tracking_file
    520522
    521523    def test_has_gpx(self, root):
    522524        workout = root['john']['1']
    523525        # without tracking file
    524         assert workout.has_gpx is False
     526        assert not workout.has_gpx
    525527        workout.tracking_filetype = 'fit'
    526         assert workout.has_gpx is False
     528        assert not workout.has_gpx
    527529        # with non-gpx tracking file
    528530        workout.tracking_file = 'faked tracking file'
    529531        workout.tracking_filetype = 'fit'
    530         assert workout.has_gpx is False
     532        assert not workout.has_gpx
    531533        # with gpx tracking file
    532534        workout.tracking_file = 'faked tracking file'
    533535        workout.tracking_filetype = 'gpx'
    534         assert workout.has_gpx is True
     536        assert workout.has_gpx
     537
     538    def test_has_fit(self, root):
     539        workout = root['john']['1']
     540        # without tracking file
     541        assert not workout.has_fit
     542        # tracking_file is a fit, this should not happen, as uploading a fit
     543        # puts the fit file into .fit_file and generates a gpx for
     544        # .tracking_file
     545        workout.tracking_file = 'faked tracking file'
     546        workout.tracking_filetype = 'fit'
     547        assert not workout.has_fit
     548        # now, having a fit file returns true
     549        workout.fit_file = 'faked fit file'
     550        assert workout.has_fit
     551        # no matter what we have in tracking_file
     552        workout.tracking_filetype = 'gpx'
     553        assert workout.has_fit
     554        workout.tracking_file = None
     555        workout.tracking_filetype = None
     556        assert workout.has_fit
     557
     558    @patch('ow.models.workout.os')
     559    @patch('ow.models.workout.save_map_screenshot')
     560    def test_map_screenshot_no_gpx(self, sms, os, root):
     561        workout = root['john']['1']
     562        assert workout.map_screenshot is None
     563        assert not os.path.abspath.called
     564        assert not os.path.dirname.called
     565        assert not os.path.join.called
     566        assert not os.path.exists.called
     567        assert not sms.called
     568
     569    @patch('ow.models.workout.os')
     570    @patch('ow.models.workout.save_map_screenshot')
     571    def test_map_screenshot_save(self, sms, os, root):
     572        """
     573        A workout with a tracking file has no map screenshot, one is
     574        saved to the filesystem.
     575        This test simply asserts the calls to the separate methods that
     576        look for existing screenshots and save a new one
     577        """
     578        os.path.abspath.return_value = 'current_dir'
     579        os.path.join.side_effect = join
     580        # This forces the "save screenshot" code to be run
     581        os.path.exists.return_value = False
     582
     583        workout = root['john']['1']
     584        workout.tracking_file = 'faked gpx file'
     585        workout.tracking_filetype = 'gpx'
     586
     587        uid = str(root['john'].uid)
     588        assert workout.map_screenshot == 'ow:/static/maps/' + uid + '/1.png'
     589        assert os.path.abspath.called
     590        assert os.path.dirname.called
     591        assert os.path.join.call_count == 2
     592        assert os.path.exists.called
     593        sms.assert_called_once_with(workout)
     594
     595    @patch('ow.models.workout.os')
     596    @patch('ow.models.workout.save_map_screenshot')
     597    def test_map_screenshot_do_not_save(self, sms, os, root):
     598        """
     599        A workout with a tracking file has a map screenshot, the path to that
     600        is returned without doing anything else
     601        """
     602        os.path.abspath.return_value = 'current_dir'
     603        os.path.join.side_effect = join
     604        # This forces the "save screenshot" code NOT to be run
     605        os.path.exists.return_value = True
     606
     607        workout = root['john']['1']
     608        workout.tracking_file = 'faked gpx file'
     609        workout.tracking_filetype = 'gpx'
     610
     611        uid = str(root['john'].uid)
     612        assert workout.map_screenshot == 'ow:/static/maps/' + uid + '/1.png'
     613        assert os.path.abspath.called
     614        assert os.path.dirname.called
     615        assert os.path.join.call_count == 2
     616        assert os.path.exists.called
     617        assert not sms.called
  • ow/tests/test_utilities.py

    r02048a6 rc6219ed  
    11import os
     2from datetime import timedelta
     3from unittest.mock import patch
    24from pyexpat import ExpatError
    35from xml.dom.minidom import Element
    46
    57import pytest
     8
     9from ow.models.root import OpenWorkouts
     10from ow.models.user import User
     11from ow.models.workout import Workout
    612
    713from ow.utilities import (
     
    1622    mps_to_kmph,
    1723    kmph_to_mps,
     24    save_map_screenshot
    1825)
    1926
     27from ow.tests.helpers import join
     28
    2029
    2130class TestUtilities(object):
     31
     32    @pytest.fixture
     33    def john(self):
     34        john = User(firstname='John', lastname='Doe',
     35                    email='john.doe@example.net')
     36        john.password = 's3cr3t'
     37        return john
     38
     39    @pytest.fixture
     40    def root(self, john):
     41        root = OpenWorkouts()
     42        root.add_user(john)
     43        john['1'] = Workout(
     44            duration=timedelta(minutes=60),
     45            distance=30
     46        )
     47        return root
    2248
    2349    def test_slugify(self):
     
    5480    def test_kmph_to_mps(self):
    5581        assert kmph_to_mps(30) == 30 * 0.277778
     82
     83    @patch('ow.utilities.os')
     84    @patch('ow.utilities.subprocess')
     85    def test_save_map_screenshot_no_gpx(self, subprocess, os, root, john):
     86        saved = save_map_screenshot(john['1'])
     87        assert not saved
     88        assert not os.path.abspath.called
     89        assert not os.path.dirname.called
     90        assert not os.path.join.called
     91        assert not os.path.exists.called
     92        assert not os.makedirs.called
     93        assert not subprocess.run.called
     94        # even having a fit tracking file, nothing is done
     95        john['1'].tracking_file = 'faked fit file'
     96        john['1'].tracking_filetype = 'fit'
     97        saved = save_map_screenshot(john['1'])
     98        assert not saved
     99        assert not os.path.abspath.called
     100        assert not os.path.dirname.called
     101        assert not os.path.join.called
     102        assert not os.path.exists.called
     103        assert not os.makedirs.called
     104        assert not subprocess.run.called
     105
     106    @patch('ow.utilities.os')
     107    @patch('ow.utilities.subprocess')
     108    def test_save_map_screenshot_with_gpx(self, subprocess, os, root, john):
     109        os.path.abspath.return_value = 'current_dir'
     110        os.path.join.side_effect = join
     111        # This mimics what happens when the directory for this user map
     112        # screenshots does not exist, which means we don'have to create one
     113        # (calling os.makedirs)
     114        os.path.exists.return_value = False
     115
     116        john['1'].tracking_file = 'faked gpx content'
     117        john['1'].tracking_filetype = 'gpx'
     118        saved = save_map_screenshot(john['1'])
     119        assert saved
     120        os.path.abspath.assert_called_once
     121        assert os.path.dirname.called
     122        assert os.path.join.call_count == 3
     123        assert os.path.exists.called
     124        assert os.makedirs.called
     125        subprocess.run.assert_called_once
     126
     127    @patch('ow.utilities.os')
     128    @patch('ow.utilities.subprocess')
     129    def test_save_map_screenshot_with_gpx_makedirs(
     130            self, subprocess, os, root, john):
     131        os.path.abspath.return_value = 'current_dir'
     132        os.path.join.side_effect = join
     133        # If os.path.exists returns True, makedirs is not called
     134        os.path.exists.return_value = True
     135
     136        john['1'].tracking_file = 'faked gpx content'
     137        john['1'].tracking_filetype = 'gpx'
     138        saved = save_map_screenshot(john['1'])
     139        assert saved
     140        os.path.abspath.assert_called_once
     141        assert os.path.dirname.called
     142        assert os.path.join.call_count == 3
     143        assert os.path.exists.called
     144        assert not os.makedirs.called
     145        subprocess.run.assert_called_once
    56146
    57147
  • ow/tests/views/test_workout.py

    r02048a6 rc6219ed  
    429429                assert response.content_type == 'application/xml'
    430430                assert expected_body in response.body
     431
     432    def test_workout_map_no_gpx(self, dummy_request):
     433        request = dummy_request
     434        user = request.root['john']
     435        workout = user.workouts()[0]
     436        response = workout_views.workout_map(workout, request)
     437        assert response == {'start_point': {}}
     438
     439    def test_workout_map(self, dummy_request):
     440        request = dummy_request
     441        user = request.root['john']
     442        workout = user.workouts()[0]
     443        # to ensure has_gpx returns true
     444        workout.tracking_filetype = 'gpx'
     445        gpx_file_path = os.path.join(
     446            os.path.dirname(os.path.dirname(__file__)),
     447            'fixtures/20131013.gpx')
     448        with patch.object(workout, 'tracking_file') as tf:
     449            with open(gpx_file_path, 'r') as gpx_file:
     450                tf.open.return_value = BytesIO(gpx_file.read().encode('utf-8'))
     451                response = workout_views.workout_map(workout, request)
     452                assert response == {
     453                    'start_point': {
     454                        'elevation': None,
     455                        'latitude': 37.108735040304566,
     456                        'longitude': 25.472489344630546
     457                    }
     458                }
  • ow/utilities.py

    r02048a6 rc6219ed  
    11import re
     2import os
     3import logging
     4import subprocess
    25from datetime import datetime
    36from decimal import Decimal
     
    710from xml.dom import minidom
    811from ZODB.blob import Blob
     12
     13log = logging.getLogger(__name__)
    914
    1015
     
    184189        open_blob.write(data)
    185190    return blob
     191
     192
     193def save_map_screenshot(workout):
     194    if workout.has_gpx:
     195        current_path = os.path.abspath(os.path.dirname(__file__))
     196        tool_path = os.path.join(current_path, '../bin/screenshot_map')
     197
     198        screenshots_path = os.path.join(
     199            current_path, 'static/maps', str(workout.owner.uid))
     200        if not os.path.exists(screenshots_path):
     201            os.makedirs(screenshots_path)
     202
     203        screenshot_path = os.path.join(
     204            screenshots_path, str(workout.workout_id))
     205        screenshot_path += '.png'
     206
     207        subprocess.run(
     208            [tool_path, str(workout.owner.uid), str(workout.workout_id),
     209             screenshot_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
     210
     211        return True
     212
     213    return False
  • ow/views/workout.py

    r02048a6 rc6219ed  
    214214        content_disposition='attachment; filename="%s"' % gpx_slug,
    215215        body_file=context.tracking_file.open())
     216
     217
     218@view_config(
     219    context=Workout,
     220    name='map',
     221    renderer='ow:templates/workout-map.pt')
     222def workout_map(context, request):
     223    """
     224    Render a page that has only a map with tracking info
     225    """
     226    start_point = {}
     227    if context.has_gpx:
     228        with context.tracking_file.open() as gpx_file:
     229            gpx_contents = gpx_file.read()
     230            gpx_contents = gpx_contents.decode('utf-8')
     231            gpx = gpxpy.parse(gpx_contents)
     232            if gpx.tracks:
     233                track = gpx.tracks[0]
     234                center_point = track.get_center()
     235                start_point = {'latitude': center_point.latitude,
     236                               'longitude': center_point.longitude,
     237                               'elevation': center_point.elevation}
     238    return {'start_point': start_point}
Note: See TracChangeset for help on using the changeset viewer.