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

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

The workouts() method of the User model accepts a new parameter, week,

which lets us filter workouts for a given week number, relative to a year

(for example, year 2018, week 44)

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