manager: add date time period support in settings (#70185)

This commit is contained in:
Valentin Deniaud 2022-10-13 16:19:13 +02:00
parent d0a8534fcd
commit aa0549f5b6
9 changed files with 311 additions and 10 deletions

View File

@ -795,7 +795,7 @@ class Agenda(models.Model):
end_datetime__gt=min_start,
)
def prefetch_desks_and_exceptions(self, with_sources=False):
def prefetch_desks_and_exceptions(self, with_sources=False, min_date=None):
if self.kind == 'meetings':
desks = self.desk_set.all()
elif self.kind == 'virtual':
@ -807,7 +807,15 @@ class Agenda(models.Model):
else:
raise ValueError('does not work with kind %r' % self.kind)
self.prefetched_desks = desks.prefetch_related('timeperiod_set', 'unavailability_calendars')
if min_date:
past_date_time_periods = TimePeriod.objects.filter(desk=OuterRef('pk'), date__lt=min_date)
desks = desks.annotate(has_past_date_time_periods=Exists(past_date_time_periods))
time_period_queryset = TimePeriod.objects.filter(Q(date__isnull=True) | Q(date__gte=min_date))
self.prefetched_desks = desks.prefetch_related(
'unavailability_calendars', Prefetch('timeperiod_set', queryset=time_period_queryset)
)
if with_sources:
self.prefetched_desks = self.prefetched_desks.prefetch_related('timeperiodexceptionsource_set')
unavailability_calendar_ids = UnavailabilityCalendar.objects.filter(

View File

@ -842,13 +842,19 @@ class TimePeriodFormBase(forms.Form):
widget=forms.CheckboxSelectMultiple(),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'date' in self.fields:
del self.fields['repeat']
del self.fields['weekday_indexes']
def clean(self):
cleaned_data = super().clean()
if cleaned_data['end_time'] <= cleaned_data['start_time']:
raise ValidationError(_('End time must come after start time.'))
if cleaned_data['repeat'] == 'every-week':
if cleaned_data.get('repeat') == 'every-week':
cleaned_data['weekday_indexes'] = None
return cleaned_data
@ -868,6 +874,7 @@ class TimePeriodForm(TimePeriodFormBase, forms.ModelForm):
class Meta:
model = TimePeriod
widgets = {
'date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'start_time': widgets.TimeWidget(),
'end_time': widgets.TimeWidget(),
}
@ -878,6 +885,7 @@ class TimePeriodForm(TimePeriodFormBase, forms.ModelForm):
self.old_weekday = self.instance.weekday
self.old_start_time = self.instance.start_time
self.old_end_time = self.instance.end_time
self.old_date = self.instance.date
if self.instance.weekday_indexes:
self.fields['repeat'].initial = 'custom'
@ -893,17 +901,26 @@ class TimePeriodForm(TimePeriodFormBase, forms.ModelForm):
for desk in self.instance.desk.agenda.desk_set.exclude(pk=self.instance.desk.pk):
timeperiod = desk.timeperiod_set.filter(
weekday=self.old_weekday, start_time=self.old_start_time, end_time=self.old_end_time
weekday=self.old_weekday,
start_time=self.old_start_time,
end_time=self.old_end_time,
date=self.old_date,
).first()
if timeperiod is not None:
timeperiod.weekday = self.instance.weekday
timeperiod.start_time = self.instance.start_time
timeperiod.end_time = self.instance.end_time
timeperiod.date = self.instance.date
timeperiod.save()
return self.instance
class DateTimePeriodForm(TimePeriodForm):
class Meta(TimePeriodForm.Meta):
fields = ['date', 'start_time', 'end_time']
class NewDeskForm(forms.ModelForm):
copy_from = forms.ModelChoiceField(
label=_('Copy settings of desk'),

View File

@ -78,7 +78,7 @@ h2 span.identifier {
padding-right: 1ex;
}
a.timeperiod-exception-all {
a.timeperiod-list-all {
font-style: italic;
}

View File

@ -0,0 +1,32 @@
{% extends "chrono/manager_agenda_view.html" %}
{% load i18n gadjo %}
{% block extrascripts %}
{{ block.super }}
{{ form.media }}
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
{% if object.id %}
<a href="">{{object}}</a>
{% else %}
<a href="">{% trans "Unique period" %}</a>
{% endif %}
{% endblock %}
{% block appbar %}
<h2>{% trans "Unique period" %}</h2>
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form|with_template }}
<div class="buttons">
<button class="submit-button">{% trans "Save" %}</button>
<a class="cancel" href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "chrono/manager_agenda_settings.html" %}
{% load i18n %}
{% block extrascripts %}
{{ block.super }}
{{ form.media }}
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href=".">{% trans "Unique periods" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans "Unique periods" %}</h2>
{% endblock %}
{% block content %}
<div class="timeperiod">
<ul class="objects-list single-links">
{% for time_period in object_list %}
<li>
<a rel="popup" href="{% url 'chrono-manager-time-period-edit' pk=time_period.id %}">{{ time_period }}</a>
<a rel="popup" class="delete" href="{% url 'chrono-manager-time-period-delete' pk=time_period.id %}">{% trans "remove" %}</a>
</li>
{% endfor %}
</ul>
{% include "gadjo/pagination.html" %}
</div>
{% endblock %}

