<filename>ow/models/user.py
from decimal import Decimal
from datetime import datetime, timedelta, timezone
from uuid import uuid1
from operator import attrgetter

import bcrypt
from repoze.folder import Folder
from pyramid.security import Allow, Deny, Everyone, ALL_PERMISSIONS

from ow.catalog import get_catalog, reindex_object
from ow.utilities import get_week_days, get_month_week_number


class User(Folder):

    __parent__ = __name__ = None

    def __acl__(self):
        permissions = [
            (Allow, str(self.uid), 'view'),
            (Allow, str(self.uid), 'edit'),
            (Deny, Everyone, ALL_PERMISSIONS),
        ]
        return permissions

    def __init__(self, **kw):
        self.uid = kw.get('uid', uuid1())
        self.nickname = kw.get('nickname', '')
        self.firstname = kw.get('firstname', '')
        self.lastname = kw.get('lastname', '')
        self.email = kw.get('email', '')
        self.bio = kw.get('bio', '')
        self.birth_date = kw.get('birth_date', None)
        self.height = kw.get('height', None)
        self.weight = kw.get('weight', None)
        self.gender = kw.get('gender', 'robot')
        self.picture = kw.get('picture', None)  # blob
        self.timezone = kw.get('timezone', 'UTC')
        self.locale = kw.get('locale', 'en')
        self.__password = None
        self.last_workout_id = 0
        # has this user verified his account?
        self.verified = False
        self.verification_token = None
        self.verification_tokens_sent = 0
        super(User, self).__init__()

    def __str__(self):
        return u'User: %s (%s)' % (self.uid, self.email)

    def __repr__(self):
        return u'<%s.%s: %s (%s)>' % (
            self.__class__.__module__,
            self.__class__.__name__,
            self.uid, self.email
        )

    @property
    def password(self):
        return self.__password

    @password.setter
    def password(self, password=None):
        """
        Sets a password for the user, hashing with bcrypt.
        """
        password = password.encode('utf-8')
        self.__password = bcrypt.hashpw(password, bcrypt.gensalt())

    def check_password(self, password):
        """
        Check a plain text password against a hashed one
        """
        hashed = bcrypt.hashpw(password.encode('utf-8'), self.__password)
        return hashed == self.__password

    @property
    def fullname(self):
        """
        Naive implementation of fullname: firstname + lastname
        """
        return u'%s %s' % (self.firstname, self.lastname)

    def add_workout(self, workout):
        # This returns the main catalog at the root folder
        catalog = get_catalog(self)
        self.last_workout_id += 1
        workout_id = str(self.last_workout_id)
        self[workout_id] = workout
        reindex_object(catalog, workout)

    def workouts(self, year=None, month=None, week=None):
        """
        Return this user workouts, sorted by date, from newer to older
        """
        workouts = self.values()
        if year:
            workouts = [w for w in workouts if w.start.year == year]
            if month:
                workouts = [w for w in workouts if w.start.month == month]
            if week:
                week = int(week)
                workouts = [
                    w for w in workouts if w.start.isocalendar()[1] == week]
        workouts = sorted(workouts, key=attrgetter('start'))
        workouts.reverse()
        return workouts

    def workout_ids(self):
        return self.keys()

    @property
    def num_workouts(self):
        return len(self.workout_ids())

    @property
    def activity_years(self):
        return sorted(list(set(w.start.year for w in self.workouts())),
                      reverse=True)

    def activity_months(self, year):
        months = set(
            w.start.month for w in self.workouts() if w.start.year == year)
        return sorted(list(months))

    @property
    def activity_dates_tree(self):
        """
        Return a dict containing information about the activity for this
        user.

        Example:

        {
            2019: {
                1: {'cycling': 12, 'running': 1}
            },
            2018: {
                1: {'cycling': 10, 'running': 3},
                2: {'cycling': 14, 'swimming': 5}
            }
        }
        """
        tree = {}
        for workout in self.workouts():
            year = workout.start.year
            month = workout.start.month
            sport = workout.sport
            if year not in tree:
                tree[year] = {}
            if month not in tree[year]:
                tree[year][month] = {}
            if sport not in tree[year][month]:
                tree[year][month][sport] = 0
            tree[year][month][sport] += 1
        return tree

    def stats(self, year=None, month=None):
        year = year or datetime.now().year
        stats = {
            'workouts': 0,
            'time': timedelta(seconds=0),
            'distance': Decimal(0),
            'elevation': Decimal(0),
            'sports': {}
        }

        for workout in self.workouts(year=year, month=month):
            stats['workouts'] += 1
            stats['time'] += workout.duration or timedelta(seconds=0)
            stats['distance'] += workout.distance or Decimal(0)
            stats['elevation'] += workout.uphill or Decimal(0)

            if workout.sport not in stats['sports']:
                stats['sports'][workout.sport] = {
                    'workouts': 0,
                    'time': timedelta(seconds=0),
                    'distance': Decimal(0),
                    'elevation': Decimal(0),
                }

            stats['sports'][workout.sport]['workouts'] += 1
            stats['sports'][workout.sport]['time'] += (
                workout.duration or timedelta(0))
            stats['sports'][workout.sport]['distance'] += (
                workout.distance or Decimal(0))
            stats['sports'][workout.sport]['elevation'] += (
                workout.uphill or Decimal(0))

        return stats

    def get_week_stats(self, day):
        """
        Return some stats for the week the given day is in.
        """
        week = get_week_days(day)

        # filter workouts
        workouts = []
        for workout in self.workouts():
            if week[0].date() <= workout.start.date() <= week[-1].date():
                workouts.append(workout)

        # build stats
        stats = {}
        for week_day in week:
            stats[week_day] = {
                'workouts': 0,
                'time': timedelta(0),
                'distance': Decimal(0),
                'elevation': Decimal(0),
                'sports': {}
            }
            for workout in workouts:
                if workout.start.date() == week_day.date():
                    day = stats[week_day]  # less typing, avoid long lines
                    day['workouts'] += 1
                    day['time'] += workout.duration or timedelta(seconds=0)
                    day['distance'] += workout.distance or Decimal(0)
                    day['elevation'] += workout.uphill or Decimal(0)
                    if workout.sport not in day['sports']:
                        day['sports'][workout.sport] = {
                            'workouts': 0,
                            'time': timedelta(seconds=0),
                            'distance': Decimal(0),
                            'elevation': Decimal(0),
                        }
                    day['sports'][workout.sport]['workouts'] += 1
                    day['sports'][workout.sport]['time'] += (
                        workout.duration or timedelta(0))
                    day['sports'][workout.sport]['distance'] += (
                        workout.distance or Decimal(0))
                    day['sports'][workout.sport]['elevation'] += (
                        workout.uphill or Decimal(0))

        return stats

    @property
    def week_stats(self):
        """
        Helper that returns the week stats for the current week
        """
        return self.get_week_stats(datetime.now(timezone.utc))

    @property
    def week_totals(self):
        week_stats = self.week_stats
        return {
            'distance': sum([week_stats[t]['distance'] for t in week_stats]),
            'time': sum([week_stats[t]['time'] for t in week_stats],
                        timedelta())
        }

    @property
    def yearly_stats(self):
        """
        Return per-month stats for the last 12 months
        """
        # set the boundaries for looking for workouts afterwards,
        # we need the current date as the "end date" and one year
        # ago from that date. Then we set the start at the first
        # day of that month.
        end = datetime.now(timezone.utc)
        start = (end - timedelta(days=365)).replace(day=1)

        # build the stats, populating first the dict with empty values
        # for each month.
        stats = {}
        for days in range((end - start).days):
            day = (start + timedelta(days=days)).date()
            if (day.year, day.month) not in stats.keys():
                stats[(day.year, day.month)] = {
                    'workouts': 0,
                    'time': timedelta(0),
                    'distance': Decimal(0),
                    'elevation': Decimal(0),
                    'sports': {}
                }

        # now loop over workouts, filtering and then adding stats to the
        # proper place
        for workout in self.workouts():
            if start.date() <= workout.start.date() <= end.date():
                # less typing, avoid long lines
                month = stats[
                    (workout.start.date().year, workout.start.date().month)]
                month['workouts'] += 1
                month['time'] += workout.duration or timedelta(seconds=0)
                month['distance'] += workout.distance or Decimal(0)
                month['elevation'] += workout.uphill or Decimal(0)
                if workout.sport not in month['sports']:
                    month['sports'][workout.sport] = {
                        'workouts': 0,
                        'time': timedelta(seconds=0),
                        'distance': Decimal(0),
                        'elevation': Decimal(0),
                    }
                month['sports'][workout.sport]['workouts'] += 1
                month['sports'][workout.sport]['time'] += (
                    workout.duration or timedelta(0))
                month['sports'][workout.sport]['distance'] += (
                    workout.distance or Decimal(0))
                month['sports'][workout.sport]['elevation'] += (
                    workout.uphill or Decimal(0))

        return stats

    @property
    def weekly_year_stats(self):
        """
        Return per-week stats for the last 12 months
        """
        # set the boundaries for looking for workouts afterwards,
        # we need the current date as the "end date" and one year
        # ago from that date. Then we set the start at the first
        # day of that month.
        end = datetime.now(timezone.utc)
        start = (end - timedelta(days=365)).replace(day=1)

        stats = {}

        # first initialize the stats dict
        for days in range((end - start).days):
            day = (start + timedelta(days=days)).date()
            week = day.isocalendar()[1]
            month_week = get_month_week_number(day)
            key = (day.year, day.month, week, month_week)
            if key not in stats.keys():
                stats[key] = {
                    'workouts': 0,
                    'time': timedelta(0),
                    'distance': Decimal(0),
                    'elevation': Decimal(0),
                    'sports': {}
                }

        # now loop over the workouts, filtering and then adding stats
        # to the proper place
        for workout in self.workouts():
            if start.date() <= workout.start.date() <= end.date():
                # less typing, avoid long lines
                start_date = workout.start.date()
                week = start_date.isocalendar()[1]
                month_week = get_month_week_number(start_date)
                week = stats[(start_date.year,
                              start_date.month,
                              week,
                              month_week)]

                week['workouts'] += 1
                week['time'] += workout.duration or timedelta(seconds=0)
                week['distance'] += workout.distance or Decimal(0)
                week['elevation'] += workout.uphill or Decimal(0)
                if workout.sport not in week['sports']:
                    week['sports'][workout.sport] = {
                        'workouts': 0,
                        'time': timedelta(seconds=0),
                        'distance': Decimal(0),
                        'elevation': Decimal(0),
                    }
                week['sports'][workout.sport]['workouts'] += 1
                week['sports'][workout.sport]['time'] += (
                    workout.duration or timedelta(0))
                week['sports'][workout.sport]['distance'] += (
                    workout.distance or Decimal(0))
                week['sports'][workout.sport]['elevation'] += (
                    workout.uphill or Decimal(0))

        return stats

