Projet

Général

Profil

0002-pricing-test-tool-for-agenda-pricing-66892.patch

Lauréline Guérin, 12 juillet 2022 23:21

Télécharger (29,4 ko)

Voir les différences:

Subject: [PATCH 2/2] pricing: test tool for agenda pricing (#66892)

 lingo/agendas/chrono.py                       |  29 ++
 lingo/manager/static/css/style.scss           |   5 +
 lingo/pricing/forms.py                        | 111 +++++++-
 lingo/pricing/models.py                       |  12 +-
 .../manager_agenda_pricing_detail.html        |  25 +-
 lingo/pricing/views.py                        |  10 +
 tests/agendas/test_chrono.py                  | 125 ++++++++-
 tests/pricing/manager/test_agenda.py          | 252 +++++++++++++++++-
 8 files changed, 560 insertions(+), 9 deletions(-)
lingo/agendas/chrono.py
17 17
import json
18 18

  
19 19
from django.conf import settings
20
from django.utils.translation import ugettext_lazy as _
20 21
from requests.exceptions import RequestException
21 22

  
22 23
from lingo.agendas.models import Agenda
23 24
from lingo.utils import requests
24 25

  
25 26

  
27
class ChronoError(Exception):
28
    pass
29

  
30

  
26 31
def is_chrono_enabled():
27 32
    return hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get('chrono')
28 33

  
......
103 108
    for slug, agenda in existing_agendas.items():
104 109
        if slug not in seen_agendas:
105 110
            agenda.delete()
111

  
112

  
113
def get_event(event_slug):
114
    result = get_chrono_json('api/agendas/events/?slots=%s' % event_slug)
115
    if not result:
116
        raise ChronoError(_('Unable to get event details'))
117
    if result.get('err'):
118
        raise ChronoError(_('Unable to get event details (%s)') % result['err_desc'])
119
    if not result.get('data'):
120
        raise ChronoError(_('Unable to get event details'))
121
    return result['data'][0]
122

  
123

  
124
def get_subscriptions(agenda_slug, user_external_id):
125
    result = get_chrono_json(
126
        'api/agenda/%s/subscription/?user_external_id=%s' % (agenda_slug, user_external_id)
127
    )
128
    if not result or not result.get('data'):
129
        raise ChronoError(_('Unable to get subscription details'))
130
    if result.get('err'):
131
        raise ChronoError(_('Unable to get subscription details (%s)') % result['err_desc'])
132
    if not result.get('data'):
133
        raise ChronoError(_('Unable to get subscription details'))
134
    return result['data']
lingo/manager/static/css/style.scss
47 47
ul.objects-list.single-links li a.link::before {
48 48
	content: "\f08e"; /* fa-external-link  */
49 49
}
50

  
51
div.test-tool-result .infonotice h3 {
52
	margin-top: 0;
53
	font-weight: normal;
54
}
lingo/pricing/forms.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import datetime
18

  
17 19
from django import forms
18 20
from django.forms import ValidationError
19 21
from django.template import Template, TemplateSyntaxError
20 22
from django.utils.timezone import now
21 23
from django.utils.translation import ugettext_lazy as _
22 24

  
25
from lingo.agendas.chrono import ChronoError, get_event, get_subscriptions
23 26
from lingo.agendas.models import CheckType
24
from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory
27
from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, PricingError
25 28

  
26 29

  
27 30
class ExportForm(forms.Form):
......
162 165
            self.fields['crit_%i' % i] = forms.DecimalField(required=True, max_digits=5, decimal_places=2)
163 166

  
164 167

  
168
class PricingTestToolForm(forms.Form):
169
    event_slug = forms.CharField(label=_('Event identifier'))
170
    user_external_id = forms.CharField(label=_('User external identifier'))
171
    adult_external_id = forms.CharField(label=_('Adult external identifier'))
172
    booking_status = forms.ChoiceField(label=_('Booking status'), choices=[])
173

  
174
    def __init__(self, *args, **kwargs):
175
        self.request = kwargs.pop('request')
176
        self.agenda_pricing = kwargs.pop('agenda_pricing')
177
        self.agenda = self.agenda_pricing.agenda
178
        self.serialized_event = None
179
        self.serialized_subscription = None
180
        self.check_type_slug = None
181
        self.booking_status = None
182
        super().__init__(*args, **kwargs)
183
        presence_check_types = (
184
            self.agenda.check_type_group.check_types.presences() if self.agenda.check_type_group else []
185
        )
186
        absence_check_types = (
187
            self.agenda.check_type_group.check_types.absences() if self.agenda.check_type_group else []
188
        )
189
        status_choices = [
190
            ('presence', _('Presence')),
191
        ]
192
        status_choices += [
193
            ('presence::%s' % ct.slug, _('Presence (%s)') % ct.label) for ct in presence_check_types
194
        ]
195
        status_choices += [('absence', _('Absence'))]
196
        status_choices += [
197
            ('absence::%s' % ct.slug, _('Absence (%s)') % ct.label) for ct in absence_check_types
198
        ]
199
        self.fields['booking_status'].choices = status_choices
200

  
201
    def clean_event_slug(self):
202
        original_event_slug = self.cleaned_data['event_slug']
203
        event_slug = original_event_slug
204
        if '@' not in event_slug:
205
            # chrono's endpoint takes event ids like '<agenda_slug>@<event_slug>'
206
            event_slug = '%s@%s' % (self.agenda.slug, event_slug)
207
        elif event_slug.split('@')[0] != self.agenda.slug:
208
            raise ValidationError(_('The agenda identifier is wrong (%s)') % event_slug.split('@')[0])
209
        try:
210
            self.serialized_event = get_event(event_slug)
211
        except ChronoError as e:
212
            raise forms.ValidationError(str(e).replace(event_slug, original_event_slug))
213

  
214
        event_date = datetime.datetime.fromisoformat(self.serialized_event['start_datetime']).date()
215
        if event_date < self.agenda_pricing.date_start or event_date >= self.agenda_pricing.date_end:
216
            raise ValidationError(_('This event takes place outside the period covered by this pricing'))
217

  
218
        return original_event_slug
219

  
220
    def clean_booking_status(self):
221
        original_booking_status = self.cleaned_data['booking_status']
222
        self.booking_status = original_booking_status
223
        if '::' in original_booking_status:
224
            # split value to get booking status and selected check_type
225
            self.booking_status, self.check_type_slug = original_booking_status.split('::')
226
        return original_booking_status
227

  
228
    def get_subscription(self, user_external_id):
229
        start_date = datetime.datetime.fromisoformat(self.serialized_event['start_datetime']).date()
230
        end_date = start_date + datetime.timedelta(days=1)
231
        try:
232
            subscriptions = get_subscriptions(self.agenda.slug, user_external_id)
233
        except ChronoError as e:
234
            self.add_error('user_external_id', str(e))
235
            return
236
        for subscription in subscriptions:
237
            sub_start_date = datetime.date.fromisoformat(subscription['date_start'])
238
            sub_end_date = datetime.date.fromisoformat(subscription['date_end'])
239
            if sub_start_date >= end_date:
240
                continue
241
            if sub_end_date <= start_date:
242
                continue
243
            return subscription
244
        self.add_error('user_external_id', _('No subscription found for this event'))
245

  
246
    def clean(self):
247
        super().clean()
248
        if self.cleaned_data.get('user_external_id') and self.serialized_event:
249
            user_external_id = self.cleaned_data['user_external_id']
250
            self.serialized_subscription = self.get_subscription(user_external_id)
251

  
252
    def compute(self):
253
        if not self.serialized_event or not self.serialized_subscription:
254
            return
255
        try:
256
            return AgendaPricing.get_pricing_data(
257
                request=self.request,
258
                agenda=self.agenda,
259
                agenda_pricing=self.agenda_pricing,
260
                event=self.serialized_event,
261
                subscription=self.serialized_subscription,
262
                check_status={
263
                    'status': self.booking_status,
264
                    'check_type': self.check_type_slug,
265
                },
266
                booking={},
267
                user_external_id=self.cleaned_data['user_external_id'],
268
                adult_external_id=self.cleaned_data['adult_external_id'],
269
            )
270
        except PricingError as e:
271
            return {'error': e.details}
272

  
273

  
165 274
class NewCheckTypeForm(forms.ModelForm):
166 275
    class Meta:
167 276
        model = CheckType
lingo/pricing/models.py
363 363

  
364 364
    @staticmethod
365 365
    def get_pricing_data(
366
        request, agenda, event, subscription, check_status, booking, user_external_id, adult_external_id
366
        request,
367
        agenda,
368
        event,
369
        subscription,
370
        check_status,
371
        booking,
372
        user_external_id,
373
        adult_external_id,
374
        agenda_pricing=None,
367 375
    ):
368
        agenda_pricing = AgendaPricing.get_agenda_pricing(agenda=agenda, event=event)
376
        agenda_pricing = agenda_pricing or AgendaPricing.get_agenda_pricing(agenda=agenda, event=event)
369 377
        data = {
370 378
            'event': event,
371 379
            'subscription': subscription,
lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html
20 20
{% endblock %}
21 21

  
22 22
{% block content %}
23
{% for matrix in object.iter_pricing_matrix %}
24 23
<div class="section">
24
  <h3>{% trans "Test tool" %}</h3>
25
<div>
26
  <form method="get" enctype="multipart/form-data">
27
    {{ test_tool_form.as_p }}
28
    <div class="buttons">
29
      <button class="submit-button">{% trans "Compute" %}</button>
30
    </div>
31
  </form>
32
  {% if request.GET and test_tool_form.is_valid %}
33
  {% with test_tool_form.compute as pricing_data %}
34
  <div class="test-tool-result">
35
    <div class="infonotice">
36
        <h3>{% trans "Computed pricing data" %}</h3>
37
        {% if pricing_data.pricing is not None %}<p>{% trans "Pricing:" %} {{ pricing_data.pricing|stringformat:".2f" }}</p>{% endif %}
38
        <pre>{{ pricing_data|pprint }}</pre>
39
    </div>
40
  </div>
41
  {% endwith %}
42
  {% endif %}
43
</div>
44
</div>
45

  
46
{% for matrix in object.iter_pricing_matrix %}
47
<div class="section pricing-matrix">
25 48
  {% if matrix.criteria %}<h3>{{ matrix.criteria.label }}</h3>{% endif %}
26 49
<div>
27 50
  <table class="main pricing-matrix-{{ matrix.criteria.slug }}">
lingo/pricing/views.py
54 54
    PricingCriteriaCategoryEditForm,
55 55
    PricingDuplicateForm,
56 56
    PricingMatrixForm,
57
    PricingTestToolForm,
57 58
    PricingVariableFormSet,
58 59
)
59 60
from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingCriteriaCategory
......
717 718
            'pricing__criterias__category'
718 719
        )
