Projet

Général

Profil

0002-manager-timesheet-for-one-event-66358.patch

Lauréline Guérin, 17 juin 2022 15:23

Télécharger (20,8 ko)

Voir les différences:

Subject: [PATCH 2/2] manager: timesheet for one event (#66358)

 chrono/manager/forms.py                       |  43 +++--
 .../chrono/manager_event_detail.html          |  31 ++--
 .../chrono/manager_event_timesheet.html       |  13 ++
 .../chrono/manager_events_timesheet.html      |  39 +---
 ...anager_events_timesheet_form_fragment.html |  44 +++++
 chrono/manager/urls.py                        |   5 +
 chrono/manager/views.py                       |  34 +++-
 tests/manager/test_event_timesheet.py         | 171 ++++++++++++++++++
 8 files changed, 305 insertions(+), 75 deletions(-)
 create mode 100644 chrono/manager/templates/chrono/manager_event_timesheet.html
 create mode 100644 chrono/manager/templates/chrono/manager_events_timesheet_form_fragment.html
chrono/manager/forms.py
584 584

  
585 585
    def __init__(self, *args, **kwargs):
586 586
        self.agenda = kwargs.pop('agenda')
587
        self.event = kwargs.pop('event', None)
587 588
        super().__init__(*args, **kwargs)
589
        if self.event is not None:
590
            del self.fields['date_start']
591
            del self.fields['date_end']
588 592

  
589 593
    def get_slots(self):
590 594
        extra_data = self.cleaned_data['extra_data'].split(',')
......
593 597
        all_extra_data = extra_data[:]
594 598
        if group_by:
595 599
            all_extra_data += [group_by]
596
        min_start = make_aware(
597
            datetime.datetime.combine(self.cleaned_data['date_start'], datetime.time(0, 0))
598
        )
599
        max_start = make_aware(datetime.datetime.combine(self.cleaned_data['date_end'], datetime.time(0, 0)))
600
        max_start = max_start + datetime.timedelta(days=1)
601

  
602
        # fetch all events in this range
603
        all_events = (
604
            self.agenda.event_set.filter(
605
                recurrence_days__isnull=True,
606
                start_datetime__gte=min_start,
607
                start_datetime__lt=max_start,
608
                cancelled=False,
600
        if self.event is not None:
601
            all_events = [self.event]
602
            min_start = self.event.start_datetime
603
            max_start = min_start + datetime.timedelta(days=1)
604
        else:
605
            min_start = make_aware(
606
                datetime.datetime.combine(self.cleaned_data['date_start'], datetime.time(0, 0))
607
            )
608
            max_start = make_aware(
609
                datetime.datetime.combine(self.cleaned_data['date_end'], datetime.time(0, 0))
610
            )
611
            max_start = max_start + datetime.timedelta(days=1)
612

  
613
            # fetch all events in this range
614
            all_events = (
615
                self.agenda.event_set.filter(
616
                    recurrence_days__isnull=True,
617
                    start_datetime__gte=min_start,
618
                    start_datetime__lt=max_start,
619
                    cancelled=False,
620
                )
621
                .select_related('primary_event')
622
                .order_by('start_datetime', 'label')
609 623
            )
610
            .select_related('primary_event')
611
            .order_by('start_datetime', 'label')
612
        )
613 624
        dates = set()
614 625
        events = []
615 626
        dates_per_event_id = defaultdict(list)
chrono/manager/templates/chrono/manager_event_detail.html
7 7
{% endblock %}
8 8

  
9 9
{% block page-title-extra-label %}
10
- {% firstof agenda.label object.label %}
10
- {% firstof agenda.label event.label %}
11 11
{% endblock %}
12 12

  
13 13
{% block breadcrumb %}
14 14
{{ block.super }}
15 15
<a href="{% url 'chrono-manager-agenda-view' pk=agenda.id %}">{{agenda.label}}</a>
16
<a href="{% url 'chrono-manager-event-view' pk=agenda.id event_pk=object.id %}">{{object}}</a>
16
<a href="{% url 'chrono-manager-event-view' pk=agenda.id event_pk=event.id %}">{{event}}</a>
17 17
{% endblock %}
18 18

  
19 19
{% block appbar %}
20 20
<h2>
21
{% if object.label %}
22
{{ object.label }} — {{object.start_datetime|date:"DATETIME_FORMAT"}}
21
{% if event.label %}
22
{{ event.label }} — {{event.start_datetime|date:"DATETIME_FORMAT"}}
23 23
{% else %}
24
{{ object.start_datetime|date:"DATETIME_FORMAT"}}
24
{{ event.start_datetime|date:"DATETIME_FORMAT"}}
25 25
{% endif %}
26
{% if object.cancellation_status %}<span class="tag">{{ event.cancellation_status }}</span>{% endif %}
26
{% if event.cancellation_status %}<span class="tag">{{ event.cancellation_status }}</span>{% endif %}
27 27
{% if event.main_list_full %}<span class="tag">{% trans "Full" %}</span>{% endif %}
28 28
{% if event.checked %}<span class="tag">{% trans "Checked" %}</span>{% endif %}
29 29
</h2>
30 30
{% block appbar_actions %}
31 31
<span class="actions">
32
  {% if user_can_manage or object.agenda.booking_form_url %}
32
  {% if user_can_manage or event.agenda.booking_form_url %}
33 33
  <a class="extra-actions-menu-opener"></a>
34 34
  <ul class="extra-actions-menu">
35 35
    {% if user_can_manage %}
36
    {% if not object.primary_event %}
37
    <li><a rel="popup" href="{% url 'chrono-manager-event-delete' pk=object.agenda.id event_pk=object.id %}">{% trans 'Delete' %}</a></li>
36
    {% if not event.primary_event %}
37
    <li><a rel="popup" href="{% url 'chrono-manager-event-delete' pk=event.agenda.id event_pk=event.id %}">{% trans 'Delete' %}</a></li>
38 38
    {% endif %}
39 39
    {% if not event.cancellation_status %}
40 40
    <li><a rel="popup" href="{% url 'chrono-manager-event-cancel' pk=agenda.pk event_pk=event.pk %}?next={{ request.path }}">{% trans "Cancel" %}</a></li>
41 41
    {% endif %}
42
    <li><a href="{% url 'chrono-manager-event-edit' pk=agenda.id event_pk=object.id %}">{% trans "Options" %}</a></li>
42
    <li><a href="{% url 'chrono-manager-event-edit' pk=agenda.id event_pk=event.id %}">{% trans "Options" %}</a></li>
43 43
    {% endif %}
44
    {% if object.agenda.booking_form_url %}
45
    <li><a href="{{ object.get_booking_form_url }}&ReturnURL={{ request.build_absolute_uri }}">{% trans "Booking form" %}</a></li>
44
    {% if event.agenda.booking_form_url %}
45
    <li><a href="{{ event.get_booking_form_url }}&ReturnURL={{ request.build_absolute_uri }}">{% trans "Booking form" %}</a></li>
46
    {% endif %}
47
    {% if not event.cancelled %}
48
    <li><a href="{% url 'chrono-manager-event-timesheet' pk=agenda.pk event_pk=event.pk %}">{% trans "Timesheet" %}</a></li>
46 49
    {% endif %}
47 50
  </ul>
48 51
  {% endif %}
49
  {% if object.is_day_past and not object.cancelled %}
50
  <a href="{% url 'chrono-manager-event-check' pk=agenda.pk event_pk=object.pk %}">{% trans "Check" %}</a>
52
  {% if event.is_day_past and not event.cancelled %}
53
  <a href="{% url 'chrono-manager-event-check' pk=agenda.pk event_pk=event.pk %}">{% trans "Check" %}</a>
51 54
  {% endif %}
52 55
</span>
53 56
{% endblock %}
chrono/manager/templates/chrono/manager_event_timesheet.html
1
{% extends "chrono/manager_event_detail.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'chrono-manager-event-timesheet' pk=agenda.pk event_pk=event.pk %}">{% trans "Timesheet" %}</a>
7
{% endblock %}
8

  
9
{% block appbar_actions %}{% endblock %}
10

  
11
{% block content %}
12
  {% include 'chrono/manager_events_timesheet_form_fragment.html' %}
13
{% endblock %}
chrono/manager/templates/chrono/manager_events_timesheet.html
9 9
{% block appbar_actions %}{% endblock %}
10 10

  
11 11
{% block content %}
12
<div class="section">
13
  <h3>{% trans "Timesheet configuration" %}</h3>
14
  <div>
15
    <form id="timesheet">
16
      {{ form.as_p }}
17
      <script>
18
      $(function() {
19
        $('#id_date_display').on('change', function() {
20
          if ($(this).val() == 'custom') {
21
            $('#id_custom_nb_dates_per_page').parent().show();
22
          } else {
23
            $('#id_custom_nb_dates_per_page').parent().hide();
24
          }
25
        });
26
        $('#id_date_display').trigger('change');
27
        $('#id_group_by').on('change', function() {
28
          if ($(this).val()) {
29
            $('#id_with_page_break').parent().show();
30
          } else {
31
            $('#id_with_page_break').parent().hide();
32
          }
33
        });
34
        $('#id_group_by').trigger('change');
35
      });
36
      </script>
37
      <button class="submit-button">{% trans "See timesheet" %}</button>
38
      {% if request.GET and form.is_valid %}
39
      <button class="submit-button" name="pdf">{% trans "Get PDF file" %}</button>
40
      {% endif %}
41
    </form>
42

  
43
    {% if request.GET and form.is_valid %}
44
    <h4>{% blocktrans with start=form.cleaned_data.date_start end=form.cleaned_data.date_end %}Timesheet from {{ start }} to {{ end }}{% endblocktrans %}</h4>
45

  
46
    {% include 'chrono/manager_events_timesheet_fragment.html' %}
47
    {% endif %}
48
  </div>
49
</div>
12
  {% include 'chrono/manager_events_timesheet_form_fragment.html' %}
50 13
{% endblock %}
chrono/manager/templates/chrono/manager_events_timesheet_form_fragment.html
1
{% load i18n %}
2

  
3
<div class="section">
4
  <h3>{% trans "Timesheet configuration" %}</h3>
5
  <div>
6
    <form id="timesheet">
7
      {{ form.as_p }}
8
      <script>
9
      $(function() {
10
        $('#id_date_display').on('change', function() {
11
          if ($(this).val() == 'custom') {
12
            $('#id_custom_nb_dates_per_page').parent().show();
13
          } else {
14
            $('#id_custom_nb_dates_per_page').parent().hide();
15
          }
16
        });
17
        $('#id_date_display').trigger('change');
18
        $('#id_group_by').on('change', function() {
19
          if ($(this).val()) {
20
            $('#id_with_page_break').parent().show();
21
          } else {
22
            $('#id_with_page_break').parent().hide();
23
          }
24
        });
25
        $('#id_group_by').trigger('change');
26
      });
27
      </script>
28
      <button class="submit-button">{% trans "See timesheet" %}</button>
29
      {% if request.GET and form.is_valid %}
30
      <button class="submit-button" name="pdf">{% trans "Get PDF file" %}</button>
31
      {% endif %}
32
    </form>
33

  
34
    {% if request.GET and form.is_valid %}
35
    {% if event %}
36
    <h4>{% blocktrans %}Timesheet{% endblocktrans %}</h4>
37
    {% else %}
38
    <h4>{% blocktrans with start=form.cleaned_data.date_start end=form.cleaned_data.date_end %}Timesheet from {{ start }} to {{ end }}{% endblocktrans %}</h4>
39
    {% endif %}
40

  
41
    {% include 'chrono/manager_events_timesheet_fragment.html' %}
42
    {% endif %}
43
  </div>
44
</div>
chrono/manager/urls.py
265 265
        views.event_checked,
266 266
        name='chrono-manager-event-checked',
267 267
    ),
268
    url(
269
        r'^agendas/(?P<pk>\d+)/events/(?P<event_pk>\d+)/timesheet$',
270
        views.events_timesheet,
271
        name='chrono-manager-event-timesheet',
272
    ),
268 273
    url(
269 274
        r'^agendas/(?P<pk>\d+)/event_cancellation_report/(?P<report_pk>\d+)/$',
270 275
        views.event_cancellation_report,
chrono/manager/views.py
2233 2233

  
2234 2234
class EventsTimesheetView(ViewableAgendaMixin, DetailView):
2235 2235
    model = Agenda
2236
    template_name = 'chrono/manager_events_timesheet.html'
2237 2236

  
2238 2237
    def set_agenda(self, **kwargs):
2239 2238
        self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
2239
        self.event = None
2240
        if 'event_pk' in kwargs:
2241
            self.event = get_object_or_404(
2242
                Event,
2243
                pk=kwargs.get('event_pk'),
2244
                agenda=self.agenda,
2245
                recurrence_days__isnull=True,
2246
                cancelled=False,
2247
            )
2240 2248

  
2241 2249
    def get_object(self, **kwargs):
2242 2250
        return self.agenda
2243 2251

  
2244 2252
    def get_context_data(self, **kwargs):
2245 2253
        context = super().get_context_data(**kwargs)
2246
        form = EventsTimesheetForm(agenda=self.agenda, data=self.request.GET or None)
2254
        form = EventsTimesheetForm(agenda=self.agenda, event=self.event, data=self.request.GET or None)
2247 2255
        if self.request.GET:
2248 2256
            form.is_valid()
2249 2257
        context['form'] = form
2258
        context['event'] = self.event
2250 2259
        return context
2251 2260

  
2261
    def get_template_names(self):
2262
        if self.event is not None:
2263
            return ['chrono/manager_event_timesheet.html']
2264
        return ['chrono/manager_events_timesheet.html']
2265

  
2252 2266
    def get(self, request, *args, **kwargs):
2253 2267
        self.object = self.get_object()
2254 2268
        context = self.get_context_data(object=self.object)
......
2263 2277
        )
2264 2278
        pdf = html.write_pdf()
2265 2279
        response = HttpResponse(pdf, content_type='application/pdf')
2266
        response['Content-Disposition'] = 'attachment; filename="timesheet_{}_{}_{}.pdf"'.format(
2267
            self.agenda.slug,
2268
            context['form'].cleaned_data['date_start'].strftime('%Y-%m-%d'),
2269
            context['form'].cleaned_data['date_end'].strftime('%Y-%m-%d'),
2270
        )
2280
        if self.event is not None:
2281
            response['Content-Disposition'] = 'attachment; filename="timesheet_{}_{}.pdf"'.format(
2282
                self.agenda.slug,
2283
                self.event.slug,
2284
            )
2285
        else:
2286
            response['Content-Disposition'] = 'attachment; filename="timesheet_{}_{}_{}.pdf"'.format(
2287
                self.agenda.slug,
2288
                context['form'].cleaned_data['date_start'].strftime('%Y-%m-%d'),
2289
                context['form'].cleaned_data['date_end'].strftime('%Y-%m-%d'),
2290
            )
2271 2291
        return response
2272 2292

  
2273 2293

  
tests/manager/test_event_timesheet.py
896 896
        % agenda.pk
897 897
    )
898 898
    assert resp.context['form'].errors['orientation'] == ['This field is required.']
899

  
900

  
901
def test_event_timesheet_wrong_kind(app, admin_user):
902
    agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
903
    event = Event.objects.create(
904
        start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda
905
    )
906

  
907
    app = login(app)
908
    app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk), status=404)
