manager: simple desk management & sources (#48924)

This commit is contained in:
Lauréline Guérin 2021-02-01 15:40:03 +01:00
parent e34761cf7b
commit 83f22ddb40
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
3 changed files with 497 additions and 171 deletions

View File

@ -636,11 +636,23 @@ class TimePeriodExceptionSourceReplaceForm(forms.ModelForm):
fields = []
def save(self, *args, **kwargs):
if bool(self.instance.ics_file):
self.instance.ics_file.delete()
self.instance.ics_file = self.cleaned_data['ics_newfile']
self.instance.ics_filename = self.cleaned_data['ics_newfile'].name
self.instance.save()
def store_source(source):
if bool(source.ics_file):
source.ics_file.delete()
source.ics_file = self.cleaned_data['ics_newfile']
source.ics_filename = self.cleaned_data['ics_newfile'].name
source.save()
self.cleaned_data['ics_newfile'].seek(0)
old_filename = self.instance.ics_filename
store_source(self.instance)
if self.instance.desk.agenda.desk_simple_management:
for desk in self.instance.desk.agenda.desk_set.exclude(pk=self.instance.desk_id):
source = desk.timeperiodexceptionsource_set.filter(ics_filename=old_filename).first()
if source is not None:
store_source(source)
return self.instance
class AgendasImportForm(forms.Form):

View File

@ -1765,29 +1765,42 @@ class UnavailabilityCalendarToggleView(ManagedDeskMixin, DetailView):
def get(self, request, *args, **kwargs):
unavailability_calendar = self.get_object()
activate = False
enabled = False
try:
self.desk.unavailability_calendars.get(pk=unavailability_calendar.pk)
self.desk.unavailability_calendars.remove(unavailability_calendar)
message = _(
'Unavailability calendar %(unavailability_calendar)s has been disabled on desk %(desk)s.'
)
except UnavailabilityCalendar.DoesNotExist:
activate = True
enabled = True
self.desk.unavailability_calendars.add(unavailability_calendar)
message = _(
'Unavailability calendar %(unavailability_calendar)s has been enabled on desk %(desk)s.'
)
if self.desk.agenda.desk_simple_management:
for desk in self.desk.agenda.desk_set.exclude(pk=self.desk.pk):
if enabled:
desk.unavailability_calendars.add(unavailability_calendar)
message = _('Unavailability calendar %(unavailability_calendar)s has been enabled.')
else:
desk.unavailability_calendars.remove(unavailability_calendar)
message = _('Unavailability calendar %(unavailability_calendar)s has been disabled.')
messages.info(
self.request, message % {'unavailability_calendar': unavailability_calendar, 'desk': self.desk}
)
if activate:
if enabled:
for exception in unavailability_calendar.timeperiodexception_set.all():
if exception.has_booking_within_time_slot(target_desk=self.desk):
message = _('One or several bookings overlap with exceptions.')
messages.warning(self.request, message)
break
desks = [self.desk]
if self.desk.agenda.desk_simple_management:
desks = self.desk.agenda.desk_set.all()
for desk in desks:
if exception.has_booking_within_time_slot(target_desk=desk):
message = _('One or several bookings overlap with exceptions.')
messages.warning(self.request, message)
break
return HttpResponseRedirect(
reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda_id})
@ -2197,17 +2210,24 @@ class DeskImportTimePeriodExceptionsView(ManagedAgendaSubobjectMixin, UpdateView
)
return context
def import_file(self, desk, form):
if form.cleaned_data['ics_file']:
exceptions = desk.import_timeperiod_exceptions_from_ics_file(form.cleaned_data['ics_file'])
form.cleaned_data['ics_file'].seek(0)
return exceptions
elif form.cleaned_data['ics_url']:
return desk.import_timeperiod_exceptions_from_remote_ics(form.cleaned_data['ics_url'])
def form_valid(self, form):
exceptions = None
desk = self.get_object()
try:
if form.cleaned_data['ics_file']:
exceptions = form.instance.import_timeperiod_exceptions_from_ics_file(
form.cleaned_data['ics_file']
)
elif form.cleaned_data['ics_url']:
exceptions = form.instance.import_timeperiod_exceptions_from_remote_ics(
form.cleaned_data['ics_url']
)
if desk.agenda.desk_simple_management:
for _desk in desk.agenda.desk_set.all():
result = self.import_file(_desk, form)
exceptions = result if exceptions is None else exceptions
else:
exceptions = self.import_file(desk, form)
except ICSError as e:
form.add_error(None, force_text(e))
return self.form_invalid(form)
@ -2231,6 +2251,24 @@ class TimePeriodExceptionSourceDeleteView(ManagedDeskSubobjectMixin, DeleteView)
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda_id})
def delete(self, request, *args, **kwargs):
source = self.get_object()
response = super().delete(request, *args, **kwargs)
if not source.desk.agenda.desk_simple_management:
return response
for desk in source.desk.agenda.desk_set.exclude(pk=source.desk_id):
if source.ics_filename:
queryset = desk.timeperiodexceptionsource_set.filter(ics_filename=source.ics_filename)
else:
queryset = desk.timeperiodexceptionsource_set.filter(ics_url=source.ics_url)
_source = queryset.first()
if _source is not None:
_source.delete()
return response
time_period_exception_source_delete = TimePeriodExceptionSourceDeleteView.as_view()
@ -2244,12 +2282,28 @@ class TimePeriodExceptionSourceReplaceView(ManagedDeskSubobjectMixin, UpdateView
queryset = super(TimePeriodExceptionSourceReplaceView, self).get_queryset()
return queryset.filter(ics_filename__isnull=False)
def import_file(self, desk, form):
exceptions = None
source = desk.timeperiodexceptionsource_set.filter(
ics_filename=self.get_object().ics_filename
).first()
if source is not None:
exceptions = desk.import_timeperiod_exceptions_from_ics_file(
form.cleaned_data['ics_newfile'], source=source
)
form.cleaned_data['ics_newfile'].seek(0)
return exceptions
def form_valid(self, form):
exceptions = None
desk = self.get_object().desk
try:
exceptions = form.instance.desk.import_timeperiod_exceptions_from_ics_file(
form.cleaned_data['ics_newfile'], source=form.instance
)
if desk.agenda.desk_simple_management:
for _desk in desk.agenda.desk_set.all():
result = self.import_file(_desk, form)
exceptions = result if exceptions is None else exceptions
else:
exceptions = self.import_file(desk, form)
except ICSError as e:
form.add_error(None, force_text(e))
return self.form_invalid(form)
@ -2273,24 +2327,35 @@ class TimePeriodExceptionSourceRefreshView(ManagedDeskSubobjectMixin, DetailView
queryset = super(TimePeriodExceptionSourceRefreshView, self).get_queryset()
return queryset.filter(ics_url__isnull=False)
def import_file(self, desk):
exceptions = None
source = desk.timeperiodexceptionsource_set.filter(ics_url=self.get_object().ics_url).first()
if source is not None:
exceptions = desk.import_timeperiod_exceptions_from_remote_ics(source.ics_url, source=source)
return exceptions
def get(self, request, *args, **kwargs):
exceptions = None
desk = self.get_object().desk
try:
source = self.get_object()
exceptions = source.desk.import_timeperiod_exceptions_from_remote_ics(
source.ics_url, source=source
)
if desk.agenda.desk_simple_management:
for _desk in desk.agenda.desk_set.all():
result = self.import_file(_desk)
exceptions = result if exceptions is None else exceptions
else:
exceptions = self.import_file(desk)
except ICSError as e:
messages.error(self.request, force_text(e))
else:
if exceptions is not None:
message = ungettext(
'An exception has been imported.', '%(count)d exceptions have been imported.', exceptions
)
message = message % {'count': exceptions}
messages.info(self.request, message)
# redirect to settings
return HttpResponseRedirect(
reverse('chrono-manager-agenda-settings', kwargs={'pk': source.desk.agenda_id})
)
return HttpResponseRedirect(reverse('chrono-manager-agenda-settings', kwargs={'pk': desk.agenda_id}))
time_period_exception_source_refresh = TimePeriodExceptionSourceRefreshView.as_view()
@ -2424,12 +2489,30 @@ class TimePeriodExceptionSourceToggleView(ManagedDeskSubobjectMixin, DetailView)
def get(self, request, *args, **kwargs):
source = self.get_object()
if source.enabled:
source.disable()
was_enabled = False
message = _('Exception source %(source)s has been disabled on desk %(desk)s.')
else:
source.enable()
was_enabled = True
message = _('Exception source %(source)s has been enabled on desk %(desk)s.')
if self.desk.agenda.desk_simple_management:
for desk in self.desk.agenda.desk_set.exclude(pk=self.desk.pk):
_source = desk.timeperiodexceptionsource_set.filter(
settings_slug=source.settings_slug
).first()
if _source is None:
continue
if was_enabled:
_source.enable()
message = _('Exception source %(source)s has been enabled.')
else:
_source.disable()
message = _('Exception source %(source)s has been disabled.')
messages.info(self.request, message % {'source': source, 'desk': source.desk})
return HttpResponseRedirect(
reverse('chrono-manager-agenda-settings', kwargs={'pk': source.desk.agenda_id})

View File

@ -9,6 +9,7 @@ import mock
import os
from django.contrib.auth.models import User, Group
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
@ -2879,16 +2880,14 @@ def test_exception_list(app, admin_user):
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')
MeetingType.objects.create(agenda=agenda, label='Foo')
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')
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
resp = app.get('/manage/agendas/%d/settings' % agenda.pk)
resp = resp.click('manage exceptions')
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
@ -2920,14 +2919,13 @@ DTEND:20180101
SUMMARY:New Year's Eve
END:VEVENT
END:VCALENDAR"""
resp = app.get('/manage/agendas/%d/settings' % agenda.pk)
resp = resp.click('manage exceptions')
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 = TimePeriodExceptionSource.objects.latest('pk')
exception = TimePeriodException.objects.latest('pk')
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
@ -2966,10 +2964,9 @@ END:VCALENDAR"""
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')
MeetingType.objects.create(agenda=agenda, label='Bar')
desk.duplicate()
login(app)
resp = app.get('/manage/agendas/%d/settings' % agenda.pk)
resp = resp.click('manage exceptions')
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
@ -2987,10 +2984,9 @@ END:VCALENDAR"""
mocked_get.return_value = mocked_response
resp = resp.form.submit(status=302)
assert TimePeriodException.objects.filter(desk=desk).count() == 1
exception = TimePeriodException.objects.get(desk=desk)
assert TimePeriodExceptionSource.objects.filter(desk=desk).count() == 1
source = TimePeriodExceptionSource.objects.latest('pk')
exception = TimePeriodException.objects.latest('pk')
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 == ''
@ -3026,7 +3022,7 @@ def test_agenda_import_time_period_exception_from_remote_ics_with_connection_err
mocked_get, app, admin_user
):
agenda = Agenda.objects.create(label='New Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='New 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)
@ -3049,7 +3045,7 @@ def test_agenda_import_time_period_exception_from_remote_ics_with_connection_err
@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 = Desk.objects.create(agenda=agenda, label='New 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)
@ -3070,7 +3066,7 @@ def test_agenda_import_time_period_exception_from_forbidden_remote_ics(mocked_ge
@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 = Desk.objects.create(agenda=agenda, label='New 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)
@ -3087,69 +3083,111 @@ def test_agenda_import_time_period_exception_from_remote_ics_with_ssl_error(mock
assert 'Failed to retrieve remote calendar (https://example.com/foo.ics, SSL error).' in resp.text
def test_meetings_agenda_delete_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')
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)
)
@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)
# import a source
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('manage exceptions')
ics_with_recurrent_exceptions = b"""BEGIN:VCALENDAR
resp = app.get('/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk)
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
RRULE:FREQ=YEARLY
END:VEVENT
END:VCALENDAR"""
resp.form['ics_file'] = Upload('exceptions.ics', ics_with_recurrent_exceptions, 'text/calendar')
resp = resp.form.submit(status=302).follow()
assert TimePeriodException.objects.filter(desk=desk).count() == 2
source1 = TimePeriodExceptionSource.objects.latest('pk')
assert source1.timeperiodexception_set.count() == 2
# import another one
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('manage exceptions')
resp.form['ics_file'] = Upload('exceptions.ics', ics_with_recurrent_exceptions, 'text/calendar')
resp = resp.form.submit(status=302).follow()
assert TimePeriodException.objects.filter(desk=desk).count() == 4
source2 = TimePeriodExceptionSource.objects.latest('pk')
assert source2.timeperiodexception_set.count() == 2
# delete the second one
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('manage exceptions')
resp = resp.click(href='/manage/time-period-exceptions-source/%d/delete' % source2.pk)
resp = resp.form.submit().follow()
mocked_get.return_value = mocked_response
resp = resp.form.submit(status=302)
assert TimePeriodException.objects.count() == 2
assert source1.timeperiodexception_set.count() == 2
assert agenda.is_available_for_simple_management() is True
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)
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
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
# delete the first one
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('manage exceptions')
resp = resp.click(href='/manage/time-period-exceptions-source/%d/delete' % source1.pk)
resp = resp.form.submit().follow()
assert TimePeriodException.objects.count() == 0
assert TimePeriodExceptionSource.objects.filter(pk=source1.pk).exists() is False
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
# 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')
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)
)
ics_file_content = b"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
@ -3163,8 +3201,7 @@ END:VCALENDAR"""
login(app)
# import a source from a file
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('manage exceptions')
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
@ -3173,32 +3210,35 @@ END:VCALENDAR"""
exceptions = list(source.timeperiodexception_set.order_by('pk'))
old_ics_file_path = source.ics_file.path
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/agendas/%s/settings' % agenda.pk)
resp = resp.click('manage exceptions')
resp = resp.click(href='/manage/time-period-exceptions-source/%d/replace' % source.pk)
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() == 2
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
@mock.patch('chrono.agendas.models.requests.get')
def test_meetings_agenda_refresh_time_period_exception_source(mocked_get, 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)
)
ics_url_content = """BEGIN:VCALENDAR
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
@ -3208,31 +3248,134 @@ 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
# 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'
mocked_response = mock.Mock()
mocked_response.text = ics_url_content
mocked_get.return_value = mocked_response
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')
mocked_response = mock.Mock()
mocked_response.text = ics_url_content
mocked_get.return_value = mocked_response
resp = resp.click('manage exceptions', index=0)
resp = resp.click(href='/manage/time-period-exceptions-source/%d/refresh' % source.pk)
assert TimePeriodException.objects.count() == 1
assert source.timeperiodexception_set.count() == 1
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(
@ -3240,38 +3383,83 @@ END:VCALENDAR"""
'holidays': {'class': 'workalendar.europe.France', 'label': 'Holidays'},
}
)
def test_meetings_agenda_time_period_exception_source_from_settings(app, admin_user):
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)
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)
)
assert TimePeriodException.objects.exists()
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/%d/' % agenda.pk).follow()
resp = resp.click('Settings')
resp = resp.click('manage exceptions')
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 TimePeriodException.objects.exists()
assert not desk.timeperiodexception_set.exists()
assert desk2.timeperiodexception_set.exists()
resp = resp.click('manage exceptions')
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 TimePeriodException.objects.exists()
assert desk.timeperiodexception_set.exists()
assert desk2.timeperiodexception_set.exists()
resp = resp.click('manage exceptions')
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
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')
@ -3300,10 +3488,6 @@ def test_meetings_agenda_time_period_exception_source_from_settings(app, admin_u
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)
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)
)
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})
@ -3312,11 +3496,11 @@ def test_meetings_agenda_time_period_exception_source_from_settings(app, admin_u
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings')
assert 'New year' in resp.text
assert not remove_url in resp.text and not edit_url 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 not remove_url in resp.text and not edit_url in resp.text
assert remove_url not in resp.text and edit_url not in resp.text
app.get(remove_url, status=404)
@ -5470,18 +5654,22 @@ def test_unavailability_calendar_delete_time_period_exeptions(app, admin_user):
assert unavailability_calendar.timeperiodexception_set.count() == 0
def test_activate_unavailability_calendar_in_desk(app, admin_user):
def test_unavailability_calendar_in_desk(app, admin_user):
app = login(app)
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
start_datetime = localtime(now()) + datetime.timedelta(days=1)
time_period_exception = TimePeriodException.objects.create(
today = localtime(now().replace(hour=0, minute=0, second=0, microsecond=0))
tomorrow = today + datetime.timedelta(days=1)
TimePeriodException.objects.create(
unavailability_calendar=unavailability_calendar,
start_datetime=start_datetime,
end_datetime=localtime(now()) + datetime.timedelta(days=2),
start_datetime=tomorrow,
end_datetime=tomorrow + datetime.timedelta(days=2),
label='what',
)
agenda = Agenda.objects.create(label='Agenda', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='desk')
desk2 = desk.duplicate()
assert not desk.unavailability_calendars.exists()
assert not desk2.unavailability_calendars.exists()
exceptions_url = '/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk
resp = app.get(exceptions_url)
assert 'calendar' in resp.text
@ -5491,7 +5679,7 @@ def test_activate_unavailability_calendar_in_desk(app, admin_user):
)
settings_url = '/manage/agendas/%s/settings' % agenda.pk
assert resp.location.endswith(settings_url)
# exception from calendar displayed on the settigns page
# exception from calendar displayed on the settings page
resp = app.get(settings_url)
assert 'what' in resp.text
assert 'One or several bookings overlap with exceptions.' not in resp.text
@ -5500,18 +5688,19 @@ def test_activate_unavailability_calendar_in_desk(app, admin_user):
assert 'calendar' in resp.text
assert 'disable' in resp.text
assert desk.unavailability_calendars.get(pk=unavailability_calendar.pk)
assert not desk2.unavailability_calendars.exists()
# reset
unavailability_calendar.desks.remove(desk)
resp = resp.click(
href='/manage/desk/%s/unavailability-calendar/%s/toggle/' % (desk.pk, unavailability_calendar.pk)
)
assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.pk)
resp = app.get(exceptions_url)
assert 'calendar' in resp.text
assert 'enable' in resp.text
assert not desk.unavailability_calendars.exists()
# info message if some exceptions overlaps with a booking
today = datetime.datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
tomorrow = make_aware(today + datetime.timedelta(days=1))
TimePeriodException.objects.create(
start_datetime=tomorrow,
end_datetime=tomorrow.replace(hour=15, minute=30, second=0),
unavailability_calendar=unavailability_calendar,
)
meeting_type = MeetingType.objects.create(label='Meeting Type', agenda=agenda)
event = Event.objects.create(
agenda=agenda,
@ -5532,6 +5721,68 @@ def test_activate_unavailability_calendar_in_desk(app, admin_user):
assert 'One or several bookings overlap with exceptions.' in resp.text
def test_unavailability_calendar_in_desk_desk_simple_management(app, admin_user):
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
today = localtime(now().replace(hour=0, minute=0, second=0, microsecond=0))
tomorrow = today + datetime.timedelta(days=1)
TimePeriodException.objects.create(
unavailability_calendar=unavailability_calendar,
start_datetime=tomorrow,
end_datetime=tomorrow + datetime.timedelta(days=2),
label='what',
)
agenda = Agenda.objects.create(label='Agenda', kind='meetings', desk_simple_management=True)
desk = Desk.objects.create(agenda=agenda, label='desk')
desk2 = desk.duplicate()
assert not desk.unavailability_calendars.exists()
assert not desk2.unavailability_calendars.exists()
assert agenda.is_available_for_simple_management() is True
meeting_type = MeetingType.objects.create(label='Meeting Type', agenda=agenda)
event = Event.objects.create(
agenda=agenda,
places=1,
desk=desk2,
meeting_type=meeting_type,
start_datetime=tomorrow.replace(hour=10, minute=30, second=0),
)
Booking.objects.create(event=event)
login(app)
resp = app.get(
'/manage/desk/%s/unavailability-calendar/%s/toggle/' % (desk.pk, unavailability_calendar.pk)
)
assert desk.unavailability_calendars.exists()
assert desk2.unavailability_calendars.exists()
assert agenda.is_available_for_simple_management() is True
resp = resp.follow()
assert 'One or several bookings overlap with exceptions.' in resp.text
app.get('/manage/desk/%s/unavailability-calendar/%s/toggle/' % (desk.pk, unavailability_calendar.pk))
assert not desk.unavailability_calendars.exists()
assert not desk2.unavailability_calendars.exists()
assert agenda.is_available_for_simple_management() is True
# should not happen: unavailability_calendar is not in the correct state
desk2.unavailability_calendars.add(unavailability_calendar)
assert not desk.unavailability_calendars.exists()
assert desk2.unavailability_calendars.exists()
assert agenda.is_available_for_simple_management() is False
app.get('/manage/desk/%s/unavailability-calendar/%s/toggle/' % (desk.pk, unavailability_calendar.pk))
assert desk.unavailability_calendars.exists()
assert desk2.unavailability_calendars.exists()
assert agenda.is_available_for_simple_management() is True
desk2.unavailability_calendars.remove(unavailability_calendar)
assert desk.unavailability_calendars.exists()
assert not desk2.unavailability_calendars.exists()
assert agenda.is_available_for_simple_management() is False
app.get('/manage/desk/%s/unavailability-calendar/%s/toggle/' % (desk.pk, unavailability_calendar.pk))
assert not desk.unavailability_calendars.exists()
assert not desk2.unavailability_calendars.exists()
assert agenda.is_available_for_simple_management() is True
def test_unavailability_calendar_exception_in_desk(app, admin_user):
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
start_datetime = localtime(now()) + datetime.timedelta(days=1)
@ -5554,26 +5805,6 @@ def test_unavailability_calendar_exception_in_desk(app, admin_user):
assert resp.text.count('exception foo bar') == 3 # displayed just once per desk
def test_deactivate_unavailability_calendar_in_desk(app, admin_user):
app = login(app)
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
agenda = Agenda.objects.create(label='Agenda', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='desk')
desk.unavailability_calendars.add(unavailability_calendar)
exceptions_url = '/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk
resp = app.get(exceptions_url)
assert 'calendar' in resp.text
assert 'disable' in resp.text
resp = resp.click(
href='/manage/desk/%s/unavailability-calendar/%s/toggle/' % (desk.pk, unavailability_calendar.pk)
)
assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.pk)
resp = app.get(exceptions_url)
assert 'calendar' in resp.text
assert 'enable' in resp.text
assert desk.unavailability_calendars.count() == 0
def test_unavailability_calendar_homepage_permission(app, manager_user):
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
app = login(app, username='manager', password='manager')