Changeset 2d91474 in OpenWorkouts-current


Ignore:
Timestamp:
Jan 15, 2019, 10:13:57 PM (5 years ago)
Author:
borja <borja@…>
Branches:
current, feature/docs, master
Children:
0c18869
Parents:
9bee49d
Message:

(#23) - Show workouts in the dashboard with date filtering

Location:
ow
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • ow/models/user.py

    r9bee49d r2d91474  
    7575        reindex_object(catalog, workout)
    7676
    77     def workouts(self):
     77    def workouts(self, year=None, month=None):
    7878        """
    7979        Return this user workouts, sorted by date, from newer to older
    8080        """
    81         workouts = sorted(self.values(), key=attrgetter('start'))
     81        workouts = self.values()
     82        if year:
     83            workouts = [w for w in workouts if w.start.year == year]
     84        if month:
     85            workouts = [w for w in workouts if w.start.month == month]
     86        workouts = sorted(workouts, key=attrgetter('start'))
    8287        workouts.reverse()
    8388        return workouts
     
    8994    def num_workouts(self):
    9095        return len(self.workout_ids())
     96
     97    @property
     98    def activity_years(self):
     99        return sorted(list(set(w.start.year for w in self.workouts())),
     100                      reverse=True)
     101
     102    def activity_months(self, year):
     103        months = set(
     104            w.start.month for w in self.workouts() if w.start.year == year)
     105        return sorted(list(months))
     106
     107    @property
     108    def activity_dates_tree(self):
     109        """
     110        Return a dict containing information about the activity for this
     111        user.
     112
     113        Example:
     114
     115        {
     116            2019: {
     117                1: {'cycling': 12, 'running': 1}
     118            },
     119            2018: {
     120                1: {'cycling': 10, 'running': 3},
     121                2: {'cycling': 14, 'swimming': 5}
     122            }
     123        }
     124        """
     125        tree = {}
     126        for workout in self.workouts():
     127            year = workout.start.year
     128            month = workout.start.month
     129            sport = workout.sport
     130            if year not in tree:
     131                tree[year] = {}
     132            if month not in tree[year]:
     133                tree[year][month] = {}
     134            if sport not in tree[year][month]:
     135                tree[year][month][sport] = 0
     136            tree[year][month][sport] += 1
     137        return tree
  • ow/static/css/main.css

    r9bee49d r2d91474  
    760760}
    761761
     762.workout-activity-tree {
     763    padding:0;
     764    list-style-type: none;
     765    transition:all 250ms ease-in-out
     766    list-style-type:none;
     767    font-size:13px;
     768    font-size:.8125rem;
     769    color: #959595;
     770}
     771
     772.workout-activity-tree a {
     773    text-decoration: none;
     774    background-color: transparent;
     775    outline: 0;
     776    color: #959595;
     777}
     778
     779
     780.workout-activity-tree li a.viewing-year {
     781    font-size: 17px;
     782    color: #151515;
     783}
     784
     785.workout-activity-tree ul.hidden {
     786    display: none;
     787}
     788
     789.workout-activity-tree-year {
     790    list-style-type: none;
     791    padding: 0;
     792    padding-left: 15px;
     793    font-size:13px;
     794    font-size:.8125rem;
     795    color: #959595;
     796}
     797
     798.workout-activity-tree-year a {
     799    text-decoration: none;
     800    background-color: transparent;
     801    outline: 0;
     802    color: #959595;
     803}
     804
     805
     806.workout-activity-tree-year li a.viewing-month {
     807    font-size: 17px;
     808    color: #151515;
     809}
     810
     811.workout-activity-tree-month {
     812    list-style-type: none;
     813    padding: 0;
     814    padding-left: 15px;
     815    font-size:13px;
     816    font-size:.8125rem
     817    color: #959595;
     818}
     819
     820.workout-activity-tree-month a {
     821    text-decoration: none;
     822    background-color: transparent;
     823    outline: 0;
     824    color: #959595;
     825}
     826
     827
     828
    762829@media (min-width:800px) {
    763830        .workout-aside {
  • ow/templates/dashboard.pt

    r9bee49d r2d91474  
    2727        <h2 tal:content="context.fullname"></h2>
    2828
    29         <tal:r tal:repeat="workout context.workouts()">
     29        <h3>
     30            (<tal:n tal:content="len(workouts)"></tal:n>/<tal:n tal:content="context.num_workouts"></tal:n>) <tal:t i18n:translate="">workouts</tal:t>
     31        </h3>
     32
     33        <tal:no-workouts tal:condition="context.num_workouts == 0">
     34            <div class="warning">
     35                <p i18n:translate="">You haven't added any workouts yet</p>
     36                <p>
     37                    <a href="" i18n:translate=""
     38                       tal:attributes="href request.resource_url(context, 'add-workout')">
     39                        Upload one</a> |
     40                    <a href="" i18n:translate=""
     41                       tal:attributes="href request.resource_url(context, 'add-workout-manually')">
     42                        Add one manually</a>
     43                </p>
     44            </div>
     45        </tal:no-workouts>
     46
     47        <tal:r tal:repeat="workout workouts">
    3048
    3149          <article class="workout-resume">
     
    85103
    86104      <aside class="workout-aside">
    87         <h2>...</h2>
     105          <tal:activity_tree tal:condition="context.num_workouts > 0">
     106              <ul class="workout-activity-tree" tal:define="tree context.activity_dates_tree">
     107                  <tal:years tal:repeat="year sorted(tree.keys(), reverse=True )">
     108                      <li tal:define="is_viewing_year year == viewing_year">
     109                          <a href="" tal:content="year"
     110                             tal:attributes="href request.resource_url(context, query={'year': year});
     111                                             class 'js-year viewing-year' if is_viewing_year  else 'js-year'">
     112                          </a>
     113                          <ul class="workout-activity-tree-year"
     114                              tal:attributes="class 'workout-activity-tree-year' if is_viewing_year  else 'workout-activity-tree-year hidden'">
     115                              <tal:months tal:repeat="month sorted(tree[year].keys())">
     116                                  <li tal:define="is_viewing_month is_viewing_year and month == viewing_month">
     117                                      <a href="" tal:content="month_name[month]"
     118                                         tal:attributes="href request.resource_url(context, query={'year': year, 'month': month});
     119                                                         class 'viewing-month' if is_viewing_month else ''">
     120                                      </a>
     121                                      <ul class="workout-activity-tree-month">
     122                                          <tal:sports tal:repeat="sport sorted(tree[year][month].keys())">
     123                                              <li>
     124                                                  <a href="#">
     125                                                      <tal:sport tal:content="sport"></tal:sport> (<tal:workouts tal:content="tree[year][month][sport]"></tal:workouts>)
     126                                                  </a>
     127                                              </li>
     128                                          </tal:sports>
     129                                      </ul>
     130                                  </li>
     131                              </tal:months>
     132                          </ul>
     133                      </li>
     134                  </tal:years>
     135              </ul>
     136          </tal:activity_tree>
    88137      </aside>
    89138
  • ow/tests/models/test_user.py

    r9bee49d r2d91474  
     1from datetime import datetime, timedelta, timezone
     2
    13import pytest
    24from pyramid.security import Allow
     
    6870        assert list(root['john'].workout_ids()) == ['1', '2', '3']
    6971        assert root['john'].num_workouts == len(workouts)
     72
     73    def test_activity_dates_tree(self, root):
     74        # first an empty test
     75        assert root['john'].activity_dates_tree == {}
     76        # now add a cycling workout in a given date (25/11/2018)
     77        workout = Workout(
     78            start=datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc),
     79            duration=timedelta(minutes=(60*4)),
     80            distance=115,
     81            sport='cycling')
     82        root['john'].add_workout(workout)
     83        assert root['john'].activity_dates_tree == {
     84            2018: {11: {'cycling': 1}}
     85        }
     86        # add a running workout on the same date
     87        workout = Workout(
     88            start=datetime(2018, 11, 25, 16, 30, tzinfo=timezone.utc),
     89            duration=timedelta(minutes=60),
     90            distance=12,
     91            sport='running')
     92        root['john'].add_workout(workout)
     93        assert root['john'].activity_dates_tree == {
     94            2018: {11: {'cycling': 1, 'running': 1}}
     95        }
     96        # add a swimming workout on a different date, same year
     97        workout = Workout(
     98            start=datetime(2018, 8, 15, 11, 30, tzinfo=timezone.utc),
     99            duration=timedelta(minutes=30),
     100            distance=2,
     101            sport='swimming')
     102        root['john'].add_workout(workout)
     103        assert root['john'].activity_dates_tree == {
     104            2018: {8: {'swimming': 1},
     105                   11: {'cycling': 1, 'running': 1}}
     106        }
     107        # now add some more cycling in a different year
     108        # add a swimming workout on a different date, same year
     109        workout = Workout(
     110            start=datetime(2017, 4, 15, 15, 00, tzinfo=timezone.utc),
     111            duration=timedelta(minutes=(60*3)),
     112            distance=78,
     113            sport='cycling')
     114        root['john'].add_workout(workout)
     115        assert root['john'].activity_dates_tree == {
     116            2017: {4: {'cycling': 1}},
     117            2018: {8: {'swimming': 1},
     118                   11: {'cycling': 1, 'running': 1}}
     119        }
  • ow/tests/views/test_user.py

    r9bee49d r2d91474  
    152152        request = dummy_request
    153153        response = user_views.dashboard(john, request)
    154         assert response == {}
     154        assert len(response) == 4
     155        assert 'month_name' in response.keys()
     156        # this user has a single workout, in 2015
     157        assert response['viewing_year'] == 2015
     158        assert response['viewing_month'] == 6
     159        assert response['workouts'] == [w for w in john.workouts()]
     160
     161    def test_dashboard_year(self, dummy_request, john):
     162        """
     163        Renders the user dashboard for a chosen year.
     164        """
     165        request = dummy_request
     166        # first test the year for which we know there is a workout
     167        request.GET['year'] = 2015
     168        response = user_views.dashboard(john, request)
     169        assert len(response) == 4
     170        assert 'month_name' in response.keys()
     171        # this user has a single workout, in 2015
     172        assert response['viewing_year'] == 2015
     173        assert response['viewing_month'] == 6
     174        assert response['workouts'] == [w for w in john.workouts()]
     175        # now, a year we know there is no workout info
     176        request.GET['year'] = 2000
     177        response = user_views.dashboard(john, request)
     178        assert len(response) == 4
     179        assert 'month_name' in response.keys()
     180        # this user has a single workout, in 2015
     181        assert response['viewing_year'] == 2000
     182        # we have no data for that year and we didn't ask for a certain month,
     183        # so the passing value for that is None
     184        assert response['viewing_month'] is None
     185        assert response['workouts'] == []
     186
     187    def test_dashboard_year_month(self, dummy_request, john):
     188        """
     189        Renders the user dashboard for a chosen year and month.
     190        """
     191        request = dummy_request
     192        # first test the year/month for which we know there is a workout
     193        request.GET['year'] = 2015
     194        request.GET['month'] = 6
     195        response = user_views.dashboard(john, request)
     196        assert len(response) == 4
     197        assert 'month_name' in response.keys()
     198        # this user has a single workout, in 2015
     199        assert response['viewing_year'] == 2015
     200        assert response['viewing_month'] == 6
     201        assert response['workouts'] == [w for w in john.workouts()]
     202        # now, change month to one without values
     203        request.GET['month'] = 2
     204        response = user_views.dashboard(john, request)
     205        assert len(response) == 4
     206        assert 'month_name' in response.keys()
     207        # this user has a single workout, in 2015
     208        assert response['viewing_year'] == 2015
     209        assert response['viewing_month'] == 2
     210        assert response['workouts'] == []
     211        # now the month with data, but in a different year
     212        request.GET['year'] = 2010
     213        request.GET['month'] = 6
     214        response = user_views.dashboard(john, request)
     215        assert len(response) == 4
     216        assert 'month_name' in response.keys()
     217        # this user has a single workout, in 2015
     218        assert response['viewing_year'] == 2010
     219        assert response['viewing_month'] == 6
     220        assert response['workouts'] == []
     221
     222    def test_dashboard_month(self, dummy_request, john):
     223        """
     224        Passing a month without a year when rendering the dashboard. The last
     225        year for which workout data is available is assumed
     226        """
     227        request = dummy_request
     228        # Set a month without workout data
     229        request.GET['month'] = 5
     230        response = user_views.dashboard(john, request)
     231        assert len(response) == 4
     232        assert 'month_name' in response.keys()
     233        # this user has a single workout, in 2015
     234        assert response['viewing_year'] == 2015
     235        assert response['viewing_month'] == 5
     236        assert response['workouts'] == []
     237        # now a month with data
     238        request.GET['month'] = 6
     239        response = user_views.dashboard(john, request)
     240        assert len(response) == 4
     241        assert 'month_name' in response.keys()
     242        # this user has a single workout, in 2015
     243        assert response['viewing_year'] == 2015
     244        assert response['viewing_month'] == 6
     245        assert response['workouts'] == [w for w in john.workouts()]
    155246
    156247    def test_profile(self, dummy_request, john):
  • ow/views/user.py

    r9bee49d r2d91474  
     1from calendar import month_name
     2
    13from pyramid.httpexceptions import HTTPFound
    24from pyramid.view import view_config
     
    112114    Render a dashboard for the current user
    113115    """
    114     # Add here some logic
    115     return {}
     116    # Look at the year we are viewing, if none is passed in the request,
     117    # pick up the latest/newer available with activity
     118    viewing_year = request.GET.get('year', None)
     119    if viewing_year is None:
     120        available_years = context.activity_years
     121        if available_years:
     122            viewing_year = available_years[0]
     123    else:
     124        # ensure this is an integer
     125        viewing_year = int(viewing_year)
     126
     127    # Same for the month, if there is a year set
     128    viewing_month = None
     129    if viewing_year:
     130        viewing_month = request.GET.get('month', None)
     131        if viewing_month is None:
     132            available_months = context.activity_months(viewing_year)
     133            if available_months:
     134                # we pick up the latest month available for the year,
     135                # which means the current month in the current year
     136                viewing_month = available_months[-1]
     137        else:
     138            # ensure this is an integer
     139            viewing_month = int(viewing_month)
     140
     141    # pick up the workouts to be shown in the dashboard
     142    workouts = context.workouts(viewing_year, viewing_month)
     143
     144    return {
     145        'month_name': month_name,
     146        'viewing_year': viewing_year,
     147        'viewing_month': viewing_month,
     148        'workouts': workouts
     149    }
    116150
    117151
Note: See TracChangeset for help on using the changeset viewer.