Skip to content

Commit 915b735

Browse files
authored
Merge pull request #66 from Xpirix/print-organizations-page
Print organizations page
2 parents 9ead2a5 + 76683da commit 915b735

File tree

9 files changed

+680
-238
lines changed

9 files changed

+680
-238
lines changed

deployment/docker/REQUIREMENTS.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,7 @@ sorl-thumbnail~=12.11
6363
resend~=2.7
6464

6565
# Celery
66-
celery~=5.3
66+
celery~=5.3
67+
68+
# PDF rendering
69+
django-renderpdf~=5.0
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
2+
@import "variables";
3+
4+
.certifying-org-pdf {
5+
.header {
6+
display: flex;
7+
align-items: center;
8+
justify-content: space-between;
9+
border-bottom: 3px solid $primary1;
10+
margin-bottom: 2em;
11+
padding-bottom: 1em;
12+
}
13+
.logo {
14+
max-height: 60px;
15+
max-width: 60px;
16+
margin-right: 1.5em;
17+
}
18+
.section-title {
19+
font-size: 1.3em;
20+
margin-bottom: 0.7em;
21+
padding-bottom: 0.2em;
22+
}
23+
.card {
24+
border: 1px solid #e0e0e0;
25+
border-radius: 8px;
26+
margin-bottom: 2em;
27+
box-shadow: 0 2px 8px #0001;
28+
padding: 1.2em 1.5em;
29+
background-color: #ecf1f4;
30+
background-image: url(../../images/light_blue_bg.svg);
31+
}
32+
.table {
33+
width: 100%;
34+
border-collapse: collapse;
35+
margin-bottom: 1em;
36+
}
37+
.table th, .table td {
38+
padding: 0.5em 0.7em;
39+
font-size: 10pt !important;
40+
}
41+
.table th {
42+
background: $primary3;
43+
color: $primary3-invert;
44+
}
45+
.notification {
46+
background: #fffbe6;
47+
border-left: 4px solid #ffe066;
48+
padding: 0.8em 1em;
49+
margin-bottom: 1em;
50+
border-radius: 4px;
51+
color: #8a6d3b;
52+
}
53+
.inactive {
54+
color: #aaa;
55+
font-style: italic;
56+
}
57+
58+
}

django_project/base/static/style/scss/style.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@charset "utf-8";
22
@import "custom.bulma.scss";
33
@import "sustaining_members.scss";
4+
@import "certifying_org_pdf.scss";
45
@import "toogle-switch.scss";
56
@import "map.scss";
67
@import "../../../../node_modules/@creativebulma/bulma-tooltip/src/sass/index.sass";