719 720

  
721
    def get_context_data(self, **kwargs):
722
        form = PricingTestToolForm(
723
            agenda_pricing=self.object, request=self.request, data=self.request.GET or None
724
        )
725
        if self.request.GET:
726
            form.is_valid()
727
        kwargs['test_tool_form'] = form
728
        return super().get_context_data(**kwargs)
729

  
720 730

  
721 731
agenda_pricing_detail = AgendaPricingDetailView.as_view()
722 732

  
tests/agendas/test_chrono.py
5 5
from requests.exceptions import ConnectionError
6 6
from requests.models import Response
7 7

  
8
from lingo.agendas.chrono import collect_agenda_data, refresh_agendas
8
from lingo.agendas.chrono import (
9
    ChronoError,
10
    collect_agenda_data,
11
    get_event,
12
    get_subscriptions,
13
    refresh_agendas,
14
)
9 15
from lingo.agendas.models import Agenda
10 16

  
11 17
pytestmark = pytest.mark.django_db
......
151 157
    mock_collect.return_value = []
152 158
    refresh_agendas()
153 159
    assert Agenda.objects.count() == 0
160

  
161

  
162
def test_get_event_no_service(settings):
163
    settings.KNOWN_SERVICES = {}
164
    with pytest.raises(ChronoError) as e:
165
        get_event('foo')
166
    assert str(e.value) == 'Unable to get event details'
167

  
168
    settings.KNOWN_SERVICES = {'other': []}
169
    with pytest.raises(ChronoError) as e:
170
        get_event('foo')
171
    assert str(e.value) == 'Unable to get event details'
172

  
173

  
174
def test_get_event():
175
    with mock.patch('requests.Session.get') as requests_get:
176
        requests_get.side_effect = ConnectionError()
177
        with pytest.raises(ChronoError) as e:
178
            get_event('foo')
179
        assert str(e.value) == 'Unable to get event details'
180

  
181
    with mock.patch('requests.Session.get') as requests_get:
182
        mock_resp = Response()
183
        mock_resp.status_code = 500
184
        requests_get.return_value = mock_resp
185
        with pytest.raises(ChronoError) as e:
186
            get_event('foo')
187
        assert str(e.value) == 'Unable to get event details'
188

  
189
    with mock.patch('requests.Session.get') as requests_get:
190
        mock_resp = Response()
191
        mock_resp.status_code = 404
192
        requests_get.return_value = mock_resp
193
        with pytest.raises(ChronoError) as e:
194
            get_event('foo')
195
        assert str(e.value) == 'Unable to get event details'
196

  
197
    with mock.patch('requests.Session.get') as requests_get:
198
        requests_get.return_value = MockedRequestResponse(content=json.dumps({'foo': 'bar'}))
199
        with pytest.raises(ChronoError) as e:
200
            get_event('foo')
201
        assert str(e.value) == 'Unable to get event details'
202

  
203
    data = {'data': []}
204
    with mock.patch('requests.Session.get') as requests_get:
205
        requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
206
        with pytest.raises(ChronoError) as e:
207
            get_event('foo')
208
        assert str(e.value) == 'Unable to get event details'
209
        assert requests_get.call_args_list[0][0] == ('api/agendas/events/?slots=foo',)
210
        assert requests_get.call_args_list[0][1]['remote_service']['url'] == 'http://chrono.example.org'
211

  
212
    data = {'data': ['foo']}
213
    with mock.patch('requests.Session.get') as requests_get:
214
        requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
215
        assert get_event('foo') == 'foo'
216

  
217
    data = {'data': ['foo', 'bar']}  # should not happen
218
    with mock.patch('requests.Session.get') as requests_get:
219
        requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
220
        assert get_event('foo') == 'foo'
221

  
222

  
223
def test_get_subscriptions_no_service(settings):
224
    settings.KNOWN_SERVICES = {}
225
    with pytest.raises(ChronoError) as e:
226
        get_subscriptions('foo', 'user:1')
227
    assert str(e.value) == 'Unable to get subscription details'
228

  
229
    settings.KNOWN_SERVICES = {'other': []}
230
    with pytest.raises(ChronoError) as e:
231
        get_subscriptions('foo', 'user:1')
232
    assert str(e.value) == 'Unable to get subscription details'
233

  
234

  
235
def test_get_subscriptions():
236
    with mock.patch('requests.Session.get') as requests_get:
237
        requests_get.side_effect = ConnectionError()
238
        with pytest.raises(ChronoError) as e:
239
            get_subscriptions('foo', 'user:1')
240
        assert str(e.value) == 'Unable to get subscription details'
241

  
242
    with mock.patch('requests.Session.get') as requests_get:
243
        mock_resp = Response()
244
        mock_resp.status_code = 500
245
        requests_get.return_value = mock_resp
246
        with pytest.raises(ChronoError) as e:
247
            get_subscriptions('foo', 'user:1')
248
        assert str(e.value) == 'Unable to get subscription details'
249

  
250
    with mock.patch('requests.Session.get') as requests_get:
251
        mock_resp = Response()
252
        mock_resp.status_code = 404
253
        requests_get.return_value = mock_resp
254
        with pytest.raises(ChronoError) as e:
255
            get_subscriptions('foo', 'user:1')
256
        assert str(e.value) == 'Unable to get subscription details'
257

  
258
    with mock.patch('requests.Session.get') as requests_get:
259
        requests_get.return_value = MockedRequestResponse(content=json.dumps({'foo': 'bar'}))
260
        with pytest.raises(ChronoError) as e:
261
            get_subscriptions('foo', 'user:1')
262
        assert str(e.value) == 'Unable to get subscription details'
263

  
264
    data = {'data': []}
265
    with mock.patch('requests.Session.get') as requests_get:
266
        requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
267
        with pytest.raises(ChronoError) as e:
268
            get_subscriptions('foo', 'user:1')
269
        assert str(e.value) == 'Unable to get subscription details'
270
        assert requests_get.call_args_list[0][0] == ('api/agenda/foo/subscription/?user_external_id=user:1',)
271
        assert requests_get.call_args_list[0][1]['remote_service']['url'] == 'http://chrono.example.org'
272

  
273
    data = {'data': ['foo', 'bar']}
274
    with mock.patch('requests.Session.get') as requests_get:
275
        requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
276
        assert get_subscriptions('foo', 'user:1') == ['foo', 'bar']
