Projet

Général

Profil

0004-manager-add-date-time-period-support-in-settings-701.patch

Valentin Deniaud, 25 octobre 2022 10:53

Télécharger (21,2 ko)

Voir les différences:

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
chrono/agendas/models.py
794 794
            end_datetime__gt=min_start,
795 795
        )
796 796

  
797
    def prefetch_desks_and_exceptions(self, with_sources=False):
797
    def prefetch_desks_and_exceptions(self, with_sources=False, min_date=None):
798 798
        if self.kind == 'meetings':
799 799
            desks = self.desk_set.all()
800 800
        elif self.kind == 'virtual':
......
806 806
        else:
807 807
            raise ValueError('does not work with kind %r' % self.kind)
808 808

  
809
        self.prefetched_desks = desks.prefetch_related('timeperiod_set', 'unavailability_calendars')
809
        if min_date:
810
            past_date_time_periods = TimePeriod.objects.filter(desk=OuterRef('pk'), date__lt=min_date)
811
            desks = desks.annotate(has_past_date_time_periods=Exists(past_date_time_periods))
812

  
813
            time_period_queryset = TimePeriod.objects.filter(Q(date__isnull=True) | Q(date__gte=min_date))
814

  
815
        self.prefetched_desks = desks.prefetch_related(
816
            'unavailability_calendars', Prefetch('timeperiod_set', queryset=time_period_queryset)
817
        )
810 818
        if with_sources:
811 819
            self.prefetched_desks = self.prefetched_desks.prefetch_related('timeperiodexceptionsource_set')
812 820
        unavailability_calendar_ids = UnavailabilityCalendar.objects.filter(
chrono/manager/forms.py
842 842
        widget=forms.CheckboxSelectMultiple(),
843 843
    )
844 844

  
845
    def __init__(self, *args, **kwargs):
846
        super().__init__(*args, **kwargs)
847
        if 'date' in self.fields:
848
            del self.fields['repeat']
849
            del self.fields['weekday_indexes']
850

  
845 851
    def clean(self):
846 852
        cleaned_data = super().clean()
847 853

  
848 854
        if cleaned_data['end_time'] <= cleaned_data['start_time']:
849 855
            raise ValidationError(_('End time must come after start time.'))
850 856

  
851
        if cleaned_data['repeat'] == 'every-week':
857
        if cleaned_data.get('repeat') == 'every-week':
852 858
            cleaned_data['weekday_indexes'] = None
853 859

  
854 860
        return cleaned_data
......
868 874
    class Meta:
869 875
        model = TimePeriod
870 876
        widgets = {
877
            'date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
871 878
            'start_time': widgets.TimeWidget(),
872 879
            'end_time': widgets.TimeWidget(),
873 880
        }
......
878 885
        self.old_weekday = self.instance.weekday
879 886
        self.old_start_time = self.instance.start_time
880 887
        self.old_end_time = self.instance.end_time
888
        self.old_date = self.instance.date
881 889

  
882 890
        if self.instance.weekday_indexes:
883 891
            self.fields['repeat'].initial = 'custom'
......
893 901

  
894 902
        for desk in self.instance.desk.agenda.desk_set.exclude(pk=self.instance.desk.pk):
895 903
            timeperiod = desk.timeperiod_set.filter(
896
                weekday=self.old_weekday, start_time=self.old_start_time, end_time=self.old_end_time
904
                weekday=self.old_weekday,
905
                start_time=self.old_start_time,
906
                end_time=self.old_end_time,
907
                date=self.old_date,
897 908
            ).first()
898 909
            if timeperiod is not None:
899 910
                timeperiod.weekday = self.instance.weekday
900 911
                timeperiod.start_time = self.instance.start_time
901 912
                timeperiod.end_time = self.instance.end_time
913
                timeperiod.date = self.instance.date
902 914
                timeperiod.save()
903 915

  
904 916
        return self.instance
905 917

  
906 918

  
919
class DateTimePeriodForm(TimePeriodForm):
920
    class Meta(TimePeriodForm.Meta):
921
        fields = ['date', 'start_time', 'end_time']
