Workout With Friends
Stay fit with a little motivation
 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  #
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  #
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  #
530  return self._get_notifications_query(unconfirmed_only=True).count()
531 
532  ##
533  #
534  # Mark all notifications as confirmed (read).
535  #
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  #
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  #
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