From ecca514aeb2e2850e5bb1e438b5e1a4a974b63df Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Thu, 13 Oct 2022 16:19:13 +0200 Subject: [PATCH 4/7] manager: add date time period support in settings (#70185) --- chrono/agendas/models.py | 12 +- chrono/manager/forms.py | 21 ++- chrono/manager/static/css/style.scss | 2 +- .../chrono/manager_date_time_period_form.html | 32 ++++ .../chrono/manager_date_time_period_list.html | 31 ++++ .../manager_meetings_agenda_settings.html | 7 +- chrono/manager/urls.py | 10 ++ chrono/manager/views.py | 58 ++++++- tests/manager/test_timeperiod.py | 147 ++++++++++++++++++ 9 files changed, 311 insertions(+), 9 deletions(-) create mode 100644 chrono/manager/templates/chrono/manager_date_time_period_form.html create mode 100644 chrono/manager/templates/chrono/manager_date_time_period_list.html diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index a839687a..9ad52697 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -794,7 +794,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': @@ -806,7 +806,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( diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index dbb8398d..d7173a13 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -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'), diff --git a/chrono/manager/static/css/style.scss b/chrono/manager/static/css/style.scss index 1e1c1abd..9215c866 100644 --- a/chrono/manager/static/css/style.scss +++ b/chrono/manager/static/css/style.scss @@ -78,7 +78,7 @@ h2 span.identifier { padding-right: 1ex; } -a.timeperiod-exception-all { +a.timeperiod-list-all { font-style: italic; } diff --git a/chrono/manager/templates/chrono/manager_date_time_period_form.html b/chrono/manager/templates/chrono/manager_date_time_period_form.html new file mode 100644 index 00000000..3ff939d8 --- /dev/null +++ b/chrono/manager/templates/chrono/manager_date_time_period_form.html @@ -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 %} + {{object}} + {% else %} + {% trans "Unique period" %} + {% endif %} +{% endblock %} + +{% block appbar %} +

{% trans "Unique period" %}

+{% endblock %} + +{% block content %} + +
+ {% csrf_token %} + {{ form|with_template }} +
+ + {% trans 'Cancel' %} +
+
+{% endblock %} diff --git a/chrono/manager/templates/chrono/manager_date_time_period_list.html b/chrono/manager/templates/chrono/manager_date_time_period_list.html new file mode 100644 index 00000000..0f25f4b8 --- /dev/null +++ b/chrono/manager/templates/chrono/manager_date_time_period_list.html @@ -0,0 +1,31 @@ +{% extends "chrono/manager_agenda_settings.html" %} +{% load i18n %} + +{% block extrascripts %} + {{ block.super }} + {{ form.media }} +{% endblock %} + +{% block breadcrumb %} + {{ block.super }} + {% trans "Unique periods" %} +{% endblock %} + +{% block appbar %} +

{% trans "Unique periods" %}

+{% endblock %} + +{% block content %} +
+ + + {% include "gadjo/pagination.html" %} +
+{% endblock %} diff --git a/chrono/manager/templates/chrono/manager_meetings_agenda_settings.html b/chrono/manager/templates/chrono/manager_meetings_agenda_settings.html index d2382b01..c06389d1 100644 --- a/chrono/manager/templates/chrono/manager_meetings_agenda_settings.html +++ b/chrono/manager/templates/chrono/manager_meetings_agenda_settings.html @@ -93,6 +93,7 @@ {% if not object.desk_simple_management or object.desk_simple_management and forloop.counter == 1 %}
{% 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 %} diff --git a/chrono/manager/urls.py b/chrono/manager/urls.py index cf1b924c..ede639f7 100644 --- a/chrono/manager/urls.py +++ b/chrono/manager/urls.py @@ -286,6 +286,16 @@ urlpatterns = [ views.time_period_delete, name='chrono-manager-time-period-delete', ), + path( + 'agendas//desk//add-date-time-period', + views.agenda_add_date_time_period, + name='chrono-manager-agenda-add-date-time-period', + ), + path( + 'timeperiods/desk//date-time-period-list', + views.agenda_date_time_period_list, + name='chrono-manager-date-time-period-list', + ), path('agendas//add-desk', views.agenda_add_desk, name='chrono-manager-agenda-add-desk'), path('desks//edit', views.desk_edit, name='chrono-manager-desk-edit'), path('desks//delete', views.desk_delete, name='chrono-manager-desk-delete'), diff --git a/chrono/manager/views.py b/chrono/manager/views.py index 60d2258f..19aa5621 100644 --- a/chrono/manager/views.py +++ b/chrono/manager/views.py @@ -109,6 +109,7 @@ from .forms import ( BookingCheckFilterSet, BookingCheckPresenceForm, CustomFieldFormSet, + DateTimePeriodForm, DeskExceptionsImportForm, DeskForm, EventCancelForm, @@ -1701,7 +1702,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): @@ -2728,11 +2729,22 @@ 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() @@ -2754,7 +2766,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() @@ -2765,6 +2780,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 diff --git a/tests/manager/test_timeperiod.py b/tests/manager/test_timeperiod.py index 83f2e9f4..189125ac 100644 --- a/tests/manager/test_timeperiod.py +++ b/tests/manager/test_timeperiod.py @@ -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') -- 2.35.1