Projet

Général

Profil

0004-manager-timesheet-to-PDF-61070.patch

Lauréline Guérin, 18 février 2022 15:32

Télécharger (11,2 ko)

Voir les différences:

Subject: [PATCH 4/5] manager: timesheet to PDF (#61070)

 .gitignore                                    |  1 +
 chrono/manager/forms.py                       |  8 ++++
 chrono/manager/static/css/timesheet.scss      | 38 +++++++++++++++++++
 .../chrono/manager_events_timesheet.html      | 38 +++----------------
 .../manager_events_timesheet_fragment.html    | 32 ++++++++++++++++
 .../chrono/manager_events_timesheet_pdf.html  | 22 +++++++++++
 chrono/manager/views.py                       | 22 +++++++++++
 debian/control                                |  1 +
 setup.py                                      |  1 +
 tests/manager/test_event.py                   | 22 +++++++++++
 10 files changed, 153 insertions(+), 32 deletions(-)
 create mode 100644 chrono/manager/static/css/timesheet.scss
 create mode 100644 chrono/manager/templates/chrono/manager_events_timesheet_fragment.html
 create mode 100644 chrono/manager/templates/chrono/manager_events_timesheet_pdf.html
.gitignore
4 4
/dist
5 5
/chrono.egg-info
6 6
/chrono/manager/static/css/style.css
7
/chrono/manager/static/css/timesheet.css
chrono/manager/forms.py
376 376
        required=False,
377 377
        help_text=_('Comma separated list of keys defined in extra_data.'),
378 378
    )
379
    orientation = forms.ChoiceField(
380
        label=_('PDF orientation'),
381
        choices=[
382
            ('portrait', _('Portrait')),
383
            ('landscape', _('Landscape')),
384
        ],
385
        initial='portrait',
386
    )
379 387

  
380 388
    def __init__(self, *args, **kwargs):
381 389
        self.agenda = kwargs.pop('agenda')
chrono/manager/static/css/timesheet.scss
1
@charset "UTF-8";
2
@page {
3
	margin: 0.5cm;
4
	margin-bottom: 1cm;
5
	@bottom-right {
6
		font-size: 10pt;
7
		content: counter(page) " / " counter(pages);
8
		height: 1cm;
9
		text-align: right;
10
		width: 2cm;
11
	}
12
}
13

  
14
body {
15
	font-family: sans-serif;
16
	font-size: 8pt;
17
}
18

  
19
table.timesheet {
20
	border-collapse: collapse;
21
	th {
22
		padding: 0.5em 0.5ex;
23
		border: 0.5px solid black;
24
		&.date {
25
			width: 30px;
26
			text-align: center;
27
		}
28
	}
29
	td {
30
		border: 0.5px solid black;
31
		padding: 0.5em 0.5ex;
32
		&.date {
33
			text-align: center;
34
			padding: 0px;
35
			max-width: 2em;
36
		}
37
	}
38
}
chrono/manager/templates/chrono/manager_events_timesheet.html
1 1
{% extends "chrono/manager_agenda_view.html" %}
2
{% load i18n chrono %}
2
{% load i18n %}
3 3

  
4 4
{% block breadcrumb %}
5 5
{{ block.super }}
......
12 12
<div class="section">
13 13
  <h3>{% trans "Timesheet configuration" %}</h3>
14 14
  <div>
15
    <form>
15
    <form id="timesheet">
16 16
      {{ form.as_p }}
17 17
      <button class="submit-button">{% trans "See timesheet" %}</button>
18
      {% if request.GET and form.is_valid %}
19
      <button class="submit-button" name="pdf">{% trans "Get PDF file" %}</button>
20
      {% endif %}
18 21
    </form>
19 22

  
20 23
    {% if request.GET and form.is_valid %}
21 24
    <h4>{% blocktrans with start=form.cleaned_data.date_start end=form.cleaned_data.date_end %}Timesheet from {{ start }} to {{ end }}{% endblocktrans %}</h4>
22 25

  
23
    {% with slots=form.get_slots %}
24
    {% with events_num=slots.events|length %}
25
    <table class="main timesheet">
26
      <thead>
27
        <tr>
28
          <th>{% trans "First name" %}</th>
29
          <th>{% trans "Last name" %}</th>
30
          {% for k in slots.extra_data %}<th>{{ k }}</th>{% endfor %}
31
          {% if events_num > 1 %}<th>{% trans "Activity" %}</th>{% endif %}
32
          {% for date in slots.dates %}<th class="date">{{ date|date:"D d/m" }}</th>{% endfor %}
33
        </tr>
34
      </thead>
35
      <tbody>
36
        {% for user in slots.users %}{% for event in user.events %}
37
        <tr>
38
          {% if forloop.first %}
39
          <td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_first_name }}</td>
40
          <td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_last_name }}</td>
41
          {% for k in slots.extra_data %}<td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.extra_data|get:k }}</td>{% endfor %}
42
          {% endif %}
43
          {% if events_num > 1 %}<td>{{ event.event }}</td>{% endif %}
44
          {% for date in slots.dates %}
45
          {% with booked=event.dates|get:date %}<td class="date">{% if booked is True %}☐{% elif booked is None %}-{% endif %}</td>{% endwith %}
46
          {% endfor %}
47
        </tr>
48
        {% endfor %}{% endfor %}
49
      </tbody>
50
    </table>
51
    {% endwith %}
52
    {% endwith %}
26
    {% include 'chrono/manager_events_timesheet_fragment.html' %}
53 27
    {% endif %}
54 28
  </div>