922

  
923

  
907 924
class NewDeskForm(forms.ModelForm):
908 925
    copy_from = forms.ModelChoiceField(
909 926
        label=_('Copy settings of desk'),
chrono/manager/static/css/style.scss
78 78
	padding-right: 1ex;
79 79
}
80 80

  
81
a.timeperiod-exception-all {
81
a.timeperiod-list-all {
82 82
    font-style: italic;
83 83
}
84 84

  
chrono/manager/templates/chrono/manager_date_time_period_form.html
1
{% extends "chrono/manager_agenda_view.html" %}
2
{% load i18n gadjo %}
3

  
4
{% block extrascripts %}
5
  {{ block.super }}
6
  {{ form.media }}
7
{% endblock %}
8

  
9
{% block breadcrumb %}
10
  {{ block.super }}
11
  {% if object.id %}
12
    <a href="">{{object}}</a>
13
  {% else %}
14
    <a href="">{% trans "Unique period" %}</a>
15
  {% endif %}
16
{% endblock %}
17

  
18
{% block appbar %}
19
  <h2>{% trans "Unique period" %}</h2>
20
{% endblock %}
21

  
22
{% block content %}
23

  
24
  <form method="post" enctype="multipart/form-data">
25
    {% csrf_token %}
26
    {{ form|with_template }}
27
    <div class="buttons">
28
      <button class="submit-button">{% trans "Save" %}</button>
29
      <a class="cancel" href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Cancel' %}</a>
30
    </div>
31
  </form>
32
{% endblock %}
chrono/manager/templates/chrono/manager_date_time_period_list.html
1
{% extends "chrono/manager_agenda_settings.html" %}
2
{% load i18n %}
3

  
4
{% block extrascripts %}
5
  {{ block.super }}
6
  {{ form.media }}
7
{% endblock %}
8

  
9
{% block breadcrumb %}
10
  {{ block.super }}
11
  <a href=".">{% trans "Unique periods" %}</a>
12
{% endblock %}
13

  
14
{% block appbar %}
15
  <h2>{% trans "Unique periods" %}</h2>
16
{% endblock %}
17

  
18
{% block content %}
19
  <div class="timeperiod">
20
    <ul class="objects-list single-links">
21
      {% for time_period in object_list %}
22
        <li>
23
          <a rel="popup" href="{% url 'chrono-manager-time-period-edit' pk=time_period.id %}">{{ time_period }}</a>
24
          <a rel="popup" class="delete" href="{% url 'chrono-manager-time-period-delete' pk=time_period.id %}">{% trans "remove" %}</a>
25
        </li>
26
      {% endfor %}
27
    </ul>
28

  
29
    {% include "gadjo/pagination.html" %}
30
  </div>
31
{% endblock %}
chrono/manager/templates/chrono/manager_meetings_agenda_settings.html
93 93
          {% if not object.desk_simple_management or object.desk_simple_management and forloop.counter == 1 %}
94 94
            <div class="timeperiod">
95 95
              {% url 'chrono-manager-agenda-add-time-period' agenda_pk=object.pk pk=desk.pk as add_time_period_url %}
96
              {% url 'chrono-manager-agenda-add-date-time-period' agenda_pk=object.pk pk=desk.pk as add_date_time_period_url %}
96 97
              <ul class="objects-list single-links">
97 98
                {% if not object.desk_simple_management and object.prefetched_desks|length > 1 %}
98 99
                  <li>
......
108 109
                    <a rel="popup" class="delete" href="{% url 'chrono-manager-time-period-delete' pk=time_period.id %}">{% trans "remove" %}</a>
109 110
                  </li>
110 111
                {% endfor %}
112
                {% if desk.has_past_date_time_periods %}
113
                  <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>
114
                {% endif %}
111 115
                <li><a class="add" rel="popup" href="{{add_time_period_url}}">{% trans 'Add repeating periods' %}</a></li>
116
                <li><a class="add" rel="popup" href="{{add_date_time_period_url}}">{% trans 'Add a unique period' %}</a></li>
112 117
                {% url 'chrono-manager-agenda-add-time-period-exception' agenda_pk=object.pk pk=desk.pk as add_time_period_exception_url %}
113 118
                <li>
114 119
                  <a><strong>{% trans 'Exceptions' %}</strong></a>
......
123 128
                  </li>
124 129
                {% endfor %}
125 130
                {% if not desk.are_all_exceptions_displayed %}
126
                  <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>
131
                  <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>
127 132
                {% endif %}
128 133
                <li><a class="add" rel="popup" href="{{add_time_period_exception_url}}">{% trans 'Add a time period exception' %}</a></li>
129 134
              </ul>
chrono/manager/urls.py
286 286
        views.time_period_delete,
287 287
        name='chrono-manager-time-period-delete',
288 288
    ),