View File

@ -93,6 +93,7 @@
{% if not object.desk_simple_management or object.desk_simple_management and forloop.counter == 1 %}
<div class="timeperiod">
{% url 'chrono-manager-agenda-add-time-period' agenda_pk=object.pk pk=desk.pk as add_time_period_url %}
{% url 'chrono-manager-agenda-add-date-time-period' agenda_pk=object.pk pk=desk.pk as add_date_time_period_url %}
<ul class="objects-list single-links">
{% if not object.desk_simple_management and object.prefetched_desks|length > 1 %}
<li>
@ -108,7 +109,11 @@
<a rel="popup" class="delete" href="{% url 'chrono-manager-time-period-delete' pk=time_period.id %}">{% trans "remove" %}</a>
</li>
{% endfor %}
{% if desk.has_past_date_time_periods %}
<li><a class="timeperiod-list-all desk-{{ desk.pk }}" data-selector="div.timeperiod" href="{% url 'chrono-manager-date-time-period-list' pk=desk.id %}">({% trans 'see all unique periods' %})</a></li>
{% endif %}
<li><a class="add" rel="popup" href="{{add_time_period_url}}">{% trans 'Add repeating periods' %}</a></li>
<li><a class="add" rel="popup" href="{{add_date_time_period_url}}">{% trans 'Add a unique period' %}</a></li>
{% url 'chrono-manager-agenda-add-time-period-exception' agenda_pk=object.pk pk=desk.pk as add_time_period_exception_url %}
<li>
<a><strong>{% trans 'Exceptions' %}</strong></a>
@ -123,7 +128,7 @@
</li>
{% endfor %}
{% if not desk.are_all_exceptions_displayed %}
<li><a class="timeperiod-exception-all desk-{{ desk.pk }}" rel="popup" data-selector="div.timeperiod" href="{% url 'chrono-manager-time-period-exception-extract-list' pk=desk.id %}">({% trans 'see all exceptions' %})</a></li>
<li><a class="timeperiod-list-all desk-{{ desk.pk }}" rel="popup" data-selector="div.timeperiod" href="{% url 'chrono-manager-time-period-exception-extract-list' pk=desk.id %}">({% trans 'see all exceptions' %})</a></li>
{% endif %}
<li><a class="add" rel="popup" href="{{add_time_period_exception_url}}">{% trans 'Add a time period exception' %}</a></li>
</ul>

View File

@ -301,6 +301,16 @@ urlpatterns = [
views.time_period_delete,
name='chrono-manager-time-period-delete',
),
path(
'agendas/<int:agenda_pk>/desk/<int:pk>/add-date-time-period',
views.agenda_add_date_time_period,
name='chrono-manager-agenda-add-date-time-period',
),
path(
'timeperiods/desk/<int:pk>/date-time-period-list',
views.agenda_date_time_period_list,
name='chrono-manager-date-time-period-list',
),
path('agendas/<int:pk>/add-desk', views.agenda_add_desk, name='chrono-manager-agenda-add-desk'),
path('desks/<int:pk>/edit', views.desk_edit, name='chrono-manager-desk-edit'),
path('desks/<int:pk>/delete', views.desk_delete, name='chrono-manager-desk-delete'),

View File

