Skip to content

Commit b47e31a

Browse files
authored
Merge pull request #77 from SADiLaR/feature/projects-page
Add projects list page:
2 parents fc3e9e5 + 09545c9 commit b47e31a

File tree

6 files changed

+434
-2
lines changed

6 files changed

+434
-2
lines changed

app/app/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
path("_health/", views.health, name="health"),
3232
path("", views.home, name="home"),
3333
path("institutions/", views.institutions, name="institutions"),
34+
path("projects/", views.projects, name="projects"),
3435
path("search/", views.search, name="search"),
3536
path("i18n/", include("django.conf.urls.i18n")),
3637
]

app/app/views.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
from django.db.models import Count
66
from django.http import HttpResponse
77
from django.shortcuts import render
8+
from django.utils.translation import gettext as _
89

9-
from general.models import DocumentFile, Institution
10+
from general.models import DocumentFile, Institution, Language, Project, Subject
1011

1112

1213
def health(request):
@@ -23,6 +24,100 @@ def home(request):
2324
return render(request, template_name=template, context=context)
2425

2526

27+
def get_date_range(project):
28+
start_date = project.start_date
29+
end_date = project.end_date
30+
31+
if (start_date is not None) and (end_date is not None) and (start_date.year != end_date.year):
32+
date = f"{start_date.year}{end_date.year}"
33+
elif (start_date is not None) and (end_date is not None) and (start_date.year == end_date.year):
34+
date = start_date.year
35+
elif start_date is not None:
36+
date = _("Since {year}").format(year=start_date.year)
37+
elif end_date is not None:
38+
date = _("Until {year}").format(year=end_date.year)
39+
else:
40+
date = None
41+
return date
42+
43+
44+
def get_logo(project):
45+
if project.logo:
46+
logo = project.logo
47+
elif project.institution.logo:
48+
logo = project.institution.logo
49+
else:
50+
logo = None
51+
return logo
52+
53+
54+
def projects(request):
55+
template = "app/projects.html"
56+
57+
subject_id = request.GET.get("subject")
58+
language_id = request.GET.get("language")
59+
institution_id = request.GET.get("institution")
60+
61+
projects = (
62+
Project.objects.select_related("institution")
63+
.prefetch_related("subjects", "languages")
64+
.all()
65+
)
66+
67+
if subject_id:
68+
projects = projects.filter(subjects__id=subject_id)
69+
if language_id:
70+
projects = projects.filter(languages__id=language_id)
71+
if institution_id:
72+
projects = projects.filter(institution__id=institution_id)
73+
74+
subjects = Subject.objects.all()
75+
languages = Language.objects.all()
76+
institutions = Institution.objects.all()
77+
78+
project_data = []
79+
for project in projects:
80+
project_subjects = project.subjects.all()
81+
project_languages = project.languages.all()
82+
83+
if project_languages.count() < 4:
84+
languages_data = ", ".join(sorted(language.name for language in project_languages))
85+
else:
86+
languages_data = _("Multilingual")
87+
88+
if project_subjects.count() < 4:
89+
subjects_data = ", ".join(sorted([subject.name for subject in project_subjects]))
90+
else:
91+
subjects_data = _("Multiple subjects")
92+
93+
logo = get_logo(project)
94+
95+
institution_name = project.institution.name
96+
97+
date = get_date_range(project)
98+
99+
project_data.append(
100+
{
101+
"project": project,
102+
"logo": logo,
103+
"subjects": subjects_data,
104+
"languages": languages_data,
105+
"date": date,
106+
"institution_name": institution_name,
107+
}
108+
)
109+
110+
context = {
111+
"current_page": "projects",
112+
"projects": project_data,
113+
"subjects": subjects,
114+
"languages": languages,
115+
"institutions": institutions,
116+
}
117+
118+
return render(request, template_name=template, context=context)
119+
120+
26121
def institutions(request):
27122
template = "app/institutions.html"
28123
context = {}

