0002-pricing-test-tool-for-agenda-pricing-66892.patch
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>{'foo': 'bar', 'pricing': Decimal('42')}</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>{'error': {'foo': 'error'}}</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 |
- |