<filename>ow/models/user.py
from decimal import Decimal
from datetime import datetime, timedelta, timezone
from uuid import uuid1
from operator import attrgetter

import bcrypt
from repoze.folder import Folder
from pyramid.security import Allow, Deny, Everyone, ALL_PERMISSIONS

from ow.catalog import get_catalog, reindex_object
from ow.utilities import get_week_days, get_month_week_number


class User(Folder):

    __parent__ = __name__ = None

    def __acl__(self):
        permissions = [
            (Allow, str(self.uid), 'view'),
            (Allow, str(self.uid), 'edit'),
            (Deny, Everyone, ALL_PERMISSIONS),
        ]
        return permissions

    def __init__(self, **kw):
        self.uid = kw.get('uid', uuid1())
        self.nickname = kw.get('nickname', '')
        self.firstname = kw.get('firstname', '')
        self.lastname = kw.get('lastname', '')
        self.email = kw.get('email', '')
        self.bio = kw.get('bio', '')
        self.birth_date = kw.get('birth_date', None)
        self.height = kw.get('height', None)
        self.weight = kw.get('weight', None)
        self.gender = kw.get('gender', 'robot')
        self.picture = kw.get('picture', None)  # blob
        self.timezone = kw.get('timezone', 'UTC')
        self.locale = kw.get('locale', 'en')
        self.__password = None
        self.last_workout_id = 0
        # has this user verified his account?
        self.verified = False
        self.verification_token = None
        self.verification_tokens_sent = 0
        super(User, self).__init__()

    def __str__(self):
        return u'User: %s (%s)' % (self.uid, self.email)

    def __repr__(self):
        return u'<%s.%s: %s (%s)>' % (
            self.__class__.__module__,
            self.__class__.__name__,
            self.uid, self.email
        )

    @property
    def password(self):
        return self.__password

    @password.setter
    def password(self, password=None):
        """
        Sets a password for the user, hashing with bcrypt.
        """
        password = password.encode('utf-8')
        self.__password = bcrypt.hashpw(password, bcrypt.gensalt())

    def check_password(self, password):
        """
        Check a plain text password against a hashed one
        """
        hashed = bcrypt.hashpw(password.encode('utf-8'), self.__password)
        return hashed == self.__password

    @property
    def fullname(self):
        """
        Naive implementation of fullname: firstname + lastname
        """
        return u'%s %s' % (self.firstname, self.lastname)

    def add_workout(self, workout):
        # This returns the main catalog at the root folder
        catalog = get_catalog(self)
        self.last_workout_id += 1
        workout_id = str(self.last_workout_id)
        self[workout_id] = workout
        reindex_object(catalog, workout)

    def workouts(self, year=None, month=None, week=None):
        """
        Return this user workouts, sorted by date, from newer to older
        """
        workouts = self.values()
        if year:
            workouts = [w for w in workouts if w.start.year == year]
            if month:
                workouts = [w for w in workouts if w.start.month == month]
            if week:
                week = int(week)
                workouts = [
                    w for w in workouts if w.start.isocalendar()[1] == week]
        workouts = sorted(workouts, key=attrgetter('start'))
        workouts.reverse()
        return workouts

    def workout_ids(self):
        return self.keys()

    @property
    def num_workouts(self):
        return len(self.workout_ids())

    @property
    def activity_years(self):
        return sorted(list(set(w.start.year for w in self.workouts())),
                      reverse=True)

    def activity_months(self, year):
        months = set(
            w.start.month for w in self.workouts() if w.start.year == year)
        return sorted(list(months))

    @property
    def activity_dates_tree(self):
        """
        Return a dict containing information about the activity for this
        user.

        Example:

        {
            2019: {
                1: {'cycling': 12, 'running': 1}
            },
            2018: {
                1: {'cycling': 10, 'running': 3},
                2: {'cycling': 14, 'swimming': 5}
            }
        }
        """
        tree = {}
        for workout in self.workouts():
            year = workout.start.year
            month = workout.start.month
            sport = workout.sport
            if year not in tree:
                tree[year] = {}
            if month not in tree[year]:
                tree[year][month] = {}
            if sport not in tree[year][month]:
                tree[year][month][sport] = 0
            tree[year][month][sport] += 1
        return tree

    def stats(self, year=None, month=None):
        year = year or datetime.now().year
        stats = {
            'workouts': 0,
            'time': timedelta(seconds=0),
            'distance': Decimal(0),
            'elevation': Decimal(0),
            'sports': {}
        }

        for workout in self.workouts(year=year, month=month):
            stats['workouts'] += 1
            stats['time'] += workout.duration or timedelta(seconds=0)
            stats['distance'] += workout.distance or Decimal(0)
            stats['elevation'] += workout.uphill or Decimal(0)

            if workout.sport not in stats['sports']:
                stats['sports'][workout.sport] = {
                    'workouts': 0,
                    'time': timedelta(seconds=0),
                    'distance': Decimal(0),
                    'elevation': Decimal(0),
                }

            stats['sports'][workout.sport]['workouts'] += 1
            stats['sports'][workout.sport]['time'] += (
                workout.duration or timedelta(0))
            stats['sports'][workout.sport]['distance'] += (
                workout.distance or Decimal(0))
            stats['sports'][workout.sport]['elevation'] += (
                workout.uphill or Decimal(0))

        return stats

    def get_week_stats(self, day):
        """
        Return some stats for the week the given day is in.
        """
        week = get_week_days(day)

        # filter workouts
        workouts = []
        for workout in self.workouts():
            if week[0].date() <= workout.start.date() <= week[-1].date():
                workouts.append(workout)

        # build stats
        stats = {}
        for week_day in week:
            stats[week_day] = {
                'workouts': 0,
                'time': timedelta(0),
                'distance': Decimal(0),
                'elevation': Decimal(0),
                'sports': {}
            }
            for workout in workouts:
                if workout.start.date() == week_day.date():
                    day = stats[week_day]  # less typing, avoid long lines
                    day['workouts'] += 1
                    day['time'] += workout.duration or timedelta(seconds=0)
                    day['distance'] += workout.distance or Decimal(0)
                    day['elevation'] += workout.uphill or Decimal(0)
                    if workout.sport not in day['sports']:
                        day['sports'][workout.sport] = {
                            'workouts': 0,
                            'time': timedelta(seconds=0),
                            'distance': Decimal(0),
                            'elevation': Decimal(0),
                        }
                    day['sports'][workout.sport]['workouts'] += 1
                    day['sports'][workout.sport]['time'] += (
                        workout.duration or timedelta(0))
                    day['sports'][workout.sport]['distance'] += (
                        workout.distance or Decimal(0))
                    day['sports'][workout.sport]['elevation'] += (
                        workout.uphill or Decimal(0))

        return stats

    @property
    def week_stats(self):
        """
        Helper that returns the week stats for the current week
        """
        return self.get_week_stats(datetime.now(timezone.utc))

    @property
    def week_totals(self):
        week_stats = self.week_stats
        return {
            'distance': sum([week_stats[t]['distance'] for t in week_stats]),
            'time': sum([week_stats[t]['time'] for t in week_stats],
                        timedelta())
        }

    @property
    def yearly_stats(self):
        """
        Return per-month stats for the last 12 months
        """
        # set the boundaries for looking for workouts afterwards,
        # we need the current date as the "end date" and one year
        # ago from that date. Then we set the start at the first
        # day of that month.
        end = datetime.now(timezone.utc)
        start = (end - timedelta(days=365)).replace(day=1)

        # build the stats, populating first the dict with empty values
        # for each month.
        stats = {}
        for days in range((end - start).days):
            day = (start + timedelta(days=days)).date()
            if (day.year, day.month) not in stats.keys():
                stats[(day.year, day.month)] = {
                    'workouts': 0,
                    'time': timedelta(0),
                    'distance': Decimal(0),
                    'elevation': Decimal(0),
                    'sports': {}
                }

        # now loop over workouts, filtering and then adding stats to the
        # proper place
        for workout in self.workouts():
            if start.date() <= workout.start.date() <= end.date():
                # less typing, avoid long lines
                month = stats[
                    (workout.start.date().year, workout.start.date().month)]
                month['workouts'] += 1
                month['time'] += workout.duration or timedelta(seconds=0)
                month['distance'] += workout.distance or Decimal(0)
                month['elevation'] += workout.uphill or Decimal(0)
                if workout.sport not in month['sports']:
                    month['sports'][workout.sport] = {
                        'workouts': 0,
                        'time': timedelta(seconds=0),
                        'distance': Decimal(0),
                        'elevation': Decimal(0),
                    }
                month['sports'][workout.sport]['workouts'] += 1
                month['sports'][workout.sport]['time'] += (
                    workout.duration or timedelta(0))
                month['sports'][workout.sport]['distance'] += (
                    workout.distance or Decimal(0))
                month['sports'][workout.sport]['elevation'] += (
                    workout.uphill or Decimal(0))

        return stats

    @property
    def weekly_year_stats(self):
        """
        Return per-week stats for the last 12 months
        """
        # set the boundaries for looking for workouts afterwards,
        # we need the current date as the "end date" and one year
        # ago from that date. Then we set the start at the first
        # day of that month.
        end = datetime.now(timezone.utc)
        start = (end - timedelta(days=365)).replace(day=1)

        stats = {}

        # first initialize the stats dict
        for days in range((end - start).days):
            day = (start + timedelta(days=days)).date()
            week = day.isocalendar()[1]
            month_week = get_month_week_number(day)
            key = (day.year, day.month, week, month_week)
            if key not in stats.keys():
                stats[key] = {
                    'workouts': 0,
                    'time': timedelta(0),
                    'distance': Decimal(0),
                    'elevation': Decimal(0),
                    'sports': {}
                }

        # now loop over the workouts, filtering and then adding stats
        # to the proper place
        for workout in self.workouts():
            if start.date() <= workout.start.date() <= end.date():
                # less typing, avoid long lines
                start_date = workout.start.date()
                week = start_date.isocalendar()[1]
                month_week = get_month_week_number(start_date)
                week = stats[(start_date.year,
                              start_date.month,
                              week,
                              month_week)]

                week['workouts'] += 1
                week['time'] += workout.duration or timedelta(seconds=0)
                week['distance'] += workout.distance or Decimal(0)
                week['elevation'] += workout.uphill or Decimal(0)
                if workout.sport not in week['sports']:
                    week['sports'][workout.sport] = {
                        'workouts': 0,
                        'time': timedelta(seconds=0),
                        'distance': Decimal(0),
                        'elevation': Decimal(0),
                    }
                week['sports'][workout.sport]['workouts'] += 1
                week['sports'][workout.sport]['time'] += (
                    workout.duration or timedelta(0))
                week['sports'][workout.sport]['distance'] += (
                    workout.distance or Decimal(0))
                week['sports'][workout.sport]['elevation'] += (
                    workout.uphill or Decimal(0))

        return stats

# blob # has this user verified his account? Sets a password for the user, hashing with bcrypt. Check a plain text password against a hashed one Naive implementation of fullname: firstname + lastname # This returns the main catalog at the root folder Return this user workouts, sorted by date, from newer to older Return a dict containing information about the activity for this         user.          Example:          {             2019: {                 1: {'cycling': 12, 'running': 1}             },             2018: {                 1: {'cycling': 10, 'running': 3},                 2: {'cycling': 14, 'swimming': 5}             }         } Return some stats for the week the given day is in. # filter workouts # build stats # less typing, avoid long lines Helper that returns the week stats for the current week Return per-month stats for the last 12 months # set the boundaries for looking for workouts afterwards, # we need the current date as the "end date" and one year # ago from that date. Then we set the start at the first # day of that month. # build the stats, populating first the dict with empty values # for each month. # now loop over workouts, filtering and then adding stats to the # proper place # less typing, avoid long lines Return per-week stats for the last 12 months # set the boundaries for looking for workouts afterwards, # we need the current date as the "end date" and one year # ago from that date. Then we set the start at the first # day of that month. # first initialize the stats dict # now loop over the workouts, filtering and then adding stats # to the proper place # less typing, avoid long lines