@ -110,6 +110,7 @@ from .forms import (
BookingCheckFilterSet,
BookingCheckPresenceForm,
CustomFieldFormSet,
DateTimePeriodForm,
DeskExceptionsImportForm,
DeskForm,
EventCancelForm,
@ -1832,7 +1833,7 @@ class AgendaSettings(ManagedAgendaMixin, DetailView):
def get_object(self, *args, **kwargs):
if self.agenda.kind == 'meetings':
self.agenda.prefetch_desks_and_exceptions(with_sources=True)
self.agenda.prefetch_desks_and_exceptions(with_sources=True, min_date=now())
return self.agenda
def get_context_data(self, **kwargs):
@ -2859,11 +2860,21 @@ virtual_agenda_add_time_period = VirtualAgendaAddTimePeriodView.as_view()
class TimePeriodEditView(ManagedTimePeriodMixin, UpdateView):
template_name = 'chrono/manager_time_period_form.html'
model = TimePeriod
form_class = TimePeriodForm
tab_anchor = 'time-periods'
def get_form_class(self):
if self.object.weekday is not None:
return TimePeriodForm
else:
return DateTimePeriodForm
def get_template_names(self):
if self.object.weekday is not None:
return ['chrono/manager_time_period_form.html']
else:
return ['chrono/manager_date_time_period_form.html']
time_period_edit = TimePeriodEditView.as_view()
@ -2885,7 +2896,10 @@ class TimePeriodDeleteView(ManagedTimePeriodMixin, DeleteView):
for desk in time_period.desk.agenda.desk_set.exclude(pk=time_period.desk.pk):
tp = desk.timeperiod_set.filter(
weekday=time_period.weekday, start_time=time_period.start_time, end_time=time_period.end_time
weekday=time_period.weekday,
start_time=time_period.start_time,
end_time=time_period.end_time,
date=time_period.date,
).first()
if tp is not None:
tp.delete()
@ -2896,6 +2910,43 @@ class TimePeriodDeleteView(ManagedTimePeriodMixin, DeleteView):
time_period_delete = TimePeriodDeleteView.as_view()
class AgendaAddDateTimePeriodView(ManagedDeskMixin, FormView):
template_name = 'chrono/manager_date_time_period_form.html'
model = TimePeriod
form_class = DateTimePeriodForm
tab_anchor = 'time-periods'
def form_valid(self, form):
create_kwargs = {
'date': form.cleaned_data['date'],
'start_time': form.cleaned_data['start_time'],
'end_time': form.cleaned_data['end_time'],
}
if self.desk.agenda.desk_simple_management:
for desk in self.desk.agenda.desk_set.all():
TimePeriod.objects.create(desk=desk, **create_kwargs)
else:
TimePeriod.objects.create(desk=self.desk, **create_kwargs)
return super().form_valid(form)
agenda_add_date_time_period = AgendaAddDateTimePeriodView.as_view()
class AgendaDateTimePeriodListView(ManagedDeskMixin, ListView):
template_name = 'chrono/manager_date_time_period_list.html'
model = TimePeriod
paginate_by = 20
def get_queryset(self):
return self.model.objects.filter(desk=self.desk, date__isnull=False).order_by('-date')
agenda_date_time_period_list = AgendaDateTimePeriodListView.as_view()
class AgendaAddDesk(ManagedAgendaMixin, CreateView):
template_name = 'chrono/manager_desk_form.html'
model = Desk

View File

@ -238,3 +238,150 @@ def test_meetings_agenda_delete_time_period_desk_simple_management(app, admin_us
resp = app.get('/manage/timeperiods/%s/delete' % time_period.pk)
resp.form.submit()
assert TimePeriod.objects.count() == 0
@pytest.mark.freeze_time('2022-10-24 10:00')
def test_meetings_agenda_date_time_period(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')
MeetingType.objects.create(agenda=agenda, label='Blah')
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('Add a unique period', index=0)
assert 'repeat' not in resp.form.fields
assert 'weekday' not in resp.form.fields
resp.form['date'] = '2022-10-24'
resp.form['start_time'] = '10:00'
resp.form['end_time'] = '17:00'
resp = resp.form.submit()
assert TimePeriod.objects.get(desk=desk).date == datetime.date(2022, 10, 24)
assert TimePeriod.objects.get(desk=desk).start_time.hour == 10
assert TimePeriod.objects.get(desk=desk).start_time.minute == 0
assert TimePeriod.objects.get(desk=desk).end_time.hour == 17
assert TimePeriod.objects.get(desk=desk).end_time.minute == 0
assert desk2.timeperiod_set.exists() is False
resp = resp.follow()
# invert start and end
resp = resp.click('Add a unique period', index=0)
resp.form['date'] = '2022-10-24'
resp.form['start_time'] = '13:00'
resp.form['end_time'] = '10:00'
resp = resp.form.submit()
assert 'End time must come after start time.' in resp.text
# edit
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('Monday 24 October 2022 / 10 a.m. → 5 p.m.', index=0)
assert 'Unique period' in resp.text
resp.form['date'] = '2022-10-25'
resp = resp.form.submit().follow()
assert 'Tuesday 25' in resp.text
# delete
resp = resp.click('remove', href='timeperiods')
resp = resp.form.submit().follow()
assert 'Tuesday 25' not in resp.text
@pytest.mark.freeze_time('2022-10-24 10:00')
def test_meetings_agenda_date_time_period_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')
assert agenda.is_available_for_simple_management() is True
app = login(app)
resp = app.get('/manage/agendas/%s/desk/%s/add-date-time-period' % (agenda.pk, desk.pk))
resp.form['date'] = '2022-10-24'
resp.form['start_time'] = '10:00'
resp.form['end_time'] = '13:00'
resp = resp.form.submit().follow()
assert TimePeriod.objects.filter(desk=desk).count() == 1
assert TimePeriod.objects.filter(desk=desk2).count() == 1
# edit
resp = resp.click('Monday 24')
resp.form['date'] = '2022-10-25'
resp.form['start_time'] = '11:00'
resp = resp.form.submit().follow()
assert TimePeriod.objects.filter(desk=desk, date__day=25, start_time__hour=11).count() == 1
assert TimePeriod.objects.filter(desk=desk2, date__day=25, start_time__hour=11).count() == 1
# delete
resp = resp.click('remove', href='timeperiods')
resp = resp.form.submit().follow()
assert TimePeriod.objects.count() == 0
@pytest.mark.freeze_time('2022-10-23')
def test_meetings_agenda_date_time_period_display(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
TimePeriod.objects.create(
desk=desk, weekday=6, start_time=datetime.time(14, 0), end_time=datetime.time(16, 0)
) # repeating period on Sunday
TimePeriod.objects.create(
desk=desk,
date=datetime.date(2022, 10, 24),
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
) # unique period on next Monday
TimePeriod.objects.create(
desk=desk,
date=datetime.date(2022, 10, 25),
start_time=datetime.time(8, 0),
end_time=datetime.time(10, 0),
) # unique period on next Tuesday
TimePeriod.objects.create(
desk=desk,
date=datetime.date(2022, 10, 17),
start_time=datetime.time(8, 0),
end_time=datetime.time(10, 0),
) # unique period on past Monday
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
assert 'Sunday / 2 p.m. → 4 p.m.' in resp.text
assert 'Monday 24 October 2022 / 10 a.m. → noon' in resp.text
assert 'Tuesday 25 October 2022 / 8 a.m. → 10 a.m.' in resp.text
# past unique periods are not displayed
assert '17 October' not in resp.text
# unique periods are displayed after repeating periods
assert resp.text.index('Sunday') < resp.text.index('Monday') < resp.text.index('Tuesday')
@pytest.mark.freeze_time('2022-10-23')
def test_meetings_agenda_date_time_period_list(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
TimePeriod.objects.create(
desk=desk,
date=datetime.date(2022, 10, 24),
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
) # unique period on next Monday
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
assert 'Monday 24 October 2022 / 10 a.m. → noon' in resp.text
assert not 'see all unique periods' in resp.text
TimePeriod.objects.create(
desk=desk,
date=datetime.date(2022, 10, 17),
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0),
) # unique period on past Monday
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
assert '17 October' not in resp.text
resp = resp.click('see all unique periods')
assert 'Monday 24 October 2022 / 10 a.m. → noon' in resp.text
assert 'Monday 17 October 2022 / 10 a.m. → noon' in resp.text
assert resp.text.index('24 October') < resp.text.index('17 October')