Projet

Général

Profil

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

Nicolas Roche, 15 avril 2019 14:22

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                          | 112 +++++++++++
 hobo/settings.py                              |   1 +
 hobo/templates/hobo/home.html                 |   1 +
 hobo/urls.py                                  |   3 +
 tests/test_matomo_views.py                    | 182 ++++++++++++++++++
 12 files changed, 523 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) 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 != '' %}
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) 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) 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 .models import MatomoStatus
24
from .utils import get_variable, get_variable_value, \
25
    get_tracking_js, put_tracking_js, \
26
    MatomoException, MatomoError, MatomoWS, \
27
    compute_cnil_acknowledgment_level, auto_configure_matomo
28

  
29

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

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

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

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

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

  
70
home = HomeView.as_view()
71

  
72

  
73
class EnableManualView(FormView):
74
    model = MatomoStatus
75
    form_class = EnableForm
76
    template_name = 'hobo/matomo_enable_manual.html'
77
    success_url = reverse_lazy('matomo-home')
78

  
79
    def form_valid(self, form):
80
        self.model.do_enter_manual_configuration = True
81
        return super(EnableManualView, self).form_valid(form)
82

  
83
enable_manual = EnableManualView.as_view()
84

  
85

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

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

  
98
enable_auto = EnableAutoView.as_view()
99

  
100

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

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

  
112
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
-