Skip to content

Commit

Permalink
feat(alerts): Trigger RECAP Alerts on related documents.
Browse files Browse the repository at this point in the history
- Group and send RT RECAP Search Alerts.
  • Loading branch information
albertisfu committed Jul 17, 2024
1 parent 8229a90 commit 32f8ca9
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 33 deletions.
34 changes: 34 additions & 0 deletions cl/alerts/management/commands/cl_send_rt_recap_alerts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import time

from cl.alerts.management.commands.cl_send_scheduled_alerts import (
send_scheduled_alerts,
)
from cl.alerts.models import Alert
from cl.lib.command_utils import VerboseCommand


class Command(VerboseCommand):
help = """Send RT Alerts for RECAP every 5 minutes. """

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.options = {}

def add_arguments(self, parser):
parser.add_argument(
"--testing-mode",
action="store_true",
help="Use this flag for testing purposes.",
)

def handle(self, *args, **options):
super().handle(*args, **options)
testing_mode = options.get("testing_mode", False)
while True:
send_scheduled_alerts(Alert.REAL_TIME)
if testing_mode:
# Perform only 1 iteration for testing purposes.
break

# Wait for 5 minutes.
time.sleep(300)
8 changes: 2 additions & 6 deletions cl/alerts/management/commands/cl_send_scheduled_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,12 @@ def query_and_send_alerts_by_rate(rate: str) -> None:


def send_scheduled_alerts(rate: str) -> None:
if rate == Alert.DAILY:
query_and_send_alerts_by_rate(Alert.DAILY)
elif rate == Alert.WEEKLY:
query_and_send_alerts_by_rate(Alert.WEEKLY)
elif rate == Alert.MONTHLY:
if rate == Alert.MONTHLY:
if datetime.date.today().day > 28:
raise InvalidDateError(
"Monthly alerts cannot be run on the 29th, 30th or 31st."
)
query_and_send_alerts_by_rate(Alert.MONTHLY)
query_and_send_alerts_by_rate(rate)


