0008-pricing-rename-rewrite-get_pricing_data-method-67675.patch
lingo/api/serializers.py | ||
---|---|---|
22 | 22 | |
23 | 23 |
from lingo.agendas.chrono import ChronoError, get_events, get_subscriptions |
24 | 24 |
from lingo.agendas.models import Agenda |
25 |
from lingo.pricing.models import AgendaPricing, PricingError |
|
25 |
from lingo.pricing.models import AgendaPricing, AgendaPricingNotFound, PricingError
|
|
26 | 26 | |
27 | 27 | |
28 | 28 |
class CommaSeparatedStringField(serializers.ListField): |
... | ... | |
119 | 119 |
event_slugs = sorted(self.serialized_events.keys()) |
120 | 120 |
for event_slug in event_slugs: |
121 | 121 |
serialized_event = self.serialized_events[event_slug] |
122 |
start_date = datetime.datetime.fromisoformat(serialized_event['start_datetime']).date() |
|
123 |
agenda = self.agendas[serialized_event['agenda']] |
|
122 | 124 |
try: |
123 |
pricing_data = AgendaPricing.get_pricing_data( |
|
125 |
agenda_pricing = AgendaPricing.get_agenda_pricing(agenda=agenda, start_date=start_date) |
|
126 |
pricing_data = agenda_pricing.get_pricing_data_for_event( |
|
124 | 127 |
request=request, |
125 |
agenda=self.agendas[serialized_event['agenda']],
|
|
128 |
agenda=agenda,
|
|
126 | 129 |
event=serialized_event, |
127 | 130 |
subscription=self.event_subscriptions[event_slug], |
128 | 131 |
check_status={ |
... | ... | |
139 | 142 |
'pricing_data': pricing_data, |
140 | 143 |
} |
141 | 144 |
) |
145 |
except AgendaPricingNotFound: |
|
146 |
result.append( |
|
147 |
{'event': event_slug, 'error': _('No agenda pricing found for event %s') % event_slug} |
|
148 |
) |
|
142 | 149 |
except PricingError as e: |
143 | 150 |
result.append({'event': event_slug, 'error': e.details}) |
144 | 151 |
lingo/pricing/forms.py | ||
---|---|---|
386 | 386 |
if not self.serialized_event or not self.serialized_subscription: |
387 | 387 |
return |
388 | 388 |
try: |
389 |
return AgendaPricing.get_pricing_data(
|
|
389 |
return self.agenda_pricing.get_pricing_data_for_event(
|
|
390 | 390 |
request=self.request, |
391 | 391 |
agenda=self.agenda, |
392 |
agenda_pricing=self.agenda_pricing, |
|
393 | 392 |
event=self.serialized_event, |
394 | 393 |
subscription=self.serialized_subscription, |
395 | 394 |
check_status={ |
lingo/pricing/models.py | ||
---|---|---|
16 | 16 | |
17 | 17 |
import copy |
18 | 18 |
import dataclasses |
19 |
import datetime |
|
20 | 19 |
import decimal |
21 | 20 |
from typing import List |
22 | 21 | |
... | ... | |
400 | 399 | |
401 | 400 |
return created, agenda_pricing |
402 | 401 | |
403 |
@staticmethod |
|
404 |
def get_pricing_data( |
|
405 |
request, |
|
406 |
agenda, |
|
407 |
event, |
|
408 |
subscription, |
|
409 |
check_status, |
|
410 |
booking, |
|
411 |
user_external_id, |
|
412 |
adult_external_id, |
|
413 |
agenda_pricing=None, |
|
402 |
def get_pricing_data_for_event( |
|
403 |
self, request, agenda, event, subscription, check_status, booking, user_external_id, adult_external_id |
|
414 | 404 |
): |
415 |
agenda_pricing = agenda_pricing or AgendaPricing.get_agenda_pricing(agenda=agenda, event=event)
|
|
405 |
# compute pricing for an event
|
|
416 | 406 |
data = { |
417 | 407 |
'event': event, |
418 | 408 |
'subscription': subscription, |
419 | 409 |
'booking': booking, |
420 | 410 |
} |
421 |
context = agenda_pricing.get_pricing_context(
|
|
411 |
context = self.get_pricing_context(
|
|
422 | 412 |
request=request, |
423 | 413 |
data=data, |
424 | 414 |
user_external_id=user_external_id, |
425 | 415 |
adult_external_id=adult_external_id, |
426 | 416 |
) |
427 |
pricing, criterias = agenda_pricing.compute_pricing(context=context)
|
|
428 |
modifier = agenda_pricing.get_booking_modifier(agenda=agenda, check_status=check_status)
|
|
429 |
return agenda_pricing.aggregate_pricing_data(
|
|
417 |
pricing, criterias = self.compute_pricing(context=context)
|
|
418 |
modifier = self.get_booking_modifier(agenda=agenda, check_status=check_status)
|
|
419 |
return self.aggregate_pricing_data(
|
|
430 | 420 |
pricing=pricing, criterias=criterias, context=context, modifier=modifier |
431 | 421 |
) |
432 | 422 | |
... | ... | |
446 | 436 |
} |
447 | 437 | |
448 | 438 |
@staticmethod |
449 |
def get_agenda_pricing(agenda, event): |
|
450 |
start_datetime = datetime.datetime.fromisoformat(event['start_datetime']) |
|
439 |
def get_agenda_pricing(agenda, start_date): |
|
451 | 440 |
try: |
452 | 441 |
return agenda.agendapricings.get( |
453 |
date_start__lte=start_datetime,
|
|
454 |
date_end__gt=start_datetime,
|
|
442 |
date_start__lte=start_date, |
|
443 |
date_end__gt=start_date, |
|
455 | 444 |
) |
456 | 445 |
except (AgendaPricing.DoesNotExist, AgendaPricing.MultipleObjectsReturned): |
457 | 446 |
raise AgendaPricingNotFound |
tests/api/test_pricing.py | ||
---|---|---|
1 |
import datetime |
|
1 | 2 |
from unittest import mock |
2 | 3 | |
3 | 4 |
import pytest |
4 | 5 | |
5 | 6 |
from lingo.agendas.chrono import ChronoError |
6 | 7 |
from lingo.agendas.models import Agenda |
7 |
from lingo.pricing.models import PricingError |
|
8 |
from lingo.pricing.models import AgendaPricing, Pricing, PricingError
|
|
8 | 9 | |
9 | 10 |
pytestmark = pytest.mark.django_db |
10 | 11 | |
... | ... | |
145 | 146 | |
146 | 147 |
@mock.patch('lingo.api.serializers.get_events') |
147 | 148 |
@mock.patch('lingo.api.serializers.get_subscriptions') |
148 |
@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data') |
|
149 |
def test_pricing_compute(mock_pricing_data, mock_subscriptions, mock_events, app, user): |
|
149 |
@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data_for_event')
|
|
150 |
def test_pricing_compute(mock_pricing_data_event, mock_subscriptions, mock_events, app, user):
|
|
150 | 151 |
agenda = Agenda.objects.create(label='Agenda') |
151 | 152 |
agenda2 = Agenda.objects.create(label='Agenda2') |
152 | 153 |
app.authorization = ('Basic', ('john.doe', 'password')) |
153 | 154 | |
155 |
# no subscription |
|
154 | 156 |
mock_events.return_value = [ |
155 | 157 |
{'start_datetime': '2021-09-01T12:00:00+02:00', 'agenda': 'agenda', 'slug': 'event-bar-slug'} |
156 | 158 |
] |
... | ... | |
169 | 171 |
assert resp.json['errors']['user_external_id'] == [ |
170 | 172 |
'No subscription found for event agenda@event-bar-slug' |
171 | 173 |
] |
172 |
assert mock_pricing_data.call_args_list == [] |
|
174 |
assert mock_pricing_data_event.call_args_list == []
|
|
173 | 175 |
assert mock_subscriptions.call_args_list == [mock.call('agenda', 'user:1')] |
174 | 176 | |
177 |
# no matching subscription |
|
175 | 178 |
mock_subscriptions.reset_mock() |
176 | 179 |
mock_subscriptions.return_value = [ |
177 | 180 |
{ |
... | ... | |
197 | 200 |
assert resp.json['errors']['user_external_id'] == [ |
198 | 201 |
'No subscription found for event agenda@event-bar-slug' |
199 | 202 |
] |
200 |
assert mock_pricing_data.call_args_list == [] |
|
203 |
assert mock_pricing_data_event.call_args_list == []
|
|
201 | 204 |
assert mock_subscriptions.call_args_list == [mock.call('agenda', 'user:1')] |
202 | 205 | |
206 |
# no matching subscription |
|
203 | 207 |
mock_events.return_value = [ |
204 | 208 |
{'start_datetime': '2021-09-02T12:00:00+02:00', 'agenda': 'agenda', 'slug': 'event-bar-slug'}, |
205 | 209 |
{'start_datetime': '2021-09-01T12:00:00+02:00', 'agenda': 'agenda2', 'slug': 'event-baz-slug'}, |
... | ... | |
229 | 233 |
assert resp.json['errors']['user_external_id'] == [ |
230 | 234 |
'No subscription found for event agenda2@event-baz-slug' |
231 | 235 |
] |
232 |
assert mock_pricing_data.call_args_list == [] |
|
236 |
assert mock_pricing_data_event.call_args_list == []
|
|
233 | 237 |
assert mock_subscriptions.call_args_list == [ |
234 | 238 |
mock.call('agenda', 'user:1'), |
235 | 239 |
mock.call('agenda2', 'user:1'), |
236 | 240 |
] |
237 | 241 | |
242 |
# no agenda_pricing found |
|
238 | 243 |
mock_subscriptions.return_value = [ |
239 | 244 |
{ |
240 | 245 |
'date_start': '2021-08-01', |
... | ... | |
249 | 254 |
'date_end': '2021-09-03', |
250 | 255 |
}, |
251 | 256 |
] |
252 |
mock_pricing_data.side_effect = [ |
|
257 |
mock_pricing_data_event.side_effect = [
|
|
253 | 258 |
{'foo': 'baz'}, |
254 | 259 |
{'foo': 'bar'}, |
255 | 260 |
] |
... | ... | |
261 | 266 |
'adult_external_id': 'adult:1', |
262 | 267 |
}, |
263 | 268 |
) |
269 |
assert resp.json['data'] == [ |
|
270 |
{ |
|
271 |
'event': 'agenda2@event-baz-slug', |
|
272 |
'error': 'No agenda pricing found for event agenda2@event-baz-slug', |
|
273 |
}, |
|
274 |
{ |
|
275 |
'event': 'agenda@event-bar-slug', |
|
276 |
'error': 'No agenda pricing found for event agenda@event-bar-slug', |
|
277 |
}, |
|
278 |
] |
|
279 | ||
280 |
# ok |
|
281 |
pricing = Pricing.objects.create(label='Foo bar') |
|
282 |
agenda_pricing = AgendaPricing.objects.create( |
|
283 |
pricing=pricing, |
|
284 |
date_start=datetime.date(year=2021, month=9, day=1), |
|
285 |
date_end=datetime.date(year=2021, month=10, day=1), |
|
286 |
) |
|
287 |
agenda_pricing.agendas.add(agenda, agenda2) |
|
288 |
resp = app.get( |
|
289 |
'/api/pricing/compute/', |
|
290 |
params={ |
|
291 |
'slots': 'agenda@event-bar-slug, agenda2@event-baz-slug', |
|
292 |
'user_external_id': 'user:1', |
|
293 |
'adult_external_id': 'adult:1', |
|
294 |
}, |
|
295 |
) |
|
264 | 296 |
assert resp.json['data'] == [ |
265 | 297 |
{'event': 'agenda2@event-baz-slug', 'pricing_data': {'foo': 'baz'}}, |
266 | 298 |
{'event': 'agenda@event-bar-slug', 'pricing_data': {'foo': 'bar'}}, |
267 | 299 |
] |
268 |
assert mock_pricing_data.call_args_list == [ |
|
300 |
assert mock_pricing_data_event.call_args_list == [
|
|
269 | 301 |
mock.call( |
270 | 302 |
request=mock.ANY, |
271 | 303 |
agenda=agenda2, |
... | ... | |
296 | 328 |
), |
297 | 329 |
] |
298 | 330 | |
299 |
mock_pricing_data.side_effect = [ |
|
331 |
# get_pricing_data_for_event with error |
|
332 |
mock_pricing_data_event.side_effect = [ |
|
300 | 333 |
PricingError(details={'foo': 'error'}), |
301 | 334 |
{'foo': 'bar'}, |
302 | 335 |
] |
tests/pricing/manager/test_agenda_pricing.py | ||
---|---|---|
821 | 821 | |
822 | 822 |
@mock.patch('lingo.pricing.forms.get_event') |
823 | 823 |
@mock.patch('lingo.pricing.forms.get_subscriptions') |
824 |
@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data') |
|
825 |
def test_detail_agenda_pricing_test_tool(mock_pricing_data, mock_subscriptions, mock_event, app, admin_user): |
|
824 |
@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data_for_event') |
|
825 |
def test_detail_agenda_pricing_test_tool( |
|
826 |
mock_pricing_data_event, mock_subscriptions, mock_event, app, admin_user |
|
827 |
): |
|
826 | 828 |
agenda = Agenda.objects.create(label='Foo bar') |
827 | 829 |
pricing = Pricing.objects.create(label='Foo bar') |
828 | 830 |
agenda_pricing = AgendaPricing.objects.create( |
... | ... | |
847 | 849 |
assert resp.context['test_tool_form'].errors['event_slug'] == [ |
848 | 850 |
'This event takes place outside the period covered by this pricing' |
849 | 851 |
] |
850 |
assert mock_pricing_data.call_args_list == [] |
|
852 |
assert mock_pricing_data_event.call_args_list == []
|
|
851 | 853 |
mock_event.return_value = {'start_datetime': '2021-10-01T12:00:00+02:00'} |
852 | 854 |
resp = resp.form.submit().follow() |
853 | 855 |
assert 'Computed pricing data' not in resp |
... | ... | |
870 | 872 |
mock_event.reset_mock() |
871 | 873 |
resp.form['event_slug'] = 'foo-bar@foo' |
872 | 874 |
resp = resp.form.submit().follow() |
873 |
assert mock_pricing_data.call_args_list == [] |
|
875 |
assert mock_pricing_data_event.call_args_list == []
|
|
874 | 876 |
assert 'Computed pricing data' not in resp |
875 | 877 |
assert resp.context['test_tool_form'].errors['user_external_id'] == [ |
876 | 878 |
'No subscription found for this event' |
... | ... | |
906 | 908 |
'date_end': '2021-09-03', |
907 | 909 |
}, |
908 | 910 |
] |
909 |
mock_pricing_data.return_value = {'foo': 'bar', 'pricing': Decimal('42')} |
|
911 |
mock_pricing_data_event.return_value = {'foo': 'bar', 'pricing': Decimal('42')}
|
|
910 | 912 |
resp = resp.form.submit().follow() |
911 | 913 |
assert 'Computed pricing data' in resp |
912 |
assert mock_pricing_data.call_args_list == [ |
|
914 |
assert mock_pricing_data_event.call_args_list == [
|
|
913 | 915 |
mock.call( |
914 | 916 |
request=mock.ANY, |
915 | 917 |
agenda=agenda, |
916 |
agenda_pricing=agenda_pricing, |
|
917 | 918 |
event={'start_datetime': '2021-09-01T12:00:00+02:00'}, |
918 | 919 |
subscription={'date_start': '2021-09-01', 'date_end': '2021-09-02'}, |
919 | 920 |
check_status={'status': 'presence', 'check_type': None}, |
... | ... | |
925 | 926 |
assert '<p>Pricing: 42.00</p>' in resp |
926 | 927 |
assert '<pre>{'foo': 'bar', 'pricing': Decimal('42')}</pre>' in resp |
927 | 928 | |
928 |
mock_pricing_data.side_effect = PricingError(details={'foo': 'error'}) |
|
929 |
mock_pricing_data_event.side_effect = PricingError(details={'foo': 'error'})
|
|
929 | 930 |
resp = resp.form.submit().follow() |
930 | 931 |
assert 'Computed pricing data' in resp |
931 | 932 |
assert '<pre>{'error': {'foo': 'error'}}</pre>' in resp |
... | ... | |
982 | 983 | |
983 | 984 |
@mock.patch('lingo.pricing.forms.get_event') |
984 | 985 |
@mock.patch('lingo.pricing.forms.get_subscriptions') |
985 |
@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data') |
|
986 |
@mock.patch('lingo.pricing.models.AgendaPricing.get_pricing_data_for_event')
|
|
986 | 987 |
def test_detail_agenda_pricing_test_tool_booking_status( |
987 |
mock_pricing_data, mock_subscriptions, mock_event, app, admin_user |
|
988 |
mock_pricing_data_event, mock_subscriptions, mock_event, app, admin_user
|
|
988 | 989 |
): |
989 | 990 |
agenda = Agenda.objects.create(label='Foo bar') |
990 | 991 |
pricing = Pricing.objects.create(label='Foo bar') |
... | ... | |
1032 | 1033 |
('absence', False, 'Absence'), |
1033 | 1034 |
('absence::foo-absence-reason', False, 'Absence (Foo absence reason)'), |
1034 | 1035 |
] |
1035 |
assert mock_pricing_data.call_args_list[0][1]['check_status'] == { |
|
1036 |
assert mock_pricing_data_event.call_args_list[0][1]['check_status'] == {
|
|
1036 | 1037 |
'check_type': None, |
1037 | 1038 |
'status': 'presence', |
1038 | 1039 |
} |
1039 | 1040 | |
1040 |
mock_pricing_data.reset_mock() |
|
1041 |
mock_pricing_data_event.reset_mock()
|
|
1041 | 1042 |
resp.form['booking_status'] = 'presence::foo-presence-reason' |
1042 | 1043 |
resp = resp.form.submit().follow() |
1043 |
assert mock_pricing_data.call_args_list[0][1]['check_status'] == { |
|
1044 |
assert mock_pricing_data_event.call_args_list[0][1]['check_status'] == {
|
|
1044 | 1045 |
'check_type': 'foo-presence-reason', |
1045 | 1046 |
'status': 'presence', |
1046 | 1047 |
} |
1047 | 1048 | |
1048 |
mock_pricing_data.reset_mock() |
|
1049 |
mock_pricing_data_event.reset_mock()
|
|
1049 | 1050 |
resp.form['booking_status'] = 'absence' |
1050 | 1051 |
resp = resp.form.submit().follow() |
1051 |
assert mock_pricing_data.call_args_list[0][1]['check_status'] == {'check_type': None, 'status': 'absence'} |
|
1052 |
assert mock_pricing_data_event.call_args_list[0][1]['check_status'] == { |
|
1053 |
'check_type': None, |
|
1054 |
'status': 'absence', |
|
1055 |
} |
|
1052 | 1056 | |
1053 |
mock_pricing_data.reset_mock() |
|
1057 |
mock_pricing_data_event.reset_mock()
|
|
1054 | 1058 |
resp.form['booking_status'] = 'absence::foo-absence-reason' |
1055 | 1059 |
resp = resp.form.submit().follow() |
1056 |
assert mock_pricing_data.call_args_list[0][1]['check_status'] == { |
|
1060 |
assert mock_pricing_data_event.call_args_list[0][1]['check_status'] == {
|
|
1057 | 1061 |
'check_type': 'foo-absence-reason', |
1058 | 1062 |
'status': 'absence', |
1059 | 1063 |
} |
tests/pricing/test_models.py | ||
---|---|---|
283 | 283 |
def test_get_agenda_pricing(): |
284 | 284 |
agenda = Agenda.objects.create(label='Foo bar') |
285 | 285 |
pricing = Pricing.objects.create(label='Foo bar') |
286 |
event = { |
|
287 |
'start_datetime': make_aware(datetime.datetime(2021, 9, 15, 12, 00)).isoformat(), |
|
288 |
} |
|
286 |
start_date = datetime.datetime(2021, 9, 15) |
|
289 | 287 | |
290 | 288 |
# not found |
291 | 289 |
with pytest.raises(AgendaPricingNotFound): |
292 |
AgendaPricing.get_agenda_pricing(agenda=agenda, event=event)
|
|
290 |
AgendaPricing.get_agenda_pricing(agenda=agenda, start_date=start_date)
|
|
293 | 291 | |
294 | 292 |
# ok |
295 | 293 |
agenda_pricing = AgendaPricing.objects.create( |
... | ... | |
298 | 296 |
date_end=datetime.date(year=2021, month=10, day=1), |
299 | 297 |
) |
300 | 298 |
agenda_pricing.agendas.add(agenda) |
301 |
assert AgendaPricing.get_agenda_pricing(agenda=agenda, event=event) == agenda_pricing
|
|
299 |
assert AgendaPricing.get_agenda_pricing(agenda=agenda, start_date=start_date) == agenda_pricing
|
|
302 | 300 | |
303 | 301 |
# more than one matching |
304 | 302 |
agenda_pricing = AgendaPricing.objects.create( |
... | ... | |
308 | 306 |
) |
309 | 307 |
agenda_pricing.agendas.add(agenda) |
310 | 308 |
with pytest.raises(AgendaPricingNotFound): |
311 |
AgendaPricing.get_agenda_pricing(agenda=agenda, event=event)
|
|
309 |
AgendaPricing.get_agenda_pricing(agenda=agenda, start_date=start_date)
|
|
312 | 310 | |
313 | 311 | |
314 | 312 |
@pytest.mark.parametrize( |
315 | 313 |
'event_date, found', |
316 | 314 |
[ |
317 | 315 |
# just before first day |
318 |
((2021, 8, 31, 12, 00), False),
|
|
316 |
((2021, 8, 31), False), |
|
319 | 317 |
# first day |
320 |
((2021, 9, 1, 12, 00), True),
|
|
318 |
((2021, 9, 1), True), |
|
321 | 319 |
# last day |
322 |
((2021, 9, 30, 12, 00), True),
|
|
320 |
((2021, 9, 30), True), |
|
323 | 321 |
# just after last day |
324 |
((2021, 10, 1, 12, 00), False),
|
|
322 |
((2021, 10, 1), False), |
|
325 | 323 |
], |
326 | 324 |
) |
327 | 325 |
def test_get_agenda_pricing_event_date(event_date, found): |
328 | 326 |
agenda = Agenda.objects.create(label='Foo bar') |
329 | 327 |
pricing = Pricing.objects.create(label='Foo bar') |
330 |
event = { |
|
331 |
'start_datetime': make_aware(datetime.datetime(*event_date)).isoformat(), |
|
332 |
} |
|
333 | 328 |
agenda_pricing = AgendaPricing.objects.create( |
334 | 329 |
pricing=pricing, |
335 | 330 |
date_start=datetime.date(year=2021, month=9, day=1), |
336 | 331 |
date_end=datetime.date(year=2021, month=10, day=1), |
337 | 332 |
) |
338 | 333 |
agenda_pricing.agendas.add(agenda) |
334 |
start_date = datetime.date(*event_date) |
|
339 | 335 |
if found: |
340 |
assert AgendaPricing.get_agenda_pricing(agenda=agenda, event=event) == agenda_pricing
|
|
336 |
assert AgendaPricing.get_agenda_pricing(agenda=agenda, start_date=start_date) == agenda_pricing
|
|
341 | 337 |
else: |
342 | 338 |
with pytest.raises(AgendaPricingNotFound): |
343 |
AgendaPricing.get_agenda_pricing(agenda=agenda, event=event)
|
|
339 |
AgendaPricing.get_agenda_pricing(agenda=agenda, start_date=start_date)
|
|
344 | 340 | |
345 | 341 | |
346 | 342 |
@mock.patch('requests.Session.send', side_effect=mocked_requests_send) |
... | ... | |
931 | 927 |
} |
932 | 928 | |
933 | 929 | |
934 |
def test_get_pricing_data(context): |
|
930 |
def test_get_pricing_data_for_event(context):
|
|
935 | 931 |
agenda = Agenda.objects.create(label='Foo bar') |
936 | 932 |
category = CriteriaCategory.objects.create(label='Foo', slug='foo') |
937 | 933 |
criteria = Criteria.objects.create(label='Bar', slug='bar', condition='True', category=category) |
... | ... | |
953 | 949 |
}, |
954 | 950 |
) |
955 | 951 |
agenda_pricing.agendas.add(agenda) |
956 |
assert AgendaPricing.get_pricing_data(
|
|
952 |
assert agenda_pricing.get_pricing_data_for_event(
|
|
957 | 953 |
request=context['request'], |
958 | 954 |
agenda=agenda, |
959 | 955 |
event={'start_datetime': make_aware(datetime.datetime(2021, 9, 15, 12, 00)).isoformat()}, |
960 |
- |