django_project/certification/templates/certifying_organisation/detail.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,14 @@ <h1 class="title">{{ certifyingorganisation.name }}</h1>
314314
<i class="fas fa-rss"></i>
315315
</button>
316316
{% if user_can_delete %}
317+
318+
<button class="button is-light has-tooltip-arrow has-tooltip-bottom icon"
319+
{% if certifyingorganisation.is_archived %}disabled{% endif %}
320+
onclick="location.href='{% url "certifyingorganisation-print" slug=certifyingorganisation.slug %}'"
321+
data-tooltip="Print this page to PDF">
322+
<i class="fas fa-print"></i>
323+
</button>
324+
317325
<button class="button is-light has-tooltip-arrow has-tooltip-bottom icon"
318326
{% if certifyingorganisation.is_archived %}disabled{% endif %}
319327
onclick="location.href='{% url "issue-certificate-organisation" organisation_slug=certifyingorganisation.slug %}'"
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
2+
{% load static %}
3+
{% load render_bundle from webpack_loader %}
4+
{% render_bundle 'main' %}
5+
6+
<!DOCTYPE html>
7+
<html lang="en">
8+
<head>
9+
<meta charset="utf-8">
10+
<title>QGIS Certification: Certifying Organisation</title>
11+
<style>
12+
@page {
13+
size: A4;
14+
margin: 2cm;
15+
@bottom-right {
16+
background: #589632;
17+
color: #fff;
18+
font-size: 9pt;
19+
content: counter(page);
20+
height: 1cm;
21+
text-align: center;
22+
width: 1cm;
23+
}
24+
@bottom-center {
25+
background: #589632;
26+
content: '';
27+
display: block;
28+
height: .05cm;
29+
opacity: .5;
30+
width: 100%;
31+
}
32+
}
33+
body {
34+
color: #222;
35+
background: #fff;
36+
margin: 0;
37+
}
38+
39+
</style>
40+
</head>
41+
<body>
42+
<div class="certifying-org-pdf">
43+
<div class="header">
44+
{% if certifyingorganisation.logo %}
45+
<img class="logo" src="file://{{ certifyingorganisation.logo.path }}" alt="{{ certifyingorganisation.name }} Logo">
46+
{% endif %}
47+
<div style="text-align: center;">
48+
<h2 class="title hero-title is-4 m-0 has-text-success">{{ certifyingorganisation.name }}</h2>
49+
<h3>QGIS Certifying Organisation </h3>
50+
</div>
51+
<img class="logo" src="{% static 'images/large-logo.svg' %}" alt="QGIS Logo">
52+
</div>
53+
54+
<div class="card">
55+
<div class="title is-5 has-text-centered">Organisation Details</div>
56+
<table class="table">
57+
<tr><th>Name</th><td>{{ certifyingorganisation.name }}</td></tr>
58+
<tr><th>Address</th><td>{{ certifyingorganisation.address }}</td></tr>
59+
<tr><th>Country</th><td>{{ certifyingorganisation.country.name }}</td></tr>
60+
<tr><th>Email</th><td><a href="mailto:{{ certifyingorganisation.organisation_email }}">{{ certifyingorganisation.organisation_email }}</a></td></tr>
61+
<tr><th>Phone</th><td>{{ certifyingorganisation.organisation_phone }}</td></tr>
62+
<tr><th>Owners</th><td>
63+
{% for organisation_owners in certifyingorganisation.organisation_owners.all %}
64+
{% if organisation_owners.first_name %}
65+
{{ organisation_owners.first_name }} {{ organisation_owners.last_name }}{% if not forloop.last %}, {% endif %}
66+
{% else %}
67+
{{ organisation_owners }}{% if not forloop.last %}, {% endif %}
68+
{% endif %}
69+
{% endfor %}
70+
</td></tr>
71+
<tr><th>URL</th><td>
72+
{% if certifyingorganisation.url %}
73+
<a href="{{ certifyingorganisation.url }}">{{ certifyingorganisation.url }}</a>
74+
{% else %}-{% endif %}
75+
</td></tr>
76+
</table>
77+
</div>
78+
79+
{% if certifyingorganisation.approved %}
80+
<div class="card">
81+
<div class="title is-5 has-text-centered">Courses</div>
82+
{% if num_course == 0 %}
83+
<div class="notification">No courses are defined.</div>
84+
{% else %}
85+
<table class="table">
86+
<thead>
87+
<tr>
88+
<th>Course Name</th>
89+
<th>Date</th>
90+
<th>Training Center</th>
91+
<th>Course Convener</th>
92+
</tr>
93+
</thead>
94+
<tbody>
95+
{% for course in courses %}
96+
<tr>
97+
<td>{{ course.course_type.name }}</td>
98+
<td>{{ course.start_date }} to {{ course.end_date }}</td>
99+
<td>{{ course.training_center.name }}</td>
100+
<td>
101+
{% if course.course_convener.user.first_name %}
102+
{{ course.course_convener.user.first_name }} {{ course.course_convener.user.last_name }}
103+
{% else %}
104+
{{ course.course_convener.user }}
105+
{% endif %}
106+
</td>
107+
</tr>
108+
{% endfor %}
109+
</tbody>
110+
</table>
111+
{% endif %}
112+
</div>
113+
114+
<div class="card">
115+
<div class="title is-5 has-text-centered">Training Centers</div>
116+
{% if num_trainingcenter == 0 %}
117+
<div class="notification">No training centers are defined.</div>
118+
{% else %}
119+
<table class="table">
120+
<tbody>
121+
{% for trainingcenter in trainingcenters %}
122+
<tr>
123+
<td>
124+
<a href="{% url 'trainingcenter-detail' certifyingorganisation.slug trainingcenter.slug %}">{{ trainingcenter.name }}</a>
125+
</td>
126+
</tr>
127+
{% endfor %}
128+
</tbody>
129+
</table>
130+
{% endif %}
131+
</div>
132+
133+
<div class="card">
134+
<div class="title is-5 has-text-centered">Course Types</div>
135+
{% if num_coursetype == 0 %}
136+
<div class="notification">No course types are defined.</div>
137+
{% else %}
138+
<table class="table">
139+
<tbody>
140+
{% for coursetype in coursetypes %}
141+
<tr>
142+
<td>
143+
<a href="{% url 'coursetype-detail' organisation_slug=certifyingorganisation.slug pk=coursetype.pk %}">{{ coursetype.name }}</a>
144+
</td>
145+
</tr>
146+
{% endfor %}
147+
</tbody>
148+
</table>
149+
{% endif %}
150+
</div>
151+
152+
<div class="card">
153+
<div class="title is-5 has-text-centered">Course Conveners</div>
154+
{% if num_courseconvener == 0 %}
155+
<div class="notification">No course conveners are defined.</div>
156+
{% else %}
157+
<table class="table">
158+
<tbody>
159+
{% for courseconvener in courseconveners %}
160+
<tr>
161+
<td>
162+
{% if courseconvener.is_active %}
163+
{% if courseconvener.user.first_name %}
164+
{{ courseconvener.user.first_name }} {{ courseconvener.user.last_name }}
165+
{% else %}
166+
{{ courseconvener.user }}
167+
{% endif %}
168+
{% elif user_can_delete %}
169+
<span class="inactive">
170+
{% if courseconvener.user.first_name %}
171+
{{ courseconvener.user.first_name }} {{ courseconvener.user.last_name }}
172+
{% else %}
173+
{{ courseconvener.user }}
174+
{% endif %}
175+
[inactive]
176+
</span>
177+
{% endif %}
178+
</td>
179+
</tr>
180+
{% endfor %}
181+
</tbody>
182+
</table>
183+
{% endif %}
184+
</div>
185+
{% endif %}
186+
</div>
187+
</body>
188+
</html>