def delete_old_scheduled_alerts() -> int:
Expand Down
10 changes: 6 additions & 4 deletions cl/alerts/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,6 @@ def process_percolator_response(response: PercolatorResponseType) -> None:
data that triggered the alert.
:return: None
"""

if not response:
return None

Expand Down Expand Up @@ -630,7 +629,10 @@ def process_percolator_response(response: PercolatorResponseType) -> None:
send_webhook_alert_hits(alert_user, hits)

# Send RT Alerts
if alert_triggered.rate == Alert.REAL_TIME:
if (
alert_triggered.rate == Alert.REAL_TIME
and app_label == "audio.Audio"
):
if not alert_user.profile.is_member:
continue

Expand All @@ -653,6 +655,8 @@ def process_percolator_response(response: PercolatorResponseType) -> None:
document_content=document_content_copy,
)
)
if alert_triggered.rate == Alert.REAL_TIME:
rt_alerts_to_send.append(alert_triggered.pk)

# Create scheduled DAILY, WEEKLY and MONTHLY Alerts in bulk.
if scheduled_hits_to_create:
Expand Down Expand Up @@ -712,11 +716,9 @@ def send_or_schedule_alerts(
case "audio.Audio":
percolator_index = AudioPercolator._index._name
es_document_index = AudioDocument._index._name
app_label = None
case "search.Docket":
percolator_index = RECAPPercolator._index._name
es_document_index = DocketDocument._index._name
app_label = None
case "search.RECAPDocument":
percolator_index = RECAPPercolator._index._name
case _:
Expand Down
202 changes: 192 additions & 10 deletions cl/alerts/tests/tests_recap_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,26 @@
from cl.lib.elasticsearch_utils import do_es_sweep_alert_query
from cl.lib.redis_utils import get_redis_interface
from cl.lib.test_helpers import RECAPSearchTestCase
from cl.people_db.factories import (
AttorneyFactory,
AttorneyOrganizationFactory,
PartyFactory,
PartyTypeFactory,
)
from cl.search.documents import (
DocketDocument,
ESRECAPDocumentPlain,
RECAPPercolator,
RECAPSweepDocument,
)
from cl.search.factories import (
BankruptcyInformationFactory,
DocketEntryWithParentsFactory,
DocketFactory,
RECAPDocumentFactory,
)
from cl.search.models import Docket
from cl.search.tasks import index_docket_parties_in_es
from cl.tests.cases import ESIndexTestCase, RECAPAlertsAssertions, TestCase
from cl.tests.utils import MockResponse
from cl.users.factories import UserProfileWithParentsFactory
Expand Down Expand Up @@ -2007,7 +2015,7 @@ def test_percolate_document_on_ingestion(self) -> None:
docket_only_alert = AlertFactory(
user=self.user_profile.user,
rate=Alert.REAL_TIME,
name="Test Alert Docket Only",
name="Test Alert Docket Only 1",
query='q="SUBPOENAS SERVED CASE"&type=r',
)
with self.captureOnCommitCallbacks(execute=True):
Expand Down Expand Up @@ -2036,7 +2044,7 @@ def test_percolate_document_on_ingestion(self) -> None:
recap_only_alert = AlertFactory(
user=self.user_profile.user,
rate=Alert.REAL_TIME,
name="Test Alert RECAP Only",
name="Test Alert RECAP Only 2",
query='q="plain text for 018036652436"&type=r',
)
with self.captureOnCommitCallbacks(execute=True):
Expand Down Expand Up @@ -2082,7 +2090,7 @@ def test_percolate_document_on_ingestion(self) -> None:
de_entry_field_alert = AlertFactory(
user=self.user_profile.user,
rate=Alert.REAL_TIME,
name="Test Alert RECAP Only",
name="Test Alert RECAP Only 3",
query='q="Hearing for Leave"&type=r',
)
with self.captureOnCommitCallbacks(execute=True):
Expand Down Expand Up @@ -2123,21 +2131,33 @@ def test_percolate_document_on_ingestion(self) -> None:
1,
)

# RD update.
# DE/RD update.
de_entry_field_alert = AlertFactory(
user=self.user_profile.user,
rate=Alert.REAL_TIME,
name="Test Alert RECAP Only",
name="Test Alert RECAP Only 4",
query='q="Hearing to File Updated"&type=r',
)
with self.captureOnCommitCallbacks(execute=True):
rd_2.description = "Hearing to File Updated"
alert_de_2.description = "Hearing to File Updated"
alert_de_2.save()

# No alert should be triggered on DE updates.
self.assertEqual(
len(mail.outbox), 3, msg="Outgoing emails don't match."
)

# Alert is triggered only after a RECAPDocument creation/update to avoid
# percolating the same document twice.
with self.captureOnCommitCallbacks(execute=True):
rd_2.document_number = 1
rd_2.save()

html_content = self.get_html_content_from_email(mail.outbox[3])
self.assertEqual(
len(mail.outbox), 4, msg="Outgoing emails don't match."
)
html_content = self.get_html_content_from_email(mail.outbox[3])

self.assertIn(rd_2.description, html_content)
self.assertIn(de_entry_field_alert.name, html_content)
self._confirm_number_of_alerts(html_content, 1)
Expand All @@ -2154,7 +2174,7 @@ def test_percolate_document_on_ingestion(self) -> None:
docket_update_alert = AlertFactory(
user=self.user_profile.user,
rate=Alert.REAL_TIME,
name="Test Alert RECAP Only",
name="Test Alert Docket Only 5",
query='q="SUBPOENAS SERVED LOREM"&type=r',
)
with self.captureOnCommitCallbacks(execute=True):
Expand All @@ -2176,14 +2196,63 @@ def test_percolate_document_on_ingestion(self) -> None:
0,
)

# Percolate Docket upon Bankruptcy data is added/updated.
docket_only_alert = AlertFactory(
user=self.user_profile.user,
rate=Alert.REAL_TIME,
name="Test Alert Docket Only 6",
query="q=(SUBPOENAS SERVED) AND chapter:7&type=r",
)
with self.captureOnCommitCallbacks(execute=True):
BankruptcyInformationFactory(docket=docket, chapter="7")

html_content = self.get_html_content_from_email(mail.outbox[5])
self.assertEqual(
len(mail.outbox), 6, msg="Outgoing emails don't match."
)
self.assertIn(docket_only_alert.name, html_content)
self._confirm_number_of_alerts(html_content, 1)

# Percolate Docket upon parties data is added/updated.
docket_only_alert = AlertFactory(
user=self.user_profile.user,
rate=Alert.REAL_TIME,
name="Test Alert Docket Only 7",
query='atty_name="John Lorem"&type=r',
)
firm = AttorneyOrganizationFactory(
name="Associates LLP 2", lookup_key="firm_llp"
)
attorney = AttorneyFactory(
name="John Lorem",
organizations=[firm],
docket=docket,
)
PartyTypeFactory.create(
party=PartyFactory(
name="Defendant Jane Roe",
docket=docket,
attorneys=[attorney],
),
docket=docket,
)
index_docket_parties_in_es.delay(docket.pk)

self.assertEqual(
len(mail.outbox), 7, msg="Outgoing emails don't match."
)
html_content = self.get_html_content_from_email(mail.outbox[6])
self.assertIn(docket_only_alert.name, html_content)
self._confirm_number_of_alerts(html_content, 1)

def test_recap_alerts_highlighting(self) -> None:
"""Confirm RECAP Search alerts are properly highlighted."""

docket_only_alert = AlertFactory(
user=self.user_profile.user,
rate=Alert.REAL_TIME,
name="Test Alert Docket Only",
query='q="SUBPOENAS SERVED CASE"&type=r',
query='q="SUBPOENAS SERVED CASE"&docket_number="1:21-bk-1234"&type=r',
)
with self.captureOnCommitCallbacks(execute=True):
docket = DocketFactory(
Expand All @@ -2200,12 +2269,13 @@ def test_recap_alerts_highlighting(self) -> None:
self.assertIn(docket_only_alert.name, html_content)
self._confirm_number_of_alerts(html_content, 1)
self.assertIn(f"<strong>{docket.case_name}</strong>", html_content)
self.assertIn(f"<strong>{docket.docket_number}</strong>", html_content)

recap_only_alert = AlertFactory(
user=self.user_profile.user,
rate=Alert.REAL_TIME,
name="Test Alert RECAP Only",
query='q="plain text for 018036652000"&type=r',
query='q="plain text for 018036652000"&description="Affidavit Of Compliance"&type=r',
)
with self.captureOnCommitCallbacks(execute=True):
alert_de = DocketEntryWithParentsFactory(
Expand All @@ -2216,6 +2286,7 @@ def test_recap_alerts_highlighting(self) -> None:
source=Docket.RECAP,
),
entry_number=1,
description="Affidavit Of Compliance",
)
rd = RECAPDocumentFactory(
docket_entry=alert_de,
Expand All @@ -2232,3 +2303,114 @@ def test_recap_alerts_highlighting(self) -> None:
self.assertIn(recap_only_alert.name, html_content)
self._confirm_number_of_alerts(html_content, 1)
self.assertIn(f"<strong>{rd.plain_text}</strong>", html_content)
self.assertIn(
f"<strong>{rd.docket_entry.description}</strong>", html_content
)

def test_group_percolator_alerts(self) -> None:
"""Test group Percolator RECAP Alerts in an email and hits."""

with self.captureOnCommitCallbacks(execute=True):
docket = DocketFactory(
court=self.court,
case_name=f"SUBPOENAS SERVED CASE",
docket_number=f"1:21-bk-123",
source=Docket.RECAP,
cause="410 Civil",
)
dockets_created = []
for i in range(3):
docket_created = DocketFactory(
court=self.court,
case_name=f"SUBPOENAS SERVED CASE {i}",
docket_number=f"1:21-bk-123{i}",
source=Docket.RECAP,
cause="410 Civil",
)
dockets_created.append(docket_created)

alert_de = DocketEntryWithParentsFactory(
docket=docket,
entry_number=1,
date_filed=datetime.date(2024, 8, 19),
description="MOTION for Leave to File Amicus Curiae Lorem Served",
)
rd = RECAPDocumentFactory(
docket_entry=alert_de,
description="Motion to File",
document_number="1",
pacer_doc_id="018036652439",
)
rd_2 = RECAPDocumentFactory(
docket_entry=alert_de,
description="Motion to File 2",
document_number="2",
pacer_doc_id="018036652440",
plain_text="plain text lorem",
)

docket_only_alert = AlertFactory(
user=self.user_profile.user,
rate=Alert.REAL_TIME,
name="Test Alert Docket Only",
query='q="410 Civil"&type=r',
)
recap_only_alert = AlertFactory(
user=self.user_profile.user,
rate=Alert.REAL_TIME,
name="Test Alert RECAP Only Docket Entry",
query=f"q=docket_entry_id:{alert_de.pk}&type=r",
)

self.assertEqual(
len(mail.outbox), 0, msg="Outgoing emails don't match."
)

# Assert webhooks.
webhook_events = WebhookEvent.objects.all().values_list(
"content", flat=True
)
self.assertEqual(
len(webhook_events), 4, msg="Webhook events didn't match."
)
with mock.patch(
"cl.api.webhooks.requests.post",
side_effect=lambda *args, **kwargs: MockResponse(
200, mock_raw=True
),
):
call_command("cl_send_rt_recap_alerts", testing_mode=True)

self.assertEqual(
len(mail.outbox), 1, msg="Outgoing emails don't match."
)

# Assert docket-only alert.
html_content = self.get_html_content_from_email(mail.outbox[0])
self.assertIn(docket_only_alert.name, html_content)
self._confirm_number_of_alerts(html_content, 2)
# The docket-only alert doesn't contain any nested child hits.
self._count_alert_hits_and_child_hits(
html_content,
docket_only_alert.name,
3,
docket.case_name,
0,
)
# Assert RECAP-only alert.
self.assertIn(recap_only_alert.name, html_content)
# The recap-only alert contain 2 child hits.
self._count_alert_hits_and_child_hits(
html_content,
recap_only_alert.name,
1,
alert_de.docket.case_name,
2,
)

self._assert_child_hits_content(
html_content,
recap_only_alert.name,
alert_de.docket.case_name,
[rd.description, rd_2.description],
)
Loading

0 comments on commit 32f8ca9

Please sign in to comment.