Projet

Général

Profil

0003-matomo-views-form-manual-and-automatic-configuration.patch

Nicolas Roche, 08 avril 2019 20:57

Télécharger (22,5 ko)

Voir les différences:

Subject: [PATCH 3/4] matomo: views form manual and automatic configuration
 (#31778)

 hobo/matomo/__init__.py                       |   0
 hobo/matomo/forms.py                          |  37 ++++
 .../matomo/templates/hobo/matomo_disable.html |  20 ++
 .../templates/hobo/matomo_enable_auto.html    |  20 ++
 .../templates/hobo/matomo_enable_manual.html  |  20 ++
 hobo/matomo/templates/hobo/matomo_home.html   | 101 ++++++++++
 hobo/matomo/urls.py                           |  26 +++
 hobo/matomo/views.py                          | 111 +++++++++++
 hobo/settings.py                              |   1 +
 hobo/templates/hobo/home.html                 |   1 +
 hobo/urls.py                                  |   3 +
 tests/test_matomo_views.py                    | 182 ++++++++++++++++++
 12 files changed, 522 insertions(+)
 create mode 100644 hobo/matomo/__init__.py
 create mode 100644 hobo/matomo/forms.py
 create mode 100644 hobo/matomo/templates/hobo/matomo_disable.html
 create mode 100644 hobo/matomo/templates/hobo/matomo_enable_auto.html
 create mode 100644 hobo/matomo/templates/hobo/matomo_enable_manual.html
 create mode 100644 hobo/matomo/templates/hobo/matomo_home.html
 create mode 100644 hobo/matomo/urls.py
 create mode 100644 hobo/matomo/views.py
 create mode 100644 tests/test_matomo_views.py
hobo/matomo/forms.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django import forms
18
from django.utils.translation import ugettext_lazy as _
19

  
20

  
21
class SettingsForm(forms.Form):
22
    """
23
    According to publik-base-theme/templates/includes/tracking.html,
24
    the 2 tracking_js variables are merged into the unique below field.
25
    If JS code added is compliant to CNIL policy, we store it into
26
    'cnil_compliant_visits_tracking_js' else into 'visits_tracking_js'.
27
    The goal is to display a banner advertising users about intrusive JS.
28
    """
29
    tracking_js = forms.CharField(
30
        label=_('Tracking Javascript'),
31
        help_text=_('See <a href="https://developer.matomo.org/guides/tracking-javascript-guide">Matomo partners site</a> for getting your Javascript\'s tracking code.'),
32
        required=False,
33
        widget=forms.Textarea(attrs={'size': 1024}))
34

  
35

  
36
class EnableForm(forms.Form):
37
    pass
hobo/matomo/templates/hobo/matomo_disable.html
1
{% extends "hobo/matomo_home.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
  <h2>{% trans "User tracking" %}</h2>
6
{% endblock %}
7

  
8
{% block content %}
9
<form method="post">
10
{% csrf_token %}
11
<p>
12
{% trans "Are you sure you want to disable user tracking support?" %}
13
{{ form.as_p }}
14
<div class="buttons">
15
  <button class="submit-button">{% trans "Disable" %}</button>
16
  <a class="cancel" href="{% url 'matomo-home' %}">{% trans "Cancel" %}</a>
17
</div>
18
</form>
19

  
20
{% endblock %}
hobo/matomo/templates/hobo/matomo_enable_auto.html
1
{% extends "hobo/matomo_home.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
  <h2>{% trans "User tracking" %}</h2>
6
{% endblock %}
7

  
8
{% block content %}
9
<form method="post">
10
{% csrf_token %}
11
<p>
12
{% trans "Are you sure you want to enable automatic user tracking support?" %}
13
{{ form.as_p }}
14
<div class="buttons">
15
<button class="submit-button">{% trans "Enable" %}</button>
16
<a class="cancel" href="{% url 'matomo-home' %}">{% trans "Cancel" %}</a>
17
</div>
18
</form>
19

  
20
{% endblock %}
hobo/matomo/templates/hobo/matomo_enable_manual.html
1
{% extends "hobo/matomo_home.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
  <h2>{% trans "User tracking" %}</h2>
6
{% endblock %}
7

  
8
{% block content %}
9
<form method="post">
10
{% csrf_token %}
11
<p>
12
{% trans "Are you sure you want to enable manual user tracking support?" %}
13
{{ form.as_p }}
14
<div class="buttons">
15
<button class="submit-button">{% trans "Enable" %}</button>
16
<a class="cancel" href="{% url 'matomo-home' %}">{% trans "Cancel" %}</a>
17
</div>
18
</form>
19

  
20
{% endblock %}
hobo/matomo/templates/hobo/matomo_home.html
1
{% extends "hobo/base.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'matomo-home' %}">Matomo</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans "User tracking" %}</h2>
11
{% if enabled %}
12
<span class="actions">
13
  <a rel="popup" href="{% url 'matomo-disable' %}">{% trans 'Disable' %}</a>
14
  {% if mode == 'auto' %}
15
  <a href={{ logme_url }}>{% trans "Open visit tracking dashboard" %}</a>
16
  {% endif %}
17
</span>
18
{% endif %}
19
{% endblock %}
20

  
21
{% block content %}
22

  
23
<div class="infonotice">
24
  {% if not enabled %}
25
  <p>
26
    {% blocktrans %}
27
    The audience measurement tools are deployed to obtain information about
28
    visitor navigation. They help to understand where users come from on a site
29
    and reconstruct their browsing activity. These tools use technologies that
30
    permit to trace users on your site and associate a "referrer" or campaign
31
    with a unique identifier.
32
    {% endblocktrans %}
33
  </p>
34
    {% if ws_available %}
35
    <p>
36
      {% blocktrans %}
37
      Publik can automatically use a tool called "Matomo", which is the tracker
38
      solution recommended by the National Commission for Data Protection and Liberties (<a
39
      href="https://www.cnil.fr/fr/solutions-pour-les-cookies-de-mesure-daudience">CNIL-France</a>).
40
      {% endblocktrans %}
41
    </p>
42
    {% else %}
43
    <p>
44
      {% blocktrans %}
45
      Matomo is the tracker solution recommended by the National Commission for Data
46
      Protection and Liberties (<a
47
      href="https://www.cnil.fr/fr/solutions-pour-les-cookies-de-mesure-daudience">CNIL-France</a>).
48
      It requires <a href="https://www.cnil.fr/sites/default/files/typo/document/Configuration_piwik.pdf"
49
      >little configuration</a> to be exempt from legal consent.
50
      {% endblocktrans %}
51
    </p>
52
  {% endif %}
53
  {% else %}
54
  {% if mode == 'manual' %}
55
  {% trans "Manual configuration." %}
56
  {% elif mode == 'auto' %}
57
  {% trans "Automatic configuration." %}
58
  {% endif %}
59
  {% endif %}
60
</div>
61

  
62
{% if not enabled %}
63
<p>
64
  {% trans "Support is currently disabled." %}
65
</p>
66
<p>
67
  <a class="button" rel="popup" href="{% url 'matomo-enable-manual' %}">{% trans 'Manual Configuration' %}</a>
68
  {% if ws_available %}
69
  <a class="button" rel="popup" href="{% url 'matomo-enable-auto' %}">{% trans 'Automatic Configuration' %}</a>
70
  {% endif %}
71
</p>
72
{% else %}
73
{% if tracking_js != '' and tracking_js != place_holder %}
74
{% if cnil_ack_level == 'success' %}
75
<div class="successnotice">
76
  {% trans "Excellent respect of user rights." %}
77
</div>
78
{% elif cnil_ack_level == 'warning' %}
79
<div class="warningnotice">
80
  {% trans "Good respect of user rights." %}
81
</div>
82
{% elif cnil_ack_level == 'error' %}
83
<div class="errornotice">
84
  {% trans "No respect of user rights." %}
85
</div>
86
{% endif %}
87
{% endif %}
88

  
89
{% if mode == 'manual' %}
90
<form method="post">
91
  {% csrf_token %}
92
  {{ form.as_p }}
93

  
94
  <div class="buttons">
95
    <button class="submit-button">{% trans "Save" %}</button>
96
  </div>
97
</form>
98
{% endif %}
99

  
100
{% endif %}
101
{% endblock %}
hobo/matomo/urls.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.conf.urls import url
18

  
19
from . import views
20

  
21
urlpatterns = [
22
    url(r'^$', views.home, name='matomo-home'),
23
    url(r'^enable-manual$', views.enable_manual, name='matomo-enable-manual'),
24
    url(r'^enable-auto$', views.enable_auto, name='matomo-enable-auto'),
25
    url(r'^disable$', views.disable, name='matomo-disable'),
26
]
hobo/matomo/views.py
1
# hobo - portal to configure and deploy applications
2
# Copyright (C) 2015-2019  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.core.urlresolvers import reverse_lazy
18
from django.conf import settings
19
from django.contrib import messages
20
from django.views.generic import RedirectView, FormView
21

  
22
from .forms import SettingsForm, EnableForm
23
from .utils import get_variable, get_variable_value, \
24
    get_tracking_js, put_tracking_js, \
25
    JAVASCRIPT_PLACE_HOLDER, MatomoException, MatomoError, MatomoWS, \
26
    compute_cnil_acknowledgment_level, auto_configure_matomo
27

  
28

  
29
class HomeView(FormView):
30
    template_name = 'hobo/matomo_home.html'
31
    form_class = SettingsForm
32
    success_url = reverse_lazy('matomo-home')
33

  
34
    def get_initial(self):
35
        initial = super(HomeView, self).get_initial()
36
        initial['tracking_js'] = get_tracking_js()
37
        return initial
38

  
39
    def form_valid(self, form):
40
        tracking_js = form.cleaned_data['tracking_js']
41
        put_tracking_js(tracking_js)
42
        return super(HomeView, self).form_valid(form)
43

  
44
    def get_context_data(self, **kwargs):
45
        context = super(HomeView, self).get_context_data(**kwargs)
46
        tracking_js = get_tracking_js()
47
        logme_url = get_variable_value('matomo_logme_url')
48
        context['logme_url'] = logme_url
49
        context['tracking_js'] = tracking_js
50
        context['place_holder'] = JAVASCRIPT_PLACE_HOLDER
51

  
52
        # compute contextual values
53
        context['cnil_ack_level'] = compute_cnil_acknowledgment_level(tracking_js)
54
        try:
55
            MatomoWS()
56
        except MatomoError:
57
            context['ws_available'] = False
58
        else:
59
            context['ws_available'] = True
60
        context['enabled'] = tracking_js != ''
61
        if logme_url != '':
62
            context['mode'] = 'auto'
63
        else:
64
            context['mode'] = 'manual'
65
        return context
66

  
67
home = HomeView.as_view()
68

  
69

  
70
class EnableManualView(FormView):
71
    form_class = EnableForm
72
    template_name = 'hobo/matomo_enable_manual.html'
73
    success_url = reverse_lazy('matomo-home')
74

  
75
    def form_valid(self, form):
76
        # place holder (non empty string)
77
        tracking_js = get_tracking_js()
78
        if tracking_js == '':
79
            put_tracking_js(JAVASCRIPT_PLACE_HOLDER)
80
        return super(EnableManualView, self).form_valid(form)
81

  
82
enable_manual = EnableManualView.as_view()
83

  
84

  
85
class EnableAutoView(FormView):
86
    form_class = EnableForm
87
    template_name = 'hobo/matomo_enable_auto.html'
88
    success_url = reverse_lazy('matomo-home')
89

  
90
    def form_valid(self, form):
91
        try:
92
            auto_configure_matomo()
93
        except MatomoException as exc:
94
            messages.error(self.request, str(exc))
95
        return super(EnableAutoView, self).form_valid(form)
96

  
97
enable_auto = EnableAutoView.as_view()
98

  
99

  
100
class DisableView(FormView):
101
    form_class = EnableForm
102
    template_name = 'hobo/matomo_disable.html'
103
    success_url = reverse_lazy('matomo-home')
104

  
105
    def form_valid(self, form):
106
        put_tracking_js('')
107
        variable = get_variable('matomo_logme_url')
108
        variable.delete()
109
        return super(DisableView, self).form_valid(form)
110

  
111
disable = DisableView.as_view()
hobo/settings.py
41 41
    'gadjo',
42 42
    'hobo.environment',
43 43
    'hobo.franceconnect',
44
    'hobo.matomo',
44 45
    'hobo.profile',
45 46
    'hobo.theme',
46 47
    'hobo.emails',
hobo/templates/hobo/home.html
10 10
    <li><a href="{% url 'theme-home' %}">{% trans 'Theme' %}</a></li>
11 11
    <li><a href="{% url 'emails-home' %}">{% trans 'Emails' %}</a></li>
12 12
    <li><a href="{% url 'franceconnect-home' %}">FranceConnect</a></li>
13
    <li><a href="{% url 'matomo-home' %}">{% trans 'User tracking' %}</a></li>
13 14
    <li><a href="{% url 'environment-home' %}">{% trans 'Services' %}</a></li>
14 15
    <li><a href="{% url 'environment-variables' %}">{% trans 'Variables' %}</a></li>
15 16
  </ul>
hobo/urls.py
8 8
from .urls_utils import decorated_includes
9 9
from .environment.urls import urlpatterns as environment_urls
10 10
from .franceconnect.urls import urlpatterns as franceconnect_urls
11
from .matomo.urls import urlpatterns as matomo_urls
11 12
from .profile.urls import urlpatterns as profile_urls
12 13
from .theme.urls import urlpatterns as theme_urls
13 14
from .emails.urls import urlpatterns as emails_urls
......
20 21
                                             include(profile_urls))),
21 22
    url(r'^franceconnect/',
22 23
        decorated_includes(admin_required, include(franceconnect_urls))),
24
    url(r'^matomo/',
25
        decorated_includes(admin_required, include(matomo_urls))),
23 26
    url(r'^theme/', decorated_includes(admin_required,
24 27
                                             include(theme_urls))),
25 28
    url(r'^emails/', decorated_includes(admin_required, include(emails_urls))),
tests/test_matomo_views.py
1
# -*- coding: utf-8 -*-
2

  
3
import mock
4
import pytest
5
from requests import Response
6
from webtest import TestApp
7

  
8
from django.conf import settings
9
from django.contrib.auth.models import User
10
from django.test import override_settings
11

  
12
from hobo.environment.models import Variable, Wcs, Combo, Fargo
13
from hobo.wsgi import application
14

  
15
pytestmark = pytest.mark.django_db
16

  
17
CONFIG = {'URL': 'https://matomo.test',
18
          'TOKEN_AUTH': '1234',
19
          'EMAIL_TEMPLATE': 'noreply+%s@entrouvert.test'}
20

  
21
GET_NO_SITE_FROM_URL = """<?xml version="1.0" encoding="utf-8" ?>
22
<result />
23
"""
24

  
25
ADD_SITE_SUCCESS = """<?xml version="1.0" encoding="utf-8" ?>
26
<result>42</result>
27
"""
28

  
29
DEL_UNKNOWN_USER = """<?xml version="1.0" encoding="utf-8" ?>
30
<result>
31
        <error message="User 'hobo.dev.publik.love' doesn't exist." />
32
</result>
33
"""
34

  
35
MATOMO_SUCCESS = """<?xml version="1.0" encoding="utf-8" ?>
36
<result>
37
    <success message="ok" />
38
</result>
39
"""
40

  
41
JAVASCRIPT_TAG_BAD_RESPONSE = """<?xml version="1.0" encoding="utf-8" ?>
42
<no_result_tag/>
43
"""
44

  
45
JAVASCRIPT_TAG = """<?xml version="1.0" encoding="utf-8" ?>
46
<result>&lt;!-- Matomo --&gt;
47
&lt;script type=&quot;text/javascript&quot;&gt;
48
  var _paq = window._paq || [];
49
  /* tracker methods like &quot;setCustomDimension&quot; should be called before &quot;trackPageView&quot; */
50
  _paq.push(['trackPageView']);
51
  _paq.push(['enableLinkTracking']);
52
  (function() {
53
    var u=&quot;//matomo-test.entrouvert.org/&quot;;
54
    _paq.push(['setTrackerUrl', u+'piwik.php']);
55
    _paq.push(['setSiteId', '7']);
56
    var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
57
    g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
58
  })();
59
&lt;/script&gt;
60
&lt;!-- End Matomo Code --&gt;
61
</result>
62
"""
63

  
64
@pytest.fixture
65
def admin_user():
66
    try:
67
        user = User.objects.get(username='admin')
68
    except User.DoesNotExist:
69
        user = User.objects.create_superuser('admin', email=None, password='admin')
70
    return user
71

  
72
def login(app, username='admin', password='admin'):
73
    login_page = app.get('/login/')
74
    login_form = login_page.forms[0]
75
    login_form['username'] = username
76
    login_form['password'] = password
77
    resp = login_form.submit()
78
    assert resp.status_int == 302
79
    return app
80

  
81
def test_unlogged_access():
82
    # connect while not being logged in
83
    app = TestApp(application)
84
    resp = app.get('/matomo/', status=302)
85
    assert resp.location.endswith('/login/?next=/matomo/')
86

  
87
def test_access(admin_user):
88
    app = login(TestApp(application))
89
    assert app.get('/matomo/', status=200)
90

  
91
def test_disable(admin_user):
92
    app = login(TestApp(application))
93
    resp1 = app.get('/matomo/disable', status=200)
94
    resp2 = resp1.form.submit()
95
    assert resp2.location.endswith('/matomo/')
96

  
97
def test_enable_manual(admin_user):
98
    """scenario where user manually paste a javascript code"""
99
    app = login(TestApp(application))
100
    resp1 = app.get('/matomo/enable-manual', status=200)
101
    resp2 = resp1.form.submit().follow()
102
    resp2.form['tracking_js'] = '...js_code...'
103
    resp3 = resp2.form.submit().follow()
104
    assert resp3.body.find('Good respect of user rights') != -1
105

  
106
def auto_conf_mocked_post(url, **kwargs):
107
    contents = [GET_NO_SITE_FROM_URL, ADD_SITE_SUCCESS,
108
                DEL_UNKNOWN_USER, MATOMO_SUCCESS,
109
                JAVASCRIPT_TAG]
110
    response = Response()
111
    response._content = contents[auto_conf_mocked_post.cpt]
112
    response.status_code = 200
113

  
114
    auto_conf_mocked_post.cpt += 1
115
    return response
116

  
117
def auto_conf_failure_mocked_post(url, **kwargs):
118
    contents = [GET_NO_SITE_FROM_URL, ADD_SITE_SUCCESS,
119
                DEL_UNKNOWN_USER, MATOMO_SUCCESS,
120
                JAVASCRIPT_TAG_BAD_RESPONSE]
121
    response = Response()
122
    response._content = contents[auto_conf_mocked_post.cpt]
123
    response.status_code = 200
124

  
125
    auto_conf_mocked_post.cpt += 1
126
    return response
127

  
128
def test_available_options(admin_user):
129
    """check available buttons (manual/automatic configurations)"""
130
    with override_settings(MATOMO_SERVER=CONFIG):
131
        app = login(TestApp(application))
132
        resp = app.get('/matomo/', status=200)
133
        assert str(resp).find('href="/matomo/enable-manual"') != -1
134
        assert str(resp).find('href="/matomo/enable-auto"') != -1
135

  
136
    # without configuration: no automatic configuration available
137
    app = login(TestApp(application))
138
    resp = app.get('/matomo/', status=200)
139
    assert str(resp).find('href="/matomo/enable-manual"') != -1
140
    assert str(resp).find('href="/matomo/enable-auto"') == -1
141

  
142
@mock.patch('requests.post', side_effect=auto_conf_mocked_post)
143
def test_enable_auto(mocked_post, admin_user):
144
    """succesfull automatic scenario"""
145
    Combo.objects.create(base_url='https://combo.dev.publik.love',
146
                         template_name='portal-user')
147
    Wcs.objects.create(base_url='https://wcs.dev.publik.love')
148
    Fargo.objects.create(base_url='https://fargo.dev.publik.love')
149

  
150
    auto_conf_mocked_post.cpt = 0
151
    with override_settings(MATOMO_SERVER=CONFIG):
152
        app = login(TestApp(application))
153
        resp1 = app.get('/matomo/enable-auto', status=200)
154
        resp2 = resp1.form.submit()
155

  
156
        # call utils.py::auto_configure_matomo()
157
        resp3 = resp2.follow()
158
        print resp3.body
159

  
160
        # expect the CNIL compliance message is displayed
161
        assert resp3.body.find('Excellent respect of user rights') != -1
162

  
163

  
164
@mock.patch('requests.post', side_effect=auto_conf_failure_mocked_post)
165
def test_enable_auto_failure(mocked_post, admin_user):
166
    """error on automatic scenario"""
167
    Combo.objects.create(base_url='https://combo.dev.publik.love',
168
                         template_name='portal-user')
169
    Wcs.objects.create(base_url='https://wcs.dev.publik.love')
170
    Fargo.objects.create(base_url='https://fargo.dev.publik.love')
171

  
172
    auto_conf_mocked_post.cpt = 0
173
    with override_settings(MATOMO_SERVER=CONFIG):
174
        app = login(TestApp(application))
175
        resp1 = app.get('/matomo/enable-auto', status=200)
176
        resp2 = resp1.form.submit()
177

  
178
        # call utils.py::auto_configure_matomo()
179
        resp3 = resp2.follow()
180

  
181
        # expect a Django error message is displayed
182
        assert resp3.body.find('class="error">get_javascript_tag fails') != -1
0
-