Compare commits

..

11 Commits

Author SHA1 Message Date
Valentin Deniaud f7c87dd0c9 manager: allow separate arrival/departure check for partial bookings (#80047)
gitea/chrono/pipeline/head This commit looks good Details
2023-11-08 17:42:46 +01:00
Frédéric Péters 031961ad80 translation update
gitea/chrono/pipeline/head This commit looks good Details
2023-11-02 14:47:56 +01:00
Lauréline Guérin 5b8419efe5 agendas: add times in notify_checked for partial bookings (#82842)
gitea/chrono/pipeline/head This commit looks good Details
2023-11-02 12:22:11 +01:00
Lauréline Guérin cff4ce0861 agendas: notify_checked, loop on booking checks instead of bookings (#82842) 2023-11-02 12:22:11 +01:00
Lauréline Guérin 737ba6f0bb manager: fix display of "check event" button (#82839)
gitea/chrono/pipeline/head Build queued... Details
2023-11-02 12:20:22 +01:00
Lauréline Guérin 72be0166f3 manager: fold check filters (#82839) 2023-11-02 12:20:22 +01:00
Lauréline Guérin dae40958f4 manager: fix event details head title (#82839) 2023-11-02 12:20:22 +01:00
Lauréline Guérin 05703dddb1 misc: fix failing tests at midnight (#82926)
gitea/chrono/pipeline/head This commit looks good Details
2023-11-02 12:19:12 +01:00
Frédéric Péters 78928bc760 api: strip white spaces and dots from received phone numbers (#82889)
gitea/chrono/pipeline/head This commit looks good Details
2023-10-31 16:32:29 +01:00
Thomas NOËL a548753f2a debian: add uwsgi/chrono SyslogIdentifier in service (#82977)
gitea/chrono/pipeline/head This commit looks good Details
2023-10-31 13:18:22 +01:00
Emmanuel Cazenave 8a7f83a02d translation update
gitea/chrono/pipeline/head This commit looks good Details
2023-10-31 12:03:11 +01:00
10 changed files with 237 additions and 143 deletions

View File

@ -2182,28 +2182,44 @@ class Event(models.Model):
self.notify_checked()
def notify_checked(self):
for booking in self.booking_set.filter(user_checks__isnull=False).prefetch_related('user_checks'):
if booking.user_check.presence is True and booking.presence_callback_url:
url = booking.presence_callback_url
elif booking.user_check.presence is False and booking.absence_callback_url:
url = booking.absence_callback_url
partial_bookings = self.agenda.partial_bookings
for user_check in BookingCheck.objects.filter(booking__event=self).select_related('booking'):
if user_check.presence is True and user_check.booking.presence_callback_url:
url = user_check.booking.presence_callback_url
elif user_check.presence is False and user_check.booking.absence_callback_url:
url = user_check.booking.absence_callback_url
else:
continue
payload = {
'user_was_present': booking.user_check.presence,
'user_check_type_slug': booking.user_check.type_slug,
'user_check_type_label': booking.user_check.type_label,
'user_was_present': user_check.presence,
'user_check_type_slug': user_check.type_slug,
'user_check_type_label': user_check.type_label,
}
if partial_bookings:
payload.update(
{
'start_time': user_check.start_time.isoformat() if user_check.start_time else None,
'end_time': user_check.end_time.isoformat() if user_check.end_time else None,
'computed_start_time': user_check.computed_start_time.isoformat()
if user_check.computed_start_time
else None,
'computed_end_time': user_check.computed_end_time.isoformat()
if user_check.computed_end_time
else None,
}
)
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
'error (HTTP %s) notifying checked booking (%s)',
response.status_code,
user_check.booking_id,
)
except requests.Timeout:
logging.error('error (timeout) notifying checked booking (%s)', booking.id)
logging.error('error (timeout) notifying checked booking (%s)', user_check.booking_id)
except Exception as e: # noqa pylint: disable=broad-except
logging.error('error (%s) notifying checked booking (%s)', e, booking.id)
logging.error('error (%s) notifying checked booking (%s)', e, user_check.booking_id)
def async_refresh_booking_computed_times(self):
if self.agenda.kind != 'events' or not self.agenda.partial_bookings:

View File

@ -1,5 +1,6 @@
import collections
import datetime
import re
from django.contrib.auth.models import Group
from django.db import models, transaction
@ -42,6 +43,15 @@ class StringOrListField(serializers.ListField):
return super().to_internal_value(data)
class PhoneNumbersStringOrListField(serializers.ListField):
def to_internal_value(self, data):
if isinstance(data, str):
data = [s.strip() for s in data.split(',') if s.strip()]
# strip white spaces and dots
data = [re.sub(r'[\s\.]', '', x) for x in data]
return super().to_internal_value(data)
class CommaSeparatedStringField(serializers.ListField):
def get_value(self, dictionary):
return super(serializers.ListField, self).get_value(dictionary)
@ -91,7 +101,7 @@ class FillSlotSerializer(serializers.Serializer):
extra_emails = StringOrListField(
required=False, child=serializers.EmailField(max_length=250, allow_blank=False)
)
extra_phone_numbers = StringOrListField(
extra_phone_numbers = PhoneNumbersStringOrListField(
required=False, child=serializers.CharField(max_length=16, allow_blank=False)
)
check_overlaps = serializers.BooleanField(default=False)

View File

@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: chrono 0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-10-09 10:54+0200\n"
"PO-Revision-Date: 2023-10-09 10:58+0200\n"
"POT-Creation-Date: 2023-11-02 13:47+0000\n"
"PO-Revision-Date: 2023-11-02 14:47+0100\n"
"Last-Translator: Frederic Peters <fpeters@entrouvert.com>\n"
"Language: French\n"
"MIME-Version: 1.0\n"
@ -291,6 +291,17 @@ msgstr "À la minute"
msgid "Tolerance"
msgstr "Tolérance"
#: agendas/models.py api/views.py apps/ants_hub/models.py
msgid "Agenda"
msgstr "Agenda"
#: agendas/models.py apps/ants_hub/templates/chrono/manager_ants_hub_place.html
#: manager/forms.py manager/templates/chrono/manager_base.html
#: manager/templates/chrono/manager_home.html
#: manager/templates/chrono/manager_no_access.html manager/views.py
msgid "Agendas"
msgstr "Agendas"
#: agendas/models.py manager/forms.py
msgid "Partial bookings"
msgstr "Plages libres"
@ -519,6 +530,12 @@ msgstr "%(Every_x_days)s, à partir du %(date)s"
msgid "%(Every_x_days)s, until %(date)s"
msgstr "%(Every_x_days)s, jusquau %(date)s"
#: agendas/models.py manager/forms.py
#: manager/templates/chrono/manager_events_type_list.html
#: manager/templates/chrono/manager_home.html
msgid "Events types"
msgstr "Types dévènements"
#: agendas/models.py
msgid "Label displayed to user"
msgstr "Libellé affiché à lusager"
@ -541,6 +558,25 @@ msgstr "Le calendrier dindisponibilités « %s » nexiste pas."
msgid "Optional description."
msgstr "Description optionnelle."
#: agendas/models.py manager/forms.py
msgid "Resource"
msgstr "Ressource"
#: agendas/models.py manager/forms.py
#: manager/templates/chrono/manager_home.html
#: manager/templates/chrono/manager_meetings_agenda_settings.html
#: manager/templates/chrono/manager_resource_list.html
msgid "Resources"
msgstr "Ressources"
#: agendas/models.py
msgid "Category (agendas)"
msgstr "Catégorie (agendas)"
#: agendas/models.py
msgid "Categories (agendas)"
msgstr "Catégories (agendas)"
#: agendas/models.py
#, python-format
msgid ""
@ -572,6 +608,15 @@ msgstr "Lévènement « %s » na pas de date de début."
msgid "Exception"
msgstr "Exception"
#: agendas/models.py
msgid "Unavailability calendar"
msgstr "Calendrier dindisponibilités"
#: agendas/models.py manager/forms.py
#: manager/templates/chrono/manager_home.html
msgid "Unavailability calendars"
msgstr "Calendrier dindisponibilités"
#: agendas/models.py
msgid "Optional Label"
msgstr "Libellé optionnel"
@ -1402,10 +1447,6 @@ msgstr "Nombre de réservations"
msgid "Interval"
msgstr "Intervalle"
#: api/views.py apps/ants_hub/models.py
msgid "Agenda"
msgstr "Agenda"
#: api/views.py manager/forms.py
msgctxt "agendas"
msgid "All"
@ -1743,13 +1784,6 @@ msgstr "Géolocalisation :"
msgid "URLs"
msgstr "URLs"
#: apps/ants_hub/templates/chrono/manager_ants_hub_place.html manager/forms.py
#: manager/templates/chrono/manager_base.html
#: manager/templates/chrono/manager_home.html
#: manager/templates/chrono/manager_no_access.html manager/views.py
msgid "Agendas"
msgstr "Agendas"
#: apps/ants_hub/templates/chrono/manager_ants_hub_place.html
msgid "Add"
msgstr "Ajouter"
@ -2051,10 +2085,6 @@ msgstr "La date de fin doit venir après la date de début."
msgid "Please select an interval of no more than 3 months."
msgstr "Veuillez choisir un intervalle dau plus 3 mois."
#: manager/forms.py
msgid "Resource"
msgstr "Ressource"
#: manager/forms.py
msgid ""
"Can't add a meetingtype to an agenda that is included in a virtual agenda."
@ -2252,26 +2282,11 @@ msgstr ""
"Le rappel sera envoyé vers ce numéro, à la place de celui lié à la "
"réservation."
#: manager/forms.py manager/templates/chrono/manager_home.html
#: manager/templates/chrono/manager_meetings_agenda_settings.html
#: manager/templates/chrono/manager_resource_list.html
msgid "Resources"
msgstr "Ressources"
#: manager/forms.py manager/templates/chrono/manager_home.html
msgid "Unavailability calendars"
msgstr "Calendrier dindisponibilités"
#: manager/forms.py manager/templates/chrono/manager_category_list.html
#: manager/templates/chrono/manager_home.html
msgid "Categories"
msgstr "Catégories"
#: manager/forms.py manager/templates/chrono/manager_events_type_list.html
#: manager/templates/chrono/manager_home.html
msgid "Events types"
msgstr "Types dévènements"
#: manager/forms.py manager/templates/chrono/manager_home.html
msgid "Shared custody"
msgstr "Garde partagée"
@ -2838,6 +2853,17 @@ msgstr "Pointage"
msgid "Bookings (%(booked_places)s/%(places)s)"
msgstr "Réservations (%(booked_places)s/%(places)s)"
#: manager/templates/chrono/manager_event_check.html
#: manager/templates/chrono/manager_partial_bookings_day_view.html
msgid "Filtering options"
msgstr "Paramètres de filtrage"
#: manager/templates/chrono/manager_event_check.html
#: manager/templates/chrono/manager_partial_bookings_day_view.html
msgctxt "form filtering action"
msgid "Apply"
msgstr "Appliquer"
#: manager/templates/chrono/manager_event_check.html
#: manager/templates/chrono/manager_partial_bookings_day_view.html
msgid "Mark the event as checked"
@ -3335,6 +3361,10 @@ msgstr "Période réservée"
msgid "Booked period:"
msgstr "Période réservée :"
#: manager/templates/chrono/manager_partial_bookings_day_view.html
msgid "Checked period"
msgstr "Période pointée"
#: manager/templates/chrono/manager_partial_bookings_day_view.html
msgid "Checked period:"
msgstr "Période pointée :"

View File

@ -15,71 +15,71 @@
</h3>
<div>
<form class="check-bookings-filters">
{{ filterset.form.as_p }}
<script>
$(function() {
$('form.check-bookings-filters input').on('change',
function() {
$(this).parents('form').submit();
});
});
</script>
<fieldset class="gadjo-foldable gadjo-folded" id="filters">
<legend class="gadjo-foldable-widget">{% trans "Filtering options" %}</legend>
<div class="gadjo-folding">
{{ filterset.form.as_p }}
<button class="submit-button">{% trans "Apply" context 'form filtering action' %}</button>
</div>
</fieldset>
</form>
<table class="main check-bookings">
<tbody>
{% if results and not event.checked and not event.check_locked %}
<tr class="booking">
</div>
</div>
<div>
<table class="main check-bookings">
<tbody>
{% if results and not event.checked and not event.check_locked %}
<tr class="booking">
<td class="booking-actions" colspan="3">
<form method="post" action="{% url 'chrono-manager-event-checked' pk=agenda.pk event_pk=object.pk %}">
{% csrf_token %}
<button class="submit-button">{% trans "Mark the event as checked" %}</button>
</form>
</td>
</tr>
{% endif %}
{% if booked_without_status %}
{% if not event.checked or not agenda.disable_check_update %}
<tr class="booking all-bookings">
<td colspan="2"><b>{% trans "Mark all bookings without status:" %}</b></td>
<td class="booking-actions">
<form method="post" action="{% url 'chrono-manager-event-checked' pk=agenda.pk event_pk=object.pk %}">
<form method="post" action="{% url 'chrono-manager-event-presence' pk=agenda.pk event_pk=object.pk %}" id="all-bookings-presence">
{% csrf_token %}
<button class="submit-button">{% trans "Mark the event as checked" %}</button>
<button class="submit-button">{% trans "Presence" %}</button>
{% if presence_form.check_type.field.choices.1 %}{{ presence_form.check_type }}{% endif %}
<script>
$(function() {
$('#all-bookings-presence select').on('change',
function() {
$('#all-bookings-presence').submit();
});
});
</script>
</form>
<form method="post" action="{% url 'chrono-manager-event-absence' pk=agenda.pk event_pk=object.pk %}" id="all-bookings-absence">
{% csrf_token %}
<button class="submit-button">{% trans "Absence" %}</button>
{% if absence_form.check_type.field.choices.1 %}{{ absence_form.check_type }}{% endif %}
<script>
$(function() {
$('#all-bookings-absence select').on('change',
function() {
$('#all-bookings-absence').submit();
});
});
</script>
</form>
</td>
</tr>
{% endif %}
{% if booked_without_status %}
{% if not event.checked or not agenda.disable_check_update %}
<tr class="booking all-bookings">
<td colspan="2"><b>{% trans "Mark all bookings without status:" %}</b></td>
<td class="booking-actions">
<form method="post" action="{% url 'chrono-manager-event-presence' pk=agenda.pk event_pk=object.pk %}" id="all-bookings-presence">
{% csrf_token %}
<button class="submit-button">{% trans "Presence" %}</button>
{% if presence_form.check_type.field.choices.1 %}{{ presence_form.check_type }}{% endif %}
<script>
$(function() {
$('#all-bookings-presence select').on('change',
function() {
$('#all-bookings-presence').submit();
});
});
</script>
</form>
<form method="post" action="{% url 'chrono-manager-event-absence' pk=agenda.pk event_pk=object.pk %}" id="all-bookings-absence">
{% csrf_token %}
<button class="submit-button">{% trans "Absence" %}</button>
{% if absence_form.check_type.field.choices.1 %}{{ absence_form.check_type }}{% endif %}
<script>
$(function() {
$('#all-bookings-absence select').on('change',
function() {
$('#all-bookings-absence').submit();
});
});
</script>
</form>
</td>
</tr>
{% endif %}
{% endif %}
{% for result in results %}
<tr class="booking {% if agenda.booking_extra_user_block_template %}untoggled{% endif %}">
{% include "chrono/manager_event_check_booking_fragment.html" with booking=result %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% for result in results %}
<tr class="booking {% if agenda.booking_extra_user_block_template %}untoggled{% endif %}">
{% include "chrono/manager_event_check_booking_fragment.html" with booking=result %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if object.waiting_list_places %}

View File

@ -7,7 +7,7 @@
{% endblock %}
{% block page-title-extra-label %}
- {% firstof agenda.label event.label %}
{% firstof agenda.label event.label %}
{% endblock %}
{% block breadcrumb %}

View File

@ -19,23 +19,29 @@
<p>{% trans "No opening hours this day." %}</p>
</div>
{% else %}
<form class="check-bookings-filters">
{{ filterset.form.as_p }}
<script>
$(function() {
$('form.check-bookings-filters input').on('change',
function() {
$(this).parents('form').submit();
});
});
</script>
</form>
<div class="section">
<div>
<form class="check-bookings-filters">
<fieldset class="gadjo-foldable gadjo-folded" id="filters">
<legend class="gadjo-foldable-widget">{% trans "Filtering options" %}</legend>
<div class="gadjo-folding">
{{ filterset.form.as_p }}
<button class="submit-button">{% trans "Apply" context 'form filtering action' %}</button>
</div>
</fieldset>
</form>
</div>
</div>
{% if results and not event.checked and not event.check_locked %}
<form method="post" action="{% url 'chrono-manager-event-checked' pk=agenda.pk event_pk=event.pk %}">
{% csrf_token %}
<button class="submit-button">{% trans "Mark the event as checked" %}</button>
</form>
<div class="section">
<div>
<form method="post" action="{% url 'chrono-manager-event-checked' pk=agenda.pk event_pk=event.pk %}">
{% csrf_token %}
<button class="submit-button">{% trans "Mark the event as checked" %}</button>
</form>
</div>
</div>
{% endif %}
<div class="partial-booking" style="--nb-hours: {{ hours|length }}">

View File

@ -4,6 +4,7 @@ After=network.target postgresql.service
Wants=postgresql.service
[Service]
SyslogIdentifier=uwsgi/%p
Environment=CHRONO_SETTINGS_FILE=/usr/lib/%p/debian_config.py
Environment=LANG=C.UTF-8
User=%p

View File

@ -79,7 +79,7 @@ def test_booking_api(app, user):
'user_phone_number': '+33 (0) 6 12 34 56 78', # long phone number
'form_url': 'http://example.net/',
'extra_emails': ['baz@baz.com', 'hop@hop.com'],
'extra_phone_numbers': ['+33123456789', '+33123456789'],
'extra_phone_numbers': ['+33123456789', '+33 1 23 45 67 89'],
'presence_callback_url': 'http://example.net/jump/trigger2/',
'absence_callback_url': 'http://example.net/jump/trigger3/',
},
@ -292,7 +292,7 @@ def test_booking_api_extra_emails(app, user):
fillslot_url,
params={
'extra_emails': 'bar.com',
'extra_phone_numbers': 'too loooooooooong',
'extra_phone_numbers': 'too loooooooooooong',
},
status=400,
)

View File

@ -1431,10 +1431,8 @@ def test_event_cancellation_forbidden(app, admin_user):
def test_event_booking_form_url(settings, app, admin_user):
settings.TEMPLATE_VARS = {'eservices_url': 'http://demarches/'}
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
)
day = localtime(event.start_datetime)
day = localtime(now()) + datetime.timedelta(days=1)
event = Event.objects.create(label='xyz', start_datetime=day, places=10, agenda=agenda)
login(app)
@ -1522,7 +1520,7 @@ def test_event_check(app, admin_user):
Desk.objects.create(agenda=agenda2, slug='_exceptions_holder')
event = Event.objects.create(
label='xyz',
start_datetime=now() + datetime.timedelta(days=1),
start_datetime=localtime(now()) + datetime.timedelta(days=1),
places=10,
waiting_list_places=5,
agenda=agenda,
@ -1617,48 +1615,48 @@ def test_event_check(app, admin_user):
user_external_id='user:1',
user_first_name='Subscription',
user_last_name='42',
date_start=now(),
date_end=now() + datetime.timedelta(days=1),
date_start=datetime.date.today(),
date_end=datetime.date.today() + datetime.timedelta(days=1),
)
Subscription.objects.create(
agenda=agenda,
user_external_id='user:9',
user_first_name='Subscription',
user_last_name='43',
date_start=now(),
date_end=now() + datetime.timedelta(days=1),
date_start=datetime.date.today(),
date_end=datetime.date.today() + datetime.timedelta(days=1),
)
Subscription.objects.create(
agenda=agenda,
user_external_id='user:10',
user_first_name='Subscription',
user_last_name='14',
date_start=now(),
date_end=now() + datetime.timedelta(days=1),
date_start=datetime.date.today(),
date_end=datetime.date.today() + datetime.timedelta(days=1),
)
Subscription.objects.create(
agenda=agenda,
user_external_id='user:7',
user_first_name='Subscription',
user_last_name='Waiting',
date_start=now(),
date_end=now() + datetime.timedelta(days=1),
date_start=datetime.date.today(),
date_end=datetime.date.today() + datetime.timedelta(days=1),
)
Subscription.objects.create(
agenda=agenda,
user_external_id='user:42',
user_first_name='Subscription',
user_last_name='Too soon',
date_start=now() - datetime.timedelta(days=1),
date_end=now(),
date_start=datetime.date.today() - datetime.timedelta(days=1),
date_end=datetime.date.today(),
)
Subscription.objects.create(
agenda=agenda,
user_external_id='user:42',
user_first_name='Subscription',
user_last_name='Too late',
date_start=now() + datetime.timedelta(days=1),
date_end=now() + datetime.timedelta(days=2),
date_start=datetime.date.today() + datetime.timedelta(days=1),
date_end=datetime.date.today() + datetime.timedelta(days=2),
)
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert (

View File

@ -1,9 +1,10 @@
import datetime
import json
from unittest import mock
import pytest
from chrono.agendas.models import Agenda, Booking, Event, Subscription
from chrono.agendas.models import Agenda, Booking, BookingCheck, Event, Subscription
from chrono.utils.lingo import CheckType
from chrono.utils.timezone import make_aware, now
from tests.utils import login
@ -760,8 +761,18 @@ def test_manager_partial_bookings_event_checked(app, admin_user):
event=event,
start_time=datetime.time(8, 00),
end_time=datetime.time(10, 00),
presence_callback_url='https://example.invalid/presence/%s' % i,
absence_callback_url='https://example.invalid/absence/%s' % i,
)
if i < 3:
if i == 0:
booking.mark_user_presence(start_time=datetime.time(8, 00), end_time=datetime.time(9, 00))
BookingCheck.objects.create(
booking=booking,
start_time=datetime.time(9, 00),
end_time=datetime.time(10, 00),
presence=False,
)
elif i < 3:
booking.mark_user_presence(start_time=datetime.time(8, 00), end_time=datetime.time(10, 00))
elif i < 7:
booking.mark_user_absence(start_time=datetime.time(8, 00), end_time=datetime.time(10, 00))
@ -776,10 +787,13 @@ def test_manager_partial_bookings_event_checked(app, admin_user):
assert 'invoiced' not in resp
token = resp.context['csrf_token']
resp = app.post(
'/manage/agendas/%s/events/%s/checked' % (agenda.pk, event.pk),
params={'csrfmiddlewaretoken': token},
)
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': token},
)
event.refresh_from_db()
assert event.checked is True
resp = resp.follow()
@ -792,6 +806,25 @@ def test_manager_partial_bookings_event_checked(app, admin_user):
assert '<span class="checked tag">Checked</span>' in resp
assert 'check-locked' not in resp
assert 'invoiced' not in resp
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/0',
'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',
'start_time',
'end_time',
'computed_start_time',
'computed_end_time',
}
# event not in past
agenda.disable_check_update = False