chrono/tests/manager/test_exception.py

1520 lines
63 KiB
Python

import datetime
import os
from unittest import mock
import pytest
import requests
from django.core.files.base import ContentFile
from django.core.management import call_command
from django.db import connection
from django.test import override_settings
from django.test.utils import CaptureQueriesContext
from django.urls import reverse
from webtest import Upload
from chrono.agendas.models import (
Agenda,
Booking,
Desk,
Event,
MeetingType,
TimePeriod,
TimePeriodException,
TimePeriodExceptionSource,
UnavailabilityCalendar,
)
from chrono.apps.snapshot.models import AgendaSnapshot
from chrono.manager.forms import TimePeriodExceptionForm
from chrono.utils.timezone import localtime, make_aware, now
from tests.utils import login
pytestmark = pytest.mark.django_db
@override_settings(
EXCEPTIONS_SOURCES={
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
}
)
def test_add_agenda_exceptions_from_settings(app, admin_user):
app = login(app)
resp = app.get('/manage/', status=200)
resp = resp.click('New')
resp.form['label'] = 'Foo bar'
resp.form['kind'] = 'meetings'
resp = resp.form.submit().follow()
assert AgendaSnapshot.objects.count() == 1
agenda = Agenda.objects.get(label='Foo bar')
assert agenda.desk_set.count() == 1
default_desk = agenda.desk_set.first()
assert default_desk.timeperiodexception_set.exists()
assert default_desk.timeperiodexceptionsource_set.count() == 1
source = default_desk.timeperiodexceptionsource_set.first()
assert source.enabled
agenda.desk_simple_management = False
agenda.save()
resp = app.get('/manage/agendas/%s/add-desk' % agenda.pk)
resp.form['label'] = 'Desk A'
resp = resp.form.submit().follow()
assert AgendaSnapshot.objects.count() == 2
desk = Desk.objects.get(slug='desk-a')
assert desk.timeperiodexception_set.exists()
assert desk.timeperiodexceptionsource_set.count() == 1
source = desk.timeperiodexceptionsource_set.first()
assert source.enabled
def test_meetings_agenda_add_time_period_exception(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
desk2 = Desk.objects.create(agenda=agenda, label='Desk B')
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('Add a time period exception', index=0)
today = datetime.datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
tomorrow = make_aware(today + datetime.timedelta(days=1))
dt_format = '%Y-%m-%d %H:%M'
resp.form['label'] = 'Exception 1'
resp.form['start_datetime_0'] = tomorrow.strftime('%Y-%m-%d')
resp.form['start_datetime_1'] = '08:00'
resp.form['end_datetime_0'] = tomorrow.strftime('%Y-%m-%d')
resp.form['end_datetime_1'] = '16:00'
resp = resp.form.submit().follow()
assert TimePeriodException.objects.count() == 1
assert desk2.timeperiodexception_set.exists() is False
time_period_exception = TimePeriodException.objects.first()
assert localtime(time_period_exception.start_datetime).strftime(dt_format) == tomorrow.replace(
hour=8
).strftime(dt_format)
assert localtime(time_period_exception.end_datetime).strftime(dt_format) == tomorrow.replace(
hour=16
).strftime(dt_format)
assert AgendaSnapshot.objects.count() == 1
# add an exception beyond 2 weeks and make sure it isn't listed
resp = resp.click('Add a time period exception', index=0)
future = tomorrow + datetime.timedelta(days=15)
resp.form['label'] = 'Exception 2'
resp.form['start_datetime_0'] = future.strftime('%Y-%m-%d')
resp.form['start_datetime_1'] = '00:00'
resp.form['end_datetime_0'] = future.strftime('%Y-%m-%d')
resp.form['end_datetime_1'] = '16:00'
resp = resp.form.submit().follow()
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)
assert 'Exception 1' in resp.text
assert 'Exception 2' in resp.text
def test_meetings_agenda_add_time_period_exception_third_millennium(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
assert TimePeriodException.objects.filter(desk=desk).count() == 0
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('Add a time period exception', index=0)
resp.form['label'] = 'Exception 1'
resp.form['start_datetime_0'] = '0022-10-24'
resp.form['start_datetime_1'] = '08:00'
resp.form['end_datetime_0'] = '0022-10-24'
resp.form['end_datetime_1'] = '16:00'
resp = resp.form.submit()
assert resp.context['form'].errors['start_datetime'] == ['Year must be after 2000.']
assert resp.context['form'].errors['end_datetime'] == ['Year must be after 2000.']
assert TimePeriodException.objects.filter(desk=desk).count() == 0
def test_meetings_agenda_add_time_period_exception_booking_overlaps(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah')
# different type of overlap
# event.start_datetime <= exception.start_datetime < event.start_datetime + meeting_type.duration
event = Event.objects.create(
agenda=agenda,
places=1,
desk=desk,
meeting_type=meeting_type,
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 30)),
)
Booking.objects.create(event=event)
app = login(app)
resp = app.get('/manage/agendas/%s/desk/%s/add-time-period-exception' % (agenda.pk, desk.pk))
resp.form['label'] = 'Exception'
resp.form['start_datetime_0'] = '2017-05-22'
resp.form['start_datetime_1'] = '10:45'
resp.form['end_datetime_0'] = '2017-05-22'
resp.form['end_datetime_1'] = '17:30'
resp = resp.form.submit().follow()
assert TimePeriodException.objects.count() == 1
assert 'Exception added.' in resp.text
assert 'One or several bookings exists within this time slot.' in resp.text
def test_meetings_agenda_add_time_period_exception_all_desks(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
# add global time exception
# only one desk: no option to apply to all desks
app = login(app)
resp = app.get('/manage/agendas/%s/desk/%s/add-time-period-exception' % (agenda.pk, desk.pk))
assert 'all_desks' not in resp.context['form'].fields
# more than one desk
Desk.objects.create(agenda=agenda, label='Desk B')
agenda2 = Agenda.objects.create(label='Foo bar', kind='meetings')
Desk.objects.create(agenda=agenda2, label='Other Desk') # check exception is not created for this one
event = Event.objects.create(
agenda=agenda, places=1, desk=desk, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 30))
)
Booking.objects.create(event=event)
resp = app.get('/manage/agendas/%s/desk/%s/add-time-period-exception' % (agenda.pk, desk.pk))
resp.form['label'] = 'Exception'
resp.form['start_datetime_0'] = '2017-05-22'
resp.form['start_datetime_1'] = '08:00'
resp.form['end_datetime_0'] = '2017-05-26'
resp.form['end_datetime_1'] = '17:30'
resp.form['all_desks'] = True
resp = resp.form.submit().follow()
assert TimePeriodException.objects.count() == 2
assert 'Exceptions added.' in resp.text
assert 'One or several bookings exists within this time slot.' in resp.text
assert AgendaSnapshot.objects.count() == 1
exception = TimePeriodException.objects.first()
resp = app.get('/manage/time-period-exceptions/%s/edit' % exception.pk)
assert 'all_desks' not in resp.context['form'].fields
def test_meetings_agenda_add_time_period_exception_when_booking_exists(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
MeetingType(agenda=agenda, label='Blah').save()
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
event = Event.objects.create(
agenda=agenda, places=1, desk=desk, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 30))
)
Booking.objects.create(event=event)
login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('Add a time period exception')
resp = resp.form.submit() # submit empty form
# fields should be marked with errors
assert resp.text.count('This field is required.') == 2
# try again with data in fields
resp.form['start_datetime_0'] = '2017-05-22'
resp.form['start_datetime_1'] = '08:00'
resp.form['end_datetime_0'] = '2017-05-26'
resp.form['end_datetime_1'] = '17:30'
resp = resp.form.submit().follow()
assert 'Exception added.' in resp.text
assert 'One or several bookings exists within this time slot.' in resp.text
assert TimePeriodException.objects.count() == 1
def test_meetings_agenda_add_time_period_exception_when_cancelled_booking_exists(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
MeetingType(agenda=agenda, label='Blah').save()
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
event = Event.objects.create(
agenda=agenda, places=1, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 30))
)
Booking.objects.create(
event=event, cancellation_datetime=make_aware(datetime.datetime(2017, 5, 20, 10, 30))
)
login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('Add a time period exception')
resp.form['start_datetime_0'] = '2017-05-22'
resp.form['start_datetime_1'] = '08:00'
resp.form['end_datetime_0'] = '2017-05-26'
resp.form['end_datetime_1'] = '17:30'
resp = resp.form.submit().follow()
assert 'Exception added' in resp.text
assert 'One or several bookings exists within this time slot.' not in resp.text
assert TimePeriodException.objects.count() == 1
def test_meetings_agenda_add_time_period_exception_desk_simple_management(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings', desk_simple_management=True)
desk = Desk.objects.create(agenda=agenda, label='Desk A')
desk2 = Desk.objects.create(agenda=agenda, label='Desk B')
event = Event.objects.create(
agenda=agenda, places=1, desk=desk2, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 30))
)
Booking.objects.create(event=event)
assert agenda.is_available_for_simple_management() is True
login(app)
resp = app.get('/manage/agendas/%s/desk/%s/add-time-period-exception' % (agenda.pk, desk.pk))
assert 'all_desks' not in resp.form.fields
resp.form['label'] = 'Exception'
resp.form['start_datetime_0'] = '2017-05-22'
resp.form['start_datetime_1'] = '8:00'
resp.form['end_datetime_0'] = '2017-05-22'
resp.form['end_datetime_1'] = '17:30'
resp = resp.form.submit().follow()
assert 'Exception added' in resp.text
assert 'One or several bookings exists within this time slot.' in resp.text
assert TimePeriodException.objects.count() == 2
assert agenda.is_available_for_simple_management() is True
assert AgendaSnapshot.objects.count() == 1
def test_meetings_agenda_edit_time_period_exception(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
exception = TimePeriodException.objects.create(
label='Exception',
desk=desk,
start_datetime=make_aware(datetime.datetime(2018, 12, 16, 5, 0)),
end_datetime=make_aware(datetime.datetime(2018, 12, 16, 23, 0)),
)
desk2 = desk.duplicate()
exception2 = desk2.timeperiodexception_set.get()
login(app)
resp = app.get('/manage/time-period-exceptions/%s/edit' % exception.pk)
resp.form['start_datetime_1'] = '8:00'
resp.form.submit()
exception.refresh_from_db()
assert exception.start_datetime == make_aware(datetime.datetime(2018, 12, 16, 8, 0))
exception2.refresh_from_db()
assert exception2.start_datetime == make_aware(datetime.datetime(2018, 12, 16, 5, 0))
assert AgendaSnapshot.objects.count() == 1
def test_meetings_agenda_edit_time_period_exception_overlaps(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
calendar = UnavailabilityCalendar.objects.create(label='foo')
calendar.desks.add(desk)
time_period_exception_desk = TimePeriodException.objects.create(
label='Exception',
desk=desk,
start_datetime=make_aware(datetime.datetime(2018, 12, 16, 5, 0)),
end_datetime=make_aware(datetime.datetime(2018, 12, 16, 23, 0)),
)
time_period_exception_calendar = TimePeriodException.objects.create(
label='Exception',
unavailability_calendar=calendar,
start_datetime=make_aware(datetime.datetime(2018, 12, 16, 5, 0)),
end_datetime=make_aware(datetime.datetime(2018, 12, 16, 23, 0)),
)
login(app)
for time_period_exception in (time_period_exception_desk, time_period_exception_calendar):
event = Event.objects.create(
agenda=agenda,
places=1,
desk=desk,
start_datetime=make_aware(datetime.datetime(2018, 12, 16, 10, 30)),
)
resp = app.get('/manage/time-period-exceptions/%s/edit' % time_period_exception.pk)
resp = resp.form.submit().follow()
assert 'One or several bookings exists within this time slot.' not in resp.text
booking = Booking.objects.create(event=event)
resp = app.get('/manage/time-period-exceptions/%s/edit' % time_period_exception.pk)
resp = resp.form.submit().follow()
assert 'One or several bookings exists within this time slot.' in resp.text
booking.cancel()
resp = app.get('/manage/time-period-exceptions/%s/edit' % time_period_exception.pk)
resp = resp.form.submit().follow()
assert 'One or several bookings exists within this time slot.' not in resp.text
booking.delete()
event.delete()
def test_meetings_agenda_edit_time_period_exception_desk_simple_management(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings', desk_simple_management=True)
desk = Desk.objects.create(agenda=agenda, label='Desk A')
exception = TimePeriodException.objects.create(
label='Exception',
desk=desk,
start_datetime=make_aware(datetime.datetime(2017, 5, 21, 5, 0)),
end_datetime=make_aware(datetime.datetime(2017, 5, 21, 23, 0)),
)
desk2 = desk.duplicate()
event = Event.objects.create(
agenda=agenda, places=1, desk=desk2, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 30))
)
Booking.objects.create(event=event)
exception2 = desk2.timeperiodexception_set.get()
assert agenda.is_available_for_simple_management() is True
login(app)
resp = app.get('/manage/time-period-exceptions/%s/edit' % exception.pk)
resp.form['label'] = 'Exception foo bar'
resp.form['start_datetime_0'] = '2017-05-22'
resp.form['start_datetime_1'] = '8:00'
resp.form['end_datetime_0'] = '2017-05-22'
resp.form['end_datetime_1'] = '17:30'
resp = resp.form.submit().follow()
assert 'One or several bookings exists within this time slot.' in resp.text
exception.refresh_from_db()
exception2.refresh_from_db()
assert exception.label == 'Exception foo bar'
assert exception.start_datetime == make_aware(datetime.datetime(2017, 5, 22, 8, 0))
assert exception.end_datetime == make_aware(datetime.datetime(2017, 5, 22, 17, 30))
assert exception2.label == 'Exception foo bar'
assert exception2.start_datetime == make_aware(datetime.datetime(2017, 5, 22, 8, 0))
assert exception2.end_datetime == make_aware(datetime.datetime(2017, 5, 22, 17, 30))
assert agenda.is_available_for_simple_management() is True
assert AgendaSnapshot.objects.count() == 1
# should not happen: corresponding exception does not exist
exception2.delete()
resp = app.get('/manage/time-period-exceptions/%s/edit' % exception.pk)
resp.form['label'] = 'Exception'
resp.form['start_datetime_0'] = '2017-05-21'
resp.form['start_datetime_1'] = '9:00'
resp.form['end_datetime_0'] = '2017-05-21'
resp.form['end_datetime_1'] = '18:30'
# no error
resp.form.submit()
exception.refresh_from_db()
assert exception.label == 'Exception'
assert exception.start_datetime == make_aware(datetime.datetime(2017, 5, 21, 9, 0))
assert exception.end_datetime == make_aware(datetime.datetime(2017, 5, 21, 18, 30))
def test_meetings_agenda_add_invalid_time_period_exception():
form = TimePeriodExceptionForm(
data={
'start_datetime_0': '2017-05-26',
'start_datetime_1': '17:30',
'end_datetime_0': '2017-05-22',
'end_datetime_1': '08:00',
}
)
assert form.is_valid() is False
assert form.errors['end_datetime'] == ['End datetime must be greater than start datetime.']
# start_datetime is invalid
form = TimePeriodExceptionForm(
data={
'start_datetime_0': '2017-05-26',
'start_datetime_1': 'foo',
'end_datetime_0': '2017-05-22',
'end_datetime_1': '08:00',
}
)
assert form.is_valid() is False
# end_datetime is invalid
form = TimePeriodExceptionForm(
data={
'start_datetime_0': '2017-05-26',
'start_datetime_1': '17:30',
'end_datetime_0': 'bar',
'end_datetime_1': '08:00',
}
)
assert form.is_valid() is False
def test_meetings_agenda_delete_time_period_exception(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
TimePeriodException.objects.create(
label='Exception Foo',
desk=desk,
start_datetime=now() + datetime.timedelta(days=1),
end_datetime=now() + datetime.timedelta(days=2),
)
desk.duplicate()
assert TimePeriodException.objects.count() == 2
login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('Exception Foo', index=0)
resp = resp.click('Delete')
resp = resp.form.submit().follow()
assert TimePeriodException.objects.count() == 1
assert resp.request.url.endswith('/manage/agendas/%d/settings' % agenda.pk)
assert AgendaSnapshot.objects.count() == 1
# stay on exception list
time_period_exception = TimePeriodException.objects.create(
label='Future Exception',
desk=desk,
start_datetime=now() + datetime.timedelta(days=1),
end_datetime=now() + datetime.timedelta(days=2),
)
resp = app.get('/manage/time-period-exceptions/%d/exception-list' % desk.pk)
resp = resp.click(href='/manage/time-period-exceptions/%d/delete' % time_period_exception.pk)
resp = resp.form.submit(
extra_environ={'HTTP_REFERER': str('/manage/time-period-exceptions/%d/exception-list' % desk.pk)}
).follow()
assert resp.request.url.endswith('/manage/time-period-exceptions/%d/exception-list' % desk.pk)
def test_meetings_agenda_delete_time_period_exception_desk_simple_management(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings', desk_simple_management=True)
desk = Desk.objects.create(agenda=agenda, label='Desk A')
exception = TimePeriodException.objects.create(
label='Exception',
desk=desk,
start_datetime=make_aware(datetime.datetime(2017, 5, 21, 5, 0)),
end_datetime=make_aware(datetime.datetime(2017, 5, 21, 23, 0)),
)
desk.duplicate()
assert TimePeriodException.objects.count() == 2
assert agenda.is_available_for_simple_management() is True
login(app)
resp = app.get('/manage/time-period-exceptions/%d/delete' % exception.pk)
resp.form.submit()
assert TimePeriodException.objects.count() == 0
assert agenda.is_available_for_simple_management() is True
assert AgendaSnapshot.objects.count() == 1
# should not happen: corresponding exception does not exist
exception = TimePeriodException.objects.create(
label='Exception',
desk=desk,
start_datetime=make_aware(datetime.datetime(2017, 5, 21, 5, 0)),
end_datetime=make_aware(datetime.datetime(2017, 5, 21, 23, 0)),
)
assert TimePeriodException.objects.count() == 1
resp = app.get('/manage/time-period-exceptions/%d/delete' % exception.pk)
resp.form.submit()
assert TimePeriodException.objects.count() == 0
@override_settings(
EXCEPTIONS_SOURCES={
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
}
)
def test_exception_list(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
MeetingType(agenda=agenda, label='Blah').save()
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
past_exception = TimePeriodException.objects.create(
label='Past Exception',
desk=desk,
start_datetime=now() - datetime.timedelta(days=2),
end_datetime=now() - datetime.timedelta(days=1),
)
current_exception = TimePeriodException.objects.create(
label='Current Exception',
desk=desk,
start_datetime=now() - datetime.timedelta(days=1),
end_datetime=now() + datetime.timedelta(days=1),
)
future_exception = TimePeriodException.objects.create(
label='Future Exception',
desk=desk,
start_datetime=now() + datetime.timedelta(days=1),
end_datetime=now() + datetime.timedelta(days=2),
)
login(app)
resp = app.get('/manage/agendas/%d/settings' % agenda.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-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)
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)
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)
assert len(ctx.captured_queries) == 6
# add an unavailability calendar
unavailability_calendar = UnavailabilityCalendar.objects.create(label='calendar')
past_exception = TimePeriodException.objects.create(
label='Calendar Past Exception',
unavailability_calendar=unavailability_calendar,
start_datetime=now() - datetime.timedelta(days=2),
end_datetime=now() - datetime.timedelta(days=1),
)
current_exception = TimePeriodException.objects.create(
label='Calendar Current Exception',
unavailability_calendar=unavailability_calendar,
start_datetime=now() - datetime.timedelta(days=1),
end_datetime=now() + datetime.timedelta(days=1),
)
future_exception = TimePeriodException.objects.create(
label='Calendar Future Exception',
unavailability_calendar=unavailability_calendar,
start_datetime=now() + datetime.timedelta(days=1),
end_datetime=now() + datetime.timedelta(days=2),
)
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,
):
resp = app.get(url)
assert 'Calendar Past Exception' not in resp.text
assert '/manage/time-period-exceptions/%d/edit' % past_exception.pk not in resp.text
assert 'Calendar Current Exception' in resp.text
assert '/manage/time-period-exceptions/%d/edit' % current_exception.pk not in resp.text
assert 'Calendar Future Exception' in resp.text
assert '/manage/time-period-exceptions/%d/edit' % future_exception.pk not in resp.text
def test_agenda_import_time_period_exception_from_ics(app, admin_user):
agenda = Agenda.objects.create(label='Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Test Desk')
desk.duplicate()
login(app)
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
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
resp.form['ics_file'] = Upload('exceptions.ics', b'invalid content', 'text/calendar')
resp = resp.form.submit(status=200)
assert 'File format is invalid' in resp.text
assert TimePeriodExceptionSource.objects.filter(desk=desk).count() == 0
ics_with_no_start_date = b"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTEND:20180101
SUMMARY:New Year's Eve
END:VEVENT
END:VCALENDAR"""
resp.form['ics_file'] = Upload('exceptions.ics', ics_with_no_start_date, 'text/calendar')
resp = resp.form.submit(status=200)
assert 'Event &quot;New Year&#x27;s Eve&quot; has no start date.' in resp.text
assert TimePeriodExceptionSource.objects.filter(desk=desk).count() == 0
ics_with_no_events = b"""BEGIN:VCALENDAR
VERSION:2.0
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 TimePeriodExceptionSource.objects.filter(desk=desk).count() == 0
ics_with_exceptions = b"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180101
SUMMARY:New Year's Eve
END:VEVENT
END:VCALENDAR"""
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
resp.form['ics_file'] = Upload('exceptions.ics', ics_with_exceptions, 'text/calendar')
resp = resp.form.submit(status=302)
assert TimePeriodException.objects.filter(desk=desk).count() == 1
assert TimePeriodExceptionSource.objects.filter(desk=desk).count() == 1
source = desk.timeperiodexceptionsource_set.get()
exception = desk.timeperiodexception_set.get()
assert exception.source == source
assert source.ics_filename == 'exceptions.ics'
assert 'exceptions.ics' in source.ics_file.name
assert source.ics_url is None
resp = resp.follow()
assert 'Exceptions will be imported in a few minutes.' in resp.text
assert AgendaSnapshot.objects.count() == 1
# Testing with a DTEND and with a DURATION
@pytest.mark.parametrize(
'recurrent_ics',
(
b"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180102
SUMMARY:New Year's Eve
RRULE:FREQ=YEARLY
END:VEVENT
END:VCALENDAR""",
b"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DURATION:P1D
SUMMARY:New Year's Eve
RRULE:FREQ=YEARLY
END:VEVENT
END:VCALENDAR""",
),
)
@pytest.mark.freeze_time('2017-12-01')
def test_agenda_import_time_period_exception_from_ics_recurrent(app, admin_user, recurrent_ics):
agenda = Agenda.objects.create(label='Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Test Desk')
MeetingType(agenda=agenda, label='Foo').save()
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('manage exceptions')
resp.form['ics_file'] = Upload('exceptions.ics', recurrent_ics, 'text/calendar')
resp = resp.form.submit(status=302).follow()
assert TimePeriodException.objects.filter(desk=desk).count() == 2
expt_start = '2018-01-01T00:00:00+0100', '2019-01-01T00:00:00T+0100'
expt_end = '2018-01-02T00:00:00+0100', '2019-01-02T00:00:00T+0100'
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == {
datetime.datetime.fromisoformat(dt) for dt in expt_start
}
assert set(TimePeriodException.objects.values_list('end_datetime', flat=True)) == {
datetime.datetime.fromisoformat(dt) for dt in expt_end
}
@pytest.mark.freeze_time('2017-12-01')
def test_agenda_import_time_period_exception_from_ics_recurrent_invalid_duration(app, admin_user):
# Specific test for invalid/missing duration : in this case
# we set the DTEND to 23:59:59.999999 the same day.
agenda = Agenda.objects.create(label='Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Test Desk')
MeetingType(agenda=agenda, label='Foo').save()
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('manage exceptions')
recurrent_ics = b"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DURATION:invalid duration as 1 day - 1us
SUMMARY:New Year's Eve
RRULE:FREQ=YEARLY
END:VEVENT
END:VCALENDAR"""
resp.form['ics_file'] = Upload('exceptions.ics', recurrent_ics, 'text/calendar')
resp = resp.form.submit(status=302).follow()
assert TimePeriodException.objects.filter(desk=desk).count() == 2
expt_start = '2018-01-01T00:00:00+0100', '2019-01-01T00:00:00T+0100'
expt_end = '2018-01-01T23:59:59.999999+0100', '2019-01-01T23:59:59.999999T+0100'
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == {
datetime.datetime.fromisoformat(dt) for dt in expt_start
}
assert set(TimePeriodException.objects.values_list('end_datetime', flat=True)) == {
datetime.datetime.fromisoformat(dt) for dt in expt_end
}
@mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_with_remote_ics(mocked_get, app, admin_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='New Desk')
desk.duplicate()
login(app)
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
assert 'ics_file' in resp.form.fields
assert 'ics_url' in resp.form.fields
resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock()
mocked_response.text = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180101
SUMMARY:New Year's Eve
END:VEVENT
END:VCALENDAR"""
mocked_get.return_value = mocked_response
resp = resp.form.submit(status=302)
assert TimePeriodException.objects.filter(desk=desk).count() == 1
assert TimePeriodExceptionSource.objects.filter(desk=desk).count() == 1
source = desk.timeperiodexceptionsource_set.get()
exception = desk.timeperiodexception_set.get()
assert exception.source == source
assert source.ics_filename is None
assert source.ics_file.name == ''
assert source.ics_url == 'http://example.com/foo.ics'
assert AgendaSnapshot.objects.count() == 1
@mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_with_remote_ics_no_events(mocked_get, app, admin_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='New Desk')
MeetingType.objects.create(agenda=agenda, label='Bar')
login(app)
resp = app.get('/manage/agendas/%d/settings' % agenda.pk)
resp = resp.click('manage exceptions')
resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock()
mocked_response.text = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180101
SUMMARY:New Year's Eve
END:VEVENT
END:VCALENDAR"""
mocked_get.return_value = mocked_response
resp = resp.form.submit(status=302)
assert TimePeriodException.objects.filter(desk=desk).count() == 1
@mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_from_remote_ics_with_connection_error(
mocked_get, app, admin_user
):
agenda = Agenda.objects.create(label='New Example', kind='meetings')
Desk.objects.create(agenda=agenda, label='New Desk')
MeetingType.objects.create(agenda=agenda, label='Bar')
login(app)
resp = app.get('/manage/agendas/%d/settings' % agenda.pk)
resp = resp.click('manage exceptions')
assert 'ics_file' in resp.form.fields
assert 'ics_url' in resp.form.fields
resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock()
mocked_get.return_value = mocked_response
def mocked_requests_connection_error(*args, **kwargs):
raise requests.exceptions.ConnectionError('unreachable')
mocked_get.side_effect = mocked_requests_connection_error
resp = resp.form.submit(status=200)
assert 'Failed to retrieve remote calendar (http://example.com/foo.ics, unreachable).' in resp.text
@mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_from_forbidden_remote_ics(mocked_get, app, admin_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings')
Desk.objects.create(agenda=agenda, label='New Desk')
MeetingType.objects.create(agenda=agenda, label='Bar')
login(app)
resp = app.get('/manage/agendas/%d/settings' % agenda.pk)
resp = resp.click('manage exceptions')
resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock()
mocked_response.status_code = 403
mocked_get.return_value = mocked_response
def mocked_requests_http_forbidden_error(*args, **kwargs):
raise requests.exceptions.HTTPError(response=mocked_response)
mocked_get.side_effect = mocked_requests_http_forbidden_error
resp = resp.form.submit(status=200)
assert 'Failed to retrieve remote calendar (http://example.com/foo.ics, HTTP error 403).' in resp.text
@mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_from_remote_ics_with_ssl_error(mocked_get, app, admin_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings')
Desk.objects.create(agenda=agenda, label='New Desk')
MeetingType.objects.create(agenda=agenda, label='Bar')
login(app)
resp = app.get('/manage/agendas/%d/settings' % agenda.pk)
resp = resp.click('manage exceptions')
resp.form['ics_url'] = 'https://example.com/foo.ics'
mocked_response = mock.Mock()
mocked_get.return_value = mocked_response
def mocked_requests_http_ssl_error(*args, **kwargs):
raise requests.exceptions.SSLError('SSL error')
mocked_get.side_effect = mocked_requests_http_ssl_error
resp = resp.form.submit(status=200)
assert 'Failed to retrieve remote calendar (https://example.com/foo.ics, SSL error).' in resp.text
@override_settings(TEMPLATE_VARS={'passerelle_url': 'https://passerelle.publik.love/'})
@mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_with_remote_ics_templated_url(mocked_get, app, admin_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='New Desk')
login(app)
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
resp.form['ics_url'] = '{{ passerelle_url }}holidays/test/holidays.ics'
mocked_response = mock.Mock()
mocked_response.text = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180101
SUMMARY:New Year's Eve
END:VEVENT
END:VCALENDAR"""
mocked_get.return_value = mocked_response
resp = resp.form.submit(status=302)
assert TimePeriodException.objects.filter(desk=desk).count() == 1
assert TimePeriodExceptionSource.objects.filter(desk=desk).count() == 1
assert mocked_get.call_args.args[0] == 'https://passerelle.publik.love/holidays/test/holidays.ics'
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
assert (
resp.pyquery('a[title="{{ passerelle_url }}holidays/test/holidays.ics"]').attr('href')
== 'https://passerelle.publik.love/holidays/test/holidays.ics'
)
resp.form['ics_url'] = '{{ unknown }}holidays/test/holidays.ics'
resp = resp.form.submit(status=200)
assert 'Enter a valid URL.' in resp.text
resp.form['ics_url'] = '{{ { }}holidays/test/holidays.ics'
resp = resp.form.submit(status=200)
assert 'syntax error' in resp.text
@mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_url_desk_all_desks(mocked_get, app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
# only one desk: no option to apply to all desks
app = login(app)
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
assert 'all_desks' not in resp.form.fields
# more than one desk
Desk.objects.create(agenda=agenda, label='Desk B')
agenda2 = Agenda.objects.create(label='Foo bar', kind='meetings')
Desk.objects.create(agenda=agenda2, label='Other Desk') # check exceptions are not created for this one
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
resp.form['all_desks'] = True
resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock()
mocked_response.text = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180101
SUMMARY:New Year's Eve
END:VEVENT
END:VCALENDAR"""
mocked_get.return_value = mocked_response
resp = resp.form.submit(status=302)
assert TimePeriodException.objects.count() == 2
assert TimePeriodException.objects.filter(desk__slug='desk-a').exists()
assert TimePeriodException.objects.filter(desk__slug='desk-b').exists()
agenda = Agenda.objects.create(label='New Example', kind='meetings', desk_simple_management=True)
desk = Desk.objects.create(agenda=agenda, label='New Desk')
desk.duplicate()
assert agenda.is_available_for_simple_management() is True
assert AgendaSnapshot.objects.count() == 1
def test_agenda_import_time_period_exception_file_desk_all_desks(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
# only one desk: no option to apply to all desks
app = login(app)
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
assert 'all_desks' not in resp.form.fields
# more than one desk
Desk.objects.create(agenda=agenda, label='Desk B')
agenda2 = Agenda.objects.create(label='Foo bar', kind='meetings')
Desk.objects.create(agenda=agenda2, label='Other Desk') # check exceptions are not created for this one
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
resp.form['all_desks'] = True
ics_exceptions = b"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180101
SUMMARY:New Year's Eve
END:VEVENT
END:VCALENDAR"""
resp.form['ics_file'] = Upload('exceptions.ics', ics_exceptions, 'text/calendar')
resp = resp.form.submit(status=302)
assert TimePeriodException.objects.count() == 2
assert TimePeriodException.objects.filter(desk__slug='desk-a').exists()
assert TimePeriodException.objects.filter(desk__slug='desk-b').exists()
agenda = Agenda.objects.create(label='New Example', kind='meetings', desk_simple_management=True)
desk = Desk.objects.create(agenda=agenda, label='New Desk')
desk.duplicate()
assert agenda.is_available_for_simple_management() is True
assert AgendaSnapshot.objects.count() == 1
@mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_url_desk_simple_management(mocked_get, app, admin_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings', desk_simple_management=True)
desk = Desk.objects.create(agenda=agenda, label='New Desk')
desk.duplicate()
assert agenda.is_available_for_simple_management() is True
login(app)
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
assert 'all_desks' not in resp.form.fields
resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock()
mocked_response.text = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180101
SUMMARY:New Year's Eve
END:VEVENT
END:VCALENDAR"""
mocked_get.return_value = mocked_response
resp = resp.form.submit(status=302)
assert TimePeriodException.objects.count() == 2
assert agenda.is_available_for_simple_management() is True
assert AgendaSnapshot.objects.count() == 1
def test_agenda_import_time_period_exception_file_desk_simple_management(app, admin_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings', desk_simple_management=True)
desk = Desk.objects.create(agenda=agenda, label='New Desk')
desk.duplicate()
assert agenda.is_available_for_simple_management() is True
login(app)
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
assert 'all_desks' not in resp.form.fields
ics_exceptions = b"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180101
SUMMARY:New Year's Eve
END:VEVENT
END:VCALENDAR"""
resp.form['ics_file'] = Upload('exceptions.ics', ics_exceptions, 'text/calendar')
resp = resp.form.submit(status=302)
assert TimePeriodException.objects.count() == 2
assert agenda.is_available_for_simple_management() is True
assert AgendaSnapshot.objects.count() == 1
def test_meetings_agenda_delete_time_period_exception_source(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
source1 = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='https://example.com/test.ics')
TimePeriodException.objects.create(
desk=desk,
source=source1,
start_datetime=now() - datetime.timedelta(days=1),
end_datetime=now() + datetime.timedelta(days=1),
)
source2 = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='https://example.com/test.ics')
TimePeriodException.objects.create(
desk=desk,
source=source2,
start_datetime=now() - datetime.timedelta(days=1),
end_datetime=now() + datetime.timedelta(days=1),
)
desk.duplicate()
assert TimePeriodException.objects.count() == 4
assert TimePeriodExceptionSource.objects.count() == 4
login(app)
resp = app.get('/manage/time-period-exceptions-source/%d/delete' % source2.pk)
resp = resp.form.submit()
assert TimePeriodException.objects.count() == 3
assert TimePeriodExceptionSource.objects.count() == 3
assert source1.timeperiodexception_set.count() == 1
assert TimePeriodExceptionSource.objects.filter(pk=source2.pk).exists() is False
assert AgendaSnapshot.objects.count() == 1
def test_meetings_agenda_delete_time_period_exception_source_desk_simple_management(app, admin_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings', desk_simple_management=True)
desk = Desk.objects.create(agenda=agenda, label='New Desk')
source = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='https://example.com/test.ics')
desk.duplicate()
assert TimePeriodExceptionSource.objects.count() == 2
assert agenda.is_available_for_simple_management() is True
login(app)
resp = app.get('/manage/time-period-exceptions-source/%d/delete' % source.pk)
resp.form.submit()
assert TimePeriodExceptionSource.objects.count() == 0
assert agenda.is_available_for_simple_management() is True
assert AgendaSnapshot.objects.count() == 1
# should not happen: corresponding source does not exist
source = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='https://example.com/test.ics')
assert TimePeriodExceptionSource.objects.count() == 1
resp = app.get('/manage/time-period-exceptions-source/%d/delete' % source.pk)
resp.form.submit()
assert TimePeriodExceptionSource.objects.count() == 0
def test_meetings_agenda_replace_time_period_exception_source(app, admin_user, freezer):
freezer.move_to('2019-12-01')
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
ics_file_content = b"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180101
SUMMARY:New Year's Eve
RRULE:FREQ=YEARLY
END:VEVENT
END:VCALENDAR"""
login(app)
# import a source from a file
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
resp.form['ics_file'] = Upload('exceptions.ics', ics_file_content, 'text/calendar')
resp = resp.form.submit(status=302).follow()
assert TimePeriodException.objects.filter(desk=desk).count() == 2
source = TimePeriodExceptionSource.objects.latest('pk')
assert source.timeperiodexception_set.count() == 2
exceptions = list(source.timeperiodexception_set.order_by('pk'))
old_ics_file_path = source.ics_file.path
assert AgendaSnapshot.objects.count() == 1
desk2 = desk.duplicate()
source2 = desk2.timeperiodexceptionsource_set.get()
exceptions2 = list(source2.timeperiodexception_set.order_by('pk'))
assert TimePeriodException.objects.count() == 4
old_ics_file_path2 = source2.ics_file.path
# replace the source
resp = app.get('/manage/time-period-exceptions-source/%d/replace' % source.pk)
resp.form['ics_newfile'] = Upload('exceptions-bis.ics', ics_file_content, 'text/calendar')
resp = resp.form.submit().follow()
source.refresh_from_db()
assert source.ics_file.path != old_ics_file_path
assert source.ics_filename == 'exceptions-bis.ics'
assert os.path.exists(old_ics_file_path) is False
assert TimePeriodException.objects.count() == 4
assert source.timeperiodexception_set.count() == 2
new_exceptions = list(source.timeperiodexception_set.order_by('pk'))
assert exceptions[0].pk != new_exceptions[0].pk
assert exceptions[1].pk != new_exceptions[1].pk
source2.refresh_from_db()
assert source2.ics_file.path == old_ics_file_path2
assert source2.ics_filename == 'exceptions.ics'
new_exceptions2 = list(source2.timeperiodexception_set.order_by('pk'))
assert exceptions2[0].pk == new_exceptions2[0].pk
assert exceptions2[1].pk == new_exceptions2[1].pk
assert AgendaSnapshot.objects.count() == 2
def test_meetings_agenda_replace_time_period_exception_source_desk_simple_management(app, admin_user):
ics_file_content = b"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180101
SUMMARY:New Year's Eve
END:VEVENT
END:VCALENDAR"""
agenda = Agenda.objects.create(label='Foo bar', kind='meetings', desk_simple_management=True)
desk = Desk.objects.create(agenda=agenda, label='Desk A')
source = TimePeriodExceptionSource.objects.create(
desk=desk,
ics_filename='sample.ics',
ics_file=ContentFile(ics_file_content, name='sample.ics'),
)
desk2 = desk.duplicate()
source2 = desk2.timeperiodexceptionsource_set.get()
assert TimePeriodExceptionSource.objects.count() == 2
assert TimePeriodException.objects.count() == 0 # not imported yet
assert agenda.is_available_for_simple_management() is True
old_ics_file_path = source.ics_file.path
old_ics_file_path2 = source2.ics_file.path
login(app)
resp = app.get('/manage/time-period-exceptions-source/%d/replace' % source.pk)
resp.form['ics_newfile'] = Upload('exceptions-bis.ics', ics_file_content, 'text/calendar')
resp.form.submit()
assert TimePeriodExceptionSource.objects.count() == 2
assert TimePeriodException.objects.count() == 2
assert desk.timeperiodexception_set.count() == 1
assert desk2.timeperiodexception_set.count() == 1
source.refresh_from_db()
assert source.ics_file.path != old_ics_file_path
assert source.ics_filename == 'exceptions-bis.ics'
assert os.path.exists(old_ics_file_path) is False
source2.refresh_from_db()
assert source2.ics_file.path != old_ics_file_path2
assert source2.ics_filename == 'exceptions-bis.ics'
assert os.path.exists(old_ics_file_path2) is False
assert agenda.is_available_for_simple_management() is True
assert AgendaSnapshot.objects.count() == 1
# should not happen: corresponding source does not exist
source2.delete()
resp = app.get('/manage/time-period-exceptions-source/%d/replace' % source.pk)
resp.form['ics_newfile'] = Upload('exceptions-ter.ics', ics_file_content, 'text/calendar')
resp.form.submit()
assert TimePeriodExceptionSource.objects.count() == 1
assert TimePeriodException.objects.count() == 1
assert desk.timeperiodexception_set.count() == 1
assert desk2.timeperiodexception_set.count() == 0
assert desk.timeperiodexceptionsource_set.get().ics_filename == 'exceptions-ter.ics'
@mock.patch('chrono.agendas.models.requests.get')
def test_meetings_agenda_refresh_time_period_exception_source(mocked_get, app, admin_user):
mocked_response = mock.Mock()
mocked_response.text = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180101
SUMMARY:New Year's Eve
END:VEVENT
END:VCALENDAR"""
mocked_get.return_value = mocked_response
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
login(app)
# import a source from an url
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('manage exceptions')
resp.form['ics_url'] = 'http://example.com/foo.ics'
resp = resp.form.submit(status=302).follow()
assert TimePeriodException.objects.filter(desk=desk).count() == 1
source = TimePeriodExceptionSource.objects.latest('pk')
assert source.timeperiodexception_set.count() == 1
exceptions = list(source.timeperiodexception_set.order_by('pk'))
desk2 = desk.duplicate()
source2 = desk2.timeperiodexceptionsource_set.get()
exceptions2 = list(source2.timeperiodexception_set.order_by('pk'))
assert TimePeriodException.objects.count() == 2
# refresh the source
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('manage exceptions', index=0)
resp = resp.click(href='/manage/time-period-exceptions-source/%d/refresh' % source.pk)
assert TimePeriodException.objects.count() == 2
new_exceptions = list(source.timeperiodexception_set.order_by('pk'))
assert exceptions[0].pk != new_exceptions[0].pk
new_exceptions2 = list(source2.timeperiodexception_set.order_by('pk'))
assert exceptions2[0].pk == new_exceptions2[0].pk
@mock.patch('chrono.agendas.models.requests.get')
def test_meetings_agenda_refresh_time_period_exception_source_desk_simple_management(
mocked_get, app, admin_user
):
mocked_response = mock.Mock()
mocked_response.text = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
DTSTART:20180101
DTEND:20180101
SUMMARY:New Year's Eve
END:VEVENT
END:VCALENDAR"""
mocked_get.return_value = mocked_response
agenda = Agenda.objects.create(label='Foo bar', kind='meetings', desk_simple_management=True)
desk = Desk.objects.create(agenda=agenda, label='Desk A')
source = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='https://example.com/test.ics')
desk2 = desk.duplicate()
source2 = desk2.timeperiodexceptionsource_set.get()
assert TimePeriodExceptionSource.objects.count() == 2
assert TimePeriodException.objects.count() == 0 # not imported yet
assert agenda.is_available_for_simple_management() is True
login(app)
app.get('/manage/time-period-exceptions-source/%d/refresh' % source.pk)
assert TimePeriodExceptionSource.objects.count() == 2
assert TimePeriodException.objects.count() == 2
assert source.timeperiodexception_set.count() == 1
assert source2.timeperiodexception_set.count() == 1
assert agenda.is_available_for_simple_management() is True
# should not happen: corresponding source does not exist
source2.delete()
app.get('/manage/time-period-exceptions-source/%d/refresh' % source.pk)
assert TimePeriodExceptionSource.objects.count() == 1
assert TimePeriodException.objects.count() == 1
assert source.timeperiodexception_set.count() == 1
@override_settings(
EXCEPTIONS_SOURCES={
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
}
)
def test_meetings_agenda_time_period_exception_source_from_settings_toggle(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
desk.import_timeperiod_exceptions_from_settings(enable=True)
desk2 = Desk.objects.create(agenda=agenda, label='Desk B')
desk2.import_timeperiod_exceptions_from_settings(enable=True)
assert desk.timeperiodexception_set.exists()
assert desk2.timeperiodexception_set.exists()
login(app)
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
assert 'Holidays' in resp.text
assert 'disabled' not in resp.text
assert 'refresh' not in resp.text
resp = resp.click('disable').follow()
assert not desk.timeperiodexception_set.exists()
assert desk2.timeperiodexception_set.exists()
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
assert 'Holidays' in resp.text
assert 'disabled' in resp.text
resp = resp.click('enable').follow()
assert desk.timeperiodexception_set.exists()
assert desk2.timeperiodexception_set.exists()
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
assert 'disabled' not in resp.text
@override_settings(
EXCEPTIONS_SOURCES={
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
}
)
def test_meetings_agenda_time_period_exception_source_from_settings_toggle_desk_simple_management(
app, admin_user
):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings', desk_simple_management=True)
desk = Desk.objects.create(agenda=agenda, label='Desk A')
desk.import_timeperiod_exceptions_from_settings(enable=True)
source = desk.timeperiodexceptionsource_set.get()
desk2 = Desk.objects.create(agenda=agenda, label='Desk B')
desk2.import_timeperiod_exceptions_from_settings(enable=True)
source2 = desk2.timeperiodexceptionsource_set.get()
assert desk.timeperiodexception_set.exists()
assert desk2.timeperiodexception_set.exists()
assert agenda.is_available_for_simple_management() is True
login(app)
app.get('/manage/time-period-exceptions-source/%s/toggle' % source.pk)
source.refresh_from_db()
source2.refresh_from_db()
assert not source.enabled
assert not source2.enabled
assert not desk.timeperiodexception_set.exists()
assert not desk2.timeperiodexception_set.exists()
assert agenda.is_available_for_simple_management() is True
assert AgendaSnapshot.objects.count() == 1
app.get('/manage/time-period-exceptions-source/%s/toggle' % source.pk)
source.refresh_from_db()
source2.refresh_from_db()
assert source.enabled
assert source2.enabled
assert desk.timeperiodexception_set.exists()
assert desk2.timeperiodexception_set.exists()
assert agenda.is_available_for_simple_management() is True
# should not happen: corresponding source does not exist
source2.delete()
app.get('/manage/time-period-exceptions-source/%s/toggle' % source.pk)
source.refresh_from_db()
assert not source.enabled
def test_meetings_agenda_time_period_exception_source_try_disable_ics(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
MeetingType(agenda=agenda, label='Blah').save()
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
source = TimePeriodExceptionSource.objects.create(desk=desk, ics_url='https://example.com/test.ics')
login(app)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow().follow()
resp = resp.click('Settings')
resp = resp.click('manage exceptions')
assert 'test.ics' in resp.text
assert app.get('/manage/time-period-exceptions-source/%s/toggle' % source.pk, status=404)
@override_settings(
EXCEPTIONS_SOURCES={
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
}
)
def test_meetings_agenda_time_period_exception_source_from_settings(app, admin_user, freezer):
freezer.move_to('2020-01-01')
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
desk.import_timeperiod_exceptions_from_settings(enable=True)
new_year = desk.timeperiodexception_set.filter(label='New year').first()
remove_url = reverse('chrono-manager-time-period-exception-delete', kwargs={'pk': new_year.pk})
edit_url = reverse('chrono-manager-time-period-exception-edit', kwargs={'pk': new_year.pk})
login(app)
resp = app.get('/manage/agendas/%d/settings' % agenda.pk)
assert 'New year' in resp.text
assert remove_url not in resp.text and edit_url not in resp.text
resp = resp.click('see all')
assert 'New year' in resp.text
assert remove_url not in resp.text and edit_url not in resp.text
app.get(remove_url, status=404)
@override_settings(
EXCEPTIONS_SOURCES={
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
}
)
def test_recurring_events_manage_exceptions(settings, app, admin_user, freezer):
freezer.move_to('2021-07-01 12:10')
app = login(app)
resp = app.get('/manage/')
resp = resp.click('New')
resp.form['label'] = 'Foo bar'
resp.form['kind'] = 'events'
resp = resp.form.submit().follow()
agenda = Agenda.objects.get(label='Foo bar')
assert agenda.desk_set.count() == 1
desk = agenda.desk_set.get(slug='_exceptions_holder')
event = Event.objects.create(start_datetime=now() + datetime.timedelta(hours=1), places=10, agenda=agenda)
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
assert 'Recurrence exceptions' not in resp.text
event.recurrence_days = list(range(1, 8))
event.recurrence_end_date = now() + datetime.timedelta(days=31)
event.save()
event.create_all_recurrences()
resp = app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.id, 2021, 7, 1))
assert len(resp.pyquery.find('.event-info')) == 31
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
assert 'Recurrence exceptions' in resp.text
resp = resp.click('Add a time period exception')
resp.form['start_datetime_0'] = now().strftime('%Y-%m-%d')
resp.form['start_datetime_1'] = now().strftime('%H:%M')
resp.form['end_datetime_0'] = (now() + datetime.timedelta(days=7)).strftime('%Y-%m-%d')
resp.form['end_datetime_1'] = (now() + datetime.timedelta(days=7)).strftime('%H:%M')
resp = resp.form.submit().follow()
assert desk.timeperiodexception_set.count() == 1
agenda.update_event_recurrences()
resp = app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.id, 2021, 7, 1))
assert len(resp.pyquery.find('.event-info')) == 24
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
resp = resp.click('Configure', href='exceptions')
resp = resp.click('enable').follow()
assert TimePeriodException.objects.count() > 1
assert 'Bastille Day' in resp.text
agenda.update_event_recurrences()
resp = app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.id, 2021, 7, 1))
assert len(resp.pyquery.find('.event-info')) == 23
def test_recurring_events_exceptions_report(settings, app, admin_user, freezer):
freezer.move_to('2021-07-01 12:10')
agenda = Agenda.objects.create(label='Foo bar', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
event = Event.objects.create(
start_datetime=now(),
places=10,
recurrence_days=list(range(1, 8)),
recurrence_end_date=now() + datetime.timedelta(days=30),
agenda=agenda,
)
event.create_all_recurrences()
app = login(app)
resp = app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.id, 2021, 7, 1))
assert len(resp.pyquery.find('.event-info')) == 30
time_period_exception = TimePeriodException.objects.create(
desk=agenda.desk_set.get(),
start_datetime=datetime.date(year=2021, month=7, day=5),
end_datetime=datetime.date(year=2021, month=7, day=10),
)
call_command('update_event_recurrences')
resp = app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.id, 2021, 7, 1))
assert len(resp.pyquery.find('.event-info')) == 25
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
assert 'warningnotice' not in resp.text
event = Event.objects.get(start_datetime__day=11)
booking = Booking.objects.create(event=event)
time_period_exception.end_datetime = datetime.date(year=2021, month=7, day=12)
time_period_exception.save()
call_command('update_event_recurrences')
resp = app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.id, 2021, 7, 1))
assert len(resp.pyquery.find('.event-info')) == 24
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
assert 'warningnotice' in resp.text
assert 'July 11, 2021, 2:10 p.m.' in resp.text
booking.cancel()
call_command('update_event_recurrences')
resp = app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.id, 2021, 7, 1))
assert len(resp.pyquery.find('.event-info')) == 23
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
assert 'warningnotice' not in resp.text