Skip to content

Commit b0bfcf1

Browse files
authored
Merge pull request #3668 from webkom/feat-export-survey-pdf
Add survey export to pdf with graphs
2 parents ae16706 + 9099bb3 commit b0bfcf1

File tree

8 files changed

+1430
-30
lines changed

8 files changed

+1430
-30
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
{% load static %}
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
<head>
5+
<style>
6+
/* Page margin and layout */
7+
@page {
8+
margin: 35mm 20mm; /* Increased top margin for PDF layout */
9+
}
10+
body {
11+
font-family: Arial, sans-serif;
12+
margin: 0;
13+
padding-top: 150px; /* Increased padding to avoid overlap with fixed header */
14+
}
15+
h1 {
16+
text-align: center;
17+
color: #333;
18+
margin-top: 10px;
19+
}
20+
21+
/* Fixed Header */
22+
.header {
23+
width: 100%;
24+
display: flex;
25+
justify-content: space-between;
26+
align-items: flex-end; /* Align items at the start for multi-line support */
27+
position: fixed;
28+
top: 0;
29+
padding: 10px 0;
30+
border-bottom: 1px solid #ddd;
31+
background-color: white;
32+
height: auto;
33+
}
34+
.header img {
35+
max-width: 80px;
36+
height: auto;
37+
margin-bottom: 15px;
38+
}
39+
.header .contact-info {
40+
font-size: 1.075em;
41+
text-align: left;
42+
margin: 0;
43+
line-height: 1.2; /* Control line spacing for header text */
44+
}
45+
.header p {
46+
margin: 0;
47+
}
48+
49+
/* Date, Company, and Title Row */
50+
.info-row {
51+
display: flex;
52+
flex-direction: column;
53+
justify-content: space-between;
54+
align-items: center;
55+
font-size: 1em;
56+
margin-top: 20px;
57+
text-align: left;
58+
}
59+
60+
.info-row-item {
61+
margin-left: 16px;
62+
}
63+
64+
.info-header {
65+
font-weight: bold;
66+
font-size: 1.2em;
67+
}
68+
69+
/* First Question Layout */
70+
.first-question {
71+
margin-top: 20px; /* Space between title and first question */
72+
}
73+
74+
/* Container for Each Question with Extra Padding */
75+
.question-container {
76+
padding-top: 90px; /* Extra padding for each question page */
77+
}
78+
79+
/* Subsequent Questions Layout */
80+
.question {
81+
margin-bottom: 20px;
82+
page-break-before: always; /* Each question starts on a new page */
83+
}
84+
.question h2 {
85+
font-size: 1.2em;
86+
color: #555;
87+
margin-top: 30px; /* Margin to prevent clipping under the header */
88+
}
89+
.charts {
90+
display: flex;
91+
justify-content: center;
92+
gap: 20px;
93+
margin-top: 25px;
94+
}
95+
.chart img {
96+
width: 300px;
97+
height: auto;
98+
}
99+
.text-answers {
100+
margin-top: 10px;
101+
font-size: 1em;
102+
color: #444;
103+
}
104+
.text-answers ul {
105+
list-style-type: disc;
106+
padding-left: 20px;
107+
}
108+
.stats-table {
109+
width: 100%;
110+
margin-top: 10px;
111+
border-collapse: collapse;
112+
}
113+
.stats-table th,
114+
.stats-table td {
115+
border: 1px solid #ddd;
116+
padding: 8px;
117+
}
118+
.stats-table th {
119+
background-color: #f2f2f2;
120+
font-weight: bold;
121+
}
122+
.average {
123+
font-weight: bold;
124+
color: #333;
125+
margin-top: 10px;
126+
}
127+
128+
a {
129+
color: rgb(45, 90, 158);
130+
}
131+
</style>
132+
</head>
133+
<body>
134+
<!-- Persistent header with only Abakus info -->
135+
<div class="header">
136+
<div class="contact-info">
137+
<img
138+
src="file://{{ logo_image_path }}"
139+
style="max-width: 150px; height: auto"
140+
alt="Abaku Logo"
141+
/>
142+
<p>Sem Sælands Vei 7-9, Gløshaugen, 7491 Trondheim</p>
143+
<p>
144+
<a href="https://abakus.no">www.abakus.no</a>
145+
| <a href="mailto:[email protected]">[email protected]</a>
146+
</p>
147+
</div>
148+
<div class="partner-logo">
149+
<p>Hovedsamarbeidspartner:</p>
150+
<img
151+
src="file://{{ hsp_image_path }}"
152+
style="max-width: 120px; height: auto"
153+
alt="Partner Logo"
154+
/>
155+
</div>
156+
</div>
157+
158+
<!-- Row for Company, Date, and Title (only on first page) -->
159+
<h1>{{ survey_title }}</h1>
160+
<div class="info-row">
161+
<p><span class="info-header">Bedrift: </span>{{ company }}</p>
162+
<p><span class="info-header">Dato: </span>{{ date }}</p>
163+
</div>
164+
165+
<!-- First Survey Question on the First Page -->
166+
{% if charts_data %}
167+
<div class="first-question">
168+
<h2>1. {{ charts_data.0.question_text }}</h2>
169+
170+
{% if charts_data.0.pie_chart_base64 or charts_data.0.bar_chart_base64 %}
171+
<div class="charts">
172+
{% if charts_data.0.pie_chart_base64 %}
173+
<div class="chart">
174+
<img
175+
src="data:image/png;base64,{{ charts_data.0.pie_chart_base64 }}"
176+
alt="Pie Chart"
177+
/>
178+
</div>
179+
{% endif %} {% if charts_data.0.bar_chart_base64 %}
180+
<div class="chart">
181+
<img
182+
src="data:image/png;base64,{{ charts_data.0.bar_chart_base64 }}"
183+
alt="Bar Chart"
184+
/>
185+
</div>
186+
{% endif %}
187+
</div>
188+
{% endif %} {% if charts_data.0.statistics %}
189+
<div class="statistics">
190+
<h3>Tabell</h3>
191+
<table class="stats-table">
192+
<tr>
193+
<th>Alternativ</th>
194+
<th>Antall</th>
195+
</tr>
196+
{% for stat in charts_data.0.statistics %}
197+
<tr>
198+
<td>{{ stat.option }}</td>
199+
<td>{{ stat.count }}</td>
200+
</tr>
201+
{% endfor %}
202+
</table>
203+
</div>
204+
{% endif %} {% if charts_data.0.average %}
205+
<div class="average">
206+
<p>Gjennomsnittlig verdi: {{ chart.average|floatformat:2 }}</p>
207+
</div>
208+
{% endif %} {% if charts_data.0.text_answers %}
209+
<div class="text-answers">
210+
<h3>Tekst svar:</h3>
211+
<ul>
212+
{% for answer in chart.text_answers %}
213+
<li>{{ answer }}</li>
214+
{% endfor %}
215+
</ul>
216+
</div>
217+
{% endif %}
218+
</div>
219+
{% endif %}
220+
221+
<!-- Remaining Questions Each on a New Page -->
222+
{% for chart in charts_data|slice:"1:" %}
223+
<div class="question-container">
224+
<div class="question">
225+
<h2>{{ forloop.counter|add:1 }}. {{ chart.question_text }}</h2>
226+
227+
{% if chart.pie_chart_base64 or chart.bar_chart_base64 %}
228+
<div class="charts">
229+
{% if chart.pie_chart_base64 %}
230+
<div class="chart">
231+
<img
232+
src="data:image/png;base64,{{ chart.pie_chart_base64 }}"
233+
alt="Pie Chart"
234+
/>
235+
</div>
236+
{% endif %} {% if chart.bar_chart_base64 %}
237+
<div class="chart">
238+
<img
239+
src="data:image/png;base64,{{ chart.bar_chart_base64 }}"
240+
alt="Bar Chart"
241+
/>
242+
</div>
243+
{% endif %}
244+
</div>
245+
{% endif %} {% if chart.statistics %}
246+
<div class="statistics">
247+
<h3>Tabell</h3>
248+
<table class="stats-table">
249+
<tr>
250+
<th>Alternativ</th>
251+
<th>Antall</th>
252+
</tr>
253+
{% for stat in chart.statistics %}
254+
<tr>
255+
<td>{{ stat.option }}</td>
256+
<td>{{ stat.count }}</td>
257+
</tr>
258+
{% endfor %}
259+
</table>
260+
</div>
261+
{% endif %} {% if chart.average %}
262+
<div class="average">
263+
<p>Gjennomsnittlig verdi: {{ chart.average|floatformat:2 }}</p>
264+
</div>
265+
{% endif %} {% if chart.text_answers %}
266+
<div class="text-answers">
267+
<h3>Tekst svar:</h3>
268+
<ul>
269+
{% for answer in chart.text_answers %}
270+
<li>{{ answer }}</li>
271+
{% endfor %}
272+
</ul>
273+
</div>
274+
{% endif %}
275+
</div>
276+
</div>
277+
{% endfor %}
278+
</body>
279+
</html>

