Denis K 9 лет назад
Родитель
Сommit
5e1aa70130

+ 0 - 0
jet/dashboard_modules/__init__.py


+ 321 - 0
jet/dashboard_modules/yandex_metrika.py

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

+ 64 - 0
jet/dashboard_modules/yandex_metrika_views.py

@@ -0,0 +1,64 @@
+from django.conf.urls import url
+from django.contrib import messages
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse
+from django.shortcuts import redirect
+from jet.dashboard_modules.yandex_metrika import YandexMetrikaClient
+from jet.models import UserDashboardModule
+from jet import dashboard
+from django.utils.translation import ugettext_lazy as _
+
+
+def yandex_metrika_grant_view(request, pk):
+    client = YandexMetrikaClient()
+    return redirect(client.get_oauth_authorize_url(pk))
+
+
+def yandex_metrika_revoke_view(request, pk):
+    try:
+        module = UserDashboardModule.objects.get(pk=pk)
+        module.pop_settings(('access_token', 'expires_in', 'token_type', 'counter'))
+        return redirect(reverse('jet:update_module', kwargs={'pk': module.pk}))
+    except UserDashboardModule.DoesNotExist:
+        return HttpResponse(_('Module not found'))
+
+
+def yandex_metrika_callback_view(request):
+    try:
+        state = request.GET['state']
+        code = request.GET['code']
+
+        module = UserDashboardModule.objects.get(pk=state)
+
+        client = YandexMetrikaClient()
+        result, exception = client.oath_token_request(code)
+
+        if result is None:
+            messages.error(request, _('API request failed.'))
+        else:
+            module.update_settings(result)
+
+        return redirect(reverse('jet:update_module', kwargs={'pk': module.pk}))
+    except KeyError:
+        return HttpResponse(_('Bad arguments'))
+    except UserDashboardModule.DoesNotExist:
+        return HttpResponse(_('Module not found'))
+
+
+dashboard.urls.register_urls([
+    url(
+        r'^yandex-metrika/grant/(?P<pk>\d+)/$',
+        yandex_metrika_grant_view,
+        name='yandex-metrika-grant'
+    ),
+    url(
+        r'^yandex-metrika/revoke/(?P<pk>\d+)/$',
+        yandex_metrika_revoke_view,
+        name='yandex-metrika-revoke'
+    ),
+    url(
+        r'^yandex-metrika/callback/$',
+        yandex_metrika_callback_view,
+        name='yandex-metrika-callback'
+    ),
+])

+ 35 - 0
jet/static/jet/modules/yandex_metrika.js

@@ -0,0 +1,35 @@
+(function ($) {
+    $.fn.extend( {
+        yandexMetrikaChart: function() {
+            var $chart = $(this);
+            var ctx = $chart.get(0).getContext("2d");
+            var $data = $chart.find('.chart-data');
+            var $dataItems = $data.find('.chart-data-item');
+            var labels = [];
+            var data = [];
+
+            $dataItems.each(function() {
+                labels.push($(this).data('date'));
+                data.push($(this).data('value'));
+            });
+
+            new Chart(ctx).Line({
+                labels: labels,
+                datasets: [
+                    {
+                        fillColor: $chart.find('.chart-fillColor').css('color'),
+                        strokeColor: $chart.find('.chart-strokeColor').css('color'),
+                        pointColor: $chart.find('.chart-pointColor').css('color'),
+                        pointHighlightFill: $chart.find('.chart-pointHighlightFill').css('color'),
+                        responsive: true,
+                        data: data
+                    }
+                ]
+            }, {
+                scaleGridLineColor: $chart.find('.chart-scaleGridLineColor').css('color'),
+                scaleLineColor: $chart.find('.chart-scaleLineColor').css('color'),
+                scaleFontColor: $chart.find('.chart-scaleFontColor').css('color')
+            });
+        }
+    });
+})(jet.jQuery);

+ 38 - 0
jet/templates/jet/dashboard/modules/yandex_metrika_period_visitors.html

@@ -0,0 +1,38 @@
+{% load i18n %}
+
+
+{% if module.error %}
+    <ul>
+        <li>
+            {{ module.error }}
+        </li>
+    </ul>
+{% elif module.children %}
+    <table class="table">
+        <thead>
+            <tr>
+                <th>{% trans "Date" %}</th>
+                <th>{% trans "Visitors" %}</th>
+                <th>{% trans "Visits" %}</th>
+                <th>{% trans "Views" %}</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for data in module.children %}
+                <tr>
+                    <th>{{ data.0 }}</th>
+                    <td width="1" align="center">{{ data.1.visitors }}</td>
+                    <td width="1" align="center">{{ data.1.visits }}</td>
+                    <td width="1" align="center">{{ data.1.page_views }}</td>
+                </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+{% else %}
+    <ul>
+        <li>
+            {% trans "Nothing to show" %}
+        </li>
+    </ul>
+{% endif %}
+

+ 33 - 0
jet/templates/jet/dashboard/modules/yandex_metrika_visitors_chart.html

@@ -0,0 +1,33 @@
+{% load i18n %}
+
+{% if module.error %}
+    <ul>
+        <li>
+            {{ module.error }}
+        </li>
+    </ul>
+{% elif module.children %}
+    <div class="padding center">
+        <canvas id="chart_{{ module.model.pk }}" style="width: 100%;">
+            <div class="chart-fillColor"></div>
+            <div class="chart-strokeColor"></div>
+            <div class="chart-pointColor"></div>
+            <div class="chart-pointHighlightFill"></div>
+            <div class="chart-scaleGridLineColor"></div>
+            <div class="chart-scaleLineColor"></div>
+            <div class="chart-scaleFontColor"></div>
+            <div class="chart-data">
+                {% for data in module.children %}
+                    <div class="chart-data-item" data-date="{{ data.0|date:"d/m" }}" data-value="{{ data.1 }}"></div>
+                {% endfor %}
+            </div>
+        </canvas>
+        <script>jet.jQuery('#chart_{{ module.model.pk }}').yandexMetrikaChart();</script>
+    </div>
+{% else %}
+    <ul>
+        <li>
+            {% trans "Nothing to show" %}
+        </li>
+    </ul>
+{% endif %}

+ 26 - 0
jet/templates/jet/dashboard/modules/yandex_metrika_visitors_totals.html

@@ -0,0 +1,26 @@
+{% load i18n %}
+
+{% if module.error %}
+    <ul>
+        <li>
+            {{ module.error }}
+        </li>
+    </ul>
+{% elif module.children %}
+    <div class="padding center">
+        <ul class="inline bordered">
+            {% for statistic in module.children %}
+                <li>
+                    <div class="big">{{ statistic.value }}</div>
+                    <div class="dim">{{ statistic.title }}</div>
+                </li>
+            {% endfor %}
+        </ul>
+    </div>
+{% else %}
+    <ul>
+        <li>
+            {% trans "Nothing to show" %}
+        </li>
+    </ul>
+{% endif %}