source: OpenWorkouts-current/ow/models/user.py @ 7dc1f81

currentfeature/docs
Last change on this file since 7dc1f81 was 7dc1f81, checked in by Borja Lopez <borja@…>, 5 years ago

Better str and repr representation for the User model.

This is basically useful for developers using pshell for debugging
purposes, as adding the email address to the repr makes easier
(faster) to spot who a given user object is.

  • Property mode set to 100644
File size: 13.0 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, Deny, Everyone, ALL_PERMISSIONS
9
10from ow.catalog import get_catalog, reindex_object
11from ow.utilities import get_week_days, get_month_week_number
12
13
14class User(Folder):
15
16    __parent__ = __name__ = None
17
18    def __acl__(self):
19        permissions = [
20            (Allow, str(self.uid), 'view'),
21            (Allow, str(self.uid), 'edit'),
22            (Deny, Everyone, ALL_PERMISSIONS),
23        ]
24        return permissions
25
26    def __init__(self, **kw):
27        self.uid = kw.get('uid', uuid1())
28        self.nickname = kw.get('nickname', '')
29        self.firstname = kw.get('firstname', '')
30        self.lastname = kw.get('lastname', '')
31        self.email = kw.get('email', '')
32        self.bio = kw.get('bio', '')
33        self.birth_date = kw.get('birth_date', None)
34        self.height = kw.get('height', None)
35        self.weight = kw.get('weight', None)
36        self.gender = kw.get('gender', 'female')
37        self.picture = kw.get('picture', None)  # blob
38        self.timezone = kw.get('timezone', 'UTC')
39        self.__password = None
40        self.last_workout_id = 0
41        super(User, self).__init__()
42
43    def __str__(self):
44        return u'User: %s (%s)' % (self.uid, self.email)
45
46    def __repr__(self):
47        return u'<%s.%s: %s (%s)>' % (
48            self.__class__.__module__,
49            self.__class__.__name__,
50            self.uid, self.email
51        )
52
53    @property
54    def password(self):
55        return self.__password
56
57    @password.setter
58    def password(self, password=None):
59        """
60        Sets a password for the user, hashing with bcrypt.
61        """
62        password = password.encode('utf-8')
63        self.__password = bcrypt.hashpw(password, bcrypt.gensalt())
64
65    def check_password(self, password):
66        """
67        Check a plain text password against a hashed one
68        """
69        hashed = bcrypt.hashpw(password.encode('utf-8'), self.__password)
70        return hashed == self.__password
71
72    @property
73    def fullname(self):
74        """
75        Naive implementation of fullname: firstname + lastname
76        """
77        return u'%s %s' % (self.firstname, self.lastname)
78
79    def add_workout(self, workout):
80        # This returns the main catalog at the root folder
81        catalog = get_catalog(self)
82        self.last_workout_id += 1
83        workout_id = str(self.last_workout_id)
84        self[workout_id] = workout
85        reindex_object(catalog, workout)
86
87    def workouts(self, year=None, month=None, week=None):
88        """
89        Return this user workouts, sorted by date, from newer to older
90        """
91        workouts = self.values()
92        if year:
93            workouts = [w for w in workouts if w.start.year == year]
94            if month:
95                workouts = [w for w in workouts if w.start.month == month]
96            if week:
97                week = int(week)
98                workouts = [
99                    w for w in workouts if w.start.isocalendar()[1] == week]
100        workouts = sorted(workouts, key=attrgetter('start'))
101        workouts.reverse()
102        return workouts
103
104    def workout_ids(self):
105        return self.keys()
106
107    @property
108    def num_workouts(self):
109        return len(self.workout_ids())
110
111    @property
112    def activity_years(self):
113        return sorted(list(set(w.start.year for w in self.workouts())),
114                      reverse=True)
115
116    def activity_months(self, year):
117        months = set(
118            w.start.month for w in self.workouts() if w.start.year == year)
119        return sorted(list(months))
120
121    @property
122    def activity_dates_tree(self):
123        """
124        Return a dict containing information about the activity for this
125        user.
126
127        Example:
128
129        {
130            2019: {
131                1: {'cycling': 12, 'running': 1}
132            },
133            2018: {
134                1: {'cycling': 10, 'running': 3},
135                2: {'cycling': 14, 'swimming': 5}
136            }
137        }
138        """
139        tree = {}
140        for workout in self.workouts():
141            year = workout.start.year
142            month = workout.start.month
143            sport = workout.sport
144            if year not in tree:
145                tree[year] = {}
146            if month not in tree[year]:
147                tree[year][month] = {}
148            if sport not in tree[year][month]:
149                tree[year][month][sport] = 0
150            tree[year][month][sport] += 1
151        return tree
152
153    def stats(self, year=None, month=None):
154        year = year or datetime.now().year
155        stats = {
156            'workouts': 0,
157            'time': timedelta(seconds=0),
158            'distance': Decimal(0),
159            'elevation': Decimal(0),
160            'sports': {}
161        }
162
163        for workout in self.workouts(year=year, month=month):
164            stats['workouts'] += 1
165            stats['time'] += workout.duration or timedelta(seconds=0)
166            stats['distance'] += workout.distance or Decimal(0)
167            stats['elevation'] += workout.uphill or Decimal(0)
168
169            if workout.sport not in stats['sports']:
170                stats['sports'][workout.sport] = {
171                    'workouts': 0,
172                    'time': timedelta(seconds=0),
173                    'distance': Decimal(0),
174                    'elevation': Decimal(0),
175                }
176
177            stats['sports'][workout.sport]['workouts'] += 1
178            stats['sports'][workout.sport]['time'] += (
179                workout.duration or timedelta(0))
180            stats['sports'][workout.sport]['distance'] += (
181                workout.distance or Decimal(0))
182            stats['sports'][workout.sport]['elevation'] += (
183                workout.uphill or Decimal(0))
184
185        return stats
186
187    def get_week_stats(self, day):
188        """
189        Return some stats for the week the given day is in.
190        """
191        week = get_week_days(day)
192
193        # filter workouts
194        workouts = []
195        for workout in self.workouts():
196            if week[0].date() <= workout.start.date() <= week[-1].date():
197                workouts.append(workout)
198
199        # build stats
200        stats = {}
201        for week_day in week:
202            stats[week_day] = {
203                'workouts': 0,
204                'time': timedelta(0),
205                'distance': Decimal(0),
206                'elevation': Decimal(0),
207                'sports': {}
208            }
209            for workout in workouts:
210                if workout.start.date() == week_day.date():
211                    day = stats[week_day]  # less typing, avoid long lines
212                    day['workouts'] += 1
213                    day['time'] += workout.duration or timedelta(seconds=0)
214                    day['distance'] += workout.distance or Decimal(0)
215                    day['elevation'] += workout.uphill or Decimal(0)
216                    if workout.sport not in day['sports']:
217                        day['sports'][workout.sport] = {
218                            'workouts': 0,
219                            'time': timedelta(seconds=0),
220                            'distance': Decimal(0),
221                            'elevation': Decimal(0),
222                        }
223                    day['sports'][workout.sport]['workouts'] += 1
224                    day['sports'][workout.sport]['time'] += (
225                        workout.duration or timedelta(0))
226                    day['sports'][workout.sport]['distance'] += (
227                        workout.distance or Decimal(0))
228                    day['sports'][workout.sport]['elevation'] += (
229                        workout.uphill or Decimal(0))
230
231        return stats
232
233    @property
234    def week_stats(self):
235        """
236        Helper that returns the week stats for the current week
237        """
238        return self.get_week_stats(datetime.now(timezone.utc))
239
240    @property
241    def week_totals(self):
242        week_stats = self.week_stats
243        return {
244            'distance': sum([week_stats[t]['distance'] for t in week_stats]),
245            'time': sum([week_stats[t]['time'] for t in week_stats],
246                        timedelta())
247        }
248
249    @property
250    def yearly_stats(self):
251        """
252        Return per-month stats for the last 12 months
253        """
254        # set the boundaries for looking for workouts afterwards,
255        # we need the current date as the "end date" and one year
256        # ago from that date. Then we set the start at the first
257        # day of that month.
258        end = datetime.now(timezone.utc)
259        start = (end - timedelta(days=365)).replace(day=1)
260
261        # build the stats, populating first the dict with empty values
262        # for each month.
263        stats = {}
264        for days in range((end - start).days):
265            day = (start + timedelta(days=days)).date()
266            if (day.year, day.month) not in stats.keys():
267                stats[(day.year, day.month)] = {
268                    'workouts': 0,
269                    'time': timedelta(0),
270                    'distance': Decimal(0),
271                    'elevation': Decimal(0),
272                    'sports': {}
273                }
274
275        # now loop over workouts, filtering and then adding stats to the
276        # proper place
277        for workout in self.workouts():
278            if start.date() <= workout.start.date() <= end.date():
279                # less typing, avoid long lines
280                month = stats[
281                    (workout.start.date().year, workout.start.date().month)]
282                month['workouts'] += 1
283                month['time'] += workout.duration or timedelta(seconds=0)
284                month['distance'] += workout.distance or Decimal(0)
285                month['elevation'] += workout.uphill or Decimal(0)
286                if workout.sport not in month['sports']:
287                    month['sports'][workout.sport] = {
288                        'workouts': 0,
289                        'time': timedelta(seconds=0),
290                        'distance': Decimal(0),
291                        'elevation': Decimal(0),
292                    }
293                month['sports'][workout.sport]['workouts'] += 1
294                month['sports'][workout.sport]['time'] += (
295                    workout.duration or timedelta(0))
296                month['sports'][workout.sport]['distance'] += (
297                    workout.distance or Decimal(0))
298                month['sports'][workout.sport]['elevation'] += (
299                    workout.uphill or Decimal(0))
300
301        return stats
302
303    @property
304    def weekly_year_stats(self):
305        """
306        Return per-week stats for the last 12 months
307        """
308        # set the boundaries for looking for workouts afterwards,
309        # we need the current date as the "end date" and one year
310        # ago from that date. Then we set the start at the first
311        # day of that month.
312        end = datetime.now(timezone.utc)
313        start = (end - timedelta(days=365)).replace(day=1)
314
315        stats = {}
316
317        # first initialize the stats dict
318        for days in range((end - start).days):
319            day = (start + timedelta(days=days)).date()
320            week = day.isocalendar()[1]
321            month_week = get_month_week_number(day)
322            key = (day.year, day.month, week, month_week)
323            if key not in stats.keys():
324                stats[key] = {
325                    'workouts': 0,
326                    'time': timedelta(0),
327                    'distance': Decimal(0),
328                    'elevation': Decimal(0),
329                    'sports': {}
330                }
331
332        # now loop over the workouts, filtering and then adding stats
333        # to the proper place
334        for workout in self.workouts():
335            if start.date() <= workout.start.date() <= end.date():
336                # less typing, avoid long lines
337                start_date = workout.start.date()
338                week = start_date.isocalendar()[1]
339                month_week = get_month_week_number(start_date)
340                week = stats[(start_date.year,
341                              start_date.month,
342                              week,
343                              month_week)]
344
345                week['workouts'] += 1
346                week['time'] += workout.duration or timedelta(seconds=0)
347                week['distance'] += workout.distance or Decimal(0)
348                week['elevation'] += workout.uphill or Decimal(0)
349                if workout.sport not in week['sports']:
350                    week['sports'][workout.sport] = {
351                        'workouts': 0,
352                        'time': timedelta(seconds=0),
353                        'distance': Decimal(0),
354                        'elevation': Decimal(0),
355                    }
356                week['sports'][workout.sport]['workouts'] += 1
357                week['sports'][workout.sport]['time'] += (
358                    workout.duration or timedelta(0))
359                week['sports'][workout.sport]['distance'] += (
360                    workout.distance or Decimal(0))
361                week['sports'][workout.sport]['elevation'] += (
362                    workout.uphill or Decimal(0))
363
364        return stats
Note: See TracBrowser for help on using the repository browser.