Changeset 778d53d in OpenWorkouts-current
- Timestamp:
- Mar 5, 2019, 11:45:32 PM (5 years ago)
- Branches:
- current
- Children:
- aa6dcaf
- Parents:
- 35953eb
- Files:
-
- 1 added
- 10 edited
Legend:
- Unmodified
- Added
- Removed
-
bin/js_deps
r35953eb r778d53d 251 251 } 252 252 253 jquery_dropdown() { 254 # We need version "master", as the latest release does not work properly 255 NAME="jquery-dropdown" 256 VERSION=master 257 URL=https://github.com/soundasleep/${NAME}/archive/${VERSION}.tar.gz 258 check_cache ${NAME} ${VERSION} 259 in_cache=$? 260 if [ ${in_cache} -eq 0 -a ${REINSTALL} -eq 1 ]; then 261 echo "${NAME}-${VERSION} $ALREADY_INSTALLED" 262 else 263 echo "> Installing ${NAME} ${VERSION}" 264 if [ -d ${COMPONENTS}/${NAME} ]; then 265 # Clean up, delete previous install before installing 266 rm -r ${COMPONENTS}/${NAME} 267 fi 268 mkdir ${COMPONENTS}/${NAME} 269 cd ${TMP} 270 ${GET} ${URL} 271 ${TAR} ${VERSION}.tar.gz 272 cd ${CURRENT} 273 mv ${TMP}/${NAME}-${VERSION}/jquery.dropdown.{css,js} ${COMPONENTS}/${NAME} 274 echo "${NAME}-${VERSION}" >> ${CACHE} 275 echo "< Installed ${NAME} ${VERSION}" 276 fi 277 } 278 253 279 254 280 echo "Installing JS dependencies in ${COMPONENTS}" … … 261 287 pickadate 262 288 d3 289 jquery_dropdown 263 290 264 291 -
ow/models/user.py
r35953eb r778d53d 113 113 def num_workouts(self): 114 114 return len(self.workout_ids()) 115 116 @property 117 def favorite_sport(self): 118 """ 119 Return which sport is the one with most workouts for this user. 120 In case of more than one sport with the maximum number of workouts, 121 return the first in reversed alphabetical ordering 122 """ 123 sports = {} 124 for w in self.workouts(): 125 if w.sport not in sports.keys(): 126 sports[w.sport] = 0 127 sports[w.sport] += 1 128 _sports = sorted(sports.items(), reverse=True, 129 key=lambda x: (x[1], x[0])) 130 if _sports: 131 return _sports[0][0] 132 return None 133 134 @property 135 def activity_sports(self): 136 return sorted(list(set(w.sport for w in self.workouts()))) 115 137 116 138 @property … … 368 390 369 391 return stats 392 393 def sport_totals(self, sport=None, year=None): 394 """ 395 Return totals for this user, filtered by sport. 396 397 If no sport is passed, the favorite sport is picked up 398 399 If the additional parameter year is passed, show stats only 400 for that year 401 """ 402 totals = { 403 'workouts': 0, 404 'time': timedelta(0), 405 'distance': Decimal(0), 406 'elevation': Decimal(0), 407 } 408 if self.activity_sports: 409 sport = sport or self.favorite_sport 410 for workout in self.workouts(): 411 if workout.sport == sport: 412 if year is None or workout.start.year == year: 413 totals['workouts'] += 1 414 totals['time'] += workout.duration or timedelta(0) 415 totals['distance'] += workout.distance or Decimal(0) 416 totals['elevation'] += workout.uphill or Decimal(0) 417 return totals -
ow/static/css/main.css
r35953eb r778d53d 642 642 background-color: #EE4056; 643 643 } 644 /* 645 Very simple way to paint dropdown-like arrows, without 646 using any external dependencies. 647 648 To use it, add something like this to your html, to display a "down" arrow: 649 650 <i class="arrow down"></i> 651 */ 652 i.arrow { 653 border: solid black; 654 border-width: 0 3px 3px 0; 655 display: inline-block; 656 padding: 3px; 657 margin: 3px; 658 margin-left: 6px; 659 margin-right: 0px; 660 } 661 .right { 662 transform: rotate(-45deg); 663 -webkit-transform: rotate(-45deg); 664 } 665 .left { 666 transform: rotate(135deg); 667 -webkit-transform: rotate(135deg); 668 } 669 .up { 670 transform: rotate(-135deg); 671 -webkit-transform: rotate(-135deg); 672 } 673 .down { 674 transform: rotate(45deg); 675 -webkit-transform: rotate(45deg); 676 } 644 677 .header-content { 645 678 padding: 1em 1.5em; … … 1488 1521 font-size: 13px; 1489 1522 font-size: 0.8125rem; 1523 } 1524 .profile-dropdown-sports, 1525 .profile-dropdown-years { 1526 color: #151515; 1527 text-decoration: none; 1528 } 1529 .profile-dropdown-sports:hover, 1530 .profile-dropdown-years:hover { 1531 color: #151515; 1532 } 1533 .profile-dropdown-sports:active, 1534 .profile-dropdown-years:active, 1535 .profile-dropdown-sports:focus, 1536 .profile-dropdown-years:focus { 1537 outline: 0; 1538 border: none; 1539 -moz-outline-style: none; 1490 1540 } 1491 1541 .verify-account-content { -
ow/static/js/ow.js
r35953eb r778d53d 486 486 487 487 }; 488 489 490 owjs.sport_stats = function(spec) { 491 492 "use strict"; 493 494 var link_selector = spec.link_selector; 495 var stats_selector = spec.stats_selector; 496 var selected = spec.selected; 497 var dropdown_selector = spec.dropdown_selector; 498 var current_year = spec.current_year; 499 var year_link_selector = spec.year_link_selector; 500 501 var setup = function setup() { 502 // Hide all sports stats by default 503 $(stats_selector).hide(); 504 // Show the pre-selected one 505 $(selected).show(); 506 507 $(link_selector).on('click', function(e) { 508 e.preventDefault(); 509 var selected = $(this).attr('class').split(' ')[1]; 510 var sport = selected.split('-')[1] 511 // Hide them all 512 $(stats_selector).hide(); 513 // Show the selected one 514 $(stats_selector + '.' + selected).show(); 515 // Update the sport on the sports selector widget 516 $(dropdown_selector + ' strong').html(sport); 517 // finally "click" on the proper year to be displayed for this sport 518 $(year_link_selector + sport + '-' + current_year).click(); 519 }); 520 }; 521 522 var that = {} 523 that.setup = setup; 524 return that 525 526 }; 527 528 529 owjs.year_stats = function(spec) { 530 531 "use strict"; 532 533 var link_selector = spec.link_selector; 534 var stats_selector = spec.stats_selector; 535 var selected = spec.selected; 536 var dropdown_selector = spec.dropdown_selector; 537 538 var setup = function setup() { 539 // Hide all years stats by default 540 $(stats_selector).hide(); 541 // Show the pre-selected one 542 $(selected).show(); 543 544 $(link_selector).on('click', function(e) { 545 e.preventDefault(); 546 var selected = $(this).attr('class').split(' ')[1]; 547 var sport = selected.split('-')[1] 548 var year = selected.split('-')[2] 549 // Hide them all 550 $(stats_selector).hide(); 551 // Show the selected one 552 $(stats_selector + '.' + selected).show(); 553 // Update the year on the years selector widget 554 $(dropdown_selector + sport + ' strong').html(year); 555 }); 556 }; 557 558 var that = {} 559 that.setup = setup; 560 return that 561 562 }; -
ow/static/less/main.less
r35953eb r778d53d 23 23 @import "ui/form.less"; 24 24 @import "ui/buttons.less"; 25 @import "ui/arrows.less"; 25 26 26 27 // Modules -
ow/static/less/pages/profile.less
r35953eb r778d53d 128 128 } 129 129 } 130 131 .profile-dropdown-sports, 132 .profile-dropdown-years { 133 color: @color-main; 134 text-decoration: none; 135 &:hover { 136 color: @color-main; 137 } 138 &:active, &:focus { 139 outline: 0; 140 border: none; 141 -moz-outline-style: none; 142 } 143 } -
ow/templates/profile.pt
r35953eb r778d53d 11 11 <tal:t i18n:translate="">My profile</tal:t> 12 12 </metal:head-title> 13 14 <metal:css metal:fill-slot="css"> 15 <link rel="stylesheet" href="${request.static_url('ow:static/components/jquery-dropdown/jquery.dropdown.css')}" /> 16 </metal:css> 13 17 14 18 <metal:content metal:fill-slot="content"> … … 168 172 169 173 <div class="workout-aside"> 174 175 <h3 i18n:translate="">Profile info</h3> 176 170 177 <ul class="profile-data"> 171 178 <li> … … 188 195 </li> 189 196 </ul> 197 198 <tal:has-workouts tal:condition="profile_stats['sports']"> 199 200 <h3 i18n:translate="">Workout stats</h3> 201 202 <p> 203 <a href="" data-jq-dropdown="#jq-dropdown-sports" 204 class="profile-dropdown-sports js-jq-dropdown-sel-sports"> 205 <strong tal:content="profile_stats['current_sport']"></strong> 206 <i class="arrow down"></i> 207 </a> 208 </p> 209 210 <tal:sports tal:repeat="sport profile_stats['sports']"> 211 <div class="" tal:attributes="class 'js-sport-stats js-' + sport"> 212 213 <a href="" data-jq-dropdown="" 214 tal:attributes="data-jq-dropdown '#jq-dropdown-' + sport; 215 class 'profile-dropdown-years js-jq-dropdown-sel-' + sport"> 216 <strong tal:content="profile_stats['current_year']"></strong> 217 <i class="arrow down"></i> 218 </a> 219 220 <tal:years tal:repeat="year profile_stats['years']"> 221 <div class="" tal:attributes="class 'js-year-stats js-' + sport + '-' + str(year)"> 222 <ul class="profile-data" 223 tal:define="sport_totals context.sport_totals(sport, year)"> 224 <li> 225 <span> 226 <tal:t i18n:translate="">Workouts</tal:t> 227 </span> 228 <tal:w tal:replace="sport_totals['workouts']"></tal:w> 229 </li> 230 <li> 231 <span> 232 <tal:t i18n:translate="">Time</tal:t> 233 </span> 234 <tal:hms tal:define="hms timedelta_to_hms(sport_totals['time'])"> 235 <tal:h tal:content="str(hms[0]).zfill(2)"></tal:h> 236 <tal:t i18n:translate="">hours</tal:t>, 237 <tal:h tal:content="str(hms[1]).zfill(2)"></tal:h> 238 <tal:t i18n:translate="">min.</tal:t> 239 </tal:hms> 240 </li> 241 <li> 242 <span> 243 <tal:t i18n:translate="">Distance</tal:t> 244 </span> 245 <tal:w tal:replace="round(sport_totals['distance'])"></tal:w> 246 <tal:t i18n:translate="">km</tal:t> 247 </li> 248 <li> 249 <span> 250 <tal:t i18n:translate="">Elevation</tal:t> 251 </span> 252 <tal:w tal:replace="round(sport_totals['elevation'])"></tal:w> 253 <tal:t i18n:translate="">m</tal:t> 254 </li> 255 </ul> 256 </div> 257 </tal:years> 258 259 <strong i18n:translate="">All time</strong> 260 <ul class="profile-data" 261 tal:define="sport_totals context.sport_totals(sport)"> 262 <li> 263 <span> 264 <tal:t i18n:translate="">Workouts</tal:t> 265 </span> 266 <tal:w tal:replace="sport_totals['workouts']"></tal:w> 267 </li> 268 <li> 269 <span> 270 <tal:t i18n:translate="">Time</tal:t> 271 </span> 272 <tal:hms tal:define="hms timedelta_to_hms(sport_totals['time'])"> 273 <tal:h tal:content="str(hms[0]).zfill(2)"></tal:h> 274 <tal:t i18n:translate="">hours</tal:t>, 275 <tal:h tal:content="str(hms[1]).zfill(2)"></tal:h> 276 <tal:t i18n:translate="">min.</tal:t> 277 </tal:hms> 278 </li> 279 <li> 280 <span> 281 <tal:t i18n:translate="">Distance</tal:t> 282 </span> 283 <tal:w tal:replace="round(sport_totals['distance'])"></tal:w> 284 <tal:t i18n:translate="">km</tal:t> 285 </li> 286 <li> 287 <span> 288 <tal:t i18n:translate="">Elevation</tal:t> 289 </span> 290 <tal:w tal:replace="round(sport_totals['elevation'])"></tal:w> 291 <tal:t i18n:translate="">m</tal:t> 292 </li> 293 </ul> 294 </div> 295 </tal:sports> 296 297 </tal:has-workouts> 298 190 299 </div> 191 300 </div> 192 301 </div> 193 302 303 <div id="jq-dropdown-sports" class="jq-dropdown jq-dropdown-tip"> 304 <ul class="jq-dropdown-menu"> 305 <tal:sports tal:repeat="sport profile_stats['sports']"> 306 <li> 307 <a href="#" class="" tal:content="sport" 308 tal:attributes="class 'js-choose-sport-stats js-' + sport"> 309 </a> 310 </li> 311 </tal:sports> 312 </ul> 313 </div> 314 315 316 <tal:sports tal:repeat="sport profile_stats['sports']"> 317 <div id="" class="jq-dropdown jq-dropdown-tip" 318 tal:attributes="id 'jq-dropdown-' + sport"> 319 <ul class="jq-dropdown-menu"> 320 <tal:years tal:repeat="year profile_stats['years']"> 321 <li> 322 <a href="#" class="" tal:content="year" 323 tal:attributes="class 'js-choose-year-stats js-' + sport + '-' + str(year)"> 324 </a> 325 </li> 326 </tal:years> 327 </ul> 328 </div> 329 </tal:sports> 330 331 194 332 </metal:content> 195 333 196 334 <metal:body-js metal:fill-slot="body-js"> 197 335 336 <script src="${request.static_url('ow:static/components/jquery-dropdown/jquery.dropdown.js')}"></script> 198 337 <script src="${request.static_url('ow:static/components/d3/d3.min.js')}"></script> 199 338 <script src="${request.static_url('ow:static/js/ow.js')}"></script> … … 204 343 }) 205 344 map_shots.run(); 345 346 var sport_stats = owjs.sport_stats({ 347 link_selector: 'a.js-choose-sport-stats', 348 stats_selector: 'div.js-sport-stats', 349 selected: 'div.js-sport-stats.js-${profile_stats['current_sport']}', 350 dropdown_selector: 'a.js-jq-dropdown-sel-sports', 351 current_year: '${profile_stats['current_year']}', 352 year_link_selector: 'a.js-choose-year-stats.js-' 353 }) 354 sport_stats.setup(); 355 356 var year_stats = owjs.year_stats({ 357 link_selector: 'a.js-choose-year-stats', 358 stats_selector: 'div.js-year-stats', 359 selected: 'div.js-year-stats.js-${profile_stats['current_sport']}-${profile_stats['current_year']}', 360 dropdown_selector: 'a.js-jq-dropdown-sel-' 361 }) 362 year_stats.setup(); 206 363 207 364 var y_axis_labels = { -
ow/tests/models/test_user.py
r35953eb r778d53d 81 81 assert list(root['john'].workout_ids()) == ['1', '2', '3'] 82 82 assert root['john'].num_workouts == len(workouts) 83 84 def test_favorite_sport(self, root): 85 assert root['john'].favorite_sport is None 86 # add a cycling workout 87 workout = Workout( 88 sport='cycling', 89 start=datetime.now(timezone.utc), 90 duration=timedelta(minutes=120), 91 distance=66, 92 ) 93 root['john'].add_workout(workout) 94 assert root['john'].favorite_sport == 'cycling' 95 # add a running workout, both sports have same amount of workouts, 96 # favorite is picked up reversed alphabetically 97 workout = Workout( 98 sport='running', 99 start=datetime.now(timezone.utc), 100 duration=timedelta(minutes=45), 101 distance=5, 102 ) 103 root['john'].add_workout(workout) 104 assert root['john'].favorite_sport == 'running' 105 # add another cycling workout, now that is the favorite sport 106 workout = Workout( 107 sport='cycling', 108 start=datetime.now(timezone.utc), 109 duration=timedelta(minutes=60), 110 distance=30, 111 ) 112 root['john'].add_workout(workout) 113 assert root['john'].favorite_sport == 'cycling' 114 115 def test_activity_sports(self, root): 116 assert root['john'].activity_sports == [] 117 workout = Workout( 118 sport='cycling', 119 start=datetime.now(timezone.utc), 120 duration=timedelta(minutes=120), 121 distance=66, 122 ) 123 root['john'].add_workout(workout) 124 assert root['john'].activity_sports == ['cycling'] 125 workout = Workout( 126 sport='running', 127 start=datetime.now(timezone.utc), 128 duration=timedelta(minutes=45), 129 distance=5, 130 ) 131 root['john'].add_workout(workout) 132 assert root['john'].activity_sports == ['cycling', 'running'] 133 134 def test_activity_years(self, root): 135 assert root['john'].activity_years == [] 136 workout = Workout( 137 sport='cycling', 138 start=datetime.now(timezone.utc), 139 duration=timedelta(minutes=120), 140 distance=66, 141 ) 142 root['john'].add_workout(workout) 143 assert root['john'].activity_years == [datetime.now(timezone.utc).year] 144 workout = Workout( 145 sport='running', 146 start=datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc), 147 duration=timedelta(minutes=45), 148 distance=5, 149 ) 150 root['john'].add_workout(workout) 151 assert root['john'].activity_years == [ 152 datetime.now(timezone.utc).year, 153 2018 154 ] 155 156 def test_activity_months(self, root): 157 # we have to pass a year parameter 158 with pytest.raises(TypeError): 159 root['john'].activity_months() 160 now = datetime.now(timezone.utc) 161 assert root['john'].activity_months(now.year) == [] 162 workout = Workout( 163 sport='cycling', 164 start=datetime.now(timezone.utc), 165 duration=timedelta(minutes=120), 166 distance=66, 167 ) 168 root['john'].add_workout(workout) 169 assert root['john'].activity_months(now.year) == [now.month] 170 assert root['john'].activity_months(now.year-1) == [] 171 assert root['john'].activity_months(now.year+1) == [] 172 workout = Workout( 173 sport='running', 174 start=datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc), 175 duration=timedelta(minutes=45), 176 distance=5, 177 ) 178 root['john'].add_workout(workout) 179 assert root['john'].activity_months(now.year) == [now.month] 180 assert root['john'].activity_months(2018) == [11] 181 assert root['john'].activity_months(now.year+1) == [] 83 182 84 183 def test_activity_dates_tree(self, root): … … 552 651 else: 553 652 assert stats == expected_no_stats_per_week 653 654 def test_sport_totals(self, root): 655 # user has no workouts, so no totals 656 assert root['john'].sport_totals() == { 657 'workouts': 0, 658 'time': timedelta(0), 659 'distance': Decimal(0), 660 'elevation': Decimal(0), 661 } 662 # add a cycling workout happening now 663 workout = Workout( 664 sport='cycling', 665 start=datetime.now(timezone.utc), 666 duration=timedelta(minutes=120), 667 distance=66, 668 ) 669 root['john'].add_workout(workout) 670 # only one workout, one sport, so the default will show totals 671 # for that sport 672 assert root['john'].sport_totals() == { 673 'workouts': 1, 674 'time': timedelta(minutes=120), 675 'distance': Decimal(66), 676 'elevation': Decimal(0), 677 } 678 # Add a running workout 679 workout = Workout( 680 sport='running', 681 start=datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc), 682 duration=timedelta(minutes=45), 683 distance=5, 684 ) 685 root['john'].add_workout(workout) 686 # the favorite sport is running now 687 assert root['john'].sport_totals() == { 688 'workouts': 1, 689 'time': timedelta(minutes=45), 690 'distance': Decimal(5), 691 'elevation': Decimal(0), 692 } 693 # but we can get the totals for cycling too 694 assert root['john'].sport_totals('cycling') == { 695 'workouts': 1, 696 'time': timedelta(minutes=120), 697 'distance': Decimal(66), 698 'elevation': Decimal(0), 699 } 700 # adding a new cycling workout, in a different year 701 workout = Workout( 702 sport='cycling', 703 start=datetime(2017, 11, 25, 10, 00, tzinfo=timezone.utc), 704 duration=timedelta(minutes=60), 705 distance=32, 706 ) 707 root['john'].add_workout(workout) 708 # now cycling is the favorite sport 709 assert root['john'].sport_totals() == { 710 'workouts': 2, 711 'time': timedelta(minutes=180), 712 'distance': Decimal(98), 713 'elevation': Decimal(0), 714 } 715 # but we can get running stats too 716 assert root['john'].sport_totals('running') == { 717 'workouts': 1, 718 'time': timedelta(minutes=45), 719 'distance': Decimal(5), 720 'elevation': Decimal(0), 721 } 722 # there are no running activities for 2016 723 assert root['john'].sport_totals('running', 2016) == { 724 'workouts': 0, 725 'time': timedelta(0), 726 'distance': Decimal(0), 727 'elevation': Decimal(0), 728 } 729 # and not activities for cycling in 2016 neither 730 assert root['john'].sport_totals('cycling', 2016) == { 731 'workouts': 0, 732 'time': timedelta(0), 733 'distance': Decimal(0), 734 'elevation': Decimal(0), 735 } 736 # and we can get the separate totals for cycling in different years 737 year = datetime.now(timezone.utc).year 738 assert root['john'].sport_totals('cycling', year) == { 739 'workouts': 1, 740 'time': timedelta(minutes=120), 741 'distance': Decimal(66), 742 'elevation': Decimal(0), 743 } 744 assert root['john'].sport_totals('cycling', 2017) == { 745 'workouts': 1, 746 'time': timedelta(minutes=60), 747 'distance': Decimal(32), 748 'elevation': Decimal(0), 749 } -
ow/tests/views/test_user.py
r35953eb r778d53d 46 46 start=datetime(2015, 6, 28, 12, 55, tzinfo=timezone.utc), 47 47 duration=timedelta(minutes=60), 48 distance=30 48 distance=30, 49 sport='cycling' 49 50 ) 50 51 john.add_workout(workout) … … 413 414 # profile page for the current day (no workouts avalable) 414 415 response = user_views.profile(john, request) 415 assert len(response.keys()) == 6416 assert len(response.keys()) == 7 416 417 current_month = datetime.now(timezone.utc).strftime('%Y-%m') 417 418 assert response['user'] == john … … 425 426 'elevation': Decimal(0) 426 427 } 428 assert response['profile_stats'] == { 429 'sports': ['cycling'], 430 'years': [2015], 431 'current_year': datetime.now(timezone.utc).year, 432 'current_sport': 'cycling' 433 } 427 434 # profile page for a previous date, that has workouts 428 435 request.GET['year'] = 2015 429 436 request.GET['month'] = 6 430 437 response = user_views.profile(john, request) 431 assert len(response.keys()) == 6438 assert len(response.keys()) == 7 432 439 assert response['user'] == john 433 440 assert response['user_gender'] == 'Robot' … … 446 453 request.GET['week'] = 25 447 454 response = user_views.profile(john, request) 448 assert len(response.keys()) == 6455 assert len(response.keys()) == 7 449 456 assert response['user'] == john 450 457 assert response['user_gender'] == 'Robot' … … 462 469 request.GET['week'] = 26 463 470 response = user_views.profile(john, request) 464 assert len(response.keys()) == 6471 assert len(response.keys()) == 7 465 472 assert response['user'] == john 466 473 assert response['user_gender'] == 'Robot' -
ow/views/user.py
r35953eb r778d53d 305 305 user_gender = localizer.translate(g[1]) 306 306 307 # get some data to be shown in the "profile stats" totals column 308 profile_stats = { 309 'sports': user.activity_sports, 310 'years': user.activity_years, 311 'current_year': request.GET.get('stats_year', now.year), 312 'current_sport': request.GET.get('stats_sport', user.favorite_sport), 313 } 314 307 315 return { 308 316 'user': user, … … 312 320 year=str(year), month=str(month).zfill(2)), 313 321 'current_week': week, 314 'totals': totals 322 'totals': totals, 323 'profile_stats': profile_stats 315 324 } 316 325
Note: See TracChangeset
for help on using the changeset viewer.