示例#1
0
    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"])
示例#2
0
 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"])
示例#3
0
 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()
示例#5
0
 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"])
示例#6
0
 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"])
示例#8
0
 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"],
     )
示例#10
0
 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()
示例#11
0
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}")
示例#12
0
    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)
示例#13
0
    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)
示例#14
0
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}")
示例#15
0
    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, [])
示例#16
0
    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}')
示例#17
0
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, [])
示例#18
0
 def test_get_report_type(self) -> None:
     context = PoMonthlyReportContext('us_va', self.recipient_data)
     self.assertEqual('po_monthly_report', context.get_report_type())
示例#19
0
 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"])