From 09545c95a7584bb8fbb10e6ca80055dfffe1740a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9COMosimege=E2=80=9D?= <“onalerona.mosimege@gmail.com”> Date: Tue, 11 Jun 2024 11:25:20 +0200 Subject: [PATCH] Add projects list page: Add projects list view, template, url, and styling Add translation text wrapping, and testing --- app/app/urls.py | 1 + app/app/views.py | 97 ++++++++++++++++++++++- app/general/tests/test_projects.py | 107 +++++++++++++++++++++++++ app/static/css/styles.css | 121 ++++++++++++++++++++++++++++- app/templates/app/projects.html | 104 +++++++++++++++++++++++++ app/templates/base.html | 6 ++ 6 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 app/general/tests/test_projects.py create mode 100644 app/templates/app/projects.html diff --git a/app/app/urls.py b/app/app/urls.py index ff50810b..364e8184 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -31,6 +31,7 @@ path("_health/", views.health, name="health"), path("", views.home, name="home"), path("institutions/", views.institutions, name="institutions"), + path("projects/", views.projects, name="projects"), path("search/", views.search, name="search"), path("i18n/", include("django.conf.urls.i18n")), ] diff --git a/app/app/views.py b/app/app/views.py index fafada32..50f75fc5 100644 --- a/app/app/views.py +++ b/app/app/views.py @@ -10,8 +10,9 @@ from django.db.models import Count from django.http import HttpResponse from django.shortcuts import render +from django.utils.translation import gettext as _ -from general.models import DocumentFile, Institution +from general.models import DocumentFile, Institution, Language, Project, Subject def health(request): @@ -28,6 +29,100 @@ def home(request): return render(request, template_name=template, context=context) +def get_date_range(project): + start_date = project.start_date + end_date = project.end_date + + if (start_date is not None) and (end_date is not None) and (start_date.year != end_date.year): + date = f"{start_date.year} – {end_date.year}" + elif (start_date is not None) and (end_date is not None) and (start_date.year == end_date.year): + date = start_date.year + elif start_date is not None: + date = _("Since {year}").format(year=start_date.year) + elif end_date is not None: + date = _("Until {year}").format(year=end_date.year) + else: + date = None + return date + + +def get_logo(project): + if project.logo: + logo = project.logo + elif project.institution.logo: + logo = project.institution.logo + else: + logo = None + return logo + + +def projects(request): + template = "app/projects.html" + + subject_id = request.GET.get("subject") + language_id = request.GET.get("language") + institution_id = request.GET.get("institution") + + projects = ( + Project.objects.select_related("institution") + .prefetch_related("subjects", "languages") + .all() + ) + + if subject_id: + projects = projects.filter(subjects__id=subject_id) + if language_id: + projects = projects.filter(languages__id=language_id) + if institution_id: + projects = projects.filter(institution__id=institution_id) + + subjects = Subject.objects.all() + languages = Language.objects.all() + institutions = Institution.objects.all() + + project_data = [] + for project in projects: + project_subjects = project.subjects.all() + project_languages = project.languages.all() + + if project_languages.count() < 4: + languages_data = ", ".join(sorted(language.name for language in project_languages)) + else: + languages_data = _("Multilingual") + + if project_subjects.count() < 4: + subjects_data = ", ".join(sorted([subject.name for subject in project_subjects])) + else: + subjects_data = _("Multiple subjects") + + logo = get_logo(project) + + institution_name = project.institution.name + + date = get_date_range(project) + + project_data.append( + { + "project": project, + "logo": logo, + "subjects": subjects_data, + "languages": languages_data, + "date": date, + "institution_name": institution_name, + } + ) + + context = { + "current_page": "projects", + "projects": project_data, + "subjects": subjects, + "languages": languages, + "institutions": institutions, + } + + return render(request, template_name=template, context=context) + + def institutions(request): template = "app/institutions.html" context = {} diff --git a/app/general/tests/test_projects.py b/app/general/tests/test_projects.py new file mode 100644 index 00000000..77ffea78 --- /dev/null +++ b/app/general/tests/test_projects.py @@ -0,0 +1,107 @@ +from datetime import date + +from django.test import Client, TestCase +from django.urls import reverse + +from app.views import get_date_range +from general.models import Institution, Language, Project, Subject + + +class ProjectViewTests(TestCase): + def setUp(self): + self.client = Client() + self.subject1 = Subject.objects.create(name="Subject 1") + self.subject2 = Subject.objects.create(name="Subject 2") + self.language1 = Language.objects.create(name="Language 1", iso_code="lang1") + self.language2 = Language.objects.create(name="Language 2", iso_code="lang2") + self.language3 = Language.objects.create(name="Language 3", iso_code="lang3") + self.language4 = Language.objects.create(name="Language 4", iso_code="lang4") + self.institution = Institution.objects.create( + name="Institution", logo="institution_logo.png" + ) + + self.project1 = Project.objects.create( + name="Project 1", + start_date="2020-01-01", + end_date="2021-01-01", + institution=self.institution, + logo="project_logo.png", + ) + self.project1.subjects.add(self.subject1) + self.project1.languages.add(self.language1) + + self.project2 = Project.objects.create( + name="Project 2", + end_date="2021-01-01", + institution=self.institution, + logo="project_logo.png", + ) + self.project2.subjects.add(self.subject1) + self.project2.languages.add(self.language1) + self.project2.languages.add(self.language2) + self.project2.languages.add(self.language3) + self.project2.languages.add(self.language4) + + self.url = reverse("projects") + + def test_projects_view(self): + response = self.client.get(reverse("projects")) + self.assertEqual(response.status_code, 200) + self.assertIn("projects", response.context) + self.assertEqual(len(response.context["projects"]), 2) + self.assertEqual(response.context["projects"][0]["project"].name, "Project 1") + self.assertEqual(response.context["projects"][1]["project"].name, "Project 2") + + def test_projects_view_with_filters(self): + response = self.client.get(reverse("projects"), {"language": self.language3.id}) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context["projects"]), 1) + self.assertEqual(response.context["projects"][0]["project"].name, "Project 2") + + def test_projects_view_multilingual(self): + response = self.client.get(reverse("projects")) + self.assertEqual(response.status_code, 200) + projects = response.context["projects"] + self.assertEqual(projects[1]["languages"], "Multilingual") + + def test_projects_view_queries(self): + response = self.client.get(self.url) + + with self.assertNumQueries(6): + response = self.client.get(self.url) + + +class GetDateRangeTests(TestCase): + def setUp(self): + self.institution = Institution.objects.create(name="Institution") + self.project = Project.objects.create(name="Project", institution=self.institution) + + def test_get_date_range_different_years(self): + self.project.start_date = date(2020, 1, 1) + self.project.end_date = date(2021, 1, 1) + self.project.save() + self.assertEqual(get_date_range(self.project), "2020 – 2021") + + def test_get_date_range_same_year(self): + self.project.start_date = date(2020, 1, 1) + self.project.end_date = date(2020, 12, 31) + self.project.save() + self.assertEqual(get_date_range(self.project), 2020) + + def test_get_date_range_only_start_date(self): + self.project.start_date = date(2020, 1, 1) + self.project.end_date = None + self.project.save() + self.assertEqual(get_date_range(self.project), "Since 2020") + + def test_get_date_range_only_end_date(self): + self.project.start_date = None + self.project.end_date = date(2020, 12, 31) + self.project.save() + self.assertEqual(get_date_range(self.project), "Until 2020") + + def test_get_date_range_no_dates(self): + self.project.start_date = None + self.project.end_date = None + self.project.save() + self.assertIsNone(get_date_range(self.project)) diff --git a/app/static/css/styles.css b/app/static/css/styles.css index 9ce1f73d..94b91659 100755 --- a/app/static/css/styles.css +++ b/app/static/css/styles.css @@ -179,6 +179,98 @@ html { padding: 0 10px 10px 0; } +/*Projects page*/ +.filter-form { + display: flex; + margin: 30px; + font-size: 0.875rem; +} +.filter-form label { + margin-right: 10px; +} +.filter-form .form-group { + margin-right: 10px; +} +.filter-form .form-control { + border: 1px solid #000; +} +.filter-form select { + padding: 0 10px; + width: 230px; +} +.btn-apply, .btn-reset{ + color: var(--primary-fg); + outline-color: var(--primary); + background-color: var(--primary); + border-color: var(--primary); + margin-right: 10px; +} +.btn-apply:hover, .btn-reset:hover{ + color: var(--primary-fg); + outline-color: var(--primary); + background-color: var(--primary); + border-color: var(--primary); +} + +.project-list { + margin: 20px; + padding: 0 30px 0 30px; +} + +.card-text-project { + font-size: 0.875rem; + margin: 3px 0 3px 0; +} + +.project-logo { + height: 100px; + width: auto; + +} +.project-border { + margin: 0 0 5px 0; + overflow: hidden; + padding: 0 5px 0 0; + word-wrap: break-word; +} +.project-left-col { + width: 70%; +} +.project-right-col { + width: 30%; + flex: 0 0 auto; + display: flex; + justify-content: flex-end; +} +.project-header { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 800px; +} +.project-body { + width: 100%; + max-width: 800px; + +} +.card-text-description { + word-wrap: break-word; + overflow: hidden; + font-size: 0.875rem; + margin: 3px 0 3px 0; +} +.project-row { + display: flex; + justify-content: space-between; + width: 100%; +} +.project-text { + font-size: 1.25rem; +} +.icon-text { + font-size: 0.95rem; +} + /*Error pages*/ .error-card { border-color: var(--primary-red); @@ -190,6 +282,10 @@ html { .bi { font-size: 50px; } +.project-icon { + font-size: 20px; + margin-right: 3px; +} /* Extra small devices (phones, 600px and down) */ @media only screen and (max-width: 600px) { @@ -213,13 +309,18 @@ html { .col-sm-6 { width: 50%; } - /*Home page*/ .content-card { font-size: smaller; padding-right: 20px; margin-right: 10px; } + + /*Projects page*/ + .filter-form select { + padding: 0 10px; + width: 100px; + } } /*small devices (tablets, 600px and up) */ @@ -253,6 +354,12 @@ html { .content-card { font-size: smaller; } + + /*Projects page*/ + .filter-form select { + padding: 0 10px; + width: 150px; + } } /*Medium screens (tablets, between 768px and 1001px)*/ @@ -304,6 +411,12 @@ html { .right-col { width: 30%; } + + /*Projects page*/ + .filter-form select { + padding: 0 10px; + width: 200px; + } } /*Larger screens (desktops, 1001px and up)*/ @@ -356,4 +469,10 @@ html { height: 200px; font-size: small; } + + /*Projects page*/ + .filter-form select { + padding: 0 10px; + width: 230px; + } } diff --git a/app/templates/app/projects.html b/app/templates/app/projects.html new file mode 100644 index 00000000..710ba9fb --- /dev/null +++ b/app/templates/app/projects.html @@ -0,0 +1,104 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} + +{% block content %} + +
+
+ + +
+ +
+ +
+ +
+ +
+ +
+ + + {% trans 'Reset' %} + +
+
+ +
+
+ + +
+ {% for project in projects %} +
+
+
+
+

{{ project.project.name }}

+ +
+ {{ project.institution_name }} +
+
+
+ {% if project.logo %} + + {% endif %} +
+
+
+
+
+ {{ project.project.description }} +
+
+ + {{ project.languages }} +
+
+ + {{ project.subjects }} +
+
+ + {{ project.date }} +
+ +
+
+ +
+
+
+ {% endfor %} +
+ + + +{% endblock content %} diff --git a/app/templates/base.html b/app/templates/base.html index cf43ac7a..2b8a3967 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -45,6 +45,12 @@ class="nav-link" href="{% url 'institutions' %}">{% trans "Institutions" %} +