manager: simple desk management & exceptions (#48924)

This commit is contained in:
Lauréline Guérin 2021-01-28 16:41:58 +01:00
parent 9efbdf2367
commit e34761cf7b
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
3 changed files with 285 additions and 90 deletions

View File

@ -345,8 +345,6 @@ class DeskForm(forms.ModelForm):
class TimePeriodExceptionForm(forms.ModelForm):
all_desks = forms.BooleanField(label=_('Apply exception on all desks of the agenda'), required=False)
class Meta:
model = TimePeriodException
fields = ['start_datetime', 'end_datetime', 'label']
@ -357,12 +355,9 @@ class TimePeriodExceptionForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk is not None:
del self.fields['all_desks']
elif self.instance.unavailability_calendar:
del self.fields['all_desks']
elif self.instance.desk_id and self.instance.desk.agenda.desk_set.count() == 1:
del self.fields['all_desks']
self.old_label = self.instance.label
self.old_start_datetime = self.instance.start_datetime
self.old_end_datetime = self.instance.end_datetime
def clean(self):
cleaned_data = super().clean()
@ -373,6 +368,64 @@ class TimePeriodExceptionForm(forms.ModelForm):
return cleaned_data
def save(self):
super().save()
self.exceptions = [self.instance]
desk_simple_management = False
if self.instance.desk_id:
desk_simple_management = self.instance.desk.agenda.desk_simple_management
if desk_simple_management:
for desk in self.instance.desk.agenda.desk_set.exclude(pk=self.instance.desk_id):
exception = desk.timeperiodexception_set.filter(
source__isnull=True,
label=self.old_label,
start_datetime=self.old_start_datetime,
end_datetime=self.old_end_datetime,
).first()
if exception is not None:
exception.label = self.instance.label
exception.start_datetime = self.instance.start_datetime
exception.end_datetime = self.instance.end_datetime
exception.save()
self.exceptions.append(exception)
return self.instance
class NewTimePeriodExceptionForm(TimePeriodExceptionForm):
all_desks = forms.BooleanField(label=_('Apply exception on all desks of the agenda'), required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.unavailability_calendar:
del self.fields['all_desks']
elif self.instance.desk_id and self.instance.desk.agenda.desk_set.count() == 1:
del self.fields['all_desks']
elif self.instance.desk_id and self.instance.desk.agenda.desk_simple_management:
del self.fields['all_desks']
def save(self):
super().save()
self.exceptions = [self.instance]
all_desks = self.cleaned_data.get('all_desks')
desk_simple_management = False
if self.instance.desk_id:
desk_simple_management = self.instance.desk.agenda.desk_simple_management
if all_desks or desk_simple_management:
for desk in self.instance.desk.agenda.desk_set.exclude(pk=self.instance.desk_id):
self.exceptions.append(
TimePeriodException.objects.create(
desk=desk,
label=self.instance.label,
start_datetime=self.instance.start_datetime,
end_datetime=self.instance.end_datetime,
)
)
return self.instance
class VirtualMemberForm(forms.ModelForm):
class Meta:

View File

@ -86,6 +86,7 @@ from .forms import (
ImportEventsForm,
NewDeskForm,
DeskForm,
NewTimePeriodExceptionForm,
TimePeriodExceptionForm,
ExceptionsImportForm,
AgendasImportForm,
@ -2040,7 +2041,7 @@ virtual_member_delete = VirtualMemberDeleteView.as_view()
class AgendaAddTimePeriodExceptionView(ManagedDeskMixin, CreateView):
template_name = 'chrono/manager_time_period_exception_form.html'
model = TimePeriodException
form_class = TimePeriodExceptionForm
form_class = NewTimePeriodExceptionForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -2050,26 +2051,14 @@ class AgendaAddTimePeriodExceptionView(ManagedDeskMixin, CreateView):
def form_valid(self, form):
result = super().form_valid(form)
exceptions = [self.object]
if form.cleaned_data.get('all_desks'):
for desk in Desk.objects.filter(agenda=self.object.desk.agenda_id):
if desk == self.object.desk:
continue
exceptions.append(
TimePeriodException.objects.create(
desk=desk,
label=self.object.label,
start_datetime=self.object.start_datetime,
end_datetime=self.object.end_datetime,
)
)
all_desks = form.cleaned_data.get('all_desks')
message = ungettext(
'Exception added.',
'Exceptions added.',
len(exceptions),
len(form.exceptions) if all_desks else 1,
)
messages.info(self.request, message)
for exception in exceptions:
for exception in form.exceptions:
if exception.has_booking_within_time_slot():
messages.warning(self.request, _('One or several bookings exists within this time slot.'))
break
@ -2101,9 +2090,11 @@ class TimePeriodExceptionEditView(ManagedTimePeriodExceptionMixin, UpdateView):
def form_valid(self, form):
result = super().form_valid(form)
if self.object.has_booking_within_time_slot():
messages.info(self.request, _('Exception updated.'))
messages.warning(self.request, _('One or several bookings exists within this time slot.'))
messages.info(self.request, _('Exception updated.'))
for exception in form.exceptions:
if exception.has_booking_within_time_slot():
messages.warning(self.request, _('One or several bookings exists within this time slot.'))
break
return result
@ -2159,6 +2150,28 @@ class TimePeriodExceptionDeleteView(ManagedTimePeriodExceptionMixin, DeleteView)
def get_queryset(self):
return super().get_queryset().filter(source__settings_slug__isnull=True)
def delete(self, request, *args, **kwargs):
exception = self.get_object()
response = super().delete(request, *args, **kwargs)
if not exception.desk_id:
return response
if not exception.desk.agenda.desk_simple_management:
return response
for desk in exception.desk.agenda.desk_set.exclude(pk=exception.desk.pk):
exc = desk.timeperiodexception_set.filter(
source__isnull=True,
label=exception.label,
start_datetime=exception.start_datetime,
end_datetime=exception.end_datetime,
).first()
if exc is not None:
exc.delete()
return response
time_period_exception_delete = TimePeriodExceptionDeleteView.as_view()
@ -2565,7 +2578,7 @@ unavailability_calendar_export = UnavailabilityCalendarExport.as_view()
class UnavailabilityCalendarAddUnavailabilityView(ManagedUnavailabilityCalendarMixin, CreateView):
template_name = 'chrono/manager_time_period_exception_form.html'
form_class = TimePeriodExceptionForm
form_class = NewTimePeriodExceptionForm
model = TimePeriodException
def get_form_kwargs(self):

View File

@ -2384,10 +2384,11 @@ def test_meetings_agenda_delete_desk(app, admin_user):
def test_meetings_agenda_add_time_period_exception(app, admin_user):
agenda = Agenda.objects.create(label=u'Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
meeting_type = MeetingType.objects.create(agenda=agenda, label='Blah')
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')
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'
@ -2398,6 +2399,7 @@ def test_meetings_agenda_add_time_period_exception(app, admin_user):
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
@ -2421,58 +2423,69 @@ def test_meetings_agenda_add_time_period_exception(app, admin_user):
assert 'Exception 1' in resp.text
assert 'Exception 2' in resp.text
# add global time exception
# only one desk: no option to apply to all desks
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
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
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)
resp = app.get('/manage/agendas/%s/desk/%s/add-time-period-exception' % (agenda.pk, desk.pk))
future = tomorrow + datetime.timedelta(days=35)
resp.form['label'] = 'Exception 3'
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() == 4
assert 'Exceptions added.' in resp.text
assert 'One or several bookings exists within this time slot.' in resp.text
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_booking_overlaps(app, admin_user):
agenda = Agenda.objects.create(label=u'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=desk2,
desk=desk,
meeting_type=meeting_type,
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, desk2.pk))
resp.form['label'] = 'Exception 4'
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.form['all_desks'] = False
resp = resp.form.submit().follow()
assert TimePeriodException.objects.count() == 5
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=u'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
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')
@ -2522,11 +2535,59 @@ def test_meetings_agenda_add_time_period_exception_when_cancelled_booking_exists
resp.form['end_datetime_0'] = '2017-05-26'
resp.form['end_datetime_1'] = '17:30'
resp = resp.form.submit().follow()
assert 'Exception added. Note: one or several bookings exists within this time slot.' not in resp.text
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
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))
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')
@ -2543,6 +2604,7 @@ def test_meetings_agenda_edit_time_period_exception(app, admin_user):
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,
@ -2551,28 +2613,76 @@ def test_meetings_agenda_edit_time_period_exception(app, admin_user):
start_datetime=make_aware(datetime.datetime(2018, 12, 16, 10, 30)),
)
login(app)
resp = app.get('/manage/time-period-exceptions/%s/edit' % time_period_exception.pk)
resp = resp.form.submit().follow()
assert 'Exception updated.' not in resp.text
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 'Exception updated.' in resp.text
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 'Exception updated.' not in resp.text
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
# 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={
@ -2611,34 +2721,21 @@ def test_meetings_agenda_add_invalid_time_period_exception():
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')
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)
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('Add a time period exception')
today = datetime.datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
tomorrow = make_aware(today + datetime.timedelta(days=15))
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
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)
resp = resp.click(href='/manage/time-period-exceptions/%d/edit' % time_period_exception.id)
resp = resp.click('Exception Foo', index=0)
resp = resp.click('Delete')
resp = resp.form.submit().follow()
assert TimePeriodException.objects.count() == 0
assert TimePeriodException.objects.count() == 1
assert resp.request.url.endswith('/manage/agendas/%d/settings' % agenda.pk)
# stay on exception list
@ -2656,6 +2753,38 @@ def test_meetings_agenda_delete_time_period_exception(app, admin_user):
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
# 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'},