From 3ae42393a646a594b2e70145a69023dfc2363a21 Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Mon, 20 Apr 2026 05:05:01 +0300 Subject: [PATCH 1/9] Updated psycopg2 --- dashboard/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/requirements.txt b/dashboard/requirements.txt index ebc23e5..1ccb8b6 100644 --- a/dashboard/requirements.txt +++ b/dashboard/requirements.txt @@ -2,7 +2,7 @@ Django==5.2.13 PyYAML==6.0.2 docutils==0.21.2 whitenoise==6.9.0 -psycopg2-binary==2.9.10 +psycopg2-binary==2.9.11 tzdata==2025.1 django-browser-reload==1.18.0 django-tinymce==4.1.0 From bfc38a171b79d802786a9eedb3eca6da753815cf Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Mon, 20 Apr 2026 05:47:48 +0300 Subject: [PATCH 2/9] Added a testing shim for OIDC. This allows tests in environments that haven't loaded OIDC to continue running. See https://docs.pytest.org/en/latest/reference/fixtures.html#conftest-py-sharing-fixtures-across-multiple-files for notes on shared fixtures. --- dashboard/conftest.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 dashboard/conftest.py diff --git a/dashboard/conftest.py b/dashboard/conftest.py new file mode 100644 index 0000000..67d462a --- /dev/null +++ b/dashboard/conftest.py @@ -0,0 +1,28 @@ +import sys +import types + +from django.http import HttpResponse + + +# Creat a "fake" mozilla_django_oidc.views so that tests will run, +# even if mozilla_django_oidc is not available at import time for +# tests. + +if "mozilla_django_oidc" not in sys.modules: + oidc_module = types.ModuleType("mozilla_django_oidc") + oidc_views_module = types.ModuleType("mozilla_django_oidc.views") + + class _DummyOIDCView: + @classmethod + def as_view(cls): + def _view(request, *args, **kwargs): + return HttpResponse("") + + return _view + + oidc_views_module.OIDCAuthenticationRequestView = _DummyOIDCView + oidc_views_module.OIDCAuthenticationCallbackView = _DummyOIDCView + oidc_views_module.OIDCLogoutView = _DummyOIDCView + oidc_module.views = oidc_views_module + sys.modules["mozilla_django_oidc"] = oidc_module + sys.modules["mozilla_django_oidc.views"] = oidc_views_module \ No newline at end of file From 3a8a10d0949647981555bbfcc8b879fc96290f6d Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Mon, 20 Apr 2026 06:26:24 +0300 Subject: [PATCH 3/9] Added a series of tests for view functions Updated: test_toggle_condition_url_patterns (Simplified, and now only checks the reversed route, rather than the string) New checks for user permissions: * test_action_toggle_commitment_denies_user_without_permission * test_action_toggle_condition_denies_user_without_permission * test_action_select_reason_denies_user_without_permission New checks for HTTP methods: * test_action_toggle_commitment_rejects_non_put_method * test_action_toggle_condition_rejects_non_put_method * test_action_select_reason_rejects_non_put_method New checks for expected results: new: test_action_toggle_commitment_allows_authorized_put_and_updates_commitment new: test_action_toggle_condition_allows_authorized_put_and_updates_status new: test_action_select_reason_allows_authorized_put_and_sets_reason I used Copilot to help create these tests. --- dashboard/projects/test_views.py | 255 ++++++++++++++++++++++++++++++- 1 file changed, 251 insertions(+), 4 deletions(-) diff --git a/dashboard/projects/test_views.py b/dashboard/projects/test_views.py index b70c423..9c3c873 100644 --- a/dashboard/projects/test_views.py +++ b/dashboard/projects/test_views.py @@ -1,10 +1,257 @@ import pytest +from urllib.parse import parse_qs, urlparse from django.urls import reverse +from django.contrib.auth.models import Permission, User + +from framework.models import Condition, Level, Objective, ObjectiveGroup, Reason, WorkCycle +from projects.models import Commitment, Project, ProjectObjective, ProjectObjectiveCondition def test_toggle_condition_url_patterns(): - assert reverse( + url = reverse("projects:action_toggle_condition", args=[1]) + assert url == "/action_toggle_condition/1" + + +@pytest.fixture +def user_without_permissions(client): + user = User.objects.create_user(username="no_perm", password="password") + client.login(username="no_perm", password="password") + return user + + +@pytest.fixture +def user_can_change_commitment(client): + user = User.objects.create_user(username="change_commitment", password="password") + permission = Permission.objects.get( + codename="change_commitment", + content_type__app_label="projects", + ) + user.user_permissions.add(permission) + client.login(username="change_commitment", password="password") + return user + + +@pytest.fixture +def user_can_change_projectobjectivecondition(client): + user = User.objects.create_user( + username="change_projectobjectivecondition", password="password" + ) + permission = Permission.objects.get( + codename="change_projectobjectivecondition", + content_type__app_label="projects", + ) + user.user_permissions.add(permission) + client.login(username="change_projectobjectivecondition", password="password") + return user + + +@pytest.fixture +def user_can_change_projectobjective(client): + user = User.objects.create_user( + username="change_projectobjective", password="password" + ) + permission = Permission.objects.get( + codename="change_projectobjective", + content_type__app_label="projects", + ) + user.user_permissions.add(permission) + client.login(username="change_projectobjective", password="password") + return user + + +@pytest.fixture +def objective_group(): + return ObjectiveGroup.objects.create(name="group") + + +@pytest.fixture +def objective(objective_group): + return Objective.objects.create(name="objective", group=objective_group, weight=1) + + +@pytest.fixture +def level(): + return Level.objects.create(name="level", value=1) + + +@pytest.fixture +def work_cycle(): + return WorkCycle.objects.create(name="wc", timestamp="2026-01-01", is_current=True) + + +@pytest.fixture +def project(objective, level, work_cycle): + return Project.objects.create(name="project") + + +@pytest.fixture +def condition(objective, level): + return Condition.objects.create(name="condition", objective=objective, level=level) + + +@pytest.fixture +def project_objective(project, objective): + return ProjectObjective.objects.get(project=project, objective=objective) + + +@pytest.fixture +def project_objective_condition(project, objective, condition): + return ProjectObjectiveCondition.objects.get( + project=project, + objective=objective, + condition=condition, + ) + + +@pytest.fixture +def commitment(project, objective, level, work_cycle): + return Commitment.objects.get( + project=project, + objective=objective, + level=level, + work_cycle=work_cycle, + ) + + +@pytest.fixture +def reason(): + return Reason.objects.create(name="not-started", value=1) + + +@pytest.mark.django_db +def test_action_toggle_commitment_denies_user_without_permission( + client, user_without_permissions, commitment +): + url = reverse("projects:action_toggle_commitment", args=[commitment.id]) + response = client.put(url) + + assert response.status_code == 302 + expected_redirect = f"{reverse('login')}?next={url}" + assert response.url == expected_redirect + + +@pytest.mark.django_db +def test_action_toggle_condition_denies_user_without_permission( + client, user_without_permissions, project_objective_condition +): + url = ( + reverse( + "projects:action_toggle_condition", + args=[project_objective_condition.id], + ) + + "?status=&target=done" + ) + response = client.put(url) + + assert response.status_code == 302 + parsed = urlparse(response.url) + assert parsed.path == reverse("login") + assert parse_qs(parsed.query)["next"][0] == url + + +@pytest.mark.django_db +def test_action_select_reason_denies_user_without_permission( + client, user_without_permissions, project_objective, reason +): + url = reverse("projects:action_select_reason", args=[project_objective.id]) + response = client.generic( + "PUT", + url, + data=f"ifnotstarted={reason.id}", + content_type="application/x-www-form-urlencoded", + ) + + assert response.status_code == 302 + expected_redirect = f"{reverse('login')}?next={url}" + assert response.url == expected_redirect + + +@pytest.mark.django_db +def test_action_toggle_commitment_rejects_non_put_method( + client, user_can_change_commitment, commitment +): + url = reverse("projects:action_toggle_commitment", args=[commitment.id]) + response = client.get(url) + + assert response.status_code == 405 + + +@pytest.mark.django_db +def test_action_toggle_commitment_allows_authorized_put_and_updates_commitment( + client, user_can_change_commitment, commitment +): + assert commitment.committed is False + + url = reverse("projects:action_toggle_commitment", args=[commitment.id]) + response = client.put(url) + + commitment.refresh_from_db() + assert response.status_code == 200 + assert commitment.committed is True + assert response["HX-Trigger-After-Swap"] == "updateCommitment" + + +@pytest.mark.django_db +def test_action_toggle_condition_rejects_non_put_method( + client, user_can_change_projectobjectivecondition, project_objective_condition +): + url = ( + reverse( + "projects:action_toggle_condition", + args=[project_objective_condition.id], + ) + + "?status=&target=done" + ) + response = client.get(url) + + assert response.status_code == 405 + + +@pytest.mark.django_db +def test_action_toggle_condition_allows_authorized_put_and_updates_status( + client, user_can_change_projectobjectivecondition, project_objective_condition +): + assert project_objective_condition.status == "" + + url = ( + reverse( "projects:action_toggle_condition", - args=[1], - query={"status": "DO", "target": "done"} - ) == "/action_toggle_condition/1?status=DO&target=done" + args=[project_objective_condition.id], + ) + + "?status=&target=done" + ) + response = client.put(url) + + project_objective_condition.refresh_from_db() + assert response.status_code == 200 + assert project_objective_condition.status == "DO" + assert "HX-Trigger-After-Swap" in response + + +@pytest.mark.django_db +def test_action_select_reason_rejects_non_put_method( + client, user_can_change_projectobjective, project_objective +): + url = reverse("projects:action_select_reason", args=[project_objective.id]) + response = client.get(url) + + assert response.status_code == 405 + + +@pytest.mark.django_db +def test_action_select_reason_allows_authorized_put_and_sets_reason( + client, user_can_change_projectobjective, project_objective, reason +): + assert project_objective.unstarted_reason is None + + url = reverse("projects:action_select_reason", args=[project_objective.id]) + response = client.generic( + "PUT", + url, + data=f"ifnotstarted={reason.id}", + content_type="application/x-www-form-urlencoded", + ) + + project_objective.refresh_from_db() + assert response.status_code == 200 + assert project_objective.unstarted_reason_id == reason.id From 77815810c6c4790be974256c0d10dfbde4e2f4f1 Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Mon, 20 Apr 2026 07:01:19 +0300 Subject: [PATCH 4/9] Added a permissions constraint for project_basic_form_save This fixes an auth issue. It was possible to POST a change to a project, without even being authenticated. The only safeguard was the user interface, that didn't offer make it possible. This adds @permission_required("projects.change_project") to the view, and some tests. * added a fixture for a user with permission to change a project * added a test: do we reject changes from unauthenticated users? * added a test: do we reject changes from unauthorised users? * added a test: do we allow and save changes from users with the right permissions? I used Copilot to uncover the issue and help create the tests. --- dashboard/projects/test_views.py | 84 ++++++++++++++++++++++++++++++++ dashboard/projects/views.py | 1 + 2 files changed, 85 insertions(+) diff --git a/dashboard/projects/test_views.py b/dashboard/projects/test_views.py index 9c3c873..468ae65 100644 --- a/dashboard/projects/test_views.py +++ b/dashboard/projects/test_views.py @@ -59,6 +59,18 @@ def user_can_change_projectobjective(client): return user +@pytest.fixture +def user_can_change_project(client): + user = User.objects.create_user(username="change_project", password="password") + permission = Permission.objects.get( + codename="change_project", + content_type__app_label="projects", + ) + user.user_permissions.add(permission) + client.login(username="change_project", password="password") + return user + + @pytest.fixture def objective_group(): return ObjectiveGroup.objects.create(name="group") @@ -118,6 +130,78 @@ def reason(): return Reason.objects.create(name="not-started", value=1) +@pytest.mark.django_db +def test_project_basic_form_save_denies_unauthenticated_user(client, project): + original_owner = project.owner + url = reverse("projects:project_basic_form_save", args=[project.id]) + response = client.post( + url, + data={ + "name": project.name, + "url": project.url, + "group": "", + "owner": "changed owner", + "driver": project.driver or "", + "agreement_status": "", + "last_review": "", + "last_review_status": "", + }, + ) + + project.refresh_from_db() + assert response.status_code == 302 + assert response.url == f"{reverse('login')}?next={url}" + assert project.owner == original_owner + + +@pytest.mark.django_db +def test_project_basic_form_save_denies_user_without_permission( + client, user_without_permissions, project +): + original_owner = project.owner + url = reverse("projects:project_basic_form_save", args=[project.id]) + response = client.post( + url, + data={ + "name": project.name, + "url": project.url, + "group": "", + "owner": "changed owner", + "driver": project.driver or "", + "agreement_status": "", + "last_review": "", + "last_review_status": "", + }, + ) + + project.refresh_from_db() + assert response.status_code == 302 + assert response.url == f"{reverse('login')}?next={url}" + assert project.owner == original_owner + + +@pytest.mark.django_db +def test_project_basic_form_save_allows_user_with_permission(client, user_can_change_project, project): + url = reverse("projects:project_basic_form_save", args=[project.id]) + response = client.post( + url, + data={ + "name": project.name, + "url": project.url, + "group": "", + "owner": "changed owner", + "driver": project.driver or "", + "agreement_status": "", + "last_review": "", + "last_review_status": "", + }, + ) + + project.refresh_from_db() + assert response.status_code == 200 + assert project.owner == "changed owner" + + @pytest.mark.django_db def test_action_toggle_commitment_denies_user_without_permission( client, user_without_permissions, commitment diff --git a/dashboard/projects/views.py b/dashboard/projects/views.py index 4e88c26..73e7a89 100644 --- a/dashboard/projects/views.py +++ b/dashboard/projects/views.py @@ -234,6 +234,7 @@ def action_select_reason(request, projectobjective_id): # form methods +@permission_required("projects.change_project") @require_http_methods(["POST"]) def project_basic_form_save(request, project_id): instance = Project.objects.get(id=project_id) From 58114d598ff9c1c82c25a5b5f89e243f4a17bf5b Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Mon, 20 Apr 2026 08:53:44 +0300 Subject: [PATCH 5/9] Added a failing test for cascading behaviour If a new Level and Condition are added, these do not cascade to relationships with Commitments. --- dashboard/framework/test_models.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/dashboard/framework/test_models.py b/dashboard/framework/test_models.py index 0647fff..677470e 100644 --- a/dashboard/framework/test_models.py +++ b/dashboard/framework/test_models.py @@ -153,3 +153,29 @@ def test_new_objective_means_new_commitments( assert project.commitment_set.count() == 2 assert work_cycle.commitment_set.count() == 2 + + +@pytest.mark.django_db +def test_new_condition_with_new_level_backfills_commitment( + project, objective, condition, work_cycle +): + # A new condition at a new level should create a matching commitment + # for existing rows. + + assert Commitment.objects.filter( + project=project, objective=objective, work_cycle=work_cycle + ).count() == 1 + + new_level = Level.objects.create(name="test_level_2", value=2) + Condition.objects.create( + name="test_condition_2", objective=objective, level=new_level + ) + + # Expected behaviour: creating a new condition/level backfills + # commitments. + assert Commitment.objects.filter( + project=project, + objective=objective, + work_cycle=work_cycle, + level=new_level, + ).exists() From 4e3bf46c21f368c71daa881322548b7ceb09a55a Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Mon, 20 Apr 2026 14:30:19 +0200 Subject: [PATCH 6/9] Added Commitments creation to Condition.save() Fixes a failing test for Commitments that don't get created properly. --- dashboard/framework/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dashboard/framework/models.py b/dashboard/framework/models.py index f2741cc..8b9a0b1 100644 --- a/dashboard/framework/models.py +++ b/dashboard/framework/models.py @@ -166,6 +166,7 @@ def save(self, *args, **kwargs): from projects.models import ( ProjectObjective, ProjectObjectiveCondition, + Commitment ) # avoids circular import super().save(*args, **kwargs) @@ -178,6 +179,13 @@ def save(self, *args, **kwargs): objective=projectobjective.objective, condition=self, ) + for work_cycle in WorkCycle.objects.all(): + Commitment.objects.get_or_create( + work_cycle=work_cycle, + project=projectobjective.project, + objective=projectobjective.objective, + level=self.level, + ) class Meta: ordering = ["objective__name", "level__value"] From 88306b4f9a1ab1cb6ad242734ca3fbb1247d69f3 Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Tue, 21 Apr 2026 21:56:47 +0100 Subject: [PATCH 7/9] Added a select_related to ProjectListView --- dashboard/projects/views.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dashboard/projects/views.py b/dashboard/projects/views.py index 73e7a89..4f752c1 100644 --- a/dashboard/projects/views.py +++ b/dashboard/projects/views.py @@ -27,6 +27,11 @@ class ProjectListView(ConditionalLoginRequiredMixin, ListView): model = Project + def get_queryset(self): + return super().get_queryset().select_related( + "group", "agreement_status", "last_review_status" + ) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) From 54953e1bb4bd1d731248cd0e5221eb0d90dfeee2 Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Wed, 22 Apr 2026 22:43:43 +0100 Subject: [PATCH 8/9] Added a test to check that key data get to the dashboard --- dashboard/projects/test_views.py | 38 +++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/dashboard/projects/test_views.py b/dashboard/projects/test_views.py index 468ae65..38500e2 100644 --- a/dashboard/projects/test_views.py +++ b/dashboard/projects/test_views.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import Permission, User from framework.models import Condition, Level, Objective, ObjectiveGroup, Reason, WorkCycle -from projects.models import Commitment, Project, ProjectObjective, ProjectObjectiveCondition +from projects.models import Commitment, Project, ProjectObjective, ProjectObjectiveCondition, QI def test_toggle_condition_url_patterns(): url = reverse("projects:action_toggle_condition", args=[1]) @@ -339,3 +339,39 @@ def test_action_select_reason_allows_authorized_put_and_sets_reason( project_objective.refresh_from_db() assert response.status_code == 200 assert project_objective.unstarted_reason_id == reason.id + + +@pytest.mark.django_db +def test_project_list_renders_qi_history_current_qi_and_levels( + client, objective, project, project_objective, work_cycle +): + user = User.objects.create_user(username="viewer", password="password") + client.login(username="viewer", password="password") + + 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 From 4acce3604fc21c3474a56abafc4d65f525a228b8 Mon Sep 17 00:00:00 2001 From: Daniele Procida Date: Wed, 22 Apr 2026 23:35:00 +0100 Subject: [PATCH 9/9] Implemented bulkloading for the project list in the view Previously, the project list was lazy-loaded with htmx. This update restores the bulk delivery of the page, along with opportuities for substantially reducing the number of per-row queries. With luck, this should turn out to be fast enough in practice that we can keep this approach. --- .../projects/partial_project_list_row.html | 4 +- .../templates/projects/project_list.html | 10 +-- dashboard/projects/urls.py | 6 -- dashboard/projects/views.py | 75 ++++++++++++------- 4 files changed, 49 insertions(+), 46 deletions(-) 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/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 4f752c1..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 @@ -30,20 +30,56 @@ class ProjectListView(ConditionalLoginRequiredMixin, ListView): 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 @@ -142,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