Changeset 2d2eb0d in OpenWorkouts-current
- Timestamp:
- Jan 23, 2019, 2:07:26 PM (5 years ago)
- Branches:
- current, feature/docs, master
- Children:
- 4dcf28d
- Parents:
- 5cbc4a0 (diff), c6219ed (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. - Files:
-
- 5 added
- 10 edited
Legend:
- Unmodified
- Added
- Removed
-
.boring
r5cbc4a0 r2d2eb0d 138 138 ow.egg-info/* 139 139 ow/static/components 140 ow/static/maps -
ow/models/workout.py
r5cbc4a0 r2d2eb0d 1 1 import os 2 2 from datetime import datetime, timedelta, timezone 3 3 from decimal import Decimal … … 12 12 copy_blob, 13 13 create_blob, 14 mps_to_kmph, 15 save_map_screenshot 14 16 ) 15 17 … … 50 52 self.duration = kw.get('duration', None) # a timedelta object 51 53 self.distance = kw.get('distance', None) # kilometers, Decimal 54 self.speed = kw.get('speed', {}) 52 55 self.hr_min = kw.get('hr_min', None) # bpm, Decimal 53 56 self.hr_max = kw.get('hr_max', None) # bpm, Decimal … … 211 214 path = None 212 215 if self.tracking_file: 213 path = self.tracking_file._ p_blob_uncommitted216 path = self.tracking_file._uncommitted() 214 217 if path is None: 215 path = self.tracking_file. _p_blob_committed218 path = self.tracking_file.committed() 216 219 return path 217 220 … … 228 231 path = None 229 232 if self.fit_file: 230 path = self.fit_file._ p_blob_uncommitted233 path = self.fit_file._uncommitted() 231 234 if path is None: 232 path = self.fit_file. _p_blob_committed235 path = self.fit_file.committed() 233 236 return path 234 237 … … 348 351 4. Grab some basic info from the fit file and store it in the Workout 349 352 """ 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) 352 360 353 361 # create an instance of our Fit class … … 378 386 self.title = fit.name 379 387 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 380 394 if fit.data['avg_hr']: 381 395 self.hr_avg = Decimal(fit.data['avg_hr']) … … 406 420 def has_fit(self): 407 421 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/js/ow.js
r5cbc4a0 r2d2eb0d 16 16 17 17 // parameters provided when creating an "instance" of a map 18 var map_id = spec.map_id; 18 19 var latitude = spec.latitude; 19 20 var longitude = spec.longitude; … … 23 24 var end_icon = spec.end_icon; 24 25 var shadow = spec.shadow; 26 var elevation = spec.elevation; 27 var zoom_control = spec.zoom_control; 25 28 26 29 // OpenStreetMap urls and references 27 var openstreetmap_url = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' 28 var openstreetmap_attr = 'Map data © <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 © <a href="http://www.osm.org">OpenStreetMap</a>'; 29 32 30 33 // Some constants reused through the code … … 36 39 var create_map = function create_map(latitude, longitude, zoom) { 37 40 /* Create a Leaflet map, set center point and add tiles */ 38 map = L.map( 'map');41 map = L.map(map_id, {zoomControl: zoom_control}); 39 42 map.setView([latitude, longitude], zoom); 40 43 var tile_layer = L.tileLayer(openstreetmap_url, { … … 90 93 }, 91 94 }); 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 96 107 gpx.addTo(map); 97 108 }; … … 100 111 // create the map, add elevation, load gpx 101 112 create_map(latitude, longitude, zoom); 102 add_elevation_chart(); 113 if (elevation) { 114 add_elevation_chart(); 115 } 103 116 // add_ow_charts(); 104 117 load_gpx(gpx_url); -
ow/templates/dashboard.pt
r5cbc4a0 r2d2eb0d 88 88 <div class="workout-intro" tal:content="workout.notes"></div> 89 89 90 <div class="workout-map" tal:condition="workout.has_gpx"> 91 <a href="" tal:attributes="href request.resource_url(workout)"> 92 <img src="" tal:attributes="src request.static_url(workout.map_screenshot); 93 alt workout.title; title workout.title"> 94 </a> 95 </div> 96 90 97 <ul class="workout-options"> 91 98 <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
r5cbc4a0 r2d2eb0d 87 87 <tal:c tal:content="context.rounded_distance"></tal:c> km 88 88 </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> 89 96 <li tal:condition="hr"> 90 97 <tal:t i18n:translate="">Heart Rate:</tal:t> … … 145 152 <script type="text/javascript" tal:condition="context.has_gpx"> 146 153 var workout_map = owjs.map({ 154 map_id: 'map', 147 155 latitude: ${start_point['latitude']}, 148 156 longitude: ${start_point['longitude']}, … … 152 160 end_icon: '${request.static_url('ow:static/components/leaflet-gpx/pin-icon-end.png')}', 153 161 shadow: '${request.static_url('ow:static/components/leaflet-gpx/pin-shadow.png')}', 162 elevation: true, 163 zoom_control: true 154 164 }); 155 165 workout_map.render(); -
ow/tests/models/test_workout.py
r5cbc4a0 r2d2eb0d 12 12 from ow.models.root import OpenWorkouts 13 13 from ow.utilities import create_blob 14 15 from ow.tests.helpers import join 14 16 15 17 … … 210 212 # workout still not saved to the db 211 213 workout.tracking_file = Mock() 212 workout.tracking_file._ p_blob_uncommitted= '/tmp/blobtempfile'213 workout.tracking_file. _p_blob_committed= None214 workout.tracking_file._uncommitted.return_value = '/tmp/blobtempfile' 215 workout.tracking_file.committed.return_value = None 214 216 assert workout.tracking_file_path == '/tmp/blobtempfile' 215 workout.tracking_file._ p_blob_uncommitted= None216 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' 217 219 assert workout.tracking_file_path == '/var/db/blobs/blobfile' 218 220 … … 223 225 # workout still not saved to the db 224 226 workout.fit_file = Mock() 225 workout.fit_file._ p_blob_uncommitted= '/tmp/blobtempfile'226 workout.fit_file. _p_blob_committed= None227 workout.fit_file._uncommitted.return_value = '/tmp/blobtempfile' 228 workout.fit_file.committed.return_value = None 227 229 assert workout.fit_file_path == '/tmp/blobtempfile' 228 workout.fit_file._ p_blob_uncommitted= None229 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' 230 232 assert workout.fit_file_path == '/var/db/blobs/blobfile' 231 233 … … 514 516 workout = root['john']['1'] 515 517 # without tracking file 516 assert workout.has_tracking_file is False518 assert not workout.has_tracking_file 517 519 # with tracking file 518 520 workout.tracking_file = 'faked tracking file' 519 assert workout.has_tracking_file is True521 assert workout.has_tracking_file 520 522 521 523 def test_has_gpx(self, root): 522 524 workout = root['john']['1'] 523 525 # without tracking file 524 assert workout.has_gpx is False526 assert not workout.has_gpx 525 527 workout.tracking_filetype = 'fit' 526 assert workout.has_gpx is False528 assert not workout.has_gpx 527 529 # with non-gpx tracking file 528 530 workout.tracking_file = 'faked tracking file' 529 531 workout.tracking_filetype = 'fit' 530 assert workout.has_gpx is False532 assert not workout.has_gpx 531 533 # with gpx tracking file 532 534 workout.tracking_file = 'faked tracking file' 533 535 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
r5cbc4a0 r2d2eb0d 1 1 import os 2 from datetime import timedelta 3 from unittest.mock import patch 2 4 from pyexpat import ExpatError 3 5 from xml.dom.minidom import Element 4 6 5 7 import pytest 8 9 from ow.models.root import OpenWorkouts 10 from ow.models.user import User 11 from ow.models.workout import Workout 6 12 7 13 from ow.utilities import ( … … 16 22 mps_to_kmph, 17 23 kmph_to_mps, 24 save_map_screenshot 18 25 ) 19 26 27 from ow.tests.helpers import join 28 20 29 21 30 class 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 22 48 23 49 def test_slugify(self): … … 54 80 def test_kmph_to_mps(self): 55 81 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 56 146 57 147 -
ow/tests/views/test_workout.py
r5cbc4a0 r2d2eb0d 429 429 assert response.content_type == 'application/xml' 430 430 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
r5cbc4a0 r2d2eb0d 1 1 import re 2 import os 3 import logging 4 import subprocess 2 5 from datetime import datetime 3 6 from decimal import Decimal … … 7 10 from xml.dom import minidom 8 11 from ZODB.blob import Blob 12 13 log = logging.getLogger(__name__) 9 14 10 15 … … 184 189 open_blob.write(data) 185 190 return blob 191 192 193 def 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
r5cbc4a0 r2d2eb0d 214 214 content_disposition='attachment; filename="%s"' % gpx_slug, 215 215 body_file=context.tracking_file.open()) 216 217 218 @view_config( 219 context=Workout, 220 name='map', 221 renderer='ow:templates/workout-map.pt') 222 def 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.