def test_attachment_content_missing_sections(self) -> None: """Given client details for just one section, it returns a formatted string to be used as the email attachment.""" recipient_data = { "revocations_clients": [ { "person_external_id": 456, "full_name": "MUNCH, EDVARD", "revocation_violation_type": "NEW_CRIME", "revocation_report_date": "2020-12-06", }, { "person_external_id": 111, "full_name": "MIRO, JOAN", "revocation_violation_type": "TECHNICAL", "revocation_report_date": "2020-12-10", }, ] } recipient = self.recipient.create_derived_recipient(recipient_data) context = PoMonthlyReportContext("US_ID", recipient) actual = context.get_prepared_data() expected = textwrap.dedent("""\ MONTHLY RECIDIVIZ REPORT Prepared on 11/05/2020, for Christopher // Revocations // [456] Munch, Edvard New Crime Revocation recommendation staffed on 12/06/2020 [111] Miro, Joan Technical Only Revocation recommendation staffed on 12/10/2020 Please send questions or data issues to [email protected]""") self.assertEqual(expected, actual["attachment_content"])
def test_congratulations_text_only_outperformed_region_averages(self) -> None: """Test that the congratulations text looks correct if only the region averages were outperformed.""" recipient_data = deepcopy(self.recipient_data) recipient_data["pos_discharges"] = "0" recipient_data["earned_discharges"] = "2" recipient_data["technical_revocations"] = "3" recipient_data["crime_revocations"] = "4" context = PoMonthlyReportContext('US_VA', recipient_data) actual = context.get_prepared_data() self.assertEqual("You out-performed other officers like you for 1 metric.", actual["congratulations_text"])
def test_congratulations_text_only_improved_from_last_month(self) -> None: """Test that the congratulations text looks correct if only the goals were met for the last month.""" recipient_data = deepcopy(self.recipient_data) recipient_data["pos_discharges_state_average"] = "6" recipient_data["pos_discharges_district_average"] = "6" recipient_data["earned_discharges"] = "0" recipient_data["technical_revocations"] = "3" recipient_data["crime_revocations"] = "4" context = PoMonthlyReportContext('US_VA', recipient_data) actual = context.get_prepared_data() self.assertEqual("You improved from last month for 1 metric.", actual["congratulations_text"])
class EmailGenerationTests(TestCase): """Tests for reporting/email_generation.py.""" def setUp(self) -> None: self.project_id_patcher = patch('recidiviz.utils.metadata.project_id') self.get_secret_patcher = patch('recidiviz.utils.secrets.get_secret') self.upload_string_to_storage_patcher = patch( 'recidiviz.reporting.email_generation.utils.upload_string_to_storage' ) test_secrets = {'po_report_cdn_static_IP': '123.456.7.8'} self.get_secret_patcher.start().side_effect = test_secrets.get self.project_id_patcher.start().return_value = 'recidiviz-test' self.mock_upload_string_to_storage = self.upload_string_to_storage_patcher.start( ) with open( os.path.join( f"{os.path.dirname(__file__)}/context/po_monthly_report", FIXTURE_FILE)) as fixture_file: self.recipient_data = json.loads(fixture_file.read()) self.state_code = "US_ID" self.mock_batch_id = 1 self.recipient_data["batch_id"] = self.mock_batch_id self.report_context = PoMonthlyReportContext(self.state_code, self.recipient_data) def tearDown(self) -> None: self.get_secret_patcher.stop() self.project_id_patcher.stop() self.upload_string_to_storage_patcher.stop() def test_generate(self) -> None: """Test that upload_string_to_storage is called with the correct bucket name, filepath, and prepared html template for the report context.""" template_path = self.report_context.get_html_template_filepath() with open(template_path) as html_file: prepared_data = self.report_context.get_prepared_data() html_template = Template(html_file.read()) prepared_html = html_template.substitute(prepared_data) generate(self.report_context) bucket_name = 'recidiviz-test-report-html' bucket_filepath = f'{self.mock_batch_id}/{self.recipient_data["email_address"]}.html' self.mock_upload_string_to_storage.assert_called_with( bucket_name, bucket_filepath, prepared_html, 'text/html') def test_generate_incomplete_data(self) -> None: """Test that upload_string_to_storage is not called and a KeyError is raised if the recipient data is missing a key needed for the HTML template.""" with self.assertRaises(KeyError): self.recipient_data.pop('officer_given_name') report_context = PoMonthlyReportContext(self.state_code, self.recipient_data) generate(report_context) self.mock_upload_string_to_storage.assert_not_called()
def test_congratulations_text_empty(self) -> None: """Test that the congratulations text is an empty string and the display is none if neither metric goals were met """ recipient_data = deepcopy(self.recipient_data) recipient_data["pos_discharges"] = "0" recipient_data["earned_discharges"] = "0" recipient_data["technical_revocations"] = "3" recipient_data["crime_revocations"] = "4" context = PoMonthlyReportContext('US_VA', recipient_data) actual = context.get_prepared_data() self.assertEqual("", actual["congratulations_text"]) self.assertEqual("none", actual["display_congratulations"])
def test_congratulations_text_only_improved_from_last_month(self) -> None: """Test that the congratulations text looks correct if only the goals were met for the last month.""" recipient_data = { "pos_discharges_state_average": "6", "pos_discharges_district_average": "6", "earned_discharges": "0", "technical_revocations": "3", "crime_revocations": "4", } recipient = self.recipient.create_derived_recipient(recipient_data) context = PoMonthlyReportContext("US_ID", recipient) actual = context.get_prepared_data() self.assertEqual("You improved from last month for 1 metric.", actual["congratulations_text"])
def test_message_body_override(self) -> None: """Test that the message body is overriden by the message_body_override""" recipient_data = { "pos_discharges": "0", "earned_discharges": "0", "supervision_downgrades": "0", "technical_revocations": "0", "crime_revocations": "0", "message_body_override": "THIS IS A TEST", } recipient = self.recipient.create_derived_recipient(recipient_data) context = PoMonthlyReportContext(StateCode.US_ID, recipient) actual = context.get_prepared_data() self.assertEqual("THIS IS A TEST", actual["message_body"])
def test_congratulations_text_empty(self) -> None: """Test that the congratulations text is an empty string and the display is none if neither metric goals were met """ recipient_data = { "pos_discharges": "0", "earned_discharges": "0", "technical_revocations": "3", "crime_revocations": "4", } recipient = self.recipient.create_derived_recipient(recipient_data) context = PoMonthlyReportContext("US_ID", recipient) actual = context.get_prepared_data() self.assertEqual("", actual["congratulations_text"]) self.assertEqual("none", actual["display_congratulations"])
def test_congratulations_text_only_outperformed_region_averages(self) -> None: """Test that the congratulations text looks correct if only the region averages were outperformed.""" recipient_data = { "pos_discharges": "0", "earned_discharges": "2", "supervision_downgrades": "0", "technical_revocations": "3", "crime_revocations": "4", } recipient = self.recipient.create_derived_recipient(recipient_data) context = PoMonthlyReportContext(StateCode.US_ID, recipient) actual = context.get_prepared_data() self.assertEqual( "You out-performed other officers like you for 1 metric.", actual["congratulations_text"], )
def test_generate_incomplete_data(self) -> None: """Test that upload_string_to_storage is not called and a KeyError is raised if the recipient data is missing a key needed for the HTML template.""" with self.assertRaises(KeyError): self.recipient_data.pop('officer_given_name') report_context = PoMonthlyReportContext(self.state_code, self.recipient_data) generate(report_context) self.mock_upload_string_to_storage.assert_not_called()
def get_report_context(state_code: str, report_type: str, recipient_data: dict) -> ReportContext: """Returns the appropriate report context for the given parameters, choosing the correct ReportContext implementation. Args: state_code: State identifier for the recipient report_type: The type of report to be sent to the recipient recipient_data: The retrieved data for this recipient """ if report_type == "po_monthly_report": return PoMonthlyReportContext(state_code, recipient_data) raise KeyError(f"Unrecognized report type: {report_type}")
def setUp(self) -> None: self.project_id_patcher = patch('recidiviz.utils.metadata.project_id') self.get_secret_patcher = patch('recidiviz.utils.secrets.get_secret') self.upload_string_to_storage_patcher = patch( 'recidiviz.reporting.email_generation.utils.upload_string_to_storage' ) test_secrets = {'po_report_cdn_static_IP': '123.456.7.8'} self.get_secret_patcher.start().side_effect = test_secrets.get self.project_id_patcher.start().return_value = 'recidiviz-test' self.mock_upload_string_to_storage = self.upload_string_to_storage_patcher.start( ) with open( os.path.join( f"{os.path.dirname(__file__)}/context/po_monthly_report", FIXTURE_FILE)) as fixture_file: self.recipient_data = json.loads(fixture_file.read()) self.state_code = "US_ID" self.mock_batch_id = 1 self.recipient_data["batch_id"] = self.mock_batch_id self.report_context = PoMonthlyReportContext(self.state_code, self.recipient_data)
def setUp(self) -> None: self.project_id_patcher = patch("recidiviz.utils.metadata.project_id") self.get_secret_patcher = patch("recidiviz.utils.secrets.get_secret") self.gcs_file_system_patcher = patch( "recidiviz.reporting.email_generation.GcsfsFactory.build" ) test_secrets = {"po_report_cdn_static_IP": "123.456.7.8"} self.get_secret_patcher.start().side_effect = test_secrets.get self.project_id_patcher.start().return_value = "recidiviz-test" self.gcs_file_system = FakeGCSFileSystem() self.mock_gcs_file_system = self.gcs_file_system_patcher.start() self.mock_gcs_file_system.return_value = self.gcs_file_system with open( os.path.join( f"{os.path.dirname(__file__)}/context/po_monthly_report", FIXTURE_FILE ) ) as fixture_file: self.recipient = Recipient.from_report_json(json.loads(fixture_file.read())) self.state_code = "US_ID" self.mock_batch_id = "1" self.recipient.data["batch_id"] = self.mock_batch_id self.report_context = PoMonthlyReportContext(self.state_code, self.recipient)
def get_report_context(state_code: StateCode, report_type: ReportType, recipient: Recipient) -> ReportContext: """Returns the appropriate report context for the given parameters, choosing the correct ReportContext implementation. Args: state_code: State identifier for the recipient report_type: The type of report to be sent to the recipient recipient: The retrieved data for this recipient """ if report_type == ReportType.POMonthlyReport: return PoMonthlyReportContext(state_code, recipient) if report_type == ReportType.TopOpportunities: return TopOpportunitiesReportContext(state_code, recipient) raise KeyError(f"Unrecognized report type: {report_type}")
def test_generate_incomplete_data(self) -> None: """Test that no files are added to Google Cloud Storage and a KeyError is raised if the recipient data is missing a key needed for the HTML template.""" with self.assertRaises(KeyError): recipient = Recipient.from_report_json( { "email_address": "*****@*****.**", "state_code": "US_ID", "district": "DISTRICT OFFICE 3", } ) report_context = PoMonthlyReportContext(self.state_code, recipient) generate(report_context) self.assertEqual(self.gcs_file_system.all_paths, [])
def test_prepare_for_generation(self) -> None: context = PoMonthlyReportContext('US_VA', self.recipient_data) actual = context.get_prepared_data() red = "#A43939" gray = "#7D9897" default_color = "#00413E" expected = deepcopy(self.recipient_data) expected["static_image_path"] = "http://123.456.7.8/US_VA/po_monthly_report/static" expected["review_month"] = "May" expected["greeting"] = "Hey there, Christopher!" # [improved] More pos_discharges than district and state average # [improved] Higher district average than state average expected["pos_discharges"] = "5" expected["pos_discharges_color"] = default_color expected["pos_discharges_change"] = "2 more than last month. You're on a roll!" expected["pos_discharges_change_color"] = gray expected["pos_discharges_district_average"] = "1.148" expected["pos_discharges_district_average_color"] = default_color expected["pos_discharges_state_average"] = "0.95" # [improved] More early discharges than district average # Lower district average compared to state average expected["earned_discharges"] = "1" expected["earned_discharges_color"] = red expected["earned_discharges_change"] = "2 fewer than last month." expected["earned_discharges_change_color"] = red expected["earned_discharges_district_average"] = "0.86" expected["earned_discharges_district_average_color"] = red expected["earned_discharges_state_average"] = "1.657" # [improved] Less technical revocations # [improved] Lower district average than state average expected["technical_revocations"] = "0" expected["technical_revocations_color"] = default_color expected["technical_revocations_district_average"] = "2.022" expected["technical_revocations_district_average_color"] = default_color expected["technical_revocations_state_average"] = "2.095" # [improved] Less crime revocations than district and state averages # [improved] Lower district average than state average expected["crime_revocations"] = "2" expected["crime_revocations_color"] = default_color expected["crime_revocations_district_average"] = "3.353" expected["crime_revocations_district_average_color"] = default_color expected["crime_revocations_state_average"] = "3.542" # Higher absconsions than district or state average # higher district average than state average expected["absconsions"] = "2" expected["absconsions_color"] = red expected["absconsions_district_average"] = "0.22" expected["absconsions_district_average_color"] = red expected["absconsions_state_average"] = "0.14" expected["total_revocations"] = "2" expected["assessment_percent"] = "73" expected["facetoface_percent"] = "45" expected["pos_discharges_label"] = "Successful Case Completions" expected["earned_discharges_label"] = "Early Discharge" expected["total_revocations_label"] = "Revocations" expected["absconsions_label"] = "Absconsions" expected["assessments_label"] = "Risk Assessments" expected["facetoface_label"] = "Face-to-Face Contacts" expected["display_congratulations"] = "inherit" expected["congratulations_text"] = "You improved from last month across 3 metrics and out-performed other " \ "officers like you across 4 metrics." for key, value in expected.items(): if key not in actual: print(f"Missing key: {key}") self.assertTrue(key in actual) for key, value in actual.items(): self.assertEqual(expected[key], value, f'key = {key}')
class EmailGenerationTests(TestCase): """Tests for reporting/email_generation.py.""" def setUp(self) -> None: self.project_id_patcher = patch("recidiviz.utils.metadata.project_id") self.get_secret_patcher = patch("recidiviz.utils.secrets.get_secret") self.gcs_file_system_patcher = patch( "recidiviz.reporting.email_generation.GcsfsFactory.build" ) test_secrets = {"po_report_cdn_static_IP": "123.456.7.8"} self.get_secret_patcher.start().side_effect = test_secrets.get self.project_id_patcher.start().return_value = "recidiviz-test" self.gcs_file_system = FakeGCSFileSystem() self.mock_gcs_file_system = self.gcs_file_system_patcher.start() self.mock_gcs_file_system.return_value = self.gcs_file_system with open( os.path.join( f"{os.path.dirname(__file__)}/context/po_monthly_report", FIXTURE_FILE ) ) as fixture_file: self.recipient = Recipient.from_report_json(json.loads(fixture_file.read())) self.state_code = "US_ID" self.mock_batch_id = "1" self.recipient.data["batch_id"] = self.mock_batch_id self.report_context = PoMonthlyReportContext(self.state_code, self.recipient) def tearDown(self) -> None: self.get_secret_patcher.stop() self.project_id_patcher.stop() self.gcs_file_system_patcher.stop() def test_generate(self) -> None: """Test that the prepared html is added to Google Cloud Storage with the correct bucket name, filepath, and prepared html template for the report context.""" template_path = self.report_context.get_html_template_filepath() with open(template_path) as html_file: prepared_data = self.report_context.get_prepared_data() html_template = Template(html_file.read()) prepared_html = html_template.substitute(prepared_data) generate(self.report_context) bucket_name = "recidiviz-test-report-html" bucket_filepath = ( f"{self.mock_batch_id}/html/{self.recipient.email_address}.html" ) path = GcsfsFilePath.from_absolute_path(f"gs://{bucket_name}/{bucket_filepath}") self.assertEqual(self.gcs_file_system.download_as_string(path), prepared_html) def test_generate_incomplete_data(self) -> None: """Test that no files are added to Google Cloud Storage and a KeyError is raised if the recipient data is missing a key needed for the HTML template.""" with self.assertRaises(KeyError): recipient = Recipient.from_report_json( { "email_address": "*****@*****.**", "state_code": "US_ID", "district": "DISTRICT OFFICE 3", } ) report_context = PoMonthlyReportContext(self.state_code, recipient) generate(report_context) self.assertEqual(self.gcs_file_system.all_paths, [])
def test_get_report_type(self) -> None: context = PoMonthlyReportContext('us_va', self.recipient_data) self.assertEqual('po_monthly_report', context.get_report_type())
def test_get_report_type(self) -> None: context = PoMonthlyReportContext("us_va", self.recipient) self.assertEqual("po_monthly_report", context.get_report_type())
def test_get_report_type(self) -> None: context = PoMonthlyReportContext(StateCode.US_ID, self.recipient) self.assertEqual(ReportType.POMonthlyReport, context.get_report_type())
def test_prepare_for_generation(self) -> None: context = PoMonthlyReportContext(StateCode.US_ID, self.recipient) actual = context.get_prepared_data() red = "#A43939" gray = "#7D9897" default_color = "#00413E" expected = deepcopy(self.recipient.data) expected[ "static_image_path" ] = "http://123.456.7.8/US_ID/po_monthly_report/static" expected["message_body"] = context.properties[DEFAULT_MESSAGE_BODY_KEY] expected["review_month"] = "May" expected["greeting"] = "Hey there, Christopher!" # No client data returns None for attachment_content expected["attachment_content"] = None # [improved] More pos_discharges than district and state average # [improved] Higher district average than state average expected["pos_discharges"] = "5" expected["pos_discharges_color"] = default_color expected["pos_discharges_change"] = "2 more than last month. You're on a roll!" expected["pos_discharges_change_color"] = gray expected["pos_discharges_district_average"] = "1.148" expected["pos_discharges_district_average_color"] = default_color expected["pos_discharges_state_average"] = "0.95" # [improved] More early discharges than district average # Lower district average compared to state average expected["earned_discharges"] = "1" expected["earned_discharges_color"] = red expected["earned_discharges_change"] = "2 fewer than last month." expected["earned_discharges_change_color"] = red expected["earned_discharges_district_average"] = "0.86" expected["earned_discharges_district_average_color"] = red expected["earned_discharges_state_average"] = "1.657" # [improved] More supervision downgrades than district average # Higher district average compared to state average expected["supervision_downgrades"] = "5" expected["supervision_downgrades_color"] = default_color expected[ "supervision_downgrades_change" ] = "2 more than last month. You're on a roll!" expected["supervision_downgrades_change_color"] = gray expected["supervision_downgrades_district_average"] = "2.346" expected["supervision_downgrades_district_average_color"] = default_color expected["supervision_downgrades_state_average"] = "1.765" # [improved] Less technical revocations # [improved] Lower district average than state average expected["technical_revocations"] = "0" expected["technical_revocations_color"] = default_color expected["technical_revocations_district_average"] = "2.022" expected["technical_revocations_district_average_color"] = default_color expected["technical_revocations_state_average"] = "2.095" # [improved] Less crime revocations than district and state averages # [improved] Lower district average than state average expected["crime_revocations"] = "2" expected["crime_revocations_color"] = default_color expected["crime_revocations_district_average"] = "3.353" expected["crime_revocations_district_average_color"] = default_color expected["crime_revocations_state_average"] = "3.542" # Higher absconsions than district or state average # higher district average than state average expected["absconsions"] = "2" expected["absconsions_color"] = red expected["absconsions_district_average"] = "0.22" expected["absconsions_district_average_color"] = red expected["absconsions_state_average"] = "0.14" expected["total_revocations"] = "2" expected["assessments_percent"] = "73" expected["facetoface_percent"] = "N/A" expected["pos_discharges_label"] = "Successful Case Completions" expected["earned_discharges_label"] = "Early Discharge" expected["supervision_downgrades_label"] = "Supervision Downgrades" expected["total_revocations_label"] = "Revocations" expected["absconsions_label"] = "Absconsions" expected["assessments_label"] = "Risk Assessments" expected["facetoface_label"] = "Face-to-Face Contacts" expected["display_congratulations"] = "inherit" expected["congratulations_text"] = ( "You improved from last month across 4 metrics and out-performed other " "officers like you across 5 metrics." ) expected[ "learn_more_link" ] = "https://docs.google.com/document/d/1kgG5LiIrFQaBupHYfoIwo59TCmYH5f_aIpRzGrtOkhU/edit#heading=h.r6s5tyc7ut6c" for key, value in expected.items(): if key not in actual: print(f"Missing key: {key}") self.assertTrue(key in actual) for key, value in actual.items(): self.assertEqual(expected[key], value, f"key = {key}") # Testing an instance where some metrics are not populated expected[ "static_image_path" ] = "http://123.456.7.8/US_PA/po_monthly_report/static" del expected["earned_discharges_color"] del expected["earned_discharges_change"] del expected["earned_discharges_change_color"] del expected["earned_discharges_district_average_color"] del expected["earned_discharges_label"] expected["earned_discharges"] = 0 expected["earned_discharges_last_month"] = 0 expected["earned_discharges_district_average"] = 0.0 expected["earned_discharges_state_average"] = 0.0 expected["congratulations_text"] = ( "You improved from last month across 4 metrics and out-performed other " "officers like you across 4 metrics." ) recipient = self.recipient.create_derived_recipient( { "earned_discharges": 0, "earned_discharges_last_month": 0, "earned_discharges_district_average": 0.0, "earned_discharges_state_average": 0.0, } ) context = PoMonthlyReportContext(StateCode.US_PA, recipient) actual = context.get_prepared_data() for key, value in expected.items(): if key not in actual: print(f"Missing key: {key}") self.assertTrue(key in actual) for key, value in actual.items(): self.assertEqual(expected[key], value, f"key = {key}")
def test_attachment_content(self) -> None: """Given client details for every section, it returns a formatted string to be used as the email attachment.""" recipient_data = { "pos_discharges_clients": [ { "person_external_id": 123, "full_name": "ROSS, BOB", "successful_completion_date": "2020-12-01", } ], "earned_discharges_clients": [ { "person_external_id": 321, "full_name": "POLLOCK, JACKSON", "earned_discharge_date": "2020-12-05", } ], "supervision_downgrades_clients": [ { "person_external_id": 246, "full_name": "GOYA, FRANCISCO", "latest_supervision_downgrade_date": "2020-12-07", "previous_supervision_level": "MEDIUM", "supervision_level": "MINIMUM", } ], "revocations_clients": [ { "person_external_id": 456, "full_name": "MUNCH, EDVARD", "revocation_violation_type": "NEW_CRIME", "revocation_report_date": "2020-12-06", }, { "person_external_id": 111, "full_name": "MIRO, JOAN", "revocation_violation_type": "TECHNICAL", "revocation_report_date": "2020-12-10", }, ], "absconsions_clients": [ { "person_external_id": 789, "full_name": "DALI, SALVADOR", "absconsion_report_date": "2020-12-11", } ], "assessments_out_of_date_clients": [ {"person_external_id": 987, "full_name": "KAHLO, FRIDA"} ], "facetoface_out_of_date_clients": [ {"person_external_id": 654, "full_name": "DEGAS, EDGAR"} ], } recipient = self.recipient.create_derived_recipient(recipient_data) context = PoMonthlyReportContext(StateCode.US_ID, recipient) actual = context.get_prepared_data() expected = textwrap.dedent( """\ MONTHLY RECIDIVIZ REPORT Prepared on 11/05/2020, for Christopher // Successful Case Completion // [123] Ross, Bob Supervision completed on 12/01/2020 // Early Discharge // [321] Pollock, Jackson Discharge granted on 12/05/2020 // Supervision Downgrades // [246] Goya, Francisco Supervision level downgraded on 12/07/2020 // Revocations // [111] Miro, Joan Technical Only Revocation recommendation staffed on 12/10/2020 [456] Munch, Edvard New Crime Revocation recommendation staffed on 12/06/2020 // Absconsions // [789] Dali, Salvador Absconsion reported on 12/11/2020 // Out of Date Risk Assessments // [987] Kahlo, Frida // Out of Date Face to Face Contacts // [654] Degas, Edgar Please send questions or data issues to [email protected] Please note: people on probation in custody who technically remain on your caseload are currently counted in your Key Supervision Task percentages, including contacts and risk assessments.""" ) self.maxDiff = None self.assertEqual(expected, actual["attachment_content"])