source: OpenWorkouts-current/ow/models/user.py @ 76ebb1b

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

(#29) Add user verification by email on signup.

From now on, when a new user signs up, we set the account into an "unverified"
state. In order to complete the signup procedure, the user has to click on a
link we send by email to the email address provided on signup.

IMPORTANT: A new dependency has been added, pyramid_mailer, so remember to
install it in any existing openworkouts environment (this is done automatically
if you use the ./bin/start script):

pip install pyramid_mailer

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