diff --git a/cl/alerts/tasks.py b/cl/alerts/tasks.py index 3d19a993b8..64db406bc2 100644 --- a/cl/alerts/tasks.py +++ b/cl/alerts/tasks.py @@ -655,8 +655,6 @@ def percolator_response_processing(response: PercolatorResponsesType) -> None: return None scheduled_hits_to_create = [] - email_alerts_to_send = [] - rt_alerts_to_send = [] ( main_alerts_triggered, rd_alerts_triggered, @@ -755,64 +753,37 @@ def percolator_response_processing(response: PercolatorResponsesType) -> None: # user's donations. send_webhook_alert_hits(alert_user, hits) - # Send RT Alerts for Audio. if ( alert_triggered.rate == Alert.REAL_TIME - and app_label_model == "audio.Audio" + and not alert_user.profile.is_member ): - if not alert_user.profile.is_member: - continue - - # Append alert RT email to be sent. - email_alerts_to_send.append((alert_user.pk, hits)) - rt_alerts_to_send.append(alert_triggered.pk) - - else: - if ( - alert_triggered.rate == Alert.REAL_TIME - and not alert_user.profile.is_member - ): - # Omit scheduling an RT alert if the user is not a member. - continue - # Schedule RT, DAILY, WEEKLY and MONTHLY Alerts - if scheduled_alert_hits_limit_reached( - alert_triggered.pk, - alert_triggered.user.pk, - instance_content_type, - object_id, - child_document, - ): - # Skip storing hits for this alert-user combination because - # the SCHEDULED_ALERT_HITS_LIMIT has been reached. - continue + # Omit scheduling an RT alert if the user is not a member. + continue + # Schedule RT, DAILY, WEEKLY and MONTHLY Alerts + if scheduled_alert_hits_limit_reached( + alert_triggered.pk, + alert_triggered.user.pk, + instance_content_type, + object_id, + child_document, + ): + # Skip storing hits for this alert-user combination because + # the SCHEDULED_ALERT_HITS_LIMIT has been reached. + continue - scheduled_hits_to_create.append( - ScheduledAlertHit( - user=alert_triggered.user, - alert=alert_triggered, - document_content=document_content_copy, - content_type=instance_content_type, - object_id=object_id, - ) + scheduled_hits_to_create.append( + ScheduledAlertHit( + user=alert_triggered.user, + alert=alert_triggered, + document_content=document_content_copy, + content_type=instance_content_type, + object_id=object_id, ) + ) # Create scheduled RT, DAILY, WEEKLY and MONTHLY Alerts in bulk. if scheduled_hits_to_create: ScheduledAlertHit.objects.bulk_create(scheduled_hits_to_create) - # Sent all the related document RT emails. - if email_alerts_to_send: - send_search_alert_emails.delay(email_alerts_to_send, schedule_alert) - - # Update RT Alerts date_last_hit, increase stats and log RT alerts sent. - if rt_alerts_to_send: - Alert.objects.filter(pk__in=rt_alerts_to_send).update( - date_last_hit=now() - ) - alerts_sent = len(rt_alerts_to_send) - async_to_sync(tally_stat)( - f"alerts.sent.{Alert.REAL_TIME}", inc=alerts_sent - ) - logger.info(f"Sent {alerts_sent} {Alert.REAL_TIME} email alerts.") # TODO: Remove after scheduled OA alerts have been processed. diff --git a/cl/alerts/tests/tests.py b/cl/alerts/tests/tests.py index 633b1d030b..97d9990023 100644 --- a/cl/alerts/tests/tests.py +++ b/cl/alerts/tests/tests.py @@ -73,10 +73,14 @@ Opinion, RECAPDocument, ) -from cl.search.tasks import add_items_to_solr from cl.stats.models import Stat from cl.tests.base import SELENIUM_TIMEOUT, BaseSeleniumTest -from cl.tests.cases import APITestCase, ESIndexTestCase, TestCase +from cl.tests.cases import ( + APITestCase, + ESIndexTestCase, + RECAPAlertsAssertions, + TestCase, +) from cl.tests.utils import MockResponse, make_client from cl.users.factories import UserFactory, UserProfileWithParentsFactory from cl.users.models import EmailSent @@ -1834,7 +1838,7 @@ def test_get_docket_notes_and_tags_by_user(self) -> None: "cl.lib.es_signal_processor.allow_es_audio_indexing", side_effect=lambda x, y: True, ) -class SearchAlertsOAESTests(ESIndexTestCase, TestCase): +class SearchAlertsOAESTests(ESIndexTestCase, TestCase, RECAPAlertsAssertions): """Test ES Search Alerts""" @classmethod @@ -1977,11 +1981,22 @@ def test_send_oa_search_alert_webhooks(self, mock_abort_audio): stt_source=Audio.STT_OPENAI_WHISPER, ) + # Send RT alerts + with time_machine.travel(mock_date, tick=False): + call_command("cl_send_rt_percolator_alerts", testing_mode=True) # Confirm Alert date_last_hit is updated. self.search_alert.refresh_from_db() self.search_alert_2.refresh_from_db() - self.assertEqual(self.search_alert.date_last_hit, mock_date) - self.assertEqual(self.search_alert_2.date_last_hit, mock_date) + self.assertEqual( + self.search_alert.date_last_hit, + mock_date, + msg="Alert date of last hit didn't match.", + ) + self.assertEqual( + self.search_alert_2.date_last_hit, + mock_date, + msg="Alert date of last hit didn't match.", + ) webhooks_enabled = Webhook.objects.filter(enabled=True) self.assertEqual(len(webhooks_enabled), 1) @@ -2093,6 +2108,8 @@ def test_send_oa_search_alert_webhooks(self, mock_abort_audio): stt_transcript=transcript, ) + # Send RT alerts + call_command("cl_send_rt_percolator_alerts", testing_mode=True) self.assertEqual(len(mail.outbox), 3, msg="Wrong number of emails.") text_content = mail.outbox[2].body @@ -2142,6 +2159,9 @@ def test_send_alert_on_document_creation(self, mock_abort_audio): docket__docket_number="19-5735", ) + # Send RT alerts + call_command("cl_send_rt_percolator_alerts", testing_mode=True) + # Two OA search alert emails should be sent, one for user_profile and # one for user_profile_2 self.assertEqual(len(mail.outbox), 2) @@ -2166,6 +2186,8 @@ def test_send_alert_on_document_creation(self, mock_abort_audio): rt_oral_argument.sha1 = "12345" rt_oral_argument.save() + # Send RT alerts + call_command("cl_send_rt_percolator_alerts", testing_mode=True) # New alerts shouldn't be sent. Since document was just updated. self.assertEqual(len(mail.outbox), 2) text_content = mail.outbox[0].body @@ -2407,6 +2429,19 @@ def test_send_alert_multiple_alert_rates(self, mock_abort_audio): ) def test_group_alerts_and_hits(self, mock_logger, mock_abort_audio): """""" + + rt_oa_search_alert = AlertFactory( + user=self.user_profile.user, + rate=Alert.REAL_TIME, + name="Test RT Alert OA", + query="q=docketNumber:19-5739 OR docketNumber:19-5740&type=oa", + ) + rt_oa_search_alert_2 = AlertFactory( + user=self.user_profile.user, + rate=Alert.REAL_TIME, + name="Test RT Alert OA 2", + query="q=docketNumber:19-5741&type=oa", + ) with mock.patch( "cl.api.webhooks.requests.post", side_effect=lambda *args, **kwargs: MockResponse( @@ -2435,20 +2470,58 @@ def test_group_alerts_and_hits(self, mock_logger, mock_abort_audio): docket__docket_number="19-5741", ) - # No emails should be sent in RT, since all the alerts triggered by the - # OA documents added are not RT. - self.assertEqual(len(mail.outbox), 0) + # Send RT alerts + call_command("cl_send_rt_percolator_alerts", testing_mode=True) + + # 1 email should be sent for the rt_oa_search_alert and rt_oa_search_alert_2 + self.assertEqual( + len(mail.outbox), 1, msg="Wrong number of emails sent." + ) + + # The OA RT alert email should contain 2 alerts, one for rt_oa_search_alert + # and one for rt_oa_search_alert_2. First alert should contain 2 hits. + # Second alert should contain only 1 hit. + + # Assert text version. + text_content = mail.outbox[0].body + self.assertIn(rt_oral_argument_1.case_name, text_content) + self.assertIn(rt_oral_argument_2.case_name, text_content) + self.assertIn(rt_oral_argument_3.case_name, text_content) + + # Assert html version. + html_content = self.get_html_content_from_email(mail.outbox[0]) + self._confirm_number_of_alerts(html_content, 2) + self._count_alert_hits_and_child_hits( + html_content, + rt_oa_search_alert.name, + 2, + rt_oral_argument_1.case_name, + 0, + ) + self._count_alert_hits_and_child_hits( + html_content, + rt_oa_search_alert.name, + 2, + rt_oral_argument_2.case_name, + 0, + ) + self._count_alert_hits_and_child_hits( + html_content, + rt_oa_search_alert_2.name, + 1, + rt_oral_argument_3.case_name, + 0, + ) - # 7 webhook events should be triggered in RT: - # rt_oral_argument_1 should trigger 3: search_alert_3, search_alert_5 - # and search_alert_6. - # rt_oral_argument_2 should trigger 3: search_alert_3, search_alert_5 - # and search_alert_6. - # rt_oral_argument_3 should trigger 1: search_alert_4 - # One webhook event should be sent to user_profile + # 10 webhook events should be triggered in RT: + # rt_oral_argument_1 should trigger 4: search_alert_3, search_alert_5, + # search_alert_6 and rt_oa_search_alert. + # rt_oral_argument_2 should trigger 4: search_alert_3, search_alert_5, + # search_alert_6 and rt_oa_search_alert. + # rt_oral_argument_3 should trigger 2: search_alert_4 and rt_oa_search_alert. webhook_events = WebhookEvent.objects.all() self.assertEqual( - len(webhook_events), 7, msg="Unexpected number of" "webhooks sent." + len(webhook_events), 10, msg="Unexpected number of webhooks sent." ) # 7 webhook event should be sent to user_profile for 4 different @@ -2459,6 +2532,8 @@ def test_group_alerts_and_hits(self, mock_logger, mock_abort_audio): self.search_alert_4.pk, self.search_alert_5.pk, self.search_alert_6.pk, + rt_oa_search_alert.pk, + rt_oa_search_alert_2.pk, ] for webhook_content in webhook_events: content = webhook_content.content["payload"] @@ -2478,8 +2553,10 @@ def test_group_alerts_and_hits(self, mock_logger, mock_abort_audio): # One OA search alert email should be sent. mock_logger.info.assert_called_with("Sent 1 dly email alerts.") - self.assertEqual(len(mail.outbox), 1) - text_content = mail.outbox[0].body + self.assertEqual( + len(mail.outbox), 2, msg="Wrong number of emails sent." + ) + text_content = mail.outbox[1].body # The right alert type template is used. self.assertIn("oral argument", text_content) @@ -2494,25 +2571,25 @@ def test_group_alerts_and_hits(self, mock_logger, mock_abort_audio): self.assertIn(self.search_alert_4.name, text_content) # Should not include the List-Unsubscribe-Post header. - self.assertIn("List-Unsubscribe", mail.outbox[0].extra_headers) - self.assertNotIn("List-Unsubscribe-Post", mail.outbox[0].extra_headers) + self.assertIn("List-Unsubscribe", mail.outbox[1].extra_headers) + self.assertNotIn("List-Unsubscribe-Post", mail.outbox[1].extra_headers) alert_list_url = reverse("disable_alert_list") self.assertIn( alert_list_url, - mail.outbox[0].extra_headers["List-Unsubscribe"], + mail.outbox[1].extra_headers["List-Unsubscribe"], ) self.assertIn( f"keys={self.search_alert_3.secret_key}", - mail.outbox[0].extra_headers["List-Unsubscribe"], + mail.outbox[1].extra_headers["List-Unsubscribe"], ) self.assertIn( f"keys={self.search_alert_4.secret_key}", - mail.outbox[0].extra_headers["List-Unsubscribe"], + mail.outbox[1].extra_headers["List-Unsubscribe"], ) # Extract HTML version. html_content = None - for content, content_type in mail.outbox[0].alternatives: + for content, content_type in mail.outbox[1].alternatives: if content_type == "text/html": html_content = content break @@ -2524,6 +2601,9 @@ def test_group_alerts_and_hits(self, mock_logger, mock_abort_audio): rt_oral_argument_2.delete() rt_oral_argument_3.delete() + # Remove test instances. + rt_oa_search_alert.delete() + @override_settings(ELASTICSEARCH_PAGINATION_BATCH_SIZE=5) def test_send_multiple_rt_alerts(self, mock_abort_audio): """Confirm all RT alerts are properly sent if the percolator response @@ -2586,6 +2666,9 @@ def test_send_multiple_rt_alerts(self, mock_abort_audio): docket__docket_number="19-5735", ) + # Send RT alerts + call_command("cl_send_rt_percolator_alerts", testing_mode=True) + # 11 OA search alert emails should be sent, one for each user that # had donated enough. self.assertEqual(len(mail.outbox), 11)