Compare commits
77 Commits
42ba71dd7d
...
d8731b6240
Author | SHA1 | Date |
---|---|---|
Lauréline Guérin | d8731b6240 | |
Lauréline Guérin | e414eedc46 | |
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 | |
Frédéric Péters | 5f14f2a47b | |
Valentin Deniaud | b0f8af1dea | |
Valentin Deniaud | b71dc670c7 | |
Valentin Deniaud | ebe3b7eb10 | |
Valentin Deniaud | 3d576b48ee | |
Valentin Deniaud | 95618bd475 | |
Valentin Deniaud | c1dd25d2c7 | |
Valentin Deniaud | a70045ea5b | |
Valentin Deniaud | 6b964d708b | |
Thomas NOËL | 7359d50232 | |
Valentin Deniaud | 7dbf299eda | |
Valentin Deniaud | d1597d7ab3 | |
Valentin Deniaud | 0cc06d2047 | |
Valentin Deniaud | 0146309c4f | |
Valentin Deniaud | bfea238c08 | |
Valentin Deniaud | 46e60b37a1 | |
Valentin Deniaud | beb31a38ca | |
Frédéric Péters | e6b5ace001 | |
Frédéric Péters | 0835bb633d | |
Valentin Deniaud | 6b79f58bd5 | |
Valentin Deniaud | 152110888c | |
Valentin Deniaud | f28cd4d104 | |
Thomas Jund | 00ad7b0747 | |
Valentin Deniaud | b615c57bad | |
Valentin Deniaud | 4834743c6d | |
Valentin Deniaud | 06af90608f | |
Valentin Deniaud | 900300dd05 | |
Lauréline Guérin | 747928c680 | |
Lauréline Guérin | 848d014720 | |
Valentin Deniaud | cdcb663f85 | |
Valentin Deniaud | 9125b7af10 | |
Valentin Deniaud | cce129b8bc | |
Lauréline Guérin | 90d3c29b72 | |
Frédéric Péters | 4492026242 | |
Frédéric Péters | 36b8fd4f9d | |
Lauréline Guérin | cb9944050e | |
Valentin Deniaud | f9ae449f7c | |
Benjamin Dauvergne | dae3c05148 | |
Benjamin Dauvergne | 1a59dcb97c | |
Emmanuel Cazenave | 51a888fe81 |
|
@ -21,3 +21,5 @@ e07c450d7c8a5f80aafe185c85ebed73fe39d9e7
|
|||
b38f5f901e1bef556bd95f45bcc041b092b1a617
|
||||
# misc: bump djhtml version (#75442)
|
||||
34309253eddc15f17a280656a3ffec072e79731a
|
||||
# misc: apply double-quote-string-fixer (#79866)
|
||||
b71dc670c7f90c675edb510643b992aaf69f852a
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: double-quote-string-fixer
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
hooks:
|
||||
|
|
|
@ -32,9 +32,9 @@ pipeline {
|
|||
'''
|
||||
).trim()
|
||||
if (env.GIT_BRANCH == 'main' || env.GIT_BRANCH == 'origin/main') {
|
||||
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye ${SHORT_JOB_NAME}"
|
||||
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye,bookworm ${SHORT_JOB_NAME}"
|
||||
} else if (env.GIT_BRANCH.startsWith('hotfix/')) {
|
||||
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye --branch ${env.GIT_BRANCH} --hotfix ${SHORT_JOB_NAME}"
|
||||
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye,bookworm --branch ${env.GIT_BRANCH} --hotfix ${SHORT_JOB_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
from chrono.agendas.models import WEEKDAY_CHOICES
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
|
@ -14,9 +16,7 @@ class Migration(migrations.Migration):
|
|||
model_name='event',
|
||||
name='recurrence_days',
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.IntegerField(
|
||||
choices=[(0, 'Mo'), (1, 'Tu'), (2, 'We'), (3, 'Th'), (4, 'Fr'), (5, 'Sa'), (6, 'Su')]
|
||||
),
|
||||
base_field=models.IntegerField(choices=WEEKDAY_CHOICES),
|
||||
blank=True,
|
||||
null=True,
|
||||
size=None,
|
||||
|
|
|
@ -4,6 +4,8 @@ import django.contrib.postgres.fields
|
|||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
from chrono.agendas.models import WEEKDAY_CHOICES
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
|
@ -60,17 +62,7 @@ class Migration(migrations.Migration):
|
|||
(
|
||||
'days',
|
||||
django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.IntegerField(
|
||||
choices=[
|
||||
(0, 'Mo'),
|
||||
(1, 'Tu'),
|
||||
(2, 'We'),
|
||||
(3, 'Th'),
|
||||
(4, 'Fr'),
|
||||
(5, 'Sa'),
|
||||
(6, 'Su'),
|
||||
]
|
||||
),
|
||||
base_field=models.IntegerField(choices=WEEKDAY_CHOICES),
|
||||
size=None,
|
||||
verbose_name='Days',
|
||||
),
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 3.2.18 on 2023-06-28 10:46
|
||||
|
||||
import django.db.models.expressions
|
||||
import django.db.models.functions.datetime
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0155_event_triggers'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveIndex(
|
||||
model_name='event',
|
||||
name='start_datetime_dow_index',
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='event',
|
||||
index=models.Index(
|
||||
django.db.models.functions.datetime.ExtractIsoWeekDay('start_datetime'),
|
||||
django.db.models.expressions.F('start_datetime'),
|
||||
condition=models.Q(('cancelled', False)),
|
||||
name='start_datetime_dow_index',
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,41 @@
|
|||
# Generated by Django 3.2.18 on 2023-06-28 10:47
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db.models import F, Func, OuterRef, Subquery
|
||||
|
||||
|
||||
class ArraySubquery(Subquery):
|
||||
template = 'ARRAY(%(subquery)s)'
|
||||
|
||||
|
||||
def convert_week_days(apps, schema_editor):
|
||||
Event = apps.get_model('agendas', 'Event')
|
||||
SharedCustodyRule = apps.get_model('agendas', 'SharedCustodyRule') # TODO
|
||||
|
||||
events_with_days = (
|
||||
Event.objects.filter(pk=OuterRef('pk'))
|
||||
.annotate(week_day=Func(F('recurrence_days'), function='unnest', output_field=models.IntegerField()))
|
||||
.annotate(new_week_day=F('week_day') + 1)
|
||||
.values('new_week_day')
|
||||
)
|
||||
Event.objects.filter(recurrence_days__isnull=False).update(
|
||||
recurrence_days=ArraySubquery(events_with_days)
|
||||
)
|
||||
|
||||
rules_with_days = (
|
||||
SharedCustodyRule.objects.filter(pk=OuterRef('pk'))
|
||||
.annotate(week_day=Func(F('days'), function='unnest', output_field=models.IntegerField()))
|
||||
.annotate(new_week_day=F('week_day') + 1)
|
||||
.values('new_week_day')
|
||||
)
|
||||
SharedCustodyRule.objects.update(days=ArraySubquery(rules_with_days))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0156_update_dow_index'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(convert_week_days, migrations.RunPython.noop),
|
||||
]
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 3.2.18 on 2023-07-05 10:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0157_convert_week_days'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='user_check_end_time',
|
||||
field=models.TimeField(null=True, verbose_name='Departure'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='booking',
|
||||
name='user_check_start_time',
|
||||
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',
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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 (
|
||||
|
@ -51,7 +53,7 @@ from django.db.models import (
|
|||
Subquery,
|
||||
Value,
|
||||
)
|
||||
from django.db.models.functions import Cast, Coalesce, Concat, ExtractWeek, ExtractWeekDay, JSONObject
|
||||
from django.db.models.functions import Cast, Coalesce, Concat, ExtractIsoWeekDay, ExtractWeek, JSONObject
|
||||
from django.template import (
|
||||
Context,
|
||||
RequestContext,
|
||||
|
@ -96,24 +98,25 @@ AGENDA_VIEWS = (
|
|||
('open_events', _('Open events')),
|
||||
)
|
||||
|
||||
WEEKDAYS_PLURAL = {
|
||||
0: _('Mondays'),
|
||||
1: _('Tuesdays'),
|
||||
2: _('Wednesdays'),
|
||||
3: _('Thursdays'),
|
||||
4: _('Fridays'),
|
||||
5: _('Saturdays'),
|
||||
6: _('Sundays'),
|
||||
ISO_WEEKDAYS = {index + 1: day for index, day in WEEKDAYS.items()}
|
||||
ISO_WEEKDAYS_PLURAL = {
|
||||
1: _('Mondays'),
|
||||
2: _('Tuesdays'),
|
||||
3: _('Wednesdays'),
|
||||
4: _('Thursdays'),
|
||||
5: _('Fridays'),
|
||||
6: _('Saturdays'),
|
||||
7: _('Sundays'),
|
||||
}
|
||||
|
||||
WEEKDAY_CHOICES = [
|
||||
(0, _('Mo')),
|
||||
(1, _('Tu')),
|
||||
(2, _('We')),
|
||||
(3, _('Th')),
|
||||
(4, _('Fr')),
|
||||
(5, _('Sa')),
|
||||
(6, _('Su')),
|
||||
(1, _('Mo')),
|
||||
(2, _('Tu')),
|
||||
(3, _('We')),
|
||||
(4, _('Th')),
|
||||
(5, _('Fr')),
|
||||
(6, _('Sa')),
|
||||
(7, _('Su')),
|
||||
]
|
||||
|
||||
|
||||
|
@ -284,13 +287,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']
|
||||
|
@ -323,6 +340,12 @@ class Agenda(models.Model):
|
|||
def get_settings_url(self):
|
||||
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.id})
|
||||
|
||||
def get_real_kind_display(self):
|
||||
if self.kind == 'events' and self.partial_bookings:
|
||||
return _('Partial bookings')
|
||||
|
||||
return self.get_kind_display()
|
||||
|
||||
def get_lingo_url(self):
|
||||
lingo = get_lingo_service()
|
||||
if not lingo:
|
||||
|
@ -935,15 +958,14 @@ class Agenda(models.Model):
|
|||
guardian__user_external_id=guardian_external_id,
|
||||
agenda=agenda,
|
||||
)
|
||||
.annotate(day=Func(F('days'), function='unnest', output_field=models.IntegerField()))
|
||||
.annotate(week_day=(F('day') + 1) % 7 + 1) # convert ISO day number to db lookup day number
|
||||
.annotate(week_day=Func(F('days'), function='unnest', output_field=models.IntegerField()))
|
||||
.values('week_day')
|
||||
)
|
||||
|
||||
rules_lookup = (
|
||||
Q(start_datetime__week_day__in=rules.filter(weeks=''))
|
||||
| Q(start_datetime__week_day__in=rules.filter(weeks='even'), odd_week=False)
|
||||
| Q(start_datetime__week_day__in=rules.filter(weeks='odd'), odd_week=True)
|
||||
Q(start_datetime__iso_week_day__in=rules.filter(weeks=''))
|
||||
| Q(start_datetime__iso_week_day__in=rules.filter(weeks='even'), odd_week=False)
|
||||
| Q(start_datetime__iso_week_day__in=rules.filter(weeks='odd'), odd_week=True)
|
||||
)
|
||||
|
||||
all_periods = SharedCustodyPeriod.objects.filter(
|
||||
|
@ -1115,6 +1137,37 @@ class Agenda(models.Model):
|
|||
|
||||
return True
|
||||
|
||||
def event_overlaps(self, start_datetime, recurrence_days, recurrence_end_date, instance=None):
|
||||
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)
|
||||
|
||||
if recurrence_days:
|
||||
qs = qs.filter(
|
||||
# check overlap with other recurring events
|
||||
Q(recurrence_end_date__isnull=True) | Q(recurrence_end_date__gte=start_datetime),
|
||||
Q(recurrence_days__overlap=recurrence_days)
|
||||
# check overlap with normal events
|
||||
| Q(start_datetime__gte=start_datetime, start_datetime__iso_week_day__in=recurrence_days),
|
||||
)
|
||||
if recurrence_end_date:
|
||||
qs = qs.filter(start_datetime__lte=recurrence_end_date)
|
||||
else:
|
||||
qs = qs.filter(
|
||||
# check overlap with other normal events
|
||||
Q(start_datetime__date=start_datetime.date())
|
||||
# check overlap with recurring events
|
||||
| Q(
|
||||
Q(recurrence_end_date__isnull=True) | Q(recurrence_end_date__gte=start_datetime),
|
||||
recurrence_days__contains=[localtime(start_datetime).isoweekday()],
|
||||
)
|
||||
)
|
||||
|
||||
return qs.exists()
|
||||
|
||||
def get_min_datetime(self, start_datetime=None):
|
||||
if self.minimal_booking_delay is None:
|
||||
return start_datetime
|
||||
|
@ -1653,7 +1706,7 @@ class TimePeriod(models.Model):
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.agenda:
|
||||
assert self.agenda.kind == 'virtual', "a time period can only reference a virtual agenda"
|
||||
assert self.agenda.kind == 'virtual', 'a time period can only reference a virtual agenda'
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
|
@ -1991,7 +2044,7 @@ class Event(models.Model):
|
|||
unique_together = ('agenda', 'slug')
|
||||
indexes = [
|
||||
models.Index(
|
||||
ExtractWeekDay('start_datetime'),
|
||||
ExtractIsoWeekDay('start_datetime'),
|
||||
'start_datetime',
|
||||
name='start_datetime_dow_index',
|
||||
condition=models.Q(cancelled=False),
|
||||
|
@ -2229,11 +2282,7 @@ class Event(models.Model):
|
|||
qs, agenda_slugs, user_external_id, start_datetime, end_datetime
|
||||
):
|
||||
recurrences = Event.objects.filter(primary_event=OuterRef('pk'))
|
||||
recurrences = recurrences.annotate(
|
||||
dj_weekday=ExtractWeekDay('start_datetime'),
|
||||
dj_weekday_int=Cast('dj_weekday', models.IntegerField()),
|
||||
weekday=(F('dj_weekday_int') - 2) % 7,
|
||||
)
|
||||
recurrences = recurrences.annotate(weekday=ExtractIsoWeekDay('start_datetime'))
|
||||
recurrences_with_overlaps = Event.annotate_queryset_with_booked_event_overlaps(
|
||||
recurrences, agenda_slugs, user_external_id, start_datetime, end_datetime
|
||||
).filter(has_overlap=True)
|
||||
|
@ -2331,6 +2380,11 @@ class Event(models.Model):
|
|||
)
|
||||
except ValueError:
|
||||
raise AgendaImportError(_('Bad datetime format "%s"') % data['start_datetime'])
|
||||
|
||||
if data.get('recurrence_days'):
|
||||
# keep stable weekday numbering after switch to ISO in db
|
||||
data['recurrence_days'] = [i + 1 for i in data['recurrence_days']]
|
||||
|
||||
data = clean_import_data(cls, data)
|
||||
if data.get('slug'):
|
||||
event, dummy = cls.objects.update_or_create(
|
||||
|
@ -2368,7 +2422,13 @@ class Event(models.Model):
|
|||
'publication_datetime': make_naive(self.publication_datetime).strftime('%Y-%m-%d %H:%M:%S')
|
||||
if self.publication_datetime
|
||||
else None,
|
||||
'recurrence_days': self.recurrence_days,
|
||||
'recurrence_days': [
|
||||
# keep stable weekday numbering after switch to ISO in db
|
||||
i - 1
|
||||
for i in self.recurrence_days
|
||||
]
|
||||
if self.recurrence_days
|
||||
else None,
|
||||
'recurrence_week_interval': self.recurrence_week_interval,
|
||||
'recurrence_end_date': recurrence_end_date,
|
||||
'places': self.places,
|
||||
|
@ -2476,12 +2536,12 @@ class Event(models.Model):
|
|||
elif days_count > 1 and (self.recurrence_days[-1] - self.recurrence_days[0]) == days_count - 1:
|
||||
# days are contiguous
|
||||
repeat = _('From %(weekday)s to %(last_weekday)s') % {
|
||||
'weekday': str(WEEKDAYS[self.recurrence_days[0]]),
|
||||
'last_weekday': str(WEEKDAYS[self.recurrence_days[-1]]),
|
||||
'weekday': str(ISO_WEEKDAYS[self.recurrence_days[0]]),
|
||||
'last_weekday': str(ISO_WEEKDAYS[self.recurrence_days[-1]]),
|
||||
}
|
||||
else:
|
||||
repeat = _('On %(weekdays)s') % {
|
||||
'weekdays': ', '.join([str(WEEKDAYS_PLURAL[i]) for i in self.recurrence_days])
|
||||
'weekdays': ', '.join([str(ISO_WEEKDAYS_PLURAL[i]) for i in self.recurrence_days])
|
||||
}
|
||||
|
||||
recurrence_display = _('%(On_day_x)s at %(time)s') % {'On_day_x': repeat, 'time': time}
|
||||
|
@ -2515,7 +2575,7 @@ class Event(models.Model):
|
|||
def recurrence_rule(self):
|
||||
recurrence_rule = {
|
||||
'freq': WEEKLY,
|
||||
'byweekday': self.recurrence_days,
|
||||
'byweekday': [i - 1 for i in self.recurrence_days or []],
|
||||
'interval': self.recurrence_week_interval,
|
||||
}
|
||||
if self.recurrence_end_date:
|
||||
|
@ -2662,6 +2722,8 @@ class Booking(models.Model):
|
|||
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)
|
||||
user_check_end_time = models.TimeField(_('Departure'), null=True)
|
||||
out_of_min_delay = models.BooleanField(default=False)
|
||||
|
||||
extra_emails = ArrayField(models.EmailField(), default=list)
|
||||
|
@ -2791,9 +2853,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(),
|
||||
|
@ -2821,6 +2881,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()
|
||||
|
||||
|
@ -2842,6 +2908,69 @@ class Booking(models.Model):
|
|||
def get_backoffice_url(self):
|
||||
return translate_from_publik_url(self.backoffice_url)
|
||||
|
||||
def _get_previous_and_next_slots(self, _time):
|
||||
minutes = {
|
||||
'hour': 60,
|
||||
'half_hour': 30,
|
||||
'quarter': 15,
|
||||
}[self.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.user_check_start_time is None:
|
||||
return None
|
||||
|
||||
start_time = self.user_check_start_time
|
||||
if self.start_time:
|
||||
start_time = min(start_time, self.start_time)
|
||||
if self.event.agenda.invoicing_unit == 'minute':
|
||||
return start_time
|
||||
|
||||
tolerance = self.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.user_check_end_time is None:
|
||||
return None
|
||||
|
||||
end_time = self.user_check_end_time
|
||||
if self.end_time:
|
||||
end_time = max(end_time, self.end_time)
|
||||
if self.event.agenda.invoicing_unit == 'minute':
|
||||
return end_time
|
||||
|
||||
tolerance = self.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 get_partial_bookings_check_url(self, agenda, event=None):
|
||||
return reverse(
|
||||
'chrono-manager-partial-booking-check',
|
||||
kwargs={'pk': agenda.pk, 'booking_pk': self.pk},
|
||||
)
|
||||
|
||||
|
||||
OpeningHour = collections.namedtuple('OpeningHour', ['begin', 'end'])
|
||||
|
||||
|
@ -2894,14 +3023,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()],
|
||||
}
|
||||
|
||||
|
@ -3325,15 +3455,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,
|
||||
|
@ -3854,6 +4000,12 @@ class Subscription(models.Model):
|
|||
except (VariableDoesNotExist, TemplateSyntaxError):
|
||||
return
|
||||
|
||||
def get_partial_bookings_check_url(self, agenda, event):
|
||||
return reverse(
|
||||
'chrono-manager-partial-booking-subscription-check',
|
||||
kwargs={'pk': agenda.pk, 'event_pk': event.pk, 'subscription_pk': self.pk},
|
||||
)
|
||||
|
||||
|
||||
class Person(models.Model):
|
||||
user_external_id = models.CharField(max_length=250, unique=True)
|
||||
|
@ -3982,7 +4134,7 @@ class SharedCustodyAgenda(models.Model):
|
|||
qs = qs.exclude(pk=instance.pk)
|
||||
|
||||
qs = qs.extra(
|
||||
where=["(date_start, date_end) OVERLAPS (%s, %s)"],
|
||||
where=['(date_start, date_end) OVERLAPS (%s, %s)'],
|
||||
params=[date_start, date_end],
|
||||
)
|
||||
return qs.exists()
|
||||
|
@ -4006,7 +4158,7 @@ class SharedCustodyRule(models.Model):
|
|||
def get_slots(self, min_date, max_date):
|
||||
recurrence_rule = {
|
||||
'freq': WEEKLY,
|
||||
'byweekday': self.days,
|
||||
'byweekday': [i - 1 for i in self.days],
|
||||
}
|
||||
if self.weeks == 'odd':
|
||||
recurrence_rule['byweekno'] = list(range(1, 55, 2))
|
||||
|
@ -4026,12 +4178,12 @@ class SharedCustodyRule(models.Model):
|
|||
elif days_count > 1 and (self.days[-1] - self.days[0]) == days_count - 1:
|
||||
# days are contiguous
|
||||
repeat = _('from %(weekday)s to %(last_weekday)s') % {
|
||||
'weekday': str(WEEKDAYS[self.days[0]]),
|
||||
'last_weekday': str(WEEKDAYS[self.days[-1]]),
|
||||
'weekday': str(ISO_WEEKDAYS[self.days[0]]),
|
||||
'last_weekday': str(ISO_WEEKDAYS[self.days[-1]]),
|
||||
}
|
||||
else:
|
||||
repeat = _('on %(weekdays)s') % {
|
||||
'weekdays': ', '.join([str(WEEKDAYS_PLURAL[i]) for i in self.days])
|
||||
'weekdays': ', '.join([str(ISO_WEEKDAYS_PLURAL[i]) for i in self.days])
|
||||
}
|
||||
|
||||
if self.weeks == 'odd':
|
||||
|
|
|
@ -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)
|
||||
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.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)
|
||||
|
@ -224,8 +224,6 @@ class RecurringFillslotsSerializer(MultipleAgendasEventsFillSlotsSerializer):
|
|||
% {'event_slug': event_slug, 'agenda_slug': agenda_slug}
|
||||
)
|
||||
|
||||
# convert ISO day number to db lookup day number
|
||||
day = (day + 1) % 7 + 1
|
||||
slots[agenda_slug][event_slug].append(day)
|
||||
|
||||
return slots
|
||||
|
@ -286,6 +284,29 @@ class BookingSerializer(serializers.ModelSerializer):
|
|||
ret['user_presence_reason'] = (
|
||||
self.instance.user_check_type_slug if self.instance.user_was_present is True else None
|
||||
)
|
||||
if self.instance.event.agenda.kind == 'events' and self.instance.event.agenda.partial_bookings:
|
||||
self.instance.computed_start_time = self.instance.get_computed_start_time()
|
||||
self.instance.computed_end_time = self.instance.get_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):
|
||||
|
@ -448,7 +469,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):
|
||||
|
@ -512,6 +533,10 @@ class EventSerializer(serializers.ModelSerializer):
|
|||
**(field_options.get(custom_field['field_type']) or {}),
|
||||
)
|
||||
|
||||
def validate_recurrence_days(self, value):
|
||||
# keep stable weekday numbering after switch to ISO in db
|
||||
return [i + 1 for i in value]
|
||||
|
||||
def validate(self, attrs):
|
||||
if not self.instance.agenda.events_type:
|
||||
return attrs
|
||||
|
@ -537,6 +562,9 @@ class EventSerializer(serializers.ModelSerializer):
|
|||
|
||||
def to_representation(self, instance):
|
||||
ret = super().to_representation(instance)
|
||||
if ret.get('recurrence_days'):
|
||||
# keep stable weekday numbering after switch to ISO in db
|
||||
ret['recurrence_days'] = [i - 1 for i in ret['recurrence_days']]
|
||||
if not self.instance.agenda.events_type:
|
||||
return ret
|
||||
defaults = {
|
||||
|
@ -575,6 +603,7 @@ class AgendaSerializer(serializers.ModelSerializer):
|
|||
'edit_role',
|
||||
'view_role',
|
||||
'category',
|
||||
'booking_form_url',
|
||||
'mark_event_checked_auto',
|
||||
'disable_check_update',
|
||||
'booking_check_filters',
|
||||
|
|
|
@ -57,9 +57,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 +124,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,6 +20,7 @@ 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
|
||||
|
@ -29,7 +30,6 @@ from django.http import Http404, HttpResponse
|
|||
from django.shortcuts import get_object_or_404
|
||||
from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist
|
||||
from django.urls import reverse
|
||||
from django.utils.dates import WEEKDAYS
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import gettext
|
||||
|
@ -43,6 +43,7 @@ from rest_framework.generics import ListAPIView
|
|||
from rest_framework.views import APIView
|
||||
|
||||
from chrono.agendas.models import (
|
||||
ISO_WEEKDAYS,
|
||||
Agenda,
|
||||
Booking,
|
||||
BookingColor,
|
||||
|
@ -95,6 +96,9 @@ 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(
|
||||
|
@ -112,9 +116,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})
|
||||
)
|
||||
|
@ -142,7 +143,14 @@ def get_event_places(event):
|
|||
return places
|
||||
|
||||
|
||||
def is_event_disabled(event, min_places=1, disable_booked=True, bookable_events=None, bypass_delays=False):
|
||||
def is_event_disabled(
|
||||
event,
|
||||
min_places=1,
|
||||
disable_booked=True,
|
||||
bookable_events=None,
|
||||
bypass_delays=False,
|
||||
enable_full_when_booked=False,
|
||||
):
|
||||
if disable_booked and getattr(event, 'user_places_count', 0) > 0:
|
||||
return True
|
||||
if event.start_datetime < now():
|
||||
|
@ -160,12 +168,18 @@ def is_event_disabled(event, min_places=1, disable_booked=True, bookable_events=
|
|||
# event is out of minimal delay and we don't want to bypass delays
|
||||
return True
|
||||
if event.remaining_places < min_places and event.remaining_waiting_list_places < min_places:
|
||||
if enable_full_when_booked and getattr(event, 'user_places_count', 0) > 0:
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
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(
|
||||
|
@ -180,7 +194,7 @@ def get_event_text(event, agenda, day=None):
|
|||
)
|
||||
elif day is not None:
|
||||
event_text = _('%(weekday)s: %(event)s') % {
|
||||
'weekday': WEEKDAYS[day].capitalize(),
|
||||
'weekday': event.weekday,
|
||||
'event': event_text,
|
||||
}
|
||||
return event_text
|
||||
|
@ -199,6 +213,7 @@ def get_event_detail(
|
|||
disable_booked=True,
|
||||
bypass_delays=False,
|
||||
with_status=False,
|
||||
enable_full_when_booked=False,
|
||||
):
|
||||
details = get_short_event_detail(
|
||||
request=request,
|
||||
|
@ -228,7 +243,11 @@ def get_event_detail(
|
|||
if event.recurrence_days:
|
||||
details.update(
|
||||
{
|
||||
'recurrence_days': event.recurrence_days,
|
||||
'recurrence_days': [
|
||||
# keep stable weekday numbering after switch to ISO in db
|
||||
i - 1
|
||||
for i in event.recurrence_days
|
||||
],
|
||||
'recurrence_week_interval': event.recurrence_week_interval,
|
||||
'recurrence_end_date': event.recurrence_end_date,
|
||||
}
|
||||
|
@ -245,6 +264,7 @@ def get_event_detail(
|
|||
disable_booked=disable_booked,
|
||||
bookable_events=bookable_events,
|
||||
bypass_delays=bypass_delays,
|
||||
enable_full_when_booked=enable_full_when_booked,
|
||||
),
|
||||
'api': {
|
||||
'bookings_url': request.build_absolute_uri(
|
||||
|
@ -743,6 +763,7 @@ class MultipleAgendasDatetimes(APIView):
|
|||
booked_user_external_id=payload.get('user_external_id'),
|
||||
multiple_agendas=True,
|
||||
disable_booked=disable_booked,
|
||||
enable_full_when_booked=True,
|
||||
bypass_delays=payload.get('bypass_delays'),
|
||||
with_status=with_status,
|
||||
)
|
||||
|
@ -958,7 +979,7 @@ class RecurringEventsList(APIView):
|
|||
for agenda in agendas:
|
||||
for event in agenda.prefetched_events:
|
||||
if event.primary_event_id:
|
||||
days_by_event[event.primary_event_id].add(event.start_datetime.weekday())
|
||||
days_by_event[event.primary_event_id].add(event.start_datetime.isoweekday())
|
||||
recurring_events = Event.objects.filter(pk__in=days_by_event).select_related(
|
||||
'agenda', 'agenda__category'
|
||||
)
|
||||
|
@ -1028,7 +1049,7 @@ class RecurringEventsList(APIView):
|
|||
'text': get_event_text(event, event.agenda, event.day),
|
||||
'slug': event.slug,
|
||||
'label': event.label or '',
|
||||
'day': WEEKDAYS[event.day].capitalize(),
|
||||
'day': ISO_WEEKDAYS[event.day].capitalize(),
|
||||
'date': format_response_date(event.start_datetime),
|
||||
'datetime': format_response_datetime(event.start_datetime),
|
||||
'description': event.description,
|
||||
|
@ -1158,28 +1179,14 @@ class AgendaResourceList(APIView):
|
|||
agenda_resource_list = AgendaResourceList.as_view()
|
||||
|
||||
|
||||
class Fillslots(APIView):
|
||||
class EventsAgendaFillslot(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.FillSlotsSerializer
|
||||
serializer_class = serializers.FillSlotSerializer
|
||||
|
||||
def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
|
||||
if not settings.LEGACY_FILLSLOTS_ENABLED:
|
||||
raise APIErrorBadRequest(N_('deprecated call'))
|
||||
|
||||
return self.fillslot(request=request, agenda_identifier=agenda_identifier, format=format)
|
||||
|
||||
def fillslot(self, request, agenda_identifier=None, slots=None, format=None, retry=False):
|
||||
slots = slots or []
|
||||
multiple_booking = bool(not slots)
|
||||
try:
|
||||
agenda = Agenda.objects.get(slug=agenda_identifier)
|
||||
except Agenda.DoesNotExist:
|
||||
try:
|
||||
# legacy access by agenda id
|
||||
agenda = Agenda.objects.get(id=int(agenda_identifier))
|
||||
except (ValueError, Agenda.DoesNotExist):
|
||||
raise Http404()
|
||||
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'}
|
||||
)
|
||||
|
@ -1189,14 +1196,12 @@ class Fillslots(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:
|
||||
|
@ -1240,159 +1245,28 @@ class Fillslots(APIView):
|
|||
|
||||
extra_data = get_extra_data(request, serializer.validated_data)
|
||||
|
||||
available_desk = None
|
||||
color = None
|
||||
user_external_id = payload.get('user_external_id') or None
|
||||
exclude_user = payload.get('exclude_user')
|
||||
event = get_events_from_slots([slot], request, agenda, payload)[0]
|
||||
|
||||
if agenda.accept_meetings():
|
||||
# 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)
|
||||
|
||||
resources = get_resources_from_request(request, agenda)
|
||||
|
||||
# get all free slots and separate them by desk
|
||||
try:
|
||||
try:
|
||||
meeting_type = agenda.get_meetingtype(slug=meeting_type_id)
|
||||
except MeetingType.DoesNotExist:
|
||||
# legacy access by id
|
||||
meeting_type = agenda.get_meetingtype(id_=meeting_type_id)
|
||||
except (MeetingType.DoesNotExist, ValueError):
|
||||
raise APIErrorBadRequest(N_('invalid meeting type id: %s'), meeting_type_id)
|
||||
all_slots = sorted(
|
||||
agenda.get_all_slots(
|
||||
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),
|
||||
),
|
||||
key=lambda slot: slot.start_datetime,
|
||||
)
|
||||
|
||||
all_free_slots = [slot for slot in all_slots if not slot.full]
|
||||
datetimes_by_desk = collections.defaultdict(set)
|
||||
for slot in all_free_slots:
|
||||
datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
|
||||
|
||||
color_label = payload.get('use_color_for')
|
||||
if color_label:
|
||||
color = BookingColor.objects.get_or_create(label=color_label)[0]
|
||||
|
||||
available_desk = None
|
||||
|
||||
if agenda.kind == 'virtual':
|
||||
# Compute fill_rate by agenda/date
|
||||
fill_rates = collections.defaultdict(dict)
|
||||
for slot in all_slots:
|
||||
ref_date = slot.start_datetime.date()
|
||||
if ref_date not in fill_rates[slot.desk.agenda]:
|
||||
date_dict = fill_rates[slot.desk.agenda][ref_date] = {'free': 0, 'full': 0}
|
||||
else:
|
||||
date_dict = fill_rates[slot.desk.agenda][ref_date]
|
||||
if slot.full:
|
||||
date_dict['full'] += 1
|
||||
else:
|
||||
date_dict['free'] += 1
|
||||
for dd in fill_rates.values():
|
||||
for date_dict in dd.values():
|
||||
date_dict['fill_rate'] = date_dict['full'] / (date_dict['full'] + date_dict['free'])
|
||||
|
||||
# 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]):
|
||||
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'
|
||||
]
|
||||
else:
|
||||
for dt in datetimes:
|
||||
desk_rate = 0
|
||||
for dt in datetimes:
|
||||
desk_rate += fill_rates[desk.agenda][dt.date()]['fill_rate']
|
||||
if desk_rate < available_desk_rate:
|
||||
available_desk = desk
|
||||
available_desk_rate = desk_rate
|
||||
# search free places. Switch to waiting list if necessary.
|
||||
in_waiting_list = False
|
||||
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:
|
||||
# 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]):
|
||||
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,
|
||||
)
|
||||
)
|
||||
in_waiting_list = False
|
||||
else:
|
||||
events = get_events_from_slots(slots, request, agenda, payload)
|
||||
|
||||
# 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.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'))
|
||||
if (event.booked_places + places_count) > event.places:
|
||||
raise APIError(N_('sold out'))
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
|
@ -1402,18 +1276,17 @@ class Fillslots(APIView):
|
|||
|
||||
# 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)
|
||||
for dummy in range(places_count):
|
||||
new_booking = make_booking(
|
||||
event, payload, extra_data, primary_booking, in_waiting_list, color
|
||||
)
|
||||
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
|
||||
|
@ -1427,14 +1300,14 @@ class Fillslots(APIView):
|
|||
# of fillslot().
|
||||
if retry:
|
||||
raise APIError(N_('no more desk available'))
|
||||
return self.fillslot(request, agenda_identifier=agenda_identifier, 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,
|
||||
|
@ -1452,61 +1325,263 @@ class Fillslots(APIView):
|
|||
'anonymize_url': request.build_absolute_uri(
|
||||
reverse('api-anonymize-booking', kwargs={'booking_pk': primary_booking.id})
|
||||
),
|
||||
'accept_url': request.build_absolute_uri(
|
||||
reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.pk})
|
||||
),
|
||||
'suspend_url': request.build_absolute_uri(
|
||||
reverse('api-suspend-booking', kwargs={'booking_pk': primary_booking.pk})
|
||||
),
|
||||
},
|
||||
}
|
||||
if agenda.kind == 'events':
|
||||
response['api']['accept_url'] = request.build_absolute_uri(
|
||||
reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.pk})
|
||||
)
|
||||
response['api']['suspend_url'] = request.build_absolute_uri(
|
||||
reverse('api-suspend-booking', kwargs={'booking_pk': primary_booking.pk})
|
||||
)
|
||||
if agenda.accept_meetings():
|
||||
response['end_datetime'] = format_response_datetime(events[-1].end_datetime)
|
||||
response['duration'] = (events[-1].end_datetime - events[-1].start_datetime).seconds // 60
|
||||
if available_desk:
|
||||
response['desk'] = {'label': available_desk.label, 'slug': available_desk.slug}
|
||||
if to_cancel_booking:
|
||||
response['cancelled_booking_id'] = cancelled_booking_id
|
||||
if agenda.kind == 'events' and 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
|
||||
if agenda.kind == 'events' and multiple_booking:
|
||||
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
|
||||
]
|
||||
if agenda.kind == 'meetings':
|
||||
response['resources'] = [r.slug for r in resources]
|
||||
|
||||
# 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
|
||||
|
||||
return Response(response)
|
||||
|
||||
|
||||
fillslots = Fillslots.as_view()
|
||||
|
||||
|
||||
class Fillslot(Fillslots):
|
||||
class MeetingsAgendaFillslot(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
serializer_class = serializers.FillSlotSerializer
|
||||
|
||||
def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
|
||||
return self.fillslot(
|
||||
request=request,
|
||||
agenda_identifier=agenda_identifier,
|
||||
slots=[event_identifier], # fill a "list on one slot"
|
||||
format=format,
|
||||
def post(self, request, agenda, slot):
|
||||
return self.fillslot(request=request, agenda=agenda, timeslot_id=slot)
|
||||
|
||||
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'}
|
||||
)
|
||||
if known_body_params:
|
||||
params = ', '.join(sorted(list(known_body_params)))
|
||||
raise APIErrorBadRequest(
|
||||
N_('parameters "%s" must be included in request body, not query'), params
|
||||
)
|
||||
|
||||
serializer = self.serializer_class(data=request.data, partial=True)
|
||||
if not serializer.is_valid():
|
||||
raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
|
||||
payload = serializer.validated_data
|
||||
|
||||
to_cancel_booking = None
|
||||
cancel_booking_id = None
|
||||
if payload.get('cancel_booking_id'):
|
||||
try:
|
||||
cancel_booking_id = int(payload.get('cancel_booking_id'))
|
||||
except (ValueError, TypeError):
|
||||
raise APIErrorBadRequest(N_('cancel_booking_id is not an integer'))
|
||||
|
||||
if cancel_booking_id is not None:
|
||||
cancel_error = None
|
||||
try:
|
||||
to_cancel_booking = Booking.objects.get(pk=cancel_booking_id)
|
||||
if to_cancel_booking.cancellation_datetime:
|
||||
cancel_error = N_('cancel booking: booking already cancelled')
|
||||
except Booking.DoesNotExist:
|
||||
cancel_error = N_('cancel booking: booking does no exist')
|
||||
|
||||
if cancel_error:
|
||||
raise APIError(N_(cancel_error))
|
||||
|
||||
extra_data = get_extra_data(request, serializer.validated_data)
|
||||
|
||||
available_desk = None
|
||||
color = None
|
||||
user_external_id = payload.get('user_external_id') or None
|
||||
exclude_user = payload.get('exclude_user')
|
||||
|
||||
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)
|
||||
|
||||
# get all free slots and separate them by desk
|
||||
try:
|
||||
try:
|
||||
meeting_type = agenda.get_meetingtype(slug=meeting_type_id)
|
||||
except MeetingType.DoesNotExist:
|
||||
# legacy access by id
|
||||
meeting_type = agenda.get_meetingtype(id_=meeting_type_id)
|
||||
except (MeetingType.DoesNotExist, ValueError):
|
||||
raise APIErrorBadRequest(N_('invalid meeting type id: %s'), meeting_type_id)
|
||||
all_slots = sorted(
|
||||
agenda.get_all_slots(
|
||||
meeting_type,
|
||||
resources=resources,
|
||||
user_external_id=user_external_id if exclude_user else None,
|
||||
start_datetime=slot_datetime,
|
||||
end_datetime=slot_datetime + datetime.timedelta(minutes=meeting_type.duration),
|
||||
),
|
||||
key=lambda slot: slot.start_datetime,
|
||||
)
|
||||
|
||||
all_free_slots = [slot for slot in all_slots if not slot.full]
|
||||
datetimes_by_desk = collections.defaultdict(set)
|
||||
for slot in all_free_slots:
|
||||
datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
|
||||
|
||||
color_label = payload.get('use_color_for')
|
||||
if color_label:
|
||||
color = BookingColor.objects.get_or_create(label=color_label)[0]
|
||||
|
||||
available_desk = None
|
||||
|
||||
if agenda.kind == 'virtual':
|
||||
# Compute fill_rate by agenda/date
|
||||
fill_rates = collections.defaultdict(dict)
|
||||
for slot in all_slots:
|
||||
ref_date = slot.start_datetime.date()
|
||||
if ref_date not in fill_rates[slot.desk.agenda]:
|
||||
date_dict = fill_rates[slot.desk.agenda][ref_date] = {'free': 0, 'full': 0}
|
||||
else:
|
||||
date_dict = fill_rates[slot.desk.agenda][ref_date]
|
||||
if slot.full:
|
||||
date_dict['full'] += 1
|
||||
else:
|
||||
date_dict['free'] += 1
|
||||
for dd in fill_rates.values():
|
||||
for date_dict in dd.values():
|
||||
date_dict['fill_rate'] = date_dict['full'] / (date_dict['full'] + date_dict['free'])
|
||||
|
||||
# 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 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 = fill_rates[available_desk.agenda][slot_datetime.date()][
|
||||
'fill_rate'
|
||||
]
|
||||
else:
|
||||
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
|
||||
|
||||
else:
|
||||
# meeting agenda
|
||||
# search first desk where all requested slots are free
|
||||
for available_desk_id in sorted(datetimes_by_desk.keys()):
|
||||
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'))
|
||||
|
||||
# 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 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():
|
||||
if to_cancel_booking:
|
||||
cancelled_booking_id = to_cancel_booking.pk
|
||||
to_cancel_booking.cancel()
|
||||
|
||||
# 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
|
||||
# check with get_all_slots() and now, new event can have been
|
||||
# created and conflict with the events we want to create, and
|
||||
# so we get an IntegrityError exception. In this case we
|
||||
# restart the fillslot() from the begginning to redo the
|
||||
# availability check and return a proper error to the client.
|
||||
#
|
||||
# To prevent looping, we raise an APIError during the second run
|
||||
# of fillslot().
|
||||
if retry:
|
||||
raise APIError(N_('no more desk available'))
|
||||
return self.fillslot(request=request, agenda=agenda, timeslot_id=timeslot_id, retry=True)
|
||||
raise
|
||||
|
||||
response = {
|
||||
'err': 0,
|
||||
'booking_id': booking.id,
|
||||
'datetime': format_response_datetime(event.start_datetime),
|
||||
'agenda': {
|
||||
'label': booking.event.agenda.label,
|
||||
'slug': booking.event.agenda.slug,
|
||||
},
|
||||
'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': booking.id})
|
||||
),
|
||||
'cancel_url': request.build_absolute_uri(
|
||||
reverse('api-cancel-booking', kwargs={'booking_pk': booking.id})
|
||||
),
|
||||
'ics_url': request.build_absolute_uri(
|
||||
reverse('api-booking-ics', kwargs={'booking_pk': booking.id})
|
||||
),
|
||||
'anonymize_url': request.build_absolute_uri(
|
||||
reverse('api-anonymize-booking', kwargs={'booking_pk': booking.id})
|
||||
),
|
||||
},
|
||||
}
|
||||
if to_cancel_booking:
|
||||
response['cancelled_booking_id'] = cancelled_booking_id
|
||||
|
||||
return Response(response)
|
||||
|
||||
|
||||
class Fillslot(APIView):
|
||||
serializer_class = serializers.FillSlotSerializer
|
||||
|
||||
def dispatch(self, request, agenda_identifier, event_identifier):
|
||||
try:
|
||||
agenda = Agenda.objects.get(slug=agenda_identifier)
|
||||
except Agenda.DoesNotExist:
|
||||
try:
|
||||
# legacy access by agenda id
|
||||
agenda = Agenda.objects.get(id=int(agenda_identifier))
|
||||
except (ValueError, Agenda.DoesNotExist):
|
||||
raise Http404()
|
||||
|
||||
if agenda.accept_meetings():
|
||||
api_view = MeetingsAgendaFillslot()
|
||||
else:
|
||||
api_view = EventsAgendaFillslot()
|
||||
return api_view.dispatch(request=request, agenda=agenda, slot=event_identifier)
|
||||
|
||||
|
||||
fillslot = Fillslot.as_view()
|
||||
|
@ -1551,6 +1626,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,
|
||||
|
@ -1584,19 +1669,20 @@ class RecurringFillslots(APIView):
|
|||
)
|
||||
events_to_book = events_to_book.exclude(has_overlap=True)
|
||||
|
||||
existing_bookings = Booking.objects.filter(
|
||||
event__in=events_to_book, user_external_id=user_external_id
|
||||
).values('event')
|
||||
|
||||
# outdated bookings to remove (cancelled bookings to replace by an active booking)
|
||||
events_cancelled_to_delete = events_to_book.filter(
|
||||
booking__user_external_id=user_external_id,
|
||||
booking__cancellation_datetime__isnull=False,
|
||||
pk__in=existing_bookings.filter(
|
||||
Q(cancellation_datetime__isnull=False) | Q(start_time__isnull=False)
|
||||
),
|
||||
full=False,
|
||||
)
|
||||
# book only events without active booking for the user
|
||||
events_to_book = events_to_book.exclude(
|
||||
pk__in=Booking.objects.filter(
|
||||
event__in=events_to_book,
|
||||
user_external_id=user_external_id,
|
||||
cancellation_datetime__isnull=True,
|
||||
).values('event')
|
||||
pk__in=existing_bookings.filter(cancellation_datetime__isnull=True, start_time__isnull=True),
|
||||
)
|
||||
|
||||
# exclude full events
|
||||
|
@ -1625,11 +1711,6 @@ class RecurringFillslots(APIView):
|
|||
|
||||
events_by_id = {x.id: x for x in list(events_to_book) + list(events_to_unbook)}
|
||||
with transaction.atomic():
|
||||
# cancel existing bookings
|
||||
cancellation_datetime = now()
|
||||
Booking.objects.filter(primary_booking__in=bookings_to_cancel).update(
|
||||
cancellation_datetime=cancellation_datetime
|
||||
)
|
||||
if payload.get('include_booked_events_detail'):
|
||||
cancelled_events = [
|
||||
get_short_event_detail(
|
||||
|
@ -1639,7 +1720,15 @@ class RecurringFillslots(APIView):
|
|||
)
|
||||
for x in bookings_to_cancel
|
||||
]
|
||||
cancelled_count = bookings_to_cancel.update(cancellation_datetime=cancellation_datetime)
|
||||
if not payload.get('start_time'):
|
||||
# cancel existing bookings
|
||||
cancellation_datetime = now()
|
||||
Booking.objects.filter(primary_booking__in=bookings_to_cancel).update(
|
||||
cancellation_datetime=cancellation_datetime
|
||||
)
|
||||
cancelled_count = bookings_to_cancel.update(cancellation_datetime=cancellation_datetime)
|
||||
else:
|
||||
cancelled_count, dummy = bookings_to_cancel.delete()
|
||||
# and delete outdated cancelled bookings
|
||||
Booking.objects.filter(
|
||||
user_external_id=user_external_id, event__in=events_cancelled_to_delete
|
||||
|
@ -1676,7 +1765,7 @@ class RecurringFillslots(APIView):
|
|||
lookups = {
|
||||
'agenda__slug': agenda_slug,
|
||||
'primary_event__slug': event_slug,
|
||||
'start_datetime__week_day__in': days,
|
||||
'start_datetime__iso_week_day__in': days,
|
||||
}
|
||||
if agenda.minimal_booking_delay:
|
||||
lookups.update({'start_datetime__gte': agenda.min_booking_datetime})
|
||||
|
@ -1709,7 +1798,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
|
||||
|
@ -1720,6 +1809,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
|
||||
|
||||
|
@ -1754,7 +1844,6 @@ recurring_fillslots = RecurringFillslots.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):
|
||||
|
@ -1915,7 +2004,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):
|
||||
|
@ -1929,6 +2022,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()
|
||||
|
||||
|
@ -2008,7 +2105,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()
|
||||
|
@ -2213,7 +2310,7 @@ class SubscriptionFilter(filters.FilterSet):
|
|||
{missing: _('This filter is required when using "%s" filter.') % not_missing}
|
||||
)
|
||||
queryset = queryset.extra(
|
||||
where=["(date_start, date_end) OVERLAPS (%s, %s)"],
|
||||
where=['(date_start, date_end) OVERLAPS (%s, %s)'],
|
||||
params=[
|
||||
self.form.cleaned_data['date_start'],
|
||||
self.form.cleaned_data['date_end'],
|
||||
|
@ -2259,7 +2356,7 @@ class SubscriptionsAPI(ListAPIView):
|
|||
agenda=self.agenda,
|
||||
user_external_id=serializer.validated_data['user_external_id'],
|
||||
).extra(
|
||||
where=["(date_start, date_end) OVERLAPS (%s, %s)"],
|
||||
where=['(date_start, date_end) OVERLAPS (%s, %s)'],
|
||||
params=[date_start, date_end],
|
||||
)
|
||||
if overlapping_subscription_qs.exists():
|
||||
|
@ -2324,7 +2421,7 @@ class SubscriptionAPI(APIView):
|
|||
)
|
||||
.exclude(pk=self.subscription.pk)
|
||||
.extra(
|
||||
where=["(date_start, date_end) OVERLAPS (%s, %s)"],
|
||||
where=['(date_start, date_end) OVERLAPS (%s, %s)'],
|
||||
params=[date_start, date_end],
|
||||
)
|
||||
)
|
||||
|
@ -2458,6 +2555,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
|
||||
|
@ -3077,7 +3197,9 @@ class BookingsStatistics(APIView):
|
|||
if not agendas:
|
||||
raise APIErrorBadRequest(_('No agendas found.'))
|
||||
|
||||
bookings = bookings.filter(event__agenda__in=agendas)
|
||||
bookings = bookings.filter(
|
||||
Q(event__agenda__in=agendas) | Q(event__agenda__virtual_agendas__in=agendas)
|
||||
)
|
||||
subfilters = self.get_subfilters(agendas=agendas)
|
||||
|
||||
bookings = bookings.annotate(day=TruncDay('event__start_datetime'))
|
||||
|
@ -3149,7 +3271,9 @@ class BookingsStatistics(APIView):
|
|||
|
||||
def get_subfilters(self, agendas):
|
||||
extra_data_keys = (
|
||||
Booking.objects.filter(event__agenda__in=agendas)
|
||||
Booking.objects.filter(
|
||||
Q(event__agenda__in=agendas) | Q(event__agenda__virtual_agendas__in=agendas)
|
||||
)
|
||||
.annotate(extra_data_keys=Func('extra_data', function='jsonb_object_keys'))
|
||||
.distinct('extra_data_keys')
|
||||
.order_by('extra_data_keys')
|
||||
|
|
|
@ -228,18 +228,23 @@ 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')
|
||||
|
@ -264,6 +269,7 @@ class ANTSPersonsNumber(models.IntegerChoices):
|
|||
TWO = 2, _('2 persons')
|
||||
THREE = 3, _('3 persons')
|
||||
FOUR = 4, _('4 persons')
|
||||
FIVE = 5, _('5 persons')
|
||||
|
||||
|
||||
class PlaceAgenda(models.Model):
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -132,7 +132,9 @@ class AgendaEditForm(forms.ModelForm):
|
|||
if not EventsType.objects.exists():
|
||||
del self.fields['events_type']
|
||||
if kwargs['instance'].partial_bookings:
|
||||
del self.fields['default_view']
|
||||
self.fields['default_view'].choices = [
|
||||
(k, v) for k, v in self.fields['default_view'].choices if k not in ('open_events', 'week')
|
||||
]
|
||||
|
||||
|
||||
class AgendaBookingDelaysForm(forms.ModelForm):
|
||||
|
@ -144,6 +146,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)
|
||||
|
@ -222,6 +227,7 @@ class NewEventForm(forms.ModelForm):
|
|||
super().__init__(*args, **kwargs)
|
||||
if self.instance.agenda.partial_bookings:
|
||||
del self.fields['duration']
|
||||
del self.fields['recurrence_week_interval']
|
||||
else:
|
||||
del self.fields['end_time']
|
||||
|
||||
|
@ -235,6 +241,14 @@ class NewEventForm(forms.ModelForm):
|
|||
if end_time and self.cleaned_data['start_datetime'].time() > end_time:
|
||||
self.add_error('end_time', _('End time must be greater than start time.'))
|
||||
|
||||
if self.instance.agenda.partial_bookings and self.instance.agenda.event_overlaps(
|
||||
start_datetime=self.cleaned_data['start_datetime'],
|
||||
recurrence_days=self.cleaned_data['recurrence_days'],
|
||||
recurrence_end_date=self.cleaned_data['recurrence_end_date'],
|
||||
instance=self.instance,
|
||||
):
|
||||
raise ValidationError(_('There can only be one event per day.'))
|
||||
|
||||
def clean_start_datetime(self):
|
||||
start_datetime = self.cleaned_data['start_datetime']
|
||||
if start_datetime.year < 2000:
|
||||
|
@ -404,7 +418,7 @@ class EventDuplicateForm(forms.ModelForm):
|
|||
def save(self, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
self.instance = self.instance.duplicate(
|
||||
label=self.cleaned_data["label"], start_datetime=self.cleaned_data["start_datetime"]
|
||||
label=self.cleaned_data['label'], start_datetime=self.cleaned_data['start_datetime']
|
||||
)
|
||||
if self.instance.recurrence_days:
|
||||
self.instance.create_all_recurrences()
|
||||
|
@ -568,6 +582,77 @@ class BookingCheckPresenceForm(forms.Form):
|
|||
]
|
||||
|
||||
|
||||
class PartialBookingCheckForm(forms.ModelForm):
|
||||
user_was_present = forms.NullBooleanField(
|
||||
label=_('Status'),
|
||||
widget=forms.RadioSelect(
|
||||
choices=(
|
||||
(None, _('Not checked')),
|
||||
(True, _('Present')),
|
||||
(False, _('Absent')),
|
||||
)
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
presence_check_type = forms.ChoiceField(label=_('Type'), required=False)
|
||||
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']
|
||||
widgets = {
|
||||
'user_check_start_time': widgets.TimeWidgetWithButton(
|
||||
step=60, button_label=_('Fill with booking start time')
|
||||
),
|
||||
'user_check_end_time': widgets.TimeWidgetWithButton(
|
||||
step=60, button_label=_('Fill with booking end time')
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
agenda = kwargs.pop('agenda')
|
||||
super().__init__(*args, **kwargs)
|
||||
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
|
||||
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
|
||||
else:
|
||||
del self.fields['absence_check_type']
|
||||
|
||||
if not self.instance.start_time:
|
||||
self.fields['user_check_start_time'].widget = widgets.TimeWidget(step=60)
|
||||
self.fields['user_check_end_time'].widget = widgets.TimeWidget(step=60)
|
||||
self.fields['user_was_present'].widget.choices = ((None, _('Not checked')), (True, _('Present')))
|
||||
self.fields.pop('absence_check_type', None)
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data['user_check_end_time'] <= self.cleaned_data['user_check_start_time']:
|
||||
raise ValidationError(_('Arrival must be before departure.'))
|
||||
|
||||
if self.cleaned_data['user_was_present'] is not None:
|
||||
kind = 'presence' if self.cleaned_data['user_was_present'] 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
|
||||
)
|
||||
|
||||
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
|
||||
return super().save()
|
||||
|
||||
|
||||
class EventsTimesheetForm(forms.Form):
|
||||
date_start = forms.DateField(
|
||||
label=_('Start date'),
|
||||
|
@ -1207,12 +1292,7 @@ class ImportEventsForm(forms.Form):
|
|||
super().__init__(**kwargs)
|
||||
|
||||
def clean_events_csv_file(self):
|
||||
class ValidationErrorWithOrdinal(ValidationError):
|
||||
def __init__(self, message, event_no):
|
||||
super().__init__(message)
|
||||
self.message = format_html(message, event_no=mark_safe(ordinal(event_no + 1)))
|
||||
|
||||
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:
|
||||
|
@ -1233,48 +1313,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')):
|
||||
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',
|
||||
|
@ -1282,93 +1432,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):
|
||||
|
@ -1525,6 +1621,15 @@ class AgendaBookingCheckSettingsForm(forms.ModelForm):
|
|||
widgets = {'booking_extra_user_block_template': forms.Textarea(attrs={'rows': 3})}
|
||||
|
||||
|
||||
class AgendaInvoicingSettingsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Agenda
|
||||
fields = [
|
||||
'invoicing_unit',
|
||||
'invoicing_tolerance',
|
||||
]
|
||||
|
||||
|
||||
class AgendaNotificationsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = AgendaNotificationsSettings
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -598,10 +599,133 @@ div#appbar a.active {
|
|||
color: white;
|
||||
}
|
||||
|
||||
table.partial-bookings {
|
||||
border-spacing: 0;
|
||||
td.hour-cell {
|
||||
outline: solid 1px;
|
||||
// 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);
|
||||
--separator-color: white;
|
||||
--separator-size: 2px;
|
||||
|
||||
position: relative;
|
||||
background: white;
|
||||
padding: 0.5rem;
|
||||
|
||||
&--hours-list {
|
||||
background: white;
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--nb-hours), 1fr);
|
||||
font-size: 80%;
|
||||
@media (min-width: 761px) {
|
||||
padding-left: var(--registrant-name-width);
|
||||
}
|
||||
}
|
||||
&--hour {
|
||||
text-align: center;
|
||||
transform: translateX(-50%);
|
||||
|
||||
&:first-child {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
&--registrant-items {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
&--registrant {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
&:nth-child(odd) {
|
||||
background-color: var(--zebra-color);
|
||||
}
|
||||
&:nth-child(even) {
|
||||
--separator-color: var(--zebra-color);
|
||||
}
|
||||
.registrant {
|
||||
&--name {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: .66rem;
|
||||
font-size: 130%;
|
||||
color: #505050;
|
||||
font-weight: normal;
|
||||
@media (min-width: 761px) {
|
||||
flex: 0 0 var(--registrant-name-width);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
&--datas {
|
||||
box-sizing: border-box;
|
||||
flex: 1 0 100%;
|
||||
padding: .33rem 0;
|
||||
@media (min-width: 761px) {
|
||||
flex-basis: auto;
|
||||
}
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
var(--separator-color) var(--separator-size),
|
||||
transparent var(--separator-size),
|
||||
transparent 100%
|
||||
);
|
||||
background-size: calc(100% / var(--nb-hours)) 100%;
|
||||
@media (min-width: 761px) {
|
||||
border-left: var(--separator-size) solid var(--separator-color);
|
||||
}
|
||||
}
|
||||
&--bar-container {
|
||||
position: relative;
|
||||
margin: 0.33rem 0;
|
||||
}
|
||||
&--bar {
|
||||
--color: white;
|
||||
box-sizing: border-box;
|
||||
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, &.computed.present {
|
||||
--background: var(--green);
|
||||
}
|
||||
&.check.absent, &.computed.absent {
|
||||
--background: var(--red);
|
||||
}
|
||||
&.computed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agenda-table.partial-bookings .booking {
|
||||
height: 70%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 15%;
|
||||
background: #1066bc;
|
||||
&.present {
|
||||
background: hsl(120, 57%, 35%);
|
||||
}
|
||||
&.absent {
|
||||
background: hsl(355, 80%, 45%);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -3,16 +3,16 @@
|
|||
{% now "m" as today_month %}
|
||||
{% now "j" as today_day %}
|
||||
{% now "Ymj" as today %}
|
||||
{% if not agenda.partial_bookings %}
|
||||
{% if not no_opened and agenda.kind == 'events' %}
|
||||
<a href="{% url 'chrono-manager-agenda-open-events-view' pk=agenda.pk %}">{% trans 'Open events' %}</a>
|
||||
{% endif %}
|
||||
<span class="buttons-group">
|
||||
<a {% if active == 'day' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Day' %}</a>
|
||||
<a {% if active == 'week' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-week-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Week' %}</a>
|
||||
<a {% if active == 'month' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-month-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Month' %}</a>
|
||||
</span>
|
||||
{% if not no_opened and agenda.kind == 'events' and not agenda.partial_bookings %}
|
||||
<a href="{% url 'chrono-manager-agenda-open-events-view' pk=agenda.pk %}">{% trans 'Open events' %}</a>
|
||||
{% endif %}
|
||||
<span class="buttons-group">
|
||||
<a {% if active == 'day' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Day' %}</a>
|
||||
{% if not agenda.partial_bookings %}
|
||||
<a {% if active == 'week' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-week-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Week' %}</a>
|
||||
{% endif %}
|
||||
<a {% if active == 'month' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-month-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Month' %}</a>
|
||||
</span>
|
||||
{% if not no_today %}
|
||||
<a {% if active == 'day' and view.date|date:"Ymj" == today %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=today_year month=today_month day=today_day %}">{% trans 'Today' %}</a>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "gadjo/base.html" %}
|
||||
{% load static i18n %}
|
||||
{% load gadjo static i18n %}
|
||||
|
||||
{% block extrascripts %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/chrono.manager.js' %}"></script>
|
||||
<script src="{% static 'js/chrono.manager.js' %}?{% start_timestamp %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block page-title %}{% block page-title-extra-label %}{% trans 'Agendas' %}{% endblock %} | {% trans 'Agendas' as default_site_title %}{% firstof global_title default_site_title %}{% endblock %}
|
||||
|
|
|
@ -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 %}">
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "chrono/manager_home.html" %}
|
||||
{% load static i18n %}
|
||||
{% load gadjo static i18n %}
|
||||
|
||||
{% block extrascripts %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/chrono_events.manager.js' %}"></script>
|
||||
<script src="{% static 'js/chrono_events.manager.js' %}?{% start_timestamp %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block page-title-extra-label %}
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
{% 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>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<button aria-controls="panel-notifications" aria-selected="false" id="tab-notifications" role="tab" tabindex="-1">{% trans "Management notifications" %}</button>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -124,6 +127,18 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% 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>
|
||||
{% endif %}
|
||||
|
||||
<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 %}
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
{% if group.grouper %}<h3>{{ group.grouper }}</h3>{% elif not forloop.first %}<h3>{% trans "Misc" %}</h3>{% endif %}
|
||||
<ul class="objects-list single-links">
|
||||
{% for object in group.list %}
|
||||
<li><a href="{% url 'chrono-manager-agenda-view' pk=object.id %}"><span class="badge">{{ object.get_kind_display }}</span> {{ object.label }}{% if user.is_staff %} <span class="identifier">[{% trans "identifier:" %} {{ object.slug }}]{% endif %}</span></a></li>
|
||||
<li><a href="{% url 'chrono-manager-agenda-view' pk=object.id %}"><span class="badge">{{ object.get_real_kind_display }}</span> {{ object.label }}{% if user.is_staff %} <span class="identifier">[{% trans "identifier:" %} {{ object.slug }}]{% endif %}</span></a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
{% extends "chrono/manager_agenda_view.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="">{% trans "Check booking" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans "Check booking" %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
data-fill-user_check_start_time="{{ booking.start_time|time:"H:i" }}"
|
||||
data-fill-user_check_end_time="{{ booking.end_time|time:"H:i" }}"
|
||||
>
|
||||
{% csrf_token %}
|
||||
{{ form|with_template }}
|
||||
<div class="buttons">
|
||||
<button class="submit-button">{% trans "Save" %}</button>
|
||||
<a class="cancel" href="{{ agenda.get_absolute_url }}">{% trans 'Cancel' %}</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(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() {
|
||||
if (!this.checked)
|
||||
return;
|
||||
if (this.value == 'True') {
|
||||
presence_check_type_select.show();
|
||||
absence_check_type_select.hide();
|
||||
} else if (this.value == 'False') {
|
||||
absence_check_type_select.show();
|
||||
presence_check_type_select.hide();
|
||||
} else {
|
||||
presence_check_type_select.hide();
|
||||
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>
|
||||
{% endblock %}
|
|
@ -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 %}
|
||||
|
@ -8,36 +19,101 @@
|
|||
<p>{% trans "No opening hours this day." %}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<table class="agenda-table partial-bookings">
|
||||
<form class="check-bookings-filters">
|
||||
{{ filterset.form.as_p }}
|
||||
<script>
|
||||
$(function() {
|
||||
$('form.check-bookings-filters input').on('change',
|
||||
function() {
|
||||
$(this).parents('form').submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</form>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<td></td>
|
||||
{% for hour in hours %}
|
||||
<th>{{ hour|date:"TIME_FORMAT" }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% 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 %}
|
||||
|
||||
<tbody>
|
||||
{% for user, bookings in bookings_by_user.items %}
|
||||
<tr class="{% cycle 'odd' 'even' %}">
|
||||
<th>{{ bookings.0.get_user_block }}</th>
|
||||
{% for _ in hours %}
|
||||
<td class="hour-cell">
|
||||
{% if forloop.first %}
|
||||
{% for booking in bookings %}
|
||||
<div class="booking"
|
||||
style="left: {{ booking.css_left }}%; width: {{ booking.css_width }}%;"
|
||||
>{{ booking.start_time }} - {{ booking.end_time }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<div class="partial-booking" style="--nb-hours: {{ hours|length }}">
|
||||
<div class="partial-booking--hours-list" aria-hidden="true">
|
||||
{% for hour in hours %}
|
||||
<div class="partial-booking--hour">{{ hour|time:"H" }} h</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</div>
|
||||
|
||||
</table>
|
||||
<div class="partial-booking--registrant-items">
|
||||
{% for user in users %}
|
||||
<section class="partial-booking--registrant">
|
||||
{% spaceless %}
|
||||
<h3 class="registrant--name">
|
||||
{% if allow_check %}
|
||||
<a
|
||||
rel="popup"
|
||||
href="{{ user.check_url }}"
|
||||
>{{ user.name }}</a>
|
||||
{% else %}
|
||||
<span>{{ user.name }}</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
{% endspaceless %}
|
||||
<div class="registrant--datas">
|
||||
<div class="registrant--bar-container">
|
||||
{% for booking in user.bookings %}
|
||||
{% if booking.start_time %}
|
||||
<p
|
||||
class="registrant--bar clearfix booking"
|
||||
title="{% trans "Booked period" %}"
|
||||
style="left: {{ booking.css_left }}%; width: {{ booking.css_width }}%;"
|
||||
>
|
||||
<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>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if user.bookings %}
|
||||
<div class="registrant--bar-container">
|
||||
{% for booking in user.bookings %}
|
||||
{% if booking.user_was_present is not None %}
|
||||
<p
|
||||
class="registrant--bar clearfix 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 class="start-time" datetime="{{ booking.user_check_start_time|time:"H:i" }}">{{ booking.user_check_start_time|time:"H:i" }}</time>
|
||||
<time class="end-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>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="registrant--bar-container">
|
||||
{% for booking in user.bookings %}
|
||||
{% if booking.user_was_present is not None and booking.computed_start_time != None and booking.computed_end_time != None %}
|
||||
<p
|
||||
class="registrant--bar clearfix computed {{ booking.check_css_class }}"
|
||||
title="{% trans "Computed period" %}"
|
||||
style="left: {{ booking.computed_css_left }}%; width: {{ booking.computed_css_width }}%;"
|
||||
>
|
||||
<strong class="sr-only">{% trans "Computed period:" %}</strong>
|
||||
<time class="start-time" datetime="{{ booking.computed_start_time|time:"H:i" }}">{{ booking.computed_start_time|time:"H:i" }}</time>
|
||||
<time class="end-time" datetime="{{ booking.computed_end_time|time:"H:i" }}">{{ booking.computed_end_time|time:"H:i" }}</time>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
{% extends "chrono/manager_agenda_month_view.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<table class="agenda-table partial-bookings">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
<td></td>
|
||||
{% for day in days %}
|
||||
<th>
|
||||
<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=day.date|date:"Y" month=day.date|date:"m" day=day.date|date:"d" %}">{{ day|date:"d" }}</a>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for booking_info in user_booking_info %}
|
||||
<tr class="{% cycle 'odd' 'even' %}">
|
||||
<th>{{ booking_info.user_name }}</th>
|
||||
{% for booking in booking_info.bookings %}
|
||||
<td class="day-cell">
|
||||
{% if booking %}
|
||||
<span class="booking {{ booking.check_css_class }}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -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>
|
|
@ -69,22 +69,22 @@ urlpatterns = [
|
|||
path('resource/add/', views.resource_add, name='chrono-manager-resource-add'),
|
||||
path('resource/<int:pk>/', views.resource_view, name='chrono-manager-resource-view'),
|
||||
re_path(
|
||||
r'^resource/(?P<pk>\d+)/month/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$',
|
||||
r'^resource/(?P<pk>\d+)/month/(?P<year>[1-9][0-9]{3})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$',
|
||||
views.resource_monthly_view,
|
||||
name='chrono-manager-resource-month-view',
|
||||
),
|
||||
re_path(
|
||||
r'^resource/(?P<pk>\d+)/week/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$',
|
||||
r'^resource/(?P<pk>\d+)/week/(?P<year>[1-9][0-9]{3})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$',
|
||||
views.resource_weekly_view,
|
||||
name='chrono-manager-resource-week-view',
|
||||
),
|
||||
re_path(
|
||||
r'^resource/(?P<pk>\d+)/day/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$',
|
||||
r'^resource/(?P<pk>\d+)/day/(?P<year>[1-9][0-9]{3})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$',
|
||||
views.resource_day_view,
|
||||
name='chrono-manager-resource-day-view',
|
||||
),
|
||||
re_path(
|
||||
r'^resource/(?P<pk>\d+)/(?P<year>[0-9]{4})/',
|
||||
r'^resource/(?P<pk>\d+)/(?P<year>[1-9][0-9]{3})/',
|
||||
views.resource_redirect_view,
|
||||
name='chrono-manager-resource-redirect-view',
|
||||
),
|
||||
|
@ -113,7 +113,7 @@ urlpatterns = [
|
|||
name='chrono-manager-agenda-month-redirect-view',
|
||||
),
|
||||
re_path(
|
||||
r'^agendas/(?P<pk>\d+)/month/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$',
|
||||
r'^agendas/(?P<pk>\d+)/month/(?P<year>[1-9][0-9]{3})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$',
|
||||
views.agenda_monthly_view,
|
||||
name='chrono-manager-agenda-month-view',
|
||||
),
|
||||
|
@ -123,7 +123,7 @@ urlpatterns = [
|
|||
name='chrono-manager-agenda-week-redirect-view',
|
||||
),
|
||||
re_path(
|
||||
r'^agendas/(?P<pk>\d+)/week/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$',
|
||||
r'^agendas/(?P<pk>\d+)/week/(?P<year>[1-9][0-9]{3})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$',
|
||||
views.agenda_weekly_view,
|
||||
name='chrono-manager-agenda-week-view',
|
||||
),
|
||||
|
@ -133,12 +133,12 @@ urlpatterns = [
|
|||
name='chrono-manager-agenda-day-redirect-view',
|
||||
),
|
||||
re_path(
|
||||
r'^agendas/(?P<pk>\d+)/day/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$',
|
||||
r'^agendas/(?P<pk>\d+)/day/(?P<year>[1-9][0-9]{3})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$',
|
||||
views.agenda_day_view,
|
||||
name='chrono-manager-agenda-day-view',
|
||||
),
|
||||
re_path(
|
||||
r'^agendas/(?P<pk>\d+)/(?P<year>[0-9]{4})/',
|
||||
r'^agendas/(?P<pk>\d+)/(?P<year>[1-9][0-9]{3})/',
|
||||
views.agenda_redirect_view,
|
||||
name='chrono-manager-agenda-redirect-view',
|
||||
),
|
||||
|
@ -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'),
|
||||
|
@ -246,7 +251,7 @@ urlpatterns = [
|
|||
),
|
||||
path(
|
||||
'agendas/<int:pk>/events/<int:event_pk>/check',
|
||||
views.event_check,
|
||||
views.event_checks,
|
||||
name='chrono-manager-event-check',
|
||||
),
|
||||
path(
|
||||
|
@ -429,6 +434,16 @@ urlpatterns = [
|
|||
views.booking_extra_user_block,
|
||||
name='chrono-manager-booking-extra-user-block',
|
||||
),
|
||||
path(
|
||||
'agendas/<int:pk>/bookings/<int:booking_pk>/check',
|
||||
views.partial_booking_check_view,
|
||||
name='chrono-manager-partial-booking-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,
|
||||
|
|
|
@ -53,7 +53,7 @@ def export_site(
|
|||
qs = Agenda.objects.all()
|
||||
if agendas != 'all':
|
||||
qs = qs.filter(category=agendas)
|
||||
data['agendas'] = [x.export_json() for x in sorted(qs, key=lambda x: x == 'virtual')]
|
||||
data['agendas'] = [x.export_json() for x in sorted(qs, key=lambda x: x.kind == 'virtual')]
|
||||
if shared_custody:
|
||||
data['shared_custody_settings'] = SharedCustodySettings.get_singleton().export_json()
|
||||
return data
|
||||
|
|
|
@ -22,7 +22,7 @@ import itertools
|
|||
import json
|
||||
import math
|
||||
import uuid
|
||||
from operator import attrgetter
|
||||
from operator import attrgetter, itemgetter
|
||||
|
||||
import requests
|
||||
from dateutil.relativedelta import MO, relativedelta
|
||||
|
@ -99,6 +99,7 @@ from .forms import (
|
|||
AgendaDisplaySettingsForm,
|
||||
AgendaDuplicateForm,
|
||||
AgendaEditForm,
|
||||
AgendaInvoicingSettingsForm,
|
||||
AgendaNotificationsForm,
|
||||
AgendaReminderForm,
|
||||
AgendaReminderTestForm,
|
||||
|
@ -126,6 +127,7 @@ from .forms import (
|
|||
NewEventForm,
|
||||
NewMeetingTypeForm,
|
||||
NewTimePeriodExceptionForm,
|
||||
PartialBookingCheckForm,
|
||||
SharedCustodyHolidayRuleForm,
|
||||
SharedCustodyPeriodForm,
|
||||
SharedCustodyRuleForm,
|
||||
|
@ -266,7 +268,7 @@ class DateMixin:
|
|||
year = int(year)
|
||||
dates[year] = {}
|
||||
for week, week_label in self.get_weeks():
|
||||
date = datetime.datetime.strptime('%s-W%s-1' % (year, week), "%Y-W%W-%w")
|
||||
date = datetime.datetime.strptime('%s-W%s-1' % (year, week), '%Y-W%W-%w')
|
||||
dates[year][date] = week_label
|
||||
return dates
|
||||
|
||||
|
@ -1029,7 +1031,7 @@ class AgendasImportView(FormView):
|
|||
else:
|
||||
message2 = import_messages[obj_name]['update'](count) % {'count': count}
|
||||
|
||||
obj_results['messages'] = "%s %s" % (message1, message2)
|
||||
obj_results['messages'] = '%s %s' % (message1, message2)
|
||||
|
||||
a_count, uc_count = (
|
||||
len(results['agendas']['all']),
|
||||
|
@ -1140,7 +1142,7 @@ agenda_roles = AgendaRolesView.as_view()
|
|||
|
||||
class AgendaDisplaySettingsView(AgendaEditView):
|
||||
form_class = AgendaDisplaySettingsForm
|
||||
title = _("Configure display options")
|
||||
title = _('Configure display options')
|
||||
tab_anchor = 'display-options'
|
||||
|
||||
def set_agenda(self, **kwargs):
|
||||
|
@ -1155,7 +1157,7 @@ agenda_display_settings = AgendaDisplaySettingsView.as_view()
|
|||
|
||||
class AgendaBookingCheckSettingsView(AgendaEditView):
|
||||
form_class = AgendaBookingCheckSettingsForm
|
||||
title = _("Configure booking check options")
|
||||
title = _('Configure booking check options')
|
||||
tab_anchor = 'booking-check-options'
|
||||
|
||||
def set_agenda(self, **kwargs):
|
||||
|
@ -1165,6 +1167,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
|
||||
|
@ -1326,7 +1340,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
|
||||
|
@ -1362,7 +1376,112 @@ class AgendaDateView(DateMixin, ViewableAgendaMixin):
|
|||
]
|
||||
|
||||
|
||||
class AgendaDayView(AgendaDateView, DayArchiveView):
|
||||
class EventChecksMixin:
|
||||
def get_filters(self, booked_queryset, subscription_queryset):
|
||||
agenda_filters = self.agenda.get_booking_check_filters()
|
||||
filters = collections.defaultdict(set)
|
||||
extra_data_from_booked = booked_queryset.filter(extra_data__has_any_keys=agenda_filters).values_list(
|
||||
'extra_data', flat=True
|
||||
)
|
||||
extra_data_from_subscriptions = subscription_queryset.filter(
|
||||
extra_data__has_any_keys=agenda_filters
|
||||
).values_list('extra_data', flat=True)
|
||||
for extra_data in list(extra_data_from_booked) + list(extra_data_from_subscriptions):
|
||||
for k, v in extra_data.items():
|
||||
if not v:
|
||||
continue
|
||||
if k in agenda_filters and not isinstance(v, (list, dict)):
|
||||
filters[k].add(v)
|
||||
filters = sorted(filters.items())
|
||||
filters = {k: sorted(list(v)) for k, v in filters}
|
||||
return filters
|
||||
|
||||
def add_filters_context(self, context, event):
|
||||
# booking base queryset
|
||||
booking_qs_kwargs = {}
|
||||
if not self.agenda.subscriptions.exists():
|
||||
booking_qs_kwargs = {'cancellation_datetime__isnull': True}
|
||||
booking_qs = event.booking_set
|
||||
booked_qs = booking_qs.filter(
|
||||
in_waiting_list=False, primary_booking__isnull=True, **booking_qs_kwargs
|
||||
)
|
||||
booked_qs = booked_qs.annotate(
|
||||
places_count=Value(1) + Count('secondary_booking_set', filter=Q(**booking_qs_kwargs))
|
||||
)
|
||||
|
||||
# waiting list queryset
|
||||
waiting_qs = booking_qs.filter(
|
||||
in_waiting_list=True, primary_booking__isnull=True, **booking_qs_kwargs
|
||||
).order_by('user_last_name', 'user_first_name')
|
||||
waiting_qs = waiting_qs.annotate(
|
||||
places_count=Value(1)
|
||||
+ Count('secondary_booking_set', filter=Q(cancellation_datetime__isnull=True))
|
||||
)
|
||||
|
||||
# subscription base queryset
|
||||
subscription_qs = (
|
||||
self.agenda.subscriptions.filter(
|
||||
date_start__lte=event.start_datetime, date_end__gt=event.start_datetime
|
||||
)
|
||||
# exclude user_external_id from booked_qs and waiting_qs
|
||||
.exclude(user_external_id__in=booked_qs.values('user_external_id')).exclude(
|
||||
user_external_id__in=waiting_qs.values('user_external_id')
|
||||
)
|
||||
)
|
||||
|
||||
# build filters from booked_qs and subscription_qs
|
||||
filters = self.get_filters(booked_queryset=booked_qs, subscription_queryset=subscription_qs)
|
||||
# and filter booked and subscriptions
|
||||
booked_filterset = BookingCheckFilterSet(
|
||||
data=self.request.GET or None, queryset=booked_qs, agenda=self.agenda, filters=filters
|
||||
)
|
||||
subscription_filterset = SubscriptionCheckFilterSet(
|
||||
data=self.request.GET or None, queryset=subscription_qs, agenda=self.agenda, filters=filters
|
||||
)
|
||||
|
||||
# build results from mixed booked and subscriptions
|
||||
results = []
|
||||
booked_without_status = False
|
||||
for booking in booked_filterset.qs:
|
||||
if booking.cancellation_datetime is None and booking.user_was_present is None:
|
||||
booked_without_status = True
|
||||
booking.absence_form = BookingCheckAbsenceForm(
|
||||
agenda=self.agenda,
|
||||
initial={'check_type': booking.user_check_type_slug},
|
||||
)
|
||||
booking.presence_form = BookingCheckPresenceForm(
|
||||
agenda=self.agenda,
|
||||
initial={'check_type': booking.user_check_type_slug},
|
||||
)
|
||||
booking.kind = 'booking'
|
||||
results.append(booking)
|
||||
for subscription in subscription_filterset.qs:
|
||||
subscription.absence_form = BookingCheckAbsenceForm(
|
||||
agenda=self.agenda,
|
||||
)
|
||||
subscription.presence_form = BookingCheckPresenceForm(
|
||||
agenda=self.agenda,
|
||||
)
|
||||
subscription.kind = 'subscription'
|
||||
results.append(subscription)
|
||||
# sort results
|
||||
if (
|
||||
booked_filterset.form.is_valid()
|
||||
and booked_filterset.form.cleaned_data.get('sort') == 'firstname,lastname'
|
||||
):
|
||||
sort_fields = ['user_first_name', 'user_last_name']
|
||||
else:
|
||||
sort_fields = ['user_last_name', 'user_first_name']
|
||||
results = sorted(results, key=attrgetter(*sort_fields, 'user_external_id'))
|
||||
|
||||
# set context
|
||||
context['booked_without_status'] = booked_without_status
|
||||
context['filterset'] = booked_filterset
|
||||
context['results'] = results
|
||||
context['waiting'] = waiting_qs
|
||||
|
||||
|
||||
class AgendaDayView(EventChecksMixin, AgendaDateView, DayArchiveView):
|
||||
kind = 'day'
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -1500,29 +1619,75 @@ class AgendaDayView(AgendaDateView, DayArchiveView):
|
|||
return context
|
||||
|
||||
def fill_partial_bookings_context(self, context):
|
||||
events = self.agenda.event_set.filter(start_datetime__date=self.date.date())
|
||||
if not events.exists():
|
||||
try:
|
||||
event = self.agenda.event_set.get(
|
||||
start_datetime__date=self.date.date(), recurrence_days__isnull=True
|
||||
)
|
||||
except Event.DoesNotExist:
|
||||
return
|
||||
|
||||
event_times = events.aggregate(Min('start_datetime'), Max('end_time'))
|
||||
min_time = localtime(event_times['start_datetime__min']).time()
|
||||
max_time = event_times['end_time__max']
|
||||
context['event'] = event
|
||||
context['allow_check'] = (
|
||||
(
|
||||
self.agenda.enable_check_for_future_events
|
||||
or localtime(event.start_datetime).date() <= localtime().date()
|
||||
)
|
||||
and (not event.checked or not self.agenda.disable_check_update)
|
||||
and not event.check_locked
|
||||
)
|
||||
self.add_filters_context(context, event)
|
||||
|
||||
start_time = datetime.time(min_time.hour - 1, 0)
|
||||
end_time = datetime.time(max_time.hour + 2, 0)
|
||||
context['hours'] = [datetime.time(hour=i) for i in range(start_time.hour, end_time.hour)]
|
||||
min_time = localtime(event.start_datetime).time()
|
||||
max_time = event.end_time
|
||||
|
||||
start_time = datetime.time(max(min_time.hour - 1, 0), 0)
|
||||
end_time = datetime.time(min(max_time.hour + 1, 23), 0)
|
||||
context['hours'] = [datetime.time(hour=i) for i in range(start_time.hour, end_time.hour + 1)]
|
||||
|
||||
opening_range_minutes = (
|
||||
(end_time.hour + 1) * 60 + end_time.minute - (start_time.hour * 60 + start_time.minute)
|
||||
)
|
||||
|
||||
def get_time_ratio(t1, t2):
|
||||
return 100 * ((t1.hour - t2.hour) * 60 + t1.minute - t2.minute) // 60
|
||||
return str(
|
||||
round(100 * ((t1.hour - t2.hour) * 60 + t1.minute - t2.minute) / opening_range_minutes, 2)
|
||||
)
|
||||
|
||||
bookings = Booking.objects.filter(event__in=events)
|
||||
bookings_by_user = collections.defaultdict(list)
|
||||
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)
|
||||
bookings_by_user[booking.user_external_id].append(booking)
|
||||
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)
|
||||
|
||||
context['bookings_by_user'] = dict(bookings_by_user)
|
||||
if booking.user_was_present is not None:
|
||||
booking.check_css_class = 'present' if booking.user_was_present else 'absent'
|
||||
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
|
||||
)
|
||||
booking.computed_start_time = booking.get_computed_start_time()
|
||||
booking.computed_end_time = booking.get_computed_end_time()
|
||||
if booking.computed_start_time is not None and booking.computed_end_time is not None:
|
||||
booking.computed_css_left = get_time_ratio(booking.computed_start_time, start_time)
|
||||
booking.computed_css_width = get_time_ratio(
|
||||
booking.computed_end_time, booking.computed_start_time
|
||||
)
|
||||
|
||||
users_info = {}
|
||||
for result in context['results']:
|
||||
user_info = users_info.setdefault(
|
||||
result.user_external_id,
|
||||
{
|
||||
'name': result.get_user_block(),
|
||||
'check_url': result.get_partial_bookings_check_url(self.agenda, event),
|
||||
'bookings': [],
|
||||
},
|
||||
)
|
||||
if result.kind != 'booking':
|
||||
continue
|
||||
user_info['bookings'].append(result)
|
||||
|
||||
context['users'] = users_info.values()
|
||||
|
||||
|
||||
agenda_day_view = AgendaDayView.as_view()
|
||||
|
@ -1555,6 +1720,7 @@ class AgendaWeekMonthMixin:
|
|||
def get_dated_items(self):
|
||||
date_list, object_list, extra_context = super().get_dated_items()
|
||||
if self.agenda.kind == 'events':
|
||||
self.events = object_list
|
||||
min_start = self.first_day
|
||||
max_start = getattr(self, 'get_next_%s' % self.kind)(self.first_day)
|
||||
exceptions = TimePeriodException.objects.filter(
|
||||
|
@ -1572,6 +1738,8 @@ class AgendaWeekMonthMixin:
|
|||
).all()
|
||||
else:
|
||||
context['single_desk'] = bool(len(self.agenda.prefetched_desks) == 1)
|
||||
if self.agenda.partial_bookings:
|
||||
self.fill_partial_bookings_context(context)
|
||||
return context
|
||||
|
||||
def get_timeperiods(self):
|
||||
|
@ -1746,6 +1914,49 @@ class AgendaWeekMonthMixin:
|
|||
|
||||
return timetable
|
||||
|
||||
def fill_partial_bookings_context(self, context):
|
||||
first_day_next_month = self.get_next_month(self.first_day)
|
||||
context['days'] = days = [
|
||||
self.first_day + datetime.timedelta(days=i)
|
||||
for i in range((first_day_next_month - self.first_day).days)
|
||||
]
|
||||
|
||||
booking_info_by_user = {}
|
||||
bookings = Booking.objects.filter(event__in=self.events)
|
||||
for booking in bookings:
|
||||
booking_info = booking_info_by_user.setdefault(
|
||||
booking.user_external_id,
|
||||
{
|
||||
'user_name': booking.user_name,
|
||||
'user_first_name': booking.user_first_name,
|
||||
'user_last_name': booking.user_last_name,
|
||||
'bookings': [None] * len(days),
|
||||
},
|
||||
)
|
||||
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'
|
||||
|
||||
user_bookings[localtime(booking.event.start_datetime).day - 1] = booking
|
||||
|
||||
subscriptions = self.agenda.subscriptions.filter(
|
||||
date_start__lt=first_day_next_month,
|
||||
date_end__gte=self.first_day,
|
||||
).exclude(user_external_id__in=booking_info_by_user.keys())
|
||||
|
||||
for subscription in subscriptions:
|
||||
booking_info_by_user[subscription.user_external_id] = {
|
||||
'user_name': subscription.user_name,
|
||||
'user_first_name': subscription.user_first_name,
|
||||
'user_last_name': subscription.user_last_name,
|
||||
'bookings': [None] * len(days),
|
||||
}
|
||||
|
||||
context['user_booking_info'] = sorted(
|
||||
booking_info_by_user.values(), key=itemgetter('user_last_name', 'user_first_name')
|
||||
)
|
||||
|
||||
|
||||
class AgendaWeekView(AgendaWeekMonthMixin, AgendaDateView, DayArchiveView, WeekMixin):
|
||||
kind = 'week'
|
||||
|
@ -1802,6 +2013,8 @@ class AgendaMonthView(AgendaWeekMonthMixin, AgendaDateView, DayArchiveView):
|
|||
def get_template_names(self):
|
||||
if self.agenda.kind == 'virtual':
|
||||
return ['chrono/manager_meetings_agenda_month_view.html']
|
||||
if self.agenda.partial_bookings:
|
||||
return ['chrono/manager_partial_bookings_month_view.html']
|
||||
return ['chrono/manager_%s_agenda_month_view.html' % self.agenda.kind]
|
||||
|
||||
def get_previous_month_url(self):
|
||||
|
@ -2065,10 +2278,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})
|
||||
|
||||
|
@ -2079,6 +2298,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()
|
||||
|
||||
|
@ -2509,6 +2733,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):
|
||||
|
@ -2537,8 +2763,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})
|
||||
|
||||
|
||||
|
@ -2556,6 +2793,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})
|
||||
|
@ -2598,7 +2836,7 @@ class EventDeleteView(ManagedAgendaMixin, DeleteView):
|
|||
event_delete = EventDeleteView.as_view()
|
||||
|
||||
|
||||
class EventCheckView(ViewableAgendaMixin, DetailView):
|
||||
class EventChecksView(ViewableAgendaMixin, EventChecksMixin, DetailView):
|
||||
template_name = 'chrono/manager_event_check.html'
|
||||
model = Event
|
||||
pk_url_kwarg = 'event_pk'
|
||||
|
@ -2608,6 +2846,7 @@ class EventCheckView(ViewableAgendaMixin, DetailView):
|
|||
Agenda,
|
||||
pk=kwargs.get('pk'),
|
||||
kind='events',
|
||||
partial_bookings=False,
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -2620,117 +2859,15 @@ class EventCheckView(ViewableAgendaMixin, DetailView):
|
|||
cancelled=False,
|
||||
)
|
||||
|
||||
def get_filters(self, booked_queryset, subscription_queryset):
|
||||
agenda_filters = self.agenda.get_booking_check_filters()
|
||||
filters = collections.defaultdict(set)
|
||||
extra_data_from_booked = booked_queryset.filter(extra_data__has_any_keys=agenda_filters).values_list(
|
||||
'extra_data', flat=True
|
||||
)
|
||||
extra_data_from_subscriptions = subscription_queryset.filter(
|
||||
extra_data__has_any_keys=agenda_filters
|
||||
).values_list('extra_data', flat=True)
|
||||
for extra_data in list(extra_data_from_booked) + list(extra_data_from_subscriptions):
|
||||
for k, v in extra_data.items():
|
||||
if k in agenda_filters and not isinstance(v, (list, dict)):
|
||||
filters[k].add(v)
|
||||
filters = sorted(filters.items())
|
||||
filters = {k: sorted(list(v)) for k, v in filters}
|
||||
return filters
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
event = self.object
|
||||
|
||||
# booking base queryset
|
||||
booking_qs_kwargs = {}
|
||||
if not self.agenda.subscriptions.exists():
|
||||
booking_qs_kwargs = {'cancellation_datetime__isnull': True}
|
||||
booking_qs = event.booking_set
|
||||
booked_qs = booking_qs.filter(
|
||||
in_waiting_list=False, primary_booking__isnull=True, **booking_qs_kwargs
|
||||
)
|
||||
booked_qs = booked_qs.annotate(
|
||||
places_count=Value(1) + Count('secondary_booking_set', filter=Q(**booking_qs_kwargs))
|
||||
)
|
||||
|
||||
# waiting list queryset
|
||||
waiting_qs = booking_qs.filter(
|
||||
in_waiting_list=True, primary_booking__isnull=True, **booking_qs_kwargs
|
||||
).order_by('user_last_name', 'user_first_name')
|
||||
waiting_qs = waiting_qs.annotate(
|
||||
places_count=Value(1)
|
||||
+ Count('secondary_booking_set', filter=Q(cancellation_datetime__isnull=True))
|
||||
)
|
||||
|
||||
# subscription base queryset
|
||||
subscription_qs = (
|
||||
self.agenda.subscriptions.filter(
|
||||
date_start__lte=event.start_datetime, date_end__gt=event.start_datetime
|
||||
)
|
||||
# exclude user_external_id from booked_qs and waiting_qs
|
||||
.exclude(user_external_id__in=booked_qs.values('user_external_id')).exclude(
|
||||
user_external_id__in=waiting_qs.values('user_external_id')
|
||||
)
|
||||
)
|
||||
|
||||
# build filters from booked_qs and subscription_qs
|
||||
filters = self.get_filters(booked_queryset=booked_qs, subscription_queryset=subscription_qs)
|
||||
# and filter booked and subscriptions
|
||||
booked_filterset = BookingCheckFilterSet(
|
||||
data=self.request.GET or None, queryset=booked_qs, agenda=self.agenda, filters=filters
|
||||
)
|
||||
subscription_filterset = SubscriptionCheckFilterSet(
|
||||
data=self.request.GET or None, queryset=subscription_qs, agenda=self.agenda, filters=filters
|
||||
)
|
||||
|
||||
# build results from mixed booked and subscriptions
|
||||
results = []
|
||||
booked_without_status = False
|
||||
for booking in booked_filterset.qs:
|
||||
if booking.cancellation_datetime is None and booking.user_was_present is None:
|
||||
booked_without_status = True
|
||||
booking.absence_form = BookingCheckAbsenceForm(
|
||||
agenda=self.agenda,
|
||||
initial={'check_type': booking.user_check_type_slug},
|
||||
)
|
||||
booking.presence_form = BookingCheckPresenceForm(
|
||||
agenda=self.agenda,
|
||||
initial={'check_type': booking.user_check_type_slug},
|
||||
)
|
||||
booking.kind = 'booking'
|
||||
results.append(booking)
|
||||
for subscription in subscription_filterset.qs:
|
||||
subscription.absence_form = BookingCheckAbsenceForm(
|
||||
agenda=self.agenda,
|
||||
)
|
||||
subscription.presence_form = BookingCheckPresenceForm(
|
||||
agenda=self.agenda,
|
||||
)
|
||||
subscription.kind = 'subscription'
|
||||
results.append(subscription)
|
||||
# sort results
|
||||
if (
|
||||
booked_filterset.form.is_valid()
|
||||
and booked_filterset.form.cleaned_data.get('sort') == 'firstname,lastname'
|
||||
):
|
||||
sort_fields = ['user_first_name', 'user_last_name']
|
||||
else:
|
||||
sort_fields = ['user_last_name', 'user_first_name']
|
||||
results = sorted(results, key=attrgetter(*sort_fields, 'user_external_id'))
|
||||
|
||||
# set context
|
||||
context['booked_without_status'] = booked_without_status
|
||||
self.add_filters_context(context, self.object)
|
||||
context['absence_form'] = BookingCheckAbsenceForm(agenda=self.agenda)
|
||||
context['presence_form'] = BookingCheckPresenceForm(agenda=self.agenda)
|
||||
context['filterset'] = booked_filterset
|
||||
context['results'] = results
|
||||
context['waiting'] = waiting_qs
|
||||
|
||||
return context
|
||||
|
||||
|
||||
event_check = EventCheckView.as_view()
|
||||
event_checks = EventChecksView.as_view()
|
||||
|
||||
|
||||
class EventCheckMixin:
|
||||
|
@ -2768,6 +2905,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',
|
||||
|
@ -3006,7 +3156,7 @@ meeting_type_delete = MeetingTypeDeleteView.as_view()
|
|||
|
||||
|
||||
def process_time_period_add_form(form, desk=None, agenda=None):
|
||||
assert desk or agenda, "a time period requires a desk or a agenda"
|
||||
assert desk or agenda, 'a time period requires a desk or a agenda'
|
||||
for weekday in form.cleaned_data.get('weekdays'):
|
||||
period = TimePeriod(
|
||||
weekday=weekday,
|
||||
|
@ -4411,6 +4561,44 @@ class SharedCustodySettingsView(UpdateView):
|
|||
shared_custody_settings = SharedCustodySettingsView.as_view()
|
||||
|
||||
|
||||
class PartialBookingCheckMixin(ViewableAgendaMixin):
|
||||
template_name = 'chrono/manager_partial_booking_form.html'
|
||||
form_class = PartialBookingCheckForm
|
||||
|
||||
def get_object(self):
|
||||
return self.get_booking(**self.kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
date = self.object.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 Booking()
|
||||
|
||||
|
||||
partial_booking_subscription_check_view = PartialBookingSubscriptionCheckView.as_view()
|
||||
|
||||
|
||||
def menu_json(request):
|
||||
if not request.user.is_staff:
|
||||
homepage_view = HomepageView(request=request)
|
||||
|
|
|
@ -57,12 +57,26 @@ class TimeWidget(TimeInput):
|
|||
input_type = 'time'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
step = kwargs.pop('step', 300) # 5 minutes by default
|
||||
kwargs['format'] = '%H:%M'
|
||||
super().__init__(**kwargs)
|
||||
self.attrs['step'] = '300' # 5 minutes
|
||||
self.attrs['step'] = step
|
||||
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
|
||||
|
|
|
@ -57,4 +57,4 @@ class EnsureJsonbType(Operation):
|
|||
pass
|
||||
|
||||
def describe(self):
|
||||
return "Migrate to postgres jsonb type"
|
||||
return 'Migrate to postgres jsonb type'
|
||||
|
|
|
@ -11,5 +11,5 @@ import os
|
|||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chrono.settings")
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chrono.settings')
|
||||
application = get_wsgi_application()
|
||||
|
|
|
@ -35,9 +35,9 @@ Depends: libcairo-gobject2,
|
|||
python3-hobo (>= 1.34),
|
||||
python3-psycopg2,
|
||||
python3-vobject,
|
||||
python3-weasyprint,
|
||||
uwsgi,
|
||||
uwsgi-plugin-python3,
|
||||
weasyprint,
|
||||
${misc:Depends},
|
||||
Recommends: nginx,
|
||||
python3-workalendar,
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
weasyprint python3-weasyprint
|
|
@ -61,7 +61,6 @@ buffer-size = 32768
|
|||
|
||||
py-tracebacker = /run/chrono/py-tracebacker.sock.
|
||||
stats = /run/chrono/stats.sock
|
||||
memory-report = true
|
||||
|
||||
ignore-sigpipe = true
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chrono.settings")
|
||||
if __name__ == '__main__':
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chrono.settings')
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
|
|
2
setup.py
2
setup.py
|
@ -167,7 +167,7 @@ setup(
|
|||
'python-dateutil',
|
||||
'requests',
|
||||
'workalendar',
|
||||
'weasyprint<0.43',
|
||||
'weasyprint',
|
||||
],
|
||||
zip_safe=False,
|
||||
cmdclass={
|
||||
|
|
|
@ -152,6 +152,17 @@ def test_add_agenda(hub, app, admin_user, city, place, agenda, freezer):
|
|||
'Rdv CNI (45 minutes)',
|
||||
'Rdv CNI (60 minutes)',
|
||||
]
|
||||
# check possible checkbox values
|
||||
assert {item.text() for item in resp.pyquery('table td li').items()} == {
|
||||
'1 person',
|
||||
'2 persons',
|
||||
'3 persons',
|
||||
'4 persons',
|
||||
'5 persons',
|
||||
'CNI',
|
||||
'CNI and passport',
|
||||
'Passport',
|
||||
}
|
||||
|
||||
resp.form.set('mt_mt-15_1', [str(ANTSMeetingType.CNI)])
|
||||
resp.form.set('mt_mt-15_2', [str(ANTSPersonsNumber.ONE)])
|
||||
|
|
|
@ -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': [
|
||||
{
|
||||
|
|
|
@ -281,7 +281,7 @@ def test_datetimes_api_exclude_slots(app):
|
|||
event = Event.objects.create(
|
||||
slug='recurrent',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=15),
|
||||
places=2,
|
||||
agenda=agenda,
|
||||
|
@ -365,7 +365,7 @@ def test_datetimes_api_user_external_id(app):
|
|||
event = Event.objects.create(
|
||||
slug='recurrent',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=15),
|
||||
places=2,
|
||||
agenda=agenda,
|
||||
|
@ -660,7 +660,7 @@ def test_datetimes_api_meta(app, freezer):
|
|||
slug='abc',
|
||||
label='Test',
|
||||
start_datetime=localtime(),
|
||||
recurrence_days=[localtime().weekday()],
|
||||
recurrence_days=[localtime().isoweekday()],
|
||||
recurrence_end_date=localtime() + datetime.timedelta(days=15),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
|
@ -679,7 +679,7 @@ def test_recurring_events_api(app, user, freezer):
|
|||
slug='abc',
|
||||
label="Rock'n roll",
|
||||
start_datetime=localtime(),
|
||||
recurrence_days=[localtime().weekday()],
|
||||
recurrence_days=[localtime().isoweekday()],
|
||||
recurrence_end_date=localtime() + datetime.timedelta(days=30),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
|
@ -749,7 +749,7 @@ def test_recurring_events_api_various_times(app, user, mock_now):
|
|||
event = Event.objects.create(
|
||||
slug='abc',
|
||||
start_datetime=localtime(),
|
||||
recurrence_days=[localtime().weekday()],
|
||||
recurrence_days=[localtime().isoweekday()],
|
||||
recurrence_end_date=localtime() + datetime.timedelta(days=30),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
|
@ -797,7 +797,7 @@ def test_recurring_events_api_exceptions(app, user, freezer):
|
|||
event = Event.objects.create(
|
||||
slug='abc',
|
||||
start_datetime=localtime(),
|
||||
recurrence_days=[localtime().weekday()],
|
||||
recurrence_days=[localtime().isoweekday()],
|
||||
recurrence_end_date=localtime() + datetime.timedelta(days=30),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
|
@ -1253,7 +1253,7 @@ def test_past_datetimes_recurring_event(app, user):
|
|||
event = Event.objects.create(
|
||||
label='Recurring',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=60),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
|
|
|
@ -41,7 +41,7 @@ def test_datetimes_multiple_agendas(app):
|
|||
event = Event.objects.create( # base recurring event not visible in datetimes api
|
||||
slug='recurring',
|
||||
start_datetime=now() + datetime.timedelta(hours=1),
|
||||
recurrence_days=[localtime().weekday()],
|
||||
recurrence_days=[localtime().isoweekday()],
|
||||
recurrence_end_date=now() + datetime.timedelta(days=15),
|
||||
places=5,
|
||||
agenda=first_agenda,
|
||||
|
@ -189,7 +189,7 @@ def test_datetimes_multiple_agendas(app):
|
|||
event = Event.objects.create( # base recurrring event not visible in datetimes api
|
||||
slug='recurring-in-past',
|
||||
start_datetime=now() - datetime.timedelta(days=15, hours=1),
|
||||
recurrence_days=[localtime().weekday()],
|
||||
recurrence_days=[localtime().isoweekday()],
|
||||
recurrence_end_date=now(),
|
||||
places=5,
|
||||
agenda=first_agenda,
|
||||
|
@ -427,8 +427,8 @@ def test_datetimes_multiple_agendas_queries(app):
|
|||
date_end=now() - datetime.timedelta(days=5 + 2 * i + 1),
|
||||
)
|
||||
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(7)), weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(7)), weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(1, 8)), weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(1, 8)), weeks='odd')
|
||||
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
resp = app.get(
|
||||
|
@ -600,7 +600,7 @@ def test_datetimes_multiple_agendas_recurring_subscribed_dates(app):
|
|||
event = Event.objects.create(
|
||||
slug='recurring',
|
||||
start_datetime=now() + datetime.timedelta(days=-15, hours=1),
|
||||
recurrence_days=[localtime().weekday()],
|
||||
recurrence_days=[localtime().isoweekday()],
|
||||
recurrence_end_date=now() + datetime.timedelta(days=15),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
|
@ -739,9 +739,9 @@ def test_datetimes_multiple_agendas_shared_custody(app):
|
|||
)
|
||||
|
||||
father_rule = SharedCustodyRule.objects.create(
|
||||
agenda=agenda, guardian=father, days=list(range(7)), weeks='even'
|
||||
agenda=agenda, guardian=father, days=list(range(1, 8)), weeks='even'
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(7)), weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(1, 8)), weeks='odd')
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/datetimes/',
|
||||
|
@ -878,8 +878,8 @@ def test_datetimes_multiple_agendas_shared_custody_other_rules(app):
|
|||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
|
||||
father_rule = SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=[0, 1, 2])
|
||||
mother_rule = SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=[3, 4, 5, 6])
|
||||
father_rule = SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=[1, 2, 3])
|
||||
mother_rule = SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=[4, 5, 6, 7])
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/datetimes/',
|
||||
|
@ -895,9 +895,9 @@ def test_datetimes_multiple_agendas_shared_custody_other_rules(app):
|
|||
assert len(resp.json['data']) == 1
|
||||
assert resp.json['data'][0]['id'] == 'first-agenda@event-thursday'
|
||||
|
||||
father_rule.days = [0, 1]
|
||||
father_rule.days = [1, 2]
|
||||
father_rule.save()
|
||||
other_father_rule = SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=[2])
|
||||
other_father_rule = SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=[3])
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/datetimes/',
|
||||
|
@ -914,7 +914,7 @@ def test_datetimes_multiple_agendas_shared_custody_other_rules(app):
|
|||
assert resp.json['data'][0]['id'] == 'first-agenda@event-thursday'
|
||||
|
||||
other_father_rule.delete()
|
||||
mother_rule.days = [2, 3, 4, 5, 6]
|
||||
mother_rule.days = [3, 4, 5, 6, 7]
|
||||
mother_rule.save()
|
||||
|
||||
resp = app.get(
|
||||
|
@ -938,7 +938,7 @@ def test_datetimes_multiple_agendas_shared_custody_recurring_event(app):
|
|||
wednesday_event = Event.objects.create(
|
||||
slug='event-wednesday',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[2],
|
||||
recurrence_days=[3],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=30),
|
||||
places=5,
|
||||
agenda=event_agenda,
|
||||
|
@ -948,7 +948,7 @@ def test_datetimes_multiple_agendas_shared_custody_recurring_event(app):
|
|||
thursday_event = Event.objects.create(
|
||||
slug='event-thursday',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[3],
|
||||
recurrence_days=[4],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=30),
|
||||
places=5,
|
||||
agenda=event_agenda,
|
||||
|
@ -969,10 +969,10 @@ def test_datetimes_multiple_agendas_shared_custody_recurring_event(app):
|
|||
)
|
||||
|
||||
father_rule = SharedCustodyRule.objects.create(
|
||||
agenda=agenda, guardian=father, days=list(range(7)), weeks='even'
|
||||
agenda=agenda, guardian=father, days=list(range(1, 8)), weeks='even'
|
||||
)
|
||||
mother_rule = SharedCustodyRule.objects.create(
|
||||
agenda=agenda, guardian=mother, days=list(range(7)), weeks='odd'
|
||||
agenda=agenda, guardian=mother, days=list(range(1, 8)), weeks='odd'
|
||||
)
|
||||
|
||||
resp = app.get(
|
||||
|
@ -1072,13 +1072,13 @@ def test_datetimes_multiple_agendas_shared_custody_recurring_event(app):
|
|||
]
|
||||
|
||||
# weirder rules
|
||||
father_rule.days = [0, 1]
|
||||
father_rule.days = [1, 2]
|
||||
father_rule.weeks = ''
|
||||
father_rule.save()
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=[2])
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=[3])
|
||||
|
||||
mother_rule.weeks = ''
|
||||
mother_rule.days = [3, 4, 5, 6]
|
||||
mother_rule.days = [4, 5, 6, 7]
|
||||
mother_rule.save()
|
||||
|
||||
resp = app.get(
|
||||
|
@ -1172,8 +1172,8 @@ def test_datetimes_multiple_agendas_shared_custody_holiday_rules(app):
|
|||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(1, 8)), weeks='even', guardian=father)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(1, 8)), weeks='odd', guardian=mother)
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/datetimes/',
|
||||
|
@ -1243,7 +1243,7 @@ def test_datetimes_multiple_agendas_shared_custody_date_start(app):
|
|||
wednesday_event = Event.objects.create(
|
||||
slug='event-wednesday',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[2],
|
||||
recurrence_days=[3],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=30),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
|
@ -1262,7 +1262,7 @@ def test_datetimes_multiple_agendas_shared_custody_date_start(app):
|
|||
agenda = SharedCustodyAgenda.objects.create(
|
||||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(7)))
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(1, 8)))
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/datetimes/',
|
||||
|
@ -1289,7 +1289,7 @@ def test_datetimes_multiple_agendas_shared_custody_date_start(app):
|
|||
child=child,
|
||||
date_start=datetime.date(year=2022, month=3, day=10),
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(7)))
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(1, 8)))
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/datetimes/',
|
||||
|
@ -1317,8 +1317,8 @@ def test_datetimes_multiple_agendas_shared_custody_date_start(app):
|
|||
child=child,
|
||||
date_start=datetime.date(year=2022, month=3, day=17),
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(7)), weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(7)), weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(1, 8)), weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(1, 8)), weeks='even')
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/datetimes/',
|
||||
|
@ -1362,8 +1362,10 @@ def test_datetimes_multiple_agendas_shared_custody_date_start(app):
|
|||
child=child,
|
||||
date_start=datetime.date(year=2022, month=3, day=22),
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=other_person, days=list(range(7)), weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(7)), weeks='even')
|
||||
SharedCustodyRule.objects.create(
|
||||
agenda=agenda, guardian=other_person, days=list(range(1, 8)), weeks='odd'
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(1, 8)), weeks='even')
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/datetimes/',
|
||||
|
@ -1399,7 +1401,7 @@ def test_datetimes_multiple_agendas_shared_custody_date_boundaries(app):
|
|||
wednesday_event = Event.objects.create(
|
||||
slug='event-wednesday',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[2],
|
||||
recurrence_days=[3],
|
||||
recurrence_end_date=datetime.datetime(year=2022, month=5, day=15),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
|
@ -1422,7 +1424,7 @@ def test_datetimes_multiple_agendas_shared_custody_date_boundaries(app):
|
|||
date_start=datetime.datetime(year=2022, month=3, day=15), # 15 days after recurring event start
|
||||
date_end=datetime.datetime(year=2022, month=3, day=30), # 30 days after recurring event start
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(7)))
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(1, 8)))
|
||||
agenda = SharedCustodyAgenda.objects.create(
|
||||
first_guardian=father,
|
||||
second_guardian=mother,
|
||||
|
@ -1430,7 +1432,7 @@ def test_datetimes_multiple_agendas_shared_custody_date_boundaries(app):
|
|||
date_start=datetime.datetime(year=2022, month=4, day=13), # 45 days after recurring event start
|
||||
date_end=datetime.datetime(year=2022, month=4, day=28), # 60 days after recurring event start
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(7)))
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(1, 8)))
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/datetimes/',
|
||||
|
@ -1702,3 +1704,42 @@ def test_datetimes_multiple_agendas_overlapping_events(app):
|
|||
params={'subscribed': 'all', 'user_external_id': 'xxx', 'check_overlaps': True},
|
||||
)
|
||||
assert len(resp.json['data']) == 0
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-05-06 14:00')
|
||||
def test_datetimes_multiple_agendas_enable_full_when_booked(app):
|
||||
agenda = Agenda.objects.create(
|
||||
label='First agenda', kind='events', minimal_booking_delay=0, maximal_booking_delay=45
|
||||
)
|
||||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
event = Event.objects.create(
|
||||
slug='recurring',
|
||||
start_datetime=now() + datetime.timedelta(hours=1),
|
||||
recurrence_days=[localtime().isoweekday()],
|
||||
recurrence_end_date=now() + datetime.timedelta(days=15),
|
||||
places=2,
|
||||
agenda=agenda,
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
||||
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda.slug, 'user_external_id': 'our_user'})
|
||||
assert [(d['id'], d['disabled']) for d in resp.json['data']] == [
|
||||
('first-agenda@recurring--2021-05-06-1700', False),
|
||||
('first-agenda@recurring--2021-05-13-1700', False),
|
||||
('first-agenda@recurring--2021-05-20-1700', False),
|
||||
]
|
||||
|
||||
first_event = Event.objects.get(slug='recurring--2021-05-06-1700')
|
||||
Booking.objects.create(event=first_event, user_external_id='our_user')
|
||||
Booking.objects.create(event=first_event, user_external_id='other_user')
|
||||
|
||||
second_event = Event.objects.get(slug='recurring--2021-05-13-1700')
|
||||
Booking.objects.create(event=second_event, user_external_id='other_user')
|
||||
Booking.objects.create(event=second_event, user_external_id='other_user_2')
|
||||
|
||||
resp = app.get('/api/agendas/datetimes/', params={'agendas': agenda.slug, 'user_external_id': 'our_user'})
|
||||
assert [(d['id'], d['disabled']) for d in resp.json['data']] == [
|
||||
('first-agenda@recurring--2021-05-06-1700', False), # full event with user booking, not disabled
|
||||
('first-agenda@recurring--2021-05-13-1700', True), # full event without user booking, disabled
|
||||
('first-agenda@recurring--2021-05-20-1700', False),
|
||||
]
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -34,7 +34,7 @@ def test_recurring_events_api_list(app, freezer):
|
|||
event = Event.objects.create(
|
||||
label='Example Event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[0, 3, 4], # Monday, Thursday, Friday
|
||||
recurrence_days=[1, 4, 5], # Monday, Thursday, Friday
|
||||
places=2,
|
||||
agenda=agenda,
|
||||
)
|
||||
|
@ -45,7 +45,7 @@ def test_recurring_events_api_list(app, freezer):
|
|||
Event.objects.create(
|
||||
label='Other',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
places=2,
|
||||
agenda=agenda,
|
||||
recurrence_end_date=now() + datetime.timedelta(days=45),
|
||||
|
@ -53,22 +53,22 @@ def test_recurring_events_api_list(app, freezer):
|
|||
|
||||
resp = app.get('/api/agendas/recurring-events/?agendas=%s&sort=day' % agenda.slug)
|
||||
assert len(resp.json['data']) == 4
|
||||
assert resp.json['data'][0]['id'] == 'foo-bar@example-event:0'
|
||||
assert resp.json['data'][0]['id'] == 'foo-bar@example-event:1'
|
||||
assert resp.json['data'][0]['text'] == 'Monday: Example Event'
|
||||
assert resp.json['data'][0]['label'] == 'Example Event'
|
||||
assert resp.json['data'][0]['day'] == 'Monday'
|
||||
assert resp.json['data'][0]['slug'] == 'example-event'
|
||||
assert resp.json['data'][1]['id'] == 'foo-bar@other:1'
|
||||
assert resp.json['data'][1]['id'] == 'foo-bar@other:2'
|
||||
assert resp.json['data'][1]['text'] == 'Tuesday: Other'
|
||||
assert resp.json['data'][1]['label'] == 'Other'
|
||||
assert resp.json['data'][1]['day'] == 'Tuesday'
|
||||
assert resp.json['data'][1]['slug'] == 'other'
|
||||
assert resp.json['data'][2]['id'] == 'foo-bar@example-event:3'
|
||||
assert resp.json['data'][2]['id'] == 'foo-bar@example-event:4'
|
||||
assert resp.json['data'][2]['text'] == 'Thursday: Example Event'
|
||||
assert resp.json['data'][2]['label'] == 'Example Event'
|
||||
assert resp.json['data'][2]['day'] == 'Thursday'
|
||||
assert resp.json['data'][2]['slug'] == 'example-event'
|
||||
assert resp.json['data'][3]['id'] == 'foo-bar@example-event:4'
|
||||
assert resp.json['data'][3]['id'] == 'foo-bar@example-event:5'
|
||||
assert resp.json['data'][3]['text'] == 'Friday: Example Event'
|
||||
assert resp.json['data'][3]['label'] == 'Example Event'
|
||||
assert resp.json['data'][3]['day'] == 'Friday'
|
||||
|
@ -76,10 +76,10 @@ def test_recurring_events_api_list(app, freezer):
|
|||
|
||||
resp = app.get('/api/agendas/recurring-events/?agendas=%s' % agenda.slug)
|
||||
assert len(resp.json['data']) == 4
|
||||
assert resp.json['data'][0]['id'] == 'foo-bar@example-event:0'
|
||||
assert resp.json['data'][1]['id'] == 'foo-bar@example-event:3'
|
||||
assert resp.json['data'][2]['id'] == 'foo-bar@example-event:4'
|
||||
assert resp.json['data'][3]['id'] == 'foo-bar@other:1'
|
||||
assert resp.json['data'][0]['id'] == 'foo-bar@example-event:1'
|
||||
assert resp.json['data'][1]['id'] == 'foo-bar@example-event:4'
|
||||
assert resp.json['data'][2]['id'] == 'foo-bar@example-event:5'
|
||||
assert resp.json['data'][3]['id'] == 'foo-bar@other:2'
|
||||
|
||||
resp = app.get('/api/agendas/recurring-events/?agendas=%s&sort=invalid' % agenda.slug, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
|
@ -89,7 +89,7 @@ def test_recurring_events_api_list(app, freezer):
|
|||
label='New event one hour before',
|
||||
slug='one-hour-before',
|
||||
start_datetime=now() - datetime.timedelta(hours=1),
|
||||
recurrence_days=[3], # Thursday
|
||||
recurrence_days=[4], # Thursday
|
||||
places=2,
|
||||
agenda=agenda,
|
||||
recurrence_end_date=now() + datetime.timedelta(days=30),
|
||||
|
@ -98,18 +98,18 @@ def test_recurring_events_api_list(app, freezer):
|
|||
label='New event two hours before but one week later',
|
||||
slug='two-hours-before',
|
||||
start_datetime=now() + datetime.timedelta(days=6, hours=22),
|
||||
recurrence_days=[3], # Thursday
|
||||
recurrence_days=[4], # Thursday
|
||||
places=2,
|
||||
agenda=agenda,
|
||||
)
|
||||
resp = app.get('/api/agendas/recurring-events/?agendas=%s&sort=day' % agenda.slug)
|
||||
assert len(resp.json['data']) == 6
|
||||
assert resp.json['data'][0]['id'] == 'foo-bar@example-event:0'
|
||||
assert resp.json['data'][1]['id'] == 'foo-bar@other:1'
|
||||
assert resp.json['data'][2]['id'] == 'foo-bar@two-hours-before:3'
|
||||
assert resp.json['data'][3]['id'] == 'foo-bar@one-hour-before:3'
|
||||
assert resp.json['data'][4]['id'] == 'foo-bar@example-event:3'
|
||||
assert resp.json['data'][5]['id'] == 'foo-bar@example-event:4'
|
||||
assert resp.json['data'][0]['id'] == 'foo-bar@example-event:1'
|
||||
assert resp.json['data'][1]['id'] == 'foo-bar@other:2'
|
||||
assert resp.json['data'][2]['id'] == 'foo-bar@two-hours-before:4'
|
||||
assert resp.json['data'][3]['id'] == 'foo-bar@one-hour-before:4'
|
||||
assert resp.json['data'][4]['id'] == 'foo-bar@example-event:4'
|
||||
assert resp.json['data'][5]['id'] == 'foo-bar@example-event:5'
|
||||
|
||||
freezer.move_to(new_event.recurrence_end_date)
|
||||
resp = app.get('/api/agendas/recurring-events/?agendas=%s' % agenda.slug)
|
||||
|
@ -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(
|
||||
|
@ -132,7 +166,7 @@ def test_recurring_events_api_list_shared_custody(app):
|
|||
event = Event.objects.create(
|
||||
slug='event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[0, 1, 2],
|
||||
recurrence_days=[1, 2, 3],
|
||||
recurrence_end_date=now() + datetime.timedelta(days=30),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
|
@ -140,7 +174,7 @@ def test_recurring_events_api_list_shared_custody(app):
|
|||
event.create_all_recurrences()
|
||||
|
||||
resp = app.get('/api/agendas/recurring-events/', params={'agendas': agenda.slug})
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:1', 'foo-bar@event:2', 'foo-bar@event:3']
|
||||
|
||||
# add shared custody agenda
|
||||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||||
|
@ -150,20 +184,20 @@ def test_recurring_events_api_list_shared_custody(app):
|
|||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=father, days=[0], weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=mother, days=[1, 2], weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=father, days=[1], weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=mother, days=[2, 3], weeks='odd')
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/recurring-events/',
|
||||
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
|
||||
)
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0']
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:1']
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/recurring-events/',
|
||||
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
|
||||
)
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:1', 'foo-bar@event:2']
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:2', 'foo-bar@event:3']
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/recurring-events/',
|
||||
|
@ -189,14 +223,14 @@ def test_recurring_events_api_list_shared_custody(app):
|
|||
'/api/agendas/recurring-events/',
|
||||
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
|
||||
)
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:1', 'foo-bar@event:2', 'foo-bar@event:3']
|
||||
|
||||
# nothing changed for father
|
||||
resp = app.get(
|
||||
'/api/agendas/recurring-events/',
|
||||
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
|
||||
)
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0']
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:1']
|
||||
|
||||
# add father custody during holidays
|
||||
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||||
|
@ -221,14 +255,14 @@ def test_recurring_events_api_list_shared_custody(app):
|
|||
'/api/agendas/recurring-events/',
|
||||
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
|
||||
)
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:1', 'foo-bar@event:2', 'foo-bar@event:3']
|
||||
|
||||
# nothing changed for mother
|
||||
resp = app.get(
|
||||
'/api/agendas/recurring-events/',
|
||||
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
|
||||
)
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:1', 'foo-bar@event:2', 'foo-bar@event:3']
|
||||
|
||||
# check exceptional custody periods take precedence over holiday rules
|
||||
SharedCustodyPeriod.objects.create(
|
||||
|
@ -243,14 +277,14 @@ def test_recurring_events_api_list_shared_custody(app):
|
|||
'/api/agendas/recurring-events/',
|
||||
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
|
||||
)
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:2']
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:1', 'foo-bar@event:3']
|
||||
|
||||
# nothing changed for mother
|
||||
resp = app.get(
|
||||
'/api/agendas/recurring-events/',
|
||||
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
|
||||
)
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:1', 'foo-bar@event:2', 'foo-bar@event:3']
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-12-13 14:00') # Monday of 50th week
|
||||
|
@ -262,7 +296,7 @@ def test_recurring_events_api_list_shared_custody_start_date(app):
|
|||
event = Event.objects.create(
|
||||
slug='event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[0, 1, 2],
|
||||
recurrence_days=[1, 2, 3],
|
||||
recurrence_end_date=now() + datetime.timedelta(days=30),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
|
@ -270,7 +304,7 @@ def test_recurring_events_api_list_shared_custody_start_date(app):
|
|||
event.create_all_recurrences()
|
||||
|
||||
resp = app.get('/api/agendas/recurring-events/', params={'agendas': agenda.slug})
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:1', 'foo-bar@event:2', 'foo-bar@event:3']
|
||||
|
||||
# add shared two custody agendas
|
||||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||||
|
@ -284,8 +318,8 @@ def test_recurring_events_api_list_shared_custody_start_date(app):
|
|||
date_start=now(),
|
||||
date_end=now() + datetime.timedelta(days=14),
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=father, days=[0], weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=mother, days=[1, 2], weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=father, days=[1], weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=mother, days=[2, 3], weeks='odd')
|
||||
|
||||
custody_agenda2 = SharedCustodyAgenda.objects.create(
|
||||
first_guardian=father,
|
||||
|
@ -293,20 +327,20 @@ def test_recurring_events_api_list_shared_custody_start_date(app):
|
|||
child=child,
|
||||
date_start=now() + datetime.timedelta(days=15),
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda2, guardian=father, days=[1], weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda2, guardian=mother, days=[0, 2], weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda2, guardian=father, days=[2], weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda2, guardian=mother, days=[1, 3], weeks='odd')
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/recurring-events/',
|
||||
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
|
||||
)
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1']
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:1', 'foo-bar@event:2']
|
||||
|
||||
resp = app.get(
|
||||
'/api/agendas/recurring-events/',
|
||||
params={'agendas': agenda.slug, 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
|
||||
)
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:0', 'foo-bar@event:1', 'foo-bar@event:2']
|
||||
assert [x['id'] for x in resp.json['data']] == ['foo-bar@event:1', 'foo-bar@event:2', 'foo-bar@event:3']
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-09-06 12:00')
|
||||
|
@ -319,11 +353,11 @@ def test_recurring_events_api_list_multiple_agendas(app):
|
|||
start_datetime=start,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[0, 2, 5],
|
||||
recurrence_days=[1, 3, 6],
|
||||
agenda=agenda,
|
||||
)
|
||||
Event.objects.create(
|
||||
label='B', start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[1], agenda=agenda
|
||||
label='B', start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[2], agenda=agenda
|
||||
)
|
||||
agenda2 = Agenda.objects.create(label='Second Agenda', kind='events')
|
||||
Desk.objects.create(agenda=agenda2, slug='_exceptions_holder')
|
||||
|
@ -332,29 +366,29 @@ def test_recurring_events_api_list_multiple_agendas(app):
|
|||
start_datetime=start,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[2, 3],
|
||||
recurrence_days=[3, 4],
|
||||
agenda=agenda2,
|
||||
)
|
||||
|
||||
resp = app.get('/api/agendas/recurring-events/?agendas=first-agenda,second-agenda&sort=day')
|
||||
event_ids = [x['id'] for x in resp.json['data']]
|
||||
assert event_ids == [
|
||||
'first-agenda@a:0',
|
||||
'first-agenda@b:1',
|
||||
'first-agenda@a:2',
|
||||
'second-agenda@c:2',
|
||||
'first-agenda@a:1',
|
||||
'first-agenda@b:2',
|
||||
'first-agenda@a:3',
|
||||
'second-agenda@c:3',
|
||||
'first-agenda@a:5',
|
||||
'second-agenda@c:4',
|
||||
'first-agenda@a:6',
|
||||
]
|
||||
assert event_ids.index('first-agenda@a:2') < event_ids.index('second-agenda@c:2')
|
||||
assert event_ids.index('first-agenda@a:3') < event_ids.index('second-agenda@c:3')
|
||||
|
||||
# sorting depends on querystring order
|
||||
resp = app.get('/api/agendas/recurring-events/?agendas=second-agenda,first-agenda&sort=day')
|
||||
event_ids = [x['id'] for x in resp.json['data']]
|
||||
assert event_ids.index('first-agenda@a:2') > event_ids.index('second-agenda@c:2')
|
||||
assert event_ids.index('first-agenda@a:3') > event_ids.index('second-agenda@c:3')
|
||||
|
||||
resp = app.get('/api/agendas/recurring-events/?agendas=second-agenda')
|
||||
assert [x['id'] for x in resp.json['data']] == ['second-agenda@c:2', 'second-agenda@c:3']
|
||||
assert [x['id'] for x in resp.json['data']] == ['second-agenda@c:3', 'second-agenda@c:4']
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-09-06 12:00')
|
||||
|
@ -366,7 +400,7 @@ def test_recurring_events_api_list_multiple_agendas_queries(app):
|
|||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
start, end = now(), now() + datetime.timedelta(days=30)
|
||||
event = Event.objects.create(
|
||||
start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[1, 2], agenda=agenda
|
||||
start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[2, 3], agenda=agenda
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
Subscription.objects.create(
|
||||
|
@ -392,8 +426,8 @@ def test_recurring_events_api_list_multiple_agendas_queries(app):
|
|||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(7)), weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(7)), weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(1, 8)), weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(1, 8)), weeks='odd')
|
||||
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
resp = app.get(
|
||||
|
@ -412,7 +446,7 @@ def test_recurring_events_api_list_subscribed(app, user):
|
|||
Event.objects.create(
|
||||
slug='event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[0, 1, 3, 6], # Monday, Tuesday, Thursday, Friday
|
||||
recurrence_days=[1, 2, 4, 7], # Monday, Tuesday, Thursday, Friday
|
||||
places=2,
|
||||
agenda=first_agenda,
|
||||
recurrence_end_date=now() + datetime.timedelta(days=364),
|
||||
|
@ -420,7 +454,7 @@ def test_recurring_events_api_list_subscribed(app, user):
|
|||
Event.objects.create(
|
||||
slug='sunday-event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[5],
|
||||
recurrence_days=[6],
|
||||
places=2,
|
||||
agenda=second_agenda,
|
||||
recurrence_end_date=now() + datetime.timedelta(days=364),
|
||||
|
@ -454,11 +488,11 @@ def test_recurring_events_api_list_subscribed(app, user):
|
|||
|
||||
# events are sorted by day
|
||||
assert [x['id'] for x in resp.json['data']] == [
|
||||
'first-agenda@event:0',
|
||||
'first-agenda@event:1',
|
||||
'first-agenda@event:3',
|
||||
'second-agenda@sunday-event:5',
|
||||
'first-agenda@event:6',
|
||||
'first-agenda@event:2',
|
||||
'first-agenda@event:4',
|
||||
'second-agenda@sunday-event:6',
|
||||
'first-agenda@event:7',
|
||||
]
|
||||
|
||||
resp = app.get('/api/agendas/recurring-events/?user_external_id=xxx&subscribed=category-b')
|
||||
|
@ -481,18 +515,18 @@ def test_recurring_events_api_list_subscribed(app, user):
|
|||
Event.objects.create(
|
||||
slug='event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[0],
|
||||
recurrence_days=[1],
|
||||
places=2,
|
||||
agenda=second_agenda,
|
||||
recurrence_end_date=now() + datetime.timedelta(days=364),
|
||||
)
|
||||
resp = app.get('/api/agendas/recurring-events/?subscribed=category-a,category-b&user_external_id=xxx')
|
||||
event_ids = [x['id'] for x in resp.json['data']]
|
||||
assert event_ids.index('first-agenda@event:0') < event_ids.index('second-agenda@event:0')
|
||||
assert event_ids.index('first-agenda@event:1') < event_ids.index('second-agenda@event:1')
|
||||
|
||||
resp = app.get('/api/agendas/recurring-events/?subscribed=category-b,category-a&user_external_id=xxx')
|
||||
event_ids = [x['id'] for x in resp.json['data']]
|
||||
assert event_ids.index('first-agenda@event:0') > event_ids.index('second-agenda@event:0')
|
||||
assert event_ids.index('first-agenda@event:1') > event_ids.index('second-agenda@event:1')
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2021-09-06 12:00')
|
||||
|
@ -506,7 +540,7 @@ def test_recurring_events_api_list_overlapping_events(app):
|
|||
duration=120,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[1],
|
||||
recurrence_days=[2],
|
||||
agenda=agenda,
|
||||
).create_all_recurrences()
|
||||
Event.objects.create(
|
||||
|
@ -515,7 +549,7 @@ def test_recurring_events_api_list_overlapping_events(app):
|
|||
duration=60,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[1],
|
||||
recurrence_days=[2],
|
||||
agenda=agenda,
|
||||
).create_all_recurrences()
|
||||
Event.objects.create(
|
||||
|
@ -524,7 +558,7 @@ def test_recurring_events_api_list_overlapping_events(app):
|
|||
duration=120,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[1, 3, 5],
|
||||
recurrence_days=[2, 4, 6],
|
||||
agenda=agenda,
|
||||
).create_all_recurrences()
|
||||
agenda2 = Agenda.objects.create(label='Second Agenda', kind='events')
|
||||
|
@ -535,7 +569,7 @@ def test_recurring_events_api_list_overlapping_events(app):
|
|||
duration=360,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[1, 5],
|
||||
recurrence_days=[2, 6],
|
||||
agenda=agenda2,
|
||||
).create_all_recurrences()
|
||||
Event.objects.create(
|
||||
|
@ -543,7 +577,7 @@ def test_recurring_events_api_list_overlapping_events(app):
|
|||
start_datetime=start,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[5],
|
||||
recurrence_days=[6],
|
||||
agenda=agenda2,
|
||||
).create_all_recurrences()
|
||||
|
||||
|
@ -556,24 +590,24 @@ def test_recurring_events_api_list_overlapping_events(app):
|
|||
custody_agenda = SharedCustodyAgenda.objects.create(
|
||||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=father, days=list(range(7)))
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=father, days=list(range(1, 8)))
|
||||
|
||||
params = {'sort': 'day', 'check_overlaps': True, 'user_external_id': 'user_id'}
|
||||
resp = app.get(
|
||||
'/api/agendas/recurring-events/', params={'agendas': 'first-agenda,second-agenda', **params}
|
||||
)
|
||||
assert [(x['id'], set(x['overlaps'])) for x in resp.json['data']] == [
|
||||
('first-agenda@event-12-14:1', {'second-agenda@event-12-18:1'}),
|
||||
('first-agenda@event-12-14:2', {'second-agenda@event-12-18:2'}),
|
||||
(
|
||||
'second-agenda@event-12-18:1',
|
||||
{'first-agenda@event-12-14:1', 'first-agenda@event-14-15:1', 'first-agenda@event-15-17:1'},
|
||||
'second-agenda@event-12-18:2',
|
||||
{'first-agenda@event-12-14:2', 'first-agenda@event-14-15:2', 'first-agenda@event-15-17:2'},
|
||||
),
|
||||
('first-agenda@event-14-15:1', {'second-agenda@event-12-18:1'}),
|
||||
('first-agenda@event-15-17:1', {'second-agenda@event-12-18:1'}),
|
||||
('first-agenda@event-15-17:3', set()),
|
||||
('second-agenda@event-12-18:5', {'first-agenda@event-15-17:5'}),
|
||||
('second-agenda@no-duration:5', set()),
|
||||
('first-agenda@event-15-17:5', {'second-agenda@event-12-18:5'}),
|
||||
('first-agenda@event-14-15:2', {'second-agenda@event-12-18:2'}),
|
||||
('first-agenda@event-15-17:2', {'second-agenda@event-12-18:2'}),
|
||||
('first-agenda@event-15-17:4', set()),
|
||||
('second-agenda@event-12-18:6', {'first-agenda@event-15-17:6'}),
|
||||
('second-agenda@no-duration:6', set()),
|
||||
('first-agenda@event-15-17:6', {'second-agenda@event-12-18:6'}),
|
||||
]
|
||||
|
||||
# same result with shared custody filter
|
||||
|
@ -587,11 +621,11 @@ def test_recurring_events_api_list_overlapping_events(app):
|
|||
|
||||
resp = app.get('/api/agendas/recurring-events/', params={'agendas': 'first-agenda', **params})
|
||||
assert [(x['id'], x['overlaps']) for x in resp.json['data']] == [
|
||||
('first-agenda@event-12-14:1', []),
|
||||
('first-agenda@event-14-15:1', []),
|
||||
('first-agenda@event-15-17:1', []),
|
||||
('first-agenda@event-15-17:3', []),
|
||||
('first-agenda@event-15-17:5', []),
|
||||
('first-agenda@event-12-14:2', []),
|
||||
('first-agenda@event-14-15:2', []),
|
||||
('first-agenda@event-15-17:2', []),
|
||||
('first-agenda@event-15-17:4', []),
|
||||
('first-agenda@event-15-17:6', []),
|
||||
]
|
||||
|
||||
del params['check_overlaps']
|
||||
|
@ -613,7 +647,7 @@ def test_recurring_events_api_list_overlapping_events_booking(app, shared_custod
|
|||
duration=120,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[1, 2],
|
||||
recurrence_days=[2, 3],
|
||||
agenda=agenda,
|
||||
).create_all_recurrences()
|
||||
event_15_16 = Event.objects.create(
|
||||
|
@ -622,7 +656,7 @@ def test_recurring_events_api_list_overlapping_events_booking(app, shared_custod
|
|||
duration=60,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[1],
|
||||
recurrence_days=[2],
|
||||
agenda=agenda,
|
||||
)
|
||||
event_15_16.create_all_recurrences()
|
||||
|
@ -634,7 +668,7 @@ def test_recurring_events_api_list_overlapping_events_booking(app, shared_custod
|
|||
duration=120,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[1, 2],
|
||||
recurrence_days=[2, 3],
|
||||
agenda=second_agenda,
|
||||
)
|
||||
event_13_15.create_all_recurrences()
|
||||
|
@ -647,7 +681,7 @@ def test_recurring_events_api_list_overlapping_events_booking(app, shared_custod
|
|||
custody_agenda = SharedCustodyAgenda.objects.create(
|
||||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=father, days=list(range(7)))
|
||||
SharedCustodyRule.objects.create(agenda=custody_agenda, guardian=father, days=list(range(1, 8)))
|
||||
|
||||
params = {
|
||||
'sort': 'day',
|
||||
|
@ -661,11 +695,11 @@ def test_recurring_events_api_list_overlapping_events_booking(app, shared_custod
|
|||
params['agendas'] = 'first-agenda'
|
||||
resp = app.get('/api/agendas/recurring-events/', params=params)
|
||||
assert len(resp.json['data']) == 3
|
||||
assert resp.json['data'][0]['id'] == 'first-agenda@event-12-14:1'
|
||||
assert resp.json['data'][0]['id'] == 'first-agenda@event-12-14:2'
|
||||
assert resp.json['data'][0]['has_booking_overlaps'] is False
|
||||
assert resp.json['data'][1]['id'] == 'first-agenda@event-15-16:1'
|
||||
assert resp.json['data'][1]['id'] == 'first-agenda@event-15-16:2'
|
||||
assert resp.json['data'][1]['has_booking_overlaps'] is False
|
||||
assert resp.json['data'][2]['id'] == 'first-agenda@event-12-14:2'
|
||||
assert resp.json['data'][2]['id'] == 'first-agenda@event-12-14:3'
|
||||
assert resp.json['data'][2]['has_booking_overlaps'] is False
|
||||
|
||||
# create one booking on first day
|
||||
|
@ -674,11 +708,11 @@ def test_recurring_events_api_list_overlapping_events_booking(app, shared_custod
|
|||
|
||||
resp = app.get('/api/agendas/recurring-events/', params=params)
|
||||
assert len(resp.json['data']) == 3
|
||||
assert resp.json['data'][0]['id'] == 'first-agenda@event-12-14:1'
|
||||
assert resp.json['data'][0]['id'] == 'first-agenda@event-12-14:2'
|
||||
assert resp.json['data'][0]['has_booking_overlaps'] is True
|
||||
assert resp.json['data'][1]['id'] == 'first-agenda@event-15-16:1'
|
||||
assert resp.json['data'][1]['id'] == 'first-agenda@event-15-16:2'
|
||||
assert resp.json['data'][1]['has_booking_overlaps'] is False
|
||||
assert resp.json['data'][2]['id'] == 'first-agenda@event-12-14:2'
|
||||
assert resp.json['data'][2]['id'] == 'first-agenda@event-12-14:3'
|
||||
assert resp.json['data'][2]['has_booking_overlaps'] is False
|
||||
|
||||
# create one booking on second day
|
||||
|
@ -687,11 +721,11 @@ def test_recurring_events_api_list_overlapping_events_booking(app, shared_custod
|
|||
|
||||
resp = app.get('/api/agendas/recurring-events/', params=params)
|
||||
assert len(resp.json['data']) == 3
|
||||
assert resp.json['data'][0]['id'] == 'first-agenda@event-12-14:1'
|
||||
assert resp.json['data'][0]['id'] == 'first-agenda@event-12-14:2'
|
||||
assert resp.json['data'][0]['has_booking_overlaps'] is True
|
||||
assert resp.json['data'][1]['id'] == 'first-agenda@event-15-16:1'
|
||||
assert resp.json['data'][1]['id'] == 'first-agenda@event-15-16:2'
|
||||
assert resp.json['data'][1]['has_booking_overlaps'] is False
|
||||
assert resp.json['data'][2]['id'] == 'first-agenda@event-12-14:2'
|
||||
assert resp.json['data'][2]['id'] == 'first-agenda@event-12-14:3'
|
||||
assert resp.json['data'][2]['has_booking_overlaps'] is True
|
||||
|
||||
# create one booking on first agenda
|
||||
|
@ -700,11 +734,11 @@ def test_recurring_events_api_list_overlapping_events_booking(app, shared_custod
|
|||
|
||||
resp = app.get('/api/agendas/recurring-events/', params=params)
|
||||
assert len(resp.json['data']) == 3
|
||||
assert resp.json['data'][0]['id'] == 'first-agenda@event-12-14:1'
|
||||
assert resp.json['data'][0]['id'] == 'first-agenda@event-12-14:2'
|
||||
assert resp.json['data'][0]['has_booking_overlaps'] is True
|
||||
assert resp.json['data'][1]['id'] == 'first-agenda@event-15-16:1'
|
||||
assert resp.json['data'][1]['id'] == 'first-agenda@event-15-16:2'
|
||||
assert resp.json['data'][1]['has_booking_overlaps'] is False # event is not marked as overlapping
|
||||
assert resp.json['data'][2]['id'] == 'first-agenda@event-12-14:2'
|
||||
assert resp.json['data'][2]['id'] == 'first-agenda@event-12-14:3'
|
||||
assert resp.json['data'][2]['has_booking_overlaps'] is True
|
||||
|
||||
# check date start
|
||||
|
|
|
@ -385,7 +385,7 @@ def test_booking_api_exclude_slots(app, user):
|
|||
event = Event.objects.create(
|
||||
slug='recurrent',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=15),
|
||||
places=3,
|
||||
agenda=agenda,
|
||||
|
@ -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')
|
||||
|
@ -1406,21 +1108,21 @@ def test_multiple_booking_api(app, user):
|
|||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
resp = app.post('/api/agenda/%s/fillslot/%s/?count=NaN' % (agenda.slug, event.id), 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)"
|
||||
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/fillslot/%s/?count=0' % (agenda.slug, event.id), status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == "count cannot be less than or equal to zero" # legacy
|
||||
assert resp.json['err_class'] == "count cannot be less than or equal to zero"
|
||||
assert resp.json['err_desc'] == "count cannot be less than or equal to zero"
|
||||
assert resp.json['reason'] == 'count cannot be less than or equal to zero' # legacy
|
||||
assert resp.json['err_class'] == 'count cannot be less than or equal to zero'
|
||||
assert resp.json['err_desc'] == 'count cannot be less than or equal to zero'
|
||||
|
||||
resp = app.post('/api/agenda/%s/fillslot/%s/?count=-3' % (agenda.slug, event.id), status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['reason'] == "count cannot be less than or equal to zero" # legacy
|
||||
assert resp.json['err_class'] == "count cannot be less than or equal to zero"
|
||||
assert resp.json['err_desc'] == "count cannot be less than or equal to zero"
|
||||
assert resp.json['reason'] == 'count cannot be less than or equal to zero' # legacy
|
||||
assert resp.json['err_class'] == 'count cannot be less than or equal to zero'
|
||||
assert resp.json['err_desc'] == 'count cannot be less than or equal to zero'
|
||||
|
||||
resp = app.post('/api/agenda/%s/fillslot/%s/?count=3' % (agenda.slug, event.id))
|
||||
Booking.objects.get(id=resp.json['booking_id'])
|
||||
|
@ -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(
|
||||
|
@ -2433,7 +1838,7 @@ def test_fillslot_past_events_recurring_event(app, user):
|
|||
event = Event.objects.create(
|
||||
label='Recurring',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=now() + datetime.timedelta(days=30),
|
||||
places=5,
|
||||
agenda=agenda,
|
||||
|
@ -2546,7 +1951,7 @@ def test_fillslot_recurring_event_booking_forbidden(app, user):
|
|||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now() + datetime.timedelta(days=7),
|
||||
recurrence_days=[now().weekday()],
|
||||
recurrence_days=[now().isoweekday()],
|
||||
places=2,
|
||||
agenda=agenda,
|
||||
)
|
||||
|
@ -2623,19 +2028,49 @@ 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'
|
||||
)
|
||||
|
||||
# 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'}
|
||||
|
@ -625,8 +626,8 @@ def test_api_events_fillslots_multiple_agendas_shared_custody(app, user):
|
|||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=[0, 1, 2])
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=[3, 4, 5, 6])
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=[1, 2, 3])
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=[4, 5, 6, 7])
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
params = {'user_external_id': 'child_id', 'slots': 'first-agenda@event-wednesday'}
|
||||
|
@ -702,7 +703,7 @@ def test_api_events_fillslots_multiple_agendas_shared_custody_date_start(app, us
|
|||
date_start=now(),
|
||||
date_end=datetime.date(year=2022, month=3, day=9),
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(7)))
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(1, 8)))
|
||||
|
||||
agenda2 = SharedCustodyAgenda.objects.create(
|
||||
first_guardian=father,
|
||||
|
@ -710,7 +711,7 @@ def test_api_events_fillslots_multiple_agendas_shared_custody_date_start(app, us
|
|||
child=child,
|
||||
date_start=datetime.date(year=2022, month=3, day=10),
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=agenda2, guardian=mother, days=list(range(7)))
|
||||
SharedCustodyRule.objects.create(agenda=agenda2, guardian=mother, days=list(range(1, 8)))
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
params = {'user_external_id': 'child_id', 'slots': 'first-agenda@event-wednesday'}
|
||||
|
@ -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'
|
||||
)
|
||||
|
|
|
@ -36,7 +36,7 @@ def test_recurring_events_api_fillslots(app, user, freezer, action):
|
|||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[0, 1, 3, 4], # Monday, Tuesday, Thursday, Friday
|
||||
recurrence_days=[1, 2, 4, 5], # Monday, Tuesday, Thursday, Friday
|
||||
places=2,
|
||||
waiting_list_places=1,
|
||||
agenda=agenda,
|
||||
|
@ -46,7 +46,7 @@ def test_recurring_events_api_fillslots(app, user, freezer, action):
|
|||
sunday_event = Event.objects.create(
|
||||
label='Sunday Event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[6],
|
||||
recurrence_days=[7],
|
||||
places=2,
|
||||
waiting_list_places=1,
|
||||
agenda=agenda,
|
||||
|
@ -61,7 +61,7 @@ def test_recurring_events_api_fillslots(app, user, freezer, action):
|
|||
fillslots_url = '/api/agendas/recurring-events/fillslots/?agendas=%s&action=%s' % (agenda.slug, action)
|
||||
params = {'user_external_id': 'user_id'}
|
||||
# Book Monday and Thursday of first event and Sunday of second event
|
||||
params['slots'] = 'foo-bar@event:0,foo-bar@event:3,foo-bar@sunday-event:6'
|
||||
params['slots'] = 'foo-bar@event:1,foo-bar@event:4,foo-bar@sunday-event:7'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 3
|
||||
assert 'booked_events' not in resp.json
|
||||
|
@ -124,7 +124,7 @@ def test_recurring_events_api_fillslots(app, user, freezer, action):
|
|||
resp = app.post_json(fillslots_url + '&date_start=2020-09-13&date_end=2020-09-19', params=params)
|
||||
assert resp.json['booking_count'] == 0
|
||||
|
||||
params['slots'] = 'foo-bar@event:1'
|
||||
params['slots'] = 'foo-bar@event:2'
|
||||
params['include_booked_events_detail'] = True
|
||||
resp = app.post_json(fillslots_url + '&date_start=2021-09-13&date_end=2021-09-19', params=params)
|
||||
assert resp.json['booking_count'] == 1
|
||||
|
@ -136,7 +136,7 @@ def test_recurring_events_api_fillslots(app, user, freezer, action):
|
|||
== Booking.objects.filter(user_external_id='user_id_4').get().pk
|
||||
)
|
||||
|
||||
resp = app.post_json(fillslots_url, params={'slots': 'foo-bar@event:0'}, status=400)
|
||||
resp = app.post_json(fillslots_url, params={'slots': 'foo-bar@event:1'}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == 'invalid payload'
|
||||
assert resp.json['errors']['user_external_id'] == ['This field is required.']
|
||||
|
@ -150,7 +150,7 @@ def test_recurring_events_api_fillslots(app, user, freezer, action):
|
|||
assert resp.json['err'] == 1
|
||||
assert resp.json['errors']['slots'] == ['invalid slot: foo-bar@a:a']
|
||||
|
||||
resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'foo-bar@a:1'}, status=400)
|
||||
resp = app.post_json(fillslots_url, params={'user_external_id': 'a', 'slots': 'foo-bar@a:2'}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['errors']['slots'] == ['event a of agenda foo-bar is not bookable']
|
||||
|
||||
|
@ -171,7 +171,7 @@ def test_recurring_events_api_fillslots(app, user, freezer, action):
|
|||
|
||||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'slots': 'foo-bar@event:1',
|
||||
'slots': 'foo-bar@event:2',
|
||||
'foo': 'bar',
|
||||
}
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
|
@ -198,7 +198,7 @@ def test_recurring_events_api_fillslots_waiting_list(app, user, freezer):
|
|||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[0],
|
||||
recurrence_days=[1],
|
||||
places=2,
|
||||
waiting_list_places=2,
|
||||
agenda=agenda,
|
||||
|
@ -214,7 +214,7 @@ def test_recurring_events_api_fillslots_waiting_list(app, user, freezer):
|
|||
assert events.filter(booked_waiting_list_places=1).count() == 5
|
||||
|
||||
# check that new bookings are put in waiting list despite free slots on main list
|
||||
params = {'user_external_id': 'user_id', 'slots': 'foo-bar@event:0'}
|
||||
params = {'user_external_id': 'user_id', 'slots': 'foo-bar@event:1'}
|
||||
resp = app.post_json(
|
||||
'/api/agendas/recurring-events/fillslots/?agendas=%s&action=update' % agenda.slug, params=params
|
||||
)
|
||||
|
@ -229,7 +229,7 @@ def test_recurring_events_api_fillslots_book_with_cancelled(app, user, freezer):
|
|||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[0, 1], # Monday, Tuesday
|
||||
recurrence_days=[1, 2], # Monday, Tuesday
|
||||
places=1,
|
||||
waiting_list_places=1,
|
||||
agenda=agenda,
|
||||
|
@ -269,7 +269,7 @@ def test_recurring_events_api_fillslots_book_with_cancelled(app, user, freezer):
|
|||
params = {'user_external_id': 'user_id'}
|
||||
|
||||
# Book Monday
|
||||
params['slots'] = 'foo-bar@event:0'
|
||||
params['slots'] = 'foo-bar@event:1'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 2
|
||||
assert resp.json['cancelled_booking_count'] == 0
|
||||
|
@ -303,7 +303,7 @@ def test_recurring_events_api_fillslots_book_with_cancelled(app, user, freezer):
|
|||
assert booking_2_1.cancellation_datetime is None
|
||||
|
||||
# Book Tuesday
|
||||
params['slots'] = 'foo-bar@event:1'
|
||||
params['slots'] = 'foo-bar@event:2'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 2
|
||||
assert resp.json['cancelled_booking_count'] == 0
|
||||
|
@ -345,7 +345,7 @@ def test_recurring_events_api_fillslots_update(app, user, freezer):
|
|||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[0, 1, 3, 4], # Monday, Tuesday, Thursday, Friday
|
||||
recurrence_days=[1, 2, 4, 5], # Monday, Tuesday, Thursday, Friday
|
||||
places=1,
|
||||
waiting_list_places=1,
|
||||
agenda=agenda,
|
||||
|
@ -357,7 +357,7 @@ def test_recurring_events_api_fillslots_update(app, user, freezer):
|
|||
fillslots_url = '/api/agendas/recurring-events/fillslots/?agendas=%s&action=update' % agenda.slug
|
||||
params = {'user_external_id': 'user_id'}
|
||||
# Book Monday and Thursday
|
||||
params['slots'] = 'foo-bar@event:0,foo-bar@event:3'
|
||||
params['slots'] = 'foo-bar@event:1,foo-bar@event:4'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 8
|
||||
assert resp.json['cancelled_booking_count'] == 0
|
||||
|
@ -366,7 +366,7 @@ def test_recurring_events_api_fillslots_update(app, user, freezer):
|
|||
assert Booking.objects.filter(event__start_datetime__week_day=5).count() == 4
|
||||
|
||||
# Book Friday without changing other bookings
|
||||
params['slots'] = 'foo-bar@event:4'
|
||||
params['slots'] = 'foo-bar@event:5'
|
||||
resp = app.post_json(fillslots_url.replace('update', 'book'), params=params)
|
||||
assert resp.json['booking_count'] == 4
|
||||
assert resp.json['cancelled_booking_count'] == 0
|
||||
|
@ -381,7 +381,7 @@ def test_recurring_events_api_fillslots_update(app, user, freezer):
|
|||
agenda.save()
|
||||
|
||||
# Change booking to Monday and Tuesday
|
||||
params['slots'] = 'foo-bar@event:0,foo-bar@event:1'
|
||||
params['slots'] = 'foo-bar@event:1,foo-bar@event:2'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 2
|
||||
assert resp.json['cancelled_booking_count'] == 4
|
||||
|
@ -446,7 +446,7 @@ def test_recurring_events_api_fillslots_update(app, user, freezer):
|
|||
assert Booking.objects.filter(cancellation_datetime__isnull=True).count() == 8
|
||||
|
||||
params = {'user_external_id': 'user_id_2'}
|
||||
params['slots'] = 'foo-bar@event:0,foo-bar@event:3'
|
||||
params['slots'] = 'foo-bar@event:1,foo-bar@event:4'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 8
|
||||
assert resp.json['cancelled_booking_count'] == 0
|
||||
|
@ -476,7 +476,7 @@ def test_recurring_events_api_fillslots_update(app, user, freezer):
|
|||
assert events.filter(booked_places=1).count() == 12
|
||||
assert events.filter(booked_waiting_list_places=1).count() == 4
|
||||
|
||||
params['slots'] = 'foo-bar@event:1,foo-bar@event:4'
|
||||
params['slots'] = 'foo-bar@event:2,foo-bar@event:5'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 8
|
||||
assert resp.json['cancelled_booking_count'] == 8
|
||||
|
@ -556,7 +556,7 @@ def test_recurring_events_api_fillslots_update(app, user, freezer):
|
|||
start_datetime=now() + datetime.timedelta(days=1), places=2, agenda=agenda
|
||||
)
|
||||
Booking.objects.create(event=normal_event, user_external_id='user_id')
|
||||
resp = app.post_json(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'foo-bar@event:0'})
|
||||
resp = app.post_json(fillslots_url, params={'user_external_id': 'user_id', 'slots': 'foo-bar@event:1'})
|
||||
assert resp.json['cancelled_booking_count'] == 3
|
||||
assert Booking.objects.filter(user_external_id='user_id', event=normal_event).count() == 1
|
||||
|
||||
|
@ -568,7 +568,7 @@ def test_recurring_events_api_fillslots_update_with_cancelled(app, user, freezer
|
|||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[0, 1], # Monday, Tuesday
|
||||
recurrence_days=[1, 2], # Monday, Tuesday
|
||||
places=1,
|
||||
waiting_list_places=1,
|
||||
agenda=agenda,
|
||||
|
@ -610,7 +610,7 @@ def test_recurring_events_api_fillslots_update_with_cancelled(app, user, freezer
|
|||
params = {'user_external_id': 'user_id'}
|
||||
|
||||
# Book Monday
|
||||
params['slots'] = 'foo-bar@event:0'
|
||||
params['slots'] = 'foo-bar@event:1'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 2
|
||||
assert resp.json['cancelled_booking_count'] == 1
|
||||
|
@ -648,7 +648,7 @@ def test_recurring_events_api_fillslots_update_with_cancelled(app, user, freezer
|
|||
assert booking_2_1_secondary.cancellation_datetime is not None
|
||||
|
||||
# Book Tuesday
|
||||
params['slots'] = 'foo-bar@event:1'
|
||||
params['slots'] = 'foo-bar@event:2'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 3
|
||||
assert resp.json['cancelled_booking_count'] == 3
|
||||
|
@ -690,7 +690,7 @@ def test_recurring_events_api_fillslots_unbook(app, user, freezer):
|
|||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[0, 1, 3, 4], # Monday, Tuesday, Thursday, Friday
|
||||
recurrence_days=[1, 2, 4, 5], # Monday, Tuesday, Thursday, Friday
|
||||
places=2,
|
||||
waiting_list_places=1,
|
||||
agenda=agenda,
|
||||
|
@ -700,7 +700,7 @@ def test_recurring_events_api_fillslots_unbook(app, user, freezer):
|
|||
sunday_event = Event.objects.create(
|
||||
label='Sunday Event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[6],
|
||||
recurrence_days=[7],
|
||||
places=2,
|
||||
waiting_list_places=1,
|
||||
agenda=agenda,
|
||||
|
@ -711,7 +711,7 @@ def test_recurring_events_api_fillslots_unbook(app, user, freezer):
|
|||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
fillslots_url = '/api/agendas/recurring-events/fillslots/?agendas=%s' % agenda.slug
|
||||
params = {'user_external_id': 'user_id'}
|
||||
params['slots'] = 'foo-bar@event:0,foo-bar@event:3,foo-bar@sunday-event:6'
|
||||
params['slots'] = 'foo-bar@event:1,foo-bar@event:4,foo-bar@sunday-event:7'
|
||||
resp = app.post_json(fillslots_url + '&action=book', params=params)
|
||||
assert resp.json['booking_count'] == 12
|
||||
|
||||
|
@ -724,7 +724,7 @@ def test_recurring_events_api_fillslots_unbook(app, user, freezer):
|
|||
agenda.maximal_booking_delay = 21
|
||||
agenda.save()
|
||||
|
||||
params['slots'] = 'foo-bar@event:0'
|
||||
params['slots'] = 'foo-bar@event:1'
|
||||
resp = app.post_json(fillslots_url + '&action=unbook', params=params)
|
||||
assert resp.json['booking_count'] == 0
|
||||
assert resp.json['cancelled_booking_count'] == 2
|
||||
|
@ -777,7 +777,7 @@ def test_recurring_events_api_fillslots_unbook(app, user, freezer):
|
|||
== 4
|
||||
)
|
||||
|
||||
params['slots'] = 'foo-bar@sunday-event:6'
|
||||
params['slots'] = 'foo-bar@sunday-event:7'
|
||||
resp = app.post_json(
|
||||
fillslots_url + '&action=unbook&date_start=2021-09-13&date_end=2021-09-20', params=params
|
||||
)
|
||||
|
@ -824,7 +824,7 @@ def test_recurring_events_api_fillslots_unbook(app, user, freezer):
|
|||
|
||||
freezer.move_to('2021-09-13 12:00')
|
||||
# old bookings are not unbooked
|
||||
params['slots'] = 'foo-bar@event:3'
|
||||
params['slots'] = 'foo-bar@event:4'
|
||||
resp = app.post_json(fillslots_url + '&action=unbook', params=params)
|
||||
assert resp.json['booking_count'] == 0
|
||||
assert resp.json['cancelled_booking_count'] == 3
|
||||
|
@ -864,7 +864,7 @@ def test_recurring_events_api_fillslots_unbook_with_cancelled(app, user, freezer
|
|||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[0, 1], # Monday, Tuesday
|
||||
recurrence_days=[1, 2], # Monday, Tuesday
|
||||
places=1,
|
||||
waiting_list_places=1,
|
||||
agenda=agenda,
|
||||
|
@ -904,7 +904,7 @@ def test_recurring_events_api_fillslots_unbook_with_cancelled(app, user, freezer
|
|||
params = {'user_external_id': 'user_id'}
|
||||
|
||||
# unbook Monday
|
||||
params['slots'] = 'foo-bar@event:0'
|
||||
params['slots'] = 'foo-bar@event:1'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 0
|
||||
assert resp.json['cancelled_booking_count'] == 1
|
||||
|
@ -939,7 +939,7 @@ def test_recurring_events_api_fillslots_unbook_with_cancelled(app, user, freezer
|
|||
assert booking_2_1.cancellation_datetime is None
|
||||
|
||||
# unbook Tuesday
|
||||
params['slots'] = 'foo-bar@event:1'
|
||||
params['slots'] = 'foo-bar@event:2'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 0
|
||||
assert resp.json['cancelled_booking_count'] == 1
|
||||
|
@ -997,7 +997,7 @@ def test_recurring_events_api_fillslots_subscribed(app, user):
|
|||
event = Event.objects.create(
|
||||
slug='event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[0, 1, 3, 4], # Monday, Tuesday, Thursday, Friday
|
||||
recurrence_days=[1, 2, 4, 5], # Monday, Tuesday, Thursday, Friday
|
||||
places=2,
|
||||
waiting_list_places=1,
|
||||
agenda=first_agenda,
|
||||
|
@ -1007,7 +1007,7 @@ def test_recurring_events_api_fillslots_subscribed(app, user):
|
|||
sunday_event = Event.objects.create(
|
||||
slug='sunday-event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=[6],
|
||||
recurrence_days=[7],
|
||||
places=2,
|
||||
waiting_list_places=1,
|
||||
agenda=second_agenda,
|
||||
|
@ -1026,7 +1026,7 @@ def test_recurring_events_api_fillslots_subscribed(app, user):
|
|||
fillslots_url = '/api/agendas/recurring-events/fillslots/?action=update&subscribed=%s'
|
||||
params = {'user_external_id': 'xxx'}
|
||||
# book Monday and Thursday of first event, in subscription range
|
||||
params['slots'] = 'first-agenda@event:0,first-agenda@event:3'
|
||||
params['slots'] = 'first-agenda@event:1,first-agenda@event:4'
|
||||
resp = app.post_json(fillslots_url % 'category-a', params=params)
|
||||
assert resp.json['booking_count'] == 9
|
||||
assert Booking.objects.count() == 9
|
||||
|
@ -1098,7 +1098,7 @@ def test_recurring_events_api_fillslots_subscribed(app, user):
|
|||
)
|
||||
|
||||
# not subscribed category
|
||||
params['slots'] = 'second-agenda@sunday-event:6'
|
||||
params['slots'] = 'second-agenda@sunday-event:7'
|
||||
resp = app.post_json(fillslots_url % 'category-b', params=params, status=400)
|
||||
|
||||
# update bookings
|
||||
|
@ -1108,7 +1108,7 @@ def test_recurring_events_api_fillslots_subscribed(app, user):
|
|||
date_start=now() + datetime.timedelta(days=100), # Wednesday 15/12
|
||||
date_end=now() + datetime.timedelta(days=150), # Thursday 03/02
|
||||
)
|
||||
params['slots'] = 'first-agenda@event:1,second-agenda@sunday-event:6'
|
||||
params['slots'] = 'first-agenda@event:2,second-agenda@sunday-event:7'
|
||||
resp = app.post_json(fillslots_url % 'all', params=params)
|
||||
assert resp.json['booking_count'] == 12
|
||||
assert resp.json['cancelled_booking_count'] == 10
|
||||
|
@ -1144,7 +1144,7 @@ def test_recurring_events_api_fillslots_subscribed(app, user):
|
|||
date_start=now() + datetime.timedelta(days=60),
|
||||
date_end=now() + datetime.timedelta(days=70),
|
||||
)
|
||||
params = {'user_external_id': 'yyy', 'slots': 'second-agenda@sunday-event:6'}
|
||||
params = {'user_external_id': 'yyy', 'slots': 'second-agenda@sunday-event:7'}
|
||||
resp = app.post_json(fillslots_url % 'category-b', params=params)
|
||||
assert resp.json['booking_count'] == 3
|
||||
assert Booking.objects.filter(cancellation_datetime__isnull=True).count() == 15
|
||||
|
@ -1170,12 +1170,12 @@ def test_recurring_events_api_fillslots_multiple_agendas(app, user):
|
|||
start_datetime=start,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[0, 2, 5],
|
||||
recurrence_days=[1, 3, 6],
|
||||
agenda=agenda,
|
||||
)
|
||||
event_a.create_all_recurrences()
|
||||
event_b = Event.objects.create(
|
||||
label='B', start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[1], agenda=agenda
|
||||
label='B', start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[2], agenda=agenda
|
||||
)
|
||||
event_b.create_all_recurrences()
|
||||
agenda2 = Agenda.objects.create(label='Second Agenda', kind='events')
|
||||
|
@ -1185,7 +1185,7 @@ def test_recurring_events_api_fillslots_multiple_agendas(app, user):
|
|||
start_datetime=start,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[2, 3],
|
||||
recurrence_days=[3, 4],
|
||||
agenda=agenda2,
|
||||
)
|
||||
event_c.create_all_recurrences()
|
||||
|
@ -1195,7 +1195,7 @@ def test_recurring_events_api_fillslots_multiple_agendas(app, user):
|
|||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
fillslots_url = '/api/agendas/recurring-events/fillslots/?action=%s&agendas=%s'
|
||||
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@a:0,first-agenda@a:5,second-agenda@c:3'}
|
||||
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@a:1,first-agenda@a:6,second-agenda@c:4'}
|
||||
resp = app.post_json(fillslots_url % ('update', 'first-agenda,second-agenda'), params=params)
|
||||
assert resp.json['booking_count'] == 13
|
||||
|
||||
|
@ -1205,7 +1205,7 @@ def test_recurring_events_api_fillslots_multiple_agendas(app, user):
|
|||
assert Booking.objects.filter(event__primary_event=event_c).count() == 4
|
||||
|
||||
# add bookings
|
||||
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@a:2,second-agenda@c:2'}
|
||||
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@a:3,second-agenda@c:3'}
|
||||
resp = app.post_json(fillslots_url % ('book', 'first-agenda,second-agenda'), params=params)
|
||||
assert resp.json['booking_count'] == 8
|
||||
assert resp.json['cancelled_booking_count'] == 0
|
||||
|
@ -1216,7 +1216,7 @@ def test_recurring_events_api_fillslots_multiple_agendas(app, user):
|
|||
assert Booking.objects.filter(event__primary_event=event_c).count() == 8
|
||||
|
||||
# unbook last week bookings
|
||||
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@a:2,second-agenda@c:2'}
|
||||
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@a:3,second-agenda@c:3'}
|
||||
date_start_param = '&date_start=%s' % (end - datetime.timedelta(days=7)).strftime('%Y-%m-%d')
|
||||
resp = app.post_json(
|
||||
(fillslots_url % ('unbook', 'first-agenda,second-agenda')) + date_start_param, params=params
|
||||
|
@ -1248,7 +1248,7 @@ def test_recurring_events_api_fillslots_multiple_agendas(app, user):
|
|||
)
|
||||
|
||||
# update bookings
|
||||
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@b:1'}
|
||||
params = {'user_external_id': 'user_id', 'slots': 'first-agenda@b:2'}
|
||||
resp = app.post_json(fillslots_url % ('update', 'first-agenda,second-agenda'), params=params)
|
||||
|
||||
assert resp.json['booking_count'] == 5
|
||||
|
@ -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')
|
||||
|
@ -1291,12 +1343,12 @@ def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user):
|
|||
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
|
||||
start, end = now(), now() + datetime.timedelta(days=30)
|
||||
event = Event.objects.create(
|
||||
start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[1, 2], agenda=agenda
|
||||
start_datetime=start, places=2, recurrence_end_date=end, recurrence_days=[2, 3], agenda=agenda
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
with connection.cursor() as cursor:
|
||||
# force an analyze pass after we load data so PG has usable statistics
|
||||
cursor.execute("ANALYZE;")
|
||||
cursor.execute('ANALYZE;')
|
||||
|
||||
agenda_slugs = ','.join(str(i) for i in range(20))
|
||||
resp = app.get('/api/agendas/recurring-events/?action=update&agendas=%s' % agenda_slugs)
|
||||
|
@ -1338,8 +1390,8 @@ def test_recurring_events_api_fillslots_multiple_agendas_queries(app, user):
|
|||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(7)), weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(7)), weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(1, 8)), weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(1, 8)), weeks='odd')
|
||||
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
resp = app.post_json(
|
||||
|
@ -1358,7 +1410,7 @@ def test_recurring_events_api_fillslots_shared_custody(app, user, freezer):
|
|||
event = Event.objects.create(
|
||||
label='Event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=list(range(7)),
|
||||
recurrence_days=list(range(1, 8)),
|
||||
places=2,
|
||||
waiting_list_places=1,
|
||||
agenda=agenda,
|
||||
|
@ -1373,11 +1425,11 @@ def test_recurring_events_api_fillslots_shared_custody(app, user, freezer):
|
|||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, weeks='odd', days=[0, 1, 2])
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, weeks='even', days=[3, 4, 5])
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, weeks='even', days=[0, 1, 2])
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, weeks='odd', days=[3, 4, 5])
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=[6])
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, weeks='odd', days=[1, 2, 3])
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, weeks='even', days=[4, 5, 6])
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, weeks='even', days=[1, 2, 3])
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, weeks='odd', days=[4, 5, 6])
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=[7])
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
fillslots_url = (
|
||||
|
@ -1385,7 +1437,7 @@ def test_recurring_events_api_fillslots_shared_custody(app, user, freezer):
|
|||
)
|
||||
params = {
|
||||
'user_external_id': 'child_id',
|
||||
'slots': ','.join('foo-bar@event:%s' % i for i in range(7)), # book every days
|
||||
'slots': ','.join('foo-bar@event:%s' % i for i in range(1, 8)), # book every days
|
||||
'include_booked_events_detail': True,
|
||||
}
|
||||
resp = app.post_json(fillslots_url % 'father_id', params=params)
|
||||
|
@ -1422,7 +1474,7 @@ def test_recurring_events_api_fillslots_shared_custody(app, user, freezer):
|
|||
date_start=datetime.date(year=2022, month=3, day=14),
|
||||
)
|
||||
|
||||
SharedCustodyRule.objects.create(agenda=agenda2, guardian=father, days=list(range(7)))
|
||||
SharedCustodyRule.objects.create(agenda=agenda2, guardian=father, days=list(range(1, 8)))
|
||||
Booking.objects.all().delete()
|
||||
|
||||
resp = app.post_json(fillslots_url % 'father_id', params=params)
|
||||
|
@ -1473,7 +1525,7 @@ def test_recurring_events_api_fillslots_overlapping_events(app, user):
|
|||
duration=120,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[1],
|
||||
recurrence_days=[2],
|
||||
agenda=agenda,
|
||||
).create_all_recurrences()
|
||||
Event.objects.create(
|
||||
|
@ -1482,7 +1534,7 @@ def test_recurring_events_api_fillslots_overlapping_events(app, user):
|
|||
duration=60,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[1],
|
||||
recurrence_days=[2],
|
||||
agenda=agenda,
|
||||
).create_all_recurrences()
|
||||
Event.objects.create(
|
||||
|
@ -1491,7 +1543,7 @@ def test_recurring_events_api_fillslots_overlapping_events(app, user):
|
|||
duration=120,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[1, 3, 5],
|
||||
recurrence_days=[2, 4, 6],
|
||||
agenda=agenda,
|
||||
).create_all_recurrences()
|
||||
agenda2 = Agenda.objects.create(label='Second Agenda', kind='events')
|
||||
|
@ -1502,7 +1554,7 @@ def test_recurring_events_api_fillslots_overlapping_events(app, user):
|
|||
duration=360,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[1, 5],
|
||||
recurrence_days=[2, 6],
|
||||
agenda=agenda2,
|
||||
).create_all_recurrences()
|
||||
Event.objects.create(
|
||||
|
@ -1510,7 +1562,7 @@ def test_recurring_events_api_fillslots_overlapping_events(app, user):
|
|||
start_datetime=start,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[5],
|
||||
recurrence_days=[6],
|
||||
agenda=agenda2,
|
||||
).create_all_recurrences()
|
||||
|
||||
|
@ -1521,7 +1573,7 @@ def test_recurring_events_api_fillslots_overlapping_events(app, user):
|
|||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'check_overlaps': 'first-agenda,second-agenda',
|
||||
'slots': 'first-agenda@event-12-14:1,first-agenda@event-14-15:1,second-agenda@event-12-18:5',
|
||||
'slots': 'first-agenda@event-12-14:2,first-agenda@event-14-15:2,second-agenda@event-12-18:6',
|
||||
}
|
||||
resp = app.post_json(fillslots_url % ('update', 'first-agenda,second-agenda'), params=params)
|
||||
assert resp.json['booking_count'] == 14
|
||||
|
@ -1534,7 +1586,7 @@ def test_recurring_events_api_fillslots_overlapping_events(app, user):
|
|||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'check_overlaps': 'first-agenda,second-agenda',
|
||||
'slots': 'second-agenda@event-12-18:1',
|
||||
'slots': 'second-agenda@event-12-18:2',
|
||||
}
|
||||
resp = app.post_json(fillslots_url % ('update', 'first-agenda,second-agenda'), params=params)
|
||||
assert resp.json['booking_count'] == 5
|
||||
|
@ -1546,7 +1598,7 @@ def test_recurring_events_api_fillslots_overlapping_events(app, user):
|
|||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'check_overlaps': 'first-agenda,second-agenda',
|
||||
'slots': 'second-agenda@event-12-18:5,second-agenda@no-duration:5',
|
||||
'slots': 'second-agenda@event-12-18:6,second-agenda@no-duration:6',
|
||||
'include_booked_events_detail': True,
|
||||
}
|
||||
resp = app.post_json(fillslots_url % ('update', 'first-agenda,second-agenda'), params=params)
|
||||
|
@ -1574,34 +1626,34 @@ def test_recurring_events_api_fillslots_overlapping_events(app, user):
|
|||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'check_overlaps': 'first-agenda,second-agenda',
|
||||
'slots': 'first-agenda@event-12-14:1,second-agenda@event-12-18:1',
|
||||
'slots': 'first-agenda@event-12-14:2,second-agenda@event-12-18:2',
|
||||
}
|
||||
resp = app.post_json(fillslots_url % ('update', 'first-agenda,second-agenda'), params=params)
|
||||
assert resp.json['err'] == 1
|
||||
assert (
|
||||
resp.json['err_desc']
|
||||
== 'Some events occur at the same time: first-agenda@event-12-14:1 / second-agenda@event-12-18:1'
|
||||
== 'Some events occur at the same time: first-agenda@event-12-14:2 / second-agenda@event-12-18:2'
|
||||
)
|
||||
|
||||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'check_overlaps': 'first-agenda,second-agenda',
|
||||
'slots': (
|
||||
'first-agenda@event-12-14:1,first-agenda@event-15-17:1,first-agenda@event-15-17:3,first-agenda@event-15-17:5,second-agenda@event-12-18:1,'
|
||||
'second-agenda@event-12-18:5,second-agenda@no-duration:5'
|
||||
'first-agenda@event-12-14:2,first-agenda@event-15-17:2,first-agenda@event-15-17:4,first-agenda@event-15-17:6,second-agenda@event-12-18:2,'
|
||||
'second-agenda@event-12-18:6,second-agenda@no-duration:6'
|
||||
),
|
||||
}
|
||||
resp = app.post_json(fillslots_url % ('update', 'first-agenda,second-agenda'), params=params)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == (
|
||||
'Some events occur at the same time: first-agenda@event-12-14:1 / second-agenda@event-12-18:1, '
|
||||
'first-agenda@event-15-17:1 / second-agenda@event-12-18:1, first-agenda@event-15-17:5 / second-agenda@event-12-18:5'
|
||||
'Some events occur at the same time: first-agenda@event-12-14:2 / second-agenda@event-12-18:2, '
|
||||
'first-agenda@event-15-17:2 / second-agenda@event-12-18:2, first-agenda@event-15-17:6 / second-agenda@event-12-18:6'
|
||||
)
|
||||
|
||||
# overlaps check is disabled by default
|
||||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'slots': 'first-agenda@event-12-14:1,second-agenda@event-12-18:1',
|
||||
'slots': 'first-agenda@event-12-14:2,second-agenda@event-12-18:2',
|
||||
}
|
||||
resp = app.post_json(fillslots_url % ('update', 'first-agenda,second-agenda'), params=params)
|
||||
assert resp.json['err'] == 0
|
||||
|
@ -1619,7 +1671,7 @@ def test_recurring_events_api_fillslots_partly_overlapping_events(app, user):
|
|||
duration=120,
|
||||
places=2,
|
||||
recurrence_end_date=end + datetime.timedelta(days=7),
|
||||
recurrence_days=[1],
|
||||
recurrence_days=[2],
|
||||
agenda=agenda,
|
||||
)
|
||||
event_12_14.create_all_recurrences()
|
||||
|
@ -1631,7 +1683,7 @@ def test_recurring_events_api_fillslots_partly_overlapping_events(app, user):
|
|||
duration=120,
|
||||
places=2,
|
||||
recurrence_end_date=end,
|
||||
recurrence_days=[1],
|
||||
recurrence_days=[2],
|
||||
agenda=agenda2,
|
||||
).create_all_recurrences()
|
||||
|
||||
|
@ -1645,7 +1697,7 @@ def test_recurring_events_api_fillslots_partly_overlapping_events(app, user):
|
|||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'check_overlaps': 'first-agenda,second-agenda',
|
||||
'slots': 'second-agenda@event-13-15:1',
|
||||
'slots': 'second-agenda@event-13-15:2',
|
||||
'include_booked_events_detail': True,
|
||||
}
|
||||
resp = app.post_json(fillslots_url % 'second-agenda', params=params)
|
||||
|
@ -1659,7 +1711,7 @@ def test_recurring_events_api_fillslots_partly_overlapping_events(app, user):
|
|||
'2021-10-05',
|
||||
]
|
||||
|
||||
params['slots'] = 'first-agenda@event-12-14:1'
|
||||
params['slots'] = 'first-agenda@event-12-14:2'
|
||||
resp = app.post_json(fillslots_url % 'first-agenda', params=params)
|
||||
assert resp.json['booking_count'] == 1
|
||||
assert resp.json['cancelled_booking_count'] == 0
|
||||
|
@ -1690,7 +1742,7 @@ def test_recurring_events_api_fillslots_partial_bookings(app, user):
|
|||
end_time=datetime.time(18, 00),
|
||||
places=2,
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=30),
|
||||
recurrence_days=[1],
|
||||
recurrence_days=[2],
|
||||
agenda=agenda,
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
@ -1699,7 +1751,7 @@ def test_recurring_events_api_fillslots_partial_bookings(app, user):
|
|||
|
||||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'slots': 'foo-bar@event-08-18:1',
|
||||
'slots': 'foo-bar@event-08-18:2',
|
||||
'start_time': '10:00',
|
||||
'end_time': '15:00',
|
||||
}
|
||||
|
@ -1737,3 +1789,73 @@ def test_recurring_events_api_fillslots_partial_bookings(app, user):
|
|||
params['end_time'] = '09:00'
|
||||
resp = app.post_json(fillslots_url, params=params, status=400)
|
||||
assert resp.json['errors']['non_field_errors'][0] == 'start_time must be before end_time'
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2023-05-01 10:00')
|
||||
def test_recurring_events_api_fillslots_partial_bookings_update(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 08-18',
|
||||
start_datetime=start_datetime,
|
||||
end_time=datetime.time(18, 00),
|
||||
places=2,
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=30),
|
||||
recurrence_days=[2, 3],
|
||||
agenda=agenda,
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
params = {
|
||||
'user_external_id': 'user_id',
|
||||
'slots': 'foo-bar@event-08-18:2',
|
||||
'start_time': '10:00',
|
||||
'end_time': '15:00',
|
||||
}
|
||||
fillslots_url = '/api/agendas/recurring-events/fillslots/?action=update&agendas=%s' % agenda.slug
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 5
|
||||
assert Booking.objects.count() == 5
|
||||
assert (
|
||||
Booking.objects.filter(
|
||||
start_time=datetime.time(10, 00),
|
||||
end_time=datetime.time(15, 00),
|
||||
event__start_datetime__iso_week_day=2,
|
||||
).count()
|
||||
== 5
|
||||
)
|
||||
|
||||
params['start_time'] = '10:00'
|
||||
params['end_time'] = '15:00'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 5
|
||||
assert resp.json['cancelled_booking_count'] == 0
|
||||
assert Booking.objects.count() == 5
|
||||
assert (
|
||||
Booking.objects.filter(
|
||||
cancellation_datetime__isnull=True,
|
||||
start_time=datetime.time(10, 00),
|
||||
end_time=datetime.time(15, 00),
|
||||
event__start_datetime__iso_week_day=2,
|
||||
).count()
|
||||
== 5
|
||||
)
|
||||
|
||||
# change day
|
||||
params['slots'] = 'foo-bar@event-08-18:3'
|
||||
resp = app.post_json(fillslots_url, params=params)
|
||||
assert resp.json['booking_count'] == 5
|
||||
assert resp.json['cancelled_booking_count'] == 5
|
||||
assert Booking.objects.count() == 5
|
||||
assert (
|
||||
Booking.objects.filter(
|
||||
cancellation_datetime__isnull=True,
|
||||
start_time=datetime.time(10, 00),
|
||||
end_time=datetime.time(15, 00),
|
||||
event__start_datetime__iso_week_day=3,
|
||||
).count()
|
||||
== 5
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
@ -72,9 +72,9 @@ def test_agendas_api(app):
|
|||
'category': 'category-a',
|
||||
'category_label': 'Category A',
|
||||
'events_type': 'type-a',
|
||||
'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,
|
||||
},
|
||||
},
|
||||
|
@ -92,9 +92,9 @@ def test_agendas_api(app):
|
|||
'category': 'category-a',
|
||||
'category_label': 'Category A',
|
||||
'events_type': 'type-b',
|
||||
'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,
|
||||
},
|
||||
},
|
||||
|
@ -112,9 +112,9 @@ def test_agendas_api(app):
|
|||
'category': None,
|
||||
'category_label': None,
|
||||
'events_type': None,
|
||||
'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,
|
||||
},
|
||||
},
|
||||
|
@ -138,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,
|
||||
},
|
||||
},
|
||||
|
@ -159,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,
|
||||
},
|
||||
},
|
||||
|
@ -178,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,
|
||||
},
|
||||
},
|
||||
|
@ -285,7 +282,7 @@ def test_agendas_api(app):
|
|||
start_datetime=now(),
|
||||
places=10,
|
||||
agenda=event_agenda,
|
||||
recurrence_days=list(range(7)),
|
||||
recurrence_days=list(range(1, 8)),
|
||||
recurrence_end_date=now() + datetime.timedelta(days=15),
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
@ -294,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')
|
||||
|
@ -301,7 +307,7 @@ def test_agendas_api(app):
|
|||
start_datetime=now(),
|
||||
places=10,
|
||||
agenda=event_agenda,
|
||||
recurrence_days=[now().weekday()],
|
||||
recurrence_days=[now().isoweekday()],
|
||||
recurrence_end_date=now() + datetime.timedelta(days=15),
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
@ -426,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,
|
||||
},
|
||||
},
|
||||
|
@ -466,6 +471,7 @@ def test_agenda_api_delete_busy(app, user):
|
|||
|
||||
@pytest.mark.freeze_time('2021-07-09T08:00:00.0+02:00')
|
||||
def test_add_agenda(app, user, settings):
|
||||
settings.TEMPLATE_VARS = {'eservices_url': 'http://demarches/'}
|
||||
events_type = EventsType.objects.create(label='Type A')
|
||||
category_a = Category.objects.create(label='Category A')
|
||||
api_url = '/api/agenda/'
|
||||
|
@ -606,6 +612,7 @@ def test_add_agenda(app, user, settings):
|
|||
'mark_event_checked_auto': False,
|
||||
'disable_check_update': True,
|
||||
'booking_check_filters': 'foo,bar,baz',
|
||||
'booking_form_url': '{{ eservices_url }}backoffice/submission/inscription-aux-activites/',
|
||||
}
|
||||
resp = app.post_json(api_url, params=params)
|
||||
assert not resp.json['err']
|
||||
|
@ -622,8 +629,14 @@ def test_add_agenda(app, user, settings):
|
|||
assert agenda.mark_event_checked_auto is False
|
||||
assert agenda.disable_check_update is True
|
||||
assert agenda.booking_check_filters == 'foo,bar,baz'
|
||||
assert agenda.booking_form_url == '{{ eservices_url }}backoffice/submission/inscription-aux-activites/'
|
||||
assert Desk.objects.filter(agenda=agenda, slug='_exceptions_holder').exists()
|
||||
|
||||
assert (
|
||||
resp.json['data'][0]['booking_form_url']
|
||||
== 'http://demarches/backoffice/submission/inscription-aux-activites/'
|
||||
)
|
||||
|
||||
resp = app.get('/api/agendas/datetimes/?agendas=%s' % agenda.slug)
|
||||
assert 'data' in resp.json
|
||||
|
||||
|
|
|
@ -100,8 +100,42 @@ def test_booking_ics(app, some_data, meetings_agenda, user, settings, rf):
|
|||
end = (
|
||||
booking.event.start_datetime + datetime.timedelta(minutes=booking.event.meeting_type.duration)
|
||||
).strftime('%Y%m%dT%H%M%S')
|
||||
assert "DTSTART:%sZ\r\n" % start in booking_ics
|
||||
assert "DTEND:%sZ\r\n" % end in booking_ics
|
||||
assert 'DTSTART:%sZ\r\n' % start in booking_ics
|
||||
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):
|
||||
|
@ -250,8 +284,8 @@ def test_bookings_api_filter_date_start(app, user):
|
|||
for value in ['foo', '2017-05-42']:
|
||||
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_start': value}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_class'] == "invalid payload"
|
||||
assert resp.json['err_desc'] == "invalid payload"
|
||||
assert resp.json['err_class'] == 'invalid payload'
|
||||
assert resp.json['err_desc'] == 'invalid payload'
|
||||
|
||||
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_start': '2017-05-21'})
|
||||
assert resp.json['err'] == 0
|
||||
|
@ -287,8 +321,8 @@ def test_bookings_api_filter_date_end(app, user):
|
|||
for value in ['foo', '2017-05-42']:
|
||||
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_end': value}, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_class'] == "invalid payload"
|
||||
assert resp.json['err_desc'] == "invalid payload"
|
||||
assert resp.json['err_class'] == 'invalid payload'
|
||||
assert resp.json['err_desc'] == 'invalid payload'
|
||||
|
||||
resp = app.get('/api/bookings/', params={'user_external_id': '42', 'date_end': '2017-05-21'})
|
||||
assert resp.json['err'] == 0
|
||||
|
@ -443,7 +477,7 @@ def test_bookings_api_filter_event(app, user):
|
|||
recurring_event = Event.objects.create(
|
||||
slug='recurring-event',
|
||||
start_datetime=now(),
|
||||
recurrence_days=list(range(7)),
|
||||
recurrence_days=list(range(1, 8)),
|
||||
places=2,
|
||||
waiting_list_places=1,
|
||||
agenda=agenda,
|
||||
|
|
|
@ -237,12 +237,12 @@ def test_event_notify_checked(app, user):
|
|||
'days_in, days_out, err_msg',
|
||||
[
|
||||
(1, None, 'Expected a list of items but got type "int".'),
|
||||
('2', [2], None),
|
||||
([3], [3], None),
|
||||
(['4'], [4], None),
|
||||
([1, 2], [1, 2], None),
|
||||
(['2', '3'], [2, 3], None),
|
||||
('4, 5', [4, 5], None),
|
||||
('2', [3], None),
|
||||
([3], [4], None),
|
||||
(['4'], [5], None),
|
||||
([1, 2], [2, 3], None),
|
||||
(['2', '3'], [3, 4], None),
|
||||
('4, 5', [5, 6], None),
|
||||
],
|
||||
)
|
||||
def test_string_or_list_serialiser(app, user, days_in, days_out, err_msg):
|
||||
|
@ -444,7 +444,7 @@ def test_add_event(app, user):
|
|||
)
|
||||
event = Event.objects.filter(agenda=agenda).get(slug='foo-bar-event-2')
|
||||
assert event.description == 'A recurrent event'
|
||||
assert event.recurrence_days == [3]
|
||||
assert event.recurrence_days == [4]
|
||||
assert event.recurrence_week_interval == 2
|
||||
assert event.recurrence_end_date is None
|
||||
# some occurrences created
|
||||
|
@ -472,7 +472,7 @@ def test_add_event(app, user):
|
|||
event = Event.objects.filter(agenda=agenda).get(slug='foo-bar-event-3')
|
||||
assert Event.objects.filter(agenda=agenda).count() == 39
|
||||
assert event.description == 'A recurrent event having recurrences'
|
||||
assert event.recurrence_days == [0, 3, 5]
|
||||
assert event.recurrence_days == [1, 4, 6]
|
||||
assert event.recurrence_week_interval == 2
|
||||
assert event.recurrence_end_date == datetime.date(2021, 12, 27)
|
||||
assert event.custom_fields == {
|
||||
|
@ -869,7 +869,7 @@ def test_delete_recurring_event_forbidden(app, user):
|
|||
start_datetime=start_datetime,
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=7),
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
@ -910,7 +910,7 @@ def test_events(app, user):
|
|||
slug='recurring-event-slug',
|
||||
label='Recurring Event Label',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=8),
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
|
@ -1322,8 +1322,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,14 +1333,20 @@ 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(
|
||||
slug='recurring-event-slug',
|
||||
label='Recurring Event Label',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=7),
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
|
@ -1392,7 +1399,13 @@ def test_events_check_status_events(app, user):
|
|||
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),
|
||||
user_check_start_time=datetime.time(7, 55),
|
||||
user_check_end_time=datetime.time(17, 15),
|
||||
)
|
||||
assert booking1.get_computed_start_time() == datetime.time(8, 0)
|
||||
assert booking1.get_computed_end_time() == datetime.time(17, 30)
|
||||
booking2 = Booking.objects.create(
|
||||
event=event, user_external_id='child:42', user_was_present=True, user_check_type_slug='foo-reason'
|
||||
)
|
||||
|
@ -1409,119 +1422,178 @@ def test_events_check_status_events(app, user):
|
|||
resp = app.get(url, params=params)
|
||||
assert len(ctx.captured_queries) == 5
|
||||
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')
|
||||
|
@ -1814,7 +1886,7 @@ def test_events_check_lock_events(app, user):
|
|||
slug='recurring-event-slug',
|
||||
label='Recurring Event Label',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=7),
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
|
@ -2090,7 +2162,7 @@ def test_events_invoiced_events(app, user):
|
|||
slug='recurring-event-slug',
|
||||
label='Recurring Event Label',
|
||||
start_datetime=start_datetime,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=7),
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from chrono.agendas.models import Agenda, Booking, Category, Event
|
||||
from chrono.agendas.models import Agenda, Booking, Category, Desk, Event, MeetingType, VirtualMember
|
||||
from chrono.utils.timezone import now
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
@ -9,6 +11,7 @@ pytestmark = pytest.mark.django_db
|
|||
def test_statistics_list(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar')
|
||||
agenda2 = Agenda.objects.create(label='Bar foo')
|
||||
Agenda.objects.create(label='Virtual', kind='virtual')
|
||||
category = Category.objects.create(label='Category A')
|
||||
category2 = Category.objects.create(label='Category B')
|
||||
|
||||
|
@ -22,6 +25,7 @@ def test_statistics_list(app, user):
|
|||
{'id': '_all', 'label': 'All'},
|
||||
{'id': 'bar-foo', 'label': 'Bar foo'},
|
||||
{'id': 'foo-bar', 'label': 'Foo bar'},
|
||||
{'id': 'virtual', 'label': 'Virtual'},
|
||||
]
|
||||
|
||||
agenda.category = category
|
||||
|
@ -38,7 +42,7 @@ def test_statistics_list(app, user):
|
|||
{'id': 'foo-bar', 'label': 'Foo bar'},
|
||||
],
|
||||
],
|
||||
['Misc', [{'id': 'bar-foo', 'label': 'Bar foo'}]],
|
||||
['Misc', [{'id': 'bar-foo', 'label': 'Bar foo'}, {'id': 'virtual', 'label': 'Virtual'}]],
|
||||
]
|
||||
|
||||
agenda2.category = category2
|
||||
|
@ -62,6 +66,7 @@ def test_statistics_list(app, user):
|
|||
{'id': 'bar-foo', 'label': 'Bar foo'},
|
||||
],
|
||||
],
|
||||
['Misc', [{'id': 'virtual', 'label': 'Virtual'}]],
|
||||
]
|
||||
|
||||
|
||||
|
@ -216,3 +221,84 @@ def test_statistics_bookings_subfilters_list(app, user):
|
|||
|
||||
resp = app.get(url + '?agenda=category:xxx', status=400)
|
||||
assert resp.json['err_desc'] == 'No agendas found.'
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2020-10-01 14:00')
|
||||
def test_statistics_bookings_virtual(app, user):
|
||||
agenda_foo = Agenda.objects.create(label='Agenda Foo', kind='meetings')
|
||||
mt = MeetingType.objects.create(agenda=agenda_foo, label='Meeting Type', duration=30)
|
||||
desk = Desk.objects.create(agenda=agenda_foo, label='Desk 1')
|
||||
|
||||
# 3 bookings on 01/10
|
||||
for i in range(3):
|
||||
start_datetime = now() + datetime.timedelta(hours=i)
|
||||
event = Event.objects.create(
|
||||
agenda=agenda_foo, meeting_type=mt, places=1, start_datetime=start_datetime, desk=desk
|
||||
)
|
||||
Booking.objects.create(event=event)
|
||||
|
||||
# 1 booking on 02/10
|
||||
start_datetime = now() + datetime.timedelta(days=1)
|
||||
event = Event.objects.create(
|
||||
agenda=agenda_foo, meeting_type=mt, places=1, start_datetime=start_datetime, desk=desk
|
||||
)
|
||||
real_booking = Booking.objects.create(event=event)
|
||||
|
||||
agenda_bar = Agenda.objects.create(label='Agenda Bar', kind='meetings')
|
||||
mt = MeetingType.objects.create(agenda=agenda_bar, label='Meeting Type', duration=30)
|
||||
desk = Desk.objects.create(agenda=agenda_bar, label='Desk 1')
|
||||
|
||||
# 1 booking on 02/10
|
||||
start_datetime = now() + datetime.timedelta(days=1, hours=1)
|
||||
event = Event.objects.create(
|
||||
agenda=agenda_bar, meeting_type=mt, places=1, start_datetime=start_datetime, desk=desk
|
||||
)
|
||||
Booking.objects.create(event=event)
|
||||
|
||||
# 2 bookings on 03/10
|
||||
for i in range(2):
|
||||
start_datetime = now() + datetime.timedelta(days=2, hours=i)
|
||||
event = Event.objects.create(
|
||||
agenda=agenda_bar, meeting_type=mt, places=1, start_datetime=start_datetime, desk=desk
|
||||
)
|
||||
Booking.objects.create(event=event)
|
||||
|
||||
virtual_agenda = Agenda.objects.create(slug='virtual_agenda', kind='virtual')
|
||||
VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=agenda_foo)
|
||||
VirtualMember.objects.create(virtual_agenda=virtual_agenda, real_agenda=agenda_bar)
|
||||
|
||||
# normal booking on 01/10
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
event = Event.objects.create(start_datetime=now(), places=5, agenda=agenda)
|
||||
Booking.objects.create(event=event)
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
resp = app.get('/api/statistics/')
|
||||
url = [x for x in resp.json['data'] if x['id'] == 'bookings_count'][0]['url']
|
||||
|
||||
resp = app.get(url + '?time_interval=day')
|
||||
assert resp.json['data']['x_labels'] == ['2020-10-01', '2020-10-02', '2020-10-03']
|
||||
assert resp.json['data']['series'][0]['data'] == [4, 2, 2]
|
||||
|
||||
resp = app.get(url + '?time_interval=day&agenda=virtual_agenda')
|
||||
assert resp.json['data']['x_labels'] == ['2020-10-01', '2020-10-02', '2020-10-03']
|
||||
assert resp.json['data']['series'][0]['data'] == [3, 2, 2]
|
||||
|
||||
# filter on category containing both real and virtual
|
||||
category = Category.objects.create(label='Category A')
|
||||
virtual_agenda.category = category
|
||||
virtual_agenda.save()
|
||||
agenda_foo.category = category
|
||||
agenda_foo.save()
|
||||
|
||||
resp = app.get(url + '?time_interval=day&agenda=category:category-a')
|
||||
assert resp.json['data']['x_labels'] == ['2020-10-01', '2020-10-02', '2020-10-03']
|
||||
assert resp.json['data']['series'][0]['data'] == [3, 2, 2]
|
||||
|
||||
# check subfilters from real agendas are found
|
||||
real_booking.extra_data = {'menu': 'vegetables'}
|
||||
real_booking.save()
|
||||
|
||||
resp = app.get(url + '?agenda=virtual_agenda')
|
||||
assert len(resp.json['data']['subfilters'][0]['options']) == 1
|
||||
assert resp.json['data']['subfilters'][0]['options'][0] == {'id': 'menu', 'label': 'Menu'}
|
||||
|
|
|
@ -414,6 +414,20 @@ def test_view_agendas_as_manager(app, manager_user):
|
|||
resp = app.get('/manage/agendas/0/settings', status=404)
|
||||
|
||||
|
||||
def test_view_agendas_kind_display(app, admin_user):
|
||||
Agenda.objects.create(label='Bar Foo', kind='meetings')
|
||||
|
||||
app = login(app)
|
||||
resp = app.get('/manage/')
|
||||
assert [x.text for x in resp.pyquery('span.badge')] == ['Meetings']
|
||||
|
||||
Agenda.objects.create(label='Bar Foo 2', kind='events')
|
||||
Agenda.objects.create(label='Bar Foo 3', kind='events', partial_bookings=True)
|
||||
|
||||
resp = app.get('/manage/')
|
||||
assert [x.text for x in resp.pyquery('span.badge')] == ['Meetings', 'Events', 'Partial bookings']
|
||||
|
||||
|
||||
def test_add_agenda(app, admin_user):
|
||||
app = login(app)
|
||||
resp = app.get('/manage/', status=200)
|
||||
|
@ -942,6 +956,7 @@ def test_agenda_day_view(app, admin_user, manager_user, api_user):
|
|||
|
||||
login(app)
|
||||
app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, 42, today.day), status=404)
|
||||
app.get('/manage/agendas/%s/day/%s/%d/%d/' % (agenda.pk, '0999', today.month, today.day), status=404)
|
||||
resp = app.get('/manage/agendas/%s/day/%s/%s/%s/' % (agenda.pk, today.year, today.month, today.day))
|
||||
assert 'No opening hours this day.' in resp.text # no time pediod
|
||||
|
||||
|
@ -1242,7 +1257,7 @@ def test_agenda_events_day_view(app, admin_user):
|
|||
start_datetime=recurring_start_datetime,
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[recurring_start_datetime.weekday()],
|
||||
recurrence_days=[recurring_start_datetime.isoweekday()],
|
||||
recurrence_end_date=recurring_start_datetime + datetime.timedelta(days=15),
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
@ -1265,7 +1280,7 @@ def test_agenda_events_day_view(app, admin_user):
|
|||
start_datetime=start_datetime,
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=15),
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
@ -1315,7 +1330,7 @@ def test_agenda_events_week_view(app, admin_user):
|
|||
start_datetime=start_datetime,
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=60),
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
@ -1346,7 +1361,7 @@ def test_agenda_events_week_view(app, admin_user):
|
|||
start_datetime=start_datetime,
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=60),
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
@ -1409,7 +1424,7 @@ def test_agenda_events_month_view(app, admin_user):
|
|||
start_datetime=start_datetime,
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=60),
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
@ -1447,7 +1462,7 @@ def test_agenda_events_month_view(app, admin_user):
|
|||
start_datetime=start_datetime,
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=60),
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
@ -1541,6 +1556,7 @@ def test_agenda_open_events_view(app, admin_user, manager_user):
|
|||
start_datetime=start_datetime,
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=30),
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
@ -1621,6 +1637,7 @@ def test_agenda_month_view(app, admin_user, manager_user, api_user):
|
|||
)
|
||||
timeperiod.save()
|
||||
app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.pk, today.year, 42, today.day), status=404)
|
||||
app.get('/manage/agendas/%s/month/%s/%d/%d/' % (agenda.pk, '0999', today.month, today.day), status=404)
|
||||
resp = app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.pk, today.year, today.month, today.day))
|
||||
assert 'No opening hours this month.' not in resp.text
|
||||
assert '<div class="booking' not in resp.text
|
||||
|
@ -2029,6 +2046,7 @@ def test_agenda_week_view(app, admin_user, manager_user, api_user):
|
|||
)
|
||||
timeperiod.save()
|
||||
app.get('/manage/agendas/%s/week/%s/%s/%s/' % (agenda.id, today.year, 72, today.day), status=404)
|
||||
app.get('/manage/agendas/%s/week/%s/%d/%d/' % (agenda.pk, '0999', today.month, today.day), status=404)
|
||||
resp = app.get('/manage/agendas/%s/week/%s/%s/%s/' % (agenda.id, today.year, today.month, today.day))
|
||||
assert 'No opening hours this week.' not in resp.text
|
||||
assert '<div class="booking' not in resp.text
|
||||
|
@ -3239,6 +3257,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')
|
||||
|
@ -3790,6 +3818,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):
|
||||
|
|
|
@ -80,7 +80,7 @@ def test_add_recurring_event(app, admin_user):
|
|||
resp.form['start_datetime_1'] = '17:00'
|
||||
resp.form['places'] = 10
|
||||
resp.form['frequency'] = 'unique' # not a recurring event
|
||||
resp.form['recurrence_days'] = [1]
|
||||
resp.form['recurrence_days'] = [2]
|
||||
resp.form.submit().follow()
|
||||
|
||||
event = Event.objects.get()
|
||||
|
@ -92,7 +92,7 @@ def test_add_recurring_event(app, admin_user):
|
|||
resp.form.submit().follow()
|
||||
|
||||
event = Event.objects.get(primary_event__isnull=True)
|
||||
assert event.recurrence_days == [1]
|
||||
assert event.recurrence_days == [2]
|
||||
assert Event.objects.filter(primary_event=event).count() == 49
|
||||
event.delete()
|
||||
|
||||
|
@ -101,7 +101,7 @@ def test_add_recurring_event(app, admin_user):
|
|||
resp.form.submit().follow()
|
||||
|
||||
event = Event.objects.get(primary_event__isnull=True)
|
||||
assert event.recurrence_days == [1]
|
||||
assert event.recurrence_days == [2]
|
||||
assert Event.objects.filter(primary_event=event).count() == 5
|
||||
|
||||
# add recurring event with end date in a very long time
|
||||
|
@ -375,7 +375,7 @@ def test_edit_recurring_event(settings, app, admin_user, freezer):
|
|||
app = login(app)
|
||||
resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
|
||||
resp.form['frequency'] = 'recurring'
|
||||
resp.form['recurrence_days'] = [localtime().weekday()]
|
||||
resp.form['recurrence_days'] = [localtime().isoweekday()]
|
||||
resp = resp.form.submit()
|
||||
|
||||
# no end date, events are created for the year to come
|
||||
|
@ -426,7 +426,7 @@ def test_edit_recurring_event(settings, app, admin_user, freezer):
|
|||
assert not Event.objects.filter(primary_event=event).exists()
|
||||
|
||||
# same goes with changing slug
|
||||
event.recurrence_days = [1]
|
||||
event.recurrence_days = [2]
|
||||
event.save()
|
||||
event.create_all_recurrences()
|
||||
event_recurrence = Event.objects.get(primary_event=event, start_datetime=event.start_datetime)
|
||||
|
@ -484,7 +484,7 @@ def test_edit_recurring_event_with_end_date(settings, app, admin_user, freezer):
|
|||
freezer.move_to('2021-01-12 12:10')
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events')
|
||||
event = Event.objects.create(
|
||||
start_datetime=now(), places=10, recurrence_days=list(range(7)), agenda=agenda
|
||||
start_datetime=now(), places=10, recurrence_days=list(range(1, 8)), agenda=agenda
|
||||
)
|
||||
|
||||
app = login(app)
|
||||
|
@ -677,7 +677,7 @@ def test_delete_recurring_event(app, admin_user, freezer):
|
|||
start_datetime=start_datetime,
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[start_datetime.weekday()],
|
||||
recurrence_days=[start_datetime.isoweekday()],
|
||||
recurrence_end_date=start_datetime + datetime.timedelta(days=15),
|
||||
)
|
||||
event.create_all_recurrences()
|
||||
|
@ -799,7 +799,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)
|
||||
|
@ -807,11 +808,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 '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')
|
||||
|
@ -821,11 +827,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)
|
||||
|
@ -985,7 +991,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)
|
||||
|
@ -993,7 +999,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()
|
||||
|
@ -1024,8 +1030,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):
|
||||
|
@ -1038,12 +1043,12 @@ def test_import_event_nested_quotes(app, admin_user):
|
|||
't.csv',
|
||||
','.join(
|
||||
[
|
||||
"2016-09-16",
|
||||
"18:00",
|
||||
"10",
|
||||
"5",
|
||||
"éléphant",
|
||||
"elephant",
|
||||
'2016-09-16',
|
||||
'18:00',
|
||||
'10',
|
||||
'5',
|
||||
'éléphant',
|
||||
'elephant',
|
||||
# the multiline description and final dot
|
||||
# and new line after ""éléphants"" are needed to trigger the bug.
|
||||
'''"Animation:
|
||||
|
@ -1189,7 +1194,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)
|
||||
|
@ -1200,6 +1205,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')
|
||||
|
@ -1238,11 +1264,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')
|
||||
|
@ -1568,7 +1602,7 @@ def test_event_check(app, admin_user):
|
|||
resp = resp.click('Check')
|
||||
assert (
|
||||
resp.text.index('Bookings (6/10)')
|
||||
< resp.text.index("User's 01")
|
||||
< resp.text.index('User's 01')
|
||||
< resp.text.index('User 05')
|
||||
< resp.text.index('User 17')
|
||||
< resp.text.index('User 35')
|
||||
|
@ -1630,7 +1664,7 @@ def test_event_check(app, admin_user):
|
|||
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
|
||||
assert (
|
||||
resp.text.index('Bookings (6/10)')
|
||||
< resp.text.index("User's 01")
|
||||
< resp.text.index('User's 01')
|
||||
< resp.text.index('User 05')
|
||||
< resp.text.index('User 12 Cancelled')
|
||||
< resp.text.index('Subscription 14')
|
||||
|
@ -1709,6 +1743,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')
|
||||
|
@ -1935,7 +1977,7 @@ def test_event_check_filters(check_types, app, admin_user):
|
|||
user_external_id='user:4',
|
||||
user_first_name='User',
|
||||
user_last_name='foo-none bar-val2 reason-foo',
|
||||
extra_data={'bar': 'val2'},
|
||||
extra_data={'bar': 'val2', 'foo': None}, # foo is ignored, empty value
|
||||
user_was_present=False,
|
||||
user_check_type_slug='foo-reason',
|
||||
)
|
||||
|
@ -1944,7 +1986,7 @@ def test_event_check_filters(check_types, app, admin_user):
|
|||
user_external_id='user:5',
|
||||
user_first_name='User',
|
||||
user_last_name='foo-none bar-val2 reason-bar',
|
||||
extra_data={'bar': 'val2'},
|
||||
extra_data={'bar': 'val2', 'foo': ''}, # foo is ignored, empty value
|
||||
user_was_present=True,
|
||||
user_check_type_slug='bar-reason',
|
||||
)
|
||||
|
@ -3297,7 +3339,7 @@ def test_duplicate_event(app, admin_user):
|
|||
label='Foo',
|
||||
duration=45,
|
||||
pricing='200€',
|
||||
url="http://example.com",
|
||||
url='http://example.com',
|
||||
description='foo',
|
||||
)
|
||||
|
||||
|
@ -3310,7 +3352,7 @@ def test_duplicate_event(app, admin_user):
|
|||
app = login(app)
|
||||
resp = app.get(f'/manage/agendas/{agenda.pk}/settings')
|
||||
resp = resp.click('Duplicate', href='events')
|
||||
resp.form['label'] = "Bar"
|
||||
resp.form['label'] = 'Bar'
|
||||
resp.form['start_datetime_0'] = str(new_datetime.date())
|
||||
resp.form['start_datetime_1'] = '17:00'
|
||||
|
||||
|
@ -3323,7 +3365,7 @@ def test_duplicate_event(app, admin_user):
|
|||
assert duplicate.start_datetime == new_datetime
|
||||
assert duplicate.agenda == event.agenda
|
||||
|
||||
updated_fields = {"label", "start_datetime"}
|
||||
updated_fields = {'label', 'start_datetime'}
|
||||
identical_fields = {f.name for f in Event._meta.fields} - updated_fields - {'id', 'slug'}
|
||||
for field in identical_fields:
|
||||
assert getattr(duplicate, field) == getattr(event, field)
|
||||
|
@ -3337,7 +3379,7 @@ def test_duplicate_event_creates_recurrences(app, admin_user):
|
|||
recurring_event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=event_start,
|
||||
recurrence_days=[0, 1, 2, 3, 4, 5, 6],
|
||||
recurrence_days=[1, 2, 3, 4, 5, 6, 7],
|
||||
recurrence_end_date=event_start + datetime.timedelta(days=6),
|
||||
places=10,
|
||||
label='Foo',
|
||||
|
@ -3348,7 +3390,7 @@ def test_duplicate_event_creates_recurrences(app, admin_user):
|
|||
app = login(app)
|
||||
resp = app.get(f'/manage/agendas/{agenda.pk}/settings')
|
||||
resp = resp.click('Duplicate', href='events')
|
||||
resp.form['label'] = "Bar"
|
||||
resp.form['label'] = 'Bar'
|
||||
resp.form['start_datetime_0'] = str(event_start.date())
|
||||
resp.form['start_datetime_1'] = '17:00'
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ def test_events_timesheet_slots(app, admin_user):
|
|||
start_datetime=start,
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[0, 1],
|
||||
recurrence_days=[1, 2],
|
||||
recurrence_end_date=end,
|
||||
)
|
||||
recurring_event1.create_all_recurrences()
|
||||
|
@ -120,7 +120,7 @@ def test_events_timesheet_slots(app, admin_user):
|
|||
start_datetime=start,
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[1, 2],
|
||||
recurrence_days=[2, 3],
|
||||
recurrence_end_date=end,
|
||||
)
|
||||
recurring_event2.create_all_recurrences()
|
||||
|
@ -457,7 +457,7 @@ def test_events_timesheet_booked(app, admin_user):
|
|||
start_datetime=event_date,
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[1],
|
||||
recurrence_days=[2],
|
||||
recurrence_end_date=event_date + datetime.timedelta(days=1),
|
||||
)
|
||||
recurring_event1.create_all_recurrences()
|
||||
|
@ -467,7 +467,7 @@ def test_events_timesheet_booked(app, admin_user):
|
|||
start_datetime=event_date,
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[1],
|
||||
recurrence_days=[2],
|
||||
recurrence_end_date=event_date + datetime.timedelta(days=1),
|
||||
)
|
||||
recurring_event2.create_all_recurrences()
|
||||
|
@ -791,7 +791,7 @@ def test_events_timesheet_date_display(app, admin_user):
|
|||
start_datetime=make_aware(datetime.datetime(2022, 1, 1, 12, 0)),
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[0],
|
||||
recurrence_days=[1],
|
||||
recurrence_end_date=datetime.date(2022, 4, 1),
|
||||
)
|
||||
recurring_event.create_all_recurrences()
|
||||
|
@ -1002,7 +1002,7 @@ def test_events_timesheet_csv(app, admin_user):
|
|||
start_datetime=make_aware(datetime.datetime(2022, 2, 1, 12, 0)),
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
recurrence_days=[0],
|
||||
recurrence_days=[1],
|
||||
recurrence_end_date=datetime.date(2022, 4, 1),
|
||||
)
|
||||
recurring_event.create_all_recurrences()
|
||||
|
@ -1140,7 +1140,7 @@ def test_event_timesheet_wrong_event(app, admin_user):
|
|||
app = login(app)
|
||||
app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk), status=404)
|
||||
event.cancelled = False
|
||||
event.recurrence_days = [1]
|
||||
event.recurrence_days = [2]
|
||||
event.save()
|
||||
app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk), status=404)
|
||||
|
||||
|
|
|
@ -105,7 +105,7 @@ def test_meetings_agenda_add_time_period_exception(app, admin_user):
|
|||
assert TimePeriodException.objects.count() == 2
|
||||
assert 'Exception 1' in resp.text
|
||||
assert 'Exception 2' not in resp.text
|
||||
resp = resp.click(href="/manage/time-period-exceptions/%d/exception-extract-list" % desk.pk)
|
||||
resp = resp.click(href='/manage/time-period-exceptions/%d/exception-extract-list' % desk.pk)
|
||||
assert 'Exception 1' in resp.text
|
||||
assert 'Exception 2' in resp.text
|
||||
|
||||
|
@ -526,23 +526,23 @@ def test_exception_list(app, admin_user):
|
|||
assert '/manage/time-period-exceptions/%d/edit' % current_exception.pk in resp.text
|
||||
assert '/manage/time-period-exceptions/%d/edit' % future_exception.pk in resp.text
|
||||
|
||||
resp = resp.click(href="/manage/time-period-exceptions/%d/exception-extract-list" % desk.pk)
|
||||
resp = resp.click(href='/manage/time-period-exceptions/%d/exception-extract-list' % desk.pk)
|
||||
assert '/manage/time-period-exceptions/%d/edit' % past_exception.pk not in resp.text
|
||||
assert '/manage/time-period-exceptions/%d/edit' % current_exception.pk in resp.text
|
||||
assert '/manage/time-period-exceptions/%d/edit' % future_exception.pk in resp.text
|
||||
|
||||
resp = resp.click(href="/manage/time-period-exceptions/%d/exception-list" % desk.pk)
|
||||
resp = resp.click(href='/manage/time-period-exceptions/%d/exception-list' % desk.pk)
|
||||
assert '/manage/time-period-exceptions/%d/edit' % past_exception.pk not in resp.text
|
||||
assert '/manage/time-period-exceptions/%d/edit' % current_exception.pk in resp.text
|
||||
assert '/manage/time-period-exceptions/%d/edit' % future_exception.pk in resp.text
|
||||
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
app.get("/manage/time-period-exceptions/%d/exception-list" % desk.pk)
|
||||
app.get('/manage/time-period-exceptions/%d/exception-list' % desk.pk)
|
||||
assert len(ctx.captured_queries) == 6
|
||||
|
||||
desk.import_timeperiod_exceptions_from_settings(enable=True)
|
||||
with CaptureQueriesContext(connection) as ctx:
|
||||
app.get("/manage/time-period-exceptions/%d/exception-list" % desk.pk)
|
||||
app.get('/manage/time-period-exceptions/%d/exception-list' % desk.pk)
|
||||
assert len(ctx.captured_queries) == 6
|
||||
|
||||
# add an unavailability calendar
|
||||
|
@ -568,8 +568,8 @@ def test_exception_list(app, admin_user):
|
|||
unavailability_calendar.desks.add(desk)
|
||||
|
||||
for url in (
|
||||
"/manage/time-period-exceptions/%d/exception-extract-list" % desk.pk,
|
||||
"/manage/time-period-exceptions/%d/exception-list" % desk.pk,
|
||||
'/manage/time-period-exceptions/%d/exception-extract-list' % desk.pk,
|
||||
'/manage/time-period-exceptions/%d/exception-list' % desk.pk,
|
||||
):
|
||||
resp = app.get(url)
|
||||
assert 'Calendar Past Exception' not in resp.text
|
||||
|
@ -588,7 +588,7 @@ def test_agenda_import_time_period_exception_from_ics(app, admin_user):
|
|||
resp = app.get('/manage/agendas/%d/settings' % agenda.pk)
|
||||
assert 'Manage exception sources' in resp.text
|
||||
resp = resp.click('manage exceptions', index=0)
|
||||
assert "To add new exceptions, you can upload a file or specify an address to a remote calendar." in resp
|
||||
assert 'To add new exceptions, you can upload a file or specify an address to a remote calendar.' in resp
|
||||
resp = resp.form.submit(status=200)
|
||||
assert 'Please provide an ICS File or an URL.' in resp.text
|
||||
assert TimePeriodExceptionSource.objects.filter(desk=desk).count() == 0
|
||||
|
@ -614,7 +614,7 @@ PRODID:-//foo.bar//EN
|
|||
END:VCALENDAR"""
|
||||
resp.form['ics_file'] = Upload('exceptions.ics', ics_with_no_events, 'text/calendar')
|
||||
resp = resp.form.submit(status=200)
|
||||
assert "The file doesn't contain any events." in resp.text
|
||||
assert 'The file doesn't contain any events.' in resp.text
|
||||
assert TimePeriodExceptionSource.objects.filter(desk=desk).count() == 0
|
||||
|
||||
ics_with_exceptions = b"""BEGIN:VCALENDAR
|
||||
|
@ -1349,7 +1349,7 @@ def test_recurring_events_manage_exceptions(settings, app, admin_user, freezer):
|
|||
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
|
||||
assert 'Recurrence exceptions' not in resp.text
|
||||
|
||||
event.recurrence_days = list(range(7))
|
||||
event.recurrence_days = list(range(1, 8))
|
||||
event.recurrence_end_date = now() + datetime.timedelta(days=31)
|
||||
event.save()
|
||||
event.create_all_recurrences()
|
||||
|
@ -1390,7 +1390,7 @@ def test_recurring_events_exceptions_report(settings, app, admin_user, freezer):
|
|||
event = Event.objects.create(
|
||||
start_datetime=now(),
|
||||
places=10,
|
||||
recurrence_days=list(range(7)),
|
||||
recurrence_days=list(range(1, 8)),
|
||||
recurrence_end_date=now() + datetime.timedelta(days=30),
|
||||
agenda=agenda,
|
||||
)
|
||||
|
@ -1442,7 +1442,7 @@ def test_unavailability_calendar_import_time_period_exception_from_ics(app, admi
|
|||
resp = app.get('/manage/unavailability-calendar/%d/settings' % calendar.pk)
|
||||
assert 'Manage unavailabilities from ICS' in resp.text
|
||||
resp = resp.click('Manage unavailabilities')
|
||||
assert "To add new exceptions, you can upload a file or specify an address to a remote calendar." in resp
|
||||
assert 'To add new exceptions, you can upload a file or specify an address to a remote calendar.' in resp
|
||||
resp = resp.form.submit(status=200)
|
||||
assert 'Please provide an ICS File or an URL.' in resp.text
|
||||
assert TimePeriodExceptionSource.objects.filter(unavailability_calendar=calendar).count() == 0
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import datetime
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from chrono.agendas.models import Agenda, Booking, Event
|
||||
from chrono.utils.timezone import make_aware
|
||||
from chrono.agendas.models import Agenda, Booking, Event, Subscription
|
||||
from chrono.utils.lingo import CheckType
|
||||
from chrono.utils.timezone import localtime, make_aware, now
|
||||
from tests.utils import login
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
@ -27,7 +29,48 @@ def test_manager_partial_bookings_add_agenda(app, admin_user, settings):
|
|||
assert agenda.default_view == 'day'
|
||||
|
||||
resp = resp.click('Options')
|
||||
assert 'default_view' not in resp.form.fields
|
||||
assert resp.form['default_view'].options == [
|
||||
('', False, '---------'),
|
||||
('day', True, 'Day view'),
|
||||
('month', False, 'Month view'),
|
||||
]
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# 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_manager_partial_bookings_add_event(app, admin_user):
|
||||
|
@ -37,6 +80,7 @@ def test_manager_partial_bookings_add_event(app, admin_user):
|
|||
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
|
||||
resp = resp.click('New Event')
|
||||
assert 'duration' not in resp.form.fields
|
||||
assert 'recurrence_week_interval' not in resp.form.fields
|
||||
|
||||
resp.form['start_datetime_0'] = '2023-02-15'
|
||||
resp.form['start_datetime_1'] = '08:00'
|
||||
|
@ -52,6 +96,18 @@ def test_manager_partial_bookings_add_event(app, admin_user):
|
|||
assert 'duration' not in resp.form.fields
|
||||
assert resp.form['end_time'].value == '18:00'
|
||||
|
||||
resp.form['end_time'] = '17:00'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
|
||||
resp = resp.click('New Event')
|
||||
resp.form['start_datetime_0'] = '2023-02-15'
|
||||
resp.form['start_datetime_1'] = '10:00'
|
||||
resp.form['end_time'] = '12:00'
|
||||
resp.form['places'] = 10
|
||||
resp = resp.form.submit()
|
||||
assert 'There can only be one event per day.' in resp.text
|
||||
|
||||
|
||||
def test_manager_partial_bookings_day_view(app, admin_user, freezer):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
|
||||
|
@ -76,8 +132,8 @@ def test_manager_partial_bookings_day_view(app, admin_user, freezer):
|
|||
event=event,
|
||||
)
|
||||
Booking.objects.create(
|
||||
user_external_id='yyy',
|
||||
user_first_name='Jon',
|
||||
user_external_id='zzz',
|
||||
user_first_name='Bruce',
|
||||
user_last_name='Doe',
|
||||
start_time=datetime.time(12, 00),
|
||||
end_time=datetime.time(14, 00),
|
||||
|
@ -88,24 +144,651 @@ def test_manager_partial_bookings_day_view(app, admin_user, freezer):
|
|||
today = start_datetime.date()
|
||||
resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
assert 'Week' not in resp.text
|
||||
assert 'Month' not in resp.text
|
||||
|
||||
# time range from one hour before event start to one hour after end
|
||||
assert (
|
||||
resp.pyquery('thead th').text()
|
||||
== '7 a.m. 8 a.m. 9 a.m. 10 a.m. 11 a.m. noon 1 p.m. 2 p.m. 3 p.m. 4 p.m. 5 p.m. 6 p.m. 7 p.m.'
|
||||
assert resp.pyquery('.partial-booking--hour')[0].text == '07\u202fh'
|
||||
assert resp.pyquery('.partial-booking--hour')[-1].text == '19\u202fh'
|
||||
assert [int(x.text.replace('\u202fh', '')) for x in resp.pyquery('.partial-booking--hour')] == list(
|
||||
range(7, 20)
|
||||
)
|
||||
|
||||
assert len(resp.pyquery('tbody tr')) == 2
|
||||
assert resp.pyquery('tbody tr th')[0].text == 'Jane Doe'
|
||||
assert resp.pyquery('tbody tr th')[1].text == 'Jon Doe'
|
||||
assert len(resp.pyquery('.partial-booking--registrant')) == 3
|
||||
assert resp.pyquery('.registrant--name')[0].text_content() == 'Bruce Doe'
|
||||
assert resp.pyquery('.registrant--name')[1].text_content() == 'Jane Doe'
|
||||
assert resp.pyquery('.registrant--name')[2].text_content() == 'Jon Doe'
|
||||
|
||||
assert resp.pyquery('tbody tr div.booking')[0].text == '11 a.m. - 1:30 p.m.'
|
||||
assert resp.pyquery('tbody tr div.booking')[0].attrib['style'] == 'left: 400%; width: 250%;'
|
||||
assert resp.pyquery('tbody tr div.booking')[1].text == '8 a.m. - 10 a.m.'
|
||||
assert resp.pyquery('tbody tr div.booking')[1].attrib['style'] == 'left: 100%; width: 200%;'
|
||||
assert resp.pyquery('tbody tr div.booking')[2].text == 'noon - 2 p.m.'
|
||||
assert resp.pyquery('tbody tr div.booking')[2].attrib['style'] == 'left: 500%; width: 200%;'
|
||||
assert resp.pyquery('.registrant--bar')[0].findall('time')[0].text == '12:00'
|
||||
assert resp.pyquery('.registrant--bar')[0].findall('time')[1].text == '14:00'
|
||||
assert resp.pyquery('.registrant--bar')[0].attrib['style'] == 'left: 38.46%; width: 15.38%;'
|
||||
assert resp.pyquery('.registrant--bar')[1].findall('time')[0].text == '11:00'
|
||||
assert resp.pyquery('.registrant--bar')[1].findall('time')[1].text == '13:30'
|
||||
assert resp.pyquery('.registrant--bar')[1].attrib['style'] == 'left: 30.77%; width: 19.23%;'
|
||||
assert resp.pyquery('.registrant--bar')[2].findall('time')[0].text == '08:00'
|
||||
assert resp.pyquery('.registrant--bar')[2].findall('time')[1].text == '10:00'
|
||||
assert resp.pyquery('.registrant--bar')[2].attrib['style'] == 'left: 7.69%; width: 15.38%;'
|
||||
|
||||
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)
|
||||
start_datetime = make_aware(datetime.datetime(2023, 5, 2, 0, 0))
|
||||
Event.objects.create(
|
||||
label='Event', start_datetime=start_datetime, end_time=datetime.time(23, 59), places=10, agenda=agenda
|
||||
)
|
||||
|
||||
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))
|
||||
|
||||
# from 00h to 23h
|
||||
assert [int(x.text.replace('\u202fh', '')) for x in resp.pyquery('.partial-booking--hour')] == list(
|
||||
range(0, 24)
|
||||
)
|
||||
|
||||
|
||||
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'
|
||||
|
||||
|
||||
@mock.patch('chrono.manager.forms.get_agenda_check_types')
|
||||
def test_manager_partial_bookings_check(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
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
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')) == 1
|
||||
assert resp.pyquery('.registrant--bar time')[0].text == '11:00'
|
||||
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')
|
||||
assert 'presence_check_type' not in resp.form.fields
|
||||
assert 'absence_check_type' not in resp.form.fields
|
||||
|
||||
assert resp.pyquery('form').attr('data-fill-user_check_start_time') == '11:00'
|
||||
assert resp.pyquery('form').attr('data-fill-user_check_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'
|
||||
|
||||
resp.form['user_check_start_time'] = '11:01'
|
||||
resp.form['user_check_end_time'] = '11:00'
|
||||
resp.form['user_was_present'] = 'True'
|
||||
resp = resp.form.submit()
|
||||
|
||||
assert 'Arrival must be before departure.' in resp.text
|
||||
|
||||
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 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()
|
||||
|
||||
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')
|
||||
assert resp.form['presence_check_type'].options == [
|
||||
('', True, '---------'),
|
||||
('bar-reason', False, 'Bar reason'),
|
||||
('baz-reason', False, 'Baz reason'),
|
||||
]
|
||||
assert resp.form['absence_check_type'].options == [
|
||||
('', True, '---------'),
|
||||
('foo-reason', False, 'Foo reason'),
|
||||
]
|
||||
resp.form['presence_check_type'] = 'bar-reason'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
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')
|
||||
assert resp.form['presence_check_type'].value == 'bar-reason'
|
||||
|
||||
resp.form['user_was_present'] = 'False'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
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.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)
|
||||
|
||||
# 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_check_future_events(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 = 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,
|
||||
)
|
||||
|
||||
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))
|
||||
|
||||
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 = 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)
|
||||
|
||||
agenda.enable_check_for_future_events = True
|
||||
agenda.save()
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
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['user_was_present'].options == [
|
||||
('', True, None),
|
||||
('True', False, None),
|
||||
] # no 'False' option
|
||||
assert not Booking.objects.exists()
|
||||
|
||||
resp.form['user_check_start_time'] = '10:00'
|
||||
resp.form['user_check_end_time'] = '16:00'
|
||||
resp.form['user_was_present'] = 'True'
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
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'
|
||||
|
||||
booking = Booking.objects.get()
|
||||
assert booking.user_external_id == 'xxx'
|
||||
assert booking.user_first_name == 'Jane'
|
||||
assert booking.user_last_name == 'Doe'
|
||||
|
||||
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['user_was_present'].options == [
|
||||
('', False, None),
|
||||
('True', True, None),
|
||||
] # no 'False' option
|
||||
|
||||
resp.form['user_was_present'] = ''
|
||||
resp = resp.form.submit().follow()
|
||||
|
||||
assert len(resp.pyquery('.registrant--bar')) == 0
|
||||
|
||||
|
||||
@mock.patch('chrono.manager.forms.get_agenda_check_types')
|
||||
def test_manager_partial_bookings_check_filters(check_types, app, admin_user):
|
||||
check_types.return_value = [
|
||||
CheckType(slug='foo-reason', label='Foo reason', kind='absence'),
|
||||
CheckType(slug='bar-reason', label='Bar reason', kind='presence'),
|
||||
]
|
||||
agenda = Agenda.objects.create(
|
||||
label='Foo bar',
|
||||
kind='events',
|
||||
partial_bookings=True,
|
||||
booking_check_filters='menu',
|
||||
)
|
||||
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='user:1',
|
||||
user_first_name='User',
|
||||
user_last_name='Not Checked',
|
||||
start_time=datetime.time(11, 00),
|
||||
end_time=datetime.time(13, 30),
|
||||
event=event,
|
||||
)
|
||||
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(
|
||||
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',
|
||||
)
|
||||
Subscription.objects.create(
|
||||
agenda=agenda,
|
||||
user_external_id='user:1',
|
||||
user_first_name='Subscription',
|
||||
user_last_name='Present Vegan',
|
||||
date_start=event.start_datetime,
|
||||
date_end=event.start_datetime + datetime.timedelta(days=1),
|
||||
)
|
||||
Subscription.objects.create(
|
||||
agenda=agenda,
|
||||
user_external_id='user:4',
|
||||
user_first_name='Subscription',
|
||||
user_last_name='Not Booked',
|
||||
date_start=event.start_datetime,
|
||||
date_end=event.start_datetime + datetime.timedelta(days=1),
|
||||
)
|
||||
|
||||
app = 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 [x.text_content() for x in resp.pyquery('.registrant--name')] == [
|
||||
'User Absent Meat Foo Reason',
|
||||
'Subscription Not Booked',
|
||||
'User Not Checked',
|
||||
'User Present Vegan',
|
||||
]
|
||||
|
||||
# one registrant has not booked, no bar is shown
|
||||
assert len(resp.pyquery('.registrant--bar.booking')) == 3
|
||||
|
||||
resp = app.get(url, params={'booking-status': 'booked'})
|
||||
assert [x.text_content() for x in resp.pyquery('.registrant--name')] == [
|
||||
'User Absent Meat Foo Reason',
|
||||
'User Not Checked',
|
||||
'User Present Vegan',
|
||||
]
|
||||
|
||||
resp = app.get(url, params={'booking-status': 'presence'})
|
||||
assert [x.text_content() for x in resp.pyquery('.registrant--name')] == ['User Present Vegan']
|
||||
|
||||
resp = app.get(url, params={'booking-status': 'presence', 'extra-data-menu': 'meat'})
|
||||
assert [x.text_content() for x in resp.pyquery('.registrant--name')] == []
|
||||
|
||||
resp = app.get(url, params={'extra-data-menu': 'meat'})
|
||||
assert [x.text_content() for x in resp.pyquery('.registrant--name')] == ['User Absent Meat Foo Reason']
|
||||
|
||||
resp = app.get(url, params={'booking-status': 'absence::foo-reason'})
|
||||
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):
|
||||
user_was_present = None
|
||||
if i < 3:
|
||||
user_was_present = True
|
||||
elif i < 7:
|
||||
user_was_present = False
|
||||
Booking.objects.create(
|
||||
event=event,
|
||||
user_was_present=user_was_present,
|
||||
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),
|
||||
)
|
||||
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
|
||||
Booking.objects.filter(user_was_present__isnull=True).update(user_was_present=True)
|
||||
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
|
||||
event.start_datetime = localtime(now()) + datetime.timedelta(days=1)
|
||||
event.save()
|
||||
app.post(
|
||||
'/manage/agendas/%s/events/%s/checked' % (agenda.pk, event.pk),
|
||||
params={'csrfmiddlewaretoken': token},
|
||||
status=404,
|
||||
)
|
||||
|
||||
# not in past, but check for future events is enabled
|
||||
agenda.enable_check_for_future_events = True
|
||||
agenda.save()
|
||||
app.post(
|
||||
'/manage/agendas/%s/events/%s/checked' % (agenda.pk, event.pk),
|
||||
params={'csrfmiddlewaretoken': token},
|
||||
status=302,
|
||||
)
|
||||
|
||||
# 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))
|
||||
event = Event.objects.create(
|
||||
label='Event', start_datetime=start_datetime, end_time=datetime.time(18, 00), places=10, agenda=agenda
|
||||
)
|
||||
event2 = Event.objects.create(
|
||||
label='Event 2',
|
||||
start_datetime=start_datetime + datetime.timedelta(days=2),
|
||||
end_time=datetime.time(18, 00),
|
||||
places=10,
|
||||
agenda=agenda,
|
||||
)
|
||||
for e in (event, event2):
|
||||
Booking.objects.create(
|
||||
user_external_id='user:1',
|
||||
user_first_name='User',
|
||||
user_last_name='Not Checked',
|
||||
start_time=datetime.time(11, 00),
|
||||
end_time=datetime.time(13, 30),
|
||||
event=e,
|
||||
)
|
||||
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(
|
||||
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,
|
||||
)
|
||||
Subscription.objects.create(
|
||||
agenda=agenda,
|
||||
user_external_id='user:1',
|
||||
user_first_name='Subscription',
|
||||
user_last_name='Present',
|
||||
date_start=event.start_datetime,
|
||||
date_end=event.start_datetime + datetime.timedelta(days=1),
|
||||
)
|
||||
Subscription.objects.create(
|
||||
agenda=agenda,
|
||||
user_external_id='user:4',
|
||||
user_first_name='Subscription',
|
||||
user_last_name='Not Booked',
|
||||
date_start=event.start_datetime,
|
||||
date_end=event.start_datetime + datetime.timedelta(days=1),
|
||||
)
|
||||
Subscription.objects.create(
|
||||
agenda=agenda,
|
||||
user_external_id='user:5',
|
||||
user_first_name='Subscription',
|
||||
user_last_name='Next Month',
|
||||
date_start=datetime.date(2023, 6, 1),
|
||||
date_end=datetime.date(2023, 6, 10),
|
||||
)
|
||||
Subscription.objects.create(
|
||||
agenda=agenda,
|
||||
user_external_id='user:6',
|
||||
user_first_name='Subscription',
|
||||
user_last_name='Previous Month',
|
||||
date_start=datetime.date(2023, 4, 20),
|
||||
date_end=datetime.date(2023, 4, 30),
|
||||
)
|
||||
|
||||
app = login(app)
|
||||
today = start_datetime.date()
|
||||
resp = app.get('/manage/agendas/%s/month/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
|
||||
|
||||
assert [int(x.text) for x in resp.pyquery('thead th a')] == list(range(1, 32))
|
||||
|
||||
assert [x.text for x in resp.pyquery('tbody tr th')] == [
|
||||
'User Absent',
|
||||
'Subscription Not Booked',
|
||||
'User Not Checked',
|
||||
'User Present',
|
||||
]
|
||||
|
||||
user_absent_row = resp.pyquery('tbody tr')[0]
|
||||
assert len(resp.pyquery(user_absent_row)('td')) == 31
|
||||
assert len(resp.pyquery(user_absent_row)('td span')) == 1
|
||||
assert len(resp.pyquery(user_absent_row)('td span.booking.absent')) == 1
|
||||
|
||||
subscription_not_booked_row = resp.pyquery('tbody tr')[1]
|
||||
assert len(resp.pyquery(subscription_not_booked_row)('td')) == 31
|
||||
assert len(resp.pyquery(subscription_not_booked_row)('td span')) == 0
|
||||
|
||||
user_not_checked_row = resp.pyquery('tbody tr')[2]
|
||||
assert len(resp.pyquery(user_not_checked_row)('td')) == 31
|
||||
assert len(resp.pyquery(user_not_checked_row)('td span.booking')) == 2
|
||||
|
||||
user_present_row = resp.pyquery('tbody tr')[3]
|
||||
assert len(resp.pyquery(user_present_row)('td')) == 31
|
||||
assert len(resp.pyquery(user_present_row)('td span')) == 1
|
||||
assert len(resp.pyquery(user_present_row)('td span.booking.present')) == 1
|
||||
|
||||
resp = resp.click('Next month')
|
||||
assert [int(x.text) for x in resp.pyquery('thead th a')] == list(range(1, 31))
|
||||
assert [x.text for x in resp.pyquery('tbody tr th')] == ['Subscription Next Month']
|
||||
assert len(resp.pyquery('tbody tr td')) == 30
|
||||
|
|
|
@ -92,6 +92,7 @@ def test_resource_day_view(app, admin_user):
|
|||
|
||||
login(app)
|
||||
app.get('/manage/resource/%s/day/%d/%d/%d/' % (resource.pk, today.year, 42, today.day), status=404)
|
||||
app.get('/manage/resource/%s/day/%s/%d/%d/' % (resource.pk, '0999', today.month, today.day), status=404)
|
||||
resp = app.get('/manage/resource/%s/day/%d/%d/%d/' % (resource.pk, today.year, today.month, today.day))
|
||||
assert 'div class="booking' not in resp.text
|
||||
assert 'No bookings this day.' in resp.text
|
||||
|
@ -252,6 +253,7 @@ def test_resource_week_view(app, admin_user):
|
|||
login(app)
|
||||
today = datetime.date(2018, 11, 10) # fixed day
|
||||
app.get('/manage/resource/%s/week/%s/%s/%s/' % (resource.pk, today.year, 72, today.day), status=404)
|
||||
app.get('/manage/resource/%s/week/%s/%d/%d/' % (resource.pk, '0999', today.month, today.day), status=404)
|
||||
resp = app.get('/manage/resource/%s/week/%s/%s/%s/' % (resource.pk, today.year, today.month, today.day))
|
||||
assert '<div class="booking' not in resp.text
|
||||
assert resp.text.count('<tr') == 9
|
||||
|
@ -593,6 +595,7 @@ def test_resource_month_view(app, admin_user):
|
|||
login(app)
|
||||
today = datetime.date(2018, 11, 10) # fixed day
|
||||
app.get('/manage/resource/%s/month/%s/%s/%s/' % (resource.pk, today.year, 42, today.day), status=404)
|
||||
app.get('/manage/resource/%s/month/%s/%d/%d/' % (resource.pk, '0999', today.month, today.day), status=404)
|
||||
resp = app.get('/manage/resource/%s/month/%s/%s/%s/' % (resource.pk, today.year, today.month, today.day))
|
||||
assert '<div class="booking' not in resp.text
|
||||
first_month_day = today.replace(day=1)
|
||||
|
|
|
@ -43,7 +43,7 @@ def test_shared_custody_agenda_settings_rules(app, admin_user):
|
|||
|
||||
resp = resp.click('Add custody rule')
|
||||
resp.form['guardian'] = father.pk
|
||||
resp.form['days'] = list(range(7))
|
||||
resp.form['days'] = list(range(1, 8))
|
||||
resp.form['weeks'] = 'even'
|
||||
resp = resp.form.submit()
|
||||
assert resp.location.endswith('/manage/shared-custody/%s/settings/' % agenda.pk)
|
||||
|
@ -53,7 +53,7 @@ def test_shared_custody_agenda_settings_rules(app, admin_user):
|
|||
|
||||
resp = resp.click('Add custody rule')
|
||||
resp.form['guardian'] = mother.pk
|
||||
resp.form['days'] = list(range(7))
|
||||
resp.form['days'] = list(range(1, 8))
|
||||
resp.form['weeks'] = 'odd'
|
||||
resp = resp.form.submit().follow()
|
||||
assert 'Custody rules are not complete.' not in resp.text
|
||||
|
@ -61,14 +61,14 @@ def test_shared_custody_agenda_settings_rules(app, admin_user):
|
|||
assert 'Jane Doe, daily, on odd weeks' in resp.text
|
||||
|
||||
resp = resp.click('John Doe, daily, on even weeks')
|
||||
resp.form['days'] = list(range(6))
|
||||
resp.form['days'] = list(range(1, 7))
|
||||
resp = resp.form.submit()
|
||||
assert resp.location.endswith('/manage/shared-custody/%s/settings/' % agenda.pk)
|
||||
resp = resp.follow()
|
||||
assert 'Custody rules are not complete.' in resp.text
|
||||
|
||||
resp = resp.click('John Doe, from Monday to Saturday, on even weeks')
|
||||
resp.form['days'] = [0]
|
||||
resp.form['days'] = [1]
|
||||
resp.form['weeks'] = 'odd'
|
||||
resp = resp.form.submit()
|
||||
assert 'Rule overlaps existing rules.' in resp.text
|
||||
|
@ -100,7 +100,7 @@ def test_shared_custody_agenda_settings_rules_require_days(app, admin_user):
|
|||
resp = resp.form.submit()
|
||||
assert 'This field is required.' in resp.text
|
||||
|
||||
resp.form['days'] = [0]
|
||||
resp.form['days'] = [1]
|
||||
resp.form.submit().follow()
|
||||
assert SharedCustodyRule.objects.count() == 1
|
||||
|
||||
|
@ -163,7 +163,7 @@ def test_shared_custody_agenda_month_view(app, admin_user):
|
|||
agenda = SharedCustodyAgenda.objects.create(
|
||||
first_guardian=father, second_guardian=mother, child=child, date_start=datetime.date(2022, 1, 1)
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(7)), weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(1, 8)), weeks='even')
|
||||
|
||||
app = login(app)
|
||||
resp = app.get('/manage/shared-custody/%s/' % agenda.pk).follow()
|
||||
|
@ -171,7 +171,7 @@ def test_shared_custody_agenda_month_view(app, admin_user):
|
|||
assert 'February 2022' in resp.text
|
||||
assert 'Configuration is not completed yet.' in resp.text
|
||||
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(7)), weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(1, 8)), weeks='odd')
|
||||
resp = app.get('/manage/shared-custody/%s/' % agenda.pk).follow()
|
||||
assert 'Configuration is not completed yet.' not in resp.text
|
||||
|
||||
|
@ -223,7 +223,7 @@ def test_shared_custody_agenda_month_view_dates(app, admin_user):
|
|||
agenda = SharedCustodyAgenda.objects.create(
|
||||
first_guardian=father, second_guardian=mother, child=child, date_start=datetime.date(2022, 1, 1)
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(7)))
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(1, 8)))
|
||||
|
||||
app = login(app)
|
||||
|
||||
|
@ -278,10 +278,10 @@ def test_shared_custody_agenda_month_view_queries(app, admin_user):
|
|||
agenda = SharedCustodyAgenda.objects.create(
|
||||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=[0, 1, 2], weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=[3, 4, 5, 6], weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=[0, 1, 2], weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=[3, 4, 5, 6], weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=[1, 2, 3], weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=[4, 5, 6, 7], weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=[1, 2, 3], weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=[4, 5, 6, 7], weeks='even')
|
||||
|
||||
for i in range(1, 10):
|
||||
SharedCustodyPeriod.objects.create(
|
||||
|
@ -397,8 +397,8 @@ def test_shared_custody_agenda_holiday_rules(app, admin_user):
|
|||
assert [x[2] for x in resp.form['holiday'].options] == ['---------', 'Vacances de Noël']
|
||||
|
||||
# holiday name is shown on month view
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(7)), weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(7)), weeks='odd')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(1, 8)), weeks='even')
|
||||
SharedCustodyRule.objects.create(agenda=agenda, guardian=mother, days=list(range(1, 8)), weeks='odd')
|
||||
|
||||
resp = app.get('/manage/shared-custody/%s/2022/12/' % agenda.pk)
|
||||
assert 'John Doe (Vacances de Noël)' in resp.text
|
||||
|
|
|
@ -12,7 +12,7 @@ DATABASES = {
|
|||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'TEST': {
|
||||
'NAME': 'chrono-test-%s' % os.environ.get("BRANCH_NAME", "").replace('/', '-')[:45],
|
||||
'NAME': 'chrono-test-%s' % os.environ.get('BRANCH_NAME', '').replace('/', '-')[:45],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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"]
|
||||
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
|
||||
|
|
|
@ -51,7 +51,7 @@ DTSTAMP:20170824T082855Z
|
|||
DTSTART:20170831T170800Z
|
||||
DTEND:20170831T203400Z
|
||||
SEQUENCE:1
|
||||
SUMMARY:Événement 1
|
||||
SUMMARY:Évènement 1
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20170824T092855Z
|
||||
|
@ -715,11 +715,11 @@ def test_timeperiodexception_creation_from_ics_without_startdt():
|
|||
if line.startswith('DTSTART:'):
|
||||
continue
|
||||
lines.append(line)
|
||||
ics_sample = ContentFile("\n".join(lines), name='sample.ics')
|
||||
ics_sample = ContentFile('\n'.join(lines), name='sample.ics')
|
||||
source = desk.timeperiodexceptionsource_set.create(ics_filename='sample.ics', ics_file=ics_sample)
|
||||
with pytest.raises(ICSError) as e:
|
||||
source._check_ics_content()
|
||||
assert str(e.value) == 'Event "Événement 1" has no start date.'
|
||||
assert str(e.value) == 'Event "Évènement 1" has no start date.'
|
||||
|
||||
|
||||
def test_timeperiodexception_creation_from_ics_without_enddt():
|
||||
|
@ -731,7 +731,7 @@ def test_timeperiodexception_creation_from_ics_without_enddt():
|
|||
if line.startswith('DTEND:'):
|
||||
continue
|
||||
lines.append(line)
|
||||
ics_sample = ContentFile("\n".join(lines), name='sample.ics')
|
||||
ics_sample = ContentFile('\n'.join(lines), name='sample.ics')
|
||||
source = desk.timeperiodexceptionsource_set.create(ics_filename='sample.ics', ics_file=ics_sample)
|
||||
source.refresh_timeperiod_exceptions_from_ics()
|
||||
for exception in TimePeriodException.objects.filter(desk=desk):
|
||||
|
@ -759,7 +759,7 @@ def test_timeexception_creation_from_ics_with_dates():
|
|||
if line.startswith('RRULE:'):
|
||||
continue
|
||||
lines.append(line)
|
||||
ics_sample = ContentFile("\n".join(lines), name='sample.ics')
|
||||
ics_sample = ContentFile('\n'.join(lines), name='sample.ics')
|
||||
source = desk.timeperiodexceptionsource_set.create(ics_filename='sample.ics', ics_file=ics_sample)
|
||||
source.refresh_timeperiod_exceptions_from_ics()
|
||||
assert TimePeriodException.objects.filter(desk=desk).count() == 2
|
||||
|
@ -800,7 +800,7 @@ def test_timeperiodexception_creation_from_remote_ics(mocked_get):
|
|||
source = desk.timeperiodexceptionsource_set.create(ics_url='http://example.com/sample.ics')
|
||||
source.refresh_timeperiod_exceptions_from_ics()
|
||||
assert TimePeriodException.objects.filter(desk=desk).count() == 2
|
||||
assert 'Événement 1' in [x.label for x in desk.timeperiodexception_set.all()]
|
||||
assert 'Évènement 1' in [x.label for x in desk.timeperiodexception_set.all()]
|
||||
|
||||
mocked_response.text = ICS_SAMPLE_WITH_NO_EVENTS
|
||||
mocked_get.return_value = mocked_response
|
||||
|
@ -820,7 +820,7 @@ def test_timeperiodexception_remote_ics_encoding(mocked_get):
|
|||
source = desk.timeperiodexceptionsource_set.create(ics_url='http://example.com/sample.ics')
|
||||
source.refresh_timeperiod_exceptions_from_ics()
|
||||
assert TimePeriodException.objects.filter(desk=desk).count() == 2
|
||||
assert 'Événement 1' in [x.label for x in desk.timeperiodexception_set.all()]
|
||||
assert 'Évènement 1' in [x.label for x in desk.timeperiodexception_set.all()]
|
||||
|
||||
|
||||
@mock.patch('chrono.agendas.models.requests.get')
|
||||
|
@ -838,7 +838,7 @@ def test_timeperiodexception_creation_from_unreachable_remote_ics(mocked_get):
|
|||
mocked_get.side_effect = mocked_requests_connection_error
|
||||
with pytest.raises(ICSError) as e:
|
||||
source._check_ics_content()
|
||||
assert str(e.value) == "Failed to retrieve remote calendar (http://example.com/sample.ics, unreachable)."
|
||||
assert str(e.value) == 'Failed to retrieve remote calendar (http://example.com/sample.ics, unreachable).'
|
||||
|
||||
|
||||
@mock.patch('chrono.agendas.models.requests.get')
|
||||
|
@ -857,7 +857,7 @@ def test_timeperiodexception_creation_from_forbidden_remote_ics(mocked_get):
|
|||
with pytest.raises(ICSError) as e:
|
||||
source._check_ics_content()
|
||||
assert (
|
||||
str(e.value) == "Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403)."
|
||||
str(e.value) == 'Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403).'
|
||||
)
|
||||
|
||||
|
||||
|
@ -1474,7 +1474,7 @@ def test_desk_duplicate():
|
|||
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
|
||||
unavailability_calendar.desks.add(desk)
|
||||
|
||||
new_desk = desk.duplicate(label="New Desk")
|
||||
new_desk = desk.duplicate(label='New Desk')
|
||||
assert new_desk.pk != desk.pk
|
||||
assert new_desk.label == 'New Desk'
|
||||
assert new_desk.slug == 'new-desk'
|
||||
|
@ -1498,7 +1498,7 @@ def test_desk_duplicate():
|
|||
assert new_desk.unavailability_calendars.get() == unavailability_calendar
|
||||
|
||||
# duplicate again !
|
||||
new_desk = desk.duplicate(label="New Desk")
|
||||
new_desk = desk.duplicate(label='New Desk')
|
||||
assert new_desk.slug == 'new-desk-1'
|
||||
|
||||
|
||||
|
@ -1511,7 +1511,7 @@ def test_desk_duplicate_exception_sources():
|
|||
source.refresh_timeperiod_exceptions_from_ics()
|
||||
assert TimePeriodException.objects.filter(desk=desk).count() == 2
|
||||
|
||||
new_desk = desk.duplicate(label="New Desk")
|
||||
new_desk = desk.duplicate(label='New Desk')
|
||||
new_source = new_desk.timeperiodexceptionsource_set.get(ics_filename='sample.ics')
|
||||
assert new_desk.timeperiodexception_set.count() == 2
|
||||
|
||||
|
@ -1541,14 +1541,14 @@ def test_desk_duplicate_exception_source_from_settings():
|
|||
assert source.enabled
|
||||
exceptions_count = desk.timeperiodexception_set.count()
|
||||
|
||||
new_desk = desk.duplicate(label="New Desk")
|
||||
new_desk = desk.duplicate(label='New Desk')
|
||||
assert new_desk.timeperiodexceptionsource_set.filter(settings_slug='holidays').count() == 1
|
||||
assert new_desk.timeperiodexceptionsource_set.get(settings_slug='holidays').enabled
|
||||
assert new_desk.timeperiodexception_set.count() == exceptions_count
|
||||
|
||||
source.disable()
|
||||
|
||||
new_desk = desk.duplicate(label="New Desk")
|
||||
new_desk = desk.duplicate(label='New Desk')
|
||||
assert not new_desk.timeperiodexceptionsource_set.get(settings_slug='holidays').enabled
|
||||
assert not new_desk.timeperiodexception_set.exists()
|
||||
|
||||
|
@ -1681,7 +1681,7 @@ def test_agenda_events_recurrence_duplicate(freezer):
|
|||
event = Event.objects.create(
|
||||
agenda=orig_agenda,
|
||||
start_datetime=now_,
|
||||
recurrence_days=[now_.weekday()],
|
||||
recurrence_days=[now_.isoweekday()],
|
||||
label='Event',
|
||||
places=10,
|
||||
recurrence_end_date=end,
|
||||
|
@ -2375,7 +2375,7 @@ def test_recurring_events(freezer):
|
|||
event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now(),
|
||||
recurrence_days=[now().weekday()],
|
||||
recurrence_days=[now().isoweekday()],
|
||||
label='Event',
|
||||
places=10,
|
||||
waiting_list_places=10,
|
||||
|
@ -2419,7 +2419,7 @@ def test_recurring_events_dst(freezer, settings):
|
|||
settings.TIME_ZONE = 'Europe/Brussels'
|
||||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||||
event = Event.objects.create(
|
||||
agenda=agenda, start_datetime=now(), recurrence_days=[now().weekday()], places=5
|
||||
agenda=agenda, start_datetime=now(), recurrence_days=[now().isoweekday()], places=5
|
||||
)
|
||||
event.refresh_from_db()
|
||||
dt = localtime()
|
||||
|
@ -2444,7 +2444,7 @@ def test_recurring_events_repetition(freezer):
|
|||
event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now(),
|
||||
recurrence_days=list(range(7)), # everyday
|
||||
recurrence_days=list(range(1, 8)), # everyday
|
||||
places=5,
|
||||
)
|
||||
event.refresh_from_db()
|
||||
|
@ -2460,7 +2460,7 @@ def test_recurring_events_repetition(freezer):
|
|||
for i in range(len(recurrences) - 1):
|
||||
assert recurrences[i].start_datetime + datetime.timedelta(days=1) == recurrences[i + 1].start_datetime
|
||||
|
||||
event.recurrence_days = list(range(5)) # from Monday to Friday
|
||||
event.recurrence_days = list(range(1, 6)) # from Monday to Friday
|
||||
event.save()
|
||||
recurrences = event.get_recurrences(
|
||||
localtime() + datetime.timedelta(days=1), localtime() + datetime.timedelta(days=7)
|
||||
|
@ -2470,7 +2470,7 @@ def test_recurring_events_repetition(freezer):
|
|||
assert recurrences[1].start_datetime == start_datetime + datetime.timedelta(days=5)
|
||||
assert recurrences[-1].start_datetime == start_datetime + datetime.timedelta(days=7)
|
||||
|
||||
event.recurrence_days = [localtime(event.start_datetime).weekday()] # from Monday to Friday
|
||||
event.recurrence_days = [localtime(event.start_datetime).isoweekday()]
|
||||
event.recurrence_week_interval = 2
|
||||
event.save()
|
||||
recurrences = event.get_recurrences(
|
||||
|
@ -2484,7 +2484,7 @@ def test_recurring_events_repetition(freezer):
|
|||
recurrences[i].start_datetime + datetime.timedelta(days=14) == recurrences[i + 1].start_datetime
|
||||
)
|
||||
|
||||
event.recurrence_days = [3] # Tuesday but start_datetime is a Wednesday
|
||||
event.recurrence_days = [4] # Tuesday but start_datetime is a Wednesday
|
||||
event.recurrence_week_interval = 1
|
||||
event.save()
|
||||
recurrences = event.get_recurrences(localtime(), localtime() + datetime.timedelta(days=10))
|
||||
|
@ -2499,7 +2499,7 @@ def test_recurring_events_with_end_date():
|
|||
event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now(),
|
||||
recurrence_days=list(range(7)),
|
||||
recurrence_days=list(range(1, 8)),
|
||||
places=5,
|
||||
recurrence_end_date=(now() + datetime.timedelta(days=5)).date(),
|
||||
)
|
||||
|
@ -2528,7 +2528,7 @@ def test_recurring_events_exceptions(freezer):
|
|||
event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now(),
|
||||
recurrence_days=list(range(7)),
|
||||
recurrence_days=list(range(1, 8)),
|
||||
places=5,
|
||||
)
|
||||
|
||||
|
@ -2593,21 +2593,21 @@ def test_recurring_events_exceptions_update_recurrences(freezer):
|
|||
daily_event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now(),
|
||||
recurrence_days=list(range(7)),
|
||||
recurrence_days=list(range(1, 8)),
|
||||
places=5,
|
||||
recurrence_end_date=datetime.date(year=2021, month=5, day=8),
|
||||
)
|
||||
weekly_event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now(),
|
||||
recurrence_days=[now().weekday()],
|
||||
recurrence_days=[now().isoweekday()],
|
||||
places=5,
|
||||
recurrence_end_date=datetime.date(year=2021, month=6, day=1),
|
||||
)
|
||||
weekly_event_no_end_date = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now() + datetime.timedelta(hours=2),
|
||||
recurrence_days=[now().weekday()],
|
||||
recurrence_days=[now().isoweekday()],
|
||||
places=5,
|
||||
)
|
||||
Event.create_events_recurrences([daily_event, weekly_event, weekly_event_no_end_date])
|
||||
|
@ -2657,7 +2657,7 @@ def test_recurring_events_exceptions_update_recurrences_start_datetime_modified(
|
|||
daily_event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now(),
|
||||
recurrence_days=list(range(7)),
|
||||
recurrence_days=list(range(1, 8)),
|
||||
places=5,
|
||||
recurrence_end_date=datetime.date(year=2021, month=9, day=13),
|
||||
)
|
||||
|
@ -2702,7 +2702,7 @@ def test_recurring_events_update_recurrences_new_event(freezer):
|
|||
daily_event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now(),
|
||||
recurrence_days=list(range(7)),
|
||||
recurrence_days=list(range(1, 8)),
|
||||
places=5,
|
||||
recurrence_end_date=now() + datetime.timedelta(days=7),
|
||||
)
|
||||
|
@ -2713,7 +2713,7 @@ def test_recurring_events_update_recurrences_new_event(freezer):
|
|||
daily_event_no_end_date = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now(),
|
||||
recurrence_days=[1],
|
||||
recurrence_days=[2],
|
||||
recurrence_week_interval=3,
|
||||
places=5,
|
||||
)
|
||||
|
@ -2723,7 +2723,7 @@ def test_recurring_events_update_recurrences_new_event(freezer):
|
|||
daily_event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now().replace(hour=10),
|
||||
recurrence_days=list(range(7)),
|
||||
recurrence_days=list(range(1, 8)),
|
||||
places=5,
|
||||
recurrence_end_date=now() + datetime.timedelta(days=7),
|
||||
)
|
||||
|
@ -2738,7 +2738,7 @@ def test_recurring_events_display(freezer):
|
|||
event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now() + datetime.timedelta(days=1),
|
||||
recurrence_days=list(range(7)),
|
||||
recurrence_days=list(range(1, 8)),
|
||||
places=5,
|
||||
)
|
||||
|
||||
|
@ -2747,19 +2747,19 @@ def test_recurring_events_display(freezer):
|
|||
freezer.move_to('2021-01-07 12:30')
|
||||
assert event.get_recurrence_display() == 'Daily at 1:30 p.m.'
|
||||
|
||||
event.recurrence_days = [1, 2, 3, 4]
|
||||
event.recurrence_days = [2, 3, 4, 5]
|
||||
event.save()
|
||||
assert event.get_recurrence_display() == 'From Tuesday to Friday at 1:30 p.m.'
|
||||
|
||||
event.recurrence_days = [4, 5, 6]
|
||||
event.recurrence_days = [5, 6, 7]
|
||||
event.save()
|
||||
assert event.get_recurrence_display() == 'From Friday to Sunday at 1:30 p.m.'
|
||||
|
||||
event.recurrence_days = [1, 4, 6]
|
||||
event.recurrence_days = [2, 5, 7]
|
||||
event.save()
|
||||
assert event.get_recurrence_display() == 'On Tuesdays, Fridays, Sundays at 1:30 p.m.'
|
||||
|
||||
event.recurrence_days = [0]
|
||||
event.recurrence_days = [1]
|
||||
event.recurrence_week_interval = 2
|
||||
event.save()
|
||||
assert event.get_recurrence_display() == 'On Mondays at 1:30 p.m., once every two weeks'
|
||||
|
@ -2788,7 +2788,7 @@ def test_event_triggered_fields(partial_bookings):
|
|||
cursor.execute("SELECT nextval('agendas_event_id_seq')")
|
||||
row = cursor.fetchone()
|
||||
if row[0] < 2**31:
|
||||
cursor.execute("ALTER SEQUENCE agendas_event_id_seq RESTART WITH %s;" % 2**31)
|
||||
cursor.execute('ALTER SEQUENCE agendas_event_id_seq RESTART WITH %s;' % 2**31)
|
||||
|
||||
agenda = Agenda.objects.create(label='Agenda', kind='events', partial_bookings=partial_bookings)
|
||||
event = Event.objects.create(
|
||||
|
@ -2967,7 +2967,7 @@ def test_recurring_events_create_past_recurrences(freezer):
|
|||
daily_event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now() - datetime.timedelta(days=3),
|
||||
recurrence_days=list(range(7)),
|
||||
recurrence_days=list(range(1, 8)),
|
||||
places=5,
|
||||
recurrence_end_date=now() + datetime.timedelta(days=3),
|
||||
)
|
||||
|
@ -2984,8 +2984,8 @@ def test_shared_custody_agenda():
|
|||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(1, 8)), weeks='even', guardian=father)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(1, 8)), weeks='odd', guardian=mother)
|
||||
|
||||
slots = agenda.get_custody_slots(now().date(), now().date() + datetime.timedelta(days=30))
|
||||
assert [x.date for x in slots] == [now().date() + datetime.timedelta(days=i) for i in range(30)]
|
||||
|
@ -3043,8 +3043,8 @@ def test_shared_custody_agenda_different_periodicity():
|
|||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=[1, 2, 3], guardian=father)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=[0, 4, 5, 6], guardian=mother)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=[2, 3, 4], guardian=father)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=[1, 5, 6, 7], guardian=mother)
|
||||
slots = agenda.get_custody_slots(now().date(), now().date() + datetime.timedelta(days=14))
|
||||
assert [(x.date.strftime('%A %d/%m'), str(x.guardian)) for x in slots] == [
|
||||
('Tuesday 22/02', 'John Doe'),
|
||||
|
@ -3257,22 +3257,22 @@ def test_shared_custody_agenda_rule_label():
|
|||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
|
||||
rule = SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(7)))
|
||||
rule = SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=list(range(1, 8)))
|
||||
assert rule.label == 'daily'
|
||||
|
||||
rule.days = [1, 2, 3, 4]
|
||||
rule.days = [2, 3, 4, 5]
|
||||
rule.save()
|
||||
assert rule.label == 'from Tuesday to Friday'
|
||||
|
||||
rule.days = [4, 5, 6]
|
||||
rule.days = [5, 6, 7]
|
||||
rule.save()
|
||||
assert rule.label == 'from Friday to Sunday'
|
||||
|
||||
rule.days = [1, 4, 6]
|
||||
rule.days = [2, 5, 7]
|
||||
rule.save()
|
||||
assert rule.label == 'on Tuesdays, Fridays, Sundays'
|
||||
|
||||
rule.days = [0]
|
||||
rule.days = [1]
|
||||
rule.weeks = 'even'
|
||||
rule.save()
|
||||
assert rule.label == 'on Mondays, on even weeks'
|
||||
|
@ -3535,8 +3535,8 @@ def test_shared_custody_agenda_holiday_rules_application():
|
|||
group=christmas_holiday,
|
||||
)
|
||||
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(1, 8)), weeks='even', guardian=father)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(1, 8)), weeks='odd', guardian=mother)
|
||||
|
||||
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday)
|
||||
rule.update_or_create_periods()
|
||||
|
@ -3585,8 +3585,8 @@ def test_shared_custody_agenda_update_holiday_rules_command():
|
|||
group=christmas_holiday,
|
||||
)
|
||||
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(1, 8)), weeks='even', guardian=father)
|
||||
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(1, 8)), weeks='odd', guardian=mother)
|
||||
|
||||
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday)
|
||||
rule.update_or_create_periods()
|
||||
|
@ -3783,3 +3783,251 @@ virtual Agenda 30
|
|||
paris('2023-04-04 14:30 30m'),
|
||||
paris('2023-04-06 15:00 30m'),
|
||||
]
|
||||
|
||||
|
||||
def test_agenda_event_overlaps():
|
||||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||||
|
||||
start_datetime = make_aware(datetime.datetime(2023, 5, 1, 14, 00))
|
||||
event_kwargs = {
|
||||
'start_datetime': start_datetime,
|
||||
'recurrence_days': None,
|
||||
'recurrence_end_date': None,
|
||||
}
|
||||
assert agenda.event_overlaps(**event_kwargs) is False
|
||||
|
||||
event = Event.objects.create(start_datetime=start_datetime, agenda=agenda, places=1)
|
||||
|
||||
assert agenda.event_overlaps(**event_kwargs) is True
|
||||
assert agenda.event_overlaps(instance=event, **event_kwargs) is False
|
||||
|
||||
event_kwargs['start_datetime'] = start_datetime + datetime.timedelta(days=1)
|
||||
assert agenda.event_overlaps(**event_kwargs) is False
|
||||
|
||||
event_kwargs['start_datetime'] = start_datetime.replace(month=2)
|
||||
assert agenda.event_overlaps(**event_kwargs) is False
|
||||
|
||||
# recurring event
|
||||
event_kwargs = {
|
||||
'start_datetime': start_datetime,
|
||||
'recurrence_days': [1, 2],
|
||||
'recurrence_end_date': None,
|
||||
}
|
||||
assert agenda.event_overlaps(**event_kwargs) is True
|
||||
|
||||
# same weekday, starts after
|
||||
event_kwargs['start_datetime'] = start_datetime + datetime.timedelta(days=7)
|
||||
assert agenda.event_overlaps(**event_kwargs) is False
|
||||
|
||||
# same weekday, starts before
|
||||
event_kwargs['start_datetime'] = start_datetime - datetime.timedelta(days=7)
|
||||
assert agenda.event_overlaps(**event_kwargs) is True
|
||||
|
||||
# same weekday, starts before
|
||||
event_kwargs['start_datetime'] = start_datetime - datetime.timedelta(days=6)
|
||||
assert agenda.event_overlaps(**event_kwargs) is True
|
||||
|
||||
# different weekday, starts before
|
||||
event_kwargs['recurrence_days'] = [3]
|
||||
assert agenda.event_overlaps(**event_kwargs) is False
|
||||
|
||||
# same weekday, starts before, ends before
|
||||
event_kwargs = {
|
||||
'start_datetime': start_datetime - datetime.timedelta(days=30),
|
||||
'recurrence_days': [1, 2],
|
||||
'recurrence_end_date': start_datetime - datetime.timedelta(days=15),
|
||||
}
|
||||
assert agenda.event_overlaps(**event_kwargs) is False
|
||||
|
||||
# same weekday, starts before, ends after
|
||||
event_kwargs['recurrence_end_date'] = start_datetime + datetime.timedelta(days=15)
|
||||
assert agenda.event_overlaps(**event_kwargs) is True
|
||||
|
||||
|
||||
def test_agenda_event_overlaps_recurring():
|
||||
agenda = Agenda.objects.create(label='Agenda', kind='events')
|
||||
|
||||
event_kwargs = {
|
||||
'start_datetime': make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
|
||||
'recurrence_days': [1, 2],
|
||||
'recurrence_end_date': datetime.date(2023, 6, 1),
|
||||
}
|
||||
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
|
||||
|
||||
# starts before, ends during existing event
|
||||
event_kwargs = {
|
||||
'start_datetime': make_aware(datetime.datetime(2023, 4, 1, 14, 00)),
|
||||
'recurrence_days': [1],
|
||||
'recurrence_end_date': datetime.date(2023, 5, 15),
|
||||
}
|
||||
assert agenda.event_overlaps(**event_kwargs) is True
|
||||
assert agenda.event_overlaps(instance=event, **event_kwargs) is False
|
||||
|
||||
# starts during, ends after existing event
|
||||
event_kwargs = {
|
||||
'start_datetime': make_aware(datetime.datetime(2023, 5, 15, 14, 00)),
|
||||
'recurrence_days': [2],
|
||||
'recurrence_end_date': datetime.date(2023, 6, 15),
|
||||
}
|
||||
assert agenda.event_overlaps(**event_kwargs) is True
|
||||
|
||||
# not the same day
|
||||
event_kwargs['recurrence_days'] = [3, 4]
|
||||
assert agenda.event_overlaps(**event_kwargs) is False
|
||||
|
||||
# starts after
|
||||
event_kwargs = {
|
||||
'start_datetime': make_aware(datetime.datetime(2023, 6, 15, 14, 00)),
|
||||
'recurrence_days': [2],
|
||||
'recurrence_end_date': datetime.date(2023, 7, 15),
|
||||
}
|
||||
assert agenda.event_overlaps(**event_kwargs) is False
|
||||
|
||||
# remove recurrence_end_date from event
|
||||
event.recurrence_end_date = None
|
||||
event.save()
|
||||
assert agenda.event_overlaps(**event_kwargs) is True
|
||||
|
||||
# starts before, ends before
|
||||
event_kwargs = {
|
||||
'start_datetime': make_aware(datetime.datetime(2023, 4, 1, 14, 00)),
|
||||
'recurrence_days': [2],
|
||||
'recurrence_end_date': datetime.date(2023, 4, 20),
|
||||
}
|
||||
assert agenda.event_overlaps(**event_kwargs) is False
|
||||
|
||||
# starts before, not end
|
||||
event_kwargs['recurrence_end_date'] = None
|
||||
assert agenda.event_overlaps(**event_kwargs) is True
|
||||
|
||||
# normal event, before
|
||||
event_kwargs['recurrence_days'] = None
|
||||
assert agenda.event_overlaps(**event_kwargs) is False
|
||||
|
||||
# normal event, after on a recurrence day
|
||||
event_kwargs['start_datetime'] = make_aware(datetime.datetime(2023, 5, 9, 14, 00))
|
||||
assert agenda.event_overlaps(**event_kwargs) is True
|
||||
|
||||
# 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, user_check_start_time=user_check_start_time
|
||||
)
|
||||
assert booking.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, user_check_end_time=user_check_end_time)
|
||||
assert booking.get_computed_end_time() == expected
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
@ -265,7 +268,7 @@ def test_import_export_recurring_event(app, freezer):
|
|||
event = Event.objects.create(
|
||||
agenda=agenda,
|
||||
start_datetime=now(),
|
||||
recurrence_days=[now().weekday()],
|
||||
recurrence_days=[now().isoweekday()],
|
||||
recurrence_week_interval=2,
|
||||
places=10,
|
||||
slug='test',
|
||||
|
@ -284,7 +287,7 @@ def test_import_export_recurring_event(app, freezer):
|
|||
assert Agenda.objects.count() == 1
|
||||
assert Event.objects.count() == 28
|
||||
event = Agenda.objects.get(label='Foo Bar').event_set.filter(primary_event__isnull=True).get()
|
||||
assert event.recurrence_days == [now().weekday()]
|
||||
assert event.recurrence_days == [now().isoweekday()]
|
||||
assert event.recurrence_week_interval == 2
|
||||
|
||||
# importing event with end recurrence date removes recurrences
|
||||
|
@ -516,7 +519,7 @@ def test_import_export_virtual_agenda(app):
|
|||
|
||||
def test_import_export_virtual_agenda_with_included_agenda(app):
|
||||
virtual_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual')
|
||||
foo_agenda = Agenda.objects.create(label='Foo', kind='meetings')
|
||||
foo_agenda = Agenda.objects.create(label='Zoo', kind='meetings')
|
||||
bar_agenda = Agenda.objects.create(label='Bar', kind='meetings')
|
||||
mt1 = MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30)
|
||||
mt2 = MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30)
|
||||
|
@ -534,7 +537,7 @@ def test_import_export_virtual_agenda_with_included_agenda(app):
|
|||
|
||||
virtual_agenda = Agenda.objects.get(label='Virtual Agenda', slug='virtual-agenda', kind='virtual')
|
||||
assert virtual_agenda.real_agendas.count() == 2
|
||||
assert virtual_agenda.real_agendas.filter(label='Foo').count() == 1
|
||||
assert virtual_agenda.real_agendas.filter(label='Zoo').count() == 1
|
||||
assert virtual_agenda.real_agendas.filter(label='Bar').count() == 1
|
||||
|
||||
# add incompatible meetingtype
|
||||
|
@ -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)
|
||||
|
|
|
@ -13,19 +13,19 @@ pytestmark = pytest.mark.django_db
|
|||
|
||||
def check_ignore_reason(event, value):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT _ignore_reason FROM agendas_event WHERE id = %s", [event.pk])
|
||||
cursor.execute('SELECT _ignore_reason FROM agendas_event WHERE id = %s', [event.pk])
|
||||
row = cursor.fetchone()
|
||||
assert row[0] == value
|
||||
|
||||
|
||||
def set_ignore_reason(event, value):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("UPDATE agendas_event SET _ignore_reason = %s WHERE id = %s", [value, event.pk])
|
||||
cursor.execute('UPDATE agendas_event SET _ignore_reason = %s WHERE id = %s', [value, event.pk])
|
||||
|
||||
|
||||
def check_end_datetime(event, value):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT _end_datetime FROM agendas_event WHERE id = %s", [event.pk])
|
||||
cursor.execute('SELECT _end_datetime FROM agendas_event WHERE id = %s', [event.pk])
|
||||
row = cursor.fetchone()
|
||||
assert row[0] == value
|
||||
|
||||
|
@ -342,3 +342,51 @@ def test_translate_holidays_exceptions(transactional_db):
|
|||
assert not desk.timeperiodexception_set.filter(label='New year').exists()
|
||||
assert desk.timeperiodexception_set.filter(label='Toussaint').count() == 1
|
||||
assert desk.timeperiodexception_set.filter(label='Jour de l’An').count() == 1
|
||||
|
||||
|
||||
def test_migration_convert_week_days(transactional_db):
|
||||
app = 'agendas'
|
||||
|
||||
migrate_from = [(app, '0156_update_dow_index')]
|
||||
migrate_to = [(app, '0157_convert_week_days')]
|
||||
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')
|
||||
SharedCustodyRule = old_apps.get_model(app, 'SharedCustodyRule')
|
||||
SharedCustodyAgenda = old_apps.get_model(app, 'SharedCustodyAgenda')
|
||||
Person = old_apps.get_model(app, 'Person')
|
||||
|
||||
agenda = Agenda.objects.create(label='Foo', kind='events')
|
||||
Event.objects.create(recurrence_days=None, start_datetime=now(), places=1, agenda=agenda, slug='none')
|
||||
Event.objects.create(recurrence_days=[], start_datetime=now(), places=1, agenda=agenda, slug='empty')
|
||||
Event.objects.create(recurrence_days=[3], start_datetime=now(), places=1, agenda=agenda, slug='[3]')
|
||||
Event.objects.create(recurrence_days=[0, 6], start_datetime=now(), places=1, agenda=agenda, slug='[0, 6]')
|
||||
Event.objects.create(
|
||||
recurrence_days=[0, 1, 2, 3, 4, 5, 6], start_datetime=now(), places=1, agenda=agenda, slug='all'
|
||||
)
|
||||
|
||||
father = Person.objects.create(user_external_id='father_id', first_name='John', last_name='Doe')
|
||||
mother = Person.objects.create(user_external_id='mother_id', first_name='Jane', last_name='Doe')
|
||||
child = Person.objects.create(user_external_id='xxx', first_name='James', last_name='Doe')
|
||||
agenda = SharedCustodyAgenda.objects.create(
|
||||
first_guardian=father, second_guardian=mother, child=child, date_start=now()
|
||||
)
|
||||
SharedCustodyRule.objects.create(days=[0, 4, 6], weeks='', guardian=father, agenda=agenda)
|
||||
|
||||
executor = MigrationExecutor(connection)
|
||||
executor.migrate(migrate_to)
|
||||
executor.loader.build_graph()
|
||||
|
||||
apps = executor.loader.project_state(migrate_to).apps
|
||||
Event = apps.get_model(app, 'Event')
|
||||
|
||||
assert Event.objects.get(slug='none').recurrence_days is None
|
||||
assert Event.objects.get(slug='empty').recurrence_days == []
|
||||
assert Event.objects.get(slug='[3]').recurrence_days == [4]
|
||||
assert Event.objects.get(slug='[0, 6]').recurrence_days == [1, 7]
|
||||
assert Event.objects.get(slug='all').recurrence_days == [1, 2, 3, 4, 5, 6, 7]
|
||||
|
||||
assert SharedCustodyRule.objects.get().days == [1, 5, 7]
|
||||
|
|
|
@ -182,7 +182,7 @@ DTSTAMP:20170824T082855Z
|
|||
DTSTART:20170831T170800Z
|
||||
DTEND:20170831T203400Z
|
||||
SEQUENCE:1
|
||||
SUMMARY:Événement 1
|
||||
SUMMARY:Évènement 1
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20170824T092855Z
|
||||
|
|
|
@ -32,4 +32,4 @@ def mocked_requests_send(request, **kwargs):
|
|||
@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
|
||||
def test_publik_django_templatetags_integration(mock_send, context, nocache):
|
||||
t = Template('{{ cards|objects:"foo"|count }}')
|
||||
assert t.render(context) == "2"
|
||||
assert t.render(context) == '2'
|
||||
|
|
Loading…
Reference in New Issue