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

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

(#69) Added translations for User gender.
(+ added a third gender option, "robot" ;-D)

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