909
    agenda.kind = 'virtual'
910
    agenda.save()
911
    app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk), status=404)
912

  
913

  
914
def test_event_timesheet_wrong_event(app, admin_user):
915
    agenda = Agenda.objects.create(label='Events', kind='events')
916
    agenda2 = Agenda.objects.create(label='Events', kind='events')
917
    event = Event.objects.create(
918
        start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)),
919
        places=10,
920
        agenda=agenda,
921
        cancelled=True,
922
    )
923

  
924
    app = login(app)
925
    app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk), status=404)
926
    event.cancelled = False
927
    event.recurrence_days = [1]
928
    event.save()
929
    app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk), status=404)
930

  
931
    event.recurrence_days = None
932
    event.save()
933
    app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk), status=200)
934

  
935
    app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda2.pk, event.pk), status=404)
936
    app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, 0), status=404)
937

  
938

  
939
def test_event_timesheet_form(app, admin_user):
940
    agenda = Agenda.objects.create(label='Events', kind='events')
941
    event = Event.objects.create(
942
        start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda
943
    )
944

  
945
    app = login(app)
946
    resp = app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk))
947
    assert 'date_start' not in resp.context['form'].fields
948
    assert 'date_end' not in resp.context['form'].fields
949
    assert resp.context['form'].errors == {}
