Changeset 22eb5de in OpenWorkouts-current
- Timestamp:
- Jan 29, 2019, 12:58:08 PM (5 years ago)
- Branches:
- current, feature/docs, master
- Children:
- 594fbe8
- Parents:
- 3e48af6 (diff), ed7e9d7 (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:
-
- 7 edited
Legend:
- Unmodified
- Added
- Removed
-
bin/install
r3e48af6 r22eb5de 10 10 # Full path to the env 11 11 env_path=${current}/env 12 13 14 set_scripts_permissions() { 15 # ensure the shell scripts we will need have proper permissions 16 chmod u+x ${current}/bin/js_deps 17 chmod u+x ${current}/bin/start 18 chmod u+x ${current}/bin/screenshot_map 19 } 12 20 13 21 check_python3() { … … 55 63 56 64 install_js_deps() { 57 chmod +x ${current}/bin/js_deps58 65 ${current}/bin/js_deps 59 66 } 60 67 61 68 setup_start_stop() { 62 chmod +x ${current}/bin/start63 69 echo "OpenWorkouts successfully installed in ${env_path}" 64 70 echo "" … … 73 79 } 74 80 81 set_script_permissions 75 82 check_python3 76 83 create_venv -
ow/models/user.py
r3e48af6 r22eb5de 234 234 timedelta()) 235 235 } 236 237 @property 238 def yearly_stats(self): 239 """ 240 Return per-month stats for the last 12 months 241 """ 242 # set the boundaries for looking for workouts afterwards, 243 # we need the current date as the "end date" and one year 244 # ago from that date. Then we set the start at the first 245 # day of that month. 246 end = datetime.now(timezone.utc) 247 start = (end - timedelta(days=365)).replace(day=1) 248 249 # build the stats, populating first the dict with empty values 250 # for each month. 251 stats = {} 252 for days in range((end - start).days): 253 day = (start + timedelta(days=days)).date() 254 if (day.year, day.month) not in stats.keys(): 255 stats[(day.year, day.month)] = { 256 'workouts': 0, 257 'time': timedelta(0), 258 'distance': Decimal(0), 259 'elevation': Decimal(0), 260 'sports': {} 261 } 262 263 # now loop over workouts, filtering and then adding stats to the 264 # proper place 265 for workout in self.workouts(): 266 if start.date() <= workout.start.date() <= end.date(): 267 # less typing, avoid long lines 268 month = stats[ 269 (workout.start.date().year, workout.start.date().month)] 270 month['workouts'] += 1 271 month['time'] += workout.duration or timedelta(seconds=0) 272 month['distance'] += workout.distance or Decimal(0) 273 month['elevation'] += workout.uphill or Decimal(0) 274 if workout.sport not in month['sports']: 275 month['sports'][workout.sport] = { 276 'workouts': 0, 277 'time': timedelta(seconds=0), 278 'distance': Decimal(0), 279 'elevation': Decimal(0), 280 } 281 month['sports'][workout.sport]['workouts'] += 1 282 month['sports'][workout.sport]['time'] += ( 283 workout.duration or timedelta(0)) 284 month['sports'][workout.sport]['distance'] += ( 285 workout.distance or Decimal(0)) 286 month['sports'][workout.sport]['elevation'] += ( 287 workout.uphill or Decimal(0)) 288 289 return stats -
ow/static/js/ow.js
r3e48af6 r22eb5de 31 31 var openstreetmap_attr = 'Map data © <a href="http://www.osm.org">OpenStreetMap</a>'; 32 32 33 // Some constants reused through the code33 // Some vars reused through the code 34 34 var map; 35 35 var gpx; … … 147 147 Build a d3 bar chart, populated with data from the given url. 148 148 */ 149 var chart = d3.select( "svg"),149 var chart = d3.select(chart_selector), 150 150 margin = {top: 17, right: 0, bottom: 20, left: 0}, 151 151 152 width = +chart.attr("width") - margin.left - margin.right, 152 153 height = +chart.attr("height") - margin.top - margin.bottom, … … 234 235 235 236 }; 237 238 239 owjs.year_chart = function(spec) { 240 241 "use strict"; 242 243 // parameters provided when creating an "instance" of the chart 244 var chart_selector = spec.chart_selector, 245 filters_selector = spec.filters_selector, 246 url = spec.url, 247 current_month = spec.current_month, 248 y_axis_labels = spec.y_axis_labels; 249 250 // Helpers 251 function select_x_axis_label(d) { 252 /* Given a value, return the label associated with it */ 253 return d3.select('.x-axis-b') 254 .selectAll('text') 255 .filter(function(x) { return x == d.name; }); 256 }; 257 258 function get_y_value(d, filter_by) { 259 return Number(d[filter_by]); 260 }; 261 262 function get_y_axis_label(filter_by) { 263 return y_axis_labels[filter_by]; 264 }; 265 266 // Methods 267 var filters_setup = function filters_setup() { 268 $(filters_selector).on('click', function(e) { 269 var filter_by = 'distance'; 270 e.preventDefault(); 271 filter_by = $(this).attr('class').split('-')[1] 272 var chart = d3.select(chart_selector); 273 chart.selectAll("*").remove(); 274 render(filter_by); 275 }); 276 }; 277 278 var render = function render(filter_by) { 279 /* 280 Build a d3 bar chart, populated with data from the given url. 281 */ 282 var chart = d3.select(chart_selector), 283 margin = {top: 20, right: 20, bottom: 30, left: 50}, 284 width = +chart.attr("width") - margin.left - margin.right, 285 height = +chart.attr("height") - margin.top - margin.bottom, 286 g = chart.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"), 287 x = d3.scaleBand().rangeRound([0, width]).padding(0.1), 288 y = d3.scaleLinear().rangeRound([height, 0]); 289 290 d3.json(url).then(function (data) { 291 x.domain(data.map(function (d) { 292 return d.name; 293 })); 294 295 y.domain([0, d3.max(data, function (d) { 296 return get_y_value(d, filter_by); 297 })]); 298 299 g.append("g") 300 .attr('class', 'x-axis-b') 301 .attr("transform", "translate(0," + height + ")") 302 .call(d3.axisBottom(x)) 303 304 g.append("g") 305 .call(d3.axisLeft(y)) 306 .append("text") 307 .attr("fill", "#000") 308 .attr("transform", "rotate(-90)") 309 .attr("y", 6) 310 .attr("dy", "0.71em") 311 .attr("text-anchor", "end") 312 .text(get_y_axis_label(filter_by)); 313 314 g.selectAll(".bar") 315 .data(data) 316 .enter().append("rect") 317 .attr("class", function(d) { 318 if (d.id == current_month){ 319 select_x_axis_label(d).attr('style', "font-weight: bold;"); 320 return 'bar current' 321 } 322 else { 323 return 'bar' 324 } 325 }) 326 .attr("x", function (d) { 327 return x(d.name); 328 }) 329 .attr("y", function (d) { 330 return y(get_y_value(d, filter_by)); 331 }) 332 .attr("width", x.bandwidth()) 333 .attr("height", function (d) { 334 return height - y(get_y_value(d, filter_by)); 335 }) 336 .on('mouseover', function(d) { 337 if (d.id != current_month){ 338 select_x_axis_label(d).attr('style', "font-weight: bold;"); 339 } 340 }) 341 .on('mouseout', function(d) { 342 if (d.id != current_month){ 343 select_x_axis_label(d).attr('style', "font-weight: regular;"); 344 } 345 }); 346 347 g.selectAll(".text") 348 .data(data) 349 .enter() 350 .append("text") 351 .attr("class","label") 352 .attr("x", function (d) { 353 return x(d.name) + x.bandwidth()/2; 354 }) 355 .attr("y", function (d) { 356 /* 357 Get the value for the current bar, then get the maximum 358 value to be displayed in the bar, which is used to 359 calculate the proper position of the label for this bar, 360 relatively to its height (1% above the bar) 361 */ 362 var value = get_y_value(d, filter_by); 363 var max = y.domain()[1]; 364 return y(value + y.domain()[1] * 0.01); 365 }) 366 .text(function(d) { 367 var value = get_y_value(d, filter_by) 368 if ( value > 0) { 369 return value; 370 } 371 }); 372 373 }); 374 }; 375 376 var that = {} 377 that.filters_setup = filters_setup; 378 that.render = render; 379 return that 380 381 }; -
ow/templates/dashboard.pt
r3e48af6 r22eb5de 204 204 <script type="text/javascript"> 205 205 var week_chart = owjs.week_chart({ 206 chart_selector: 'div.js-week-stats ',206 chart_selector: 'div.js-week-stats svg', 207 207 url: "${request.resource_url(context, 'week')}", 208 208 current_day_name: "${current_day_name}" -
ow/templates/profile.pt
r3e48af6 r22eb5de 81 81 </div> 82 82 83 <div class="month-stats js-month-stats"> 84 <svg width="600" height="300"></svg> 85 <div class="filters js-filters"> 86 <a href="#" class="js-distance" i18n:translate="">distance</a> 87 <a href="#" class="js-time" i18n:translate="">time</a> 88 <a href="#" class="js-elevation" i18n:translate="">elevation</a> 89 </div> 90 </div> 91 83 92 </div> 84 93 85 94 </metal:content> 86 95 96 <metal:body-js metal:fill-slot="body-js"> 97 98 <script src="${request.static_url('ow:static/components/d3/d3.min.js')}"></script> 99 <script src="${request.static_url('ow:static/js/ow.js')}"></script> 100 101 <script type="text/javascript"> 102 var y_axis_labels = { 103 "distance": "Kilometers", 104 "time": "Hours", 105 "elevation": "Meters" 106 }; 107 108 var year_chart = owjs.year_chart({ 109 chart_selector: 'div.js-month-stats svg', 110 filters_selector: 'div.js-month-stats div.js-filters a', 111 url: "${request.resource_url(context, 'yearly')}", 112 current_month: "${current_month}", 113 y_axis_labels: y_axis_labels, 114 }); 115 year_chart.render("distance"); 116 year_chart.filters_setup(); 117 </script> 118 119 </metal:body-js> 120 87 121 </html> -
ow/tests/views/test_user.py
r3e48af6 r22eb5de 268 268 request = dummy_request 269 269 response = user_views.profile(john, request) 270 assert response == {} 270 assert len(response.keys()) == 1 271 current_month = datetime.now(timezone.utc).strftime('%Y-%m') 272 assert response['current_month'] == current_month 271 273 272 274 def test_login_get(self, dummy_request): -
ow/views/user.py
r3e48af6 r22eb5de 1 1 import json 2 2 from calendar import month_name 3 from datetime import datetime 3 from datetime import datetime, timezone 4 4 5 5 from pyramid.httpexceptions import HTTPFound … … 146 146 147 147 return { 148 'current_year': datetime.now( ).year,149 'current_day_name': datetime.now( ).strftime('%a'),148 'current_year': datetime.now(timezone.utc).year, 149 'current_day_name': datetime.now(timezone.utc).strftime('%a'), 150 150 'month_name': month_name, 151 151 'viewing_year': viewing_year, … … 165 165 basic info, stats, etc 166 166 """ 167 return {} 167 now = datetime.now(timezone.utc) 168 return { 169 'current_month': now.strftime('%Y-%m') 170 } 168 171 169 172 … … 250 253 charset='utf-8', 251 254 body=json.dumps(json_stats)) 255 256 257 @view_config( 258 context=User, 259 permission='view', 260 name='yearly') 261 def last_months_stats(context, request): 262 """ 263 Return a json-encoded stream with statistics for the last 12 months 264 """ 265 stats = context.yearly_stats 266 # this sets which month is 2 times in the stats, once this year, once 267 # the previous year. We will show it a bit different in the UI (showing 268 # the year too to prevent confusion) 269 repeated_month = datetime.now(timezone.utc).date().month 270 json_stats = [] 271 for month in stats: 272 hms = timedelta_to_hms(stats[month]['time']) 273 name = month_name[month[1]][:3] 274 if month[1] == repeated_month: 275 name += ' ' + str(month[0]) 276 month_stats = { 277 'id': str(month[0]) + '-' + str(month[1]).zfill(2), 278 'name': name, 279 'time': str(hms[0]).zfill(2), 280 'distance': int(round(stats[month]['distance'])), 281 'elevation': int(stats[month]['elevation']), 282 'workouts': stats[month]['workouts'] 283 } 284 json_stats.append(month_stats) 285 return Response(content_type='application/json', 286 charset='utf-8', 287 body=json.dumps(json_stats))
Note: See TracChangeset
for help on using the changeset viewer.