django_project/certification/tests/views/test_certifying_organisation_views.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,3 +631,82 @@ def test_update_status_command(self):
631631
self.pending_certifying_organisation.refresh_from_db()
632632
self.assertEqual(self.certifying_organisation.status.name, "Approved")
633633
self.assertEqual(self.pending_certifying_organisation.status.name, "Pending")
634+
635+
636+
class TestCertifyingOrganisationPrintView(TestCase):
637+
"""Test that Certifying Organisation Print View works."""
638+
639+
@override_settings(VALID_DOMAIN=["testserver"])
640+
def setUp(self):
641+
self.client = Client()
642+
self.client.post("/set_language/", data={"language": "en"})
643+
self.user = UserF.create(username="anita", password="password")
644+
self.user.set_password("password")
645+
self.user.save()
646+
self.orgmanager = UserF.create(username="orgmanager", password="password")
647+
self.orgmanager.set_password("password")
648+
self.orgmanager.save()
649+
self.project = ProjectF.create()
650+
self.certifying_organisation = CertifyingOrganisationF.create(
651+
project=self.project, approved=True
652+
)
653+
self.certifying_organisation.organisation_owners.set([self.orgmanager])
654+
self.certifying_organisation.save()
655+
self.unapproved_organisation = CertifyingOrganisationF.create(
656+
project=self.project, approved=False
657+
)
658+
659+
@override_settings(VALID_DOMAIN=["testserver"])
660+
def tearDown(self):
661+
self.certifying_organisation.delete()
662+
self.unapproved_organisation.delete()
663+
self.project.delete()
664+
self.user.delete()
665+
666+
@override_settings(VALID_DOMAIN=["testserver"])
667+
def test_print_view_approved_org(self):
668+
self.client.login(username="orgmanager", password="password")
669+
response = self.client.get(
670+
reverse(
671+
"certifyingorganisation-print",
672+
kwargs={"slug": self.certifying_organisation.slug},
673+
)
674+
)
675+
self.assertEqual(response.status_code, 200)
676+
self.assertEqual(response["Content-Type"], "application/pdf")
677+
678+
@override_settings(VALID_DOMAIN=["testserver"])
679+
def test_print_view_unapproved_org(self):
680+
self.client.login(username="orgmanager", password="password")
681+
response = self.client.get(
682+
reverse(
683+
"certifyingorganisation-print",
684+
kwargs={"slug": self.unapproved_organisation.slug},
685+
)
686+
)
687+
self.assertNotEqual(response.status_code, 200)
688+
# Should be forbidden or not found for unapproved org
689+
self.assertIn(response.status_code, [403, 404])
690+
691+
@override_settings(VALID_DOMAIN=["testserver"])
692+
def test_print_view_as_non_owner(self):
693+
self.client.login(username="anita", password="password")
694+
response = self.client.get(
695+
reverse(
696+
"certifyingorganisation-print",
697+
kwargs={"slug": self.certifying_organisation.slug},
698+
)
699+
)
700+
# Should be forbidden or not found for non owner
701+
self.assertIn(response.status_code, [403, 404])
702+
703+
@override_settings(VALID_DOMAIN=["testserver"])
704+
def test_print_view_requires_login(self):
705+
response = self.client.get(
706+
reverse(
707+
"certifyingorganisation-print",
708+
kwargs={"slug": self.certifying_organisation.slug},
709+
)
710+
)
711+
# Should redirect to login
712+
self.assertIn(response.status_code, [302, 403])

django_project/certification/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
CertifyingOrganisationDetailView,
3838
CertifyingOrganisationJson,
3939
CertifyingOrganisationListView,
40+
CertifyingOrganisationPrintView,
4041
CertifyingOrganisationUpdateView,
4142
CheckoutSessionSuccessView,
4243
CourseAttendeeCreateView,
@@ -138,6 +139,11 @@
138139
view=CertifyingOrganisationDetailView.as_view(),
139140
name="certifyingorganisation-detail",
140141
),
142+
url(
143+
r"^certifyingorganisation/(?P<slug>[\w-]+)/print/$",
144+
view=CertifyingOrganisationPrintView.as_view(),
145+
name="certifyingorganisation-print",
146+
),
141147
url(
142148
r"^certifyingorganisation/(?P<slug>[\w-]+)/archiving/(?P<toogle_archive>[\w-]+)/$",
143149
view=CertifyingOrganisationArchivingView.as_view(),

0 commit comments

Comments
 (0)