Parcourir la source

Add google analytics widgets

Denis K il y a 9 ans
Parent
commit
a123094ff6

+ 348 - 0
jet/dashboard_modules/google_analytics.py

@@ -0,0 +1,348 @@
+import datetime
+import os
+from django import forms
+from django.core.urlresolvers import reverse
+from django.forms import Widget
+from django.forms.utils import flatatt
+from django.utils import formats
+from django.utils.html import format_html
+from django.utils.safestring import mark_safe
+from googleapiclient.discovery import build
+import httplib2
+from jet.modules import DashboardModule
+from oauth2client.client import flow_from_clientsecrets, OAuth2Credentials, AccessTokenRefreshError
+from django.utils.translation import ugettext_lazy as _
+from django.conf import settings
+try:
+    from django.utils.encoding import force_unicode
+except ImportError:
+    from django.utils.encoding import force_text as force_unicode
+
+JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE = getattr(
+    settings,
+    'JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE',
+    ''
+)
+
+
+class GoogleAnalyticsClient:
+    credential = None
+    analytics_service = None
+
+    def __init__(self, credential=None, redirect_uri=None):
+        self.FLOW = flow_from_clientsecrets(
+            JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE,
+            scope='https://www.googleapis.com/auth/analytics.readonly',
+            redirect_uri=redirect_uri
+        )
+
+        if credential is not None:
+            self.set_credential(OAuth2Credentials.from_json(credential))
+
+    def get_oauth_authorize_url(self, state=''):
+        self.FLOW.params['state'] = state
+        authorize_url = self.FLOW.step1_get_authorize_url()
+        return authorize_url
+
+    def set_credential(self, credential):
+        self.credential = credential
+        self.set_analytics_service(self.credential)
+
+    def set_credential_from_request(self, request):
+        self.set_credential(self.FLOW.step2_exchange(request.GET))
+
+    def set_analytics_service(self, credential):
+        http = httplib2.Http()
+        http = credential.authorize(http)
+        self.analytics_service = build('analytics', 'v3', http=http)
+
+    def api_profiles(self):
+        if self.analytics_service is None:
+            return None, None
+
+        try:
+            profiles = self.analytics_service.management().profiles().list(
+                accountId='~all',
+                webPropertyId='~all'
+            ).execute()
+
+            return profiles['items'], None
+        except (TypeError, KeyError) as e:
+            return None, e
+
+    def api_ga(self, profile_id, date1, date2, group=None):
+        if self.analytics_service is None:
+            return None, None
+
+        if group == 'day':
+            dimensions = 'ga:date'
+        elif group == 'week':
+            dimensions = 'ga:year,ga:week'
+        elif group == 'month':
+            dimensions = 'ga:year,ga:month'
+        else:
+            dimensions = ''
+
+        try:
+            data = self.analytics_service.data().ga().get(
+                ids='ga:' + profile_id,
+                start_date=date1.strftime('%Y-%m-%d'),
+                end_date=date2.strftime('%Y-%m-%d'),
+                metrics='ga:users,ga:sessions,ga:pageviews',
+                dimensions=dimensions
+            ).execute()
+
+            return data, None
+        except TypeError as e:
+            return None, e
+
+
+class CredentialWidget(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:google-analytics-revoke', kwargs={'pk': self.module.model.pk})
+        else:
+            link = '<a href="%s">Grant access</a>' % reverse('jet:google-analytics-grant', kwargs={'pk': self.module.model.pk})
+
+        attrs = self.build_attrs({
+            'type': 'hidden',
+            'name': 'credential',
+        })
+        attrs['value'] = force_unicode(value)
+
+        return format_html('%s<input{} />' % link, flatatt(attrs))
+
+
+class GoogleAnalyticsSettingsForm(forms.Form):
+    credential = forms.CharField(label=_('Credential'), widget=CredentialWidget)
+    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['credential'].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['websiteUrl']), counters))
+        else:
+            label = _('grant access first') if module.credential is None else _('counters loading failed')
+            self.fields['counter'].choices = (('', '-- %s -- ' % label),)
+
+
+class GoogleAnalyticsChartSettingsForm(GoogleAnalyticsSettingsForm):
+    show = forms.ChoiceField(label=_('Show'), choices=(
+        ('ga:users', _('Users')),
+        ('ga:sessions', _('Sessions')),
+        ('ga:pageviews', _('Views')),
+    ))
+    group = forms.ChoiceField(label=_('Group'), choices=(
+        ('day', _('By day')),
+        ('week', _('By week')),
+        ('month', _('By month')),
+    ))
+
+
+class GoogleAnalyticsPeriodVisitorsSettingsForm(GoogleAnalyticsSettingsForm):
+    group = forms.ChoiceField(label=_('Group'), choices=(
+        ('day', _('By day')),
+        ('week', _('By week')),
+        ('month', _('By month')),
+    ))
+
+
+class GoogleAnalyticsBase(DashboardModule):
+    settings_form = GoogleAnalyticsSettingsForm
+    ajax_load = True
+    contrast = True
+    period = None
+    credential = None
+    counter = None
+    error = None
+
+    class Media:
+        js = ('jet/vendor/chart.js/Chart.min.js', 'jet/modules/google_analytics.js')
+
+    def __init__(self, title=None, period=None, **kwargs):
+        kwargs.update({'period': period})
+        super(GoogleAnalyticsBase, self).__init__(title, **kwargs)
+
+    def settings_dict(self):
+        return {
+            'period': self.period,
+            'credential': self.credential,
+            'counter': self.counter
+        }
+
+    def load_settings(self, settings):
+        try:
+            self.period = int(settings.get('period'))
+        except TypeError:
+            self.period = 0
+        self.credential = settings.get('credential')
+        self.counter = settings.get('counter')
+
+    def init_with_context(self, context):
+        raise NotImplementedError('subclasses of GoogleAnalytics must provide a init_with_context() method')
+
+    def counters(self):
+        try:
+            client = GoogleAnalyticsClient(self.credential)
+            profiles, exception = client.api_profiles()
+            return profiles
+        except Exception:
+            return None
+
+    def get_grouped_date(self, data, group):
+        if group == 'week':
+            date = datetime.datetime.strptime(
+                '%s-%s-%s' % (data['ga_year'], data['ga_week'], '0'),
+                '%Y-%W-%w'
+            )
+        elif group == 'month':
+            date = datetime.datetime.strptime(data['ga_year'] + data['ga_month'], '%Y%m')
+        else:
+            date = datetime.datetime.strptime(data['ga_date'], '%Y%m%d')
+        return date
+
+    def format_grouped_date(self, data, group):
+        date = self.get_grouped_date(data, group)
+
+        if group == 'week':
+            date = u'%s — %s' % (
+                (date - datetime.timedelta(days=6)).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.credential is None:
+            self.error = mark_safe(_('Please <a href="%s">attach Google account and choose Google Analytics counter</a> to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk}))
+            return False
+        elif self.counter is None:
+            self.error = mark_safe(_('Please <a href="%s">select Google Analytics counter</a> to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk}))
+            return False
+        else:
+            return True
+
+    def api_ga(self, group=None):
+        if self.counter_attached():
+            date1 = datetime.datetime.utcnow() - datetime.timedelta(days=self.period)
+            date2 = datetime.datetime.utcnow()
+
+            try:
+                client = GoogleAnalyticsClient(self.credential)
+                result, exception = client.api_ga(self.counter, date1, date2, group)
+
+                if exception is not None:
+                        raise exception
+
+                return result
+            except Exception as e:
+                error = _('API request failed.')
+                if isinstance(e, AccessTokenRefreshError):
+                    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)
+
+
+class GoogleAnalyticsVisitorsTotals(GoogleAnalyticsBase):
+    title = _('Google Analytics visitors totals')
+    template = 'jet/dashboard/modules/google_analytics_visitors_totals.html'
+
+    def init_with_context(self, context):
+        result = self.api_ga()
+
+        if result is not None:
+            try:
+                self.children.append({'title': _('users'), 'value': result['totalsForAllResults']['ga:users']})
+                self.children.append({'title': _('sessions'), 'value': result['totalsForAllResults']['ga:sessions']})
+                self.children.append({'title': _('views'), 'value': result['totalsForAllResults']['ga:pageviews']})
+            except KeyError:
+                self.error = _('Bad server response')
+
+
+class GoogleAnalyticsVisitorsChart(GoogleAnalyticsBase):
+    title = _('Google Analytics visitors chart')
+    template = 'jet/dashboard/modules/google_analytics_visitors_chart.html'
+    style = 'overflow-x: auto;'
+    show = None
+    group = None
+    settings_form = GoogleAnalyticsChartSettingsForm
+
+    def settings_dict(self):
+        settings = super(GoogleAnalyticsVisitorsChart, self).settings_dict()
+        settings['show'] = self.show
+        settings['group'] = self.group
+        return settings
+
+    def load_settings(self, settings):
+        super(GoogleAnalyticsVisitorsChart, self).load_settings(settings)
+        self.show = settings.get('show')
+        self.group = settings.get('group')
+
+    def init_with_context(self, context):
+        result = self.api_ga(self.group)
+
+        if result is not None:
+            try:
+                for data in result['rows']:
+                    row_data = {}
+
+                    i = 0
+                    for column in result['columnHeaders']:
+                        row_data[column['name'].replace(':', '_')] = data[i]
+                        i += 1
+
+                    date = self.get_grouped_date(row_data, self.group)
+                    self.children.append((date, row_data[self.show.replace(':', '_')]))
+            except KeyError:
+                self.error = _('Bad server response')
+
+
+class GoogleAnalyticsPeriodVisitors(GoogleAnalyticsBase):
+    title = _('Google Analytics period visitors')
+    template = 'jet/dashboard/modules/google_analytics_period_visitors.html'
+    group = None
+    contrast = False
+    settings_form = GoogleAnalyticsPeriodVisitorsSettingsForm
+
+    def settings_dict(self):
+        settings = super(GoogleAnalyticsPeriodVisitors, self).settings_dict()
+        settings['group'] = self.group
+        return settings
+
+    def load_settings(self, settings):
+        super(GoogleAnalyticsPeriodVisitors, self).load_settings(settings)
+        self.group = settings.get('group')
+
+    def init_with_context(self, context):
+        result = self.api_ga(self.group)
+
+        if result is not None:
+            try:
+                for data in reversed(result['rows']):
+                    row_data = {}
+
+                    i = 0
+                    for column in result['columnHeaders']:
+                        row_data[column['name'].replace(':', '_')] = data[i]
+                        i += 1
+
+                    date = self.format_grouped_date(row_data, self.group)
+                    self.children.append((date, row_data))
+            except KeyError:
+                self.error = _('Bad server response')

+ 54 - 0
jet/dashboard_modules/google_analytics_views.py

@@ -0,0 +1,54 @@
+from django.core.urlresolvers import reverse
+from django.conf.urls import url
+from django.contrib import messages
+from django.shortcuts import redirect
+from httplib2 import ServerNotFoundError
+from jet.dashboard_modules.google_analytics import GoogleAnalyticsClient
+from jet.models import UserDashboardModule
+from jet import dashboard
+from django.http import HttpResponse
+from oauth2client.client import FlowExchangeError
+from django.utils.translation import ugettext_lazy as _
+
+
+def google_analytics_grant_view(request, pk):
+    redirect_uri = request.build_absolute_uri(reverse('jet:google-analytics-callback'))
+    client = GoogleAnalyticsClient(redirect_uri=redirect_uri)
+    return redirect(client.get_oauth_authorize_url(pk))
+
+
+def google_analytics_revoke_view(request, pk):
+    try:
+        module = UserDashboardModule.objects.get(pk=pk)
+        module.pop_settings(('credential',))
+        return redirect(reverse('jet:update_module', kwargs={'pk': module.pk}))
+    except UserDashboardModule.DoesNotExist:
+        return HttpResponse(_('Module not found'))
+
+
+def google_analytics_callback_view(request):
+    module = None
+
+    try:
+        state = request.GET['state']
+        module = UserDashboardModule.objects.get(pk=state)
+
+        redirect_uri = request.build_absolute_uri(reverse('jet:google-analytics-callback'))
+        client = GoogleAnalyticsClient(redirect_uri=redirect_uri)
+        client.set_credential_from_request(request)
+
+        module.update_settings({'credential': client.credential.to_json()})
+    except (FlowExchangeError, ValueError, ServerNotFoundError):
+        messages.error(request, _('API request failed.'))
+    except KeyError:
+        return HttpResponse(_('Bad arguments'))
+    except UserDashboardModule.DoesNotExist:
+        return HttpResponse(_('Module not found'))
+
+    return redirect(reverse('jet:update_module', kwargs={'pk': module.pk}))
+
+dashboard.urls.register_urls([
+    url(r'^google-analytics/grant/(?P<pk>\d+)/$', google_analytics_grant_view, name='google-analytics-grant'),
+    url(r'^google-analytics/revoke/(?P<pk>\d+)/$', google_analytics_revoke_view, name='google-analytics-revoke'),
+    url(r'^google-analytics/callback/', google_analytics_callback_view, name='google-analytics-callback'),
+])

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

@@ -0,0 +1,35 @@
+(function ($) {
+    $.fn.extend( {
+        googleAnalyticsChart: 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/google_analytics_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 "Users" %}</th>
+                <th>{% trans "Sessions" %}</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.ga_users }}</td>
+                    <td width="1" align="center">{{ data.1.ga_sessions }}</td>
+                    <td width="1" align="center">{{ data.1.ga_pageviews }}</td>
+                </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+{% else %}
+    <ul>
+        <li>
+            {% trans "Nothing to show" %}
+        </li>
+    </ul>
+{% endif %}
+

+ 33 - 0
jet/templates/jet/dashboard/modules/google_analytics_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 }}').googleAnalyticsChart();</script>
+    </div>
+{% else %}
+    <ul>
+        <li>
+            {% trans "Nothing to show" %}
+        </li>
+    </ul>
+{% endif %}

+ 26 - 0
jet/templates/jet/dashboard/modules/google_analytics_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 %}