Changes in / [4dcf28d:d0fc76b] in OpenWorkouts-current
- Files:
-
- 1 added
- 12 edited
Legend:
- Unmodified
- Added
- Removed
-
bin/js_deps
r4dcf28d rd0fc76b 227 227 } 228 228 229 d3() { 230 NAME="d3" 231 VERSION=5.7.0 232 URL=https://github.com/${NAME}/${NAME}/releases/download/v${VERSION}/${NAME}.zip 233 check_cache ${NAME} ${VERSION} 234 in_cache=$? 235 if [ ${in_cache} -eq 0 -a ${REINSTALL} -eq 1 ]; then 236 echo "${NAME}-${VERSION} $ALREADY_INSTALLED" 237 else 238 echo "> Installing ${NAME} ${VERSION}" 239 if [ -d ${COMPONENTS}/${NAME} ]; then 240 # Clean up, delete previous install before installing 241 rm -r ${COMPONENTS}/${NAME} 242 fi 243 cd ${TMP} 244 ${GET} ${URL} 245 ${UNZIP} ${NAME}.zip -d ${NAME} 246 cd ${CURRENT} 247 mv ${TMP}/${NAME} ${COMPONENTS}/ 248 echo "${NAME}-${VERSION}" >> ${CACHE} 249 echo "< Installed ${NAME} ${VERSION}" 250 fi 251 } 252 253 229 254 echo "Installing JS dependencies in ${COMPONENTS}" 230 255 … … 235 260 leaflet_elevation 236 261 pickadate 262 d3 263 237 264 238 265 # Clean up, remove the tmp directory -
ow/models/user.py
r4dcf28d rd0fc76b 1 1 from decimal import Decimal 2 from datetime import datetime, timedelta, timezone 2 3 from uuid import uuid1 3 4 from operator import attrgetter … … 8 9 9 10 from ow.catalog import get_catalog, reindex_object 11 from ow.utilities import get_week_days 10 12 11 13 … … 136 138 tree[year][month][sport] += 1 137 139 return tree 140 141 def stats(self, year=None, month=None): 142 year = year or datetime.now().year 143 stats = { 144 'workouts': 0, 145 'time': timedelta(seconds=0), 146 'distance': Decimal(0), 147 'elevation': Decimal(0), 148 'sports': {} 149 } 150 151 for workout in self.workouts(year=year, month=month): 152 stats['workouts'] += 1 153 stats['time'] += workout.duration or timedelta(seconds=0) 154 stats['distance'] += workout.distance or Decimal(0) 155 stats['elevation'] += workout.uphill or Decimal(0) 156 157 if workout.sport not in stats['sports']: 158 stats['sports'][workout.sport] = { 159 'workouts': 0, 160 'time': timedelta(seconds=0), 161 'distance': Decimal(0), 162 'elevation': Decimal(0), 163 } 164 165 stats['sports'][workout.sport]['workouts'] += 1 166 stats['sports'][workout.sport]['time'] += ( 167 workout.duration or timedelta(0)) 168 stats['sports'][workout.sport]['distance'] += ( 169 workout.distance or Decimal(0)) 170 stats['sports'][workout.sport]['elevation'] += ( 171 workout.uphill or Decimal(0)) 172 173 return stats 174 175 def get_week_stats(self, day): 176 """ 177 Return some stats for the week the given day is in. 178 """ 179 week = get_week_days(day) 180 181 # filter workouts 182 workouts = [] 183 for workout in self.workouts(): 184 if week[0].date() <= workout.start.date() <= week[-1].date(): 185 workouts.append(workout) 186 187 # build stats 188 stats = {} 189 for week_day in week: 190 stats[week_day] = { 191 'workouts': 0, 192 'time': timedelta(0), 193 'distance': Decimal(0), 194 'elevation': Decimal(0), 195 'sports': {} 196 } 197 for workout in workouts: 198 if workout.start.date() == week_day.date(): 199 day = stats[week_day] # less typing, avoid long lines 200 day['workouts'] += 1 201 day['time'] += workout.duration or timedelta(seconds=0) 202 day['distance'] += workout.distance or Decimal(0) 203 day['elevation'] += workout.uphill or Decimal(0) 204 if workout.sport not in day['sports']: 205 day['sports'][workout.sport] = { 206 'workouts': 0, 207 'time': timedelta(seconds=0), 208 'distance': Decimal(0), 209 'elevation': Decimal(0), 210 } 211 day['sports'][workout.sport]['workouts'] += 1 212 day['sports'][workout.sport]['time'] += ( 213 workout.duration or timedelta(0)) 214 day['sports'][workout.sport]['distance'] += ( 215 workout.distance or Decimal(0)) 216 day['sports'][workout.sport]['elevation'] += ( 217 workout.uphill or Decimal(0)) 218 219 return stats 220 221 @property 222 def week_stats(self): 223 """ 224 Helper that returns the week stats for the current week 225 """ 226 return self.get_week_stats(datetime.now(timezone.utc)) 227 228 @property 229 def week_totals(self): 230 week_stats = self.week_stats 231 return { 232 'distance': sum([week_stats[t]['distance'] for t in week_stats]), 233 'time': sum([week_stats[t]['time'] for t in week_stats], 234 timedelta()) 235 } -
ow/models/workout.py
r4dcf28d rd0fc76b 13 13 create_blob, 14 14 mps_to_kmph, 15 save_map_screenshot 15 save_map_screenshot, 16 timedelta_to_hms 16 17 ) 17 18 … … 110 111 111 112 def split_duration(self): 112 hours, remainder = divmod(int(self.duration.total_seconds()), 3600) 113 minutes, seconds = divmod(remainder, 60) 114 return hours, minutes, seconds 113 return timedelta_to_hms(self.duration) 115 114 116 115 @property -
ow/static/______css/__openworkouts.css
r4dcf28d rd0fc76b 12 12 background-color: #eeeeee; 13 13 } 14 15 /* dashboard, weekly stats */ 16 17 span.week_totals_left { 18 padding-right: 5px; 19 border-right: 1px solid #e1e1e1; 20 } 21 22 span.week_totals_right { 23 padding-left: 5px; 24 } 25 26 .x-axis path, .x-axis line { 27 fill: none; 28 stroke: none; 29 } 30 31 .bar { 32 fill: #f8b5be; 33 } 34 35 .bar:hover { 36 fill: #ee4056; 37 } 38 39 .current { 40 fill: #ee4056; 41 } 42 43 text.label { 44 font-size: 0.6em; 45 text-anchor: middle; 46 } -
ow/static/js/ow.js
r4dcf28d rd0fc76b 123 123 124 124 }; 125 126 127 owjs.week_chart = function(spec) { 128 129 "use strict"; 130 131 // parameters provided when creating an "instance" of the chart 132 var chart_selector = spec.chart_selector, 133 url = spec.url, 134 current_day_name = spec.current_day_name 135 136 // Helpers 137 function select_x_axis_label(d) { 138 /* Given a value, return the label associated with it */ 139 return d3.select('.x-axis') 140 .selectAll('text') 141 .filter(function(x) { return x == d.name; }); 142 } 143 144 // Methods 145 var render = function render() { 146 /* 147 Build a d3 bar chart, populated with data from the given url. 148 */ 149 var chart = d3.select("svg"), 150 margin = {top: 20, right: 20, bottom: 30, left: 50}, 151 width = +chart.attr("width") - margin.left - margin.right, 152 height = +chart.attr("height") - margin.top - margin.bottom, 153 g = chart.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"), 154 x = d3.scaleBand().rangeRound([0, width]).padding(0.1), 155 y = d3.scaleLinear().rangeRound([height, 0]); 156 157 d3.json(url).then(function (data) { 158 x.domain(data.map(function (d) { 159 return d.name; 160 })); 161 162 y.domain([0, d3.max(data, function (d) { 163 return Number(d.distance); 164 })]); 165 166 g.append("g") 167 .attr('class', 'x-axis') 168 .attr("transform", "translate(0," + height + ")") 169 .call(d3.axisBottom(x)) 170 171 g.selectAll(".bar") 172 .data(data) 173 .enter().append("rect") 174 .attr("class", function(d) { 175 if (d.name == current_day_name){ 176 select_x_axis_label(d).attr('style', "font-weight: bold;"); 177 return 'bar current' 178 } 179 else { 180 return 'bar' 181 } 182 }) 183 .attr("x", function (d) { 184 return x(d.name); 185 }) 186 .attr("y", function (d) { 187 return y(Number(d.distance)); 188 }) 189 .attr("width", x.bandwidth()) 190 .attr("height", function (d) { 191 return height - y(Number(d.distance)); 192 }) 193 .on('mouseover', function(d) { 194 if (d.name != current_day_name){ 195 select_x_axis_label(d).attr('style', "font-weight: bold;"); 196 } 197 }) 198 .on('mouseout', function(d) { 199 if (d.name != current_day_name){ 200 select_x_axis_label(d).attr('style', "font-weight: regular;"); 201 } 202 }); 203 204 g.selectAll(".text") 205 .data(data) 206 .enter() 207 .append("text") 208 .attr("class","label") 209 .attr("x", function (d) { 210 return x(d.name) + x.bandwidth()/2; 211 }) 212 .attr("y", function (d) { 213 return y(Number(d.distance) + 5); 214 }) 215 .text(function(d) { 216 if (Number(d.distance) > 0) { 217 return d.distance; 218 } 219 }); 220 221 }); 222 }; 223 224 var that = {} 225 that.render = render; 226 return that 227 228 }; -
ow/templates/dashboard.pt
r4dcf28d rd0fc76b 145 145 </ul> 146 146 </tal:activity_tree> 147 148 <tal:stats> 149 150 <div class="week-stats js-week-stats"> 151 <h3><tal:t i18n:translate="">This week</tal:t></h3> 152 <h4 tal:define="totals context.week_totals"> 153 <span class="week_totals_left"> 154 <tal:d tal:content="round(totals['distance'])"></tal:d> 155 <tal:t i18n:translate="">kms</tal:t> 156 </span> 157 <span class="week_totals_right"> 158 <tal:hms tal:define="hms timedelta_to_hms(totals['time'])"> 159 <tal:h tal:content="str(hms[0]).zfill(2)"></tal:h> 160 <tal:t i18n:translate="">hours</tal:t>, 161 <tal:h tal:content="str(hms[1]).zfill(2)"></tal:h> 162 <tal:t i18n:translate="">min.</tal:t> 163 </tal:hms> 164 </span> 165 </h4> 166 <svg width="300" height="200"></svg> 167 <style> 168 169 </style> 170 </div> 171 172 <div class="user-stats"> 173 <tal:year-stats tal:repeat="year context.activity_years"> 174 <h3><a href="" tal:content="year"></a></h3> 175 <ul tal:define="stats context.stats(year)"> 176 <li> 177 <span i18n:translate="">Workouts:</span> 178 <span tal:content="stats['workouts']"></span> 179 </li> 180 <li> 181 <span i18n:translate="">Distance:</span> 182 <span tal:content="round(stats['distance'])"></span> kms 183 </li> 184 <li> 185 <span i18n:translate="">Time:</span> 186 <tal:hms tal:define="hms timedelta_to_hms(stats['time'])"> 187 <span> 188 <tal:h tal:content="str(hms[0]).zfill(2)"></tal:h> 189 <tal:t i18n:translate="">hours</tal:t>, 190 <tal:h tal:content="str(hms[1]).zfill(2)"></tal:h> 191 <tal:t i18n:translate="">min.</tal:t> 192 </span> 193 </tal:hms> 194 </li> 195 <li> 196 <span i18n:translate="">Elevation:</span> 197 <span tal:content="stats['elevation']"></span> m 198 </li> 199 </ul> 200 </tal:year-stats> 201 </div> 202 </tal:stats> 203 147 204 </aside> 148 205 … … 151 208 </metal:content> 152 209 210 <metal:body-js metal:fill-slot="body-js"> 211 <script src="${request.static_url('ow:static/components/d3/d3.min.js')}"></script> 212 <script src="${request.static_url('ow:static/js/ow.js')}"></script> 213 214 <script type="text/javascript"> 215 var week_chart = owjs.week_chart({ 216 chart_selector: 'div.js-week-stats', 217 url: "${request.resource_url(context, 'week')}", 218 current_day_name: "${current_day_name}" 219 }); 220 week_chart.render(); 221 </script> 222 223 </metal:body-js> 224 153 225 </html> -
ow/tests/models/test_user.py
r4dcf28d rd0fc76b 1 from decimal import Decimal 1 2 from datetime import datetime, timedelta, timezone 2 3 … … 118 119 11: {'cycling': 1, 'running': 1}} 119 120 } 121 122 def test_stats(self, root): 123 expected_no_stats = { 124 'workouts': 0, 125 'time': timedelta(seconds=0), 126 'distance': Decimal(0), 127 'elevation': Decimal(0), 128 'sports': {} 129 } 130 # no stats 131 assert root['john'].stats() == expected_no_stats 132 # add a cycling workout 133 workout = Workout( 134 start=datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc), 135 duration=timedelta(minutes=(60*4)), 136 distance=115, 137 sport='cycling') 138 root['john'].add_workout(workout) 139 # asking for a different year, future 140 assert root['john'].stats(2019) == expected_no_stats 141 # asking for a different year, past 142 assert root['john'].stats(2016) == expected_no_stats 143 # asking fot the year the workout is in 144 assert root['john'].stats(2018) == { 145 'workouts': 1, 146 'time': timedelta(minutes=(60*4)), 147 'distance': Decimal(115), 148 'elevation': Decimal(0), 149 'sports': { 150 'cycling': { 151 'workouts': 1, 152 'time': timedelta(minutes=(60*4)), 153 'distance': Decimal(115), 154 'elevation': Decimal(0), 155 } 156 } 157 } 158 # add a second cycling workout 159 workout = Workout( 160 start=datetime(2018, 11, 26, 10, 00, tzinfo=timezone.utc), 161 duration=timedelta(minutes=(60*3)), 162 distance=100, 163 sport='cycling') 164 root['john'].add_workout(workout) 165 assert root['john'].stats(2018) == { 166 'workouts': 2, 167 'time': timedelta(minutes=(60*7)), 168 'distance': Decimal(215), 169 'elevation': Decimal(0), 170 'sports': { 171 'cycling': { 172 'workouts': 2, 173 'time': timedelta(minutes=(60*7)), 174 'distance': Decimal(215), 175 'elevation': Decimal(0), 176 } 177 } 178 } 179 # add a running workout 180 workout = Workout( 181 start=datetime(2018, 11, 26, 16, 00, tzinfo=timezone.utc), 182 duration=timedelta(minutes=(60)), 183 distance=10, 184 sport='running') 185 root['john'].add_workout(workout) 186 assert root['john'].stats(2018) == { 187 'workouts': 3, 188 'time': timedelta(minutes=(60*8)), 189 'distance': Decimal(225), 190 'elevation': Decimal(0), 191 'sports': { 192 'cycling': { 193 'workouts': 2, 194 'time': timedelta(minutes=(60*7)), 195 'distance': Decimal(215), 196 'elevation': Decimal(0), 197 }, 198 'running': { 199 'workouts': 1, 200 'time': timedelta(minutes=(60)), 201 'distance': Decimal(10), 202 'elevation': Decimal(0), 203 } 204 } 205 } 206 # ensure the stats for future/past years did not change after 207 # adding those workouts 208 assert root['john'].stats(2019) == expected_no_stats 209 assert root['john'].stats(2016) == expected_no_stats 210 211 def test_get_week_stats(self, root): 212 expected_no_stats_per_day = { 213 'workouts': 0, 214 'time': timedelta(0), 215 'distance': Decimal(0), 216 'elevation': Decimal(0), 217 'sports': {} 218 } 219 220 expected_no_stats = {} 221 for i in range(19, 26): 222 day = datetime(2018, 11, i, 10, 00, tzinfo=timezone.utc) 223 expected_no_stats[day] = expected_no_stats_per_day 224 225 day = datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc) 226 assert root['john'].get_week_stats(day) == expected_no_stats 227 228 # add a cycling workout 229 workout = Workout( 230 start=datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc), 231 duration=timedelta(minutes=(60*4)), 232 distance=115, 233 sport='cycling') 234 root['john'].add_workout(workout) 235 236 # check a week in the future 237 day = datetime(2019, 11, 25, 10, 00, tzinfo=timezone.utc) 238 week_stats = root['john'].get_week_stats(day) 239 for day in week_stats: 240 assert week_stats[day] == expected_no_stats_per_day 241 242 # check a week in the past 243 day = datetime(2017, 11, 25, 10, 00, tzinfo=timezone.utc) 244 week_stats = root['john'].get_week_stats(day) 245 for day in week_stats: 246 assert week_stats[day] == expected_no_stats_per_day 247 248 # Check the week where the workout is 249 day = datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc) 250 week_stats = root['john'].get_week_stats(day) 251 for day in week_stats: 252 if day.day == 25: 253 # this is the day where we have a workout 254 assert week_stats[day] == { 255 'workouts': 1, 256 'time': timedelta(minutes=(60*4)), 257 'distance': Decimal(115), 258 'elevation': Decimal(0), 259 'sports': { 260 'cycling': { 261 'workouts': 1, 262 'time': timedelta(minutes=(60*4)), 263 'distance': Decimal(115), 264 'elevation': Decimal(0) 265 } 266 } 267 } 268 else: 269 # day without workout 270 assert week_stats[day] == expected_no_stats_per_day 271 272 # add a second cycling workout 273 workout = Workout( 274 start=datetime(2018, 11, 23, 10, 00, tzinfo=timezone.utc), 275 duration=timedelta(minutes=(60*3)), 276 distance=100, 277 sport='cycling') 278 root['john'].add_workout(workout) 279 day = datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc) 280 week_stats = root['john'].get_week_stats(day) 281 for day in week_stats: 282 if day.day == 25: 283 # this is the day where we have a workout 284 assert week_stats[day] == { 285 'workouts': 1, 286 'time': timedelta(minutes=(60*4)), 287 'distance': Decimal(115), 288 'elevation': Decimal(0), 289 'sports': { 290 'cycling': { 291 'workouts': 1, 292 'time': timedelta(minutes=(60*4)), 293 'distance': Decimal(115), 294 'elevation': Decimal(0) 295 } 296 } 297 } 298 elif day.day == 23: 299 # this is the day where we have a workout 300 assert week_stats[day] == { 301 'workouts': 1, 302 'time': timedelta(minutes=(60*3)), 303 'distance': Decimal(100), 304 'elevation': Decimal(0), 305 'sports': { 306 'cycling': { 307 'workouts': 1, 308 'time': timedelta(minutes=(60*3)), 309 'distance': Decimal(100), 310 'elevation': Decimal(0) 311 } 312 } 313 } 314 else: 315 # day without workout 316 assert week_stats[day] == expected_no_stats_per_day 317 318 def test_week_stats(self, root): 319 expected_no_stats_per_day = { 320 'workouts': 0, 321 'time': timedelta(0), 322 'distance': Decimal(0), 323 'elevation': Decimal(0), 324 'sports': {} 325 } 326 327 # no workouts for the current week (this tests is for coverage 328 # purposes mostly, as the main logic is tested in test_get_week_stats) 329 day = datetime.now(timezone.utc) 330 week_stats = root['john'].get_week_stats(day) 331 for day in week_stats: 332 assert week_stats[day] == expected_no_stats_per_day 333 334 def test_week_totals(self, root): 335 # no data, empty totals 336 assert root['john'].week_totals == { 337 'distance': Decimal(0), 338 'time': timedelta(0) 339 } -
ow/tests/test_utilities.py
r4dcf28d rd0fc76b 1 1 import os 2 from datetime import timedelta 2 from datetime import timedelta, datetime 3 3 from unittest.mock import patch 4 4 from pyexpat import ExpatError … … 22 22 mps_to_kmph, 23 23 kmph_to_mps, 24 save_map_screenshot 24 save_map_screenshot, 25 timedelta_to_hms, 26 get_week_days 25 27 ) 26 28 … … 144 146 assert not os.makedirs.called 145 147 subprocess.run.assert_called_once 148 149 def test_timedelta_to_hms(self): 150 value = timedelta(seconds=0) 151 assert timedelta_to_hms(value) == (0, 0, 0) 152 value = timedelta(seconds=3600) 153 assert timedelta_to_hms(value) == (1, 0, 0) 154 value = timedelta(seconds=3900) 155 assert timedelta_to_hms(value) == (1, 5, 0) 156 value = timedelta(seconds=3940) 157 assert timedelta_to_hms(value) == (1, 5, 40) 158 value = timedelta(seconds=4) 159 assert timedelta_to_hms(value) == (0, 0, 4) 160 value = timedelta(seconds=150) 161 assert timedelta_to_hms(value) == (0, 2, 30) 162 # try now something that is not a timedelta 163 with pytest.raises(AttributeError): 164 timedelta_to_hms('not a timedelta') 165 166 def test_week_days(self): 167 # get days from a monday, week starting on monday 168 days = get_week_days(datetime(2019, 1, 21)) 169 assert len(days) == 7 170 matches = [ 171 [days[0], datetime(2019, 1, 21)], 172 [days[1], datetime(2019, 1, 22)], 173 [days[2], datetime(2019, 1, 23)], 174 [days[3], datetime(2019, 1, 24)], 175 [days[4], datetime(2019, 1, 25)], 176 [days[5], datetime(2019, 1, 26)], 177 [days[6], datetime(2019, 1, 27)] 178 ] 179 for m in matches: 180 assert m[0] == m[1] 181 # get days from a wednesday, week starting on monday 182 days = get_week_days(datetime(2019, 1, 23)) 183 assert len(days) == 7 184 matches = [ 185 [days[0], datetime(2019, 1, 21)], 186 [days[1], datetime(2019, 1, 22)], 187 [days[2], datetime(2019, 1, 23)], 188 [days[3], datetime(2019, 1, 24)], 189 [days[4], datetime(2019, 1, 25)], 190 [days[5], datetime(2019, 1, 26)], 191 [days[6], datetime(2019, 1, 27)] 192 ] 193 for m in matches: 194 assert m[0] == m[1] 195 # get days from a monday, but week starting on sunday now 196 days = get_week_days(datetime(2019, 1, 21), start_day=0) 197 assert len(days) == 7 198 matches = [ 199 [days[0], datetime(2019, 1, 20)], 200 [days[1], datetime(2019, 1, 21)], 201 [days[2], datetime(2019, 1, 22)], 202 [days[3], datetime(2019, 1, 23)], 203 [days[4], datetime(2019, 1, 24)], 204 [days[5], datetime(2019, 1, 25)], 205 [days[6], datetime(2019, 1, 26)] 206 ] 207 for m in matches: 208 assert m[0] == m[1] 146 209 147 210 -
ow/tests/views/test_user.py
r4dcf28d rd0fc76b 1 1 import os 2 import json 2 3 from datetime import datetime, timedelta, timezone 3 4 from shutil import copyfileobj … … 152 153 request = dummy_request 153 154 response = user_views.dashboard(john, request) 154 assert len(response) == 4 155 assert 'month_name' in response.keys() 155 assert len(response) == 6 156 assert 'month_name' in response.keys() 157 assert response['current_year'] == datetime.now().year 158 assert response['current_day_name'] == datetime.now().strftime('%a') 156 159 # this user has a single workout, in 2015 157 160 assert response['viewing_year'] == 2015 … … 167 170 request.GET['year'] = 2015 168 171 response = user_views.dashboard(john, request) 169 assert len(response) == 4 170 assert 'month_name' in response.keys() 172 assert len(response) == 6 173 assert 'month_name' in response.keys() 174 assert response['current_year'] == datetime.now().year 175 assert response['current_day_name'] == datetime.now().strftime('%a') 171 176 # this user has a single workout, in 2015 172 177 assert response['viewing_year'] == 2015 … … 176 181 request.GET['year'] = 2000 177 182 response = user_views.dashboard(john, request) 178 assert len(response) == 4 179 assert 'month_name' in response.keys() 183 assert len(response) == 6 184 assert 'month_name' in response.keys() 185 assert response['current_year'] == datetime.now().year 186 assert response['current_day_name'] == datetime.now().strftime('%a') 180 187 # this user has a single workout, in 2015 181 188 assert response['viewing_year'] == 2000 … … 194 201 request.GET['month'] = 6 195 202 response = user_views.dashboard(john, request) 196 assert len(response) == 4 197 assert 'month_name' in response.keys() 203 assert len(response) == 6 204 assert 'month_name' in response.keys() 205 assert response['current_year'] == datetime.now().year 206 assert response['current_day_name'] == datetime.now().strftime('%a') 198 207 # this user has a single workout, in 2015 199 208 assert response['viewing_year'] == 2015 … … 203 212 request.GET['month'] = 2 204 213 response = user_views.dashboard(john, request) 205 assert len(response) == 4 206 assert 'month_name' in response.keys() 214 assert len(response) == 6 215 assert 'month_name' in response.keys() 216 assert response['current_year'] == datetime.now().year 217 assert response['current_day_name'] == datetime.now().strftime('%a') 207 218 # this user has a single workout, in 2015 208 219 assert response['viewing_year'] == 2015 … … 213 224 request.GET['month'] = 6 214 225 response = user_views.dashboard(john, request) 215 assert len(response) == 4 216 assert 'month_name' in response.keys() 226 assert len(response) == 6 227 assert 'month_name' in response.keys() 228 assert response['current_year'] == datetime.now().year 229 assert response['current_day_name'] == datetime.now().strftime('%a') 217 230 # this user has a single workout, in 2015 218 231 assert response['viewing_year'] == 2010 … … 229 242 request.GET['month'] = 5 230 243 response = user_views.dashboard(john, request) 231 assert len(response) == 4 232 assert 'month_name' in response.keys() 244 assert len(response) == 6 245 assert 'month_name' in response.keys() 246 assert response['current_year'] == datetime.now().year 247 assert response['current_day_name'] == datetime.now().strftime('%a') 233 248 # this user has a single workout, in 2015 234 249 assert response['viewing_year'] == 2015 … … 238 253 request.GET['month'] = 6 239 254 response = user_views.dashboard(john, request) 240 assert len(response) == 4 241 assert 'month_name' in response.keys() 255 assert len(response) == 6 256 assert 'month_name' in response.keys() 257 assert response['current_year'] == datetime.now().year 258 assert response['current_day_name'] == datetime.now().strftime('%a') 242 259 # this user has a single workout, in 2015 243 260 assert response['viewing_year'] == 2015 … … 583 600 assert 'jack.black@example.net' not in request.root.emails 584 601 assert 'JackBlack' not in request.root.all_nicknames 602 603 def test_week_stats_no_stats(self, dummy_request, john): 604 response = user_views.week_stats(john, dummy_request) 605 assert isinstance(response, Response) 606 assert response.content_type == 'application/json' 607 # the body is a valid json-encoded stream 608 obj = json.loads(response.body) 609 assert obj == [ 610 {'distance': 0, 'elevation': 0, 'name': 'Mon', 611 'time': '00', 'workouts': 0}, 612 {'distance': 0, 'elevation': 0, 'name': 'Tue', 613 'time': '00', 'workouts': 0}, 614 {'distance': 0, 'elevation': 0, 'name': 'Wed', 615 'time': '00', 'workouts': 0}, 616 {'distance': 0, 'elevation': 0, 'name': 'Thu', 617 'time': '00', 'workouts': 0}, 618 {'distance': 0, 'elevation': 0, 'name': 'Fri', 619 'time': '00', 'workouts': 0}, 620 {'distance': 0, 'elevation': 0, 'name': 'Sat', 621 'time': '00', 'workouts': 0}, 622 {'distance': 0, 'elevation': 0, 'name': 'Sun', 623 'time': '00', 'workouts': 0} 624 ] 625 626 def test_week_stats(self, dummy_request, john): 627 workout = Workout( 628 start=datetime.now(timezone.utc), 629 duration=timedelta(minutes=60), 630 distance=30, 631 elevation=540 632 ) 633 john.add_workout(workout) 634 response = user_views.week_stats(john, dummy_request) 635 assert isinstance(response, Response) 636 assert response.content_type == 'application/json' 637 # the body is a valid json-encoded stream 638 obj = json.loads(response.body) 639 assert len(obj) == 7 640 for day in obj: 641 if datetime.now(timezone.utc).strftime('%a') == day['name']: 642 day['distance'] == 30 643 day['elevation'] == 540 644 day['time'] == '01' 645 day['workouts'] == 1 646 else: 647 day['distance'] == 0 648 day['elevation'] == 0 649 day['time'] == '00' 650 day['workouts'] == 0 -
ow/utilities.py
r4dcf28d rd0fc76b 3 3 import logging 4 4 import subprocess 5 from datetime import datetime 5 from datetime import datetime, timedelta 6 6 from decimal import Decimal 7 7 from shutil import copyfileobj … … 212 212 213 213 return False 214 215 216 def timedelta_to_hms(value): 217 """ 218 Return hours, minutes, seconds from a timedelta object 219 """ 220 hours, remainder = divmod(int(value.total_seconds()), 3600) 221 minutes, seconds = divmod(remainder, 60) 222 return hours, minutes, seconds 223 224 225 def get_week_days(day, start_day=1): 226 """ 227 Return a list of datetime objects for the days of the week "day" is in. 228 229 day is a datetime object (like in datetime.now() for "today") 230 231 start_day can be used to set if week starts on monday (1) or sunday (0) 232 """ 233 first_day = day - timedelta(days=day.isoweekday() - start_day) 234 week_days = [first_day + timedelta(days=i) for i in range(7)] 235 return week_days -
ow/views/user.py
r4dcf28d rd0fc76b 1 import json 1 2 from calendar import month_name 3 from datetime import datetime 2 4 3 5 from pyramid.httpexceptions import HTTPFound … … 17 19 from ..models.root import OpenWorkouts 18 20 from ..views.renderers import OWFormRenderer 21 from ..utilities import timedelta_to_hms 19 22 20 23 _ = TranslationStringFactory('OpenWorkouts') … … 143 146 144 147 return { 148 'current_year': datetime.now().year, 149 'current_day_name': datetime.now().strftime('%a'), 145 150 'month_name': month_name, 146 151 'viewing_year': viewing_year, … … 223 228 return HTTPFound(location=request.resource_url(context, 'profile')) 224 229 return {'form': OWFormRenderer(form)} 230 231 232 @view_config( 233 context=User, 234 permission='view', 235 name='week') 236 def week_stats(context, request): 237 stats = context.week_stats 238 json_stats = [] 239 for day in stats: 240 hms = timedelta_to_hms(stats[day]['time']) 241 day_stats = { 242 'name': day.strftime('%a'), 243 'time': str(hms[0]).zfill(2), 244 'distance': int(round(stats[day]['distance'])), 245 'elevation': int(stats[day]['elevation']), 246 'workouts': stats[day]['workouts'] 247 } 248 json_stats.append(day_stats) 249 return Response(content_type='application/json', 250 charset='utf-8', 251 body=json.dumps(json_stats)) -
ow/views/workout.py
r4dcf28d rd0fc76b 1 from decimal import Decimal 1 2 from datetime import datetime, timedelta, time, timezone 2 3 … … 121 122 tzinfo=timezone.utc) 122 123 context.start = start 124 # ensure distance is a decimal 125 context.distance = Decimal(context.distance) 123 126 catalog = get_catalog(context) 124 127 reindex_object(catalog, context)
Note: See TracChangeset
for help on using the changeset viewer.