950

  
951

  
952
def test_event_timesheet_slots(app, admin_user):
953
    agenda = Agenda.objects.create(label='Events', kind='events')
954
    event = Event.objects.create(
955
        start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda
956
    )
957
    Subscription.objects.create(
958
        agenda=agenda,
959
        user_external_id='user:1',
960
        user_first_name='Subscription',
961
        user_last_name='42',
962
        date_start=datetime.date(2022, 2, 15),
963
        date_end=datetime.date(2022, 2, 16),
964
    )
965

  
966
    login(app)
967
    resp = app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk))
968
    with CaptureQueriesContext(connection) as ctx:
969
        resp = resp.form.submit()
970
        assert len(ctx.captured_queries) == 7
971

  
972
    slots = resp.context['form'].get_slots()
973
    assert slots['dates'] == [
974
        [
975
            datetime.date(2022, 2, 15),
976
        ]
977
    ]
978
    assert slots['events'] == [event]
979
    assert slots['users'][0]['users'] == [
980
        {
981
            'user_id': 'user:1',
982
            'user_first_name': 'Subscription',
983
            'user_last_name': '42',
984
            'extra_data': {},
985
            'events': [
986
                {
987
                    'event': event,
988
                    'dates': {datetime.date(2022, 2, 15): False},
989
                },
990
            ],
991
        },
992
    ]
