callback sur les réservations lorsqu'un événement est marqué pointé (#75896) #63
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 3.2.16 on 2023-03-31 06:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('agendas', '0151_event_invoiced'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='booking',
|
||||||
|
name='absence_callback_url',
|
||||||
|
field=models.URLField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='booking',
|
||||||
|
name='presence_callback_url',
|
||||||
|
field=models.URLField(blank=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -20,6 +20,7 @@ import dataclasses
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
|
import logging
|
||||||
import math
|
import math
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
@ -1595,6 +1596,45 @@ class Event(models.Model):
|
||||||
return
|
return
|
||||||
self.checked = True
|
self.checked = True
|
||||||
self.save(update_fields=['checked'])
|
self.save(update_fields=['checked'])
|
||||||
|
self.async_notify_checked()
|
||||||
|
|
||||||
|
def async_notify_checked(self):
|
||||||
|
if 'uwsgi' in sys.modules:
|
||||||
|
from chrono.utils.spooler import event_notify_checked
|
||||||
|
|
||||||
|
tenant = getattr(connection, 'tenant', None)
|
||||||
|
transaction.on_commit(
|
||||||
|
lambda: event_notify_checked.spool(
|
||||||
|
event_id=str(self.pk), domain=getattr(tenant, 'domain_url', None)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.notify_checked()
|
||||||
|
|
||||||
|
def notify_checked(self):
|
||||||
|
for booking in self.booking_set.filter(user_was_present__isnull=False):
|
||||||
|
if booking.user_was_present is True and booking.presence_callback_url:
|
||||||
|
url = booking.presence_callback_url
|
||||||
|
elif booking.user_was_present is False and booking.absence_callback_url:
|
||||||
|
url = booking.absence_callback_url
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
payload = {
|
||||||
|
'user_was_present': booking.user_was_present,
|
||||||
|
'user_check_type_slug': booking.user_check_type_slug,
|
||||||
|
'user_check_type_label': booking.user_check_type_label,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests_wrapper.post(url, json=payload, remote_service='auto', timeout=15)
|
||||||
|
if response and not response.ok:
|
||||||
|
logging.error(
|
||||||
|
'error (HTTP %s) notifying checked booking (%s)', response.status_code, booking.id
|
||||||
|
)
|
||||||
|
except requests.Timeout:
|
||||||
|
logging.error('error (timeout) notifying checked booking (%s)', booking.id)
|
||||||
|
except Exception as e: # noqa pylint: disable=broad-except
|
||||||
|
logging.error('error (%s) notifying checked booking (%s)', e, booking.id)
|
||||||
|
|
||||||
def in_bookable_period(self, bypass_delays=False):
|
def in_bookable_period(self, bypass_delays=False):
|
||||||
if self.publication_datetime and now() < self.publication_datetime:
|
if self.publication_datetime and now() < self.publication_datetime:
|
||||||
|
@ -2163,6 +2203,8 @@ class Booking(models.Model):
|
||||||
form_url = models.URLField(blank=True)
|
form_url = models.URLField(blank=True)
|
||||||
backoffice_url = models.URLField(blank=True)
|
backoffice_url = models.URLField(blank=True)
|
||||||
cancel_callback_url = models.URLField(blank=True)
|
cancel_callback_url = models.URLField(blank=True)
|
||||||
|
presence_callback_url = models.URLField(blank=True)
|
||||||
|
absence_callback_url = models.URLField(blank=True)
|
||||||
color = models.ForeignKey(BookingColor, null=True, on_delete=models.SET_NULL, related_name='bookings')
|
color = models.ForeignKey(BookingColor, null=True, on_delete=models.SET_NULL, related_name='bookings')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -82,6 +82,8 @@ class FillSlotSerializer(serializers.Serializer):
|
||||||
form_url = serializers.CharField(max_length=250, allow_blank=True)
|
form_url = serializers.CharField(max_length=250, allow_blank=True)
|
||||||
backoffice_url = serializers.URLField(allow_blank=True)
|
backoffice_url = serializers.URLField(allow_blank=True)
|
||||||
cancel_callback_url = serializers.URLField(allow_blank=True)
|
cancel_callback_url = serializers.URLField(allow_blank=True)
|
||||||
|
presence_callback_url = serializers.URLField(allow_blank=True)
|
||||||
|
absence_callback_url = serializers.URLField(allow_blank=True)
|
||||||
count = serializers.IntegerField(min_value=1)
|
count = serializers.IntegerField(min_value=1)
|
||||||
cancel_booking_id = serializers.CharField(max_length=250, allow_blank=True, allow_null=True)
|
cancel_booking_id = serializers.CharField(max_length=250, allow_blank=True, allow_null=True)
|
||||||
force_waiting_list = serializers.BooleanField(default=False)
|
force_waiting_list = serializers.BooleanField(default=False)
|
||||||
|
|
|
@ -727,6 +727,8 @@ def make_booking(event, payload, extra_data, primary_booking=None, in_waiting_li
|
||||||
form_url=translate_to_publik_url(payload.get('form_url', '')),
|
form_url=translate_to_publik_url(payload.get('form_url', '')),
|
||||||
backoffice_url=translate_to_publik_url(payload.get('backoffice_url', '')),
|
backoffice_url=translate_to_publik_url(payload.get('backoffice_url', '')),
|
||||||
cancel_callback_url=translate_to_publik_url(payload.get('cancel_callback_url', '')),
|
cancel_callback_url=translate_to_publik_url(payload.get('cancel_callback_url', '')),
|
||||||
|
presence_callback_url=translate_to_publik_url(payload.get('presence_callback_url', '')),
|
||||||
|
absence_callback_url=translate_to_publik_url(payload.get('absence_callback_url', '')),
|
||||||
user_display_label=payload.get('user_display_label', ''),
|
user_display_label=payload.get('user_display_label', ''),
|
||||||
extra_emails=payload.get('extra_emails', []),
|
extra_emails=payload.get('extra_emails', []),
|
||||||
extra_phone_numbers=payload.get('extra_phone_numbers', []),
|
extra_phone_numbers=payload.get('extra_phone_numbers', []),
|
||||||
|
@ -3064,6 +3066,7 @@ class EventCheck(APIView):
|
||||||
if not event.checked:
|
if not event.checked:
|
||||||
event.checked = True
|
event.checked = True
|
||||||
event.save(update_fields=['checked'])
|
event.save(update_fields=['checked'])
|
||||||
|
event.async_notify_checked()
|
||||||
response = {
|
response = {
|
||||||
'err': 0,
|
'err': 0,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2749,6 +2749,7 @@ class EventCheckedView(EventCheckMixin, ViewableAgendaMixin, View):
|
||||||
if not self.event.checked:
|
if not self.event.checked:
|
||||||
self.event.checked = True
|
self.event.checked = True
|
||||||
self.event.save(update_fields=['checked'])
|
self.event.save(update_fields=['checked'])
|
||||||
lguerin marked this conversation as resolved
Outdated
|
|||||||
|
self.event.async_notify_checked()
|
||||||
return self.response(request)
|
return self.response(request)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from uwsgidecorators import spool # pylint: disable=import-error
|
from uwsgidecorators import spool # pylint: disable=import-error
|
||||||
|
|
||||||
from chrono.agendas.models import ICSError, TimePeriodExceptionSource
|
from chrono.agendas.models import Event, ICSError, TimePeriodExceptionSource
|
||||||
|
|
||||||
|
|
||||||
def set_connection(domain):
|
def set_connection(domain):
|
||||||
|
@ -55,3 +55,17 @@ def refresh_exceptions_from_settings(args):
|
||||||
return
|
return
|
||||||
|
|
||||||
source.refresh_from_settings()
|
source.refresh_from_settings()
|
||||||
|
|
||||||
|
|
||||||
|
@spool
|
||||||
|
def event_notify_checked(args):
|
||||||
|
if args.get('domain'):
|
||||||
|
# multitenant installation
|
||||||
|
set_connection(args['domain'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
event = Event.objects.get(pk=args['event_id'])
|
||||||
|
except Event.DoesNotExist:
|
||||||
|
return
|
||||||
|
|
||||||
|
event.notify_checked()
|
||||||
|
|
|
@ -80,6 +80,8 @@ def test_booking_api(app, user):
|
||||||
'form_url': 'http://example.net/',
|
'form_url': 'http://example.net/',
|
||||||
'extra_emails': ['baz@baz.com', 'hop@hop.com'],
|
'extra_emails': ['baz@baz.com', 'hop@hop.com'],
|
||||||
'extra_phone_numbers': ['+33123456789', '+33123456789'],
|
'extra_phone_numbers': ['+33123456789', '+33123456789'],
|
||||||
|
'presence_callback_url': 'http://example.net/jump/trigger2/',
|
||||||
|
'absence_callback_url': 'http://example.net/jump/trigger3/',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
booking = Booking.objects.get(id=resp.json['booking_id'])
|
booking = Booking.objects.get(id=resp.json['booking_id'])
|
||||||
|
@ -88,6 +90,8 @@ def test_booking_api(app, user):
|
||||||
assert booking.user_last_name == 'bar'
|
assert booking.user_last_name == 'bar'
|
||||||
assert booking.backoffice_url == 'http://example.net/'
|
assert booking.backoffice_url == 'http://example.net/'
|
||||||
assert booking.cancel_callback_url == 'http://example.net/jump/trigger/'
|
assert booking.cancel_callback_url == 'http://example.net/jump/trigger/'
|
||||||
|
assert booking.presence_callback_url == 'http://example.net/jump/trigger2/'
|
||||||
|
assert booking.absence_callback_url == 'http://example.net/jump/trigger3/'
|
||||||
assert booking.user_email == 'bar@bar.com'
|
assert booking.user_email == 'bar@bar.com'
|
||||||
assert booking.user_phone_number == '+33 (0) 6 12 34 56 78'
|
assert booking.user_phone_number == '+33 (0) 6 12 34 56 78'
|
||||||
assert booking.form_url == 'http://example.net/'
|
assert booking.form_url == 'http://example.net/'
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
@ -189,6 +190,49 @@ def test_event_checked(app, user):
|
||||||
app.post('/api/agenda/%s/check/%s/' % (agenda.slug, event.slug), status=404)
|
app.post('/api/agenda/%s/check/%s/' % (agenda.slug, event.slug), status=404)
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_notify_checked(app, user):
|
||||||
|
agenda = Agenda.objects.create(label='Events', kind='events')
|
||||||
|
event = Event.objects.create(
|
||||||
|
label='xyz',
|
||||||
|
start_datetime=now() - datetime.timedelta(days=1),
|
||||||
|
places=10,
|
||||||
|
agenda=agenda,
|
||||||
|
)
|
||||||
|
assert event.checked is False
|
||||||
|
|
||||||
|
for i in range(8):
|
||||||
|
user_was_present = None
|
||||||
|
if i < 3:
|
||||||
|
user_was_present = True
|
||||||
|
elif i < 7:
|
||||||
|
user_was_present = False
|
||||||
|
Booking.objects.create(
|
||||||
|
event=event,
|
||||||
|
user_was_present=user_was_present,
|
||||||
|
presence_callback_url='https://example.invalid/presence/%s' % i,
|
||||||
|
absence_callback_url='https://example.invalid/absence/%s' % i,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||||
|
|
||||||
|
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||||||
|
mock_response = mock.Mock(status_code=200)
|
||||||
|
mock_send.return_value = mock_response
|
||||||
|
app.post('/api/agenda/%s/check/%s/' % (agenda.slug, event.slug))
|
||||||
|
event.refresh_from_db()
|
||||||
|
assert event.checked is True
|
||||||
|
|
||||||
|
assert {x[0][0].url for x in mock_send.call_args_list} == {
|
||||||
|
'https://example.invalid/presence/0',
|
||||||
|
'https://example.invalid/presence/1',
|
||||||
|
'https://example.invalid/presence/2',
|
||||||
|
'https://example.invalid/absence/3',
|
||||||
|
'https://example.invalid/absence/4',
|
||||||
|
'https://example.invalid/absence/5',
|
||||||
|
'https://example.invalid/absence/6',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'days_in, days_out, err_msg',
|
'days_in, days_out, err_msg',
|
||||||
[
|
[
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import codecs
|
import codecs
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -1798,6 +1799,53 @@ def test_event_checked(app, admin_user):
|
||||||
assert '<span class="invoiced tag">Invoiced</span>' in resp
|
assert '<span class="invoiced tag">Invoiced</span>' in resp
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_notify_checked(app, admin_user):
|
||||||
|
agenda = Agenda.objects.create(label='Events', kind='events', booking_check_filters='foo,bar')
|
||||||
|
event = Event.objects.create(
|
||||||
|
label='xyz',
|
||||||
|
start_datetime=now() - datetime.timedelta(days=1),
|
||||||
|
places=10,
|
||||||
|
agenda=agenda,
|
||||||
|
)
|
||||||
|
login(app)
|
||||||
|
|
||||||
|
for i in range(8):
|
||||||
|
user_was_present = None
|
||||||
|
if i < 3:
|
||||||
|
user_was_present = True
|
||||||
|
elif i < 7:
|
||||||
|
user_was_present = False
|
||||||
|
Booking.objects.create(
|
||||||
|
event=event,
|
||||||
|
user_was_present=user_was_present,
|
||||||
|
presence_callback_url='https://example.invalid/presence/%s' % i,
|
||||||
|
absence_callback_url='https://example.invalid/absence/%s' % i,
|
||||||
|
)
|
||||||
|
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
|
||||||
|
|
||||||
|
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||||||
|
mock_response = mock.Mock(status_code=200)
|
||||||
|
mock_send.return_value = mock_response
|
||||||
|
resp = app.post(
|
||||||
|
'/manage/agendas/%s/events/%s/checked' % (agenda.pk, event.pk),
|
||||||
|
params={'csrfmiddlewaretoken': resp.context['csrf_token']},
|
||||||
|
)
|
||||||
|
assert {x[0][0].url for x in mock_send.call_args_list} == {
|
||||||
|
'https://example.invalid/presence/0',
|
||||||
|
'https://example.invalid/presence/1',
|
||||||
|
'https://example.invalid/presence/2',
|
||||||
|
'https://example.invalid/absence/3',
|
||||||
|
'https://example.invalid/absence/4',
|
||||||
|
'https://example.invalid/absence/5',
|
||||||
|
'https://example.invalid/absence/6',
|
||||||
|
}
|
||||||
|
assert set(json.loads(mock_send.call_args_list[0][0][0].body).keys()) == {
|
||||||
|
'user_check_type_label',
|
||||||
|
'user_check_type_slug',
|
||||||
|
'user_was_present',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('chrono.manager.forms.get_agenda_check_types')
|
@mock.patch('chrono.manager.forms.get_agenda_check_types')
|
||||||
def test_event_check_filters(check_types, app, admin_user):
|
def test_event_check_filters(check_types, app, admin_user):
|
||||||
check_types.return_value = [
|
check_types.return_value = [
|
||||||
|
|
Loading…
Reference in New Issue
on a aussi un endpoint pour marquer un event comme pointé, EventCheck
J'étais passé à côté, voilà il est traité, avec un test dédié.
(et c'est rebasé pour la migration)