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')