diff --git a/dashboard/projects/templates/projects/partial_project_list_row.html b/dashboard/projects/templates/projects/partial_project_list_row.html
index d8026d1..ec61264 100644
--- a/dashboard/projects/templates/projects/partial_project_list_row.html
+++ b/dashboard/projects/templates/projects/partial_project_list_row.html
@@ -9,9 +9,9 @@
{{ project.last_review|default:"" }}
- {% for qi in project.quality_history %}
{{ qi.value }} | {% endfor %}
+ {% for qi in project.quality_history_values %}{{ qi.value }} | {% endfor %}
- {{ project.quality_indicator }} |
+ {{ project.quality_indicator_value }} |
{{ project.expectations_review_status|default:"Unreviewed" }} |
{% for po in project.projectobjectives %}
diff --git a/dashboard/projects/templates/projects/project_list.html b/dashboard/projects/templates/projects/project_list.html
index 310e7d2..af1e45d 100644
--- a/dashboard/projects/templates/projects/project_list.html
+++ b/dashboard/projects/templates/projects/project_list.html
@@ -52,15 +52,7 @@
| {{ group }} |
{% endif %}
{% for project in projects %}
-
-
- | … |
-
-
+ {% include "projects/partial_project_list_row.html" %}
{% endfor %}
{% endfor %}
diff --git a/dashboard/projects/test_views.py b/dashboard/projects/test_views.py
index 371ebd1..c5e030a 100644
--- a/dashboard/projects/test_views.py
+++ b/dashboard/projects/test_views.py
@@ -17,6 +17,7 @@
Project,
ProjectObjective,
ProjectObjectiveCondition,
+ QI,
)
@@ -297,6 +298,39 @@ def test_action_select_reason_allows_authorized_put_and_sets_reason(
assert project_objective.unstarted_reason_id == reason.id
+@pytest.mark.django_db
+def test_project_list_renders_qi_history_current_qi_and_levels(
+ client, user_without_permissions, objective, project, project_objective, work_cycle
+):
+ second_work_cycle = WorkCycle.objects.create(
+ name="wc-2", timestamp="2026-02-01", is_current=False
+ )
+ QI.objects.update_or_create(
+ project=project,
+ workcycle=work_cycle,
+ defaults={"value": 3},
+ )
+ QI.objects.update_or_create(
+ project=project,
+ workcycle=second_work_cycle,
+ defaults={"value": 5},
+ )
+
+ level_for_display = Level.objects.create(name="LEVEL-ASSERT-ONLY", value=7)
+ ProjectObjective.objects.filter(id=project_objective.id).update(
+ level_achieved=level_for_display
+ )
+
+ response = client.get(reverse("projects:project_list"))
+ content = response.content.decode()
+
+ assert response.status_code == 200
+ assert "3 | " in content
+ assert "5 | " in content
+ assert ">7" in content
+ assert "LEVEL-ASSERT-ONLY" in content
+
+
# Check that the project list and project detail pages are correctly public/private,
# depending on whether OIDC is configured.
diff --git a/dashboard/projects/urls.py b/dashboard/projects/urls.py
index 2119715..3cf7d56 100644
--- a/dashboard/projects/urls.py
+++ b/dashboard/projects/urls.py
@@ -11,17 +11,11 @@
status_projectobjective,
status_dashboardprojectobjective,
admin_recalculate_all_levels,
- project_row
)
app_name = "projects"
urlpatterns = [
path("", ProjectListView.as_view(), name="project_list"),
- path(
- "status_row/",
- project_row,
- name="project_row",
- ),
path("/", project, name="project"),
# forms
path(
diff --git a/dashboard/projects/views.py b/dashboard/projects/views.py
index 73e7a89..0280102 100644
--- a/dashboard/projects/views.py
+++ b/dashboard/projects/views.py
@@ -1,5 +1,5 @@
-import datetime
import json
+from django.db.models import F, Sum
from django.shortcuts import render, HttpResponse, HttpResponseRedirect
from django.views.generic import ListView
from django.views.decorators.http import require_http_methods
@@ -27,18 +27,59 @@
class ProjectListView(ConditionalLoginRequiredMixin, ListView):
model = Project
+ def get_queryset(self):
+ return super().get_queryset().select_related(
+ "group", "agreement_status", "last_review_status"
+ ).prefetch_related("qi_set")
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- context["workcycle_list"] = WorkCycle.objects.all()
- context["workcycle_count"] = WorkCycle.objects.count()
- context["objective_list"] = Objective.objects.all()
- context["objective_count"] = Objective.objects.count()
- context["column_count"] = (
- Objective.objects.count() + WorkCycle.objects.count() + 7
- )
- context["quality_cols_count"] = 4 + WorkCycle.objects.count()
+
+ # Build a per-project objective map once to avoid repeated row-level lookups.
+ pos_by_project = {}
+ for po in ProjectObjective.objects.all().values(
+ "project_id",
+ "objective__name",
+ "id",
+ "level_achieved__name",
+ "unstarted_reason__name",
+ ):
+ pos_by_project.setdefault(po["project_id"], []).append(po)
+
+ projects = list(context["object_list"])
+ project_ids = [project.id for project in projects]
+
+ # Collect quality indicator totals in one grouped query.
+ quality_indicator_by_project = {
+ row["project_id"]: row["total"]
+ for row in ProjectObjective.objects.filter(
+ project_id__in=project_ids, level_achieved__isnull=False
+ )
+ .values("project_id")
+ .annotate(total=Sum(F("level_achieved__value") * F("objective__weight")))
+ }
+
+ # Attach prepared values to each project so the template can render without extra ORM work.
+ for project in projects:
+ project.projectobjectives = pos_by_project.get(project.id, [])
+ project.quality_history_values = project.qi_set.all()
+ project.quality_indicator_value = quality_indicator_by_project.get(
+ project.id, 0
+ )
+
+ workcycle_list = list(WorkCycle.objects.all())
+ objective_list = list(Objective.objects.all())
+ workcycle_count = len(workcycle_list)
+ objective_count = len(objective_list)
+
+ context["workcycle_list"] = workcycle_list
+ context["workcycle_count"] = workcycle_count
+ context["objective_list"] = objective_list
+ context["objective_count"] = objective_count
+ context["column_count"] = objective_count + workcycle_count + 7
+ context["quality_cols_count"] = 4 + workcycle_count
return context
@@ -137,25 +178,6 @@ def status_dashboardprojectobjective(request, projectobjective_id):
},
)
-def project_row(request, project_id):
- project = Project.objects.get(id=project_id)
-
- project.projectobjectives = project.projectobjective_set.all().values(
- "objective__name",
- "id",
- "level_achieved__name",
- "unstarted_reason__name"
- )
-
- return render(
- request,
- "projects/partial_project_list_row.html",
- {
- "project": project,
- }
- )
-
-
# action methods