289
    path(
290
        'agendas/<int:agenda_pk>/desk/<int:pk>/add-date-time-period',
291
        views.agenda_add_date_time_period,
292
        name='chrono-manager-agenda-add-date-time-period',
293
    ),
294
    path(
295
        'timeperiods/desk/<int:pk>/date-time-period-list',
296
        views.agenda_date_time_period_list,
297
        name='chrono-manager-date-time-period-list',
298
    ),
289 299
    path('agendas/<int:pk>/add-desk', views.agenda_add_desk, name='chrono-manager-agenda-add-desk'),
290 300
    path('desks/<int:pk>/edit', views.desk_edit, name='chrono-manager-desk-edit'),
291 301
    path('desks/<int:pk>/delete', views.desk_delete, name='chrono-manager-desk-delete'),
chrono/manager/views.py
109 109
    BookingCheckFilterSet,
110 110
    BookingCheckPresenceForm,
111 111
    CustomFieldFormSet,
112
    DateTimePeriodForm,
112 113
    DeskExceptionsImportForm,
113 114
    DeskForm,
114 115
    EventCancelForm,
......
1701 1702

  
1702 1703
    def get_object(self, *args, **kwargs):
1703 1704
        if self.agenda.kind == 'meetings':
1704
            self.agenda.prefetch_desks_and_exceptions(with_sources=True)
1705
            self.agenda.prefetch_desks_and_exceptions(with_sources=True, min_date=now())
1705 1706
        return self.agenda
1706 1707

  
1707 1708
    def get_context_data(self, **kwargs):
......
2728 2729

  
2729 2730

  
2730 2731
class TimePeriodEditView(ManagedTimePeriodMixin, UpdateView):
2731
    template_name = 'chrono/manager_time_period_form.html'
2732 2732
    model = TimePeriod
2733 2733
    form_class = TimePeriodForm
2734 2734
    tab_anchor = 'time-periods'
2735 2735

  
2736
    def get_form_class(self):
2737
        if self.object.weekday is not None:
2738
            return TimePeriodForm
2739
        else:
2740
            return DateTimePeriodForm
2741

  
2742
    def get_template_names(self):
2743
        if self.object.weekday is not None:
2744
            return ['chrono/manager_time_period_form.html']
2745
        else:
2746
            return ['chrono/manager_date_time_period_form.html']
2747

  
2736 2748

  
2737 2749
time_period_edit = TimePeriodEditView.as_view()
2738 2750

  
......
2754 2766

  
2755 2767
        for desk in time_period.desk.agenda.desk_set.exclude(pk=time_period.desk.pk):
2756 2768
            tp = desk.timeperiod_set.filter(
2757
                weekday=time_period.weekday, start_time=time_period.start_time, end_time=time_period.end_time
2769
                weekday=time_period.weekday,
2770
                start_time=time_period.start_time,
2771
                end_time=time_period.end_time,
2772
                date=time_period.date,
2758 2773
            ).first()
2759 2774
            if tp is not None:
2760 2775
                tp.delete()
......
2765 2780
time_period_delete = TimePeriodDeleteView.as_view()
2766 2781

  
2767 2782

  
2783
class AgendaAddDateTimePeriodView(ManagedDeskMixin, FormView):
2784
    template_name = 'chrono/manager_date_time_period_form.html'
2785
    model = TimePeriod
2786
    form_class = DateTimePeriodForm
2787
    tab_anchor = 'time-periods'
2788

  
2789
    def form_valid(self, form):
2790
        create_kwargs = {
2791
            'date': form.cleaned_data['date'],
2792
            'start_time': form.cleaned_data['start_time'],
2793
            'end_time': form.cleaned_data['end_time'],
2794
        }
