Compare commits

..

21 Commits

Author SHA1 Message Date
Valentin Deniaud 5ce8ea55cd misc: update git-blame-ignore-revs (#75446)
gitea/chrono/pipeline/head There was a failure building this commit Details
2023-03-29 11:50:32 +02:00
Valentin Deniaud e4e3a30981 tests: remove Django 2 compatibility code (#75442) 2023-03-29 11:48:44 +02:00
Valentin Deniaud a770d74e25 misc: bump djhtml version (#75442) 2023-03-29 11:48:44 +02:00
Valentin Deniaud c491db6c77 misc: bump black version (#75442) 2023-03-29 11:48:44 +02:00
Valentin Deniaud 41d948ba6a misc: change pyupgrade target version to 3.9 (#75442) 2023-03-29 11:48:44 +02:00
Valentin Deniaud 5ae3ecb607 misc: change django-upgrade target version to 3.2 (#75442) 2023-03-29 11:48:44 +02:00
Valentin Deniaud db6f11c7c0 misc: require django 3.2 (#75442) 2023-03-29 11:48:44 +02:00
Emmanuel Cazenave 1827cfe5c6 api: add minutes filter on meetings datetimes endpoint (#73832)
gitea/chrono/pipeline/head This commit looks good Details
2023-03-28 13:16:19 +02:00
Benjamin Dauvergne bdae8cd217 translation update (#56284)
gitea/chrono/pipeline/head This commit looks good Details
2023-03-28 10:26:12 +02:00
Benjamin Dauvergne dbd62e3f3f agendas: reimplement min/max_booking_datetime based on minimal_booking_time (#56284) 2023-03-28 10:26:12 +02:00
Benjamin Dauvergne ddc52a4116 agendas: add Agenda.minimal_booking_time field (#56284) 2023-03-28 10:26:12 +02:00
Benjamin Dauvergne 22e8d00e31 misc: when excluding user_external_id's event search all real agendas (#75587)
gitea/chrono/pipeline/head This commit looks good Details
2023-03-24 15:41:51 +01:00
Benjamin Dauvergne 07f996ea76 tests: add test on user_external_id exclusion between agendas of different kind or timeperiod (#75587) 2023-03-24 15:41:51 +01:00
Lauréline Guérin be3eedc3fe
misc: remove some warnings (#75590)
gitea/chrono/pipeline/head This commit looks good Details
models.W042
fields.W903
fields.W904
2023-03-20 09:07:30 +01:00
Lauréline Guérin dc4dbe62e1
translation update
gitea/chrono/pipeline/head This commit looks good Details
2023-03-17 18:25:06 +01:00
Lauréline Guérin 1ba55c2f18
translation update
gitea/chrono/pipeline/head This commit looks good Details
2023-03-16 17:03:03 +01:00
Lauréline Guérin 2b9b2f2607
manager: enable check of future event (#75277)
gitea/chrono/pipeline/head There was a failure building this commit Details
2023-03-16 15:00:00 +01:00
Lauréline Guérin 0a9e36883c
agendas: add a flag to enable check of future events (#75277) 2023-03-16 15:00:00 +01:00
Lauréline Guérin 970ba42075
manager: reset user_was_present on booking check page (#75276)
gitea/chrono/pipeline/head There was a failure building this commit Details
2023-03-16 14:59:33 +01:00
Lauréline Guérin 6c134bfa3f
manager: allow check of subscription with no booking (#62235)
gitea/chrono/pipeline/head This commit looks good Details
2023-03-16 14:59:13 +01:00
Lauréline Guérin 45b72d53de
manager: allow check for cancelled booking if subscription exist (#62235) 2023-03-16 14:57:14 +01:00
30 changed files with 1179 additions and 158 deletions

View File

@ -18,6 +18,6 @@ e07c450d7c8a5f80aafe185c85ebed73fe39d9e7
# misc: apply djhtml (#69404)
7c0eb04c453f50cdffe0095148aa52580c20143e
# misc: bump black version (#75442)
03fa44aca0d2c46e84755ec91d91d7525943804f
c491db6c777cde0684159f821e779f89939459f0
# misc: bump djhtml version (#75442)
04b405ba3d0fff638763866ee169ed196ae6b27e
a770d74e252f3bdee1425db9c65cb876b5dcc3f3

View File

@ -16,8 +16,7 @@
from django.apps import apps
from django.core.management.base import BaseCommand, CommandError
from django.db import connection
from django.db.models import JSONField
from django.db import connection, models
class Command(BaseCommand):
@ -26,7 +25,7 @@ class Command(BaseCommand):
def handle(self, **options):
for app in apps.get_models():
for field in app._meta.get_fields():
if isinstance(field, JSONField):
if isinstance(field, models.JSONField):
table_name = app._meta.db_table
column_name = app._meta.get_field(field.name).column
with connection.cursor() as cursor:

View File

@ -1,4 +1,3 @@
from django.contrib.postgres.fields import JSONField
from django.db import migrations, models
@ -15,7 +14,7 @@ class Migration(migrations.Migration):
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('extra_data', JSONField(null=True)),
('extra_data', models.JSONField(null=True)),
('event', models.ForeignKey(to='agendas.Event', on_delete=models.CASCADE)),
],
options={},

View File

@ -1,7 +1,6 @@
# Generated by Django 1.11.18 on 2020-08-11 14:11
import django.db.models.deletion
from django.contrib.postgres.fields import JSONField
from django.db import migrations, models
@ -20,7 +19,7 @@ class Migration(migrations.Migration):
),
('timestamp', models.DateTimeField(auto_now_add=True)),
('seen', models.BooleanField(default=False)),
('booking_errors', JSONField(default=dict)),
('booking_errors', models.JSONField(default=dict)),
('bookings', models.ManyToManyField(to='agendas.Booking')),
],
options={

View File

@ -10,6 +10,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='booking',
name='user_was_present',
field=models.NullBooleanField(),
field=models.BooleanField(null=True),
),
]

View File

@ -1,4 +1,3 @@
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
@ -11,7 +10,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='subscription',
name='extra_data',
field=django.contrib.postgres.fields.jsonb.JSONField(null=True),
field=models.JSONField(null=True),
),
migrations.AddField(
model_name='subscription',

View File

@ -1,4 +1,3 @@
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
@ -17,7 +16,7 @@ class Migration(migrations.Migration):
),
('slug', models.SlugField(max_length=160, unique=True, verbose_name='Identifier')),
('label', models.CharField(max_length=150, verbose_name='Label')),
('custom_fields', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list)),
('custom_fields', models.JSONField(blank=True, default=list)),
],
),
]

View File

@ -1,5 +1,4 @@
import django.contrib.postgres.fields.jsonb
from django.db import migrations
from django.db import migrations, models
class Migration(migrations.Migration):
@ -11,6 +10,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='event',
name='custom_fields',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
field=models.JSONField(blank=True, default=dict),
),
]

View File

@ -0,0 +1,17 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agendas', '0146_event_bigautofield'),
]
operations = [
migrations.AddField(
model_name='agenda',
name='enable_check_for_future_events',
field=models.BooleanField(
default=False, verbose_name='Enable the check of bookings when event has not passed'
),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.18 on 2023-03-08 17:20
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agendas', '0147_check_future_events'),
]
operations = [
migrations.AddField(
model_name='agenda',
name='minimal_booking_time',
field=models.TimeField(
blank=True, default=datetime.time(0, 0), null=True, verbose_name='Minimal booking time'
),
),
]

View File

@ -25,6 +25,7 @@ import sys
import uuid
from contextlib import contextmanager
import django
import requests
import vobject
from dateutil.relativedelta import SU, relativedelta
@ -43,7 +44,6 @@ from django.db.models import (
F,
Func,
IntegerField,
JSONField,
Max,
OuterRef,
Prefetch,
@ -51,7 +51,7 @@ from django.db.models import (
Subquery,
Value,
)
from django.db.models.functions import Cast, Coalesce, Concat, ExtractWeek, ExtractWeekDay
from django.db.models.functions import Cast, Coalesce, Concat, ExtractWeek, ExtractWeekDay, JSONObject
from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist, engines
from django.urls import reverse
from django.utils import functional
@ -227,6 +227,9 @@ class Agenda(models.Model):
disable_check_update = models.BooleanField(
_('Prevent the check of bookings when event was marked as checked'), default=False
)
enable_check_for_future_events = models.BooleanField(
_('Enable the check of bookings when event has not passed'), default=False
)
booking_check_filters = models.CharField(
_('Filters'),
max_length=250,
@ -259,6 +262,15 @@ class Agenda(models.Model):
null=True,
blank=True,
)
minimal_booking_time = models.TimeField(
verbose_name=_('Minimal booking time'),
default=datetime.time(0, 0, 0), # booking is possible starting and finishin at 00:00
help_text=_(
'Ex.: 08:00:00. If left empty, available events will be those that are later than the current time.'
),
null=True,
blank=True,
)
class Meta:
ordering = ['label']
@ -631,31 +643,27 @@ class Agenda(models.Model):
if self.maximal_booking_delay is None:
return None
# compute middle of today with localtime
# 28 Mar 2021 12:00 +01:00
t = localtime(now()).replace(hour=12, minute=0)
# reference is now, in local timezone
t = localtime(now())
# advance of self.maximal_booking_delay - 1 days
# 28 Mar 2021 12:00 +01:00 == 28 Mars 2021 13:00 +02:00 as DST happend on 28 Mar 2021.
# add delay
t += datetime.timedelta(days=self.maximal_booking_delay)
# move to midnight of the day before, DST happen between 2h/3h so it
# always exists because +/- timedelta does not move the timezone, only
# localtime() does it.
# 27 Mar 2021 12:00 +01:00 == 28 Mars 2021 01:00 +02:00
return localtime(t).replace(hour=0, minute=0, second=0, microsecond=0)
# replace time if needed
if self.minimal_booking_time:
t = datetime.datetime.combine(t.date(), self.minimal_booking_time, tzinfo=t.tzinfo)
# t could not exist, recompute it as an existing datetime by converting to UTC then to localtime
return localtime(t.astimezone(utc))
@functional.cached_property
def min_booking_datetime(self):
if self.minimal_booking_delay is None:
return None
# compute middle of today with localtime
# 28 Mar 2021 12:00 +01:00
t = localtime(now()).replace(hour=12, minute=0)
# reference is now, in local timezone
t = localtime(now())
# advance of self.minimal_booking_delay - 1 days
# 28 Mar 2021 12:00 +01:00 == 28 Mars 2021 13:00 +02:00 as DST happend on 28 Mar 2021.
# add delay
if settings.WORKING_DAY_CALENDAR is not None and self.minimal_booking_delay_in_working_days:
source_class = import_string(settings.WORKING_DAY_CALENDAR)
calendar = source_class()
@ -663,11 +671,11 @@ class Agenda(models.Model):
else:
t += datetime.timedelta(days=self.minimal_booking_delay)
# move to midnight of the day before, DST happen between 2h/3h so it
# always exists because +/- timedelta does not move the timezone, only
# localtime() does it.
# 27 Mar 2021 12:00 +01:00 == 28 Mars 2021 01:00 +02:00
return localtime(t).replace(hour=0, minute=0, second=0, microsecond=0)
# replace time if needed
if self.minimal_booking_time:
t = datetime.datetime.combine(t.date(), self.minimal_booking_time, tzinfo=t.tzinfo)
# t could not exist, recompute it as an existing datetime by converting to UTC then to localtime
return localtime(t.astimezone(utc))
def get_open_events(
self,
@ -1504,7 +1512,7 @@ class Event(models.Model):
meeting_type = models.ForeignKey(MeetingType, null=True, on_delete=models.CASCADE)
desk = models.ForeignKey('Desk', null=True, on_delete=models.CASCADE)
resources = models.ManyToManyField('Resource')
custom_fields = JSONField(blank=True, default=dict)
custom_fields = models.JSONField(blank=True, default=dict)
almost_full_notification_timestamp = models.DateTimeField(null=True, blank=True)
full_notification_timestamp = models.DateTimeField(null=True, blank=True)
@ -1689,8 +1697,6 @@ class Event(models.Model):
if agendas:
overlapping_events = overlapping_events.filter(agenda__in=agendas)
from django.db.models.functions import JSONObject
json_object = JSONObject(
slug=F('computed_slug'),
days=F('recurrence_days'),
@ -1699,7 +1705,7 @@ class Event(models.Model):
return qs.annotate(
overlaps=ArraySubquery(
overlapping_events.values(json=json_object),
output_field=ArrayField(JSONField()),
output_field=ArrayField(models.JSONField()),
)
)
@ -2041,7 +2047,7 @@ class Event(models.Model):
class EventsType(models.Model):
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
label = models.CharField(_('Label'), max_length=150)
custom_fields = JSONField(blank=True, default=list)
custom_fields = models.JSONField(blank=True, default=list)
def __str__(self):
return self.label
@ -2111,7 +2117,7 @@ class BookingColor(models.Model):
class Booking(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE)
extra_data = JSONField(null=True)
extra_data = models.JSONField(null=True)
anonymization_datetime = models.DateTimeField(null=True)
cancellation_datetime = models.DateTimeField(null=True)
email_reminder_datetime = models.DateTimeField(null=True)
@ -2185,14 +2191,28 @@ class Booking(models.Model):
self.secondary_booking_set.update(in_waiting_list=True)
self.save()
def reset_user_was_present(self):
self.user_check_type_slug = None
self.user_check_type_label = None
self.user_was_present = None
with transaction.atomic():
self.secondary_booking_set.update(user_check_type_slug=None)
self.secondary_booking_set.update(user_check_type_label=None)
self.secondary_booking_set.update(user_was_present=None)
self.save()
self.event.checked = False
self.event.save(update_fields=['checked'])
def mark_user_absence(self, check_type_slug=None, check_type_label=None):
self.user_check_type_slug = check_type_slug
self.user_check_type_label = check_type_label
self.user_was_present = False
self.cancellation_datetime = None
with transaction.atomic():
self.secondary_booking_set.update(user_check_type_slug=check_type_slug)
self.secondary_booking_set.update(user_check_type_label=check_type_label)
self.secondary_booking_set.update(user_was_present=False)
self.secondary_booking_set.update(cancellation_datetime=None)
self.save()
self.event.set_is_checked()
@ -2200,10 +2220,12 @@ class Booking(models.Model):
self.user_check_type_slug = check_type_slug
self.user_check_type_label = check_type_label
self.user_was_present = True
self.cancellation_datetime = None
with transaction.atomic():
self.secondary_booking_set.update(user_check_type_slug=check_type_slug)
self.secondary_booking_set.update(user_check_type_label=check_type_label)
self.secondary_booking_set.update(user_was_present=True)
self.secondary_booking_set.update(cancellation_datetime=None)
self.save()
self.event.set_is_checked()
@ -3014,7 +3036,7 @@ class EventCancellationReport(models.Model):
timestamp = models.DateTimeField(auto_now_add=True)
seen = models.BooleanField(default=False)
bookings = models.ManyToManyField(Booking)
booking_errors = JSONField(default=dict)
booking_errors = models.JSONField(default=dict)
def __str__(self):
return '%s - %s' % (self.timestamp.strftime('%Y-%m-%d %H:%M:%S'), self.event)
@ -3258,7 +3280,7 @@ class Subscription(models.Model):
user_first_name = models.CharField(max_length=250)
user_email = models.EmailField(blank=True)
user_phone_number = models.CharField(max_length=30, blank=True)
extra_data = JSONField(null=True)
extra_data = models.JSONField(null=True)
date_start = models.DateField()
date_end = models.DateField()

View File

@ -521,6 +521,7 @@ class AgendaSerializer(serializers.ModelSerializer):
'minimal_booking_delay',
'minimal_booking_delay_in_working_days',
'maximal_booking_delay',
'minimal_booking_time',
'anonymize_delay',
'edit_role',
'view_role',
@ -771,3 +772,9 @@ class SharedCustodyAgendaSerializer(SharedCustodyAgendaMixin, serializers.ModelS
def get_child_id(self, attrs):
return self.instance.child.user_external_id
class MinutesSerializer(serializers.Serializer):
minutes = CommaSeparatedStringField(
required=False, child=serializers.IntegerField(min_value=0, max_value=59)
)

View File

@ -265,7 +265,7 @@ def get_all_slots(
)
booked_events = (
Event.objects.filter(
agenda__in=agenda_ids,
agenda__in=agendas,
start_datetime__gte=used_min_datetime - max_meeting_duration_td,
start_datetime__lte=used_max_datetime,
booking__user_external_id=user_external_id,
@ -278,7 +278,10 @@ def get_all_slots(
# compute exclusion set by desk from all bookings, using
# itertools.groupby() to group them by desk_id
user_bookings = IntervalSet.from_ordered(
(event_start_datetime, event_start_datetime + datetime.timedelta(minutes=event_duration))
(
event_start_datetime,
event_start_datetime + datetime.timedelta(minutes=event_duration),
)
for event_start_datetime, event_duration in booked_events
)
@ -358,6 +361,7 @@ def get_agenda_detail(request, agenda, check_events=False):
'kind': agenda.kind,
'minimal_booking_delay': agenda.minimal_booking_delay,
'maximal_booking_delay': agenda.maximal_booking_delay,
'minimal_booking_time': agenda.minimal_booking_time,
'edit_role': agenda.edit_role.name if agenda.edit_role else None,
'view_role': agenda.view_role.name if agenda.view_role else None,
'category': agenda.category.slug if agenda.category else None,
@ -696,6 +700,13 @@ def get_start_and_end_datetime_from_request(request):
return serializer.validated_data.get('date_start'), serializer.validated_data.get('date_end')
def get_minutes_from_request(request):
serializer = serializers.MinutesSerializer(data=request.query_params)
if not serializer.is_valid():
raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
return serializer.validated_data.get('minutes')
def make_booking(event, payload, extra_data, primary_booking=None, in_waiting_list=False, color=None):
out_of_min_delay = False
if event.agenda.min_booking_datetime and event.start_datetime < event.agenda.min_booking_datetime:
@ -1017,6 +1028,7 @@ class MeetingDatetimes(APIView):
resources = get_resources_from_request(request, agenda)
start_datetime, end_datetime = get_start_and_end_datetime_from_request(request)
minutes = get_minutes_from_request(request)
booked_user_external_id = request.GET.get('user_external_id') or None
excluded_user_external_id = request.GET.get('exclude_user_external_id') or None
if (
@ -1081,6 +1093,8 @@ class MeetingDatetimes(APIView):
for slot in generator_of_unique_slots:
if request.GET.get('hide_disabled') and slot.full:
continue
if minutes and slot.start_datetime.minute not in minutes:
continue
# Make virtual id for a slot, combining meeting_type.id and
# iso-format of date and time.

View File

@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: chrono 0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-02 17:57+0000\n"
"PO-Revision-Date: 2023-03-02 18:58+0100\n"
"POT-Creation-Date: 2023-03-23 10:33+0100\n"
"PO-Revision-Date: 2023-03-23 10:35+0100\n"
"Last-Translator: Frederic Peters <fpeters@entrouvert.com>\n"
"Language: French\n"
"MIME-Version: 1.0\n"
@ -205,6 +205,10 @@ msgstr ""
"Interdire le pointage des réservations une fois lévénement marqué comme "
"pointé"
#: agendas/models.py
msgid "Enable the check of bookings when event has not passed"
msgstr "Activer le pointage des réservations sur les événements à venir"
#: agendas/models.py
msgid "Filters"
msgstr "Filtres"
@ -243,6 +247,17 @@ msgstr ""
msgid "Events type"
msgstr "Type dévénements"
#: agendas/models.py
msgid "Minimal booking time"
msgstr "Heure de réservation minimale"
#: agendas/models.py
msgid ""
"Ex.: 08:00:00. If left empty, available events will be those that are later "
"than the current time."
msgstr ""
"Ex.: 08:00:00. Si ce champ est laissé vide alors les évènements ou rendez-vous disponibles à la réservation sont ceux dont le début est plus tard que l'heure courante, en prenant en compte les délais minimaux et maximaux de réservation en jours"
#: agendas/models.py
#, python-format
msgid "Missing \"%s\" resource"
@ -408,6 +423,7 @@ msgstr "URL"
#: agendas/models.py manager/forms.py
#: manager/templates/chrono/manager_event_check.html
#: manager/templates/chrono/manager_event_check_booking_fragment.html
msgid "Cancelled"
msgstr "Annulé"
@ -2176,6 +2192,14 @@ msgstr[1] "%(count)s jours"
msgid "Maximal booking delay:"
msgstr "Délai de réservation maximal :"
#: manager/templates/chrono/manager_agenda_settings.html
msgid "Minimal booking time:"
msgstr "Heure de réservation minimale :"
#: manager/templates/chrono/manager_agenda_settings.html
msgid "current time"
msgstr "heure actuelle"
#: manager/templates/chrono/manager_agenda_settings.html
msgid "Edit Role:"
msgstr "Rôle dédition :"
@ -2435,10 +2459,6 @@ msgstr "Marquer lévénement comme étant pointé"
msgid "Mark all bookings without status:"
msgstr "Marquer toutes les réservations non précisées :"
#: manager/templates/chrono/manager_event_check.html
msgid "Not booked"
msgstr "Non réservé"
#: manager/templates/chrono/manager_event_check.html
#, python-format
msgid "Waiting List (%(booked_places)s/%(places)s)"
@ -2449,10 +2469,19 @@ msgstr "Liste dattente (%(booked_places)s/%(places)s)"
msgid "places"
msgstr "places"
#: manager/templates/chrono/manager_event_check_booking_fragment.html
msgid "Not booked"
msgstr "Non réservé"
#: manager/templates/chrono/manager_event_check_booking_fragment.html
msgid "Present,Absent,-"
msgstr "Présence,Absence,-"
#: manager/templates/chrono/manager_event_check_booking_fragment.html
msgctxt "check"
msgid "Reset"
msgstr "Réinitialiser"
#: manager/templates/chrono/manager_event_detail_fragment.html
msgid "Publication date:"
msgstr "Date de publication :"
@ -2620,6 +2649,10 @@ msgstr ""
"Interdire le pointage des réservations une fois lévénement marqué comme "
"pointé :"
#: manager/templates/chrono/manager_events_agenda_settings.html
msgid "Enable the check of bookings when event has not passed:"
msgstr "Activer le pointage des réservations sur les événements à venir :"
#: manager/templates/chrono/manager_events_agenda_settings.html
#, python-format
msgid "%(label)s: %(display_value)s will be notified."

View File

@ -129,6 +129,7 @@ class AgendaBookingDelaysForm(forms.ModelForm):
'minimal_booking_delay',
'minimal_booking_delay_in_working_days',
'maximal_booking_delay',
'minimal_booking_time',
]
def __init__(self, *args, **kwargs):
@ -1415,6 +1416,7 @@ class AgendaBookingCheckSettingsForm(forms.ModelForm):
'booking_check_filters',
'mark_event_checked_auto',
'disable_check_update',
'enable_check_for_future_events',
]

View File

@ -47,6 +47,8 @@ $(function() {
$form.parent().parent().html(html);
if (!$('.booking-status.without-status').length) {
$('tr.booking.all-bookings').hide();
} else {
$('tr.booking.all-bookings').show();
}
}).fail(function() {
location.reload();

View File

@ -89,6 +89,10 @@
{% if agenda.maximal_booking_delay is not None %}
{% blocktrans count count=agenda.maximal_booking_delay %}{{ count }} day{% plural %}{{ count }} days{% endblocktrans %}
{% else %}<i>{% trans "undefined" %}</i>{% endif %}</li>
{% if agenda.minimal_booking_delay is not None or agenda.maximal_booking_delay is not None %}
<li>{% trans "Minimal booking time:" %}
{% if agenda.minimal_booking_time is not None %}{{ agenda.minimal_booking_time }}{% else %}{% trans "current time" %}{% endif %}</li>
{% endif %}
</ul>
<div class="panel--buttons">
<a rel="popup" class="button" href="{% url 'chrono-manager-agenda-booking-delays' pk=object.id %}">{% trans 'Configure' %}</a>

View File

@ -74,23 +74,7 @@
{% endif %}
{% for result in results %}
<tr class="booking">
{% if result.kind == 'booking' %}
{% if result.cancellation_datetime is None %}
{% include "chrono/manager_event_check_booking_fragment.html" with booking=result %}
{% else %}
<td class="booking-username main-list">{{ result.get_user_block }}</td>
<td class="booking-status">({% trans "Cancelled" %})</td>
{% if not event.checked or not agenda.disable_check_update %}
<td class="booking-actions"></td>
{% endif %}
{% endif %}
{% elif result.kind == 'subscription' %}
<td class="booking-username main-list">{{ result.get_user_block }}</td>
<td class="booking-status">({% trans "Not booked" %})</td>
{% if not event.checked or not agenda.disable_check_update %}
<td class="booking-actions"></td>
{% endif %}
{% endif %}
{% include "chrono/manager_event_check_booking_fragment.html" with booking=result %}
</tr>
{% endfor %}
</tbody>

View File

@ -1,43 +1,74 @@
{% load i18n %}
<td class="booking-username main-list">{{ booking.get_user_block }}{% if booking.places_count > 1 %} ({{ booking.places_count }} {% trans "places" %}){% endif %}</td>
<td class="booking-status {% if booking.user_was_present is None %}without-status{% endif %}">
{{ booking.user_was_present|yesno:_('Present,Absent,-') }}
{% if booking.user_was_present is not None and booking.user_check_type_label %}
({{ booking.user_check_type_label }})
<td class="booking-status {% if booking.kind != "subscription" and booking.cancellation_datetime is None and booking.user_was_present is None %}without-status{% endif %}" data-{{ booking.kind }}-id="{{ booking.id }}">
{% if booking.kind == "subscription" %}
({% trans "Not booked" %})
{% elif booking.cancellation_datetime is None %}
{{ booking.user_was_present|yesno:_('Present,Absent,-') }}
{% if booking.user_was_present is not None and booking.user_check_type_label %}
({{ booking.user_check_type_label }})
{% endif %}
{% else %}
({% trans "Cancelled" %})
{% endif %}
{% if not event.checked or not agenda.disable_check_update %}
{% if booking.user_was_present is not None %}
<form method="post" action="{% url 'chrono-manager-booking-reset' pk=agenda.pk booking_pk=booking.pk %}" class="with-ajax reset">
{% csrf_token %}
<a href="#">{% trans "Reset" context "check" %}</a>
<script>
$(function() {
$('td.booking-status[data-{{ booking.kind }}-id="{{ booking.id }}"] form.reset a').on('click',
function(e) {
e.preventDefault();
$(this).parents('form.reset').submit();
});
});
</script>
</form>
{% endif %}
{% endif %}
</td>
{% if not event.checked or not agenda.disable_check_update %}
<td class="booking-actions" data-booking-id="{{ booking.id }}">
<form method="post" action="{% url 'chrono-manager-booking-presence' pk=agenda.pk booking_pk=booking.pk %}" class="with-ajax presence">
{% csrf_token %}
<button class="submit-button"
{% if booking.user_was_present is True %}disabled{% endif %}
>{% trans "Presence" %}</button>
{% if booking.presence_form.check_type.field.choices.1 %}{{ booking.presence_form.check_type }}{% endif %}
<script>
$(function() {
$('td[data-booking-id="{{ booking.id }}"] form.presence select').on('change',
function() {
$(this).parents('form.presence').submit();
});
});
</script>
</form>
<td class="booking-actions" data-{{ booking.kind }}-id="{{ booking.id }}">
{% if booking.kind == "subscription" %}
<form method="post" action="{% url 'chrono-manager-subscription-presence' pk=agenda.pk subscription_pk=booking.pk event_pk=event.pk %}" class="with-ajax presence">
{% else %}
<form method="post" action="{% url 'chrono-manager-booking-presence' pk=agenda.pk booking_pk=booking.pk %}" class="with-ajax presence">
{% endif %}
{% csrf_token %}
<button class="submit-button"
{% if booking.user_was_present is True %}disabled{% endif %}
>{% trans "Presence" %}</button>
{% if booking.presence_form.check_type.field.choices.1 %}{{ booking.presence_form.check_type }}{% endif %}
<script>
$(function() {
$('td.booking-actions[data-{{ booking.kind }}-id="{{ booking.id }}"] form.presence select').on('change',
function() {
$(this).parents('form.presence').submit();
});
});
</script>
</form>
{% if booking.kind == "subscription" %}
<form method="post" action="{% url 'chrono-manager-subscription-absence' pk=agenda.pk subscription_pk=booking.pk event_pk=event.pk %}" class="with-ajax absence">
{% else %}
<form method="post" action="{% url 'chrono-manager-booking-absence' pk=agenda.pk booking_pk=booking.pk %}" class="with-ajax absence">
{% csrf_token %}
<button class="submit-button"
{% if booking.user_was_present is False %}disabled{% endif %}
>{% trans "Absence" %}</button>
{% if booking.absence_form.check_type.field.choices.1 %}{{ booking.absence_form.check_type }}{% endif %}
<script>
$(function() {
$('td[data-booking-id="{{ booking.id }}"] form.absence select').on('change',
function() {
$(this).parents('form.absence').submit();
});
{% endif %}
{% csrf_token %}
<button class="submit-button"
{% if booking.user_was_present is False %}disabled{% endif %}
>{% trans "Absence" %}</button>
{% if booking.absence_form.check_type.field.choices.1 %}{{ booking.absence_form.check_type }}{% endif %}
<script>
$(function() {
$('td.booking-actions[data-{{ booking.kind }}-id="{{ booking.id }}"] form.absence select').on('change',
function() {
$(this).parents('form.absence').submit();
});
</script>
</form>
});
</script>
</form>
</td>
{% endif %}

View File

@ -56,8 +56,10 @@
{% endif %}
</ul>
{% endif %}
{% if event.is_day_past and not event.cancelled %}
<a href="{% url 'chrono-manager-event-check' pk=agenda.pk event_pk=event.pk %}">{% trans "Check" %}</a>
{% if not event.cancelled %}
{% if event.is_day_past or agenda.enable_check_for_future_events %}
<a href="{% url 'chrono-manager-event-check' pk=agenda.pk event_pk=event.pk %}">{% trans "Check" %}</a>
{% endif %}
{% endif %}
</span>
{% endblock %}

View File

@ -113,6 +113,7 @@
{% endwith %}
<li>{% trans "Automatically mark event as checked when all bookings have been checked:" %} {{ agenda.mark_event_checked_auto|yesno }}</li>
<li>{% trans "Prevent the check of bookings when event was marked as checked:" %} {{ agenda.disable_check_update|yesno }}</li>
<li>{% trans "Enable the check of bookings when event has not passed:" %} {{ agenda.enable_check_for_future_events|yesno }}</li>
</ul>
<div class="panel--buttons">
<a rel="popup" class="button" href="{% url 'chrono-manager-agenda-booking-check-settings' pk=object.pk %}">{% trans 'Configure' %}</a>

View File

@ -409,6 +409,21 @@ urlpatterns = [
views.booking_absence,
name='chrono-manager-booking-absence',
),
path(
'agendas/<int:pk>/bookings/<int:booking_pk>/reset',
views.booking_reset,
name='chrono-manager-booking-reset',
),
path(
'agendas/<int:pk>/subscriptions/<int:subscription_pk>/presence/<int:event_pk>',
views.subscription_presence,
name='chrono-manager-subscription-presence',
),
path(
'agendas/<int:pk>/subscriptions/<int:subscription_pk>/absence/<int:event_pk>',
views.subscription_absence,
name='chrono-manager-subscription-absence',
),
re_path(
r'^agendas/events.csv$',
views.agenda_import_events_sample_csv,

View File

@ -80,6 +80,7 @@ from chrono.agendas.models import (
SharedCustodyPeriod,
SharedCustodyRule,
SharedCustodySettings,
Subscription,
TimePeriod,
TimePeriodException,
TimePeriodExceptionSource,
@ -2540,7 +2541,11 @@ class EventCheckView(ViewableAgendaMixin, DetailView):
def get_queryset(self):
queryset = super().get_queryset()
queryset = Event.annotate_booking_checks(queryset)
return queryset.filter(agenda=self.agenda, start_datetime__date__lte=now().date(), cancelled=False)
return queryset.filter(
Q(start_datetime__date__lte=now().date()) | Q(agenda__enable_check_for_future_events=True),
agenda=self.agenda,
cancelled=False,
)
def get_filters(self, booked_queryset, subscription_queryset):
agenda_filters = self.agenda.get_booking_check_filters()
@ -2623,6 +2628,12 @@ class EventCheckView(ViewableAgendaMixin, DetailView):
booking.kind = 'booking'
results.append(booking)
for subscription in subscription_filterset.qs:
subscription.absence_form = BookingCheckAbsenceForm(
agenda=self.agenda,
)
subscription.presence_form = BookingCheckPresenceForm(
agenda=self.agenda,
)
subscription.kind = 'subscription'
results.append(subscription)
# sort results
@ -2637,9 +2648,8 @@ class EventCheckView(ViewableAgendaMixin, DetailView):
# set context
context['booked_without_status'] = booked_without_status
if context['booked_without_status']:
context['absence_form'] = BookingCheckAbsenceForm(agenda=self.agenda)
context['presence_form'] = BookingCheckPresenceForm(agenda=self.agenda)
context['absence_form'] = BookingCheckAbsenceForm(agenda=self.agenda)
context['presence_form'] = BookingCheckPresenceForm(agenda=self.agenda)
context['filterset'] = booked_filterset
context['results'] = results
context['waiting'] = waiting_qs
@ -2660,16 +2670,15 @@ class EventCheckMixin:
self.event = get_object_or_404(
Event,
Q(checked=False) | Q(agenda__disable_check_update=False),
Q(start_datetime__date__lte=now().date()) | Q(agenda__enable_check_for_future_events=True),
pk=kwargs.get('event_pk'),
agenda=self.agenda,
start_datetime__date__lte=now().date(),
cancelled=False,
)
def get_bookings(self):
return self.event.booking_set.filter(
event__agenda=self.agenda,
event__start_datetime__date__lte=now().date(),
event__cancelled=False,
cancellation_datetime__isnull=True,
in_waiting_list=False,
@ -3481,17 +3490,20 @@ booking_cancel = BookingCancelView.as_view()
class BookingCheckMixin:
def get_booking(self, **kwargs):
return get_object_or_404(
booking = get_object_or_404(
Booking,
Q(event__checked=False) | Q(event__agenda__disable_check_update=False),
Q(event__start_datetime__date__lte=now().date())
| Q(event__agenda__enable_check_for_future_events=True),
pk=kwargs['booking_pk'],
event__agenda=self.agenda,
event__start_datetime__date__lte=now().date(),
event__cancelled=False,
cancellation_datetime__isnull=True,
in_waiting_list=False,
primary_booking__isnull=True,
)
if not booking.event.agenda.subscriptions.exists() and booking.cancellation_datetime is not None:
raise Http404
return booking
def get_check_type(self, kind):
form = self.get_form()
@ -3509,6 +3521,7 @@ class BookingCheckMixin:
booking.presence_form = BookingCheckPresenceForm(
agenda=self.agenda, initial={'check_type': booking.user_check_type_slug}
)
booking.kind = 'booking'
return render(
request,
'chrono/manager_event_check_booking_fragment.html',
@ -3522,9 +3535,7 @@ class BookingCheckMixin:
)
class BookingPresenceView(ViewableAgendaMixin, BookingCheckMixin, FormView):
form_class = BookingCheckPresenceForm
class PresenceViewMixin:
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['agenda'] = self.agenda
@ -3540,12 +3551,7 @@ class BookingPresenceView(ViewableAgendaMixin, BookingCheckMixin, FormView):
return self.response(request, booking)
booking_presence = BookingPresenceView.as_view()
class BookingAbsenceView(ViewableAgendaMixin, BookingCheckMixin, FormView):
form_class = BookingCheckAbsenceForm
class AbsenceViewMixin:
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['agenda'] = self.agenda
@ -3561,9 +3567,81 @@ class BookingAbsenceView(ViewableAgendaMixin, BookingCheckMixin, FormView):
return self.response(request, booking)
class BookingPresenceView(ViewableAgendaMixin, BookingCheckMixin, PresenceViewMixin, FormView):
form_class = BookingCheckPresenceForm
booking_presence = BookingPresenceView.as_view()
class BookingAbsenceView(ViewableAgendaMixin, BookingCheckMixin, AbsenceViewMixin, FormView):
form_class = BookingCheckAbsenceForm
booking_absence = BookingAbsenceView.as_view()
class BookingResetView(ViewableAgendaMixin, BookingCheckMixin, FormView):
def post(self, request, *args, **kwargs):
booking = self.get_booking(**kwargs)
booking.reset_user_was_present()
return self.response(request, booking)
booking_reset = BookingResetView.as_view()
class SubscriptionCheckMixin(BookingCheckMixin):
def get_booking(self, **kwargs):
event = get_object_or_404(
Event,
Q(checked=False) | Q(agenda__disable_check_update=False),
Q(start_datetime__date__lte=now().date()) | Q(agenda__enable_check_for_future_events=True),
agenda=self.agenda,
cancelled=False,
pk=kwargs['event_pk'],
)
subscription = get_object_or_404(
Subscription,
agenda=self.agenda,
pk=kwargs['subscription_pk'],
date_start__lte=event.start_datetime,
date_end__gt=event.start_datetime,
)
try:
booking = event.booking_set.get(user_external_id=subscription.user_external_id)
raise Http404
except Booking.MultipleObjectsReturned:
raise Http404
except Booking.DoesNotExist:
pass
booking = event.booking_set.create(
user_external_id=subscription.user_external_id,
user_last_name=subscription.user_last_name,
user_first_name=subscription.user_first_name,
user_email=subscription.user_email,
user_phone_number=subscription.user_phone_number,
extra_data=subscription.extra_data,
)
# create booking
return booking
class SubscriptionPresenceView(ViewableAgendaMixin, SubscriptionCheckMixin, PresenceViewMixin, FormView):
form_class = BookingCheckPresenceForm
subscription_presence = SubscriptionPresenceView.as_view()
class SubscriptionAbsenceView(ViewableAgendaMixin, SubscriptionCheckMixin, AbsenceViewMixin, FormView):
form_class = BookingCheckAbsenceForm
subscription_absence = SubscriptionAbsenceView.as_view()
class EventCancelView(ViewableAgendaMixin, UpdateView):
template_name = 'chrono/manager_confirm_event_cancellation.html'
model = Event

View File

@ -87,6 +87,8 @@ DATABASES = {
}
}
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/

View File

@ -2608,3 +2608,55 @@ def test_datetimes_end_datetime(app):
('2023-03-10 10:00:00', '2023-03-10 10:30:00'),
('2023-03-10 10:30:00', '2023-03-10 11:00:00'),
]
def test_datetimes_api_meetings_agenda_filter_minutes(app):
agenda = Agenda(
label='Foo bar Meeting', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=2
)
agenda.save()
meeting_type = MeetingType(agenda=agenda, label='Blah', duration=15)
meeting_type.save()
tomorrow_weekday = (localtime(now()).weekday() + 1) % 7
default_desk, _ = Desk.objects.get_or_create(agenda=agenda, label='Desk 1')
time_period = TimePeriod(
weekday=tomorrow_weekday,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
desk=default_desk,
)
time_period.save()
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meeting_type.agenda.slug, meeting_type.slug)
resp = app.get(api_url)
assert len(resp.json['data']) == 8
assert datetime_from_str(resp.json['data'][0]['datetime']).minute == 0
assert datetime_from_str(resp.json['data'][1]['datetime']).minute == 15
assert datetime_from_str(resp.json['data'][2]['datetime']).minute == 30
assert datetime_from_str(resp.json['data'][3]['datetime']).minute == 45
assert datetime_from_str(resp.json['data'][4]['datetime']).minute == 0
assert datetime_from_str(resp.json['data'][5]['datetime']).minute == 15
assert datetime_from_str(resp.json['data'][6]['datetime']).minute == 30
assert datetime_from_str(resp.json['data'][7]['datetime']).minute == 45
# filter on minutes
api_url = '/api/agenda/%s/meetings/%s/datetimes/?minutes=0' % (
meeting_type.agenda.slug,
meeting_type.slug,
)
resp = app.get(api_url)
assert len(resp.json['data']) == 2
assert datetime_from_str(resp.json['data'][0]['datetime']).minute == 0
assert datetime_from_str(resp.json['data'][1]['datetime']).minute == 0
# filter on minutes, with more choices
api_url = '/api/agenda/%s/meetings/%s/datetimes/?minutes=0,30' % (
meeting_type.agenda.slug,
meeting_type.slug,
)
resp = app.get(api_url)
assert len(resp.json['data']) == 4
assert datetime_from_str(resp.json['data'][0]['datetime']).minute == 0
assert datetime_from_str(resp.json['data'][1]['datetime']).minute == 30
assert datetime_from_str(resp.json['data'][2]['datetime']).minute == 0
assert datetime_from_str(resp.json['data'][3]['datetime']).minute == 30

View File

@ -2550,3 +2550,70 @@ def test_fillslot_recurring_event_booking_forbidden(app, user):
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug))
assert resp.json['err'] == 1
assert resp.json['err_class'] == 'event is recurrent'
# monday 15th march at 12:00 CEST
@pytest.mark.freeze_time('2021-03-15T12:00:00+01:00')
def test_user_external_id(app, user):
user_external_id = 'foobar'
meeting_agenda_1 = Agenda.objects.create(
label='Meeting agenda', slug='mt1', kind='meetings', minimal_booking_delay=0
)
desk = Desk.objects.create(agenda=meeting_agenda_1, slug='desk')
meeting_type = MeetingType.objects.create(agenda=meeting_agenda_1, slug='mtt')
TimePeriod.objects.create(
weekday=0, start_time=datetime.time(9, 0), end_time=datetime.time(17, 00), desk=desk
)
meeting_agenda_2 = meeting_agenda_1.duplicate()
meeting_agenda_2.maximal_booking_delay = 20
meeting_agenda_2.save()
meeting_agenda_3 = meeting_agenda_1.duplicate()
meeting_agenda_3.maximal_booking_delay = 30
meeting_agenda_3.save()
virtual_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual')
virtual_agenda.real_agendas.add(meeting_agenda_1, meeting_agenda_2, meeting_agenda_3)
meeting_agenda_1_datetimes_url = f'/api/agenda/{meeting_agenda_1.slug}/meetings/{meeting_type.slug}/datetimes/?user_external_id={user_external_id}'
meeting_agenda_2_datetimes_url = f'/api/agenda/{meeting_agenda_2.slug}/meetings/{meeting_type.slug}/datetimes/?user_external_id={user_external_id}'
meeting_agenda_3_datetimes_url = f'/api/agenda/{meeting_agenda_3.slug}/meetings/{meeting_type.slug}/datetimes/?user_external_id={user_external_id}'
virtual_agenda_datetimes_url = f'/api/agenda/{virtual_agenda.slug}/meetings/{meeting_type.slug}/datetimes/?user_external_id={user_external_id}'
today_at_15 = localtime(now()).replace(hour=15, minute=0)
# check all slots are free
resp = app.get(virtual_agenda_datetimes_url)
assert not any(x['disabled'] for x in resp.json['data'])
resp = app.get(meeting_agenda_1_datetimes_url)
assert not any(x['disabled'] for x in resp.json['data'])
resp = app.get(meeting_agenda_2_datetimes_url)
assert not any(x['disabled'] for x in resp.json['data'])
resp = app.get(meeting_agenda_3_datetimes_url)
assert not any(x['disabled'] for x in resp.json['data'])
for agenda in virtual_agenda.get_real_agendas():
meeting_event = Event.objects.create(
label='Event',
slug='meeting1',
start_datetime=today_at_15,
agenda=agenda,
places=1,
meeting_type=agenda.meetingtype_set.all()[0],
desk=agenda.desk_set.all()[0],
)
Booking.objects.create(event=meeting_event, user_external_id=user_external_id)
# now some slots are disabled
resp = app.get(virtual_agenda_datetimes_url)
assert any(x['disabled'] for x in resp.json['data'])
agenda_datetimes_url = f'/api/agenda/{agenda.slug}/meetings/{meeting_type.slug}/datetimes/?user_external_id={user_external_id}'
resp = app.get(agenda_datetimes_url)
assert any(x['disabled'] for x in resp.json['data'])
for other_agenda in virtual_agenda.get_real_agendas():
if other_agenda == agenda:
continue
other_agenda_datetimes_url = f'/api/agenda/{other_agenda.slug}/meetings/{meeting_type.slug}/datetimes/?user_external_id={user_external_id}'
resp = app.get(other_agenda_datetimes_url)
assert not any(x['disabled'] for x in resp.json['data'])
meeting_event.delete()

View File

@ -66,6 +66,7 @@ def test_agendas_api(app):
'minimal_booking_delay': 1,
'minimal_booking_delay_in_working_days': False,
'maximal_booking_delay': 56,
'minimal_booking_time': '00:00:00',
'edit_role': 'Edit',
'view_role': None,
'category': 'category-a',
@ -85,6 +86,7 @@ def test_agendas_api(app):
'minimal_booking_delay': 1,
'minimal_booking_delay_in_working_days': False,
'maximal_booking_delay': 56,
'minimal_booking_time': '00:00:00',
'edit_role': None,
'view_role': None,
'category': 'category-a',
@ -104,6 +106,7 @@ def test_agendas_api(app):
'minimal_booking_delay': 1,
'minimal_booking_delay_in_working_days': False,
'maximal_booking_delay': 56,
'minimal_booking_time': '00:00:00',
'edit_role': None,
'view_role': None,
'category': None,
@ -121,6 +124,7 @@ def test_agendas_api(app):
'slug': 'foo-bar-meeting',
'minimal_booking_delay': 1,
'maximal_booking_delay': 56,
'minimal_booking_time': '00:00:00',
'edit_role': None,
'view_role': 'View',
'category': 'category-b',
@ -144,6 +148,7 @@ def test_agendas_api(app):
'slug': 'foo-bar-meeting-2',
'minimal_booking_delay': 1,
'maximal_booking_delay': 56,
'minimal_booking_time': '00:00:00',
'edit_role': None,
'view_role': None,
'category': None,
@ -164,6 +169,7 @@ def test_agendas_api(app):
'slug': 'virtual-agenda',
'minimal_booking_delay': 1,
'maximal_booking_delay': 56,
'minimal_booking_time': '00:00:00',
'edit_role': 'Edit',
'view_role': 'View',
'category': None,
@ -411,6 +417,7 @@ def test_virtual_agenda_detail(app, virtual_meetings_agenda):
'slug': 'virtual-agenda',
'minimal_booking_delay': None,
'maximal_booking_delay': None,
'minimal_booking_time': '00:00:00',
'edit_role': None,
'view_role': None,
'category': None,
@ -457,25 +464,25 @@ def test_agenda_api_delete_busy(app, user):
assert not Agenda.objects.exists()
@pytest.mark.freeze_time('2021-07-09')
@pytest.mark.freeze_time('2021-07-09T08:00:00.0+02:00')
def test_add_agenda(app, user, settings):
events_type = EventsType.objects.create(label='Type A')
category_a = Category.objects.create(label='Category A')
api_url = '/api/agenda/'
# no authentication
resp = app.post(api_url, status=401)
resp = app.post_json(api_url, status=401)
assert resp.json['detail'] == 'Authentication credentials were not provided.'
# wrong password
app.authorization = ('Basic', ('john.doe', 'wrong'))
resp = app.post(api_url, status=401)
resp = app.post_json(api_url, status=401)
assert resp.json['detail'] == 'Invalid username/password.'
app.authorization = ('Basic', ('john.doe', 'password'))
# missing fields
resp = app.post(api_url, status=400)
resp = app.post_json(api_url, status=400)
assert resp.json['err']
assert resp.json['errors'] == {'label': ['This field is required.'], 'slug': ['This field is required.']}
@ -492,7 +499,7 @@ def test_add_agenda(app, user, settings):
'category': 'oups',
'events_type': 'oups',
}
resp = app.post(api_url, params=params, status=400)
resp = app.post_json(api_url, params=params, status=400)
assert resp.json['err']
assert resp.json['errors'] == {
'kind': ['"oups" is not a valid choice.'],
@ -512,7 +519,7 @@ def test_add_agenda(app, user, settings):
'label': 'foo',
'slug': meeting_agenda.slug,
}
resp = app.post(api_url, params=params, status=400)
resp = app.post_json(api_url, params=params, status=400)
assert resp.json['err']
assert resp.json['errors'] == {'slug': ['agenda with this Identifier already exists.']}
@ -523,7 +530,7 @@ def test_add_agenda(app, user, settings):
'kind': 'meetings',
'minimal_booking_delay_in_working_days': True,
}
resp = app.post(api_url, params=params, status=400)
resp = app.post_json(api_url, params=params, status=400)
assert resp.json['err']
assert resp.json['errors'] == {
'minimal_booking_delay_in_working_days': ['Option not available on meetings agenda']
@ -534,7 +541,7 @@ def test_add_agenda(app, user, settings):
'kind': 'meetings',
'events_type': 'type-a',
}
resp = app.post(api_url, params=params, status=400)
resp = app.post_json(api_url, params=params, status=400)
assert resp.json['err']
assert resp.json['errors'] == {'events_type': ['Option not available on meetings agenda']}
@ -543,7 +550,7 @@ def test_add_agenda(app, user, settings):
'label': 'My Agenda',
'slug': 'my-agenda',
}
resp = app.post(api_url, params=params)
resp = app.post_json(api_url, params=params)
assert not resp.json['err']
assert len(resp.json['data']) == 1
agenda = Agenda.objects.get(slug='my-agenda')
@ -567,11 +574,14 @@ def test_add_agenda(app, user, settings):
'mark_event_checked_auto': True,
'disable_check_update': False,
}
resp = app.post(api_url, params=params)
resp = app.post_json(api_url, params=params)
assert not resp.json['err']
assert len(resp.json['data']) == 1
agenda = Agenda.objects.get(slug='foo-meetings')
assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10)
assert agenda.min_booking_datetime == localtime(now()).replace(
day=10, hour=0, minute=0, second=0, microsecond=0
)
assert agenda.minimal_booking_time == datetime.time(0)
assert agenda.edit_role == edit_group
assert agenda.view_role == view_group
assert agenda.category == category_a
@ -587,6 +597,7 @@ def test_add_agenda(app, user, settings):
'minimal_booking_delay': 1,
'minimal_booking_delay_in_working_days': True,
'maximal_booking_delay': 3,
'minimal_booking_time': None,
'anonymize_delay': 30,
'edit_role': 'Edit',
'view_role': 'View',
@ -596,13 +607,14 @@ def test_add_agenda(app, user, settings):
'disable_check_update': True,
'booking_check_filters': 'foo,bar,baz',
}
resp = app.post(api_url, params=params)
resp = app.post_json(api_url, params=params)
assert not resp.json['err']
assert len(resp.json['data']) == 1
agenda = Agenda.objects.get(slug='foo-events')
assert agenda.edit_role == edit_group
assert agenda.view_role == view_group
assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 12)
assert agenda.min_booking_datetime == localtime(now()).replace(day=12)
assert agenda.minimal_booking_time is None
assert agenda.category == category_a
assert agenda.events_type == events_type
assert agenda.mark_event_checked_auto is False

View File

@ -689,6 +689,14 @@ def test_options_agenda_booking_check_options(app, admin_user):
agenda.refresh_from_db()
assert agenda.disable_check_update is True
# check enable check future events
assert agenda.enable_check_for_future_events is False
resp = app.get(url)
resp.form['enable_check_for_future_events'] = True
resp = resp.form.submit()
agenda.refresh_from_db()
assert agenda.enable_check_for_future_events is True
# check kind
agenda.kind = 'meetings'
agenda.save()

View File

@ -1504,14 +1504,26 @@ def test_event_check(app, admin_user):
login(app)
# event not in past
assert agenda.enable_check_for_future_events is False
resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk))
assert '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk) not in resp
app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), status=404)
# not in past, but check for future events is enabled
agenda.enable_check_for_future_events = True
agenda.save()
resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk))
assert '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk) in resp
app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), status=200)
# event is today
agenda.enable_check_for_future_events = False
agenda.save()
event.start_datetime = localtime(now()).replace(hour=22, minute=0) # it's ok all the day
event.save()
resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk))
assert '/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk) in resp
app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), status=200)
# unknown agenda
app.get('/manage/agendas/%s/events/%s/check' % (0, event.pk), status=404)
@ -1608,20 +1620,8 @@ def test_event_check(app, admin_user):
assert '&lt;b&gt;Subscription 14&lt;/b&gt; Foo Bar' in resp
assert '&lt;b&gt;User Waiting&lt;/b&gt; Foo Bar' in resp
# cancelled booking
token = resp.context['csrf_token']
app.post(
'/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking6.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking6.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
# booking in waiting list
token = resp.context['csrf_token']
app.post(
'/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking7.pk),
params={'csrfmiddlewaretoken': token},
@ -1740,6 +1740,27 @@ def test_event_checked(app, admin_user):
assert '<span class="meta meta-error">Absents 4</span>' in resp
assert 'meta meta-disabled' not in resp
# event not in past
agenda.disable_check_update = False
agenda.save()
assert agenda.enable_check_for_future_events is False
event.start_datetime = now() + datetime.timedelta(days=1)
event.save()
app.post(
'/manage/agendas/%s/events/%s/checked' % (agenda.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
# not in past, but check for future events is enabled
agenda.enable_check_for_future_events = True
agenda.save()
app.post(
'/manage/agendas/%s/events/%s/checked' % (agenda.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=302,
)
@mock.patch('chrono.manager.forms.get_agenda_check_types')
def test_event_check_filters(check_types, app, admin_user):
@ -2179,12 +2200,32 @@ def test_event_check_booking(check_types, app, admin_user):
)
assert agenda.mark_event_checked_auto is False
def _test_reset():
resp = app.post(
'/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
).follow()
assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) in resp
assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp
assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) not in resp
booking.refresh_from_db()
assert booking.user_was_present is None
assert booking.user_check_type_slug is None
assert booking.user_check_type_label is None
secondary_booking.refresh_from_db()
assert secondary_booking.user_was_present is None
assert secondary_booking.user_check_type_slug is None
assert secondary_booking.user_check_type_label is None
event.refresh_from_db()
assert event.checked is False
login(app)
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert resp.pyquery.find('td.booking-status')[0].text.strip() == '-'
assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 0
assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) in resp
assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp
assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) not in resp
# set as present
token = resp.context['csrf_token']
@ -2195,6 +2236,9 @@ def test_event_check_booking(check_types, app, admin_user):
assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Present'
assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1
assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text == 'Presence'
assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) in resp
assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp
assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) in resp
booking.refresh_from_db()
assert booking.user_was_present is True
assert booking.user_check_type_slug is None
@ -2206,6 +2250,9 @@ def test_event_check_booking(check_types, app, admin_user):
event.refresh_from_db()
assert event.checked is False
# reset
_test_reset()
agenda.mark_event_checked_auto = True
agenda.save()
@ -2235,6 +2282,9 @@ def test_event_check_booking(check_types, app, admin_user):
assert len(resp.pyquery.find('td.booking-actions form.absence select')) == 1
assert len(resp.pyquery.find('td.booking-actions form.presence select')) == 0
# reset
_test_reset()
# set as absent with check_type
resp = app.post(
'/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk),
@ -2249,6 +2299,11 @@ def test_event_check_booking(check_types, app, admin_user):
assert secondary_booking.user_was_present is False
assert secondary_booking.user_check_type_slug == 'foo-reason'
assert secondary_booking.user_check_type_label == 'Foo reason'
event.refresh_from_db()
assert event.checked is True
# reset
_test_reset()
# set as present without check_type
resp = app.post(
@ -2281,6 +2336,9 @@ def test_event_check_booking(check_types, app, admin_user):
assert len(resp.pyquery.find('td.booking-actions form.absence select')) == 1
assert len(resp.pyquery.find('td.booking-actions form.presence select')) == 1
# reset
_test_reset()
# set as present with check_type
resp = app.post(
'/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk),
@ -2295,14 +2353,59 @@ def test_event_check_booking(check_types, app, admin_user):
assert secondary_booking.user_was_present is True
assert secondary_booking.user_check_type_slug == 'bar-reason'
assert secondary_booking.user_check_type_label == 'Bar reason'
event.refresh_from_db()
assert event.checked is True
# mark the event as checked
event.checked = True
event.save()
# event is checked
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) in resp
assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp
resp = app.post(
assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) in resp
app.post(
'/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
status=302,
)
app.post(
'/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
status=302,
)
app.post(
'/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
status=302,
)
# event not in past
assert agenda.enable_check_for_future_events is False
event.start_datetime = now() + datetime.timedelta(days=1)
event.save()
app.post(
'/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
# not in past, but check for future events is enabled
agenda.enable_check_for_future_events = True
agenda.save()
app.post(
'/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
status=302,
)
app.post(
'/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
status=302,
@ -2319,6 +2422,50 @@ def test_event_check_booking(check_types, app, admin_user):
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) not in resp
assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) not in resp
assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) not in resp
app.post(
'/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
@mock.patch('chrono.manager.forms.get_agenda_check_types')
def test_event_check_cancelled_booking(check_types, app, admin_user):
check_types.return_value = []
agenda = Agenda.objects.create(label='Events', kind='events')
event = Event.objects.create(
label='xyz',
start_datetime=now() - datetime.timedelta(days=1),
places=10,
waiting_list_places=5,
agenda=agenda,
)
booking = Booking.objects.create(event=event, user_first_name='User', user_last_name='42')
secondary_booking = Booking.objects.create(
event=event, user_first_name='User', user_last_name='42', primary_booking=booking
)
booking.cancel()
Booking.objects.create(event=event, user_first_name='User', user_last_name='43')
# no suscription: cancelled bookings are not displayed
login(app)
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert len(resp.pyquery.find('td.booking-status')) == 1
assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) not in resp
assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) not in resp
assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) not in resp
token = resp.context['csrf_token']
resp = app.post(
'/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
@ -2330,6 +2477,52 @@ def test_event_check_booking(check_types, app, admin_user):
status=404,
)
Subscription.objects.create(
agenda=agenda,
user_external_id='user:1',
user_first_name='Subscription',
user_last_name='42',
date_start=now(),
date_end=now() + datetime.timedelta(days=1),
)
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert len(resp.pyquery.find('td.booking-status')) == 2
assert resp.pyquery.find('td.booking-status')[0].text.strip() == '(Cancelled)'
assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 0
assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) in resp
assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp
assert '/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk) not in resp
resp = app.post(
'/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
).follow()
assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Present'
assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1
assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text == 'Presence'
booking.refresh_from_db()
assert booking.cancellation_datetime is None
assert booking.user_was_present is True
secondary_booking.refresh_from_db()
assert secondary_booking.cancellation_datetime is None
assert secondary_booking.user_was_present is True
booking.cancel()
resp = app.post(
'/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
).follow()
assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Absent'
assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1
assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text == 'Absence'
booking.refresh_from_db()
assert booking.cancellation_datetime is None
assert booking.user_was_present is False
secondary_booking.refresh_from_db()
assert secondary_booking.cancellation_datetime is None
assert secondary_booking.user_was_present is False
@mock.patch('chrono.manager.forms.get_agenda_check_types')
def test_event_check_booking_ajax(check_types, app, admin_user):
@ -2358,7 +2551,7 @@ def test_event_check_booking_ajax(check_types, app, admin_user):
extra_environ={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'},
)
assert '<tr>' not in resp # because this is a fragment
assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Present\n \n (Bar reason)'
assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Present\n \n (Bar reason)'
assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1
assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text == 'Presence'
assert '<option value="bar-reason" selected>Bar reason</option>' in resp
@ -2370,7 +2563,284 @@ def test_event_check_booking_ajax(check_types, app, admin_user):
extra_environ={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'},
)
assert '<tr>' not in resp # because this is a fragment
assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Absent\n \n (Foo reason)'
assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Absent\n \n (Foo reason)'
assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1
assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text.startswith('Absence')
assert '<option value="foo-reason" selected>Foo reason</option>' in resp
# reset
resp = app.post(
'/manage/agendas/%s/bookings/%s/reset' % (agenda.pk, booking.pk),
params={'csrfmiddlewaretoken': token},
extra_environ={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'},
)
assert '<tr>' not in resp # because this is a fragment
assert resp.pyquery.find('td.booking-status')[0].text.strip() == '-'
@mock.patch('chrono.manager.forms.get_agenda_check_types')
def test_event_check_subscription(check_types, app, admin_user):
check_types.return_value = [
CheckType(slug='foo-reason', label='Foo reason', kind='absence'),
CheckType(slug='bar-reason', label='Bar reason', kind='presence'),
]
agenda = Agenda.objects.create(label='Events', kind='events')
event = Event.objects.create(
label='xyz',
start_datetime=now() - datetime.timedelta(days=1),
places=10,
waiting_list_places=5,
agenda=agenda,
)
subscription = Subscription.objects.create(
agenda=agenda,
user_external_id='user:1',
user_first_name='Subscription',
user_last_name='42',
user_email='foo@bar.com',
user_phone_number='06',
extra_data={'foo': 'bar'},
date_start=now() - datetime.timedelta(days=1),
date_end=now(),
)
# existing booking: no check for subscription
booking = Booking.objects.create(
event=event, user_first_name='User', user_last_name='42', user_external_id='user:1'
)
booking2 = Booking.objects.create(
event=event, user_first_name='User', user_last_name='42', user_external_id='user:1'
)
login(app)
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert (
'/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk) not in resp
)
assert (
'/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk) not in resp
)
token = resp.context['csrf_token']
app.post(
'/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
booking2.delete()
booking.cancel()
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert (
'/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk) not in resp
)
assert (
'/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk) not in resp
)
app.post(
'/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
booking.delete()
# set as present
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert '/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk) in resp
assert '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk) in resp
app.post(
'/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token, 'check_type': 'bar-reason'},
)
booking = Booking.objects.latest('pk')
assert booking.user_was_present is True
assert booking.user_check_type_slug == 'bar-reason'
assert booking.user_check_type_label == 'Bar reason'
assert booking.event == event
assert booking.user_external_id == subscription.user_external_id
assert booking.user_first_name == subscription.user_first_name
assert booking.user_last_name == subscription.user_last_name
assert booking.user_email == subscription.user_email
assert booking.user_phone_number == subscription.user_phone_number
assert booking.extra_data == subscription.extra_data
booking.delete()
# set as absent
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk) in resp
assert '/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk) in resp
app.post(
'/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token, 'check_type': 'foo-reason'},
)
booking = Booking.objects.latest('pk')
assert booking.user_was_present is False
assert booking.user_check_type_slug == 'foo-reason'
assert booking.user_check_type_label == 'Foo reason'
assert booking.event == event
assert booking.user_external_id == subscription.user_external_id
assert booking.user_first_name == subscription.user_first_name
assert booking.user_last_name == subscription.user_last_name
assert booking.user_email == subscription.user_email
assert booking.user_phone_number == subscription.user_phone_number
assert booking.extra_data == subscription.extra_data
booking.delete()
# mark the event as checked
assert agenda.disable_check_update is False
event.checked = True
event.save()
app.post(
'/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=302,
)
Booking.objects.all().delete()
app.post(
'/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=302,
)
Booking.objects.all().delete()
agenda.disable_check_update = True
agenda.save()
app.post(
'/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
# check subscription dates
agenda.disable_check_update = False
agenda.save()
subscription.date_start = now() - datetime.timedelta(days=2)
subscription.date_end = now() - datetime.timedelta(days=1)
subscription.save()
app.post(
'/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
subscription.date_start = now()
subscription.date_end = now() + datetime.timedelta(days=1)
subscription.save()
app.post(
'/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
# event not in past
subscription.date_end = now() + datetime.timedelta(days=2)
subscription.save()
assert agenda.enable_check_for_future_events is False
event.start_datetime = now() + datetime.timedelta(days=1)
event.save()
app.post(
'/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
# not in past, but check for future events is enabled
agenda.enable_check_for_future_events = True
agenda.save()
app.post(
'/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=302,
)
Booking.objects.all().delete()
app.post(
'/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=302,
)
@mock.patch('chrono.manager.forms.get_agenda_check_types')
def test_event_check_subscription_ajax(check_types, app, admin_user):
check_types.return_value = [
CheckType(slug='foo-reason', label='Foo reason', kind='absence'),
CheckType(slug='bar-reason', label='Bar reason', kind='presence'),
]
agenda = Agenda.objects.create(label='Events', kind='events')
event = Event.objects.create(
label='xyz',
start_datetime=now() - datetime.timedelta(days=1),
places=10,
waiting_list_places=5,
agenda=agenda,
)
subscription = Subscription.objects.create(
agenda=agenda,
user_external_id='user:1',
user_first_name='Subscription',
user_last_name='42',
date_start=now() - datetime.timedelta(days=1),
date_end=now(),
)
login(app)
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
token = resp.context['csrf_token']
# set as present
resp = app.post(
'/manage/agendas/%s/subscriptions/%s/presence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token, 'check_type': 'bar-reason'},
extra_environ={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'},
)
assert '<tr>' not in resp # because this is a fragment
assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Present\n \n (Bar reason)'
assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1
assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text == 'Presence'
assert '<option value="bar-reason" selected>Bar reason</option>' in resp
Booking.objects.all().delete()
# set as absent
resp = app.post(
'/manage/agendas/%s/subscriptions/%s/absence/%s' % (agenda.pk, subscription.pk, event.pk),
params={'csrfmiddlewaretoken': token, 'check_type': 'foo-reason'},
extra_environ={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'},
)
assert '<tr>' not in resp # because this is a fragment
assert resp.pyquery.find('td.booking-status')[0].text.strip() == 'Absent\n \n (Foo reason)'
assert len(resp.pyquery.find('td.booking-actions button[disabled]')) == 1
assert resp.pyquery.find('td.booking-actions button[disabled]')[0].text.startswith('Absence')
assert '<option value="foo-reason" selected>Foo reason</option>' in resp
@ -2517,6 +2987,37 @@ def test_event_check_all_bookings(check_types, app, admin_user):
status=404,
)
# event not in past
agenda.disable_check_update = False
agenda.save()
assert agenda.enable_check_for_future_events is False
event.start_datetime = now() + datetime.timedelta(days=1)
event.save()
app.post(
'/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
app.post(
'/manage/agendas/%s/events/%s/presence' % (agenda.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=404,
)
# not in past, but check for future events is enabled
agenda.enable_check_for_future_events = True
agenda.save()
app.post(
'/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=302,
)
app.post(
'/manage/agendas/%s/events/%s/presence' % (agenda.pk, event.pk),
params={'csrfmiddlewaretoken': token},
status=302,
)
def test_event_check_primary_booking(app, admin_user):
agenda = Agenda.objects.create(label='Events', kind='events')

View File

@ -36,7 +36,7 @@ from chrono.agendas.models import (
UnavailabilityCalendar,
VirtualMember,
)
from chrono.utils.timezone import localtime, make_aware, now
from chrono.utils.timezone import localtime, make_aware, make_naive, now
pytestmark = pytest.mark.django_db
@ -268,6 +268,158 @@ def test_agenda_maximal_booking_delay(freezer):
assert agenda.max_booking_datetime == make_aware(datetime.datetime(2021, 11, 2, 0, 0, 0))
def delay_parameter_to_label(argvalue):
if isinstance(argvalue, str):
return argvalue
return repr(argvalue)
@pytest.mark.parametrize(
'current_time,min_booking_datetime',
[
('2021-07-09T08:00:00+02:00', datetime.datetime(2021, 7, 13, 8)),
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 22, 7)),
# summer DST change on sunday 28th
('2021-03-25T01:30:00+01:00', datetime.datetime(2021, 3, 29, 1, 30)),
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 29, 2, 30)),
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 29, 3, 30)),
('2021-03-28T01:30:00+01:00', datetime.datetime(2021, 4, 1, 1, 30)),
('2021-03-28T03:30:00+02:00', datetime.datetime(2021, 4, 1, 3, 30)),
# winter DST change on sunday 31th
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 2, 1, 30)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 2, 30)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 2, 30)),
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 4, 1, 30)),
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 4, 2, 30)),
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 4, 2, 30)),
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 4, 3, 30)),
],
ids=delay_parameter_to_label,
)
def test_agenda_minimal_booking_delay_no_minimal_booking_time(freezer, current_time, min_booking_datetime):
freezer.move_to(current_time)
agenda = Agenda.objects.create(label='Agenda', minimal_booking_delay=4, minimal_booking_time=None)
assert make_naive(agenda.min_booking_datetime) == min_booking_datetime
@pytest.mark.parametrize(
'current_time,min_booking_datetime',
[
('2021-07-09T08:00:00+02:00', datetime.datetime(2021, 7, 16, 8)),
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 24, 7)),
# summer DST change on sunday 28th
('2021-03-25T01:30:00+01:00', datetime.datetime(2021, 3, 31, 1, 30)),
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 31, 2, 30)),
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 31, 3, 30)),
('2021-03-28T01:30:00+01:00', datetime.datetime(2021, 4, 1, 1, 30)),
('2021-03-28T03:30:00+02:00', datetime.datetime(2021, 4, 1, 3, 30)),
# winter DST change on sunday 31th
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 5, 1, 30)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 5, 2, 30)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 5, 2, 30)),
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 5, 1, 30)),
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 5, 2, 30)),
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 5, 2, 30)),
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 5, 3, 30)),
],
ids=delay_parameter_to_label,
)
def test_agenda_minimal_booking_delay_in_working_days_no_minimal_booking_time(
settings, freezer, current_time, min_booking_datetime
):
settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France'
freezer.move_to(current_time)
agenda = Agenda.objects.create(
label='Agenda',
minimal_booking_delay=4,
minimal_booking_time=None,
minimal_booking_delay_in_working_days=True,
)
assert make_naive(agenda.min_booking_datetime) == min_booking_datetime
@pytest.mark.parametrize(
'current_time,min_booking_datetime',
[
('2021-07-09T08:00:00+02:00', datetime.datetime(2021, 7, 13, 8)),
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 22, 8)),
# summer DST change on sunday
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 29, 8)),
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 29, 8)),
# winter DST change on sunday
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 2, 8)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 8)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 8)),
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 4, 8)),
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 4, 8)),
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 4, 8)),
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 4, 8)),
],
ids=delay_parameter_to_label,
)
def test_agenda_minimal_booking_delay_minimal_booking_time_at_8(freezer, current_time, min_booking_datetime):
freezer.move_to(current_time)
agenda = Agenda.objects.create(
label='Agenda', minimal_booking_delay=4, minimal_booking_time=datetime.time(8)
)
assert make_naive(agenda.min_booking_datetime) == min_booking_datetime
@pytest.mark.parametrize(
'current_time,max_booking_datetime',
[
('2021-07-09T08:00:00+02:00', datetime.datetime(2021, 7, 13, 8)),
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 22, 7)),
# summer DST change on sunday 28th
('2021-03-25T01:30:00+01:00', datetime.datetime(2021, 3, 29, 1, 30)),
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 29, 2, 30)),
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 29, 3, 30)),
('2021-03-28T01:30:00+01:00', datetime.datetime(2021, 4, 1, 1, 30)),
('2021-03-28T03:30:00+02:00', datetime.datetime(2021, 4, 1, 3, 30)),
# winter DST change on sunday 31th
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 2, 1, 30)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 2, 30)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 2, 30)),
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 4, 1, 30)),
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 4, 2, 30)),
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 4, 2, 30)),
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 4, 3, 30)),
],
ids=delay_parameter_to_label,
)
def test_agenda_maximal_booking_delay_no_minimal_booking_time(freezer, current_time, max_booking_datetime):
freezer.move_to(current_time)
agenda = Agenda.objects.create(label='Agenda', maximal_booking_delay=4, minimal_booking_time=None)
assert make_naive(agenda.max_booking_datetime) == max_booking_datetime
@pytest.mark.parametrize(
'current_time,max_booking_datetime',
[
('2021-07-09T08:00:00+02:00', datetime.datetime(2021, 7, 13, 8)),
('2021-03-18T07:00:00+01:00', datetime.datetime(2021, 3, 22, 8)),
# summer DST change on sunday
('2021-03-25T02:30:00+01:00', datetime.datetime(2021, 3, 29, 8)),
('2021-03-25T03:30:00+01:00', datetime.datetime(2021, 3, 29, 8)),
# winter DST change on sunday
('2021-10-29T01:30:00+02:00', datetime.datetime(2021, 11, 2, 8)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 8)),
('2021-10-29T02:30:00+02:00', datetime.datetime(2021, 11, 2, 8)),
('2021-10-31T01:30:00+02:00', datetime.datetime(2021, 11, 4, 8)),
('2021-10-31T02:30:00+02:00', datetime.datetime(2021, 11, 4, 8)),
('2021-10-31T02:30:00+01:00', datetime.datetime(2021, 11, 4, 8)),
('2021-10-31T03:30:00+01:00', datetime.datetime(2021, 11, 4, 8)),
],
ids=delay_parameter_to_label,
)
def test_agenda_maximal_booking_delay_minimal_booking_time_at_8(freezer, current_time, max_booking_datetime):
freezer.move_to(current_time)
agenda = Agenda.objects.create(
label='Agenda', maximal_booking_delay=4, minimal_booking_time=datetime.time(8)
)
assert make_naive(agenda.max_booking_datetime) == max_booking_datetime
@pytest.mark.parametrize('with_prefetch', [True, False])
def test_agenda_is_available_for_simple_management(settings, with_prefetch):
settings.EXCEPTIONS_SOURCES = {