|  | @@ -0,0 +1,321 @@
 | 
	
		
			
				|  |  | +import datetime
 | 
	
		
			
				|  |  | +import json
 | 
	
		
			
				|  |  | +from urllib.error import HTTPError, URLError
 | 
	
		
			
				|  |  | +from django import forms
 | 
	
		
			
				|  |  | +from django.core.urlresolvers import reverse
 | 
	
		
			
				|  |  | +from django.forms import Widget
 | 
	
		
			
				|  |  | +import urllib
 | 
	
		
			
				|  |  | +from django.utils import formats
 | 
	
		
			
				|  |  | +from django.utils.html import format_html
 | 
	
		
			
				|  |  | +from django.utils.safestring import mark_safe
 | 
	
		
			
				|  |  | +from jet.modules import DashboardModule
 | 
	
		
			
				|  |  | +from django.utils.translation import ugettext_lazy as _
 | 
	
		
			
				|  |  | +from django.conf import settings
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +JET_MODULE_YANDEX_METRIKA_CLIENT_ID = getattr(settings, 'JET_MODULE_YANDEX_METRIKA_CLIENT_ID', '')
 | 
	
		
			
				|  |  | +JET_MODULE_YANDEX_METRIKA_CLIENT_SECRET = getattr(settings, 'JET_MODULE_YANDEX_METRIKA_CLIENT_SECRET', '')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class YandexMetrikaClient:
 | 
	
		
			
				|  |  | +    OAUTH_BASE_URL = 'https://oauth.yandex.ru/'
 | 
	
		
			
				|  |  | +    API_BASE_URL = 'https://api-metrika.yandex.ru/'
 | 
	
		
			
				|  |  | +    CLIENT_ID = JET_MODULE_YANDEX_METRIKA_CLIENT_ID
 | 
	
		
			
				|  |  | +    CLIENT_SECRET = JET_MODULE_YANDEX_METRIKA_CLIENT_SECRET
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __init__(self, access_token=None):
 | 
	
		
			
				|  |  | +        self.access_token = access_token
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def request(self, base_url, url, data=None, headers=None):
 | 
	
		
			
				|  |  | +        url = '%s%s' % (base_url, url)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if data is not None:
 | 
	
		
			
				|  |  | +            data = urllib.parse.urlencode(data).encode()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if headers is None:
 | 
	
		
			
				|  |  | +            headers = {}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        req = urllib.request.Request(url, data, headers)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        try:
 | 
	
		
			
				|  |  | +            f = urllib.request.urlopen(req)
 | 
	
		
			
				|  |  | +            result = f.read().decode()
 | 
	
		
			
				|  |  | +            result = json.loads(result)
 | 
	
		
			
				|  |  | +        except URLError as e:
 | 
	
		
			
				|  |  | +            return None, e
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return result, None
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def get_oauth_authorize_url(self, state=''):
 | 
	
		
			
				|  |  | +        return '%sauthorize' \
 | 
	
		
			
				|  |  | +               '?response_type=code' \
 | 
	
		
			
				|  |  | +               '&state=%s' \
 | 
	
		
			
				|  |  | +               '&client_id=%s' % (self.OAUTH_BASE_URL, state, self.CLIENT_ID)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def oauth_request(self, url, data=None):
 | 
	
		
			
				|  |  | +        return self.request(self.OAUTH_BASE_URL, url, data)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def oath_token_request(self, code):
 | 
	
		
			
				|  |  | +        data = {
 | 
	
		
			
				|  |  | +            'grant_type': 'authorization_code',
 | 
	
		
			
				|  |  | +            'code': code,
 | 
	
		
			
				|  |  | +            'client_id': self.CLIENT_ID,
 | 
	
		
			
				|  |  | +            'client_secret': self.CLIENT_SECRET
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        return self.oauth_request('token', data)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def api_request(self, url, data=None):
 | 
	
		
			
				|  |  | +        headers = None
 | 
	
		
			
				|  |  | +        if self.access_token is not None:
 | 
	
		
			
				|  |  | +            headers = {'Authorization': 'OAuth %s' % self.access_token}
 | 
	
		
			
				|  |  | +        return self.request(self.API_BASE_URL, url, data, headers)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def api_counters_request(self):
 | 
	
		
			
				|  |  | +        return self.api_request('counters.json')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def api_stat_traffic_summary(self, counter, date1, date2, group=None):
 | 
	
		
			
				|  |  | +        if group is None:
 | 
	
		
			
				|  |  | +            group = 'day'
 | 
	
		
			
				|  |  | +        return self.api_request('stat/traffic/summary.json?id=%s&date1=%s&date2=%s&group=%s' % (
 | 
	
		
			
				|  |  | +            counter,
 | 
	
		
			
				|  |  | +            date1.strftime('%Y%m%d'),
 | 
	
		
			
				|  |  | +            date2.strftime('%Y%m%d'),
 | 
	
		
			
				|  |  | +            group
 | 
	
		
			
				|  |  | +        ))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class AccessTokenWidget(Widget):
 | 
	
		
			
				|  |  | +    module = None
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def render(self, name, value, attrs=None):
 | 
	
		
			
				|  |  | +        if value and len(value) > 0:
 | 
	
		
			
				|  |  | +            link = '<a href="%s">Revoke access</a>' % reverse('jet:yandex-metrika-revoke', kwargs={'pk': self.module.model.pk})
 | 
	
		
			
				|  |  | +        else:
 | 
	
		
			
				|  |  | +            link = '<a href="%s">Grant access</a>' % reverse('jet:yandex-metrika-grant', kwargs={'pk': self.module.model.pk})
 | 
	
		
			
				|  |  | +        return format_html('%s<input type="hidden" name="access_token" value="%s">' % (link, value))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class YandexMetrikaSettingsForm(forms.Form):
 | 
	
		
			
				|  |  | +    access_token = forms.CharField(label=_('Token'), widget=AccessTokenWidget)
 | 
	
		
			
				|  |  | +    counter = forms.ChoiceField(label=_('Counter'))
 | 
	
		
			
				|  |  | +    period = forms.ChoiceField(label=_('Statistics period'), choices=(
 | 
	
		
			
				|  |  | +        (0, _('Today')),
 | 
	
		
			
				|  |  | +        (6, _('Last week')),
 | 
	
		
			
				|  |  | +        (30, _('Last month')),
 | 
	
		
			
				|  |  | +        (31 * 3 - 1, _('Last quarter')),
 | 
	
		
			
				|  |  | +        (364, _('Last year')),
 | 
	
		
			
				|  |  | +    ))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def set_module(self, module):
 | 
	
		
			
				|  |  | +        self.fields['access_token'].widget.module = module
 | 
	
		
			
				|  |  | +        self.set_counter_choices(module)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def set_counter_choices(self, module):
 | 
	
		
			
				|  |  | +        counters = module.counters()
 | 
	
		
			
				|  |  | +        if counters is not None:
 | 
	
		
			
				|  |  | +            self.fields['counter'].choices = (('', _('-- none --')),)
 | 
	
		
			
				|  |  | +            self.fields['counter'].choices.extend(map(lambda x: (x['id'], x['site']), counters))
 | 
	
		
			
				|  |  | +        else:
 | 
	
		
			
				|  |  | +            label = _('grant access first') if module.access_token is None else _('counters loading failed')
 | 
	
		
			
				|  |  | +            self.fields['counter'].choices = (('', '-- %s -- ' % label),)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class YandexMetrikaChartSettingsForm(YandexMetrikaSettingsForm):
 | 
	
		
			
				|  |  | +    show = forms.ChoiceField(label=_('Show'), choices=(
 | 
	
		
			
				|  |  | +        ('visitors', _('Visitors')),
 | 
	
		
			
				|  |  | +        ('visits', _('Visits')),
 | 
	
		
			
				|  |  | +        ('page_views', _('Views')),
 | 
	
		
			
				|  |  | +    ))
 | 
	
		
			
				|  |  | +    group = forms.ChoiceField(label=_('Group'), choices=(
 | 
	
		
			
				|  |  | +        ('day', _('By day')),
 | 
	
		
			
				|  |  | +        ('week', _('By week')),
 | 
	
		
			
				|  |  | +        ('month', _('By month')),
 | 
	
		
			
				|  |  | +    ))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class YandexMetrikaListSettingsForm(YandexMetrikaSettingsForm):
 | 
	
		
			
				|  |  | +    group = forms.ChoiceField(label=_('Group'), choices=(
 | 
	
		
			
				|  |  | +        ('day', _('By day')),
 | 
	
		
			
				|  |  | +        ('week', _('By week')),
 | 
	
		
			
				|  |  | +        ('month', _('By month')),
 | 
	
		
			
				|  |  | +    ))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class YandexMetrika(DashboardModule):
 | 
	
		
			
				|  |  | +    settings_form = YandexMetrikaSettingsForm
 | 
	
		
			
				|  |  | +    ajax_load = True
 | 
	
		
			
				|  |  | +    contrast = True
 | 
	
		
			
				|  |  | +    period = None
 | 
	
		
			
				|  |  | +    access_token = None
 | 
	
		
			
				|  |  | +    expires_in = None
 | 
	
		
			
				|  |  | +    token_type = None
 | 
	
		
			
				|  |  | +    counter = None
 | 
	
		
			
				|  |  | +    error = None
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    class Media:
 | 
	
		
			
				|  |  | +        js = ('jet/vendor/chart.js/Chart.min.js', 'jet/modules/yandex_metrika.js')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def __init__(self, title=None, period=None, **kwargs):
 | 
	
		
			
				|  |  | +        kwargs.update({'period': period})
 | 
	
		
			
				|  |  | +        super(YandexMetrika, self).__init__(title, **kwargs)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def settings_dict(self):
 | 
	
		
			
				|  |  | +        return {
 | 
	
		
			
				|  |  | +            'period': self.period,
 | 
	
		
			
				|  |  | +            'access_token': self.access_token,
 | 
	
		
			
				|  |  | +            'expires_in': self.expires_in,
 | 
	
		
			
				|  |  | +            'token_type': self.token_type,
 | 
	
		
			
				|  |  | +            'counter': self.counter
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def load_settings(self, settings):
 | 
	
		
			
				|  |  | +        try:
 | 
	
		
			
				|  |  | +            self.period = int(settings.get('period'))
 | 
	
		
			
				|  |  | +        except TypeError:
 | 
	
		
			
				|  |  | +            self.period = 0
 | 
	
		
			
				|  |  | +        self.access_token = settings.get('access_token')
 | 
	
		
			
				|  |  | +        self.expires_in = settings.get('expires_in')
 | 
	
		
			
				|  |  | +        self.token_type = settings.get('token_type')
 | 
	
		
			
				|  |  | +        self.counter = settings.get('counter')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def init_with_context(self, context):
 | 
	
		
			
				|  |  | +        raise NotImplementedError('subclasses of YandexMetrika must provide a init_with_context() method')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def counters(self):
 | 
	
		
			
				|  |  | +        client = YandexMetrikaClient(self.access_token)
 | 
	
		
			
				|  |  | +        counters, exception = client.api_counters_request()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if counters is not None:
 | 
	
		
			
				|  |  | +            return counters['counters']
 | 
	
		
			
				|  |  | +        else:
 | 
	
		
			
				|  |  | +            return None
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def format_grouped_date(self, date, group):
 | 
	
		
			
				|  |  | +        if group == 'week':
 | 
	
		
			
				|  |  | +            date = '%s — %s' % (
 | 
	
		
			
				|  |  | +                (date - datetime.timedelta(days=7)).strftime('%d.%m'),
 | 
	
		
			
				|  |  | +                date.strftime('%d.%m')
 | 
	
		
			
				|  |  | +            )
 | 
	
		
			
				|  |  | +        elif group == 'month':
 | 
	
		
			
				|  |  | +            date = date.strftime('%b, %Y')
 | 
	
		
			
				|  |  | +        else:
 | 
	
		
			
				|  |  | +            date = formats.date_format(date, 'DATE_FORMAT')
 | 
	
		
			
				|  |  | +        return date
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class YandexMetrikaVisitorsTotals(YandexMetrika):
 | 
	
		
			
				|  |  | +    title = _('Yandex Metrika visitors totals')
 | 
	
		
			
				|  |  | +    template = 'jet/dashboard/modules/yandex_metrika_visitors_totals.html'
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def init_with_context(self, context):
 | 
	
		
			
				|  |  | +        if self.access_token is None:
 | 
	
		
			
				|  |  | +            self.error = mark_safe(_('Please <a href="%s">attach Yandex account and choose Yandex Metrika counter</a> to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk}))
 | 
	
		
			
				|  |  | +        elif self.counter is None:
 | 
	
		
			
				|  |  | +            self.error = mark_safe(_('Please <a href="%s">select Yandex Metrika counter</a> to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk}))
 | 
	
		
			
				|  |  | +        else:
 | 
	
		
			
				|  |  | +            date1 = datetime.datetime.utcnow() - datetime.timedelta(days=self.period)
 | 
	
		
			
				|  |  | +            date2 = datetime.datetime.utcnow()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            client = YandexMetrikaClient(self.access_token)
 | 
	
		
			
				|  |  | +            result, exception = client.api_stat_traffic_summary(self.counter, date1, date2)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if exception is not None:
 | 
	
		
			
				|  |  | +                error = _('API request failed.')
 | 
	
		
			
				|  |  | +                if isinstance(exception, HTTPError) and exception.code == 403:
 | 
	
		
			
				|  |  | +                    error += _(' Try to <a href="%s">revoke and grant access</a> again') % reverse('jet:update_module', kwargs={'pk': self.model.pk})
 | 
	
		
			
				|  |  | +                self.error = mark_safe(error)
 | 
	
		
			
				|  |  | +            else:
 | 
	
		
			
				|  |  | +                try:
 | 
	
		
			
				|  |  | +                    self.children.append({'title': _('visitors'), 'value': result['totals']['visitors']})
 | 
	
		
			
				|  |  | +                    self.children.append({'title': _('visits'), 'value': result['totals']['visits']})
 | 
	
		
			
				|  |  | +                    self.children.append({'title': _('views'), 'value': result['totals']['page_views']})
 | 
	
		
			
				|  |  | +                except KeyError:
 | 
	
		
			
				|  |  | +                    self.error = _('Bad server response')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class YandexMetrikaVisitorsChart(YandexMetrika):
 | 
	
		
			
				|  |  | +    title = _('Yandex Metrika visitors chart')
 | 
	
		
			
				|  |  | +    template = 'jet/dashboard/modules/yandex_metrika_visitors_chart.html'
 | 
	
		
			
				|  |  | +    style = 'overflow-x: auto;'
 | 
	
		
			
				|  |  | +    show = None
 | 
	
		
			
				|  |  | +    group = None
 | 
	
		
			
				|  |  | +    settings_form = YandexMetrikaChartSettingsForm
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def settings_dict(self):
 | 
	
		
			
				|  |  | +        settings = super(YandexMetrikaVisitorsChart, self).settings_dict()
 | 
	
		
			
				|  |  | +        settings['show'] = self.show
 | 
	
		
			
				|  |  | +        settings['group'] = self.group
 | 
	
		
			
				|  |  | +        return settings
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def load_settings(self, settings):
 | 
	
		
			
				|  |  | +        super(YandexMetrikaVisitorsChart, self).load_settings(settings)
 | 
	
		
			
				|  |  | +        self.show = settings.get('show')
 | 
	
		
			
				|  |  | +        self.group = settings.get('group')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def init_with_context(self, context):
 | 
	
		
			
				|  |  | +        if self.access_token is None:
 | 
	
		
			
				|  |  | +            self.error = mark_safe(_('Please <a href="%s">attach Yandex account and choose Yandex Metrika counter</a> to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk}))
 | 
	
		
			
				|  |  | +        elif self.counter is None:
 | 
	
		
			
				|  |  | +            self.error = mark_safe(_('Please <a href="%s">select Yandex Metrika counter</a> to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk}))
 | 
	
		
			
				|  |  | +        else:
 | 
	
		
			
				|  |  | +            date1 = datetime.datetime.utcnow() - datetime.timedelta(days=self.period)
 | 
	
		
			
				|  |  | +            date2 = datetime.datetime.utcnow()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            client = YandexMetrikaClient(self.access_token)
 | 
	
		
			
				|  |  | +            result, exception = client.api_stat_traffic_summary(self.counter, date1, date2, self.group)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if exception is not None:
 | 
	
		
			
				|  |  | +                error = _('API request failed.')
 | 
	
		
			
				|  |  | +                if isinstance(exception, HTTPError) and exception.code == 403:
 | 
	
		
			
				|  |  | +                    error += _(' Try to <a href="%s">revoke and grant access</a> again') % reverse('jet:update_module', kwargs={'pk': self.model.pk})
 | 
	
		
			
				|  |  | +                self.error = mark_safe(error)
 | 
	
		
			
				|  |  | +            else:
 | 
	
		
			
				|  |  | +                try:
 | 
	
		
			
				|  |  | +                    for data in result['data']:
 | 
	
		
			
				|  |  | +                        date = datetime.datetime.strptime(data['date'], '%Y%m%d')
 | 
	
		
			
				|  |  | +                        key = self.show if self.show is not None else 'visitors'
 | 
	
		
			
				|  |  | +                        self.children.append((date, data[key]))
 | 
	
		
			
				|  |  | +                except KeyError:
 | 
	
		
			
				|  |  | +                    self.error = _('Bad server response')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class YandexMetrikaPeriodVisitors(YandexMetrika):
 | 
	
		
			
				|  |  | +    title = _('Yandex Metrika period visitors')
 | 
	
		
			
				|  |  | +    template = 'jet/dashboard/modules/yandex_metrika_period_visitors.html'
 | 
	
		
			
				|  |  | +    group = None
 | 
	
		
			
				|  |  | +    contrast = False
 | 
	
		
			
				|  |  | +    settings_form = YandexMetrikaListSettingsForm
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def settings_dict(self):
 | 
	
		
			
				|  |  | +        settings = super(YandexMetrikaPeriodVisitors, self).settings_dict()
 | 
	
		
			
				|  |  | +        settings['group'] = self.group
 | 
	
		
			
				|  |  | +        return settings
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def load_settings(self, settings):
 | 
	
		
			
				|  |  | +        super(YandexMetrikaPeriodVisitors, self).load_settings(settings)
 | 
	
		
			
				|  |  | +        self.group = settings.get('group')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def init_with_context(self, context):
 | 
	
		
			
				|  |  | +        if self.access_token is None:
 | 
	
		
			
				|  |  | +            self.error = mark_safe(_('Please <a href="%s">attach Yandex account and choose Yandex Metrika counter</a> to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk}))
 | 
	
		
			
				|  |  | +        elif self.counter is None:
 | 
	
		
			
				|  |  | +            self.error = mark_safe(_('Please <a href="%s">select Yandex Metrika counter</a> to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk}))
 | 
	
		
			
				|  |  | +        else:
 | 
	
		
			
				|  |  | +            date1 = datetime.datetime.utcnow() - datetime.timedelta(days=self.period)
 | 
	
		
			
				|  |  | +            date2 = datetime.datetime.utcnow()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            client = YandexMetrikaClient(self.access_token)
 | 
	
		
			
				|  |  | +            result, exception = client.api_stat_traffic_summary(self.counter, date1, date2, self.group)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if exception is not None:
 | 
	
		
			
				|  |  | +                error = _('API request failed.')
 | 
	
		
			
				|  |  | +                if isinstance(exception, HTTPError) and exception.code == 403:
 | 
	
		
			
				|  |  | +                    error += _(' Try to <a href="%s">revoke and grant access</a> again') % reverse('jet:update_module', kwargs={'pk': self.model.pk})
 | 
	
		
			
				|  |  | +                self.error = mark_safe(error)
 | 
	
		
			
				|  |  | +            else:
 | 
	
		
			
				|  |  | +                try:
 | 
	
		
			
				|  |  | +                    for data in reversed(result['data']):
 | 
	
		
			
				|  |  | +                        date = datetime.datetime.strptime(data['date'], '%Y%m%d')
 | 
	
		
			
				|  |  | +                        date = self.format_grouped_date(date, self.group)
 | 
	
		
			
				|  |  | +                        self.children.append((date, data))
 | 
	
		
			
				|  |  | +                except KeyError:
 | 
	
		
			
				|  |  | +                    self.error = _('Bad server response')
 |