2795

  
2796
        if self.desk.agenda.desk_simple_management:
2797
            for desk in self.desk.agenda.desk_set.all():
2798
                TimePeriod.objects.create(desk=desk, **create_kwargs)
2799
        else:
2800
            TimePeriod.objects.create(desk=self.desk, **create_kwargs)
2801

  
2802
        return super().form_valid(form)
2803

  
2804

  
2805
agenda_add_date_time_period = AgendaAddDateTimePeriodView.as_view()
2806

  
2807

  
2808
class AgendaDateTimePeriodListView(ManagedDeskMixin, ListView):
2809
    template_name = 'chrono/manager_date_time_period_list.html'
2810
    model = TimePeriod
2811
    paginate_by = 20
2812

  
2813
    def get_queryset(self):
2814
        return self.model.objects.filter(desk=self.desk, date__isnull=False).order_by('-date')
2815

  
2816

  
2817
agenda_date_time_period_list = AgendaDateTimePeriodListView.as_view()
2818

  
2819

  
2768 2820
class AgendaAddDesk(ManagedAgendaMixin, CreateView):
2769 2821
    template_name = 'chrono/manager_desk_form.html'
2770 2822
    model = Desk
tests/manager/test_timeperiod.py
238 238
    resp = app.get('/manage/timeperiods/%s/delete' % time_period.pk)
239 239
    resp.form.submit()
240 240
    assert TimePeriod.objects.count() == 0
241

  
242

  
243
@pytest.mark.freeze_time('2022-10-24 10:00')
244
def test_meetings_agenda_date_time_period(app, admin_user):
245
    agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
246
    desk = Desk.objects.create(agenda=agenda, label='Desk A')
247
    desk2 = Desk.objects.create(agenda=agenda, label='Desk B')
248
    MeetingType.objects.create(agenda=agenda, label='Blah')
249
    app = login(app)
250
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
251
    resp = resp.click('Add a unique period', index=0)
252
    assert 'repeat' not in resp.form.fields
253
    assert 'weekday' not in resp.form.fields
254
    resp.form['date'] = '2022-10-24'
255
    resp.form['start_time'] = '10:00'
256
    resp.form['end_time'] = '17:00'
257
    resp = resp.form.submit()
258
    assert TimePeriod.objects.get(desk=desk).date == datetime.date(2022, 10, 24)
259
    assert TimePeriod.objects.get(desk=desk).start_time.hour == 10
260
    assert TimePeriod.objects.get(desk=desk).start_time.minute == 0
261
    assert TimePeriod.objects.get(desk=desk).end_time.hour == 17
262
    assert TimePeriod.objects.get(desk=desk).end_time.minute == 0
263
    assert desk2.timeperiod_set.exists() is False
264
    resp = resp.follow()
265

  
266
    # invert start and end
267
    resp = resp.click('Add a unique period', index=0)
268
    resp.form['date'] = '2022-10-24'
269
    resp.form['start_time'] = '13:00'
270
    resp.form['end_time'] = '10:00'
271
    resp = resp.form.submit()
272
    assert 'End time must come after start time.' in resp.text
273

  
274
    # edit
275
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
276
    resp = resp.click('Monday 24 October 2022 / 10 a.m. → 5 p.m.', index=0)
277
    assert 'Unique period' in resp.text
278
    resp.form['date'] = '2022-10-25'
279
    resp = resp.form.submit().follow()
280
    assert 'Tuesday 25' in resp.text
281

  
282
    # delete
283
    resp = resp.click('remove', href='timeperiods')
284
    resp = resp.form.submit().follow()
285
    assert 'Tuesday 25' not in resp.text
286

  
287

  
288
@pytest.mark.freeze_time('2022-10-24 10:00')
289
def test_meetings_agenda_date_time_period_desk_simple_management(app, admin_user):
290
    agenda = Agenda.objects.create(label='Foo bar', kind='meetings', desk_simple_management=True)
291
    desk = Desk.objects.create(agenda=agenda, label='Desk A')
292
    desk2 = Desk.objects.create(agenda=agenda, label='Desk B')
293
    assert agenda.is_available_for_simple_management() is True
294

  
295
    app = login(app)
