Changeset 22eb5de in OpenWorkouts-current


Ignore:
Timestamp:
Jan 29, 2019, 12:58:08 PM (5 years ago)
Author:
Segundo Fdez <segun.2@…>
Branches:
current, feature/docs, master
Children:
594fbe8
Parents:
3e48af6 (diff), ed7e9d7 (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 branch 'master' into feature/ui

# Conflicts:
# ow/static/js/ow.js

Files:
7 edited

Legend:

Unmodified
Added
Removed
  • bin/install

    r3e48af6 r22eb5de  
    1010# Full path to the env
    1111env_path=${current}/env
     12
     13
     14set_scripts_permissions() {
     15    # ensure the shell scripts we will need have proper permissions
     16    chmod u+x ${current}/bin/js_deps
     17    chmod u+x ${current}/bin/start
     18    chmod u+x ${current}/bin/screenshot_map
     19}
    1220
    1321check_python3() {
     
    5563
    5664install_js_deps() {
    57     chmod +x ${current}/bin/js_deps
    5865    ${current}/bin/js_deps
    5966}
    6067
    6168setup_start_stop() {
    62     chmod +x ${current}/bin/start
    6369    echo "OpenWorkouts successfully installed in ${env_path}"
    6470    echo ""
     
    7379}
    7480
     81set_script_permissions
    7582check_python3
    7683create_venv
  • ow/models/user.py

    r3e48af6 r22eb5de  
    234234                        timedelta())
    235235        }
     236
     237    @property
     238    def yearly_stats(self):
     239        """
     240        Return per-month stats for the last 12 months
     241        """
     242        # set the boundaries for looking for workouts afterwards,
     243        # we need the current date as the "end date" and one year
     244        # ago from that date. Then we set the start at the first
     245        # day of that month.
     246        end = datetime.now(timezone.utc)
     247        start = (end - timedelta(days=365)).replace(day=1)
     248
     249        # build the stats, populating first the dict with empty values
     250        # for each month.
     251        stats = {}
     252        for days in range((end - start).days):
     253            day = (start + timedelta(days=days)).date()
     254            if (day.year, day.month) not in stats.keys():
     255                stats[(day.year, day.month)] = {
     256                    'workouts': 0,
     257                    'time': timedelta(0),
     258                    'distance': Decimal(0),
     259                    'elevation': Decimal(0),
     260                    'sports': {}
     261                }
     262
     263        # now loop over workouts, filtering and then adding stats to the
     264        # proper place
     265        for workout in self.workouts():
     266            if start.date() <= workout.start.date() <= end.date():
     267                # less typing, avoid long lines
     268                month = stats[
     269                    (workout.start.date().year, workout.start.date().month)]
     270                month['workouts'] += 1
     271                month['time'] += workout.duration or timedelta(seconds=0)
     272                month['distance'] += workout.distance or Decimal(0)
     273                month['elevation'] += workout.uphill or Decimal(0)
     274                if workout.sport not in month['sports']:
     275                    month['sports'][workout.sport] = {
     276                        'workouts': 0,
     277                        'time': timedelta(seconds=0),
     278                        'distance': Decimal(0),
     279                        'elevation': Decimal(0),
     280                    }
     281                month['sports'][workout.sport]['workouts'] += 1
     282                month['sports'][workout.sport]['time'] += (
     283                    workout.duration or timedelta(0))
     284                month['sports'][workout.sport]['distance'] += (
     285                    workout.distance or Decimal(0))
     286                month['sports'][workout.sport]['elevation'] += (
     287                    workout.uphill or Decimal(0))
     288
     289        return stats
  • ow/static/js/ow.js

    r3e48af6 r22eb5de  
    3131    var openstreetmap_attr = 'Map data &copy; <a href="http://www.osm.org">OpenStreetMap</a>';
    3232
    33     // Some constants reused through the code
     33    // Some vars reused through the code
    3434    var map;
    3535    var gpx;
     
    147147           Build a d3 bar chart, populated with data from the given url.
    148148         */
    149         var chart = d3.select("svg"),
     149        var chart = d3.select(chart_selector),
    150150            margin = {top: 17, right: 0, bottom: 20, left: 0},
     151
    151152            width = +chart.attr("width") - margin.left - margin.right,
    152153            height = +chart.attr("height") - margin.top - margin.bottom,
     
    234235
    235236};
     237
     238
     239owjs.year_chart = function(spec) {
     240
     241    "use strict";
     242
     243    // parameters provided when creating an "instance" of the chart
     244    var chart_selector = spec.chart_selector,
     245        filters_selector = spec.filters_selector,
     246        url = spec.url,
     247        current_month = spec.current_month,
     248        y_axis_labels = spec.y_axis_labels;
     249
     250    // Helpers
     251    function select_x_axis_label(d) {
     252        /* Given a value, return the label associated with it */
     253        return d3.select('.x-axis-b')
     254            .selectAll('text')
     255            .filter(function(x) { return x == d.name; });
     256    };
     257
     258    function get_y_value(d, filter_by) {
     259        return Number(d[filter_by]);
     260    };
     261
     262    function get_y_axis_label(filter_by) {
     263        return y_axis_labels[filter_by];
     264    };
     265
     266    // Methods
     267    var filters_setup = function filters_setup() {
     268        $(filters_selector).on('click', function(e) {
     269            var filter_by = 'distance';
     270            e.preventDefault();
     271            filter_by = $(this).attr('class').split('-')[1]
     272            var chart = d3.select(chart_selector);
     273            chart.selectAll("*").remove();
     274            render(filter_by);
     275        });
     276    };
     277
     278    var render = function render(filter_by) {
     279        /*
     280          Build a d3 bar chart, populated with data from the given url.
     281        */
     282        var chart = d3.select(chart_selector),
     283            margin = {top: 20, right: 20, bottom: 30, left: 50},
     284            width = +chart.attr("width") - margin.left - margin.right,
     285            height = +chart.attr("height") - margin.top - margin.bottom,
     286            g = chart.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"),
     287            x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
     288            y = d3.scaleLinear().rangeRound([height, 0]);
     289
     290        d3.json(url).then(function (data) {
     291            x.domain(data.map(function (d) {
     292                return d.name;
     293            }));
     294
     295            y.domain([0, d3.max(data, function (d) {
     296                return get_y_value(d, filter_by);
     297            })]);
     298
     299            g.append("g")
     300                .attr('class', 'x-axis-b')
     301                .attr("transform", "translate(0," + height + ")")
     302                .call(d3.axisBottom(x))
     303
     304            g.append("g")
     305                .call(d3.axisLeft(y))
     306                .append("text")
     307                .attr("fill", "#000")
     308                .attr("transform", "rotate(-90)")
     309                .attr("y", 6)
     310                .attr("dy", "0.71em")
     311                .attr("text-anchor", "end")
     312                .text(get_y_axis_label(filter_by));
     313
     314            g.selectAll(".bar")
     315                .data(data)
     316                .enter().append("rect")
     317                .attr("class", function(d) {
     318                    if (d.id == current_month){
     319                        select_x_axis_label(d).attr('style', "font-weight: bold;");
     320                        return 'bar current'
     321                    }
     322                    else {
     323                        return 'bar'
     324                    }
     325                })
     326                .attr("x", function (d) {
     327                    return x(d.name);
     328                })
     329                .attr("y", function (d) {
     330                    return y(get_y_value(d, filter_by));
     331                })
     332                .attr("width", x.bandwidth())
     333                .attr("height", function (d) {
     334                    return height - y(get_y_value(d, filter_by));
     335                })
     336                .on('mouseover', function(d) {
     337                    if (d.id != current_month){
     338                        select_x_axis_label(d).attr('style', "font-weight: bold;");
     339                    }
     340                })
     341                .on('mouseout', function(d) {
     342                    if (d.id != current_month){
     343                        select_x_axis_label(d).attr('style', "font-weight: regular;");
     344                    }
     345                });
     346
     347            g.selectAll(".text")
     348                .data(data)
     349                .enter()
     350                .append("text")
     351                .attr("class","label")
     352                .attr("x", function (d) {
     353                    return x(d.name) + x.bandwidth()/2;
     354                })
     355                .attr("y", function (d) {
     356                    /*
     357                      Get the value for the current bar, then get the maximum
     358                      value to be displayed in the bar, which is used to
     359                      calculate the proper position of the label for this bar,
     360                      relatively to its height (1% above the bar)
     361                     */
     362                    var value = get_y_value(d, filter_by);
     363                    var max = y.domain()[1];
     364                    return y(value + y.domain()[1] * 0.01);
     365                })
     366                .text(function(d) {
     367                    var value = get_y_value(d, filter_by)
     368                    if ( value > 0) {
     369                        return value;
     370                    }
     371                });
     372
     373        });
     374    };
     375
     376    var that = {}
     377    that.filters_setup = filters_setup;
     378    that.render = render;
     379    return that
     380
     381};
  • ow/templates/dashboard.pt

    r3e48af6 r22eb5de  
    204204    <script type="text/javascript">
    205205     var week_chart = owjs.week_chart({
    206          chart_selector: 'div.js-week-stats',
     206         chart_selector: 'div.js-week-stats svg',
    207207         url: "${request.resource_url(context, 'week')}",
    208208         current_day_name: "${current_day_name}"
  • ow/templates/profile.pt

    r3e48af6 r22eb5de  
    8181      </div>
    8282
     83      <div class="month-stats js-month-stats">
     84        <svg width="600" height="300"></svg>
     85        <div class="filters js-filters">
     86          <a href="#" class="js-distance" i18n:translate="">distance</a>
     87          <a href="#" class="js-time" i18n:translate="">time</a>
     88          <a href="#" class="js-elevation" i18n:translate="">elevation</a>
     89        </div>
     90      </div>
     91
    8392    </div>
    8493
    8594  </metal:content>
    8695
     96  <metal:body-js metal:fill-slot="body-js">
     97
     98    <script src="${request.static_url('ow:static/components/d3/d3.min.js')}"></script>
     99    <script src="${request.static_url('ow:static/js/ow.js')}"></script>
     100
     101    <script type="text/javascript">
     102     var y_axis_labels = {
     103         "distance": "Kilometers",
     104         "time": "Hours",
     105         "elevation": "Meters"
     106     };
     107
     108     var year_chart = owjs.year_chart({
     109         chart_selector: 'div.js-month-stats svg',
     110         filters_selector: 'div.js-month-stats div.js-filters a',
     111         url: "${request.resource_url(context, 'yearly')}",
     112         current_month: "${current_month}",
     113         y_axis_labels: y_axis_labels,
     114     });
     115     year_chart.render("distance");
     116     year_chart.filters_setup();
     117    </script>
     118
     119  </metal:body-js>
     120
    87121</html>
  • ow/tests/views/test_user.py

    r3e48af6 r22eb5de  
    268268        request = dummy_request
    269269        response = user_views.profile(john, request)
    270         assert response == {}
     270        assert len(response.keys()) == 1
     271        current_month = datetime.now(timezone.utc).strftime('%Y-%m')
     272        assert response['current_month'] == current_month
    271273
    272274    def test_login_get(self, dummy_request):
  • ow/views/user.py

    r3e48af6 r22eb5de  
    11import json
    22from calendar import month_name
    3 from datetime import datetime
     3from datetime import datetime, timezone
    44
    55from pyramid.httpexceptions import HTTPFound
     
    146146
    147147    return {
    148         'current_year': datetime.now().year,
    149         'current_day_name': datetime.now().strftime('%a'),
     148        'current_year': datetime.now(timezone.utc).year,
     149        'current_day_name': datetime.now(timezone.utc).strftime('%a'),
    150150        'month_name': month_name,
    151151        'viewing_year': viewing_year,
     
    165165    basic info, stats, etc
    166166    """
    167     return {}
     167    now = datetime.now(timezone.utc)
     168    return {
     169        'current_month': now.strftime('%Y-%m')
     170    }
    168171
    169172
     
    250253                    charset='utf-8',
    251254                    body=json.dumps(json_stats))
     255
     256
     257@view_config(
     258    context=User,
     259    permission='view',
     260    name='yearly')
     261def last_months_stats(context, request):
     262    """
     263    Return a json-encoded stream with statistics for the last 12 months
     264    """
     265    stats = context.yearly_stats
     266    # this sets which month is 2 times in the stats, once this year, once
     267    # the previous year. We will show it a bit different in the UI (showing
     268    # the year too to prevent confusion)
     269    repeated_month = datetime.now(timezone.utc).date().month
     270    json_stats = []
     271    for month in stats:
     272        hms = timedelta_to_hms(stats[month]['time'])
     273        name = month_name[month[1]][:3]
     274        if month[1] == repeated_month:
     275            name += ' ' + str(month[0])
     276        month_stats = {
     277            'id': str(month[0]) + '-' + str(month[1]).zfill(2),
     278            'name': name,
     279            'time': str(hms[0]).zfill(2),
     280            'distance': int(round(stats[month]['distance'])),
     281            'elevation': int(stats[month]['elevation']),
     282            'workouts': stats[month]['workouts']
     283        }
     284        json_stats.append(month_stats)
     285    return Response(content_type='application/json',
     286                    charset='utf-8',
     287                    body=json.dumps(json_stats))
Note: See TracChangeset for help on using the changeset viewer.