From bdcdb3456ab975ef04f85452795d65f70d67c7ad Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Thu, 21 Dec 2023 16:09:45 -0500 Subject: [PATCH 001/149] created pre-check error --- tdrs-backend/Dockerfile | 1 + tdrs-backend/tdpservice/parsers/parse.py | 13 +++++++ .../tdpservice/parsers/test/test_parse.py | 36 +++++++++++++++++++ tdrs-backend/tdpservice/parsers/validators.py | 22 ++++++++++++ 4 files changed, 72 insertions(+) diff --git a/tdrs-backend/Dockerfile b/tdrs-backend/Dockerfile index 4ce147031..b1e97f178 100644 --- a/tdrs-backend/Dockerfile +++ b/tdrs-backend/Dockerfile @@ -16,6 +16,7 @@ RUN apt-get -y update RUN apt-get -y upgrade # Install a new package: RUN apt-get install -y gcc && apt-get install -y graphviz && apt-get install -y graphviz-dev +RUN apt-get install postgresql-client -y # Install pipenv RUN pip install --upgrade pip pipenv RUN pipenv install --dev --system --deploy diff --git a/tdrs-backend/tdpservice/parsers/parse.py b/tdrs-backend/tdpservice/parsers/parse.py index 7b5177e74..7a380659e 100644 --- a/tdrs-backend/tdpservice/parsers/parse.py +++ b/tdrs-backend/tdpservice/parsers/parse.py @@ -34,6 +34,7 @@ def parse_datafile(datafile): # ensure file section matches upload section program_type = header['program_type'] section = header['type'] + print('***********************', header) logger.debug(f"Program type: {program_type}, Section: {section}.") section_is_valid, section_error = validators.validate_header_section_matches_submission( @@ -49,6 +50,18 @@ def parse_datafile(datafile): bulk_create_errors(unsaved_parser_errors, 1, flush=True) return errors + rpt_month_year_is_valid, rpt_month_year_error = validators.validate_header_rpt_month_year( + datafile, + header, + util.make_generate_parser_error(datafile, 1) + ) + if not rpt_month_year_is_valid: + logger.info(f"Preparser Error -> Rpt Month Year is not valid: {rpt_month_year_error.error_message}") + errors['document'] = [rpt_month_year_error] + unsaved_parser_errors = {1: [rpt_month_year_error]} + bulk_create_errors(unsaved_parser_errors, 1, flush=True) + return errors + line_errors = parse_datafile_lines(datafile, program_type, section, is_encrypted) errors = errors | line_errors diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index 6b6a7489d..e77b01725 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -2,6 +2,7 @@ import pytest +import datetime from .. import parse from ..models import ParserError, ParserErrorCategoryChoices, DataFileSummary from tdpservice.search_indexes.models.tanf import TANF_T1, TANF_T2, TANF_T3, TANF_T4, TANF_T5, TANF_T6, TANF_T7 @@ -34,6 +35,10 @@ def test_parse_small_correct_file(test_datafile, dfs): dfs.datafile = test_datafile dfs.save() + """ + small correct file has the following header: + HEADER20204A06 TAN1 N + """ parse.parse_datafile(test_datafile) dfs.status = dfs.get_status() dfs.case_aggregates = util.case_aggregates_by_month( @@ -991,3 +996,34 @@ def test_parse_ssp_section3_file(ssp_section3_file): assert first.NUM_RECIPIENTS == 51355 assert second.NUM_RECIPIENTS == 51696 assert third.NUM_RECIPIENTS == 51348 + +@pytest.mark.django_db +def test_rpt_month_year_mismatch(test_datafile): + """Test that the rpt_month_year mismatch error is raised.""" + datafile = test_datafile + + datafile.section = 'Active Case Data' + # test_datafile fixture uses create_test_data_file which assigns + # a default year / quarter of 2021 / Q1 + datafile.year = 2020 + datafile.quarter = 'Q4' + datafile.save() + + parse.parse_datafile(datafile) + + parser_errors = ParserError.objects.filter(file=datafile) + assert parser_errors.count() == 0 + + datafile.year = 2023 + datafile.save() + + parse.parse_datafile(datafile) + + parser_errors = ParserError.objects.filter(file=datafile) + assert parser_errors.count() == 1 + + err = parser_errors.first() + assert err.error_type == ParserErrorCategoryChoices.PRE_CHECK + assert err.error_message == 'RPT_MONTH_YEAR does not match the file name.' + assert err.content_type.model == 'tanf_t1' + assert err.object_id is not None \ No newline at end of file diff --git a/tdrs-backend/tdpservice/parsers/validators.py b/tdrs-backend/tdpservice/parsers/validators.py index ee86063fc..a53736eca 100644 --- a/tdrs-backend/tdpservice/parsers/validators.py +++ b/tdrs-backend/tdpservice/parsers/validators.py @@ -548,3 +548,25 @@ def validate_header_section_matches_submission(datafile, section, generate_error ) return is_valid, error + +def validate_header_rpt_month_year(datafile, header, generate_error): + """Validate header rpt_month_year.""" + is_valid = datafile.year is not None and datafile.quarter is not None + is_valid = is_valid and datafile.year == header['year'] and datafile.quarter == f'Q{header["quarter"]}' + + print('_____ datafile.year:',datafile.year) + print('_____ datafile.quarter:',datafile.quarter) + print('_____ header[year]:',header['year']) + print('_____ fQ{header["quarter"]:',f'Q{header["quarter"]}') + print(f'is_valid: {is_valid}') + error = None + if not is_valid: + error = generate_error( + schema=None, + error_category=ParserErrorCategoryChoices.PRE_CHECK, + error_message=f"Reporting file year:{header['year']}, quarter:{header['quarter']} doesn't match .", + record=None, + field=None, + ) + print(f'error: {error}') + return is_valid, error \ No newline at end of file From 6547d9a3a1ba7605331c4eee22e8b935eedabe2a Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Wed, 27 Dec 2023 11:50:14 -0500 Subject: [PATCH 002/149] corrected some of failing tests --- tdrs-backend/tdpservice/parsers/parse.py | 1 - .../small_incorrect_file_cross_validator.txt | 4 +-- .../test/data/tanf_section1_header_test.txt | 3 +++ .../tdpservice/parsers/test/test_parse.py | 27 ++++++++++++------- tdrs-backend/tdpservice/parsers/util.py | 4 ++- tdrs-backend/tdpservice/parsers/validators.py | 11 +++----- 6 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 tdrs-backend/tdpservice/parsers/test/data/tanf_section1_header_test.txt diff --git a/tdrs-backend/tdpservice/parsers/parse.py b/tdrs-backend/tdpservice/parsers/parse.py index 7a380659e..ad2de28a5 100644 --- a/tdrs-backend/tdpservice/parsers/parse.py +++ b/tdrs-backend/tdpservice/parsers/parse.py @@ -34,7 +34,6 @@ def parse_datafile(datafile): # ensure file section matches upload section program_type = header['program_type'] section = header['type'] - print('***********************', header) logger.debug(f"Program type: {program_type}, Section: {section}.") section_is_valid, section_error = validators.validate_header_section_matches_submission( diff --git a/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt b/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt index f01ab84d1..44e10437e 100644 --- a/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt +++ b/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt @@ -1,3 +1,3 @@ -HEADER20204A06 TAN1 N -T12020101111111111223003403361110213120000300000000000008730000000000000000000000000000000000000222222000000002229012 +HEADER20211A06 TAN1 N +T12021101111111111223003403361110213120000300000000000008730000000000000000000000000000000000000222222000000002229012 TRAILER0000001 \ No newline at end of file diff --git a/tdrs-backend/tdpservice/parsers/test/data/tanf_section1_header_test.txt b/tdrs-backend/tdpservice/parsers/test/data/tanf_section1_header_test.txt new file mode 100644 index 000000000..7452a511b --- /dev/null +++ b/tdrs-backend/tdpservice/parsers/test/data/tanf_section1_header_test.txt @@ -0,0 +1,3 @@ +HEADER20204A06 TAN1 N +T12020101111111111223003403361110213120000300000000000008730010000000000000000000000000000000000222222000000002229012 +TRAILER0000001 diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index e77b01725..9c019f356 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -2,7 +2,6 @@ import pytest -import datetime from .. import parse from ..models import ParserError, ParserErrorCategoryChoices, DataFileSummary from tdpservice.search_indexes.models.tanf import TANF_T1, TANF_T2, TANF_T3, TANF_T4, TANF_T5, TANF_T6, TANF_T7 @@ -23,6 +22,12 @@ def test_datafile(stt_user, stt): return util.create_test_datafile('small_correct_file.txt', stt_user, stt) +@pytest.fixture +def test_header_datafile(stt_user, stt): + """Fixture for header test.""" + return util.create_test_datafile('tanf_section1_header_test.txt', stt_user, stt) + + @pytest.fixture def dfs(): """Fixture for DataFileSummary.""" @@ -431,8 +436,8 @@ def test_parse_small_ssp_section1_datafile(small_ssp_section1_datafile, dfs): expected_m2_record_count = 6 expected_m3_record_count = 8 - small_ssp_section1_datafile.year = 2024 - small_ssp_section1_datafile.quarter = 'Q1' + small_ssp_section1_datafile.year = 2023 + small_ssp_section1_datafile.quarter = 'Q4' small_ssp_section1_datafile.save() dfs.datafile = small_ssp_section1_datafile @@ -440,10 +445,15 @@ def test_parse_small_ssp_section1_datafile(small_ssp_section1_datafile, dfs): parse.parse_datafile(small_ssp_section1_datafile) + parser_errors = ParserError.objects.filter(file=small_ssp_section1_datafile) + for i in parser_errors: + print('___________ parser_errors:', i.__dict__) + dfs.status = dfs.get_status() assert dfs.status == DataFileSummary.Status.ACCEPTED_WITH_ERRORS dfs.case_aggregates = util.case_aggregates_by_month( dfs.datafile, dfs.status) + print('___________ dfs.case_aggregates:', dfs.case_aggregates) assert dfs.case_aggregates == {'rejected': 1, 'months': [ {'accepted_without_errors': 0, 'accepted_with_errors': 5, 'month': 'Oct'}, @@ -998,12 +1008,12 @@ def test_parse_ssp_section3_file(ssp_section3_file): assert third.NUM_RECIPIENTS == 51348 @pytest.mark.django_db -def test_rpt_month_year_mismatch(test_datafile): +def test_rpt_month_year_mismatch(test_header_datafile): """Test that the rpt_month_year mismatch error is raised.""" - datafile = test_datafile + datafile = test_header_datafile datafile.section = 'Active Case Data' - # test_datafile fixture uses create_test_data_file which assigns + # test_datafile fixture uses create_test_data_file which assigns # a default year / quarter of 2021 / Q1 datafile.year = 2020 datafile.quarter = 'Q4' @@ -1024,6 +1034,5 @@ def test_rpt_month_year_mismatch(test_datafile): err = parser_errors.first() assert err.error_type == ParserErrorCategoryChoices.PRE_CHECK - assert err.error_message == 'RPT_MONTH_YEAR does not match the file name.' - assert err.content_type.model == 'tanf_t1' - assert err.object_id is not None \ No newline at end of file + assert err.error_message == "Submitted reporting year:2020, quarter:Q4 doesn't match " + \ + "file reporting year:2023, quarter:Q4." diff --git a/tdrs-backend/tdpservice/parsers/util.py b/tdrs-backend/tdpservice/parsers/util.py index 2a497e48e..665e5f3e0 100644 --- a/tdrs-backend/tdpservice/parsers/util.py +++ b/tdrs-backend/tdpservice/parsers/util.py @@ -308,7 +308,9 @@ def case_aggregates_by_month(df, dfs_status): accepted = 0 month_int = month_to_int(month) rpt_month_year = int(f"{calendar_year}{month_int}") - + print('++++++++++++++ month:', month) + print('++++++++++++++ rpt_month_year:', rpt_month_year) + print('++++++++++++++ dfs_status:', dfs_status) if dfs_status == "Rejected": # we need to be careful here on examples of bad headers or empty files, since no month will be found # but we can rely on the frontend submitted year-quarter to still generate the list of months diff --git a/tdrs-backend/tdpservice/parsers/validators.py b/tdrs-backend/tdpservice/parsers/validators.py index a53736eca..f84c0bcad 100644 --- a/tdrs-backend/tdpservice/parsers/validators.py +++ b/tdrs-backend/tdpservice/parsers/validators.py @@ -554,19 +554,14 @@ def validate_header_rpt_month_year(datafile, header, generate_error): is_valid = datafile.year is not None and datafile.quarter is not None is_valid = is_valid and datafile.year == header['year'] and datafile.quarter == f'Q{header["quarter"]}' - print('_____ datafile.year:',datafile.year) - print('_____ datafile.quarter:',datafile.quarter) - print('_____ header[year]:',header['year']) - print('_____ fQ{header["quarter"]:',f'Q{header["quarter"]}') - print(f'is_valid: {is_valid}') error = None if not is_valid: error = generate_error( schema=None, error_category=ParserErrorCategoryChoices.PRE_CHECK, - error_message=f"Reporting file year:{header['year']}, quarter:{header['quarter']} doesn't match .", + error_message=f"Submitted reporting year:{header['year']}, quarter:Q{header['quarter']} doesn't match " + + f"file reporting year:{datafile.year}, quarter:{datafile.quarter}.", record=None, field=None, ) - print(f'error: {error}') - return is_valid, error \ No newline at end of file + return is_valid, error From ad014b036abd9073f411152c92e9513da3e512e6 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Wed, 27 Dec 2023 13:29:00 -0500 Subject: [PATCH 003/149] corrected failing tests --- .../tdpservice/parsers/test/test_parse.py | 25 ++++++++++++------- .../tdpservice/parsers/test/test_util.py | 2 +- .../tdpservice/parsers/test/test_views.py | 2 +- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index 9c019f356..b458c46cd 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -37,6 +37,9 @@ def dfs(): @pytest.mark.django_db def test_parse_small_correct_file(test_datafile, dfs): """Test parsing of small_correct_file.""" + test_datafile.year = 2020 + test_datafile.quarter = 'Q4' + test_datafile.save() dfs.datafile = test_datafile dfs.save() @@ -45,18 +48,18 @@ def test_parse_small_correct_file(test_datafile, dfs): HEADER20204A06 TAN1 N """ parse.parse_datafile(test_datafile) + + pes = ParserError.objects.filter(file=test_datafile) dfs.status = dfs.get_status() dfs.case_aggregates = util.case_aggregates_by_month( dfs.datafile, dfs.status) - assert dfs.case_aggregates == {'rejected': 0, - 'months': [ - {'accepted_without_errors': 1, - 'accepted_with_errors': 0, 'month': 'Oct'}, - {'accepted_without_errors': 0, - 'accepted_with_errors': 0, 'month': 'Nov'}, - {'accepted_without_errors': 0, - 'accepted_with_errors': 0, 'month': 'Dec'} - ]} + for month in dfs.case_aggregates['months']: + if month['month'] == 'Jul': + assert month['accepted_without_errors'] == 0 + assert month['accepted_with_errors'] == 0 + else: + assert month['accepted_without_errors'] == 0 + assert month['accepted_with_errors'] == 0 assert dfs.get_status() == DataFileSummary.Status.ACCEPTED assert TANF_T1.objects.count() == 1 @@ -254,6 +257,9 @@ def bad_file_multiple_headers(stt_user, stt): @pytest.mark.django_db def test_parse_bad_file_multiple_headers(bad_file_multiple_headers, dfs): """Test parsing of bad_two_headers.""" + bad_file_multiple_headers.year = 2023 + bad_file_multiple_headers.quarter = 'Q4' + bad_file_multiple_headers.save() errors = parse.parse_datafile(bad_file_multiple_headers) dfs.datafile = bad_file_multiple_headers dfs.save() @@ -281,6 +287,7 @@ def big_bad_test_file(stt_user, stt): @pytest.mark.django_db def test_parse_big_bad_test_file(big_bad_test_file, dfs): """Test parsing of bad_TANF_S1.""" + big_bad_test_file.quarter = 'Q4' parse.parse_datafile(big_bad_test_file) parser_errors = ParserError.objects.filter(file=big_bad_test_file) diff --git a/tdrs-backend/tdpservice/parsers/test/test_util.py b/tdrs-backend/tdpservice/parsers/test/test_util.py index 5bb8c72e2..ef5cbca16 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_util.py +++ b/tdrs-backend/tdpservice/parsers/test/test_util.py @@ -456,7 +456,7 @@ def test_multi_record_schema_parses_and_validates(): @pytest.fixture def test_datafile_empty_file(stt_user, stt): - """Fixture for small_correct_file.""" + """Fixture for empty_file.""" return create_test_datafile('empty_file', stt_user, stt) @pytest.mark.django_db() diff --git a/tdrs-backend/tdpservice/parsers/test/test_views.py b/tdrs-backend/tdpservice/parsers/test/test_views.py index 7f5c7ec35..d9dae0e18 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_views.py +++ b/tdrs-backend/tdpservice/parsers/test/test_views.py @@ -9,7 +9,7 @@ @pytest.fixture def test_datafile(stt_user, stt): - """Fixture for small_correct_file.""" + """Fixture for small_incorrect_file_cross_validator.""" return util.create_test_datafile('small_incorrect_file_cross_validator.txt', stt_user, stt) From b35ac52b86a1d27da930c56b8cdb81f6fe6f05e2 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Thu, 28 Dec 2023 13:46:14 -0500 Subject: [PATCH 004/149] two tests still failing --- .../tdpservice/parsers/test/test_parse.py | 64 ++++++++++++++++--- tdrs-backend/tdpservice/parsers/util.py | 3 - 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index b458c46cd..6a34e8b48 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -49,7 +49,6 @@ def test_parse_small_correct_file(test_datafile, dfs): """ parse.parse_datafile(test_datafile) - pes = ParserError.objects.filter(file=test_datafile) dfs.status = dfs.get_status() dfs.case_aggregates = util.case_aggregates_by_month( dfs.datafile, dfs.status) @@ -311,6 +310,8 @@ def bad_trailer_file(stt_user, stt): @pytest.mark.django_db def test_parse_bad_trailer_file(bad_trailer_file, dfs): """Test parsing bad_trailer_1.""" + bad_trailer_file.year = 2020 + bad_trailer_file.quarter = 'Q4' dfs.datafile = bad_trailer_file dfs.save() @@ -346,6 +347,8 @@ def bad_trailer_file_2(stt_user, stt): @pytest.mark.django_db() def test_parse_bad_trailer_file2(bad_trailer_file_2): """Test parsing bad_trailer_2.""" + bad_trailer_file_2.year = 2020 + bad_trailer_file_2.quarter = 'Q4' errors = parse.parse_datafile(bad_trailer_file_2) parser_errors = ParserError.objects.filter(file=bad_trailer_file_2) @@ -439,28 +442,29 @@ def small_ssp_section1_datafile(stt_user, stt): @pytest.mark.django_db def test_parse_small_ssp_section1_datafile(small_ssp_section1_datafile, dfs): """Test parsing small_ssp_section1_datafile.""" + small_ssp_section1_datafile.year = 2023 + small_ssp_section1_datafile.quarter = 'Q4' + expected_m1_record_count = 5 expected_m2_record_count = 6 expected_m3_record_count = 8 - small_ssp_section1_datafile.year = 2023 - small_ssp_section1_datafile.quarter = 'Q4' - small_ssp_section1_datafile.save() - dfs.datafile = small_ssp_section1_datafile dfs.save() - parse.parse_datafile(small_ssp_section1_datafile) parser_errors = ParserError.objects.filter(file=small_ssp_section1_datafile) - for i in parser_errors: - print('___________ parser_errors:', i.__dict__) - dfs.status = dfs.get_status() assert dfs.status == DataFileSummary.Status.ACCEPTED_WITH_ERRORS dfs.case_aggregates = util.case_aggregates_by_month( dfs.datafile, dfs.status) - print('___________ dfs.case_aggregates:', dfs.case_aggregates) + for month in dfs.case_aggregates['months']: + if month['month'] == 'Oct': + assert month['accepted_without_errors'] == 0 + assert month['accepted_with_errors'] == 0 + else: + assert month['accepted_without_errors'] == 0 + assert month['accepted_with_errors'] == 0 assert dfs.case_aggregates == {'rejected': 1, 'months': [ {'accepted_without_errors': 0, 'accepted_with_errors': 5, 'month': 'Oct'}, @@ -484,6 +488,9 @@ def ssp_section1_datafile(stt_user, stt): @pytest.mark.django_db() def test_parse_ssp_section1_datafile(ssp_section1_datafile): """Test parsing ssp_section1_datafile.""" + ssp_section1_datafile.year = 2018 + ssp_section1_datafile.quarter = 'Q4' + expected_m1_record_count = 7849 expected_m2_record_count = 9373 expected_m3_record_count = 16764 @@ -515,6 +522,8 @@ def small_tanf_section1_datafile(stt_user, stt): @pytest.mark.django_db def test_parse_tanf_section1_datafile(small_tanf_section1_datafile, dfs): """Test parsing of small_tanf_section1_datafile and validate T2 model data.""" + small_tanf_section1_datafile.year = 2020 + small_tanf_section1_datafile.quarter = 'Q4' dfs.datafile = small_tanf_section1_datafile dfs.save() @@ -554,6 +563,9 @@ def test_parse_tanf_section1_datafile(small_tanf_section1_datafile, dfs): @pytest.mark.django_db() def test_parse_tanf_section1_datafile_obj_counts(small_tanf_section1_datafile): """Test parsing of small_tanf_section1_datafile in general.""" + small_tanf_section1_datafile.year = 2020 + small_tanf_section1_datafile.quarter = 'Q4' + parse.parse_datafile(small_tanf_section1_datafile) assert TANF_T1.objects.count() == 5 @@ -564,6 +576,8 @@ def test_parse_tanf_section1_datafile_obj_counts(small_tanf_section1_datafile): @pytest.mark.django_db() def test_parse_tanf_section1_datafile_t3s(small_tanf_section1_datafile): """Test parsing of small_tanf_section1_datafile and validate T3 model data.""" + small_tanf_section1_datafile.year = 2020 + small_tanf_section1_datafile.quarter = 'Q4' parse.parse_datafile(small_tanf_section1_datafile) assert TANF_T3.objects.count() == 6 @@ -641,6 +655,9 @@ def bad_tanf_s1__row_missing_required_field(stt_user, stt): @pytest.mark.django_db def test_parse_bad_tfs1_missing_required(bad_tanf_s1__row_missing_required_field, dfs): """Test parsing a bad TANF Section 1 submission where a row is missing required data.""" + bad_tanf_s1__row_missing_required_field.year = 2020 + bad_tanf_s1__row_missing_required_field.quarter = 'Q4' + dfs.datafile = bad_tanf_s1__row_missing_required_field dfs.save() @@ -688,6 +705,9 @@ def bad_ssp_s1__row_missing_required_field(stt_user, stt): @pytest.mark.django_db() def test_parse_bad_ssp_s1_missing_required(bad_ssp_s1__row_missing_required_field): """Test parsing a bad TANF Section 1 submission where a row is missing required data.""" + bad_ssp_s1__row_missing_required_field.year = 2018 + bad_ssp_s1__row_missing_required_field.quarter = 'Q4' + parse.parse_datafile(bad_ssp_s1__row_missing_required_field) parser_errors = ParserError.objects.filter(file=bad_ssp_s1__row_missing_required_field) @@ -736,6 +756,8 @@ def test_parse_bad_ssp_s1_missing_required(bad_ssp_s1__row_missing_required_fiel @pytest.mark.django_db def test_dfs_set_case_aggregates(test_datafile, dfs): """Test that the case aggregates are set correctly.""" + test_datafile.year = 2020 + test_datafile.quarter = 'Q4' test_datafile.section = 'Active Case Data' test_datafile.save() # this still needs to execute to create db objects to be queried @@ -807,6 +829,9 @@ def small_tanf_section2_file(stt_user, stt): @pytest.mark.django_db() def test_parse_small_tanf_section2_file(small_tanf_section2_file): """Test parsing a small TANF Section 2 submission.""" + small_tanf_section2_file.year = 2020 + small_tanf_section2_file.quarter = 'Q4' + parse.parse_datafile(small_tanf_section2_file) assert TANF_T4.objects.all().count() == 1 @@ -835,6 +860,8 @@ def tanf_section2_file(stt_user, stt): @pytest.mark.django_db() def test_parse_tanf_section2_file(tanf_section2_file): """Test parsing TANF Section 2 submission.""" + tanf_section2_file.year = 2020 + tanf_section2_file.quarter = 'Q4' parse.parse_datafile(tanf_section2_file) assert TANF_T4.objects.all().count() == 223 @@ -858,6 +885,9 @@ def tanf_section3_file(stt_user, stt): @pytest.mark.django_db() def test_parse_tanf_section3_file(tanf_section3_file): """Test parsing TANF Section 3 submission.""" + tanf_section3_file.year = 2020 + tanf_section3_file.quarter = 'Q4' + parse.parse_datafile(tanf_section3_file) assert TANF_T6.objects.all().count() == 3 @@ -891,6 +921,9 @@ def tanf_section1_file_with_blanks(stt_user, stt): @pytest.mark.django_db() def test_parse_tanf_section1_blanks_file(tanf_section1_file_with_blanks): """Test section 1 fields that are allowed to have blanks.""" + tanf_section1_file_with_blanks.year = 2020 + tanf_section1_file_with_blanks.quarter = 'Q4' + parse.parse_datafile(tanf_section1_file_with_blanks) parser_errors = ParserError.objects.filter(file=tanf_section1_file_with_blanks) @@ -918,6 +951,9 @@ def tanf_section4_file(stt_user, stt): @pytest.mark.django_db() def test_parse_tanf_section4_file(tanf_section4_file): """Test parsing TANF Section 4 submission.""" + tanf_section4_file.year = 2020 + tanf_section4_file.quarter = 'Q4' + parse.parse_datafile(tanf_section4_file) assert TANF_T7.objects.all().count() == 18 @@ -947,6 +983,9 @@ def ssp_section4_file(stt_user, stt): @pytest.mark.django_db() def test_parse_ssp_section4_file(ssp_section4_file): """Test parsing SSP Section 4 submission.""" + ssp_section4_file.year = 2018 + ssp_section4_file.quarter = 'Q4' + parse.parse_datafile(ssp_section4_file) m7_objs = SSP_M7.objects.all().order_by('FAMILIES_MONTH') @@ -965,6 +1004,9 @@ def ssp_section2_file(stt_user, stt): @pytest.mark.django_db() def test_parse_ssp_section2_file(ssp_section2_file): """Test parsing SSP Section 2 submission.""" + ssp_section2_file.year = 2018 + ssp_section2_file.quarter = 'Q4' + parse.parse_datafile(ssp_section2_file) m4_objs = SSP_M4.objects.all().order_by('id') @@ -990,6 +1032,8 @@ def ssp_section3_file(stt_user, stt): @pytest.mark.django_db() def test_parse_ssp_section3_file(ssp_section3_file): """Test parsing TANF Section 3 submission.""" + ssp_section3_file.year = 2018 + ssp_section3_file.quarter = 'Q4' parse.parse_datafile(ssp_section3_file) m6_objs = SSP_M6.objects.all().order_by('RPT_MONTH_YEAR') diff --git a/tdrs-backend/tdpservice/parsers/util.py b/tdrs-backend/tdpservice/parsers/util.py index 665e5f3e0..384dfb5c9 100644 --- a/tdrs-backend/tdpservice/parsers/util.py +++ b/tdrs-backend/tdpservice/parsers/util.py @@ -308,9 +308,6 @@ def case_aggregates_by_month(df, dfs_status): accepted = 0 month_int = month_to_int(month) rpt_month_year = int(f"{calendar_year}{month_int}") - print('++++++++++++++ month:', month) - print('++++++++++++++ rpt_month_year:', rpt_month_year) - print('++++++++++++++ dfs_status:', dfs_status) if dfs_status == "Rejected": # we need to be careful here on examples of bad headers or empty files, since no month will be found # but we can rely on the frontend submitted year-quarter to still generate the list of months From 98dc8f6b612068931e449795599cd43cce3ecec3 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Thu, 4 Jan 2024 17:31:42 -0500 Subject: [PATCH 005/149] Passing tests --- tdrs-backend/tdpservice/parsers/fields.py | 11 ++- tdrs-backend/tdpservice/parsers/row_schema.py | 37 ++++++- .../tdpservice/parsers/schema_defs/ssp/m1.py | 3 +- .../tdpservice/parsers/schema_defs/ssp/m2.py | 3 +- .../tdpservice/parsers/schema_defs/ssp/m3.py | 3 +- .../tdpservice/parsers/schema_defs/ssp/m4.py | 3 +- .../tdpservice/parsers/schema_defs/ssp/m5.py | 3 +- .../tdpservice/parsers/schema_defs/ssp/m6.py | 3 +- .../tdpservice/parsers/schema_defs/ssp/m7.py | 3 +- .../tdpservice/parsers/schema_defs/tanf/t1.py | 3 +- .../tdpservice/parsers/schema_defs/tanf/t2.py | 3 +- .../tdpservice/parsers/schema_defs/tanf/t3.py | 3 +- .../tdpservice/parsers/schema_defs/tanf/t4.py | 3 +- .../tdpservice/parsers/schema_defs/tanf/t5.py | 3 +- .../tdpservice/parsers/schema_defs/tanf/t6.py | 3 +- .../tdpservice/parsers/schema_defs/tanf/t7.py | 3 +- .../tdpservice/parsers/test/test_parse.py | 99 +++++++++---------- .../tdpservice/parsers/test/test_util.py | 4 +- .../tdpservice/parsers/test/test_views.py | 4 + tdrs-backend/tdpservice/parsers/util.py | 25 +---- tdrs-backend/tdpservice/parsers/validators.py | 10 +- 21 files changed, 122 insertions(+), 110 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/fields.py b/tdrs-backend/tdpservice/parsers/fields.py index acd94b14b..62de634e7 100644 --- a/tdrs-backend/tdpservice/parsers/fields.py +++ b/tdrs-backend/tdpservice/parsers/fields.py @@ -1,10 +1,19 @@ """Datafile field representations.""" import logging -from .validators import value_is_empty logger = logging.getLogger(__name__) +def value_is_empty(value, length): + """Handle 'empty' values as field inputs.""" + empty_values = [ + '', + ' '*length, # ' ' + '#'*length, # '#####' + '_'*length, # '_____' + ] + + return value is None or value in empty_values class Field: """Provides a mapping between a field name and its position.""" diff --git a/tdrs-backend/tdpservice/parsers/row_schema.py b/tdrs-backend/tdpservice/parsers/row_schema.py index 97a9ccc65..ca7b9164c 100644 --- a/tdrs-backend/tdpservice/parsers/row_schema.py +++ b/tdrs-backend/tdpservice/parsers/row_schema.py @@ -1,11 +1,20 @@ """Row schema for datafile.""" from .models import ParserErrorCategoryChoices -from .fields import Field -from .validators import value_is_empty +from .fields import Field, TransformField import logging logger = logging.getLogger(__name__) +def value_is_empty(value, length): + """Handle 'empty' values as field inputs.""" + empty_values = [ + '', + ' '*length, # ' ' + '#'*length, # '#####' + '_'*length, # '_____' + ] + + return value is None or value in empty_values class RowSchema: """Maps the schema for data lines.""" @@ -174,3 +183,27 @@ def get_field_by_name(self, name): if field.name == name: return field return None + + +class SchemaManager: + """Manages one or more RowSchema's and runs all parsers and validators.""" + + def __init__(self, schemas): + self.schemas = schemas + + def parse_and_validate(self, line, generate_error): + """Run `parse_and_validate` for each schema provided and bubble up errors.""" + records = [] + + for schema in self.schemas: + record, is_valid, errors = schema.parse_and_validate(line, generate_error) + records.append((record, is_valid, errors)) + + return records + + def update_encrypted_fields(self, is_encrypted): + """Update whether schema fields are encrypted or not.""" + for schema in self.schemas: + for field in schema.fields: + if type(field) == TransformField and "is_encrypted" in field.kwargs: + field.kwargs['is_encrypted'] = is_encrypted diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m1.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m1.py index ea0aaf189..0285d00ab 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m1.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m1.py @@ -1,9 +1,8 @@ """Schema for SSP M1 record type.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.fields import Field -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers import validators from tdpservice.search_indexes.models.ssp import SSP_M1 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py index 4dd0a00ec..8ce7dc6c9 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py @@ -1,10 +1,9 @@ """Schema for SSP M1 record type.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.transforms import ssp_ssn_decryption_func from tdpservice.parsers.fields import TransformField, Field -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers import validators from tdpservice.search_indexes.models.ssp import SSP_M2 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py index fb05903f6..551a6c342 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py @@ -1,10 +1,9 @@ """Schema for SSP M1 record type.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.transforms import ssp_ssn_decryption_func from tdpservice.parsers.fields import TransformField, Field -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers import validators from tdpservice.search_indexes.models.ssp import SSP_M3 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m4.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m4.py index 4d8f04f64..3457a9e1e 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m4.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m4.py @@ -1,9 +1,8 @@ """Schema for SSP M1 record type.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.fields import Field -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers import validators from tdpservice.search_indexes.models.ssp import SSP_M4 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py index a63cd6591..0ca9f94fc 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py @@ -1,10 +1,9 @@ """Schema for SSP M1 record type.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.transforms import ssp_ssn_decryption_func from tdpservice.parsers.fields import TransformField, Field -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers import validators from tdpservice.search_indexes.models.ssp import SSP_M5 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m6.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m6.py index 6f266168b..b71ea6761 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m6.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m6.py @@ -1,10 +1,9 @@ """Schema for HEADER row of all submission types.""" -from ...util import SchemaManager from ...transforms import calendar_quarter_to_rpt_month_year from ...fields import Field, TransformField -from ...row_schema import RowSchema +from ...row_schema import RowSchema, SchemaManager from ... import validators from tdpservice.search_indexes.models.ssp import SSP_M6 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m7.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m7.py index cde0c1fc3..553753ceb 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m7.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m7.py @@ -1,8 +1,7 @@ """Schema for TANF T7 Row.""" -from ...util import SchemaManager from ...fields import Field, TransformField -from ...row_schema import RowSchema +from ...row_schema import RowSchema, SchemaManager from ...transforms import calendar_quarter_to_rpt_month_year from ... import validators from tdpservice.search_indexes.models.ssp import SSP_M7 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t1.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t1.py index 76ce8f0f5..580f88caa 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t1.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t1.py @@ -1,8 +1,7 @@ """Schema for t1 record types.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.fields import Field -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers import validators from tdpservice.search_indexes.models.tanf import TANF_T1 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py index 63f8706ac..a976da6b2 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py @@ -1,10 +1,9 @@ """Schema for HEADER row of all submission types.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.transforms import tanf_ssn_decryption_func from tdpservice.parsers.fields import TransformField, Field -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers import validators from tdpservice.search_indexes.models.tanf import TANF_T2 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py index 54995f23a..a97c60495 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py @@ -1,10 +1,9 @@ """Schema for HEADER row of all submission types.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.transforms import tanf_ssn_decryption_func from tdpservice.parsers.fields import TransformField, Field -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers import validators from tdpservice.search_indexes.models.tanf import TANF_T3 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t4.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t4.py index 1a5a13c54..32fcfb980 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t4.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t4.py @@ -1,9 +1,8 @@ """Schema for HEADER row of all submission types.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.fields import Field -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers import validators from tdpservice.search_indexes.models.tanf import TANF_T4 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t5.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t5.py index 199c08f0c..ae390a704 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t5.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t5.py @@ -1,10 +1,9 @@ """Schema for HEADER row of all submission types.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.transforms import tanf_ssn_decryption_func from tdpservice.parsers.fields import TransformField, Field -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers import validators from tdpservice.search_indexes.models.tanf import TANF_T5 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t6.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t6.py index 77925781a..7f66127ca 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t6.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t6.py @@ -1,10 +1,9 @@ """Schema for HEADER row of all submission types.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.transforms import calendar_quarter_to_rpt_month_year from tdpservice.parsers.fields import Field, TransformField -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers import validators from tdpservice.search_indexes.models.tanf import TANF_T6 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t7.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t7.py index e6a05bb9a..479ade999 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t7.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t7.py @@ -1,8 +1,7 @@ """Schema for TANF T7 Row.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.fields import Field, TransformField -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers.transforms import calendar_quarter_to_rpt_month_year from tdpservice.parsers import validators from tdpservice.search_indexes.models.tanf import TANF_T7 diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index 6a34e8b48..b8e4c3ec0 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -37,24 +37,20 @@ def dfs(): @pytest.mark.django_db def test_parse_small_correct_file(test_datafile, dfs): """Test parsing of small_correct_file.""" - test_datafile.year = 2020 - test_datafile.quarter = 'Q4' + test_datafile.year = 2021 + test_datafile.quarter = 'Q1' test_datafile.save() dfs.datafile = test_datafile dfs.save() - """ - small correct file has the following header: - HEADER20204A06 TAN1 N - """ parse.parse_datafile(test_datafile) dfs.status = dfs.get_status() dfs.case_aggregates = util.case_aggregates_by_month( dfs.datafile, dfs.status) for month in dfs.case_aggregates['months']: - if month['month'] == 'Jul': - assert month['accepted_without_errors'] == 0 + if month['month'] == 'Oct': + assert month['accepted_without_errors'] == 1 assert month['accepted_with_errors'] == 0 else: assert month['accepted_without_errors'] == 0 @@ -256,8 +252,8 @@ def bad_file_multiple_headers(stt_user, stt): @pytest.mark.django_db def test_parse_bad_file_multiple_headers(bad_file_multiple_headers, dfs): """Test parsing of bad_two_headers.""" - bad_file_multiple_headers.year = 2023 - bad_file_multiple_headers.quarter = 'Q4' + bad_file_multiple_headers.year = 2024 + bad_file_multiple_headers.quarter = 'Q1' bad_file_multiple_headers.save() errors = parse.parse_datafile(bad_file_multiple_headers) dfs.datafile = bad_file_multiple_headers @@ -286,7 +282,8 @@ def big_bad_test_file(stt_user, stt): @pytest.mark.django_db def test_parse_big_bad_test_file(big_bad_test_file, dfs): """Test parsing of bad_TANF_S1.""" - big_bad_test_file.quarter = 'Q4' + big_bad_test_file.year = 2022 + big_bad_test_file.quarter = 'Q1' parse.parse_datafile(big_bad_test_file) parser_errors = ParserError.objects.filter(file=big_bad_test_file) @@ -310,8 +307,8 @@ def bad_trailer_file(stt_user, stt): @pytest.mark.django_db def test_parse_bad_trailer_file(bad_trailer_file, dfs): """Test parsing bad_trailer_1.""" - bad_trailer_file.year = 2020 - bad_trailer_file.quarter = 'Q4' + bad_trailer_file.year = 2021 + bad_trailer_file.quarter = 'Q1' dfs.datafile = bad_trailer_file dfs.save() @@ -347,8 +344,8 @@ def bad_trailer_file_2(stt_user, stt): @pytest.mark.django_db() def test_parse_bad_trailer_file2(bad_trailer_file_2): """Test parsing bad_trailer_2.""" - bad_trailer_file_2.year = 2020 - bad_trailer_file_2.quarter = 'Q4' + bad_trailer_file_2.year = 2021 + bad_trailer_file_2.quarter = 'Q1' errors = parse.parse_datafile(bad_trailer_file_2) parser_errors = ParserError.objects.filter(file=bad_trailer_file_2) @@ -442,8 +439,8 @@ def small_ssp_section1_datafile(stt_user, stt): @pytest.mark.django_db def test_parse_small_ssp_section1_datafile(small_ssp_section1_datafile, dfs): """Test parsing small_ssp_section1_datafile.""" - small_ssp_section1_datafile.year = 2023 - small_ssp_section1_datafile.quarter = 'Q4' + small_ssp_section1_datafile.year = 2024 + small_ssp_section1_datafile.quarter = 'Q1' expected_m1_record_count = 5 expected_m2_record_count = 6 @@ -461,7 +458,7 @@ def test_parse_small_ssp_section1_datafile(small_ssp_section1_datafile, dfs): for month in dfs.case_aggregates['months']: if month['month'] == 'Oct': assert month['accepted_without_errors'] == 0 - assert month['accepted_with_errors'] == 0 + assert month['accepted_with_errors'] == 5 else: assert month['accepted_without_errors'] == 0 assert month['accepted_with_errors'] == 0 @@ -488,8 +485,8 @@ def ssp_section1_datafile(stt_user, stt): @pytest.mark.django_db() def test_parse_ssp_section1_datafile(ssp_section1_datafile): """Test parsing ssp_section1_datafile.""" - ssp_section1_datafile.year = 2018 - ssp_section1_datafile.quarter = 'Q4' + ssp_section1_datafile.year = 2019 + ssp_section1_datafile.quarter = 'Q1' expected_m1_record_count = 7849 expected_m2_record_count = 9373 @@ -522,8 +519,8 @@ def small_tanf_section1_datafile(stt_user, stt): @pytest.mark.django_db def test_parse_tanf_section1_datafile(small_tanf_section1_datafile, dfs): """Test parsing of small_tanf_section1_datafile and validate T2 model data.""" - small_tanf_section1_datafile.year = 2020 - small_tanf_section1_datafile.quarter = 'Q4' + small_tanf_section1_datafile.year = 2021 + small_tanf_section1_datafile.quarter = 'Q1' dfs.datafile = small_tanf_section1_datafile dfs.save() @@ -563,8 +560,8 @@ def test_parse_tanf_section1_datafile(small_tanf_section1_datafile, dfs): @pytest.mark.django_db() def test_parse_tanf_section1_datafile_obj_counts(small_tanf_section1_datafile): """Test parsing of small_tanf_section1_datafile in general.""" - small_tanf_section1_datafile.year = 2020 - small_tanf_section1_datafile.quarter = 'Q4' + small_tanf_section1_datafile.year = 2021 + small_tanf_section1_datafile.quarter = 'Q1' parse.parse_datafile(small_tanf_section1_datafile) @@ -576,8 +573,8 @@ def test_parse_tanf_section1_datafile_obj_counts(small_tanf_section1_datafile): @pytest.mark.django_db() def test_parse_tanf_section1_datafile_t3s(small_tanf_section1_datafile): """Test parsing of small_tanf_section1_datafile and validate T3 model data.""" - small_tanf_section1_datafile.year = 2020 - small_tanf_section1_datafile.quarter = 'Q4' + small_tanf_section1_datafile.year = 2021 + small_tanf_section1_datafile.quarter = 'Q1' parse.parse_datafile(small_tanf_section1_datafile) assert TANF_T3.objects.count() == 6 @@ -655,8 +652,8 @@ def bad_tanf_s1__row_missing_required_field(stt_user, stt): @pytest.mark.django_db def test_parse_bad_tfs1_missing_required(bad_tanf_s1__row_missing_required_field, dfs): """Test parsing a bad TANF Section 1 submission where a row is missing required data.""" - bad_tanf_s1__row_missing_required_field.year = 2020 - bad_tanf_s1__row_missing_required_field.quarter = 'Q4' + bad_tanf_s1__row_missing_required_field.year = 2021 + bad_tanf_s1__row_missing_required_field.quarter = 'Q1' dfs.datafile = bad_tanf_s1__row_missing_required_field dfs.save() @@ -705,8 +702,8 @@ def bad_ssp_s1__row_missing_required_field(stt_user, stt): @pytest.mark.django_db() def test_parse_bad_ssp_s1_missing_required(bad_ssp_s1__row_missing_required_field): """Test parsing a bad TANF Section 1 submission where a row is missing required data.""" - bad_ssp_s1__row_missing_required_field.year = 2018 - bad_ssp_s1__row_missing_required_field.quarter = 'Q4' + bad_ssp_s1__row_missing_required_field.year = 2019 + bad_ssp_s1__row_missing_required_field.quarter = 'Q1' parse.parse_datafile(bad_ssp_s1__row_missing_required_field) @@ -757,7 +754,7 @@ def test_parse_bad_ssp_s1_missing_required(bad_ssp_s1__row_missing_required_fiel def test_dfs_set_case_aggregates(test_datafile, dfs): """Test that the case aggregates are set correctly.""" test_datafile.year = 2020 - test_datafile.quarter = 'Q4' + test_datafile.quarter = 'Q3' test_datafile.section = 'Active Case Data' test_datafile.save() # this still needs to execute to create db objects to be queried @@ -829,8 +826,8 @@ def small_tanf_section2_file(stt_user, stt): @pytest.mark.django_db() def test_parse_small_tanf_section2_file(small_tanf_section2_file): """Test parsing a small TANF Section 2 submission.""" - small_tanf_section2_file.year = 2020 - small_tanf_section2_file.quarter = 'Q4' + small_tanf_section2_file.year = 2021 + small_tanf_section2_file.quarter = 'Q1' parse.parse_datafile(small_tanf_section2_file) @@ -860,8 +857,8 @@ def tanf_section2_file(stt_user, stt): @pytest.mark.django_db() def test_parse_tanf_section2_file(tanf_section2_file): """Test parsing TANF Section 2 submission.""" - tanf_section2_file.year = 2020 - tanf_section2_file.quarter = 'Q4' + tanf_section2_file.year = 2021 + tanf_section2_file.quarter = 'Q1' parse.parse_datafile(tanf_section2_file) assert TANF_T4.objects.all().count() == 223 @@ -885,8 +882,8 @@ def tanf_section3_file(stt_user, stt): @pytest.mark.django_db() def test_parse_tanf_section3_file(tanf_section3_file): """Test parsing TANF Section 3 submission.""" - tanf_section3_file.year = 2020 - tanf_section3_file.quarter = 'Q4' + tanf_section3_file.year = 2021 + tanf_section3_file.quarter = 'Q1' parse.parse_datafile(tanf_section3_file) @@ -921,8 +918,8 @@ def tanf_section1_file_with_blanks(stt_user, stt): @pytest.mark.django_db() def test_parse_tanf_section1_blanks_file(tanf_section1_file_with_blanks): """Test section 1 fields that are allowed to have blanks.""" - tanf_section1_file_with_blanks.year = 2020 - tanf_section1_file_with_blanks.quarter = 'Q4' + tanf_section1_file_with_blanks.year = 2021 + tanf_section1_file_with_blanks.quarter = 'Q1' parse.parse_datafile(tanf_section1_file_with_blanks) @@ -951,8 +948,8 @@ def tanf_section4_file(stt_user, stt): @pytest.mark.django_db() def test_parse_tanf_section4_file(tanf_section4_file): """Test parsing TANF Section 4 submission.""" - tanf_section4_file.year = 2020 - tanf_section4_file.quarter = 'Q4' + tanf_section4_file.year = 2021 + tanf_section4_file.quarter = 'Q1' parse.parse_datafile(tanf_section4_file) @@ -983,8 +980,8 @@ def ssp_section4_file(stt_user, stt): @pytest.mark.django_db() def test_parse_ssp_section4_file(ssp_section4_file): """Test parsing SSP Section 4 submission.""" - ssp_section4_file.year = 2018 - ssp_section4_file.quarter = 'Q4' + ssp_section4_file.year = 2019 + ssp_section4_file.quarter = 'Q1' parse.parse_datafile(ssp_section4_file) @@ -1004,8 +1001,8 @@ def ssp_section2_file(stt_user, stt): @pytest.mark.django_db() def test_parse_ssp_section2_file(ssp_section2_file): """Test parsing SSP Section 2 submission.""" - ssp_section2_file.year = 2018 - ssp_section2_file.quarter = 'Q4' + ssp_section2_file.year = 2019 + ssp_section2_file.quarter = 'Q1' parse.parse_datafile(ssp_section2_file) @@ -1032,8 +1029,8 @@ def ssp_section3_file(stt_user, stt): @pytest.mark.django_db() def test_parse_ssp_section3_file(ssp_section3_file): """Test parsing TANF Section 3 submission.""" - ssp_section3_file.year = 2018 - ssp_section3_file.quarter = 'Q4' + ssp_section3_file.year = 2019 + ssp_section3_file.quarter = 'Q1' parse.parse_datafile(ssp_section3_file) m6_objs = SSP_M6.objects.all().order_by('RPT_MONTH_YEAR') @@ -1066,8 +1063,8 @@ def test_rpt_month_year_mismatch(test_header_datafile): datafile.section = 'Active Case Data' # test_datafile fixture uses create_test_data_file which assigns # a default year / quarter of 2021 / Q1 - datafile.year = 2020 - datafile.quarter = 'Q4' + datafile.year = 2021 + datafile.quarter = 'Q1' datafile.save() parse.parse_datafile(datafile) @@ -1085,5 +1082,5 @@ def test_rpt_month_year_mismatch(test_header_datafile): err = parser_errors.first() assert err.error_type == ParserErrorCategoryChoices.PRE_CHECK - assert err.error_message == "Submitted reporting year:2020, quarter:Q4 doesn't match " + \ - "file reporting year:2023, quarter:Q4." + assert err.error_message == "Submitted reporting year:2020, quarter:Q4 doesn't" + \ + " match file reporting year:2023, quarter:Q1." diff --git a/tdrs-backend/tdpservice/parsers/test/test_util.py b/tdrs-backend/tdpservice/parsers/test/test_util.py index ef5cbca16..8ec9169a8 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_util.py +++ b/tdrs-backend/tdpservice/parsers/test/test_util.py @@ -2,8 +2,8 @@ import pytest from ..fields import Field -from ..row_schema import RowSchema -from ..util import SchemaManager, make_generate_parser_error, create_test_datafile +from ..row_schema import RowSchema, SchemaManager +from ..util import make_generate_parser_error, create_test_datafile def passing_validator(): diff --git a/tdrs-backend/tdpservice/parsers/test/test_views.py b/tdrs-backend/tdpservice/parsers/test/test_views.py index d9dae0e18..cec2b8b5f 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_views.py +++ b/tdrs-backend/tdpservice/parsers/test/test_views.py @@ -17,6 +17,8 @@ def test_datafile(stt_user, stt): def test_parsing_error_viewset_list(client, mocker, test_datafile): """Test the django rest framework parsing error viewset list.""" # parse datafile + test_datafile.year = 2021 + test_datafile.quarter = 'Q2' parse.parse_datafile(test_datafile) id = test_datafile.id @@ -50,6 +52,8 @@ def test_parsing_error_viewset_list(client, mocker, test_datafile): def test_parsing_error_viewset_list_no_fields_json(mocker, test_datafile): """Test the django rest framework parsing error viewset list.""" # parse datafile + test_datafile.year = 2021 + test_datafile.quarter = 'Q2' parse.parse_datafile(test_datafile) # set fields_json to None diff --git a/tdrs-backend/tdpservice/parsers/util.py b/tdrs-backend/tdpservice/parsers/util.py index 384dfb5c9..ed66bb51c 100644 --- a/tdrs-backend/tdpservice/parsers/util.py +++ b/tdrs-backend/tdpservice/parsers/util.py @@ -5,7 +5,7 @@ from tdpservice.data_files.models import DataFile from datetime import datetime from pathlib import Path -from .fields import TransformField +from .row_schema import SchemaManager import logging logger = logging.getLogger(__name__) @@ -87,29 +87,6 @@ def generate(schema, error_category, error_message, record=None, field=None): return generate -class SchemaManager: - """Manages one or more RowSchema's and runs all parsers and validators.""" - - def __init__(self, schemas): - self.schemas = schemas - - def parse_and_validate(self, line, generate_error): - """Run `parse_and_validate` for each schema provided and bubble up errors.""" - records = [] - - for schema in self.schemas: - record, is_valid, errors = schema.parse_and_validate(line, generate_error) - records.append((record, is_valid, errors)) - - return records - - def update_encrypted_fields(self, is_encrypted): - """Update whether schema fields are encrypted or not.""" - for schema in self.schemas: - for field in schema.fields: - if type(field) == TransformField and "is_encrypted" in field.kwargs: - field.kwargs['is_encrypted'] = is_encrypted - def contains_encrypted_indicator(line, encryption_field): """Determine if line contains encryption indicator.""" if encryption_field is not None: diff --git a/tdrs-backend/tdpservice/parsers/validators.py b/tdrs-backend/tdpservice/parsers/validators.py index f84c0bcad..f885f551e 100644 --- a/tdrs-backend/tdpservice/parsers/validators.py +++ b/tdrs-backend/tdpservice/parsers/validators.py @@ -1,6 +1,7 @@ """Generic parser validator functions for use in schema definitions.""" from .models import ParserErrorCategoryChoices +from .util import fiscal_to_calendar from datetime import date import logging @@ -551,8 +552,13 @@ def validate_header_section_matches_submission(datafile, section, generate_error def validate_header_rpt_month_year(datafile, header, generate_error): """Validate header rpt_month_year.""" - is_valid = datafile.year is not None and datafile.quarter is not None - is_valid = is_valid and datafile.year == header['year'] and datafile.quarter == f'Q{header["quarter"]}' + # the header year/quarter represent a calendar period, and frontend year/qtr represents a fiscal period + header_calendar_qtr = f"Q{header['quarter']}" + header_calendar_year = header['year'] + file_calendar_year, file_calendar_qtr = fiscal_to_calendar(datafile.year, f"{datafile.quarter}") + + is_valid = file_calendar_year is not None and file_calendar_qtr is not None + is_valid = is_valid and file_calendar_year == header_calendar_year and file_calendar_qtr == header_calendar_qtr error = None if not is_valid: From 4aabc95347b075e5533d65fe51206a945f8a7bd2 Mon Sep 17 00:00:00 2001 From: raftmsohani <97037188+raftmsohani@users.noreply.github.com> Date: Fri, 5 Jan 2024 09:05:15 -0500 Subject: [PATCH 006/149] Update small_incorrect_file_cross_validator.txt --- .../test/data/small_incorrect_file_cross_validator.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt b/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt index 44e10437e..06acaed9f 100644 --- a/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt +++ b/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt @@ -1,3 +1,3 @@ -HEADER20211A06 TAN1 N -T12021101111111111223003403361110213120000300000000000008730000000000000000000000000000000000000222222000000002229012 -TRAILER0000001 \ No newline at end of file +EADER20204A06 TAN1 N +T12020101111111111223003403361110213120000300000000000008730000000000000000000000000000000000000222222000000002229012 +TRAILER0000001 From 7946fcba0a349edff22d01762debbea302fc2887 Mon Sep 17 00:00:00 2001 From: raftmsohani <97037188+raftmsohani@users.noreply.github.com> Date: Fri, 5 Jan 2024 09:05:44 -0500 Subject: [PATCH 007/149] Update small_incorrect_file_cross_validator.txt --- .../parsers/test/data/small_incorrect_file_cross_validator.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt b/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt index 06acaed9f..219c36347 100644 --- a/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt +++ b/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt @@ -1,3 +1,3 @@ -EADER20204A06 TAN1 N +HEADER20204A06 TAN1 N T12020101111111111223003403361110213120000300000000000008730000000000000000000000000000000000000222222000000002229012 TRAILER0000001 From ef6368ecbf666f6b0e18163810f2db3fbe3346c2 Mon Sep 17 00:00:00 2001 From: raftmsohani <97037188+raftmsohani@users.noreply.github.com> Date: Fri, 5 Jan 2024 09:06:21 -0500 Subject: [PATCH 008/149] Update small_incorrect_file_cross_validator.txt --- .../parsers/test/data/small_incorrect_file_cross_validator.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt b/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt index 219c36347..22ef8b350 100644 --- a/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt +++ b/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt @@ -1,3 +1,3 @@ HEADER20204A06 TAN1 N T12020101111111111223003403361110213120000300000000000008730000000000000000000000000000000000000222222000000002229012 -TRAILER0000001 +TRAILER0000001 From 66e746ae86f98a1baf2846fe1b36714faaa79b65 Mon Sep 17 00:00:00 2001 From: raftmsohani <97037188+raftmsohani@users.noreply.github.com> Date: Fri, 5 Jan 2024 09:06:42 -0500 Subject: [PATCH 009/149] Update small_incorrect_file_cross_validator.txt --- .../parsers/test/data/small_incorrect_file_cross_validator.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt b/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt index 22ef8b350..219c36347 100644 --- a/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt +++ b/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt @@ -1,3 +1,3 @@ HEADER20204A06 TAN1 N T12020101111111111223003403361110213120000300000000000008730000000000000000000000000000000000000222222000000002229012 -TRAILER0000001 +TRAILER0000001 From 4bc60a7596c76eb2f97f1e516cc53d5940e29f00 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Fri, 5 Jan 2024 09:12:17 -0500 Subject: [PATCH 010/149] revert changes on test file --- .../parsers/test/data/small_incorrect_file_cross_validator.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt b/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt index 219c36347..f01ab84d1 100644 --- a/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt +++ b/tdrs-backend/tdpservice/parsers/test/data/small_incorrect_file_cross_validator.txt @@ -1,3 +1,3 @@ HEADER20204A06 TAN1 N T12020101111111111223003403361110213120000300000000000008730000000000000000000000000000000000000222222000000002229012 -TRAILER0000001 +TRAILER0000001 \ No newline at end of file From ae4719ccb943af6c882756da125d4d9020b65fba Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Fri, 5 Jan 2024 09:20:28 -0500 Subject: [PATCH 011/149] corrected the failing test --- tdrs-backend/tdpservice/parsers/test/test_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/test/test_views.py b/tdrs-backend/tdpservice/parsers/test/test_views.py index cec2b8b5f..ce1e08c2c 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_views.py +++ b/tdrs-backend/tdpservice/parsers/test/test_views.py @@ -18,7 +18,7 @@ def test_parsing_error_viewset_list(client, mocker, test_datafile): """Test the django rest framework parsing error viewset list.""" # parse datafile test_datafile.year = 2021 - test_datafile.quarter = 'Q2' + test_datafile.quarter = 'Q1' parse.parse_datafile(test_datafile) id = test_datafile.id @@ -53,7 +53,7 @@ def test_parsing_error_viewset_list_no_fields_json(mocker, test_datafile): """Test the django rest framework parsing error viewset list.""" # parse datafile test_datafile.year = 2021 - test_datafile.quarter = 'Q2' + test_datafile.quarter = 'Q1' parse.parse_datafile(test_datafile) # set fields_json to None From d709a8ca3c49bef78de286456e9de29c3114cb30 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Fri, 5 Jan 2024 11:35:29 -0500 Subject: [PATCH 012/149] resolve circular import --- tdrs-backend/tdpservice/parsers/aggregates.py | 56 +++++++++++++++++++ tdrs-backend/tdpservice/parsers/fields.py | 12 +--- tdrs-backend/tdpservice/parsers/row_schema.py | 12 +--- .../tdpservice/parsers/test/test_parse.py | 18 +++--- tdrs-backend/tdpservice/parsers/util.py | 52 ----------------- .../tdpservice/scheduling/parser_task.py | 2 +- 6 files changed, 68 insertions(+), 84 deletions(-) create mode 100644 tdrs-backend/tdpservice/parsers/aggregates.py diff --git a/tdrs-backend/tdpservice/parsers/aggregates.py b/tdrs-backend/tdpservice/parsers/aggregates.py new file mode 100644 index 000000000..af5af6cba --- /dev/null +++ b/tdrs-backend/tdpservice/parsers/aggregates.py @@ -0,0 +1,56 @@ +"""Aggregate methods for the parsers.""" +from .row_schema import SchemaManager +from .models import ParserError +from .util import month_to_int, get_program_models, get_text_from_df, \ + transform_to_months, fiscal_to_calendar, get_prog_from_section + + +def case_aggregates_by_month(df, dfs_status): + """Return case aggregates by month.""" + section = str(df.section) # section -> text + program_type = get_prog_from_section(section) # section -> program_type -> text + + # from datafile year/quarter, generate short month names for each month in quarter ala 'Jan', 'Feb', 'Mar' + calendar_year, calendar_qtr = fiscal_to_calendar(df.year, df.quarter) + month_list = transform_to_months(calendar_qtr) + + short_section = get_text_from_df(df)['section'] + schema_models_dict = get_program_models(program_type, short_section) + schema_models = [model for model in schema_models_dict.values()] + + aggregate_data = {"months": [], "rejected": 0} + for month in month_list: + total = 0 + cases_with_errors = 0 + accepted = 0 + month_int = month_to_int(month) + rpt_month_year = int(f"{calendar_year}{month_int}") + if dfs_status == "Rejected": + # we need to be careful here on examples of bad headers or empty files, since no month will be found + # but we can rely on the frontend submitted year-quarter to still generate the list of months + aggregate_data["months"].append({"accepted_with_errors": "N/A", + "accepted_without_errors": "N/A", + "month": month}) + continue + + case_numbers = set() + for schema_model in schema_models: + if isinstance(schema_model, SchemaManager): + schema_model = schema_model.schemas[0] + + curr_case_numbers = set(schema_model.model.objects.filter(datafile=df).filter(RPT_MONTH_YEAR=rpt_month_year) + .distinct("CASE_NUMBER").values_list("CASE_NUMBER", flat=True)) + case_numbers = case_numbers.union(curr_case_numbers) + + total += len(case_numbers) + cases_with_errors += ParserError.objects.filter(file=df).filter( + case_number__in=case_numbers).distinct('case_number').count() + accepted = total - cases_with_errors + + aggregate_data['months'].append({"month": month, + "accepted_without_errors": accepted, + "accepted_with_errors": cases_with_errors}) + + aggregate_data['rejected'] = ParserError.objects.filter(file=df).filter(case_number=None).count() + + return aggregate_data diff --git a/tdrs-backend/tdpservice/parsers/fields.py b/tdrs-backend/tdpservice/parsers/fields.py index 62de634e7..076743096 100644 --- a/tdrs-backend/tdpservice/parsers/fields.py +++ b/tdrs-backend/tdpservice/parsers/fields.py @@ -1,20 +1,10 @@ """Datafile field representations.""" import logging +from .validators import value_is_empty logger = logging.getLogger(__name__) -def value_is_empty(value, length): - """Handle 'empty' values as field inputs.""" - empty_values = [ - '', - ' '*length, # ' ' - '#'*length, # '#####' - '_'*length, # '_____' - ] - - return value is None or value in empty_values - class Field: """Provides a mapping between a field name and its position.""" diff --git a/tdrs-backend/tdpservice/parsers/row_schema.py b/tdrs-backend/tdpservice/parsers/row_schema.py index ca7b9164c..8e6c9f5eb 100644 --- a/tdrs-backend/tdpservice/parsers/row_schema.py +++ b/tdrs-backend/tdpservice/parsers/row_schema.py @@ -1,21 +1,11 @@ """Row schema for datafile.""" from .models import ParserErrorCategoryChoices from .fields import Field, TransformField +from .validators import value_is_empty import logging logger = logging.getLogger(__name__) -def value_is_empty(value, length): - """Handle 'empty' values as field inputs.""" - empty_values = [ - '', - ' '*length, # ' ' - '#'*length, # '#####' - '_'*length, # '_____' - ] - - return value is None or value in empty_values - class RowSchema: """Maps the schema for data lines.""" diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index b8e4c3ec0..6743f4086 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -8,7 +8,7 @@ from tdpservice.search_indexes.models.ssp import SSP_M1, SSP_M2, SSP_M3, SSP_M4, SSP_M5, SSP_M6, SSP_M7 from .factories import DataFileSummaryFactory from tdpservice.data_files.models import DataFile -from .. import schema_defs, util +from .. import schema_defs, aggregates, util import logging @@ -46,7 +46,7 @@ def test_parse_small_correct_file(test_datafile, dfs): parse.parse_datafile(test_datafile) dfs.status = dfs.get_status() - dfs.case_aggregates = util.case_aggregates_by_month( + dfs.case_aggregates = aggregates.case_aggregates_by_month( dfs.datafile, dfs.status) for month in dfs.case_aggregates['months']: if month['month'] == 'Oct': @@ -86,7 +86,7 @@ def test_parse_section_mismatch(test_datafile, dfs): dfs.status = dfs.get_status() assert dfs.status == DataFileSummary.Status.REJECTED parser_errors = ParserError.objects.filter(file=test_datafile) - dfs.case_aggregates = util.case_aggregates_by_month( + dfs.case_aggregates = aggregates.case_aggregates_by_month( dfs.datafile, dfs.status) assert dfs.case_aggregates == {'rejected': 1, 'months': [ @@ -160,7 +160,7 @@ def test_parse_big_file(test_big_file, dfs): parse.parse_datafile(test_big_file) dfs.status = dfs.get_status() assert dfs.status == DataFileSummary.Status.ACCEPTED_WITH_ERRORS - dfs.case_aggregates = util.case_aggregates_by_month( + dfs.case_aggregates = aggregates.case_aggregates_by_month( dfs.datafile, dfs.status) assert dfs.case_aggregates == {'rejected': 0, 'months': [ @@ -398,7 +398,7 @@ def test_parse_empty_file(empty_file, dfs): errors = parse.parse_datafile(empty_file) dfs.status = dfs.get_status() - dfs.case_aggregates = util.case_aggregates_by_month(empty_file, dfs.status) + dfs.case_aggregates = aggregates.case_aggregates_by_month(empty_file, dfs.status) assert dfs.status == DataFileSummary.Status.REJECTED assert dfs.case_aggregates == {'rejected': 2, @@ -453,7 +453,7 @@ def test_parse_small_ssp_section1_datafile(small_ssp_section1_datafile, dfs): parser_errors = ParserError.objects.filter(file=small_ssp_section1_datafile) dfs.status = dfs.get_status() assert dfs.status == DataFileSummary.Status.ACCEPTED_WITH_ERRORS - dfs.case_aggregates = util.case_aggregates_by_month( + dfs.case_aggregates = aggregates.case_aggregates_by_month( dfs.datafile, dfs.status) for month in dfs.case_aggregates['months']: if month['month'] == 'Oct': @@ -528,7 +528,7 @@ def test_parse_tanf_section1_datafile(small_tanf_section1_datafile, dfs): dfs.status = dfs.get_status() assert dfs.status == DataFileSummary.Status.ACCEPTED - dfs.case_aggregates = util.case_aggregates_by_month( + dfs.case_aggregates = aggregates.case_aggregates_by_month( dfs.datafile, dfs.status) assert dfs.case_aggregates == {'rejected': 0, 'months': [ @@ -762,7 +762,7 @@ def test_dfs_set_case_aggregates(test_datafile, dfs): dfs.file = test_datafile dfs.save() dfs.status = dfs.get_status() - dfs.case_aggregates = util.case_aggregates_by_month( + dfs.case_aggregates = aggregates.case_aggregates_by_month( test_datafile, dfs.status) dfs.save() @@ -790,7 +790,7 @@ def test_get_schema_options(dfs): # from text: schema = parse.get_schema_manager('T1xx', 'A', 'TAN') - assert isinstance(schema, util.SchemaManager) + assert isinstance(schema, aggregates.SchemaManager) assert schema == schema_defs.tanf.t1 # get model diff --git a/tdrs-backend/tdpservice/parsers/util.py b/tdrs-backend/tdpservice/parsers/util.py index ed66bb51c..dfdeb6cc7 100644 --- a/tdrs-backend/tdpservice/parsers/util.py +++ b/tdrs-backend/tdpservice/parsers/util.py @@ -5,7 +5,6 @@ from tdpservice.data_files.models import DataFile from datetime import datetime from pathlib import Path -from .row_schema import SchemaManager import logging logger = logging.getLogger(__name__) @@ -263,54 +262,3 @@ def transform_to_months(quarter): def month_to_int(month): """Return the integer value of a month.""" return datetime.strptime(month, '%b').strftime('%m') - - -def case_aggregates_by_month(df, dfs_status): - """Return case aggregates by month.""" - section = str(df.section) # section -> text - program_type = get_prog_from_section(section) # section -> program_type -> text - - # from datafile year/quarter, generate short month names for each month in quarter ala 'Jan', 'Feb', 'Mar' - calendar_year, calendar_qtr = fiscal_to_calendar(df.year, df.quarter) - month_list = transform_to_months(calendar_qtr) - - short_section = get_text_from_df(df)['section'] - schema_models_dict = get_program_models(program_type, short_section) - schema_models = [model for model in schema_models_dict.values()] - - aggregate_data = {"months": [], "rejected": 0} - for month in month_list: - total = 0 - cases_with_errors = 0 - accepted = 0 - month_int = month_to_int(month) - rpt_month_year = int(f"{calendar_year}{month_int}") - if dfs_status == "Rejected": - # we need to be careful here on examples of bad headers or empty files, since no month will be found - # but we can rely on the frontend submitted year-quarter to still generate the list of months - aggregate_data["months"].append({"accepted_with_errors": "N/A", - "accepted_without_errors": "N/A", - "month": month}) - continue - - case_numbers = set() - for schema_model in schema_models: - if isinstance(schema_model, SchemaManager): - schema_model = schema_model.schemas[0] - - curr_case_numbers = set(schema_model.model.objects.filter(datafile=df).filter(RPT_MONTH_YEAR=rpt_month_year) - .distinct("CASE_NUMBER").values_list("CASE_NUMBER", flat=True)) - case_numbers = case_numbers.union(curr_case_numbers) - - total += len(case_numbers) - cases_with_errors += ParserError.objects.filter(file=df).filter( - case_number__in=case_numbers).distinct('case_number').count() - accepted = total - cases_with_errors - - aggregate_data['months'].append({"month": month, - "accepted_without_errors": accepted, - "accepted_with_errors": cases_with_errors}) - - aggregate_data['rejected'] = ParserError.objects.filter(file=df).filter(case_number=None).count() - - return aggregate_data diff --git a/tdrs-backend/tdpservice/scheduling/parser_task.py b/tdrs-backend/tdpservice/scheduling/parser_task.py index be47703c5..a7462f0e3 100644 --- a/tdrs-backend/tdpservice/scheduling/parser_task.py +++ b/tdrs-backend/tdpservice/scheduling/parser_task.py @@ -5,7 +5,7 @@ from tdpservice.data_files.models import DataFile from tdpservice.parsers.parse import parse_datafile from tdpservice.parsers.models import DataFileSummary -from tdpservice.parsers.util import case_aggregates_by_month +from tdpservice.parsers.aggregates import case_aggregates_by_month logger = logging.getLogger(__name__) From bfbd7fa56d977004aa700a9398283f87c97973c7 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Fri, 5 Jan 2024 11:53:14 -0500 Subject: [PATCH 013/149] merge conflict resolution --- .../tdpservice/parsers/schema_defs/tribal_tanf/t1.py | 3 +-- .../tdpservice/parsers/schema_defs/tribal_tanf/t2.py | 3 +-- .../tdpservice/parsers/schema_defs/tribal_tanf/t3.py | 3 +-- tdrs-backend/tdpservice/parsers/test/test_parse.py | 2 +- tdrs-backend/tdpservice/parsers/validators.py | 9 +++------ 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t1.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t1.py index 2820b76fa..b3ebae375 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t1.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t1.py @@ -1,8 +1,7 @@ """Schema for Tribal TANF T1 record types.""" -from ...util import SchemaManager from ...fields import Field -from ...row_schema import RowSchema +from ...row_schema import RowSchema, SchemaManager from ... import validators from tdpservice.search_indexes.models.tribal import Tribal_TANF_T1 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py index ed1d03e8d..330a4ca41 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py @@ -1,10 +1,9 @@ """Schema for Tribal TANF T2 row of all submission types.""" -from ...util import SchemaManager from ...transforms import tanf_ssn_decryption_func from ...fields import TransformField, Field -from ...row_schema import RowSchema +from ...row_schema import RowSchema, SchemaManager from ... import validators from tdpservice.search_indexes.models.tribal import Tribal_TANF_T2 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py index 4d68ec457..b09fb2323 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py @@ -1,10 +1,9 @@ """Schema for Tribal TANF T3 row of all submission types.""" -from ...util import SchemaManager from ...transforms import tanf_ssn_decryption_func from ...fields import TransformField, Field -from ...row_schema import RowSchema +from ...row_schema import RowSchema, SchemaManager from ... import validators from tdpservice.search_indexes.models.tribal import Tribal_TANF_T3 diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index 3dc6cda96..cb541e001 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -1105,7 +1105,7 @@ def test_parse_tribal_section_1_file(tribal_section_1_file, dfs): dfs.status = dfs.get_status() assert dfs.status == DataFileSummary.Status.ACCEPTED - dfs.case_aggregates = util.case_aggregates_by_month( + dfs.case_aggregates = aggregates.case_aggregates_by_month( dfs.datafile, dfs.status) assert dfs.case_aggregates == {'rejected': 0, 'months': [{'month': 'Oct', 'accepted_without_errors': 1, 'accepted_with_errors': 0}, diff --git a/tdrs-backend/tdpservice/parsers/validators.py b/tdrs-backend/tdpservice/parsers/validators.py index 6fd9db40b..a2f3c8ec7 100644 --- a/tdrs-backend/tdpservice/parsers/validators.py +++ b/tdrs-backend/tdpservice/parsers/validators.py @@ -576,8 +576,8 @@ def validate_tribe_fips_program_agree(program_type, tribe_code, state_fips_code, return is_valid, error - - def validate_header_rpt_month_year(datafile, header, generate_error): + +def validate_header_rpt_month_year(datafile, header, generate_error): """Validate header rpt_month_year.""" # the header year/quarter represent a calendar period, and frontend year/qtr represents a fiscal period header_calendar_qtr = f"Q{header['quarter']}" @@ -597,7 +597,4 @@ def validate_header_rpt_month_year(datafile, header, generate_error): record=None, field=None, ) - return is_valid, error - - - \ No newline at end of file + return is_valid, error From 5efd2037840bc6594693823a2ab9c6637caf7e5b Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Fri, 5 Jan 2024 12:08:14 -0500 Subject: [PATCH 014/149] linting --- tdrs-backend/tdpservice/parsers/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tdrs-backend/tdpservice/parsers/validators.py b/tdrs-backend/tdpservice/parsers/validators.py index a2f3c8ec7..b62387273 100644 --- a/tdrs-backend/tdpservice/parsers/validators.py +++ b/tdrs-backend/tdpservice/parsers/validators.py @@ -597,4 +597,4 @@ def validate_header_rpt_month_year(datafile, header, generate_error): record=None, field=None, ) - return is_valid, error + return is_valid, error From 7dffa01d6412cd66176160e0cdc680e37629d4b8 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Tue, 9 Jan 2024 13:36:00 -0500 Subject: [PATCH 015/149] correct failing tests --- .../tdpservice/parsers/schema_defs/tribal_tanf/t4.py | 3 +-- .../tdpservice/parsers/schema_defs/tribal_tanf/t5.py | 3 +-- .../tdpservice/parsers/schema_defs/tribal_tanf/t6.py | 3 +-- tdrs-backend/tdpservice/parsers/test/test_parse.py | 5 +++++ 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t4.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t4.py index f5227781b..be4dc5cf8 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t4.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t4.py @@ -1,8 +1,7 @@ """Schema for Tribal TANF T4 record types.""" -from ...util import SchemaManager from ...fields import Field -from ...row_schema import RowSchema +from ...row_schema import RowSchema, SchemaManager from ... import validators from tdpservice.search_indexes.models.tribal import Tribal_TANF_T4 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py index d1a38ae50..98a5e41e6 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py @@ -1,10 +1,9 @@ """Schema for Tribal TANF T5 row of all submission types.""" -from ...util import SchemaManager from ...transforms import tanf_ssn_decryption_func from ...fields import TransformField, Field -from ...row_schema import RowSchema +from ...row_schema import RowSchema, SchemaManager from ... import validators from tdpservice.search_indexes.models.tribal import Tribal_TANF_T5 diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t6.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t6.py index 844427c77..6f5cff615 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t6.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t6.py @@ -1,10 +1,9 @@ """Schema for Tribal T6 record.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.transforms import calendar_quarter_to_rpt_month_year from tdpservice.parsers.fields import Field, TransformField -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers import validators from tdpservice.search_indexes.models.tribal import Tribal_TANF_T6 diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index 0453a3392..2f38be9f5 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -1156,6 +1156,8 @@ def tribal_section_2_file(stt_user, stt): @pytest.mark.django_db() def test_parse_tribal_section_2_file(tribal_section_2_file): """Test parsing Tribal TANF Section 2 submission.""" + tribal_section_2_file.year = 2020 + tribal_section_2_file.quarter = 'Q1' parse.parse_datafile(tribal_section_2_file) assert Tribal_TANF_T4.objects.all().count() == 6 @@ -1178,6 +1180,9 @@ def tribal_section_3_file(stt_user, stt): @pytest.mark.django_db() def test_parse_tribal_section_3_file(tribal_section_3_file): """Test parsing Tribal TANF Section 3 submission.""" + tribal_section_3_file.year = 2020 + tribal_section_3_file.quarter = 'Q1' + parse.parse_datafile(tribal_section_3_file) assert Tribal_TANF_T6.objects.all().count() == 3 From 620bd224764a1d2a50b02b2137c481932b5b5585 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Wed, 10 Jan 2024 13:05:46 -0500 Subject: [PATCH 016/149] corrected t7 tests --- tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t7.py | 3 +-- tdrs-backend/tdpservice/parsers/test/test_parse.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t7.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t7.py index 332caed97..8dda3f03d 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t7.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t7.py @@ -1,8 +1,7 @@ """Schema for Tribal TANF T7 Row.""" -from tdpservice.parsers.util import SchemaManager from tdpservice.parsers.fields import Field, TransformField -from tdpservice.parsers.row_schema import RowSchema +from tdpservice.parsers.row_schema import RowSchema, SchemaManager from tdpservice.parsers.transforms import calendar_quarter_to_rpt_month_year from tdpservice.parsers import validators from tdpservice.search_indexes.models.tribal import Tribal_TANF_T7 diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index 9170582cf..96518daac 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -1203,6 +1203,8 @@ def tribal_section_4_file(stt_user, stt): @pytest.mark.django_db() def test_parse_tribal_section_4_file(tribal_section_4_file): """Test parsing Tribal TANF Section 4 submission.""" + tribal_section_4_file.year = 2020 + tribal_section_4_file.quarter = 'Q1' parse.parse_datafile(tribal_section_4_file) assert Tribal_TANF_T7.objects.all().count() == 18 From e7554d6f98e628f25b2032e4a336e5a64d255677 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Thu, 18 Jan 2024 15:29:13 -0500 Subject: [PATCH 017/149] first changes --- tdrs-backend/tdpservice/parsers/parse.py | 5 ++++- tdrs-backend/tdpservice/parsers/row_schema.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tdrs-backend/tdpservice/parsers/parse.py b/tdrs-backend/tdpservice/parsers/parse.py index b86070789..919dcb68a 100644 --- a/tdrs-backend/tdpservice/parsers/parse.py +++ b/tdrs-backend/tdpservice/parsers/parse.py @@ -5,7 +5,7 @@ import itertools import logging from .models import ParserErrorCategoryChoices, ParserError -from . import schema_defs, validators, util +from . import schema_defs, validators, util, row_schema logger = logging.getLogger(__name__) @@ -213,6 +213,9 @@ def parse_datafile_lines(datafile, program_type, section, is_encrypted): schema_manager = get_schema_manager(line, section, program_type) + if type(schema_manager) is row_schema.SchemaManager: + schema_manager.datafile = datafile + records = manager_parse_line(line, schema_manager, generate_error, is_encrypted) record_number = 0 diff --git a/tdrs-backend/tdpservice/parsers/row_schema.py b/tdrs-backend/tdpservice/parsers/row_schema.py index 3e4db7011..17a8f8066 100644 --- a/tdrs-backend/tdpservice/parsers/row_schema.py +++ b/tdrs-backend/tdpservice/parsers/row_schema.py @@ -22,6 +22,7 @@ def __init__( self.postparsing_validators = postparsing_validators self.fields = fields self.quiet_preparser_errors = quiet_preparser_errors + #self.datafile = None def _add_field(self, item, name, length, start, end, type): """Add a field to the schema.""" @@ -40,6 +41,7 @@ def get_all_fields(self): def parse_and_validate(self, line, generate_error): """Run all validation steps in order, and parse the given line into a record.""" + errors = [] # run preparsing validators @@ -54,6 +56,13 @@ def parse_and_validate(self, line, generate_error): # parse line to model record = self.parse_line(line) + # field validators + if self.model.__name__ != 'dict': + print('__________________') + #print('_______ record:', record.__dict__) + #print('__________ self.datafile.year:', self.datafile.year) + #print('__________ self.datafile.month:', self.datafile.month) + # run field validators fields_are_valid, field_errors = self.run_field_validators(record, generate_error) @@ -188,12 +197,14 @@ class SchemaManager: def __init__(self, schemas): self.schemas = schemas + self.datafile = None def parse_and_validate(self, line, generate_error): """Run `parse_and_validate` for each schema provided and bubble up errors.""" records = [] for schema in self.schemas: + #schema.datafile = self.datafile record, is_valid, errors = schema.parse_and_validate(line, generate_error) records.append((record, is_valid, errors)) From 175e903732ffc6cd11b76a6b8eb02087d364e4dd Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Thu, 18 Jan 2024 15:34:43 -0500 Subject: [PATCH 018/149] remove old changes --- tdrs-backend/tdpservice/parsers/util.py | 53 ------------------------- 1 file changed, 53 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/util.py b/tdrs-backend/tdpservice/parsers/util.py index 48e77673c..53ef27952 100644 --- a/tdrs-backend/tdpservice/parsers/util.py +++ b/tdrs-backend/tdpservice/parsers/util.py @@ -291,56 +291,3 @@ def transform_to_months(quarter): def month_to_int(month): """Return the integer value of a month.""" return datetime.strptime(month, '%b').strftime('%m') - - -def case_aggregates_by_month(df, dfs_status): - """Return case aggregates by month.""" - section = str(df.section) # section -> text - program_type = get_prog_from_section(section) # section -> program_type -> text - - # from datafile year/quarter, generate short month names for each month in quarter ala 'Jan', 'Feb', 'Mar' - calendar_year, calendar_qtr = fiscal_to_calendar(df.year, df.quarter) - month_list = transform_to_months(calendar_qtr) - - short_section = get_text_from_df(df)['section'] - schema_models_dict = get_program_models(program_type, short_section) - schema_models = [model for model in schema_models_dict.values()] - - aggregate_data = {"months": [], "rejected": 0} - for month in month_list: - total = 0 - cases_with_errors = 0 - accepted = 0 - month_int = month_to_int(month) - rpt_month_year = int(f"{calendar_year}{month_int}") - - if dfs_status == "Rejected": - # we need to be careful here on examples of bad headers or empty files, since no month will be found - # but we can rely on the frontend submitted year-quarter to still generate the list of months - aggregate_data["months"].append({"accepted_with_errors": "N/A", - "accepted_without_errors": "N/A", - "month": month}) - continue - - case_numbers = set() - for schema_model in schema_models: - if isinstance(schema_model, SchemaManager): - schema_model = schema_model.schemas[0] - - curr_case_numbers = set(schema_model.document.Django.model.objects.filter(datafile=df) - .filter(RPT_MONTH_YEAR=rpt_month_year) - .distinct("CASE_NUMBER").values_list("CASE_NUMBER", flat=True)) - case_numbers = case_numbers.union(curr_case_numbers) - - total += len(case_numbers) - cases_with_errors += ParserError.objects.filter(file=df).filter( - case_number__in=case_numbers).distinct('case_number').count() - accepted = total - cases_with_errors - - aggregate_data['months'].append({"month": month, - "accepted_without_errors": accepted, - "accepted_with_errors": cases_with_errors}) - - aggregate_data['rejected'] = ParserError.objects.filter(file=df).filter(case_number=None).count() - - return aggregate_data From 0c4f3d2a895f8d5ec7fb9073cec866b0368d529d Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Fri, 19 Jan 2024 09:29:07 -0500 Subject: [PATCH 019/149] tep changes --- tdrs-backend/tdpservice/parsers/row_schema.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/row_schema.py b/tdrs-backend/tdpservice/parsers/row_schema.py index f49ff1953..eb0bf558e 100644 --- a/tdrs-backend/tdpservice/parsers/row_schema.py +++ b/tdrs-backend/tdpservice/parsers/row_schema.py @@ -56,9 +56,10 @@ def parse_and_validate(self, line, generate_error): # parse line to model record = self.parse_line(line) - # field validators - if self.model.__name__ != 'dict': - print('__________________') + # field validators + print('__________________ self:', self.__dict__) + #if self.model.__name__ != 'dict': + #print('__________________') #print('_______ record:', record.__dict__) #print('__________ self.datafile.year:', self.datafile.year) #print('__________ self.datafile.month:', self.datafile.month) From 65dae3bf147b0c67a92c7ef1cf20c297eef01851 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Fri, 19 Jan 2024 14:32:25 -0500 Subject: [PATCH 020/149] getting the reporting month --- tdrs-backend/tdpservice/parsers/aggregates.py | 2 +- tdrs-backend/tdpservice/parsers/row_schema.py | 18 +++++++++++------ tdrs-backend/tdpservice/parsers/util.py | 20 +++++++++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/aggregates.py b/tdrs-backend/tdpservice/parsers/aggregates.py index af5af6cba..4fa607ef2 100644 --- a/tdrs-backend/tdpservice/parsers/aggregates.py +++ b/tdrs-backend/tdpservice/parsers/aggregates.py @@ -38,7 +38,7 @@ def case_aggregates_by_month(df, dfs_status): if isinstance(schema_model, SchemaManager): schema_model = schema_model.schemas[0] - curr_case_numbers = set(schema_model.model.objects.filter(datafile=df).filter(RPT_MONTH_YEAR=rpt_month_year) + curr_case_numbers = set(schema_model.document.Django.model.objects.filter(datafile=df).filter(RPT_MONTH_YEAR=rpt_month_year) .distinct("CASE_NUMBER").values_list("CASE_NUMBER", flat=True)) case_numbers = case_numbers.union(curr_case_numbers) diff --git a/tdrs-backend/tdpservice/parsers/row_schema.py b/tdrs-backend/tdpservice/parsers/row_schema.py index eb0bf558e..de3732cd2 100644 --- a/tdrs-backend/tdpservice/parsers/row_schema.py +++ b/tdrs-backend/tdpservice/parsers/row_schema.py @@ -22,7 +22,7 @@ def __init__( self.postparsing_validators = postparsing_validators self.fields = fields self.quiet_preparser_errors = quiet_preparser_errors - #self.datafile = None + self.datafile = None def _add_field(self, item, name, length, start, end, type): """Add a field to the schema.""" @@ -56,13 +56,19 @@ def parse_and_validate(self, line, generate_error): # parse line to model record = self.parse_line(line) - # field validators - print('__________________ self:', self.__dict__) - #if self.model.__name__ != 'dict': + # parsing field values + if self.document: + datafile_quarter = self.datafile.quarter + datafile_year = self.datafile.year + reporting_month_year = getattr(record, 'RPT_MONTH_YEAR', None) + print('__________________reporting_month_year:', reporting_month_year) + from .utils import year_month_to_year_quarter + print() #print('__________________') #print('_______ record:', record.__dict__) + #print('_______ self.datafile:', self.datafile.__dict__) #print('__________ self.datafile.year:', self.datafile.year) - #print('__________ self.datafile.month:', self.datafile.month) + #print('__________ self.datafile.month:', self.datafile.quarter) # run field validators fields_are_valid, field_errors = self.run_field_validators(record, generate_error) @@ -205,7 +211,7 @@ def parse_and_validate(self, line, generate_error): records = [] for schema in self.schemas: - #schema.datafile = self.datafile + schema.datafile = self.datafile record, is_valid, errors = schema.parse_and_validate(line, generate_error) records.append((record, is_valid, errors)) diff --git a/tdrs-backend/tdpservice/parsers/util.py b/tdrs-backend/tdpservice/parsers/util.py index 53ef27952..04cf8b767 100644 --- a/tdrs-backend/tdpservice/parsers/util.py +++ b/tdrs-backend/tdpservice/parsers/util.py @@ -291,3 +291,23 @@ def transform_to_months(quarter): def month_to_int(month): """Return the integer value of a month.""" return datetime.strptime(month, '%b').strftime('%m') + +def year_month_to_year_quarter(year_month): + """Return the year and quarter from a year_month string.""" + def get_quarter_from_month(month): + """Return the quarter from a month.""" + if month in ["01", "02", "03"]: + return "Q1" + elif month in ["04", "05", "06"]: + return "Q2" + elif month in ["07", "08", "09"]: + return "Q3" + elif month in ["10", "11", "12"]: + return "Q4" + else: + raise ValueError("Invalid month value.") + + year = year_month[:4] + month = year_month[4:] + quarter = get_quarter_from_month(month) + return year, quarter From 909a39cb07b827036dd35acdf7d464ec8b66c686 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Tue, 23 Jan 2024 09:10:04 -0500 Subject: [PATCH 021/149] correct .util --- tdrs-backend/tdpservice/parsers/row_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tdrs-backend/tdpservice/parsers/row_schema.py b/tdrs-backend/tdpservice/parsers/row_schema.py index de3732cd2..5d7f79852 100644 --- a/tdrs-backend/tdpservice/parsers/row_schema.py +++ b/tdrs-backend/tdpservice/parsers/row_schema.py @@ -62,7 +62,7 @@ def parse_and_validate(self, line, generate_error): datafile_year = self.datafile.year reporting_month_year = getattr(record, 'RPT_MONTH_YEAR', None) print('__________________reporting_month_year:', reporting_month_year) - from .utils import year_month_to_year_quarter + from .util import year_month_to_year_quarter print() #print('__________________') #print('_______ record:', record.__dict__) From 44b04099ea548979dd1d92bdfdadb99516119d68 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Mon, 29 Jan 2024 12:37:29 -0500 Subject: [PATCH 022/149] tests failing but the preparsing error works --- tdrs-backend/tdpservice/parsers/aggregates.py | 6 +- tdrs-backend/tdpservice/parsers/parse.py | 5 +- tdrs-backend/tdpservice/parsers/row_schema.py | 15 +- .../tdpservice/parsers/schema_defs/tanf/t1.py | 1 + .../tdpservice/parsers/schema_defs/util.py | 145 ++++++++++++++++++ .../tdpservice/parsers/test/test_parse.py | 5 +- tdrs-backend/tdpservice/parsers/util.py | 141 ----------------- tdrs-backend/tdpservice/parsers/validators.py | 57 ++++++- 8 files changed, 210 insertions(+), 165 deletions(-) create mode 100644 tdrs-backend/tdpservice/parsers/schema_defs/util.py diff --git a/tdrs-backend/tdpservice/parsers/aggregates.py b/tdrs-backend/tdpservice/parsers/aggregates.py index 8f27a7445..b327eab50 100644 --- a/tdrs-backend/tdpservice/parsers/aggregates.py +++ b/tdrs-backend/tdpservice/parsers/aggregates.py @@ -1,8 +1,10 @@ """Aggregate methods for the parsers.""" from .row_schema import SchemaManager from .models import ParserError -from .util import month_to_int, get_program_models, get_text_from_df, \ - transform_to_months, fiscal_to_calendar, get_prog_from_section +from .util import month_to_int, get_prog_from_section, \ + transform_to_months, fiscal_to_calendar + +from .schema_defs.util import get_program_models, get_text_from_df def case_aggregates_by_month(df, dfs_status): diff --git a/tdrs-backend/tdpservice/parsers/parse.py b/tdrs-backend/tdpservice/parsers/parse.py index e0800ee32..89ace129c 100644 --- a/tdrs-backend/tdpservice/parsers/parse.py +++ b/tdrs-backend/tdpservice/parsers/parse.py @@ -6,6 +6,7 @@ import logging from .models import ParserErrorCategoryChoices, ParserError from . import schema_defs, validators, util, row_schema +from .schema_defs.util import get_section_reference, get_program_model logger = logging.getLogger(__name__) @@ -58,7 +59,7 @@ def parse_datafile(datafile): section_is_valid, section_error = validators.validate_header_section_matches_submission( datafile, - util.get_section_reference(program_type, section), + get_section_reference(program_type, section), util.make_generate_parser_error(datafile, 1) ) @@ -297,4 +298,4 @@ def manager_parse_line(line, schema_manager, generate_error, is_encrypted=False) def get_schema_manager(line, section, program_type): """Return the appropriate schema for the line.""" line_type = line[0:2] - return util.get_program_model(program_type, section, line_type) + return get_program_model(program_type, section, line_type) diff --git a/tdrs-backend/tdpservice/parsers/row_schema.py b/tdrs-backend/tdpservice/parsers/row_schema.py index 5d7f79852..44d47cf83 100644 --- a/tdrs-backend/tdpservice/parsers/row_schema.py +++ b/tdrs-backend/tdpservice/parsers/row_schema.py @@ -56,19 +56,6 @@ def parse_and_validate(self, line, generate_error): # parse line to model record = self.parse_line(line) - # parsing field values - if self.document: - datafile_quarter = self.datafile.quarter - datafile_year = self.datafile.year - reporting_month_year = getattr(record, 'RPT_MONTH_YEAR', None) - print('__________________reporting_month_year:', reporting_month_year) - from .util import year_month_to_year_quarter - print() - #print('__________________') - #print('_______ record:', record.__dict__) - #print('_______ self.datafile:', self.datafile.__dict__) - #print('__________ self.datafile.year:', self.datafile.year) - #print('__________ self.datafile.month:', self.datafile.quarter) # run field validators fields_are_valid, field_errors = self.run_field_validators(record, generate_error) @@ -87,7 +74,7 @@ def run_preparsing_validators(self, line, generate_error): errors = [] for validator in self.preparsing_validators: - validator_is_valid, validator_error = validator(line) + validator_is_valid, validator_error = validator(line, self) is_valid = False if not validator_is_valid else is_valid if validator_error and not self.quiet_preparser_errors: diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t1.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t1.py index 84e86eb7e..80c809dee 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t1.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t1.py @@ -12,6 +12,7 @@ document=TANF_T1DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(156), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.if_then_validator( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/util.py b/tdrs-backend/tdpservice/parsers/schema_defs/util.py new file mode 100644 index 000000000..964bef8a3 --- /dev/null +++ b/tdrs-backend/tdpservice/parsers/schema_defs/util.py @@ -0,0 +1,145 @@ +from .. import schema_defs +from tdpservice.data_files.models import DataFile + +import logging + +logger = logging.getLogger(__name__) + +def get_schema_options(program, section, query=None, model=None, model_name=None): + """Centralized function to return the appropriate schema for a given program, section, and query. + + TODO: need to rework this docstring as it is outdated hence the weird ';;' for some of them. + + @param program: the abbreviated program type (.e.g, 'TAN') + @param section: the section of the file (.e.g, 'A');; or ACTIVE_CASE_DATA + @param query: the query for section_names (.e.g, 'section', 'models', etc.) + @return: the appropriate references (e.g., ACTIVE_CASE_DATA or {t1,t2,t3}) ;; returning 'A' + """ + schema_options = { + 'TAN': { + 'A': { + 'section': DataFile.Section.ACTIVE_CASE_DATA, + 'models': { + 'T1': schema_defs.tanf.t1, + 'T2': schema_defs.tanf.t2, + 'T3': schema_defs.tanf.t3, + } + }, + 'C': { + 'section': DataFile.Section.CLOSED_CASE_DATA, + 'models': { + 'T4': schema_defs.tanf.t4, + 'T5': schema_defs.tanf.t5, + } + }, + 'G': { + 'section': DataFile.Section.AGGREGATE_DATA, + 'models': { + 'T6': schema_defs.tanf.t6, + } + }, + 'S': { + 'section': DataFile.Section.STRATUM_DATA, + 'models': { + 'T7': schema_defs.tanf.t7, + } + } + }, + 'SSP': { + 'A': { + 'section': DataFile.Section.SSP_ACTIVE_CASE_DATA, + 'models': { + 'M1': schema_defs.ssp.m1, + 'M2': schema_defs.ssp.m2, + 'M3': schema_defs.ssp.m3, + } + }, + 'C': { + 'section': DataFile.Section.SSP_CLOSED_CASE_DATA, + 'models': { + 'M4': schema_defs.ssp.m4, + 'M5': schema_defs.ssp.m5, + } + }, + 'G': { + 'section': DataFile.Section.SSP_AGGREGATE_DATA, + 'models': { + 'M6': schema_defs.ssp.m6, + } + }, + 'S': { + 'section': DataFile.Section.SSP_STRATUM_DATA, + 'models': { + 'M7': schema_defs.ssp.m7, + } + } + }, + 'Tribal TAN': { + 'A': { + 'section': DataFile.Section.TRIBAL_ACTIVE_CASE_DATA, + 'models': { + 'T1': schema_defs.tribal_tanf.t1, + 'T2': schema_defs.tribal_tanf.t2, + 'T3': schema_defs.tribal_tanf.t3, + } + }, + 'C': { + 'section': DataFile.Section.TRIBAL_CLOSED_CASE_DATA, + 'models': { + 'T4': schema_defs.tribal_tanf.t4, + 'T5': schema_defs.tribal_tanf.t5, + } + }, + 'G': { + 'section': DataFile.Section.TRIBAL_AGGREGATE_DATA, + 'models': { + 'T6': schema_defs.tribal_tanf.t6, + } + }, + 'S': { + 'section': DataFile.Section.TRIBAL_STRATUM_DATA, + 'models': { + 'T7': schema_defs.tribal_tanf.t7, + } + }, + }, + } + + if query == "text": + for prog_name, prog_dict in schema_options.items(): + for sect, val in prog_dict.items(): + if val['section'] == section: + return {'program_type': prog_name, 'section': sect} + raise ValueError("Model not found in schema_defs") + elif query == "section": + return schema_options.get(program, {}).get(section, None)[query] + elif query == "models": + links = schema_options.get(program, {}).get(section, None) + + # if query is not chosen or wrong input, return all options + # query = 'models', model = 'T1' + models = links.get(query, links) + + if model_name is None: + return models + elif model_name not in models.keys(): + logger.debug(f"Model {model_name} not found in schema_defs") + return [] # intentionally trigger the error_msg for unknown record type + else: + return models.get(model_name, models) + +def get_program_models(str_prog, str_section): + """Return the models dict for a given program and section.""" + return get_schema_options(program=str_prog, section=str_section, query='models') + +def get_program_model(str_prog, str_section, str_model): + """Return singular model for a given program, section, and name.""" + return get_schema_options(program=str_prog, section=str_section, query='models', model_name=str_model) + +def get_section_reference(str_prog, str_section): + """Return the named section reference for a given program and section.""" + return get_schema_options(program=str_prog, section=str_section, query='section') + +def get_text_from_df(df): + """Return the short-hand text for program, section for a given datafile.""" + return get_schema_options("", section=df.section, query='text') \ No newline at end of file diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index ffed26422..0646ceb89 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -12,6 +12,7 @@ from .factories import DataFileSummaryFactory from tdpservice.data_files.models import DataFile from .. import schema_defs, aggregates, util +from ..schema_defs.util import get_section_reference, get_program_models import logging @@ -847,7 +848,7 @@ def test_get_schema_options(dfs): assert schema == schema_defs.tanf.t1 # get model - models = util.get_program_models('TAN', 'A') + models = get_program_models('TAN', 'A') assert models == { 'T1': schema_defs.tanf.t1, 'T2': schema_defs.tanf.t2, @@ -857,7 +858,7 @@ def test_get_schema_options(dfs): model = util.get_program_model('TAN', 'A', 'T1') assert model == schema_defs.tanf.t1 # get section - section = util.get_section_reference('TAN', 'C') + section = get_section_reference('TAN', 'C') assert section == DataFile.Section.CLOSED_CASE_DATA # from datafile: diff --git a/tdrs-backend/tdpservice/parsers/util.py b/tdrs-backend/tdpservice/parsers/util.py index 04cf8b767..593c3b6de 100644 --- a/tdrs-backend/tdpservice/parsers/util.py +++ b/tdrs-backend/tdpservice/parsers/util.py @@ -1,7 +1,6 @@ """Utility file for functions shared between all parsers even preparser.""" from .models import ParserError from django.contrib.contenttypes.models import ContentType -from . import schema_defs from tdpservice.data_files.models import DataFile from datetime import datetime from pathlib import Path @@ -92,131 +91,6 @@ def contains_encrypted_indicator(line, encryption_field): return encryption_field.parse_value(line) == "E" return False - -def get_schema_options(program, section, query=None, model=None, model_name=None): - """Centralized function to return the appropriate schema for a given program, section, and query. - - TODO: need to rework this docstring as it is outdated hence the weird ';;' for some of them. - - @param program: the abbreviated program type (.e.g, 'TAN') - @param section: the section of the file (.e.g, 'A');; or ACTIVE_CASE_DATA - @param query: the query for section_names (.e.g, 'section', 'models', etc.) - @return: the appropriate references (e.g., ACTIVE_CASE_DATA or {t1,t2,t3}) ;; returning 'A' - """ - schema_options = { - 'TAN': { - 'A': { - 'section': DataFile.Section.ACTIVE_CASE_DATA, - 'models': { - 'T1': schema_defs.tanf.t1, - 'T2': schema_defs.tanf.t2, - 'T3': schema_defs.tanf.t3, - } - }, - 'C': { - 'section': DataFile.Section.CLOSED_CASE_DATA, - 'models': { - 'T4': schema_defs.tanf.t4, - 'T5': schema_defs.tanf.t5, - } - }, - 'G': { - 'section': DataFile.Section.AGGREGATE_DATA, - 'models': { - 'T6': schema_defs.tanf.t6, - } - }, - 'S': { - 'section': DataFile.Section.STRATUM_DATA, - 'models': { - 'T7': schema_defs.tanf.t7, - } - } - }, - 'SSP': { - 'A': { - 'section': DataFile.Section.SSP_ACTIVE_CASE_DATA, - 'models': { - 'M1': schema_defs.ssp.m1, - 'M2': schema_defs.ssp.m2, - 'M3': schema_defs.ssp.m3, - } - }, - 'C': { - 'section': DataFile.Section.SSP_CLOSED_CASE_DATA, - 'models': { - 'M4': schema_defs.ssp.m4, - 'M5': schema_defs.ssp.m5, - } - }, - 'G': { - 'section': DataFile.Section.SSP_AGGREGATE_DATA, - 'models': { - 'M6': schema_defs.ssp.m6, - } - }, - 'S': { - 'section': DataFile.Section.SSP_STRATUM_DATA, - 'models': { - 'M7': schema_defs.ssp.m7, - } - } - }, - 'Tribal TAN': { - 'A': { - 'section': DataFile.Section.TRIBAL_ACTIVE_CASE_DATA, - 'models': { - 'T1': schema_defs.tribal_tanf.t1, - 'T2': schema_defs.tribal_tanf.t2, - 'T3': schema_defs.tribal_tanf.t3, - } - }, - 'C': { - 'section': DataFile.Section.TRIBAL_CLOSED_CASE_DATA, - 'models': { - 'T4': schema_defs.tribal_tanf.t4, - 'T5': schema_defs.tribal_tanf.t5, - } - }, - 'G': { - 'section': DataFile.Section.TRIBAL_AGGREGATE_DATA, - 'models': { - 'T6': schema_defs.tribal_tanf.t6, - } - }, - 'S': { - 'section': DataFile.Section.TRIBAL_STRATUM_DATA, - 'models': { - 'T7': schema_defs.tribal_tanf.t7, - } - }, - }, - } - - if query == "text": - for prog_name, prog_dict in schema_options.items(): - for sect, val in prog_dict.items(): - if val['section'] == section: - return {'program_type': prog_name, 'section': sect} - raise ValueError("Model not found in schema_defs") - elif query == "section": - return schema_options.get(program, {}).get(section, None)[query] - elif query == "models": - links = schema_options.get(program, {}).get(section, None) - - # if query is not chosen or wrong input, return all options - # query = 'models', model = 'T1' - models = links.get(query, links) - - if model_name is None: - return models - elif model_name not in models.keys(): - logger.debug(f"Model {model_name} not found in schema_defs") - return [] # intentionally trigger the error_msg for unknown record type - else: - return models.get(model_name, models) - - ''' text -> section YES text -> models{} YES @@ -230,21 +104,6 @@ def get_schema_options(program, section, query=None, model=None, model_name=None text**: input string from the header/file ''' -def get_program_models(str_prog, str_section): - """Return the models dict for a given program and section.""" - return get_schema_options(program=str_prog, section=str_section, query='models') - -def get_program_model(str_prog, str_section, str_model): - """Return singular model for a given program, section, and name.""" - return get_schema_options(program=str_prog, section=str_section, query='models', model_name=str_model) - -def get_section_reference(str_prog, str_section): - """Return the named section reference for a given program and section.""" - return get_schema_options(program=str_prog, section=str_section, query='section') - -def get_text_from_df(df): - """Return the short-hand text for program, section for a given datafile.""" - return get_schema_options("", section=df.section, query='text') def get_prog_from_section(str_section): """Return the program type for a given section.""" diff --git a/tdrs-backend/tdpservice/parsers/validators.py b/tdrs-backend/tdpservice/parsers/validators.py index b62387273..d5890bfe0 100644 --- a/tdrs-backend/tdpservice/parsers/validators.py +++ b/tdrs-backend/tdpservice/parsers/validators.py @@ -1,7 +1,7 @@ """Generic parser validator functions for use in schema definitions.""" from .models import ParserErrorCategoryChoices -from .util import fiscal_to_calendar +from .util import fiscal_to_calendar, year_month_to_year_quarter from datetime import date import logging @@ -23,10 +23,25 @@ def value_is_empty(value, length, extra_vals={}): # higher order validator func +class change_func_str: + class FNMagic: + def __init__(self,fn,fn_name): + self.fn = fn + self.fn_name = fn_name + def __call__(self,*args,**kwargs): + return self.fn(*args,**kwargs) + def __str__(self): + return self.fn_name + def __init__(self,name): + self.fn_name = name + def __call__(self,fn): + return self.FNMagic(fn,self.fn_name) def make_validator(validator_func, error_func): """Return a function accepting a value input and returning (bool, string) to represent validation state.""" - def validator(value): + + #@change_func_str(validator_func.__str__()) + def validator(value, instance=None): try: if validator_func(value): return (True, None) @@ -157,6 +172,40 @@ def sumIsEqualFunc(value): return lambda value: sumIsEqualFunc(value) +""" +def field_year_month_with_header_year_quarter(): + def validate_reporting_month_year_fields_with_header(line, df_quarter, df_year): + + print('++++++++++++++++++', f"{line}") + + # get reporting month year from header + field_year, field_quarter = year_month_to_year_quarter(f"{field_month_year}") + file_calendar_year, file_calendar_qtr = fiscal_to_calendar(df_year, f"{df_quarter}") + return (True, None) if str(file_calendar_year) == str(field_year) and file_calendar_qtr == field_quarter else ( + False, f"Reporting month year {field_month_year} does not match file reporting year:{df_year}, quarter:{df_quarter}.", + ) + + return lambda value, df_quarter, df_year: validate_reporting_month_year_fields_with_header(value, df_quarter, df_year) +""" + +def field_year_month_with_header_year_quarter(): + def validate_reporting_month_year_fields_with_header(line, row_schema_instance): + + field_month_year = row_schema_instance.get_field_values_by_names(line, ['RPT_MONTH_YEAR']).get('RPT_MONTH_YEAR') + df_quarter = row_schema_instance.datafile.quarter + df_year = row_schema_instance.datafile.year + + # get reporting month year from header + field_year, field_quarter = year_month_to_year_quarter(f"{field_month_year}") + file_calendar_year, file_calendar_qtr = fiscal_to_calendar(df_year, f"{df_quarter}") + print('_______________str(file_calendar_year) == str(field_year):', str(file_calendar_year) == str(field_year)) + print('_______________file_calendar_qtr == field_quarter:', file_calendar_qtr == field_quarter) + return (True, None) if str(file_calendar_year) == str(field_year) and file_calendar_qtr == field_quarter else ( + False, f"Reporting month year {field_month_year} does not match file reporting year:{df_year}, quarter:{df_quarter}.", + ) + + return lambda value, row_schema_instance: validate_reporting_month_year_fields_with_header(value, row_schema_instance) + def sumIsLarger(fields, val): """Validate that the sum of the fields is larger than val.""" @@ -240,8 +289,8 @@ def contains(substring): def startsWith(substring): """Validate that string value starts with the given substring param.""" return make_validator( - lambda value: value.startswith(substring), - lambda value: f"{value} does not start with {substring}.", + lambda value, instance=None: value.startswith(substring), + lambda value, instance=None: f"{value} does not start with {substring}.", ) From d21c65061acc90374e9a0d771214ec2b59628bd4 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Mon, 29 Jan 2024 13:54:28 -0500 Subject: [PATCH 023/149] corrected tests, need to stop parsing if pre-validator error --- tdrs-backend/tdpservice/parsers/test/test_parse.py | 7 +++++-- tdrs-backend/tdpservice/parsers/test/test_util.py | 4 ++-- tdrs-backend/tdpservice/parsers/util.py | 2 +- tdrs-backend/tdpservice/parsers/validators.py | 2 -- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index 0646ceb89..2338a5552 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -12,7 +12,7 @@ from .factories import DataFileSummaryFactory from tdpservice.data_files.models import DataFile from .. import schema_defs, aggregates, util -from ..schema_defs.util import get_section_reference, get_program_models +from ..schema_defs.util import get_section_reference, get_program_models, get_program_model import logging @@ -324,8 +324,11 @@ def test_parse_bad_trailer_file(bad_trailer_file, dfs): dfs.save() errors = parse.parse_datafile(bad_trailer_file) + parser_errors = ParserError.objects.filter(file=bad_trailer_file) + for i in parser_errors: + print('================', i.__dict__) assert parser_errors.count() == 2 trailer_error = parser_errors.get(row_number=3) @@ -855,7 +858,7 @@ def test_get_schema_options(dfs): 'T3': schema_defs.tanf.t3, } - model = util.get_program_model('TAN', 'A', 'T1') + model = get_program_model('TAN', 'A', 'T1') assert model == schema_defs.tanf.t1 # get section section = get_section_reference('TAN', 'C') diff --git a/tdrs-backend/tdpservice/parsers/test/test_util.py b/tdrs-backend/tdpservice/parsers/test/test_util.py index b0453494a..dbf0b975a 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_util.py +++ b/tdrs-backend/tdpservice/parsers/test/test_util.py @@ -8,12 +8,12 @@ def passing_validator(): """Fake validator that always returns valid.""" - return lambda _: (True, None) + return lambda _, instance=None: (True, None) def failing_validator(): """Fake validator that always returns invalid.""" - return lambda _: (False, 'Value is not valid.') + return lambda _, instance=None: (False, 'Value is not valid.') def passing_postparsing_validator(): """Fake validator that always returns valid.""" diff --git a/tdrs-backend/tdpservice/parsers/util.py b/tdrs-backend/tdpservice/parsers/util.py index 593c3b6de..158bf9e97 100644 --- a/tdrs-backend/tdpservice/parsers/util.py +++ b/tdrs-backend/tdpservice/parsers/util.py @@ -164,7 +164,7 @@ def get_quarter_from_month(month): elif month in ["10", "11", "12"]: return "Q4" else: - raise ValueError("Invalid month value.") + return "Invalid month value." year = year_month[:4] month = year_month[4:] diff --git a/tdrs-backend/tdpservice/parsers/validators.py b/tdrs-backend/tdpservice/parsers/validators.py index d5890bfe0..022c9ad6e 100644 --- a/tdrs-backend/tdpservice/parsers/validators.py +++ b/tdrs-backend/tdpservice/parsers/validators.py @@ -198,8 +198,6 @@ def validate_reporting_month_year_fields_with_header(line, row_schema_instance): # get reporting month year from header field_year, field_quarter = year_month_to_year_quarter(f"{field_month_year}") file_calendar_year, file_calendar_qtr = fiscal_to_calendar(df_year, f"{df_quarter}") - print('_______________str(file_calendar_year) == str(field_year):', str(file_calendar_year) == str(field_year)) - print('_______________file_calendar_qtr == field_quarter:', file_calendar_qtr == field_quarter) return (True, None) if str(file_calendar_year) == str(field_year) and file_calendar_qtr == field_quarter else ( False, f"Reporting month year {field_month_year} does not match file reporting year:{df_year}, quarter:{df_quarter}.", ) From 357cfb736ec8f475a95cd8c01279468d491e8fbc Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Tue, 30 Jan 2024 12:10:08 -0500 Subject: [PATCH 024/149] solving failing tests --- .../tdpservice/parsers/test/test_parse.py | 64 +++++++++++-------- tdrs-backend/tdpservice/parsers/validators.py | 15 ----- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index 2338a5552..4fbe5893b 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -327,9 +327,7 @@ def test_parse_bad_trailer_file(bad_trailer_file, dfs): parser_errors = ParserError.objects.filter(file=bad_trailer_file) - for i in parser_errors: - print('================', i.__dict__) - assert parser_errors.count() == 2 + assert parser_errors.count() == 3 trailer_error = parser_errors.get(row_number=3) assert trailer_error.error_type == ParserErrorCategoryChoices.PRE_CHECK @@ -337,16 +335,21 @@ def test_parse_bad_trailer_file(bad_trailer_file, dfs): assert trailer_error.content_type is None assert trailer_error.object_id is None - row_error = parser_errors.get(row_number=2) - assert row_error.error_type == ParserErrorCategoryChoices.PRE_CHECK - assert row_error.error_message == 'Value length 7 does not match 156.' - assert row_error.content_type is None - assert row_error.object_id is None - - assert errors == { - 'trailer': [trailer_error], - "2_0": [row_error] - } + row_errors = parser_errors.filter(row_number=2) + row_errors_list = [] + for row_error in row_errors: + row_errors_list.append(row_error) + assert row_error.error_type == ParserErrorCategoryChoices.PRE_CHECK + assert trailer_error.error_message in [ + 'Trailer length is 11 but must be 23 characters.', + 'Reporting month year None does not match file reporting year:2021, quarter:Q1.'] + assert row_error.content_type is None + assert row_error.object_id is None + + assert errors['trailer'] == [trailer_error] + + for error_2_0 in errors["2_0"]: + assert error_2_0 in row_errors_list @pytest.fixture @@ -363,7 +366,7 @@ def test_parse_bad_trailer_file2(bad_trailer_file_2): errors = parse.parse_datafile(bad_trailer_file_2) parser_errors = ParserError.objects.filter(file=bad_trailer_file_2) - assert parser_errors.count() == 4 + assert parser_errors.count() == 5 trailer_errors = parser_errors.filter(row_number=3).order_by('id') @@ -385,17 +388,28 @@ def test_parse_bad_trailer_file2(bad_trailer_file_2): assert row_2_error.content_type is None assert row_2_error.object_id is None - row_3_error = trailer_errors[2] - assert row_3_error.error_type == ParserErrorCategoryChoices.PRE_CHECK - assert row_3_error.error_message == 'Value length 7 does not match 156.' - assert row_3_error.content_type is None - assert row_3_error.object_id is None - - assert errors == { - "2_0": [row_2_error], - "3_0": [row_3_error], - "trailer": [trailer_error_1, trailer_error_2], - } + row_3_errors = parser_errors.filter(row_number=3) + #row_3_error = trailer_errors[2] + row_3_error_list = [] + for row_3_error in row_3_errors: + row_3_error_list.append(row_3_error) + assert row_3_error.error_type == ParserErrorCategoryChoices.PRE_CHECK + assert row_3_error.error_message in [ + 'Value length 7 does not match 156.', + 'Reporting month year None does not match file reporting year:2021, quarter:Q1.', + 'T1trash does not start with TRAILER.', + 'Trailer length is 7 but must be 23 characters.'] + assert row_3_error.content_type is None + assert row_3_error.object_id is None + + errors_2_0 = errors["2_0"] + errors_3_0 = errors["3_0"] + error_trailer = errors["trailer"] + for error_2_0 in errors_2_0: + assert error_2_0 in [row_2_error] + for error_3_0 in errors_3_0: + assert error_3_0 in row_3_error_list + assert error_trailer == [trailer_error_1, trailer_error_2] @pytest.fixture diff --git a/tdrs-backend/tdpservice/parsers/validators.py b/tdrs-backend/tdpservice/parsers/validators.py index 022c9ad6e..31afbcb5c 100644 --- a/tdrs-backend/tdpservice/parsers/validators.py +++ b/tdrs-backend/tdpservice/parsers/validators.py @@ -172,21 +172,6 @@ def sumIsEqualFunc(value): return lambda value: sumIsEqualFunc(value) -""" -def field_year_month_with_header_year_quarter(): - def validate_reporting_month_year_fields_with_header(line, df_quarter, df_year): - - print('++++++++++++++++++', f"{line}") - - # get reporting month year from header - field_year, field_quarter = year_month_to_year_quarter(f"{field_month_year}") - file_calendar_year, file_calendar_qtr = fiscal_to_calendar(df_year, f"{df_quarter}") - return (True, None) if str(file_calendar_year) == str(field_year) and file_calendar_qtr == field_quarter else ( - False, f"Reporting month year {field_month_year} does not match file reporting year:{df_year}, quarter:{df_quarter}.", - ) - - return lambda value, df_quarter, df_year: validate_reporting_month_year_fields_with_header(value, df_quarter, df_year) -""" def field_year_month_with_header_year_quarter(): def validate_reporting_month_year_fields_with_header(line, row_schema_instance): From 3e7dd740c1f6bd6e87dd5fa358eed600e4118e3c Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Fri, 2 Feb 2024 09:55:32 -0500 Subject: [PATCH 025/149] linting --- tdrs-backend/tdpservice/parsers/parse.py | 1 + tdrs-backend/tdpservice/parsers/row_schema.py | 2 -- .../tdpservice/parsers/schema_defs/util.py | 9 ++++++- .../tdpservice/parsers/test/test_parse.py | 12 ++++----- tdrs-backend/tdpservice/parsers/util.py | 5 +--- tdrs-backend/tdpservice/parsers/validators.py | 25 +++++-------------- 6 files changed, 21 insertions(+), 33 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/parse.py b/tdrs-backend/tdpservice/parsers/parse.py index 89ace129c..3d092015b 100644 --- a/tdrs-backend/tdpservice/parsers/parse.py +++ b/tdrs-backend/tdpservice/parsers/parse.py @@ -158,6 +158,7 @@ def rollback_parser_errors(datafile): num_deleted, models = ParserError.objects.filter(file=datafile).delete() logger.debug(f"Deleted {num_deleted} {ParserError}.") + def parse_datafile_lines(datafile, program_type, section, is_encrypted): """Parse lines with appropriate schema and return errors.""" rawfile = datafile.file diff --git a/tdrs-backend/tdpservice/parsers/row_schema.py b/tdrs-backend/tdpservice/parsers/row_schema.py index 44d47cf83..af4393bfa 100644 --- a/tdrs-backend/tdpservice/parsers/row_schema.py +++ b/tdrs-backend/tdpservice/parsers/row_schema.py @@ -41,7 +41,6 @@ def get_all_fields(self): def parse_and_validate(self, line, generate_error): """Run all validation steps in order, and parse the given line into a record.""" - errors = [] # run preparsing validators @@ -56,7 +55,6 @@ def parse_and_validate(self, line, generate_error): # parse line to model record = self.parse_line(line) - # run field validators fields_are_valid, field_errors = self.run_field_validators(record, generate_error) diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/util.py b/tdrs-backend/tdpservice/parsers/schema_defs/util.py index 964bef8a3..5e7d3f9d2 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/util.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/util.py @@ -1,3 +1,5 @@ +"""Utility functions for schema definitions.""" + from .. import schema_defs from tdpservice.data_files.models import DataFile @@ -142,4 +144,9 @@ def get_section_reference(str_prog, str_section): def get_text_from_df(df): """Return the short-hand text for program, section for a given datafile.""" - return get_schema_options("", section=df.section, query='text') \ No newline at end of file + return get_schema_options("", section=df.section, query='text') + +def get_schema(line, section, program_type): + """Return the appropriate schema for the line.""" + line_type = line[0:2] + return get_schema_options(program_type, section, query='models', model_name=line_type) diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index 4fbe5893b..a98d27ab8 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -324,7 +324,6 @@ def test_parse_bad_trailer_file(bad_trailer_file, dfs): dfs.save() errors = parse.parse_datafile(bad_trailer_file) - parser_errors = ParserError.objects.filter(file=bad_trailer_file) assert parser_errors.count() == 3 @@ -347,7 +346,7 @@ def test_parse_bad_trailer_file(bad_trailer_file, dfs): assert row_error.object_id is None assert errors['trailer'] == [trailer_error] - + for error_2_0 in errors["2_0"]: assert error_2_0 in row_errors_list @@ -389,7 +388,6 @@ def test_parse_bad_trailer_file2(bad_trailer_file_2): assert row_2_error.object_id is None row_3_errors = parser_errors.filter(row_number=3) - #row_3_error = trailer_errors[2] row_3_error_list = [] for row_3_error in row_3_errors: row_3_error_list.append(row_3_error) @@ -737,13 +735,13 @@ def test_parse_bad_tfs1_missing_required(bad_tanf_s1__row_missing_required_field file=bad_tanf_s1__row_missing_required_field) assert parser_errors.count() == 4 - error_message = 'RPT_MONTH_YEAR is required but a value was not provided.' + error_message = 'Reporting month year None does not match file reporting year:2021, quarter:Q1.' row_2_error = parser_errors.get(row_number=2, error_message=error_message) - assert row_2_error.error_type == ParserErrorCategoryChoices.FIELD_VALUE + assert row_2_error.error_type == ParserErrorCategoryChoices.PRE_CHECK assert row_2_error.error_message == error_message - assert row_2_error.content_type.model == 'tanf_t1' - assert row_2_error.object_id is not None + assert row_2_error.object_id is None + error_message = 'RPT_MONTH_YEAR is required but a value was not provided.' row_3_error = parser_errors.get(row_number=3, error_message=error_message) assert row_3_error.error_type == ParserErrorCategoryChoices.FIELD_VALUE assert row_3_error.error_message == error_message diff --git a/tdrs-backend/tdpservice/parsers/util.py b/tdrs-backend/tdpservice/parsers/util.py index 158bf9e97..03c5ed673 100644 --- a/tdrs-backend/tdpservice/parsers/util.py +++ b/tdrs-backend/tdpservice/parsers/util.py @@ -91,6 +91,7 @@ def contains_encrypted_indicator(line, encryption_field): return encryption_field.parse_value(line) == "E" return False + ''' text -> section YES text -> models{} YES @@ -118,10 +119,6 @@ def get_prog_from_section(str_section): # TODO: if given a datafile (section), we can reverse back to the program b/c the # section string has "tribal/ssp" in it, then process of elimination we have tanf -def get_schema(line, section, program_type): - """Return the appropriate schema for the line.""" - line_type = line[0:2] - return get_schema_options(program_type, section, query='models', model_name=line_type) def fiscal_to_calendar(year, fiscal_quarter): """Decrement the input quarter text by one.""" diff --git a/tdrs-backend/tdpservice/parsers/validators.py b/tdrs-backend/tdpservice/parsers/validators.py index 31afbcb5c..2027fb0bd 100644 --- a/tdrs-backend/tdpservice/parsers/validators.py +++ b/tdrs-backend/tdpservice/parsers/validators.py @@ -21,26 +21,11 @@ def value_is_empty(value, length, extra_vals={}): return value is None or value in empty_values -# higher order validator func - -class change_func_str: - class FNMagic: - def __init__(self,fn,fn_name): - self.fn = fn - self.fn_name = fn_name - def __call__(self,*args,**kwargs): - return self.fn(*args,**kwargs) - def __str__(self): - return self.fn_name - def __init__(self,name): - self.fn_name = name - def __call__(self,fn): - return self.FNMagic(fn,self.fn_name) +# higher order validator functions def make_validator(validator_func, error_func): """Return a function accepting a value input and returning (bool, string) to represent validation state.""" - #@change_func_str(validator_func.__str__()) def validator(value, instance=None): try: if validator_func(value): @@ -174,7 +159,8 @@ def sumIsEqualFunc(value): def field_year_month_with_header_year_quarter(): - def validate_reporting_month_year_fields_with_header(line, row_schema_instance): + """Validate that the field year and month match the header year and quarter.""" + def validate_reporting_month_year_fields_header(line, row_schema_instance): field_month_year = row_schema_instance.get_field_values_by_names(line, ['RPT_MONTH_YEAR']).get('RPT_MONTH_YEAR') df_quarter = row_schema_instance.datafile.quarter @@ -184,10 +170,11 @@ def validate_reporting_month_year_fields_with_header(line, row_schema_instance): field_year, field_quarter = year_month_to_year_quarter(f"{field_month_year}") file_calendar_year, file_calendar_qtr = fiscal_to_calendar(df_year, f"{df_quarter}") return (True, None) if str(file_calendar_year) == str(field_year) and file_calendar_qtr == field_quarter else ( - False, f"Reporting month year {field_month_year} does not match file reporting year:{df_year}, quarter:{df_quarter}.", + False, f"Reporting month year {field_month_year} " + + f"does not match file reporting year:{df_year}, quarter:{df_quarter}.", ) - return lambda value, row_schema_instance: validate_reporting_month_year_fields_with_header(value, row_schema_instance) + return lambda value, row_schema_instance: validate_reporting_month_year_fields_header(value, row_schema_instance) def sumIsLarger(fields, val): From 328a2665014cd26c1999a1d9ab9eeb11cd67f25c Mon Sep 17 00:00:00 2001 From: Andrew <84722778+andrew-jameson@users.noreply.github.com> Date: Fri, 2 Feb 2024 14:12:00 -0500 Subject: [PATCH 026/149] can we just remove return code? (#2831) Co-authored-by: andrew-jameson --- scripts/zap-scanner.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/zap-scanner.sh b/scripts/zap-scanner.sh index d03259221..d66f12371 100755 --- a/scripts/zap-scanner.sh +++ b/scripts/zap-scanner.sh @@ -199,5 +199,3 @@ if [ "$ENVIRONMENT" = "nightly" ]; then echo "export ZAP_${TARGET}_FAIL_COUNT=$ZAP_FAIL_COUNT" } >> "$BASH_ENV" fi - -exit $ZAP_EXIT From 86e5c85b321d848ca654c15dbf8f841a2586146f Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Sat, 3 Feb 2024 08:12:30 -0500 Subject: [PATCH 027/149] added field validator to all submission --- tdrs-backend/tdpservice/parsers/schema_defs/ssp/m1.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/ssp/m4.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/ssp/m6.py | 3 +++ tdrs-backend/tdpservice/parsers/schema_defs/ssp/m7.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py | 2 ++ tdrs-backend/tdpservice/parsers/schema_defs/tanf/t4.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/tanf/t5.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/tanf/t6.py | 3 +++ tdrs-backend/tdpservice/parsers/schema_defs/tanf/t7.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t1.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py | 2 ++ tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t4.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py | 1 + tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t6.py | 3 +++ tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t7.py | 1 + 20 files changed, 28 insertions(+) diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m1.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m1.py index a7c63e2ab..716ed6eb5 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m1.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m1.py @@ -12,6 +12,7 @@ document=SSP_M1DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(150), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.if_then_validator( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py index 390cf5480..c3e77d530 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py @@ -14,6 +14,7 @@ document=SSP_M2DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(150), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.validate__FAM_AFF__SSN(), diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py index 90ecdcc05..46620ce73 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py @@ -11,6 +11,7 @@ document=SSP_M3DataSubmissionDocument(), preparsing_validators=[ validators.notEmpty(start=19, end=60), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.if_then_validator( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m4.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m4.py index 070a56459..6ebcd1e41 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m4.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m4.py @@ -12,6 +12,7 @@ document=SSP_M4DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(66), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[], fields=[ diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py index c23a69bd5..27dda1b6c 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py @@ -14,6 +14,7 @@ document=SSP_M5DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(66), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.if_then_validator( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m6.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m6.py index 6f7f67d34..2144f50ba 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m6.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m6.py @@ -11,6 +11,7 @@ document=SSP_M6DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(259), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.sumIsEqual( @@ -172,6 +173,7 @@ document=SSP_M6DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(259), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.sumIsEqual( @@ -333,6 +335,7 @@ document=SSP_M6DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(259), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.sumIsEqual( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m7.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m7.py index 24a680b24..2edb161b5 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m7.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m7.py @@ -25,6 +25,7 @@ validators.hasLength(247), validators.notEmpty(0, 7), validators.notEmpty(validator_index, validator_index + 24), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[], fields=[ diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py index f1a79cb65..e11376e85 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py @@ -14,6 +14,7 @@ document=TANF_T2DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(156), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.validate__FAM_AFF__SSN(), diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py index 72067fb76..34380d876 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py @@ -12,6 +12,7 @@ document=TANF_T3DataSubmissionDocument(), preparsing_validators=[ validators.notEmpty(start=19, end=60), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.if_then_validator( @@ -316,6 +317,7 @@ quiet_preparser_errors=True, preparsing_validators=[ validators.notEmpty(start=60, end=101), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.if_then_validator( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t4.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t4.py index 03564c2c4..f01d913c2 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t4.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t4.py @@ -13,6 +13,7 @@ document=TANF_T4DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(71), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[], fields=[ diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t5.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t5.py index afa0d119e..cd9da817d 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t5.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t5.py @@ -14,6 +14,7 @@ document=TANF_T5DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(71), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.if_then_validator( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t6.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t6.py index e800aafc4..fd646d70a 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t6.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t6.py @@ -12,6 +12,7 @@ document=TANF_T6DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(379), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.sumIsEqual( @@ -229,6 +230,7 @@ document=TANF_T6DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(379), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.sumIsEqual( @@ -440,6 +442,7 @@ document=TANF_T6DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(379), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.sumIsEqual( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t7.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t7.py index 80ec21f20..c247c6427 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t7.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t7.py @@ -25,6 +25,7 @@ validators.hasLength(247), validators.notEmpty(0, 7), validators.notEmpty(validator_index, validator_index + 24), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[], fields=[ diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t1.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t1.py index 861b355e6..49b3b7ca2 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t1.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t1.py @@ -12,6 +12,7 @@ document=Tribal_TANF_T1DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(122), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.if_then_validator( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py index fe88a4284..cc3caf2f2 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py @@ -14,6 +14,7 @@ document=Tribal_TANF_T2DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(122), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.validate__FAM_AFF__SSN(), diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py index 59b80fbb5..53b54562d 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py @@ -13,6 +13,7 @@ preparsing_validators=[ validators.notEmpty(start=19, end=60), validators.hasLength(122), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.if_then_validator( @@ -318,6 +319,7 @@ preparsing_validators=[ validators.notEmpty(start=60, end=101), validators.hasLength(122), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.if_then_validator( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t4.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t4.py index 71ac7309c..b71024b65 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t4.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t4.py @@ -12,6 +12,7 @@ document=Tribal_TANF_T4DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(71), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[], fields=[ diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py index a16b2c018..ef1afc1f2 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py @@ -14,6 +14,7 @@ document=Tribal_TANF_T5DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(71), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.if_then_validator( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t6.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t6.py index 2d6d230b8..a1df02b07 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t6.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t6.py @@ -12,6 +12,7 @@ document=Tribal_TANF_T6DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(379), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.sumIsEqual("NUM_APPLICATIONS", ["NUM_APPROVED", "NUM_DENIED"]), @@ -217,6 +218,7 @@ document=Tribal_TANF_T6DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(379), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.sumIsEqual("NUM_APPLICATIONS", ["NUM_APPROVED", "NUM_DENIED"]), @@ -416,6 +418,7 @@ document=Tribal_TANF_T6DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(379), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[ validators.sumIsEqual("NUM_APPLICATIONS", ["NUM_APPROVED", "NUM_DENIED"]), diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t7.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t7.py index 1212f300d..b6ec33538 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t7.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t7.py @@ -25,6 +25,7 @@ validators.hasLength(247), validators.notEmpty(0, 7), validators.notEmpty(validator_index, validator_index + 24), + validators.field_year_month_with_header_year_quarter(), ], postparsing_validators=[], fields=[ From e465eea70d1a8b1f66f03568c21128b588425674 Mon Sep 17 00:00:00 2001 From: Smithh-Co <121890311+Smithh-Co@users.noreply.github.com> Date: Fri, 9 Feb 2024 07:13:23 -0800 Subject: [PATCH 028/149] Create sprint-91-summary.md (#2830) Sprint 91 summary Co-authored-by: Andrew <84722778+andrew-jameson@users.noreply.github.com> --- docs/Sprint-Review/sprint-91-summary.md | 62 +++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/Sprint-Review/sprint-91-summary.md diff --git a/docs/Sprint-Review/sprint-91-summary.md b/docs/Sprint-Review/sprint-91-summary.md new file mode 100644 index 000000000..bfa6372a2 --- /dev/null +++ b/docs/Sprint-Review/sprint-91-summary.md @@ -0,0 +1,62 @@ +# Sprint 91 Summary + +01/17/2024 - 01/30/2024 + +Velocity (Dev): 24 + +## Sprint Goal +* Dev: + * Continue parsing engine development and begin work on enhancement tickets + * #2536 Cat 4 validation + * #1858 Secure OFA staff access to Kibana + * Unblocks #1350 when complete +* DevOps: + * #2790 - Update deployment code to support Kibana and integrate with Standing Elastic instance +* Design: + * Tie up current documentation work + * Continue refinement of research roadmap + + +## Tickets +### Completed/Merged +* [#2751 Resource Card updated with latest coding instructions](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2751) + +### Ready to Merge +* [#2772 Elastic bulk document creation](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2772) +* [#1350 Kibana access from TDP](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/1350) +* [#1858 Spike: Secure Kibana access](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/1858) +* [#2711 Catch report month / year mismatches](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2711) + + + + +### Submitted (QASP Review, OCIO Review) +* [#2790 Kibana Deployment](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2790) +* [#2681 Section 1 Validation clean-up](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2681) + + + +### Closed (not merged) +* N/A + + +--- + +## Moved to Next Sprint (In Progress, Blocked, Raft Review) +### In Progress +* [#2646 - Populate data file summary case aggregates differently per section](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2646) +* [#2820 [bug] Uncaught exception re: parsing error preventing feedback report generation](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2820) +* [#2768 Fix production OWASP scan reporting](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2768) +* [#2799 Generate error mismatching field rpt_month_year w/ header](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2799) +* [#2781 As a developer, I want to have documentation on django migration best practices](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2781) + + +### Blocked +* N/A + +### Raft Review +* [#2536 [spike] Cat 4 validation](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2536) +* [#2592 Deploy celery as a separate cloud.gov app](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2592) +* [#2746 As an STT, I need to know if there are issues with the DOBs reported in my data files](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2746) +* [#2813 Reduce dev environment count](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2813) +* [#2729 As a developer, I want to move migration commands in the pipeline to CircleCI](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2729) From 6efa24fcafe013be1c1fa692d38b2650d2ffc8ce Mon Sep 17 00:00:00 2001 From: Eric Lipe Date: Fri, 9 Feb 2024 14:25:52 -0700 Subject: [PATCH 029/149] - Updated validators to match Errors Audit language - Parametrized startsWith --- tdrs-backend/tdpservice/parsers/schema_defs/header.py | 3 ++- tdrs-backend/tdpservice/parsers/schema_defs/trailer.py | 3 ++- tdrs-backend/tdpservice/parsers/validators.py | 8 +++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/header.py b/tdrs-backend/tdpservice/parsers/schema_defs/header.py index a0574ab7b..8fe4e202a 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/header.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/header.py @@ -13,7 +13,8 @@ 23, lambda value, length: f"Header length is {len(value)} but must be {length} characters.", ), - validators.startsWith("HEADER"), + validators.startsWith("HEADER", + lambda value: f"Your file does not begin with a {value} record"), ], postparsing_validators=[], fields=[ diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/trailer.py b/tdrs-backend/tdpservice/parsers/schema_defs/trailer.py index 06a23dcb3..684673dff 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/trailer.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/trailer.py @@ -13,7 +13,8 @@ 23, lambda value, length: f"Trailer length is {len(value)} but must be {length} characters.", ), - validators.startsWith("TRAILER"), + validators.startsWith("TRAILER", + lambda value: f"Your file does not end with a {value} record"), ], postparsing_validators=[], fields=[ diff --git a/tdrs-backend/tdpservice/parsers/validators.py b/tdrs-backend/tdpservice/parsers/validators.py index b62387273..5cdc01bf4 100644 --- a/tdrs-backend/tdpservice/parsers/validators.py +++ b/tdrs-backend/tdpservice/parsers/validators.py @@ -225,7 +225,7 @@ def hasLength(length, error_func=None): lambda value: len(value) == length, lambda value: error_func(value, length) if error_func - else f"Value length {len(value)} does not match {length}.", + else f"Record length is {len(value)} characters but must be {length}.", ) @@ -237,11 +237,13 @@ def contains(substring): ) -def startsWith(substring): +def startsWith(substring, error_func=None): """Validate that string value starts with the given substring param.""" return make_validator( lambda value: value.startswith(substring), - lambda value: f"{value} does not start with {substring}.", + lambda value: error_func(value) + if error_func + else f"{value} does not start with {substring}.", ) From d316686cdff3b96d53737b2e2899ba8375476281 Mon Sep 17 00:00:00 2001 From: Eric Lipe Date: Fri, 9 Feb 2024 15:08:17 -0700 Subject: [PATCH 030/149] - updated tests to turn on trailer errors - updated test error messages --- tdrs-backend/tdpservice/parsers/parse.py | 17 ++++++++++++++--- .../tdpservice/parsers/schema_defs/header.py | 2 +- .../tdpservice/parsers/schema_defs/trailer.py | 2 +- .../tdpservice/parsers/test/test_parse.py | 11 +++++++---- tdrs-backend/tdpservice/parsers/validators.py | 2 +- tdrs-backend/tdpservice/settings/common.py | 2 ++ 6 files changed, 26 insertions(+), 10 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/parse.py b/tdrs-backend/tdpservice/parsers/parse.py index 4994b57eb..3879d25af 100644 --- a/tdrs-backend/tdpservice/parsers/parse.py +++ b/tdrs-backend/tdpservice/parsers/parse.py @@ -1,6 +1,7 @@ """Convert raw uploaded Datafile into a parsed model, and accumulate/return any errors.""" +from django.conf import settings from django.db import DatabaseError import itertools import logging @@ -157,6 +158,15 @@ def rollback_parser_errors(datafile): num_deleted, models = ParserError.objects.filter(file=datafile).delete() logger.debug(f"Deleted {num_deleted} {ParserError}.") +def generate_trailer_errors(trailer_errors, errors, unsaved_parser_errors, num_errors): + """Generate trailer errors if we care to see them.""" + if settings.GENERATE_TRAILER_ERRORS: + errors['trailer'] = trailer_errors + unsaved_parser_errors.update({"trailer": trailer_errors}) + num_errors += len(trailer_errors) + return errors, unsaved_parser_errors, num_errors + + def parse_datafile_lines(datafile, program_type, section, is_encrypted): """Parse lines with appropriate schema and return errors.""" rawfile = datafile.file @@ -192,9 +202,10 @@ def parse_datafile_lines(datafile, program_type, section, is_encrypted): if trailer_errors is not None: logger.debug(f"{len(trailer_errors)} trailer error(s) detected for file " + f"'{datafile.original_filename}' on line {line_number}.") - errors['trailer'] = trailer_errors - unsaved_parser_errors.update({"trailer": trailer_errors}) - num_errors += len(trailer_errors) + errors, unsaved_parser_errors, num_errors = generate_trailer_errors(trailer_errors, + errors, + unsaved_parser_errors, + num_errors) generate_error = util.make_generate_parser_error(datafile, line_number) diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/header.py b/tdrs-backend/tdpservice/parsers/schema_defs/header.py index 8fe4e202a..6ed8faf9c 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/header.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/header.py @@ -14,7 +14,7 @@ lambda value, length: f"Header length is {len(value)} but must be {length} characters.", ), validators.startsWith("HEADER", - lambda value: f"Your file does not begin with a {value} record"), + lambda value: f"Your file does not begin with a {value} record."), ], postparsing_validators=[], fields=[ diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/trailer.py b/tdrs-backend/tdpservice/parsers/schema_defs/trailer.py index 684673dff..8916bf5e8 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/trailer.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/trailer.py @@ -14,7 +14,7 @@ lambda value, length: f"Trailer length is {len(value)} but must be {length} characters.", ), validators.startsWith("TRAILER", - lambda value: f"Your file does not end with a {value} record"), + lambda value: f"Your file does not end with a {value} record."), ], postparsing_validators=[], fields=[ diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index ffed26422..6faf9ca19 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -12,6 +12,9 @@ from .factories import DataFileSummaryFactory from tdpservice.data_files.models import DataFile from .. import schema_defs, aggregates, util +from django.conf import settings + +settings.GENERATE_TRAILER_ERRORS = True import logging @@ -335,7 +338,7 @@ def test_parse_bad_trailer_file(bad_trailer_file, dfs): row_error = parser_errors.get(row_number=2) assert row_error.error_type == ParserErrorCategoryChoices.PRE_CHECK - assert row_error.error_message == 'Value length 7 does not match 156.' + assert row_error.error_message == 'Record length is 7 characters but must be 156.' assert row_error.content_type is None assert row_error.object_id is None @@ -371,19 +374,19 @@ def test_parse_bad_trailer_file2(bad_trailer_file_2): trailer_error_2 = trailer_errors[1] assert trailer_error_2.error_type == ParserErrorCategoryChoices.PRE_CHECK - assert trailer_error_2.error_message == 'T1trash does not start with TRAILER.' + assert trailer_error_2.error_message == "Your file does not end with a TRAILER record." assert trailer_error_2.content_type is None assert trailer_error_2.object_id is None row_2_error = parser_errors.get(row_number=2) assert row_2_error.error_type == ParserErrorCategoryChoices.PRE_CHECK - assert row_2_error.error_message == 'Value length 117 does not match 156.' + assert row_2_error.error_message == 'Record length is 117 characters but must be 156.' assert row_2_error.content_type is None assert row_2_error.object_id is None row_3_error = trailer_errors[2] assert row_3_error.error_type == ParserErrorCategoryChoices.PRE_CHECK - assert row_3_error.error_message == 'Value length 7 does not match 156.' + assert row_3_error.error_message == 'Record length is 7 characters but must be 156.' assert row_3_error.content_type is None assert row_3_error.object_id is None diff --git a/tdrs-backend/tdpservice/parsers/validators.py b/tdrs-backend/tdpservice/parsers/validators.py index 5cdc01bf4..48199a9fd 100644 --- a/tdrs-backend/tdpservice/parsers/validators.py +++ b/tdrs-backend/tdpservice/parsers/validators.py @@ -241,7 +241,7 @@ def startsWith(substring, error_func=None): """Validate that string value starts with the given substring param.""" return make_validator( lambda value: value.startswith(substring), - lambda value: error_func(value) + lambda value: error_func(substring) if error_func else f"{value} does not start with {substring}.", ) diff --git a/tdrs-backend/tdpservice/settings/common.py b/tdrs-backend/tdpservice/settings/common.py index dc4e4c51e..f823c475e 100644 --- a/tdrs-backend/tdpservice/settings/common.py +++ b/tdrs-backend/tdpservice/settings/common.py @@ -473,3 +473,5 @@ class Common(Configuration): } CYPRESS_TOKEN = os.getenv('CYPRESS_TOKEN', None) + + GENERATE_TRAILER_ERRORS = strtobool(os.getenv("GENERATE_TRAILER_ERRORS", "no")) From 0d15d594257a23d2db33e9ac7d6f9bfaf47bf8a4 Mon Sep 17 00:00:00 2001 From: Eric Lipe Date: Fri, 9 Feb 2024 15:19:45 -0700 Subject: [PATCH 031/149] - Fixed test --- tdrs-backend/tdpservice/parsers/test/test_validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tdrs-backend/tdpservice/parsers/test/test_validators.py b/tdrs-backend/tdpservice/parsers/test/test_validators.py index bd3eb88ce..e9df49d51 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_validators.py +++ b/tdrs-backend/tdpservice/parsers/test/test_validators.py @@ -235,7 +235,7 @@ def test_hasLength_returns_invalid(): is_valid, error = validator(value) assert is_valid is False - assert error == 'Value length 7 does not match 22.' + assert error == 'Record length is 7 characters but must be 22.' def test_contains_returns_valid(): From 6b7b7717d588c067cb0a2604f8680bf028490a9a Mon Sep 17 00:00:00 2001 From: Eric Lipe Date: Mon, 12 Feb 2024 08:17:03 -0700 Subject: [PATCH 032/149] - Fix lint errors --- tdrs-backend/tdpservice/parsers/test/test_parse.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index 6faf9ca19..cb669aebd 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -13,11 +13,10 @@ from tdpservice.data_files.models import DataFile from .. import schema_defs, aggregates, util from django.conf import settings - -settings.GENERATE_TRAILER_ERRORS = True - import logging + +settings.GENERATE_TRAILER_ERRORS = True es_logger = logging.getLogger('elasticsearch') es_logger.setLevel(logging.WARNING) From eecad702171d187c6626984b7661254eee1c4cf9 Mon Sep 17 00:00:00 2001 From: raftmsohani <97037188+raftmsohani@users.noreply.github.com> Date: Mon, 12 Feb 2024 14:31:49 -0500 Subject: [PATCH 033/149] 2781-django migration best practices (#2783) * added new markdown for migration best practices * Naming and data migration * added useful commands --------- Co-authored-by: Andrew <84722778+andrew-jameson@users.noreply.github.com> --- .../migration-best-practices.md | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/Technical-Documentation/migration-best-practices.md diff --git a/docs/Technical-Documentation/migration-best-practices.md b/docs/Technical-Documentation/migration-best-practices.md new file mode 100644 index 000000000..1f7fbfcf0 --- /dev/null +++ b/docs/Technical-Documentation/migration-best-practices.md @@ -0,0 +1,80 @@ +# Project migrations and database best practices + +## Indexing + +- If some column is going to be queried repeatedly then, create the database indexes on that column. +- Django migration has built-in support for creating indexes in database. +- If multiple columns are going to be queried together then index_together can be used to create composite indexes. +- Tools such as pgbadger can be used to analyse the query pattern on the production. +- PostgreSQL has support for different kind of indexes which can outperform the performance in various conditions and workloads. Research about it for other databases and apply the best one for the application. + +## Naming + +Migrations have to be named as follows: + +{migration number}_{model_name}_{change being applied} + +E.g: + +```shell +0002_datafilesumary_change_pk_to_uuid.py +``` + + +## Data migration + +It is possible to create/add data in the migration. This is called data migration and is a feature available in Django, however, it has it's own shortcomings and should be avoided. + +## Version control + +All migrations shall be version controlled. Each PR should only include one migration. This ensures better control over the model changes and gives a better option to revert changes back to previous version. + +However, in cloud.gov the database has limited disk space. Since alter/add transactions in one migration need to cache the status in disk and then apply the changes, it is strongly suggested to limit the number of transactions and changes in one migration. + +Considering notes above, we conclude that there is a tradeoff between the number of migrations and the number of changes in one migration. One should be careful with this tradeoff specifically in apps that have larger database tables such as search_indexes. + +## Back up before migration + +Before applying any new migration into production, take a full backup from the DB. + +## Check before migration + +Check django [migration transactions](https://docs.djangoproject.com/en/3.2/topics/migrations/#transactions) details for database and assess the behavior on what will happen in case of server network lost between the server running the migration and db, server went out of memory, server freeze etc behaviors. + +## Useful commands + +### List the existing migrations + +```python +from django.db.migrations.recorder import MigrationRecorder + +existing_migration = MigrationRecorder.Migration.objects.all() +for migration in existing_migration: + print(migration) +``` + +will output lines similar to: + +``` +. +.. +Migration 0007_ssp_m1_ssp_m2_ssp_m3 for search_indexes +Migration 0038_user_access_requested_date for users +Migration 0008_auto_20230522_1850 for search_indexes +Migration 0002_alter_parsererror_error_type for parsers +Migration 0003_auto_20230518_1339 for parsers +Migration 0004_parsererror_object_uuid for parsers +Migration 0005_auto_20230601_1510 for parsers +Migration 0006_alter_parsererror_item_number for parsers +Migration 0009_auto_20230525_1959 for search_indexes +Migration 0010_add_tmp_uuid for search_indexes +Migration 0039_alter_user_options for users +Migration 0011_gen_uuid for search_indexes +Migration 0012_set_uuid_pk for search_indexes +Migration 0013_rename_uuid for search_indexes +Migration 0014_auto_20230707_1952 for search_indexes +Migration 0006_auto_20230726_1448 for parsers +Migration 0015_auto_20230724_1830 for search_indexes +Migration 0016_auto_20230803_1721 for search_indexes +Migration 0006_auto_20230810_1500 for parsers +``` From c9c0c7437d2c6797884bff6ac0b71f0e37fd295f Mon Sep 17 00:00:00 2001 From: Eric Lipe <125676261+elipe17@users.noreply.github.com> Date: Mon, 12 Feb 2024 13:31:30 -0700 Subject: [PATCH 034/149] Proof of Concept TDP Based Kibana Auth (#2775) * Added formating for header and autofit columns * Formatted the headers * added year/month to the columns * Added contants - translation column * added friendly names to T1 and T2 * added friendly name to m1 and m2 * added friendly name to m3 * added friendly_name to t3 * added friendly_name to t4 and t5 * added friendly_name to t7 * correct missing friendly_name * correction on failing tests * addedfriendly name to excel report * linting * linting * linting * delete contants.py * added test for json field in error model * linting * linting * linting * 2599-added friendly name to postparsing validators * refining the validator tests * added returning fields names to validators * added friendly_name to error field * linting * corrections on views/tests * corrections for fields * failing test corrected * failing test corrected * correcting test failures * linting * corrected the excel fiel generator * removed excessive space in validator * linting * listing * added m6 * lint * corrected new line break * refactored validator logic * linting and correction on t1 * friendly_name correction from comments * friendly_name correction * corrected failing test for m5 * refactor the field_json creation DRY * - Added Kibana config * friendly_name corrections * linting and cleaning errors * linting * correction on friendly_names * corrected friendly_name for test_util * correction child care - number of months * fixed a few more typos and some spacing. (#2767) * fixed a few more typos and some spacing. * fixed linting issues * missed a spot. --------- Co-authored-by: George Hudson * - Added basic security to Kibana/Elastic - Added setup container to init elastic users, roles, and passwords * - Remove debug code * - change provider name * - Updating settings to reference environment variables * - Add elastic dependency * - Fix network issue * - Added bulk creation of elastic indices * - Updated schemas to reference model based off of elastic document * - Remove password auth from elastic/kibana * - Remove password auth * - Fix tests * - Fix lint * - remove debug print * Changes for fully local development - Enables direct frontend/backend communication sans Login.gov/Cloud.gov - Drives off new DEVELOPMENT env var - Pre-configures and disables frontend auth functionality - Testing based on new dev user - Install via web: ./manage.py generate_dev_user * Reorganized front end logic on REACT_APP_DEVAUTH env var * Reorganized backend logic on REACT_APP_DEVAUTH env var * - Added proof on concept for tdp based kibana auth * - Fixing type issue * added is_superuser and is_staff attrs to dev user * - Add group check * - Add frontend group check for kibana * - fix lint * - Fix lint errors * - Fix doc strings * - Adding authenticated permission * - Renaming variables to clarify things * - fix lint * Revert "- Remove password auth from elastic/kibana" This reverts commit 522ca381651e005e35701d51b94e6cf4d78c011a. * - Setting up anonymous users with kibana_admin privileges * - Adding password to settings in cloud.gov * - remove incorrect auth - use admin only in frontend and backend * - Add elastic profile * DevAuth feature redesign inspired by Cypress - Initializing frontend w/POST /login/cypress: {devEmail, local-cypress-token} - Changed REACT_APP_DEVAUTH to provide the email of the desired dev user - Modified CustomAuthentication.authenticate to handle both known use cases - Added stt_id=31 to the initial dev user - Disabled ES disk threshold checking for local dev which blocked ES startup - Removed DevAuthentication and other now unnecessary code * Fixed CustomAuthentication.authenticate return val for login.py use case * Fixed CustomAuthentication.authenticate logging for login.py use case * Removed unneeded permissions import * Updates to REACT_APP_DEVAUTH env var settings - Enabled with an email address value - Disabled by default * - debugging env vars * - Testing what settings are used * Revert "- debugging env vars" This reverts commit 900efa879e8cb33f0c62140ed4d8e9a1e1b5496a. * Revert "- Testing what settings are used" This reverts commit 784530e49d584db5bda89714a7de4f56c3ee805e. * - debugging env vars again * - Switching to container networking * Restored support for CustomAuthentication.authenticate username keyword * Modified CustomAuthentication.authenticate comment to satisfy flake8 * commit * asdfgvasd * Revert "Modified CustomAuthentication.authenticate comment to satisfy flake8" This reverts commit 761e4eb253d366ef742dd8caf94b6220ed9e81a1. * Revert "Restored support for CustomAuthentication.authenticate username keyword" This reverts commit 4bf895722e356e79b8bbe3674361b90888b91752. * Revert "Updates to REACT_APP_DEVAUTH env var settings" This reverts commit 7fc2a09353804fb728852e9accc041dff08e44e3. * Revert "Removed unneeded permissions import" This reverts commit c18383fe2bf8352c50dd84d2a84408fef2b71367. * Revert "Fixed CustomAuthentication.authenticate logging for login.py use case" This reverts commit 2b9b46f0e719638b320d0a7bbb2bd87eda97eeef. * Revert "Fixed CustomAuthentication.authenticate return val for login.py use case" This reverts commit 97a0cf6995dc17937f083d5efde1d74866c01ff4. * Revert "DevAuth feature redesign inspired by Cypress" This reverts commit 1497d4ab7549bf674e1f71d8f8f039ec7de363bf. * Revert "commit" This reverts commit a284856c66cb2caaca4b471c2f81fc39421e8f00. * Revert "added is_superuser and is_staff attrs to dev user" This reverts commit 6ffbee8f588f12b7595abd7adcfe15ad7e70d11b. * Revert "Reorganized backend logic on REACT_APP_DEVAUTH env var" This reverts commit 7fd7b4d48cd30a7e2c142ea7f1d85f8df95b80d8. * Revert "Reorganized front end logic on REACT_APP_DEVAUTH env var" This reverts commit 32a46713ae102fa15fdab0fcb66fca99b42eb7e2. * Revert "Changes for fully local development" This reverts commit 556221b310b73bee5e9af32eb4cd603b61a25d02. * asdf * - Adding integration tests for elastic bulk doc creation * Revert "asdf" This reverts commit 26455b48582ca9c6d986377e56475644525f7665. * - fix lint * fasdf * - Added usage of document to tribal * - Updated based on feedback * - Fixing error * - Updating frontend to only allow access to kibana sitemap if the user is Dev or Sys Admin * - fix lint --------- Co-authored-by: Mo Sohani Co-authored-by: raftmsohani <97037188+raftmsohani@users.noreply.github.com> Co-authored-by: George Hudson Co-authored-by: George Hudson Co-authored-by: Thomas Tignor Co-authored-by: Thomas Tignor Co-authored-by: Andrew <84722778+andrew-jameson@users.noreply.github.com> --- .circleci/build-and-test/jobs.yml | 6 +- .circleci/util/commands.yml | 6 + tdrs-backend/docker-compose.yml | 35 ++- tdrs-backend/elastic_setup/Dockerfile | 10 + tdrs-backend/elastic_setup/entrypoint.sh | 110 ++++++++ tdrs-backend/elastic_setup/util.sh | 240 ++++++++++++++++++ tdrs-backend/kibana.yml | 10 + tdrs-backend/tdpservice/settings/common.py | 5 +- tdrs-backend/tdpservice/urls.py | 3 +- .../users/api/authorization_check.py | 22 +- tdrs-backend/tdpservice/users/models.py | 5 + tdrs-frontend/nginx/local/locations.conf | 2 +- .../src/components/Header/Header.jsx | 9 + .../src/components/SiteMap/SiteMap.jsx | 9 + tdrs-frontend/src/selectors/auth.js | 4 + 15 files changed, 467 insertions(+), 9 deletions(-) create mode 100644 tdrs-backend/elastic_setup/Dockerfile create mode 100644 tdrs-backend/elastic_setup/entrypoint.sh create mode 100644 tdrs-backend/elastic_setup/util.sh diff --git a/.circleci/build-and-test/jobs.yml b/.circleci/build-and-test/jobs.yml index 4e32831f8..5e58a99ae 100644 --- a/.circleci/build-and-test/jobs.yml +++ b/.circleci/build-and-test/jobs.yml @@ -4,7 +4,7 @@ steps: - checkout - docker-compose-check - - docker-compose-up-backend + - docker-compose-up-with-elastic-backend - run: name: Run Unit Tests And Create Code Coverage Report command: | @@ -47,7 +47,7 @@ steps: - checkout - docker-compose-check - - docker-compose-up-backend + - docker-compose-up-with-elastic-backend - docker-compose-up-frontend - install-nodejs-machine - disable-npm-audit @@ -61,7 +61,7 @@ wait-for-it --service http://web:8080 --timeout 180 -- echo \"Django is ready\"" - run: name: apply the migrations - command: cd tdrs-backend; docker-compose exec web bash -c "python manage.py makemigrations; python manage.py migrate" + command: cd tdrs-backend; docker-compose exec web bash -c "python manage.py makemigrations; python manage.py migrate" - run: name: Remove existing cypress test users command: cd tdrs-backend; docker-compose exec web python manage.py delete_cypress_users -usernames new-cypress@teamraft.com cypress-admin@teamraft.com diff --git a/.circleci/util/commands.yml b/.circleci/util/commands.yml index ebbdfb7e1..09d175b69 100644 --- a/.circleci/util/commands.yml +++ b/.circleci/util/commands.yml @@ -11,6 +11,12 @@ name: Build and spin-up Django API service command: cd tdrs-backend; docker network create external-net; docker-compose up -d --build + docker-compose-up-with-elastic-backend: + steps: + - run: + name: Build and spin-up Django API service + command: cd tdrs-backend; docker network create external-net; docker-compose --profile elastic_setup up -d --build + cf-check: steps: - run: diff --git a/tdrs-backend/docker-compose.yml b/tdrs-backend/docker-compose.yml index a6624688b..6a09c3944 100644 --- a/tdrs-backend/docker-compose.yml +++ b/tdrs-backend/docker-compose.yml @@ -50,7 +50,7 @@ services: ports: - 5601:5601 environment: - - xpack.security.encryptionKey="something_at_least_32_characters" + - xpack.security.encryptionKey=${KIBANA_ENCRYPTION_KEY:-something_at_least_32_characters} - xpack.security.session.idleTimeout="1h" - xpack.security.session.lifespan="30d" volumes: @@ -58,12 +58,42 @@ services: depends_on: - elastic + # This task only needs to be performed once, during the *initial* startup of + # the stack. Any subsequent run will reset the passwords of existing users to + # the values defined inside the '.env' file, and the built-in roles to their + # default permissions. + # + # By default, it is excluded from the services started by 'docker compose up' + # due to the non-default profile it belongs to. To run it, either provide the + # '--profile=elastic_setup' CLI flag to Compose commands, or "up" the service by name + # such as 'docker compose up elastic_setup'. + elastic_setup: + profiles: + - elastic_setup + build: + context: elastic_setup/ + args: + ELASTIC_VERSION: "7.17.6" + init: true + environment: + ELASTIC_PASSWORD: ${ELASTIC_PASSWORD:-changeme} + KIBANA_SYSTEM_PASSWORD: ${KIBANA_SYSTEM_PASSWORD:-changeme} + OFA_ADMIN_PASSWORD: ${OFA_ADMIN_PASSWORD:-changeme} + ELASTICSEARCH_HOST: ${ELASTICSEARCH_HOST:-elastic} + depends_on: + - elastic + elastic: image: elasticsearch:7.17.6 environment: - discovery.type=single-node - logger.discovery.level=debug - - xpack.security.enabled=false + - xpack.security.enabled=true + - xpack.security.authc.anonymous.username="ofa_admin" + - xpack.security.authc.anonymous.roles="ofa_admin" + - xpack.security.authc.anonymous.authz_exception=true + - ELASTIC_PASSWORD=${ELASTIC_PASSWORD:-changeme} + - KIBANA_SYSTEM_PASSWORD=${KIBANA_SYSTEM_PASSWORD:-changeme} ports: - 9200:9200 - 9300:9300 @@ -101,6 +131,7 @@ services: - CYPRESS_TOKEN - DJANGO_DEBUG - SENDGRID_API_KEY + - BYPASS_KIBANA_AUTH volumes: - .:/tdpapp image: tdp diff --git a/tdrs-backend/elastic_setup/Dockerfile b/tdrs-backend/elastic_setup/Dockerfile new file mode 100644 index 000000000..32e6429f6 --- /dev/null +++ b/tdrs-backend/elastic_setup/Dockerfile @@ -0,0 +1,10 @@ +ARG ELASTIC_VERSION + +FROM docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION} + +COPY . / + +RUN ["chmod", "+x", "/entrypoint.sh"] +RUN ["chmod", "+x", "/util.sh"] + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/tdrs-backend/elastic_setup/entrypoint.sh b/tdrs-backend/elastic_setup/entrypoint.sh new file mode 100644 index 000000000..6073b0540 --- /dev/null +++ b/tdrs-backend/elastic_setup/entrypoint.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +source "${BASH_SOURCE[0]%/*}"/util.sh + + +# -------------------------------------------------------- +# Users declarations + +declare -A users_passwords +users_passwords=( + [kibana_system]="${KIBANA_SYSTEM_PASSWORD:-}" + [ofa_admin]="${OFA_ADMIN_PASSWORD:-}" +) + +declare -A users_roles +users_roles=( + [kibana_system]='kibana_system' + [ofa_admin]='kibana_admin' +) + +# -------------------------------------------------------- +# Roles declarations for custom roles + +declare -A roles_files +roles_files=( + +) + +# -------------------------------------------------------- + + +log 'Waiting for availability of Elasticsearch. This can take several minutes.' + +declare -i exit_code=0 +wait_for_elasticsearch || exit_code=$? + +if ((exit_code)); then + case $exit_code in + 6) + suberr 'Could not resolve host. Is Elasticsearch running?' + ;; + 7) + suberr 'Failed to connect to host. Is Elasticsearch healthy?' + ;; + 28) + suberr 'Timeout connecting to host. Is Elasticsearch healthy?' + ;; + *) + suberr "Connection to Elasticsearch failed. Exit code: ${exit_code}" + ;; + esac + + exit $exit_code +fi + +sublog 'Elasticsearch is running' + +log 'Waiting for initialization of built-in users' + +wait_for_builtin_users || exit_code=$? + +if ((exit_code)); then + suberr 'Timed out waiting for condition' + exit $exit_code +fi + +sublog 'Built-in users were initialized' + +for role in "${!roles_files[@]}"; do + log "Role '$role'" + + declare body_file + body_file="${BASH_SOURCE[0]%/*}/roles/${roles_files[$role]:-}" + if [[ ! -f "${body_file:-}" ]]; then + sublog "No role body found at '${body_file}', skipping" + continue + fi + + sublog 'Creating/updating' + ensure_role "$role" "$(<"${body_file}")" +done + +for user in "${!users_passwords[@]}"; do + log "User '$user'" + if [[ -z "${users_passwords[$user]:-}" ]]; then + sublog 'No password defined, skipping' + continue + fi + + declare -i user_exists=0 + user_exists="$(check_user_exists "$user")" + + if ((user_exists)); then + sublog 'User exists, setting password' + set_user_password "$user" "${users_passwords[$user]}" + else + if [[ -z "${users_roles[$user]:-}" ]]; then + suberr ' No role defined, skipping creation' + continue + fi + + sublog 'User does not exist, creating' + create_user "$user" "${users_passwords[$user]}" "${users_roles[$user]}" + fi +done + +log "Elastic setup completed. Exiting with code: $?" diff --git a/tdrs-backend/elastic_setup/util.sh b/tdrs-backend/elastic_setup/util.sh new file mode 100644 index 000000000..045110249 --- /dev/null +++ b/tdrs-backend/elastic_setup/util.sh @@ -0,0 +1,240 @@ +#!/usr/bin/env bash + +# Log a message. +function log { + echo "[+] $1" +} + +# Log a message at a sub-level. +function sublog { + echo " ⠿ $1" +} + +# Log an error. +function err { + echo "[x] $1" >&2 +} + +# Log an error at a sub-level. +function suberr { + echo " ⠍ $1" >&2 +} + +# Poll the 'elasticsearch' service until it responds with HTTP code 200. +function wait_for_elasticsearch { + local elasticsearch_host="${ELASTICSEARCH_HOST:-elastic}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' "http://${elasticsearch_host}:9200/" ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local output + + # retry for max 300s (60*5s) + for _ in $(seq 1 60); do + local -i exit_code=0 + output="$(curl "${args[@]}")" || exit_code=$? + + if ((exit_code)); then + result=$exit_code + fi + + if [[ "${output: -3}" -eq 200 ]]; then + result=0 + break + fi + + sleep 5 + done + + if ((result)) && [[ "${output: -3}" -ne 000 ]]; then + echo -e "\n${output::-3}" + fi + + return $result +} + +# Poll the Elasticsearch users API until it returns users. +function wait_for_builtin_users { + local elasticsearch_host="${ELASTICSEARCH_HOST:-elastic}" + + local -a args=( '-s' '-D-' '-m15' "http://${elasticsearch_host}:9200/_security/user?pretty" ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + + local line + local -i exit_code + local -i num_users + + # retry for max 30s (30*1s) + for _ in $(seq 1 30); do + num_users=0 + + # read exits with a non-zero code if the last read input doesn't end + # with a newline character. The printf without newline that follows the + # curl command ensures that the final input not only contains curl's + # exit code, but causes read to fail so we can capture the return value. + # Ref. https://unix.stackexchange.com/a/176703/152409 + while IFS= read -r line || ! exit_code="$line"; do + if [[ "$line" =~ _reserved.+true ]]; then + (( num_users++ )) + fi + done < <(curl "${args[@]}"; printf '%s' "$?") + + if ((exit_code)); then + result=$exit_code + fi + + # we expect more than just the 'elastic' user in the result + if (( num_users > 1 )); then + result=0 + break + fi + + sleep 1 + done + + return $result +} + +# Verify that the given Elasticsearch user exists. +function check_user_exists { + local username=$1 + + local elasticsearch_host="${ELASTICSEARCH_HOST:-elastic}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' + "http://${elasticsearch_host}:9200/_security/user/${username}" + ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local -i exists=0 + local output + + output="$(curl "${args[@]}")" + if [[ "${output: -3}" -eq 200 || "${output: -3}" -eq 404 ]]; then + result=0 + fi + if [[ "${output: -3}" -eq 200 ]]; then + exists=1 + fi + + if ((result)); then + echo -e "\n${output::-3}" + else + echo "$exists" + fi + + return $result +} + +# Set password of a given Elasticsearch user. +function set_user_password { + local username=$1 + local password=$2 + + local elasticsearch_host="${ELASTICSEARCH_HOST:-elastic}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' + "http://${elasticsearch_host}:9200/_security/user/${username}/_password" + '-X' 'POST' + '-H' 'Content-Type: application/json' + '-d' "{\"password\" : \"${password}\"}" + ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local output + + output="$(curl "${args[@]}")" + if [[ "${output: -3}" -eq 200 ]]; then + result=0 + fi + + if ((result)); then + echo -e "\n${output::-3}\n" + fi + + return $result +} + +# Create the given Elasticsearch user. +function create_user { + local username=$1 + local password=$2 + local role=$3 + + local elasticsearch_host="${ELASTICSEARCH_HOST:-elastic}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' + "http://${elasticsearch_host}:9200/_security/user/${username}" + '-X' 'POST' + '-H' 'Content-Type: application/json' + '-d' "{\"password\":\"${password}\",\"roles\":[\"${role}\"]}" + ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local output + + output="$(curl "${args[@]}")" + if [[ "${output: -3}" -eq 200 ]]; then + result=0 + fi + + if ((result)); then + echo -e "\n${output::-3}\n" + fi + + return $result +} + +# Ensure that the given Elasticsearch role is up-to-date, create it if required. +function ensure_role { + local name=$1 + local body=$2 + + local elasticsearch_host="${ELASTICSEARCH_HOST:-elastic}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' + "http://${elasticsearch_host}:9200/_security/role/${name}" + '-X' 'POST' + '-H' 'Content-Type: application/json' + '-d' "$body" + ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local output + + output="$(curl "${args[@]}")" + if [[ "${output: -3}" -eq 200 ]]; then + result=0 + fi + + if ((result)); then + echo -e "\n${output::-3}\n" + fi + + return $result +} \ No newline at end of file diff --git a/tdrs-backend/kibana.yml b/tdrs-backend/kibana.yml index dad4335d0..e98d2438d 100644 --- a/tdrs-backend/kibana.yml +++ b/tdrs-backend/kibana.yml @@ -1,2 +1,12 @@ elasticsearch.hosts: ["http://elastic:9200"] server.host: kibana +elasticsearch.username: kibana_system +elasticsearch.password: changeme +xpack.security.authc.providers: + anonymous.anonymous1: + order: 0 + description: "OFA Admin Login" + hint: "" + credentials: + username: "ofa_admin" + password: "changeme" diff --git a/tdrs-backend/tdpservice/settings/common.py b/tdrs-backend/tdpservice/settings/common.py index dc4e4c51e..108586c80 100644 --- a/tdrs-backend/tdpservice/settings/common.py +++ b/tdrs-backend/tdpservice/settings/common.py @@ -465,11 +465,14 @@ class Common(Configuration): } } - # Elastic + # Elastic/Kibana ELASTICSEARCH_DSL = { 'default': { 'hosts': os.getenv('ELASTIC_HOST', 'elastic:9200'), + 'http_auth': ('elastic', os.getenv('ELASTIC_PASSWORD', 'changeme')) }, } + KIBANA_BASE_URL = os.getenv('KIBANA_BASE_URL', 'http://localhost:5601') + BYPASS_KIBANA_AUTH = strtobool(os.getenv("BYPASS_KIBANA_AUTH", "no")) CYPRESS_TOKEN = os.getenv('CYPRESS_TOKEN', None) diff --git a/tdrs-backend/tdpservice/urls.py b/tdrs-backend/tdpservice/urls.py index 26858b356..368314c92 100755 --- a/tdrs-backend/tdpservice/urls.py +++ b/tdrs-backend/tdpservice/urls.py @@ -11,7 +11,7 @@ from rest_framework.permissions import AllowAny -from .users.api.authorization_check import AuthorizationCheck +from .users.api.authorization_check import AuthorizationCheck, KibanaAuthorizationCheck from .users.api.login import TokenAuthorizationLoginDotGov, TokenAuthorizationAMS from .users.api.login import CypressLoginDotGovAuthenticationOverride from .users.api.login_redirect_oidc import LoginRedirectAMS, LoginRedirectLoginDotGov @@ -52,6 +52,7 @@ urlpatterns = [ path("v1/", include(urlpatterns)), path("admin/", admin.site.urls, name="admin"), + path("kibana/", KibanaAuthorizationCheck.as_view(), name="kibana-authorization-check"), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # TODO: Supply `terms_of_service` argument in OpenAPI Info once implemented diff --git a/tdrs-backend/tdpservice/users/api/authorization_check.py b/tdrs-backend/tdpservice/users/api/authorization_check.py index 3ac867be0..76afeecb1 100644 --- a/tdrs-backend/tdpservice/users/api/authorization_check.py +++ b/tdrs-backend/tdpservice/users/api/authorization_check.py @@ -4,10 +4,12 @@ from django.contrib.auth import logout from django.middleware import csrf from django.utils import timezone -from rest_framework.permissions import AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from ..serializers import UserProfileSerializer +from django.http import HttpResponseRedirect +from django.conf import settings logger = logging.getLogger(__name__) @@ -49,3 +51,21 @@ def get(self, request, *args, **kwargs): else: logger.info("Auth check FAIL for user on %s", timezone.now()) return Response({"authenticated": False}) + +class KibanaAuthorizationCheck(APIView): + """Check if user is authorized to view Kibana.""" + + query_string = False + pattern_name = "kibana-authorization-check" + permission_classes = [IsAuthenticated] + + def get(self, request, *args, **kwargs): + """Handle get request and verify user is authorized to access kibana.""" + user = request.user + + user_in_valid_group = user.is_ofa_sys_admin + + if (user.hhs_id is not None and user_in_valid_group) or settings.BYPASS_KIBANA_AUTH: + return HttpResponseRedirect(settings.KIBANA_BASE_URL) + else: + return HttpResponseRedirect(settings.FRONTEND_BASE_URL) diff --git a/tdrs-backend/tdpservice/users/models.py b/tdrs-backend/tdpservice/users/models.py index d0a9c924d..2dd8dd3c1 100644 --- a/tdrs-backend/tdpservice/users/models.py +++ b/tdrs-backend/tdpservice/users/models.py @@ -180,6 +180,11 @@ def is_ocio_staff(self) -> bool: """Return whether or not the user is in the ACF OCIO Group.""" return self.is_in_group("ACF OCIO") + @property + def is_ofa_sys_admin(self) -> bool: + """Return whether or not the user is in the OFA System Admin Group.""" + return self.is_in_group("OFA System Admin") + @property def is_deactivated(self): """Check if the user's account status has been set to 'Deactivated'.""" diff --git a/tdrs-frontend/nginx/local/locations.conf b/tdrs-frontend/nginx/local/locations.conf index 2fc38d3ad..154cda557 100644 --- a/tdrs-frontend/nginx/local/locations.conf +++ b/tdrs-frontend/nginx/local/locations.conf @@ -4,7 +4,7 @@ location = /nginx_status { deny all; } -location ~ ^/(v1|admin|static/admin|swagger|redocs) { +location ~ ^/(v1|admin|static/admin|swagger|redocs|kibana) { limit_req zone=limitreqsbyaddr delay=5; proxy_pass http://${BACK_END}:8080$request_uri; proxy_set_header Host $host:3000; diff --git a/tdrs-frontend/src/components/Header/Header.jsx b/tdrs-frontend/src/components/Header/Header.jsx index 2f6c5335b..201cd55bf 100644 --- a/tdrs-frontend/src/components/Header/Header.jsx +++ b/tdrs-frontend/src/components/Header/Header.jsx @@ -7,6 +7,7 @@ import { accountStatusIsApproved, accountIsInReview, accountCanViewAdmin, + accountCanViewKibana, } from '../../selectors/auth' import NavItem from '../NavItem/NavItem' @@ -29,6 +30,7 @@ function Header() { const userAccessRequestPending = useSelector(accountIsInReview) const userAccessRequestApproved = useSelector(accountStatusIsApproved) const userIsAdmin = useSelector(accountCanViewAdmin) + const userIsSysAdmin = useSelector(accountCanViewKibana) const menuRef = useRef() @@ -137,6 +139,13 @@ function Header() { href={`${process.env.REACT_APP_BACKEND_HOST}/admin/`} /> )} + {userIsSysAdmin && ( + + )} )} diff --git a/tdrs-frontend/src/components/SiteMap/SiteMap.jsx b/tdrs-frontend/src/components/SiteMap/SiteMap.jsx index 1df805e7d..5ad40fc4e 100644 --- a/tdrs-frontend/src/components/SiteMap/SiteMap.jsx +++ b/tdrs-frontend/src/components/SiteMap/SiteMap.jsx @@ -3,11 +3,13 @@ import { useSelector } from 'react-redux' import { accountStatusIsApproved, accountCanViewAdmin, + accountCanViewKibana, } from '../../selectors/auth' const SiteMap = ({ user }) => { const userIsApproved = useSelector(accountStatusIsApproved) const userIsAdmin = useSelector(accountCanViewAdmin) + const userIsSysAdmin = useSelector(accountCanViewKibana) return (
@@ -31,6 +33,13 @@ const SiteMap = ({ user }) => { link={`${process.env.REACT_APP_BACKEND_HOST}/admin/`} /> )} + + {userIsSysAdmin && ( + + )}
) } diff --git a/tdrs-frontend/src/selectors/auth.js b/tdrs-frontend/src/selectors/auth.js index b79d2b6b1..ab962e275 100644 --- a/tdrs-frontend/src/selectors/auth.js +++ b/tdrs-frontend/src/selectors/auth.js @@ -59,3 +59,7 @@ export const accountCanViewAdmin = (state) => ['Developer', 'OFA System Admin', 'ACF OCIO', 'OFA Admin'].includes( selectPrimaryUserRole(state)?.name ) + +export const accountCanViewKibana = (state) => + accountStatusIsApproved(state) && + ['Developer', 'OFA System Admin'].includes(selectPrimaryUserRole(state)?.name) From e06fba6f7d4cc5433b0cb01a5f0a132521694bc2 Mon Sep 17 00:00:00 2001 From: Miles Reiter Date: Tue, 13 Feb 2024 14:05:20 -0500 Subject: [PATCH 035/149] Accessibility Guide (#2832) * Delete rafts-accessibility-dos-and-donts.md * Update README.md * Create accessibility-guide.md * Update accessibility-guide.md Cleans up some inline link markup * Update accessibility-guide.md --- docs/Technical-Documentation/README.md | 2 +- .../accessibility-guide.md | 236 ++++++++++++++++++ .../rafts-accessibility-dos-and-donts.md | 53 ---- 3 files changed, 237 insertions(+), 54 deletions(-) create mode 100644 docs/Technical-Documentation/accessibility-guide.md delete mode 100644 docs/Technical-Documentation/rafts-accessibility-dos-and-donts.md diff --git a/docs/Technical-Documentation/README.md b/docs/Technical-Documentation/README.md index 31d6a3214..e6ef1b203 100644 --- a/docs/Technical-Documentation/README.md +++ b/docs/Technical-Documentation/README.md @@ -19,7 +19,7 @@ This directory contains system and architecture documentation including diagrams - [jwt-key-rotation.md](./jwt-key-rotation.md) : Describes the process for rotating JWT keys in Login.gov. - [nexus-repo.md](./nexus-repo.md) : Setup, connection information, and how to use our Nexus Artifact Repository - [openid-connect.md](./openid-connect.md) : Provides an architecture-level view of the OpenID Connect prototocol. -- [rafts-accessibility-dos-and-donts.md](./rafts-accessibility-dos-and-donts.md) : A succint list of UX guidelines for frontend accessibility. +- [accessibility-guide.md](./accessibility-guide.md) : A guide on getting started with accessibility testing tools and TDP-relevant resources. - [remote-development.md](./remote-development.md) : A guide on doing live remote development in Cloud.gov. - [unit-tests.md](./unit-tests.md) : Outlines our unit testing frameworks and how to run these manually. - [user_role_management.md](./user_role_management.md) : Provides an overview of our user management in Django Administrator Console. diff --git a/docs/Technical-Documentation/accessibility-guide.md b/docs/Technical-Documentation/accessibility-guide.md new file mode 100644 index 000000000..ae938ef1f --- /dev/null +++ b/docs/Technical-Documentation/accessibility-guide.md @@ -0,0 +1,236 @@ +# Accessibility Guide + +**Table of Contents:** +- [Background](#Background) +- [Relevant standards](#Relevant-standards) +- [State of a11y in TDP](#State-of-a11y-in-TDP) +- [What to keep in mind when testing](#What-to-keep-in-mind-when-testing) +- [Testing tools](#Testing-tools) +- [Screen reader use and setup](#Screen-reader-use-and-setup) +- [Do's and don'ts when designing](#Dos-and-donts-when-designing) +- [References](#References) + +--- + +## Background +This document has evolved from its initial state of "Helpful a11y stuff" to one intended to serve more as a guided tour of Raft's accessibility (a11y) practice aimed at enabling more testing and broadly more *consideration* of a11y to be shifted left. + +Additionally, this resource will also aim to document the current state of a11y in TDP to track outstanding enhancements or a11y fixes and to help commentate some issues that may be encountered when testing TDP pages for accessibility conformance. While TDP remains highly accessible as a whole there are certain issues we've identified that demand follow-on work to research or correct. There are also a number of false positives that certain a11y testing tools can identify as problematic. + +--- + +## Relevant standards +There are numerous areas of accessibility law that apply to our work ranging from the ADA (Americans with Disabilities Act) which lays out the broad requirement of accessibility in public spaces and systems to Section 508 of the Rehabilitation Act which mandates a specific standard that Federal systems & resources need to adhere to—specifically WCAG (Web Content Accessibility Standards) 2.0 AA. [See more on US accessibility law](https://www.ada.gov/resources/disability-rights-guide/#top). + +WCAG 2.x standards have four categories with which to evaluate accessibility; Perceivable, Operable, Understandable, and Robust. Within all four categories are three levels of conformance, A, AA, AAA; respectively these correspond to the most barebones standards, good baseline standards, and the most specific standards. + +--- + +## State of a11y in TDP +The [errors audit](https://hackmd.io/79rAOVzISbOvaTNv8nSpeA) tracks all outstanding accessibility issues in the TANF Data Portal, its knowledge center, and Django Admin console. + +--- + +## What to keep in mind when testing +The full picture of what makes for *complete* accessibility testing involves every check in Accessibility Insight's Full Assessment tool, thoughtfulness around what makes for a good experience through the lens of various assistive technologies, and real-world usability testing with people experiencing disabilities. This guide isn't going to be able to deliver all that—but it will seek to lay out some illustrative examples and frequently useful questions to pose of an experience when you're testing it for conformance. + +The following checklist is organized via WCAG 2.x's categories. While a few items explicitly involve screen-readers or other assistive technologies, most items should be able to be checked "yes" regardless of your mode of interaction with the webpage (vision & mouse, keyboard only, screenreader, etc...). + + +### Is it Perceivable? +The user must be able to *perceive* all the information being presented. + +- [ ] If there are non-decorative images on the page, do they have alt-text? + - [ ] Does the alt text convey all the relevant information a sighted user would get from the image? +- [ ] Is there a visible focus indicator for every element of the interface as you tab through it? +- [ ] Do all interface items have sufficient contrast against their backgrounds? + - [ ] If information is communicated by color are there also alternatives beyond color for folks who can't see or distinguish the colors? +- [ ] Do related areas of the interface have visual proximity to each other? +- [ ] Are all interface items read correctly by screenreaders? + +### Is it Operable? +The user must be able to *use* all interactive portions of the experience—and navigate to all areas of it. + +- [ ] When navigating the page with the keyboard can you tab to every interactive element? + - [ ] Is the order in which items are focused logical? + - [ ] If there's a disabled element is it correctly marked up with ARIA and read to screen readers? +- [ ] Is there sufficient (read as: generous) time to read and interact with transient elements of the page (e.g. the timeout modal dialog)? +- [ ] Does the navigable experience "shrink" when pop-over content is open? (e.g. Modal dialogs & the opened side navigation) + - [ ] Is content behind the pop-over content shaded out? + - [ ] Is keyboard focus constrained to the pop-over content alone? + +### Is it Understandable? +The user must be able to understand the information being presented by the experience and *how* to operate it. + +- [ ] Is the language used on the page [plain](https://www.plainlanguage.gov/guidelines/) rather than technical or overcomplicated? + - [ ] Are abbreviations defined alongside their first usage? + - [ ] Are unusual words explained? +- [ ] Do interface elements used for navigation appear and behave consistently throughout the experience? +- [ ] Does the experience make it clear when elements or page contexts change? (e.g. When you navigate to a new page or a new piece of content appears) +- [ ] Are there labels and instructions on the page to help prevent errors? + - [ ] When errors appear is it clear what action or form item they relate to? + - [ ] When errors appear do they suggest something for the user to try next to correct or move past them? + +### Is it Robust? +The experience and the content within it need to be *reliable* and play nicely with a range of assistive technologies. + +- [ ] Does this experience work as intended when navigated via Safari and Voiceover? + - [ ] Does it work as intended when tested in other browsers and via other screen readers? +- [ ] Is the page parsed correctly by testing tools? + + +--- + +## Testing tools + + + + +### In-browser tools + +| Extension | Description | Link | +| -------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| **Alt-text tester** | Flags images on a page that are missing alt text, provides an easy way to view alt text for any image that has it. | [Chrome](https://chrome.google.com/webstore/detail/alt-text-tester/koldhcllpbdfcdpfpbldbicbgddglodk?hl=en) | +| **Accessibility Insights** | Great for getting familiar with WCAG. It has both fast-pass assessments and a guided way to do manual testing. Plus—when doing manual testing—deep dive explainers on each test (see the info buttons next to the headings of any given test). | [Chrome](https://chrome.google.com/webstore/detail/accessibility-insights-fo/pbjjkligggfmakdaogkfomddhfmpjeni), [Edge](https://microsoftedge.microsoft.com/addons/detail/ghbhpcookfemncgoinjblecnilppimih) | +| **Axe DevTools** | Great fast-pass scanner, will identify best practice issues as well as WCAG compliance violations. | [Chrome](https://chrome.google.com/webstore/detail/axe-devtools-web-accessib/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Edge](https://microsoftedge.microsoft.com/addons/detail/axe-devtools-web-access/kcenlimkmjjkdfcaleembgmldmnnlfkn) | +| **Accessible Colors** | Web tool tool that will tell you whether two hex codes have sufficient contrast with each other, show you what they look like, and suggests the closest alternative color if they don't have sufficient contrast. Note that the default values for font size, weight, and compliance can be left alone for most purposes. | [Site](https://accessible-colors.com/) | + + +If you go through all the manual tests in the full Accessibility Insights assessment having these scripts bookmarked will be useful! Running them is as simple as opening the bookmark while viewing the page you're testing. + +**Tests whether page text can be spaced out and comply with requirements without breaking layout** + +```` +javascript:(function(){ var style = document.createElement(%27style%27), styleContent = document.createTextNode(%27* { line-height: 1.5 !important; letter-spacing: 0.12em !important; word-spacing: 0.16em !important; } p{ margin-bottom: 2em !important; } %27); style.appendChild(styleContent ); document.getElementsByTagName(%27head%27)[0].appendChild(style); var iframes = document.querySelectorAll(%27iframe%27);for (var i=0; i";break;case Node.COMMENT_NODE:b+="<\!--"+a.nodeValue+"--\>";break;case Node.DOCUMENT_TYPE_NODE:b+="\n"}a=a.nextSibling}return b}(document),d=document.createElement("form");d.method="POST";d.action="https://validator.w3.org/nu/";d.enctype="multipart/form-data";d.target="_blank";d.acceptCharset="utf-8";c("showsource","yes");c("content",e);document.body.appendChild(d);d.submit()})(); +```` + +**Filters results of the above to only WCAG 2.0 violations** + +```` +javascript:(function(){var removeNg=true;var filterStrings=["tag seen","Stray end tag","Bad start tag","violates nesting rules","Duplicate ID","first occurrence of ID","Unclosed element","not allowed as child of element","unclosed elements","not allowed on element","unquoted attribute value","Duplicate attribute"];var filterRE,root,results,result,resultText,i,cnt=0;filterRE=filterStrings.join("|");root=document.getElementById("results");if(!root){alert("No results container found.");return}results=root.getElementsByTagName("li");for(i=0;i Date: Wed, 14 Feb 2024 09:29:18 -0700 Subject: [PATCH 036/149] - Made var a bool instead of string - Setting it with dockerfile --- tdrs-backend/docker-compose.yml | 1 + tdrs-backend/tdpservice/parsers/test/test_parse.py | 2 -- tdrs-backend/tdpservice/settings/common.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tdrs-backend/docker-compose.yml b/tdrs-backend/docker-compose.yml index a6624688b..2d3417a93 100644 --- a/tdrs-backend/docker-compose.yml +++ b/tdrs-backend/docker-compose.yml @@ -101,6 +101,7 @@ services: - CYPRESS_TOKEN - DJANGO_DEBUG - SENDGRID_API_KEY + - GENERATE_TRAILER_ERRORS=True volumes: - .:/tdpapp image: tdp diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index cb669aebd..5e3ef7192 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -12,11 +12,9 @@ from .factories import DataFileSummaryFactory from tdpservice.data_files.models import DataFile from .. import schema_defs, aggregates, util -from django.conf import settings import logging -settings.GENERATE_TRAILER_ERRORS = True es_logger = logging.getLogger('elasticsearch') es_logger.setLevel(logging.WARNING) diff --git a/tdrs-backend/tdpservice/settings/common.py b/tdrs-backend/tdpservice/settings/common.py index f823c475e..8f4195691 100644 --- a/tdrs-backend/tdpservice/settings/common.py +++ b/tdrs-backend/tdpservice/settings/common.py @@ -474,4 +474,4 @@ class Common(Configuration): CYPRESS_TOKEN = os.getenv('CYPRESS_TOKEN', None) - GENERATE_TRAILER_ERRORS = strtobool(os.getenv("GENERATE_TRAILER_ERRORS", "no")) + GENERATE_TRAILER_ERRORS = os.getenv("GENERATE_TRAILER_ERRORS", False) From 25034288966972780c8d6b19962fa1b3548d240d Mon Sep 17 00:00:00 2001 From: Eric Lipe <125676261+elipe17@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:01:29 -0700 Subject: [PATCH 037/149] Kibana Deployment (#2805) * Added formating for header and autofit columns * Formatted the headers * added year/month to the columns * Added contants - translation column * added friendly names to T1 and T2 * added friendly name to m1 and m2 * added friendly name to m3 * added friendly_name to t3 * added friendly_name to t4 and t5 * added friendly_name to t7 * correct missing friendly_name * correction on failing tests * addedfriendly name to excel report * linting * linting * linting * delete contants.py * added test for json field in error model * linting * linting * linting * 2599-added friendly name to postparsing validators * refining the validator tests * added returning fields names to validators * added friendly_name to error field * linting * corrections on views/tests * corrections for fields * failing test corrected * failing test corrected * correcting test failures * linting * corrected the excel fiel generator * removed excessive space in validator * linting * listing * added m6 * lint * corrected new line break * refactored validator logic * linting and correction on t1 * friendly_name correction from comments * friendly_name correction * corrected failing test for m5 * refactor the field_json creation DRY * - Added Kibana config * friendly_name corrections * linting and cleaning errors * linting * correction on friendly_names * corrected friendly_name for test_util * correction child care - number of months * fixed a few more typos and some spacing. (#2767) * fixed a few more typos and some spacing. * fixed linting issues * missed a spot. --------- Co-authored-by: George Hudson * - Added basic security to Kibana/Elastic - Added setup container to init elastic users, roles, and passwords * - Remove debug code * - change provider name * - Updating settings to reference environment variables * - Add elastic dependency * - Fix network issue * - Added bulk creation of elastic indices * - Updated schemas to reference model based off of elastic document * - Remove password auth from elastic/kibana * - Remove password auth * - Fix tests * - Fix lint * - remove debug print * Changes for fully local development - Enables direct frontend/backend communication sans Login.gov/Cloud.gov - Drives off new DEVELOPMENT env var - Pre-configures and disables frontend auth functionality - Testing based on new dev user - Install via web: ./manage.py generate_dev_user * Reorganized front end logic on REACT_APP_DEVAUTH env var * Reorganized backend logic on REACT_APP_DEVAUTH env var * - Added proof on concept for tdp based kibana auth * - Fixing type issue * added is_superuser and is_staff attrs to dev user * - Add group check * - Add frontend group check for kibana * - fix lint * - Fix lint errors * - Fix doc strings * - Adding authenticated permission * - Renaming variables to clarify things * - fix lint * Revert "- Remove password auth from elastic/kibana" This reverts commit 522ca381651e005e35701d51b94e6cf4d78c011a. * - Setting up anonymous users with kibana_admin privileges * - Adding password to settings in cloud.gov * - remove incorrect auth - use admin only in frontend and backend * - Add elastic profile * DevAuth feature redesign inspired by Cypress - Initializing frontend w/POST /login/cypress: {devEmail, local-cypress-token} - Changed REACT_APP_DEVAUTH to provide the email of the desired dev user - Modified CustomAuthentication.authenticate to handle both known use cases - Added stt_id=31 to the initial dev user - Disabled ES disk threshold checking for local dev which blocked ES startup - Removed DevAuthentication and other now unnecessary code * Fixed CustomAuthentication.authenticate return val for login.py use case * Fixed CustomAuthentication.authenticate logging for login.py use case * Removed unneeded permissions import * Updates to REACT_APP_DEVAUTH env var settings - Enabled with an email address value - Disabled by default * - debugging env vars * - Testing what settings are used * Revert "- debugging env vars" This reverts commit 900efa879e8cb33f0c62140ed4d8e9a1e1b5496a. * Revert "- Testing what settings are used" This reverts commit 784530e49d584db5bda89714a7de4f56c3ee805e. * - debugging env vars again * - Switching to container networking * Restored support for CustomAuthentication.authenticate username keyword * Modified CustomAuthentication.authenticate comment to satisfy flake8 * commit * asdfgvasd * Revert "Modified CustomAuthentication.authenticate comment to satisfy flake8" This reverts commit 761e4eb253d366ef742dd8caf94b6220ed9e81a1. * Revert "Restored support for CustomAuthentication.authenticate username keyword" This reverts commit 4bf895722e356e79b8bbe3674361b90888b91752. * Revert "Updates to REACT_APP_DEVAUTH env var settings" This reverts commit 7fc2a09353804fb728852e9accc041dff08e44e3. * Revert "Removed unneeded permissions import" This reverts commit c18383fe2bf8352c50dd84d2a84408fef2b71367. * Revert "Fixed CustomAuthentication.authenticate logging for login.py use case" This reverts commit 2b9b46f0e719638b320d0a7bbb2bd87eda97eeef. * Revert "Fixed CustomAuthentication.authenticate return val for login.py use case" This reverts commit 97a0cf6995dc17937f083d5efde1d74866c01ff4. * Revert "DevAuth feature redesign inspired by Cypress" This reverts commit 1497d4ab7549bf674e1f71d8f8f039ec7de363bf. * Revert "commit" This reverts commit a284856c66cb2caaca4b471c2f81fc39421e8f00. * Revert "added is_superuser and is_staff attrs to dev user" This reverts commit 6ffbee8f588f12b7595abd7adcfe15ad7e70d11b. * Revert "Reorganized backend logic on REACT_APP_DEVAUTH env var" This reverts commit 7fd7b4d48cd30a7e2c142ea7f1d85f8df95b80d8. * Revert "Reorganized front end logic on REACT_APP_DEVAUTH env var" This reverts commit 32a46713ae102fa15fdab0fcb66fca99b42eb7e2. * Revert "Changes for fully local development" This reverts commit 556221b310b73bee5e9af32eb4cd603b61a25d02. * asdf * - Adding integration tests for elastic bulk doc creation * Revert "asdf" This reverts commit 26455b48582ca9c6d986377e56475644525f7665. * - fix lint * fasdf * - Failed buildpack deploy. Commiting for history * - Updating manifests to deploy proxy and kibana * - Adding working manifests for kibana and proxy * - moving manifest to its own directory * - Updating backend deployment to include kibana and proxy for circi deploys * - remove port * - allowing manifest * - Update kibana and proxy hostnames * - adding debug * - Updating schemas * - Fix mispelling * - inplace update * - Fixing var names * - remove unset - Add set-env for proxy * - parametrizing proxy host name for kibana * - Adding debug logging to see whats up * - fix lint * - adding kibana to deployed nginx conf * - Added usage of document to tribal * - making url public for the time being to allow redirect * - testing 2GB again * - making stt searchable - update proxy mem limits * - back to internal route * - pushing temp changes for now * - adding extra setting * - pushing changes * - removing secondary proxy * - nginx auth * - Updated to allow nginx auth proxy for kibana * - adding back headers * - Undoing temp changes * - Updating to support cloud.gov deploy * - Fixing warnings * - fix env var * - Add netpol to allow kibana to talk to frontend * - Adding env vars for kibana * - fixing env var name * - remove host as test - remove invalid params from search indices * - remover server host param * - remove request limiter * - Adding unsafe-inline * - Updating to use path based env vars * - whitelisting kibana in CSP * - converting back to env vars only * - adding unsafe eval * - Updated based on feedback * - Fixing error * - Updating local docker image to match deployed image * - Removing elastic setup as it is irrelevant now * - Updating frontend to only allow access to kibana sitemap if the user is Dev or Sys Admin * - fix lint * - remove unnecessary nginx settings * - Updated Kibana tab/link to only display when user is HHS AMS authenticated - Added environment variable to show the tab/link for dev purposes * - Commenting env var to default to correct behavior in any environment * - update frontend memory quota * - Add resolver to nginx to avoid ip address change on app restart * - Add dns fix for clamav * - OFA sys admin only * - Testing dns resolution * - Fix merge conflict that was causing incorrect routing to kibana locally * - Updated to be boolean * - Remove merge conflict * - fix var * Revert "- Testing dns resolution" This reverts commit 84aac744fd999752d3dea8c91c9dcf3b9b3173bc. * - removing unnecessary setting * - Renaming var to capture its use better * - Keeping var commented out --------- Co-authored-by: Mo Sohani Co-authored-by: raftmsohani <97037188+raftmsohani@users.noreply.github.com> Co-authored-by: George Hudson Co-authored-by: George Hudson Co-authored-by: Thomas Tignor Co-authored-by: Thomas Tignor Co-authored-by: Andrew <84722778+andrew-jameson@users.noreply.github.com> --- .circleci/deployment/commands.yml | 18 +++++++ .gitconfig | 1 + scripts/deploy-backend.sh | 49 ++++++++++++++++++- scripts/deploy-frontend.sh | 12 ++++- tdrs-backend/clamav-router/nginx.conf | 7 +-- tdrs-backend/docker-compose.yml | 44 +++-------------- tdrs-backend/kibana.yml | 12 ----- tdrs-backend/manifest.kibana.yml | 16 ++++++ tdrs-backend/manifest.proxy.yml | 15 ++++++ .../search_indexes/documents/document_base.py | 5 ++ tdrs-backend/tdpservice/settings/common.py | 23 +++++---- tdrs-backend/tdpservice/urls.py | 2 +- .../users/api/authorization_check.py | 8 +-- tdrs-frontend/.env | 3 ++ tdrs-frontend/docker-compose.yml | 9 ++-- tdrs-frontend/manifest.buildpack.yml | 2 +- tdrs-frontend/manifest.yml | 2 +- .../nginx/cloud.gov/buildpack.nginx.conf | 14 +++--- tdrs-frontend/nginx/cloud.gov/locations.conf | 39 ++++++++++++++- .../nginx/local/default.conf.template | 14 +++--- tdrs-frontend/nginx/local/locations.conf | 39 ++++++++++++++- tdrs-frontend/src/components/Home/Home.jsx | 1 - .../components/STTComboBox/STTComboBox.jsx | 3 -- tdrs-frontend/src/selectors/auth.js | 4 +- 24 files changed, 243 insertions(+), 99 deletions(-) delete mode 100644 tdrs-backend/kibana.yml create mode 100644 tdrs-backend/manifest.kibana.yml create mode 100644 tdrs-backend/manifest.proxy.yml diff --git a/.circleci/deployment/commands.yml b/.circleci/deployment/commands.yml index 43adb60e3..347f119b5 100644 --- a/.circleci/deployment/commands.yml +++ b/.circleci/deployment/commands.yml @@ -79,12 +79,24 @@ frontend-appname: default: tdp-frontend type: string + kibana-appname: + default: tdp-kibana + type: string + proxy-appname: + default: tdp-elastic-proxy + type: string cf-space: default: tanf-dev type: string steps: - get-app-deploy-strategy: appname: <> + - run: + name: Install dependencies + command: | + sudo apt update + sudo add-apt-repository ppa:rmescandon/yq + sudo apt-get install yq - run: name: Deploy backend application command: | @@ -92,6 +104,8 @@ $DEPLOY_STRATEGY \ <> \ <> \ + <> \ + <> \ <> deploy-clamav: @@ -115,6 +129,9 @@ frontend-appname: default: tdp-frontend type: string + kibana-appname: + default: tdp-kibana + type: string # So the frontend knows what space its in for the banner. # I am unclear if the domain is a reliable metric to make this function # It seems like it might not be working @@ -136,6 +153,7 @@ $DEPLOY_STRATEGY \ <> \ <> \ + <> \ <> \ <> diff --git a/.gitconfig b/.gitconfig index 4c46daac8..2b2b988bc 100644 --- a/.gitconfig +++ b/.gitconfig @@ -7,6 +7,7 @@ allowed = [A-Z]+_KEY=..echo \".{S3_CREDENTIALS}\" [|] jq -r .+ allowed = ./tdrs-backend/.env.example:.* allowed = ./tdrs-backend/docker-compose.yml:57:.* + allowed = ./tdrs-backend/manifest.proxy.yml:* allowed = regexes.json:.* allowed = ./scripts/copy-login-gov-keypair.sh:14:JWT_KEY=.* allowed = scripts/deploy-backend.sh:.+:DJANGO_SECRET_KEY=..python -c .from secrets import token_urlsafe. print.token_urlsafe..* diff --git a/scripts/deploy-backend.sh b/scripts/deploy-backend.sh index 6e46fe93a..0dc5ba5b4 100755 --- a/scripts/deploy-backend.sh +++ b/scripts/deploy-backend.sh @@ -10,7 +10,9 @@ DEPLOY_STRATEGY=${1} #The application name defined via the manifest yml for the frontend CGAPPNAME_FRONTEND=${2} CGAPPNAME_BACKEND=${3} -CF_SPACE=${4} +CGAPPNAME_KIBANA=${4} +CGAPPNAME_PROXY=${5} +CF_SPACE=${6} strip() { # Usage: strip "string" "pattern" @@ -20,8 +22,14 @@ strip() { env=$(strip $CF_SPACE "tanf-") backend_app_name=$(echo $CGAPPNAME_BACKEND | cut -d"-" -f3) +# Update the Kibana and Elastic proxy names to include the environment +CGAPPNAME_KIBANA="${CGAPPNAME_KIBANA}-${backend_app_name}" +CGAPPNAME_PROXY="${CGAPPNAME_PROXY}-${backend_app_name}" + echo DEPLOY_STRATEGY: "$DEPLOY_STRATEGY" echo BACKEND_HOST: "$CGAPPNAME_BACKEND" +echo KIBANA_HOST: "$CGAPPNAME_KIBANA" +echo ELASTIC_PROXY_HOST: "$CGAPPNAME_PROXY" echo CF_SPACE: "$CF_SPACE" echo env: "$env" echo backend_app_name: "$backend_app_name" @@ -49,6 +57,7 @@ set_cf_envs() "DJANGO_SETTINGS_MODULE" "DJANGO_SU_NAME" "FRONTEND_BASE_URL" + "KIBANA_BASE_URL" "LOGGING_LEVEL" "REDIS_URI" "JWT_KEY" @@ -86,6 +95,36 @@ generate_jwt_cert() cf set-env "$CGAPPNAME_BACKEND" JWT_KEY "$(cat key.pem)" } +update_kibana() +{ + cd tdrs-backend || exit + + # Run template evaluation on manifest + yq eval -i ".applications[0].services[0] = \"es-${backend_app_name}\"" manifest.proxy.yml + yq eval -i ".applications[0].env.CGAPPNAME_PROXY = \"${CGAPPNAME_PROXY}\"" manifest.kibana.yml + + if [ "$1" = "rolling" ] ; then + # Do a zero downtime deploy. This requires enough memory for + # two apps to exist in the org/space at one time. + cf push "$CGAPPNAME_PROXY" --no-route -f manifest.proxy.yml -t 180 --strategy rolling || exit 1 + cf push "$CGAPPNAME_KIBANA" --no-route -f manifest.kibana.yml -t 180 --strategy rolling || exit 1 + else + cf push "$CGAPPNAME_PROXY" --no-route -f manifest.proxy.yml -t 180 + cf push "$CGAPPNAME_KIBANA" --no-route -f manifest.kibana.yml -t 180 + fi + + cf map-route "$CGAPPNAME_PROXY" apps.internal --hostname "$CGAPPNAME_PROXY" + cf map-route "$CGAPPNAME_KIBANA" apps.internal --hostname "$CGAPPNAME_KIBANA" + + # Add network policy allowing Kibana to talk to the proxy and to allow the backend to talk to Kibana + cf add-network-policy "$CGAPPNAME_KIBANA" "$CGAPPNAME_PROXY" --protocol tcp --port 8080 + cf add-network-policy "$CGAPPNAME_BACKEND" "$CGAPPNAME_KIBANA" --protocol tcp --port 5601 + cf add-network-policy "$CGAPPNAME_FRONTEND" "$CGAPPNAME_KIBANA" --protocol tcp --port 5601 + cf add-network-policy "$CGAPPNAME_KIBANA" "$CGAPPNAME_FRONTEND" --protocol tcp --port 80 + + cd .. +} + update_backend() { cd tdrs-backend || exit @@ -189,6 +228,8 @@ else FRONTEND_BASE_URL="$DEFAULT_FRONTEND_ROUTE" fi +KIBANA_BASE_URL="http://$CGAPPNAME_KIBANA.apps.internal" + # Dynamically generate a new DJANGO_SECRET_KEY DJANGO_SECRET_KEY=$(python3 -c "from secrets import token_urlsafe; print(token_urlsafe(50))") @@ -208,6 +249,7 @@ if [ "$DEPLOY_STRATEGY" = "rolling" ] ; then # Perform a rolling update for the backend and frontend deployments if # specified, otherwise perform a normal deployment update_backend 'rolling' + update_kibana 'rolling' elif [ "$DEPLOY_STRATEGY" = "bind" ] ; then # Bind the services the application depends on and restage the app. bind_backend_to_services @@ -216,15 +258,20 @@ elif [ "$DEPLOY_STRATEGY" = "initial" ]; then # for it to work. the app will fail to start once, have the services bind, # and then get restaged. update_backend + update_kibana bind_backend_to_services elif [ "$DEPLOY_STRATEGY" = "rebuild" ]; then # You want to redeploy the instance under the same name # Delete the existing app (with out deleting the services) # and perform the initial deployment strategy. cf delete "$CGAPPNAME_BACKEND" -r -f + cf delete "$CGAPPNAME_KIBANA" -r -f + cf delete "$CGAPPNAME_PROXY" -r -f update_backend + update_kibana bind_backend_to_services else # No changes to deployment config, just deploy the changes and restart update_backend + update_kibana fi diff --git a/scripts/deploy-frontend.sh b/scripts/deploy-frontend.sh index 96af218f2..2638b0d6e 100755 --- a/scripts/deploy-frontend.sh +++ b/scripts/deploy-frontend.sh @@ -8,14 +8,21 @@ DEPLOY_STRATEGY=${1} #The application name defined via the manifest yml for the frontend CGHOSTNAME_FRONTEND=${2} CGHOSTNAME_BACKEND=${3} -CF_SPACE=${4} -ENVIRONMENT=${5} +CGAPPNAME_KIBANA=${4} +CF_SPACE=${5} +ENVIRONMENT=${6} + +backend_app_name=$(echo $CGHOSTNAME_BACKEND | cut -d"-" -f3) + +# Update the Kibana name to include the environment +KIBANA_BASE_URL="${CGAPPNAME_KIBANA}-${backend_app_name}.apps.internal" update_frontend() { echo DEPLOY_STRATEGY: "$DEPLOY_STRATEGY" echo FRONTEND_HOST: "$CGHOSTNAME_FRONTEND" echo BACKEND_HOST: "$CGHOSTNAME_BACKEND" + echo KIBANA_BASE_URL: "$KIBANA_BASE_URL" cd tdrs-frontend || exit if [ "$CF_SPACE" = "tanf-prod" ]; then @@ -44,6 +51,7 @@ update_frontend() fi cf set-env "$CGHOSTNAME_FRONTEND" BACKEND_HOST "$CGHOSTNAME_BACKEND" + cf set-env "$CGHOSTNAME_FRONTEND" KIBANA_BASE_URL "$KIBANA_BASE_URL" npm run build:$ENVIRONMENT unlink .env.production diff --git a/tdrs-backend/clamav-router/nginx.conf b/tdrs-backend/clamav-router/nginx.conf index 35e95e7a7..bec070813 100644 --- a/tdrs-backend/clamav-router/nginx.conf +++ b/tdrs-backend/clamav-router/nginx.conf @@ -1,8 +1,9 @@ -events { worker_connections 1024; +events { + worker_connections 1024; } - # This opens a route to clamav prod http{ + resolver {{nameservers}} valid=10s; server { client_max_body_size 100m; listen {{port}}; @@ -21,4 +22,4 @@ http{ proxy_pass_request_headers on; } } -} +} \ No newline at end of file diff --git a/tdrs-backend/docker-compose.yml b/tdrs-backend/docker-compose.yml index 6a09c3944..e6636e14d 100644 --- a/tdrs-backend/docker-compose.yml +++ b/tdrs-backend/docker-compose.yml @@ -46,40 +46,15 @@ services: - ../scripts/localstack-setup.sh:/docker-entrypoint-initaws.d/localstack-setup.sh kibana: - image: elastic/kibana:7.17.10 + image: docker.elastic.co/kibana/kibana-oss:7.4.2 ports: - 5601:5601 environment: - - xpack.security.encryptionKey=${KIBANA_ENCRYPTION_KEY:-something_at_least_32_characters} - - xpack.security.session.idleTimeout="1h" - - xpack.security.session.lifespan="30d" - volumes: - - ./kibana.yml:/usr/share/kibana/config/kibana.yml - depends_on: - - elastic - - # This task only needs to be performed once, during the *initial* startup of - # the stack. Any subsequent run will reset the passwords of existing users to - # the values defined inside the '.env' file, and the built-in roles to their - # default permissions. - # - # By default, it is excluded from the services started by 'docker compose up' - # due to the non-default profile it belongs to. To run it, either provide the - # '--profile=elastic_setup' CLI flag to Compose commands, or "up" the service by name - # such as 'docker compose up elastic_setup'. - elastic_setup: - profiles: - - elastic_setup - build: - context: elastic_setup/ - args: - ELASTIC_VERSION: "7.17.6" - init: true - environment: - ELASTIC_PASSWORD: ${ELASTIC_PASSWORD:-changeme} - KIBANA_SYSTEM_PASSWORD: ${KIBANA_SYSTEM_PASSWORD:-changeme} - OFA_ADMIN_PASSWORD: ${OFA_ADMIN_PASSWORD:-changeme} - ELASTICSEARCH_HOST: ${ELASTICSEARCH_HOST:-elastic} + - ELASTICSEARCH_HOSTS="http://elastic:9200" + - SERVER_HOST=kibana + - SERVER_BASEPATH=/kibana + - SERVER_SECURITYRESPONSEHEADERS_REFERRERPOLICY=no-referrer + - CSP_WARNLEGACYBROWSERS=false depends_on: - elastic @@ -88,12 +63,7 @@ services: environment: - discovery.type=single-node - logger.discovery.level=debug - - xpack.security.enabled=true - - xpack.security.authc.anonymous.username="ofa_admin" - - xpack.security.authc.anonymous.roles="ofa_admin" - - xpack.security.authc.anonymous.authz_exception=true - - ELASTIC_PASSWORD=${ELASTIC_PASSWORD:-changeme} - - KIBANA_SYSTEM_PASSWORD=${KIBANA_SYSTEM_PASSWORD:-changeme} + - xpack.security.enabled=false ports: - 9200:9200 - 9300:9300 diff --git a/tdrs-backend/kibana.yml b/tdrs-backend/kibana.yml deleted file mode 100644 index e98d2438d..000000000 --- a/tdrs-backend/kibana.yml +++ /dev/null @@ -1,12 +0,0 @@ -elasticsearch.hosts: ["http://elastic:9200"] -server.host: kibana -elasticsearch.username: kibana_system -elasticsearch.password: changeme -xpack.security.authc.providers: - anonymous.anonymous1: - order: 0 - description: "OFA Admin Login" - hint: "" - credentials: - username: "ofa_admin" - password: "changeme" diff --git a/tdrs-backend/manifest.kibana.yml b/tdrs-backend/manifest.kibana.yml new file mode 100644 index 000000000..181b29ec0 --- /dev/null +++ b/tdrs-backend/manifest.kibana.yml @@ -0,0 +1,16 @@ +version: 1 +applications: + - name: tdp-kibana + memory: 2G + disk_quota: 2G + instances: 1 + env: + CGAPPNAME_PROXY: {{ proxy_hostname }} + SERVER_BASEPATH: /kibana + SERVER_SECURITYRESPONSEHEADERS_REFERRERPOLICY: no-referrer + CSP_WARNLEGACYBROWSERS: false + docker: + image: docker.elastic.co/kibana/kibana-oss:7.4.2 + command: | + export ELASTICSEARCH_HOSTS=http://$CGAPPNAME_PROXY.apps.internal:8080 && + /usr/local/bin/dumb-init -- /usr/local/bin/kibana-docker diff --git a/tdrs-backend/manifest.proxy.yml b/tdrs-backend/manifest.proxy.yml new file mode 100644 index 000000000..7ef739c4f --- /dev/null +++ b/tdrs-backend/manifest.proxy.yml @@ -0,0 +1,15 @@ +version: 1 +applications: +- name: tdp-elastic-proxy + memory: 64M + disk_quota: 64M + instances: 1 + services: + - {{ service_0 }} + docker: + image: elipe17/aws-es-proxy:latest + command: | + export ENDPOINT=$(echo $VCAP_SERVICES | grep -Eo 'host[^,]*' | grep -Eo '[^:]*$' | tr -d '"' | sed -e 's/^/https:\/\//') && + export AWS_ACCESS_KEY_ID=$(echo $VCAP_SERVICES | grep -Eo 'access_key[^,]*' | grep -Eo '[^:]*$' | tr -d '"') && + export AWS_SECRET_ACCESS_KEY=$(echo $VCAP_SERVICES | grep -Eo 'secret_key[^,]*' | grep -Eo '[^:]*$' | tr -d '"') && + /usr/local/bin/aws-es-proxy -endpoint $ENDPOINT -listen 0.0.0.0:8080 -verbose -debug diff --git a/tdrs-backend/tdpservice/search_indexes/documents/document_base.py b/tdrs-backend/tdpservice/search_indexes/documents/document_base.py index ea377b283..67903e7e2 100644 --- a/tdrs-backend/tdpservice/search_indexes/documents/document_base.py +++ b/tdrs-backend/tdpservice/search_indexes/documents/document_base.py @@ -13,6 +13,11 @@ class DocumentBase(Document): 'version': fields.IntegerField(), 'quarter': fields.TextField(), 'year': fields.IntegerField(), + 'stt': fields.ObjectField(properties={ + 'name': fields.TextField(), + 'type': fields.TextField(), + 'stt_code': fields.TextField() + }) }) def get_instances_from_related(self, related_instance): diff --git a/tdrs-backend/tdpservice/settings/common.py b/tdrs-backend/tdpservice/settings/common.py index 108586c80..7d9126716 100644 --- a/tdrs-backend/tdpservice/settings/common.py +++ b/tdrs-backend/tdpservice/settings/common.py @@ -340,10 +340,19 @@ class Common(Configuration): # The number of seconds to wait for socket response from clamav-rest AV_SCAN_TIMEOUT = int(os.getenv('AV_SCAN_TIMEOUT', 30)) + # Elastic/Kibana + ELASTICSEARCH_DSL = { + 'default': { + 'hosts': os.getenv('ELASTIC_HOST', 'elastic:9200'), + }, + } + KIBANA_BASE_URL = os.getenv('KIBANA_BASE_URL', 'http://kibana:5601') + BYPASS_KIBANA_AUTH = os.getenv("BYPASS_KIBANA_AUTH", False) + s3_src = "s3-us-gov-west-1.amazonaws.com" CSP_DEFAULT_SRC = ("'none'") - CSP_SCRIPT_SRC = ("'self'", s3_src) + CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'", s3_src, KIBANA_BASE_URL) CSP_IMG_SRC = ("'self'", "data:", s3_src) CSP_FONT_SRC = ("'self'", s3_src) CSP_CONNECT_SRC = ("'self'", "*.cloud.gov") @@ -351,7 +360,7 @@ class Common(Configuration): CSP_OBJECT_SRC = ("'none'") CSP_FRAME_ANCESTORS = ("'none'") CSP_FORM_ACTION = ("'self'") - CSP_STYLE_SRC = ("'self'", s3_src, "'unsafe-inline'") + CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", s3_src, KIBANA_BASE_URL) #################################### @@ -465,14 +474,4 @@ class Common(Configuration): } } - # Elastic/Kibana - ELASTICSEARCH_DSL = { - 'default': { - 'hosts': os.getenv('ELASTIC_HOST', 'elastic:9200'), - 'http_auth': ('elastic', os.getenv('ELASTIC_PASSWORD', 'changeme')) - }, - } - KIBANA_BASE_URL = os.getenv('KIBANA_BASE_URL', 'http://localhost:5601') - BYPASS_KIBANA_AUTH = strtobool(os.getenv("BYPASS_KIBANA_AUTH", "no")) - CYPRESS_TOKEN = os.getenv('CYPRESS_TOKEN', None) diff --git a/tdrs-backend/tdpservice/urls.py b/tdrs-backend/tdpservice/urls.py index 368314c92..ee4c37701 100755 --- a/tdrs-backend/tdpservice/urls.py +++ b/tdrs-backend/tdpservice/urls.py @@ -52,7 +52,7 @@ urlpatterns = [ path("v1/", include(urlpatterns)), path("admin/", admin.site.urls, name="admin"), - path("kibana/", KibanaAuthorizationCheck.as_view(), name="kibana-authorization-check"), + path("kibana_auth_check/", KibanaAuthorizationCheck.as_view(), name="kibana-authorization-check"), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # TODO: Supply `terms_of_service` argument in OpenAPI Info once implemented diff --git a/tdrs-backend/tdpservice/users/api/authorization_check.py b/tdrs-backend/tdpservice/users/api/authorization_check.py index 76afeecb1..191e80055 100644 --- a/tdrs-backend/tdpservice/users/api/authorization_check.py +++ b/tdrs-backend/tdpservice/users/api/authorization_check.py @@ -8,7 +8,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from ..serializers import UserProfileSerializer -from django.http import HttpResponseRedirect +from django.http import HttpResponse from django.conf import settings logger = logging.getLogger(__name__) @@ -66,6 +66,8 @@ def get(self, request, *args, **kwargs): user_in_valid_group = user.is_ofa_sys_admin if (user.hhs_id is not None and user_in_valid_group) or settings.BYPASS_KIBANA_AUTH: - return HttpResponseRedirect(settings.KIBANA_BASE_URL) + logger.debug(f"User: {user} has correct authentication credentials. Allowing access to Kibana.") + return HttpResponse(status=200) else: - return HttpResponseRedirect(settings.FRONTEND_BASE_URL) + logger.debug(f"User: {user} has incorrect authentication credentials. Not allowing access to Kibana.") + return HttpResponse(status=401) diff --git a/tdrs-frontend/.env b/tdrs-frontend/.env index 882a4aafa..11902ac81 100644 --- a/tdrs-frontend/.env +++ b/tdrs-frontend/.env @@ -36,6 +36,9 @@ REACT_APP_DEBOUNCE_TIME=30000 # 60 seconds == 60 * 1000 == 60000 REACT_APP_EVENT_THROTTLE_TIME=60000 +# Enable the Kibana tab for dev purposes. +# REACT_APP_DEV_KIBANA=true + # Setup SCSS: # The following makes it possible to import SASS modules # without relative paths. Removing this will require all @imports in diff --git a/tdrs-frontend/docker-compose.yml b/tdrs-frontend/docker-compose.yml index 23c0a0669..abfb2ba18 100644 --- a/tdrs-frontend/docker-compose.yml +++ b/tdrs-frontend/docker-compose.yml @@ -28,18 +28,19 @@ services: - NGINX_FRONTEND=tdp-frontend - BACK_END=web - LOCAL_DEV=true + - KIBANA=kibana - REACT_APP_DEVAUTH=${REACT_APP_DEVAUTH} command: > /bin/sh -c "echo 'starting nginx' && - envsubst '$${BACK_END}' < /etc/nginx/locations.conf > /etc/nginx/locations_.conf && + envsubst '$${BACK_END} $${KIBANA}' < /etc/nginx/locations.conf > /etc/nginx/locations_.conf && rm /etc/nginx/locations.conf && cp /etc/nginx/locations_.conf /etc/nginx/locations.conf && envsubst ' - $${BACK_END} $${NGINX_FRONTEND} $${LOCAL_DEV} + $${KIBANA} $${BACK_END} $${NGINX_FRONTEND} $${LOCAL_DEV} '< /etc/nginx/default.conf.template - > /etc/nginx/nginx.conf - && nginx -g 'daemon off;'" + > /etc/nginx/nginx.conf && + nginx -g 'daemon off;'" networks: default: diff --git a/tdrs-frontend/manifest.buildpack.yml b/tdrs-frontend/manifest.buildpack.yml index 93956849a..d8a134a87 100755 --- a/tdrs-frontend/manifest.buildpack.yml +++ b/tdrs-frontend/manifest.buildpack.yml @@ -4,7 +4,7 @@ applications: - name: tdp-frontend buildpacks: - https://github.com/cloudfoundry/nginx-buildpack.git#v1.2.6 - memory: 128M + memory: 256M instances: 1 disk_quota: 256M timeout: 180 diff --git a/tdrs-frontend/manifest.yml b/tdrs-frontend/manifest.yml index f42e7d092..d9b8ef423 100755 --- a/tdrs-frontend/manifest.yml +++ b/tdrs-frontend/manifest.yml @@ -2,7 +2,7 @@ version: 1 applications: - name: tdp-frontend - memory: 32M + memory: 256M instances: 1 disk_quota: 256M timeout: 180 diff --git a/tdrs-frontend/nginx/cloud.gov/buildpack.nginx.conf b/tdrs-frontend/nginx/cloud.gov/buildpack.nginx.conf index 4ed6804f9..514010873 100644 --- a/tdrs-frontend/nginx/cloud.gov/buildpack.nginx.conf +++ b/tdrs-frontend/nginx/cloud.gov/buildpack.nginx.conf @@ -34,6 +34,8 @@ http { limit_req_zone $binary_remote_addr zone=limitreqsbyaddr:20m rate=1000r/s; limit_req_status 444; + resolver {{nameservers}} valid=10s; + server { root public; listen {{port}}; @@ -61,9 +63,9 @@ http { set $ALLOWED_ORIGIN {{env "ALLOWED_ORIGIN"}}; set $CSP "default-src 'self';"; - set $CSP "${CSP}script-src 'self';"; - set $CSP "${CSP}script-src-elem 'self';"; - set $CSP "${CSP}script-src-attr 'self' 'unsafe-inline';"; + set $CSP "${CSP}script-src 'self' 'unsafe-eval' 'unsafe-inline' http://{{env "KIBANA_BASE_URL"}}:5601;"; + set $CSP "${CSP}script-src-elem 'self' 'unsafe-inline' http://{{env "KIBANA_BASE_URL"}}:5601;"; + set $CSP "${CSP}script-src-attr 'self' 'unsafe-inline' http://{{env "KIBANA_BASE_URL"}}:5601;"; set $CSP "${CSP}img-src 'self' data:;"; set $CSP "${CSP}font-src 'self';"; set $CSP "${CSP}connect-src 'self' ${CONNECT_SRC};"; @@ -75,9 +77,9 @@ http { set $CSP "${CSP}child-src 'none';"; set $CSP "${CSP}media-src 'none';"; set $CSP "${CSP}prefetch-src 'none';"; - set $CSP "${CSP}style-src 'self' 'unsafe-inline';"; - set $CSP "${CSP}style-src-elem 'self' 'unsafe-inline';"; - set $CSP "${CSP}style-src-attr 'self' 'unsafe-inline';"; + set $CSP "${CSP}style-src 'self' 'unsafe-inline' http://{{env "KIBANA_BASE_URL"}}:5601;"; + set $CSP "${CSP}style-src-elem 'self' 'unsafe-inline' http://{{env "KIBANA_BASE_URL"}}:5601;"; + set $CSP "${CSP}style-src-attr 'self' 'unsafe-inline' http://{{env "KIBANA_BASE_URL"}}:5601;"; set $CSP "${CSP}worker-src 'none';"; add_header Content-Security-Policy $CSP; diff --git a/tdrs-frontend/nginx/cloud.gov/locations.conf b/tdrs-frontend/nginx/cloud.gov/locations.conf index a6c6d7b42..b7cd5517f 100644 --- a/tdrs-frontend/nginx/cloud.gov/locations.conf +++ b/tdrs-frontend/nginx/cloud.gov/locations.conf @@ -22,8 +22,43 @@ location ~ ^/(v1|admin|static/admin|swagger|redocs) { add_header Access-Control-Allow-Origin 's3-us-gov-west-1.amazonaws.com'; } -if ($request_method ~ ^(TRACE|OPTION)$) { - return 405; +location /kibana/ { + auth_request /kibana_auth_check; + auth_request_set $auth_status $upstream_status; + + proxy_pass http://{{env "KIBANA_BASE_URL"}}:5601/; + proxy_pass_header x-csrftoken; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + + proxy_connect_timeout 900; + proxy_read_timeout 300; + proxy_send_timeout 300; + send_timeout 900; + proxy_buffer_size 4k; + + proxy_hide_header Referrer-Policy; +} + +location = /kibana_auth_check { + internal; + proxy_pass http://{{env "BACKEND_HOST"}}.apps.internal:8080/kibana_auth_check/; + proxy_pass_header x-csrftoken; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + + proxy_connect_timeout 900; + proxy_read_timeout 300; + proxy_send_timeout 300; + send_timeout 900; +} + +if ($request_method ~ ^(TRACE|OPTION)$) { + return 405; } location = /profile { diff --git a/tdrs-frontend/nginx/local/default.conf.template b/tdrs-frontend/nginx/local/default.conf.template index c4d306340..8cf7b79ab 100644 --- a/tdrs-frontend/nginx/local/default.conf.template +++ b/tdrs-frontend/nginx/local/default.conf.template @@ -68,8 +68,8 @@ http { # CSP header options. All options are set either to none or self except set $CSP "default-src 'none';"; - set $CSP "${CSP}script-src 'self';"; - set $CSP "${CSP}style-src 'self' 'unsafe-inline' http://localhost:3000;"; + set $CSP "${CSP}script-src 'self' 'unsafe-eval' 'unsafe-inline' http://${KIBANA}:5601;"; + set $CSP "${CSP}style-src 'self' 'unsafe-inline' http://localhost:3000 http://${KIBANA}:5601;"; set $CSP "${CSP}img-src 'self' data:;"; set $CSP "${CSP}font-src 'self';"; set $CSP "${CSP}connect-src 'self' localhost:*;"; @@ -79,12 +79,11 @@ http { set $CSP "${CSP}frame-ancestors 'none';"; set $CSP "${CSP}child-src 'none';"; set $CSP "${CSP}media-src 'none';"; - set $CSP "${CSP}prefetch-src 'none';"; set $CSP "${CSP}form-action *;"; - set $CSP "${CSP}script-src-elem 'self' http://localhost:* http://www.w3.org;"; - set $CSP "${CSP}script-src-attr 'self' 'unsafe-inline';"; - set $CSP "${CSP}style-src-elem 'self' 'unsafe-inline';"; - set $CSP "${CSP}style-src-attr 'self';"; + set $CSP "${CSP}script-src-elem 'self' 'unsafe-inline' http://localhost:* http://www.w3.org http://${KIBANA}:5601;"; + set $CSP "${CSP}script-src-attr 'self' 'unsafe-inline' http://${KIBANA}:5601;"; + set $CSP "${CSP}style-src-elem 'self' 'unsafe-inline' http://${KIBANA}:5601;"; + set $CSP "${CSP}style-src-attr 'self' 'unsafe-inline' http://${KIBANA}:5601;"; set $CSP "${CSP}worker-src 'none';"; add_header Content-Security-Policy $CSP; @@ -136,7 +135,6 @@ http { set $CSP "${CSP}frame-src 'None';"; set $CSP "${CSP}child-src 'none';"; set $CSP "${CSP}media-src 'none';"; - set $CSP "${CSP}prefetch-src 'none';"; set $CSP "${CSP}style-src 'self' 'unsafe-inline';"; set $CSP "${CSP}style-src-elem 'self' 'unsafe-inline';"; set $CSP "${CSP}style-src-attr 'self' 'unsafe-inline';"; diff --git a/tdrs-frontend/nginx/local/locations.conf b/tdrs-frontend/nginx/local/locations.conf index 154cda557..e7ff75fcb 100644 --- a/tdrs-frontend/nginx/local/locations.conf +++ b/tdrs-frontend/nginx/local/locations.conf @@ -4,7 +4,7 @@ location = /nginx_status { deny all; } -location ~ ^/(v1|admin|static/admin|swagger|redocs|kibana) { +location ~ ^/(v1|admin|static/admin|swagger|redocs) { limit_req zone=limitreqsbyaddr delay=5; proxy_pass http://${BACK_END}:8080$request_uri; proxy_set_header Host $host:3000; @@ -21,6 +21,43 @@ location ~ ^/(v1|admin|static/admin|swagger|redocs|kibana) { proxy_pass_header x-csrftoken; } +location /kibana/ { + auth_request /kibana_auth_check; + auth_request_set $auth_status $upstream_status; + + proxy_pass http://${KIBANA}:5601/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + + proxy_connect_timeout 300; + proxy_read_timeout 300; + proxy_send_timeout 300; + send_timeout 900; + proxy_buffer_size 4k; + + proxy_hide_header Referrer-Policy; +} + +location = /kibana_auth_check { + internal; + proxy_pass http://${BACK_END}:8080/kibana_auth_check/; + proxy_set_header Host $host:3000; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; + + proxy_connect_timeout 300; + proxy_read_timeout 300; + proxy_send_timeout 300; + send_timeout 900; + proxy_pass_header x-csrftoken; +} + location = /profile { index index.html index.htm; try_files $uri $uri/ /index.html; diff --git a/tdrs-frontend/src/components/Home/Home.jsx b/tdrs-frontend/src/components/Home/Home.jsx index e102f8acc..8d6b57e36 100644 --- a/tdrs-frontend/src/components/Home/Home.jsx +++ b/tdrs-frontend/src/components/Home/Home.jsx @@ -23,7 +23,6 @@ function Home() { const errorRef = useRef(null) const user = useSelector((state) => state.auth.user) - const role = useSelector(selectPrimaryUserRole) const [errors, setErrors] = useState({}) const [profileInfo, setProfileInfo] = useState({ diff --git a/tdrs-frontend/src/components/STTComboBox/STTComboBox.jsx b/tdrs-frontend/src/components/STTComboBox/STTComboBox.jsx index 4f8adf0f2..21da77488 100644 --- a/tdrs-frontend/src/components/STTComboBox/STTComboBox.jsx +++ b/tdrs-frontend/src/components/STTComboBox/STTComboBox.jsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types' import { useDispatch, useSelector } from 'react-redux' import { fetchSttList } from '../../actions/sttList' import ComboBox from '../ComboBox' -import Button from '../Button' import Modal from '../Modal' /** @@ -38,8 +37,6 @@ function STTComboBox({ selectStt, selectedStt, handleBlur, error }) { } }, [dispatch, sttListRequest.sttList, numTries, reachedMaxTries]) - const modalRef = useRef() - const headerRef = useRef() const onSignOut = () => { window.location.href = `${process.env.REACT_APP_BACKEND_URL}/logout/oidc` } diff --git a/tdrs-frontend/src/selectors/auth.js b/tdrs-frontend/src/selectors/auth.js index ab962e275..eac564a09 100644 --- a/tdrs-frontend/src/selectors/auth.js +++ b/tdrs-frontend/src/selectors/auth.js @@ -62,4 +62,6 @@ export const accountCanViewAdmin = (state) => export const accountCanViewKibana = (state) => accountStatusIsApproved(state) && - ['Developer', 'OFA System Admin'].includes(selectPrimaryUserRole(state)?.name) + (selectUser(state)?.email?.includes('@acf.hhs.gov') || + process.env.REACT_APP_DEV_KIBANA) && + ['OFA System Admin'].includes(selectPrimaryUserRole(state)?.name) From 8da16dc266b12b1cedc5855d5484c19029ac46c6 Mon Sep 17 00:00:00 2001 From: Mo Sohani Date: Thu, 15 Feb 2024 13:34:57 -0500 Subject: [PATCH 038/149] fixing failing tests --- tdrs-backend/tdpservice/parsers/parse.py | 9 ++-- .../test/data/tribal_section_4_fake.txt | 2 +- .../tdpservice/parsers/test/test_parse.py | 46 ++++++++----------- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/parse.py b/tdrs-backend/tdpservice/parsers/parse.py index 3d092015b..38f0a7c21 100644 --- a/tdrs-backend/tdpservice/parsers/parse.py +++ b/tdrs-backend/tdpservice/parsers/parse.py @@ -223,10 +223,7 @@ def parse_datafile_lines(datafile, program_type, section, is_encrypted): schema_manager = get_schema_manager(line, section, program_type) - if type(schema_manager) is row_schema.SchemaManager: - schema_manager.datafile = datafile - - records = manager_parse_line(line, schema_manager, generate_error, is_encrypted) + records = manager_parse_line(line, schema_manager, generate_error, datafile, is_encrypted) record_number = 0 for i in range(len(records)): @@ -278,8 +275,10 @@ def parse_datafile_lines(datafile, program_type, section, is_encrypted): return errors -def manager_parse_line(line, schema_manager, generate_error, is_encrypted=False): +def manager_parse_line(line, schema_manager, generate_error, datafile, is_encrypted=False): """Parse and validate a datafile line using SchemaManager.""" + if type(schema_manager) is row_schema.SchemaManager: + schema_manager.datafile = datafile try: schema_manager.update_encrypted_fields(is_encrypted) records = schema_manager.parse_and_validate(line, generate_error) diff --git a/tdrs-backend/tdpservice/parsers/test/data/tribal_section_4_fake.txt b/tdrs-backend/tdpservice/parsers/test/data/tribal_section_4_fake.txt index 904d0bb79..9ec1943d3 100644 --- a/tdrs-backend/tdpservice/parsers/test/data/tribal_section_4_fake.txt +++ b/tdrs-backend/tdpservice/parsers/test/data/tribal_section_4_fake.txt @@ -1,3 +1,3 @@ -HEADER20194S00142TAN1EU +HEADER20204S00142TAN1EU T720204101006853700680540068454103000312400037850003180104000347400036460003583106000044600004360000325299000506200036070003385202000039100002740000499 TRAILER0000001 \ No newline at end of file diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index a98d27ab8..19a861f65 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -514,9 +514,9 @@ def test_parse_ssp_section1_datafile(ssp_section1_datafile): ssp_section1_datafile.year = 2019 ssp_section1_datafile.quarter = 'Q1' - expected_m1_record_count = 7849 - expected_m2_record_count = 9373 - expected_m3_record_count = 16764 + expected_m1_record_count = 820 + expected_m2_record_count = 992 + expected_m3_record_count = 1757 parse.parse_datafile(ssp_section1_datafile) @@ -529,7 +529,7 @@ def test_parse_ssp_section1_datafile(ssp_section1_datafile): assert err.error_message == '3 is not larger or equal to 1 and smaller or equal to 2.' assert err.content_type is not None assert err.object_id is not None - assert parser_errors.count() == 19846 + assert parser_errors.count() == 32486 assert SSP_M1.objects.count() == expected_m1_record_count assert SSP_M2.objects.count() == expected_m2_record_count @@ -741,18 +741,14 @@ def test_parse_bad_tfs1_missing_required(bad_tanf_s1__row_missing_required_field assert row_2_error.error_message == error_message assert row_2_error.object_id is None - error_message = 'RPT_MONTH_YEAR is required but a value was not provided.' + error_message = 'Reporting month year None does not match file reporting year:2021, quarter:Q1.' row_3_error = parser_errors.get(row_number=3, error_message=error_message) - assert row_3_error.error_type == ParserErrorCategoryChoices.FIELD_VALUE + assert row_3_error.error_type == ParserErrorCategoryChoices.PRE_CHECK assert row_3_error.error_message == error_message - assert row_3_error.content_type.model == 'tanf_t2' - assert row_3_error.object_id is not None row_4_error = parser_errors.get(row_number=4, error_message=error_message) - assert row_4_error.error_type == ParserErrorCategoryChoices.FIELD_VALUE + assert row_4_error.error_type == ParserErrorCategoryChoices.PRE_CHECK assert row_4_error.error_message == error_message - assert row_4_error.content_type.model == 'tanf_t3' - assert row_4_error.object_id is not None error_message = 'Unknown Record_Type was found.' row_5_error = parser_errors.get(row_number=5, error_message=error_message) @@ -777,31 +773,25 @@ def test_parse_bad_ssp_s1_missing_required(bad_ssp_s1__row_missing_required_fiel parse.parse_datafile(bad_ssp_s1__row_missing_required_field) parser_errors = ParserError.objects.filter(file=bad_ssp_s1__row_missing_required_field) - assert parser_errors.count() == 9 + assert parser_errors.count() == 5 row_2_error = parser_errors.get( row_number=2, - error_message='RPT_MONTH_YEAR is required but a value was not provided.' + error_message='Reporting month year None does not match file reporting year:2019, quarter:Q1.' ) - assert row_2_error.error_type == ParserErrorCategoryChoices.FIELD_VALUE - assert row_2_error.content_type.model == 'ssp_m1' - assert row_2_error.object_id is not None + assert row_2_error.error_type == ParserErrorCategoryChoices.PRE_CHECK row_3_error = parser_errors.get( row_number=3, - error_message='RPT_MONTH_YEAR is required but a value was not provided.' + error_message='Reporting month year None does not match file reporting year:2019, quarter:Q1.' ) - assert row_3_error.error_type == ParserErrorCategoryChoices.FIELD_VALUE - assert row_3_error.content_type.model == 'ssp_m2' - assert row_3_error.object_id is not None + assert row_3_error.error_type == ParserErrorCategoryChoices.PRE_CHECK row_4_error = parser_errors.get( row_number=4, - error_message='RPT_MONTH_YEAR is required but a value was not provided.' + error_message='Reporting month year None does not match file reporting year:2019, quarter:Q1.' ) - assert row_4_error.error_type == ParserErrorCategoryChoices.FIELD_VALUE - assert row_4_error.content_type.model == 'ssp_m3' - assert row_4_error.object_id is not None + assert row_4_error.error_type == ParserErrorCategoryChoices.PRE_CHECK row_5_error = parser_errors.get( row_number=5, @@ -1064,7 +1054,7 @@ def test_parse_ssp_section4_file(ssp_section4_file): @pytest.fixture def ssp_section2_file(stt_user, stt): - """Fixture for ADS.E2J.FTP4.TS06.""" + """Fixture for ADS.E2J.NDM2.MS24.""" return util.create_test_datafile('ADS.E2J.NDM2.MS24', stt_user, stt, 'SSP Closed Case Data') @pytest.mark.django_db() @@ -1078,8 +1068,8 @@ def test_parse_ssp_section2_file(ssp_section2_file): m4_objs = SSP_M4.objects.all().order_by('id') m5_objs = SSP_M5.objects.all().order_by('AMOUNT_EARNED_INCOME') - expected_m4_count = 2205 - expected_m5_count = 6736 + expected_m4_count = 231 + expected_m5_count = 703 assert SSP_M4.objects.all().count() == expected_m4_count assert SSP_M5.objects.all().count() == expected_m5_count @@ -1287,7 +1277,7 @@ def tribal_section_4_file(stt_user, stt): @pytest.mark.django_db() def test_parse_tribal_section_4_file(tribal_section_4_file): """Test parsing Tribal TANF Section 4 submission.""" - tribal_section_4_file.year = 2020 + tribal_section_4_file.year = 2021 tribal_section_4_file.quarter = 'Q1' parse.parse_datafile(tribal_section_4_file) From 9b62b1cd5da401c5dce0135805b3ef8c2ce51a27 Mon Sep 17 00:00:00 2001 From: Eric Lipe <125676261+elipe17@users.noreply.github.com> Date: Fri, 16 Feb 2024 07:43:55 -0700 Subject: [PATCH 039/149] Case Number Validators (#2834) * - Updated case number validators to accept any characters and only throw error on values of all spaces * - Add preparser check for case number * - fixed tests * - fix lint --------- Co-authored-by: Alex P <63075587+ADPennington@users.noreply.github.com> --- .../tdpservice/parsers/schema_defs/ssp/m1.py | 3 +- .../tdpservice/parsers/schema_defs/ssp/m2.py | 3 +- .../tdpservice/parsers/schema_defs/ssp/m3.py | 6 ++-- .../tdpservice/parsers/schema_defs/ssp/m4.py | 3 +- .../tdpservice/parsers/schema_defs/ssp/m5.py | 3 +- .../tdpservice/parsers/schema_defs/tanf/t1.py | 3 +- .../tdpservice/parsers/schema_defs/tanf/t2.py | 3 +- .../tdpservice/parsers/schema_defs/tanf/t3.py | 6 ++-- .../tdpservice/parsers/schema_defs/tanf/t4.py | 3 +- .../tdpservice/parsers/schema_defs/tanf/t5.py | 3 +- .../parsers/schema_defs/tribal_tanf/t1.py | 3 +- .../parsers/schema_defs/tribal_tanf/t2.py | 3 +- .../parsers/schema_defs/tribal_tanf/t3.py | 6 ++-- .../parsers/schema_defs/tribal_tanf/t4.py | 3 +- .../parsers/schema_defs/tribal_tanf/t5.py | 3 +- .../tdpservice/parsers/test/test_parse.py | 30 ++++++++++--------- 16 files changed, 52 insertions(+), 32 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m1.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m1.py index a7c63e2ab..7f99dc856 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m1.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m1.py @@ -12,6 +12,7 @@ document=SSP_M1DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(150), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.if_then_validator( @@ -125,7 +126,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()] + validators=[validators.notEmpty()] ), Field( item="2", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py index 390cf5480..af5a8c0bd 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py @@ -14,6 +14,7 @@ document=SSP_M2DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(150), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.validate__FAM_AFF__SSN(), @@ -152,7 +153,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()] + validators=[validators.notEmpty()] ), Field( item="26", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py index 90ecdcc05..1ed05c6d7 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py @@ -11,6 +11,7 @@ document=SSP_M3DataSubmissionDocument(), preparsing_validators=[ validators.notEmpty(start=19, end=60), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.if_then_validator( @@ -118,7 +119,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()] + validators=[validators.notEmpty()] ), Field( item="60", @@ -318,6 +319,7 @@ quiet_preparser_errors=True, preparsing_validators=[ validators.notEmpty(start=60, end=101), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.if_then_validator( @@ -425,7 +427,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()] + validators=[validators.notEmpty()] ), Field( item="60", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m4.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m4.py index 070a56459..ccda94c83 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m4.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m4.py @@ -12,6 +12,7 @@ document=SSP_M4DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(66), + validators.notEmpty(8, 19) ], postparsing_validators=[], fields=[ @@ -46,7 +47,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="2", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py index c23a69bd5..6dccd5a23 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py @@ -14,6 +14,7 @@ document=SSP_M5DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(66), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.if_then_validator( @@ -131,7 +132,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="13", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t1.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t1.py index 84e86eb7e..7ed9d55de 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t1.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t1.py @@ -12,6 +12,7 @@ document=TANF_T1DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(156), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.if_then_validator( @@ -149,7 +150,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="2", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py index f1a79cb65..87b887603 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py @@ -14,6 +14,7 @@ document=TANF_T2DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(156), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.validate__FAM_AFF__SSN(), @@ -154,7 +155,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="30", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py index 72067fb76..d61d102b8 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py @@ -12,6 +12,7 @@ document=TANF_T3DataSubmissionDocument(), preparsing_validators=[ validators.notEmpty(start=19, end=60), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.if_then_validator( @@ -116,7 +117,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="67", @@ -316,6 +317,7 @@ quiet_preparser_errors=True, preparsing_validators=[ validators.notEmpty(start=60, end=101), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.if_then_validator( @@ -420,7 +422,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="67", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t4.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t4.py index 03564c2c4..69485920e 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t4.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t4.py @@ -13,6 +13,7 @@ document=TANF_T4DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(71), + validators.notEmpty(8, 19) ], postparsing_validators=[], fields=[ @@ -47,7 +48,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="2", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t5.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t5.py index afa0d119e..fdb16a10e 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t5.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t5.py @@ -14,6 +14,7 @@ document=TANF_T5DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(71), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.if_then_validator( @@ -132,7 +133,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="14", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t1.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t1.py index 861b355e6..5dfae4856 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t1.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t1.py @@ -12,6 +12,7 @@ document=Tribal_TANF_T1DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(122), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.if_then_validator( @@ -149,7 +150,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="2", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py index fe88a4284..752685113 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py @@ -14,6 +14,7 @@ document=Tribal_TANF_T2DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(122), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.validate__FAM_AFF__SSN(), @@ -143,7 +144,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="30", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py index 59b80fbb5..c38f9bdc9 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py @@ -13,6 +13,7 @@ preparsing_validators=[ validators.notEmpty(start=19, end=60), validators.hasLength(122), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.if_then_validator( @@ -117,7 +118,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="66", @@ -318,6 +319,7 @@ preparsing_validators=[ validators.notEmpty(start=60, end=101), validators.hasLength(122), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.if_then_validator( @@ -422,7 +424,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="66", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t4.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t4.py index 71ac7309c..104dcea40 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t4.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t4.py @@ -12,6 +12,7 @@ document=Tribal_TANF_T4DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(71), + validators.notEmpty(8, 19) ], postparsing_validators=[], fields=[ @@ -46,7 +47,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="2", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py index a16b2c018..7278c6ab8 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py @@ -14,6 +14,7 @@ document=Tribal_TANF_T5DataSubmissionDocument(), preparsing_validators=[ validators.hasLength(71), + validators.notEmpty(8, 19) ], postparsing_validators=[ validators.if_then_validator( @@ -126,7 +127,7 @@ startIndex=8, endIndex=19, required=True, - validators=[validators.isAlphaNumeric()], + validators=[validators.notEmpty()], ), Field( item="14", diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index ffed26422..67fa3bf8c 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -325,7 +325,7 @@ def test_parse_bad_trailer_file(bad_trailer_file, dfs): errors = parse.parse_datafile(bad_trailer_file) parser_errors = ParserError.objects.filter(file=bad_trailer_file) - assert parser_errors.count() == 2 + assert parser_errors.count() == 3 trailer_error = parser_errors.get(row_number=3) assert trailer_error.error_type == ParserErrorCategoryChoices.PRE_CHECK @@ -333,15 +333,16 @@ def test_parse_bad_trailer_file(bad_trailer_file, dfs): assert trailer_error.content_type is None assert trailer_error.object_id is None - row_error = parser_errors.get(row_number=2) - assert row_error.error_type == ParserErrorCategoryChoices.PRE_CHECK - assert row_error.error_message == 'Value length 7 does not match 156.' - assert row_error.content_type is None - assert row_error.object_id is None + row_errors = list(parser_errors.filter(row_number=2).order_by("id")) + length_error = row_errors[0] + assert length_error.error_type == ParserErrorCategoryChoices.PRE_CHECK + assert length_error.error_message == 'Value length 7 does not match 156.' + assert length_error.content_type is None + assert length_error.object_id is None assert errors == { 'trailer': [trailer_error], - "2_0": [row_error] + "2_0": row_errors } @@ -359,7 +360,7 @@ def test_parse_bad_trailer_file2(bad_trailer_file_2): errors = parse.parse_datafile(bad_trailer_file_2) parser_errors = ParserError.objects.filter(file=bad_trailer_file_2) - assert parser_errors.count() == 4 + assert parser_errors.count() == 5 trailer_errors = parser_errors.filter(row_number=3).order_by('id') @@ -381,15 +382,16 @@ def test_parse_bad_trailer_file2(bad_trailer_file_2): assert row_2_error.content_type is None assert row_2_error.object_id is None - row_3_error = trailer_errors[2] - assert row_3_error.error_type == ParserErrorCategoryChoices.PRE_CHECK - assert row_3_error.error_message == 'Value length 7 does not match 156.' - assert row_3_error.content_type is None - assert row_3_error.object_id is None + row_3_errors = [trailer_errors[2], trailer_errors[3]] + length_error = row_3_errors[0] + assert length_error.error_type == ParserErrorCategoryChoices.PRE_CHECK + assert length_error.error_message == 'Value length 7 does not match 156.' + assert length_error.content_type is None + assert length_error.object_id is None assert errors == { "2_0": [row_2_error], - "3_0": [row_3_error], + "3_0": row_3_errors, "trailer": [trailer_error_1, trailer_error_2], } From 81598f8eb871cb48e2feb1fe1267cce5a2e2eae0 Mon Sep 17 00:00:00 2001 From: Andrew <84722778+andrew-jameson@users.noreply.github.com> Date: Tue, 20 Feb 2024 09:28:30 -0500 Subject: [PATCH 040/149] Feat/2813 reduce dev env (#2827) * Deletions from global search, need manual intervention in github/circleci cfg * Updated diagram * removing documentation references to sandbox env * Updating png, forgot to export. --------- Co-authored-by: andrew-jameson Co-authored-by: Alex P <63075587+ADPennington@users.noreply.github.com> --- .circleci/README.md | 2 +- .../008-deployment-flow.md | 3 +- .../TDP-environments-README.md | 1 - .../diagrams/tdp-environments.drawio | 744 +++++++++++++++++- .../diagrams/tdp-environments.drawio.png | Bin 0 -> 380809 bytes scripts/update-ssh-config.sh | 2 +- terraform/dev/variables.tf | 2 +- 7 files changed, 747 insertions(+), 7 deletions(-) create mode 100644 docs/Technical-Documentation/diagrams/tdp-environments.drawio.png diff --git a/.circleci/README.md b/.circleci/README.md index 1b0c4c031..785695735 100644 --- a/.circleci/README.md +++ b/.circleci/README.md @@ -79,7 +79,7 @@ updated along with the secondary apps running in CF. ### Frontend/Backend - Before updating, make sure the current buildpacks that these apps use are supported by the latest OS. If they aren't you can update the manifest to point them to the correct buildpacks. - - To update the apps you can either deploy each of the environments (sandbox, raft, qasp, etc) from CircleCi or you can use the `tdrs-deploy ` command from `commands.sh`. Assuming the buildpacks are up to date, that is all you need to do. + - To update the apps you can either deploy each of the environments (raft, qasp, etc) from CircleCi or you can use the `tdrs-deploy ` command from `commands.sh`. Assuming the buildpacks are up to date, that is all you need to do. ### Secondary apps - Before you can make the update, you need to ensure you have the CF plugin that allows you to do so. Download the binary for your respective OS [HERE](https://github.com/cloudfoundry/stack-auditor/releases) and follow the installation instructions [HERE](https://docs.cloudfoundry.org/adminguide/stack-auditor.html#install). diff --git a/docs/Technical-Documentation/Architecture-Decision-Record/008-deployment-flow.md b/docs/Technical-Documentation/Architecture-Decision-Record/008-deployment-flow.md index 8701f405e..613a4bfd7 100644 --- a/docs/Technical-Documentation/Architecture-Decision-Record/008-deployment-flow.md +++ b/docs/Technical-Documentation/Architecture-Decision-Record/008-deployment-flow.md @@ -33,7 +33,6 @@ Within the dev space, there is no correlation for branch to environment as these | Dev Site | Frontend URL | Backend URL | Purpose | | -------- | -------- | -------- |--------------------------------------------------| -| Sandbox | https://tdp-frontend-sandbox.app.cloud.gov | https://tdp-backend-sandbox.app.cloud.gov/admin/ | Space for development in a deployed environment | | A11y | https://tdp-frontend-a11y.app.cloud.gov | https://tdp-backend-a11y.app.cloud.gov/admin/ | Space for accessibility testing | | QASP | https://tdp-frontend-qasp.app.cloud.gov | https://tdp-backend-qasp.app.cloud.gov/admin/ | Space for QASP review | | raft | https://tdp-frontend-raft.app.cloud.gov | https://tdp-backend-raft.app.cloud.gov/admin/ | Space for Raft review | @@ -53,4 +52,4 @@ Within the dev space, there is no correlation for branch to environment as these ## Notes -- As of June 2022, CircleCI supplies environment variable key-value pairs to multiple environments (e.g. vendor's CircleCI deploys applications to dev and staging environments). The values from CircleCI are expected to be unique per environment, so until [#1826](https://github.com/raft-tech/TANF-app/issues/1826) is researched and addressed, these values will need to be manually corrected in cloud.gov immediately following the execution of the execution of the [`-deployment` CircleCI workflow](../../.circleci/config.yml) CircleCI workflow. This workaround applies to backend applications in the TDP staging environment. \ No newline at end of file +- As of June 2022, CircleCI supplies environment variable key-value pairs to multiple environments (e.g. vendor's CircleCI deploys applications to dev and staging environments). The values from CircleCI are expected to be unique per environment, so until [#1826](https://github.com/raft-tech/TANF-app/issues/1826) is researched and addressed, these values will need to be manually corrected in cloud.gov immediately following the execution of the execution of the [`-deployment` CircleCI workflow](../../.circleci/config.yml) CircleCI workflow. This workaround applies to backend applications in the TDP staging environment. diff --git a/docs/Technical-Documentation/TDP-environments-README.md b/docs/Technical-Documentation/TDP-environments-README.md index 62e396688..162ac7275 100644 --- a/docs/Technical-Documentation/TDP-environments-README.md +++ b/docs/Technical-Documentation/TDP-environments-README.md @@ -4,7 +4,6 @@ | Dev Site | Frontend URL | Backend URL | Purpose | | -------- | -------- | -------- | -------- | -| Sandbox | https://tdp-frontend-sandbox.app.cloud.gov | https://tdp-frontend-sandbox.app.cloud.gov/admin/ | Space for devs to test in a deployed environment | | A11y | https://tdp-frontend-a11y.app.cloud.gov | https://tdp-frontend-a11y.app.cloud.gov/admin/ | Space for accessibility testing | | QASP | https://tdp-frontend-qasp.app.cloud.gov | https://tdp-frontend-qasp.app.cloud.gov/admin/ | Space for QASP review | | raft | https://tdp-frontend-raft.app.cloud.gov | https://tdp-frontend-raft.app.cloud.gov/admin/ | Space for raft review | diff --git a/docs/Technical-Documentation/diagrams/tdp-environments.drawio b/docs/Technical-Documentation/diagrams/tdp-environments.drawio index c95b343b6..255c6061e 100644 --- a/docs/Technical-Documentation/diagrams/tdp-environments.drawio +++ b/docs/Technical-Documentation/diagrams/tdp-environments.drawio @@ -1 +1,743 @@ -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 \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Technical-Documentation/diagrams/tdp-environments.drawio.png b/docs/Technical-Documentation/diagrams/tdp-environments.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..a56b1c516d95e5db3d1525453f6a9915d8f4254d GIT binary patch literal 380809 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

wI9(|oZ7u>t6&Jj1p@UjOCUnyhBdXfHJEm~YP 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 z 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 Kt|VMiclkdT*RKKq literal 0 HcmV?d00001 diff --git a/scripts/update-ssh-config.sh b/scripts/update-ssh-config.sh index 3b2dc2116..fd59ed0ce 100644 --- a/scripts/update-ssh-config.sh +++ b/scripts/update-ssh-config.sh @@ -3,7 +3,7 @@ sshcfg=~/.ssh/config echo "" > $sshcfg -for host in a11y raft qasp sandbox; do +for host in a11y raft qasp; do guid=$(cf curl /v3/apps/$(cf app "tdp-backend-$host" --guid)/processes | jq --raw-output '.resources | .[] | select(.type == "web").guid') echo "Host $host HostName ssh.fr.cloud.gov diff --git a/terraform/dev/variables.tf b/terraform/dev/variables.tf index 58173eaf1..e7a159945 100644 --- a/terraform/dev/variables.tf +++ b/terraform/dev/variables.tf @@ -40,5 +40,5 @@ variable "cf_user" { variable "dev_app_names" { type = list(string) description = "list of app names deployed in the dev environment" - default = ["a11y", "qasp", "raft", "sandbox"] + default = ["a11y", "qasp", "raft"] } From 44d6913cb014e9b6c1d1365b79bbe42cc1505cc7 Mon Sep 17 00:00:00 2001 From: Eric Lipe <125676261+elipe17@users.noreply.github.com> Date: Tue, 20 Feb 2024 07:41:21 -0700 Subject: [PATCH 041/149] SSN Cat2 Updates (#2829) * - Updated all cat2 SSN validators to allow all 1's through 9's - Fixed datafile that had encrypted SSN values * - Remove redundent length validator * - remove debug line --------- Co-authored-by: Alex P <63075587+ADPennington@users.noreply.github.com> Co-authored-by: Andrew <84722778+andrew-jameson@users.noreply.github.com> --- tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py | 2 +- tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py | 4 ++-- tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py | 2 +- tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py | 2 +- tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py | 4 ++-- tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py | 2 +- tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py | 4 ++-- tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py | 2 +- .../tdpservice/parsers/test/data/small_bad_tanf_s1.txt | 2 +- tdrs-backend/tdpservice/parsers/test/test_parse.py | 1 + 10 files changed, 13 insertions(+), 12 deletions(-) diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py index af5a8c0bd..7470315cb 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m2.py @@ -194,7 +194,7 @@ startIndex=29, endIndex=38, required=True, - validators=[validators.validateSSN()], + validators=[validators.isNumber()], is_encrypted=False ), Field( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py index 1ed05c6d7..3bd341926 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m3.py @@ -154,7 +154,7 @@ endIndex=37, required=True, is_encrypted=False, - validators=[validators.validateSSN()] + validators=[validators.isNumber()] ), Field( item="63A", @@ -462,7 +462,7 @@ endIndex=78, required=True, is_encrypted=False, - validators=[validators.validateSSN()] + validators=[validators.isNumber()] ), Field( item="63A", diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py index 6dccd5a23..32d870ccb 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/ssp/m5.py @@ -166,7 +166,7 @@ startIndex=28, endIndex=37, required=True, - validators=[validators.validateSSN()], + validators=[validators.isNumber()], is_encrypted=False, ), Field( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py index 87b887603..27d47b3e0 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t2.py @@ -199,7 +199,7 @@ startIndex=29, endIndex=38, required=True, - validators=[validators.validateSSN()], + validators=[validators.isNumber()], is_encrypted=False, ), Field( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py index d61d102b8..4e769e370 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tanf/t3.py @@ -151,7 +151,7 @@ startIndex=28, endIndex=37, required=True, - validators=[validators.validateSSN()], + validators=[validators.isNumber()], is_encrypted=False, ), Field( @@ -456,7 +456,7 @@ startIndex=69, endIndex=78, required=True, - validators=[validators.validateSSN()], + validators=[validators.isNumber()], is_encrypted=False, ), Field( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py index 752685113..83ea13dbc 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t2.py @@ -188,7 +188,7 @@ startIndex=29, endIndex=38, required=True, - validators=[validators.validateSSN()], + validators=[validators.isNumber()], is_encrypted=False, ), Field( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py index c38f9bdc9..f631ab5a1 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t3.py @@ -152,7 +152,7 @@ startIndex=28, endIndex=37, required=True, - validators=[validators.validateSSN()], + validators=[validators.isNumber()], is_encrypted=False, ), Field( @@ -458,7 +458,7 @@ startIndex=69, endIndex=78, required=True, - validators=[validators.validateSSN()], + validators=[validators.isNumber()], is_encrypted=False, ), Field( diff --git a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py index 7278c6ab8..2a4bcf131 100644 --- a/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py +++ b/tdrs-backend/tdpservice/parsers/schema_defs/tribal_tanf/t5.py @@ -161,7 +161,7 @@ startIndex=28, endIndex=37, required=True, - validators=[validators.validateSSN()], + validators=[validators.isNumber()], is_encrypted=False, ), Field( diff --git a/tdrs-backend/tdpservice/parsers/test/data/small_bad_tanf_s1.txt b/tdrs-backend/tdpservice/parsers/test/data/small_bad_tanf_s1.txt index c40e6ffe4..c7e5c38ea 100644 --- a/tdrs-backend/tdpservice/parsers/test/data/small_bad_tanf_s1.txt +++ b/tdrs-backend/tdpservice/parsers/test/data/small_bad_tanf_s1.txt @@ -1,4 +1,4 @@ -HEADER20204A06 TAN1 N +HEADER20204A06 TAN1EN T1 1111111114721801401711120212110374300000000000003820060000000000000000000000000000000000222222000000002229012 T2 111111111471219800223WTTTT@TTW2222212222221012212110065423010700000000000000000000000000000000000000000000000000000000000000000000000000000000000000 T3 11111111147120201101WTTTTTZWY22222112204398100000000 diff --git a/tdrs-backend/tdpservice/parsers/test/test_parse.py b/tdrs-backend/tdpservice/parsers/test/test_parse.py index 67fa3bf8c..54d680e14 100644 --- a/tdrs-backend/tdpservice/parsers/test/test_parse.py +++ b/tdrs-backend/tdpservice/parsers/test/test_parse.py @@ -719,6 +719,7 @@ def test_parse_bad_tfs1_missing_required(bad_tanf_s1__row_missing_required_field parser_errors = ParserError.objects.filter( file=bad_tanf_s1__row_missing_required_field) + assert parser_errors.count() == 4 error_message = 'RPT_MONTH_YEAR is required but a value was not provided.' From ae6894e7e621c04990b9f308b470564603960fbb Mon Sep 17 00:00:00 2001 From: Andrew <84722778+andrew-jameson@users.noreply.github.com> Date: Tue, 20 Feb 2024 14:18:30 -0500 Subject: [PATCH 042/149] Update Failed-Deployment-Troubleshooting.md (#2855) * Update Failed-Deployment-Troubleshooting.md * Update Failed-Deployment-Troubleshooting.md --- .../Failed-Deployment-Troubleshooting.md | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/Technical-Documentation/Failed-Deployment-Troubleshooting.md b/docs/Technical-Documentation/Failed-Deployment-Troubleshooting.md index 19af7fc2a..497f36ca8 100644 --- a/docs/Technical-Documentation/Failed-Deployment-Troubleshooting.md +++ b/docs/Technical-Documentation/Failed-Deployment-Troubleshooting.md @@ -13,6 +13,7 @@ In preparation for production-ready infrastructure, we wanted to create a living + [CircleCI failures](./Failed-Deployment-Troubleshooting.md#circleci-failures) + [Runtime failures](./Failed-Deployment-Troubleshooting.md#compilationruntime-failure) + [App Connectivity issues](./Failed-Deployment-Troubleshooting.md#app-connectivity-issues) ++ [App roll-back](./Failed-Deployment-Troubleshooting.md#revision-rollback) ## CircleCI failures **Symptom:** I deployed new code (via merging) but the app in Cloud.gov didn't update and is still running old code. @@ -77,4 +78,26 @@ export DJANGO_SU_NAME=yourname@goraft.tech export LOGGING_LEVEL=DEBUG [...] bash scripts/deploy-backend.sh rebuild tdp-backend-raft tanf-dev -``` \ No newline at end of file +``` + +## Revision Rollback + +First we need to get list of revisions and select a stable revision id. +```cf revisions {app-name}``` + +Then use the last successful guid, we can populate this reversion command: +``` +cf curl v3/deployments \ +-X POST \ +-d '{ + "revision": { + "guid": "{last stable guid from list above}" + }, + "relationships": { + "app": { + "data": { + "guid": "{current app guid}" + } + } + } +}'``` From 382c856707ad91e6c58c358d19640a61dcf05778 Mon Sep 17 00:00:00 2001 From: jtimpe <111305129+jtimpe@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:30:37 -0500 Subject: [PATCH 043/149] Fix XSS vulnerability (pentest findings) (#2836) * add escapeHtml util * impl escapeHtml when rendering file names un-safely --------- Co-authored-by: Alex P <63075587+ADPennington@users.noreply.github.com> Co-authored-by: Andrew <84722778+andrew-jameson@users.noreply.github.com> --- .../src/components/FileUpload/utils.jsx | 4 +++- tdrs-frontend/src/utils/escapeHtml.js | 12 ++++++++++++ tdrs-frontend/src/utils/escapeHtml.test.js | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 tdrs-frontend/src/utils/escapeHtml.js create mode 100644 tdrs-frontend/src/utils/escapeHtml.test.js diff --git a/tdrs-frontend/src/components/FileUpload/utils.jsx b/tdrs-frontend/src/components/FileUpload/utils.jsx index 5bd920807..96a74fe0d 100644 --- a/tdrs-frontend/src/components/FileUpload/utils.jsx +++ b/tdrs-frontend/src/components/FileUpload/utils.jsx @@ -1,5 +1,7 @@ //This file contains modified versions of code from: //https://github.com/uswds/uswds/blob/develop/src/js/components/file-input.js +import escapeHtml from '../../utils/escapeHtml' + export const PREFIX = 'usa' export const PREVIEW_HEADING_CLASS = `${PREFIX}-file-input__preview-heading` @@ -78,7 +80,7 @@ export const handlePreview = (fileName, targetClassName) => { 'afterend', `