Index: ow/models/user.py
===================================================================
--- ow/models/user.py	(revision 9bee49da1793c891556377df44b57a6744bd7ed7)
+++ ow/models/user.py	(revision 2d91474e12335560df474ab7b46521eeb1ae9330)
@@ -75,9 +75,14 @@
         reindex_object(catalog, workout)
 
-    def workouts(self):
+    def workouts(self, year=None, month=None):
         """
         Return this user workouts, sorted by date, from newer to older
         """
-        workouts = sorted(self.values(), key=attrgetter('start'))
+        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]
+        workouts = sorted(workouts, key=attrgetter('start'))
         workouts.reverse()
         return workouts
@@ -89,2 +94,44 @@
     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
Index: ow/static/css/main.css
===================================================================
--- ow/static/css/main.css	(revision 9bee49da1793c891556377df44b57a6744bd7ed7)
+++ ow/static/css/main.css	(revision 2d91474e12335560df474ab7b46521eeb1ae9330)
@@ -760,4 +760,71 @@
 }
 
+.workout-activity-tree {
+    padding:0;
+    list-style-type: none;
+    transition:all 250ms ease-in-out
+    list-style-type:none;
+    font-size:13px;
+    font-size:.8125rem;
+    color: #959595;
+}
+
+.workout-activity-tree a {
+    text-decoration: none;
+    background-color: transparent;
+    outline: 0;
+    color: #959595;
+}
+
+
+.workout-activity-tree li a.viewing-year {
+    font-size: 17px;
+    color: #151515;
+}
+
+.workout-activity-tree ul.hidden {
+    display: none;
+}
+
+.workout-activity-tree-year {
+    list-style-type: none;
+    padding: 0;
+    padding-left: 15px;
+    font-size:13px;
+    font-size:.8125rem;
+    color: #959595;
+}
+
+.workout-activity-tree-year a {
+    text-decoration: none;
+    background-color: transparent;
+    outline: 0;
+    color: #959595;
+}
+
+
+.workout-activity-tree-year li a.viewing-month {
+    font-size: 17px;
+    color: #151515;
+}
+
+.workout-activity-tree-month {
+    list-style-type: none;
+    padding: 0;
+    padding-left: 15px;
+    font-size:13px;
+    font-size:.8125rem
+    color: #959595;
+}
+
+.workout-activity-tree-month a {
+    text-decoration: none;
+    background-color: transparent;
+    outline: 0;
+    color: #959595;
+}
+
+
+
 @media (min-width:800px) {
 	.workout-aside {
Index: ow/templates/dashboard.pt
===================================================================
--- ow/templates/dashboard.pt	(revision 9bee49da1793c891556377df44b57a6744bd7ed7)
+++ ow/templates/dashboard.pt	(revision 2d91474e12335560df474ab7b46521eeb1ae9330)
@@ -27,5 +27,23 @@
         <h2 tal:content="context.fullname"></h2>
 
-        <tal:r tal:repeat="workout context.workouts()">
+        <h3>
+            (<tal:n tal:content="len(workouts)"></tal:n>/<tal:n tal:content="context.num_workouts"></tal:n>) <tal:t i18n:translate="">workouts</tal:t>
+        </h3>
+
+        <tal:no-workouts tal:condition="context.num_workouts == 0">
+            <div class="warning">
+                <p i18n:translate="">You haven't added any workouts yet</p>
+                <p>
+                    <a href="" i18n:translate=""
+                       tal:attributes="href request.resource_url(context, 'add-workout')">
+                        Upload one</a> |
+                    <a href="" i18n:translate=""
+                       tal:attributes="href request.resource_url(context, 'add-workout-manually')">
+                        Add one manually</a>
+                </p>
+            </div>
+        </tal:no-workouts>
+
+        <tal:r tal:repeat="workout workouts">
 
           <article class="workout-resume">
@@ -85,5 +103,36 @@
 
       <aside class="workout-aside">
-        <h2>...</h2>
+          <tal:activity_tree tal:condition="context.num_workouts > 0">
+              <ul class="workout-activity-tree" tal:define="tree context.activity_dates_tree">
+                  <tal:years tal:repeat="year sorted(tree.keys(), reverse=True )">
+                      <li tal:define="is_viewing_year year == viewing_year">
+                          <a href="" tal:content="year"
+                             tal:attributes="href request.resource_url(context, query={'year': year});
+                                             class 'js-year viewing-year' if is_viewing_year  else 'js-year'">
+                          </a>
+                          <ul class="workout-activity-tree-year"
+                              tal:attributes="class 'workout-activity-tree-year' if is_viewing_year  else 'workout-activity-tree-year hidden'">
+                              <tal:months tal:repeat="month sorted(tree[year].keys())">
+                                  <li tal:define="is_viewing_month is_viewing_year and month == viewing_month">
+                                      <a href="" tal:content="month_name[month]"
+                                         tal:attributes="href request.resource_url(context, query={'year': year, 'month': month});
+                                                         class 'viewing-month' if is_viewing_month else ''">
+                                      </a>
+                                      <ul class="workout-activity-tree-month">
+                                          <tal:sports tal:repeat="sport sorted(tree[year][month].keys())">
+                                              <li>
+                                                  <a href="#">
+                                                      <tal:sport tal:content="sport"></tal:sport> (<tal:workouts tal:content="tree[year][month][sport]"></tal:workouts>)
+                                                  </a>
+                                              </li>
+                                          </tal:sports>
+                                      </ul>
+                                  </li>
+                              </tal:months>
+                          </ul>
+                      </li>
+                  </tal:years>
+              </ul>
+          </tal:activity_tree>
       </aside>
 
Index: ow/tests/models/test_user.py
===================================================================
--- ow/tests/models/test_user.py	(revision 9bee49da1793c891556377df44b57a6744bd7ed7)
+++ ow/tests/models/test_user.py	(revision 2d91474e12335560df474ab7b46521eeb1ae9330)
@@ -1,2 +1,4 @@
+from datetime import datetime, timedelta, timezone
+
 import pytest
 from pyramid.security import Allow
@@ -68,2 +70,50 @@
         assert list(root['john'].workout_ids()) == ['1', '2', '3']
         assert root['john'].num_workouts == len(workouts)
+
+    def test_activity_dates_tree(self, root):
+        # first an empty test
+        assert root['john'].activity_dates_tree == {}
+        # now add a cycling workout in a given date (25/11/2018)
+        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)
+        assert root['john'].activity_dates_tree == {
+            2018: {11: {'cycling': 1}}
+        }
+        # add a running workout on the same date
+        workout = Workout(
+            start=datetime(2018, 11, 25, 16, 30, tzinfo=timezone.utc),
+            duration=timedelta(minutes=60),
+            distance=12,
+            sport='running')
+        root['john'].add_workout(workout)
+        assert root['john'].activity_dates_tree == {
+            2018: {11: {'cycling': 1, 'running': 1}}
+        }
+        # add a swimming workout on a different date, same year
+        workout = Workout(
+            start=datetime(2018, 8, 15, 11, 30, tzinfo=timezone.utc),
+            duration=timedelta(minutes=30),
+            distance=2,
+            sport='swimming')
+        root['john'].add_workout(workout)
+        assert root['john'].activity_dates_tree == {
+            2018: {8: {'swimming': 1},
+                   11: {'cycling': 1, 'running': 1}}
+        }
+        # now add some more cycling in a different year
+        # add a swimming workout on a different date, same year
+        workout = Workout(
+            start=datetime(2017, 4, 15, 15, 00, tzinfo=timezone.utc),
+            duration=timedelta(minutes=(60*3)),
+            distance=78,
+            sport='cycling')
+        root['john'].add_workout(workout)
+        assert root['john'].activity_dates_tree == {
+            2017: {4: {'cycling': 1}},
+            2018: {8: {'swimming': 1},
+                   11: {'cycling': 1, 'running': 1}}
+        }
Index: ow/tests/views/test_user.py
===================================================================
--- ow/tests/views/test_user.py	(revision 9bee49da1793c891556377df44b57a6744bd7ed7)
+++ ow/tests/views/test_user.py	(revision 2d91474e12335560df474ab7b46521eeb1ae9330)
@@ -152,5 +152,96 @@
         request = dummy_request
         response = user_views.dashboard(john, request)
-        assert response == {}
+        assert len(response) == 4
+        assert 'month_name' in response.keys()
+        # this user has a single workout, in 2015
+        assert response['viewing_year'] == 2015
+        assert response['viewing_month'] == 6
+        assert response['workouts'] == [w for w in john.workouts()]
+
+    def test_dashboard_year(self, dummy_request, john):
+        """
+        Renders the user dashboard for a chosen year.
+        """
+        request = dummy_request
+        # first test the year for which we know there is a workout
+        request.GET['year'] = 2015
+        response = user_views.dashboard(john, request)
+        assert len(response) == 4
+        assert 'month_name' in response.keys()
+        # this user has a single workout, in 2015
+        assert response['viewing_year'] == 2015
+        assert response['viewing_month'] == 6
+        assert response['workouts'] == [w for w in john.workouts()]
+        # now, a year we know there is no workout info
+        request.GET['year'] = 2000
+        response = user_views.dashboard(john, request)
+        assert len(response) == 4
+        assert 'month_name' in response.keys()
+        # this user has a single workout, in 2015
+        assert response['viewing_year'] == 2000
+        # we have no data for that year and we didn't ask for a certain month,
+        # so the passing value for that is None
+        assert response['viewing_month'] is None
+        assert response['workouts'] == []
+
+    def test_dashboard_year_month(self, dummy_request, john):
+        """
+        Renders the user dashboard for a chosen year and month.
+        """
+        request = dummy_request
+        # first test the year/month for which we know there is a workout
+        request.GET['year'] = 2015
+        request.GET['month'] = 6
+        response = user_views.dashboard(john, request)
+        assert len(response) == 4
+        assert 'month_name' in response.keys()
+        # this user has a single workout, in 2015
+        assert response['viewing_year'] == 2015
+        assert response['viewing_month'] == 6
+        assert response['workouts'] == [w for w in john.workouts()]
+        # now, change month to one without values
+        request.GET['month'] = 2
+        response = user_views.dashboard(john, request)
+        assert len(response) == 4
+        assert 'month_name' in response.keys()
+        # this user has a single workout, in 2015
+        assert response['viewing_year'] == 2015
+        assert response['viewing_month'] == 2
+        assert response['workouts'] == []
+        # now the month with data, but in a different year
+        request.GET['year'] = 2010
+        request.GET['month'] = 6
+        response = user_views.dashboard(john, request)
+        assert len(response) == 4
+        assert 'month_name' in response.keys()
+        # this user has a single workout, in 2015
+        assert response['viewing_year'] == 2010
+        assert response['viewing_month'] == 6
+        assert response['workouts'] == []
+
+    def test_dashboard_month(self, dummy_request, john):
+        """
+        Passing a month without a year when rendering the dashboard. The last
+        year for which workout data is available is assumed
+        """
+        request = dummy_request
+        # Set a month without workout data
+        request.GET['month'] = 5
+        response = user_views.dashboard(john, request)
+        assert len(response) == 4
+        assert 'month_name' in response.keys()
+        # this user has a single workout, in 2015
+        assert response['viewing_year'] == 2015
+        assert response['viewing_month'] == 5
+        assert response['workouts'] == []
+        # now a month with data
+        request.GET['month'] = 6
+        response = user_views.dashboard(john, request)
+        assert len(response) == 4
+        assert 'month_name' in response.keys()
+        # this user has a single workout, in 2015
+        assert response['viewing_year'] == 2015
+        assert response['viewing_month'] == 6
+        assert response['workouts'] == [w for w in john.workouts()]
 
     def test_profile(self, dummy_request, john):
Index: ow/views/user.py
===================================================================
--- ow/views/user.py	(revision 9bee49da1793c891556377df44b57a6744bd7ed7)
+++ ow/views/user.py	(revision 2d91474e12335560df474ab7b46521eeb1ae9330)
@@ -1,2 +1,4 @@
+from calendar import month_name
+
 from pyramid.httpexceptions import HTTPFound
 from pyramid.view import view_config
@@ -112,6 +114,38 @@
     Render a dashboard for the current user
     """
-    # Add here some logic
-    return {}
+    # Look at the year we are viewing, if none is passed in the request,
+    # pick up the latest/newer available with activity
+    viewing_year = request.GET.get('year', None)
+    if viewing_year is None:
+        available_years = context.activity_years
+        if available_years:
+            viewing_year = available_years[0]
+    else:
+        # ensure this is an integer
+        viewing_year = int(viewing_year)
+
+    # Same for the month, if there is a year set
+    viewing_month = None
+    if viewing_year:
+        viewing_month = request.GET.get('month', None)
+        if viewing_month is None:
+            available_months = context.activity_months(viewing_year)
+            if available_months:
+                # we pick up the latest month available for the year,
+                # which means the current month in the current year
+                viewing_month = available_months[-1]
+        else:
+            # ensure this is an integer
+            viewing_month = int(viewing_month)
+
+    # pick up the workouts to be shown in the dashboard
+    workouts = context.workouts(viewing_year, viewing_month)
+
+    return {
+        'month_name': month_name,
+        'viewing_year': viewing_year,
+        'viewing_month': viewing_month,
+        'workouts': workouts
+    }
 
 