tests/pricing/manager/test_agenda.py
1 1
import datetime
2
from decimal import Decimal
2 3
from unittest import mock
3 4

  
4 5
import pytest
5 6
from django.utils.timezone import now
6 7

  
7
from lingo.agendas.models import Agenda, CheckTypeGroup
8
from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing
8
from lingo.agendas.chrono import ChronoError
9
from lingo.agendas.models import Agenda, CheckType, CheckTypeGroup
10
from lingo.pricing.models import AgendaPricing, Criteria, CriteriaCategory, Pricing, PricingError
9 11
from tests.utils import login
10 12

  
11 13
pytestmark = pytest.mark.django_db
......
353 355

  
354 356
    app = login(app)
355 357
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
356
    assert '<h3>' not in resp
358
    assert len(resp.pyquery.find('div.section.prixing-matrix h3')) == 0
357 359
    ths = resp.pyquery.find('table thead th')
358 360
    assert len(ths) == 4
359 361
    assert ths[0].text is None
......
400 402

  
401 403
    app = login(app)
402 404
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
403
    assert '<h3>' not in resp
405
    assert len(resp.pyquery.find('div.section.prixing-matrix h3')) == 0
404 406
    ths = resp.pyquery.find('table thead')
405 407
    assert len(ths) == 0
406 408
    assert resp.pyquery.find('table tr.pricing-row-crit-3-1 th')[0].text == 'Crit 3-1'
......
413 415
    assert resp.pyquery.find('table tr.pricing-row-crit-3-4 td')[0].text == '114.00'
414 416

  
415 417

  
418
@mock.patch('lingo.pricing.forms.get_event')
419
@mock.patch('lingo.pricing.forms.get_subscriptions')
420
@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data')
421
def test_detail_agenda_pricing_test_tool(mock_pricing_data, mock_subscriptions, mock_event, app, admin_user):
422
    agenda = Agenda.objects.create(label='Foo bar')
423
    pricing = Pricing.objects.create(label='Foo bar')
424
    agenda_pricing = AgendaPricing.objects.create(
425
        agenda=agenda,
426
        pricing=pricing,
427
        date_start=datetime.date(year=2021, month=9, day=1),
428
        date_end=datetime.date(year=2021, month=10, day=1),
429
    )
430

  
431
    app = login(app)
432
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
433
    assert 'Computed pricing data' not in resp
434

  
435
    # check event date
436
    mock_event.return_value = {'start_datetime': '2021-08-31T12:00:00+02:00'}
437
    resp.form['event_slug'] = 'foo'
438
    resp.form['user_external_id'] = 'user:1'
439
    resp.form['adult_external_id'] = 'adult:1'
440
    resp.form['booking_status'] = 'presence'
441
    resp = resp.form.submit()
442
    assert 'Computed pricing data' not in resp
443
    assert resp.context['test_tool_form'].errors['event_slug'] == [
444
        'This event takes place outside the period covered by this pricing'
445
    ]
446
    assert mock_event.call_args_list == [mock.call('foo-bar@foo')]
447
    assert mock_pricing_data.call_args_list == []
448
    mock_event.return_value = {'start_datetime': '2021-10-01T12:00:00+02:00'}
449
    resp = resp.form.submit()
450
    assert 'Computed pricing data' not in resp
451
    assert resp.context['test_tool_form'].errors['event_slug'] == [
452
        'This event takes place outside the period covered by this pricing'
453
    ]
454

  
455
    mock_event.return_value = {'start_datetime': '2021-09-01T12:00:00+02:00'}
456

  
457
    # check event_slug & agenda
458
    resp.form['event_slug'] = 'foo@foo'
459
    resp = resp.form.submit()
460
    assert resp.context['test_tool_form'].errors['event_slug'] == ['The agenda identifier is wrong (foo)']
461

  
462
    # check subscriptions dates
463
    mock_subscriptions.return_value = []
464
    mock_event.reset_mock()
465
    resp.form['event_slug'] = 'foo-bar@foo'
466
    resp = resp.form.submit()
467
    assert mock_event.call_args_list == [mock.call('foo-bar@foo')]
468
    assert mock_pricing_data.call_args_list == []
469
    assert 'Computed pricing data' not in resp
470
    assert resp.context['test_tool_form'].errors['user_external_id'] == [
471
        'No subscription found for this event'
472
    ]
