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

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

Fix permissions. From now on users can see (and edit, delete, etc) their own data

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