Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Django FSM migration to Viewflow #4296

Merged
merged 5 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 17 additions & 25 deletions backend/audit/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
SacValidationWaiver,
UeiValidationWaiver,
)
from audit.models.models import STATUS
from audit.models.viewflow import sac_transition
from audit.validators import (
validate_auditee_certification_json,
validate_auditor_certification_json,
Expand Down Expand Up @@ -154,8 +156,8 @@ def save_model(self, request, obj, form, change):
try:
sac = SingleAuditChecklist.objects.get(report_id=obj.report_id_id)
if sac.submission_status in [
SingleAuditChecklist.STATUS.READY_FOR_CERTIFICATION,
SingleAuditChecklist.STATUS.AUDITOR_CERTIFIED,
STATUS.READY_FOR_CERTIFICATION,
STATUS.AUDITOR_CERTIFIED,
]:
logger.info(
f"User {request.user.email} is applying waiver for SAC with status: {sac.submission_status}"
Expand All @@ -167,7 +169,7 @@ def save_model(self, request, obj, form, change):
f"SAC {sac.report_id} updated successfully with waiver by user: {request.user.email}."
)
elif (
SingleAuditChecklist.STATUS.IN_PROGRESS
STATUS.IN_PROGRESS
and SacValidationWaiver.TYPES.FINDING_REFERENCE_NUMBER
in obj.waiver_types
):
Expand All @@ -182,7 +184,7 @@ def save_model(self, request, obj, form, change):
messages.set_level(request, messages.WARNING)
messages.warning(
request,
f"Cannot apply waiver to SAC with status {sac.submission_status}. Expected status to be one of {SingleAuditChecklist.STATUS.READY_FOR_CERTIFICATION}, {SingleAuditChecklist.STATUS.AUDITOR_CERTIFIED}, or {SingleAuditChecklist.STATUS.IN_PROGRESS}.",
f"Cannot apply waiver to SAC with status {sac.submission_status}. Expected status to be one of {STATUS.READY_FOR_CERTIFICATION}, {STATUS.AUDITOR_CERTIFIED}, or {STATUS.IN_PROGRESS}.",
)
logger.warning(
f"User {request.user.email} attempted to apply waiver to SAC with invalid status: {sac.submission_status}"
Expand Down Expand Up @@ -218,20 +220,13 @@ def handle_auditor_certification(self, request, obj, sac):
},
}
)
if (
sac.submission_status
== SingleAuditChecklist.STATUS.READY_FOR_CERTIFICATION
):
if sac.submission_status == STATUS.READY_FOR_CERTIFICATION:
validated = validate_auditor_certification_json(auditor_certification)
sac.auditor_certification = validated
sac.transition_to_auditor_certified()
sac.save(
event_user=request.user,
event_type=SubmissionEvent.EventType.AUDITOR_CERTIFICATION_COMPLETED,
)
logger.info(
f"Auditor certification completed for SAC {sac.report_id} by user: {request.user.email}."
)
if sac_transition(request, sac, transition_to=STATUS.AUDITOR_CERTIFIED):
logger.info(
f"Auditor certification completed for SAC {sac.report_id} by user: {request.user.email}."
)

def handle_auditee_certification(self, request, obj, sac):
if SacValidationWaiver.TYPES.AUDITEE_CERTIFYING_OFFICIAL in obj.waiver_types:
Expand All @@ -257,17 +252,14 @@ def handle_auditee_certification(self, request, obj, sac):
},
}
)
if sac.submission_status == SingleAuditChecklist.STATUS.AUDITOR_CERTIFIED:
if sac.submission_status == STATUS.AUDITOR_CERTIFIED:
validated = validate_auditee_certification_json(auditee_certification)
sac.auditee_certification = validated
sac.transition_to_auditee_certified()
sac.save(
event_user=request.user,
event_type=SubmissionEvent.EventType.AUDITEE_CERTIFICATION_COMPLETED,
)
logger.info(
f"Auditee certification completed for SAC {sac.report_id} by user: {request.user.email}."
)

if sac_transition(request, sac, transition_to=STATUS.AUDITEE_CERTIFIED):
logger.info(
f"Auditee certification completed for SAC {sac.report_id} by user: {request.user.email}."
)


class UeiValidationWaiverAdmin(admin.ModelAdmin):
Expand Down
2 changes: 1 addition & 1 deletion backend/audit/intake_to_dissemination.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ def load_general(self):
oversight_agency = self.single_audit_checklist.oversight_agency

dates_by_status = self._get_dates_from_sac()
status = self.single_audit_checklist.STATUS
status = self.single_audit_checklist.get_statuses()
ready_for_certification_date = dates_by_status[status.READY_FOR_CERTIFICATION]
if self.mode == IntakeToDissemination.DISSEMINATION:
submitted_date = self._convert_utc_to_american_samoa_zone(
Expand Down
47 changes: 47 additions & 0 deletions backend/audit/migrations/0013_singleauditchecklistflow_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 5.1 on 2024-09-18 18:44

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("audit", "0012_alter_sacvalidationwaiver_waiver_types"),
]

