Compare commits
58 Commits
f94e46daf5
...
21cae6a31f
Author | SHA1 | Date |
---|---|---|
Valentin Deniaud | 21cae6a31f | |
Valentin Deniaud | a25a8e6ef1 | |
Valentin Deniaud | 2a56ba5432 | |
Valentin Deniaud | 2e22706c08 | |
Valentin Deniaud | 81e93dd4c5 | |
Valentin Deniaud | 3cb80d478a | |
Valentin Deniaud | ec497c66d9 | |
Valentin Deniaud | bcae843c0d | |
Valentin Deniaud | 6d31c85dd7 | |
Frédéric Péters | 17cddfbd4a | |
Frédéric Péters | 57a67073e3 | |
Frédéric Péters | ca32dc3a36 | |
Valentin Deniaud | fb7d928206 | |
Valentin Deniaud | 9b315f4be3 | |
Valentin Deniaud | 1fd95681fe | |
Frédéric Péters | fc86701ab2 | |
Valentin Deniaud | f34af55592 | |
Valentin Deniaud | 2cae3b7724 | |
Valentin Deniaud | 23a1b70dd7 | |
Valentin Deniaud | a13003cdec | |
Frédéric Péters | 6aa243817e | |
Lauréline Guérin | 15b2b26c08 | |
Lauréline Guérin | 918903fc8c | |
Valentin Deniaud | 33e53a694a | |
Valentin Deniaud | ec86a9bbcc | |
Lauréline Guérin | 28c3641d50 | |
Frédéric Péters | 655ffeb610 | |
Thomas Jund | bdce64d56e | |
Thomas Jund | e6be5342e6 | |
Lauréline Guérin | 60de169359 | |
Lauréline Guérin | e231d27751 | |
Valentin Deniaud | 42cc548a33 | |
Thomas Jund | 8b924ef670 | |
Valentin Deniaud | 7c34e4fe7f | |
Valentin Deniaud | 11ef5b4bd2 | |
Valentin Deniaud | 9b340a01d6 | |
Frédéric Péters | 9a841fc31e | |
Serghei Mihai | 5fe881fdb5 | |
Lauréline Guérin | 93081c6e46 | |
Lauréline Guérin | c8d71aa997 | |
Lauréline Guérin | 2b288340b6 | |
Lauréline Guérin | 16e3602391 | |
Lauréline Guérin | df0223abf2 | |
Lauréline Guérin | 0fe3933ed1 | |
Lauréline Guérin | 0b6ca9d5d2 | |
Lauréline Guérin | d16b35067e | |
Lauréline Guérin | fbe2deea93 | |
Valentin Deniaud | 7e946138ac | |
Benjamin Dauvergne | a68026e839 | |
Benjamin Dauvergne | 5fbbe0e984 | |
Frédéric Péters | 0f81147829 | |
Valentin Deniaud | 7859f0558e | |
Valentin Deniaud | e2d70795b1 | |
Valentin Deniaud | 84463c84bf | |
Valentin Deniaud | 8127fbff66 | |
Valentin Deniaud | b9c02c20bd | |
Valentin Deniaud | 60f31525ee | |
Valentin Deniaud | f371341d7d |
|
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
|||
blank=True,
|
||||
default=datetime.time(0, 0),
|
||||
null=True,
|
||||
help_text='Ex.: 08:00:00. If left empty, available events will be those that are later than the current time.',
|
||||
help_text='If left empty, available events will be those that are later than the current time.',
|
||||
verbose_name='Booking opening time',
|
||||
),
|
||||
),
|
||||
|
|
|
@ -12,11 +12,11 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='user_check_end_time',
|
||||
field=models.TimeField(null=True, blank=True, verbose_name='Departure'),
|
||||
field=models.TimeField(null=True, verbose_name='Departure'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='user_check_start_time',
|
||||
field=models.TimeField(null=True, blank=True, verbose_name='Arrival'),
|
||||
field=models.TimeField(null=True, verbose_name='Arrival'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0158_partial_booking_check_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='agenda',
|
||||
name='invoicing_tolerance',
|
||||
field=models.PositiveSmallIntegerField(
|
||||
default=0, validators=[django.core.validators.MaxValueValidator(59)], verbose_name='Tolerance'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='agenda',
|
||||
name='invoicing_unit',
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
('hour', 'Per hour'),
|
||||
('half_hour', 'Per half hour'),
|
||||
('quarter', 'Per quarter-hour'),
|
||||
('minute', 'Per minute'),
|
||||
],
|
||||
default='hour',
|
||||
max_length=10,
|
||||
verbose_name='Invoicing',
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0159_partial_bookings_invoicing'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='computed_end_time',
|
||||
field=models.TimeField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='computed_start_time',
|
||||
field=models.TimeField(null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,37 @@
|
|||
# Generated by Django 3.2.21 on 2023-10-04 13:51
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0160_computed_times'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BookingCheck',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('presence', models.BooleanField()),
|
||||
('start_time', models.TimeField(null=True, blank=True, verbose_name='Arrival')),
|
||||
('end_time', models.TimeField(null=True, blank=True, verbose_name='Departure')),
|
||||
('computed_end_time', models.TimeField(null=True)),
|
||||
('computed_start_time', models.TimeField(null=True)),
|
||||
('type_slug', models.CharField(blank=True, max_length=160, null=True)),
|
||||
('type_label', models.CharField(blank=True, max_length=150, null=True)),
|
||||
(
|
||||
'booking',
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='user_check',
|
||||
to='agendas.booking',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,62 @@
|
|||
# Generated by Django 3.2.18 on 2023-08-22 15:47
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_booking_check_data(apps, schema_editor):
|
||||
Booking = apps.get_model('agendas', 'Booking')
|
||||
BookingCheck = apps.get_model('agendas', 'BookingCheck')
|
||||
|
||||
booking_checks = []
|
||||
bookings = list(Booking.objects.filter(user_was_present__isnull=False))
|
||||
for booking in bookings:
|
||||
booking_check = BookingCheck(
|
||||
booking=booking,
|
||||
presence=booking.user_was_present,
|
||||
start_time=booking.user_check_start_time,
|
||||
end_time=booking.user_check_end_time,
|
||||
computed_start_time=booking.computed_start_time,
|
||||
computed_end_time=booking.computed_end_time,
|
||||
type_slug=booking.user_check_type_slug,
|
||||
type_label=booking.user_check_type_label,
|
||||
)
|
||||
booking_checks.append(booking_check)
|
||||
|
||||
BookingCheck.objects.bulk_create(booking_checks)
|
||||
|
||||
|
||||
def reverse_migrate_booking_check_data(apps, schema_editor):
|
||||
Booking = apps.get_model('agendas', 'Booking')
|
||||
|
||||
bookings = list(Booking.objects.filter(user_check__isnull=False).select_related('user_check'))
|
||||
for booking in bookings:
|
||||
booking.user_was_present = booking.user_check.presence
|
||||
booking.user_check_start_time = booking.user_check.start_time
|
||||
booking.user_check_end_time = booking.user_check.end_time
|
||||
booking.computed_start_time = booking.computed_start_time
|
||||
booking.computed_end_time = booking.computed_end_time
|
||||
booking.user_check_type_slug = booking.user_check.type_slug
|
||||
booking.user_check_type_label = booking.user_check.type_label
|
||||
|
||||
Booking.objects.bulk_update(
|
||||
bookings,
|
||||
fields=[
|
||||
'user_was_present',
|
||||
'user_check_start_time',
|
||||
'user_check_end_time',
|
||||
'computed_start_time',
|
||||
'computed_end_time',
|
||||
'user_check_type_slug',
|
||||
'user_check_type_label',
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0161_add_booking_check_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_booking_check_data, reverse_migrate_booking_check_data),
|
||||
]
|
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 3.2.18 on 2023-08-22 15:51
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0162_migrate_booking_check_data'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='booking',
|
||||
name='user_check_end_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='booking',
|
||||
name='user_check_start_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='booking',
|
||||
name='user_check_type_label',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='booking',
|
||||
name='user_check_type_slug',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='booking',
|
||||
name='user_was_present',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='booking',
|
||||
name='computed_end_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='booking',
|
||||
name='computed_start_time',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 3.2.21 on 2023-10-05 11:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0163_remove_booking_check_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='bookingcheck',
|
||||
name='booking',
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name='user_checks', to='agendas.booking'
|
||||
),
|
||||
),
|
||||
]
|
|
@ -14,6 +14,7 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import collections
|
||||
import copy
|
||||
import dataclasses
|
||||
|
@ -35,6 +36,7 @@ from django.contrib.auth.models import Group
|
|||
from django.contrib.humanize.templatetags.humanize import ordinal
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import connection, models, transaction
|
||||
from django.db.models import (
|
||||
|
@ -284,13 +286,27 @@ class Agenda(models.Model):
|
|||
minimal_booking_time = models.TimeField(
|
||||
verbose_name=_('Booking opening 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.'
|
||||
),
|
||||
help_text=_('If left empty, available events will be those that are later than the current time.'),
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
partial_bookings = models.BooleanField(default=False)
|
||||
invoicing_unit = models.CharField(
|
||||
verbose_name=_('Invoicing'),
|
||||
max_length=10,
|
||||
choices=[
|
||||
('hour', _('Per hour')),
|
||||
('half_hour', _('Per half hour')),
|
||||
('quarter', _('Per quarter-hour')),
|
||||
('minute', _('Per minute')),
|
||||
],
|
||||
default='hour',
|
||||
)
|
||||
invoicing_tolerance = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('Tolerance'),
|
||||
default=0,
|
||||
validators=[MaxValueValidator(59)],
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['label']
|
||||
|
@ -1121,7 +1137,10 @@ class Agenda(models.Model):
|
|||
return True
|
||||
|
||||
def event_overlaps(self, start_datetime, recurrence_days, recurrence_end_date, instance=None):
|
||||
qs = self.event_set
|
||||
qs = self.event_set.filter(
|
||||
# exclude recurrences, check only recurring and normal events
|
||||
primary_event__isnull=True
|
||||
)
|
||||
if hasattr(instance, 'pk'):
|
||||
qs = qs.exclude(pk=instance.pk)
|
||||
|
||||
|
@ -1567,6 +1586,44 @@ class Agenda(models.Model):
|
|||
free_time += desk_free_time
|
||||
return free_time
|
||||
|
||||
def async_refresh_booking_computed_times(self):
|
||||
if self.kind != 'events' or not self.partial_bookings:
|
||||
return
|
||||
|
||||
if 'uwsgi' in sys.modules:
|
||||
from chrono.utils.spooler import refresh_booking_computed_times_from_agenda
|
||||
|
||||
tenant = getattr(connection, 'tenant', None)
|
||||
transaction.on_commit(
|
||||
lambda: refresh_booking_computed_times_from_agenda.spool(
|
||||
agenda_id=str(self.pk), domain=getattr(tenant, 'domain_url', None)
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
self.refresh_booking_computed_times()
|
||||
|
||||
def refresh_booking_computed_times(self):
|
||||
bookings_queryset = Booking.objects.filter(
|
||||
event__agenda__kind='events',
|
||||
event__agenda__partial_bookings=True,
|
||||
event__agenda=self,
|
||||
event__check_locked=False,
|
||||
event__invoiced=False,
|
||||
event__cancelled=False,
|
||||
cancellation_datetime__isnull=True,
|
||||
)
|
||||
booking_checks = BookingCheck.objects.filter(booking__in=bookings_queryset).select_related(
|
||||
'booking', 'booking__event__agenda'
|
||||
)
|
||||
to_update = []
|
||||
for booking_check in booking_checks:
|
||||
changed = booking_check.refresh_computed_times()
|
||||
if changed:
|
||||
to_update.append(booking_check)
|
||||
if to_update:
|
||||
BookingCheck.objects.bulk_update(to_update, ['computed_start_time', 'computed_end_time'])
|
||||
|
||||
|
||||
class VirtualMember(models.Model):
|
||||
"""Trough model to link virtual agendas to their real agendas.
|
||||
|
@ -2084,7 +2141,8 @@ class Event(models.Model):
|
|||
booking_qs = self.booking_set.filter(
|
||||
cancellation_datetime__isnull=True,
|
||||
in_waiting_list=False,
|
||||
user_was_present__isnull=True,
|
||||
user_checks__isnull=True,
|
||||
primary_booking__isnull=True,
|
||||
)
|
||||
if booking_qs.exists():
|
||||
return
|
||||
|
@ -2107,17 +2165,17 @@ class Event(models.Model):
|
|||
self.notify_checked()
|
||||
|
||||
def notify_checked(self):
|
||||
for booking in self.booking_set.filter(user_was_present__isnull=False):
|
||||
if booking.user_was_present is True and booking.presence_callback_url:
|
||||
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_was_present is False and booking.absence_callback_url:
|
||||
elif booking.user_check.presence is False and booking.absence_callback_url:
|
||||
url = booking.absence_callback_url
|
||||
else:
|
||||
continue
|
||||
payload = {
|
||||
'user_was_present': booking.user_was_present,
|
||||
'user_check_type_slug': booking.user_check_type_slug,
|
||||
'user_check_type_label': booking.user_check_type_label,
|
||||
'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,
|
||||
}
|
||||
try:
|
||||
response = requests_wrapper.post(url, json=payload, remote_service='auto', timeout=15)
|
||||
|
@ -2130,6 +2188,45 @@ class Event(models.Model):
|
|||
except Exception as e: # noqa pylint: disable=broad-except
|
||||
logging.error('error (%s) notifying checked booking (%s)', e, booking.id)
|
||||
|
||||
def async_refresh_booking_computed_times(self):
|
||||
if self.agenda.kind != 'events' or not self.agenda.partial_bookings:
|
||||
return
|
||||
if self.check_locked or self.invoiced or self.cancelled:
|
||||
return
|
||||
|
||||
if 'uwsgi' in sys.modules:
|
||||
from chrono.utils.spooler import refresh_booking_computed_times_from_event
|
||||
|
||||
tenant = getattr(connection, 'tenant', None)
|
||||
transaction.on_commit(
|
||||
lambda: refresh_booking_computed_times_from_event.spool(
|
||||
event_id=str(self.pk), domain=getattr(tenant, 'domain_url', None)
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
self.refresh_booking_computed_times()
|
||||
|
||||
def refresh_booking_computed_times(self):
|
||||
bookings_queryset = Booking.objects.filter(
|
||||
event__agenda__kind='events',
|
||||
event__agenda__partial_bookings=True,
|
||||
event=self,
|
||||
event__check_locked=False,
|
||||
event__invoiced=False,
|
||||
event__cancelled=False,
|
||||
cancellation_datetime__isnull=True,
|
||||
)
|
||||
booking_checks = BookingCheck.objects.filter(booking__in=bookings_queryset).select_related('booking')
|
||||
to_update = []
|
||||
for booking_check in booking_checks:
|
||||
booking_check.booking.event = self # to avoid lots of querysets
|
||||
changed = booking_check.refresh_computed_times()
|
||||
if changed:
|
||||
to_update.append(booking_check)
|
||||
if to_update:
|
||||
BookingCheck.objects.bulk_update(to_update, ['computed_start_time', 'computed_end_time'])
|
||||
|
||||
def in_bookable_period(self, bypass_delays=False):
|
||||
if self.publication_datetime and now() < self.publication_datetime:
|
||||
return False
|
||||
|
@ -2181,7 +2278,7 @@ class Event(models.Model):
|
|||
'booking',
|
||||
filter=Q(
|
||||
booking__cancellation_datetime__isnull=True,
|
||||
booking__user_was_present=False,
|
||||
booking__user_checks__presence=False,
|
||||
booking__user_external_id=user_external_id,
|
||||
),
|
||||
),
|
||||
|
@ -2300,10 +2397,14 @@ class Event(models.Model):
|
|||
.order_by()
|
||||
.values('event')
|
||||
)
|
||||
present_count = bookings.filter(user_was_present=True).annotate(count=Count('event')).values('count')
|
||||
absent_count = bookings.filter(user_was_present=False).annotate(count=Count('event')).values('count')
|
||||
present_count = (
|
||||
bookings.filter(user_checks__presence=True).annotate(count=Count('event')).values('count')
|
||||
)
|
||||
absent_count = (
|
||||
bookings.filter(user_checks__presence=False).annotate(count=Count('event')).values('count')
|
||||
)
|
||||
notchecked_count = (
|
||||
bookings.filter(user_was_present__isnull=True).annotate(count=Count('event')).values('count')
|
||||
bookings.filter(user_checks__isnull=True).annotate(count=Count('event')).values('count')
|
||||
)
|
||||
return qs.annotate(
|
||||
present_count=Coalesce(Subquery(present_count, output_field=IntegerField()), Value(0)),
|
||||
|
@ -2699,11 +2800,6 @@ class Booking(models.Model):
|
|||
user_first_name = models.CharField(max_length=250, blank=True)
|
||||
user_email = models.EmailField(blank=True)
|
||||
user_phone_number = models.CharField(max_length=30, blank=True)
|
||||
user_was_present = models.BooleanField(null=True)
|
||||
user_check_type_slug = models.CharField(max_length=160, blank=True, null=True)
|
||||
user_check_type_label = models.CharField(max_length=150, blank=True, null=True)
|
||||
user_check_start_time = models.TimeField(_('Arrival'), null=True, blank=True)
|
||||
user_check_end_time = models.TimeField(_('Departure'), null=True, blank=True)
|
||||
out_of_min_delay = models.BooleanField(default=False)
|
||||
|
||||
extra_emails = ArrayField(models.EmailField(), default=list)
|
||||
|
@ -2723,6 +2819,15 @@ class Booking(models.Model):
|
|||
def user_name(self):
|
||||
return ('%s %s' % (self.user_first_name, self.user_last_name)).strip()
|
||||
|
||||
@cached_property
|
||||
def user_check(self): # pylint: disable=method-hidden
|
||||
user_checks = list(self.user_checks.all())
|
||||
|
||||
if len(user_checks) > 1:
|
||||
raise AttributeError('booking has multiple checks')
|
||||
|
||||
return user_checks[0] if user_checks else None
|
||||
|
||||
@cached_property
|
||||
def emails(self):
|
||||
emails = set(self.extra_emails)
|
||||
|
@ -2737,6 +2842,11 @@ class Booking(models.Model):
|
|||
phone_numbers.add(self.user_phone_number)
|
||||
return list(phone_numbers)
|
||||
|
||||
def refresh_from_db(self, *args, **kwargs):
|
||||
if hasattr(self, 'user_check'):
|
||||
del self.user_check
|
||||
return super().refresh_from_db(*args, **kwargs)
|
||||
|
||||
def cancel(self, trigger_callback=False):
|
||||
timestamp = now()
|
||||
with transaction.atomic():
|
||||
|
@ -2760,39 +2870,41 @@ class Booking(models.Model):
|
|||
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()
|
||||
if self.user_check:
|
||||
self.user_check.delete()
|
||||
self.user_check = None
|
||||
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
|
||||
def mark_user_absence(self, check_type_slug=None, check_type_label=None, start_time=None, end_time=None):
|
||||
if not self.user_check:
|
||||
self.user_check = BookingCheck(booking=self)
|
||||
self.user_check.presence = False
|
||||
self.user_check.type_slug = check_type_slug
|
||||
self.user_check.type_label = check_type_label
|
||||
self.user_check.start_time = start_time
|
||||
self.user_check.end_time = end_time
|
||||
|
||||
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.user_check.save()
|
||||
self.secondary_booking_set.update(cancellation_datetime=None)
|
||||
self.save()
|
||||
self.event.set_is_checked()
|
||||
|
||||
def mark_user_presence(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 = True
|
||||
def mark_user_presence(self, check_type_slug=None, check_type_label=None, start_time=None, end_time=None):
|
||||
if not self.user_check:
|
||||
self.user_check = BookingCheck(booking=self)
|
||||
self.user_check.presence = True
|
||||
self.user_check.type_slug = check_type_slug
|
||||
self.user_check.type_label = check_type_label
|
||||
self.user_check.start_time = start_time
|
||||
self.user_check.end_time = end_time
|
||||
|
||||
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.user_check.save()
|
||||
self.secondary_booking_set.update(cancellation_datetime=None)
|
||||
self.save()
|
||||
self.event.set_is_checked()
|
||||
|
@ -2833,9 +2945,7 @@ class Booking(models.Model):
|
|||
anonymization_datetime=now(),
|
||||
)
|
||||
|
||||
def get_ics(self, request=None):
|
||||
ics = vobject.iCalendar()
|
||||
ics.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
|
||||
def get_vevent_ics(self, request=None):
|
||||
vevent = vobject.newFromBehavior('vevent')
|
||||
vevent.add('uid').value = '%s-%s-%s' % (
|
||||
self.event.start_datetime.isoformat(),
|
||||
|
@ -2863,6 +2973,12 @@ class Booking(models.Model):
|
|||
field_value = request and request.GET.get(field) or (self.extra_data or {}).get(field)
|
||||
if field_value:
|
||||
vevent.add(field).value = field_value
|
||||
return vevent
|
||||
|
||||
def get_ics(self, request=None):
|
||||
ics = vobject.iCalendar()
|
||||
ics.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
|
||||
vevent = self.get_vevent_ics(request)
|
||||
ics.add(vevent)
|
||||
return ics.serialize()
|
||||
|
||||
|
@ -2885,6 +3001,100 @@ class Booking(models.Model):
|
|||
return translate_from_publik_url(self.backoffice_url)
|
||||
|
||||
|
||||
class BookingCheck(models.Model):
|
||||
booking = models.ForeignKey(Booking, on_delete=models.CASCADE, related_name='user_checks')
|
||||
|
||||
presence = models.BooleanField()
|
||||
|
||||
start_time = models.TimeField(_('Arrival'), null=True, blank=True)
|
||||
end_time = models.TimeField(_('Departure'), null=True, blank=True)
|
||||
computed_start_time = models.TimeField(null=True)
|
||||
computed_end_time = models.TimeField(null=True)
|
||||
|
||||
type_slug = models.CharField(max_length=160, blank=True, null=True)
|
||||
type_label = models.CharField(max_length=150, blank=True, null=True)
|
||||
|
||||
def _get_previous_and_next_slots(self, _time):
|
||||
minutes = {
|
||||
'hour': 60,
|
||||
'half_hour': 30,
|
||||
'quarter': 15,
|
||||
}[self.booking.event.agenda.invoicing_unit]
|
||||
|
||||
time_minutes = _time.hour * 60 + _time.minute
|
||||
previous_slot_minutes = math.trunc(time_minutes / minutes) * minutes
|
||||
previous_slot = datetime.time(*divmod(previous_slot_minutes, 60))
|
||||
next_slot = datetime.time(*divmod(previous_slot_minutes + minutes, 60))
|
||||
return previous_slot, next_slot
|
||||
|
||||
def get_computed_start_time(self):
|
||||
if self.start_time is None:
|
||||
return None
|
||||
|
||||
start_time = self.start_time
|
||||
if self.booking.start_time:
|
||||
start_time = min(self.start_time, self.booking.start_time)
|
||||
if self.booking.event.agenda.invoicing_unit == 'minute':
|
||||
return start_time
|
||||
|
||||
tolerance = self.booking.event.agenda.invoicing_tolerance
|
||||
|
||||
# compute previous and next slot
|
||||
previous_slot, next_slot = self._get_previous_and_next_slots(start_time)
|
||||
|
||||
# in tolerance ? take next_slot
|
||||
if (next_slot.minute or 60 - start_time.minute) <= tolerance:
|
||||
return next_slot
|
||||
|
||||
# else take previous_slot
|
||||
return previous_slot
|
||||
|
||||
def get_computed_end_time(self):
|
||||
if self.end_time is None:
|
||||
return None
|
||||
|
||||
end_time = self.end_time
|
||||
if self.booking.end_time:
|
||||
end_time = max(self.end_time, self.booking.end_time)
|
||||
if self.booking.event.agenda.invoicing_unit == 'minute':
|
||||
return end_time
|
||||
|
||||
tolerance = self.booking.event.agenda.invoicing_tolerance
|
||||
|
||||
# compute previous and next slot
|
||||
previous_slot, next_slot = self._get_previous_and_next_slots(end_time)
|
||||
|
||||
# in tolerance ? take previous_slot
|
||||
if (end_time.minute - previous_slot.minute) <= tolerance:
|
||||
return previous_slot
|
||||
|
||||
# else take next_slot
|
||||
return next_slot
|
||||
|
||||
def refresh_computed_times(self):
|
||||
old_computed_start_time = self.computed_start_time
|
||||
old_computed_end_time = self.computed_end_time
|
||||
self.computed_start_time = self.get_computed_start_time()
|
||||
self.computed_end_time = self.get_computed_end_time()
|
||||
# return True if changed, else False
|
||||
if (
|
||||
old_computed_start_time == self.computed_start_time
|
||||
and old_computed_end_time == self.computed_end_time
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def overlaps_existing_check(self, start_time, end_time):
|
||||
booking_checks = BookingCheck.objects.filter(booking=self.booking).exclude(pk=self.pk)
|
||||
if not booking_checks:
|
||||
return False
|
||||
|
||||
if len(booking_checks) > 1:
|
||||
raise ValueError('too many booking checks') # should not happen
|
||||
|
||||
return bool(start_time < booking_checks[0].end_time and end_time > booking_checks[0].start_time)
|
||||
|
||||
|
||||
OpeningHour = collections.namedtuple('OpeningHour', ['begin', 'end'])
|
||||
|
||||
|
||||
|
@ -2936,14 +3146,15 @@ class Desk(models.Model):
|
|||
desk.unavailability_calendars.add(target_calendar)
|
||||
|
||||
def export_json(self):
|
||||
time_period_exceptions = self.timeperiodexception_set.filter(source__settings_slug__isnull=True)
|
||||
time_period_exception_sources = self.timeperiodexceptionsource_set.filter(settings_slug__isnull=False)
|
||||
time_period_exceptions = self.timeperiodexception_set.filter(source__isnull=True)
|
||||
return {
|
||||
'label': self.label,
|
||||
'slug': self.slug,
|
||||
'timeperiods': [time_period.export_json() for time_period in self.timeperiod_set.filter()],
|
||||
'exceptions': [exception.export_json() for exception in time_period_exceptions],
|
||||
'exception_sources': [source.export_json() for source in time_period_exception_sources],
|
||||
'exception_sources': [
|
||||
source.export_json() for source in self.timeperiodexceptionsource_set.all()
|
||||
],
|
||||
'unavailability_calendars': [{'slug': x.slug} for x in self.unavailability_calendars.all()],
|
||||
}
|
||||
|
||||
|
@ -3367,15 +3578,31 @@ class TimePeriodExceptionSource(models.Model):
|
|||
@classmethod
|
||||
def import_json(cls, data):
|
||||
data = clean_import_data(cls, data)
|
||||
|
||||
if data.get('ics_file'):
|
||||
data['ics_file'] = ContentFile(base64.b64decode(data['ics_file']), name=data['ics_filename'])
|
||||
|
||||
desk = data.pop('desk')
|
||||
settings_slug = data.pop('settings_slug')
|
||||
source, _ = cls.objects.update_or_create(desk=desk, settings_slug=settings_slug, defaults=data)
|
||||
if source.enabled:
|
||||
source.enable()
|
||||
ics_url = data.pop('ics_url', None)
|
||||
ics_filename = data.pop('ics_filename', None)
|
||||
source, _ = cls.objects.update_or_create(
|
||||
desk=desk, settings_slug=settings_slug, ics_filename=ics_filename, ics_url=ics_url, defaults=data
|
||||
)
|
||||
if settings_slug:
|
||||
if source.enabled:
|
||||
source.enable()
|
||||
else:
|
||||
try:
|
||||
source.refresh_timeperiod_exceptions_from_ics()
|
||||
except ICSError:
|
||||
pass
|
||||
|
||||
def export_json(self):
|
||||
'''Export only sources from settings.'''
|
||||
return {
|
||||
'ics_filename': self.ics_filename,
|
||||
'ics_file': base64.b64encode(self.ics_file.read()).decode() if self.ics_file else None,
|
||||
'ics_url': self.ics_url,
|
||||
'settings_slug': self.settings_slug,
|
||||
'settings_label': self.settings_label,
|
||||
'enabled': self.enabled,
|
||||
|
|
|
@ -95,6 +95,18 @@ class FillSlotSerializer(serializers.Serializer):
|
|||
required=False, child=serializers.CharField(max_length=16, allow_blank=False)
|
||||
)
|
||||
check_overlaps = serializers.BooleanField(default=False)
|
||||
start_time = serializers.TimeField(required=False, allow_null=True)
|
||||
end_time = serializers.TimeField(required=False, allow_null=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
super().validate(attrs)
|
||||
use_partial_bookings = any(agenda.partial_bookings for agenda in self.context.get('agendas', []))
|
||||
if use_partial_bookings:
|
||||
if not attrs.get('start_time') or not attrs.get('end_time'):
|
||||
raise ValidationError(_('must include start_time and end_time for partial bookings agenda'))
|
||||
if attrs['start_time'] > attrs['end_time']:
|
||||
raise ValidationError(_('start_time must be before end_time'))
|
||||
return attrs
|
||||
|
||||
|
||||
class SlotsSerializer(serializers.Serializer):
|
||||
|
@ -188,18 +200,6 @@ class RecurringFillslotsSerializer(MultipleAgendasEventsFillSlotsSerializer):
|
|||
check_overlaps = CommaSeparatedStringField(
|
||||
required=False, child=serializers.SlugField(max_length=160, allow_blank=False)
|
||||
)
|
||||
start_time = serializers.TimeField(required=False)
|
||||
end_time = serializers.TimeField(required=False)
|
||||
|
||||
def validate(self, attrs):
|
||||
super().validate(attrs)
|
||||
use_partial_bookings = any(agenda.partial_bookings for agenda in self.context['agendas'])
|
||||
if use_partial_bookings:
|
||||
if not attrs.get('start_time') or not attrs.get('end_time'):
|
||||
raise ValidationError(_('must include start_time and end_time for partial bookings agenda'))
|
||||
if attrs['start_time'] > attrs['end_time']:
|
||||
raise ValidationError(_('start_time must be before end_time'))
|
||||
return attrs
|
||||
|
||||
def validate_slots(self, value):
|
||||
super().validate_slots(value)
|
||||
|
@ -229,7 +229,60 @@ class RecurringFillslotsSerializer(MultipleAgendasEventsFillSlotsSerializer):
|
|||
return slots
|
||||
|
||||
|
||||
class RecurringFillslotsByDaySerializer(FillSlotSerializer):
|
||||
weekdays = {
|
||||
'monday': 1,
|
||||
'tuesday': 2,
|
||||
'wednesday': 3,
|
||||
'thursday': 4,
|
||||
'friday': 5,
|
||||
'saturday': 6,
|
||||
'sunday': 7,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for weekday in self.weekdays:
|
||||
self.fields[weekday] = CommaSeparatedStringField(
|
||||
child=serializers.TimeField(), required=False, min_length=2, max_length=2, allow_null=True
|
||||
)
|
||||
setattr(self, 'validate_%s' % weekday, self.validate_hour_range)
|
||||
|
||||
def validate_hour_range(self, value):
|
||||
if not value:
|
||||
return None
|
||||
|
||||
start_time, end_time = value
|
||||
if start_time >= end_time:
|
||||
raise ValidationError(_('Start hour must be before end hour.'))
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
agendas = self.context['agendas']
|
||||
if len(agendas) > 1:
|
||||
raise ValidationError('Multiple agendas are not supported.')
|
||||
agenda = agendas[0]
|
||||
|
||||
if not agenda.partial_bookings:
|
||||
raise ValidationError('Agenda kind must be partial bookings.')
|
||||
|
||||
attrs['hours_by_days'] = hours_by_days = {}
|
||||
for weekday, weekday_index in self.weekdays.items():
|
||||
if attrs.get(weekday):
|
||||
hours_by_days[weekday_index] = attrs[weekday]
|
||||
|
||||
days_by_event = collections.defaultdict(list)
|
||||
for event in agenda.get_open_recurring_events():
|
||||
for day in event.recurrence_days:
|
||||
if day in hours_by_days:
|
||||
days_by_event[event.slug].append(day)
|
||||
attrs['slots'] = {agenda.slug: days_by_event}
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class BookingSerializer(serializers.ModelSerializer):
|
||||
user_was_present = serializers.BooleanField(required=False, allow_null=True)
|
||||
user_absence_reason = serializers.CharField(required=False, allow_blank=True, allow_null=True)
|
||||
user_presence_reason = serializers.CharField(required=False, allow_blank=True, allow_null=True)
|
||||
use_color_for = serializers.CharField(required=False, allow_blank=True, allow_null=True, source='color')
|
||||
|
@ -278,12 +331,39 @@ class BookingSerializer(serializers.ModelSerializer):
|
|||
ret.pop('user_absence_reason', None)
|
||||
ret.pop('user_presence_reason', None)
|
||||
else:
|
||||
user_was_present = self.instance.user_check.presence if self.instance.user_check else None
|
||||
ret['user_was_present'] = user_was_present
|
||||
ret['user_absence_reason'] = (
|
||||
self.instance.user_check_type_slug if self.instance.user_was_present is False else None
|
||||
self.instance.user_check.type_slug if user_was_present is False else None
|
||||
)
|
||||
ret['user_presence_reason'] = (
|
||||
self.instance.user_check_type_slug if self.instance.user_was_present is True else None
|
||||
self.instance.user_check.type_slug if user_was_present is True else None
|
||||
)
|
||||
if self.instance.event.agenda.kind == 'events' and self.instance.event.agenda.partial_bookings:
|
||||
self.instance.user_check_start_time = self.instance.user_check.start_time
|
||||
self.instance.user_check_end_time = self.instance.user_check.end_time
|
||||
self.instance.computed_start_time = self.instance.user_check.computed_start_time
|
||||
self.instance.computed_end_time = self.instance.user_check.computed_end_time
|
||||
for key in ['', 'user_check_', 'computed_']:
|
||||
start_key, end_key, minutes_key = (
|
||||
'%sstart_time' % key,
|
||||
'%send_time' % key,
|
||||
'%sduration' % key,
|
||||
)
|
||||
ret[start_key] = getattr(self.instance, start_key)
|
||||
ret[end_key] = getattr(self.instance, end_key)
|
||||
ret[minutes_key] = None
|
||||
if (
|
||||
getattr(self.instance, start_key) is not None
|
||||
and getattr(self.instance, end_key) is not None
|
||||
):
|
||||
start_minutes = (
|
||||
getattr(self.instance, start_key).hour * 60 + getattr(self.instance, start_key).minute
|
||||
)
|
||||
end_minutes = (
|
||||
getattr(self.instance, end_key).hour * 60 + getattr(self.instance, end_key).minute
|
||||
)
|
||||
ret[minutes_key] = end_minutes - start_minutes
|
||||
return ret
|
||||
|
||||
def _validate_check_type(self, kind, value):
|
||||
|
@ -446,7 +526,7 @@ class AgendaOrSubscribedSlugsSerializer(AgendaOrSubscribedSlugsMixin, DateRangeM
|
|||
|
||||
|
||||
class RecurringFillslotsQueryStringSerializer(AgendaOrSubscribedSlugsSerializer):
|
||||
action = serializers.ChoiceField(required=True, choices=['update', 'book', 'unbook'])
|
||||
action = serializers.ChoiceField(required=True, choices=['update', 'update-from-date', 'book', 'unbook'])
|
||||
|
||||
|
||||
class RecurringEventsListSerializer(AgendaOrSubscribedSlugsSerializer):
|
||||
|
@ -495,11 +575,12 @@ class EventSerializer(serializers.ModelSerializer):
|
|||
field_classes = {
|
||||
'text': serializers.CharField,
|
||||
'textarea': serializers.CharField,
|
||||
'bool': serializers.NullBooleanField,
|
||||
'bool': serializers.BooleanField,
|
||||
}
|
||||
field_options = {
|
||||
'text': {'allow_blank': True},
|
||||
'textarea': {'allow_blank': True},
|
||||
'bool': {'allow_null': True},
|
||||
}
|
||||
for custom_field in self.instance.agenda.events_type.get_custom_fields():
|
||||
field_class = field_classes[custom_field['field_type']]
|
||||
|
|
|
@ -23,6 +23,11 @@ urlpatterns = [
|
|||
path('agendas/datetimes/', views.agendas_datetimes, name='api-agendas-datetimes'),
|
||||
path('agendas/recurring-events/', views.recurring_events_list, name='api-agenda-recurring-events'),
|
||||
path('agendas/recurring-events/fillslots/', views.recurring_fillslots, name='api-recurring-fillslots'),
|
||||
path(
|
||||
'agendas/recurring-events/fillslots-by-day/',
|
||||
views.recurring_fillslots_by_day,
|
||||
name='api-recurring-fillslots-by-day',
|
||||
),
|
||||
path(
|
||||
'agendas/events/',
|
||||
views.agendas_events,
|
||||
|
@ -57,9 +62,6 @@ urlpatterns = [
|
|||
views.fillslot,
|
||||
name='api-fillslot',
|
||||
),
|
||||
re_path(
|
||||
r'^agenda/(?P<agenda_identifier>[\w-]+)/fillslots/$', views.fillslots, name='api-agenda-fillslots'
|
||||
),
|
||||
re_path(
|
||||
r'^agenda/(?P<agenda_identifier>[\w-]+)/events/fillslots/$',
|
||||
views.events_fillslots,
|
||||
|
@ -127,6 +129,7 @@ urlpatterns = [
|
|||
name='api-agenda-subscription',
|
||||
),
|
||||
path('bookings/', views.bookings, name='api-bookings'),
|
||||
path('bookings/ics/', views.bookings_ics, name='api-bookings-ics'),
|
||||
path('booking/<int:booking_pk>/', views.booking, name='api-booking'),
|
||||
path('booking/<int:booking_pk>/cancel/', views.cancel_booking, name='api-cancel-booking'),
|
||||
path('booking/<int:booking_pk>/accept/', views.accept_booking, name='api-accept-booking'),
|
||||
|
|
|
@ -20,9 +20,10 @@ import datetime
|
|||
import json
|
||||
import uuid
|
||||
|
||||
import vobject
|
||||
from django.conf import settings
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db.models import BooleanField, Count, ExpressionWrapper, F, Func, Prefetch, Q
|
||||
from django.db.models import BooleanField, Case, Count, ExpressionWrapper, F, Func, Prefetch, Q, When
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.db.models.functions import TruncDay
|
||||
from django.http import Http404, HttpResponse
|
||||
|
@ -45,6 +46,7 @@ from chrono.agendas.models import (
|
|||
ISO_WEEKDAYS,
|
||||
Agenda,
|
||||
Booking,
|
||||
BookingCheck,
|
||||
BookingColor,
|
||||
Desk,
|
||||
Event,
|
||||
|
@ -96,6 +98,8 @@ def get_agenda_detail(request, agenda, check_events=False):
|
|||
if check_events:
|
||||
agenda_detail['opened_events_available'] = bool(agenda.get_open_events().filter(full=False))
|
||||
agenda_detail['booking_form_url'] = agenda.get_booking_form_url()
|
||||
if settings.PARTIAL_BOOKINGS_ENABLED:
|
||||
agenda_detail['partial_bookings'] = agenda.partial_bookings
|
||||
elif agenda.accept_meetings():
|
||||
agenda_detail['api'] = {
|
||||
'meetings_url': request.build_absolute_uri(
|
||||
|
@ -113,9 +117,6 @@ def get_agenda_detail(request, agenda, check_events=False):
|
|||
),
|
||||
}
|
||||
)
|
||||
agenda_detail['api']['fillslots_url'] = request.build_absolute_uri(
|
||||
reverse('api-agenda-fillslots', kwargs={'agenda_identifier': agenda.slug})
|
||||
)
|
||||
agenda_detail['api']['backoffice_url'] = request.build_absolute_uri(
|
||||
reverse('chrono-manager-agenda-view', kwargs={'pk': agenda.pk})
|
||||
)
|
||||
|
@ -176,6 +177,10 @@ def is_event_disabled(
|
|||
|
||||
def get_event_text(event, agenda, day=None):
|
||||
event_text = force_str(event)
|
||||
|
||||
if day is not None:
|
||||
event.weekday = ISO_WEEKDAYS[day].capitalize()
|
||||
|
||||
if agenda.event_display_template:
|
||||
try:
|
||||
event_text = Template(agenda.event_display_template).render(
|
||||
|
@ -190,7 +195,7 @@ def get_event_text(event, agenda, day=None):
|
|||
)
|
||||
elif day is not None:
|
||||
event_text = _('%(weekday)s: %(event)s') % {
|
||||
'weekday': ISO_WEEKDAYS[day].capitalize(),
|
||||
'weekday': event.weekday,
|
||||
'event': event_text,
|
||||
}
|
||||
return event_text
|
||||
|
@ -1175,20 +1180,14 @@ class AgendaResourceList(APIView):
|
|||
agenda_resource_list = AgendaResourceList.as_view()
|
||||
|
||||
|
||||
class EventsAgendaFillslots(APIView):
|
||||
class EventsAgendaFillslot(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.FillSlotsSerializer
|
||||
serializer_class = serializers.FillSlotSerializer
|
||||
|
||||
def post(self, request, agenda, slots):
|
||||
if not settings.LEGACY_FILLSLOTS_ENABLED and slots is None:
|
||||
raise APIErrorBadRequest(N_('deprecated call'))
|
||||
|
||||
return self.fillslot(request=request, agenda=agenda, slots=slots)
|
||||
|
||||
def fillslot(self, request, agenda, slots=None, retry=False):
|
||||
slots = slots or []
|
||||
multiple_booking = bool(not slots)
|
||||
def post(self, request, agenda, slot):
|
||||
return self.fillslot(request=request, agenda=agenda, slot=slot)
|
||||
|
||||
def fillslot(self, request, agenda, slot, retry=False):
|
||||
known_body_params = set(request.query_params).intersection(
|
||||
{'label', 'user_name', 'backoffice_url', 'user_display_label'}
|
||||
)
|
||||
|
@ -1198,14 +1197,12 @@ class EventsAgendaFillslots(APIView):
|
|||
N_('parameters "%s" must be included in request body, not query'), params
|
||||
)
|
||||
|
||||
serializer = self.serializer_class(data=request.data, partial=True)
|
||||
context = {'agendas': [agenda]}
|
||||
serializer = self.serializer_class(data=request.data, partial=True, context=context)
|
||||
if not serializer.is_valid():
|
||||
raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
|
||||
payload = serializer.validated_data
|
||||
|
||||
if 'slots' in payload:
|
||||
slots = payload['slots']
|
||||
|
||||
if 'count' in payload:
|
||||
places_count = payload['count']
|
||||
elif 'count' in request.query_params:
|
||||
|
@ -1249,29 +1246,28 @@ class EventsAgendaFillslots(APIView):
|
|||
|
||||
extra_data = get_extra_data(request, serializer.validated_data)
|
||||
|
||||
events = get_events_from_slots(slots, request, agenda, payload)
|
||||
event = get_events_from_slots([slot], request, agenda, payload)[0]
|
||||
|
||||
# search free places. Switch to waiting list if necessary.
|
||||
in_waiting_list = False
|
||||
for event in events:
|
||||
if event.start_datetime > now():
|
||||
if payload.get('force_waiting_list') and not event.waiting_list_places:
|
||||
raise APIError(N_('no waiting list'))
|
||||
if event.start_datetime > now():
|
||||
if payload.get('force_waiting_list') and not event.waiting_list_places:
|
||||
raise APIError(N_('no waiting list'))
|
||||
|
||||
if event.waiting_list_places:
|
||||
if (
|
||||
payload.get('force_waiting_list')
|
||||
or (event.booked_places + places_count) > event.places
|
||||
or event.booked_waiting_list_places
|
||||
):
|
||||
# if this is full or there are people waiting, put new bookings
|
||||
# in the waiting list.
|
||||
in_waiting_list = True
|
||||
if (event.booked_waiting_list_places + places_count) > event.waiting_list_places:
|
||||
raise APIError(N_('sold out'))
|
||||
else:
|
||||
if (event.booked_places + places_count) > event.places:
|
||||
if event.waiting_list_places:
|
||||
if (
|
||||
payload.get('force_waiting_list')
|
||||
or (event.booked_places + places_count) > event.places
|
||||
or event.booked_waiting_list_places
|
||||
):
|
||||
# if this is full or there are people waiting, put new bookings
|
||||
# in the waiting list.
|
||||
in_waiting_list = True
|
||||
if (event.booked_waiting_list_places + places_count) > event.waiting_list_places:
|
||||
raise APIError(N_('sold out'))
|
||||
else:
|
||||
if (event.booked_places + places_count) > event.places:
|
||||
raise APIError(N_('sold out'))
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
|
@ -1281,18 +1277,17 @@ class EventsAgendaFillslots(APIView):
|
|||
|
||||
# now we have a list of events, book them.
|
||||
primary_booking = None
|
||||
for event in events:
|
||||
for dummy in range(places_count):
|
||||
new_booking = make_booking(
|
||||
event=event,
|
||||
payload=payload,
|
||||
extra_data=extra_data,
|
||||
primary_booking=primary_booking,
|
||||
in_waiting_list=in_waiting_list,
|
||||
)
|
||||
new_booking.save()
|
||||
if primary_booking is None:
|
||||
primary_booking = new_booking
|
||||
for dummy in range(places_count):
|
||||
new_booking = make_booking(
|
||||
event=event,
|
||||
payload=payload,
|
||||
extra_data=extra_data,
|
||||
primary_booking=primary_booking,
|
||||
in_waiting_list=in_waiting_list,
|
||||
)
|
||||
new_booking.save()
|
||||
if primary_booking is None:
|
||||
primary_booking = new_booking
|
||||
except IntegrityError as e:
|
||||
if 'tstzrange_constraint' in str(e):
|
||||
# "optimistic concurrency control", between our availability
|
||||
|
@ -1306,14 +1301,14 @@ class EventsAgendaFillslots(APIView):
|
|||
# of fillslot().
|
||||
if retry:
|
||||
raise APIError(N_('no more desk available'))
|
||||
return self.fillslot(request=request, agenda=agenda, slots=slots, retry=True)
|
||||
return self.fillslot(request=request, agenda=agenda, slot=slot, retry=True)
|
||||
raise
|
||||
|
||||
response = {
|
||||
'err': 0,
|
||||
'in_waiting_list': in_waiting_list,
|
||||
'booking_id': primary_booking.id,
|
||||
'datetime': format_response_datetime(events[0].start_datetime),
|
||||
'datetime': format_response_datetime(event.start_datetime),
|
||||
'agenda': {
|
||||
'label': primary_booking.event.agenda.label,
|
||||
'slug': primary_booking.event.agenda.slug,
|
||||
|
@ -1341,47 +1336,26 @@ class EventsAgendaFillslots(APIView):
|
|||
}
|
||||
if to_cancel_booking:
|
||||
response['cancelled_booking_id'] = cancelled_booking_id
|
||||
if not multiple_booking:
|
||||
event = events[0]
|
||||
# event.full is not up to date, it might have been changed by previous new_booking.save().
|
||||
event.refresh_from_db()
|
||||
response['places'] = get_event_places(event)
|
||||
if event.end_datetime:
|
||||
response['end_datetime'] = format_response_datetime(event.end_datetime)
|
||||
else:
|
||||
response['end_datetime'] = None
|
||||
|
||||
# event.full is not up to date, it might have been changed by previous new_booking.save().
|
||||
event.refresh_from_db()
|
||||
response['places'] = get_event_places(event)
|
||||
if event.end_datetime:
|
||||
response['end_datetime'] = format_response_datetime(event.end_datetime)
|
||||
else:
|
||||
response['events'] = [
|
||||
{
|
||||
'slug': x.slug,
|
||||
'text': str(x),
|
||||
'datetime': format_response_datetime(x.start_datetime),
|
||||
'end_datetime': format_response_datetime(x.end_datetime) if x.end_datetime else None,
|
||||
'description': x.description,
|
||||
}
|
||||
for x in events
|
||||
]
|
||||
response['end_datetime'] = None
|
||||
|
||||
return Response(response)
|
||||
|
||||
|
||||
class EventsAgendaFillslot(EventsAgendaFillslots):
|
||||
class MeetingsAgendaFillslot(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.FillSlotSerializer
|
||||
|
||||
def post(self, request, agenda, slot):
|
||||
return self.fillslot(request=request, agenda=agenda, timeslot_id=slot)
|
||||
|
||||
class MeetingsAgendaFillslots(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.FillSlotsSerializer
|
||||
|
||||
def post(self, request, agenda, slots):
|
||||
if not settings.LEGACY_FILLSLOTS_ENABLED and slots is None:
|
||||
raise APIErrorBadRequest(N_('deprecated call'))
|
||||
|
||||
return self.fillslot(request=request, agenda=agenda, slots=slots)
|
||||
|
||||
def fillslot(self, request, agenda, slots=None, retry=False):
|
||||
slots = slots or []
|
||||
|
||||
def fillslot(self, request, agenda, timeslot_id, retry=False):
|
||||
known_body_params = set(request.query_params).intersection(
|
||||
{'label', 'user_name', 'backoffice_url', 'user_display_label'}
|
||||
)
|
||||
|
@ -1396,9 +1370,6 @@ class MeetingsAgendaFillslots(APIView):
|
|||
raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
|
||||
payload = serializer.validated_data
|
||||
|
||||
if 'slots' in payload:
|
||||
slots = payload['slots']
|
||||
|
||||
to_cancel_booking = None
|
||||
cancel_booking_id = None
|
||||
if payload.get('cancel_booking_id'):
|
||||
|
@ -1426,23 +1397,15 @@ class MeetingsAgendaFillslots(APIView):
|
|||
user_external_id = payload.get('user_external_id') or None
|
||||
exclude_user = payload.get('exclude_user')
|
||||
|
||||
# slots are actually timeslot ids (meeting_type:start_datetime), not events ids.
|
||||
# split them back to get both parts
|
||||
meeting_type_id = slots[0].split(':')[0]
|
||||
datetimes = set()
|
||||
for slot in slots:
|
||||
try:
|
||||
meeting_type_id_, datetime_str = slot.split(':')
|
||||
except ValueError:
|
||||
raise APIErrorBadRequest(N_('invalid slot: %s'), slot)
|
||||
if meeting_type_id_ != meeting_type_id:
|
||||
raise APIErrorBadRequest(
|
||||
N_('all slots must have the same meeting type id (%s)'), meeting_type_id
|
||||
)
|
||||
try:
|
||||
datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M')))
|
||||
except ValueError:
|
||||
raise APIErrorBadRequest(N_('bad datetime format: %s'), datetime_str)
|
||||
try:
|
||||
meeting_type_id, datetime_str = timeslot_id.split(':')
|
||||
except ValueError:
|
||||
raise APIErrorBadRequest(N_('invalid timeslot_id: %s'), timeslot_id)
|
||||
|
||||
try:
|
||||
slot_datetime = make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M'))
|
||||
except ValueError:
|
||||
raise APIErrorBadRequest(N_('bad datetime format: %s'), datetime_str)
|
||||
|
||||
resources = get_resources_from_request(request, agenda)
|
||||
|
||||
|
@ -1460,8 +1423,8 @@ class MeetingsAgendaFillslots(APIView):
|
|||
meeting_type,
|
||||
resources=resources,
|
||||
user_external_id=user_external_id if exclude_user else None,
|
||||
start_datetime=min(datetimes),
|
||||
end_datetime=max(datetimes) + datetime.timedelta(minutes=meeting_type.duration),
|
||||
start_datetime=slot_datetime,
|
||||
end_datetime=slot_datetime + datetime.timedelta(minutes=meeting_type.duration),
|
||||
),
|
||||
key=lambda slot: slot.start_datetime,
|
||||
)
|
||||
|
@ -1496,18 +1459,15 @@ class MeetingsAgendaFillslots(APIView):
|
|||
|
||||
# select a desk on the agenda with min fill_rate on the given date
|
||||
for available_desk_id in sorted(datetimes_by_desk.keys()):
|
||||
if datetimes.issubset(datetimes_by_desk[available_desk_id]):
|
||||
if slot_datetime in datetimes_by_desk[available_desk_id]:
|
||||
desk = Desk.objects.get(id=available_desk_id)
|
||||
if available_desk is None:
|
||||
available_desk = desk
|
||||
available_desk_rate = 0
|
||||
for dt in datetimes:
|
||||
available_desk_rate += fill_rates[available_desk.agenda][dt.date()]['fill_rate']
|
||||
available_desk_rate = fill_rates[available_desk.agenda][slot_datetime.date()][
|
||||
'fill_rate'
|
||||
]
|
||||
else:
|
||||
for dt in datetimes:
|
||||
desk_rate = 0
|
||||
for dt in datetimes:
|
||||
desk_rate += fill_rates[desk.agenda][dt.date()]['fill_rate']
|
||||
desk_rate = fill_rates[desk.agenda][slot_datetime.date()]['fill_rate']
|
||||
if desk_rate < available_desk_rate:
|
||||
available_desk = desk
|
||||
available_desk_rate = desk_rate
|
||||
|
@ -1516,36 +1476,28 @@ class MeetingsAgendaFillslots(APIView):
|
|||
# meeting agenda
|
||||
# search first desk where all requested slots are free
|
||||
for available_desk_id in sorted(datetimes_by_desk.keys()):
|
||||
if datetimes.issubset(datetimes_by_desk[available_desk_id]):
|
||||
if slot_datetime in datetimes_by_desk[available_desk_id]:
|
||||
available_desk = Desk.objects.get(id=available_desk_id)
|
||||
break
|
||||
|
||||
if available_desk is None:
|
||||
raise APIError(N_('no more desk available'))
|
||||
|
||||
# all datetimes are free, book them in order
|
||||
datetimes = list(datetimes)
|
||||
datetimes.sort()
|
||||
|
||||
# get a real meeting_type for virtual agenda
|
||||
if agenda.kind == 'virtual':
|
||||
meeting_type = MeetingType.objects.get(agenda=available_desk.agenda, slug=meeting_type.slug)
|
||||
|
||||
# booking requires real Event objects (not lazy Timeslots);
|
||||
# create them now, with data from the slots and the desk we found.
|
||||
events = []
|
||||
for start_datetime in datetimes:
|
||||
events.append(
|
||||
Event(
|
||||
agenda=available_desk.agenda,
|
||||
slug=str(uuid.uuid4()), # set slug to avoid queries during slug generation
|
||||
meeting_type=meeting_type,
|
||||
start_datetime=start_datetime,
|
||||
full=False,
|
||||
places=1,
|
||||
desk=available_desk,
|
||||
)
|
||||
)
|
||||
# booking requires real Event object (not lazy Timeslot);
|
||||
# create it now, with data from the slot and the desk we found.
|
||||
event = Event(
|
||||
agenda=available_desk.agenda,
|
||||
slug=str(uuid.uuid4()), # set slug to avoid queries during slug generation
|
||||
meeting_type=meeting_type,
|
||||
start_datetime=slot_datetime,
|
||||
full=False,
|
||||
places=1,
|
||||
desk=available_desk,
|
||||
)
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
|
@ -1553,23 +1505,17 @@ class MeetingsAgendaFillslots(APIView):
|
|||
cancelled_booking_id = to_cancel_booking.pk
|
||||
to_cancel_booking.cancel()
|
||||
|
||||
# now we have a list of events, book them.
|
||||
primary_booking = None
|
||||
for event in events:
|
||||
if agenda.accept_meetings():
|
||||
event.save()
|
||||
if resources:
|
||||
event.resources.add(*resources)
|
||||
new_booking = make_booking(
|
||||
event=event,
|
||||
payload=payload,
|
||||
extra_data=extra_data,
|
||||
primary_booking=primary_booking,
|
||||
color=color,
|
||||
)
|
||||
new_booking.save()
|
||||
if primary_booking is None:
|
||||
primary_booking = new_booking
|
||||
# book event
|
||||
event.save()
|
||||
if resources:
|
||||
event.resources.add(*resources)
|
||||
booking = make_booking(
|
||||
event=event,
|
||||
payload=payload,
|
||||
extra_data=extra_data,
|
||||
color=color,
|
||||
)
|
||||
booking.save()
|
||||
except IntegrityError as e:
|
||||
if 'tstzrange_constraint' in str(e):
|
||||
# "optimistic concurrency control", between our availability
|
||||
|
@ -1583,33 +1529,33 @@ class MeetingsAgendaFillslots(APIView):
|
|||
# of fillslot().
|
||||
if retry:
|
||||
raise APIError(N_('no more desk available'))
|
||||
return self.fillslot(request=request, agenda=agenda, slots=slots, retry=True)
|
||||
return self.fillslot(request=request, agenda=agenda, timeslot_id=timeslot_id, retry=True)
|
||||
raise
|
||||
|
||||
response = {
|
||||
'err': 0,
|
||||
'booking_id': primary_booking.id,
|
||||
'datetime': format_response_datetime(events[0].start_datetime),
|
||||
'booking_id': booking.id,
|
||||
'datetime': format_response_datetime(event.start_datetime),
|
||||
'agenda': {
|
||||
'label': primary_booking.event.agenda.label,
|
||||
'slug': primary_booking.event.agenda.slug,
|
||||
'label': booking.event.agenda.label,
|
||||
'slug': booking.event.agenda.slug,
|
||||
},
|
||||
'end_datetime': format_response_datetime(events[-1].end_datetime),
|
||||
'duration': (events[-1].end_datetime - events[-1].start_datetime).seconds // 60,
|
||||
'end_datetime': format_response_datetime(event.end_datetime),
|
||||
'duration': (event.end_datetime - event.start_datetime).seconds // 60,
|
||||
'resources': [r.slug for r in resources],
|
||||
'desk': {'label': available_desk.label, 'slug': available_desk.slug},
|
||||
'api': {
|
||||
'booking_url': request.build_absolute_uri(
|
||||
reverse('api-booking', kwargs={'booking_pk': primary_booking.id})
|
||||
reverse('api-booking', kwargs={'booking_pk': booking.id})
|
||||
),
|
||||
'cancel_url': request.build_absolute_uri(
|
||||
reverse('api-cancel-booking', kwargs={'booking_pk': primary_booking.id})
|
||||
reverse('api-cancel-booking', kwargs={'booking_pk': booking.id})
|
||||
),
|
||||
'ics_url': request.build_absolute_uri(
|
||||
reverse('api-booking-ics', kwargs={'booking_pk': primary_booking.id})
|
||||
reverse('api-booking-ics', kwargs={'booking_pk': booking.id})
|
||||
),
|
||||
'anonymize_url': request.build_absolute_uri(
|
||||
reverse('api-anonymize-booking', kwargs={'booking_pk': primary_booking.id})
|
||||
reverse('api-anonymize-booking', kwargs={'booking_pk': booking.id})
|
||||
),
|
||||
},
|
||||
}
|
||||
|
@ -1619,13 +1565,10 @@ class MeetingsAgendaFillslots(APIView):
|
|||
return Response(response)
|
||||
|
||||
|
||||
class MeetingsAgendaFillslot(MeetingsAgendaFillslots):
|
||||
class Fillslot(APIView):
|
||||
serializer_class = serializers.FillSlotSerializer
|
||||
|
||||
|
||||
class Fillslots(APIView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
agenda_identifier = kwargs['agenda_identifier']
|
||||
def dispatch(self, request, agenda_identifier, event_identifier):
|
||||
try:
|
||||
agenda = Agenda.objects.get(slug=agenda_identifier)
|
||||
except Agenda.DoesNotExist:
|
||||
|
@ -1635,31 +1578,11 @@ class Fillslots(APIView):
|
|||
except (ValueError, Agenda.DoesNotExist):
|
||||
raise Http404()
|
||||
|
||||
if kwargs.get('slots'):
|
||||
if agenda.accept_meetings():
|
||||
api_view = MeetingsAgendaFillslot()
|
||||
else:
|
||||
api_view = EventsAgendaFillslot()
|
||||
if agenda.accept_meetings():
|
||||
api_view = MeetingsAgendaFillslot()
|
||||
else:
|
||||
if agenda.accept_meetings():
|
||||
api_view = MeetingsAgendaFillslots()
|
||||
else:
|
||||
api_view = EventsAgendaFillslots()
|
||||
return api_view.dispatch(request=request, agenda=agenda, slots=kwargs.get('slots'))
|
||||
|
||||
|
||||
fillslots = Fillslots.as_view()
|
||||
|
||||
|
||||
class Fillslot(Fillslots):
|
||||
serializer_class = serializers.FillSlotSerializer
|
||||
|
||||
def dispatch(self, request, agenda_identifier=None, event_identifier=None):
|
||||
return super().dispatch(
|
||||
request=request,
|
||||
agenda_identifier=agenda_identifier,
|
||||
slots=[event_identifier], # fill a "list on one slot"
|
||||
)
|
||||
api_view = EventsAgendaFillslot()
|
||||
return api_view.dispatch(request=request, agenda=agenda, slot=event_identifier)
|
||||
|
||||
|
||||
fillslot = Fillslot.as_view()
|
||||
|
@ -1704,6 +1627,16 @@ class RecurringFillslots(APIView):
|
|||
guardian_external_id,
|
||||
)
|
||||
events_to_unbook = self.get_events_to_unbook(agendas, events_to_book)
|
||||
elif data['action'] == 'update-from-date':
|
||||
events_to_book = self.get_event_recurrences(
|
||||
agendas,
|
||||
payload['slots'],
|
||||
start_datetime,
|
||||
end_datetime,
|
||||
user_external_id,
|
||||
guardian_external_id,
|
||||
)
|
||||
events_to_unbook = self.get_events_to_unbook(agendas, events_to_book, start_datetime)
|
||||
elif data['action'] == 'book':
|
||||
events_to_book = self.get_event_recurrences(
|
||||
agendas,
|
||||
|
@ -1771,7 +1704,7 @@ class RecurringFillslots(APIView):
|
|||
# don't reload agendas and events types
|
||||
for event in events_to_book:
|
||||
event.agenda = agendas_by_id[event.agenda_id]
|
||||
bookings = [make_booking(event, payload, extra_data) for event in events_to_book]
|
||||
bookings = self.make_bookings(events_to_book, payload, extra_data)
|
||||
|
||||
bookings_to_cancel = Booking.objects.filter(
|
||||
user_external_id=user_external_id, event__in=events_to_unbook, cancellation_datetime__isnull=True
|
||||
|
@ -1866,7 +1799,7 @@ class RecurringFillslots(APIView):
|
|||
|
||||
return events
|
||||
|
||||
def get_events_to_unbook(self, agendas, events_to_book):
|
||||
def get_events_to_unbook(self, agendas, events_to_book, start_datetime=None):
|
||||
events_to_book_ids = set(events_to_book.values_list('pk', flat=True))
|
||||
events_to_unbook = [
|
||||
e
|
||||
|
@ -1877,6 +1810,7 @@ class RecurringFillslots(APIView):
|
|||
and e.pk not in events_to_book_ids
|
||||
and (not agenda.minimal_booking_delay or e.start_datetime >= agenda.min_booking_datetime)
|
||||
and (not agenda.maximal_booking_delay or e.start_datetime <= agenda.max_booking_datetime)
|
||||
and (not start_datetime or e.start_datetime >= start_datetime)
|
||||
]
|
||||
return events_to_unbook
|
||||
|
||||
|
@ -1904,14 +1838,32 @@ class RecurringFillslots(APIView):
|
|||
% ', '.join(sorted('%s / %s' % (x, y) for x, y in overlaps))
|
||||
)
|
||||
|
||||
def make_bookings(self, events, payload, extra_data):
|
||||
return [make_booking(event, payload, extra_data) for event in events]
|
||||
|
||||
|
||||
recurring_fillslots = RecurringFillslots.as_view()
|
||||
|
||||
|
||||
class RecurringFillslotsByDay(RecurringFillslots):
|
||||
serializer_class = serializers.RecurringFillslotsByDaySerializer
|
||||
|
||||
def make_bookings(self, events, payload, extra_data):
|
||||
bookings = []
|
||||
for event in events:
|
||||
payload['start_time'], payload['end_time'] = payload['hours_by_days'][
|
||||
event.start_datetime.isoweekday()
|
||||
]
|
||||
bookings.append(make_booking(event, payload, extra_data))
|
||||
return bookings
|
||||
|
||||
|
||||
recurring_fillslots_by_day = RecurringFillslotsByDay.as_view()
|
||||
|
||||
|
||||
class EventsFillslots(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.EventsFillSlotsSerializer
|
||||
serializer_extra_context = None
|
||||
multiple_agendas = False
|
||||
|
||||
def post(self, request, agenda_identifier):
|
||||
|
@ -2072,7 +2024,11 @@ class EventsFillslots(APIView):
|
|||
],
|
||||
'cancelled_booking_count': cancelled_count,
|
||||
'cancelled_events': cancelled_events,
|
||||
'bookings_ics_url': request.build_absolute_uri(reverse('api-bookings-ics'))
|
||||
+ '?user_external_id=%s' % user_external_id,
|
||||
}
|
||||
if not self.multiple_agendas:
|
||||
response['bookings_ics_url'] += '&agenda=%s' % self.agenda.slug
|
||||
return Response(response)
|
||||
|
||||
def get_events(self, request, payload, start_datetime, end_datetime):
|
||||
|
@ -2086,6 +2042,10 @@ class EventsFillslots(APIView):
|
|||
def get_agendas_by_ids(self):
|
||||
return {self.agenda.pk: self.agenda}
|
||||
|
||||
@property
|
||||
def serializer_extra_context(self):
|
||||
return {'agendas': [self.agenda]}
|
||||
|
||||
|
||||
events_fillslots = EventsFillslots.as_view()
|
||||
|
||||
|
@ -2165,7 +2125,7 @@ class MultipleAgendasEventsFillslots(EventsFillslots):
|
|||
|
||||
@property
|
||||
def serializer_extra_context(self):
|
||||
return {'allowed_agenda_slugs': self.agenda_slugs}
|
||||
return {'allowed_agenda_slugs': self.agenda_slugs, 'agendas': self.agendas}
|
||||
|
||||
|
||||
agendas_events_fillslots = MultipleAgendasEventsFillslots.as_view()
|
||||
|
@ -2244,7 +2204,7 @@ class MultipleAgendasEventsCheckStatus(APIView):
|
|||
booking_queryset = Booking.objects.filter(
|
||||
event__in=events,
|
||||
user_external_id=user_external_id,
|
||||
)
|
||||
).prefetch_related('user_checks')
|
||||
bookings_by_event_id = collections.defaultdict(list)
|
||||
for booking in booking_queryset:
|
||||
bookings_by_event_id[booking.event_id].append(booking)
|
||||
|
@ -2265,12 +2225,12 @@ class MultipleAgendasEventsCheckStatus(APIView):
|
|||
booking.event = event # prevent db calls
|
||||
if booking.cancellation_datetime is not None:
|
||||
check_status = {'status': 'cancelled'}
|
||||
elif booking.user_was_present is None:
|
||||
elif not booking.user_check:
|
||||
check_status = {'status': 'error', 'error_reason': 'booking-not-checked'}
|
||||
else:
|
||||
check_status = {
|
||||
'status': 'presence' if booking.user_was_present else 'absence',
|
||||
'check_type': booking.user_check_type_slug,
|
||||
'status': 'presence' if booking.user_check.presence else 'absence',
|
||||
'check_type': booking.user_check.type_slug,
|
||||
}
|
||||
data.append(
|
||||
{
|
||||
|
@ -2308,6 +2268,9 @@ class MultipleAgendasEventsCheckLock(APIView):
|
|||
start_datetime__lt=date_end,
|
||||
)
|
||||
events.update(check_locked=check_locked)
|
||||
if check_locked is False:
|
||||
for event in events:
|
||||
event.async_refresh_booking_computed_times()
|
||||
|
||||
return Response({'err': 0})
|
||||
|
||||
|
@ -2547,6 +2510,7 @@ class BookingFilter(filters.FilterSet):
|
|||
category = filters.CharFilter(field_name='event__agenda__category__slug', lookup_expr='exact')
|
||||
date_start = filters.DateFilter(field_name='event__start_datetime', lookup_expr='gte')
|
||||
date_end = filters.DateFilter(field_name='event__start_datetime', lookup_expr='lt')
|
||||
user_was_present = filters.CharFilter(method='filter_user_was_present')
|
||||
user_absence_reason = filters.CharFilter(method='filter_user_absence_reason')
|
||||
user_presence_reason = filters.CharFilter(method='filter_user_presence_reason')
|
||||
|
||||
|
@ -2554,16 +2518,19 @@ class BookingFilter(filters.FilterSet):
|
|||
# we want to include bookings of event recurrences
|
||||
return queryset.filter(Q(event__slug=value) | Q(event__primary_event__slug=value))
|
||||
|
||||
def filter_user_was_present(self, queryset, name, value):
|
||||
return queryset.filter(user_checks__presence=value)
|
||||
|
||||
def filter_user_absence_reason(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(user_check_type_slug=value) | Q(user_check_type_label=value),
|
||||
user_was_present=False,
|
||||
Q(user_checks__type_slug=value) | Q(user_checks__type_label=value),
|
||||
user_checks__presence=False,
|
||||
)
|
||||
|
||||
def filter_user_presence_reason(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(user_check_type_slug=value) | Q(user_check_type_label=value),
|
||||
user_was_present=True,
|
||||
Q(user_checks__type_slug=value) | Q(user_checks__type_label=value),
|
||||
user_checks__presence=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -2608,6 +2575,7 @@ class BookingsAPI(ListAPIView):
|
|||
return (
|
||||
Booking.objects.filter(primary_booking__isnull=True, cancellation_datetime__isnull=True)
|
||||
.select_related('event', 'event__agenda', 'event__desk')
|
||||
.prefetch_related('user_checks')
|
||||
.order_by('event__start_datetime', 'event__slug', 'event__agenda__slug', 'pk')
|
||||
)
|
||||
|
||||
|
@ -2615,6 +2583,29 @@ class BookingsAPI(ListAPIView):
|
|||
bookings = BookingsAPI.as_view()
|
||||
|
||||
|
||||
class BookingsICS(BookingsAPI):
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not request.GET.get('user_external_id'):
|
||||
raise APIError(N_('missing param user_external_id'))
|
||||
|
||||
try:
|
||||
bookings = self.filter_queryset(self.get_queryset())
|
||||
except ValidationError as e:
|
||||
raise APIErrorBadRequest(N_('invalid payload'), errors=e.detail)
|
||||
|
||||
ics = vobject.iCalendar()
|
||||
ics.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
|
||||
|
||||
for booking in bookings:
|
||||
vevent = booking.get_vevent_ics()
|
||||
ics.add(vevent)
|
||||
|
||||
return HttpResponse(ics.serialize(), content_type='text/calendar')
|
||||
|
||||
|
||||
bookings_ics = BookingsICS.as_view()
|
||||
|
||||
|
||||
class BookingAPI(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.BookingSerializer
|
||||
|
@ -2672,7 +2663,10 @@ class BookingAPI(APIView):
|
|||
):
|
||||
raise APIErrorBadRequest(N_('event is marked as checked'), err=5)
|
||||
|
||||
user_was_present = serializer.validated_data.get('user_was_present', self.booking.user_was_present)
|
||||
user_was_present = serializer.validated_data.get(
|
||||
'user_was_present',
|
||||
self.booking.user_check.presence if self.booking.user_check else None,
|
||||
)
|
||||
if user_was_present is True and 'user_absence_reason' in request.data:
|
||||
raise APIErrorBadRequest(N_('user is marked as present, can not set absence reason'), err=6)
|
||||
if user_was_present is False and 'user_presence_reason' in request.data:
|
||||
|
@ -2685,9 +2679,23 @@ class BookingAPI(APIView):
|
|||
self.booking.extra_data.update(extra_data)
|
||||
self.booking.save()
|
||||
|
||||
if user_was_present is None:
|
||||
if self.booking.user_check:
|
||||
self.booking.user_check.delete()
|
||||
self.booking.user_check = None
|
||||
else:
|
||||
if not self.booking.user_check:
|
||||
self.booking.user_check = BookingCheck(booking=self.booking)
|
||||
self.booking.user_check.presence = user_was_present
|
||||
self.booking.user_check.save()
|
||||
|
||||
if 'user_check_type_slug' in serializer.validated_data and self.booking.user_check:
|
||||
self.booking.user_check.type_slug = serializer.validated_data['user_check_type_slug']
|
||||
self.booking.user_check.type_label = serializer.validated_data['user_check_type_label']
|
||||
self.booking.user_check.save()
|
||||
|
||||
secondary_bookings_update = {}
|
||||
for key in [
|
||||
'user_was_present',
|
||||
'user_first_name',
|
||||
'user_last_name',
|
||||
'user_email',
|
||||
|
@ -2697,9 +2705,6 @@ class BookingAPI(APIView):
|
|||
secondary_bookings_update[key] = getattr(self.booking, key)
|
||||
if 'use_color_for' in request.data:
|
||||
secondary_bookings_update['color'] = self.booking.color
|
||||
if 'user_absence_reason' in request.data or 'user_presence_reason' in request.data:
|
||||
secondary_bookings_update['user_check_type_slug'] = self.booking.user_check_type_slug
|
||||
secondary_bookings_update['user_check_type_label'] = self.booking.user_check_type_label
|
||||
if extra_data:
|
||||
secondary_bookings_update['extra_data'] = self.booking.extra_data
|
||||
if secondary_bookings_update:
|
||||
|
@ -3253,9 +3258,17 @@ class BookingsStatistics(APIView):
|
|||
if not isinstance(group_by, list): # legacy support
|
||||
group_by = [group_by]
|
||||
|
||||
lookups = [
|
||||
'extra_data__%s' % field if field != 'user_was_present' else field for field in group_by
|
||||
]
|
||||
lookups = []
|
||||
if 'user_was_present' in group_by:
|
||||
bookings = bookings.annotate(
|
||||
presence=Case(
|
||||
When(primary_booking__isnull=True, then=F('user_checks__presence')),
|
||||
When(primary_booking__isnull=False, then=F('primary_booking__user_checks__presence')),
|
||||
)
|
||||
)
|
||||
lookups.append('presence')
|
||||
|
||||
lookups += ['extra_data__%s' % field for field in group_by if field != 'user_was_present']
|
||||
bookings = bookings.values('day', *lookups).annotate(total=Count('id')).order_by('day')
|
||||
|
||||
days = bookings_by_day = collections.OrderedDict(
|
||||
|
|
|
@ -22,6 +22,7 @@ from django.conf import settings
|
|||
from django.db import models, transaction
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import pgettext_lazy
|
||||
|
||||
from chrono.agendas.models import Agenda, Booking
|
||||
from chrono.utils.timezone import localtime, now
|
||||
|
@ -228,22 +229,27 @@ class Place(models.Model):
|
|||
.values_list(
|
||||
'extra_data__ants_identifiant_predemande', 'event__start_datetime', 'cancellation_datetime'
|
||||
)
|
||||
.order_by('event__state_datetime')
|
||||
.order_by('event__start_datetime')
|
||||
)
|
||||
for identifiant_predemande, start_datetime, cancellation_datetime in bookings:
|
||||
if not isinstance(identifiant_predemande, str):
|
||||
for identifiant_predemande_data, start_datetime, cancellation_datetime in bookings:
|
||||
if not isinstance(identifiant_predemande_data, str):
|
||||
continue
|
||||
rdv = {
|
||||
'id': identifiant_predemande,
|
||||
'date': start_datetime.isoformat(),
|
||||
}
|
||||
if cancellation_datetime is not None:
|
||||
rdv['annule'] = True
|
||||
yield rdv
|
||||
# split data on commas, and remove trailing whitespaces
|
||||
identifiant_predemandes = filter(
|
||||
None, (part.strip() for part in identifiant_predemande_data.split(','))
|
||||
)
|
||||
for identifiant_predemande in identifiant_predemandes:
|
||||
rdv = {
|
||||
'id': identifiant_predemande,
|
||||
'date': start_datetime.isoformat(),
|
||||
}
|
||||
if cancellation_datetime is not None:
|
||||
rdv['annule'] = True
|
||||
yield rdv
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('place')
|
||||
verbose_name_plural = _('places')
|
||||
verbose_name = pgettext_lazy('location', 'place')
|
||||
verbose_name_plural = pgettext_lazy('location', 'places')
|
||||
unique_together = [
|
||||
('city', 'name'),
|
||||
]
|
||||
|
|
|
@ -7,8 +7,8 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: chrono 0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-07-18 18:13+0200\n"
|
||||
"PO-Revision-Date: 2023-07-18 18:16+0200\n"
|
||||
"POT-Creation-Date: 2023-10-09 10:54+0200\n"
|
||||
"PO-Revision-Date: 2023-10-09 10:58+0200\n"
|
||||
"Last-Translator: Frederic Peters <fpeters@entrouvert.com>\n"
|
||||
"Language: French\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
@ -260,12 +260,40 @@ msgstr "Horaire d’ouverture des réservations"
|
|||
|
||||
#: agendas/models.py
|
||||
msgid ""
|
||||
"Ex.: 08:00:00. If left empty, available events will be those that are later "
|
||||
"than the current time."
|
||||
"If left empty, available events will be those that are later than the "
|
||||
"current time."
|
||||
msgstr ""
|
||||
"Exemple : 08:00:00. Si ce champ est laissé vide, les évènements ou rendez-"
|
||||
"vous disponibles à la réservation seront ceux qui commencent après l’heure "
|
||||
"actuelle, en prenant en compte les délais de réservation minimal et maximal."
|
||||
"Si ce champ est laissé vide, les évènements ou rendez-vous disponibles à la "
|
||||
"réservation seront ceux qui commencent après l’heure actuelle, en prenant en "
|
||||
"compte les délais de réservation minimal et maximal."
|
||||
|
||||
#: agendas/models.py
|
||||
msgid "Invoicing"
|
||||
msgstr "Facturation"
|
||||
|
||||
#: agendas/models.py
|
||||
msgid "Per hour"
|
||||
msgstr "À l’heure"
|
||||
|
||||
#: agendas/models.py
|
||||
msgid "Per half hour"
|
||||
msgstr "À la demi-heure"
|
||||
|
||||
#: agendas/models.py
|
||||
msgid "Per quarter-hour"
|
||||
msgstr "Au quart d’heure"
|
||||
|
||||
#: agendas/models.py
|
||||
msgid "Per minute"
|
||||
msgstr "À la minute"
|
||||
|
||||
#: agendas/models.py
|
||||
msgid "Tolerance"
|
||||
msgstr "Tolérance"
|
||||
|
||||
#: agendas/models.py manager/forms.py
|
||||
msgid "Partial bookings"
|
||||
msgstr "Plages libres"
|
||||
|
||||
#: agendas/models.py
|
||||
#, python-format
|
||||
|
@ -980,6 +1008,15 @@ msgstr "Armistice 1945"
|
|||
msgid "Whit Monday"
|
||||
msgstr "Lundi de Pentecôte"
|
||||
|
||||
#: api/serializers.py
|
||||
msgid "must include start_time and end_time for partial bookings agenda"
|
||||
msgstr ""
|
||||
"start_time et end_time doivent être inclus pour les agendas plages libres"
|
||||
|
||||
#: api/serializers.py
|
||||
msgid "start_time must be before end_time"
|
||||
msgstr "start_time doit précéder end_time"
|
||||
|
||||
#: api/serializers.py manager/forms.py
|
||||
msgid "This field is required."
|
||||
msgstr "Le champ est obligatoire."
|
||||
|
@ -1009,15 +1046,6 @@ msgid "Events from the following agendas cannot be booked: %s"
|
|||
msgstr "Les évènements de ces agendas ne peuvent pas être réservés : %s"
|
||||
|
||||
#: api/serializers.py
|
||||
msgid "must include start_time and end_time for partial bookings agenda"
|
||||
msgstr ""
|
||||
"start_time et end_time doivent être inclus pour les agendas plages libres"
|
||||
|
||||
#: api/serializers.py
|
||||
msgid "start_time must be before end_time"
|
||||
msgstr "start_time doit précéder end_time"
|
||||
|
||||
#: api/serializers.py api/views.py
|
||||
#, python-format
|
||||
msgid "invalid slot: %s"
|
||||
msgstr "créneau invalide : %s"
|
||||
|
@ -1028,6 +1056,10 @@ msgid "event %(event_slug)s of agenda %(agenda_slug)s is not bookable"
|
|||
msgstr ""
|
||||
"l’évènement %(event_slug)s de l’agenda %(agenda_slug)s n’est pas réservable"
|
||||
|
||||
#: api/serializers.py
|
||||
msgid "Start hour must be before end hour."
|
||||
msgstr "L’heure d’arrivée doit précéder l’heure de départ."
|
||||
|
||||
#: api/serializers.py
|
||||
msgid "unknown absence reason"
|
||||
msgstr "motif d’absence inconnu"
|
||||
|
@ -1187,10 +1219,6 @@ msgstr ""
|
|||
msgid "it is not possible to change kind value"
|
||||
msgstr "il n’est pas possible de modifier le type"
|
||||
|
||||
#: api/views.py
|
||||
msgid "deprecated call"
|
||||
msgstr "appel déprécié"
|
||||
|
||||
#: api/views.py
|
||||
#, python-format
|
||||
msgid "parameters \"%s\" must be included in request body, not query"
|
||||
|
@ -1236,8 +1264,8 @@ msgstr "plus de guichet disponible"
|
|||
|
||||
#: api/views.py
|
||||
#, python-format
|
||||
msgid "all slots must have the same meeting type id (%s)"
|
||||
msgstr "tous les créneaux doivent être pour le même type d’évènement (%s)"
|
||||
msgid "invalid timeslot_id: %s"
|
||||
msgstr "timeslot_id invalide : %s"
|
||||
|
||||
#: api/views.py
|
||||
#, python-format
|
||||
|
@ -1512,13 +1540,14 @@ msgid "Full synchronization"
|
|||
msgstr "Synchronisation complète"
|
||||
|
||||
#: apps/ants_hub/models.py
|
||||
msgctxt "location"
|
||||
msgid "place"
|
||||
msgstr "lieu"
|
||||
|
||||
#: apps/ants_hub/models.py manager/templates/chrono/manager_event_check.html
|
||||
#: manager/templates/chrono/manager_event_check_booking_fragment.html
|
||||
#: apps/ants_hub/models.py
|
||||
msgctxt "location"
|
||||
msgid "places"
|
||||
msgstr "lieus"
|
||||
msgstr "lieux"
|
||||
|
||||
#: apps/ants_hub/models.py
|
||||
msgid "CNI"
|
||||
|
@ -1781,10 +1810,6 @@ msgstr "%(mt_label)s (%(mt_duration)s minutes)"
|
|||
msgid "Synchronization has been launched."
|
||||
msgstr "La synchronisation a été lancée."
|
||||
|
||||
#: manager/forms.py
|
||||
msgid "Partial bookings"
|
||||
msgstr "Plages libres"
|
||||
|
||||
#: manager/forms.py
|
||||
msgid "Desk 1"
|
||||
msgstr "Guichet 1"
|
||||
|
@ -1934,6 +1959,22 @@ msgstr "Statut"
|
|||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
#: manager/forms.py
|
||||
msgid "Fill with booking start time"
|
||||
msgstr "Prendre l’heure de début de la réservation"
|
||||
|
||||
#: manager/forms.py
|
||||
msgid "Fill with booking end time"
|
||||
msgstr "Prendre l’heure de fin de la réservation"
|
||||
|
||||
#: manager/forms.py
|
||||
msgid "Arrival must be before departure."
|
||||
msgstr "L’heure d’arrivée doit précéder l’heure de départ."
|
||||
|
||||
#: manager/forms.py
|
||||
msgid "Booking check hours overlap existing check."
|
||||
msgstr "Les heures de pointage chevauchent un pointage existant."
|
||||
|
||||
#: manager/forms.py
|
||||
msgid "Start date"
|
||||
msgstr "Date de début"
|
||||
|
@ -2077,58 +2118,49 @@ msgstr ""
|
|||
msgid "Invalid file format."
|
||||
msgstr "Format de fichier invalide."
|
||||
|
||||
#: manager/forms.py
|
||||
#, python-brace-format
|
||||
msgid "Invalid file format. ({event_no} event)"
|
||||
msgstr "Format de fichier invalide. ({event_no} évènement)"
|
||||
|
||||
#: manager/forms.py manager/templates/chrono/manager_sample_events.txt
|
||||
#: manager/views.py
|
||||
msgid "date"
|
||||
msgstr "date"
|
||||
|
||||
#: manager/forms.py
|
||||
#, python-brace-format
|
||||
msgid "Invalid file format. (date/time format, {event_no} event)"
|
||||
msgstr "Format de fichier invalide. (format date/heure, {event_no} évènement)"
|
||||
msgid "Invalid file format:"
|
||||
msgstr "Format de fichier invalide :"
|
||||
|
||||
#: manager/forms.py
|
||||
#, python-brace-format
|
||||
msgid "Invalid file format. (number of places, {event_no} event)"
|
||||
msgstr "Format de fichier invalide. (nombre de places, {event_no} évènement)"
|
||||
msgid "Not enough columns."
|
||||
msgstr "Pas assez de colonnes."
|
||||
|
||||
#: manager/forms.py
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Invalid file format. (number of places in waiting list, {event_no} event)"
|
||||
msgid "Wrong start date/time format."
|
||||
msgstr "Format de date/heure invalide."
|
||||
|
||||
#: manager/forms.py
|
||||
msgid "Number of places must be an integer."
|
||||
msgstr "Le nombre de places doit être un nombre entier."
|
||||
|
||||
#: manager/forms.py
|
||||
msgid "Number of places in waiting list must be an integer."
|
||||
msgstr ""
|
||||
"Format de fichier invalide. (nombre de places sur la liste d’attente, "
|
||||
"{event_no} évènement)"
|
||||
"Le nombre de places dans la liste d’attente doit être un nombre entier."
|
||||
|
||||
#: manager/forms.py
|
||||
#, python-brace-format
|
||||
msgid "Invalid file format. (missing end_time, {event_no} event)"
|
||||
msgstr "Format de fichier invalide. (end_time manquant, {event_no} évènement)"
|
||||
msgid "Wrong publication date/time format."
|
||||
msgstr "Format de date/heure de publication invalide."
|
||||
|
||||
#: manager/forms.py
|
||||
#, python-brace-format
|
||||
msgid "Invalid file format. (duration, {event_no} event)"
|
||||
msgstr "Format de fichier invalide. (durée, {event_no} évènement)"
|
||||
msgid "Missing end_time."
|
||||
msgstr "end_time absent."
|
||||
|
||||
#: manager/forms.py
|
||||
msgid "Invalid file format:\n"
|
||||
msgstr "Format de fichier invalide :\n"
|
||||
msgid "Duration must be an integer."
|
||||
msgstr "La durée doit être un nombre entier."
|
||||
|
||||
#: manager/forms.py
|
||||
#, python-format
|
||||
msgid "%s: "
|
||||
msgstr "%s : "
|
||||
|
||||
#: manager/forms.py
|
||||
#, python-format
|
||||
msgid "%(errors)s (line %(line)d)"
|
||||
msgstr "%(errors)s (ligne %(line)d)"
|
||||
|
||||
#: manager/forms.py
|
||||
msgid "ICS File"
|
||||
msgstr "Fichier ICS"
|
||||
|
@ -2362,16 +2394,19 @@ msgstr "Complet"
|
|||
|
||||
#: manager/templates/chrono/manager_agenda_event_fragment.html
|
||||
#: manager/templates/chrono/manager_event_detail.html
|
||||
#: manager/templates/chrono/manager_partial_bookings_day_view.html
|
||||
msgid "Invoiced"
|
||||
msgstr "Facturé"
|
||||
|
||||
#: manager/templates/chrono/manager_agenda_event_fragment.html
|
||||
#: manager/templates/chrono/manager_event_detail.html
|
||||
#: manager/templates/chrono/manager_partial_bookings_day_view.html
|
||||
msgid "Check locked"
|
||||
msgstr "Pointage verrouillé"
|
||||
|
||||
#: manager/templates/chrono/manager_agenda_event_fragment.html
|
||||
#: manager/templates/chrono/manager_event_detail.html
|
||||
#: manager/templates/chrono/manager_partial_bookings_day_view.html
|
||||
msgid "Checked"
|
||||
msgstr "Pointé"
|
||||
|
||||
|
@ -2804,6 +2839,7 @@ 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 "Mark the event as checked"
|
||||
msgstr "Marquer l’évènement comme étant pointé"
|
||||
|
||||
|
@ -2816,13 +2852,18 @@ msgstr "Marquer toutes les réservations non précisées :"
|
|||
msgid "Waiting List (%(booked_places)s/%(places)s)"
|
||||
msgstr "Liste d’attente (%(booked_places)s/%(places)s)"
|
||||
|
||||
#: manager/templates/chrono/manager_event_check.html
|
||||
#: manager/templates/chrono/manager_event_check_booking_fragment.html
|
||||
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,-"
|
||||
msgid "Present,Absent"
|
||||
msgstr "Présence,Absence"
|
||||
|
||||
#: manager/templates/chrono/manager_event_check_booking_fragment.html
|
||||
msgctxt "check"
|
||||
|
@ -2923,6 +2964,10 @@ msgstr "Paramètres d’affichage"
|
|||
msgid "Booking check options"
|
||||
msgstr "Paramètres du pointage"
|
||||
|
||||
#: manager/templates/chrono/manager_events_agenda_settings.html
|
||||
msgid "Invoicing options"
|
||||
msgstr "Paramètres de facturation"
|
||||
|
||||
#: manager/templates/chrono/manager_events_agenda_settings.html
|
||||
msgid "Management notifications"
|
||||
msgstr "Notifications d’administration"
|
||||
|
@ -3004,6 +3049,21 @@ msgstr "Activer le pointage des réservations sur les évènements à venir :"
|
|||
msgid "Extra user block template:"
|
||||
msgstr "Gabarit supplémentaire pour les données de l’usager :"
|
||||
|
||||
#: manager/templates/chrono/manager_events_agenda_settings.html
|
||||
msgid "Invoicing:"
|
||||
msgstr "Facturation :"
|
||||
|
||||
#: manager/templates/chrono/manager_events_agenda_settings.html
|
||||
msgid "Tolerance:"
|
||||
msgstr "Tolérance :"
|
||||
|
||||
#: manager/templates/chrono/manager_events_agenda_settings.html
|
||||
#, python-format
|
||||
msgid "%(num)s minute"
|
||||
msgid_plural "%(num)s minutes"
|
||||
msgstr[0] "%(num)s minute"
|
||||
msgstr[1] "%(num)s minutes"
|
||||
|
||||
#: manager/templates/chrono/manager_events_agenda_settings.html
|
||||
#, python-format
|
||||
msgid "%(label)s: %(display_value)s will be notified."
|
||||
|
@ -3263,6 +3323,10 @@ msgstr "Malheureusement il n’y en a pour le moment aucun accessible."
|
|||
msgid "Check booking"
|
||||
msgstr "Pointage"
|
||||
|
||||
#: manager/templates/chrono/manager_partial_booking_form.html
|
||||
msgid "Add second booking check"
|
||||
msgstr "Ajouter un deuxième pointage"
|
||||
|
||||
#: manager/templates/chrono/manager_partial_bookings_day_view.html
|
||||
msgid "Booked period"
|
||||
msgstr "Période réservée"
|
||||
|
@ -3275,6 +3339,14 @@ msgstr "Période réservée :"
|
|||
msgid "Checked period:"
|
||||
msgstr "Période pointée :"
|
||||
|
||||
#: manager/templates/chrono/manager_partial_bookings_day_view.html
|
||||
msgid "Computed period"
|
||||
msgstr "Période calculée"
|
||||
|
||||
#: manager/templates/chrono/manager_partial_bookings_day_view.html
|
||||
msgid "Computed period:"
|
||||
msgstr "Période calculée :"
|
||||
|
||||
#: manager/templates/chrono/manager_replace_exceptions.html
|
||||
msgid "Replace exceptions"
|
||||
msgstr "Remplacer les exceptions"
|
||||
|
@ -3706,6 +3778,10 @@ msgstr "Configurer les paramètres d’affichage"
|
|||
msgid "Configure booking check options"
|
||||
msgstr "Configurer les paramètres de pointage"
|
||||
|
||||
#: manager/views.py
|
||||
msgid "Configure invoicing options"
|
||||
msgstr "Configurer les paramètres de facturation"
|
||||
|
||||
#: manager/views.py
|
||||
msgid "Event successfully duplicated."
|
||||
msgstr "L’évènement a bien été dupliqué."
|
||||
|
|
|
@ -47,6 +47,7 @@ from chrono.agendas.models import (
|
|||
AgendaNotificationsSettings,
|
||||
AgendaReminderSettings,
|
||||
Booking,
|
||||
BookingCheck,
|
||||
Category,
|
||||
Desk,
|
||||
Event,
|
||||
|
@ -94,6 +95,7 @@ class AgendaAddForm(forms.ModelForm):
|
|||
self.cleaned_data['kind'] = 'events'
|
||||
self.instance.partial_bookings = True
|
||||
self.instance.default_view = 'day'
|
||||
self.instance.enable_check_for_future_events = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
create = self.instance.pk is None
|
||||
|
@ -145,6 +147,9 @@ class AgendaBookingDelaysForm(forms.ModelForm):
|
|||
'maximal_booking_delay',
|
||||
'minimal_booking_time',
|
||||
]
|
||||
widgets = {
|
||||
'minimal_booking_time': widgets.TimeWidget,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -526,15 +531,15 @@ class BookingCheckFilterSet(django_filters.FilterSet):
|
|||
if value == 'booked':
|
||||
return queryset
|
||||
if value == 'not-checked':
|
||||
return queryset.filter(user_was_present__isnull=True)
|
||||
return queryset.filter(user_checks__isnull=True)
|
||||
if value == 'presence':
|
||||
return queryset.filter(user_was_present=True)
|
||||
return queryset.filter(user_checks__presence=True)
|
||||
if value == 'absence':
|
||||
return queryset.filter(user_was_present=False)
|
||||
return queryset.filter(user_checks__presence=False)
|
||||
if value.startswith('absence::'):
|
||||
return queryset.filter(user_was_present=False, user_check_type_slug=value.split('::')[1])
|
||||
return queryset.filter(user_checks__presence=False, user_checks__type_slug=value.split('::')[1])
|
||||
if value.startswith('presence::'):
|
||||
return queryset.filter(user_was_present=True, user_check_type_slug=value.split('::')[1])
|
||||
return queryset.filter(user_checks__presence=True, user_checks__type_slug=value.split('::')[1])
|
||||
return queryset
|
||||
|
||||
def do_nothing(self, queryset, name, value):
|
||||
|
@ -579,7 +584,7 @@ class BookingCheckPresenceForm(forms.Form):
|
|||
|
||||
|
||||
class PartialBookingCheckForm(forms.ModelForm):
|
||||
user_was_present = forms.NullBooleanField(
|
||||
presence = forms.NullBooleanField(
|
||||
label=_('Status'),
|
||||
widget=forms.RadioSelect(
|
||||
choices=(
|
||||
|
@ -594,44 +599,70 @@ class PartialBookingCheckForm(forms.ModelForm):
|
|||
absence_check_type = forms.ChoiceField(label=_('Type'), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Booking
|
||||
fields = ['user_check_start_time', 'user_check_end_time', 'user_was_present']
|
||||
model = BookingCheck
|
||||
fields = ['start_time', 'end_time', 'presence', 'type_label', 'type_slug']
|
||||
widgets = {
|
||||
'user_check_start_time': widgets.TimeWidget(step=60),
|
||||
'user_check_end_time': widgets.TimeWidget(step=60),
|
||||
'start_time': widgets.TimeWidgetWithButton(
|
||||
step=60, button_label=_('Fill with booking start time')
|
||||
),
|
||||
'end_time': widgets.TimeWidgetWithButton(step=60, button_label=_('Fill with booking end time')),
|
||||
'type_label': forms.HiddenInput(),
|
||||
'type_slug': forms.HiddenInput(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
agenda = kwargs.pop('agenda')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.check_types = get_agenda_check_types(self.instance.event.agenda)
|
||||
self.check_types = get_agenda_check_types(agenda)
|
||||
absence_check_types = [(ct.slug, ct.label) for ct in self.check_types if ct.kind == 'absence']
|
||||
presence_check_types = [(ct.slug, ct.label) for ct in self.check_types if ct.kind == 'presence']
|
||||
|
||||
if presence_check_types:
|
||||
self.fields['presence_check_type'].choices = [(None, '---------')] + presence_check_types
|
||||
self.fields['presence_check_type'].initial = self.instance.user_check_type_slug
|
||||
self.fields['presence_check_type'].initial = self.instance.type_slug
|
||||
else:
|
||||
del self.fields['presence_check_type']
|
||||
|
||||
if absence_check_types:
|
||||
self.fields['absence_check_type'].choices = [(None, '---------')] + absence_check_types
|
||||
self.fields['absence_check_type'].initial = self.instance.user_check_type_slug
|
||||
self.fields['absence_check_type'].initial = self.instance.type_slug
|
||||
else:
|
||||
del self.fields['absence_check_type']
|
||||
|
||||
if not self.instance.booking.start_time:
|
||||
self.fields['start_time'].widget = widgets.TimeWidget(step=60)
|
||||
self.fields['end_time'].widget = widgets.TimeWidget(step=60)
|
||||
self.fields['presence'].widget.choices = ((None, _('Not checked')), (True, _('Present')))
|
||||
self.fields.pop('absence_check_type', None)
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data['user_was_present'] is not None:
|
||||
kind = 'presence' if self.cleaned_data['user_was_present'] else 'absence'
|
||||
if (
|
||||
self.cleaned_data.get('start_time')
|
||||
and self.cleaned_data.get('end_time')
|
||||
and self.cleaned_data['end_time'] <= self.cleaned_data['start_time']
|
||||
):
|
||||
raise ValidationError(_('Arrival must be before departure.'))
|
||||
|
||||
if self.instance.overlaps_existing_check(
|
||||
self.cleaned_data['start_time'], self.cleaned_data['end_time']
|
||||
):
|
||||
raise ValidationError(_('Booking check hours overlap existing check.'))
|
||||
|
||||
if self.cleaned_data['presence'] is not None:
|
||||
kind = 'presence' if self.cleaned_data['presence'] else 'absence'
|
||||
if f'{kind}_check_type' in self.cleaned_data:
|
||||
self.check_type_slug = self.cleaned_data[f'{kind}_check_type']
|
||||
self.check_type_label = dict(self.fields[f'{kind}_check_type'].choices).get(
|
||||
self.check_type_slug
|
||||
self.cleaned_data['type_slug'] = self.cleaned_data[f'{kind}_check_type']
|
||||
self.cleaned_data['type_label'] = dict(self.fields[f'{kind}_check_type'].choices).get(
|
||||
self.cleaned_data['type_slug']
|
||||
)
|
||||
|
||||
def save(self):
|
||||
if hasattr(self, 'check_type_slug'):
|
||||
self.instance.user_check_type_slug = self.check_type_slug
|
||||
self.instance.user_check_type_label = self.check_type_label
|
||||
if self.cleaned_data['presence'] is None:
|
||||
self.instance.delete()
|
||||
return self.instance
|
||||
|
||||
self.instance.refresh_computed_times()
|
||||
|
||||
return super().save()
|
||||
|
||||
|
||||
|
@ -1206,14 +1237,7 @@ class ImportEventsForm(forms.Form):
|
|||
super().__init__(**kwargs)
|
||||
|
||||
def clean_events_csv_file(self):
|
||||
class ValidationErrorWithOrdinal(ValidationError):
|
||||
line_offset = 1
|
||||
|
||||
def __init__(self, message, event_no):
|
||||
super().__init__(message)
|
||||
self.message = format_html(message, event_no=mark_safe(ordinal(event_no + self.line_offset)))
|
||||
|
||||
exclude_from_validation = ['desk', 'meeting_type', 'primary_event']
|
||||
self.exclude_from_validation = ['desk', 'meeting_type', 'primary_event']
|
||||
|
||||
content = self.cleaned_data['events_csv_file'].read()
|
||||
if b'\0' in content:
|
||||
|
@ -1234,49 +1258,118 @@ class ImportEventsForm(forms.Form):
|
|||
except csv.Error:
|
||||
dialect = None
|
||||
|
||||
events = []
|
||||
warnings = {}
|
||||
events_by_slug = {x.slug: x for x in Event.objects.filter(agenda=self.agenda.pk)}
|
||||
event_ids_with_bookings = set(
|
||||
errors = []
|
||||
self.events = []
|
||||
self.warnings = {}
|
||||
self.events_by_slug = {x.slug: x for x in Event.objects.filter(agenda=self.agenda.pk)}
|
||||
self.event_ids_with_bookings = set(
|
||||
Booking.objects.filter(
|
||||
event__agenda=self.agenda.pk, cancellation_datetime__isnull=True
|
||||
).values_list('event_id', flat=True)
|
||||
)
|
||||
seen_slugs = set(events_by_slug.keys())
|
||||
self.seen_slugs = set(self.events_by_slug.keys())
|
||||
line_offset = 1
|
||||
for i, csvline in enumerate(csv.reader(StringIO(content), dialect=dialect)):
|
||||
if not csvline:
|
||||
continue
|
||||
if len(csvline) < 3:
|
||||
raise ValidationErrorWithOrdinal(_('Invalid file format. ({event_no} event)'), i)
|
||||
|
||||
if i == 0 and csvline[0].strip('#') in ('date', 'Date', _('date'), _('Date')):
|
||||
ValidationErrorWithOrdinal.line_offset = 0
|
||||
line_offset = 0
|
||||
continue
|
||||
|
||||
# label needed to generate a slug
|
||||
label = None
|
||||
if len(csvline) >= 5:
|
||||
label = force_str(csvline[4])
|
||||
try:
|
||||
event = self.parse_csvline(csvline)
|
||||
except ValidationError as e:
|
||||
for error in getattr(e, 'error_list', [e]):
|
||||
errors.append(
|
||||
format_html(
|
||||
'{message} ({event_no} event)',
|
||||
message=error.message,
|
||||
event_no=mark_safe(ordinal(i + line_offset)),
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.events.append(event)
|
||||
|
||||
# get or create event
|
||||
event = None
|
||||
slug = None
|
||||
if len(csvline) >= 6:
|
||||
slug = force_str(csvline[5]) if csvline[5] else None
|
||||
# get existing event if relevant
|
||||
if slug and slug in seen_slugs:
|
||||
event = events_by_slug[slug]
|
||||
# update label
|
||||
event.label = label
|
||||
if event is None:
|
||||
# new event
|
||||
event = Event(agenda_id=self.agenda.pk, label=label)
|
||||
# generate a slug if not provided
|
||||
event.slug = slug or generate_slug(event, seen_slugs=seen_slugs, agenda=self.agenda.pk)
|
||||
# maintain caches
|
||||
seen_slugs.add(event.slug)
|
||||
events_by_slug[event.slug] = event
|
||||
if errors:
|
||||
errors = [_('Invalid file format:')] + errors
|
||||
raise ValidationError(errors)
|
||||
|
||||
def parse_csvline(self, csvline):
|
||||
if len(csvline) < 3:
|
||||
raise ValidationError(_('Not enough columns.'))
|
||||
|
||||
# label needed to generate a slug
|
||||
label = None
|
||||
if len(csvline) >= 5:
|
||||
label = force_str(csvline[4])
|
||||
|
||||
# get or create event
|
||||
event = None
|
||||
slug = None
|
||||
if len(csvline) >= 6:
|
||||
slug = force_str(csvline[5]) if csvline[5] else None
|
||||
# get existing event if relevant
|
||||
if slug and slug in self.seen_slugs:
|
||||
event = self.events_by_slug[slug]
|
||||
# update label
|
||||
event.label = label
|
||||
if event is None:
|
||||
# new event
|
||||
event = Event(agenda_id=self.agenda.pk, label=label)
|
||||
# generate a slug if not provided
|
||||
event.slug = slug or generate_slug(event, seen_slugs=self.seen_slugs, agenda=self.agenda.pk)
|
||||
# maintain caches
|
||||
self.seen_slugs.add(event.slug)
|
||||
self.events_by_slug[event.slug] = event
|
||||
|
||||
for datetime_fmt in (
|
||||
'%Y-%m-%d %H:%M',
|
||||
'%d/%m/%Y %H:%M',
|
||||
'%d/%m/%Y %Hh%M',
|
||||
'%Y-%m-%d %H:%M:%S',
|
||||
'%d/%m/%Y %H:%M:%S',
|
||||
):
|
||||
try:
|
||||
event_datetime = make_aware(
|
||||
datetime.datetime.strptime('%s %s' % tuple(csvline[:2]), datetime_fmt)
|
||||
)
|
||||
except ValueError:
|
||||
continue
|
||||
if (
|
||||
event.pk is not None
|
||||
and event.start_datetime != event_datetime
|
||||
and event.start_datetime > now()
|
||||
and event.pk in self.event_ids_with_bookings
|
||||
and event.pk not in self.warnings
|
||||
):
|
||||
# event start datetime has changed, event is not past and has not cancelled bookings
|
||||
# => warn the user
|
||||
self.warnings[event.pk] = event
|
||||
event.start_datetime = event_datetime
|
||||
break
|
||||
else:
|
||||
raise ValidationError(_('Wrong start date/time format.'))
|
||||
try:
|
||||
event.places = int(csvline[2])
|
||||
except ValueError:
|
||||
raise ValidationError(_('Number of places must be an integer.'))
|
||||
if len(csvline) >= 4:
|
||||
try:
|
||||
event.waiting_list_places = int(csvline[3])
|
||||
except ValueError:
|
||||
raise ValidationError(_('Number of places in waiting list must be an integer.'))
|
||||
|
||||
column_index = 7
|
||||
for more_attr in ('description', 'pricing', 'url'):
|
||||
if len(csvline) >= column_index:
|
||||
setattr(event, more_attr, csvline[column_index - 1])
|
||||
column_index += 1
|
||||
|
||||
if len(csvline) >= 10 and csvline[9]: # publication date is optional
|
||||
for datetime_fmt in (
|
||||
'%Y-%m-%d',
|
||||
'%d/%m/%Y',
|
||||
'%Y-%m-%d %H:%M',
|
||||
'%d/%m/%Y %H:%M',
|
||||
'%d/%m/%Y %Hh%M',
|
||||
|
@ -1284,93 +1377,39 @@ class ImportEventsForm(forms.Form):
|
|||
'%d/%m/%Y %H:%M:%S',
|
||||
):
|
||||
try:
|
||||
event_datetime = make_aware(
|
||||
datetime.datetime.strptime('%s %s' % tuple(csvline[:2]), datetime_fmt)
|
||||
event.publication_datetime = make_aware(
|
||||
datetime.datetime.strptime(csvline[9], datetime_fmt)
|
||||
)
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
if (
|
||||
event.pk is not None
|
||||
and event.start_datetime != event_datetime
|
||||
and event.start_datetime > now()
|
||||
and event.pk in event_ids_with_bookings
|
||||
and event.pk not in warnings
|
||||
):
|
||||
# event start datetime has changed, event is not past and has not cancelled bookings
|
||||
# => warn the user
|
||||
warnings[event.pk] = event
|
||||
event.start_datetime = event_datetime
|
||||
break
|
||||
else:
|
||||
raise ValidationErrorWithOrdinal(
|
||||
_('Invalid file format. (date/time format, {event_no} event)'), i
|
||||
)
|
||||
try:
|
||||
event.places = int(csvline[2])
|
||||
except ValueError:
|
||||
raise ValidationError(_('Invalid file format. (number of places, {event_no} event)'), i)
|
||||
if len(csvline) >= 4:
|
||||
raise ValidationError(_('Wrong publication date/time format.'))
|
||||
|
||||
if self.agenda.partial_bookings:
|
||||
if len(csvline) < 11 or not csvline[10]:
|
||||
raise ValidationError(_('Missing end_time.'))
|
||||
event.end_time = csvline[10]
|
||||
else:
|
||||
self.exclude_from_validation.append('end_time')
|
||||
if len(csvline) >= 11 and csvline[10]: # duration is optional
|
||||
try:
|
||||
event.waiting_list_places = int(csvline[3])
|
||||
event.duration = int(csvline[10])
|
||||
except ValueError:
|
||||
raise ValidationError(
|
||||
_('Invalid file format. (number of places in waiting list, {event_no} event)'), i
|
||||
)
|
||||
raise ValidationError(_('Duration must be an integer.'))
|
||||
|
||||
column_index = 7
|
||||
for more_attr in ('description', 'pricing', 'url'):
|
||||
if len(csvline) >= column_index:
|
||||
setattr(event, more_attr, csvline[column_index - 1])
|
||||
column_index += 1
|
||||
try:
|
||||
event.full_clean(exclude=self.exclude_from_validation)
|
||||
except ValidationError as e:
|
||||
errors = []
|
||||
for label, field_errors in e.message_dict.items():
|
||||
label_name = self.get_verbose_name(label)
|
||||
msg = _('%s: ') % label_name if label_name else ''
|
||||
msg += ', '.join(field_errors)
|
||||
errors.append(msg)
|
||||
raise ValidationError(errors)
|
||||
|
||||
if len(csvline) >= 10 and csvline[9]: # publication date is optional
|
||||
for datetime_fmt in (
|
||||
'%Y-%m-%d',
|
||||
'%d/%m/%Y',
|
||||
'%Y-%m-%d %H:%M',
|
||||
'%d/%m/%Y %H:%M',
|
||||
'%d/%m/%Y %Hh%M',
|
||||
'%Y-%m-%d %H:%M:%S',
|
||||
'%d/%m/%Y %H:%M:%S',
|
||||
):
|
||||
try:
|
||||
event.publication_datetime = make_aware(
|
||||
datetime.datetime.strptime(csvline[9], datetime_fmt)
|
||||
)
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
raise ValidationError(_('Invalid file format. (date/time format, {event_no} event)'), i)
|
||||
|
||||
if self.agenda.partial_bookings:
|
||||
if len(csvline) < 11 or not csvline[10]:
|
||||
raise ValidationError(_('Invalid file format. (missing end_time, {event_no} event)'), i)
|
||||
event.end_time = csvline[10]
|
||||
else:
|
||||
exclude_from_validation.append('end_time')
|
||||
if len(csvline) >= 11 and csvline[10]: # duration is optional
|
||||
try:
|
||||
event.duration = int(csvline[10])
|
||||
except ValueError:
|
||||
raise ValidationError(_('Invalid file format. (duration, {event_no} event)'), i)
|
||||
|
||||
try:
|
||||
event.full_clean(exclude=exclude_from_validation)
|
||||
except ValidationError as e:
|
||||
errors = [_('Invalid file format:\n')]
|
||||
for label, field_errors in e.message_dict.items():
|
||||
label_name = self.get_verbose_name(label)
|
||||
msg = _('%s: ') % label_name if label_name else ''
|
||||
msg += _('%(errors)s (line %(line)d)') % {
|
||||
'errors': ', '.join(field_errors),
|
||||
'line': i + 1,
|
||||
}
|
||||
errors.append(msg)
|
||||
raise ValidationError(errors)
|
||||
events.append(event)
|
||||
self.events = events
|
||||
self.warnings = warnings
|
||||
return event
|
||||
|
||||
@staticmethod
|
||||
def get_verbose_name(field_name):
|
||||
|
@ -1504,9 +1543,12 @@ class AgendaDisplaySettingsForm(forms.ModelForm):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if kwargs['instance'].kind == 'events':
|
||||
self.fields['booking_user_block_template'].help_text = (
|
||||
_('Displayed for each booking in event page and check page'),
|
||||
)
|
||||
if self.instance.partial_bookings:
|
||||
del self.fields['booking_user_block_template']
|
||||
else:
|
||||
self.fields['booking_user_block_template'].help_text = (
|
||||
_('Displayed for each booking in event page and check page'),
|
||||
)
|
||||
else:
|
||||
self.fields['booking_user_block_template'].help_text = (
|
||||
_('Displayed for each booking in agenda view pages'),
|
||||
|
@ -1526,6 +1568,26 @@ class AgendaBookingCheckSettingsForm(forms.ModelForm):
|
|||
]
|
||||
widgets = {'booking_extra_user_block_template': forms.Textarea(attrs={'rows': 3})}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance.partial_bookings:
|
||||
del self.fields['enable_check_for_future_events']
|
||||
del self.fields['booking_extra_user_block_template']
|
||||
|
||||
|
||||
class AgendaInvoicingSettingsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Agenda
|
||||
fields = [
|
||||
'invoicing_unit',
|
||||
'invoicing_tolerance',
|
||||
]
|
||||
|
||||
def save(self):
|
||||
super().save()
|
||||
self.instance.async_refresh_booking_computed_times()
|
||||
return self.instance
|
||||
|
||||
|
||||
class AgendaNotificationsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
|
|
@ -420,7 +420,8 @@ div.event-title-meta span.tag {
|
|||
color: white;
|
||||
}
|
||||
|
||||
div.ui-dialog form p span.datetime input {
|
||||
div.ui-dialog form p span.datetime input,
|
||||
div.ui-dialog form input[type=time] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
@ -599,6 +600,10 @@ div#appbar a.active {
|
|||
}
|
||||
|
||||
// Partial booking view
|
||||
div#main-content.partial-booking-dayview {
|
||||
// change default overflow to allow sticky hours list
|
||||
overflow: visible;
|
||||
}
|
||||
.partial-booking {
|
||||
--registrant-name-width: 15rem;
|
||||
--zebra-color: hsla(0,0%,0%,0.05);
|
||||
|
@ -657,6 +662,7 @@ div#appbar a.active {
|
|||
&--datas {
|
||||
box-sizing: border-box;
|
||||
flex: 1 0 100%;
|
||||
padding: .33rem 0;
|
||||
@media (min-width: 761px) {
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
@ -673,24 +679,35 @@ div#appbar a.active {
|
|||
}
|
||||
&--bar-container {
|
||||
position: relative;
|
||||
padding: .33rem 0;
|
||||
margin: 0.33rem 0;
|
||||
}
|
||||
&--bar {
|
||||
--color: white;
|
||||
box-sizing: border-box;
|
||||
margin: 0.33rem 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
padding: 0.33em 0.66em;
|
||||
background-color: var(--background);
|
||||
color: var(--color);
|
||||
&:not(:first-child) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
.end-time {
|
||||
float: right;
|
||||
margin-left: .66em;
|
||||
}
|
||||
&.booking {
|
||||
--background: #1066bc;
|
||||
}
|
||||
&.check.present {
|
||||
--background: hsl(120, 57%, 35%);
|
||||
&.check.present, &.computed.present {
|
||||
--background: var(--green);
|
||||
}
|
||||
&.check.absent {
|
||||
--background: hsl(355, 80%, 45%);
|
||||
&.check.absent, &.computed.absent {
|
||||
--background: var(--red);
|
||||
}
|
||||
&.computed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,5 +22,6 @@
|
|||
<button>{% trans 'Set Date' %}</button>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% block extra_date_title %}{% endblock %}
|
||||
</h2>
|
||||
{% endblock %}
|
||||
|
|
|
@ -21,7 +21,9 @@
|
|||
<ul class="extra-actions-menu">
|
||||
<li><a rel="popup" href="{% url 'chrono-manager-agenda-edit' pk=object.id %}">{% trans 'Options' %}</a></li>
|
||||
{% block agenda-extra-menu-actions %}{% endblock %}
|
||||
<li><a rel="popup" class="action-duplicate" href="{% url 'chrono-manager-agenda-duplicate' pk=object.pk %}">{% trans 'Duplicate' %}</a></li>
|
||||
{% if user.is_staff %}
|
||||
<li><a rel="popup" class="action-duplicate" href="{% url 'chrono-manager-agenda-duplicate' pk=object.pk %}">{% trans 'Duplicate' %}</a></li>
|
||||
{% endif %}
|
||||
<li><a download href="{% url 'chrono-manager-agenda-export' pk=object.id %}">{% trans 'Export Configuration (JSON)' %}</a></li>
|
||||
{% if object.kind == 'events' %}
|
||||
<li><a download href="{% url 'chrono-manager-agenda-export-events' pk=object.pk %}">{% trans 'Export Events (CSV)' %}</a></li>
|
||||
|
@ -39,7 +41,7 @@
|
|||
<div class="pk-tabs">
|
||||
<div class="pk-tabs--tab-list" role="tablist">
|
||||
{% block agenda-settings-extra-tab-buttons %}{% endblock %}
|
||||
{% if object.kind != 'virtual' %}
|
||||
{% if object.kind != 'virtual' and not object.partial_bookings %}
|
||||
<button aria-controls="panel-reminders" aria-selected="false" id="tab-reminders" role="tab" tabindex="-1">{% trans "Booking reminders" %}</button>
|
||||
{% endif %}
|
||||
<button aria-controls="panel-delays" aria-selected="false" id="tab-delays" role="tab" tabindex="-1">{% trans "Booking Delays" %}</button>
|
||||
|
@ -49,7 +51,7 @@
|
|||
|
||||
{% block agenda-settings-extra-tab-list %}{% endblock %}
|
||||
|
||||
{% if object.kind != 'virtual' %}
|
||||
{% if object.kind != 'virtual' and not object.partial_bookings %}
|
||||
<div aria-labelledby="tab-reminders" id="panel-reminders" role="tabpanel" tabindex="0" hidden="">
|
||||
{% for info in agenda.reminder_settings.display_info %}
|
||||
<p>{{ info }}</p>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
</form>
|
||||
<table class="main check-bookings">
|
||||
<tbody>
|
||||
{% if results and not event.checked %}
|
||||
{% if results and not event.checked and not event.check_locked %}
|
||||
<tr class="booking">
|
||||
<td class="booking-actions">
|
||||
<form method="post" action="{% url 'chrono-manager-event-checked' pk=agenda.pk event_pk=object.pk %}">
|
||||
|
|
|
@ -4,19 +4,19 @@
|
|||
{% if agenda.booking_extra_user_block_template %}<span class="togglable"></span>{% endif %}
|
||||
{{ booking.get_user_block }}{% if booking.places_count > 1 %} ({{ booking.places_count }} {% trans "places" %}){% endif %}
|
||||
</td>
|
||||
<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 }}">
|
||||
<td class="booking-status {% if booking.kind != "subscription" and booking.cancellation_datetime is None and booking.user_check %}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 }})
|
||||
{% if booking.user_check %}{{ booking.user_check.presence|yesno:_('Present,Absent') }}{% else %}-{% endif %}
|
||||
{% if 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 and not event.check_locked %}
|
||||
{% if booking.user_check and not event.check_locked %}
|
||||
<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>
|
||||
|
@ -43,7 +43,7 @@
|
|||
{% endif %}
|
||||
{% csrf_token %}
|
||||
<button class="submit-button"
|
||||
{% if booking.user_was_present is True %}disabled{% endif %}
|
||||
{% if booking.user_check.presence %}disabled{% endif %}
|
||||
>{% trans "Presence" %}</button>
|
||||
{% if booking.presence_form.check_type.field.choices.1 %}{{ booking.presence_form.check_type }}{% endif %}
|
||||
<script>
|
||||
|
@ -62,7 +62,7 @@
|
|||
{% endif %}
|
||||
{% csrf_token %}
|
||||
<button class="submit-button"
|
||||
{% if booking.user_was_present is False %}disabled{% endif %}
|
||||
{% if booking.user_check.presence is False %}disabled{% endif %}
|
||||
>{% trans "Absence" %}</button>
|
||||
{% if booking.absence_form.check_type.field.choices.1 %}{{ booking.absence_form.check_type }}{% endif %}
|
||||
<script>
|
||||
|
|
|
@ -19,7 +19,11 @@
|
|||
{% endif %}
|
||||
<button aria-controls="panel-display-options" aria-selected="false" id="tab-display-options" role="tab" tabindex="-1">{% trans "Display options" %}</button>
|
||||
<button aria-controls="panel-booking-check-options" aria-selected="false" id="tab-booking-check-options" role="tab" tabindex="-1">{% trans "Booking check options" %}</button>
|
||||
<button aria-controls="panel-notifications" aria-selected="false" id="tab-notifications" role="tab" tabindex="-1">{% trans "Management notifications" %}</button>
|
||||
{% if agenda.partial_bookings %}
|
||||
<button aria-controls="panel-invoicing-options" aria-selected="false" id="tab-invoicing-options" role="tab" tabindex="-1">{% trans "Invoicing options" %}</button>
|
||||
{% else %}
|
||||
<button aria-controls="panel-notifications" aria-selected="false" id="tab-notifications" role="tab" tabindex="-1">{% trans "Management notifications" %}</button>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block agenda-settings-extra-tab-list %}
|
||||
|
@ -86,10 +90,12 @@
|
|||
{% trans "No event display template configured for this agenda." %}
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Booking display template:" %}
|
||||
{% if not agenda.partial_bookings %}
|
||||
<li>
|
||||
{% trans "Booking display template:" %}
|
||||
<pre>{{ agenda.get_booking_user_block_template }}</pre>
|
||||
</li>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="panel--buttons">
|
||||
<a rel="popup" class="button" href="{% url 'chrono-manager-agenda-display-settings' pk=object.pk %}">{% trans 'Configure' %}</a>
|
||||
|
@ -113,32 +119,48 @@
|
|||
{% 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>
|
||||
<li>
|
||||
{% trans "Extra user block template:" %}
|
||||
{% if not agenda.partial_bookings %}
|
||||
<li>{% trans "Enable the check of bookings when event has not passed:" %} {{ agenda.enable_check_for_future_events|yesno }}</li>
|
||||
<li>
|
||||
{% trans "Extra user block template:" %}
|
||||
<pre>{{ agenda.booking_extra_user_block_template }}</pre>
|
||||
</li>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="panel--buttons">
|
||||
<a rel="popup" class="button" href="{% url 'chrono-manager-agenda-booking-check-settings' pk=object.pk %}">{% trans 'Configure' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div aria-labelledby="tab-notifications" hidden="" id="panel-notifications" role="tabpanel" tabindex="0">
|
||||
{% for notification_type in object.notifications_settings.get_notification_types %}
|
||||
{% if forloop.first %}<ul>{% endif %}
|
||||
<li>
|
||||
{% blocktrans trimmed with display_value=notification_type.display_value label=notification_type.label %}
|
||||
{{ label }}: {{ display_value }} will be notified.
|
||||
{% endblocktrans %}
|
||||
</li>
|
||||
{% if forloop.last %}</ul>{% endif %}
|
||||
{% empty %}
|
||||
<p>{% trans "Notifications are disabled for this agenda." %}</p>
|
||||
{% endfor %}
|
||||
<div class="panel--buttons">
|
||||
<a rel="popup" class="button" href="{% url 'chrono-manager-agenda-notifications-settings' pk=object.id %}">{% trans 'Configure' %}</a>
|
||||
{% if agenda.partial_bookings %}
|
||||
<div aria-labelledby="tab-invoicing-options" hidden="" id="panel-invoicing-options" role="tabpanel" tabindex="0">
|
||||
<ul>
|
||||
<li>{% trans "Invoicing:" %} {{ agenda.get_invoicing_unit_display }}</li>
|
||||
<li>{% trans "Tolerance:" %} {% blocktrans count num=agenda.invoicing_tolerance %}{{ num }} minute{% plural %}{{ num }} minutes{% endblocktrans %}</li>
|
||||
</ul>
|
||||
<div class="panel--buttons">
|
||||
<a rel="popup" class="button" href="{% url 'chrono-manager-agenda-invoicing-settings' pk=object.pk %}">{% trans 'Configure' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not agenda.partial_bookings %}
|
||||
<div aria-labelledby="tab-notifications" hidden="" id="panel-notifications" role="tabpanel" tabindex="0">
|
||||
{% for notification_type in object.notifications_settings.get_notification_types %}
|
||||
{% if forloop.first %}<ul>{% endif %}
|
||||
<li>
|
||||
{% blocktrans trimmed with display_value=notification_type.display_value label=notification_type.label %}
|
||||
{{ label }}: {{ display_value }} will be notified.
|
||||
{% endblocktrans %}
|
||||
</li>
|
||||
{% if forloop.last %}</ul>{% endif %}
|
||||
{% empty %}
|
||||
<p>{% trans "Notifications are disabled for this agenda." %}</p>
|
||||
{% endfor %}
|
||||
<div class="panel--buttons">
|
||||
<a rel="popup" class="button" href="{% url 'chrono-manager-agenda-notifications-settings' pk=object.id %}">{% trans 'Configure' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -11,7 +11,22 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<form
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
data-fill-start_time="{{ object.booking.start_time|time:"H:i" }}"
|
||||
data-fill-end_time="{{ object.booking.end_time|time:"H:i" }}"
|
||||
>
|
||||
{% if allow_adding_check %}
|
||||
<p>
|
||||
<a
|
||||
rel="popup"
|
||||
href="{% url 'chrono-manager-partial-booking-check' pk=agenda.pk booking_pk=object.booking.pk %}"
|
||||
>
|
||||
{% trans "Add second booking check" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
{{ form|with_template }}
|
||||
<div class="buttons">
|
||||
|
@ -23,7 +38,7 @@
|
|||
$(function () {
|
||||
presence_check_type_select = $('.widget[id=id_presence_check_type_p]');
|
||||
absence_check_type_select = $('.widget[id=id_absence_check_type_p]');
|
||||
$('input[type=radio][name=user_was_present]').change(function() {
|
||||
$('input[type=radio][name=presence]').change(function() {
|
||||
if (!this.checked)
|
||||
return;
|
||||
if (this.value == 'True') {
|
||||
|
@ -37,6 +52,12 @@
|
|||
absence_check_type_select.hide();
|
||||
}
|
||||
}).change();
|
||||
|
||||
$('.time-widget-button').on('click', function() {
|
||||
var widget_name = $(this).data('related-widget');
|
||||
var value = $(this).parents('form').data('fill-' + widget_name);
|
||||
$('[name="' + widget_name + '"]').val(value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</form>
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
{% extends "chrono/manager_agenda_day_view.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block main-content-attributes %}class="partial-booking-dayview"{% endblock %}
|
||||
|
||||
{% block extra_date_title %}
|
||||
{% if event.invoiced %}
|
||||
<span class="invoiced tag">{% trans "Invoiced" %}</span>
|
||||
{% elif event.check_locked %}
|
||||
<span class="check-locked tag">{% trans "Check locked" %}</span>
|
||||
{% endif %}
|
||||
{% if event.checked %}<span class="checked tag">{% trans "Checked" %}</span>{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if not hours %}
|
||||
|
@ -20,6 +31,13 @@
|
|||
</script>
|
||||
</form>
|
||||
|
||||
{% 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>
|
||||
{% endif %}
|
||||
|
||||
<div class="partial-booking" style="--nb-hours: {{ hours|length }}">
|
||||
<div class="partial-booking--hours-list" aria-hidden="true">
|
||||
{% for hour in hours %}
|
||||
|
@ -28,48 +46,77 @@
|
|||
</div>
|
||||
|
||||
<div class="partial-booking--registrant-items">
|
||||
{% for booking in results %}
|
||||
{% for user in users %}
|
||||
<section class="partial-booking--registrant">
|
||||
{% spaceless %}
|
||||
<h3 class="registrant--name">
|
||||
{% if booking.kind == "booking" and allow_check %}
|
||||
{% if allow_check and user.check_url %}
|
||||
<a
|
||||
rel="popup"
|
||||
href="{% url 'chrono-manager-partial-booking-check' pk=agenda.pk booking_pk=booking.pk %}"
|
||||
>{{ booking.get_user_block }}</a>
|
||||
href="{{ user.check_url }}"
|
||||
>{{ user.name }}</a>
|
||||
{% else %}
|
||||
<span>{{ booking.get_user_block }}</span>
|
||||
<span>{{ user.name }}</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
{% endspaceless %}
|
||||
<div class="registrant--datas">
|
||||
{% if booking.kind == "booking" %}
|
||||
<div class="registrant--bar-container">
|
||||
{% for booking in user.bookings %}
|
||||
{% if booking.start_time %}
|
||||
<a
|
||||
class="registrant--bar clearfix booking"
|
||||
title="{% trans "Booked period" %}"
|
||||
style="left: {{ booking.css_left }}%; width: {{ booking.css_width }}%;"
|
||||
{% if allow_check and not booking.user_check %}
|
||||
rel="popup"
|
||||
href="{% url 'chrono-manager-partial-booking-check' pk=agenda.pk booking_pk=booking.pk %}"
|
||||
{% endif %}
|
||||
>
|
||||
<strong class="sr-only">{% trans "Booked period:" %}</strong>
|
||||
<time class="start-time" datetime="{{ booking.start_time|time:"H:i" }}">{{ booking.start_time|time:"H:i" }}</time>
|
||||
<time class="end-time" datetime="{{ booking.end_time|time:"H:i" }}">{{ booking.end_time|time:"H:i" }}</time>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if user.bookings %}
|
||||
<div class="registrant--bar-container">
|
||||
<p
|
||||
class="registrant--bar booking"
|
||||
title="{% trans "Booked period" %}"
|
||||
style="left: {{ booking.css_left }}%; width: {{ booking.css_width }}%;"
|
||||
>
|
||||
<strong class="sr-only">{% trans "Booked period:" %}</strong>
|
||||
<time datetime="{{ booking.start_time|time:"H:i" }}">{{ booking.start_time|time:"H:i" }}</time>
|
||||
–
|
||||
<time datetime="{{ booking.end_time|time:"H:i" }}">{{ booking.end_time|time:"H:i" }}</time>
|
||||
</p>
|
||||
{% for check in user.booking_checks %}
|
||||
<a
|
||||
class="registrant--bar clearfix check {{ check.css_class }}"
|
||||
title="{% trans "Checked period:" %}"
|
||||
style="left: {{ check.css_left }}%; width: {{ check.css_width }}%;"
|
||||
{% if allow_check %}
|
||||
rel="popup"
|
||||
href="{% url 'chrono-manager-partial-booking-update-check' pk=agenda.pk check_pk=check.pk %}"
|
||||
{% endif %}
|
||||
>
|
||||
<strong class="sr-only">{% trans "Checked period:" %}</strong>
|
||||
{% if check.start_time %}
|
||||
<time class="start-time" datetime="{{ check.start_time|time:"H:i" }}">{{ check.start_time|time:"H:i" }}</time>
|
||||
{% endif %}
|
||||
{% if check.end_time %}
|
||||
<time class="end-time" datetime="{{ check.end_time|time:"H:i" }}">{{ check.end_time|time:"H:i" }}</time>
|
||||
{% endif %}
|
||||
{% if check.type_label %}<span>{{ check.type_label }}</span>{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if booking.user_was_present is not None %}
|
||||
<div class="registrant--bar-container">
|
||||
<p
|
||||
class="registrant--bar check {{ booking.check_css_class }}"
|
||||
title="{% trans "Checked period:" %}"
|
||||
style="left: {{ booking.check_css_left }}%; width: {{ booking.check_css_width }}%;"
|
||||
>
|
||||
<strong class="sr-only">{% trans "Checked period:" %}</strong>
|
||||
<time datetime="{{ booking.user_check_start_time|time:"H:i" }}">{{ booking.user_check_start_time|time:"H:i" }}</time>
|
||||
{% if booking.user_check_start_time and booking.user_check_end_time %}–{% endif %}
|
||||
<time datetime="{{ booking.user_check_end_time|time:"H:i" }}">{{ booking.user_check_end_time|time:"H:i" }}</time>
|
||||
{% if booking.user_check_type_label %}<span>{{ booking.user_check_type_label }}</span>{% endif %}
|
||||
</p>
|
||||
{% for check in user.booking_checks %}
|
||||
{% if check.computed_start_time and check.computed_end_time %}
|
||||
<p
|
||||
class="registrant--bar clearfix computed {{ check.css_class }}"
|
||||
title="{% trans "Computed period" %}"
|
||||
style="left: {{ check.computed_css_left }}%; width: {{ check.computed_css_width }}%;"
|
||||
>
|
||||
<strong class="sr-only">{% trans "Computed period:" %}</strong>
|
||||
<time class="start-time" datetime="{{ check.computed_start_time|time:"H:i" }}">{{ check.computed_start_time|time:"H:i" }}</time>
|
||||
<time class="end-time" datetime="{{ check.computed_end_time|time:"H:i" }}">{{ check.computed_end_time|time:"H:i" }}</time>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{% load i18n %}{% include "django/forms/widgets/input.html" %} <button type="button" class="time-widget-button" data-related-widget="{{ widget.name }}">{{ widget.button_label }}</button>
|
|
@ -170,6 +170,11 @@ urlpatterns = [
|
|||
views.agenda_booking_check_settings,
|
||||
name='chrono-manager-agenda-booking-check-settings',
|
||||
),
|
||||
path(
|
||||
'agendas/<int:pk>/invoicing-options',
|
||||
views.agenda_invoicing_settings,
|
||||
name='chrono-manager-agenda-invoicing-settings',
|
||||
),
|
||||
path('agendas/<int:pk>/delete', views.agenda_delete, name='chrono-manager-agenda-delete'),
|
||||
path('agendas/<int:pk>/export', views.agenda_export, name='chrono-manager-agenda-export'),
|
||||
path('agendas/<int:pk>/add-event', views.agenda_add_event, name='chrono-manager-agenda-add-event'),
|
||||
|
@ -434,6 +439,16 @@ urlpatterns = [
|
|||
views.partial_booking_check_view,
|
||||
name='chrono-manager-partial-booking-check',
|
||||
),
|
||||
path(
|
||||
'agendas/<int:pk>/booking-checks/<int:check_pk>',
|
||||
views.partial_booking_update_check_view,
|
||||
name='chrono-manager-partial-booking-update-check',
|
||||
),
|
||||
path(
|
||||
'agendas/<int:pk>/subscriptions/<int:subscription_pk>/check/<int:event_pk>',
|
||||
views.partial_booking_subscription_check_view,
|
||||
name='chrono-manager-partial-booking-subscription-check',
|
||||
),
|
||||
path(
|
||||
'agendas/<int:pk>/subscriptions/<int:subscription_pk>/extra-user-block',
|
||||
views.subscription_extra_user_block,
|
||||
|
|
|
@ -67,6 +67,7 @@ from chrono.agendas.models import (
|
|||
AgendaNotificationsSettings,
|
||||
AgendaReminderSettings,
|
||||
Booking,
|
||||
BookingCheck,
|
||||
BookingColor,
|
||||
Category,
|
||||
Desk,
|
||||
|
@ -98,6 +99,7 @@ from .forms import (
|
|||
AgendaDisplaySettingsForm,
|
||||
AgendaDuplicateForm,
|
||||
AgendaEditForm,
|
||||
AgendaInvoicingSettingsForm,
|
||||
AgendaNotificationsForm,
|
||||
AgendaReminderForm,
|
||||
AgendaReminderTestForm,
|
||||
|
@ -1163,6 +1165,18 @@ class AgendaBookingCheckSettingsView(AgendaEditView):
|
|||
agenda_booking_check_settings = AgendaBookingCheckSettingsView.as_view()
|
||||
|
||||
|
||||
class AgendaInvoicingSettingsView(AgendaEditView):
|
||||
form_class = AgendaInvoicingSettingsForm
|
||||
title = _('Configure invoicing options')
|
||||
tab_anchor = 'invoicing-options'
|
||||
|
||||
def set_agenda(self, **kwargs):
|
||||
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events', partial_bookings=True)
|
||||
|
||||
|
||||
agenda_invoicing_settings = AgendaInvoicingSettingsView.as_view()
|
||||
|
||||
|
||||
class AgendaDeleteView(DeleteView):
|
||||
template_name = 'chrono/manager_confirm_delete.html'
|
||||
model = Agenda
|
||||
|
@ -1324,7 +1338,7 @@ class AgendaDateView(DateMixin, ViewableAgendaMixin):
|
|||
except ValueError: # no meeting types defined
|
||||
context['hour_span'] = 1
|
||||
context['booking_colors'] = BookingColor.objects.filter(
|
||||
bookings__event__in=self.object_list
|
||||
bookings__event__in=self.object_list, bookings__cancellation_datetime__isnull=True
|
||||
).distinct()
|
||||
context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
|
||||
return context
|
||||
|
@ -1380,12 +1394,12 @@ class EventChecksMixin:
|
|||
filters = {k: sorted(list(v)) for k, v in filters}
|
||||
return filters
|
||||
|
||||
def add_filters_context(self, context, event):
|
||||
def add_filters_context(self, context, event, add_check_forms=True):
|
||||
# booking base queryset
|
||||
booking_qs_kwargs = {}
|
||||
if not self.agenda.subscriptions.exists():
|
||||
booking_qs_kwargs = {'cancellation_datetime__isnull': True}
|
||||
booking_qs = event.booking_set
|
||||
booking_qs = event.booking_set.prefetch_related('user_checks').order_by('start_time')
|
||||
booked_qs = booking_qs.filter(
|
||||
in_waiting_list=False, primary_booking__isnull=True, **booking_qs_kwargs
|
||||
)
|
||||
|
@ -1427,27 +1441,35 @@ class EventChecksMixin:
|
|||
results = []
|
||||
booked_without_status = False
|
||||
for booking in booked_filterset.qs:
|
||||
if booking.cancellation_datetime is None and booking.user_was_present is None:
|
||||
booking.kind = 'booking'
|
||||
results.append(booking)
|
||||
|
||||
if not add_check_forms:
|
||||
continue
|
||||
|
||||
if booking.cancellation_datetime is None and not booking.user_check:
|
||||
booked_without_status = True
|
||||
booking.absence_form = BookingCheckAbsenceForm(
|
||||
agenda=self.agenda,
|
||||
initial={'check_type': booking.user_check_type_slug},
|
||||
initial={'check_type': booking.user_check.type_slug if booking.user_check else None},
|
||||
)
|
||||
booking.presence_form = BookingCheckPresenceForm(
|
||||
agenda=self.agenda,
|
||||
initial={'check_type': booking.user_check_type_slug},
|
||||
initial={'check_type': booking.user_check.type_slug if booking.user_check else None},
|
||||
)
|
||||
booking.kind = 'booking'
|
||||
results.append(booking)
|
||||
for subscription in subscription_filterset.qs:
|
||||
subscription.kind = 'subscription'
|
||||
results.append(subscription)
|
||||
|
||||
if not add_check_forms:
|
||||
continue
|
||||
|
||||
subscription.absence_form = BookingCheckAbsenceForm(
|
||||
agenda=self.agenda,
|
||||
)
|
||||
subscription.presence_form = BookingCheckPresenceForm(
|
||||
agenda=self.agenda,
|
||||
)
|
||||
subscription.kind = 'subscription'
|
||||
results.append(subscription)
|
||||
# sort results
|
||||
if (
|
||||
booked_filterset.form.is_valid()
|
||||
|
@ -1604,15 +1626,17 @@ class AgendaDayView(EventChecksMixin, AgendaDateView, DayArchiveView):
|
|||
|
||||
def fill_partial_bookings_context(self, context):
|
||||
try:
|
||||
event = self.agenda.event_set.get(start_datetime__date=self.date.date())
|
||||
event = self.agenda.event_set.get(
|
||||
start_datetime__date=self.date.date(), recurrence_days__isnull=True
|
||||
)
|
||||
except Event.DoesNotExist:
|
||||
return
|
||||
|
||||
context['allow_check'] = bool(
|
||||
self.agenda.enable_check_for_future_events
|
||||
or localtime(event.start_datetime).date() <= localtime().date()
|
||||
)
|
||||
self.add_filters_context(context, event)
|
||||
context['event'] = event
|
||||
context['allow_check'] = (
|
||||
not event.checked or not self.agenda.disable_check_update
|
||||
) and not event.check_locked
|
||||
self.add_filters_context(context, event, add_check_forms=False)
|
||||
|
||||
min_time = localtime(event.start_datetime).time()
|
||||
max_time = event.end_time
|
||||
|
@ -1632,22 +1656,52 @@ class AgendaDayView(EventChecksMixin, AgendaDateView, DayArchiveView):
|
|||
|
||||
bookings = [x for x in context['results'] if x.kind == 'booking']
|
||||
for booking in bookings:
|
||||
booking.css_left = get_time_ratio(booking.start_time, start_time)
|
||||
booking.css_width = get_time_ratio(booking.end_time, booking.start_time)
|
||||
if booking.start_time:
|
||||
booking.css_left = get_time_ratio(booking.start_time, start_time)
|
||||
booking.css_width = get_time_ratio(booking.end_time, booking.start_time)
|
||||
|
||||
if booking.user_was_present is not None:
|
||||
booking.check_css_class = 'present' if booking.user_was_present else 'absent'
|
||||
if booking.user_check_start_time and booking.user_check_end_time:
|
||||
booking.check_css_left = get_time_ratio(booking.user_check_start_time, start_time)
|
||||
booking.check_css_width = get_time_ratio(
|
||||
booking.user_check_end_time, booking.user_check_start_time
|
||||
)
|
||||
elif booking.user_check_start_time:
|
||||
booking.check_css_left = get_time_ratio(booking.user_check_start_time, start_time)
|
||||
booking.check_css_width = 1
|
||||
booking.user_check_list = list(booking.user_checks.all()) # queryset is prefetched
|
||||
for check in booking.user_check_list:
|
||||
check.css_class = 'present' if check.presence else 'absent'
|
||||
|
||||
if not check.start_time:
|
||||
check.css_class += ' end-only'
|
||||
check.css_left = booking.css_left
|
||||
check.css_width = booking.css_width
|
||||
elif not check.end_time:
|
||||
check.css_class += ' start-only'
|
||||
check.css_left = get_time_ratio(check.start_time, start_time)
|
||||
check.css_width = 4
|
||||
else:
|
||||
booking.check_css_left = get_time_ratio(booking.user_check_end_time, start_time) - 1
|
||||
booking.check_css_width = 1
|
||||
check.css_left = get_time_ratio(check.start_time, start_time)
|
||||
check.css_width = get_time_ratio(check.end_time, check.start_time)
|
||||
|
||||
if check.computed_start_time and check.computed_end_time:
|
||||
check.computed_css_left = get_time_ratio(check.computed_start_time, start_time)
|
||||
check.computed_css_width = get_time_ratio(
|
||||
check.computed_end_time, check.computed_start_time
|
||||
)
|
||||
|
||||
users_info = {}
|
||||
for result in context['results']:
|
||||
user_info = users_info.setdefault(
|
||||
result.user_external_id,
|
||||
{
|
||||
'name': result.user_name,
|
||||
'bookings': [],
|
||||
'booking_checks': [],
|
||||
},
|
||||
)
|
||||
if result.kind == 'subscription':
|
||||
user_info['check_url'] = reverse(
|
||||
'chrono-manager-partial-booking-subscription-check',
|
||||
kwargs={'pk': self.agenda.pk, 'event_pk': event.pk, 'subscription_pk': result.pk},
|
||||
)
|
||||
continue
|
||||
user_info['bookings'].append(result)
|
||||
user_info['booking_checks'].extend(result.user_check_list)
|
||||
|
||||
context['users'] = users_info.values()
|
||||
|
||||
|
||||
agenda_day_view = AgendaDayView.as_view()
|
||||
|
@ -1882,7 +1936,7 @@ class AgendaWeekMonthMixin:
|
|||
]
|
||||
|
||||
booking_info_by_user = {}
|
||||
bookings = Booking.objects.filter(event__in=self.events)
|
||||
bookings = Booking.objects.filter(event__in=self.events).prefetch_related('user_checks')
|
||||
for booking in bookings:
|
||||
booking_info = booking_info_by_user.setdefault(
|
||||
booking.user_external_id,
|
||||
|
@ -1895,8 +1949,8 @@ class AgendaWeekMonthMixin:
|
|||
)
|
||||
user_bookings = booking_info['bookings']
|
||||
|
||||
if booking.user_was_present is not None:
|
||||
booking.check_css_class = 'present' if booking.user_was_present else 'absent'
|
||||
if booking.user_check:
|
||||
booking.check_css_class = 'present' if booking.user_check.presence else 'absent'
|
||||
|
||||
user_bookings[localtime(booking.event.start_datetime).day - 1] = booking
|
||||
|
||||
|
@ -2238,10 +2292,16 @@ class AgendaExport(ManagedAgendaMixin, DetailView):
|
|||
agenda_export = AgendaExport.as_view()
|
||||
|
||||
|
||||
class AgendaDuplicate(ManagedAgendaMixin, FormView):
|
||||
class AgendaDuplicate(FormView):
|
||||
form_class = AgendaDuplicateForm
|
||||
template_name = 'chrono/manager_agenda_duplicate_form.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'))
|
||||
if not request.user.is_staff:
|
||||
raise PermissionDenied()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.new_agenda.pk})
|
||||
|
||||
|
@ -2252,6 +2312,11 @@ class AgendaDuplicate(ManagedAgendaMixin, FormView):
|
|||
self.new_agenda = self.agenda.duplicate(label=form.cleaned_data['label'])
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['agenda'] = self.agenda
|
||||
return context
|
||||
|
||||
|
||||
agenda_duplicate = AgendaDuplicate.as_view()
|
||||
|
||||
|
@ -2682,6 +2747,8 @@ class EventDetailView(ViewableAgendaMixin, DetailView):
|
|||
def dispatch(self, request, *args, **kwargs):
|
||||
if self.get_object().recurrence_days:
|
||||
raise Http404('this view makes no sense for recurring events')
|
||||
if self.get_object().agenda.partial_bookings:
|
||||
raise Http404('this view makes no sense for partial bookings')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_template_names(self):
|
||||
|
@ -2693,15 +2760,17 @@ class EventDetailView(ViewableAgendaMixin, DetailView):
|
|||
context = super().get_context_data(**kwargs)
|
||||
context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
|
||||
event = self.object
|
||||
context['booked'] = event.booking_set.filter(
|
||||
cancellation_datetime__isnull=True, in_waiting_list=False
|
||||
).order_by('creation_datetime')
|
||||
context['booked'] = (
|
||||
event.booking_set.filter(cancellation_datetime__isnull=True, in_waiting_list=False)
|
||||
.prefetch_related('user_checks')
|
||||
.order_by('creation_datetime')
|
||||
)
|
||||
context['waiting'] = event.booking_set.filter(
|
||||
cancellation_datetime__isnull=True, in_waiting_list=True
|
||||
).order_by('creation_datetime')
|
||||
event.present_count = len([b for b in context['booked'] if b.user_was_present is True])
|
||||
event.absent_count = len([b for b in context['booked'] if b.user_was_present is False])
|
||||
event.notchecked_count = len([b for b in context['booked'] if b.user_was_present is None])
|
||||
event.present_count = len([b for b in context['booked'] if b.user_check and b.user_check.presence])
|
||||
event.absent_count = len([b for b in context['booked'] if b.user_check and not b.user_check.presence])
|
||||
event.notchecked_count = len([b for b in context['booked'] if not b.user_check])
|
||||
return context
|
||||
|
||||
|
||||
|
@ -2710,8 +2779,19 @@ event_view = EventDetailView.as_view()
|
|||
|
||||
class EventDetailRedirectView(RedirectView):
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
agenda = get_object_or_404(Agenda, slug=kwargs['slug'])
|
||||
agenda = get_object_or_404(Agenda, slug=kwargs['slug'], kind='events')
|
||||
event = get_object_or_404(Event, slug=kwargs['event_slug'], agenda=agenda)
|
||||
if agenda.partial_bookings:
|
||||
day = localtime(event.start_datetime)
|
||||
return reverse(
|
||||
'chrono-manager-agenda-day-view',
|
||||
kwargs={
|
||||
'pk': agenda.pk,
|
||||
'year': day.year,
|
||||
'month': day.strftime('%m'),
|
||||
'day': day.strftime('%d'),
|
||||
},
|
||||
)
|
||||
return reverse('chrono-manager-event-view', kwargs={'pk': agenda.pk, 'event_pk': event.pk})
|
||||
|
||||
|
||||
|
@ -2729,6 +2809,7 @@ class EventEditView(ManagedAgendaMixin, UpdateView):
|
|||
self.request.GET.get('next') == 'settings'
|
||||
or self.request.POST.get('next') == 'settings'
|
||||
or self.object.recurrence_days
|
||||
or self.object.agenda.partial_bookings
|
||||
):
|
||||
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
|
||||
return reverse('chrono-manager-event-view', kwargs={'pk': self.agenda.id, 'event_pk': self.object.id})
|
||||
|
@ -2781,6 +2862,7 @@ class EventChecksView(ViewableAgendaMixin, EventChecksMixin, DetailView):
|
|||
Agenda,
|
||||
pk=kwargs.get('pk'),
|
||||
kind='events',
|
||||
partial_bookings=False,
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -2827,7 +2909,7 @@ class EventCheckMixin:
|
|||
event__cancelled=False,
|
||||
cancellation_datetime__isnull=True,
|
||||
in_waiting_list=False,
|
||||
user_was_present__isnull=True,
|
||||
user_checks__isnull=True,
|
||||
)
|
||||
|
||||
def get_check_type(self, kind):
|
||||
|
@ -2839,6 +2921,19 @@ class EventCheckMixin:
|
|||
return ct
|
||||
|
||||
def response(self, request):
|
||||
if self.agenda.partial_bookings:
|
||||
day = localtime(self.event.start_datetime)
|
||||
return HttpResponseRedirect(
|
||||
reverse(
|
||||
'chrono-manager-agenda-day-view',
|
||||
kwargs={
|
||||
'pk': self.agenda.pk,
|
||||
'year': day.year,
|
||||
'month': day.strftime('%m'),
|
||||
'day': day.strftime('%d'),
|
||||
},
|
||||
)
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
reverse(
|
||||
'chrono-manager-event-check',
|
||||
|
@ -2858,10 +2953,19 @@ class EventPresenceView(EventCheckMixin, ViewableAgendaMixin, FormView):
|
|||
def post(self, request, *args, **kwargs):
|
||||
qs_kwargs = {}
|
||||
check_type = self.get_check_type(kind='presence')
|
||||
qs_kwargs['user_check_type_slug'] = check_type.slug if check_type else None
|
||||
qs_kwargs['user_check_type_label'] = check_type.label if check_type else None
|
||||
qs_kwargs['type_slug'] = check_type.slug if check_type else None
|
||||
qs_kwargs['type_label'] = check_type.label if check_type else None
|
||||
bookings = self.get_bookings()
|
||||
bookings.update(user_was_present=True, **qs_kwargs)
|
||||
booking_checks_to_create = []
|
||||
for booking in bookings:
|
||||
if booking.user_check:
|
||||
continue
|
||||
|
||||
booking_check = BookingCheck(booking=booking, presence=True, **qs_kwargs)
|
||||
booking_checks_to_create.append(booking_check)
|
||||
|
||||
BookingCheck.objects.filter(booking__in=bookings).update(presence=True, **qs_kwargs)
|
||||
BookingCheck.objects.bulk_create(booking_checks_to_create)
|
||||
self.event.set_is_checked()
|
||||
return self.response(request)
|
||||
|
||||
|
@ -2880,10 +2984,19 @@ class EventAbsenceView(EventCheckMixin, ViewableAgendaMixin, FormView):
|
|||
def post(self, request, *args, **kwargs):
|
||||
qs_kwargs = {}
|
||||
check_type = self.get_check_type(kind='absence')
|
||||
qs_kwargs['user_check_type_slug'] = check_type.slug if check_type else None
|
||||
qs_kwargs['user_check_type_label'] = check_type.label if check_type else None
|
||||
qs_kwargs['type_slug'] = check_type.slug if check_type else None
|
||||
qs_kwargs['type_label'] = check_type.label if check_type else None
|
||||
bookings = self.get_bookings()
|
||||
bookings.update(user_was_present=False, **qs_kwargs)
|
||||
booking_checks_to_create = []
|
||||
for booking in bookings:
|
||||
if booking.user_check:
|
||||
continue
|
||||
|
||||
booking_check = BookingCheck(booking=booking, presence=False, **qs_kwargs)
|
||||
booking_checks_to_create.append(booking_check)
|
||||
|
||||
BookingCheck.objects.filter(booking__in=bookings).update(presence=False, **qs_kwargs)
|
||||
BookingCheck.objects.bulk_create(booking_checks_to_create)
|
||||
self.event.set_is_checked()
|
||||
return self.response(request)
|
||||
|
||||
|
@ -3663,10 +3776,12 @@ class BookingCheckMixin:
|
|||
def response(self, request, booking):
|
||||
if is_ajax(request):
|
||||
booking.absence_form = BookingCheckAbsenceForm(
|
||||
agenda=self.agenda, initial={'check_type': booking.user_check_type_slug}
|
||||
agenda=self.agenda,
|
||||
initial={'check_type': booking.user_check.type_slug if booking.user_check else None},
|
||||
)
|
||||
booking.presence_form = BookingCheckPresenceForm(
|
||||
agenda=self.agenda, initial={'check_type': booking.user_check_type_slug}
|
||||
agenda=self.agenda,
|
||||
initial={'check_type': booking.user_check.type_slug if booking.user_check else None},
|
||||
)
|
||||
booking.kind = 'booking'
|
||||
return render(
|
||||
|
@ -4416,30 +4531,61 @@ class SharedCustodySettingsView(UpdateView):
|
|||
shared_custody_settings = SharedCustodySettingsView.as_view()
|
||||
|
||||
|
||||
class PartialBookingCheckView(ViewableAgendaMixin, UpdateView):
|
||||
class PartialBookingCheckMixin(ViewableAgendaMixin):
|
||||
template_name = 'chrono/manager_partial_booking_form.html'
|
||||
model = Booking
|
||||
pk_url_kwarg = 'booking_pk'
|
||||
form_class = PartialBookingCheckForm
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset()
|
||||
return qs.filter(
|
||||
Q(event__start_datetime__date__lte=localtime().date())
|
||||
| Q(event__agenda__enable_check_for_future_events=True),
|
||||
)
|
||||
def get_object(self):
|
||||
booking = self.get_booking(**self.kwargs)
|
||||
return BookingCheck(booking=booking)
|
||||
|
||||
def get_success_url(self):
|
||||
date = self.object.event.start_datetime
|
||||
date = self.object.booking.event.start_datetime
|
||||
return reverse(
|
||||
'chrono-manager-agenda-day-view',
|
||||
kwargs={'pk': self.agenda.pk, 'year': date.year, 'month': date.month, 'day': date.day},
|
||||
)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['agenda'] = self.agenda
|
||||
return kwargs
|
||||
|
||||
|
||||
class PartialBookingCheckView(PartialBookingCheckMixin, BookingCheckMixin, UpdateView):
|
||||
pass
|
||||
|
||||
|
||||
partial_booking_check_view = PartialBookingCheckView.as_view()
|
||||
|
||||
|
||||
class PartialBookingSubscriptionCheckView(PartialBookingCheckMixin, SubscriptionCheckMixin, UpdateView):
|
||||
def get_object(self):
|
||||
if self.request.method == 'POST':
|
||||
return super().get_object()
|
||||
else:
|
||||
return BookingCheck(booking=Booking())
|
||||
|
||||
|
||||
partial_booking_subscription_check_view = PartialBookingSubscriptionCheckView.as_view()
|
||||
|
||||
|
||||
class PartialBookingUpdateCheckView(PartialBookingCheckMixin, UpdateView):
|
||||
model = BookingCheck
|
||||
pk_url_kwarg = 'check_pk'
|
||||
|
||||
def get_object(self):
|
||||
return super(PartialBookingCheckMixin, self).get_object()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['allow_adding_check'] = bool(self.object.booking.user_checks.count() == 1)
|
||||
return context
|
||||
|
||||
|
||||
partial_booking_update_check_view = PartialBookingUpdateCheckView.as_view()
|
||||
|
||||
|
||||
def menu_json(request):
|
||||
if not request.user.is_staff:
|
||||
homepage_view = HomepageView(request=request)
|
||||
|
|
|
@ -64,6 +64,19 @@ class TimeWidget(TimeInput):
|
|||
self.attrs['pattern'] = '[0-9]{2}:[0-9]{2}'
|
||||
|
||||
|
||||
class TimeWidgetWithButton(TimeWidget):
|
||||
template_name = 'chrono/widgets/time_with_button.html'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.button_label = kwargs.pop('button_label')
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_context(self, *args, **kwargs):
|
||||
ctx = super().get_context(*args, **kwargs)
|
||||
ctx['widget']['button_label'] = self.button_label
|
||||
return ctx
|
||||
|
||||
|
||||
class WeekdaysWidget(CheckboxSelectMultiple):
|
||||
template_name = 'chrono/widgets/weekdays.html'
|
||||
|
||||
|
|
|
@ -199,7 +199,6 @@ SMS_SENDER = ''
|
|||
REST_FRAMEWORK = {'EXCEPTION_HANDLER': 'chrono.api.utils.exception_handler'}
|
||||
|
||||
SHARED_CUSTODY_ENABLED = False
|
||||
LEGACY_FILLSLOTS_ENABLED = False
|
||||
PARTIAL_BOOKINGS_ENABLED = False
|
||||
|
||||
CHRONO_ANTS_HUB_URL = None
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
from django.db import connection
|
||||
from uwsgidecorators import spool # pylint: disable=import-error
|
||||
|
||||
from chrono.agendas.models import Event, ICSError, TimePeriodExceptionSource
|
||||
from chrono.agendas.models import Agenda, Event, ICSError, TimePeriodExceptionSource
|
||||
|
||||
|
||||
def set_connection(domain):
|
||||
|
@ -83,3 +83,31 @@ def ants_hub_city_push(args):
|
|||
City.push()
|
||||
except Exception: # noqa pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
|
||||
@spool
|
||||
def refresh_booking_computed_times_from_agenda(args):
|
||||
if args.get('domain'):
|
||||
# multitenant installation
|
||||
set_connection(args['domain'])
|
||||
|
||||
try:
|
||||
agenda = Agenda.objects.get(pk=args['agenda_id'])
|
||||
except Agenda.DoesNotExist:
|
||||
return
|
||||
|
||||
agenda.refresh_booking_computed_times()
|
||||
|
||||
|
||||
@spool
|
||||
def refresh_booking_computed_times_from_event(args):
|
||||
if args.get('domain'):
|
||||
# multitenant installation
|
||||
set_connection(args['domain'])
|
||||
|
||||
try:
|
||||
event = Event.objects.get(pk=args['event_id'])
|
||||
except Event.DoesNotExist:
|
||||
return
|
||||
|
||||
event.refresh_booking_computed_times()
|
||||
|
|
2
setup.py
2
setup.py
|
@ -161,7 +161,7 @@ setup(
|
|||
install_requires=[
|
||||
'django>=3.2, <3.3',
|
||||
'gadjo',
|
||||
'djangorestframework>=3.4,<3.14',
|
||||
'djangorestframework>=3.4,<3.15',
|
||||
'django-filter',
|
||||
'vobject',
|
||||
'python-dateutil',
|
||||
|
|
|
@ -126,7 +126,7 @@ def ants_setup(db, freezer):
|
|||
mairie_agenda,
|
||||
paris('2023-04-11 11:00'),
|
||||
meeting_type='mt-30',
|
||||
extra_data={'ants_identifiant_predemande': 'ABCDEFGH'},
|
||||
extra_data={'ants_identifiant_predemande': 'ABCDEFGH , IJKLMNOP'},
|
||||
)
|
||||
|
||||
add_meeting(
|
||||
|
@ -185,6 +185,7 @@ def test_export_to_push(ants_setup):
|
|||
'rdvs': [
|
||||
{'annule': True, 'date': '2023-04-10T07:00:00+00:00', 'id': '12345678'},
|
||||
{'date': '2023-04-11T09:00:00+00:00', 'id': 'ABCDEFGH'},
|
||||
{'date': '2023-04-11T09:00:00+00:00', 'id': 'IJKLMNOP'},
|
||||
],
|
||||
'plages': [
|
||||
{
|
||||
|
|
|
@ -1494,38 +1494,33 @@ def test_datetimes_multiple_agendas_with_status(app):
|
|||
places=5,
|
||||
agenda=agenda,
|
||||
)
|
||||
Booking.objects.create(event=event_absence, user_external_id='xxx', user_was_present=False)
|
||||
booking = Booking.objects.create(event=event_absence, user_external_id='xxx')
|
||||
booking.mark_user_absence()
|
||||
|
||||
event_absence_with_reason = Event.objects.create(
|
||||
slug='event-absence_with_reason',
|
||||
start_datetime=now() - datetime.timedelta(days=11),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
)
|
||||
Booking.objects.create(
|
||||
event=event_absence_with_reason,
|
||||
user_external_id='xxx',
|
||||
user_was_present=False,
|
||||
user_check_type_slug='foo-reason',
|
||||
)
|
||||
booking = Booking.objects.create(event=event_absence_with_reason, user_external_id='xxx')
|
||||
booking.mark_user_absence(check_type_slug='foo-reason')
|
||||
event_presence = Event.objects.create(
|
||||
slug='event-presence',
|
||||
start_datetime=now() - datetime.timedelta(days=10),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
)
|
||||
Booking.objects.create(event=event_presence, user_external_id='xxx', user_was_present=True)
|
||||
booking = Booking.objects.create(event=event_presence, user_external_id='xxx')
|
||||
booking.mark_user_presence()
|
||||
event_presence_with_reason = Event.objects.create(
|
||||
slug='event-presence_with_reason',
|
||||
start_datetime=now() - datetime.timedelta(days=9),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
)
|
||||
Booking.objects.create(
|
||||
event=event_presence_with_reason,
|
||||
user_external_id='xxx',
|
||||
user_was_present=True,
|
||||
user_check_type_slug='foo-reason',
|
||||
)
|
||||
booking = Booking.objects.create(event=event_presence_with_reason, user_external_id='xxx')
|
||||
booking.mark_user_presence(check_type_slug='foo-reason')
|
||||
event_booked_future = Event.objects.create(
|
||||
slug='event-booked-future',
|
||||
start_datetime=now() + datetime.timedelta(days=1),
|
||||
|
|
|
@ -434,7 +434,6 @@ def test_datetimes_api_meetings_agenda_short_time_periods(app, meetings_agenda,
|
|||
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
|
||||
assert len(resp.json['data']) == 2
|
||||
fillslot_url = resp.json['data'][0]['api']['fillslot_url']
|
||||
two_slots = [resp.json['data'][0]['id'], resp.json['data'][1]['id']]
|
||||
|
||||
time_period.end_time = datetime.time(10, 15)
|
||||
time_period.save()
|
||||
|
@ -448,13 +447,6 @@ def test_datetimes_api_meetings_agenda_short_time_periods(app, meetings_agenda,
|
|||
assert resp.json['reason'] == 'no more desk available' # legacy
|
||||
assert resp.json['err_class'] == 'no more desk available'
|
||||
assert resp.json['err_desc'] == 'no more desk available'
|
||||
# booking the two slots fails too
|
||||
fillslots_url = '/api/agenda/%s/fillslots/' % meeting_type.agenda.slug
|
||||
resp = app.post(fillslots_url, params={'slots': two_slots})
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'no more desk available' # legacy
|
||||
assert resp.json['err_class'] == 'no more desk available'
|
||||
assert resp.json['err_desc'] == 'no more desk available'
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-02-25')
|
||||
|
|
|
@ -123,6 +123,40 @@ def test_recurring_events_api_list(app, freezer):
|
|||
assert not any('example_event' in x['id'] for x in resp.json['data'])
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-12-13 14:00')
|
||||
def test_recurring_events_api_list_display_template(app):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
Event.objects.create(label='Normal event', start_datetime=now(), places=5, agenda=agenda)
|
||||
event = Event.objects.create(
|
||||
label='Example Event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[1, 4, 5], # Monday, Thursday, Friday
|
||||
recurrence_end_date=now() + datetime.timedelta(days=30),
|
||||
places=2,
|
||||
agenda=agenda,
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
||||
resp = app.get('/api/agendas/recurring-events/?agendas=%s' % agenda.slug)
|
||||
assert [x['text'] for x in resp.json['data']] == [
|
||||
'Monday: Example Event',
|
||||
'Thursday: Example Event',
|
||||
'Friday: Example Event',
|
||||
]
|
||||
|
||||
agenda.event_display_template = (
|
||||
'{% if event.recurrence_days %}{{ event.weekday }}{% else %}{{ event }}{% endif %}'
|
||||
)
|
||||
agenda.save()
|
||||
|
||||
resp = app.get('/api/agendas/recurring-events/?agendas=%s' % agenda.slug)
|
||||
assert [x['text'] for x in resp.json['data']] == ['Monday', 'Thursday', 'Friday']
|
||||
|
||||
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
|
||||
assert [x['text'] for x in resp.json['data']][:3] == ['Example Event', 'Example Event', 'Example Event']
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-12-13 14:00') # Monday of 50th week
|
||||
def test_recurring_events_api_list_shared_custody(app):
|
||||
agenda = Agenda.objects.create(
|
||||
|
|
|
@ -454,205 +454,6 @@ def test_booking_api_meetings_agenda_exclude_slots(app, user):
|
|||
assert resp.json['err'] == 0
|
||||
|
||||
|
||||
def test_booking_api_fillslots(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
events = []
|
||||
for i in range(3):
|
||||
events.append(
|
||||
Event.objects.create(
|
||||
label='Event', start_datetime=now() + datetime.timedelta(days=5 + i), places=20, agenda=agenda
|
||||
)
|
||||
)
|
||||
events_ids = [x.id for x in events]
|
||||
events_slugs = [x.slug for x in events]
|
||||
event = events[0]
|
||||
|
||||
# unauthenticated
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, status=401)
|
||||
|
||||
for agenda_key in (agenda.slug, agenda.id): # acces datetimes via agenda slug or id (legacy)
|
||||
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda_key)
|
||||
api_event_slugs = [x['id'] for x in resp_datetimes.json['data']]
|
||||
assert api_event_slugs == events_slugs
|
||||
|
||||
assert Booking.objects.count() == 0
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': events_ids})
|
||||
primary_booking_id = resp.json['booking_id']
|
||||
Booking.objects.get(id=primary_booking_id)
|
||||
assert resp.json['datetime'] == localtime(event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
|
||||
assert 'booking_url' in resp.json['api']
|
||||
assert 'accept_url' in resp.json['api']
|
||||
assert 'suspend_url' in resp.json['api']
|
||||
assert 'cancel_url' in resp.json['api']
|
||||
assert urlparse.urlparse(resp.json['api']['booking_url']).netloc
|
||||
assert urlparse.urlparse(resp.json['api']['accept_url']).netloc
|
||||
assert urlparse.urlparse(resp.json['api']['suspend_url']).netloc
|
||||
assert urlparse.urlparse(resp.json['api']['cancel_url']).netloc
|
||||
assert Booking.objects.count() == 3
|
||||
# these 3 bookings are related, the first is the primary one
|
||||
bookings = Booking.objects.all().order_by('pk')
|
||||
assert bookings[0].primary_booking is None
|
||||
assert bookings[1].primary_booking.id == bookings[0].id == primary_booking_id
|
||||
assert bookings[2].primary_booking.id == bookings[0].id == primary_booking_id
|
||||
|
||||
# access by slug
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': events_slugs})
|
||||
primary_booking_id_2 = resp.json['booking_id']
|
||||
assert Booking.objects.count() == 6
|
||||
assert Booking.objects.filter(event__agenda=agenda).count() == 6
|
||||
# 6 = 2 primary + 2*2 secondary
|
||||
assert Booking.objects.filter(event__agenda=agenda, primary_booking__isnull=True).count() == 2
|
||||
assert Booking.objects.filter(event__agenda=agenda, primary_booking=primary_booking_id).count() == 2
|
||||
assert Booking.objects.filter(event__agenda=agenda, primary_booking=primary_booking_id_2).count() == 2
|
||||
|
||||
# test with additional data
|
||||
resp = app.post_json(
|
||||
'/api/agenda/%s/fillslots/' % agenda.id,
|
||||
params={
|
||||
'slots': events_ids,
|
||||
'label': 'foo',
|
||||
'user_external_id': 'some_external_id',
|
||||
'user_last_name': 'bar',
|
||||
'user_display_label': 'foo',
|
||||
'backoffice_url': 'http://example.net/',
|
||||
},
|
||||
)
|
||||
booking_id = resp.json['booking_id']
|
||||
booking = Booking.objects.get(pk=booking_id)
|
||||
assert booking.label == 'foo'
|
||||
assert booking.user_external_id == 'some_external_id'
|
||||
assert booking.user_last_name == 'bar'
|
||||
assert booking.user_display_label == 'foo'
|
||||
assert booking.backoffice_url == 'http://example.net/'
|
||||
assert Booking.objects.filter(primary_booking=booking_id, label='foo').count() == 2
|
||||
# cancel
|
||||
cancel_url = resp.json['api']['cancel_url']
|
||||
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 0
|
||||
assert Booking.objects.get(id=booking_id).cancellation_datetime is None
|
||||
resp_cancel = app.post(cancel_url)
|
||||
assert resp_cancel.json['err'] == 0
|
||||
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 3
|
||||
assert Booking.objects.get(id=booking_id).cancellation_datetime is not None
|
||||
|
||||
# extra data stored in extra_data field
|
||||
resp = app.post_json(
|
||||
'/api/agenda/%s/fillslots/' % agenda.id,
|
||||
params={'slots': events_ids, 'label': 'l', 'user_last_name': 'u', 'backoffice_url': '', 'foo': 'bar'},
|
||||
)
|
||||
assert Booking.objects.get(id=resp.json['booking_id']).label == 'l'
|
||||
assert Booking.objects.get(id=resp.json['booking_id']).user_last_name == 'u'
|
||||
assert Booking.objects.get(id=resp.json['booking_id']).backoffice_url == ''
|
||||
assert Booking.objects.get(id=resp.json['booking_id']).extra_data == {'foo': 'bar'}
|
||||
for booking in Booking.objects.filter(primary_booking=resp.json['booking_id']):
|
||||
assert booking.extra_data == {'foo': 'bar'}
|
||||
|
||||
# test invalid data are refused
|
||||
resp = app.post_json(
|
||||
'/api/agenda/%s/fillslots/' % agenda.id,
|
||||
params={'slots': events_ids, 'user_last_name': {'foo': 'bar'}},
|
||||
status=400,
|
||||
)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'invalid payload' # legacy
|
||||
assert resp.json['err_class'] == 'invalid payload'
|
||||
assert resp.json['err_desc'] == 'invalid payload'
|
||||
assert len(resp.json['errors']) == 1
|
||||
assert 'user_last_name' in resp.json['errors']
|
||||
|
||||
# extra_data list/dict values are refused
|
||||
resp = app.post_json(
|
||||
'/api/agenda/%s/fillslots/' % agenda.id,
|
||||
params={'slots': events_ids, 'foo': ['bar', 'baz']},
|
||||
status=400,
|
||||
)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_class'] == 'wrong type for extra_data foo value'
|
||||
resp = app.post_json(
|
||||
'/api/agenda/%s/fillslots/' % agenda.id,
|
||||
params={'slots': events_ids, 'foo': {'bar': 'baz'}},
|
||||
status=400,
|
||||
)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_class'] == 'wrong type for extra_data foo value'
|
||||
|
||||
# empty or missing slots
|
||||
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, params={'slots': []}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'invalid payload' # legacy
|
||||
assert resp.json['err_desc'] == 'invalid payload'
|
||||
assert resp.json['errors']['slots'] == ['This field is required.']
|
||||
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'invalid payload' # legacy
|
||||
assert resp.json['err_desc'] == 'invalid payload'
|
||||
assert resp.json['errors']['slots'] == ['This field is required.']
|
||||
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, params={'slots': ''}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == 'invalid payload'
|
||||
assert resp.json['errors']['slots'] == ['This field is required.']
|
||||
# invalid slots format
|
||||
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, params={'slots': 'foobar'}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'invalid slugs: foobar' # legacy
|
||||
assert resp.json['err_class'] == 'invalid slugs: foobar'
|
||||
assert resp.json['err_desc'] == 'invalid slugs: foobar'
|
||||
|
||||
# unknown agendas
|
||||
resp = app.post('/api/agenda/foobar/fillslots/', status=404)
|
||||
resp = app.post('/api/agenda/0/fillslots/', status=404)
|
||||
|
||||
# check bookable period
|
||||
with mock.patch('chrono.agendas.models.Event.in_bookable_period') as in_bookable_period:
|
||||
in_bookable_period.return_value = True
|
||||
resp = app.post_json(
|
||||
'/api/agenda/%s/fillslots/' % agenda.id,
|
||||
params={'slots': events_ids},
|
||||
)
|
||||
assert resp.json['err'] == 0
|
||||
in_bookable_period.return_value = False
|
||||
resp = app.post_json(
|
||||
'/api/agenda/%s/fillslots/' % agenda.id,
|
||||
params={'slots': events_ids},
|
||||
)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'event not bookable' # legacy
|
||||
assert resp.json['err_class'] == 'event not bookable'
|
||||
assert resp.json['err_desc'] == 'event event is not bookable'
|
||||
|
||||
|
||||
def test_booking_api_fillslots_slots_string_param(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
Event.objects.create(
|
||||
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=10, agenda=agenda
|
||||
)
|
||||
Event.objects.create(
|
||||
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=10, agenda=agenda
|
||||
)
|
||||
events_ids = [x.id for x in Event.objects.filter(agenda=agenda)]
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
# empty string
|
||||
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': ''}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_class'] == 'invalid payload'
|
||||
assert resp.json['err_desc'] == 'invalid payload'
|
||||
|
||||
slots_string_param = ','.join([str(e) for e in events_ids])
|
||||
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots_string_param})
|
||||
assert Booking.objects.count() == 2
|
||||
|
||||
start = now() + datetime.timedelta(days=2)
|
||||
Event.objects.create(label='Long Slug', slug='a' * 100, start_datetime=start, places=2, agenda=agenda)
|
||||
Event.objects.create(label='Long Slug', slug='b' * 100, start_datetime=start, places=2, agenda=agenda)
|
||||
events_ids = [x.id for x in Event.objects.filter(label='Long Slug')]
|
||||
slots_string_param = ','.join([str(e) for e in events_ids])
|
||||
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots_string_param})
|
||||
assert Booking.objects.count() == 4
|
||||
|
||||
|
||||
def test_booking_api_meeting(app, meetings_agenda, user):
|
||||
agenda_id = meetings_agenda.slug
|
||||
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
|
||||
|
@ -867,90 +668,6 @@ def test_booking_api_meeting_with_resources(app, user):
|
|||
assert list(booking.event.resources.all()) == [resource1, resource2]
|
||||
|
||||
|
||||
def test_booking_api_meeting_fillslots(app, meetings_agenda, user):
|
||||
agenda_id = meetings_agenda.slug
|
||||
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
|
||||
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
|
||||
slots = [resp.json['data'][0]['id'], resp.json['data'][1]['id']]
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
resp_booking = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': slots})
|
||||
assert Booking.objects.count() == 2
|
||||
primary_booking = Booking.objects.filter(primary_booking__isnull=True).first()
|
||||
secondary_booking = Booking.objects.filter(primary_booking=primary_booking.id).first()
|
||||
assert resp_booking.json['datetime'] == localtime(primary_booking.event.start_datetime).strftime(
|
||||
'%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
assert resp_booking.json['end_datetime'] == localtime(secondary_booking.event.end_datetime).strftime(
|
||||
'%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
|
||||
assert len(resp.json['data']) == len([x for x in resp2.json['data'] if not x.get('disabled')]) + 2
|
||||
|
||||
# try booking the same timeslots
|
||||
resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': slots})
|
||||
assert resp2.json['err'] == 1
|
||||
assert resp2.json['reason'] == 'no more desk available' # legacy
|
||||
assert resp2.json['err_class'] == 'no more desk available'
|
||||
assert resp2.json['err_desc'] == 'no more desk available'
|
||||
|
||||
# try booking partially free timeslots (one free, one busy)
|
||||
nonfree_slots = [resp.json['data'][0]['id'], resp.json['data'][2]['id']]
|
||||
resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': nonfree_slots})
|
||||
assert resp2.json['err'] == 1
|
||||
assert resp2.json['reason'] == 'no more desk available' # legacy
|
||||
assert resp2.json['err_class'] == 'no more desk available'
|
||||
assert resp2.json['err_desc'] == 'no more desk available'
|
||||
|
||||
# booking other free timeslots
|
||||
free_slots = [resp.json['data'][3]['id'], resp.json['data'][2]['id']]
|
||||
resp2 = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': free_slots})
|
||||
assert resp2.json['err'] == 0
|
||||
cancel_url = resp2.json['api']['cancel_url']
|
||||
assert Booking.objects.count() == 4
|
||||
# 4 = 2 primary + 2 secondary
|
||||
assert Booking.objects.filter(primary_booking__isnull=True).count() == 2
|
||||
assert Booking.objects.filter(primary_booking__isnull=False).count() == 2
|
||||
# cancel
|
||||
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 0
|
||||
resp_cancel = app.post(cancel_url)
|
||||
assert resp_cancel.json['err'] == 0
|
||||
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 2
|
||||
|
||||
|
||||
def test_booking_api_meeting_fillslots_wrong_slot(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo', kind='meetings')
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
impossible_slots = ['1:2017-05-22-1130', '2:2017-05-22-1100']
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': impossible_slots}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'all slots must have the same meeting type id (1)' # legacy
|
||||
assert resp.json['err_class'] == 'all slots must have the same meeting type id (1)'
|
||||
assert resp.json['err_desc'] == 'all slots must have the same meeting type id (1)'
|
||||
|
||||
unknown_slots = ['0:2017-05-22-1130']
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': unknown_slots}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'invalid meeting type id: 0' # legacy
|
||||
assert resp.json['err_class'] == 'invalid meeting type id: 0'
|
||||
assert resp.json['err_desc'] == 'invalid meeting type id: 0'
|
||||
unknown_slots = ['foobar:2017-05-22-1130']
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': unknown_slots}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'invalid meeting type id: foobar' # legacy
|
||||
assert resp.json['err_class'] == 'invalid meeting type id: foobar'
|
||||
assert resp.json['err_desc'] == 'invalid meeting type id: foobar'
|
||||
|
||||
badformat_slots = ['foo:2020-10-28-14h00']
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': badformat_slots}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'bad datetime format: 2020-10-28-14h00' # legacy
|
||||
assert resp.json['err_class'] == 'bad datetime format: 2020-10-28-14h00'
|
||||
assert resp.json['err_desc'] == 'bad datetime format: 2020-10-28-14h00'
|
||||
|
||||
|
||||
def test_booking_api_meeting_across_daylight_saving_time(app, meetings_agenda, user):
|
||||
meetings_agenda.maximal_booking_delay = 365
|
||||
meetings_agenda.save()
|
||||
|
@ -1002,12 +719,6 @@ def test_booking_api_meeting_weekday_indexes(app, user):
|
|||
assert Booking.objects.count() == 1
|
||||
assert resp.json['duration'] == 30
|
||||
|
||||
# multiple slots
|
||||
slots = [datetimes_resp.json['data'][1]['id'], datetimes_resp.json['data'][2]['id']]
|
||||
assert slots == ['plop:2022-02-03-1130', 'plop:2022-02-17-1100']
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots})
|
||||
assert Booking.objects.count() == 3
|
||||
|
||||
# try to book slot on a skipped week
|
||||
slot = datetimes_resp.json['data'][3]['id']
|
||||
time_period.weekday_indexes = [1]
|
||||
|
@ -1057,11 +768,12 @@ def test_booking_api_meeting_date_time_period(app, user):
|
|||
assert Booking.objects.count() == 1
|
||||
assert resp.json['duration'] == 30
|
||||
|
||||
# multiple slots
|
||||
# book another two slots
|
||||
slots = [datetimes_resp.json['data'][1]['id'], datetimes_resp.json['data'][2]['id']]
|
||||
assert slots == ['plop:2022-10-24-1230', 'plop:2022-10-24-1300']
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots})
|
||||
assert Booking.objects.count() == 3
|
||||
for i, slot in enumerate(slots, 2):
|
||||
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot))
|
||||
assert Booking.objects.count() == i
|
||||
assert resp.json['duration'] == 30
|
||||
|
||||
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot))
|
||||
assert resp.json['err'] == 1
|
||||
|
@ -1148,16 +860,6 @@ def test_booking_api_available(app, user):
|
|||
assert resp.json['err'] == 0
|
||||
assert 'places' not in resp.json
|
||||
|
||||
# not for multiple booking
|
||||
events = [
|
||||
x for x in Event.objects.filter(agenda=agenda).order_by('start_datetime') if x.in_bookable_period()
|
||||
][:2]
|
||||
slots = [x.pk for x in events]
|
||||
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': '3'})
|
||||
assert resp.json['err'] == 0
|
||||
assert 'places' not in resp.json
|
||||
|
||||
|
||||
def test_booking_api_force_waiting_list(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
|
@ -1487,164 +1189,6 @@ def test_multiple_booking_api(app, user):
|
|||
assert Event.objects.get(id=event.id).booked_waiting_list_places == 2
|
||||
|
||||
|
||||
def test_multiple_booking_api_fillslots(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
events = []
|
||||
for i in range(2):
|
||||
events.append(
|
||||
Event.objects.create(
|
||||
label='Event', start_datetime=now() + datetime.timedelta(days=5 + i), places=20, agenda=agenda
|
||||
)
|
||||
)
|
||||
events_slugs = [x.slug for x in events]
|
||||
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id)
|
||||
slots = [x['id'] for x in resp_datetimes.json['data'] if x['id'] in events_slugs]
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
resp = app.post('/api/agenda/%s/fillslots/?count=NaN' % agenda.slug, params={'slots': slots}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'invalid value for count (NaN)' # legacy
|
||||
assert resp.json['err_class'] == 'invalid value for count (NaN)'
|
||||
assert resp.json['err_desc'] == 'invalid value for count (NaN)'
|
||||
|
||||
resp = app.post(
|
||||
'/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 'NaN'}, status=400
|
||||
)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'invalid payload' # legacy
|
||||
assert resp.json['err_class'] == 'invalid payload'
|
||||
assert resp.json['err_desc'] == 'invalid payload'
|
||||
assert 'count' in resp.json['errors']
|
||||
|
||||
# get 3 places on 2 slots
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': '3'})
|
||||
# one booking with 5 children
|
||||
booking = Booking.objects.get(id=resp.json['booking_id'])
|
||||
cancel_url = resp.json['api']['cancel_url']
|
||||
assert Booking.objects.filter(primary_booking=booking).count() == 5
|
||||
assert resp.json['datetime'] == localtime(events[0].start_datetime).strftime('%Y-%m-%d %H:%M:%S')
|
||||
assert 'accept_url' in resp.json['api']
|
||||
assert 'cancel_url' in resp.json['api']
|
||||
assert 'ics_url' in resp.json['api']
|
||||
resp_events = resp.json['events']
|
||||
assert len(resp_events) == len(events)
|
||||
for e, resp_e in zip(events, resp_events):
|
||||
assert e.slug == resp_e['slug']
|
||||
assert e.description == resp_e['description']
|
||||
assert str(e) == resp_e['text']
|
||||
assert localtime(e.start_datetime).strftime('%Y-%m-%d %H:%M:%S') == resp_e['datetime']
|
||||
for event in events:
|
||||
assert Event.objects.get(id=event.id).booked_places == 3
|
||||
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 2})
|
||||
for event in events:
|
||||
assert Event.objects.get(id=event.id).booked_places == 5
|
||||
|
||||
resp = app.post(cancel_url)
|
||||
for event in events:
|
||||
assert Event.objects.get(id=event.id).booked_places == 2
|
||||
|
||||
# check available places overflow
|
||||
# NB: limit only the first event !
|
||||
events[0].places = 3
|
||||
events[0].waiting_list_places = 8
|
||||
events[0].save()
|
||||
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 5})
|
||||
for event in events:
|
||||
assert Event.objects.get(id=event.id).booked_places == 2
|
||||
assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
|
||||
accept_url = resp.json['api']['accept_url']
|
||||
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 5})
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'sold out' # legacy
|
||||
assert resp.json['err_class'] == 'sold out'
|
||||
assert resp.json['err_desc'] == 'sold out'
|
||||
for event in events:
|
||||
assert Event.objects.get(id=event.id).booked_places == 2
|
||||
assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
|
||||
|
||||
# accept the waiting list
|
||||
resp = app.post(accept_url)
|
||||
for event in events:
|
||||
assert Event.objects.get(id=event.id).booked_places == 7
|
||||
assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
|
||||
|
||||
# check with a short waiting list
|
||||
Booking.objects.all().delete()
|
||||
# NB: limit only the first event !
|
||||
events[0].places = 4
|
||||
events[0].waiting_list_places = 2
|
||||
events[0].save()
|
||||
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 5})
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'sold out' # legacy
|
||||
assert resp.json['err_class'] == 'sold out'
|
||||
assert resp.json['err_desc'] == 'sold out'
|
||||
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 3})
|
||||
assert resp.json['err'] == 0
|
||||
for event in events:
|
||||
assert Event.objects.get(id=event.id).booked_places == 3
|
||||
assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
|
||||
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 3})
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'sold out' # legacy
|
||||
assert resp.json['err_class'] == 'sold out'
|
||||
assert resp.json['err_desc'] == 'sold out'
|
||||
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': '2'})
|
||||
assert resp.json['err'] == 0
|
||||
for event in events:
|
||||
assert Event.objects.get(id=event.id).booked_places == 3
|
||||
assert Event.objects.get(id=event.id).booked_waiting_list_places == 2
|
||||
|
||||
|
||||
def test_multiple_booking_move_booking(app, user):
|
||||
agenda = Agenda(label='Foo bar')
|
||||
agenda.save()
|
||||
first_date = localtime(now()).replace(hour=17, minute=0, second=0, microsecond=0)
|
||||
first_date += datetime.timedelta(days=1)
|
||||
events = []
|
||||
for i in range(10):
|
||||
event = Event(start_datetime=first_date + datetime.timedelta(days=i), places=20, agenda=agenda)
|
||||
event.save()
|
||||
events.append(event)
|
||||
|
||||
first_two_events = events[:2]
|
||||
events_slugs = [x.slug for x in first_two_events]
|
||||
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id)
|
||||
slots = [x['id'] for x in resp_datetimes.json['data'] if x['id'] in events_slugs]
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
# get 1 place on 2 slots
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots})
|
||||
booking = Booking.objects.get(id=resp.json['booking_id'])
|
||||
assert Booking.objects.filter(primary_booking=booking).count() == 1
|
||||
for event in first_two_events:
|
||||
assert Event.objects.get(id=event.id).booked_places == 1
|
||||
|
||||
# change, 1 place on 2 other slots
|
||||
last_two_events = events[-2:]
|
||||
events_slugs = [x.slug for x in last_two_events]
|
||||
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id)
|
||||
slots = [x['id'] for x in resp_datetimes.json['data'] if x['id'] in events_slugs]
|
||||
|
||||
resp = app.post(
|
||||
'/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'cancel_booking_id': booking.pk}
|
||||
)
|
||||
booking = Booking.objects.get(id=resp.json['booking_id'])
|
||||
assert Booking.objects.filter(primary_booking=booking).count() == 1
|
||||
for event in first_two_events:
|
||||
assert Event.objects.get(id=event.id).booked_places == 0
|
||||
for event in last_two_events:
|
||||
assert Event.objects.get(id=event.id).booked_places == 1
|
||||
|
||||
|
||||
def test_agenda_meeting_api_multiple_desk(app, user):
|
||||
agenda = Agenda.objects.create(
|
||||
label='foo', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=56
|
||||
|
@ -1743,92 +1287,6 @@ def test_agenda_meeting_api_multiple_desk(app, user):
|
|||
assert len(ctx.captured_queries) == 9
|
||||
|
||||
|
||||
def test_agenda_meeting_api_fillslots_multiple_desks(app, user):
|
||||
agenda = Agenda.objects.create(
|
||||
label='foo', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=56
|
||||
)
|
||||
meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah', duration=30)
|
||||
default_desk = Desk.objects.create(agenda=agenda, label='Desk 1')
|
||||
time_period = TimePeriod.objects.create(
|
||||
weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=default_desk
|
||||
)
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
# add a second desk, same timeperiods
|
||||
time_period = agenda.desk_set.first().timeperiod_set.first()
|
||||
desk2 = Desk.objects.create(label='Desk 2', agenda=agenda)
|
||||
TimePeriod.objects.create(
|
||||
start_time=time_period.start_time,
|
||||
end_time=time_period.end_time,
|
||||
weekday=time_period.weekday,
|
||||
desk=desk2,
|
||||
)
|
||||
|
||||
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
|
||||
slots = [x['id'] for x in resp.json['data'][:3]]
|
||||
|
||||
def get_free_places():
|
||||
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
|
||||
return len([x for x in resp.json['data'] if not x['disabled']])
|
||||
|
||||
start_free_places = get_free_places()
|
||||
|
||||
# booking 3 slots on desk 1
|
||||
fillslots_url = '/api/agenda/%s/fillslots/' % agenda.pk
|
||||
resp = app.post(fillslots_url, params={'slots': slots})
|
||||
assert resp.json['err'] == 0
|
||||
desk1 = resp.json['desk']['slug']
|
||||
cancel_url = resp.json['api']['cancel_url']
|
||||
assert get_free_places() == start_free_places
|
||||
|
||||
# booking same slots again, will be on desk 2
|
||||
resp = app.post(fillslots_url, params={'slots': slots})
|
||||
assert resp.json['err'] == 0
|
||||
assert resp.json['desk']['slug'] != desk2
|
||||
# 3 places are disabled in datetimes list
|
||||
assert get_free_places() == start_free_places - len(slots)
|
||||
|
||||
# try booking again: no desk available
|
||||
resp = app.post(fillslots_url, params={'slots': slots})
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'no more desk available' # legacy
|
||||
assert resp.json['err_class'] == 'no more desk available'
|
||||
assert resp.json['err_desc'] == 'no more desk available'
|
||||
assert get_free_places() == start_free_places - len(slots)
|
||||
|
||||
# cancel desk 1 booking
|
||||
resp = app.post(cancel_url)
|
||||
assert resp.json['err'] == 0
|
||||
# all places are free again
|
||||
assert get_free_places() == start_free_places
|
||||
|
||||
# booking a single slot (must be on desk 1)
|
||||
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.pk, slots[1]))
|
||||
assert resp.json['err'] == 0
|
||||
assert resp.json['desk']['slug'] == desk1
|
||||
cancel_url = resp.json['api']['cancel_url']
|
||||
assert get_free_places() == start_free_places - 1
|
||||
|
||||
# try booking the 3 slots again: no desk available, one slot is not fully available
|
||||
resp = app.post(fillslots_url, params={'slots': slots})
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == 'no more desk available' # legacy
|
||||
assert resp.json['err_class'] == 'no more desk available'
|
||||
assert resp.json['err_desc'] == 'no more desk available'
|
||||
|
||||
# cancel last signel slot booking, desk1 will be free
|
||||
resp = app.post(cancel_url)
|
||||
assert resp.json['err'] == 0
|
||||
assert get_free_places() == start_free_places
|
||||
|
||||
# booking again is ok, on desk 1
|
||||
resp = app.post(fillslots_url, params={'slots': slots})
|
||||
assert resp.json['err'] == 0
|
||||
assert resp.json['desk']['slug'] == desk1
|
||||
assert get_free_places() == start_free_places - len(slots)
|
||||
|
||||
|
||||
def test_agenda_meeting_same_day(app, mock_now, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
agenda = Agenda(label='Foo', kind='meetings')
|
||||
|
@ -2237,59 +1695,6 @@ def test_duration_on_booking_api_fillslot_response(app, user):
|
|||
assert 'DTEND:20170521T235700Z' in ics
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2017-04-01')
|
||||
def test_duration_on_booking_api_fillslots_response(app, user):
|
||||
agenda = Agenda(label='Foo bar')
|
||||
agenda.save()
|
||||
first_date = datetime.datetime(2017, 5, 20, 1, 12)
|
||||
durations = [None, 0, 45]
|
||||
evt = []
|
||||
for i in range(3):
|
||||
evt.append(
|
||||
Event(
|
||||
start_datetime=first_date + datetime.timedelta(days=i),
|
||||
duration=durations[i],
|
||||
places=20,
|
||||
agenda=agenda,
|
||||
)
|
||||
)
|
||||
evt[i].save()
|
||||
|
||||
assert evt[0].end_datetime is None
|
||||
assert evt[1].end_datetime == evt[1].start_datetime
|
||||
assert evt[2].end_datetime == evt[2].start_datetime + datetime.timedelta(minutes=45)
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
# first event having null duration
|
||||
string_param = ','.join([str(e.id) for e in evt[::-1]]) # unordered parameters
|
||||
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': string_param})
|
||||
r_evt = resp.json['events']
|
||||
|
||||
assert r_evt[0]['datetime'] == '2017-05-20 01:12:00'
|
||||
assert r_evt[0]['end_datetime'] is None
|
||||
assert r_evt[1]['datetime'] == '2017-05-21 01:12:00'
|
||||
assert r_evt[1]['end_datetime'] == r_evt[1]['datetime']
|
||||
assert r_evt[2]['datetime'] == '2017-05-22 01:12:00'
|
||||
assert r_evt[2]['end_datetime'] == '2017-05-22 01:57:00'
|
||||
assert 'ics_url' in resp.json['api']
|
||||
ics = app.get(resp.json['api']['ics_url']).text
|
||||
assert 'DTSTART:20170519T231200Z' in ics
|
||||
assert 'DTEND:' not in ics
|
||||
|
||||
# first event having duration
|
||||
evt[0].duration = 90
|
||||
evt[0].save()
|
||||
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': string_param})
|
||||
r_evt = resp.json['events']
|
||||
|
||||
assert r_evt[0]['datetime'] == '2017-05-20 01:12:00'
|
||||
assert r_evt[0]['end_datetime'] == '2017-05-20 02:42:00'
|
||||
assert 'ics_url' in resp.json['api']
|
||||
ics = app.get(resp.json['api']['ics_url']).text
|
||||
assert 'DTSTART:20170519T231200Z' in ics
|
||||
assert 'DTEND:20170520T004200Z' in ics
|
||||
|
||||
|
||||
def test_fillslot_past_event(app, user):
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
agenda = Agenda.objects.create(
|
||||
|
@ -2623,19 +2028,60 @@ def test_user_external_id(app, user):
|
|||
meeting_event.delete()
|
||||
|
||||
|
||||
def test_booking_api_fillslots_deprecated(app, user, settings):
|
||||
settings.LEGACY_FILLSLOTS_ENABLED = False
|
||||
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
@pytest.mark.freeze_time('2021-02-23 14:00')
|
||||
def test_booking_api_partial_booking(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
event = Event.objects.create(
|
||||
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=10, agenda=agenda
|
||||
label='Event',
|
||||
start_datetime=now() + datetime.timedelta(days=5),
|
||||
duration=120,
|
||||
places=1,
|
||||
agenda=agenda,
|
||||
)
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': [event.id]}, status=400)
|
||||
assert 'deprecated' in resp.json['err_desc']
|
||||
assert Booking.objects.count() == 0
|
||||
resp = app.post(
|
||||
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id),
|
||||
params={'start_time': '10:00', 'end_time': '15:00'},
|
||||
)
|
||||
booking = Booking.objects.get()
|
||||
assert booking.start_time == datetime.time(10, 00)
|
||||
assert booking.end_time == datetime.time(15, 00)
|
||||
|
||||
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id))
|
||||
assert Booking.objects.count() == 1
|
||||
# missing start_time
|
||||
resp = app.post(
|
||||
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id), params={'end_time': '10:00'}, status=400
|
||||
)
|
||||
assert (
|
||||
resp.json['errors']['non_field_errors'][0]
|
||||
== 'must include start_time and end_time for partial bookings agenda'
|
||||
)
|
||||
|
||||
# missing end_time
|
||||
resp = app.post(
|
||||
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id), params={'start_time': '10:00'}, status=400
|
||||
)
|
||||
assert (
|
||||
resp.json['errors']['non_field_errors'][0]
|
||||
== 'must include start_time and end_time for partial bookings agenda'
|
||||
)
|
||||
|
||||
# null end_time
|
||||
resp = app.post_json(
|
||||
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id),
|
||||
params={'start_time': '10:00', 'end_time': None},
|
||||
status=400,
|
||||
)
|
||||
assert (
|
||||
resp.json['errors']['non_field_errors'][0]
|
||||
== 'must include start_time and end_time for partial bookings agenda'
|
||||
)
|
||||
|
||||
# end before start
|
||||
resp = app.post(
|
||||
'/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id),
|
||||
params={'start_time': '10:00', 'end_time': '09:00'},
|
||||
status=400,
|
||||
)
|
||||
assert resp.json['errors']['non_field_errors'][0] == 'start_time must be before end_time'
|
||||
|
|
|
@ -48,6 +48,10 @@ def test_api_events_fillslots(app, user):
|
|||
assert len(resp.json['waiting_list_events']) == 0
|
||||
assert resp.json['cancelled_booking_count'] == 0
|
||||
assert len(resp.json['cancelled_events']) == 0
|
||||
assert (
|
||||
resp.json['bookings_ics_url']
|
||||
== 'http://testserver/api/bookings/ics/?user_external_id=user_id&agenda=foo-bar'
|
||||
)
|
||||
|
||||
events = Event.objects.all()
|
||||
assert events.filter(booked_places=1).count() == 2
|
||||
|
@ -556,3 +560,31 @@ def test_api_events_fillslots_exclude_user_forbidden(app, user):
|
|||
resp = app.post_json(fillslots_url, params=params, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['errors']['exclude_user'][0] == 'This parameter is not supported.'
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-02-23 14:00')
|
||||
def test_api_events_fillslots_partial_bookings(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now() + datetime.timedelta(days=5),
|
||||
duration=120,
|
||||
places=1,
|
||||
agenda=agenda,
|
||||
)
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
fillslots_url = '/api/agenda/foo-bar/events/fillslots/'
|
||||
params = {'user_external_id': 'user_id', 'start_time': '10:00', 'end_time': '15:00', 'slots': 'event'}
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
|
||||
booking = Booking.objects.get()
|
||||
assert booking.start_time == datetime.time(10, 00)
|
||||
assert booking.end_time == datetime.time(15, 00)
|
||||
|
||||
del params['start_time']
|
||||
resp = app.post_json(fillslots_url, params=params, status=400)
|
||||
assert (
|
||||
resp.json['errors']['non_field_errors'][0]
|
||||
== 'must include start_time and end_time for partial bookings agenda'
|
||||
)
|
||||
|
|
|
@ -178,6 +178,7 @@ def test_api_events_fillslots_multiple_agendas(app, user):
|
|||
)
|
||||
assert first_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
|
||||
assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
|
||||
assert resp.json['bookings_ics_url'] == 'http://testserver/api/bookings/ics/?user_external_id=user_id'
|
||||
|
||||
# booking modification
|
||||
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@event'}
|
||||
|
@ -795,3 +796,36 @@ def test_api_events_fillslots_multiple_agendas_overlapping_events(app, user, fre
|
|||
params={'user_external_id': 'user_id', 'check_overlaps': True, 'slots': 'foo-bar-2@event-2'},
|
||||
)
|
||||
assert resp.json['booking_count'] == 1
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-02-23 14:00')
|
||||
def test_api_events_fillslots_multiple_agendas_partial_bookings(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now() + datetime.timedelta(days=5),
|
||||
duration=120,
|
||||
places=1,
|
||||
agenda=agenda,
|
||||
)
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
fillslots_url = '/api/agendas/events/fillslots/?agendas=foo-bar'
|
||||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'start_time': '10:00',
|
||||
'end_time': '15:00',
|
||||
'slots': 'foo-bar@event',
|
||||
}
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
|
||||
booking = Booking.objects.get()
|
||||
assert booking.start_time == datetime.time(10, 00)
|
||||
assert booking.end_time == datetime.time(15, 00)
|
||||
|
||||
del params['start_time']
|
||||
resp = app.post_json(fillslots_url, params=params, status=400)
|
||||
assert (
|
||||
resp.json['errors']['non_field_errors'][0]
|
||||
== 'must include start_time and end_time for partial bookings agenda'
|
||||
)
|
||||
|
|
|
@ -1283,6 +1283,58 @@ def test_recurring_events_api_fillslots_multiple_agendas(app, user):
|
|||
]
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-09-06 12:00')
|
||||
def test_recurring_events_api_fillslots_update_from_date(app, user):
|
||||
agenda = Agenda.objects.create(
|
||||
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=0
|
||||
)
|
||||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[1, 2, 4, 5], # Monday, Tuesday, Thursday, Friday
|
||||
places=1,
|
||||
waiting_list_places=1,
|
||||
agenda=agenda,
|
||||
recurrence_end_date=now() + datetime.timedelta(days=28), # 4 weeks
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
fillslots_url = '/api/agendas/recurring-events/fillslots/?agendas=foo-bar&'
|
||||
params = {'user_external_id': 'user_id'}
|
||||
# Book Monday and Thursday
|
||||
params['slots'] = 'foo-bar@event:1,foo-bar@event:4'
|
||||
resp = app.post_json(fillslots_url + 'action=book', params=params)
|
||||
assert resp.json['booking_count'] == 8
|
||||
assert resp.json['cancelled_booking_count'] == 0
|
||||
bookings = Booking.objects.filter(cancellation_datetime__isnull=True).order_by('event__start_datetime')
|
||||
assert [x.strftime('%A %d/%m') for x in bookings.values_list('event__start_datetime', flat=True)] == [
|
||||
'Monday 06/09',
|
||||
'Thursday 09/09',
|
||||
'Monday 13/09',
|
||||
'Thursday 16/09',
|
||||
'Monday 20/09',
|
||||
'Thursday 23/09',
|
||||
'Monday 27/09',
|
||||
'Thursday 30/09',
|
||||
]
|
||||
|
||||
# Book only Friday from 20/09
|
||||
params['slots'] = 'foo-bar@event:5'
|
||||
resp = app.post_json(fillslots_url + 'action=update-from-date&date_start=2021-09-20', params=params)
|
||||
assert resp.json['booking_count'] == 2
|
||||
assert resp.json['cancelled_booking_count'] == 4
|
||||
assert [x.strftime('%A %d/%m') for x in bookings.values_list('event__start_datetime', flat=True)] == [
|
||||
'Monday 06/09',
|
||||
'Thursday 09/09',
|
||||
'Monday 13/09',
|
||||
'Thursday 16/09',
|
||||
'Friday 24/09',
|
||||
'Friday 01/10',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-09-06 12:00')
|
||||
def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user):
|
||||
events_type = EventsType.objects.create(label='Foo')
|
||||
|
@ -1807,3 +1859,120 @@ def test_recurring_events_api_fillslots_partial_bookings_update(app, user):
|
|||
).count()
|
||||
== 5
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2023-05-01 10:00')
|
||||
def test_recurring_events_api_fillslots_by_days_partial_bookings(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0))
|
||||
event = Event.objects.create(
|
||||
label='Event Mon-Wed',
|
||||
start_datetime=start_datetime,
|
||||
end_time=datetime.time(18, 00),
|
||||
places=2,
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=30),
|
||||
recurrence_days=[1, 2, 3],
|
||||
agenda=agenda,
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
event = Event.objects.create(
|
||||
label='Event Thu-Sat',
|
||||
start_datetime=start_datetime,
|
||||
end_time=datetime.time(18, 00),
|
||||
places=2,
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=30),
|
||||
recurrence_days=[4, 5, 6],
|
||||
agenda=agenda,
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'tuesday': '09:00,12:00',
|
||||
'friday': '08:00,18:00',
|
||||
}
|
||||
fillslots_url = '/api/agendas/recurring-events/fillslots-by-day/?action=update&agendas=%s' % agenda.slug
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 9
|
||||
assert Booking.objects.count() == 9
|
||||
assert (
|
||||
Booking.objects.filter(
|
||||
event__slug__startswith='event-mon-wed--2023-05-',
|
||||
start_time=datetime.time(9, 00),
|
||||
end_time=datetime.time(12, 00),
|
||||
event__start_datetime__iso_week_day=2,
|
||||
).count()
|
||||
== 5
|
||||
)
|
||||
assert (
|
||||
Booking.objects.filter(
|
||||
event__slug__startswith='event-thu-sat--2023-05-',
|
||||
start_time=datetime.time(8, 00),
|
||||
end_time=datetime.time(18, 00),
|
||||
event__start_datetime__iso_week_day=5,
|
||||
).count()
|
||||
== 4
|
||||
)
|
||||
|
||||
# change bookings
|
||||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'wednesday': '10:00,14:00',
|
||||
'thursday': None, # null values are allowed and ignored
|
||||
'sunday': '12:00,16:00', # unbookable day will be ignored
|
||||
'slots': 'xxx', # parameter of normal API, ignored
|
||||
'start_time': None, # parameter of normal API, ignored
|
||||
}
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 5
|
||||
assert resp.json['cancelled_booking_count'] == 9
|
||||
assert Booking.objects.count() == 5
|
||||
assert (
|
||||
Booking.objects.filter(
|
||||
event__slug__startswith='event-mon-wed--2023-05-',
|
||||
start_time=datetime.time(10, 00),
|
||||
end_time=datetime.time(14, 00),
|
||||
event__start_datetime__iso_week_day=3,
|
||||
).count()
|
||||
== 5
|
||||
)
|
||||
|
||||
agenda2 = Agenda.objects.create(label='Foo Bar 2', kind='events', partial_bookings=True)
|
||||
resp = app.post_json(
|
||||
'/api/agendas/recurring-events/fillslots-by-day/?action=update&agendas=%s,%s'
|
||||
% (agenda.slug, agenda2.slug),
|
||||
params=params,
|
||||
status=400,
|
||||
)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['errors']['non_field_errors'][0] == 'Multiple agendas are not supported.'
|
||||
|
||||
agenda_events = Agenda.objects.create(label='Not partial bookings', kind='events')
|
||||
resp = app.post_json(
|
||||
'/api/agendas/recurring-events/fillslots-by-day/?action=update&agendas=%s' % agenda_events.slug,
|
||||
params=params,
|
||||
status=400,
|
||||
)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['errors']['non_field_errors'][0] == 'Agenda kind must be partial bookings.'
|
||||
|
||||
params = {'user_external_id': 'user_id', 'wednesday': '11:00,10:00'}
|
||||
resp = app.post_json(fillslots_url, params=params, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['errors']['wednesday'][0] == 'Start hour must be before end hour.'
|
||||
|
||||
params = {'user_external_id': 'user_id', 'wednesday': '11:00,xxx'}
|
||||
resp = app.post_json(fillslots_url, params=params, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['errors']['wednesday']['1'][0].startswith('Time has wrong format')
|
||||
|
||||
params = {'user_external_id': 'user_id', 'wednesday': '11:00'}
|
||||
resp = app.post_json(fillslots_url, params=params, status=400)
|
||||
assert resp.json['errors']['wednesday'][0] == 'Ensure this field has at least 2 elements.'
|
||||
|
||||
params = {'user_external_id': 'user_id', 'wednesday': '11:00,13:00,15:00'}
|
||||
resp = app.post_json(fillslots_url, params=params, status=400)
|
||||
assert resp.json['errors']['wednesday'][0] == 'Ensure this field has no more than 2 elements.'
|
||||
|
|
|
@ -20,7 +20,7 @@ from chrono.utils.timezone import localtime, now
|
|||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_agendas_api(app):
|
||||
def test_agendas_api(settings, app):
|
||||
edit_group = Group.objects.create(name='Edit')
|
||||
view_group = Group.objects.create(name='View')
|
||||
category_a = Category.objects.create(label='Category A')
|
||||
|
@ -36,7 +36,7 @@ def test_agendas_api(app):
|
|||
Desk.objects.create(agenda=event_agenda, slug='_exceptions_holder')
|
||||
event_agenda2 = Agenda.objects.create(label='Foo bar 2', category=category_a, events_type=events_type2)
|
||||
Desk.objects.create(agenda=event_agenda2, slug='_exceptions_holder')
|
||||
event_agenda3 = Agenda.objects.create(label='Foo bar 3')
|
||||
event_agenda3 = Agenda.objects.create(label='Foo bar 3', partial_bookings=True)
|
||||
Desk.objects.create(agenda=event_agenda3, slug='_exceptions_holder')
|
||||
meetings_agenda1 = Agenda.objects.create(
|
||||
label='Foo bar Meeting', kind='meetings', category=category_b, view_role=view_group
|
||||
|
@ -75,7 +75,6 @@ def test_agendas_api(app):
|
|||
'booking_form_url': None,
|
||||
'api': {
|
||||
'datetimes_url': 'http://testserver/api/agenda/foo-bar/datetimes/',
|
||||
'fillslots_url': 'http://testserver/api/agenda/foo-bar/fillslots/',
|
||||
'backoffice_url': 'http://testserver/manage/agendas/%s/' % event_agenda.pk,
|
||||
},
|
||||
},
|
||||
|
@ -96,7 +95,6 @@ def test_agendas_api(app):
|
|||
'booking_form_url': None,
|
||||
'api': {
|
||||
'datetimes_url': 'http://testserver/api/agenda/foo-bar-2/datetimes/',
|
||||
'fillslots_url': 'http://testserver/api/agenda/foo-bar-2/fillslots/',
|
||||
'backoffice_url': 'http://testserver/manage/agendas/%s/' % event_agenda2.pk,
|
||||
},
|
||||
},
|
||||
|
@ -117,7 +115,6 @@ def test_agendas_api(app):
|
|||
'booking_form_url': None,
|
||||
'api': {
|
||||
'datetimes_url': 'http://testserver/api/agenda/foo-bar-3/datetimes/',
|
||||
'fillslots_url': 'http://testserver/api/agenda/foo-bar-3/fillslots/',
|
||||
'backoffice_url': 'http://testserver/manage/agendas/%s/' % event_agenda3.pk,
|
||||
},
|
||||
},
|
||||
|
@ -141,7 +138,6 @@ def test_agendas_api(app):
|
|||
'meetings_url': 'http://testserver/api/agenda/foo-bar-meeting/meetings/',
|
||||
'desks_url': 'http://testserver/api/agenda/foo-bar-meeting/desks/',
|
||||
'resources_url': 'http://testserver/api/agenda/foo-bar-meeting/resources/',
|
||||
'fillslots_url': 'http://testserver/api/agenda/foo-bar-meeting/fillslots/',
|
||||
'backoffice_url': 'http://testserver/manage/agendas/%s/' % meetings_agenda1.pk,
|
||||
},
|
||||
},
|
||||
|
@ -162,7 +158,6 @@ def test_agendas_api(app):
|
|||
'meetings_url': 'http://testserver/api/agenda/foo-bar-meeting-2/meetings/',
|
||||
'desks_url': 'http://testserver/api/agenda/foo-bar-meeting-2/desks/',
|
||||
'resources_url': 'http://testserver/api/agenda/foo-bar-meeting-2/resources/',
|
||||
'fillslots_url': 'http://testserver/api/agenda/foo-bar-meeting-2/fillslots/',
|
||||
'backoffice_url': 'http://testserver/manage/agendas/%s/' % meetings_agenda2.pk,
|
||||
},
|
||||
},
|
||||
|
@ -181,7 +176,6 @@ def test_agendas_api(app):
|
|||
'api': {
|
||||
'meetings_url': 'http://testserver/api/agenda/virtual-agenda/meetings/',
|
||||
'desks_url': 'http://testserver/api/agenda/virtual-agenda/desks/',
|
||||
'fillslots_url': 'http://testserver/api/agenda/virtual-agenda/fillslots/',
|
||||
'backoffice_url': 'http://testserver/manage/agendas/%s/' % virtual_agenda.pk,
|
||||
},
|
||||
},
|
||||
|
@ -297,6 +291,15 @@ def test_agendas_api(app):
|
|||
resp = app.get('/api/agenda/', params={'with_open_events': '1'})
|
||||
assert len(resp.json['data']) == 1
|
||||
|
||||
settings.PARTIAL_BOOKINGS_ENABLED = True
|
||||
resp = app.get('/api/agenda/')
|
||||
assert resp.json['data'][0]['kind'] == 'events'
|
||||
assert resp.json['data'][0]['partial_bookings'] is False
|
||||
assert resp.json['data'][1]['kind'] == 'events'
|
||||
assert resp.json['data'][1]['partial_bookings'] is False
|
||||
assert resp.json['data'][2]['kind'] == 'events'
|
||||
assert resp.json['data'][2]['partial_bookings'] is True
|
||||
|
||||
for _ in range(10):
|
||||
event_agenda = Agenda.objects.create(label='Foo bar', category=category_a)
|
||||
Desk.objects.create(agenda=event_agenda, slug='_exceptions_holder')
|
||||
|
@ -429,7 +432,6 @@ def test_virtual_agenda_detail(app, virtual_meetings_agenda):
|
|||
'api': {
|
||||
'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % virtual_meetings_agenda.slug,
|
||||
'desks_url': 'http://testserver/api/agenda/%s/desks/' % virtual_meetings_agenda.slug,
|
||||
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % virtual_meetings_agenda.slug,
|
||||
'backoffice_url': 'http://testserver/manage/agendas/%s/' % virtual_meetings_agenda.pk,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -5,7 +5,16 @@ import pytest
|
|||
from django.db import connection
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
|
||||
from chrono.agendas.models import Agenda, Booking, BookingColor, Category, Desk, Event, MeetingType
|
||||
from chrono.agendas.models import (
|
||||
Agenda,
|
||||
Booking,
|
||||
BookingCheck,
|
||||
BookingColor,
|
||||
Category,
|
||||
Desk,
|
||||
Event,
|
||||
MeetingType,
|
||||
)
|
||||
from chrono.utils.lingo import CheckType
|
||||
from chrono.utils.timezone import localtime, make_aware, now
|
||||
|
||||
|
@ -104,6 +113,40 @@ def test_booking_ics(app, some_data, meetings_agenda, user, settings, rf):
|
|||
assert 'DTEND:%sZ\r\n' % end in booking_ics
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2023-09-18 14:00')
|
||||
def test_bookings_ics(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
event = Event.objects.create(
|
||||
label='Event A', agenda=agenda, start_datetime=now() + datetime.timedelta(days=3), places=10
|
||||
)
|
||||
Booking.objects.create(event=event, user_external_id='enfant-1234')
|
||||
|
||||
agenda = Agenda.objects.create(label='Foo bar 2', kind='events')
|
||||
event = Event.objects.create(
|
||||
label='Event B', agenda=agenda, start_datetime=now() + datetime.timedelta(days=4), places=10
|
||||
)
|
||||
Booking.objects.create(event=event, user_external_id='enfant-1234')
|
||||
|
||||
resp = app.get('/api/bookings/ics/')
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == 'missing param user_external_id'
|
||||
|
||||
resp = app.get('/api/bookings/ics/', params={'user_external_id': 'enfant-1234'})
|
||||
assert 'BEGIN:VCALENDAR' in resp.text
|
||||
assert resp.text.count('UID') == 2
|
||||
assert 'DTSTART:20230921' in resp.text
|
||||
assert 'DTSTART:20230922' in resp.text
|
||||
|
||||
resp = app.get('/api/bookings/ics/', params={'user_external_id': 'enfant-1234', 'agenda': 'foo-bar'})
|
||||
assert resp.text.count('UID') == 1
|
||||
assert 'DTSTART:20230921' in resp.text
|
||||
assert 'DTSTART:20230922' not in resp.text
|
||||
|
||||
resp = app.get('/api/bookings/ics/', params={'user_external_id': 'enfant-1234', 'agenda': 'xxx'})
|
||||
assert 'BEGIN:VCALENDAR' in resp.text
|
||||
assert resp.text.count('UID') == 0
|
||||
|
||||
|
||||
def test_bookings_api(app, user):
|
||||
events_agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
events_event = Event.objects.create(
|
||||
|
@ -143,7 +186,7 @@ def test_bookings_api(app, user):
|
|||
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
resp = app.get('/api/bookings/', params={'user_external_id': 'enfant-1234'})
|
||||
assert len(ctx.captured_queries) == 2
|
||||
assert len(ctx.captured_queries) == 3
|
||||
|
||||
assert resp.json['err'] == 0
|
||||
assert resp.json['data'] == [
|
||||
|
@ -319,8 +362,10 @@ def test_bookings_api_filter_user_was_present(app, user):
|
|||
agenda=agenda, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 0, 0)), places=10
|
||||
)
|
||||
Booking.objects.create(event=event, user_external_id='42')
|
||||
booking2 = Booking.objects.create(event=event, user_external_id='42', user_was_present=True)
|
||||
booking3 = Booking.objects.create(event=event, user_external_id='42', user_was_present=False)
|
||||
booking2 = Booking.objects.create(event=event, user_external_id='42')
|
||||
booking2.mark_user_presence()
|
||||
booking3 = Booking.objects.create(event=event, user_external_id='42')
|
||||
booking3.mark_user_absence()
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_was_present': True})
|
||||
|
@ -336,21 +381,12 @@ def test_bookings_api_filter_user_absence_reason(app, user):
|
|||
event = Event.objects.create(
|
||||
agenda=agenda, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 0, 0)), places=10
|
||||
)
|
||||
Booking.objects.create(event=event, user_external_id='42', user_was_present=False)
|
||||
booking2 = Booking.objects.create(
|
||||
event=event,
|
||||
user_external_id='42',
|
||||
user_was_present=False,
|
||||
user_check_type_slug='foo-bar',
|
||||
user_check_type_label='Foo bar',
|
||||
)
|
||||
Booking.objects.create(
|
||||
event=event,
|
||||
user_external_id='42',
|
||||
user_was_present=True,
|
||||
user_check_type_slug='foo-bar-2',
|
||||
user_check_type_label='Foo bar 2',
|
||||
)
|
||||
booking = Booking.objects.create(event=event, user_external_id='42')
|
||||
booking.mark_user_absence()
|
||||
booking2 = Booking.objects.create(event=event, user_external_id='42')
|
||||
booking2.mark_user_absence(check_type_slug='foo-bar', check_type_label='Foo bar')
|
||||
booking3 = Booking.objects.create(event=event, user_external_id='42')
|
||||
booking3.mark_user_presence(check_type_slug='foo-bar-2', check_type_label='Foo bar 2')
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_absence_reason': 'foo'})
|
||||
|
@ -365,7 +401,7 @@ def test_bookings_api_filter_user_absence_reason(app, user):
|
|||
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_absence_reason': 'foo-bar-2'})
|
||||
assert resp.json['err'] == 0
|
||||
assert [b['id'] for b in resp.json['data']] == []
|
||||
Booking.objects.update(user_was_present=True)
|
||||
BookingCheck.objects.update(presence=True)
|
||||
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_absence_reason': 'Foo bar 2'})
|
||||
assert resp.json['err'] == 0
|
||||
assert [b['id'] for b in resp.json['data']] == []
|
||||
|
@ -376,21 +412,12 @@ def test_bookings_api_filter_user_presence_reason(app, user):
|
|||
event = Event.objects.create(
|
||||
agenda=agenda, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 0, 0)), places=10
|
||||
)
|
||||
Booking.objects.create(event=event, user_external_id='42', user_was_present=True)
|
||||
booking2 = Booking.objects.create(
|
||||
event=event,
|
||||
user_external_id='42',
|
||||
user_was_present=True,
|
||||
user_check_type_slug='foo-bar',
|
||||
user_check_type_label='Foo bar',
|
||||
)
|
||||
Booking.objects.create(
|
||||
event=event,
|
||||
user_external_id='42',
|
||||
user_was_present=False,
|
||||
user_check_type_slug='foo-bar-2',
|
||||
user_check_type_label='Foo bar 2',
|
||||
)
|
||||
booking = Booking.objects.create(event=event, user_external_id='42')
|
||||
booking.mark_user_presence()
|
||||
booking2 = Booking.objects.create(event=event, user_external_id='42')
|
||||
booking2.mark_user_presence(check_type_slug='foo-bar', check_type_label='Foo bar')
|
||||
booking3 = Booking.objects.create(event=event, user_external_id='42')
|
||||
booking3.mark_user_absence(check_type_slug='foo-bar-2', check_type_label='Foo bar 2')
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_presence_reason': 'foo'})
|
||||
|
@ -405,7 +432,7 @@ def test_bookings_api_filter_user_presence_reason(app, user):
|
|||
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_presence_reason': 'foo-bar-2'})
|
||||
assert resp.json['err'] == 0
|
||||
assert [b['id'] for b in resp.json['data']] == []
|
||||
Booking.objects.update(user_was_present=False)
|
||||
BookingCheck.objects.update(presence=False)
|
||||
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'user_presence_reason': 'Foo bar 2'})
|
||||
assert resp.json['err'] == 0
|
||||
assert [b['id'] for b in resp.json['data']] == []
|
||||
|
@ -481,7 +508,11 @@ def test_bookings_api_filter_event(app, user):
|
|||
def test_booking_api_present(app, user, flag):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
|
||||
booking = Booking.objects.create(event=event, user_was_present=flag, user_check_type_slug='foo-bar')
|
||||
booking = Booking.objects.create(event=event)
|
||||
if flag is True:
|
||||
booking.mark_user_presence(check_type_slug='foo-bar')
|
||||
elif flag is False:
|
||||
booking.mark_user_absence(check_type_slug='foo-bar')
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
resp = app.get('/api/booking/%s/' % booking.pk)
|
||||
|
@ -580,8 +611,13 @@ def test_booking_patch_api_present(app, user, flag):
|
|||
|
||||
# set flag
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_was_present': flag})
|
||||
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_was_present == flag
|
||||
if flag is not None:
|
||||
assert booking.user_check.presence == flag
|
||||
else:
|
||||
assert not booking.user_check
|
||||
|
||||
event.refresh_from_db()
|
||||
assert event.checked is False
|
||||
|
||||
|
@ -589,14 +625,17 @@ def test_booking_patch_api_present(app, user, flag):
|
|||
agenda.save()
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_was_present': flag})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_was_present == flag
|
||||
if flag is not None:
|
||||
assert booking.user_check.presence == flag
|
||||
else:
|
||||
assert not booking.user_check
|
||||
event.refresh_from_db()
|
||||
assert event.checked == (flag is not None)
|
||||
|
||||
# reset
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_was_present': None})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_was_present is None
|
||||
assert not booking.user_check
|
||||
|
||||
# make secondary bookings
|
||||
Booking.objects.create(event=event, primary_booking=booking)
|
||||
|
@ -606,11 +645,14 @@ def test_booking_patch_api_present(app, user, flag):
|
|||
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_was_present': flag})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_was_present == flag
|
||||
# all secondary bookings are updated
|
||||
assert list(booking.secondary_booking_set.values_list('user_was_present', flat=True)) == [flag, flag]
|
||||
if flag is not None:
|
||||
assert booking.user_check.presence == flag
|
||||
else:
|
||||
assert not booking.user_check
|
||||
# secondary bookings are left untouched
|
||||
assert list(booking.secondary_booking_set.values_list('user_checks', flat=True)) == [None, None]
|
||||
other_booking.refresh_from_db()
|
||||
assert other_booking.user_was_present is None # not changed
|
||||
assert not other_booking.user_check # not changed
|
||||
|
||||
# mark the event as checked
|
||||
event.checked = True
|
||||
|
@ -639,7 +681,8 @@ def test_booking_patch_api_absence_reason(check_types, app, user):
|
|||
check_types.return_value = []
|
||||
agenda = Agenda.objects.create(kind='events')
|
||||
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
|
||||
booking = Booking.objects.create(event=event, user_was_present=False)
|
||||
booking = Booking.objects.create(event=event)
|
||||
booking.mark_user_absence()
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
|
@ -664,8 +707,8 @@ def test_booking_patch_api_absence_reason(check_types, app, user):
|
|||
# it works with label
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_absence_reason': 'Foo bar'})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_check_type_slug == 'foo-bar'
|
||||
assert booking.user_check_type_label == 'Foo bar'
|
||||
assert booking.user_check.type_slug == 'foo-bar'
|
||||
assert booking.user_check.type_label == 'Foo bar'
|
||||
|
||||
# unknown
|
||||
resp = app.patch_json(
|
||||
|
@ -679,51 +722,47 @@ def test_booking_patch_api_absence_reason(check_types, app, user):
|
|||
# reset
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_absence_reason': ''})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_check_type_slug is None
|
||||
assert booking.user_check_type_label is None
|
||||
assert booking.user_check.type_slug is None
|
||||
assert booking.user_check.type_label is None
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_absence_reason': None})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_check_type_slug is None
|
||||
assert booking.user_check_type_label is None
|
||||
assert booking.user_check.type_slug is None
|
||||
assert booking.user_check.type_label is None
|
||||
|
||||
# make secondary bookings
|
||||
Booking.objects.create(event=event, primary_booking=booking, user_was_present=False)
|
||||
Booking.objects.create(event=event, primary_booking=booking, user_was_present=False)
|
||||
Booking.objects.create(event=event, primary_booking=booking)
|
||||
Booking.objects.create(event=event, primary_booking=booking)
|
||||
# and other booking
|
||||
other_booking = Booking.objects.create(event=event)
|
||||
|
||||
# it works also with slug
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_absence_reason': 'foo-bar'})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_check_type_slug == 'foo-bar'
|
||||
assert booking.user_check_type_label == 'Foo bar'
|
||||
# all secondary bookings are updated
|
||||
assert list(booking.secondary_booking_set.values_list('user_check_type_slug', flat=True)) == [
|
||||
'foo-bar',
|
||||
'foo-bar',
|
||||
]
|
||||
assert booking.user_check.type_slug == 'foo-bar'
|
||||
assert booking.user_check.type_label == 'Foo bar'
|
||||
# secondary bookings are left unchanged
|
||||
assert list(booking.secondary_booking_set.values_list('user_checks', flat=True)) == [None, None]
|
||||
other_booking.refresh_from_db()
|
||||
assert other_booking.user_check_type_slug is None # not changed
|
||||
assert other_booking.user_check_type_label is None # not changed
|
||||
assert not other_booking.user_check # not changed
|
||||
|
||||
# user_was_present is True, can not set user_absence_reason
|
||||
Booking.objects.update(user_was_present=True)
|
||||
# presence is True, can not set user_absence_reason
|
||||
BookingCheck.objects.update(presence=True)
|
||||
resp = app.patch_json(
|
||||
'/api/booking/%s/' % booking.pk, params={'user_absence_reason': 'foo-bar'}, status=400
|
||||
)
|
||||
assert resp.json['err'] == 6
|
||||
assert resp.json['err_desc'] == 'user is marked as present, can not set absence reason'
|
||||
|
||||
# but it's ok if user_was_present is set to False
|
||||
# but it's ok if presence is set to False
|
||||
resp = app.patch_json(
|
||||
'/api/booking/%s/' % booking.pk,
|
||||
params={'user_absence_reason': 'foo-bar', 'user_was_present': False},
|
||||
)
|
||||
assert resp.json['err'] == 0
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_was_present is False
|
||||
assert booking.user_check_type_slug == 'foo-bar'
|
||||
assert booking.user_check_type_label == 'Foo bar'
|
||||
assert booking.user_check.presence is False
|
||||
assert booking.user_check.type_slug == 'foo-bar'
|
||||
assert booking.user_check.type_label == 'Foo bar'
|
||||
|
||||
# mark the event as checked
|
||||
event.checked = True
|
||||
|
@ -756,7 +795,8 @@ def test_booking_patch_api_presence_reason(check_types, app, user):
|
|||
check_types.return_value = []
|
||||
agenda = Agenda.objects.create(kind='events')
|
||||
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
|
||||
booking = Booking.objects.create(event=event, user_was_present=True)
|
||||
booking = Booking.objects.create(event=event)
|
||||
booking.mark_user_presence()
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
|
@ -781,8 +821,8 @@ def test_booking_patch_api_presence_reason(check_types, app, user):
|
|||
# it works with label
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_presence_reason': 'Foo bar'})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_check_type_slug == 'foo-bar'
|
||||
assert booking.user_check_type_label == 'Foo bar'
|
||||
assert booking.user_check.type_slug == 'foo-bar'
|
||||
assert booking.user_check.type_label == 'Foo bar'
|
||||
|
||||
# unknown
|
||||
resp = app.patch_json(
|
||||
|
@ -796,51 +836,47 @@ def test_booking_patch_api_presence_reason(check_types, app, user):
|
|||
# reset
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_presence_reason': ''})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_check_type_slug is None
|
||||
assert booking.user_check_type_label is None
|
||||
assert booking.user_check.type_slug is None
|
||||
assert booking.user_check.type_label is None
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_presence_reason': None})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_check_type_slug is None
|
||||
assert booking.user_check_type_label is None
|
||||
assert booking.user_check.type_slug is None
|
||||
assert booking.user_check.type_label is None
|
||||
|
||||
# make secondary bookings
|
||||
Booking.objects.create(event=event, primary_booking=booking, user_was_present=True)
|
||||
Booking.objects.create(event=event, primary_booking=booking, user_was_present=True)
|
||||
Booking.objects.create(event=event, primary_booking=booking)
|
||||
Booking.objects.create(event=event, primary_booking=booking)
|
||||
# and other booking
|
||||
other_booking = Booking.objects.create(event=event)
|
||||
|
||||
# it works also with slug
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'user_presence_reason': 'foo-bar'})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_check_type_slug == 'foo-bar'
|
||||
assert booking.user_check_type_label == 'Foo bar'
|
||||
# all secondary bookings are updated
|
||||
assert list(booking.secondary_booking_set.values_list('user_check_type_slug', flat=True)) == [
|
||||
'foo-bar',
|
||||
'foo-bar',
|
||||
]
|
||||
assert booking.user_check.type_slug == 'foo-bar'
|
||||
assert booking.user_check.type_label == 'Foo bar'
|
||||
# secondary bookings are left unchanged
|
||||
assert list(booking.secondary_booking_set.values_list('user_checks', flat=True)) == [None, None]
|
||||
other_booking.refresh_from_db()
|
||||
assert other_booking.user_check_type_slug is None # not changed
|
||||
assert other_booking.user_check_type_label is None # not changed
|
||||
assert not other_booking.user_check # not changed
|
||||
|
||||
# user_was_present is False, can not set user_presence_reason
|
||||
Booking.objects.update(user_was_present=False)
|
||||
# presence is False, can not set user_presence_reason
|
||||
BookingCheck.objects.update(presence=False)
|
||||
resp = app.patch_json(
|
||||
'/api/booking/%s/' % booking.pk, params={'user_presence_reason': 'foo-bar'}, status=400
|
||||
)
|
||||
assert resp.json['err'] == 6
|
||||
assert resp.json['err_desc'] == 'user is marked as absent, can not set presence reason'
|
||||
|
||||
# but it's ok if user_was_present is set to True
|
||||
# but it's ok if presence is set to True
|
||||
resp = app.patch_json(
|
||||
'/api/booking/%s/' % booking.pk,
|
||||
params={'user_presence_reason': 'foo-bar', 'user_was_present': True},
|
||||
)
|
||||
assert resp.json['err'] == 0
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_was_present is True
|
||||
assert booking.user_check_type_slug == 'foo-bar'
|
||||
assert booking.user_check_type_label == 'Foo bar'
|
||||
assert booking.user_check.presence is True
|
||||
assert booking.user_check.type_slug == 'foo-bar'
|
||||
assert booking.user_check.type_label == 'Foo bar'
|
||||
|
||||
# mark the event as checked
|
||||
event.checked = True
|
||||
|
@ -971,54 +1007,51 @@ def test_booking_patch_api_user_fields(app, user):
|
|||
def test_booking_patch_api_extra_data(app, user):
|
||||
agenda = Agenda.objects.create(kind='events')
|
||||
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
|
||||
booking = Booking.objects.create(event=event, user_was_present=True)
|
||||
booking = Booking.objects.create(event=event)
|
||||
booking.mark_user_presence()
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'foo': 'bar'})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_was_present is True # not changed
|
||||
assert booking.user_check.presence is True # not changed
|
||||
assert booking.extra_data == {'foo': 'bar'}
|
||||
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'foo': 'baz'})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_was_present is True # not changed
|
||||
assert booking.user_check.presence is True # not changed
|
||||
assert booking.extra_data == {'foo': 'baz'}
|
||||
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'foooo': 'bar'})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_was_present is True # not changed
|
||||
assert booking.user_check.presence is True # not changed
|
||||
assert booking.extra_data == {'foo': 'baz', 'foooo': 'bar'}
|
||||
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'foooo': 'baz', 'foo': None})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_was_present is True # not changed
|
||||
assert booking.user_check.presence is True # not changed
|
||||
assert booking.extra_data == {'foo': None, 'foooo': 'baz'}
|
||||
|
||||
# make secondary bookings
|
||||
Booking.objects.create(event=event, primary_booking=booking, user_was_present=False)
|
||||
Booking.objects.create(event=event, primary_booking=booking, user_was_present=False)
|
||||
Booking.objects.create(event=event, primary_booking=booking)
|
||||
Booking.objects.create(event=event, primary_booking=booking)
|
||||
# and other booking
|
||||
other_booking = Booking.objects.create(event=event)
|
||||
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={'foooo': 'baz', 'foo': None})
|
||||
assert booking.user_was_present is True # not changed
|
||||
assert booking.user_check.presence is True # not changed
|
||||
assert booking.extra_data == {'foo': None, 'foooo': 'baz'}
|
||||
# all secondary bookings are upadted
|
||||
assert list(booking.secondary_booking_set.values_list('extra_data', flat=True)) == [
|
||||
{'foo': None, 'foooo': 'baz'},
|
||||
{'foo': None, 'foooo': 'baz'},
|
||||
]
|
||||
assert list(booking.secondary_booking_set.values_list('user_was_present', flat=True)) == [
|
||||
False,
|
||||
False,
|
||||
] # not changed
|
||||
other_booking.refresh_from_db()
|
||||
assert other_booking.extra_data is None # not changed
|
||||
|
||||
app.patch_json('/api/booking/%s/' % booking.pk, params={})
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_was_present is True # not changed
|
||||
assert booking.user_check.presence is True # not changed
|
||||
assert booking.extra_data == {'foo': None, 'foooo': 'baz'} # not changed
|
||||
|
||||
resp = app.patch_json('/api/booking/%s/' % booking.pk, params={'foo': ['bar', 'baz']}, status=400)
|
||||
|
|
|
@ -201,17 +201,15 @@ def test_event_notify_checked(app, user):
|
|||
assert event.checked is False
|
||||
|
||||
for i in range(8):
|
||||
user_was_present = None
|
||||
if i < 3:
|
||||
user_was_present = True
|
||||
elif i < 7:
|
||||
user_was_present = False
|
||||
Booking.objects.create(
|
||||
booking = Booking.objects.create(
|
||||
event=event,
|
||||
user_was_present=user_was_present,
|
||||
presence_callback_url='https://example.invalid/presence/%s' % i,
|
||||
absence_callback_url='https://example.invalid/absence/%s' % i,
|
||||
)
|
||||
if i < 3:
|
||||
booking.mark_user_presence()
|
||||
elif i < 7:
|
||||
booking.mark_user_absence()
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
|
@ -1265,8 +1263,7 @@ def test_events_check_status(app, user):
|
|||
assert resp.json['data'][0]['booking']['cancellation_datetime'] is None
|
||||
|
||||
# absence
|
||||
booking.user_was_present = False
|
||||
booking.save()
|
||||
booking.mark_user_absence()
|
||||
resp = app.get(url, params=params)
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']) == 1
|
||||
|
@ -1279,8 +1276,8 @@ def test_events_check_status(app, user):
|
|||
assert resp.json['data'][0]['booking']['user_presence_reason'] is None
|
||||
|
||||
# absence with check type
|
||||
booking.user_check_type_slug = 'foo-reason'
|
||||
booking.save()
|
||||
booking.user_check.type_slug = 'foo-reason'
|
||||
booking.user_check.save()
|
||||
resp = app.get(url, params=params)
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']) == 1
|
||||
|
@ -1293,9 +1290,7 @@ def test_events_check_status(app, user):
|
|||
assert resp.json['data'][0]['booking']['user_presence_reason'] is None
|
||||
|
||||
# presence
|
||||
booking.user_check_type_slug = None
|
||||
booking.user_was_present = True
|
||||
booking.save()
|
||||
booking.mark_user_presence()
|
||||
resp = app.get(url, params=params)
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']) == 1
|
||||
|
@ -1308,8 +1303,8 @@ def test_events_check_status(app, user):
|
|||
assert resp.json['data'][0]['booking']['user_presence_reason'] is None
|
||||
|
||||
# presence with check type
|
||||
booking.user_check_type_slug = 'foo-reason'
|
||||
booking.save()
|
||||
booking.user_check.type_slug = 'foo-reason'
|
||||
booking.user_check.save()
|
||||
resp = app.get(url, params=params)
|
||||
assert resp.json['err'] == 0
|
||||
assert len(resp.json['data']) == 1
|
||||
|
@ -1322,8 +1317,9 @@ def test_events_check_status(app, user):
|
|||
assert resp.json['data'][0]['booking']['user_presence_reason'] == 'foo-reason'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('partial_bookings', [True, False])
|
||||
@pytest.mark.freeze_time('2022-05-30 14:00')
|
||||
def test_events_check_status_events(app, user):
|
||||
def test_events_check_status_events(app, user, partial_bookings):
|
||||
events_type = EventsType.objects.create(
|
||||
label='Foo',
|
||||
custom_fields=[
|
||||
|
@ -1332,7 +1328,13 @@ def test_events_check_status_events(app, user):
|
|||
{'varname': 'bool', 'label': 'Bool', 'field_type': 'bool'},
|
||||
],
|
||||
)
|
||||
agenda = Agenda.objects.create(label='Foo', events_type=events_type)
|
||||
agenda = Agenda.objects.create(
|
||||
label='Foo',
|
||||
events_type=events_type,
|
||||
partial_bookings=partial_bookings,
|
||||
invoicing_unit='half_hour',
|
||||
invoicing_tolerance=10,
|
||||
)
|
||||
start_datetime = now()
|
||||
# recurring event
|
||||
recurring_event = Event.objects.create(
|
||||
|
@ -1390,12 +1392,18 @@ def test_events_check_status_events(app, user):
|
|||
booking1 = Booking.objects.create(
|
||||
event=first_event,
|
||||
user_external_id='child:42',
|
||||
user_was_present=True,
|
||||
user_check_type_slug='foo-reason',
|
||||
start_time=datetime.time(8, 0),
|
||||
end_time=datetime.time(17, 0),
|
||||
)
|
||||
booking2 = Booking.objects.create(
|
||||
event=event, user_external_id='child:42', user_was_present=True, user_check_type_slug='foo-reason'
|
||||
booking1.mark_user_presence(
|
||||
check_type_slug='foo-reason', start_time=datetime.time(7, 55), end_time=datetime.time(17, 15)
|
||||
)
|
||||
booking1.user_check.refresh_computed_times()
|
||||
booking1.user_check.save()
|
||||
assert booking1.user_check.computed_start_time == datetime.time(8, 0)
|
||||
assert booking1.user_check.computed_end_time == datetime.time(17, 30)
|
||||
booking2 = Booking.objects.create(event=event, user_external_id='child:42')
|
||||
booking2.mark_user_presence(check_type_slug='foo-reason')
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
url = '/api/agendas/events/check-status/'
|
||||
|
@ -1407,121 +1415,180 @@ def test_events_check_status_events(app, user):
|
|||
}
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
resp = app.get(url, params=params)
|
||||
assert len(ctx.captured_queries) == 5
|
||||
assert len(ctx.captured_queries) == 6
|
||||
assert resp.json['err'] == 0
|
||||
assert resp.json['data'] == [
|
||||
{
|
||||
'event': {
|
||||
'description': None,
|
||||
'duration': None,
|
||||
'label': 'Not Checked Event Label',
|
||||
'slug': 'notchecked-event-slug',
|
||||
'places': 10,
|
||||
'pricing': None,
|
||||
'publication_datetime': None,
|
||||
'recurrence_days': None,
|
||||
'recurrence_end_date': None,
|
||||
'recurrence_week_interval': 1,
|
||||
'start_datetime': localtime(notchecked_event.start_datetime).isoformat(),
|
||||
'url': None,
|
||||
'waiting_list_places': 0,
|
||||
'agenda': agenda.slug,
|
||||
'primary_event': None,
|
||||
'check_locked': False,
|
||||
'checked': False,
|
||||
'invoiced': False,
|
||||
'custom_field_bool': None,
|
||||
'custom_field_text': '',
|
||||
'custom_field_textarea': '',
|
||||
},
|
||||
'check_status': {'error_reason': 'event-not-checked', 'status': 'error'},
|
||||
'booking': {},
|
||||
},
|
||||
{
|
||||
'event': {
|
||||
'description': None,
|
||||
'duration': None,
|
||||
'label': 'Event Label',
|
||||
'slug': 'event-slug',
|
||||
'places': 10,
|
||||
'pricing': None,
|
||||
'publication_datetime': None,
|
||||
'recurrence_days': None,
|
||||
'recurrence_end_date': None,
|
||||
'recurrence_week_interval': 1,
|
||||
'start_datetime': localtime(event.start_datetime).isoformat(),
|
||||
'url': None,
|
||||
'waiting_list_places': 0,
|
||||
'agenda': agenda.slug,
|
||||
'primary_event': None,
|
||||
'check_locked': True,
|
||||
'checked': True,
|
||||
'invoiced': True,
|
||||
'custom_field_bool': None,
|
||||
'custom_field_text': '',
|
||||
'custom_field_textarea': '',
|
||||
},
|
||||
'check_status': {'check_type': 'foo-reason', 'status': 'presence'},
|
||||
'booking': {
|
||||
'cancellation_datetime': None,
|
||||
'use_color_for': None,
|
||||
'creation_datetime': localtime(now()).isoformat(),
|
||||
'extra_data': None,
|
||||
'id': booking2.pk,
|
||||
'in_waiting_list': False,
|
||||
'user_absence_reason': None,
|
||||
'user_email': '',
|
||||
'user_first_name': '',
|
||||
'user_last_name': '',
|
||||
'user_phone_number': '',
|
||||
'user_presence_reason': 'foo-reason',
|
||||
'user_was_present': True,
|
||||
'label': '',
|
||||
},
|
||||
},
|
||||
{
|
||||
'event': {
|
||||
'description': None,
|
||||
'duration': None,
|
||||
'label': 'Recurring Event Label',
|
||||
'slug': 'recurring-event-slug--2022-05-30-1600',
|
||||
'places': 10,
|
||||
'pricing': None,
|
||||
'publication_datetime': None,
|
||||
'recurrence_days': None,
|
||||
'recurrence_end_date': None,
|
||||
'recurrence_week_interval': 1,
|
||||
'start_datetime': localtime(first_event.start_datetime).isoformat(),
|
||||
'url': None,
|
||||
'waiting_list_places': 0,
|
||||
'agenda': agenda.slug,
|
||||
'primary_event': recurring_event.slug,
|
||||
'check_locked': False,
|
||||
'checked': True,
|
||||
'invoiced': False,
|
||||
'custom_field_text': 'foo',
|
||||
'custom_field_textarea': 'foo bar',
|
||||
'custom_field_bool': True,
|
||||
},
|
||||
'check_status': {'check_type': 'foo-reason', 'status': 'presence'},
|
||||
'booking': {
|
||||
'cancellation_datetime': None,
|
||||
'use_color_for': None,
|
||||
'creation_datetime': localtime(now()).isoformat(),
|
||||
'extra_data': None,
|
||||
'id': booking1.pk,
|
||||
'in_waiting_list': False,
|
||||
'user_absence_reason': None,
|
||||
'user_email': '',
|
||||
'user_first_name': '',
|
||||
'user_last_name': '',
|
||||
'user_phone_number': '',
|
||||
'user_presence_reason': 'foo-reason',
|
||||
'user_was_present': True,
|
||||
'label': '',
|
||||
},
|
||||
},
|
||||
]
|
||||
assert len(resp.json['data']) == 3
|
||||
assert list(resp.json['data'][0].keys()) == ['event', 'check_status', 'booking']
|
||||
assert resp.json['data'][0]['event'] == {
|
||||
'description': None,
|
||||
'duration': None,
|
||||
'label': 'Not Checked Event Label',
|
||||
'slug': 'notchecked-event-slug',
|
||||
'places': 10,
|
||||
'pricing': None,
|
||||
'publication_datetime': None,
|
||||
'recurrence_days': None,
|
||||
'recurrence_end_date': None,
|
||||
'recurrence_week_interval': 1,
|
||||
'start_datetime': localtime(notchecked_event.start_datetime).isoformat(),
|
||||
'url': None,
|
||||
'waiting_list_places': 0,
|
||||
'agenda': agenda.slug,
|
||||
'primary_event': None,
|
||||
'check_locked': False,
|
||||
'checked': False,
|
||||
'invoiced': False,
|
||||
'custom_field_bool': None,
|
||||
'custom_field_text': '',
|
||||
'custom_field_textarea': '',
|
||||
}
|
||||
assert resp.json['data'][0]['check_status'] == {
|
||||
'error_reason': 'event-not-checked',
|
||||
'status': 'error',
|
||||
}
|
||||
assert resp.json['data'][0]['booking'] == {}
|
||||
assert list(resp.json['data'][1].keys()) == ['event', 'check_status', 'booking']
|
||||
assert resp.json['data'][1]['event'] == {
|
||||
'description': None,
|
||||
'duration': None,
|
||||
'label': 'Event Label',
|
||||
'slug': 'event-slug',
|
||||
'places': 10,
|
||||
'pricing': None,
|
||||
'publication_datetime': None,
|
||||
'recurrence_days': None,
|
||||
'recurrence_end_date': None,
|
||||
'recurrence_week_interval': 1,
|
||||
'start_datetime': localtime(event.start_datetime).isoformat(),
|
||||
'url': None,
|
||||
'waiting_list_places': 0,
|
||||
'agenda': agenda.slug,
|
||||
'primary_event': None,
|
||||
'check_locked': True,
|
||||
'checked': True,
|
||||
'invoiced': True,
|
||||
'custom_field_bool': None,
|
||||
'custom_field_text': '',
|
||||
'custom_field_textarea': '',
|
||||
}
|
||||
assert resp.json['data'][1]['check_status'] == {
|
||||
'check_type': 'foo-reason',
|
||||
'status': 'presence',
|
||||
}
|
||||
if partial_bookings:
|
||||
assert resp.json['data'][1]['booking'] == {
|
||||
'cancellation_datetime': None,
|
||||
'use_color_for': None,
|
||||
'creation_datetime': localtime(now()).isoformat(),
|
||||
'extra_data': None,
|
||||
'id': booking2.pk,
|
||||
'in_waiting_list': False,
|
||||
'user_absence_reason': None,
|
||||
'user_email': '',
|
||||
'user_first_name': '',
|
||||
'user_last_name': '',
|
||||
'user_phone_number': '',
|
||||
'user_presence_reason': 'foo-reason',
|
||||
'user_was_present': True,
|
||||
'label': '',
|
||||
'start_time': None,
|
||||
'end_time': None,
|
||||
'duration': None,
|
||||
'user_check_start_time': None,
|
||||
'user_check_end_time': None,
|
||||
'user_check_duration': None,
|
||||
'computed_start_time': None,
|
||||
'computed_end_time': None,
|
||||
'computed_duration': None,
|
||||
}
|
||||
else:
|
||||
assert resp.json['data'][1]['booking'] == {
|
||||
'cancellation_datetime': None,
|
||||
'use_color_for': None,
|
||||
'creation_datetime': localtime(now()).isoformat(),
|
||||
'extra_data': None,
|
||||
'id': booking2.pk,
|
||||
'in_waiting_list': False,
|
||||
'user_absence_reason': None,
|
||||
'user_email': '',
|
||||
'user_first_name': '',
|
||||
'user_last_name': '',
|
||||
'user_phone_number': '',
|
||||
'user_presence_reason': 'foo-reason',
|
||||
'user_was_present': True,
|
||||
'label': '',
|
||||
}
|
||||
assert list(resp.json['data'][2].keys()) == ['event', 'check_status', 'booking']
|
||||
assert resp.json['data'][2]['event'] == {
|
||||
'description': None,
|
||||
'duration': None,
|
||||
'label': 'Recurring Event Label',
|
||||
'slug': 'recurring-event-slug--2022-05-30-1600',
|
||||
'places': 10,
|
||||
'pricing': None,
|
||||
'publication_datetime': None,
|
||||
'recurrence_days': None,
|
||||
'recurrence_end_date': None,
|
||||
'recurrence_week_interval': 1,
|
||||
'start_datetime': localtime(first_event.start_datetime).isoformat(),
|
||||
'url': None,
|
||||
'waiting_list_places': 0,
|
||||
'agenda': agenda.slug,
|
||||
'primary_event': recurring_event.slug,
|
||||
'check_locked': False,
|
||||
'checked': True,
|
||||
'invoiced': False,
|
||||
'custom_field_text': 'foo',
|
||||
'custom_field_textarea': 'foo bar',
|
||||
'custom_field_bool': True,
|
||||
}
|
||||
assert resp.json['data'][2]['check_status'] == {
|
||||
'check_type': 'foo-reason',
|
||||
'status': 'presence',
|
||||
}
|
||||
if partial_bookings:
|
||||
assert resp.json['data'][2]['booking'] == {
|
||||
'cancellation_datetime': None,
|
||||
'use_color_for': None,
|
||||
'creation_datetime': localtime(now()).isoformat(),
|
||||
'extra_data': None,
|
||||
'id': booking1.pk,
|
||||
'in_waiting_list': False,
|
||||
'user_absence_reason': None,
|
||||
'user_email': '',
|
||||
'user_first_name': '',
|
||||
'user_last_name': '',
|
||||
'user_phone_number': '',
|
||||
'user_presence_reason': 'foo-reason',
|
||||
'user_was_present': True,
|
||||
'label': '',
|
||||
'start_time': '08:00:00',
|
||||
'end_time': '17:00:00',
|
||||
'duration': 540,
|
||||
'user_check_start_time': '07:55:00',
|
||||
'user_check_end_time': '17:15:00',
|
||||
'user_check_duration': 560,
|
||||
'computed_start_time': '08:00:00',
|
||||
'computed_end_time': '17:30:00',
|
||||
'computed_duration': 570,
|
||||
}
|
||||
else:
|
||||
assert resp.json['data'][2]['booking'] == {
|
||||
'cancellation_datetime': None,
|
||||
'use_color_for': None,
|
||||
'creation_datetime': localtime(now()).isoformat(),
|
||||
'extra_data': None,
|
||||
'id': booking1.pk,
|
||||
'in_waiting_list': False,
|
||||
'user_absence_reason': None,
|
||||
'user_email': '',
|
||||
'user_first_name': '',
|
||||
'user_last_name': '',
|
||||
'user_phone_number': '',
|
||||
'user_presence_reason': 'foo-reason',
|
||||
'user_was_present': True,
|
||||
'label': '',
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2022-05-30 14:00')
|
||||
|
@ -1770,9 +1837,12 @@ def test_events_check_lock_params(app, user):
|
|||
assert 'wrong format' in resp.json['errors']['date_end'][0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('partial_bookings', [True, False])
|
||||
@pytest.mark.freeze_time('2022-05-30 14:00')
|
||||
def test_events_check_lock(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo')
|
||||
def test_events_check_lock(app, user, partial_bookings):
|
||||
agenda = Agenda.objects.create(label='Foo', partial_bookings=partial_bookings)
|
||||
assert agenda.invoicing_unit == 'hour'
|
||||
assert agenda.invoicing_tolerance == 0
|
||||
event = Event.objects.create(
|
||||
slug='event-slug',
|
||||
label='Event Label',
|
||||
|
@ -1781,6 +1851,20 @@ def test_events_check_lock(app, user):
|
|||
agenda=agenda,
|
||||
checked=True,
|
||||
)
|
||||
booking = Booking.objects.create(
|
||||
event=event,
|
||||
user_external_id='child:42',
|
||||
start_time=datetime.time(8, 0),
|
||||
end_time=datetime.time(17, 0),
|
||||
)
|
||||
booking.mark_user_presence(
|
||||
check_type_slug='foo-reason',
|
||||
start_time=datetime.time(7, 55),
|
||||
end_time=datetime.time(17, 15),
|
||||
)
|
||||
# computed times are None, because refresh_computed_times was not called in this test
|
||||
assert booking.user_check.computed_start_time is None
|
||||
assert booking.user_check.computed_end_time is None
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
url = '/api/agendas/events/check-lock/'
|
||||
|
@ -1794,12 +1878,24 @@ def test_events_check_lock(app, user):
|
|||
assert resp.json['err'] == 0
|
||||
event.refresh_from_db()
|
||||
assert event.check_locked is True
|
||||
booking.refresh_from_db()
|
||||
# computed times are still None, refresh_computed_times is not called on lock
|
||||
assert booking.user_check.computed_start_time is None
|
||||
assert booking.user_check.computed_end_time is None
|
||||
|
||||
params['check_locked'] = False
|
||||
resp = app.post_json(url, params=params)
|
||||
assert resp.json['err'] == 0
|
||||
event.refresh_from_db()
|
||||
assert event.check_locked is False
|
||||
booking.refresh_from_db()
|
||||
if partial_bookings:
|
||||
assert booking.user_check.computed_start_time == datetime.time(7, 0)
|
||||
assert booking.user_check.computed_end_time == datetime.time(18, 0)
|
||||
else:
|
||||
# not refreshed, not a partial_bookings agenda
|
||||
assert booking.user_check.computed_start_time is None
|
||||
assert booking.user_check.computed_end_time is None
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2022-05-30 14:00')
|
||||
|
|
|
@ -121,32 +121,42 @@ def test_statistics_bookings(app, user, freezer):
|
|||
|
||||
# absence/presence
|
||||
for i in range(10):
|
||||
Booking.objects.create(event=event3, user_was_present=bool(i % 2))
|
||||
booking = Booking.objects.create(event=event3)
|
||||
if i % 2:
|
||||
booking.mark_user_presence()
|
||||
else:
|
||||
booking.mark_user_absence()
|
||||
|
||||
event4 = Event.objects.create(start_datetime=now().replace(month=11, day=1), places=5, agenda=agenda)
|
||||
Booking.objects.create(event=event4, user_was_present=True)
|
||||
booking = Booking.objects.create(event=event4)
|
||||
booking.mark_user_presence()
|
||||
Booking.objects.create(event=event4, primary_booking=booking)
|
||||
|
||||
resp = app.get(url + '?group_by=user_was_present')
|
||||
assert resp.json['data']['x_labels'] == ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01']
|
||||
assert resp.json['data']['series'] == [
|
||||
{'label': 'Absent', 'data': [None, None, 5, None]},
|
||||
{'label': 'Booked', 'data': [10, 1, 1, None]},
|
||||
{'label': 'Present', 'data': [None, None, 5, 1]},
|
||||
{'label': 'Present', 'data': [None, None, 5, 2]},
|
||||
]
|
||||
|
||||
for i in range(9):
|
||||
Booking.objects.create(
|
||||
event=event3 if i % 2 else event4, extra_data={'menu': 'vegetables'}, user_was_present=bool(i % 3)
|
||||
)
|
||||
booking = Booking.objects.create(event=event3 if i % 2 else event4, extra_data={'menu': 'vegetables'})
|
||||
if i % 3:
|
||||
booking.mark_user_presence()
|
||||
else:
|
||||
booking.mark_user_absence()
|
||||
for i in range(5):
|
||||
Booking.objects.create(
|
||||
event=event3 if i % 2 else event4, extra_data={'menu': 'meet'}, user_was_present=bool(i % 3)
|
||||
)
|
||||
booking = Booking.objects.create(event=event3 if i % 2 else event4, extra_data={'menu': 'meet'})
|
||||
if i % 3:
|
||||
booking.mark_user_presence()
|
||||
else:
|
||||
booking.mark_user_absence()
|
||||
|
||||
resp = app.get(url + '?group_by=menu')
|
||||
assert resp.json['data']['x_labels'] == ['2020-10-10', '2020-10-15', '2020-10-25', '2020-11-01']
|
||||
assert resp.json['data']['series'] == [
|
||||
{'label': 'None', 'data': [10, 1, 11, 1]},
|
||||
{'label': 'None', 'data': [10, 1, 11, 2]},
|
||||
{'label': 'meet', 'data': [None, None, 2, 3]},
|
||||
{'label': 'vegetables', 'data': [None, None, 4, 5]},
|
||||
]
|
||||
|
@ -158,7 +168,7 @@ def test_statistics_bookings(app, user, freezer):
|
|||
{'label': 'Absent / meet', 'data': [None, None, 1, 1]},
|
||||
{'label': 'Absent / vegetables', 'data': [None, None, 1, 2]},
|
||||
{'label': 'Booked / None', 'data': [10, 1, 1, None]},
|
||||
{'label': 'Present / None', 'data': [None, None, 5, 1]},
|
||||
{'label': 'Present / None', 'data': [None, None, 5, 2]},
|
||||
{'label': 'Present / meet', 'data': [None, None, 1, 2]},
|
||||
{'label': 'Present / vegetables', 'data': [None, None, 3, 3]},
|
||||
]
|
||||
|
|
|
@ -3243,6 +3243,16 @@ def test_duplicate_agenda(app, admin_user):
|
|||
assert 'hop' in resp.text
|
||||
|
||||
|
||||
def test_duplicate_agenda_as_manager(app, manager_user):
|
||||
agenda = Agenda(label='Foo bar')
|
||||
agenda.edit_role = manager_user.groups.all()[0]
|
||||
agenda.save()
|
||||
app = login(app, username='manager', password='manager')
|
||||
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
|
||||
assert '/manage/agendas/%s/duplicate' % agenda.pk not in resp
|
||||
app.get('/manage/agendas/%s/duplicate' % agenda.pk, status=403)
|
||||
|
||||
|
||||
def test_booking_cancellation_meetings_agenda(app, admin_user, manager_user, managers_group, api_user):
|
||||
agenda = Agenda.objects.create(label='Passeports', kind='meetings', view_role=managers_group)
|
||||
desk = Desk.objects.create(agenda=agenda, label='Desk A')
|
||||
|
@ -3794,6 +3804,13 @@ def test_agenda_booking_colors(app, admin_user, api_user, view):
|
|||
assert 'Booking colors:' in resp.text
|
||||
assert len(resp.pyquery.find('div.booking-colors span.booking-color-label')) == 2
|
||||
|
||||
new_booking.cancel()
|
||||
resp = app.get(url)
|
||||
assert len(resp.pyquery.find('div.booking')) == 3
|
||||
assert len(resp.pyquery.find('div.booking.booking-color-%s' % new_booking.color.index)) == 0
|
||||
assert resp.text.count('Swimming') == 0 # no booking and no legend
|
||||
assert len(resp.pyquery.find('div.booking-colors span.booking-color-label')) == 1
|
||||
|
||||
|
||||
@freezegun.freeze_time('2022-03-01 14:00')
|
||||
def test_agenda_day_and_month_views_weekday_indexes(app, admin_user):
|
||||
|
|
|
@ -798,7 +798,8 @@ def test_import_events(app, admin_user):
|
|||
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
|
||||
resp.form['events_csv_file'] = Upload('t.csv', b'xx', 'text/csv')
|
||||
resp = resp.form.submit(status=200)
|
||||
assert 'Invalid file format.' in resp.text
|
||||
assert 'Invalid file format:' in resp.text
|
||||
assert 'Wrong start date/time format. (1st event)' in resp.text
|
||||
|
||||
resp.form['events_csv_file'] = Upload('t.csv', b'xxxx\0\0xxxx', 'text/csv')
|
||||
resp = resp.form.submit(status=200)
|
||||
|
@ -806,15 +807,16 @@ def test_import_events(app, admin_user):
|
|||
|
||||
resp.form['events_csv_file'] = Upload('t.csv', b'2016-14-16,18:00', 'text/csv')
|
||||
resp = resp.form.submit(status=200)
|
||||
assert 'Invalid file format.' in resp.text
|
||||
assert 'Invalid file format:' in resp.text
|
||||
assert 'Not enough columns. (1st event)' in resp.text
|
||||
|
||||
resp.form['events_csv_file'] = Upload('t.csv', b'date,time,etc.\n2016-14-16,18:00,10', 'text/csv')
|
||||
resp = resp.form.submit(status=200)
|
||||
assert 'Invalid file format. (date/time format, 1st event)' in resp.text
|
||||
assert 'Wrong start date/time format. (1st event)' in resp.text
|
||||
|
||||
resp.form['events_csv_file'] = Upload('t.csv', b'2016-14-16,18:00,10', 'text/csv')
|
||||
resp = resp.form.submit(status=200)
|
||||
assert 'Invalid file format. (date/time format, 1st event)' in resp.text
|
||||
assert 'Wrong start date/time format. (1st event)' in resp.text
|
||||
|
||||
with override_settings(LANGUAGE_CODE='fr-fr'):
|
||||
resp.form['events_csv_file'] = Upload('t.csv', b'2016-14-16,18:00,10', 'text/csv')
|
||||
|
@ -824,11 +826,11 @@ def test_import_events(app, admin_user):
|
|||
|
||||
resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,blah', 'text/csv')
|
||||
resp = resp.form.submit(status=200)
|
||||
assert 'Invalid file format. (number of places,' in resp.text
|
||||
assert 'Number of places must be an integer. (1st event)' in resp.text
|
||||
|
||||
resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,blah', 'text/csv')
|
||||
resp = resp.form.submit(status=200)
|
||||
assert 'Invalid file format. (number of places in waiting list,' in resp.text
|
||||
assert 'Number of places in waiting list must be an integer. (1st event)' in resp.text
|
||||
|
||||
resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,5,' + b'x' * 151, 'text/csv')
|
||||
resp = resp.form.submit(status=200)
|
||||
|
@ -988,7 +990,7 @@ def test_import_events(app, admin_user):
|
|||
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,foobar', 'text/csv'
|
||||
)
|
||||
resp = resp.form.submit(status=200)
|
||||
assert 'Invalid file format. (date/time format' in resp.text
|
||||
assert 'Wrong publication date/time format. (1st event)' in resp.text
|
||||
|
||||
# duration bad format
|
||||
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
|
||||
|
@ -996,7 +998,7 @@ def test_import_events(app, admin_user):
|
|||
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,2016-09-16,foobar', 'text/csv'
|
||||
)
|
||||
resp = resp.form.submit(status=200)
|
||||
assert 'Invalid file format. (duration' in resp.text
|
||||
assert 'Duration must be an integer. (1st event)' in resp.text
|
||||
|
||||
# import events with empty slugs
|
||||
Event.objects.all().delete()
|
||||
|
@ -1027,8 +1029,7 @@ def test_import_events(app, admin_user):
|
|||
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
|
||||
resp.form['events_csv_file'] = Upload('t.csv', b'2016-09-16,18:00,10,5,label,1234', 'text/csv')
|
||||
resp = resp.form.submit(status=200)
|
||||
assert 'value cannot be a number' in resp.text
|
||||
assert 'Identifier:' in resp.text # verbose_name is shown, not field name ('slug:')
|
||||
assert 'Identifier: This value cannot be a number. (1st event)' in resp.text
|
||||
|
||||
|
||||
def test_import_event_nested_quotes(app, admin_user):
|
||||
|
@ -1192,7 +1193,7 @@ def test_import_events_partial_bookings(app, admin_user):
|
|||
't.csv', b'2016-09-16,18:00,10,5,label,slug,description,pricing,url,2016-09-16', 'text/csv'
|
||||
)
|
||||
resp = resp.form.submit(status=200)
|
||||
assert 'Invalid file format. (missing end_time' in resp.text
|
||||
assert 'Missing end_time.' in resp.text
|
||||
|
||||
# invalid end time
|
||||
resp = app.get('/manage/agendas/%s/import-events' % agenda.pk)
|
||||
|
@ -1203,6 +1204,27 @@ def test_import_events_partial_bookings(app, admin_user):
|
|||
assert '“xxx” value has an invalid format' in resp.text
|
||||
|
||||
|
||||
def test_import_events_multiple_errors(app, admin_user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
|
||||
app = login(app)
|
||||
resp = app.get('/manage/agendas/%s/import-events' % agenda.pk)
|
||||
resp.form['events_csv_file'] = Upload(
|
||||
't.csv',
|
||||
b'2016-09-17,18:00,10,5,label,slug\n' # valid event
|
||||
b'2016-09-17,19:00,xxx,5,label2,slug2\n' # invalid places
|
||||
b'2016-09-17,20:00,10,5,,1234\n', # invalid slug
|
||||
'text/csv',
|
||||
)
|
||||
resp = resp.form.submit(status=200)
|
||||
assert [x.text for x in resp.pyquery('.errorlist li')] == [
|
||||
'Invalid file format:',
|
||||
'Number of places must be an integer. (2nd event)',
|
||||
'Identifier: This value cannot be a number. (3rd event)',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2022-05-24')
|
||||
def test_event_detail(app, admin_user):
|
||||
agenda = Agenda.objects.create(label='Events', kind='events')
|
||||
|
@ -1241,11 +1263,19 @@ def test_event_detail_redirect(app, admin_user):
|
|||
waiting_list_places=2,
|
||||
agenda=agenda,
|
||||
)
|
||||
day = localtime(event.start_datetime)
|
||||
|
||||
login(app)
|
||||
resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.slug, event.slug), status=302)
|
||||
assert resp.location.endswith('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk))
|
||||
|
||||
agenda.partial_bookings = True
|
||||
agenda.save()
|
||||
resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.slug, event.slug), status=302)
|
||||
assert resp.location.endswith(
|
||||
'/manage/agendas/%s/day/%d/%02d/%02d/' % (agenda.pk, day.year, day.month, day.day)
|
||||
)
|
||||
|
||||
|
||||
def test_event_cancellation(app, admin_user):
|
||||
agenda = Agenda.objects.create(label='Events', kind='events')
|
||||
|
@ -1712,6 +1742,14 @@ def test_event_check(app, admin_user):
|
|||
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)
|
||||
|
||||
# partial bookings
|
||||
event.cancellation_datetime = None
|
||||
event.save()
|
||||
agenda.partial_bookings = True
|
||||
agenda.save()
|
||||
app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk), status=404)
|
||||
app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), status=404)
|
||||
|
||||
|
||||
def test_event_checked(app, admin_user):
|
||||
agenda = Agenda.objects.create(label='Events', kind='events', booking_check_filters='foo,bar')
|
||||
|
@ -1727,15 +1765,12 @@ def test_event_checked(app, admin_user):
|
|||
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
|
||||
assert 'Mark the event as checked' not in resp
|
||||
for i in range(8):
|
||||
user_was_present = None
|
||||
booking = Booking.objects.create(event=event)
|
||||
if i < 3:
|
||||
user_was_present = True
|
||||
booking.mark_user_presence()
|
||||
elif i < 7:
|
||||
user_was_present = False
|
||||
Booking.objects.create(
|
||||
event=event,
|
||||
user_was_present=user_was_present,
|
||||
)
|
||||
booking.mark_user_absence()
|
||||
|
||||
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
|
||||
assert 'Mark the event as checked' in resp
|
||||
assert event.checked is False
|
||||
|
@ -1772,7 +1807,8 @@ def test_event_checked(app, admin_user):
|
|||
assert 'Mark the event as checked' not in resp
|
||||
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
|
||||
assert 'checked tag' in resp
|
||||
Booking.objects.filter(user_was_present__isnull=True).update(user_was_present=True)
|
||||
for booking in Booking.objects.filter(user_checks__isnull=True):
|
||||
booking.mark_user_presence()
|
||||
for url in urls:
|
||||
resp = app.get(url)
|
||||
assert '<span class="checked tag">Checked</span>' in resp
|
||||
|
@ -1847,17 +1883,16 @@ def test_event_notify_checked(app, admin_user):
|
|||
login(app)
|
||||
|
||||
for i in range(8):
|
||||
user_was_present = None
|
||||
if i < 3:
|
||||
user_was_present = True
|
||||
elif i < 7:
|
||||
user_was_present = False
|
||||
Booking.objects.create(
|
||||
booking = Booking.objects.create(
|
||||
event=event,
|
||||
user_was_present=user_was_present,
|
||||
presence_callback_url='https://example.invalid/presence/%s' % i,
|
||||
absence_callback_url='https://example.invalid/absence/%s' % i,
|
||||
)
|
||||
if i < 3:
|
||||
booking.mark_user_presence()
|
||||
elif i < 7:
|
||||
booking.mark_user_absence()
|
||||
|
||||
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
|
||||
|
||||
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
|
||||
|
@ -1910,22 +1945,22 @@ def test_event_check_filters(check_types, app, admin_user):
|
|||
user_last_name='empty',
|
||||
extra_data={},
|
||||
)
|
||||
Booking.objects.create(
|
||||
booking = Booking.objects.create(
|
||||
event=event,
|
||||
user_external_id='user:1',
|
||||
user_first_name='User',
|
||||
user_last_name='foo-val1 bar-none presence',
|
||||
extra_data={'foo': 'val1', 'bar': ['val1']}, # bar is ignored, wrong value
|
||||
user_was_present=True,
|
||||
)
|
||||
Booking.objects.create(
|
||||
booking.mark_user_presence()
|
||||
booking = Booking.objects.create(
|
||||
event=event,
|
||||
user_external_id='user:2',
|
||||
user_first_name='User',
|
||||
user_last_name='foo-val2 bar-val1 absence',
|
||||
extra_data={'foo': 'val2', 'bar': 'val1'},
|
||||
user_was_present=False,
|
||||
)
|
||||
booking.mark_user_absence()
|
||||
Booking.objects.create(
|
||||
event=event,
|
||||
user_external_id='user:3',
|
||||
|
@ -1933,44 +1968,42 @@ def test_event_check_filters(check_types, app, admin_user):
|
|||
user_last_name='foo-val1 bar-val2 not-checked',
|
||||
extra_data={'foo': 'val1', 'bar': 'val2'},
|
||||
)
|
||||
Booking.objects.create(
|
||||
booking = Booking.objects.create(
|
||||
event=event,
|
||||
user_external_id='user:4',
|
||||
user_first_name='User',
|
||||
user_last_name='foo-none bar-val2 reason-foo',
|
||||
extra_data={'bar': 'val2', 'foo': None}, # foo is ignored, empty value
|
||||
user_was_present=False,
|
||||
user_check_type_slug='foo-reason',
|
||||
)
|
||||
Booking.objects.create(
|
||||
booking.mark_user_absence(check_type_slug='foo-reason')
|
||||
booking = Booking.objects.create(
|
||||
event=event,
|
||||
user_external_id='user:5',
|
||||
user_first_name='User',
|
||||
user_last_name='foo-none bar-val2 reason-bar',
|
||||
extra_data={'bar': 'val2', 'foo': ''}, # foo is ignored, empty value
|
||||
user_was_present=True,
|
||||
user_check_type_slug='bar-reason',
|
||||
)
|
||||
Booking.objects.create(
|
||||
booking.mark_user_presence(check_type_slug='bar-reason')
|
||||
booking = Booking.objects.create(
|
||||
event=event,
|
||||
user_external_id='user:6',
|
||||
user_first_name='User',
|
||||
user_last_name='foo-none bar-val2 cancelled-absence',
|
||||
extra_data={'bar': 'val2'},
|
||||
user_was_present=False,
|
||||
user_check_type_slug='foo-reason',
|
||||
cancellation_datetime=now(),
|
||||
)
|
||||
Booking.objects.create(
|
||||
booking.mark_user_absence(check_type_slug='foo-reason')
|
||||
booking.cancellation_datetime = now()
|
||||
booking.save()
|
||||
booking = Booking.objects.create(
|
||||
event=event,
|
||||
user_external_id='user:7',
|
||||
user_first_name='User',
|
||||
user_last_name='foo-none bar-val2 cancelled-presence',
|
||||
extra_data={'bar': 'val2'},
|
||||
user_was_present=True,
|
||||
user_check_type_slug='bar-reason',
|
||||
cancellation_datetime=now(),
|
||||
)
|
||||
booking.mark_user_presence(check_type_slug='bar-reason')
|
||||
booking.cancellation_datetime = now()
|
||||
booking.save()
|
||||
|
||||
Subscription.objects.create(
|
||||
agenda=agenda,
|
||||
|
@ -2054,7 +2087,7 @@ def test_event_check_filters(check_types, app, admin_user):
|
|||
resp = app.get(
|
||||
'/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk), params={'extra-data-foo': 'val1'}
|
||||
)
|
||||
assert len(ctx.captured_queries) == 10
|
||||
assert len(ctx.captured_queries) == 11
|
||||
assert 'User none' not in resp
|
||||
assert 'User empty' not in resp
|
||||
assert 'User foo-val1 bar-none presence' in resp
|
||||
|
@ -2330,13 +2363,9 @@ def test_event_check_booking(check_types, app, admin_user):
|
|||
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
|
||||
assert not booking.user_check
|
||||
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
|
||||
assert not secondary_booking.user_check
|
||||
event.refresh_from_db()
|
||||
assert event.checked is False
|
||||
|
||||
|
@ -2361,13 +2390,11 @@ def test_event_check_booking(check_types, app, admin_user):
|
|||
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
|
||||
assert booking.user_check_type_label is None
|
||||
assert booking.user_check.presence is True
|
||||
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 True
|
||||
assert secondary_booking.user_check_type_slug is None
|
||||
assert secondary_booking.user_check_type_label is None
|
||||
assert not secondary_booking.user_check
|
||||
event.refresh_from_db()
|
||||
assert event.checked is False
|
||||
|
||||
|
@ -2386,13 +2413,11 @@ def test_event_check_booking(check_types, app, admin_user):
|
|||
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.user_was_present is False
|
||||
assert booking.user_check_type_slug is None
|
||||
assert booking.user_check_type_label is None
|
||||
assert booking.user_check.presence is False
|
||||
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 False
|
||||
assert secondary_booking.user_check_type_slug is None
|
||||
assert secondary_booking.user_check_type_label is None
|
||||
assert not secondary_booking.user_check
|
||||
event.refresh_from_db()
|
||||
assert event.checked is True
|
||||
|
||||
|
@ -2413,13 +2438,11 @@ def test_event_check_booking(check_types, app, admin_user):
|
|||
).follow()
|
||||
assert 'Foo reason' in resp
|
||||
booking.refresh_from_db()
|
||||
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.user_check.presence is False
|
||||
assert booking.user_check.type_slug == 'foo-reason'
|
||||
assert booking.user_check.type_label == 'Foo reason'
|
||||
secondary_booking.refresh_from_db()
|
||||
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'
|
||||
assert not secondary_booking.user_check
|
||||
event.refresh_from_db()
|
||||
assert event.checked is True
|
||||
|
||||
|
@ -2435,13 +2458,11 @@ def test_event_check_booking(check_types, app, admin_user):
|
|||
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.user_was_present is True
|
||||
assert booking.user_check_type_slug is None
|
||||
assert booking.user_check_type_label is None
|
||||
assert booking.user_check.presence is True
|
||||
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 True
|
||||
assert secondary_booking.user_check_type_slug is None
|
||||
assert secondary_booking.user_check_type_label is None
|
||||
assert not secondary_booking.user_check
|
||||
event.refresh_from_db()
|
||||
assert event.checked is True
|
||||
|
||||
|
@ -2467,13 +2488,11 @@ def test_event_check_booking(check_types, app, admin_user):
|
|||
).follow()
|
||||
assert 'Bar reason' in resp
|
||||
booking.refresh_from_db()
|
||||
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.user_check.presence is True
|
||||
assert booking.user_check.type_slug == 'bar-reason'
|
||||
assert booking.user_check.type_label == 'Bar reason'
|
||||
secondary_booking.refresh_from_db()
|
||||
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'
|
||||
assert not secondary_booking.user_check
|
||||
event.refresh_from_db()
|
||||
assert event.checked is True
|
||||
|
||||
|
@ -2649,10 +2668,10 @@ def test_event_check_cancelled_booking(check_types, app, admin_user):
|
|||
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
|
||||
assert booking.user_check.presence is True
|
||||
secondary_booking.refresh_from_db()
|
||||
assert secondary_booking.cancellation_datetime is None
|
||||
assert secondary_booking.user_was_present is True
|
||||
assert not secondary_booking.user_check
|
||||
|
||||
booking.cancel()
|
||||
resp = app.post(
|
||||
|
@ -2664,10 +2683,10 @@ def test_event_check_cancelled_booking(check_types, app, admin_user):
|
|||
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
|
||||
assert booking.user_check.presence is False
|
||||
secondary_booking.refresh_from_db()
|
||||
assert secondary_booking.cancellation_datetime is None
|
||||
assert secondary_booking.user_was_present is False
|
||||
assert not secondary_booking.user_check
|
||||
|
||||
|
||||
@mock.patch('chrono.manager.forms.get_agenda_check_types')
|
||||
|
@ -2809,9 +2828,9 @@ def test_event_check_subscription(check_types, app, admin_user):
|
|||
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.user_check.presence 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
|
||||
|
@ -2831,9 +2850,9 @@ def test_event_check_subscription(check_types, app, admin_user):
|
|||
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.user_check.presence 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
|
||||
|
@ -3106,9 +3125,9 @@ def test_event_check_all_bookings(check_types, app, admin_user):
|
|||
'/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token}
|
||||
)
|
||||
booking1.refresh_from_db()
|
||||
assert booking1.user_was_present is False
|
||||
assert booking1.user_check_type_slug is None
|
||||
assert booking1.user_check_type_label is None
|
||||
assert booking1.user_check.presence is False
|
||||
assert booking1.user_check.type_slug is None
|
||||
assert booking1.user_check.type_label is None
|
||||
event.refresh_from_db()
|
||||
assert event.checked is False
|
||||
|
||||
|
@ -3133,17 +3152,17 @@ def test_event_check_all_bookings(check_types, app, admin_user):
|
|||
'/manage/agendas/%s/events/%s/presence' % (agenda.pk, event.pk), params={'csrfmiddlewaretoken': token}
|
||||
)
|
||||
booking1.refresh_from_db()
|
||||
assert booking1.user_was_present is False
|
||||
assert booking1.user_check_type_slug is None
|
||||
assert booking1.user_check_type_label is None
|
||||
assert booking1.user_check.presence is False
|
||||
assert booking1.user_check.type_slug is None
|
||||
assert booking1.user_check.type_label is None
|
||||
booking2.refresh_from_db()
|
||||
assert booking2.user_was_present is True
|
||||
assert booking2.user_check_type_slug is None
|
||||
assert booking2.user_check_type_label is None
|
||||
assert booking2.user_check.presence is True
|
||||
assert booking2.user_check.type_slug is None
|
||||
assert booking2.user_check.type_label is None
|
||||
secondary_booking.refresh_from_db()
|
||||
assert secondary_booking.user_was_present is True
|
||||
assert secondary_booking.user_check_type_slug is None
|
||||
assert secondary_booking.user_check_type_label is None
|
||||
assert secondary_booking.user_check.presence is True
|
||||
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 True
|
||||
|
||||
|
@ -3156,17 +3175,17 @@ def test_event_check_all_bookings(check_types, app, admin_user):
|
|||
params={'csrfmiddlewaretoken': token, 'check_type': 'foo-reason'},
|
||||
)
|
||||
booking1.refresh_from_db()
|
||||
assert booking1.user_was_present is False
|
||||
assert booking1.user_check_type_slug is None
|
||||
assert booking1.user_check_type_label is None
|
||||
assert booking1.user_check.presence is False
|
||||
assert booking1.user_check.type_slug is None
|
||||
assert booking1.user_check.type_label is None
|
||||
booking2.refresh_from_db()
|
||||
assert booking2.user_was_present is True
|
||||
assert booking2.user_check_type_slug is None
|
||||
assert booking2.user_check_type_label is None
|
||||
assert booking2.user_check.presence is True
|
||||
assert booking2.user_check.type_slug is None
|
||||
assert booking2.user_check.type_label is None
|
||||
booking3.refresh_from_db()
|
||||
assert booking3.user_was_present is False
|
||||
assert booking3.user_check_type_slug == 'foo-reason'
|
||||
assert booking3.user_check_type_label == 'Foo reason'
|
||||
assert booking3.user_check.presence is False
|
||||
assert booking3.user_check.type_slug == 'foo-reason'
|
||||
assert booking3.user_check.type_label == 'Foo reason'
|
||||
|
||||
booking4 = Booking.objects.create(event=event, user_first_name='User', user_last_name='52')
|
||||
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
|
||||
|
@ -3176,21 +3195,21 @@ def test_event_check_all_bookings(check_types, app, admin_user):
|
|||
params={'csrfmiddlewaretoken': token, 'check_type': 'bar-reason'},
|
||||
)
|
||||
booking1.refresh_from_db()
|
||||
assert booking1.user_was_present is False
|
||||
assert booking1.user_check_type_slug is None
|
||||
assert booking1.user_check_type_label is None
|
||||
assert booking1.user_check.presence is False
|
||||
assert booking1.user_check.type_slug is None
|
||||
assert booking1.user_check.type_label is None
|
||||
booking2.refresh_from_db()
|
||||
assert booking2.user_was_present is True
|
||||
assert booking2.user_check_type_slug is None
|
||||
assert booking2.user_check_type_label is None
|
||||
assert booking2.user_check.presence is True
|
||||
assert booking2.user_check.type_slug is None
|
||||
assert booking2.user_check.type_label is None
|
||||
booking3.refresh_from_db()
|
||||
assert booking3.user_was_present is False
|
||||
assert booking3.user_check_type_slug == 'foo-reason'
|
||||
assert booking3.user_check_type_label == 'Foo reason'
|
||||
assert booking3.user_check.presence is False
|
||||
assert booking3.user_check.type_slug == 'foo-reason'
|
||||
assert booking3.user_check.type_label == 'Foo reason'
|
||||
booking4.refresh_from_db()
|
||||
assert booking4.user_was_present is True
|
||||
assert booking4.user_check_type_slug == 'bar-reason'
|
||||
assert booking4.user_check_type_label == 'Bar reason'
|
||||
assert booking4.user_check.presence is True
|
||||
assert booking4.user_check.type_slug == 'bar-reason'
|
||||
assert booking4.user_check.type_label == 'Bar reason'
|
||||
|
||||
# now disable check update
|
||||
agenda.disable_check_update = True
|
||||
|
|
|
@ -5,7 +5,7 @@ import pytest
|
|||
|
||||
from chrono.agendas.models import Agenda, Booking, Event, Subscription
|
||||
from chrono.utils.lingo import CheckType
|
||||
from chrono.utils.timezone import make_aware
|
||||
from chrono.utils.timezone import make_aware, now
|
||||
from tests.utils import login
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
@ -27,6 +27,7 @@ def test_manager_partial_bookings_add_agenda(app, admin_user, settings):
|
|||
assert agenda.kind == 'events'
|
||||
assert agenda.partial_bookings is True
|
||||
assert agenda.default_view == 'day'
|
||||
assert agenda.enable_check_for_future_events is True
|
||||
|
||||
resp = resp.click('Options')
|
||||
assert resp.form['default_view'].options == [
|
||||
|
@ -36,6 +37,82 @@ def test_manager_partial_bookings_add_agenda(app, admin_user, settings):
|
|||
]
|
||||
|
||||
|
||||
def test_options_partial_bookings_invoicing_settings(app, admin_user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
assert agenda.invoicing_unit == 'hour'
|
||||
assert agenda.invoicing_tolerance == 0
|
||||
start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0))
|
||||
event = Event.objects.create(
|
||||
label='Event', start_datetime=start_datetime, end_time=datetime.time(18, 00), places=10, agenda=agenda
|
||||
)
|
||||
booking = Booking.objects.create(
|
||||
user_external_id='xxx',
|
||||
user_first_name='Jane',
|
||||
user_last_name='Doe',
|
||||
start_time=datetime.time(11, 00),
|
||||
end_time=datetime.time(13, 30),
|
||||
event=event,
|
||||
)
|
||||
booking.mark_user_presence(start_time=datetime.time(10, 55), end_time=datetime.time(14, 4))
|
||||
agenda.refresh_booking_computed_times()
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_check.computed_start_time == datetime.time(10, 0)
|
||||
assert booking.user_check.computed_end_time == datetime.time(15, 0)
|
||||
|
||||
app = login(app)
|
||||
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
|
||||
resp = resp.click(href='/manage/agendas/%s/invoicing-options' % agenda.pk)
|
||||
assert resp.form['invoicing_unit'].options == [
|
||||
('hour', True, 'Per hour'),
|
||||
('half_hour', False, 'Per half hour'),
|
||||
('quarter', False, 'Per quarter-hour'),
|
||||
('minute', False, 'Per minute'),
|
||||
]
|
||||
resp.form['invoicing_unit'] = 'half_hour'
|
||||
resp.form['invoicing_tolerance'] = 10
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
agenda.refresh_from_db()
|
||||
assert agenda.invoicing_unit == 'half_hour'
|
||||
assert agenda.invoicing_tolerance == 10
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_check.computed_start_time == datetime.time(11, 0)
|
||||
assert booking.user_check.computed_end_time == datetime.time(14, 0)
|
||||
|
||||
# check kind
|
||||
agenda.partial_bookings = False
|
||||
agenda.save()
|
||||
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
|
||||
assert '/manage/agendas/%s/invoicing-options' % agenda.pk not in resp
|
||||
agenda.kind = 'meetings'
|
||||
agenda.save()
|
||||
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
|
||||
assert '/manage/agendas/%s/invoicing-options' % agenda.pk not in resp
|
||||
agenda.kind = 'virtual'
|
||||
agenda.save()
|
||||
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
|
||||
assert '/manage/agendas/%s/invoicing-options' % agenda.pk not in resp
|
||||
|
||||
|
||||
def test_options_partial_bookings_simpler_settings(app, admin_user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
|
||||
app = login(app)
|
||||
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
|
||||
assert 'Management notifications' not in resp.text
|
||||
assert 'Booking reminders' not in resp.text
|
||||
assert 'Booking display template' not in resp.text
|
||||
assert 'Extra user block template' not in resp.text
|
||||
assert 'Enable the check of bookings when event has not passed' not in resp.text
|
||||
|
||||
resp = app.get('/manage/agendas/%s/display-options' % agenda.pk)
|
||||
assert 'booking_user_block_template' not in resp.form.fields
|
||||
|
||||
resp = app.get('/manage/agendas/%s/check-options' % agenda.pk)
|
||||
assert 'enable_check_for_future_events' not in resp.form.fields
|
||||
assert 'booking_extra_user_block_template' not in resp.form.fields
|
||||
|
||||
|
||||
def test_manager_partial_bookings_add_event(app, admin_user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
|
||||
|
@ -133,6 +210,21 @@ def test_manager_partial_bookings_day_view(app, admin_user, freezer):
|
|||
resp = resp.click('Next day')
|
||||
assert 'No opening hours this day.' in resp.text
|
||||
|
||||
Event.objects.all().delete()
|
||||
event = Event.objects.create(
|
||||
label='Other Event',
|
||||
start_datetime=start_datetime,
|
||||
end_time=datetime.time(18, 00),
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[1, 2, 3, 4, 5, 6, 7],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=7),
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
assert resp.pyquery('.partial-booking--hour')[0].text == '07\u202fh'
|
||||
assert resp.pyquery('.partial-booking--hour')[-1].text == '19\u202fh'
|
||||
|
||||
|
||||
def test_manager_partial_bookings_day_view_24_hours(app, admin_user, freezer):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
|
@ -151,6 +243,71 @@ def test_manager_partial_bookings_day_view_24_hours(app, admin_user, freezer):
|
|||
)
|
||||
|
||||
|
||||
def test_manager_partial_bookings_day_view_multiple_bookings(app, admin_user, freezer):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0))
|
||||
event = Event.objects.create(
|
||||
label='Event', start_datetime=start_datetime, end_time=datetime.time(18, 00), places=10, agenda=agenda
|
||||
)
|
||||
Booking.objects.create(
|
||||
user_external_id='xxx',
|
||||
user_first_name='Jane',
|
||||
user_last_name='Doe',
|
||||
start_time=datetime.time(9, 00),
|
||||
end_time=datetime.time(12, 00),
|
||||
event=event,
|
||||
)
|
||||
Booking.objects.create(
|
||||
user_external_id='xxx',
|
||||
user_first_name='Jane',
|
||||
user_last_name='Doe',
|
||||
start_time=datetime.time(15, 00),
|
||||
end_time=datetime.time(18, 00),
|
||||
event=event,
|
||||
)
|
||||
|
||||
app = login(app)
|
||||
today = start_datetime.date()
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
|
||||
assert len(resp.pyquery('.partial-booking--registrant')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar')) == 2
|
||||
assert resp.pyquery('.registrant--bar')[0].findall('time')[0].text == '09:00'
|
||||
assert resp.pyquery('.registrant--bar')[0].findall('time')[1].text == '12:00'
|
||||
assert resp.pyquery('.registrant--bar')[1].findall('time')[0].text == '15:00'
|
||||
assert resp.pyquery('.registrant--bar')[1].findall('time')[1].text == '18:00'
|
||||
|
||||
# check first booking
|
||||
resp = resp.click('Booked period', index=0)
|
||||
resp.form['start_time'] = '09:30'
|
||||
resp.form['end_time'] = '12:00'
|
||||
resp.form['presence'] = 'True'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert len(resp.pyquery('.partial-booking--registrant')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar')) == 4
|
||||
assert len(resp.pyquery('.registrant--bar.check')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar.computed')) == 1
|
||||
assert resp.pyquery('.registrant--bar.check')[0].findall('time')[0].text == '09:30'
|
||||
assert resp.pyquery('.registrant--bar.check')[0].findall('time')[1].text == '12:00'
|
||||
|
||||
# check second booking
|
||||
resp = resp.click('Booked period')
|
||||
resp.form['start_time'] = '15:00'
|
||||
resp.form['end_time'] = '17:00'
|
||||
resp.form['presence'] = 'True'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert len(resp.pyquery('.partial-booking--registrant')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar')) == 6
|
||||
assert len(resp.pyquery('.registrant--bar.check')) == 2
|
||||
assert len(resp.pyquery('.registrant--bar.computed')) == 2
|
||||
assert resp.pyquery('.registrant--bar.check')[0].findall('time')[0].text == '09:30'
|
||||
assert resp.pyquery('.registrant--bar.check')[0].findall('time')[1].text == '12:00'
|
||||
assert resp.pyquery('.registrant--bar.check')[1].findall('time')[0].text == '15:00'
|
||||
assert resp.pyquery('.registrant--bar.check')[1].findall('time')[1].text == '17:00'
|
||||
|
||||
|
||||
@mock.patch('chrono.manager.forms.get_agenda_check_types')
|
||||
def test_manager_partial_bookings_check(check_types, app, admin_user):
|
||||
check_types.return_value = []
|
||||
|
@ -159,7 +316,7 @@ def test_manager_partial_bookings_check(check_types, app, admin_user):
|
|||
event = Event.objects.create(
|
||||
label='Event', start_datetime=start_datetime, end_time=datetime.time(18, 00), places=10, agenda=agenda
|
||||
)
|
||||
Booking.objects.create(
|
||||
booking = Booking.objects.create(
|
||||
user_external_id='xxx',
|
||||
user_first_name='Jane',
|
||||
user_last_name='Doe',
|
||||
|
@ -177,29 +334,60 @@ def test_manager_partial_bookings_check(check_types, app, admin_user):
|
|||
assert resp.pyquery('.registrant--bar time')[1].text == '13:30'
|
||||
assert resp.pyquery('.registrant--bar')[0].attrib['style'] == 'left: 30.77%; width: 19.23%;'
|
||||
|
||||
resp = resp.click('Jane Doe')
|
||||
resp = resp.click('Booked period')
|
||||
assert 'presence_check_type' not in resp.form.fields
|
||||
assert 'absence_check_type' not in resp.form.fields
|
||||
|
||||
resp.form['user_check_start_time'] = '11:01'
|
||||
resp.form['user_check_end_time'] = '13:15'
|
||||
resp.form['user_was_present'] = 'True'
|
||||
resp = resp.form.submit().follow()
|
||||
assert resp.pyquery('form').attr('data-fill-start_time') == '11:00'
|
||||
assert resp.pyquery('form').attr('data-fill-end_time') == '13:30'
|
||||
assert resp.pyquery('.time-widget-button')[0].text == 'Fill with booking start time'
|
||||
assert resp.pyquery('.time-widget-button')[1].text == 'Fill with booking end time'
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar')) == 2
|
||||
resp.form['start_time'] = '11:01'
|
||||
resp.form['end_time'] = '11:00'
|
||||
resp.form['presence'] = 'True'
|
||||
resp = resp.form.submit()
|
||||
|
||||
assert 'Arrival must be before departure.' in resp.text
|
||||
|
||||
resp.form['start_time'] = '11:01'
|
||||
resp.form['end_time'] = '13:15'
|
||||
resp.form['presence'] = 'True'
|
||||
resp = resp.form.submit().follow()
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_check.start_time == datetime.time(11, 1)
|
||||
assert booking.user_check.end_time == datetime.time(13, 15)
|
||||
assert booking.user_check.computed_start_time == datetime.time(11, 0)
|
||||
assert booking.user_check.computed_end_time == datetime.time(14, 0)
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar')) == 3
|
||||
assert len(resp.pyquery('.registrant--bar.booking')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar.check.present')) == 1
|
||||
assert resp.pyquery('.registrant--bar.check time')[0].text == '11:01'
|
||||
assert resp.pyquery('.registrant--bar.check time')[1].text == '13:15'
|
||||
assert resp.pyquery('.registrant--bar.check')[0].attrib['style'] == 'left: 30.9%; width: 17.18%;'
|
||||
assert resp.pyquery('.registrant--bar span').text() == ''
|
||||
assert len(resp.pyquery('.registrant--bar.computed.present')) == 1
|
||||
assert resp.pyquery('.registrant--bar.computed time')[0].text == '11:00'
|
||||
assert resp.pyquery('.registrant--bar.computed time')[1].text == '14:00'
|
||||
assert resp.pyquery('.registrant--bar.computed')[0].attrib['style'] == 'left: 30.77%; width: 23.08%;'
|
||||
|
||||
agenda.invoicing_unit = 'half_hour'
|
||||
agenda.invoicing_tolerance = 10
|
||||
agenda.save()
|
||||
agenda.refresh_booking_computed_times()
|
||||
booking.refresh_from_db()
|
||||
assert booking.user_check.start_time == datetime.time(11, 1)
|
||||
assert booking.user_check.end_time == datetime.time(13, 15)
|
||||
assert booking.user_check.computed_start_time == datetime.time(11, 0)
|
||||
assert booking.user_check.computed_end_time == datetime.time(13, 30)
|
||||
|
||||
check_types.return_value = [
|
||||
CheckType(slug='foo-reason', label='Foo reason', kind='absence'),
|
||||
CheckType(slug='bar-reason', label='Bar reason', kind='presence'),
|
||||
CheckType(slug='baz-reason', label='Baz reason', kind='presence'),
|
||||
]
|
||||
resp = resp.click('Jane Doe')
|
||||
resp = resp.click('Checked period')
|
||||
assert resp.form['presence_check_type'].options == [
|
||||
('', True, '---------'),
|
||||
('bar-reason', False, 'Bar reason'),
|
||||
|
@ -212,67 +400,196 @@ def test_manager_partial_bookings_check(check_types, app, admin_user):
|
|||
resp.form['presence_check_type'] = 'bar-reason'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar')) == 2
|
||||
assert len(resp.pyquery('.registrant--bar')) == 3
|
||||
assert len(resp.pyquery('.registrant--bar.booking')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar.check.present')) == 1
|
||||
assert resp.pyquery('.registrant--bar.check time')[0].text == '11:01'
|
||||
assert resp.pyquery('.registrant--bar.check time')[1].text == '13:15'
|
||||
assert resp.pyquery('.registrant--bar.check')[0].attrib['style'] == 'left: 30.9%; width: 17.18%;'
|
||||
assert resp.pyquery('.registrant--bar span').text() == 'Bar reason'
|
||||
assert len(resp.pyquery('.registrant--bar.computed.present')) == 1
|
||||
assert resp.pyquery('.registrant--bar.computed time')[0].text == '11:00'
|
||||
assert resp.pyquery('.registrant--bar.computed time')[1].text == '13:30'
|
||||
assert resp.pyquery('.registrant--bar.computed')[0].attrib['style'] == 'left: 30.77%; width: 19.23%;'
|
||||
|
||||
resp = resp.click('Jane Doe')
|
||||
resp = resp.click('Checked period')
|
||||
assert resp.form['presence_check_type'].value == 'bar-reason'
|
||||
|
||||
resp.form['user_was_present'] = 'False'
|
||||
resp.form['presence'] = 'False'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar')) == 2
|
||||
assert len(resp.pyquery('.registrant--bar')) == 3
|
||||
assert len(resp.pyquery('.registrant--bar.booking')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar.check.absent')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar.computed.absent')) == 1
|
||||
assert resp.pyquery('.registrant--bar span').text() == ''
|
||||
|
||||
resp = resp.click('Jane Doe')
|
||||
resp.form['user_was_present'] = ''
|
||||
resp = resp.click('Checked period')
|
||||
resp.form['presence'] = ''
|
||||
resp = resp.form.submit().follow()
|
||||
assert len(resp.pyquery('.registrant--bar')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar.booking')) == 1
|
||||
assert resp.pyquery('.registrant--bar span').text() == ''
|
||||
|
||||
# event is checked
|
||||
event.checked = True
|
||||
event.save()
|
||||
assert agenda.disable_check_update is False
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
assert '/manage/agendas/%s/bookings/%s/check' % (agenda.pk, booking.pk) in resp
|
||||
app.get('/manage/agendas/%s/bookings/%s/check' % (agenda.pk, booking.pk), status=200)
|
||||
|
||||
def test_manager_partial_bookings_check_future_events(app, admin_user, freezer):
|
||||
# event check is locked
|
||||
event.check_locked = True
|
||||
event.save()
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
assert '/manage/agendas/%s/bookings/%s/check' % (agenda.pk, booking.pk) not in resp
|
||||
app.get('/manage/agendas/%s/bookings/%s/check' % (agenda.pk, booking.pk), status=404)
|
||||
|
||||
# now disable check update
|
||||
event.check_locked = False
|
||||
event.save()
|
||||
agenda.disable_check_update = True
|
||||
agenda.save()
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
assert '/manage/agendas/%s/bookings/%s/check' % (agenda.pk, booking.pk) not in resp
|
||||
app.get('/manage/agendas/%s/bookings/%s/check' % (agenda.pk, booking.pk), status=404)
|
||||
|
||||
|
||||
def test_manager_partial_bookings_multiple_checks(app, admin_user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0))
|
||||
event = Event.objects.create(
|
||||
label='Event', start_datetime=start_datetime, end_time=datetime.time(18, 00), places=10, agenda=agenda
|
||||
)
|
||||
booking = Booking.objects.create(
|
||||
Booking.objects.create(
|
||||
user_external_id='xxx',
|
||||
user_first_name='Jane',
|
||||
user_last_name='Doe',
|
||||
start_time=datetime.time(11, 00),
|
||||
end_time=datetime.time(13, 30),
|
||||
start_time=datetime.time(9, 00),
|
||||
end_time=datetime.time(18, 00),
|
||||
event=event,
|
||||
)
|
||||
|
||||
app = login(app)
|
||||
day = start_datetime.date()
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, day.year, day.month, day.day))
|
||||
today = start_datetime.date()
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
|
||||
resp = resp.click('Booked period')
|
||||
assert 'Add second booking check' not in resp.text
|
||||
|
||||
resp.form['start_time'] = '09:30'
|
||||
resp.form['end_time'] = '12:00'
|
||||
resp.form['presence'] = 'True'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar')) == 3
|
||||
assert len(resp.pyquery('.registrant--bar.check')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar.computed')) == 1
|
||||
assert resp.pyquery('.registrant--bar.check')[0].findall('time')[0].text == '09:30'
|
||||
assert resp.pyquery('.registrant--bar.check')[0].findall('time')[1].text == '12:00'
|
||||
|
||||
resp = resp.click('Checked period')
|
||||
resp = resp.click('Add second booking check')
|
||||
|
||||
resp.form['start_time'] = '12:30'
|
||||
resp.form['end_time'] = '17:30'
|
||||
resp.form['presence'] = 'False'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar')) == 5
|
||||
assert len(resp.pyquery('.registrant--bar.check')) == 2
|
||||
assert len(resp.pyquery('.registrant--bar.computed')) == 2
|
||||
assert resp.pyquery('.registrant--bar.check')[1].findall('time')[0].text == '12:30'
|
||||
assert resp.pyquery('.registrant--bar.check')[1].findall('time')[1].text == '17:30'
|
||||
|
||||
resp = resp.click('Checked period', index=1)
|
||||
assert 'Add second booking check' not in resp.text
|
||||
|
||||
resp.form['start_time'] = '11:30'
|
||||
resp = resp.form.submit()
|
||||
assert 'Booking check hours overlap existing check.' in resp.text
|
||||
|
||||
resp.form['start_time'] = '12:30'
|
||||
resp.form['presence'] = ''
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar')) == 3
|
||||
assert len(resp.pyquery('.registrant--bar.check')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar.computed')) == 1
|
||||
assert resp.pyquery('.registrant--bar.check')[0].findall('time')[0].text == '09:30'
|
||||
assert resp.pyquery('.registrant--bar.check')[0].findall('time')[1].text == '12:00'
|
||||
|
||||
|
||||
@mock.patch('chrono.manager.forms.get_agenda_check_types')
|
||||
def test_manager_partial_bookings_check_subscription(check_types, app, admin_user):
|
||||
check_types.return_value = []
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0))
|
||||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=start_datetime,
|
||||
end_time=datetime.time(18, 00),
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=list(range(1, 8)),
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=30),
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
Subscription.objects.create(
|
||||
agenda=agenda,
|
||||
user_external_id='xxx',
|
||||
user_first_name='Jane',
|
||||
user_last_name='Doe',
|
||||
date_start=event.start_datetime,
|
||||
date_end=event.start_datetime + datetime.timedelta(days=2),
|
||||
)
|
||||
|
||||
app = login(app)
|
||||
today = start_datetime.date()
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar')) == 0
|
||||
assert len(resp.pyquery('.registrant--name a')) == 1
|
||||
app.get('/manage/agendas/%s/bookings/%s/check' % (agenda.pk, booking.pk), status=200)
|
||||
|
||||
freezer.move_to(start_datetime - datetime.timedelta(days=1))
|
||||
resp = resp.click('Jane Doe')
|
||||
assert 'Fill with booking start time' not in resp.text
|
||||
assert 'absence_check_type' not in resp.form.fields
|
||||
assert resp.form['presence'].options == [
|
||||
('', True, None),
|
||||
('True', False, None),
|
||||
] # no 'False' option
|
||||
assert not Booking.objects.exists()
|
||||
|
||||
resp.form['start_time'] = '10:00'
|
||||
resp.form['end_time'] = '16:00'
|
||||
resp.form['presence'] = 'True'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, day.year, day.month, day.day))
|
||||
assert len(resp.pyquery('.registrant--name a')) == 0
|
||||
app.get('/manage/agendas/%s/bookings/%s/check' % (agenda.pk, booking.pk), status=404)
|
||||
assert len(resp.pyquery('.registrant--bar')) == 2
|
||||
assert len(resp.pyquery('.registrant--bar.check.present')) == 1
|
||||
assert len(resp.pyquery('.registrant--bar.computed.present')) == 1
|
||||
assert resp.pyquery('.registrant--bar.check time')[0].text == '10:00'
|
||||
assert resp.pyquery('.registrant--bar.check time')[1].text == '16:00'
|
||||
|
||||
agenda.enable_check_for_future_events = True
|
||||
agenda.save()
|
||||
booking = Booking.objects.get()
|
||||
assert booking.user_external_id == 'xxx'
|
||||
assert booking.user_first_name == 'Jane'
|
||||
assert booking.user_last_name == 'Doe'
|
||||
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, day.year, day.month, day.day))
|
||||
assert len(resp.pyquery('.registrant--name a')) == 1
|
||||
app.get('/manage/agendas/%s/bookings/%s/check' % (agenda.pk, booking.pk), status=200)
|
||||
resp = resp.click('Checked period')
|
||||
assert 'Fill with booking start time' not in resp.text
|
||||
assert 'absence_check_type' not in resp.form.fields
|
||||
assert resp.form['presence'].options == [
|
||||
('', False, None),
|
||||
('True', True, None),
|
||||
] # no 'False' option
|
||||
|
||||
resp.form['presence'] = ''
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar')) == 0
|
||||
|
||||
|
||||
@mock.patch('chrono.manager.forms.get_agenda_check_types')
|
||||
|
@ -299,30 +616,27 @@ def test_manager_partial_bookings_check_filters(check_types, app, admin_user):
|
|||
end_time=datetime.time(13, 30),
|
||||
event=event,
|
||||
)
|
||||
Booking.objects.create(
|
||||
booking = Booking.objects.create(
|
||||
user_external_id='user:2',
|
||||
user_first_name='User',
|
||||
user_last_name='Present Vegan',
|
||||
start_time=datetime.time(8, 00),
|
||||
end_time=datetime.time(10, 00),
|
||||
user_check_start_time=datetime.time(8, 00),
|
||||
user_check_end_time=datetime.time(10, 00),
|
||||
event=event,
|
||||
extra_data={'menu': 'vegan'},
|
||||
user_was_present=True,
|
||||
)
|
||||
Booking.objects.create(
|
||||
booking.mark_user_presence(start_time=datetime.time(8, 00), end_time=datetime.time(10, 00))
|
||||
booking = Booking.objects.create(
|
||||
user_external_id='user:3',
|
||||
user_first_name='User',
|
||||
user_last_name='Absent Meat Foo Reason',
|
||||
start_time=datetime.time(12, 00),
|
||||
end_time=datetime.time(14, 00),
|
||||
user_check_start_time=datetime.time(12, 30),
|
||||
user_check_end_time=datetime.time(14, 30),
|
||||
event=event,
|
||||
extra_data={'menu': 'meat'},
|
||||
user_was_present=False,
|
||||
user_check_type_slug='foo-reason',
|
||||
)
|
||||
booking.mark_user_absence(
|
||||
check_type_slug='foo-reason', start_time=datetime.time(12, 30), end_time=datetime.time(14, 30)
|
||||
)
|
||||
Subscription.objects.create(
|
||||
agenda=agenda,
|
||||
|
@ -352,9 +666,8 @@ def test_manager_partial_bookings_check_filters(check_types, app, admin_user):
|
|||
'User Present Vegan',
|
||||
]
|
||||
|
||||
# one registrant has not booked, no bar is shown and no booking check link
|
||||
# one registrant has not booked, no bar is shown
|
||||
assert len(resp.pyquery('.registrant--bar.booking')) == 3
|
||||
assert len(resp.pyquery('.registrant--name a')) == 3
|
||||
|
||||
resp = app.get(url, params={'booking-status': 'booked'})
|
||||
assert [x.text_content() for x in resp.pyquery('.registrant--name')] == [
|
||||
|
@ -376,6 +689,106 @@ def test_manager_partial_bookings_check_filters(check_types, app, admin_user):
|
|||
assert [x.text_content() for x in resp.pyquery('.registrant--name')] == ['User Absent Meat Foo Reason']
|
||||
|
||||
|
||||
def test_manager_partial_bookings_event_checked(app, admin_user):
|
||||
agenda = Agenda.objects.create(
|
||||
label='Foo bar',
|
||||
kind='events',
|
||||
partial_bookings=True,
|
||||
)
|
||||
start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0))
|
||||
event = Event.objects.create(
|
||||
label='Event', start_datetime=start_datetime, end_time=datetime.time(18, 00), places=10, agenda=agenda
|
||||
)
|
||||
login(app)
|
||||
|
||||
today = start_datetime.date()
|
||||
url = '/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day)
|
||||
resp = app.get(url)
|
||||
assert 'Mark the event as checked' not in resp
|
||||
for i in range(8):
|
||||
booking = Booking.objects.create(
|
||||
event=event,
|
||||
start_time=datetime.time(8, 00),
|
||||
end_time=datetime.time(10, 00),
|
||||
)
|
||||
if 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))
|
||||
resp = app.get(url)
|
||||
assert 'Mark the event as checked' in resp
|
||||
assert event.checked is False
|
||||
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
|
||||
assert 'checked tag' not in resp
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
assert '<span class="checked tag">Checked</span>' not in resp
|
||||
assert 'check-locked' not in resp
|
||||
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},
|
||||
)
|
||||
event.refresh_from_db()
|
||||
assert event.checked is True
|
||||
resp = resp.follow()
|
||||
assert 'Mark the event as checked' not in resp
|
||||
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
|
||||
assert 'checked tag' in resp
|
||||
for booking in Booking.objects.filter(user_checks__isnull=True):
|
||||
booking.mark_user_presence(start_time=datetime.time(8, 00), end_time=datetime.time(10, 00))
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
assert '<span class="checked tag">Checked</span>' in resp
|
||||
assert 'check-locked' not in resp
|
||||
assert 'invoiced' not in resp
|
||||
|
||||
# event not in past
|
||||
agenda.disable_check_update = False
|
||||
agenda.save()
|
||||
assert agenda.enable_check_for_future_events is False
|
||||
today = now().date()
|
||||
event.start_datetime = make_aware(
|
||||
datetime.datetime(today.year, today.month, today.day, 8, 0)
|
||||
) + 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,
|
||||
)
|
||||
|
||||
# event check is locked
|
||||
event.checked = False
|
||||
event.check_locked = True
|
||||
event.save()
|
||||
today = event.start_datetime.date()
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
assert '<span class="check-locked tag">Check locked</span>' in resp
|
||||
assert 'invoiced' not in resp
|
||||
app.post(
|
||||
'/manage/agendas/%s/events/%s/checked' % (agenda.pk, event.pk),
|
||||
params={'csrfmiddlewaretoken': token},
|
||||
status=404,
|
||||
)
|
||||
|
||||
# event check is locked and envent is invoiced
|
||||
event.invoiced = True
|
||||
event.save()
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
assert 'check-locked' not in resp
|
||||
assert '<span class="invoiced tag">Invoiced</span>' in resp
|
||||
|
||||
|
||||
def test_manager_partial_bookings_month_view(app, admin_user, freezer):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0))
|
||||
|
@ -398,28 +811,24 @@ def test_manager_partial_bookings_month_view(app, admin_user, freezer):
|
|||
end_time=datetime.time(13, 30),
|
||||
event=e,
|
||||
)
|
||||
Booking.objects.create(
|
||||
booking = Booking.objects.create(
|
||||
user_external_id='user:2',
|
||||
user_first_name='User',
|
||||
user_last_name='Present',
|
||||
start_time=datetime.time(8, 00),
|
||||
end_time=datetime.time(10, 00),
|
||||
user_check_start_time=datetime.time(8, 00),
|
||||
user_check_end_time=datetime.time(10, 00),
|
||||
event=event,
|
||||
user_was_present=True,
|
||||
)
|
||||
Booking.objects.create(
|
||||
booking.mark_user_presence(start_time=datetime.time(8, 00), end_time=datetime.time(10, 00))
|
||||
booking = Booking.objects.create(
|
||||
user_external_id='user:3',
|
||||
user_first_name='User',
|
||||
user_last_name='Absent',
|
||||
start_time=datetime.time(12, 00),
|
||||
end_time=datetime.time(14, 00),
|
||||
user_check_start_time=datetime.time(12, 30),
|
||||
user_check_end_time=datetime.time(14, 30),
|
||||
event=event,
|
||||
user_was_present=False,
|
||||
)
|
||||
booking.mark_user_absence(start_time=datetime.time(12, 30), end_time=datetime.time(14, 30))
|
||||
Subscription.objects.create(
|
||||
agenda=agenda,
|
||||
user_external_id='user:1',
|
||||
|
|
|
@ -45,6 +45,5 @@ EXCEPTIONS_SOURCES = {}
|
|||
SITE_BASE_URL = 'https://example.com'
|
||||
|
||||
SHARED_CUSTODY_ENABLED = True
|
||||
LEGACY_FILLSLOTS_ENABLED = True
|
||||
|
||||
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
|
||||
|
|
|
@ -17,6 +17,7 @@ from chrono.agendas.models import (
|
|||
AgendaNotificationsSettings,
|
||||
AgendaReminderSettings,
|
||||
Booking,
|
||||
BookingCheck,
|
||||
Category,
|
||||
Desk,
|
||||
Event,
|
||||
|
@ -3855,6 +3856,7 @@ def test_agenda_event_overlaps_recurring():
|
|||
assert agenda.event_overlaps(**event_kwargs) is False
|
||||
|
||||
event = Event.objects.create(agenda=agenda, places=1, **event_kwargs)
|
||||
event.create_all_recurrences()
|
||||
|
||||
assert agenda.event_overlaps(**event_kwargs) is True
|
||||
assert agenda.event_overlaps(instance=event, **event_kwargs) is False
|
||||
|
@ -3916,3 +3918,324 @@ def test_agenda_event_overlaps_recurring():
|
|||
# normal event, after not on a recurrence day
|
||||
event_kwargs['start_datetime'] = make_aware(datetime.datetime(2023, 5, 10, 14, 00))
|
||||
assert agenda.event_overlaps(**event_kwargs) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'start_time, user_check_start_time, tolerance, unit, expected',
|
||||
[
|
||||
# no check
|
||||
(None, None, 0, 'hour', None),
|
||||
(None, None, 0, 'half_hour', None),
|
||||
(None, None, 0, 'quarter', None),
|
||||
(None, None, 0, 'minutes', None),
|
||||
# hour unit - no booking
|
||||
(None, datetime.time(7, 50), 10, 'hour', datetime.time(8, 0)),
|
||||
(None, datetime.time(7, 49), 10, 'hour', datetime.time(7, 0)),
|
||||
(None, datetime.time(7, 50), 0, 'hour', datetime.time(7, 0)),
|
||||
# hour unit - with booking
|
||||
(datetime.time(8, 0), datetime.time(7, 50), 10, 'hour', datetime.time(8, 0)),
|
||||
(datetime.time(8, 0), datetime.time(7, 49), 10, 'hour', datetime.time(7, 0)),
|
||||
(datetime.time(8, 0), datetime.time(8, 30), 10, 'hour', datetime.time(8, 0)),
|
||||
(datetime.time(8, 0), datetime.time(7, 50), 0, 'hour', datetime.time(7, 0)),
|
||||
# half_hour unit - no booking
|
||||
(None, datetime.time(7, 50), 10, 'half_hour', datetime.time(8, 0)),
|
||||
(None, datetime.time(7, 49), 10, 'half_hour', datetime.time(7, 30)),
|
||||
(None, datetime.time(7, 50), 0, 'half_hour', datetime.time(7, 30)),
|
||||
# half_hour unit - with booking
|
||||
(datetime.time(8, 0), datetime.time(7, 50), 10, 'half_hour', datetime.time(8, 0)),
|
||||
(datetime.time(8, 0), datetime.time(7, 49), 10, 'half_hour', datetime.time(7, 30)),
|
||||
(datetime.time(8, 0), datetime.time(8, 30), 10, 'half_hour', datetime.time(8, 0)),
|
||||
(datetime.time(8, 0), datetime.time(7, 50), 0, 'half_hour', datetime.time(7, 30)),
|
||||
# quarter unit - no booking
|
||||
(None, datetime.time(7, 50), 10, 'quarter', datetime.time(8, 0)),
|
||||
(None, datetime.time(7, 49), 10, 'quarter', datetime.time(7, 45)),
|
||||
(None, datetime.time(7, 50), 0, 'quarter', datetime.time(7, 45)),
|
||||
# quarter unit - with booking
|
||||
(datetime.time(8, 0), datetime.time(7, 50), 10, 'quarter', datetime.time(8, 0)),
|
||||
(datetime.time(8, 0), datetime.time(7, 49), 10, 'quarter', datetime.time(7, 45)),
|
||||
(datetime.time(8, 0), datetime.time(8, 30), 10, 'quarter', datetime.time(8, 0)),
|
||||
(datetime.time(8, 0), datetime.time(7, 50), 0, 'quarter', datetime.time(7, 45)),
|
||||
# minute unit - no booking
|
||||
(None, datetime.time(7, 50), 0, 'minute', datetime.time(7, 50)),
|
||||
# minute unit - with booking
|
||||
(datetime.time(8, 0), datetime.time(7, 50), 0, 'minute', datetime.time(7, 50)),
|
||||
(datetime.time(8, 0), datetime.time(8, 5), 0, 'minute', datetime.time(8, 0)),
|
||||
],
|
||||
)
|
||||
def test_booking_get_computed_start_time(start_time, user_check_start_time, tolerance, unit, expected):
|
||||
agenda = Agenda.objects.create(
|
||||
label='Agenda', kind='events', invoicing_unit=unit, invoicing_tolerance=tolerance
|
||||
)
|
||||
event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
|
||||
places=10,
|
||||
)
|
||||
|
||||
booking = Booking.objects.create(event=event, start_time=start_time)
|
||||
booking.mark_user_presence(start_time=user_check_start_time)
|
||||
assert booking.user_check.get_computed_start_time() == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'end_time, user_check_end_time, tolerance, unit, expected',
|
||||
[
|
||||
# no check
|
||||
(None, None, 0, 'hour', None),
|
||||
(None, None, 0, 'half_hour', None),
|
||||
(None, None, 0, 'quarter', None),
|
||||
(None, None, 0, 'minutes', None),
|
||||
# hour unit - no booking
|
||||
(None, datetime.time(17, 10), 10, 'hour', datetime.time(17, 0)),
|
||||
(None, datetime.time(17, 11), 10, 'hour', datetime.time(18, 0)),
|
||||
(None, datetime.time(17, 10), 0, 'hour', datetime.time(18, 0)),
|
||||
# hour unit - with booking
|
||||
(datetime.time(17, 0), datetime.time(17, 10), 10, 'hour', datetime.time(17, 0)),
|
||||
(datetime.time(17, 0), datetime.time(17, 11), 10, 'hour', datetime.time(18, 0)),
|
||||
(datetime.time(17, 0), datetime.time(16, 30), 10, 'hour', datetime.time(17, 0)),
|
||||
(datetime.time(17, 0), datetime.time(17, 10), 0, 'hour', datetime.time(18, 0)),
|
||||
# half_hour unit - no booking
|
||||
(None, datetime.time(17, 10), 10, 'half_hour', datetime.time(17, 0)),
|
||||
(None, datetime.time(17, 11), 10, 'half_hour', datetime.time(17, 30)),
|
||||
(None, datetime.time(17, 10), 0, 'half_hour', datetime.time(17, 30)),
|
||||
# half_hour unit - with booking
|
||||
(datetime.time(17, 0), datetime.time(17, 10), 10, 'half_hour', datetime.time(17, 0)),
|
||||
(datetime.time(17, 0), datetime.time(17, 11), 10, 'half_hour', datetime.time(17, 30)),
|
||||
(datetime.time(17, 0), datetime.time(16, 30), 10, 'half_hour', datetime.time(17, 0)),
|
||||
(datetime.time(17, 0), datetime.time(17, 10), 0, 'half_hour', datetime.time(17, 30)),
|
||||
# quarter unit - no booking
|
||||
(None, datetime.time(17, 10), 10, 'quarter', datetime.time(17, 0)),
|
||||
(None, datetime.time(17, 11), 10, 'quarter', datetime.time(17, 15)),
|
||||
(None, datetime.time(17, 10), 0, 'quarter', datetime.time(17, 15)),
|
||||
# quarter unit - with booking
|
||||
(datetime.time(17, 0), datetime.time(17, 10), 10, 'quarter', datetime.time(17, 0)),
|
||||
(datetime.time(17, 0), datetime.time(17, 11), 10, 'quarter', datetime.time(17, 15)),
|
||||
(datetime.time(17, 0), datetime.time(16, 30), 10, 'quarter', datetime.time(17, 0)),
|
||||
(datetime.time(17, 0), datetime.time(17, 10), 0, 'quarter', datetime.time(17, 15)),
|
||||
# minute unit - no booking
|
||||
(None, datetime.time(17, 10), 0, 'minute', datetime.time(17, 10)),
|
||||
# minute unit - with booking
|
||||
(datetime.time(17, 0), datetime.time(17, 10), 0, 'minute', datetime.time(17, 10)),
|
||||
(datetime.time(17, 0), datetime.time(16, 50), 0, 'minute', datetime.time(17, 0)),
|
||||
],
|
||||
)
|
||||
def test_booking_get_computed_end_time(end_time, user_check_end_time, tolerance, unit, expected):
|
||||
agenda = Agenda.objects.create(
|
||||
label='Agenda', kind='events', invoicing_unit=unit, invoicing_tolerance=tolerance
|
||||
)
|
||||
event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
|
||||
places=10,
|
||||
)
|
||||
|
||||
booking = Booking.objects.create(event=event, end_time=end_time)
|
||||
booking.mark_user_presence(end_time=user_check_end_time)
|
||||
assert booking.user_check.get_computed_end_time() == expected
|
||||
|
||||
|
||||
def test_agenda_refresh_booking_computed_times():
|
||||
agenda = Agenda.objects.create(
|
||||
label='Agenda',
|
||||
kind='events',
|
||||
partial_bookings=True,
|
||||
)
|
||||
agenda2 = Agenda.objects.create(label='other')
|
||||
assert agenda.invoicing_unit == 'hour'
|
||||
assert agenda.invoicing_tolerance == 0
|
||||
event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
|
||||
places=10,
|
||||
)
|
||||
booking = Booking.objects.create(
|
||||
event=event,
|
||||
start_time=datetime.time(8, 0),
|
||||
end_time=datetime.time(17, 0),
|
||||
)
|
||||
booking.mark_user_presence(start_time=datetime.time(7, 55), end_time=datetime.time(17, 15))
|
||||
|
||||
def reset_booking():
|
||||
booking.user_check.computed_start_time = None
|
||||
booking.user_check.computed_end_time = None
|
||||
booking.user_check.save()
|
||||
|
||||
def test_booking(success):
|
||||
agenda.refresh_booking_computed_times()
|
||||
booking.refresh_from_db()
|
||||
if success is True:
|
||||
assert booking.user_check.computed_start_time == datetime.time(7, 0)
|
||||
assert booking.user_check.computed_end_time == datetime.time(18, 0)
|
||||
reset_booking()
|
||||
else:
|
||||
assert booking.user_check.computed_start_time is booking.user_check.computed_end_time is None
|
||||
|
||||
test_booking(True)
|
||||
|
||||
# wrong agenda kind
|
||||
agenda.partial_bookings = False
|
||||
agenda.save()
|
||||
test_booking(False)
|
||||
|
||||
agenda.partial_bookings = True
|
||||
agenda.kind = 'meetings'
|
||||
agenda.save()
|
||||
test_booking(False)
|
||||
|
||||
agenda.kind = 'virtual'
|
||||
agenda.save()
|
||||
test_booking(False)
|
||||
|
||||
# reset kind
|
||||
agenda.kind = 'events'
|
||||
agenda.save()
|
||||
test_booking(True)
|
||||
|
||||
# wrong agenda
|
||||
event.agenda = agenda2
|
||||
event.save()
|
||||
test_booking(False)
|
||||
|
||||
# event check locked
|
||||
event.agenda = agenda
|
||||
event.check_locked = True
|
||||
event.save()
|
||||
test_booking(False)
|
||||
|
||||
# event invoiced
|
||||
event.check_locked = False
|
||||
event.invoiced = True
|
||||
event.save()
|
||||
test_booking(False)
|
||||
|
||||
# event cancelled
|
||||
event.invoiced = False
|
||||
event.cancelled = True
|
||||
event.save()
|
||||
test_booking(False)
|
||||
|
||||
# booking cancelled
|
||||
event.cancelled = False
|
||||
event.save()
|
||||
booking.cancellation_datetime = now()
|
||||
booking.save()
|
||||
test_booking(False)
|
||||
|
||||
# ok
|
||||
booking.cancellation_datetime = None
|
||||
booking.save()
|
||||
test_booking(True)
|
||||
|
||||
|
||||
def test_event_refresh_booking_computed_times():
|
||||
agenda = Agenda.objects.create(
|
||||
label='Agenda',
|
||||
kind='events',
|
||||
partial_bookings=True,
|
||||
)
|
||||
assert agenda.invoicing_unit == 'hour'
|
||||
assert agenda.invoicing_tolerance == 0
|
||||
event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
|
||||
places=10,
|
||||
)
|
||||
event2 = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
|
||||
places=10,
|
||||
)
|
||||
booking = Booking.objects.create(
|
||||
event=event,
|
||||
start_time=datetime.time(8, 0),
|
||||
end_time=datetime.time(17, 0),
|
||||
)
|
||||
booking.mark_user_presence(start_time=datetime.time(7, 55), end_time=datetime.time(17, 15))
|
||||
|
||||
def reset_booking():
|
||||
booking.user_check.computed_start_time = None
|
||||
booking.user_check.computed_end_time = None
|
||||
booking.user_check.save()
|
||||
|
||||
def test_booking(success):
|
||||
event.refresh_booking_computed_times()
|
||||
booking.refresh_from_db()
|
||||
if success is True:
|
||||
assert booking.user_check.computed_start_time == datetime.time(7, 0)
|
||||
assert booking.user_check.computed_end_time == datetime.time(18, 0)
|
||||
reset_booking()
|
||||
else:
|
||||
assert booking.user_check.computed_start_time is booking.user_check.computed_end_time is None
|
||||
|
||||
test_booking(True)
|
||||
|
||||
# wrong agenda kind
|
||||
agenda.partial_bookings = False
|
||||
agenda.save()
|
||||
test_booking(False)
|
||||
|
||||
agenda.partial_bookings = True
|
||||
agenda.kind = 'meetings'
|
||||
agenda.save()
|
||||
test_booking(False)
|
||||
|
||||
agenda.kind = 'virtual'
|
||||
agenda.save()
|
||||
test_booking(False)
|
||||
|
||||
# reset kind
|
||||
agenda.kind = 'events'
|
||||
agenda.save()
|
||||
test_booking(True)
|
||||
|
||||
# wrong event
|
||||
booking.event = event2
|
||||
booking.save()
|
||||
test_booking(False)
|
||||
|
||||
# event check locked
|
||||
booking.event = event
|
||||
booking.save()
|
||||
event.check_locked = True
|
||||
event.save()
|
||||
test_booking(False)
|
||||
|
||||
# event invoiced
|
||||
event.check_locked = False
|
||||
event.invoiced = True
|
||||
event.save()
|
||||
test_booking(False)
|
||||
|
||||
# event cancelled
|
||||
event.invoiced = False
|
||||
event.cancelled = True
|
||||
event.save()
|
||||
test_booking(False)
|
||||
|
||||
# booking cancelled
|
||||
event.cancelled = False
|
||||
event.save()
|
||||
booking.cancellation_datetime = now()
|
||||
booking.save()
|
||||
test_booking(False)
|
||||
|
||||
# ok
|
||||
booking.cancellation_datetime = None
|
||||
booking.save()
|
||||
test_booking(True)
|
||||
|
||||
|
||||
def test_agenda_booking_user_check_property():
|
||||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||||
event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
|
||||
booking = Booking.objects.create(event=event)
|
||||
|
||||
booking_check = BookingCheck.objects.create(booking=booking, presence=True)
|
||||
assert booking.user_check == booking_check
|
||||
|
||||
BookingCheck.objects.create(booking=booking, presence=False)
|
||||
booking.refresh_from_db()
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
# pylint: disable=pointless-statement
|
||||
booking.user_check
|
||||
|
|
|
@ -10,6 +10,7 @@ from unittest import mock
|
|||
|
||||
import pytest
|
||||
from django.contrib.auth.models import Group
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.management import CommandError, call_command
|
||||
from django.test import override_settings
|
||||
from django.utils.encoding import force_bytes
|
||||
|
@ -35,6 +36,8 @@ from chrono.agendas.models import (
|
|||
from chrono.manager.utils import import_site
|
||||
from chrono.utils.timezone import make_aware, now
|
||||
|
||||
from .test_agendas import ICS_SAMPLE, ICS_SAMPLE_WITH_NO_EVENTS
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
|
@ -799,6 +802,99 @@ def test_import_export_time_period_exception_source_enabled():
|
|||
assert source.settings_slug == 'holidays'
|
||||
|
||||
|
||||
@mock.patch('chrono.agendas.models.requests.get')
|
||||
def test_import_export_time_period_exception_source_remote_ics(mocked_get):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
|
||||
desk = Desk.objects.create(label='Desk', agenda=agenda)
|
||||
source = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='http://example.com/sample.ics')
|
||||
|
||||
mocked_response = mock.Mock()
|
||||
mocked_response.text = ICS_SAMPLE
|
||||
mocked_get.return_value = mocked_response
|
||||
|
||||
source.refresh_timeperiod_exceptions_from_ics()
|
||||
assert TimePeriodException.objects.count() == 2
|
||||
|
||||
output = get_output_of_command('export_site')
|
||||
payload = json.loads(output)
|
||||
|
||||
Agenda.objects.all().delete()
|
||||
assert not TimePeriodExceptionSource.objects.exists()
|
||||
assert not TimePeriodException.objects.exists()
|
||||
|
||||
import_site(payload)
|
||||
source = TimePeriodExceptionSource.objects.get()
|
||||
assert source.timeperiodexception_set.count() == 2
|
||||
assert TimePeriodException.objects.count() == 2
|
||||
|
||||
# import again changes nothing
|
||||
import_site(payload)
|
||||
assert TimePeriodExceptionSource.objects.count() == 1
|
||||
assert TimePeriodException.objects.count() == 2
|
||||
|
||||
# empty remote ics
|
||||
mocked_response.text = ICS_SAMPLE_WITH_NO_EVENTS
|
||||
|
||||
Agenda.objects.all().delete()
|
||||
import_site(payload)
|
||||
|
||||
assert TimePeriodExceptionSource.objects.count() == 1
|
||||
assert TimePeriodException.objects.count() == 0
|
||||
|
||||
|
||||
@mock.patch('chrono.agendas.models.requests.get')
|
||||
def test_import_export_time_period_exception_source_ics_file(mocked_get):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
|
||||
desk = Desk.objects.create(label='Desk', agenda=agenda)
|
||||
source = TimePeriodExceptionSource.objects.create(
|
||||
desk=desk, ics_filename='sample.ics', ics_file=ContentFile(ICS_SAMPLE, name='sample.ics')
|
||||
)
|
||||
|
||||
source.refresh_timeperiod_exceptions_from_ics()
|
||||
assert TimePeriodException.objects.count() == 2
|
||||
|
||||
output = get_output_of_command('export_site')
|
||||
payload = json.loads(output)
|
||||
|
||||
Agenda.objects.all().delete()
|
||||
assert not TimePeriodExceptionSource.objects.exists()
|
||||
assert not TimePeriodException.objects.exists()
|
||||
|
||||
import_site(payload)
|
||||
source = TimePeriodExceptionSource.objects.get()
|
||||
assert source.timeperiodexception_set.count() == 2
|
||||
assert TimePeriodException.objects.count() == 2
|
||||
|
||||
# import again changes nothing
|
||||
import_site(payload)
|
||||
assert TimePeriodExceptionSource.objects.count() == 1
|
||||
assert TimePeriodException.objects.count() == 2
|
||||
|
||||
|
||||
@override_settings(
|
||||
EXCEPTIONS_SOURCES={
|
||||
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
|
||||
}
|
||||
)
|
||||
def test_import_export_time_period_exception_legacy_file():
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
desk = Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
desk.import_timeperiod_exceptions_from_settings()
|
||||
|
||||
output = get_output_of_command('export_site')
|
||||
payload = json.loads(output)
|
||||
source = payload['agendas'][0]['exceptions_desk']['exception_sources'][0]
|
||||
del source['ics_file']
|
||||
del source['ics_filename']
|
||||
del source['ics_url']
|
||||
|
||||
agenda.delete()
|
||||
assert not TimePeriodExceptionSource.objects.exists()
|
||||
|
||||
import_site(payload)
|
||||
assert TimePeriodExceptionSource.objects.count() == 1
|
||||
|
||||
|
||||
def test_import_export_do_not_duplicate_timeperiod_and_exceptions():
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
|
||||
desk = Desk.objects.create(slug='test', agenda=agenda)
|
||||
|
|
|
@ -390,3 +390,54 @@ def test_migration_convert_week_days(transactional_db):
|
|||
assert Event.objects.get(slug='all').recurrence_days == [1, 2, 3, 4, 5, 6, 7]
|
||||
|
||||
assert SharedCustodyRule.objects.get().days == [1, 5, 7]
|
||||
|
||||
|
||||
def test_migration_booking_check_data(transactional_db):
|
||||
app = 'agendas'
|
||||
|
||||
migrate_from = [(app, '0161_add_booking_check_model')]
|
||||
migrate_to = [(app, '0162_migrate_booking_check_data')]
|
||||
executor = MigrationExecutor(connection)
|
||||
old_apps = executor.loader.project_state(migrate_from).apps
|
||||
executor.migrate(migrate_from)
|
||||
|
||||
Agenda = old_apps.get_model(app, 'Agenda')
|
||||
Event = old_apps.get_model(app, 'Event')
|
||||
Booking = old_apps.get_model(app, 'Booking')
|
||||
|
||||
agenda = Agenda.objects.create(label='Foo', kind='events')
|
||||
event = Event.objects.create(start_datetime=now(), places=10, agenda=agenda, slug='event')
|
||||
|
||||
not_checked = Booking.objects.create(event=event)
|
||||
present = Booking.objects.create(event=event, user_was_present=True)
|
||||
absent = Booking.objects.create(event=event, user_was_present=False)
|
||||
with_check_type = Booking.objects.create(
|
||||
event=event,
|
||||
user_was_present=False,
|
||||
user_check_type_slug='xxx',
|
||||
user_check_type_label='XXX',
|
||||
user_check_start_time=datetime.time(12, 0),
|
||||
user_check_end_time=datetime.time(14, 0),
|
||||
computed_start_time=datetime.time(12, 30),
|
||||
computed_end_time=datetime.time(14, 30),
|
||||
)
|
||||
|
||||
executor = MigrationExecutor(connection)
|
||||
executor.migrate(migrate_to)
|
||||
executor.loader.build_graph()
|
||||
|
||||
apps = executor.loader.project_state(migrate_to).apps
|
||||
Booking = apps.get_model(app, 'Booking')
|
||||
|
||||
assert not hasattr(Booking.objects.get(pk=not_checked.pk), 'user_check')
|
||||
assert Booking.objects.get(pk=present.pk).user_check.presence is True
|
||||
assert Booking.objects.get(pk=absent.pk).user_check.presence is False
|
||||
|
||||
with_check_type = Booking.objects.get(pk=with_check_type.pk)
|
||||
assert with_check_type.user_check.presence is False
|
||||
assert with_check_type.user_check.type_slug == 'xxx'
|
||||
assert with_check_type.user_check.type_label == 'XXX'
|
||||
assert with_check_type.user_check.start_time == datetime.time(12, 0)
|
||||
assert with_check_type.user_check.end_time == datetime.time(14, 0)
|
||||
assert with_check_type.computed_start_time == datetime.time(12, 30)
|
||||
assert with_check_type.computed_end_time == datetime.time(14, 30)
|
||||
|
|
7
tox.ini
7
tox.ini
|
@ -23,7 +23,8 @@ deps =
|
|||
WebTest
|
||||
mock<4
|
||||
httmock
|
||||
pylint
|
||||
pylint<3
|
||||
astroid<3
|
||||
pylint-django
|
||||
django-webtest
|
||||
pytz
|
||||
|
@ -34,6 +35,7 @@ deps =
|
|||
weasyprint<52
|
||||
django32: django>=3.2,<3.3
|
||||
django32: psycopg2-binary>=2.9
|
||||
djangorestframework>=3.12,<3.13 # matching debian bullseye
|
||||
codestyle: pre-commit
|
||||
git+https://git.entrouvert.org/publik-django-templatetags.git
|
||||
responses
|
||||
|
@ -53,7 +55,8 @@ deps =
|
|||
WebTest
|
||||
mock<4
|
||||
httmock
|
||||
pylint
|
||||
pylint<3
|
||||
astroid<3
|
||||
pylint-django
|
||||
django-webtest
|
||||
pytz
|
||||
|
|
Loading…
Reference in New Issue