Changeset 3357e47 in OpenWorkouts-current
- Timestamp:
- Feb 4, 2019, 12:38:29 PM (5 years ago)
- Branches:
- current, feature/docs, master
- Children:
- 6993c72
- Parents:
- 1183d5a (diff), 5cf5787 (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. - Location:
- ow
- Files:
-
- 6 edited
Legend:
- Unmodified
- Added
- Removed
-
ow/models/user.py
r1183d5a r3357e47 9 9 10 10 from ow.catalog import get_catalog, reindex_object 11 from ow.utilities import get_week_days 11 from ow.utilities import get_week_days, get_month_week_number 12 12 13 13 … … 77 77 reindex_object(catalog, workout) 78 78 79 def workouts(self, year=None, month=None ):79 def workouts(self, year=None, month=None, week=None): 80 80 """ 81 81 Return this user workouts, sorted by date, from newer to older … … 84 84 if year: 85 85 workouts = [w for w in workouts if w.start.year == year] 86 if month: 87 workouts = [w for w in workouts if w.start.month == month] 86 if month: 87 workouts = [w for w in workouts if w.start.month == month] 88 if week: 89 week = int(week) 90 workouts = [ 91 w for w in workouts if w.start.isocalendar()[1] == week] 88 92 workouts = sorted(workouts, key=attrgetter('start')) 89 93 workouts.reverse() … … 288 292 289 293 return stats 294 295 @property 296 def weekly_year_stats(self): 297 """ 298 Return per-week stats for the last 12 months 299 """ 300 # set the boundaries for looking for workouts afterwards, 301 # we need the current date as the "end date" and one year 302 # ago from that date. Then we set the start at the first 303 # day of that month. 304 end = datetime.now(timezone.utc) 305 start = (end - timedelta(days=365)).replace(day=1) 306 307 stats = {} 308 309 # first initialize the stats dict 310 for days in range((end - start).days): 311 day = (start + timedelta(days=days)).date() 312 week = day.isocalendar()[1] 313 month_week = get_month_week_number(day) 314 key = (day.year, day.month, week, month_week) 315 if key not in stats.keys(): 316 stats[key] = { 317 'workouts': 0, 318 'time': timedelta(0), 319 'distance': Decimal(0), 320 'elevation': Decimal(0), 321 'sports': {} 322 } 323 324 # now loop over the workouts, filtering and then adding stats 325 # to the proper place 326 for workout in self.workouts(): 327 if start.date() <= workout.start.date() <= end.date(): 328 # less typing, avoid long lines 329 start_date = workout.start.date() 330 week = start_date.isocalendar()[1] 331 month_week = get_month_week_number(start_date) 332 week = stats[(start_date.year, 333 start_date.month, 334 week, 335 month_week)] 336 337 week['workouts'] += 1 338 week['time'] += workout.duration or timedelta(seconds=0) 339 week['distance'] += workout.distance or Decimal(0) 340 week['elevation'] += workout.uphill or Decimal(0) 341 if workout.sport not in week['sports']: 342 week['sports'][workout.sport] = { 343 'workouts': 0, 344 'time': timedelta(seconds=0), 345 'distance': Decimal(0), 346 'elevation': Decimal(0), 347 } 348 week['sports'][workout.sport]['workouts'] += 1 349 week['sports'][workout.sport]['time'] += ( 350 workout.duration or timedelta(0)) 351 week['sports'][workout.sport]['distance'] += ( 352 workout.distance or Decimal(0)) 353 week['sports'][workout.sport]['elevation'] += ( 354 workout.uphill or Decimal(0)) 355 356 return stats -
ow/static/js/ow.js
r1183d5a r3357e47 236 236 var chart_selector = spec.chart_selector, 237 237 filters_selector = spec.filters_selector, 238 url = spec.url, 238 switcher_selector = spec.switcher_selector, 239 urls = spec.urls, 239 240 current_month = spec.current_month, 240 y_axis_labels = spec.y_axis_labels; 241 current_week = spec.current_week, 242 y_axis_labels = spec.y_axis_labels, 243 filter_by = spec.filter_by, 244 url = spec.url; 241 245 242 246 // Helpers … … 256 260 }; 257 261 262 function get_name_for_x(d) { 263 if (d.week == undefined || d.week == 0) { 264 return d.name; 265 } 266 else { 267 return d.id.split('-')[2]; 268 } 269 } 270 258 271 // Methods 259 272 var filters_setup = function filters_setup() { 260 273 $(filters_selector).on('click', function(e) { 261 var filter_by = 'distance';262 274 e.preventDefault(); 263 275 filter_by = $(this).attr('class').split('-')[1] 264 276 var chart = d3.select(chart_selector); 265 277 chart.selectAll("*").remove(); 266 render(filter_by); 267 }); 268 }; 269 270 var render = function render(filter_by) { 278 render(filter_by, url); 279 }); 280 }; 281 282 var switcher_setup = function switcher_setup() { 283 $(switcher_selector).on('click', function(e) { 284 e.preventDefault(); 285 url = $(this).attr('class').split('-')[1] 286 var chart = d3.select(chart_selector); 287 chart.selectAll("*").remove(); 288 render(filter_by, url); 289 }); 290 }; 291 292 var render = function render(filter_by, url) { 271 293 /* 272 294 Build a d3 bar chart, populated with data from the given url. … … 280 302 y = d3.scaleLinear().rangeRound([height, 0]); 281 303 282 d3.json(url ).then(function (data) {304 d3.json(urls[url]).then(function (data) { 283 305 x.domain(data.map(function (d) { 284 return d.name; 306 return get_name_for_x(d); 307 // return d.name; 285 308 })); 286 309 … … 308 331 .enter().append("rect") 309 332 .attr("class", function(d) { 310 if (d.id == current_month){ 333 var sel_week = current_month + '-' + current_week; 334 if (d.id == current_month || d.id == sel_week){ 335 /* Bar for the currently selected month or week */ 311 336 select_x_axis_label(d).attr('style', "font-weight: bold;"); 312 return 'bar current' 337 return 'bar current'; 313 338 } 314 339 else { 315 return 'bar' 340 if (!current_week && d.id.indexOf(current_month) >=0 ) { 341 /* 342 User selected a month, then switched to weekly 343 view, we do highlight all the bars for weeks in 344 that month 345 */ 346 select_x_axis_label(d).attr('style', "font-weight: bold;"); 347 return 'bar current'; 348 } 349 else { 350 /* Non-selected bar */ 351 return 'bar'; 352 } 353 316 354 } 317 355 }) 318 356 .attr("x", function (d) { 319 return x( d.name);357 return x(get_name_for_x(d)); 320 358 }) 321 359 .attr("y", function (d) { … … 340 378 }); 341 379 342 g.selectAll(".text") 343 .data(data) 344 .enter() 345 .append("text") 346 .attr("class","label") 347 .attr("x", function (d) { 348 return x(d.name) + x.bandwidth()/2; 349 }) 350 .attr("y", function (d) { 351 /* 352 Get the value for the current bar, then get the maximum 353 value to be displayed in the bar, which is used to 354 calculate the proper position of the label for this bar, 355 relatively to its height (1% above the bar) 356 */ 357 var value = get_y_value(d, filter_by); 358 var max = y.domain()[1]; 359 return y(value + y.domain()[1] * 0.01); 360 }) 361 .text(function(d) { 362 var value = get_y_value(d, filter_by) 363 if ( value > 0) { 364 return value; 365 } 366 }); 367 380 if (url == 'monthly') { 381 g.selectAll(".text") 382 .data(data) 383 .enter() 384 .append("text") 385 .attr("class","label") 386 .attr("x", function (d) { 387 return x(get_name_for_x(d)) + x.bandwidth()/2; 388 }) 389 .attr("y", function (d) { 390 /* 391 Get the value for the current bar, then get the maximum 392 value to be displayed in the bar, which is used to 393 calculate the proper position of the label for this bar, 394 relatively to its height (1% above the bar) 395 */ 396 var value = get_y_value(d, filter_by); 397 var max = y.domain()[1]; 398 return y(value + y.domain()[1] * 0.01); 399 }) 400 .text(function(d) { 401 var value = get_y_value(d, filter_by) 402 if ( value > 0) { 403 return value; 404 } 405 }); 406 } 407 408 if (url == 'weekly') { 409 g.selectAll(".tick") 410 .each(function (d, i) { 411 /* 412 Remove from the x-axis tickets those without letters 413 on them (useful for the weekly chart) 414 */ 415 if (d !== parseInt(d, 10)) { 416 if(!d.match(/[a-z]/i)) { 417 this.remove(); 418 } 419 } 420 }); 421 } 368 422 }); 369 423 }; … … 371 425 var that = {} 372 426 that.filters_setup = filters_setup; 427 that.switcher_setup = switcher_setup; 373 428 that.render = render; 374 429 return that -
ow/templates/profile.pt
r1183d5a r3357e47 72 72 <a href="#" class="js-time" i18n:translate="">time</a> 73 73 <a href="#" class="js-elevation" i18n:translate="">elevation</a> 74 </div> 75 <div class="switcher js-switcher"> 76 <a href="#" class="js-weekly" i18n:translate="">weekly</a> 77 <a href="#" class="js-monthly" i18n:translate="">monthly</a> 74 78 </div> 75 79 </div> … … 154 158 chart_selector: 'div.js-month-stats svg', 155 159 filters_selector: 'div.js-month-stats div.js-filters a', 156 url: "${request.resource_url(context, 'yearly')}", 160 switcher_selector: 'div.js-month-stats div.js-switcher a', 161 urls: {"monthly": "${request.resource_url(context, 'monthly')}", 162 "weekly": "${request.resource_url(context, 'weekly')}"}, 157 163 current_month: "${current_month}", 164 current_week: "${current_week}", 158 165 y_axis_labels: y_axis_labels, 166 filter_by: "distance", 167 url: "${'monthly' if current_week is None else 'weekly'}", 159 168 }); 160 year_chart.render("distance" );169 year_chart.render("distance", "${'monthly' if current_week is None else 'weekly'}"); 161 170 year_chart.filters_setup(); 171 year_chart.switcher_setup(); 162 172 </script> 163 173 -
ow/tests/views/test_user.py
r1183d5a r3357e47 269 269 # profile page for the current day (no workouts avalable) 270 270 response = user_views.profile(john, request) 271 assert len(response.keys()) == 2271 assert len(response.keys()) == 3 272 272 current_month = datetime.now(timezone.utc).strftime('%Y-%m') 273 273 assert response['current_month'] == current_month 274 assert response['current_week'] is None 274 275 assert response['workouts'] == [] 275 276 # profile page for a previous date, that has workouts … … 277 278 request.GET['month'] = 8 278 279 response = user_views.profile(john, request) 279 assert len(response.keys()) == 2280 assert len(response.keys()) == 3 280 281 assert response['current_month'] == '2015-08' 282 assert response['current_week'] is None 283 assert response['workouts'] == john.workouts(2015, 8) 284 # same, passing a week, first on a week without workouts 285 request.GET['year'] = 2015 286 request.GET['month'] = 8 287 request.GET['week'] = 25 288 response = user_views.profile(john, request) 289 assert len(response.keys()) == 3 290 assert response['current_month'] == '2015-08' 291 assert response['current_week'] is 25 292 assert response['workouts'] == [] 293 # now in a week with workoutss 294 request.GET['year'] = 2015 295 request.GET['month'] = 8 296 request.GET['week'] = 26 297 response = user_views.profile(john, request) 298 assert len(response.keys()) == 3 299 assert response['current_month'] == '2015-08' 300 assert response['current_week'] is 26 281 301 assert response['workouts'] == john.workouts(2015, 8) 282 302 -
ow/utilities.py
r1183d5a r3357e47 2 2 import os 3 3 import logging 4 import calendar 4 5 import subprocess 5 6 from datetime import datetime, timedelta … … 234 235 week_days = [first_day + timedelta(days=i) for i in range(7)] 235 236 return week_days 237 238 239 def get_month_week_number(day): 240 """ 241 Given a datetime object (day), return the number of week the day is 242 in the current month (week 1, 2, 3, etc) 243 """ 244 weeks = calendar.monthcalendar(day.year, day.month) 245 for week in weeks: 246 if day.day in week: 247 return weeks.index(week) 248 return None -
ow/views/user.py
r1183d5a r3357e47 168 168 year = int(request.GET.get('year', now.year)) 169 169 month = int(request.GET.get('month', now.month)) 170 week = request.GET.get('week', None) 170 171 return { 171 'workouts': context.workouts(year, month ),172 'workouts': context.workouts(year, month, week), 172 173 'current_month': '{year}-{month}'.format( 173 year=str(year), month=str(month).zfill(2)) 174 year=str(year), month=str(month).zfill(2)), 175 'current_week': week 174 176 } 175 177 … … 262 264 context=User, 263 265 permission='view', 264 name=' yearly')266 name='monthly') 265 267 def last_months_stats(context, request): 266 268 """ … … 294 296 charset='utf-8', 295 297 body=json.dumps(json_stats)) 298 299 300 @view_config( 301 context=User, 302 permission='view', 303 name='weekly') 304 def last_weeks_stats(context, request): 305 """ 306 Return a json-encoded stream with statistics for the last 12-months, but 307 in a per-week basis 308 """ 309 stats = context.weekly_year_stats 310 # this sets which month is 2 times in the stats, once this year, once 311 # the previous year. We will show it a bit different in the UI (showing 312 # the year too to prevent confusion) 313 repeated_month = datetime.now(timezone.utc).date().month 314 json_stats = [] 315 for week in stats: 316 hms = timedelta_to_hms(stats[week]['time']) 317 name = month_name[week[1]][:3] 318 if week[1] == repeated_month: 319 name += ' ' + str(week[0]) 320 week_stats = { 321 'id': '-'.join( 322 [str(week[0]), str(week[1]).zfill(2), str(week[2])]), 323 'week': str(week[3]), # the number of week in the current month 324 'name': name, 325 'time': str(hms[0]).zfill(2), 326 'distance': int(round(stats[week]['distance'])), 327 'elevation': int(stats[week]['elevation']), 328 'workouts': stats[week]['workouts'], 329 'url': request.resource_url( 330 context, 'profile', 331 query={'year': str(week[0]), 332 'month': str(week[1]), 333 'week': str(week[2])}, 334 anchor='workouts') 335 } 336 json_stats.append(week_stats) 337 return Response(content_type='application/json', 338 charset='utf-8', 339 body=json.dumps(json_stats))
Note: See TracChangeset
for help on using the changeset viewer.