Changeset 26220ba in OpenWorkouts-current


Ignore:
Timestamp:
Jan 25, 2019, 12:48:51 AM (5 years ago)
Author:
Borja Lopez <borja@…>
Branches:
current, feature/docs, master
Children:
d0fc76b, ed7e9d7
Parents:
c6219ed (diff), 5bdfbfb (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Merge patches from darcs

Files:
1 added
12 edited

Legend:

Unmodified
Added
Removed
  • bin/js_deps

    rc6219ed r26220ba  
    227227}
    228228
     229d3() {
     230    NAME="d3"
     231    VERSION=5.7.0
     232    URL=https://github.com/${NAME}/${NAME}/releases/download/v${VERSION}/${NAME}.zip
     233    check_cache ${NAME} ${VERSION}
     234    in_cache=$?
     235    if [ ${in_cache} -eq 0 -a ${REINSTALL} -eq 1 ]; then
     236        echo "${NAME}-${VERSION} $ALREADY_INSTALLED"
     237    else
     238        echo "> Installing ${NAME} ${VERSION}"
     239        if [ -d ${COMPONENTS}/${NAME} ]; then
     240            # Clean up, delete previous install before installing
     241            rm -r ${COMPONENTS}/${NAME}
     242        fi
     243        cd ${TMP}
     244        ${GET} ${URL}
     245        ${UNZIP} ${NAME}.zip -d ${NAME}
     246        cd ${CURRENT}
     247        mv ${TMP}/${NAME} ${COMPONENTS}/
     248        echo "${NAME}-${VERSION}" >> ${CACHE}
     249        echo "< Installed ${NAME} ${VERSION}"
     250    fi
     251}
     252
     253
    229254echo "Installing JS dependencies in ${COMPONENTS}"
    230255
     
    235260leaflet_elevation
    236261pickadate
     262d3
     263
    237264
    238265# Clean up, remove the tmp directory
  • ow/models/user.py

    rc6219ed r26220ba  
    1 
     1from decimal import Decimal
     2from datetime import datetime, timedelta, timezone
    23from uuid import uuid1
    34from operator import attrgetter
     
    89
    910from ow.catalog import get_catalog, reindex_object
     11from ow.utilities import get_week_days
    1012
    1113
     
    136138            tree[year][month][sport] += 1
    137139        return tree
     140
     141    def stats(self, year=None, month=None):
     142        year = year or datetime.now().year
     143        stats = {
     144            'workouts': 0,
     145            'time': timedelta(seconds=0),
     146            'distance': Decimal(0),
     147            'elevation': Decimal(0),
     148            'sports': {}
     149        }
     150
     151        for workout in self.workouts(year=year, month=month):
     152            stats['workouts'] += 1
     153            stats['time'] += workout.duration or timedelta(seconds=0)
     154            stats['distance'] += workout.distance or Decimal(0)
     155            stats['elevation'] += workout.uphill or Decimal(0)
     156
     157            if workout.sport not in stats['sports']:
     158                stats['sports'][workout.sport] = {
     159                    'workouts': 0,
     160                    'time': timedelta(seconds=0),
     161                    'distance': Decimal(0),
     162                    'elevation': Decimal(0),
     163                }
     164
     165            stats['sports'][workout.sport]['workouts'] += 1
     166            stats['sports'][workout.sport]['time'] += (
     167                workout.duration or timedelta(0))
     168            stats['sports'][workout.sport]['distance'] += (
     169                workout.distance or Decimal(0))
     170            stats['sports'][workout.sport]['elevation'] += (
     171                workout.uphill or Decimal(0))
     172
     173        return stats
     174
     175    def get_week_stats(self, day):
     176        """
     177        Return some stats for the week the given day is in.
     178        """
     179        week = get_week_days(day)
     180
     181        # filter workouts
     182        workouts = []
     183        for workout in self.workouts():
     184            if week[0].date() <= workout.start.date() <= week[-1].date():
     185                workouts.append(workout)
     186
     187        # build stats
     188        stats = {}
     189        for week_day in week:
     190            stats[week_day] = {
     191                'workouts': 0,
     192                'time': timedelta(0),
     193                'distance': Decimal(0),
     194                'elevation': Decimal(0),
     195                'sports': {}
     196            }
     197            for workout in workouts:
     198                if workout.start.date() == week_day.date():
     199                    day = stats[week_day]  # less typing, avoid long lines
     200                    day['workouts'] += 1
     201                    day['time'] += workout.duration or timedelta(seconds=0)
     202                    day['distance'] += workout.distance or Decimal(0)
     203                    day['elevation'] += workout.uphill or Decimal(0)
     204                    if workout.sport not in day['sports']:
     205                        day['sports'][workout.sport] = {
     206                            'workouts': 0,
     207                            'time': timedelta(seconds=0),
     208                            'distance': Decimal(0),
     209                            'elevation': Decimal(0),
     210                        }
     211                    day['sports'][workout.sport]['workouts'] += 1
     212                    day['sports'][workout.sport]['time'] += (
     213                        workout.duration or timedelta(0))
     214                    day['sports'][workout.sport]['distance'] += (
     215                        workout.distance or Decimal(0))
     216                    day['sports'][workout.sport]['elevation'] += (
     217                        workout.uphill or Decimal(0))
     218
     219        return stats
     220
     221    @property
     222    def week_stats(self):
     223        """
     224        Helper that returns the week stats for the current week
     225        """
     226        return self.get_week_stats(datetime.now(timezone.utc))
     227
     228    @property
     229    def week_totals(self):
     230        week_stats = self.week_stats
     231        return {
     232            'distance': sum([week_stats[t]['distance'] for t in week_stats]),
     233            'time': sum([week_stats[t]['time'] for t in week_stats],
     234                        timedelta())
     235        }
  • ow/models/workout.py

    rc6219ed r26220ba  
    1313    create_blob,
    1414    mps_to_kmph,
    15     save_map_screenshot
     15    save_map_screenshot,
     16    timedelta_to_hms
    1617)
    1718
     
    110111
    111112    def split_duration(self):
    112         hours, remainder = divmod(int(self.duration.total_seconds()), 3600)
    113         minutes, seconds = divmod(remainder, 60)
    114         return hours, minutes, seconds
     113        return timedelta_to_hms(self.duration)
    115114
    116115    @property
  • ow/static/css/openworkouts.css

    rc6219ed r26220ba  
    1212    background-color: #eeeeee;
    1313}
     14
     15/* dashboard, weekly stats */
     16
     17span.week_totals_left {
     18    padding-right: 5px;
     19    border-right: 1px solid #e1e1e1;
     20}
     21
     22span.week_totals_right {
     23    padding-left: 5px;
     24}
     25
     26.x-axis path, .x-axis line {
     27    fill: none;
     28    stroke: none;
     29}
     30
     31.bar {
     32    fill: #f8b5be;
     33}
     34
     35.bar:hover {
     36    fill: #ee4056;
     37}
     38
     39.current {
     40    fill: #ee4056;
     41}
     42
     43text.label {
     44    font-size: 0.6em;
     45    text-anchor: middle;
     46}
  • ow/static/js/ow.js

    rc6219ed r26220ba  
    123123
    124124};
     125
     126
     127owjs.week_chart = function(spec) {
     128
     129    "use strict";
     130
     131    // parameters provided when creating an "instance" of the chart
     132    var chart_selector = spec.chart_selector,
     133        url = spec.url,
     134        current_day_name = spec.current_day_name
     135
     136    // Helpers
     137    function select_x_axis_label(d) {
     138        /* Given a value, return the label associated with it */
     139        return d3.select('.x-axis')
     140            .selectAll('text')
     141            .filter(function(x) { return x == d.name; });
     142    }
     143
     144    // Methods
     145    var render = function render() {
     146        /*
     147           Build a d3 bar chart, populated with data from the given url.
     148         */
     149        var chart = d3.select("svg"),
     150            margin = {top: 20, right: 20, bottom: 30, left: 50},
     151            width = +chart.attr("width") - margin.left - margin.right,
     152            height = +chart.attr("height") - margin.top - margin.bottom,
     153            g = chart.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"),
     154            x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
     155            y = d3.scaleLinear().rangeRound([height, 0]);
     156
     157        d3.json(url).then(function (data) {
     158            x.domain(data.map(function (d) {
     159                return d.name;
     160            }));
     161
     162            y.domain([0, d3.max(data, function (d) {
     163                return Number(d.distance);
     164            })]);
     165
     166            g.append("g")
     167                .attr('class', 'x-axis')
     168                .attr("transform", "translate(0," + height + ")")
     169                .call(d3.axisBottom(x))
     170
     171            g.selectAll(".bar")
     172                .data(data)
     173                .enter().append("rect")
     174                .attr("class", function(d) {
     175                    if (d.name == current_day_name){
     176                        select_x_axis_label(d).attr('style', "font-weight: bold;");
     177                        return 'bar current'
     178                    }
     179                    else {
     180                        return 'bar'
     181                    }
     182                })
     183                .attr("x", function (d) {
     184                    return x(d.name);
     185                })
     186                .attr("y", function (d) {
     187                    return y(Number(d.distance));
     188                })
     189                .attr("width", x.bandwidth())
     190                .attr("height", function (d) {
     191                    return height - y(Number(d.distance));
     192                })
     193                .on('mouseover', function(d) {
     194                    if (d.name != current_day_name){
     195                        select_x_axis_label(d).attr('style', "font-weight: bold;");
     196                    }
     197                })
     198                .on('mouseout', function(d) {
     199                    if (d.name != current_day_name){
     200                        select_x_axis_label(d).attr('style', "font-weight: regular;");
     201                    }
     202                });
     203
     204            g.selectAll(".text")
     205                .data(data)
     206                .enter()
     207                .append("text")
     208                .attr("class","label")
     209                .attr("x", function (d) {
     210                    return x(d.name) + x.bandwidth()/2;
     211                })
     212                .attr("y", function (d) {
     213                    return y(Number(d.distance) + 5);
     214                })
     215                .text(function(d) {
     216                    if (Number(d.distance) > 0) {
     217                        return d.distance;
     218                    }
     219                });
     220
     221        });
     222    };
     223
     224    var that = {}
     225    that.render = render;
     226    return that
     227
     228};
  • ow/templates/dashboard.pt

    rc6219ed r26220ba  
    142142              </ul>
    143143          </tal:activity_tree>
     144
     145          <tal:stats>
     146
     147            <div class="week-stats js-week-stats">
     148              <h3><tal:t i18n:translate="">This week</tal:t></h3>
     149              <h4 tal:define="totals context.week_totals">
     150                <span class="week_totals_left">
     151                  <tal:d tal:content="round(totals['distance'])"></tal:d>
     152                  <tal:t i18n:translate="">kms</tal:t>
     153                </span>
     154                <span class="week_totals_right">
     155                  <tal:hms tal:define="hms timedelta_to_hms(totals['time'])">
     156                    <tal:h tal:content="str(hms[0]).zfill(2)"></tal:h>
     157                    <tal:t i18n:translate="">hours</tal:t>,
     158                    <tal:h tal:content="str(hms[1]).zfill(2)"></tal:h>
     159                    <tal:t i18n:translate="">min.</tal:t>
     160                  </tal:hms>
     161                </span>
     162              </h4>
     163              <svg width="300" height="200"></svg>
     164              <style>
     165
     166              </style>
     167            </div>
     168
     169            <div class="user-stats">
     170              <tal:year-stats tal:repeat="year context.activity_years">
     171                <h3><a href="" tal:content="year"></a></h3>
     172                <ul tal:define="stats context.stats(year)">
     173                  <li>
     174                    <span i18n:translate="">Workouts:</span>
     175                    <span tal:content="stats['workouts']"></span>
     176                  </li>
     177                  <li>
     178                    <span i18n:translate="">Distance:</span>
     179                    <span tal:content="round(stats['distance'])"></span> kms
     180                  </li>
     181                  <li>
     182                    <span i18n:translate="">Time:</span>
     183                    <tal:hms tal:define="hms timedelta_to_hms(stats['time'])">
     184                      <span>
     185                        <tal:h tal:content="str(hms[0]).zfill(2)"></tal:h>
     186                        <tal:t i18n:translate="">hours</tal:t>,
     187                        <tal:h tal:content="str(hms[1]).zfill(2)"></tal:h>
     188                        <tal:t i18n:translate="">min.</tal:t>
     189                      </span>
     190                    </tal:hms>
     191                  </li>
     192                  <li>
     193                    <span i18n:translate="">Elevation:</span>
     194                    <span tal:content="stats['elevation']"></span> m
     195                  </li>
     196                </ul>
     197              </tal:year-stats>
     198            </div>
     199          </tal:stats>
     200
    144201      </aside>
    145202
     
    148205  </metal:content>
    149206
     207  <metal:body-js metal:fill-slot="body-js">
     208    <script src="${request.static_url('ow:static/components/d3/d3.min.js')}"></script>
     209    <script src="${request.static_url('ow:static/js/ow.js')}"></script>
     210
     211    <script type="text/javascript">
     212     var week_chart = owjs.week_chart({
     213         chart_selector: 'div.js-week-stats',
     214         url: "${request.resource_url(context, 'week')}",
     215         current_day_name: "${current_day_name}"
     216     });
     217     week_chart.render();
     218    </script>
     219
     220  </metal:body-js>
     221
    150222</html>
  • ow/tests/models/test_user.py

    rc6219ed r26220ba  
     1from decimal import Decimal
    12from datetime import datetime, timedelta, timezone
    23
     
    118119                   11: {'cycling': 1, 'running': 1}}
    119120        }
     121
     122    def test_stats(self, root):
     123        expected_no_stats = {
     124            'workouts': 0,
     125            'time': timedelta(seconds=0),
     126            'distance': Decimal(0),
     127            'elevation': Decimal(0),
     128            'sports': {}
     129        }
     130        # no stats
     131        assert root['john'].stats() == expected_no_stats
     132        # add a cycling workout
     133        workout = Workout(
     134            start=datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc),
     135            duration=timedelta(minutes=(60*4)),
     136            distance=115,
     137            sport='cycling')
     138        root['john'].add_workout(workout)
     139        # asking for a different year, future
     140        assert root['john'].stats(2019) == expected_no_stats
     141        # asking for a different year, past
     142        assert root['john'].stats(2016) == expected_no_stats
     143        # asking fot the year the workout is in
     144        assert root['john'].stats(2018) == {
     145            'workouts': 1,
     146            'time': timedelta(minutes=(60*4)),
     147            'distance': Decimal(115),
     148            'elevation': Decimal(0),
     149            'sports': {
     150                'cycling': {
     151                    'workouts': 1,
     152                    'time': timedelta(minutes=(60*4)),
     153                    'distance': Decimal(115),
     154                    'elevation': Decimal(0),
     155                }
     156            }
     157        }
     158        # add a second cycling workout
     159        workout = Workout(
     160            start=datetime(2018, 11, 26, 10, 00, tzinfo=timezone.utc),
     161            duration=timedelta(minutes=(60*3)),
     162            distance=100,
     163            sport='cycling')
     164        root['john'].add_workout(workout)
     165        assert root['john'].stats(2018) == {
     166            'workouts': 2,
     167            'time': timedelta(minutes=(60*7)),
     168            'distance': Decimal(215),
     169            'elevation': Decimal(0),
     170            'sports': {
     171                'cycling': {
     172                    'workouts': 2,
     173                    'time': timedelta(minutes=(60*7)),
     174                    'distance': Decimal(215),
     175                    'elevation': Decimal(0),
     176                }
     177            }
     178        }
     179        # add a running workout
     180        workout = Workout(
     181            start=datetime(2018, 11, 26, 16, 00, tzinfo=timezone.utc),
     182            duration=timedelta(minutes=(60)),
     183            distance=10,
     184            sport='running')
     185        root['john'].add_workout(workout)
     186        assert root['john'].stats(2018) == {
     187            'workouts': 3,
     188            'time': timedelta(minutes=(60*8)),
     189            'distance': Decimal(225),
     190            'elevation': Decimal(0),
     191            'sports': {
     192                'cycling': {
     193                    'workouts': 2,
     194                    'time': timedelta(minutes=(60*7)),
     195                    'distance': Decimal(215),
     196                    'elevation': Decimal(0),
     197                },
     198                'running': {
     199                    'workouts': 1,
     200                    'time': timedelta(minutes=(60)),
     201                    'distance': Decimal(10),
     202                    'elevation': Decimal(0),
     203                }
     204            }
     205        }
     206        # ensure the stats for future/past years did not change after
     207        # adding those workouts
     208        assert root['john'].stats(2019) == expected_no_stats
     209        assert root['john'].stats(2016) == expected_no_stats
     210
     211    def test_get_week_stats(self, root):
     212        expected_no_stats_per_day = {
     213            'workouts': 0,
     214            'time': timedelta(0),
     215            'distance': Decimal(0),
     216            'elevation': Decimal(0),
     217            'sports': {}
     218        }
     219
     220        expected_no_stats = {}
     221        for i in range(19, 26):
     222            day = datetime(2018, 11, i, 10, 00, tzinfo=timezone.utc)
     223            expected_no_stats[day] = expected_no_stats_per_day
     224
     225        day = datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc)
     226        assert root['john'].get_week_stats(day) == expected_no_stats
     227
     228        # add a cycling workout
     229        workout = Workout(
     230            start=datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc),
     231            duration=timedelta(minutes=(60*4)),
     232            distance=115,
     233            sport='cycling')
     234        root['john'].add_workout(workout)
     235
     236        # check a week in the future
     237        day = datetime(2019, 11, 25, 10, 00, tzinfo=timezone.utc)
     238        week_stats = root['john'].get_week_stats(day)
     239        for day in week_stats:
     240            assert week_stats[day] == expected_no_stats_per_day
     241
     242        # check a week in the past
     243        day = datetime(2017, 11, 25, 10, 00, tzinfo=timezone.utc)
     244        week_stats = root['john'].get_week_stats(day)
     245        for day in week_stats:
     246            assert week_stats[day] == expected_no_stats_per_day
     247
     248        # Check the week where the workout is
     249        day = datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc)
     250        week_stats = root['john'].get_week_stats(day)
     251        for day in week_stats:
     252            if day.day == 25:
     253                # this is the day where we have a workout
     254                assert week_stats[day] == {
     255                    'workouts': 1,
     256                    'time': timedelta(minutes=(60*4)),
     257                    'distance': Decimal(115),
     258                    'elevation': Decimal(0),
     259                    'sports': {
     260                        'cycling': {
     261                            'workouts': 1,
     262                            'time': timedelta(minutes=(60*4)),
     263                            'distance': Decimal(115),
     264                            'elevation': Decimal(0)
     265                        }
     266                    }
     267                }
     268            else:
     269                # day without workout
     270                assert week_stats[day] == expected_no_stats_per_day
     271
     272        # add a second cycling workout
     273        workout = Workout(
     274            start=datetime(2018, 11, 23, 10, 00, tzinfo=timezone.utc),
     275            duration=timedelta(minutes=(60*3)),
     276            distance=100,
     277            sport='cycling')
     278        root['john'].add_workout(workout)
     279        day = datetime(2018, 11, 25, 10, 00, tzinfo=timezone.utc)
     280        week_stats = root['john'].get_week_stats(day)
     281        for day in week_stats:
     282            if day.day == 25:
     283                # this is the day where we have a workout
     284                assert week_stats[day] == {
     285                    'workouts': 1,
     286                    'time': timedelta(minutes=(60*4)),
     287                    'distance': Decimal(115),
     288                    'elevation': Decimal(0),
     289                    'sports': {
     290                        'cycling': {
     291                            'workouts': 1,
     292                            'time': timedelta(minutes=(60*4)),
     293                            'distance': Decimal(115),
     294                            'elevation': Decimal(0)
     295                        }
     296                    }
     297                }
     298            elif day.day == 23:
     299                # this is the day where we have a workout
     300                assert week_stats[day] == {
     301                    'workouts': 1,
     302                    'time': timedelta(minutes=(60*3)),
     303                    'distance': Decimal(100),
     304                    'elevation': Decimal(0),
     305                    'sports': {
     306                        'cycling': {
     307                            'workouts': 1,
     308                            'time': timedelta(minutes=(60*3)),
     309                            'distance': Decimal(100),
     310                            'elevation': Decimal(0)
     311                        }
     312                    }
     313                }
     314            else:
     315                # day without workout
     316                assert week_stats[day] == expected_no_stats_per_day
     317
     318    def test_week_stats(self, root):
     319        expected_no_stats_per_day = {
     320            'workouts': 0,
     321            'time': timedelta(0),
     322            'distance': Decimal(0),
     323            'elevation': Decimal(0),
     324            'sports': {}
     325        }
     326
     327        # no workouts for the current week (this tests is for coverage
     328        # purposes mostly, as the main logic is tested in test_get_week_stats)
     329        day = datetime.now(timezone.utc)
     330        week_stats = root['john'].get_week_stats(day)
     331        for day in week_stats:
     332            assert week_stats[day] == expected_no_stats_per_day
     333
     334    def test_week_totals(self, root):
     335        # no data, empty totals
     336        assert root['john'].week_totals == {
     337            'distance': Decimal(0),
     338            'time': timedelta(0)
     339        }
  • ow/tests/test_utilities.py

    rc6219ed r26220ba  
    11import os
    2 from datetime import timedelta
     2from datetime import timedelta, datetime
    33from unittest.mock import patch
    44from pyexpat import ExpatError
     
    2222    mps_to_kmph,
    2323    kmph_to_mps,
    24     save_map_screenshot
     24    save_map_screenshot,
     25    timedelta_to_hms,
     26    get_week_days
    2527)
    2628
     
    144146        assert not os.makedirs.called
    145147        subprocess.run.assert_called_once
     148
     149    def test_timedelta_to_hms(self):
     150        value = timedelta(seconds=0)
     151        assert timedelta_to_hms(value) == (0, 0, 0)
     152        value = timedelta(seconds=3600)
     153        assert timedelta_to_hms(value) == (1, 0, 0)
     154        value = timedelta(seconds=3900)
     155        assert timedelta_to_hms(value) == (1, 5, 0)
     156        value = timedelta(seconds=3940)
     157        assert timedelta_to_hms(value) == (1, 5, 40)
     158        value = timedelta(seconds=4)
     159        assert timedelta_to_hms(value) == (0, 0, 4)
     160        value = timedelta(seconds=150)
     161        assert timedelta_to_hms(value) == (0, 2, 30)
     162        # try now something that is not a timedelta
     163        with pytest.raises(AttributeError):
     164            timedelta_to_hms('not a timedelta')
     165
     166    def test_week_days(self):
     167        # get days from a monday, week starting on monday
     168        days = get_week_days(datetime(2019, 1, 21))
     169        assert len(days) == 7
     170        matches = [
     171            [days[0], datetime(2019, 1, 21)],
     172            [days[1], datetime(2019, 1, 22)],
     173            [days[2], datetime(2019, 1, 23)],
     174            [days[3], datetime(2019, 1, 24)],
     175            [days[4], datetime(2019, 1, 25)],
     176            [days[5], datetime(2019, 1, 26)],
     177            [days[6], datetime(2019, 1, 27)]
     178        ]
     179        for m in matches:
     180            assert m[0] == m[1]
     181        # get days from a wednesday, week starting on monday
     182        days = get_week_days(datetime(2019, 1, 23))
     183        assert len(days) == 7
     184        matches = [
     185            [days[0], datetime(2019, 1, 21)],
     186            [days[1], datetime(2019, 1, 22)],
     187            [days[2], datetime(2019, 1, 23)],
     188            [days[3], datetime(2019, 1, 24)],
     189            [days[4], datetime(2019, 1, 25)],
     190            [days[5], datetime(2019, 1, 26)],
     191            [days[6], datetime(2019, 1, 27)]
     192        ]
     193        for m in matches:
     194            assert m[0] == m[1]
     195        # get days from a monday, but week starting on sunday now
     196        days = get_week_days(datetime(2019, 1, 21), start_day=0)
     197        assert len(days) == 7
     198        matches = [
     199            [days[0], datetime(2019, 1, 20)],
     200            [days[1], datetime(2019, 1, 21)],
     201            [days[2], datetime(2019, 1, 22)],
     202            [days[3], datetime(2019, 1, 23)],
     203            [days[4], datetime(2019, 1, 24)],
     204            [days[5], datetime(2019, 1, 25)],
     205            [days[6], datetime(2019, 1, 26)]
     206        ]
     207        for m in matches:
     208            assert m[0] == m[1]
    146209
    147210
  • ow/tests/views/test_user.py

    rc6219ed r26220ba  
    11import os
     2import json
    23from datetime import datetime, timedelta, timezone
    34from shutil import copyfileobj
     
    152153        request = dummy_request
    153154        response = user_views.dashboard(john, request)
    154         assert len(response) == 4
    155         assert 'month_name' in response.keys()
     155        assert len(response) == 6
     156        assert 'month_name' in response.keys()
     157        assert response['current_year'] == datetime.now().year
     158        assert response['current_day_name'] == datetime.now().strftime('%a')
    156159        # this user has a single workout, in 2015
    157160        assert response['viewing_year'] == 2015
     
    167170        request.GET['year'] = 2015
    168171        response = user_views.dashboard(john, request)
    169         assert len(response) == 4
    170         assert 'month_name' in response.keys()
     172        assert len(response) == 6
     173        assert 'month_name' in response.keys()
     174        assert response['current_year'] == datetime.now().year
     175        assert response['current_day_name'] == datetime.now().strftime('%a')
    171176        # this user has a single workout, in 2015
    172177        assert response['viewing_year'] == 2015
     
    176181        request.GET['year'] = 2000
    177182        response = user_views.dashboard(john, request)
    178         assert len(response) == 4
    179         assert 'month_name' in response.keys()
     183        assert len(response) == 6
     184        assert 'month_name' in response.keys()
     185        assert response['current_year'] == datetime.now().year
     186        assert response['current_day_name'] == datetime.now().strftime('%a')
    180187        # this user has a single workout, in 2015
    181188        assert response['viewing_year'] == 2000
     
    194201        request.GET['month'] = 6
    195202        response = user_views.dashboard(john, request)
    196         assert len(response) == 4
    197         assert 'month_name' in response.keys()
     203        assert len(response) == 6
     204        assert 'month_name' in response.keys()
     205        assert response['current_year'] == datetime.now().year
     206        assert response['current_day_name'] == datetime.now().strftime('%a')
    198207        # this user has a single workout, in 2015
    199208        assert response['viewing_year'] == 2015
     
    203212        request.GET['month'] = 2
    204213        response = user_views.dashboard(john, request)
    205         assert len(response) == 4
    206         assert 'month_name' in response.keys()
     214        assert len(response) == 6
     215        assert 'month_name' in response.keys()
     216        assert response['current_year'] == datetime.now().year
     217        assert response['current_day_name'] == datetime.now().strftime('%a')
    207218        # this user has a single workout, in 2015
    208219        assert response['viewing_year'] == 2015
     
    213224        request.GET['month'] = 6
    214225        response = user_views.dashboard(john, request)
    215         assert len(response) == 4
    216         assert 'month_name' in response.keys()
     226        assert len(response) == 6
     227        assert 'month_name' in response.keys()
     228        assert response['current_year'] == datetime.now().year
     229        assert response['current_day_name'] == datetime.now().strftime('%a')
    217230        # this user has a single workout, in 2015
    218231        assert response['viewing_year'] == 2010
     
    229242        request.GET['month'] = 5
    230243        response = user_views.dashboard(john, request)
    231         assert len(response) == 4
    232         assert 'month_name' in response.keys()
     244        assert len(response) == 6
     245        assert 'month_name' in response.keys()
     246        assert response['current_year'] == datetime.now().year
     247        assert response['current_day_name'] == datetime.now().strftime('%a')
    233248        # this user has a single workout, in 2015
    234249        assert response['viewing_year'] == 2015
     
    238253        request.GET['month'] = 6
    239254        response = user_views.dashboard(john, request)
    240         assert len(response) == 4
    241         assert 'month_name' in response.keys()
     255        assert len(response) == 6
     256        assert 'month_name' in response.keys()
     257        assert response['current_year'] == datetime.now().year
     258        assert response['current_day_name'] == datetime.now().strftime('%a')
    242259        # this user has a single workout, in 2015
    243260        assert response['viewing_year'] == 2015
     
    583600        assert 'jack.black@example.net' not in request.root.emails
    584601        assert 'JackBlack' not in request.root.all_nicknames
     602
     603    def test_week_stats_no_stats(self, dummy_request, john):
     604        response = user_views.week_stats(john, dummy_request)
     605        assert isinstance(response, Response)
     606        assert response.content_type == 'application/json'
     607        # the body is a valid json-encoded stream
     608        obj = json.loads(response.body)
     609        assert obj == [
     610            {'distance': 0, 'elevation': 0, 'name': 'Mon',
     611             'time': '00', 'workouts': 0},
     612            {'distance': 0, 'elevation': 0, 'name': 'Tue',
     613             'time': '00', 'workouts': 0},
     614            {'distance': 0, 'elevation': 0, 'name': 'Wed',
     615             'time': '00', 'workouts': 0},
     616            {'distance': 0, 'elevation': 0, 'name': 'Thu',
     617             'time': '00', 'workouts': 0},
     618            {'distance': 0, 'elevation': 0, 'name': 'Fri',
     619             'time': '00', 'workouts': 0},
     620            {'distance': 0, 'elevation': 0, 'name': 'Sat',
     621             'time': '00', 'workouts': 0},
     622            {'distance': 0, 'elevation': 0, 'name': 'Sun',
     623             'time': '00', 'workouts': 0}
     624        ]
     625
     626    def test_week_stats(self, dummy_request, john):
     627        workout = Workout(
     628            start=datetime.now(timezone.utc),
     629            duration=timedelta(minutes=60),
     630            distance=30,
     631            elevation=540
     632        )
     633        john.add_workout(workout)
     634        response = user_views.week_stats(john, dummy_request)
     635        assert isinstance(response, Response)
     636        assert response.content_type == 'application/json'
     637        # the body is a valid json-encoded stream
     638        obj = json.loads(response.body)
     639        assert len(obj) == 7
     640        for day in obj:
     641            if datetime.now(timezone.utc).strftime('%a') == day['name']:
     642                day['distance'] == 30
     643                day['elevation'] == 540
     644                day['time'] == '01'
     645                day['workouts'] == 1
     646            else:
     647                day['distance'] == 0
     648                day['elevation'] == 0
     649                day['time'] == '00'
     650                day['workouts'] == 0
  • ow/utilities.py

    rc6219ed r26220ba  
    33import logging
    44import subprocess
    5 from datetime import datetime
     5from datetime import datetime, timedelta
    66from decimal import Decimal
    77from shutil import copyfileobj
     
    212212
    213213    return False
     214
     215
     216def timedelta_to_hms(value):
     217    """
     218    Return hours, minutes, seconds from a timedelta object
     219    """
     220    hours, remainder = divmod(int(value.total_seconds()), 3600)
     221    minutes, seconds = divmod(remainder, 60)
     222    return hours, minutes, seconds
     223
     224
     225def get_week_days(day, start_day=1):
     226    """
     227    Return a list of datetime objects for the days of the week "day" is in.
     228
     229    day is a datetime object (like in datetime.now() for "today")
     230
     231    start_day can be used to set if week starts on monday (1) or sunday (0)
     232    """
     233    first_day = day - timedelta(days=day.isoweekday() - start_day)
     234    week_days = [first_day + timedelta(days=i) for i in range(7)]
     235    return week_days
  • ow/views/user.py

    rc6219ed r26220ba  
     1import json
    12from calendar import month_name
     3from datetime import datetime
    24
    35from pyramid.httpexceptions import HTTPFound
     
    1719from ..models.root import OpenWorkouts
    1820from ..views.renderers import OWFormRenderer
     21from ..utilities import timedelta_to_hms
    1922
    2023_ = TranslationStringFactory('OpenWorkouts')
     
    143146
    144147    return {
     148        'current_year': datetime.now().year,
     149        'current_day_name': datetime.now().strftime('%a'),
    145150        'month_name': month_name,
    146151        'viewing_year': viewing_year,
     
    223228        return HTTPFound(location=request.resource_url(context, 'profile'))
    224229    return {'form': OWFormRenderer(form)}
     230
     231
     232@view_config(
     233    context=User,
     234    permission='view',
     235    name='week')
     236def week_stats(context, request):
     237    stats = context.week_stats
     238    json_stats = []
     239    for day in stats:
     240        hms = timedelta_to_hms(stats[day]['time'])
     241        day_stats = {
     242            'name': day.strftime('%a'),
     243            'time': str(hms[0]).zfill(2),
     244            'distance': int(round(stats[day]['distance'])),
     245            'elevation': int(stats[day]['elevation']),
     246            'workouts': stats[day]['workouts']
     247        }
     248        json_stats.append(day_stats)
     249    return Response(content_type='application/json',
     250                    charset='utf-8',
     251                    body=json.dumps(json_stats))
  • ow/views/workout.py

    rc6219ed r26220ba  
     1from decimal import Decimal
    12from datetime import datetime, timedelta, time, timezone
    23
     
    121122                                 tzinfo=timezone.utc)
    122123        context.start = start
     124        # ensure distance is a decimal
     125        context.distance = Decimal(context.distance)
    123126        catalog = get_catalog(context)
    124127        reindex_object(catalog, context)
Note: See TracChangeset for help on using the changeset viewer.