473

  
474
    mock_subscriptions.return_value = [
475
        {
476
            'date_start': '2021-08-01',
477
            'date_end': '2021-09-01',
478
        },
479
        {
480
            'date_start': '2021-09-02',
481
            'date_end': '2021-09-03',
482
        },
483
    ]
484
    resp = resp.form.submit()
485
    assert 'Computed pricing data' not in resp
486
    assert resp.context['test_tool_form'].errors['user_external_id'] == [
487
        'No subscription found for this event'
488
    ]
489

  
490
    mock_subscriptions.return_value = [
491
        {
492
            'date_start': '2021-08-01',
493
            'date_end': '2021-09-01',
494
        },
495
        {
496
            'date_start': '2021-09-01',
497
            'date_end': '2021-09-02',
498
        },
499
        {
500
            'date_start': '2021-09-02',
501
            'date_end': '2021-09-03',
502
        },
503
    ]
504
    mock_pricing_data.return_value = {'foo': 'bar', 'pricing': Decimal('42')}
505
    resp = resp.form.submit()
506
    assert 'Computed pricing data' in resp
507
    assert mock_pricing_data.call_args_list == [
508
        mock.call(
509
            request=mock.ANY,
510
            agenda=agenda,
511
            agenda_pricing=agenda_pricing,
512
            event={'start_datetime': '2021-09-01T12:00:00+02:00'},
513
            subscription={'date_start': '2021-09-01', 'date_end': '2021-09-02'},
514
            check_status={'status': 'presence', 'check_type': None},
515
            booking={},
516
            user_external_id='user:1',
517
            adult_external_id='adult:1',
518
        )
519
    ]
520
    assert '<p>Pricing: 42.00</p>' in resp
521
    assert '<pre>{&#39;foo&#39;: &#39;bar&#39;, &#39;pricing&#39;: Decimal(&#39;42&#39;)}</pre>' in resp
522

  
523
    mock_pricing_data.side_effect = PricingError(details={'foo': 'error'})
524
    resp = resp.form.submit()
525
    assert 'Computed pricing data' in resp
526
    assert '<pre>{&#39;error&#39;: {&#39;foo&#39;: &#39;error&#39;}}</pre>' in resp
527

  
528

  
529
@mock.patch('lingo.pricing.forms.get_event')
530
@mock.patch('lingo.pricing.forms.get_subscriptions')
531
def test_detail_agenda_pricing_test_tool_event_error(mock_subscriptions, mock_event, app, admin_user):
532
    agenda = Agenda.objects.create(label='Foo bar')
533
    pricing = Pricing.objects.create(label='Foo bar')
534
    agenda_pricing = AgendaPricing.objects.create(
535
        agenda=agenda,
536
        pricing=pricing,
537
        date_start=datetime.date(year=2021, month=9, day=1),
538
        date_end=datetime.date(year=2021, month=10, day=1),
539
    )
540

  
541
    mock_event.side_effect = ChronoError('foo bar foo-bar@foo-event')
542

  
543
    app = login(app)
544
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
545
    resp.form['event_slug'] = 'foo-event'
546
    resp.form['user_external_id'] = 'user:1'
547
    resp.form['adult_external_id'] = 'adult:1'
548
    resp.form['booking_status'] = 'presence'
549

  
550
    # agenda slug is removed from error message
551
    resp = resp.form.submit()
552
    assert resp.context['test_tool_form'].errors['event_slug'] == ['foo bar foo-event']
553

  
554
    # except it was included in event_slug
555
    resp.form['event_slug'] = 'foo-bar@foo-event'
556
    resp = resp.form.submit()
557
    assert resp.context['test_tool_form'].errors['event_slug'] == ['foo bar foo-bar@foo-event']
558

  
559

  
560
@mock.patch('lingo.pricing.forms.get_event')
561
@mock.patch('lingo.pricing.forms.get_subscriptions')
562
def test_detail_agenda_pricing_test_tool_subscription_error(mock_subscriptions, mock_event, app, admin_user):
563
    agenda = Agenda.objects.create(label='Foo bar')
564
    pricing = Pricing.objects.create(label='Foo bar')
565
    agenda_pricing = AgendaPricing.objects.create(
566
        agenda=agenda,
567
        pricing=pricing,
568
        date_start=datetime.date(year=2021, month=9, day=1),
569
        date_end=datetime.date(year=2021, month=10, day=1),
570
    )
