Changeset ed7e9d7 in OpenWorkouts-current
- Timestamp:
- Jan 29, 2019, 12:41:01 PM (5 years ago)
- Branches:
- current, feature/docs, master
- Children:
- 1183d5a, 22eb5de
- Parents:
- 26220ba (diff), bd8eeb4 (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
r26220ba red7e9d7 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
r26220ba red7e9d7 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
r26220ba red7e9d7 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: 20, right: 20, bottom: 30, left: 50}, 151 151 width = +chart.attr("width") - margin.left - margin.right, … … 227 227 228 228 }; 229 230 231 owjs.year_chart = function(spec) { 232 233 "use strict"; 234 235 // parameters provided when creating an "instance" of the chart 236 var chart_selector = spec.chart_selector, 237 filters_selector = spec.filters_selector, 238 url = spec.url, 239 current_month = spec.current_month, 240 y_axis_labels = spec.y_axis_labels; 241 242 // Helpers 243 function select_x_axis_label(d) { 244 /* Given a value, return the label associated with it */ 245 return d3.select('.x-axis-b') 246 .selectAll('text') 247 .filter(function(x) { return x == d.name; }); 248 }; 249 250 function get_y_value(d, filter_by) { 251 return Number(d[filter_by]); 252 }; 253 254 function get_y_axis_label(filter_by) { 255 return y_axis_labels[filter_by]; 256 }; 257 258 // Methods 259 var filters_setup = function filters_setup() { 260 $(filters_selector).on('click', function(e) { 261 var filter_by = 'distance'; 262 e.preventDefault(); 263 filter_by = $(this).attr('class').split('-')[1] 264 var chart = d3.select(chart_selector); 265 chart.selectAll("*").remove(); 266 render(filter_by); 267 }); 268 }; 269 270 var render = function render(filter_by) { 271 /* 272 Build a d3 bar chart, populated with data from the given url. 273 */ 274 var chart = d3.select(chart_selector), 275 margin = {top: 20, right: 20, bottom: 30, left: 50}, 276 width = +chart.attr("width") - margin.left - margin.right, 277 height = +chart.attr("height") - margin.top - margin.bottom, 278 g = chart.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"), 279 x = d3.scaleBand().rangeRound([0, width]).padding(0.1), 280 y = d3.scaleLinear().rangeRound([height, 0]); 281 282 d3.json(url).then(function (data) { 283 x.domain(data.map(function (d) { 284 return d.name; 285 })); 286 287 y.domain([0, d3.max(data, function (d) { 288 return get_y_value(d, filter_by); 289 })]); 290 291 g.append("g") 292 .attr('class', 'x-axis-b') 293 .attr("transform", "translate(0," + height + ")") 294 .call(d3.axisBottom(x)) 295 296 g.append("g") 297 .call(d3.axisLeft(y)) 298 .append("text") 299 .attr("fill", "#000") 300 .attr("transform", "rotate(-90)") 301 .attr("y", 6) 302 .attr("dy", "0.71em") 303 .attr("text-anchor", "end") 304 .text(get_y_axis_label(filter_by)); 305 306 g.selectAll(".bar") 307 .data(data) 308 .enter().append("rect") 309 .attr("class", function(d) { 310 if (d.id == current_month){ 311 select_x_axis_label(d).attr('style', "font-weight: bold;"); 312 return 'bar current' 313 } 314 else { 315 return 'bar' 316 } 317 }) 318 .attr("x", function (d) { 319 return x(d.name); 320 }) 321 .attr("y", function (d) { 322 return y(get_y_value(d, filter_by)); 323 }) 324 .attr("width", x.bandwidth()) 325 .attr("height", function (d) { 326 return height - y(get_y_value(d, filter_by)); 327 }) 328 .on('mouseover', function(d) { 329 if (d.id != current_month){ 330 select_x_axis_label(d).attr('style', "font-weight: bold;"); 331 } 332 }) 333 .on('mouseout', function(d) { 334 if (d.id != current_month){ 335 select_x_axis_label(d).attr('style', "font-weight: regular;"); 336 } 337 }); 338 339 g.selectAll(".text") 340 .data(data) 341 .enter() 342 .append("text") 343 .attr("class","label") 344 .attr("x", function (d) { 345 return x(d.name) + x.bandwidth()/2; 346 }) 347 .attr("y", function (d) { 348 /* 349 Get the value for the current bar, then get the maximum 350 value to be displayed in the bar, which is used to 351 calculate the proper position of the label for this bar, 352 relatively to its height (1% above the bar) 353 */ 354 var value = get_y_value(d, filter_by); 355 var max = y.domain()[1]; 356 return y(value + y.domain()[1] * 0.01); 357 }) 358 .text(function(d) { 359 var value = get_y_value(d, filter_by) 360 if ( value > 0) { 361 return value; 362 } 363 }); 364 365 }); 366 }; 367 368 var that = {} 369 that.filters_setup = filters_setup; 370 that.render = render; 371 return that 372 373 }; -
ow/templates/dashboard.pt
r26220ba red7e9d7 211 211 <script type="text/javascript"> 212 212 var week_chart = owjs.week_chart({ 213 chart_selector: 'div.js-week-stats ',213 chart_selector: 'div.js-week-stats svg', 214 214 url: "${request.resource_url(context, 'week')}", 215 215 current_day_name: "${current_day_name}" -
ow/templates/profile.pt
r26220ba red7e9d7 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
r26220ba red7e9d7 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
r26220ba red7e9d7 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.