296
    resp = app.get('/manage/agendas/%s/desk/%s/add-date-time-period' % (agenda.pk, desk.pk))
297
    resp.form['date'] = '2022-10-24'
298
    resp.form['start_time'] = '10:00'
299
    resp.form['end_time'] = '13:00'
300
    resp = resp.form.submit().follow()
301

  
302
    assert TimePeriod.objects.filter(desk=desk).count() == 1
303
    assert TimePeriod.objects.filter(desk=desk2).count() == 1
304

  
305
    # edit
306
    resp = resp.click('Monday 24')
307
    resp.form['date'] = '2022-10-25'
308
    resp.form['start_time'] = '11:00'
309
    resp = resp.form.submit().follow()
310

  
311
    assert TimePeriod.objects.filter(desk=desk, date__day=25, start_time__hour=11).count() == 1
312
    assert TimePeriod.objects.filter(desk=desk2, date__day=25, start_time__hour=11).count() == 1
313

  
314
    # delete
315
    resp = resp.click('remove', href='timeperiods')
316
    resp = resp.form.submit().follow()
317
    assert TimePeriod.objects.count() == 0
318

  
319

  
320
@pytest.mark.freeze_time('2022-10-23')
321
def test_meetings_agenda_date_time_period_display(app, admin_user):
322
    agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
323
    desk = Desk.objects.create(agenda=agenda, label='Desk A')
324
    TimePeriod.objects.create(
325
        desk=desk, weekday=6, start_time=datetime.time(14, 0), end_time=datetime.time(16, 0)
326
    )  # repeating period on Sunday
327
    TimePeriod.objects.create(
328
        desk=desk,
329
        date=datetime.date(2022, 10, 24),
330
        start_time=datetime.time(10, 0),
331
        end_time=datetime.time(12, 0),
332
    )  # unique period on next Monday
333
    TimePeriod.objects.create(
334
        desk=desk,
335
        date=datetime.date(2022, 10, 25),
336
        start_time=datetime.time(8, 0),
337
        end_time=datetime.time(10, 0),
338
    )  # unique period on next Tuesday
339
    TimePeriod.objects.create(
340
        desk=desk,
341
        date=datetime.date(2022, 10, 17),
342
        start_time=datetime.time(8, 0),
343
        end_time=datetime.time(10, 0),
344
    )  # unique period on past Monday
345

  
346
    app = login(app)
347
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
348
    assert 'Sunday / 2 p.m. → 4 p.m.' in resp.text
349
    assert 'Monday 24 October 2022 / 10 a.m. → noon' in resp.text
350
    assert 'Tuesday 25 October 2022 / 8 a.m. → 10 a.m.' in resp.text
351

  
352
    # past unique periods are not displayed
353
    assert '17 October' not in resp.text
354

  
355
    # unique periods are displayed after repeating periods
356
    assert resp.text.index('Sunday') < resp.text.index('Monday') < resp.text.index('Tuesday')
357

  
358

  
359
@pytest.mark.freeze_time('2022-10-23')
360
def test_meetings_agenda_date_time_period_list(app, admin_user):
361
    agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
362
    desk = Desk.objects.create(agenda=agenda, label='Desk A')
363
    TimePeriod.objects.create(
364
        desk=desk,
365
        date=datetime.date(2022, 10, 24),
366
        start_time=datetime.time(10, 0),
367
        end_time=datetime.time(12, 0),
368
    )  # unique period on next Monday
369

  
370
    app = login(app)
371
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
372
    assert 'Monday 24 October 2022 / 10 a.m. → noon' in resp.text
373
    assert not 'see all unique periods' in resp.text
374

  
375
    TimePeriod.objects.create(
376
        desk=desk,
377
        date=datetime.date(2022, 10, 17),
378
        start_time=datetime.time(10, 0),
379
        end_time=datetime.time(12, 0),
380
    )  # unique period on past Monday
381
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
382
    assert '17 October' not in resp.text
383

  
384
    resp = resp.click('see all unique periods')
385
    assert 'Monday 24 October 2022 / 10 a.m. → noon' in resp.text
386
    assert 'Monday 17 October 2022 / 10 a.m. → noon' in resp.text
387
    assert resp.text.index('24 October') < resp.text.index('17 October')
241
-