993
    assert slots['extra_data'] == []
994

  
995

  
996
def test_event_timesheet_subscription_limits(app, admin_user):
997
    agenda = Agenda.objects.create(label='Events', kind='events')
998
    event = Event.objects.create(
999
        start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)), places=10, agenda=agenda
1000
    )
1001

  
1002
    dates = [
1003
        ('2022-01-31', '2022-02-01'),
1004
        ('2022-02-01', '2022-02-02'),
1005
        ('2022-02-01', '2022-02-15'),
1006
        ('2022-02-01', '2022-02-16'),
1007
        ('2022-02-15', '2022-02-28'),
1008
        ('2022-02-15', '2022-03-01'),
1009
        ('2022-02-16', '2022-03-01'),
1010
        ('2022-02-01', '2022-03-01'),
1011
        ('2022-02-28', '2022-03-01'),
1012
        ('2022-03-01', '2022-03-02'),
1013
    ]
1014

  
1015
    for start, end in dates:
1016
        Subscription.objects.create(
1017
            agenda=agenda,
1018
            user_external_id='user:%s-%s' % (start, end),
1019
            user_first_name='Subscription',
1020
            user_last_name='%s - %s' % (start, end),
1021
            date_start=datetime.datetime.strptime(start, '%Y-%m-%d'),
1022
            date_end=datetime.datetime.strptime(end, '%Y-%m-%d'),
1023
        )
