Changeset ed7e9d7 in OpenWorkouts-current


Ignore:
Timestamp:
Jan 29, 2019, 12:41:01 PM (4 years ago)
Author:
Borja Lopez <borja@…>
Branches:
current, feature/docs, master
Children:
1183d5a, 22eb5de
Parents:
26220ba (diff), bd8eeb4 (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:

Merged patches from darcs

Files:
7 edited

Legend:

Unmodified
Added
Removed
  • bin/install

    r26220ba red7e9d7  
    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

    r26220ba red7e9d7  
    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

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

    r26220ba red7e9d7  
    211211    <script type="text/javascript">
    212212     var week_chart = owjs.week_chart({
    213          chart_selector: 'div.js-week-stats',
     213         chart_selector: 'div.js-week-stats svg',
    214214         url: "${request.resource_url(context, 'week')}",
    215215         current_day_name: "${current_day_name}"
  • ow/templates/profile.pt

    r26220ba red7e9d7  
    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

    r26220ba red7e9d7  
    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

    r26220ba red7e9d7  
    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.