app/general/tests/test_projects.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from datetime import date
2+
3+
from django.test import Client, TestCase
4+
from django.urls import reverse
5+
6+
from app.views import get_date_range
7+
from general.models import Institution, Language, Project, Subject
8+
9+
10+
class ProjectViewTests(TestCase):
11+
def setUp(self):
12+
self.client = Client()
13+
self.subject1 = Subject.objects.create(name="Subject 1")
14+
self.subject2 = Subject.objects.create(name="Subject 2")
15+
self.language1 = Language.objects.create(name="Language 1", iso_code="lang1")
16+
self.language2 = Language.objects.create(name="Language 2", iso_code="lang2")
17+
self.language3 = Language.objects.create(name="Language 3", iso_code="lang3")
18+
self.language4 = Language.objects.create(name="Language 4", iso_code="lang4")
19+
self.institution = Institution.objects.create(
20+
name="Institution", logo="institution_logo.png"
21+
)
22+
23+
self.project1 = Project.objects.create(
24+
name="Project 1",
25+
start_date="2020-01-01",
26+
end_date="2021-01-01",
27+
institution=self.institution,
28+
logo="project_logo.png",
29+
)
30+
self.project1.subjects.add(self.subject1)
31+
self.project1.languages.add(self.language1)
32+
33+
self.project2 = Project.objects.create(
34+
name="Project 2",
35+
end_date="2021-01-01",
36+
institution=self.institution,
37+
logo="project_logo.png",
38+
)
39+
self.project2.subjects.add(self.subject1)
40+
self.project2.languages.add(self.language1)
41+
self.project2.languages.add(self.language2)
42+
self.project2.languages.add(self.language3)
43+
self.project2.languages.add(self.language4)
44+
45+
self.url = reverse("projects")
46+
47+
def test_projects_view(self):
48+
response = self.client.get(reverse("projects"))
49+
self.assertEqual(response.status_code, 200)
50+
self.assertIn("projects", response.context)
51+
self.assertEqual(len(response.context["projects"]), 2)
52+
self.assertEqual(response.context["projects"][0]["project"].name, "Project 1")
53+
self.assertEqual(response.context["projects"][1]["project"].name, "Project 2")
54+
55+
def test_projects_view_with_filters(self):
56+
response = self.client.get(reverse("projects"), {"language": self.language3.id})
57+
self.assertEqual(response.status_code, 200)
58+
self.assertEqual(len(response.context["projects"]), 1)
59+
self.assertEqual(response.context["projects"][0]["project"].name, "Project 2")
60+
61+
def test_projects_view_multilingual(self):
62+
response = self.client.get(reverse("projects"))
63+
self.assertEqual(response.status_code, 200)
64+
projects = response.context["projects"]
65+
self.assertEqual(projects[1]["languages"], "Multilingual")
66+
67+
def test_projects_view_queries(self):
68+
response = self.client.get(self.url)
69+
70+
with self.assertNumQueries(6):
71+
response = self.client.get(self.url)
72+
73+
74+
class GetDateRangeTests(TestCase):
75+
def setUp(self):
76+
self.institution = Institution.objects.create(name="Institution")
77+
self.project = Project.objects.create(name="Project", institution=self.institution)
78+
79+
def test_get_date_range_different_years(self):
80+
self.project.start_date = date(2020, 1, 1)
81+
self.project.end_date = date(2021, 1, 1)
82+
self.project.save()
83+
self.assertEqual(get_date_range(self.project), "2020 – 2021")
84+
85+
def test_get_date_range_same_year(self):
86+
self.project.start_date = date(2020, 1, 1)
87+
self.project.end_date = date(2020, 12, 31)
88+
self.project.save()
89+
self.assertEqual(get_date_range(self.project), 2020)
90+
91+
def test_get_date_range_only_start_date(self):
92+
self.project.start_date = date(2020, 1, 1)
93+
self.project.end_date = None
94+
self.project.save()
95+
self.assertEqual(get_date_range(self.project), "Since 2020")
96+
97+
def test_get_date_range_only_end_date(self):
98+
self.project.start_date = None
99+
self.project.end_date = date(2020, 12, 31)
100+
self.project.save()
101+
self.assertEqual(get_date_range(self.project), "Until 2020")
102+
103+
def test_get_date_range_no_dates(self):
104+
self.project.start_date = None
105+
self.project.end_date = None
106+
self.project.save()
107+
self.assertIsNone(get_date_range(self.project))

app/static/css/styles.css

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,98 @@ html {
179179
padding: 0 10px 10px 0;
180180
}
181181

