Changes in / [6993c72:bf01534] in OpenWorkouts-current
- Files:
-
- 11 edited
Legend:
- Unmodified
- Added
- Removed
-
README.txt
r6993c72 rbf01534 2 2 ============ 3 3 4 TBW, if you look for installation instructions, take a look at the script 5 bin/install 4 Getting Started 5 --------------- 6 7 - Change directory into your newly created project. 8 9 cd ow 10 11 - Create a Python virtual environment. 12 13 python3 -m venv env 14 15 - Upgrade packaging tools. 16 17 env/bin/pip install --upgrade pip setuptools 18 19 - Install the project in editable mode with its testing requirements. 20 21 env/bin/pip install -e ".[testing]" 22 23 - Run your project's tests. 24 25 env/bin/pytest 26 27 - Run your project. 28 29 env/bin/pserve development.ini -
bin/install
r6993c72 rbf01534 58 58 install_openworkouts() { 59 59 . ${env_path}/bin/activate 60 yes | pip install --upgrade - e ${current}[testing]60 yes | pip install --upgrade --process-dependency-links -e ${current}[testing] 61 61 deactivate 62 62 } -
ow/models/user.py
r6993c72 rbf01534 9 9 10 10 from ow.catalog import get_catalog, reindex_object 11 from ow.utilities import get_week_days , get_month_week_number11 from ow.utilities import get_week_days 12 12 13 13 … … 77 77 reindex_object(catalog, workout) 78 78 79 def workouts(self, year=None, month=None , week=None):79 def workouts(self, year=None, month=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] 88 if week: 89 week = int(week) 90 workouts = [ 91 w for w in workouts if w.start.isocalendar()[1] == week] 86 if month: 87 workouts = [w for w in workouts if w.start.month == month] 92 88 workouts = sorted(workouts, key=attrgetter('start')) 93 89 workouts.reverse() … … 292 288 293 289 return stats 294 295 @property296 def weekly_year_stats(self):297 """298 Return per-week stats for the last 12 months299 """300 # set the boundaries for looking for workouts afterwards,301 # we need the current date as the "end date" and one year302 # ago from that date. Then we set the start at the first303 # 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 dict310 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 stats325 # to the proper place326 for workout in self.workouts():327 if start.date() <= workout.start.date() <= end.date():328 # less typing, avoid long lines329 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'] += 1338 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'] += 1349 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/css/main.css
r6993c72 rbf01534 1006 1006 <a class="login-remember" href="#">Forgot your password?</a> 1007 1007 */ 1008 .user-profile .workout-options { 1009 font-size: 13px; 1010 font-size: 0.8125rem; 1011 } 1012 .user-profile-account { 1013 background-color: #fbfbfb; 1014 padding: 2em 1em; 1015 } 1016 @media (min-width: 480px) { 1017 .user-profile-account { 1018 padding: 2em 6em; 1019 } 1020 } 1021 .user-profile-account img { 1008 .user-profile img { 1022 1009 width: 140px; 1023 1010 height: 140px; … … 1026 1013 margin-bottom: 0.5em; 1027 1014 } 1015 .user-profile .workout-options { 1016 font-size: 13px; 1017 font-size: 0.8125rem; 1018 } 1019 .user-profile-account { 1020 background-color: #fbfbfb; 1021 padding: 2em 1em; 1022 } 1023 @media (min-width: 480px) { 1024 .user-profile-account { 1025 padding: 2em 6em; 1026 } 1027 } 1028 1028 .user-profile-account h2 { 1029 1029 margin: 0 0 0.15em 0; … … 1040 1040 color: #959595; 1041 1041 } 1042 . total-workouts {1043 font-size: 1 3px;1044 font-size: 0.8125rem;1042 .user-profile-account .workouts { 1043 font-size: 18px; 1044 font-size: 1.125rem; 1045 1045 font-weight: bold; 1046 1046 } -
ow/static/js/ow.js
r6993c72 rbf01534 244 244 var chart_selector = spec.chart_selector, 245 245 filters_selector = spec.filters_selector, 246 switcher_selector = spec.switcher_selector, 247 urls = spec.urls, 246 url = spec.url, 248 247 current_month = spec.current_month, 249 current_week = spec.current_week, 250 y_axis_labels = spec.y_axis_labels, 251 filter_by = spec.filter_by, 252 url = spec.url; 248 y_axis_labels = spec.y_axis_labels; 253 249 254 250 // Helpers … … 268 264 }; 269 265 270 function get_name_for_x(d) {271 if (d.week == undefined || d.week == 0) {272 return d.name;273 }274 else {275 return d.id.split('-')[2];276 }277 }278 279 266 // Methods 280 267 var filters_setup = function filters_setup() { 281 268 $(filters_selector).on('click', function(e) { 269 var filter_by = 'distance'; 282 270 e.preventDefault(); 283 271 filter_by = $(this).attr('class').split('-')[1] 284 272 var chart = d3.select(chart_selector); 285 273 chart.selectAll("*").remove(); 286 render(filter_by, url); 287 }); 288 }; 289 290 var switcher_setup = function switcher_setup() { 291 $(switcher_selector).on('click', function(e) { 292 e.preventDefault(); 293 url = $(this).attr('class').split('-')[1] 294 var chart = d3.select(chart_selector); 295 chart.selectAll("*").remove(); 296 render(filter_by, url); 297 }); 298 }; 299 300 var render = function render(filter_by, url) { 274 render(filter_by); 275 }); 276 }; 277 278 var render = function render(filter_by) { 301 279 /* 302 280 Build a d3 bar chart, populated with data from the given url. … … 310 288 y = d3.scaleLinear().rangeRound([height, 0]); 311 289 312 d3.json(url s[url]).then(function (data) {290 d3.json(url).then(function (data) { 313 291 x.domain(data.map(function (d) { 314 return get_name_for_x(d); 315 // return d.name; 292 return d.name; 316 293 })); 317 294 … … 339 316 .enter().append("rect") 340 317 .attr("class", function(d) { 341 var sel_week = current_month + '-' + current_week; 342 if (d.id == current_month || d.id == sel_week){ 343 /* Bar for the currently selected month or week */ 318 if (d.id == current_month){ 344 319 select_x_axis_label(d).attr('style', "font-weight: bold;"); 345 return 'bar current' ;320 return 'bar current' 346 321 } 347 322 else { 348 if (!current_week && d.id.indexOf(current_month) >=0 ) { 349 /* 350 User selected a month, then switched to weekly 351 view, we do highlight all the bars for weeks in 352 that month 353 */ 354 select_x_axis_label(d).attr('style', "font-weight: bold;"); 355 return 'bar current'; 356 } 357 else { 358 /* Non-selected bar */ 359 return 'bar'; 360 } 361 323 return 'bar' 362 324 } 363 325 }) 364 326 .attr("x", function (d) { 365 return x( get_name_for_x(d));327 return x(d.name); 366 328 }) 367 329 .attr("y", function (d) { … … 381 343 select_x_axis_label(d).attr('style', "font-weight: regular;"); 382 344 } 383 })384 .on('click', function(d) {385 window.location.href = d.url;386 345 }); 387 346 388 if (url == 'monthly') { 389 g.selectAll(".text") 390 .data(data) 391 .enter() 392 .append("text") 393 .attr("class","label") 394 .attr("x", function (d) { 395 return x(get_name_for_x(d)) + x.bandwidth()/2; 396 }) 397 .attr("y", function (d) { 398 /* 399 Get the value for the current bar, then get the maximum 400 value to be displayed in the bar, which is used to 401 calculate the proper position of the label for this bar, 402 relatively to its height (1% above the bar) 403 */ 404 var value = get_y_value(d, filter_by); 405 var max = y.domain()[1]; 406 return y(value + y.domain()[1] * 0.01); 407 }) 408 .text(function(d) { 409 var value = get_y_value(d, filter_by) 410 if ( value > 0) { 411 return value; 412 } 413 }); 414 } 415 416 if (url == 'weekly') { 417 g.selectAll(".tick") 418 .each(function (d, i) { 419 /* 420 Remove from the x-axis tickets those without letters 421 on them (useful for the weekly chart) 422 */ 423 if (d !== parseInt(d, 10)) { 424 if(!d.match(/[a-z]/i)) { 425 this.remove(); 426 } 427 } 428 }); 429 } 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 430 373 }); 431 374 }; … … 433 376 var that = {} 434 377 that.filters_setup = filters_setup; 435 that.switcher_setup = switcher_setup;436 378 that.render = render; 437 379 return that -
ow/static/less/pages/profile.less
r6993c72 rbf01534 1 1 .user-profile { 2 img { 3 width: 140px; 4 height: 140px; 5 object-fit: cover; 6 border-radius: 50%; 7 margin-bottom: .5em; 8 } 2 9 .workout-options { 3 10 .font-size(13); … … 10 17 @media (min-width: @screen-s){ 11 18 padding: 2em 6em; 12 }13 img {14 width: 140px;15 height: 140px;16 object-fit: cover;17 border-radius: 50%;18 margin-bottom: .5em;19 19 } 20 20 h2 { … … 30 30 } 31 31 } 32 } 33 .total-workouts { 34 .font-size(13);35 font-weight: bold;32 .workouts { 33 .font-size(18); 34 font-weight: bold; 35 } 36 36 } 37 37 -
ow/templates/profile.pt
r6993c72 rbf01534 26 26 <tal:has-nickname tal:condition="context.nickname"> 27 27 <tal:nickname tal:content="context.nickname"></tal:nickname> 28 </tal:has-nickname> | 28 </tal:has-nickname> | 29 29 <span><tal:email tal:content="context.email"></tal:email></span> 30 30 </p> … … 60 60 i18n:translate="">change password</a></li> 61 61 </ul> 62 </div>63 62 64 <div class="total-workouts"> 65 <tal:w tal:replace="context.num_workouts"></tal:w> 66 <tal:t i18n:translate="">workouts</tal:t> 63 <div class="workouts"> 64 <tal:w tal:replace="context.num_workouts"></tal:w> 65 <tal:t i18n:translate="">Workouts</tal:t> 66 </div> 67 67 </div> 68 68 69 69 <div class="month-stats js-month-stats"> 70 70 <div class="svg-cotent"> 71 <svg width=" 800" height="180" viewBox="0 0 800 180"></svg>71 <svg width="600" height="300" viewBox="0 0 600 300"></svg> 72 72 </div> 73 73 <ul class="workout-options filters js-filters"> … … 76 76 <li><a href="#" class="js-elevation" i18n:translate="">elevation</a></li> 77 77 </ul> 78 79 <ul class="workout-options switcher js-switcher">80 <li><a href="#" class="js-weekly" i18n:translate="">weekly</a></li>81 <li><a href="#" class="js-monthly is-active" i18n:translate="">monthly</a></li>82 </ul>83 78 </div> 84 79 80 <div class="latest-workouts"> 81 <h3 i18n:translate="">Latest workouts</h3> 82 <tal:r tal:repeat="workout context.workouts()[:5]"> 83 <div class="workout"> 84 <h4> 85 <a href="" tal:content="workout.title" 86 tal:attributes="href request.resource_url(workout)"></a> 87 </h4> 88 <span><tal:c tal:content="workout.sport"></tal:c></span> 89 <p><tal:c tal:content="workout.start"></tal:c>, 90 <tal:c tal:content="workout.duration"></tal:c>, 91 <tal:c tal:content="workout.rounded_distance"></tal:c> km 92 </p> 93 </div> 94 </tal:r> 95 </div> 85 96 86 <tal:r tal:repeat="workout workouts">87 88 <a name="workouts"></a>89 90 91 <article class="workout-resume">92 93 <h2 class="workout-title">94 <a href="" tal:content="workout.title"95 tal:attributes="href request.resource_url(workout)"></a>96 </h2>97 98 <ul class="workout-info">99 <li>100 <tal:c tal:content="workout.start_in_timezone(context.timezone)"></tal:c>101 </li>102 <li>103 <!--! use the properly formatted duration instead of the timedelta object -->104 <tal:c tal:content="workout._duration"></tal:c>105 </li>106 <li tal:condition="workout.distance">107 <tal:c tal:content="workout.rounded_distance"></tal:c> km108 </li>109 </ul>110 111 <ul class="workout-info" tal:define="hr workout.hr; cad workout.cad">112 <li tal:condition="hr">113 <span i18n:translate="">HR (bpm)</span>:114 <tal:c tal:content="hr['min']"></tal:c>115 <tal:t i18n:translate="">Min.</tal:t>,116 <tal:c tal:content="hr['avg']"></tal:c>117 <tal:t i18n:translate="">Avg.</tal:t>,118 <tal:c tal:content="hr['max']"></tal:c>119 <tal:t i18n:translate="">Max.</tal:t>120 </li>121 <li tal:condition="cad">122 <span i18n:translate="">Cad</span>:123 <tal:c tal:content="cad['min']"></tal:c>124 <tal:t i18n:translate="">Min.</tal:t>,125 <tal:c tal:content="cad['avg']"></tal:c>126 <tal:t i18n:translate="">Avg.</tal:t>,127 <tal:c tal:content="cad['max']"></tal:c>128 <tal:t i18n:translate="">Max.</tal:t>129 </li>130 </ul>131 132 <div class="workout-intro" tal:content="workout.notes"></div>133 134 <div class="workout-map" tal:condition="workout.has_gpx">135 <a href="" tal:attributes="href request.resource_url(workout)">136 <img src="" tal:attributes="src request.static_url(workout.map_screenshot);137 alt workout.title; title workout.title">138 </a>139 </div>140 141 </article>142 143 </tal:r>144 97 </div> 145 98 … … 161 114 chart_selector: '.js-month-stats svg', 162 115 filters_selector: '.js-month-stats .js-filters a', 163 switcher_selector: '.js-month-stats .js-switcher a', 164 urls: {"monthly": "${request.resource_url(context, 'monthly')}", 165 "weekly": "${request.resource_url(context, 'weekly')}"}, 116 url: "${request.resource_url(context, 'yearly')}", 166 117 current_month: "${current_month}", 167 current_week: "${current_week}",168 118 y_axis_labels: y_axis_labels, 169 filter_by: "distance",170 url: "${'monthly' if current_week is None else 'weekly'}",171 119 }); 172 year_chart.render("distance" , "${'monthly' if current_week is None else 'weekly'}");120 year_chart.render("distance"); 173 121 year_chart.filters_setup(); 174 year_chart.switcher_setup();175 122 </script> 176 123 -
ow/tests/views/test_user.py
r6993c72 rbf01534 267 267 """ 268 268 request = dummy_request 269 # profile page for the current day (no workouts avalable)270 269 response = user_views.profile(john, request) 271 assert len(response.keys()) == 3270 assert len(response.keys()) == 1 272 271 current_month = datetime.now(timezone.utc).strftime('%Y-%m') 273 272 assert response['current_month'] == current_month 274 assert response['current_week'] is None275 assert response['workouts'] == []276 # profile page for a previous date, that has workouts277 request.GET['year'] = 2015278 request.GET['month'] = 8279 response = user_views.profile(john, request)280 assert len(response.keys()) == 3281 assert response['current_month'] == '2015-08'282 assert response['current_week'] is None283 assert response['workouts'] == john.workouts(2015, 8)284 # same, passing a week, first on a week without workouts285 request.GET['year'] = 2015286 request.GET['month'] = 8287 request.GET['week'] = 25288 response = user_views.profile(john, request)289 assert len(response.keys()) == 3290 assert response['current_month'] == '2015-08'291 assert response['current_week'] is 25292 assert response['workouts'] == []293 # now in a week with workoutss294 request.GET['year'] = 2015295 request.GET['month'] = 8296 request.GET['week'] = 26297 response = user_views.profile(john, request)298 assert len(response.keys()) == 3299 assert response['current_month'] == '2015-08'300 assert response['current_week'] is 26301 assert response['workouts'] == john.workouts(2015, 8)302 273 303 274 def test_login_get(self, dummy_request): -
ow/utilities.py
r6993c72 rbf01534 2 2 import os 3 3 import logging 4 import calendar5 4 import subprocess 6 5 from datetime import datetime, timedelta … … 235 234 week_days = [first_day + timedelta(days=i) for i in range(7)] 236 235 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 is242 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
r6993c72 rbf01534 166 166 """ 167 167 now = datetime.now(timezone.utc) 168 year = int(request.GET.get('year', now.year)) 169 month = int(request.GET.get('month', now.month)) 170 week = request.GET.get('week', None) 171 return { 172 'workouts': context.workouts(year, month, week), 173 'current_month': '{year}-{month}'.format( 174 year=str(year), month=str(month).zfill(2)), 175 'current_week': week 168 return { 169 'current_month': now.strftime('%Y-%m') 176 170 } 177 171 … … 264 258 context=User, 265 259 permission='view', 266 name=' monthly')260 name='yearly') 267 261 def last_months_stats(context, request): 268 262 """ … … 286 280 'distance': int(round(stats[month]['distance'])), 287 281 'elevation': int(stats[month]['elevation']), 288 'workouts': stats[month]['workouts'], 289 'url': request.resource_url( 290 context, 'profile', 291 query={'year': str(month[0]), 'month': str(month[1])}, 292 anchor='workouts') 282 'workouts': stats[month]['workouts'] 293 283 } 294 284 json_stats.append(month_stats) … … 296 286 charset='utf-8', 297 287 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, but307 in a per-week basis308 """309 stats = context.weekly_year_stats310 # this sets which month is 2 times in the stats, once this year, once311 # the previous year. We will show it a bit different in the UI (showing312 # the year too to prevent confusion)313 repeated_month = datetime.now(timezone.utc).date().month314 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 month324 '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 }335 json_stats.append(week_stats)336 return Response(content_type='application/json',337 charset='utf-8',338 body=json.dumps(json_stats)) -
setup.py
r6993c72 rbf01534 21 21 'waitress', 22 22 'repoze.folder', 23 'repoze.catalog @ git+https://github.com/WuShell/repoze.catalog.git@0.8.4' 24 '#egg=repoze.catalog-0.8.4', 23 'repoze.catalog==0.8.4', 25 24 'bcrypt', 26 25 'FormEncode', … … 40 39 'pytest-xdist', 41 40 'pytest-codestyle', 41 ] 42 43 dependency_links = [ 44 'git+https://github.com/WuShell/repoze.catalog.git@0.8.4' 45 '#egg=repoze.catalog-0.8.4' 42 46 ] 43 47 … … 60 64 include_package_data=True, 61 65 zip_safe=False, 66 dependency_links=dependency_links, 62 67 extras_require={ 63 68 'testing': tests_require,
Note: See TracChangeset
for help on using the changeset viewer.