Workout With Friends Stay fit with a little motivation

- Main Page
- Namespaces
- Classes
- Files

- File List
- File Members

All Classes Namespaces Files Functions Variables Properties
user.py
Go to the documentation of this file.
1 from __future__ import unicode_literals
2 import hashlib
3 import os
4 from sqlalchemy.orm import synonym
5 from sqlalchemy.schema import Column
6 from sqlalchemy.sql import and_, or_, func
7 from sqlalchemy.types import (
8 Boolean, CHAR, Date, DateTime, Enum, Integer, Numeric,
9 Unicode)
10 from wowf.config import settings
11 from wowf.lib.image import StoredImage, upload_and_make_thumbnails
12 from wowf.lib.pagination import Pager
13 from wowf.lib.utils import calculate_bmi, current_timestamp, years_ago
14 from wowf.models.meta import Base, DBSession
15
16
17 # BMI category ranges
18 UNDER = 18.5
19 NORMAL = (18.5, 24.9)
20 OVER = (25.0, 29.9)
21 OBESE = 30.0
22
23
24 class User(Base):
25
26 __tablename__ = 'users'
27 index_fields = ['username']
28 id = Column(Integer(unsigned=True), primary_key=True)
29 username = Column(Unicode(10), nullable=False, unique=True)
30 email = Column(Unicode(254), nullable=False, unique=True)
31 _password = Column('password', CHAR(60), nullable=False)
32 gender = Column(Enum('F', 'M', name='user_genders'), nullable=False)
33 dob = Column(Date, nullable=False)
34 weight = Column(Numeric(7, 4), nullable=False, doc='weight in kilograms')
35 height = Column(Numeric(5, 4), nullable=False, doc='height in meters')
36 timezone = Column(Unicode(50))
37 _avatar = Column('avatar', Unicode(40))
38 is_active = Column(Boolean, nullable=False, default=True)
39 created_at = Column(DateTime, nullable=False, default=current_timestamp)
40 last_active_at = Column(DateTime, nullable=False, default=current_timestamp)
41
42 def _get_password(self):
43 return self._password
44
45 ##
46 #
47 # Hash the given password.
48 #
49 def _set_password(self, password):
50 from wowf.lib.auth import Auth
51 self._password = Auth.hash_password(password)
52
53 password = synonym('_password', descriptor=property(_get_password, _set_password))
54
55 ##
56 #
57 # Return a stored image, to allow different versions of the avatar to be
58 # served.
59 #
60 # e.g: `user.avatar.large` would return the large version, while
61 # `user.avatar` would return the original.
62 #
63 def _get_avatar(self):
64 avatar = self._avatar
65 versions = settings.from_prefix('avatar_size_')
66 if not avatar:
67 avatar = settings.avatar_default
68 return StoredImage(settings.avatar_dir, avatar, versions)
69
70 ##
71 #
72 # Upload the avatar and set the necessary reference to it.
73 #
74 def _set_avatar(self, avatar):
75 filename = '%s%s' % (hashlib.md5(str(self.id)).hexdigest(),
76 os.path.splitext(avatar.filename)[1])
77 versions = settings.from_prefix('avatar_size_')
78 upload_and_make_thumbnails(avatar.file, settings.avatar_dir, filename, versions)
79 self._avatar = filename
80
81 avatar = synonym('_avatar', descriptor=property(_get_avatar, _set_avatar))
82
83 @property
84 ##
85 #
86 # Calculate this users age.
87 #
88 def age(self):
89 return years_ago(self.dob)
90
91 @property
92 ##
93 #
94 # Calculate this users Body Mass Index, which is a useful indicator of
95 # health.
96 #
97 def bmi(self):
98 return calculate_bmi(self.weight, self.height)
99
100 @property
101 ##
102 #
103 # Return the category this user belongs to based on BMI.
104 #
105 def bmi_category(self):
106 bmi = self.bmi
107 if bmi < UNDER:
108 return 'under weight'
109 elif NORMAL[0] <= bmi <= NORMAL[1]:
110 return 'normal weight'
111 elif OVER[0] <= bmi <= OVER[1]:
112 return 'over weight'
113 elif bmi >= OBESE:
114 return'obese'
115
116 @property
117 ##
118 #
119 # Calculate how many points this user has in total.
120 #
121 def total_points(self):
122 points = (DBSession.query(func.sum(Workout.points))
123 .filter(Workout.user_id==self.id)
124 .scalar())
125 return int(points or 0)
126
127 def __init__(self, username, email, password, gender, dob, weight, height):
128 self.username = username
129 self.email = email
130 self.password = password
131 self.gender = gender
132 self.dob = dob
133 self.weight = weight
134 self.height = height
135
136 def __unicode__(self):
137 return self.username
138
139 @classmethod
140 ##
141 #
142 # Search by username.
143 #
144 def get_by_username(cls, username):
145 return cls.query.filter(cls.username==username).first()
146
147 @classmethod
148 ##
149 #
150 # Search by email.
151 #
152 def get_by_email(cls, email):
153 return cls.query.filter(cls.email==email).first()
154
155 @classmethod
156 def create(cls, username, email, password, gender, dob, weight, height):
157 return super(User, cls).create(
158 username=username, email=email, password=password, gender=gender,
159 dob=dob, weight=weight, height=height)
160
161 @classmethod
162 ##
163 #
164 # Perform a fulltext search, matching the given terms.
165 #
166 def search(cls, terms, limit=50, page=1):
167 pager = Pager(page, limit)
168 return (cls._get_search_query(terms)
169 .order_by(cls.username.asc())
170 .limit(pager.limit)
171 .offset(pager.offset)
172 .all())
173
174 @classmethod
175 def count_search_results(cls, terms):
176 return cls._get_search_query(terms).count()
177
178 ##
179 #
180 # Quick and easy test to check if the given user is this user.
181 #
182 # Some templating engines don't allow the use of `is` in conditionals.
183 #
184 def is_user(self, user):
185 return user is self
186
187 ##
188 #
189 # Update this users profile, with both required and optional data.
190 #
191 def update_profile(self, username, email, gender, dob, weight, height, timezone):
192 self.username = username
193 self.email = email
194 self.gender = gender
195 self.dob = dob
196 self.weight = weight
197 self.height = height
198 self.timezone = timezone or None
199
200 ##
201 #
202 # Update this users password.
203 #
204 def update_password(self, password):
205 self.password = password
206
207 ##
208 #
209 # Update this users avatar (profile pic)
210 #
211 def update_avatar(self, avatar):
212 self.avatar = avatar
213
214 ##
215 #
216 # Calculate the average speed (speed challenge) for a given distance.
217 #
218 def get_average_speed(self, distance):
219 speed = (DBSession.query(func.avg(SpeedWorkout.speed))
220 .outerjoin(SpeedChallenge, SpeedChallenge.id==SpeedWorkout.challenge_id)
221 .filter(SpeedChallenge.distance==distance)
222 .filter(SpeedWorkout.user_id==self.id)
223 .scalar())
224 return speed
225
226 ##
227 #
228 # Calculate the average heart rate (endurance challenge) for a given
229 # duration.
230 #
231 def get_average_heart_rate(self, duration):
232 heart_rate = (DBSession.query(func.avg(EnduranceWorkout.heart_rate))
233 .outerjoin(EnduranceChallenge, EnduranceChallenge.id==EnduranceWorkout.challenge_id)
234 .filter(EnduranceChallenge.duration==duration)
235 .filter(EnduranceWorkout.user_id==self.id)
236 .scalar())
237 return heart_rate
238
239 ##
240 #
241 # Calculate the average calories burned (endurance challenge) for a given
242 # duration.
243 #
244 def get_average_calories_burned(self, duration):
245 calories_burned = (DBSession.query(func.avg(EnduranceWorkout.calories_burned))
246 .outerjoin(EnduranceChallenge, EnduranceChallenge.id==EnduranceWorkout.challenge_id)
247 .filter(EnduranceChallenge.duration==duration)
248 .filter(EnduranceWorkout.user_id==self.id)
249 .scalar())
250 return calories_burned
251
252 ##
253 #
254 # Calculate the average bench press repetitions (bench press challenge)
255 # for a given percentage.
256 #
257 def get_average_bench_press_repetitions(self, percentage):
258 repetitions = (DBSession.query(func.avg(BenchPressWorkout.repetitions))
259 .outerjoin(BenchPressChallenge, BenchPressChallenge.id==BenchPressWorkout.challenge_id)
260 .filter(BenchPressChallenge.percentage==percentage)
261 .filter(BenchPressWorkout.user_id==self.id)
262 .scalar())
263 return repetitions
264
265 ##
266 #
267 # Calculate the average squat repetitions (squat challenge) for a given
268 # percentage.
269 #
270 def get_average_squat_repetitions(self, percentage):
271 repetitions = (DBSession.query(func.avg(SquatWorkout.repetitions))
272 .outerjoin(SquatChallenge, SquatChallenge.id==SquatWorkout.challenge_id)
273 .filter(SquatChallenge.percentage==percentage)
274 .filter(SquatWorkout.user_id==self.id)
275 .scalar())
276 return repetitions
277
278 ##
279 #
280 # Calculate the average speed by day for a given distance.
281 #
282 def group_average_speed(self, distance):
283 return (DBSession.query(func.avg(SpeedWorkout.speed), SpeedWorkout.created_at)
284 .outerjoin(SpeedChallenge, SpeedChallenge.id==SpeedWorkout.challenge_id)
285 .filter(SpeedChallenge.distance==distance)
286 .filter(SpeedWorkout.user_id==self.id)
287 .group_by(func.day(SpeedWorkout.created_at))
288 .all())
289
290 ##
291 #
292 # Calculate the average heart rate by day for a given duration.
293 #
294 def group_average_heart_rate(self, duration):
295 return (DBSession.query(func.avg(EnduranceWorkout.heart_rate), EnduranceWorkout.created_at)
296 .outerjoin(EnduranceChallenge, EnduranceChallenge.id==EnduranceWorkout.challenge_id)
297 .filter(EnduranceChallenge.duration==duration)
298 .filter(EnduranceWorkout.user_id==self.id)
299 .group_by(func.day(EnduranceWorkout.created_at))
300 .all())
301
302 ##
303 #
304 # Calculate the average calories burned by day for a given duration.
305 #
306 def group_average_calories_burned(self, duration):
307 return (DBSession.query(func.avg(EnduranceWorkout.calories_burned), EnduranceWorkout.created_at)
308 .outerjoin(EnduranceChallenge, EnduranceChallenge.id==EnduranceWorkout.challenge_id)
309 .filter(EnduranceChallenge.duration==duration)
310 .filter(EnduranceWorkout.user_id==self.id)
311 .group_by(func.day(EnduranceWorkout.created_at))
312 .all())
313
314 ##
315 #
316 # Calculate the average bench press by day for a given percentage.
317 #
318 def group_average_bench_press_repetitions(self, percentage):
319 return (DBSession.query(func.avg(BenchPressWorkout.repetitions), BenchPressWorkout.created_at)
320 .outerjoin(BenchPressChallenge, BenchPressChallenge.id==BenchPressWorkout.challenge_id)
321 .filter(BenchPressChallenge.percentage==percentage)
322 .filter(BenchPressWorkout.user_id==self.id)
323 .group_by(func.day(BenchPressWorkout.created_at))
324 .all())
325
326 ##
327 #
328 # Calculate the average squat repetitions by day for a given percentage.
329 #
330 def group_average_squat_repetitions(self, percentage):
331 return (DBSession.query(func.avg(SquatWorkout.repetitions), SquatWorkout.created_at)
332 .outerjoin(SquatChallenge, SquatChallenge.id==SquatWorkout.challenge_id)
333 .filter(SquatChallenge.percentage==percentage)
334 .filter(SquatWorkout.user_id==self.id)
335 .group_by(func.day(BenchPressWorkout.created_at))
336 .all())
337
338 ##
339 #
340 # Return all of this users workout buddies.
341 #
342 def get_buddies(self, limit=50, page=1):
343 pager = Pager(page, limit)
344 return (self._get_buddies_query()
345 .order_by(User.username.asc())
346 .limit(pager.limit)
347 .offset(pager.offset)
348 .all())
349
350 ##
351 #
352 # Count how many workout buddies this user has.
353 #
354 def count_buddies(self):
355 return self._get_buddies_query().count()
356
357 ##
358 #
359 # Check whether the given user is a workout buddy.
360 #
361 def is_buddy(self, user):
362 return bool(self._get_buddy(user))
363
364 ##
365 #
366 # Add the user to the list of workout buddies.
367 #
368 def add_buddy(self, user):
369 buddy = self._get_buddy(user)
370 if not buddy:
371 Buddy.create(user_id=self.id, buddy_id=user.id)
372 notification = NewBuddyNotification.create(user=self)
373 notification.add_recipients([user])
374
375 ##
376 #
377 # Remove the user from the list of workout buddies.
378 #
379 def remove_buddy(self, user):
380 buddy = self._get_buddy(user)
381 if buddy:
382 buddy.delete()
383
384 ##
385 #
386 # Return all challenges this user is apart of (paginated).
387 #
388 def get_challenges(self, limit=50, page=1):
389 pager = Pager(page, limit)
390 return (self.challenges
391 .order_by(Challenge.created_at.desc())
392 .limit(pager.limit)
393 .offset(pager.offset)
394 .all())
395
396 ##
397 #
398 # Return this users workout for the given challenge.
399 #
400 def get_workout_for_challenge(self, challenge):
401 return (Workout.query
402 .filter(Workout.user_id==self.id, Workout.challenge_id==challenge.id)
403 .first())
404
405 ##
406 #
407 # Count all challenges this user is a competitor in.
408 #
409 def count_challenges(self):
410 return self.challenges.count()
411
412 def create_speed_challenge(self, competitor, distance):
413 challenge = SpeedChallenge.create(self, distance)
414 return self._create_challenge(challenge, competitor)
415
416 def create_endurance_challenge(self, competitor, duration):
417 challenge = EnduranceChallenge.create(self, duration)
418 return self._create_challenge(challenge, competitor)
419
420 def create_bench_press_challenge(self, competitor, percentage):
421 challenge = BenchPressChallenge.create(self, percentage)
422 return self._create_challenge(challenge, competitor)
423
424 def create_squat_challenge(self, competitor, percentage):
425 challenge = SquatChallenge.create(self, percentage)
426 return self._create_challenge(challenge, competitor)
427
428 def create_speed_workout(self, challenge, samples):
429 workout = SpeedWorkout.create(self, challenge, samples)
430 return self._create_workout(workout)
431
432 def create_endurance_workout(self, challenge, samples):
433 workout = EnduranceWorkout.create(self, challenge, samples)
434 return self._create_workout(workout)
435
436 def create_bench_press_workout(self, challenge, repetitions):
437 workout = BenchPressWorkout.create(self, challenge, repetitions)
438 return self._create_workout(workout)
439
440 def create_squat_workout(self, challenge, repetitions):
441 workout = SquatWorkout.create(self, challenge, repetitions)
442 return self._create_workout(workout)
443
444 ##
445 #
446 # Check whether this user is a competitor in the given challenge.
447 #
448 def in_challenge(self, challenge):
449 return challenge in self.challenges
450
451 ##
452 #
453 # Check whether this user is the creator of the given challenge.
454 #
455 def owns_challenge(self, challenge):
456 return self is challenge.creator
457
458 ##
459 #
460 # Check whether this user has accepted the given challenge.
461 #
462 def accepted_challenge(self, challenge):
463 pivot = self._get_challenge_link(challenge)
464 return pivot.is_accepted is True
465
466 ##
467 #
468 # Check whether this user has denied the given challenge.
469 #
470 def denied_challenge(self, challenge):
471 pivot = self._get_challenge_link(challenge)
472 return pivot.is_accepted is False
473
474 ##
475 #
476 # Change the status between this user and the given challenge to accepted,
477 # and send all other competitors a notification.
478 #
479 def accept_challenge(self, challenge):
480 pivot = self._get_challenge_link(challenge)
481 pivot.accept()
482 notification = AcceptedChallengeNotification.create(user=self, challenge=challenge)
483 notification.add_recipients(set(challenge.competitors) - set([self]))
484
485 ##
486 #
487 # Change the status between this user and the given challenge to denied,
488 # and send all other competitors a notification.
489 #
490 def deny_challenge(self, challenge):
491 pivot = self._get_challenge_link(challenge)
492 pivot.deny()
493 notification = DeniedChallengeNotification.create(user=self, challenge=challenge)
494 notification.add_recipients(set(challenge.competitors) - set([self]))
495
496 ##
497 #
498 # Return all notifications, regardless of status (paginated).
499 #
500 def get_all_notifications(self, limit=50, page=1):
501 pager = Pager(page, limit)
502 return (self._get_notifications_query(unconfirmed_only=False)
503 .limit(pager.limit)
504 .offset(pager.offset)
505 .all())
506
507 ##
508 #
509 # Count all notifications, regardless of status.
510 #
511 def count_all_notifications(self):
512 return self._get_notifications_query(unconfirmed_only=False).count()
513
514 ##
515 #
516 # Return all notifications which have not been confirmed.
517 #
518 def get_unconfirmed_notifications(self, limit=50, page=1):
519 pager = Pager(page, limit)
520 return (self._get_notifications_query(unconfirmed_only=True)
521 .limit(pager.limit)
522 .offset(pager.offset)
523 .all())
524
525 ##
526 #
527 # Count all notifications which have not been confirmed.
528 #
529 def count_unconfirmed_notifications(self):
530 return self._get_notifications_query(unconfirmed_only=True).count()
531
532 ##
533 #
534 # Mark all notifications as confirmed (read).
535 #
536 def confirm_all_notifications(self):
537 unconfirmed_notifications = (
538 UserNotification.query.filter(UserNotification.user_id==self.id))
539 for notification in unconfirmed_notifications:
540 notification.confirm()
541
542 ##
543 #
544 # Check whether this user has any unconfirmed (unread) notifications.
545 #
546 def has_unconfirmed_notifications(self):
547 return bool(self.count_unconfirmed_notifications())
548
549 @classmethod
550 def _get_search_query(cls, terms):
551 return cls._get_fulltext_query(terms)
552
553 ##
554 #
555 # Add the competitor to the challenge and send them a notification.
556 #
557 def _create_challenge(self, challenge, competitor):
558 challenge.add_competitor(competitor)
559 notification = RequestedChallengeNotification.create(user=self, challenge=challenge)
560 notification.add_recipients([competitor])
561 return challenge
562
563 ##
564 #
565 # Send the other competitor(s) a notification.
566 #
567 def _create_workout(self, workout):
568 notification = UploadedWorkoutNotification.create(user=self, challenge=workout.challenge)
569 notification.add_recipients(set(workout.challenge.competitors) - set([self]))
570 return workout
571
572 ##
573 #
574 # Query for all of this users buddies.
575 #
576 def _get_buddies_query(self):
577 users = (User.query
578 .outerjoin(Buddy, Buddy.user_id==User.id)
579 .filter(Buddy.buddy_id==self.id))
580 buddies = (User.query
581 .outerjoin(Buddy, Buddy.buddy_id==User.id)
582 .filter(Buddy.user_id==self.id))
583 return users.union(buddies)
584
585 ##
586 #
587 # Return the buddy link between this user and the given user.
588 #
589 def _get_buddy(self, user):
590 return (Buddy.query
591 .filter(or_(
592 and_(Buddy.user_id==self.id,
593 Buddy.buddy_id==user.id),
594 and_(Buddy.buddy_id==self.id,
595 Buddy.user_id==user.id)))
596 .first())
597
598 ##
599 #
600 # Return the link between this user and the given challenge.
601 #
602 def _get_challenge_link(self, challenge):
603 return (UserChallenge.query
604 .filter(UserChallenge.user_id==self.id, UserChallenge.challenge_id==challenge.id)
605 .first())
606
607 ##
608 #
609 # @param unconfirmed_only Whether to only query for unconfirmed notifications
610 #
611 def _get_notifications_query(self, unconfirmed_only=True):
612 notifications = (Notification.query
613 .outerjoin(UserNotification,
614 UserNotification.notification_id==Notification.id)
615 .filter(UserNotification.user_id==self.id)
616 .order_by(Notification.created_at.desc()))
617 if unconfirmed_only:
618 notifications = notifications.filter(UserNotification.is_confirmed==False)
619 return notifications
620
621
622 # Avoid circular imports
623 from wowf.models.challenge import (
624 BenchPressChallenge, Challenge, EnduranceChallenge,
625 SpeedChallenge, SquatChallenge)
626 from wowf.models.notification import (
627 AcceptedChallengeNotification, DeniedChallengeNotification,
628 NewBuddyNotification, Notification, RequestedChallengeNotification,
629 UploadedWorkoutNotification)
630 from wowf.models.pivot_tables import Buddy, UserChallenge, UserNotification
631 from wowf.models.workout import (
632 BenchPressWorkout, EnduranceWorkout, SpeedWorkout,
633 SquatWorkout, Workout)
634