182+
/*Projects page*/
183+
.filter-form {
184+
display: flex;
185+
margin: 30px;
186+
font-size: 0.875rem;
187+
}
188+
.filter-form label {
189+
margin-right: 10px;
190+
}
191+
.filter-form .form-group {
192+
margin-right: 10px;
193+
}
194+
.filter-form .form-control {
195+
border: 1px solid #000;
196+
}
197+
.filter-form select {
198+
padding: 0 10px;
199+
width: 230px;
200+
}
201+
.btn-apply, .btn-reset{
202+
color: var(--primary-fg);
203+
outline-color: var(--primary);
204+
background-color: var(--primary);
205+
border-color: var(--primary);
206+
margin-right: 10px;
207+
}
208+
.btn-apply:hover, .btn-reset:hover{
209+
color: var(--primary-fg);
210+
outline-color: var(--primary);
211+
background-color: var(--primary);
212+
border-color: var(--primary);
213+
}
214+
215+
.project-list {
216+
margin: 20px;
217+
padding: 0 30px 0 30px;
218+
}
219+
220+
.card-text-project {
221+
font-size: 0.875rem;
222+
margin: 3px 0 3px 0;
223+
}
224+
225+
.project-logo {
226+
height: 100px;
227+
width: auto;
228+
229+
}
230+
.project-border {
231+
margin: 0 0 5px 0;
232+
overflow: hidden;
233+
padding: 0 5px 0 0;
234+
word-wrap: break-word;
235+
}
236+
.project-left-col {
237+
width: 70%;
238+
}
239+
.project-right-col {
240+
width: 30%;
241+
flex: 0 0 auto;
242+
display: flex;
243+
justify-content: flex-end;
244+
}
245+
.project-header {
246+
display: flex;
247+
justify-content: space-between;
248+
align-items: center;
249+
max-width: 800px;
250+
}
251+
.project-body {
252+
width: 100%;
253+
max-width: 800px;
254+
255+
}
256+
.card-text-description {
257+
word-wrap: break-word;
258+
overflow: hidden;
259+
font-size: 0.875rem;
260+
margin: 3px 0 3px 0;
261+
}
262+
.project-row {
263+
display: flex;
264+
justify-content: space-between;
265+
width: 100%;
266+
}
267+
.project-text {
268+
font-size: 1.25rem;
269+
}
270+
.icon-text {
271+
font-size: 0.95rem;
272+
}
273+
182274
/*Error pages*/
183275
.error-card {
184276
border-color: var(--primary-red);
@@ -190,6 +282,10 @@ html {
190282
.bi {
191283
font-size: 50px;
192284
}
285+
.project-icon {
286+
font-size: 20px;
287+
margin-right: 3px;
288+
}
193289

194290
/* Extra small devices (phones, 600px and down) */
195291
@media only screen and (max-width: 600px) {
@@ -213,13 +309,18 @@ html {
213309
.col-sm-6 {
214310
width: 50%;
215311
}
216-
217312
/*Home page*/
218313
.content-card {
219314
font-size: smaller;
220315
padding-right: 20px;
221316
margin-right: 10px;
222317
}
318+
319+
/*Projects page*/
320+
.filter-form select {
321+
padding: 0 10px;
322+
width: 100px;
323+
}
223324
}
224325

225326
/*small devices (tablets, 600px and up) */
@@ -253,6 +354,12 @@ html {
253354
.content-card {
254355
font-size: smaller;
255356
}
357+
358+
/*Projects page*/
359+
.filter-form select {
360+
padding: 0 10px;
361+
width: 150px;
362+
}
256363
}
257364

258365
/*Medium screens (tablets, between 768px and 1001px)*/
@@ -304,6 +411,12 @@ html {
304411
.right-col {
305412
width: 30%;
306413
}
414+
415+
/*Projects page*/
416+
.filter-form select {
417+
padding: 0 10px;
418+
width: 200px;
419+
}
307420
}
308421

309422
/*Larger screens (desktops, 1001px and up)*/
@@ -356,4 +469,10 @@ html {
356469
height: 200px;
357470
font-size: small;
358471
}
472+
473+
/*Projects page*/
474+
.filter-form select {
475+
padding: 0 10px;
476+
width: 230px;
477+
}
359478
}

0 commit comments

Comments
 (0)