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 = 'Revoke access' % reverse('jet:yandex-metrika-revoke', kwargs={'pk': self.module.model.pk}) else: link = 'Grant access' % reverse('jet:yandex-metrika-grant', kwargs={'pk': self.module.model.pk}) return format_html('%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 YandexMetrikaPeriodVisitorsSettingsForm(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 attach Yandex account and choose Yandex Metrika counter to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk})) elif self.counter is None: self.error = mark_safe(_('Please select Yandex Metrika counter 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 revoke and grant access 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 attach Yandex account and choose Yandex Metrika counter to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk})) elif self.counter is None: self.error = mark_safe(_('Please select Yandex Metrika counter 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 revoke and grant access 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 = YandexMetrikaPeriodVisitorsSettingsForm 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 attach Yandex account and choose Yandex Metrika counter to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk})) elif self.counter is None: self.error = mark_safe(_('Please select Yandex Metrika counter 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 revoke and grant access 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')