Compare commits

..

77 Commits

Author SHA1 Message Date
Lauréline Guérin d8731b6240
manager: use gadjo select multiple widget for agendas in report (#75417)
gitea/chrono/pipeline/head There was a failure building this commit Details
2023-10-02 15:50:10 +02:00
Lauréline Guérin e414eedc46
manager: report for not checked or not invoiced events (#75417) 2023-10-02 15:50:10 +02:00
Valentin Deniaud 33e53a694a manager: forbid checking arrival after departure for partial bookings (#81619)
gitea/chrono/pipeline/head This commit looks good Details
2023-10-02 10:11:54 +02:00
Valentin Deniaud ec86a9bbcc manager: partial bookings, allow user check without booking (#80369)
gitea/chrono/pipeline/head This commit looks good Details
2023-10-02 10:11:32 +02:00
Lauréline Guérin 28c3641d50
manager: need to be staff to duplicate an agenda (#81583)
gitea/chrono/pipeline/head This commit looks good Details
2023-09-26 16:23:51 +02:00
Frédéric Péters 655ffeb610 translation update
gitea/chrono/pipeline/head This commit looks good Details
2023-09-21 20:12:47 +02:00
Thomas Jund bdce64d56e partial booking manager: change #main-content overflow (#80356)
gitea/chrono/pipeline/head This commit looks good Details
to allow sticky hours list
2023-09-21 12:02:37 +02:00
Thomas Jund e6be5342e6 partial booking manager: move end time at right (#80356) 2023-09-21 11:59:10 +02:00
Lauréline Guérin 60de169359 manager: disable check for partial bookings if check locked (#80983)
gitea/chrono/pipeline/head This commit looks good Details
2023-09-21 10:47:42 +02:00
Lauréline Guérin e231d27751 manager: mark event as checked for partial bookings (#80983) 2023-09-21 10:47:42 +02:00
Valentin Deniaud 42cc548a33 api: allow getting all user bookings as ICS (#80685)
gitea/chrono/pipeline/head This commit looks good Details
2023-09-21 10:34:45 +02:00
Thomas Jund 8b924ef670 manager css: allow multiple partial bookings on the same line (#80050)
gitea/chrono/pipeline/head This commit looks good Details
2023-09-19 17:00:19 +02:00
Valentin Deniaud 7c34e4fe7f manager: allow multiple partial bookings for one user on the same day (#80050) 2023-09-19 16:59:12 +02:00
Valentin Deniaud 11ef5b4bd2 api: allow partial booking in all event fillslot endpoints (#80050) 2023-09-19 16:57:23 +02:00
Valentin Deniaud 9b340a01d6 manager: add button to prefill partial booking check hours (#80045)
gitea/chrono/pipeline/head This commit looks good Details
2023-09-18 15:33:53 +02:00
Frédéric Péters 9a841fc31e translation update
gitea/chrono/pipeline/head This commit looks good Details
2023-09-15 10:44:42 +02:00
Serghei Mihai 5fe881fdb5 manager: don't show booking colours of cancelled bookings (#81110)
gitea/chrono/pipeline/head This commit looks good Details
2023-09-15 09:33:53 +02:00
Lauréline Guérin 93081c6e46
manager: partial bookings, events redirect to day view (#80982)
gitea/chrono/pipeline/head This commit looks good Details
2023-09-15 08:57:21 +02:00
Lauréline Guérin c8d71aa997
api: events check endpoint, return also times and minutes (#80973)
gitea/chrono/pipeline/head Build queued... Details
2023-09-15 08:57:01 +02:00
Lauréline Guérin 2b288340b6
manager: display computed period for partial bookings (#80842)
gitea/chrono/pipeline/head This commit looks good Details
2023-09-15 08:54:09 +02:00
Lauréline Guérin 16e3602391
agendas: methods to compute start and end times from check times (#80842) 2023-09-15 08:54:09 +02:00
Lauréline Guérin df0223abf2
manager: configure invoicing options for partial bookings (#80842) 2023-09-15 08:54:09 +02:00
Lauréline Guérin 0fe3933ed1
manager: fix wording for checked period in day view (#80842) 2023-09-15 08:54:09 +02:00
Lauréline Guérin 0b6ca9d5d2
agendas: fix event_overlaps method with recurrences (#80851)
gitea/chrono/pipeline/head This commit looks good Details
2023-09-08 12:08:55 +02:00
Lauréline Guérin d16b35067e
manager: fix day view for partial bookings and recurring event (#80851) 2023-09-08 12:08:55 +02:00
Lauréline Guérin fbe2deea93
api: add partial_bookings field in agenda details (#81002)
gitea/chrono/pipeline/head This commit looks good Details
2023-09-08 12:01:12 +02:00
Valentin Deniaud 7e946138ac agendas: import/export all exception sources (#80219)
gitea/chrono/pipeline/head This commit looks good Details
2023-09-04 16:55:43 +02:00
Benjamin Dauvergne a68026e839 ants_hub: fix typo in order_by (#80590)
gitea/chrono/pipeline/head This commit looks good Details
2023-08-28 13:15:11 +02:00
Benjamin Dauvergne 5fbbe0e984 ants_hub: allow multiple identifiant_predemande (#80592)
gitea/chrono/pipeline/head This commit looks good Details
2023-08-28 11:30:55 +02:00
Frédéric Péters 0f81147829 translation update
gitea/chrono/pipeline/head This commit looks good Details
2023-08-17 21:46:35 +02:00
Valentin Deniaud 7859f0558e manager: use proper widget for agenda minimal booking time (#75884)
gitea/chrono/pipeline/head This commit looks good Details
2023-08-17 09:52:46 +02:00
Valentin Deniaud e2d70795b1 tests: add missing ordering in test_recurring_events (#80402)
gitea/chrono/pipeline/head This commit looks good Details
2023-08-16 14:24:22 +02:00
Valentin Deniaud 84463c84bf api: remove legacy fillslots views (#80352)
gitea/chrono/pipeline/head This commit looks good Details
2023-08-16 11:13:38 +02:00
Valentin Deniaud 8127fbff66 manager: report all errors at once in CSV import (#70523)
gitea/chrono/pipeline/head This commit looks good Details
2023-08-16 09:46:26 +02:00
Valentin Deniaud b9c02c20bd manager: split methods of CSV import (#70523) 2023-08-16 09:46:26 +02:00
Valentin Deniaud 60f31525ee api: allow changing bookings from date in recurring fillslots (#78921)
gitea/chrono/pipeline/head This commit looks good Details
2023-08-16 09:46:01 +02:00
Valentin Deniaud f371341d7d api: allow weekday name in recurring event display template (#80042)
gitea/chrono/pipeline/head Build queued... Details
2023-08-03 17:42:17 +02:00
Frédéric Péters 5f14f2a47b general: add a timestamp to static URLs, to avoid caching issues (#80227)
gitea/chrono/pipeline/head This commit looks good Details
2023-08-03 09:23:09 +02:00
Valentin Deniaud b0f8af1dea misc: update git-blame-ignore-revs to ignore quote changes (#79866)
gitea/chrono/pipeline/head This commit looks good Details
2023-08-02 12:18:24 +02:00
Valentin Deniaud b71dc670c7 misc: apply double-quote-string-fixer (#79866) 2023-08-02 12:17:51 +02:00
Valentin Deniaud ebe3b7eb10 misc: add pre commit hook to force single quotes (#79866) 2023-08-02 12:17:51 +02:00
Valentin Deniaud 3d576b48ee api: return virtual agenda booking count in statistics (#79355)
gitea/chrono/pipeline/head This commit looks good Details
2023-08-02 12:17:40 +02:00
Valentin Deniaud 95618bd475 manager: fix event index when CSV import file has header (#79845)
gitea/chrono/pipeline/head This commit looks good Details
2023-08-02 12:08:04 +02:00
Valentin Deniaud c1dd25d2c7 manager: hide empty check type field for partial bookings (#80048)
gitea/chrono/pipeline/head This commit looks good Details
2023-07-31 10:04:36 +02:00
Valentin Deniaud a70045ea5b manager: allow more precise time check for partial bookings (#80046)
gitea/chrono/pipeline/head Build queued... Details
2023-07-31 10:04:10 +02:00
Valentin Deniaud 6b964d708b manager: display partial booking agenda badge on homepage (#80041)
gitea/chrono/pipeline/head This commit looks good Details
2023-07-27 14:57:15 +02:00
Thomas NOËL 7359d50232 debian: remove memory-report from uwsgi default configuration (#79890)
gitea/chrono/pipeline/head This commit looks good Details
2023-07-20 17:56:25 +02:00
Valentin Deniaud 7dbf299eda translation update
gitea/chrono/pipeline/head This commit looks good Details
2023-07-18 18:17:00 +02:00
Valentin Deniaud d1597d7ab3 manager: add partial bookings month view (#79654)
gitea/chrono/pipeline/head This commit looks good Details
2023-07-18 09:47:55 +02:00
Valentin Deniaud 0cc06d2047 manager: respect enable_check_for_future_events in partial bookings (#79642)
gitea/chrono/pipeline/head This commit looks good Details
2023-07-18 09:44:31 +02:00
Valentin Deniaud 0146309c4f manager: include booking check filters in partial bookings view (#79623)
gitea/chrono/pipeline/head Build queued... Details
2023-07-18 09:43:24 +02:00
Valentin Deniaud bfea238c08 manager: move event checks code to mixin (#79623) 2023-07-18 09:43:24 +02:00
Valentin Deniaud 46e60b37a1 manager: allow booking check in partial bookings agendas (#78081)
gitea/chrono/pipeline/head This commit looks good Details
2023-07-18 09:42:00 +02:00
Valentin Deniaud beb31a38ca manager: handle only one partial booking by user (#78081) 2023-07-18 09:42:00 +02:00
Frédéric Péters e6b5ace001 tox: limit weasyprint (#76965)
gitea/chrono/pipeline/head This commit looks good Details
2023-07-14 17:13:47 +02:00
Frédéric Péters 0835bb633d build: remove weasyprint limit (#76965) 2023-07-14 17:13:41 +02:00
Valentin Deniaud 6b79f58bd5 tests: fix recurring event test (#78084)
gitea/chrono/pipeline/head This commit looks good Details
2023-07-04 13:41:09 +02:00
Valentin Deniaud 152110888c api: allow updating partial bookings in recurring events fillslots (#78084)
gitea/chrono/pipeline/head There was a failure building this commit Details
2023-07-04 13:29:45 +02:00
Valentin Deniaud f28cd4d104 api: factorize booking filter in recurring events fillslots (#78084) 2023-07-04 13:29:45 +02:00
Thomas Jund 00ad7b0747 manager: update partial bookings html & css (#78728)
gitea/chrono/pipeline/head This commit looks good Details
2023-07-03 17:29:18 +02:00
Valentin Deniaud b615c57bad translation update
gitea/chrono/pipeline/head This commit looks good Details
2023-07-03 16:31:43 +02:00
Valentin Deniaud 4834743c6d manager: forbid multiple events on same day in partial bookings agenda (#79112)
gitea/chrono/pipeline/head This commit looks good Details
2023-07-03 15:42:20 +02:00
Valentin Deniaud 06af90608f agendas: convert week day in db to iso numbering (#79168)
gitea/chrono/pipeline/head This commit looks good Details
2023-07-03 14:42:32 +02:00
Valentin Deniaud 900300dd05 agendas: use iso week days in events and shared custody (#79168) 2023-07-03 14:42:32 +02:00
Lauréline Guérin 747928c680 api: remove useless code in fillslots views (#79300)
gitea/chrono/pipeline/head This commit looks good Details
2023-07-03 11:45:05 +02:00
Lauréline Guérin 848d014720 api: dispatch fillslots views in other views (#79300) 2023-07-03 11:45:05 +02:00
Valentin Deniaud cdcb663f85 manager: support full day opening in partial bookings agenda (#79171)
gitea/chrono/pipeline/head This commit looks good Details
2023-07-03 09:45:59 +02:00
Valentin Deniaud 9125b7af10 api: return agenda booking_form_url (#72545)
gitea/chrono/pipeline/head Build queued... Details
2023-06-29 15:16:58 +02:00
Valentin Deniaud cce129b8bc api: do not disabled full events when booked in events datetimes (#79120)
gitea/chrono/pipeline/head This commit looks good Details
2023-06-28 09:37:23 +02:00
Lauréline Guérin 90d3c29b72
manager: fix check page with empty values in extra_data filters (#79053)
gitea/chrono/pipeline/head This commit looks good Details
2023-06-26 15:26:06 +02:00
Frédéric Péters 4492026242 misc: apply french orthography rectifications of 1990 (#79004)
gitea/chrono/pipeline/head This commit looks good Details
2023-06-25 09:40:15 +02:00
Frédéric Péters 36b8fd4f9d ci: build deb package for bookworm (#78968)
gitea/chrono/pipeline/head This commit looks good Details
2023-06-23 17:25:27 +02:00
Lauréline Guérin cb9944050e manager: fix views with year < 1000 (#78231)
gitea/chrono/pipeline/head This commit looks good Details
2023-06-23 08:02:37 +02:00
Valentin Deniaud f9ae449f7c manager: fix import of virtual agendas (#78897)
gitea/chrono/pipeline/head This commit looks good Details
2023-06-22 14:41:08 +02:00
Benjamin Dauvergne dae3c05148 translation update (#78588)
gitea/chrono/pipeline/head This commit looks good Details
2023-06-15 19:15:50 +02:00
Benjamin Dauvergne 1a59dcb97c ants-hub: add the case for 5 persons (#78588) 2023-06-15 19:14:55 +02:00
Emmanuel Cazenave 51a888fe81 translation update
gitea/chrono/pipeline/head This commit looks good Details
2023-06-15 15:38:22 +02:00
70 changed files with 4197 additions and 1981 deletions

View File

@ -21,3 +21,5 @@ e07c450d7c8a5f80aafe185c85ebed73fe39d9e7
b38f5f901e1bef556bd95f45bcc041b092b1a617
# misc: bump djhtml version (#75442)
34309253eddc15f17a280656a3ffec072e79731a
# misc: apply double-quote-string-fixer (#79866)
b71dc670c7f90c675edb510643b992aaf69f852a

View File

@ -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:

4
Jenkinsfile vendored
View File

@ -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}"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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':

View File

@ -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',

View File

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

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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%);
}
}

View File

@ -22,5 +22,6 @@
<button>{% trans 'Set Date' %}</button>
</div>
{% endwith %}
{% block extra_date_title %}{% endblock %}
</h2>
{% endblock %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}">

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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" }}&#x202Fh</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 %}

View File

@ -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 %}

View File

@ -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>

View File

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

View File

@ -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

View File

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

View File

@ -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'

View File

@ -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

View File

@ -57,4 +57,4 @@ class EnsureJsonbType(Operation):
pass
def describe(self):
return "Migrate to postgres jsonb type"
return 'Migrate to postgres jsonb type'

View File

@ -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()

2
debian/control vendored
View File

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

View File

@ -1 +0,0 @@
weasyprint python3-weasyprint

1
debian/uwsgi.ini vendored
View File

@ -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

View File

@ -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

View File

@ -167,7 +167,7 @@ setup(
'python-dateutil',
'requests',
'workalendar',
'weasyprint<0.43',
'weasyprint',
],
zip_safe=False,
cmdclass={

View File

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

View File

@ -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': [
{

View File

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

View File

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

View File

@ -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')

View File

@ -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

View File

@ -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'

View File

@ -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'
)

View File

@ -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'
)

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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'}

View File

@ -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):

View File

@ -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&#x27;s 01")
< resp.text.index('User&#x27;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&#x27;s 01")
< resp.text.index('User&#x27;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'

View File

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

View File

@ -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&#x27;t contain any events." in resp.text
assert 'The file doesn&#x27;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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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']

View File

@ -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

View File

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

View File

@ -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 lAn').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]

View File

@ -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

View File

@ -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'

View File

@ -31,6 +31,7 @@ deps =
django-mellon>=1.6.1
pytest-freezegun
django-filter>=2.4,<2.5
weasyprint<52
django32: django>=3.2,<3.3
django32: psycopg2-binary>=2.9
codestyle: pre-commit