source: OpenWorkouts-current/ow/models/user.py @ 2f8a48f

currentfeature/docs
Last change on this file since 2f8a48f was 2f8a48f, checked in by borja <borja@…>, 5 years ago

(#7) Added several methods to the User model to gather some stats (yearly,

monthly, weekly).

Added two new utilities:

  • timedelta_to_hms (so we can print timedelta objects properly in template code)
  • get_week_days (returns a list of datetime objects for the days in the same week as a given day)

Added a template_helpers module, containing code that affects template
rendering.

Added timedelta_to_hms as a global to the default template rendering context

Refactored some code in the Workout model so it uses timedelta_to_hms instead
of running the same code twice.

  • Property mode set to 100644
File size: 7.6 KB
Line 
1from decimal import Decimal
2from datetime import datetime, timedelta, timezone
3from uuid import uuid1
4from operator import attrgetter
5
6import bcrypt
7from repoze.folder import Folder
8from pyramid.security import Allow
9
10from ow.catalog import get_catalog, reindex_object
11from ow.utilities import get_week_days
12
13
14class User(Folder):
15
16    __parent__ = __name__ = None
17
18    def __acl__(self):
19        permissions = [
20            (Allow, str(self.uid), 'edit'),
21            (Allow, str(self.uid), 'view'),
22        ]
23        return permissions
24
25    def __init__(self, **kw):
26        self.uid = kw.get('uid', uuid1())
27        self.nickname = kw.get('nickname', '')
28        self.firstname = kw.get('firstname', '')
29        self.lastname = kw.get('lastname', '')
30        self.email = kw.get('email', '')
31        self.bio = kw.get('bio', '')
32        self.birth_date = kw.get('birth_date', None)
33        self.height = kw.get('height', None)
34        self.weight = kw.get('weight', None)
35        self.gender = kw.get('gender', 'female')
36        self.picture = kw.get('picture', None)  # blob
37        self.timezone = kw.get('timezone', 'UTC')
38        self.__password = None
39        self.last_workout_id = 0
40        super(User, self).__init__()
41
42    def __str__(self):
43        return u'User: %s (%s)' % (self.email, self.uid)
44
45    @property
46    def password(self):
47        return self.__password
48
49    @password.setter
50    def password(self, password=None):
51        """
52        Sets a password for the user, hashing with bcrypt.
53        """
54        password = password.encode('utf-8')
55        self.__password = bcrypt.hashpw(password, bcrypt.gensalt())
56
57    def check_password(self, password):
58        """
59        Check a plain text password against a hashed one
60        """
61        hashed = bcrypt.hashpw(password.encode('utf-8'), self.__password)
62        return hashed == self.__password
63
64    @property
65    def fullname(self):
66        """
67        Naive implementation of fullname: firstname + lastname
68        """
69        return u'%s %s' % (self.firstname, self.lastname)
70
71    def add_workout(self, workout):
72        # This returns the main catalog at the root folder
73        catalog = get_catalog(self)
74        self.last_workout_id += 1
75        workout_id = str(self.last_workout_id)
76        self[workout_id] = workout
77        reindex_object(catalog, workout)
78
79    def workouts(self, year=None, month=None):
80        """
81        Return this user workouts, sorted by date, from newer to older
82        """
83        workouts = self.values()
84        if year:
85            workouts = [w for w in workouts if w.start.year == year]
86        if month:
87            workouts = [w for w in workouts if w.start.month == month]
88        workouts = sorted(workouts, key=attrgetter('start'))
89        workouts.reverse()
90        return workouts
91
92    def workout_ids(self):
93        return self.keys()
94
95    @property
96    def num_workouts(self):
97        return len(self.workout_ids())
98
99    @property
100    def activity_years(self):
101        return sorted(list(set(w.start.year for w in self.workouts())),
102                      reverse=True)
103
104    def activity_months(self, year):
105        months = set(
106            w.start.month for w in self.workouts() if w.start.year == year)
107        return sorted(list(months))
108
109    @property
110    def activity_dates_tree(self):
111        """
112        Return a dict containing information about the activity for this
113        user.
114
115        Example:
116
117        {
118            2019: {
119                1: {'cycling': 12, 'running': 1}
120            },
121            2018: {
122                1: {'cycling': 10, 'running': 3},
123                2: {'cycling': 14, 'swimming': 5}
124            }
125        }
126        """
127        tree = {}
128        for workout in self.workouts():
129            year = workout.start.year
130            month = workout.start.month
131            sport = workout.sport
132            if year not in tree:
133                tree[year] = {}
134            if month not in tree[year]:
135                tree[year][month] = {}
136            if sport not in tree[year][month]:
137                tree[year][month][sport] = 0
138            tree[year][month][sport] += 1
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        }
Note: See TracBrowser for help on using the repository browser.