Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3102 Group Real Time Oral Argument Alerts #4251

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 22 additions & 51 deletions cl/alerts/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
131 changes: 107 additions & 24 deletions cl/alerts/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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"]
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading