Index: ow/models/user.py
===================================================================
--- ow/models/user.py	(revision be40b029b0ce623b12cc7ade52eeba2fda3b6d45)
+++ ow/models/user.py	(revision 2f8a48f0eded1be10dbb2771b302dfb9c1ddbd0b)
@@ -1,3 +1,4 @@
-
+from decimal import Decimal
+from datetime import datetime, timedelta, timezone
 from uuid import uuid1
 from operator import attrgetter
@@ -8,4 +9,5 @@
 
 from ow.catalog import get_catalog, reindex_object
+from ow.utilities import get_week_days
 
 
@@ -136,2 +138,98 @@
             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())
+        }
Index: ow/models/workout.py
===================================================================
--- ow/models/workout.py	(revision be40b029b0ce623b12cc7ade52eeba2fda3b6d45)
+++ ow/models/workout.py	(revision 2f8a48f0eded1be10dbb2771b302dfb9c1ddbd0b)
@@ -13,5 +13,6 @@
     create_blob,
     mps_to_kmph,
-    save_map_screenshot
+    save_map_screenshot,
+    timedelta_to_hms
 )
 
@@ -110,7 +111,5 @@
 
     def split_duration(self):
-        hours, remainder = divmod(int(self.duration.total_seconds()), 3600)
-        minutes, seconds = divmod(remainder, 60)
-        return hours, minutes, seconds
+        return timedelta_to_hms(self.duration)
 
     @property
Index: ow/template_helpers.py
===================================================================
--- ow/template_helpers.py	(revision 2f8a48f0eded1be10dbb2771b302dfb9c1ddbd0b)
+++ ow/template_helpers.py	(revision 2f8a48f0eded1be10dbb2771b302dfb9c1ddbd0b)
@@ -0,0 +1,8 @@
+from pyramid.events import subscriber, BeforeRender
+
+from ow.utilities import timedelta_to_hms
+
+
+@subscriber(BeforeRender)
+def add_renderer_globals(event):  # pragma: no cover
+    event['timedelta_to_hms'] = timedelta_to_hms
Index: ow/tests/models/test_user.py
===================================================================
--- ow/tests/models/test_user.py	(revision be40b029b0ce623b12cc7ade52eeba2fda3b6d45)
+++ ow/tests/models/test_user.py	(revision 2f8a48f0eded1be10dbb2771b302dfb9c1ddbd0b)
@@ -1,2 +1,3 @@
+from decimal import Decimal
 from datetime import datetime, timedelta, timezone
 
@@ -118,2 +119,221 @@
                    11: {'cycling': 1, 'running': 1}}
         }
+
+    def test_stats(self, root):
+        expected_no_stats = {
+            'workouts': 0,
+            'time': timedelta(seconds=0),
+            'distance': Decimal(0),
+            'elevation': Decimal(0),
+            'sports': {}
+        }
+        # no stats
+        assert root['john'].stats() == expected_no_stats
+        # add a cycling workout
+        workout = Workout(
+            start=datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc),
+            duration=timedelta(minutes=(60*4)),
+            distance=115,
+            sport='cycling')
+        root['john'].add_workout(workout)
+        # asking for a different year, future
+        assert root['john'].stats(2019) == expected_no_stats
+        # asking for a different year, past
+        assert root['john'].stats(2016) == expected_no_stats
+        # asking fot the year the workout is in
+        assert root['john'].stats(2018) == {
+            'workouts': 1,
+            'time': timedelta(minutes=(60*4)),
+            'distance': Decimal(115),
+            'elevation': Decimal(0),
+            'sports': {
+                'cycling': {
+                    'workouts': 1,
+                    'time': timedelta(minutes=(60*4)),
+                    'distance': Decimal(115),
+                    'elevation': Decimal(0),
+                }
+            }
+        }
+        # add a second cycling workout
+        workout = Workout(
+            start=datetime(2018, 11, 26, 10, 00, tzinfo=timezone.utc),
+            duration=timedelta(minutes=(60*3)),
+            distance=100,
+            sport='cycling')
+        root['john'].add_workout(workout)
+        assert root['john'].stats(2018) == {
+            'workouts': 2,
+            'time': timedelta(minutes=(60*7)),
+            'distance': Decimal(215),
+            'elevation': Decimal(0),
+            'sports': {
+                'cycling': {
+                    'workouts': 2,
+                    'time': timedelta(minutes=(60*7)),
+                    'distance': Decimal(215),
+                    'elevation': Decimal(0),
+                }
+            }
+        }
+        # add a running workout
+        workout = Workout(
+            start=datetime(2018, 11, 26, 16, 00, tzinfo=timezone.utc),
+            duration=timedelta(minutes=(60)),
+            distance=10,
+            sport='running')
+        root['john'].add_workout(workout)
+        assert root['john'].stats(2018) == {
+            'workouts': 3,
+            'time': timedelta(minutes=(60*8)),
+            'distance': Decimal(225),
+            'elevation': Decimal(0),
+            'sports': {
+                'cycling': {
+                    'workouts': 2,
+                    'time': timedelta(minutes=(60*7)),
+                    'distance': Decimal(215),
+                    'elevation': Decimal(0),
+                },
+                'running': {
+                    'workouts': 1,
+                    'time': timedelta(minutes=(60)),
+                    'distance': Decimal(10),
+                    'elevation': Decimal(0),
+                }
+            }
+        }
+        # ensure the stats for future/past years did not change after
+        # adding those workouts
+        assert root['john'].stats(2019) == expected_no_stats
+        assert root['john'].stats(2016) == expected_no_stats
+
+    def test_get_week_stats(self, root):
+        expected_no_stats_per_day = {
+            'workouts': 0,
+            'time': timedelta(0),
+            'distance': Decimal(0),
+            'elevation': Decimal(0),
+            'sports': {}
+        }
+
+        expected_no_stats = {}
+        for i in range(19, 26):
+            day = datetime(2018, 11, i, 10, 00, tzinfo=timezone.utc)
+            expected_no_stats[day] = expected_no_stats_per_day
+
+        day = datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc)
+        assert root['john'].get_week_stats(day) == expected_no_stats
+
+        # add a cycling workout
+        workout = Workout(
+            start=datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc),
+            duration=timedelta(minutes=(60*4)),
+            distance=115,
+            sport='cycling')
+        root['john'].add_workout(workout)
+
+        # check a week in the future
+        day = datetime(2019, 11, 25, 10, 00, tzinfo=timezone.utc)
+        week_stats = root['john'].get_week_stats(day)
+        for day in week_stats:
+            assert week_stats[day] == expected_no_stats_per_day
+
+        # check a week in the past
+        day = datetime(2017, 11, 25, 10, 00, tzinfo=timezone.utc)
+        week_stats = root['john'].get_week_stats(day)
+        for day in week_stats:
+            assert week_stats[day] == expected_no_stats_per_day
+
+        # Check the week where the workout is
+        day = datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc)
+        week_stats = root['john'].get_week_stats(day)
+        for day in week_stats:
+            if day.day == 25:
+                # this is the day where we have a workout
+                assert week_stats[day] == {
+                    'workouts': 1,
+                    'time': timedelta(minutes=(60*4)),
+                    'distance': Decimal(115),
+                    'elevation': Decimal(0),
+                    'sports': {
+                        'cycling': {
+                            'workouts': 1,
+                            'time': timedelta(minutes=(60*4)),
+                            'distance': Decimal(115),
+                            'elevation': Decimal(0)
+                        }
+                    }
+                }
+            else:
+                # day without workout
+                assert week_stats[day] == expected_no_stats_per_day
+
+        # add a second cycling workout
+        workout = Workout(
+            start=datetime(2018, 11, 23, 10, 00, tzinfo=timezone.utc),
+            duration=timedelta(minutes=(60*3)),
+            distance=100,
+            sport='cycling')
+        root['john'].add_workout(workout)
+        day = datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc)
+        week_stats = root['john'].get_week_stats(day)
+        for day in week_stats:
+            if day.day == 25:
+                # this is the day where we have a workout
+                assert week_stats[day] == {
+                    'workouts': 1,
+                    'time': timedelta(minutes=(60*4)),
+                    'distance': Decimal(115),
+                    'elevation': Decimal(0),
+                    'sports': {
+                        'cycling': {
+                            'workouts': 1,
+                            'time': timedelta(minutes=(60*4)),
+                            'distance': Decimal(115),
+                            'elevation': Decimal(0)
+                        }
+                    }
+                }
+            elif day.day == 23:
+                # this is the day where we have a workout
+                assert week_stats[day] == {
+                    'workouts': 1,
+                    'time': timedelta(minutes=(60*3)),
+                    'distance': Decimal(100),
+                    'elevation': Decimal(0),
+                    'sports': {
+                        'cycling': {
+                            'workouts': 1,
+                            'time': timedelta(minutes=(60*3)),
+                            'distance': Decimal(100),
+                            'elevation': Decimal(0)
+                        }
+                    }
+                }
+            else:
+                # day without workout
+                assert week_stats[day] == expected_no_stats_per_day
+
+    def test_week_stats(self, root):
+        expected_no_stats_per_day = {
+            'workouts': 0,
+            'time': timedelta(0),
+            'distance': Decimal(0),
+            'elevation': Decimal(0),
+            'sports': {}
+        }
+
+        # no workouts for the current week (this tests is for coverage
+        # purposes mostly, as the main logic is tested in test_get_week_stats)
+        day = datetime.now(timezone.utc)
+        week_stats = root['john'].get_week_stats(day)
+        for day in week_stats:
+            assert week_stats[day] == expected_no_stats_per_day
+
+    def test_week_totals(self, root):
+        # no data, empty totals
+        assert root['john'].week_totals == {
+            'distance': Decimal(0),
+            'time': timedelta(0)
+        }
Index: ow/tests/test_utilities.py
===================================================================
--- ow/tests/test_utilities.py	(revision be40b029b0ce623b12cc7ade52eeba2fda3b6d45)
+++ ow/tests/test_utilities.py	(revision 2f8a48f0eded1be10dbb2771b302dfb9c1ddbd0b)
@@ -1,4 +1,4 @@
 import os
-from datetime import timedelta
+from datetime import timedelta, datetime
 from unittest.mock import patch
 from pyexpat import ExpatError
@@ -22,5 +22,7 @@
     mps_to_kmph,
     kmph_to_mps,
-    save_map_screenshot
+    save_map_screenshot,
+    timedelta_to_hms,
+    get_week_days
 )
 
@@ -144,4 +146,65 @@
         assert not os.makedirs.called
         subprocess.run.assert_called_once
+
+    def test_timedelta_to_hms(self):
+        value = timedelta(seconds=0)
+        assert timedelta_to_hms(value) == (0, 0, 0)
+        value = timedelta(seconds=3600)
+        assert timedelta_to_hms(value) == (1, 0, 0)
+        value = timedelta(seconds=3900)
+        assert timedelta_to_hms(value) == (1, 5, 0)
+        value = timedelta(seconds=3940)
+        assert timedelta_to_hms(value) == (1, 5, 40)
+        value = timedelta(seconds=4)
+        assert timedelta_to_hms(value) == (0, 0, 4)
+        value = timedelta(seconds=150)
+        assert timedelta_to_hms(value) == (0, 2, 30)
+        # try now something that is not a timedelta
+        with pytest.raises(AttributeError):
+            timedelta_to_hms('not a timedelta')
+
+    def test_week_days(self):
+        # get days from a monday, week starting on monday
+        days = get_week_days(datetime(2019, 1, 21))
+        assert len(days) == 7
+        matches = [
+            [days[0], datetime(2019, 1, 21)],
+            [days[1], datetime(2019, 1, 22)],
+            [days[2], datetime(2019, 1, 23)],
+            [days[3], datetime(2019, 1, 24)],
+            [days[4], datetime(2019, 1, 25)],
+            [days[5], datetime(2019, 1, 26)],
+            [days[6], datetime(2019, 1, 27)]
+        ]
+        for m in matches:
+            assert m[0] == m[1]
+        # get days from a wednesday, week starting on monday
+        days = get_week_days(datetime(2019, 1, 23))
+        assert len(days) == 7
+        matches = [
+            [days[0], datetime(2019, 1, 21)],
+            [days[1], datetime(2019, 1, 22)],
+            [days[2], datetime(2019, 1, 23)],
+            [days[3], datetime(2019, 1, 24)],
+            [days[4], datetime(2019, 1, 25)],
+            [days[5], datetime(2019, 1, 26)],
+            [days[6], datetime(2019, 1, 27)]
+        ]
+        for m in matches:
+            assert m[0] == m[1]
+        # get days from a monday, but week starting on sunday now
+        days = get_week_days(datetime(2019, 1, 21), start_day=0)
+        assert len(days) == 7
+        matches = [
+            [days[0], datetime(2019, 1, 20)],
+            [days[1], datetime(2019, 1, 21)],
+            [days[2], datetime(2019, 1, 22)],
+            [days[3], datetime(2019, 1, 23)],
+            [days[4], datetime(2019, 1, 24)],
+            [days[5], datetime(2019, 1, 25)],
+            [days[6], datetime(2019, 1, 26)]
+        ]
+        for m in matches:
+            assert m[0] == m[1]
 
 
Index: ow/utilities.py
===================================================================
--- ow/utilities.py	(revision be40b029b0ce623b12cc7ade52eeba2fda3b6d45)
+++ ow/utilities.py	(revision 2f8a48f0eded1be10dbb2771b302dfb9c1ddbd0b)
@@ -3,5 +3,5 @@
 import logging
 import subprocess
-from datetime import datetime
+from datetime import datetime, timedelta
 from decimal import Decimal
 from shutil import copyfileobj
@@ -212,2 +212,24 @@
 
     return False
+
+
+def timedelta_to_hms(value):
+    """
+    Return hours, minutes, seconds from a timedelta object
+    """
+    hours, remainder = divmod(int(value.total_seconds()), 3600)
+    minutes, seconds = divmod(remainder, 60)
+    return hours, minutes, seconds
+
+
+def get_week_days(day, start_day=1):
+    """
+    Return a list of datetime objects for the days of the week "day" is in.
+
+    day is a datetime object (like in datetime.now() for "today")
+
+    start_day can be used to set if week starts on monday (1) or sunday (0)
+    """
+    first_day = day - timedelta(days=day.isoweekday() - start_day)
+    week_days = [first_day + timedelta(days=i) for i in range(7)]
+    return week_days
