Changeset 2f8a48f in OpenWorkouts-current
- Timestamp:
- Jan 25, 2019, 12:38:55 AM (5 years ago)
- Branches:
- current, feature/docs, master
- Children:
- 421f05f
- Parents:
- 6d1b54b
- Location:
- ow
- Files:
-
- 1 added
- 5 edited
Legend:
- Unmodified
- Added
- Removed
-
ow/models/user.py
r6d1b54b r2f8a48f 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
r6d1b54b r2f8a48f 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/tests/models/test_user.py
r6d1b54b r2f8a48f 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
r6d1b54b r2f8a48f 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/utilities.py
r6d1b54b r2f8a48f 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
Note: See TracChangeset
for help on using the changeset viewer.