1024

  
1025
    login(app)
1026
    resp = app.get('/manage/agendas/%s/events/%s/timesheet' % (agenda.pk, event.pk))
1027
    resp = resp.form.submit()
1028

  
1029
    slots = resp.context['form'].get_slots()
1030
    assert slots['dates'] == [
1031
        [
1032
            datetime.date(2022, 2, 15),
1033
        ]
1034
    ]
1035

  
1036
    assert slots['events'] == [
1037
        event,
1038
    ]
1039
    users = slots['users'][0]['users']
1040
    assert len(users) == 4
1041
    assert users[0]['user_id'] == 'user:2022-02-01-2022-02-16'
1042
    assert users[1]['user_id'] == 'user:2022-02-01-2022-03-01'
1043
    assert users[2]['user_id'] == 'user:2022-02-15-2022-02-28'
1044
    assert users[3]['user_id'] == 'user:2022-02-15-2022-03-01'
1045

  
1046

  
1047
def test_event_timesheet_pdf(app, admin_user):
1048
    agenda = Agenda.objects.create(label='Foo', kind='events')
1049
    event = Event.objects.create(
1050
        label='Bar',
1051
        start_datetime=make_aware(datetime.datetime(2022, 2, 15, 17, 0)),
1052
        places=10,
1053
        agenda=agenda,
1054
    )
1055

  
1056
    login(app)
1057
    resp = app.get(
1058
        '/manage/agendas/%s/events/%s/timesheet?pdf=&sort=lastname,firstname&date_display=all&orientation=portrait'
1059
        % (agenda.pk, event.pk)
1060
    )
1061
    assert resp.headers['Content-Type'] == 'application/pdf'
1062
    assert resp.headers['Content-Disposition'] == 'attachment; filename="timesheet_foo_bar.pdf"'
1063

  
1064
    # form invalid
1065
    resp = app.get(
1066
        '/manage/agendas/%s/events/%s/timesheet?pdf=&sort=lastname,firstname&date_display=all'
1067
        % (agenda.pk, event.pk)
1068
    )
1069
    assert resp.context['form'].errors['orientation'] == ['This field is required.']
899
-