Skip to content

Commit

Permalink
Merge pull request #2564 from GSA-TTS/main
Browse files Browse the repository at this point in the history
  • Loading branch information
jadudm authored Oct 20, 2023
2 parents a3666d0 + 87e9c62 commit d291f3b
Show file tree
Hide file tree
Showing 52 changed files with 670 additions and 173 deletions.
3 changes: 2 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [ ] Make sure you’ve merged `main` into your branch shortly before creating the PR. (You should also be merging `main` into your branch regularly during development.)
- [ ] Make sure you’ve accounted for any migrations. When you’re about to create the PR, bring up the application locally and then run `git status | grep migrations`. If there are any results, you probably need to add them to the branch for the PR. Your PR should have only **one** new migration file for each of the component apps, except in rare circumstances; you may need to delete some and re-run `python manage.py makemigrations` to reduce the number to one. (Also, unless in exceptional circumstances, your PR should not delete any migration files.)
- [ ] Make sure that whatever feature you’re adding has tests that cover the feature. This includes test coverage to make sure that the previous workflow still works, if applicable.
- [ ] Make sure the full-submission.cy.js [Cypress test](https://github.com/GSA-TTS/FAC/blob/main/docs/testing.md#end-to-end-testing) passes, if applicable.
- [ ] Do manual testing locally. Our tests are not good enough yet to allow us to skip this step. If that’s not applicable for some reason, check this box.
- [ ] Verify that no Git surgery was necessary, or, if it was necessary at any point, repeat the testing after it’s finished.
- [ ] Once a PR is merged, keep an eye on it until it’s deployed to dev, and do enough testing on dev to verify that it deployed successfully, the feature works as expected, and the happy path for the broad feature area (such as submission) still works.
Expand All @@ -17,5 +18,5 @@
- [ ] Manually test out the changes locally, or check this box to verify that it wasn’t applicable in this case.
- [ ] Check that the PR has appropriate tests. Look out for changes in HTML/JS/JSON Schema logic that may need to be captured in Python tests even though the logic isn’t in Python.
- [ ] Verify that no Git surgery is necessary at any point (such as during a merge party), or, if it was, repeat the testing after it’s finished.

The larger the PR, the stricter we should be about these points.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,7 @@ terraform/**/*.tfstate*
terraform/**/*.tfvars
terraform/shared/modules/egress-proxy/proxy.zip
terraform/shared/modules/egress-proxy/test/client.zip

# XLSX ignores
.~*#

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import logging
import re
from audit.intakelib.intermediate_representation import (
get_range_values_by_name,
get_range_by_name,
)
from .check_aln_three_digit_extension_pattern import (
REGEX_RD_EXTENSION,
REGEX_U_EXTENSION,
)
from .util import get_message, build_cell_error_tuple

logger = logging.getLogger(__name__)


# DESCRIPTION
# Additional award identification should be present if the ALN three digit extension is RD#, or U##
def additional_award_identification(ir):
extension = get_range_values_by_name(ir, "three_digit_extension")
additional = get_range_values_by_name(ir, "additional_award_identification")
errors = []
patterns = [REGEX_RD_EXTENSION, REGEX_U_EXTENSION]
for index, (ext, add) in enumerate(zip(extension, additional)):
if any(re.match(pattern, ext) for pattern in patterns) and not add:
errors.append(
build_cell_error_tuple(
ir,
get_range_by_name(ir, "additional_award_identification"),
index,
get_message("check_additional_award_identification_present"),
)
)

return errors
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import logging
import re
from audit.intakelib.intermediate_representation import (
get_range_values_by_name,
get_range_by_name,
)
from .util import get_message, build_cell_error_tuple

logger = logging.getLogger(__name__)

# A version of these regexes also exists in Base.libsonnet
REGEX_RD_EXTENSION = r"^RD[0-9]?$"
REGEX_THREE_DIGIT_EXTENSION = r"^[0-9]{3}[A-Za-z]{0,1}$"
REGEX_U_EXTENSION = r"^U[0-9]{2}$"


# DESCRIPTION
# The three digit extension should follow one of these formats: ###, RD#, or U##, where # represents a number
def aln_three_digit_extension(ir):
extension = get_range_values_by_name(ir, "three_digit_extension")
errors = []
# Define regex patterns
patterns = [REGEX_RD_EXTENSION, REGEX_THREE_DIGIT_EXTENSION, REGEX_U_EXTENSION]
for index, ext in enumerate(extension):
# Check if ext is None or does not match any of the regex patterns
if not ext:
errors.append(
build_cell_error_tuple(
ir,
get_range_by_name(ir, "three_digit_extension"),
index,
get_message("check_aln_three_digit_extension_missing"),
)
)
elif not any(re.match(pattern, ext) for pattern in patterns):
errors.append(
build_cell_error_tuple(
ir,
get_range_by_name(ir, "three_digit_extension"),
index,
get_message("check_aln_three_digit_extension_invalid"),
)
)

return errors
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import logging
from audit.intakelib.intermediate_representation import (
get_range_by_name,
)
from .util import get_message, build_cell_error_tuple

logger = logging.getLogger(__name__)


# DESCRIPTION
# If users provide a `|` in the passthrough id number field,
# we expect multiple names in the name field, too.
def cardinality_of_passthrough_names_and_ids(ir):
passthrough_name = get_range_by_name(ir, "passthrough_name")
passthrough_identifying_number = get_range_by_name(
ir, "passthrough_identifying_number"
)

errors = []

for index, (pass_name, pass_id) in enumerate(
zip(passthrough_name["values"], passthrough_identifying_number["values"])
):
if (pass_name and pass_id) and ("|" in pass_name or "|" in pass_id):
names = pass_name.split("|")
ids = pass_id.split("|")
print(names, ids)
if len(names) != len(ids):
errors.append(
build_cell_error_tuple(
ir,
get_range_by_name(ir, "passthrough_name"),
index,
(
get_message(
"check_cardinality_of_passthrough_names_and_ids"
).format(
len(names),
"s" if len(names) > 1 else "",
len(ids),
"s" if len(ids) > 1 else "",
)
),
)
)

return errors
75 changes: 75 additions & 0 deletions backend/audit/intakelib/checks/check_cluster_total.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import logging
from audit.intakelib.intermediate_representation import (
get_range_values_by_name,
get_range_by_name,
)
from .util import get_message, build_cell_error_tuple

logger = logging.getLogger(__name__)

STATE_CLUSTER = "STATE CLUSTER"
OTHER_CLUSTER = "OTHER CLUSTER NOT LISTED ABOVE"
NOT_APPLICABLE = "N/A"


# DESCRIPTION
# The sum of the amount_expended for a given cluster should equal the corresponding cluster_total
# K{0}=IF(G{0}="OTHER CLUSTER NOT LISTED ABOVE",SUMIFS(amount_expended,uniform_other_cluster_name,X{0}),IF(AND(OR(G{0}="N/A",G{0}=""),H{0}=""),0,IF(G{0}="STATE CLUSTER",SUMIFS(amount_expended,uniform_state_cluster_name,W{0}),SUMIFS(amount_expended,cluster_name,G{0}))))
def cluster_total_is_correct(ir):
uniform_other_cluster_name = get_range_values_by_name(
ir, "uniform_other_cluster_name"
)
uniform_state_cluster_name = get_range_values_by_name(
ir, "uniform_state_cluster_name"
)
state_cluster_name = get_range_values_by_name(ir, "state_cluster_name")
cluster_name = get_range_values_by_name(ir, "cluster_name")
cluster_total = get_range_values_by_name(ir, "cluster_total")
amount_expended = get_range_values_by_name(ir, "amount_expended")

errors = []

# Validating each cluster_total
for idx, name in enumerate(cluster_name):
# Based on the formula's conditions
if name == OTHER_CLUSTER:
expected_value = sum(
[
amount
for k, amount in zip(uniform_other_cluster_name, amount_expended)
if k == uniform_other_cluster_name[idx]
]
)
elif (name == NOT_APPLICABLE or not name) and not state_cluster_name[idx]:
expected_value = 0
elif name == STATE_CLUSTER:
expected_value = sum(
[
amount
for k, amount in zip(uniform_state_cluster_name, amount_expended)
if k == uniform_state_cluster_name[idx]
]
)
elif name:
expected_value = sum(
[
amount
for k, amount in zip(cluster_name, amount_expended)
if k == name
]
)

# Check if the calculated value matches the provided one
if expected_value != cluster_total[idx]:
errors.append(
build_cell_error_tuple(
ir,
get_range_by_name(ir, "cluster_total"),
idx,
get_message("check_cluster_total").format(
cluster_total[idx], expected_value
),
)
)

return errors
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import logging
from audit.intakelib.intermediate_representation import (
get_range_values_by_name,
get_range_by_name,
)
from .util import get_message, build_cell_error_tuple

logger = logging.getLogger(__name__)


# DESCRIPTION
# Amount passed through must be present if "Y" is selected for "Federal Award Passed Through"
def federal_award_amount_passed_through_optional(ir):
is_passed_values = get_range_values_by_name(ir, "is_passed")
subrecipient_amount_values = get_range_values_by_name(ir, "subrecipient_amount")
errors = []

for index, (is_passed, subrecipient_amount) in enumerate(
zip(is_passed_values, subrecipient_amount_values)
):
if is_passed == "Y" and subrecipient_amount is None:
errors.append(
build_cell_error_tuple(
ir,
get_range_by_name(ir, "subrecipient_amount"),
index,
get_message("check_federal_award_amount_passed_through_required"),
)
)
elif is_passed == "N" and subrecipient_amount is not None:
errors.append(
build_cell_error_tuple(
ir,
get_range_by_name(ir, "subrecipient_amount"),
index,
get_message(
"check_federal_award_amount_passed_through_not_allowed"
),
)
)

return errors
39 changes: 39 additions & 0 deletions backend/audit/intakelib/checks/check_federal_program_total.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import logging
from audit.intakelib.intermediate_representation import (
get_range_values_by_name,
get_range_by_name,
)
from .util import get_message, build_cell_error_tuple

logger = logging.getLogger(__name__)


# DESCRIPTION
# The sum of the amount_expended for a given cfda_key should equal the corresponding federal_program_total
# J{0}=SUMIFS(amount_expended,cfda_key,V{0}))
def federal_program_total_is_correct(ir):
federal_program_total = get_range_values_by_name(ir, "federal_program_total")
cfda_key = get_range_values_by_name(ir, "cfda_key")
amount_expended = get_range_values_by_name(ir, "amount_expended")

errors = []

# Validating each federal_program_total
for idx, key in enumerate(cfda_key):
# Compute the sum for current cfda_key
computed_sum = sum(
[amount for k, amount in zip(cfda_key, amount_expended) if k == key]
)
if computed_sum != federal_program_total[idx]:
errors.append(
build_cell_error_tuple(
ir,
get_range_by_name(ir, "federal_program_total"),
idx,
get_message("check_federal_program_total").format(
federal_program_total[idx], computed_sum
),
)
)

return errors
40 changes: 40 additions & 0 deletions backend/audit/intakelib/checks/check_loan_balance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import logging
from audit.intakelib.intermediate_representation import (
get_range_values_by_name,
get_range_by_name,
)
from .util import get_message, build_cell_error_tuple

logger = logging.getLogger(__name__)


# DESCRIPTION
# This makes sure that the loan guarantee is either a numerical value or N/A or an empty string.
def loan_balance(ir):
loan_balance_at_period_end = get_range_values_by_name(
ir, "loan_balance_at_audit_period_end"
)

errors = []
for index, balance in enumerate(loan_balance_at_period_end):
# Check if balance is not a number, empty string, or "N/A"
if not (balance in ["N/A", "", None] or _is_int(balance)):
errors.append(
build_cell_error_tuple(
ir,
get_range_by_name(ir, "loan_balance_at_audit_period_end"),
index,
get_message("check_loan_balance").format(balance),
)
)

return errors


# check if the given string can be converted to an int
def _is_int(s):
try:
value = int(s)
return value >= 0
except ValueError:
return False
34 changes: 34 additions & 0 deletions backend/audit/intakelib/checks/check_total_amount_expended.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import logging
from audit.intakelib.intermediate_representation import (
get_range_values_by_name,
get_range_by_name,
)
from .util import get_message, build_cell_error_tuple

logger = logging.getLogger(__name__)


# DESCRIPTION
# The sum of the amount_expended should equal the total_amount_expended
# B5=SUM(Form!F$2:F$5000)
def total_amount_expended_is_correct(ir):
total_amount_expended_value = get_range_values_by_name(ir, "total_amount_expended")
amount_expended_values = get_range_values_by_name(ir, "amount_expended")

errors = []

# Validating total_amount_expended
computed_sum = sum(amount_expended_values)
if computed_sum != total_amount_expended_value[0]:
errors.append(
build_cell_error_tuple(
ir,
get_range_by_name(ir, "total_amount_expended"),
0,
get_message("check_total_amount_expended").format(
total_amount_expended_value[0], computed_sum
),
)
)

return errors
Loading

0 comments on commit d291f3b

Please sign in to comment.