# encoding: utf-8 import datetime import json from django import forms try: from django.core.urlresolvers import reverse except ImportError: # Django 1.11 from django.urls import reverse from django.forms import Widget from django.utils import formats from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.text import capfirst from jet.dashboard.modules import DashboardModule from django.utils.translation import ugettext_lazy as _ from django.conf import settings from django.utils.encoding import force_text try: from urllib import request from urllib.parse import urlencode from urllib.error import URLError, HTTPError except ImportError: import urllib2 as request from urllib2 import URLError, HTTPError from urllib import urlencode 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 = urlencode(data).encode() if headers is None: headers = {} req = request.Request(url, data, headers) try: f = request.urlopen(req) result = f.read().decode('utf8') 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 = '%s' % ( reverse('jet-dashboard:yandex-metrika-revoke', kwargs={'pk': self.module.model.pk}), force_text(_('Revoke access')) ) else: link = '%s' % ( reverse('jet-dashboard:yandex-metrika-grant', kwargs={'pk': self.module.model.pk}), force_text(_('Grant access')) ) if value is None: value = '' return format_html('%s' % (link, value)) class YandexMetrikaSettingsForm(forms.Form): access_token = forms.CharField(label=_('Access'), 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 = (('', '-- %s --' % force_text(_('none'))),) self.fields['counter'].choices.extend(map(lambda x: (x['id'], x['site']), counters)) else: label = force_text(_('grant access first')) if module.access_token is None else force_text(_('counters loading failed')) self.fields['counter'].choices = (('', '-- %s -- ' % label),) class YandexMetrikaChartSettingsForm(YandexMetrikaSettingsForm): show = forms.ChoiceField(label=_('Show'), choices=( ('visitors', capfirst(_('visitors'))), ('visits', capfirst(_('visits'))), ('page_views', capfirst(_('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 YandexMetrikaBase(DashboardModule): settings_form = YandexMetrikaSettingsForm ajax_load = True contrast = True period = None access_token = None expires_in = None token_type = None counter = None error = None 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 = u'%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 def counter_attached(self): 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-dashboard:update_module', kwargs={'pk': self.model.pk})) return False elif self.counter is None: self.error = mark_safe(_('Please select Yandex Metrika counter to start using widget') % reverse('jet-dashboard:update_module', kwargs={'pk': self.model.pk})) return False else: return True def api_stat_traffic_summary(self, group=None): if self.counter_attached(): date1 = datetime.datetime.now() - datetime.timedelta(days=self.period) date2 = datetime.datetime.now() client = YandexMetrikaClient(self.access_token) result, exception = client.api_stat_traffic_summary(self.counter, date1, date2, 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-dashboard:update_module', kwargs={'pk': self.model.pk}) self.error = mark_safe(error) else: return result class YandexMetrikaVisitorsTotals(YandexMetrikaBase): """ Yandex Metrika widget that shows total number of visitors, visits and viewers for a particular period of time. Period may be following: Today, Last week, Last month, Last quarter, Last year """ title = _('Yandex Metrika visitors totals') template = 'jet.dashboard/modules/yandex_metrika_visitors_totals.html' #: Which period should be displayed. Allowed values - integer of days period = None def __init__(self, title=None, period=None, **kwargs): kwargs.update({'period': period}) super(YandexMetrikaVisitorsTotals, self).__init__(title, **kwargs) def init_with_context(self, context): result = self.api_stat_traffic_summary() if result is not None: 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(YandexMetrikaBase): """ Yandex Metrika widget that shows visitors/visits/viewer chart for a particular period of time. Data is grouped by day, week or month Period may be following: Today, Last week, Last month, Last quarter, Last year """ title = _('Yandex Metrika visitors chart') template = 'jet.dashboard/modules/yandex_metrika_visitors_chart.html' style = 'overflow-x: auto;' #: Which period should be displayed. Allowed values - integer of days period = None #: What data should be shown. Possible values: ``visitors``, ``visits``, ``page_views`` show = None #: Sets grouping of data. Possible values: ``day``, ``week``, ``month`` group = None settings_form = YandexMetrikaChartSettingsForm class Media: js = ('jet.dashboard/vendor/chart.js/Chart.min.js', 'jet.dashboard/dashboard_modules/yandex_metrika.js') def __init__(self, title=None, period=None, show=None, group=None, **kwargs): kwargs.update({'period': period, 'show': show, 'group': group}) super(YandexMetrikaVisitorsChart, self).__init__(title, **kwargs) 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): result = self.api_stat_traffic_summary(self.group) if result is not None: 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(YandexMetrikaBase): """ Yandex Metrika widget that shows visitors, visits and viewers for a particular period of time. Data is grouped by day, week or month Period may be following: Today, Last week, Last month, Last quarter, Last year """ title = _('Yandex Metrika period visitors') template = 'jet.dashboard/modules/yandex_metrika_period_visitors.html' #: Which period should be displayed. Allowed values - integer of days period = None #: Sets grouping of data. Possible values: ``day``, ``week``, ``month`` group = None contrast = False settings_form = YandexMetrikaPeriodVisitorsSettingsForm def __init__(self, title=None, period=None, group=None, **kwargs): kwargs.update({'period': period, 'group': group}) super(YandexMetrikaPeriodVisitors, self).__init__(title, **kwargs) 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): result = self.api_stat_traffic_summary(self.group) if result is not None: 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')