operations = [
migrations.CreateModel(
name="SingleAuditChecklistFlow",
fields=[
(
"singleauditchecklist_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="audit.singleauditchecklist",
),
),
],
bases=("audit.singleauditchecklist",),
),
migrations.AlterField(
model_name="singleauditchecklist",
name="submission_status",
field=models.CharField(
choices=[
("in_progress", "In Progress"),
("ready_for_certification", "Ready for Certification"),
("auditor_certified", "Auditor Certified"),
("auditee_certified", "Auditee Certified"),
("certified", "Certified"),
("submitted", "Submitted"),
("disseminated", "Disseminated"),
],
default="in_progress",
),
),
]
165 changes: 31 additions & 134 deletions backend/audit/models/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timezone, timedelta
from datetime import timedelta
from itertools import chain
import json
import logging
Expand All @@ -13,8 +13,6 @@
from django.utils.translation import gettext_lazy as _
from django.utils import timezone as django_timezone

from django_fsm import FSMField, transition

import audit.cross_validation
from audit.cross_validation.naming import SECTION_NAMES
from audit.intake_to_dissemination import IntakeToDissemination
Expand Down Expand Up @@ -161,6 +159,20 @@ class LateChangeError(Exception):
"""


class STATUS:
"""
The possible states of a submission.
"""

IN_PROGRESS = "in_progress"
READY_FOR_CERTIFICATION = "ready_for_certification"
AUDITOR_CERTIFIED = "auditor_certified"
AUDITEE_CERTIFIED = "auditee_certified"
CERTIFIED = "certified"
SUBMITTED = "submitted"
DISSEMINATED = "disseminated"


class SingleAuditChecklist(models.Model, GeneralInformationMixin): # type: ignore
"""
Monolithic Single Audit Checklist.
Expand All @@ -185,7 +197,7 @@ def save(self, *args, **kwargs):
in progress isn't being altered; skip this if we know this submission is
in progress.
"""
if self.submission_status != self.STATUS.IN_PROGRESS:
if self.submission_status != STATUS.IN_PROGRESS:
try:
self._reject_late_changes()
except LateChangeError as err:
Expand Down Expand Up @@ -278,18 +290,11 @@ def get_friendly_status(self) -> str:
"""Return the friendly version of submission_status."""
return dict(self.STATUS_CHOICES)[self.submission_status]

# Constants:
class STATUS:
"""The states that a submission can be in."""

IN_PROGRESS = "in_progress"
READY_FOR_CERTIFICATION = "ready_for_certification"
AUDITOR_CERTIFIED = "auditor_certified"
AUDITEE_CERTIFIED = "auditee_certified"
CERTIFIED = "certified"
SUBMITTED = "submitted"
DISSEMINATED = "disseminated"
def get_statuses(self) -> type[STATUS]:
"""Return all possible statuses."""
return STATUS

# Constants:
STATUS_CHOICES = (
(STATUS.IN_PROGRESS, "In Progress"),
(STATUS.READY_FOR_CERTIFICATION, "Ready for Certification"),
Expand Down Expand Up @@ -324,7 +329,9 @@ class STATUS:
# 0. Meta data
submitted_by = models.ForeignKey(User, on_delete=models.PROTECT)
date_created = models.DateTimeField(auto_now_add=True)
submission_status = FSMField(default=STATUS.IN_PROGRESS, choices=STATUS_CHOICES)
submission_status = models.CharField(
default=STATUS.IN_PROGRESS, choices=STATUS_CHOICES
)
data_source = models.CharField(default="GSAFAC")

# implement an array of tuples as two arrays since we can only have simple fields inside an array
Expand Down Expand Up @@ -502,134 +509,24 @@ def validate_individually(self):

return result

@transition(
field="submission_status",
source=STATUS.IN_PROGRESS,
target=STATUS.READY_FOR_CERTIFICATION,
)
def transition_to_ready_for_certification(self):
"""
The permission checks verifying that the user attempting to do this has
the appropriate privileges will be done at the view level.
"""
self.transition_name.append(SingleAuditChecklist.STATUS.READY_FOR_CERTIFICATION)
self.transition_date.append(datetime.now(timezone.utc))

@transition(
field="submission_status",
source=[
STATUS.READY_FOR_CERTIFICATION,
STATUS.AUDITOR_CERTIFIED,
STATUS.AUDITEE_CERTIFIED,
],
target=STATUS.IN_PROGRESS,
)
def transition_to_in_progress_again(self):
"""
The permission checks verifying that the user attempting to do this has
the appropriate privileges will be done at the view level.
"""

# null out any existing certifications on this submission
self.auditor_certification = None
self.auditee_certification = None

self.transition_name.append(SingleAuditChecklist.STATUS.IN_PROGRESS)
self.transition_date.append(datetime.now(timezone.utc))

@transition(
field="submission_status",
source=STATUS.READY_FOR_CERTIFICATION,
target=STATUS.AUDITOR_CERTIFIED,
)
def transition_to_auditor_certified(self):
"""
The permission checks verifying that the user attempting to do this has
the appropriate privileges will be done at the view level.
"""
self.transition_name.append(SingleAuditChecklist.STATUS.AUDITOR_CERTIFIED)
self.transition_date.append(datetime.now(timezone.utc))

@transition(
field="submission_status",
source=STATUS.AUDITOR_CERTIFIED,
target=STATUS.AUDITEE_CERTIFIED,
)
def transition_to_auditee_certified(self):
"""
The permission checks verifying that the user attempting to do this has
the appropriate privileges will be done at the view level.
"""
self.transition_name.append(SingleAuditChecklist.STATUS.AUDITEE_CERTIFIED)
self.transition_date.append(datetime.now(timezone.utc))

@transition(
field="submission_status",
source=STATUS.AUDITEE_CERTIFIED,
target=STATUS.SUBMITTED,
)
def transition_to_submitted(self):
"""
The permission checks verifying that the user attempting to do this has
the appropriate privileges will be done at the view level.
"""
self.transition_name.append(SingleAuditChecklist.STATUS.SUBMITTED)
self.transition_date.append(datetime.now(timezone.utc))

@transition(
field="submission_status",
source=STATUS.SUBMITTED,
target=STATUS.DISSEMINATED,
)
def transition_to_disseminated(self):
self.transition_name.append(SingleAuditChecklist.STATUS.DISSEMINATED)
self.transition_date.append(datetime.now(timezone.utc))

@transition(
field="submission_status",
source=[
STATUS.READY_FOR_CERTIFICATION,
STATUS.AUDITOR_CERTIFIED,
STATUS.AUDITEE_CERTIFIED,
STATUS.CERTIFIED,
],
target=STATUS.AUDITEE_CERTIFIED,
)
def transition_to_in_progress(self):
"""
Any edit to a submission in the following states should result in it
moving back to STATUS.IN_PROGRESS:

