callback sur les réservations lorsqu'un événement est marqué pointé (#75896) #63

Merged
fpeters merged 2 commits from wip/75896-callback-checked-events into main 2023-04-03 17:25:03 +02:00
9 changed files with 181 additions and 1 deletions

View File

@ -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),
),
]

View File

@ -20,6 +20,7 @@ import dataclasses
import datetime
import functools
import itertools
import logging
import math
import sys
import uuid
@ -1595,6 +1596,45 @@ class Event(models.Model):
return
self.checked = True
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):
if self.publication_datetime and now() < self.publication_datetime:
@ -2163,6 +2203,8 @@ class Booking(models.Model):
form_url = models.URLField(blank=True)
backoffice_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')
@property

View File

@ -82,6 +82,8 @@ class FillSlotSerializer(serializers.Serializer):
form_url = serializers.CharField(max_length=250, allow_blank=True)
backoffice_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)
cancel_booking_id = serializers.CharField(max_length=250, allow_blank=True, allow_null=True)
force_waiting_list = serializers.BooleanField(default=False)

View File

@ -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', '')),
backoffice_url=translate_to_publik_url(payload.get('backoffice_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', ''),
extra_emails=payload.get('extra_emails', []),
extra_phone_numbers=payload.get('extra_phone_numbers', []),
@ -3064,6 +3066,7 @@ class EventCheck(APIView):
if not event.checked:
event.checked = True
event.save(update_fields=['checked'])
event.async_notify_checked()
response = {
'err': 0,
}

View File

@ -2749,6 +2749,7 @@ class EventCheckedView(EventCheckMixin, ViewableAgendaMixin, View):
if not self.event.checked:
self.event.checked = True
self.event.save(update_fields=['checked'])
lguerin marked this conversation as resolved Outdated

on a aussi un endpoint pour marquer un event comme pointé, EventCheck

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)

J'étais passé à côté, voilà il est traité, avec un test dédié. (et c'est rebasé pour la migration)
self.event.async_notify_checked()
return self.response(request)

View File

@ -17,7 +17,7 @@
from django.db import connection
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):
@ -55,3 +55,17 @@ def refresh_exceptions_from_settings(args):
return
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()

View File

@ -80,6 +80,8 @@ def test_booking_api(app, user):
'form_url': 'http://example.net/',
'extra_emails': ['baz@baz.com', 'hop@hop.com'],
'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'])
@ -88,6 +90,8 @@ def test_booking_api(app, user):
assert booking.user_last_name == 'bar'
assert booking.backoffice_url == 'http://example.net/'
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_phone_number == '+33 (0) 6 12 34 56 78'
assert booking.form_url == 'http://example.net/'

View File

@ -1,4 +1,5 @@
import datetime
from unittest import mock
import pytest
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)
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(
'days_in, days_out, err_msg',
[

View File

@ -1,5 +1,6 @@
import codecs
import datetime
import json
from unittest import mock
import pytest
@ -1798,6 +1799,53 @@ def test_event_checked(app, admin_user):
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')
def test_event_check_filters(check_types, app, admin_user):
check_types.return_value = [