55 29
</div>
chrono/manager/templates/chrono/manager_events_timesheet_fragment.html
1
{% load i18n chrono %}
2

  
3
{% with slots=form.get_slots %}
4
{% with events_num=slots.events|length %}
5
<table class="main timesheet">
6
  <thead>
7
    <tr>
8
      <th>{% trans "First name" %}</th>
9
      <th>{% trans "Last name" %}</th>
10
      {% for k in slots.extra_data %}<th>{{ k }}</th>{% endfor %}
11
      {% if events_num > 1 %}<th>{% trans "Activity" %}</th>{% endif %}
12
      {% for date in slots.dates %}<th class="date">{{ date|date:"D d/m" }}</th>{% endfor %}
13
    </tr>
14
  </thead>
15
  <tbody>
16
    {% for user in slots.users %}{% for event in user.events %}
17
    <tr>
18
      {% if forloop.first %}
19
      <td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_first_name }}</td>
20
      <td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.user_last_name }}</td>
21
      {% for k in slots.extra_data %}<td {% if events_num > 1 %}rowspan="{{ events_num }}"{% endif %}>{{ user.extra_data|get:k }}</td>{% endfor %}
22
      {% endif %}
23
      {% if events_num > 1 %}<td>{{ event.event }}</td>{% endif %}
24
      {% for date in slots.dates %}
25
      {% with booked=event.dates|get:date %}<td class="date">{% if booked is True %}☐{% elif booked is None %}-{% endif %}</td>{% endwith %}
26
      {% endfor %}
27
    </tr>
28
    {% endfor %}{% endfor %}
29
  </tbody>
30
</table>
31
{% endwith %}
32
{% endwith %}
chrono/manager/templates/chrono/manager_events_timesheet_pdf.html
1
{% load static i18n %}
2
<html>
3
<head>
4
  <base href="{{ base_uri }}" />
5
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
6
  <title>{% blocktrans with start=form.cleaned_data.date_start end=form.cleaned_data.date_end %}Timesheet from {{ start }} to {{ end }}{% endblocktrans %}</title>
7
  <meta name="author" content="Entr'ouvert" />
8
  <link href="{% static 'css/timesheet.css' %}" media="print" rel="stylesheet" />
9
  <style media="print">
10
    @page {
11
        size: {{ form.cleaned_data.orientation|default:"portrait" }};
12
    }
13
  </style>
14
</head>
15
<body>
16
  <div id="main">
17
  {% if request.GET and form.is_valid %}
18
  {% include 'chrono/manager_events_timesheet_fragment.html' %}
19
  {% endif %}
20
  </div>
21
</body>
22
</html>
chrono/manager/views.py
54 54
    UpdateView,
55 55
    View,
56 56
)
57
from weasyprint import HTML
57 58

  
58 59
from chrono.agendas.models import (
59 60
    AbsenceReason,
......
1972 1973
        context['form'] = form
1973 1974
        return context
1974 1975

  
1976
    def get(self, request, *args, **kwargs):
1977
        self.object = self.get_object()
1978
        context = self.get_context_data(object=self.object)
1979
        if 'pdf' in request.GET and context['form'].is_valid():
1980
            return self.pdf(request, context)
1981
        return self.render_to_response(context)
1982

  
1983
    def pdf(self, request, context):
1984
        context['base_uri'] = request.build_absolute_uri('/')
1985
        html = HTML(
1986
            string=render_to_string('chrono/manager_events_timesheet_pdf.html', context, request=request)
1987
        )
1988
        pdf = html.write_pdf()
1989
        response = HttpResponse(pdf, content_type='application/pdf')
1990
        response['Content-Disposition'] = 'attachment; filename="timesheet_{}_{}_{}.pdf"'.format(
1991
            self.agenda.slug,
1992
            context['form'].cleaned_data['date_start'].strftime('%Y-%m-%d'),
1993
            context['form'].cleaned_data['date_end'].strftime('%Y-%m-%d'),
1994
        )
1995
        return response
1996

  
1975 1997

  
1976 1998
events_timesheet = EventsTimesheetView.as_view()
1977 1999

  
debian/control
25 25
    python3-psycopg2,
26 26
    python3-django-mellon,
27 27
    python3-dateutil,
28
    python3-weasyprint,
28 29
    uwsgi,
29 30
    uwsgi-plugin-python3
30 31
Recommends: nginx,
setup.py
167 167
        'python-dateutil',
168 168
        'requests',
169 169
        'workalendar',
170
        'weasyprint<0.43',
170 171
    ],
171 172
    zip_safe=False,
172 173
    cmdclass={
tests/manager/test_event.py
2565 2565
    assert slots['extra_data'] == ['foo', 'baz']
2566 2566
    assert slots['users'][0]['extra_data']['foo'] == 'baz'
2567 2567
    assert slots['users'][0]['extra_data']['baz'] == ''
2568

  
2569

  
2570
def test_events_timesheet_pdf(app, admin_user):
2571
    agenda = Agenda.objects.create(label='Events', kind='events')
2572

  
2573
    login(app)
2574
    resp = app.get(
2575
        '/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&extra_data=&orientation=portrait'
2576
        % agenda.pk
2577
    )
2578
    assert resp.headers['Content-Type'] == 'application/pdf'
2579
    assert (
2580
        resp.headers['Content-Disposition']
2581
        == 'attachment; filename="timesheet_events_2022-02-01_2022-02-28.pdf"'
2582
    )
2583

  
2584
    # form invalid
2585
    resp = app.get(
2586
        '/manage/agendas/%s/events/timesheet?pdf=&date_start=2022-02-01&date_end=2022-02-28&extra_data='
2587
        % agenda.pk
2588
    )
2589
    assert resp.context['form'].errors['orientation'] == ['This field is required.']
2568
-