diff --git a/accessibility_monitoring_platform/apps/audits/admin.py b/accessibility_monitoring_platform/apps/audits/admin.py index 86e7cab50..055c550ac 100644 --- a/accessibility_monitoring_platform/apps/audits/admin.py +++ b/accessibility_monitoring_platform/apps/audits/admin.py @@ -45,13 +45,14 @@ class CheckResultAdmin(admin.ModelAdmin): """Django admin configuration for CheckResult model""" search_fields = [ + "issue_identifier", "audit__case__organisation_name", "audit__case__case_number", "wcag_definition__name", "page__name", "page__page_type", ] - list_display = ["__str__", "audit", "page"] + list_display = ["issue_identifier", "__str__", "audit", "page"] list_filter = ["check_result_state"] @@ -75,7 +76,14 @@ class StatementCheckAdmin(admin.ModelAdmin): """Django admin configuration for StatementCheck model""" search_fields = ["label", "success_criteria", "report_text"] - list_display = ["label", "type", "position", "date_start", "date_end"] + list_display = [ + "issue_number", + "label", + "type", + "position", + "date_start", + "date_end", + ] list_filter = ["type"] fieldsets = ( ( @@ -97,12 +105,13 @@ class StatementCheckResultAdmin(admin.ModelAdmin): """Django admin configuration for StatementCheck model""" search_fields = [ + "issue_identifier", "audit__case__organisation_name", "statement_check__label", "statement_check__success_criteria", "statement_check__report_text", ] - list_display = ["statement_check", "audit", "is_deleted"] + list_display = ["issue_identifier", "statement_check", "audit", "is_deleted"] list_filter = ["is_deleted", "check_result_state", "retest_state"] fieldsets = ( ( @@ -154,12 +163,14 @@ class RetestCheckResultAdmin(admin.ModelAdmin): """Django admin configuration for RetestCheckResult model""" search_fields = [ + "issue_identifier", "check_result__wcag_definition__name", "retest__case__organisation_name", "retest_page__page__name", "retest_page__page__url", ] list_display = [ + "issue_identifier", "check_result", "retest", "retest_page", @@ -182,13 +193,14 @@ class RetestStatementCheckResultAdmin(admin.ModelAdmin): """Django admin configuration for RetestStatementCheckResult model""" search_fields = [ + "issue_identifier", "retest__case__case_number", "retest__case__organisation_name", "statement_check__label", "comment", ] list_display = [ - "id", + "issue_identifier", "retest", "type", "check_result_state", diff --git a/accessibility_monitoring_platform/apps/audits/migrations/0012_retestcheckresult_id_within_case_and_more.py b/accessibility_monitoring_platform/apps/audits/migrations/0012_retestcheckresult_id_within_case_and_more.py new file mode 100644 index 000000000..619e49d42 --- /dev/null +++ b/accessibility_monitoring_platform/apps/audits/migrations/0012_retestcheckresult_id_within_case_and_more.py @@ -0,0 +1,77 @@ +# Generated by Django 5.1.4 on 2025-01-21 08:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("audits", "0011_remove_audit_accessibility_statement_backup_url_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="checkresult", + name="issue_identifier", + field=models.CharField(max_length=20, default=""), + ), + migrations.AddField( + model_name="retestcheckresult", + name="id_within_case", + field=models.IntegerField(blank=True, default=0), + ), + migrations.AddField( + model_name="retestcheckresult", + name="issue_identifier", + field=models.CharField(max_length=20, default=""), + ), + migrations.AddField( + model_name="reteststatementcheckresult", + name="id_within_case", + field=models.IntegerField(blank=True, default=0), + ), + migrations.AddField( + model_name="reteststatementcheckresult", + name="issue_identifier", + field=models.CharField(max_length=20, default=""), + ), + migrations.AddField( + model_name="statementcheck", + name="issue_number", + field=models.IntegerField(blank=True, default=0), + ), + migrations.AddField( + model_name="statementcheckresult", + name="id_within_case", + field=models.IntegerField(blank=True, default=0), + ), + migrations.AddField( + model_name="statementcheckresult", + name="issue_identifier", + field=models.CharField(max_length=20, default=""), + ), + migrations.AddIndex( + model_name="checkresult", + index=models.Index( + fields=["issue_identifier"], name="audits_chec_issue_i_a8fd9c_idx" + ), + ), + migrations.AddIndex( + model_name="retestcheckresult", + index=models.Index( + fields=["issue_identifier"], name="audits_rete_issue_i_4df993_idx" + ), + ), + migrations.AddIndex( + model_name="reteststatementcheckresult", + index=models.Index( + fields=["issue_identifier"], name="audits_rete_issue_i_a854a4_idx" + ), + ), + migrations.AddIndex( + model_name="statementcheckresult", + index=models.Index( + fields=["issue_identifier"], name="audits_stat_issue_i_d20fe4_idx" + ), + ), + ] diff --git a/accessibility_monitoring_platform/apps/audits/migrations/0013_populate_ids_within_case.py b/accessibility_monitoring_platform/apps/audits/migrations/0013_populate_ids_within_case.py new file mode 100644 index 000000000..a6eb75aeb --- /dev/null +++ b/accessibility_monitoring_platform/apps/audits/migrations/0013_populate_ids_within_case.py @@ -0,0 +1,94 @@ +# Generated by Django 5.1.4 on 2025-01-21 08:48 + +from django.db import migrations + +ISSUE_IDENTIFIER_WCAG: str = "A" +ISSUE_IDENTIFIER_STATEMENT: str = "S" +CUSTOM_ISSUE: str = "C" + + +def build_issue_identifier(case, issue, issue_type): + """Format and return issue identifier""" + return f"{case.case_number}-{issue_type}-{issue.id_within_case}" + + +def populate_id_within_case(apps, schema_editor): # pylint: disable=unused-argument + Audit = apps.get_model("audits", "Audit") + CheckResult = apps.get_model("audits", "CheckResult") + StatementCheck = apps.get_model("audits", "StatementCheck") + StatementCheckResult = apps.get_model("audits", "StatementCheckResult") + Retest = apps.get_model("audits", "Retest") + RetestCheckResult = apps.get_model("audits", "RetestCheckResult") + RetestStatementCheckResult = apps.get_model("audits", "RetestStatementCheckResult") + + issue_number = 0 + for statement_check in StatementCheck.objects.all(): + issue_number += 1 + statement_check.issue_number = issue_number + statement_check.save() + + for audit in Audit.objects.all(): + for check_result in CheckResult.objects.filter(audit=audit): + check_result.issue_identifier = build_issue_identifier( + case=audit.case, + issue=check_result, + issue_type=ISSUE_IDENTIFIER_WCAG, + ) + check_result.save() + id_within_case = 0 + + for statement_check_result in StatementCheckResult.objects.filter(audit=audit): + id_within_case += 1 + statement_check_result.id_within_case = id_within_case + issue_type = ISSUE_IDENTIFIER_STATEMENT + if statement_check_result.statement_check is None: + issue_type += CUSTOM_ISSUE + statement_check_result.issue_identifier = build_issue_identifier( + case=audit.case, + issue=statement_check_result, + issue_type=issue_type, + ) + statement_check_result.save() + + for retest in Retest.objects.all(): + for retest_check_result in RetestCheckResult.objects.filter(retest=retest): + retest_check_result.id_within_case = ( + retest_check_result.check_result.id_within_case + ) + retest_check_result.issue_identifier = build_issue_identifier( + case=audit.case, + issue=retest_check_result, + issue_type=ISSUE_IDENTIFIER_WCAG, + ) + retest_check_result.save() + + id_within_case = 0 + for retest_statement_check_result in RetestStatementCheckResult.objects.filter( + retest=retest + ): + id_within_case += 1 + retest_statement_check_result.id_within_case = id_within_case + issue_type = ISSUE_IDENTIFIER_STATEMENT + if retest_statement_check_result.statement_check is None: + issue_type += CUSTOM_ISSUE + retest_statement_check_result.issue_identifier = build_issue_identifier( + case=retest.case, + issue=retest_statement_check_result, + issue_type=issue_type, + ) + retest_statement_check_result.save() + + +def reverse_code(apps, schema_editor): # pylint: disable=unused-argument + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ("audits", "0012_retestcheckresult_id_within_case_and_more"), + ] + + operations = [ + migrations.RunPython(populate_id_within_case, reverse_code=reverse_code), + ] diff --git a/accessibility_monitoring_platform/apps/audits/models.py b/accessibility_monitoring_platform/apps/audits/models.py index 05eb7e459..4bf1e5ca8 100644 --- a/accessibility_monitoring_platform/apps/audits/models.py +++ b/accessibility_monitoring_platform/apps/audits/models.py @@ -2,11 +2,13 @@ Models - audits (called tests by the users) """ +from __future__ import annotations + from datetime import date from django.db import models from django.db.models import Case as DjangoCase -from django.db.models import Q, When +from django.db.models import Max, Q, When from django.db.models.query import QuerySet from django.urls import reverse from django.utils import timezone @@ -16,6 +18,30 @@ from ..common.models import Boolean, StartEndDateManager, VersionModel from ..common.utils import amp_format_date, calculate_percentage +ISSUE_IDENTIFIER_WCAG: str = "A" +ISSUE_IDENTIFIER_STATEMENT: str = "S" + + +def build_issue_identifier( + case: Case, + issue: ( + CheckResult + | StatementCheckResult + | RetestCheckResult + | RetestStatementCheckResult + ), + custom_issue: bool = False, +) -> str: + """Format and return issue identifier""" + issue_type: str = ( + ISSUE_IDENTIFIER_WCAG + if isinstance(issue, (CheckResult, RetestCheckResult)) + else ISSUE_IDENTIFIER_STATEMENT + ) + if custom_issue: + issue_type += "C" + return f"{case.case_number}-{issue_type}-{issue.id_within_case}" + class Audit(VersionModel): """ @@ -202,7 +228,7 @@ class Meta: ordering = ["-id"] def __str__(self) -> str: - return str(f"{self.case}" f" (Test {amp_format_date(self.date_of_test)})") + return f"{self.case} (Test {amp_format_date(self.date_of_test)})" def get_absolute_url(self) -> str: return reverse("audits:edit-audit-metadata", kwargs={"pk": self.pk}) @@ -647,7 +673,7 @@ class Meta: def __str__(self) -> str: if self.description: - return str(f"{self.name}: {self.description} ({self.get_type_display()})") + return f"{self.name}: {self.description} ({self.get_type_display()})" return f"{self.name} ({self.get_type_display()})" def get_absolute_url(self) -> str: @@ -676,6 +702,7 @@ class RetestResult(models.TextChoices): Page, on_delete=models.PROTECT, related_name="checkresult_page" ) id_within_case = models.IntegerField(default=0, blank=True) + issue_identifier = models.CharField(max_length=20, default="") is_deleted = models.BooleanField(default=False) type = models.CharField( max_length=20, @@ -712,16 +739,24 @@ def dict_for_retest(self) -> dict[str, str]: class Meta: ordering = ["id"] + indexes = [ + models.Index( + fields=[ + "issue_identifier", + ] + ), + ] def __str__(self) -> str: - return str( - f"{self.page} | {self.wcag_definition} | {self.unique_id_within_case}" - ) + return f"{self.page} | {self.wcag_definition} | {self.issue_identifier}" def save(self, *args, **kwargs) -> None: self.updated = timezone.now() if not self.id: self.id_within_case = self.audit.checkresult_audit.all().count() + 1 + self.issue_identifier = build_issue_identifier( + case=self.audit.case, issue=self + ) super().save(*args, **kwargs) @property @@ -733,11 +768,6 @@ def matching_wcag_with_retest_notes_check_results(self) -> dict[str, str]: .exclude(retest_notes="") ) - @property - def unique_id_within_case(self) -> str: - """Unique identifies of check result within case to aid QA audit communication""" - return f"#E{self.id_within_case}" - class StatementCheck(models.Model): """ @@ -758,6 +788,7 @@ class Type(models.TextChoices): choices=Type.choices, default=Type.CUSTOM, ) + issue_number = models.IntegerField(default=0, blank=True) label = models.TextField(default="", blank=True) success_criteria = models.TextField(default="", blank=True) report_text = models.TextField(default="", blank=True) @@ -772,11 +803,14 @@ class Meta: def __str__(self) -> str: if self.success_criteria: - return str( - f"{self.label}: {self.success_criteria} ({self.get_type_display()})" - ) + return f"{self.label}: {self.success_criteria} ({self.get_type_display()})" return f"{self.label} ({self.get_type_display()})" + def save(self, *args, **kwargs) -> None: + if not self.id: + self.issue_number = StatementCheck.objects.all().count() + 1 + super().save(*args, **kwargs) + def get_absolute_url(self) -> str: return reverse("audits:statement-check-update", kwargs={"pk": self.pk}) @@ -800,6 +834,8 @@ class Result(models.TextChoices): NOT_TESTED = "not-tested", "Not tested" audit = models.ForeignKey(Audit, on_delete=models.PROTECT) + id_within_case = models.IntegerField(default=0, blank=True) + issue_identifier = models.CharField(max_length=20, default="") statement_check = models.ForeignKey( StatementCheck, on_delete=models.PROTECT, null=True, blank=True ) @@ -825,13 +861,30 @@ class Result(models.TextChoices): class Meta: ordering = ["statement_check__position", "id"] + indexes = [ + models.Index( + fields=[ + "issue_identifier", + ] + ), + ] def __str__(self) -> str: if self.statement_check is None: - return str(f"{self.audit} | Custom") - return str(f"{self.audit} | {self.statement_check}") + return f"{self.audit} | Custom ({self.issue_identifier})" + return f"{self.audit} | {self.statement_check} ({self.issue_identifier})" def save(self, *args, **kwargs) -> None: + if not self.id: + if self.statement_check: + self.id_within_case = self.statement_check.issue_number + else: + self.id_within_case = self.audit.statement_check_results.count() + 1 + self.issue_identifier = build_issue_identifier( + case=self.audit.case, + issue=self, + custom_issue=self.statement_check is None, + ) super().save(*args, **kwargs) @property @@ -906,7 +959,7 @@ def get_absolute_url(self) -> str: def __str__(self) -> str: if self.id_within_case == 0: return "12-week retest" - return str(f"Retest #{self.id_within_case}") + return f"Retest #{self.id_within_case}" @property def is_incomplete(self) -> bool: @@ -1079,6 +1132,8 @@ class RetestCheckResult(models.Model): """ retest = models.ForeignKey(Retest, on_delete=models.PROTECT) + id_within_case = models.IntegerField(default=0, blank=True) + issue_identifier = models.CharField(max_length=20, default="") retest_page = models.ForeignKey(RetestPage, on_delete=models.PROTECT) check_result = models.ForeignKey(CheckResult, on_delete=models.PROTECT) is_deleted = models.BooleanField(default=False) @@ -1103,12 +1158,25 @@ def matching_wcag_retest_check_results(self) -> dict[str, str]: class Meta: ordering = ["id"] + indexes = [ + models.Index( + fields=[ + "issue_identifier", + ] + ), + ] def __str__(self) -> str: - return str(f"{self.retest_page} | {self.check_result}") + return f"{self.retest_page} | {self.check_result}" def save(self, *args, **kwargs) -> None: self.updated = timezone.now() + if not self.id: + if self.id_within_case == 0: + self.id_within_case = self.check_result.id_within_case + self.issue_identifier = build_issue_identifier( + case=self.retest.case, issue=self + ) super().save(*args, **kwargs) @property @@ -1153,6 +1221,8 @@ class Result(models.TextChoices): NOT_TESTED = "not-tested", "Not tested" retest = models.ForeignKey(Retest, on_delete=models.PROTECT) + id_within_case = models.IntegerField(default=0, blank=True) + issue_identifier = models.CharField(max_length=20, default="") statement_check = models.ForeignKey( StatementCheck, on_delete=models.PROTECT, null=True, blank=True ) @@ -1171,11 +1241,34 @@ class Result(models.TextChoices): class Meta: ordering = ["statement_check__position", "id"] + indexes = [ + models.Index( + fields=[ + "issue_identifier", + ] + ), + ] + + def save(self, *args, **kwargs) -> None: + if not self.id: + if self.id_within_case == 0: + self.id_within_case = ( + RetestStatementCheckResult.objects.filter( + retest__case=self.retest.case + ).aggregate(Max("id_within_case", default=0))["id_within_case__max"] + + 1 + ) + self.issue_identifier = build_issue_identifier( + case=self.retest.case, + issue=self, + custom_issue=self.statement_check is None, + ) + super().save(*args, **kwargs) def __str__(self) -> str: if self.statement_check is None: - return str(f"{self.retest} | Custom") - return str(f"{self.retest} | {self.statement_check}") + return f"{self.retest} | Custom ({self.issue_identifier})" + return f"{self.retest} | {self.statement_check} ({self.issue_identifier})" @property def label(self): diff --git a/accessibility_monitoring_platform/apps/audits/templates/audits/forms/equality_body_retest_comparison_update.html b/accessibility_monitoring_platform/apps/audits/templates/audits/forms/equality_body_retest_comparison_update.html index f8155bf7b..f46f54fce 100644 --- a/accessibility_monitoring_platform/apps/audits/templates/audits/forms/equality_body_retest_comparison_update.html +++ b/accessibility_monitoring_platform/apps/audits/templates/audits/forms/equality_body_retest_comparison_update.html @@ -109,6 +109,9 @@
{{ retest_check_result.retest_page.page.location }}
{% endif %} +