571

  
572
    mock_event.return_value = {'start_datetime': '2021-09-01T12:00:00+02:00'}
573
    mock_subscriptions.side_effect = ChronoError('foo bar')
574

  
575
    app = login(app)
576
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
577
    resp.form['event_slug'] = 'foo-event'
578
    resp.form['user_external_id'] = 'user:1'
579
    resp.form['adult_external_id'] = 'adult:1'
580
    resp.form['booking_status'] = 'presence'
581
    resp = resp.form.submit()
582
    assert resp.context['test_tool_form'].errors['user_external_id'] == ['foo bar']
583

  
584

  
585
@mock.patch('lingo.pricing.forms.get_event')
586
@mock.patch('lingo.pricing.forms.get_subscriptions')
587
@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data')
588
def test_detail_agenda_pricing_test_tool_booking_status(
589
    mock_pricing_data, mock_subscriptions, mock_event, app, admin_user
590
):
591
    agenda = Agenda.objects.create(label='Foo bar')
592
    pricing = Pricing.objects.create(label='Foo bar')
593
    agenda_pricing = AgendaPricing.objects.create(
594
        agenda=agenda,
595
        pricing=pricing,
596
        date_start=datetime.date(year=2021, month=9, day=1),
597
        date_end=datetime.date(year=2021, month=10, day=1),
598
    )
599

  
600
    mock_event.return_value = {'start_datetime': '2021-09-01T12:00:00+02:00'}
601
    mock_subscriptions.return_value = [
602
        {
603
            'date_start': '2021-09-01',
604
            'date_end': '2021-09-02',
605
        },
606
    ]
607

  
608
    app = login(app)
609
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
610
    assert resp.form['booking_status'].options == [
611
        ('presence', False, 'Presence'),
612
        ('absence', False, 'Absence'),
613
    ]
614

  
615
    group = CheckTypeGroup.objects.create(label='Foo bar')
616
    CheckType.objects.create(label='Foo presence reason', group=group, kind='presence')
617
    CheckType.objects.create(label='Foo absence reason', group=group, kind='absence')
618
    agenda.check_type_group = group
619
    agenda.save()
620

  
621
    resp = app.get('/manage/pricing/agenda/%s/pricing/%s/' % (agenda.pk, agenda_pricing.pk))
622
    assert resp.form['booking_status'].options == [
623
        ('presence', False, 'Presence'),
624
        ('presence::foo-presence-reason', False, 'Presence (Foo presence reason)'),
625
        ('absence', False, 'Absence'),
626
        ('absence::foo-absence-reason', False, 'Absence (Foo absence reason)'),
627
    ]
628
    resp.form['event_slug'] = 'foo'
629
    resp.form['user_external_id'] = 'user:1'
630
    resp.form['adult_external_id'] = 'adult:1'
631
    resp.form['booking_status'] = 'presence'
632
    resp = resp.form.submit()
633
    assert mock_pricing_data.call_args_list[0][1]['check_status'] == {
634
        'check_type': None,
635
        'status': 'presence',
636
    }
637

  
638
    mock_pricing_data.reset_mock()
639
    resp.form['booking_status'] = 'presence::foo-presence-reason'
640
    resp = resp.form.submit()
641
    assert mock_pricing_data.call_args_list[0][1]['check_status'] == {
642
        'check_type': 'foo-presence-reason',
643
        'status': 'presence',
644
    }
645

  
646
    mock_pricing_data.reset_mock()
647
    resp.form['booking_status'] = 'absence'
648
    resp = resp.form.submit()
649
    assert mock_pricing_data.call_args_list[0][1]['check_status'] == {'check_type': None, 'status': 'absence'}
650

  
651
    mock_pricing_data.reset_mock()
652
    resp.form['booking_status'] = 'absence::foo-absence-reason'
653
    resp = resp.form.submit()
654
    assert mock_pricing_data.call_args_list[0][1]['check_status'] == {
655
        'check_type': 'foo-absence-reason',
656
        'status': 'absence',
657
    }
658

  
659

  
416 660
def test_edit_agenda_pricing_matrix_3_categories(app, admin_user):
417 661
    category1 = CriteriaCategory.objects.create(label='Cat 1')
418 662
    criteria11 = Criteria.objects.create(label='Crit 1-1', slug='crit-1-1', category=category1, order=1)
419
-