lego/apps/surveys/tests/test_surveys_api.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def _get_token_url(pk):
3636
"mandatory": False,
3737
"relativeIndex": 1,
3838
"options": [],
39+
"display_type": "pie_chart",
3940
},
4041
{
4142
"id": 2,
@@ -52,6 +53,7 @@ def _get_token_url(pk):
5253
"mandatory": True,
5354
"relativeIndex": 3,
5455
"options": [],
56+
"display_type": "bar_chart",
5557
},
5658
]
5759
},
@@ -371,10 +373,39 @@ def test_survey_export_admin(self):
371373
response = self.client.get(url)
372374
self.assertEqual(response.status_code, status.HTTP_200_OK)
373375

376+
def test_survey_export_admin_pdf(self):
377+
"""Test that admins can export a survey as pdf"""
378+
self.client.force_authenticate(user=self.admin_user)
379+
response = self.client.post(_get_list_url(), self.survey_data)
380+
381+
url = _get_detail_url(response.json()["id"]) + "pdf/"
382+
response = self.client.get(url)
383+
self.assertEqual(response.status_code, status.HTTP_200_OK)
384+
374385
def test_survey_export_regular(self):
375386
"""Test that normal users can't export a survey as csv"""
376387
self.client.force_authenticate(user=self.attended_user)
377388

378389
url = _get_detail_url(1) + "csv/"
379390
response = self.client.get(url)
380391
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
392+
393+
def test_survey_export_regular_pdf(self):
394+
"""Test that normal users can't export a survey as pdf"""
395+
self.client.force_authenticate(user=self.attended_user)
396+
397+
url = _get_detail_url(1) + "pdf/"
398+
response = self.client.get(url)
399+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
400+
401+
def test_survey_export_admin_pdf_larger_survey(self):
402+
"""Test that admins can export a survey as pdf"""
403+
self.client.force_authenticate(user=self.admin_user)
404+
response = self.client.post(
405+
_get_list_url(),
406+
{"title": "Survey", "event": 5, "questions": _test_surveys[1]["questions"]},
407+
)
408+
409+
url = _get_detail_url(response.json()["id"]) + "pdf/"
410+
response = self.client.get(url)
411+
self.assertEqual(response.status_code, status.HTTP_200_OK)

0 commit comments

Comments
 (0)