+ STATUS.READY_FOR_CERTIFICATION
+ STATUS.AUDITOR_CERTIFIED
+ STATUS.AUDITEE_CERTIFIED
+ STATUS.CERTIFIED

For the moment we're not trying anything fancy like catching changes at
the model level, and will again leave it up to the views to track that
changes have been made at that point.
"""
self.transition_name.append(SingleAuditChecklist.STATUS.AUDITEE_CERTIFIED)
self.transition_date.append(datetime.now(timezone.utc))

@property
def is_auditee_certified(self):
return self.submission_status in [
SingleAuditChecklist.STATUS.AUDITEE_CERTIFIED,
SingleAuditChecklist.STATUS.CERTIFIED,
STATUS.AUDITEE_CERTIFIED,
STATUS.CERTIFIED,
]

@property
def is_auditor_certified(self):
return self.submission_status in [
SingleAuditChecklist.STATUS.AUDITEE_CERTIFIED,
SingleAuditChecklist.STATUS.AUDITOR_CERTIFIED,
SingleAuditChecklist.STATUS.CERTIFIED,
STATUS.AUDITEE_CERTIFIED,
STATUS.AUDITOR_CERTIFIED,
STATUS.CERTIFIED,
]

@property
def is_submitted(self):
return self.submission_status in [SingleAuditChecklist.STATUS.DISSEMINATED]
return self.submission_status in [STATUS.DISSEMINATED]

def get_transition_date(self, status):
index = self.transition_name.index(status)
Expand Down Expand Up @@ -659,7 +556,7 @@ class ExcelFile(models.Model):
date_created = models.DateTimeField(auto_now_add=True)

def save(self, *args, **kwargs):
if self.sac.submission_status != SingleAuditChecklist.STATUS.IN_PROGRESS:
if self.sac.submission_status != STATUS.IN_PROGRESS:
raise LateChangeError("Attemtped Excel file upload")

self.filename = f"{self.sac.report_id}--{self.form_section}.xlsx"
Expand Down Expand Up @@ -706,7 +603,7 @@ class SingleAuditReportFile(models.Model):
def save(self, *args, **kwargs):
report_id = SingleAuditChecklist.objects.get(id=self.sac.id).report_id
self.filename = f"{report_id}.pdf"
if self.sac.submission_status != self.sac.STATUS.IN_PROGRESS:
if self.sac.submission_status != STATUS.IN_PROGRESS:
raise LateChangeError("Attempted PDF upload")

event_user = kwargs.pop("event_user", None)
Expand Down
Loading
Loading