Skip to content

Commit

Permalink
Django FSM migration to Viewflow
Browse files Browse the repository at this point in the history
- Added new `django-viewflow` and `django-filter` dependencies to `requirements.txt`.
- New file `viewflow.py` under `/audit/models/` which contains the FSM logic for transitioning an SAC.
- Moved `STATUS` enumeration outside of the `SingleAuditChecklist` class. This required import/reference changes across many files and tests.
- Removed references of old deprecated library `django-fsm`.
- New migration to handle the changing the SAC's `submission_name` field to remove dependency on the deprecated `django-fsm`.
  • Loading branch information
rnovak338 committed Sep 19, 2024
1 parent baabe97 commit 97e41c5
Show file tree
Hide file tree
Showing 19 changed files with 776 additions and 652 deletions.
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",
),
),
]
171 changes: 33 additions & 138 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,138 +509,26 @@ 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):
logger.info("Transitioning to DISSEMINATED")
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.SUBMITTED,
)
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.SUBMITTED)
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,
SingleAuditChecklist.STATUS.SUBMITTED,
STATUS.AUDITEE_CERTIFIED,
STATUS.CERTIFIED,
STATUS.SUBMITTED,
]

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

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

def get_transition_date(self, status):
index = self.transition_name.index(status)
Expand Down Expand Up @@ -663,7 +558,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 @@ -710,7 +605,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

0 comments on commit 97e41c5

Please sign in to comment.