Projet

Général

Profil

0001-api-add-statistics-API-for-direct-combo-usage-52731.patch

Frédéric Péters, 12 avril 2021 14:07

Télécharger (14,1 ko)

Voir les différences:

Subject: [PATCH] api: add statistics API for direct combo usage (#52731)

 tests/api/test_statistics.py | 155 +++++++++++++++++++++++++++++++++++
 wcs/backoffice/management.py |  18 +---
 wcs/sql.py                   |  16 +++-
 wcs/statistics/__init__.py   |   0
 wcs/statistics/views.py      | 128 +++++++++++++++++++++++++++++
 wcs/urls.py                  |   7 ++
 6 files changed, 306 insertions(+), 18 deletions(-)
 create mode 100644 tests/api/test_statistics.py
 create mode 100644 wcs/statistics/__init__.py
 create mode 100644 wcs/statistics/views.py
tests/api/test_statistics.py
1
import datetime
2
import os
3

  
4
import pytest
5

  
6
from wcs.categories import Category
7
from wcs.formdef import FormDef
8
from wcs.qommon.http_request import HTTPRequest
9

  
10
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app
11
from .utils import sign_uri
12

  
13

  
14
@pytest.fixture
15
def pub():
16
    pub = create_temporary_pub(sql_mode=True)
17
    Category.wipe()
18
    FormDef.wipe()
19

  
20
    req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
21
    pub.set_app_dir(req)
22
    pub.cfg['identification'] = {'methods': ['password']}
23
    pub.cfg['language'] = {'language': 'en'}
24
    pub.write_cfg()
25

  
26
    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
27
        fd.write(
28
            '''\
29
[api-secrets]
30
coucou = 1234
31
'''
32
        )
33

  
34
    return pub
35

  
36

  
37
def teardown_module(module):
38
    clean_temporary_pub()
39

  
40

  
41
def test_statistics_index(pub):
42
    get_app(pub).get('/api/statistics/', status=403)
43
    resp = get_app(pub).get(sign_uri('/api/statistics/'))
44
    assert resp.json['data'][0]['name'] == 'Forms Count'
45
    assert resp.json['data'][0]['url'] == 'http://example.net/api/statistics/forms/count/'
46

  
47

  
48
def test_statistics_index_no_sql(pub):
49
    pub.is_using_postgresql = lambda: False
50
    assert get_app(pub).get(sign_uri('/api/statistics/')).json == {'data': [], 'err': 0}
51

  
52

  
53
def test_statistics_index_categories(pub):
54
    Category(name='Category A').store()
55
    Category(name='Category B').store()
56
    resp = get_app(pub).get(sign_uri('/api/statistics/'))
57
    category_filter = [x for x in resp.json['data'][0]['filters'] if x['id'] == 'category'][0]
58
    assert len(category_filter['options']) == 3
59

  
60

  
61
def test_statistics_forms_count(pub):
62
    category_a = Category(name='Category A')
63
    category_a.store()
64
    category_b = Category(name='Category B')
65
    category_b.store()
66

  
67
    formdef = FormDef()
68
    formdef.name = 'test 1'
69
    formdef.category_id = category_a.id
70
    formdef.fields = []
71
    formdef.store()
72
    formdef.data_class().wipe()
73

  
74
    formdef2 = FormDef()
75
    formdef2.name = 'test 2'
76
    formdef2.category_id = category_b.id
77
    formdef2.fields = []
78
    formdef2.store()
79
    formdef2.data_class().wipe()
80

  
81
    for _i in range(20):
82
        formdata = formdef.data_class()()
83
        formdata.just_created()
84
        formdata.receipt_time = datetime.datetime(2021, 1, 1, 0, 0).timetuple()
85
        formdata.store()
86

  
87
    for _i in range(30):
88
        formdata = formdef2.data_class()()
89
        formdata.just_created()
90
        formdata.receipt_time = datetime.datetime(2021, 3, 1, 2, 0).timetuple()
91
        formdata.store()
92

  
93
    resp = get_app(pub).get(sign_uri('/api/statistics/forms/count/'))
94
    assert resp.json == {
95
        'data': {
96
            'series': [{'data': [20, 0, 30], 'label': 'Forms Count'}],
97
            'x_labels': ['2021-01', '2021-02', '2021-03'],
98
        },
99
        'err': 0,
100
    }
101

  
102
    resp = get_app(pub).get(sign_uri('/api/statistics/forms/count/?time_interval=year'))
103
    assert resp.json == {
104
        'data': {
105
            'series': [{'data': [50], 'label': 'Forms Count'}],
106
            'x_labels': ['2021'],
107
        },
108
        'err': 0,
109
    }
110

  
111
    resp = get_app(pub).get(sign_uri('/api/statistics/forms/count/?time_interval=weekday'))
112
    assert resp.json == {
113
        'data': {
114
            'series': [{'data': [30, 0, 0, 0, 20, 0, 0], 'label': 'Forms Count'}],
115
            'x_labels': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
116
        },
117
        'err': 0,
118
    }
119

  
120
    resp = get_app(pub).get(sign_uri('/api/statistics/forms/count/?time_interval=hour'))
121
    assert resp.json == {
122
        'data': {
123
            'series': [
124
                {
125
                    'label': 'Forms Count',
126
                    'data': [20, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
127
                }
128
            ],
129
            'x_labels': list(range(24)),
130
        },
131
        'err': 0,
132
    }
133

  
134
    # time_interval=day is not supported
135
    get_app(pub).get(sign_uri('/api/statistics/forms/count/?time_interval=day'), status=400)
136

  
137
    # apply category filter
138
    resp = get_app(pub).get(sign_uri('/api/statistics/forms/count/?category=%s' % category_a.id))
139
    assert resp.json == {
140
        'data': {
141
            'series': [{'data': [20], 'label': 'Forms Count'}],
142
            'x_labels': ['2021-01'],
143
        },
144
        'err': 0,
145
    }
146

  
147
    # apply period filter
148
    resp = get_app(pub).get(sign_uri('/api/statistics/forms/count/?end=2021-02-01'))
149
    assert resp.json == {
150
        'data': {
151
            'series': [{'data': [20], 'label': 'Forms Count'}],
152
            'x_labels': ['2021-01'],
153
        },
154
        'err': 0,
155
    }
wcs/backoffice/management.py
3455 3455
        yearly_totals = [(datetime.date.today().year, 0)]
3456 3456

  
3457 3457
    weekday_totals = sql.get_weekday_totals(period_start, period_end, criterias)
3458
    weekday_line = []
3459
    weekday_names = [
3460
        _('Sunday'),
3461
        _('Monday'),
3462
        _('Tuesday'),
3463
        _('Wednesday'),
3464
        _('Thursday'),
3465
        _('Friday'),
3466
        _('Saturday'),
3467
    ]
3468
    for weekday, total in weekday_totals:
3469
        label = weekday_names[weekday]
3470
        weekday_line.append((label, total))
3471
    # move Sunday to the last place
3472
    weekday_line = weekday_line[1:] + [weekday_line[0]]
3473

  
3474 3458
    hour_totals = sql.get_hour_totals(period_start, period_end, criterias)
3475 3459

  
3476 3460
    r += htmltext(
......
3481 3465
var year_line = %(year_line)s;
3482 3466
</script>'''
3483 3467
        % {
3484
            'weekday_line': json.dumps(weekday_line),
3468
            'weekday_line': json.dumps(weekday_totals),
3485 3469
            'hour_line': json.dumps(hour_totals),
3486 3470
            'month_line': json.dumps(monthly_totals),
3487 3471
            'year_line': json.dumps(yearly_totals),
wcs/sql.py
46 46

  
47 47
from . import qommon
48 48
from .publisher import UnpicklerClass
49
from .qommon import get_cfg
49
from .qommon import _, get_cfg
50 50
from .qommon.misc import strftime
51 51
from .qommon.storage import _take, deep_bytes2str
52 52
from .qommon.storage import parse_clause as parse_storage_clause
......
3178 3178
            result.append((weekday, 0))
3179 3179
    result.sort()
3180 3180

  
3181
    # add labels,
3182
    weekday_names = [
3183
        _('Sunday'),
3184
        _('Monday'),
3185
        _('Tuesday'),
3186
        _('Wednesday'),
3187
        _('Thursday'),
3188
        _('Friday'),
3189
        _('Saturday'),
3190
    ]
3191
    result = [(weekday_names[x], y) for (x, y) in result]
3192
    # and move Sunday last
3193
    result = result[1:] + [result[0]]
3194

  
3181 3195
    conn.commit()
3182 3196
    cur.close()
3183 3197

  
wcs/statistics/views.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2021  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 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 General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.http import HttpResponseBadRequest, HttpResponseForbidden, JsonResponse
18
from django.urls import reverse
19
from django.views.generic import View
20
from quixote import get_publisher
21

  
22
from wcs.api_utils import is_url_signed
23
from wcs.categories import Category
24
from wcs.qommon import _, misc
25
from wcs.qommon.misc import C_
26
from wcs.qommon.storage import Equal
27

  
28

  
29
class RestrictedView(View):
30
    def dispatch(self, *args, **kwargs):
31
        if not is_url_signed():
32
            return HttpResponseForbidden()
33
        return super().dispatch(*args, **kwargs)
34

  
35

  
36
class IndexView(RestrictedView):
37
    def get(self, request, *args, **kwargs):
38
        if not get_publisher().is_using_postgresql():
39
            return JsonResponse({'data': [], 'err': 0})
40
        categories = Category.select()
41
        categories.sort(key=lambda x: misc.simplify(x.name))
42
        category_options = [{'id': '_all', 'label': C_('categories|All')}] + [
43
            {'id': x.id, 'label': x.name} for x in categories
44
        ]
45
        return JsonResponse(
46
            {
47
                'data': [
48
                    {
49
                        'name': _('Forms Count'),
50
                        'url': request.build_absolute_uri(reverse('api-statistics-forms-count')),
51
                        'id': 'forms_counts',
52
                        'filters': [
53
                            {
54
                                'id': 'time_interval',
55
                                'label': _('Interval'),
56
                                'options': [
57
                                    {
58
                                        'id': 'month',
59
                                        'label': _('Month'),
60
                                    },
61
                                    {
62
                                        'id': 'year',
63
                                        'label': _('Year'),
64
                                    },
65
                                    {
66
                                        'id': 'weekday',
67
                                        'label': _('Week day'),
68
                                    },
69
                                    {
70
                                        'id': 'hour',
71
                                        'label': _('Hour'),
72
                                    },
73
                                ],
74
                                'required': True,
75
                                'default': 'month',
76
                            },
77
                            {
78
                                'id': 'category',
79
                                'label': _('Category'),
80
                                'options': category_options,
81
                                'required': False,
82
                                'default': '_all',
83
                            },
84
                        ],
85
                    }
86
                ]
87
            }
88
        )
89

  
90

  
91
class FormsCountView(RestrictedView):
92
    def get(self, request, *args, **kwargs):
93
        from wcs import sql
94

  
95
        time_interval = request.GET.get('time_interval', 'month')
96
        totals_kwargs = {
97
            'period_start': request.GET.get('start'),
98
            'period_end': request.GET.get('end'),
99
            'criterias': [],
100
        }
101
        category_id = request.GET.get('category')
102
        if category_id and category_id != '_all':
103
            totals_kwargs['criterias'].append(Equal('category_id', category_id))
104
        time_interval_methods = {
105
            'month': sql.get_monthly_totals,
106
            'year': sql.get_yearly_totals,
107
            'weekday': sql.get_weekday_totals,
108
            'hour': sql.get_hour_totals,
109
        }
110
        if time_interval in time_interval_methods:
111
            totals = time_interval_methods[time_interval](**totals_kwargs)
112
        else:
113
            return HttpResponseBadRequest('invalid time_interval parameter')
114

  
115
        return JsonResponse(
116
            {
117
                'data': {
118
                    'x_labels': [x[0] for x in totals],
119
                    'series': [
120
                        {
121
                            'label': _('Forms Count'),
122
                            'data': [x[1] for x in totals],
123
                        }
124
                    ],
125
                },
126
                'err': 0,
127
            }
128
        )
wcs/urls.py
17 17
from django.conf.urls import url
18 18

  
19 19
from . import api, compat, views
20
from .statistics import views as statistics_views
20 21

  
21 22
urlpatterns = [
22 23
    url(r'^robots.txt$', views.robots_txt),
......
26 27
    url(r'^api/validate-expression$', api.validate_expression, name='api-validate-expression'),
27 28
    url(r'^api/reverse-geocoding$', api.reverse_geocoding, name='api-reverse-geocoding'),
28 29
    url(r'^api/geocoding$', api.geocoding, name='api-geocoding'),
30
    url(r'^api/statistics/$', statistics_views.IndexView.as_view()),
31
    url(
32
        r'^api/statistics/forms/count/$',
33
        statistics_views.FormsCountView.as_view(),
34
        name='api-statistics-forms-count',
35
    ),
29 36
    # provide django.contrib.auth view names for compatibility with
30 37
    # templates created for classic django applications.
31 38
    url(r'^login/$', compat.quixote, name='auth_login'),
32
-