From c589bbf6c2a36319895aef665e857f19fb16add8 Mon Sep 17 00:00:00 2001 From: v_anne <69829523+v-anne@users.noreply.github.com> Date: Mon, 3 Mar 2025 15:18:55 -0500 Subject: [PATCH 1/8] modifying ranking algorithm --- cl/favorites/utils.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/cl/favorites/utils.py b/cl/favorites/utils.py index 5be48bf8f9..dc153e3c1b 100644 --- a/cl/favorites/utils.py +++ b/cl/favorites/utils.py @@ -103,11 +103,13 @@ async def get_existing_prayers_in_bulk( async def get_top_prayers() -> QuerySet[RECAPDocument]: - # Calculate the age of each prayer - prayer_age = ExpressionWrapper( - Extract(Now() - F("prayers__date_created"), "epoch"), - output_field=FloatField(), - ) + """Retrieve the most desired documents that have open prayers. It first + ranks by the number of requests and then by the number of views the particular + docket has received. + + :return: A queryset of RECAPDocuments in descending order of preference. + """ + waiting_prayers = Prayer.objects.filter(status=Prayer.WAITING).values( "recap_document_id" ) @@ -144,20 +146,9 @@ async def get_top_prayers() -> QuerySet[RECAPDocument]: prayer_count=Count( "prayers", filter=Q(prayers__status=Prayer.WAITING) ), - avg_prayer_age=Avg( - prayer_age, filter=Q(prayers__status=Prayer.WAITING) - ), - ) - .annotate( - geometric_mean=Sqrt( - Cast( - F("prayer_count") - * Cast(F("avg_prayer_age"), FloatField()), - FloatField(), - ) - ) + view_count=F("docket_entry__docket__view_count") ) - .order_by("-geometric_mean")[:50] + .order_by("-prayer_count", "-view_count") ) return documents From e3167a37772b3a027d25016f0aa65198f7fec70e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 20:32:10 +0000 Subject: [PATCH 2/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- cl/favorites/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cl/favorites/utils.py b/cl/favorites/utils.py index dc153e3c1b..2a446c9348 100644 --- a/cl/favorites/utils.py +++ b/cl/favorites/utils.py @@ -146,7 +146,7 @@ async def get_top_prayers() -> QuerySet[RECAPDocument]: prayer_count=Count( "prayers", filter=Q(prayers__status=Prayer.WAITING) ), - view_count=F("docket_entry__docket__view_count") + view_count=F("docket_entry__docket__view_count"), ) .order_by("-prayer_count", "-view_count") ) From 9130308e6cd9f8058780bbb5bc8e6e61c137867d Mon Sep 17 00:00:00 2001 From: v_anne <69829523+v-anne@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:50:24 -0500 Subject: [PATCH 3/8] modifying test to account for docket view_count instead of age --- cl/favorites/tests.py | 28 ++++++++++------------------ cl/search/factories.py | 1 + 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/cl/favorites/tests.py b/cl/favorites/tests.py index 4379693479..338485c137 100644 --- a/cl/favorites/tests.py +++ b/cl/favorites/tests.py @@ -797,35 +797,27 @@ async def test_get_top_prayers_by_number(self) -> None: msg="Wrong top_prayers based on prayers count.", ) - async def test_get_top_prayers_by_age(self) -> None: + async def test_get_top_prayers_by_views(self) -> None: """Does the get_top_prayers method work properly?""" - # Test top documents based on prayer age. - current_time = now() - with time_machine.travel( - current_time - timedelta(minutes=1), tick=False - ): - await create_prayer(self.user, self.rd_4) + # Test top documents based on docket views. + self.rd_2.docket_entry.docket.view_count = 4 + self.rd_3.docket_entry.docket.view_count = 12 + self.rd_4.docket_entry.docket.view_count = 6 - with time_machine.travel( - current_time - timedelta(minutes=2), tick=False - ): - await create_prayer(self.user, self.rd_2) - - with time_machine.travel( - current_time - timedelta(minutes=3), tick=False - ): - await create_prayer(self.user_2, self.rd_3) + await create_prayer(self.user, self.rd_4) + await create_prayer(self.user, self.rd_2) + await create_prayer(self.user_2, self.rd_3) top_prayers = await get_top_prayers() self.assertEqual(await top_prayers.acount(), 3) - expected_top_prayers = [self.rd_3.pk, self.rd_2.pk, self.rd_4.pk] + expected_top_prayers = [self.rd_3.pk, self.rd_4.pk, self.rd_2.pk] actual_top_prayers = [top_rd.pk async for top_rd in top_prayers] self.assertEqual( actual_top_prayers, expected_top_prayers, - msg="Wrong top_prayers based on prayers age.", + msg="Wrong top_prayers based on docket view count.", ) async def test_get_top_prayers_by_number_and_age(self) -> None: diff --git a/cl/search/factories.py b/cl/search/factories.py index fdf60e379c..ca9243dd25 100644 --- a/cl/search/factories.py +++ b/cl/search/factories.py @@ -284,6 +284,7 @@ class Meta: docket_number = Faker("federal_district_docket_number") slug = Faker("slug") date_argued = Faker("date_object") + view_count = 0 """ This hook is necessary to make this factory compatible with the From 4b0d0363191150e529575492253df6f32330458a Mon Sep 17 00:00:00 2001 From: v_anne <69829523+v-anne@users.noreply.github.com> Date: Wed, 5 Mar 2025 18:10:10 -0500 Subject: [PATCH 4/8] other test --- cl/favorites/tests.py | 56 +++++++++++++------------------------------ 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/cl/favorites/tests.py b/cl/favorites/tests.py index 338485c137..b546f708a2 100644 --- a/cl/favorites/tests.py +++ b/cl/favorites/tests.py @@ -820,65 +820,41 @@ async def test_get_top_prayers_by_views(self) -> None: msg="Wrong top_prayers based on docket view count.", ) - async def test_get_top_prayers_by_number_and_age(self) -> None: + async def test_get_top_prayers_by_number_and_views(self) -> None: """Does the get_top_prayers method work properly?""" - # Create prayers with different counts and ages - current_time = now() - with time_machine.travel(current_time - timedelta(days=5), tick=False): - await create_prayer(self.user, self.rd_5) # 1 prayer, 5 days old - with time_machine.travel(current_time - timedelta(days=3), tick=False): - await create_prayer(self.user, self.rd_2) - await create_prayer( - self.user_2, self.rd_2 - ) # 2 prayers, 3 days old + self.rd_2.docket_entry.docket.view_count = 4 + self.rd_3.docket_entry.docket.view_count = 1 + self.rd_4.docket_entry.docket.view_count = 6 + self.rd_5.docket_entry.docket.view_count = 8 - with time_machine.travel(current_time - timedelta(days=1), tick=False): - await create_prayer(self.user, self.rd_3) - await create_prayer(self.user_2, self.rd_3) - await create_prayer(self.user_3, self.rd_3) # 3 prayers, 1 day old + # Create prayers with different counts and views - with time_machine.travel(current_time - timedelta(days=4), tick=False): - await create_prayer(self.user, self.rd_4) - await create_prayer( - self.user_2, self.rd_4 - ) # 2 prayers, 4 days old + await create_prayer(self.user, self.rd_5) + await create_prayer(self.user, self.rd_2) + await create_prayer(self.user_2, self.rd_2) + await create_prayer(self.user, self.rd_3) + await create_prayer(self.user_2, self.rd_3) + await create_prayer(self.user_3, self.rd_3) + await create_prayer(self.user, self.rd_4) + await create_prayer(self.user_2, self.rd_4) top_prayers = await get_top_prayers() self.assertEqual(await top_prayers.acount(), 4) expected_top_prayers = [ + self.rd_3.pk, self.rd_4.pk, self.rd_2.pk, self.rd_5.pk, - self.rd_3.pk, ] actual_top_prayers = [top_rd.pk async for top_rd in top_prayers] self.assertEqual( actual_top_prayers, expected_top_prayers, - msg="Wrong top_prayers based on combined prayer count and age.", - ) - - # Compute expected geometric means - rd_4_score = math.sqrt(2 * (4 * 3600 * 24)) - rd_2_score = math.sqrt(2 * (3 * 3600 * 24)) - rd_5_score = math.sqrt(1 * (5 * 3600 * 24)) - rd_3_score = math.sqrt(3 * (1 * 3600 * 24)) - - self.assertAlmostEqual( - top_prayers[0].geometric_mean, rd_4_score, places=2 - ) - self.assertAlmostEqual( - top_prayers[1].geometric_mean, rd_2_score, places=2 - ) - self.assertAlmostEqual( - top_prayers[2].geometric_mean, rd_5_score, places=2 - ) - self.assertAlmostEqual( - top_prayers[3].geometric_mean, rd_3_score, places=2 + msg="Wrong top_prayers based on combined prayer count and docket view count.", ) async def test_get_user_prayer_history(self) -> None: From 6ba549f910bc05ece5949a636b8616b481d4e9f4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 5 Mar 2025 23:11:14 +0000 Subject: [PATCH 5/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- cl/favorites/tests.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cl/favorites/tests.py b/cl/favorites/tests.py index b546f708a2..28affb57ff 100644 --- a/cl/favorites/tests.py +++ b/cl/favorites/tests.py @@ -823,7 +823,6 @@ async def test_get_top_prayers_by_views(self) -> None: async def test_get_top_prayers_by_number_and_views(self) -> None: """Does the get_top_prayers method work properly?""" - self.rd_2.docket_entry.docket.view_count = 4 self.rd_3.docket_entry.docket.view_count = 1 self.rd_4.docket_entry.docket.view_count = 6 From 68fd6b4a7bdb11fa4382b766aa8b558d29ee3d4f Mon Sep 17 00:00:00 2001 From: v_anne <69829523+v-anne@users.noreply.github.com> Date: Wed, 5 Mar 2025 18:54:03 -0500 Subject: [PATCH 6/8] saving --- cl/favorites/tests.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cl/favorites/tests.py b/cl/favorites/tests.py index 01eeae473f..5f319c9666 100644 --- a/cl/favorites/tests.py +++ b/cl/favorites/tests.py @@ -806,6 +806,10 @@ async def test_get_top_prayers_by_views(self) -> None: self.rd_3.docket_entry.docket.view_count = 12 self.rd_4.docket_entry.docket.view_count = 6 + self.rd_2.docket_entry.docket.save() + self.rd_3.docket_entry.docket.save() + self.rd_4.docket_entry.docket.save() + await create_prayer(self.user, self.rd_4) await create_prayer(self.user, self.rd_2) await create_prayer(self.user_2, self.rd_3) @@ -829,6 +833,11 @@ async def test_get_top_prayers_by_number_and_views(self) -> None: self.rd_4.docket_entry.docket.view_count = 6 self.rd_5.docket_entry.docket.view_count = 8 + self.rd_2.docket_entry.docket.save() + self.rd_3.docket_entry.docket.save() + self.rd_4.docket_entry.docket.save() + self.rd_5.docket_entry.docket.save() + # Create prayers with different counts and views await create_prayer(self.user, self.rd_5) From eec7571caad7041fd7a8fd9480a98bcf56344116 Mon Sep 17 00:00:00 2001 From: v_anne <69829523+v-anne@users.noreply.github.com> Date: Wed, 5 Mar 2025 19:09:12 -0500 Subject: [PATCH 7/8] async --- cl/favorites/tests.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cl/favorites/tests.py b/cl/favorites/tests.py index 5f319c9666..66e189db68 100644 --- a/cl/favorites/tests.py +++ b/cl/favorites/tests.py @@ -806,9 +806,9 @@ async def test_get_top_prayers_by_views(self) -> None: self.rd_3.docket_entry.docket.view_count = 12 self.rd_4.docket_entry.docket.view_count = 6 - self.rd_2.docket_entry.docket.save() - self.rd_3.docket_entry.docket.save() - self.rd_4.docket_entry.docket.save() + self.rd_2.docket_entry.docket.asave() + self.rd_3.docket_entry.docket.asave() + self.rd_4.docket_entry.docket.asave() await create_prayer(self.user, self.rd_4) await create_prayer(self.user, self.rd_2) @@ -833,10 +833,10 @@ async def test_get_top_prayers_by_number_and_views(self) -> None: self.rd_4.docket_entry.docket.view_count = 6 self.rd_5.docket_entry.docket.view_count = 8 - self.rd_2.docket_entry.docket.save() - self.rd_3.docket_entry.docket.save() - self.rd_4.docket_entry.docket.save() - self.rd_5.docket_entry.docket.save() + self.rd_2.docket_entry.docket.asave() + self.rd_3.docket_entry.docket.asave() + self.rd_4.docket_entry.docket.asave() + self.rd_5.docket_entry.docket.asave() # Create prayers with different counts and views From a4b4f2dd6093703f6f6f1cd21373591dbabe3bbd Mon Sep 17 00:00:00 2001 From: v_anne <69829523+v-anne@users.noreply.github.com> Date: Sun, 9 Mar 2025 21:46:01 -0400 Subject: [PATCH 8/8] adjusting help page to account for new algorithm --- cl/simple_pages/templates/help/prayer_help.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cl/simple_pages/templates/help/prayer_help.html b/cl/simple_pages/templates/help/prayer_help.html index 766286e0ae..dae5ac449e 100644 --- a/cl/simple_pages/templates/help/prayer_help.html +++ b/cl/simple_pages/templates/help/prayer_help.html @@ -64,7 +64,7 @@

Pray and Pay Project

Fulfilling a Prayer

-

The most-wanted documents are shown on a leaderboard ranked by how many users have requested a particular document and how old the requests are. As requests are fulfilled, they are removed from the leaderboard. +

The most-wanted documents are shown on a leaderboard ranked by how many users have requested a particular document and how many views their corresponding docket has received. As requests are fulfilled, they are removed from the leaderboard.

Fulfilling somebody's prayer only works if you install the RECAP extension. Once it's installed, all your PACER purchases will be sent to CourtListener automatically — Prayers will be granted!