source: OpenWorkouts-current/ow/models/user.py @ 8bae554

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

(#67) Allow users to send again the verification link (up to 3 times)
to the email address they provided when signing up.

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