Changeset 3357e47 in OpenWorkouts-current


Ignore:
Timestamp:
Feb 4, 2019, 12:38:29 PM (5 years ago)
Author:
Borja Lopez <borja@…>
Branches:
current, feature/docs, master
Children:
6993c72
Parents:
1183d5a (diff), 5cf5787 (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

Location:
ow
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • ow/models/user.py

    r1183d5a r3357e47  
    99
    1010from ow.catalog import get_catalog, reindex_object
    11 from ow.utilities import get_week_days
     11from ow.utilities import get_week_days, get_month_week_number
    1212
    1313
     
    7777        reindex_object(catalog, workout)
    7878
    79     def workouts(self, year=None, month=None):
     79    def workouts(self, year=None, month=None, week=None):
    8080        """
    8181        Return this user workouts, sorted by date, from newer to older
     
    8484        if year:
    8585            workouts = [w for w in workouts if w.start.year == year]
    86         if month:
    87             workouts = [w for w in workouts if w.start.month == month]
     86            if month:
     87                workouts = [w for w in workouts if w.start.month == month]
     88            if week:
     89                week = int(week)
     90                workouts = [
     91                    w for w in workouts if w.start.isocalendar()[1] == week]
    8892        workouts = sorted(workouts, key=attrgetter('start'))
    8993        workouts.reverse()
     
    288292
    289293        return stats
     294
     295    @property
     296    def weekly_year_stats(self):
     297        """
     298        Return per-week stats for the last 12 months
     299        """
     300        # set the boundaries for looking for workouts afterwards,
     301        # we need the current date as the "end date" and one year
     302        # ago from that date. Then we set the start at the first
     303        # day of that month.
     304        end = datetime.now(timezone.utc)
     305        start = (end - timedelta(days=365)).replace(day=1)
     306
     307        stats = {}
     308
     309        # first initialize the stats dict
     310        for days in range((end - start).days):
     311            day = (start + timedelta(days=days)).date()
     312            week = day.isocalendar()[1]
     313            month_week = get_month_week_number(day)
     314            key = (day.year, day.month, week, month_week)
     315            if key not in stats.keys():
     316                stats[key] = {
     317                    'workouts': 0,
     318                    'time': timedelta(0),
     319                    'distance': Decimal(0),
     320                    'elevation': Decimal(0),
     321                    'sports': {}
     322                }
     323
     324        # now loop over the workouts, filtering and then adding stats
     325        # to the proper place
     326        for workout in self.workouts():
     327            if start.date() <= workout.start.date() <= end.date():
     328                # less typing, avoid long lines
     329                start_date = workout.start.date()
     330                week = start_date.isocalendar()[1]
     331                month_week = get_month_week_number(start_date)
     332                week = stats[(start_date.year,
     333                              start_date.month,
     334                              week,
     335                              month_week)]
     336
     337                week['workouts'] += 1
     338                week['time'] += workout.duration or timedelta(seconds=0)
     339                week['distance'] += workout.distance or Decimal(0)
     340                week['elevation'] += workout.uphill or Decimal(0)
     341                if workout.sport not in week['sports']:
     342                    week['sports'][workout.sport] = {
     343                        'workouts': 0,
     344                        'time': timedelta(seconds=0),
     345                        'distance': Decimal(0),
     346                        'elevation': Decimal(0),
     347                    }
     348                week['sports'][workout.sport]['workouts'] += 1
     349                week['sports'][workout.sport]['time'] += (
     350                    workout.duration or timedelta(0))
     351                week['sports'][workout.sport]['distance'] += (
     352                    workout.distance or Decimal(0))
     353                week['sports'][workout.sport]['elevation'] += (
     354                    workout.uphill or Decimal(0))
     355
     356        return stats
  • ow/static/js/ow.js

    r1183d5a r3357e47  
    236236    var chart_selector = spec.chart_selector,
    237237        filters_selector = spec.filters_selector,
    238         url = spec.url,
     238        switcher_selector = spec.switcher_selector,
     239        urls = spec.urls,
    239240        current_month = spec.current_month,
    240         y_axis_labels = spec.y_axis_labels;
     241        current_week = spec.current_week,
     242        y_axis_labels = spec.y_axis_labels,
     243        filter_by = spec.filter_by,
     244        url = spec.url;
    241245
    242246    // Helpers
     
    256260    };
    257261
     262    function get_name_for_x(d) {
     263        if (d.week == undefined || d.week == 0) {
     264            return d.name;
     265        }
     266        else {
     267            return d.id.split('-')[2];
     268        }
     269    }
     270
    258271    // Methods
    259272    var filters_setup = function filters_setup() {
    260273        $(filters_selector).on('click', function(e) {
    261             var filter_by = 'distance';
    262274            e.preventDefault();
    263275            filter_by = $(this).attr('class').split('-')[1]
    264276            var chart = d3.select(chart_selector);
    265277            chart.selectAll("*").remove();
    266             render(filter_by);
    267         });
    268     };
    269 
    270     var render = function render(filter_by) {
     278            render(filter_by, url);
     279        });
     280    };
     281
     282    var switcher_setup = function switcher_setup() {
     283        $(switcher_selector).on('click', function(e) {
     284            e.preventDefault();
     285            url = $(this).attr('class').split('-')[1]
     286            var chart = d3.select(chart_selector);
     287            chart.selectAll("*").remove();
     288            render(filter_by, url);
     289        });
     290    };
     291
     292    var render = function render(filter_by, url) {
    271293        /*
    272294          Build a d3 bar chart, populated with data from the given url.
     
    280302            y = d3.scaleLinear().rangeRound([height, 0]);
    281303
    282         d3.json(url).then(function (data) {
     304        d3.json(urls[url]).then(function (data) {
    283305            x.domain(data.map(function (d) {
    284                 return d.name;
     306                return get_name_for_x(d);
     307                // return d.name;
    285308            }));
    286309
     
    308331                .enter().append("rect")
    309332                .attr("class", function(d) {
    310                     if (d.id == current_month){
     333                    var sel_week = current_month + '-' + current_week;
     334                    if (d.id == current_month || d.id == sel_week){
     335                        /* Bar for the currently selected month or week */
    311336                        select_x_axis_label(d).attr('style', "font-weight: bold;");
    312                         return 'bar current'
     337                        return 'bar current';
    313338                    }
    314339                    else {
    315                         return 'bar'
     340                        if (!current_week && d.id.indexOf(current_month) >=0 ) {
     341                            /*
     342                               User selected a month, then switched to weekly
     343                               view, we do highlight all the bars for weeks in
     344                               that month
     345                            */
     346                            select_x_axis_label(d).attr('style', "font-weight: bold;");
     347                            return 'bar current';
     348                        }
     349                        else {
     350                            /* Non-selected bar */
     351                            return 'bar';
     352                        }
     353
    316354                    }
    317355                })
    318356                .attr("x", function (d) {
    319                     return x(d.name);
     357                    return x(get_name_for_x(d));
    320358                })
    321359                .attr("y", function (d) {
     
    340378                });
    341379
    342             g.selectAll(".text")
    343                 .data(data)
    344                 .enter()
    345                 .append("text")
    346                 .attr("class","label")
    347                 .attr("x", function (d) {
    348                     return x(d.name) + x.bandwidth()/2;
    349                 })
    350                 .attr("y", function (d) {
    351                     /*
    352                       Get the value for the current bar, then get the maximum
    353                       value to be displayed in the bar, which is used to
    354                       calculate the proper position of the label for this bar,
    355                       relatively to its height (1% above the bar)
    356                      */
    357                     var value = get_y_value(d, filter_by);
    358                     var max = y.domain()[1];
    359                     return y(value + y.domain()[1] * 0.01);
    360                 })
    361                 .text(function(d) {
    362                     var value = get_y_value(d, filter_by)
    363                     if ( value > 0) {
    364                         return value;
    365                     }
    366                 });
    367 
     380            if (url == 'monthly') {
     381                g.selectAll(".text")
     382                    .data(data)
     383                    .enter()
     384                    .append("text")
     385                    .attr("class","label")
     386                    .attr("x", function (d) {
     387                        return x(get_name_for_x(d)) + x.bandwidth()/2;
     388                    })
     389                    .attr("y", function (d) {
     390                        /*
     391                          Get the value for the current bar, then get the maximum
     392                          value to be displayed in the bar, which is used to
     393                          calculate the proper position of the label for this bar,
     394                          relatively to its height (1% above the bar)
     395                        */
     396                        var value = get_y_value(d, filter_by);
     397                        var max = y.domain()[1];
     398                        return y(value + y.domain()[1] * 0.01);
     399                    })
     400                    .text(function(d) {
     401                        var value = get_y_value(d, filter_by)
     402                        if ( value > 0) {
     403                            return value;
     404                        }
     405                    });
     406            }
     407
     408            if (url == 'weekly') {
     409                g.selectAll(".tick")
     410                    .each(function (d, i) {
     411                        /*
     412                          Remove from the x-axis tickets those without letters
     413                          on them (useful for the weekly chart)
     414                        */
     415                        if (d !== parseInt(d, 10)) {
     416                            if(!d.match(/[a-z]/i)) {
     417                                this.remove();
     418                            }
     419                        }
     420                    });
     421            }
    368422        });
    369423    };
     
    371425    var that = {}
    372426    that.filters_setup = filters_setup;
     427    that.switcher_setup = switcher_setup;
    373428    that.render = render;
    374429    return that
  • ow/templates/profile.pt

    r1183d5a r3357e47  
    7272          <a href="#" class="js-time" i18n:translate="">time</a>
    7373          <a href="#" class="js-elevation" i18n:translate="">elevation</a>
     74        </div>
     75        <div class="switcher js-switcher">
     76          <a href="#" class="js-weekly" i18n:translate="">weekly</a>
     77          <a href="#" class="js-monthly" i18n:translate="">monthly</a>
    7478        </div>
    7579      </div>
     
    154158         chart_selector: 'div.js-month-stats svg',
    155159         filters_selector: 'div.js-month-stats div.js-filters a',
    156          url: "${request.resource_url(context, 'yearly')}",
     160         switcher_selector: 'div.js-month-stats div.js-switcher a',
     161         urls: {"monthly": "${request.resource_url(context, 'monthly')}",
     162                "weekly": "${request.resource_url(context, 'weekly')}"},
    157163         current_month: "${current_month}",
     164         current_week: "${current_week}",
    158165         y_axis_labels: y_axis_labels,
     166         filter_by: "distance",
     167         url: "${'monthly' if current_week is None else 'weekly'}",
    159168     });
    160      year_chart.render("distance");
     169     year_chart.render("distance", "${'monthly' if current_week is None else 'weekly'}");
    161170     year_chart.filters_setup();
     171     year_chart.switcher_setup();
    162172    </script>
    163173
  • ow/tests/views/test_user.py

    r1183d5a r3357e47  
    269269        # profile page for the current day (no workouts avalable)
    270270        response = user_views.profile(john, request)
    271         assert len(response.keys()) == 2
     271        assert len(response.keys()) == 3
    272272        current_month = datetime.now(timezone.utc).strftime('%Y-%m')
    273273        assert response['current_month'] == current_month
     274        assert response['current_week'] is None
    274275        assert response['workouts'] == []
    275276        # profile page for a previous date, that has workouts
     
    277278        request.GET['month'] = 8
    278279        response = user_views.profile(john, request)
    279         assert len(response.keys()) == 2
     280        assert len(response.keys()) == 3
    280281        assert response['current_month'] == '2015-08'
     282        assert response['current_week'] is None
     283        assert response['workouts'] == john.workouts(2015, 8)
     284        # same, passing a week, first on a week without workouts
     285        request.GET['year'] = 2015
     286        request.GET['month'] = 8
     287        request.GET['week'] = 25
     288        response = user_views.profile(john, request)
     289        assert len(response.keys()) == 3
     290        assert response['current_month'] == '2015-08'
     291        assert response['current_week'] is 25
     292        assert response['workouts'] == []
     293        # now in a week with workoutss
     294        request.GET['year'] = 2015
     295        request.GET['month'] = 8
     296        request.GET['week'] = 26
     297        response = user_views.profile(john, request)
     298        assert len(response.keys()) == 3
     299        assert response['current_month'] == '2015-08'
     300        assert response['current_week'] is 26
    281301        assert response['workouts'] == john.workouts(2015, 8)
    282302
  • ow/utilities.py

    r1183d5a r3357e47  
    22import os
    33import logging
     4import calendar
    45import subprocess
    56from datetime import datetime, timedelta
     
    234235    week_days = [first_day + timedelta(days=i) for i in range(7)]
    235236    return week_days
     237
     238
     239def get_month_week_number(day):
     240    """
     241    Given a datetime object (day), return the number of week the day is
     242    in the current month (week 1, 2, 3, etc)
     243    """
     244    weeks = calendar.monthcalendar(day.year, day.month)
     245    for week in weeks:
     246        if day.day in week:
     247            return weeks.index(week)
     248    return None
  • ow/views/user.py

    r1183d5a r3357e47  
    168168    year = int(request.GET.get('year', now.year))
    169169    month = int(request.GET.get('month', now.month))
     170    week = request.GET.get('week', None)
    170171    return {
    171         'workouts': context.workouts(year, month),
     172        'workouts': context.workouts(year, month, week),
    172173        'current_month': '{year}-{month}'.format(
    173             year=str(year), month=str(month).zfill(2))
     174            year=str(year), month=str(month).zfill(2)),
     175        'current_week': week
    174176    }
    175177
     
    262264    context=User,
    263265    permission='view',
    264     name='yearly')
     266    name='monthly')
    265267def last_months_stats(context, request):
    266268    """
     
    294296                    charset='utf-8',
    295297                    body=json.dumps(json_stats))
     298
     299
     300@view_config(
     301    context=User,
     302    permission='view',
     303    name='weekly')
     304def last_weeks_stats(context, request):
     305    """
     306    Return a json-encoded stream with statistics for the last 12-months, but
     307    in a per-week basis
     308    """
     309    stats = context.weekly_year_stats
     310    # this sets which month is 2 times in the stats, once this year, once
     311    # the previous year. We will show it a bit different in the UI (showing
     312    # the year too to prevent confusion)
     313    repeated_month = datetime.now(timezone.utc).date().month
     314    json_stats = []
     315    for week in stats:
     316        hms = timedelta_to_hms(stats[week]['time'])
     317        name = month_name[week[1]][:3]
     318        if week[1] == repeated_month:
     319            name += ' ' + str(week[0])
     320        week_stats = {
     321            'id': '-'.join(
     322                [str(week[0]), str(week[1]).zfill(2), str(week[2])]),
     323            'week': str(week[3]),  # the number of week in the current month
     324            'name': name,
     325            'time': str(hms[0]).zfill(2),
     326            'distance': int(round(stats[week]['distance'])),
     327            'elevation': int(stats[week]['elevation']),
     328            'workouts': stats[week]['workouts'],
     329            'url': request.resource_url(
     330                context, 'profile',
     331                query={'year': str(week[0]),
     332                       'month': str(week[1]),
     333                       'week': str(week[2])},
     334                anchor='workouts')
     335        }
     336        json_stats.append(week_stats)
     337    return Response(content_type='application/json',
     338                    charset='utf-8',
     339                    body=json.dumps(json_stats))
Note: See TracChangeset for help on using the changeset viewer.