r |
diff --git a/cl/api/templates/webhooks-docs-vlatest.html b/cl/api/templates/webhooks-docs-vlatest.html
index c1e9fadc7d..aafcf0344d 100644
--- a/cl/api/templates/webhooks-docs-vlatest.html
+++ b/cl/api/templates/webhooks-docs-vlatest.html
@@ -1,6 +1,7 @@
{% extends "base.html" %}
{% load static %}
{% load extras %}
+{% load waffle_tags %}
{% block title %}Webhook API – CourtListener.com{% endblock %}
{% block description %}
@@ -216,7 +217,7 @@ Retries and Automatic Endpoint Disablement
Fixed webhook endpoints can be re-enabled in the webhooks panel.
- Change Log
v1 First release
+
+ v2 - This release introduces support for Case Law Search Alerts results with nested documents.
+ You can now select the webhook version when configuring an endpoint. For most webhook event types, there are no differences between v1
and v2
, as the payload remains unchanged.
+
+ In the Search Alert webhook event type, the Oral Arguments search response remains identical between v1
and v2
.
+ For Case Law {% flag "recap-alerts-active" %}and RECAP{% endflag %} v2
now includes nested results, which are based on the new changes introduced in v4
of the Search API.
+
{% endblock %}
diff --git a/cl/api/templates/webhooks-getting-started.html b/cl/api/templates/webhooks-getting-started.html
index 18dc46ea65..a28c6399ad 100644
--- a/cl/api/templates/webhooks-getting-started.html
+++ b/cl/api/templates/webhooks-getting-started.html
@@ -104,7 +104,7 @@ Set Up a Webhook Endpoint in CourtListener
To set up a webhook endpoint, begin by logging into CourtListener and going to the Webhooks panel in your profile:
- Set Up a Webhook Endpoint in CourtListener
Click the “Add webhook” button and the “Add webhook endpoint” modal pops up:
- Set Up a Webhook Endpoint in CourtListener
Select the Event Type for which you wish to receive events.
- You can only create one Webhook endpoint for each type of event. Please get in touch if this limitation causes difficulty for your application.
+
+
+ Choose the webhook version you wish to set up.
+ We recommend selecting the highest available version for your webhook. Refer to the Change Log for more details on webhook versions.
+ You can only create one Webhook endpoint for each type of event and version. Please get in touch if this limitation causes difficulty for your application.
@@ -141,7 +145,7 @@ Set Up a Webhook Endpoint in CourtListener
Click “Create webhook”
Your Webhook endpoint is now created:
- Testing a Webhook endpoint.
Getting a webhook working properly can be difficult, so we have a testing tool that will send you a sample webhook event on demand.
To use the tool, go to webhooks panel and click the “Test” button for the endpoint you wish to test:
- Testing a Webhook endpoint.
In the “As JSON” tab, you can ask our server to send a test event to your endpoint. When you click “Send Webhook Test Event” a new event is created with the information shown and is sent to your endpoint. Test events are not retried, but can be seen in the “Test Logs” tab.
- Testing a Webhook endpoint.
In the “As cURL” tab, you can copy/paste a curl command that can be used to send a test event to your local dev environment.
- Dict[str, Dict[str, int]]:
- """Invert the user logs for a period of time
+ """Aggregate API usage statistics per user over a date range.
- The user logs have the date in the key and the user as part of the set:
+ - Anonymous users are aggregated under the key 'AnonymousUser'.
+ - Both v3 and v4 API counts are combined in the results.
- 'api:v3.user.d:2016-10-01.counts': {
- mlissner: 22,
- joe_hazard: 33,
- }
-
- This inverts these entries to:
+ :param start: Beginning date (inclusive) for the query range
+ :param end: End date (inclusive) for the query range
+ :param add_usernames: If True, replaces user IDs with usernames as keys.
+ When False, uses only user IDs as keys.
- users: {
- mlissner: {
- 2016-10-01: 22,
- total: 22,
- },
- joe_hazard: {
- 2016-10-01: 33,
- total: 33,
- }
- }
- :param start: The beginning date (inclusive) you want the results for. A
- :param end: The end date (inclusive) you want the results for.
- :param add_usernames: Stats are stored with the user ID. If this is True,
- add an alias in the returned dictionary that contains the username as well.
- :return The inverted dictionary
+ :return: Dictionary mapping user identifiers (usernames if `add_usernames=True`,
+ otherwise user IDs) to their daily API usage counts and totals.
+ Inner dictionaries are ordered by date. Only dates with usage are included.
"""
r = get_redis_interface("STATS")
pipe = r.pipeline()
dates = make_date_str_list(start, end)
+ versions = ["v3", "v4"]
for d in dates:
- pipe.zrange(f"api:v3.user.d:{d}.counts", 0, -1, withscores=True)
+ for version in versions:
+ pipe.zrange(
+ f"api:{version}.user.d:{d}.counts",
+ 0,
+ -1,
+ withscores=True,
+ )
+
+ # results contains alternating v3/v4 API usage data for each date queried.
+ # For example, if querying 2023-01-01 to 2023-01-02, results might look like:
+ # [
+ # # 2023-01-01 v3 data: [(user_id, count), ...]
+ # [("1", 100.0), ("2", 50.0)],
+ # # 2023-01-01 v4 data
+ # [("1", 50.0), ("2", 25.0)],
+ # # 2023-01-02 v3 data
+ # [("1", 200.0), ("2", 100.0)],
+ # # 2023-01-02 v4 data
+ # [("1", 100.0), ("2", 50.0)]
+ # ]
+ # We zip this with dates to combine v3/v4 counts per user per day
results = pipe.execute()
- # results is a list of results for each of the zrange queries above. Zip
- # those results with the date that created it, and invert the whole thing.
out: defaultdict = defaultdict(dict)
- for d, result in zip(dates, results):
- for user_id, count in result:
- if user_id == "None" or user_id == "AnonymousUser":
- user_id = "AnonymousUser"
- else:
- user_id = int(user_id)
- count = int(count)
- if out.get(user_id):
- out[user_id][d] = count
- out[user_id]["total"] += count
- else:
- out[user_id] = {d: count, "total": count}
+
+ def update_user_counts(_user_id, _count, _date):
+ user_is_anonymous = _user_id == "None" or _user_id == "AnonymousUser"
+ _user_id = "AnonymousUser" if user_is_anonymous else int(_user_id)
+ _count = int(_count)
+ out.setdefault(_user_id, OrderedDict())
+ out[_user_id].setdefault(_date, 0)
+ out[_user_id][_date] += _count
+ out[_user_id].setdefault("total", 0)
+ out[_user_id]["total"] += _count
+
+ for d, api_usage in zip(dates, batched(results, len(versions))):
+ for user_id, count in chain(*api_usage):
+ update_user_counts(user_id, count, d)
# Sort the values
for k, v in out.items():
diff --git a/cl/assets/templates/admin/docket_change_form.html b/cl/assets/templates/admin/docket_change_form.html
new file mode 100644
index 0000000000..c3a4b000fc
--- /dev/null
+++ b/cl/assets/templates/admin/docket_change_form.html
@@ -0,0 +1,19 @@
+{% extends "admin/change_form.html" %}
+
+{% block object-tools-items %}
+ {% if docket_entries_url %}
+
+
+ View Docket Entries
+
+
+ {% endif %}
+ {% if docket_alerts_url %}
+
+
+ View Docket Alerts
+
+
+ {% endif %}
+ {{ block.super }}
+{% endblock object-tools-items %}
diff --git a/cl/assets/templates/admin/user_change_form.html b/cl/assets/templates/admin/user_change_form.html
new file mode 100644
index 0000000000..1fff0b9c1f
--- /dev/null
+++ b/cl/assets/templates/admin/user_change_form.html
@@ -0,0 +1,19 @@
+{% extends "admin/change_form.html" %}
+
+{% block object-tools-items %}
+ {% if proxy_events_url %}
+
+
+ View UserProxy Events
+
+
+ {% endif %}
+ {% if profile_events_url %}
+
+
+ View UserProfile Events
+
+
+ {% endif %}
+ {{ block.super }}
+{% endblock object-tools-items %}
diff --git a/cl/favorites/templates/top_prayers.html b/cl/favorites/templates/top_prayers.html
index a3506a14a0..f19935dd8c 100644
--- a/cl/favorites/templates/top_prayers.html
+++ b/cl/favorites/templates/top_prayers.html
@@ -5,25 +5,21 @@
{% load pacer %}
{% load humanize %}
-{% block title %}RECAP Requests – CourtListener.com{% endblock %}
-{% block og_title %}RECAP Requests – CourtListener.com{% endblock %}
-
-{% block description %}RECAP Requests on CourtListener.{% endblock %}
-{% block og_description %}RECAP Requests on CourtListener.{% endblock %}
+{% block title %}Most Wanted PACER Documents — CourtListener.com{% endblock %}
+{% block og_title %}Most Wanted PACER Documents — CourtListener.com{% endblock %}
+{% block description %}CourtListener lets you request the purchase of legal documents. View the community's most wanted documents.{% endblock %}
+{% block og_description %}CourtListener lets you request the purchase of legal documents. View the community's most wanted documents.{% endblock %}
{% block content %}
-
Community's Most Requested Documents
+ Community's Most Wanted PACER Documents
+ {{ granted_stats.prayer_count|intcomma }} prayers granted totaling ${{ granted_stats.total_cost }}.
+ {{ waiting_stats.prayer_count|intcomma }} prayers pending totaling at least ${{ waiting_stats.total_cost }}.
-
-
There have been {{ granted_stats.prayer_count|intcomma }} requests granted for {{ granted_stats.distinct_count|intcomma }} unique documents that cost ${{ granted_stats.total_cost }}.
-
-
There are {{ waiting_stats.prayer_count|intcomma }} requests pending for {{ waiting_stats.distinct_count|intcomma }} unique documents that cost at least ${{ waiting_stats.total_cost }}.
-
{% if user.is_authenticated %}
View your prayers
{% else %}
@@ -55,7 +51,7 @@
Community's Most Requested Documents
-
+
{{ prayer.document_number }}
|
diff --git a/cl/favorites/templates/user_prayers.html b/cl/favorites/templates/user_prayers.html
index 4c85d82435..4ada025de6 100644
--- a/cl/favorites/templates/user_prayers.html
+++ b/cl/favorites/templates/user_prayers.html
@@ -6,14 +6,15 @@
{% load tz %}
{% load humanize %}
-{% block title %}{% if is_page_owner %}Your RECAP Requests {% else %}RECAP Requests for: {{ requested_user }}{% endif %} – CourtListener.com{% endblock %}
-{% block og_title %}{% if is_page_owner %}Your RECAP Requests {% else %}RECAP Requests for: {{ requested_user }}{% endif %} – CourtListener.com{% endblock %}
-{% block description %}CourtListener lets you request purchase of legal documents. View the document requests for {{ requested_user }}.{% endblock %}
-{% block og_description %}CourtListener lets you request purchase of legal documents. View the document requests for {{ requested_user }}.{% endblock %}
+{% block title %}{% if is_page_owner %}Your PACER document Prayers{% else %}PACER Document Requests for: {{ requested_user }}{% endif %} – CourtListener.com{% endblock %}
+{% block og_title %}{% if is_page_owner %}Your PACER document Prayers{% else %}PACER Document Requests for: {{ requested_user }}{% endif %} – CourtListener.com{% endblock %}
+{% block description %}CourtListener lets you request the purchase of legal documents. View the documents requested by {{ requested_user }}.{% endblock %}
+{% block og_description %}CourtListener lets you request the purchase of legal documents. View the documents requested by {{ requested_user }}.{% endblock %}
{% block content %}
-
{% if is_page_owner %}Your RECAP Requests {% else %}RECAP Requests for: {{ requested_user }}{% endif %}
+ {% if is_page_owner %}Your PACER Document Prayers{% else %}PACER Document Requests for: {{ requested_user }}{% endif %}
+ {% if is_page_owner %}{{ count|intcomma }} prayers granted totaling ${{total_cost|floatformat:2 }}.
{% endif %}
{% if is_page_owner %}Your RECAP Requests {% else %}RECA
>
{% if is_page_owner %}
-
- You have had {{ count|intcomma }} requests fulfilled for documents that cost ${{total_cost|floatformat:2 }}.
-
{% if is_eligible %}You are eligible to make document requests.{% else %}You have reached your daily limit; wait 24 hours to make new requests.{% endif %}
@@ -59,7 +57,7 @@
{% if is_page_owner %}Your RECAP Requests {% else %}RECA
-
+
{{ rd.document_number }}
|
diff --git a/cl/favorites/utils.py b/cl/favorites/utils.py
index 03d93ba8aa..9e87389890 100644
--- a/cl/favorites/utils.py
+++ b/cl/favorites/utils.py
@@ -200,7 +200,7 @@ async def get_user_prayers(user: User) -> QuerySet[RECAPDocument]:
prayer_status=F("prayers__status"),
prayer_date_created=F("prayers__date_created"),
)
- .order_by("prayers__date_created")
+ .order_by("-prayers__date_created")
)
return documents
diff --git a/cl/lib/admin.py b/cl/lib/admin.py
index 754bce6f08..41f1fe4931 100644
--- a/cl/lib/admin.py
+++ b/cl/lib/admin.py
@@ -1,4 +1,9 @@
+from typing import Any, Type
+from urllib.parse import urlencode
+
from django.contrib.contenttypes.admin import GenericTabularInline
+from django.db.models import Model
+from django.urls import reverse
from cl.lib.models import Note
@@ -13,3 +18,30 @@ class Media:
class NotesInline(GenericTabularInline):
model = Note
extra = 1
+
+
+def build_admin_url(
+ model_class: Type[Model],
+ query_params: dict[str, Any] | None = None,
+) -> str:
+ """
+ Construct a URL for a given model's admin view, optionally appending query parameters.
+
+ :param model_class: The Django model class for which the admin URL will be built.
+ :param query_params: A dictionary of query parameters to append to the URL (e.g. {"docket": 123}).
+ :return: A string representing the fully constructed admin URL, including any query parameters.
+
+ Example usage:
+ >>> from cl.search.models import DocketEntry
+ >>> build_admin_url(DocketEntry, {"docket": "1234"})
+ '/admin/search/docketentry/?docket=1234'
+ """
+ query_params = query_params or {}
+ app_label = model_class._meta.app_label
+ model_name = model_class._meta.model_name
+ # "admin:app_label_modelname_changelist" is the standard naming for admin changelist
+ entries_changelist_url = reverse(
+ f"admin:{app_label}_{model_name}_changelist"
+ )
+ encoded_query_params = urlencode(query_params)
+ return f"{entries_changelist_url}?{encoded_query_params}"
diff --git a/cl/package-lock.json b/cl/package-lock.json
index b8a7038e1e..c2aa3c8d41 100644
--- a/cl/package-lock.json
+++ b/cl/package-lock.json
@@ -3435,9 +3435,9 @@
}
},
"node_modules/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"dev": true,
"engines": {
"node": ">= 0.6"
@@ -4477,9 +4477,9 @@
}
},
"node_modules/express": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
- "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dev": true,
"dependencies": {
"accepts": "~1.3.8",
@@ -4487,7 +4487,7 @@
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.6.0",
+ "cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -4501,7 +4501,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
+ "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -4516,6 +4516,10 @@
},
"engines": {
"node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/array-flatten": {
@@ -6505,9 +6509,9 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-to-regexp": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
- "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"dev": true
},
"node_modules/path-type": {
@@ -11781,9 +11785,9 @@
}
},
"cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"dev": true
},
"cookie-signature": {
@@ -12535,9 +12539,9 @@
}
},
"express": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
- "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dev": true,
"requires": {
"accepts": "~1.3.8",
@@ -12545,7 +12549,7 @@
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.6.0",
+ "cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -12559,7 +12563,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
+ "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -14010,9 +14014,9 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"path-to-regexp": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
- "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"dev": true
},
"path-type": {
diff --git a/cl/recap/admin.py b/cl/recap/admin.py
index 73391d39dd..8f78037988 100644
--- a/cl/recap/admin.py
+++ b/cl/recap/admin.py
@@ -21,8 +21,10 @@ class ProcessingQueueAdmin(CursorPaginatorAdmin):
"pacer_case_id",
"document_number",
"attachment_number",
+ "date_created",
)
- list_filter = ("status",)
+ list_filter = ("status", "date_created")
+ search_help_text = "Search ProcessingQueues by pacer_case_id or court__pk."
search_fields = (
"pacer_case_id",
"court__pk",
@@ -41,15 +43,8 @@ class ProcessingQueueAdmin(CursorPaginatorAdmin):
@admin.register(PacerFetchQueue)
class PacerFetchQueueAdmin(CursorPaginatorAdmin):
- list_display = (
- "__str__",
- "court",
- "request_type",
- )
- list_filter = (
- "status",
- "request_type",
- )
+ list_display = ("__str__", "court", "request_type", "date_created")
+ list_filter = ("status", "request_type", "date_created")
readonly_fields = (
"date_created",
"date_modified",
@@ -94,14 +89,15 @@ def reprocess_failed_epq(modeladmin, request, queryset):
@admin.register(EmailProcessingQueue)
class EmailProcessingQueueAdmin(CursorPaginatorAdmin):
- list_display = (
- "__str__",
- "status",
- )
- list_filter = ("status",)
+ list_display = ("__str__", "status", "date_created")
+ list_filter = ("status", "date_created")
actions = [reprocess_failed_epq]
raw_id_fields = ["uploader", "court"]
exclude = ["recap_documents", "filepath"]
+ readonly_fields = (
+ "date_created",
+ "date_modified",
+ )
admin.site.register(FjcIntegratedDatabase)
diff --git a/cl/search/admin.py b/cl/search/admin.py
index eebdef50ad..7a42e7117e 100644
--- a/cl/search/admin.py
+++ b/cl/search/admin.py
@@ -3,9 +3,11 @@
from django.db.models import QuerySet
from django.http import HttpRequest
-from cl.alerts.admin import DocketAlertInline
+from cl.alerts.models import DocketAlert
+from cl.lib.admin import build_admin_url
from cl.lib.cloud_front import invalidate_cloudfront
from cl.lib.models import THUMBNAIL_STATUSES
+from cl.lib.string_utils import trunc
from cl.recap.management.commands.delete_document_from_ia import delete_from_ia
from cl.search.models import (
BankruptcyInformation,
@@ -88,7 +90,14 @@ class OpinionClusterAdmin(CursorPaginatorAdmin):
@admin.register(Court)
class CourtAdmin(admin.ModelAdmin):
- list_display = ("full_name", "short_name", "position", "in_use", "pk")
+ list_display = (
+ "full_name",
+ "short_name",
+ "position",
+ "in_use",
+ "pk",
+ "jurisdiction",
+ )
list_filter = (
"jurisdiction",
"in_use",
@@ -215,17 +224,36 @@ class RECAPDocumentInline(admin.StackedInline):
@admin.register(DocketEntry)
class DocketEntryAdmin(CursorPaginatorAdmin):
inlines = (RECAPDocumentInline,)
+ search_help_text = (
+ "Search DocketEntries by Docket ID or RECAP sequence number."
+ )
+ search_fields = (
+ "docket__id",
+ "recap_sequence_number",
+ )
+ list_display = (
+ "get_pk",
+ "get_trunc_description",
+ "date_filed",
+ "time_filed",
+ "entry_number",
+ "recap_sequence_number",
+ "pacer_sequence_number",
+ )
raw_id_fields = ("docket", "tags")
readonly_fields = (
"date_created",
"date_modified",
)
+ list_filter = ("date_filed", "date_created", "date_modified")
+ @admin.display(description="Docket entry")
+ def get_pk(self, obj):
+ return obj.pk
-class DocketEntryInline(admin.TabularInline):
- model = DocketEntry
- extra = 1
- raw_id_fields = ("tags",)
+ @admin.display(description="Description")
+ def get_trunc_description(self, obj):
+ return trunc(obj.description, 35, ellipsis="...")
@admin.register(OriginatingCourtInformation)
@@ -238,17 +266,25 @@ class OriginatingCourtInformationAdmin(admin.ModelAdmin):
@admin.register(Docket)
class DocketAdmin(CursorPaginatorAdmin):
+ change_form_template = "admin/docket_change_form.html"
prepopulated_fields = {"slug": ["case_name"]}
- inlines = (
- DocketEntryInline,
- BankruptcyInformationInline,
- DocketAlertInline,
+ list_display = (
+ "__str__",
+ "pacer_case_id",
+ "docket_number",
)
+ search_help_text = "Search dockets by PK, PACER case ID, or Docket number."
+ search_fields = ("pk", "pacer_case_id", "docket_number")
+ inlines = (BankruptcyInformationInline,)
readonly_fields = (
"date_created",
"date_modified",
"view_count",
)
+ autocomplete_fields = (
+ "court",
+ "appeal_from",
+ )
raw_id_fields = (
"panel",
"tags",
@@ -259,6 +295,25 @@ class DocketAdmin(CursorPaginatorAdmin):
"parent_docket",
)
+ def change_view(self, request, object_id, form_url="", extra_context=None):
+ """Add links to pre-filtered related admin pages."""
+ extra_context = extra_context or {}
+ query_params = {"docket": object_id}
+
+ extra_context["docket_entries_url"] = build_admin_url(
+ DocketEntry,
+ query_params,
+ )
+
+ extra_context["docket_alerts_url"] = build_admin_url(
+ DocketAlert,
+ query_params,
+ )
+
+ return super().change_view(
+ request, object_id, form_url, extra_context=extra_context
+ )
+
@admin.register(OpinionsCited)
class OpinionsCitedAdmin(CursorPaginatorAdmin):
diff --git a/cl/simple_pages/static/png/pray-button.png b/cl/simple_pages/static/png/pray-button.png
index 76c1f6c7ed..8ef8b9caa2 100644
Binary files a/cl/simple_pages/static/png/pray-button.png and b/cl/simple_pages/static/png/pray-button.png differ
diff --git a/cl/simple_pages/static/png/prayer-email.png b/cl/simple_pages/static/png/prayer-email.png
index 012b988ca2..088f483836 100644
Binary files a/cl/simple_pages/static/png/prayer-email.png and b/cl/simple_pages/static/png/prayer-email.png differ
diff --git a/cl/users/admin.py b/cl/users/admin.py
index dd34ba4641..7a949910d8 100644
--- a/cl/users/admin.py
+++ b/cl/users/admin.py
@@ -1,4 +1,6 @@
+from django.apps import apps
from django.contrib import admin
+from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.models import Permission, User
from rest_framework.authtoken.models import Token
@@ -10,7 +12,7 @@
NeonMembershipInline,
)
from cl.favorites.admin import NoteInline, UserTagInline
-from cl.lib.admin import AdminTweaksMixin
+from cl.lib.admin import AdminTweaksMixin, build_admin_url
from cl.users.models import (
BarMembership,
EmailFlag,
@@ -19,30 +21,37 @@
UserProfile,
)
+UserProxyEvent = apps.get_model("users", "UserProxyEvent")
+UserProfileEvent = apps.get_model("users", "UserProfileEvent")
-def get_email_confirmed(obj):
- return obj.profile.email_confirmed
-
-
-get_email_confirmed.short_description = "Email Confirmed?"
+class TokenInline(admin.StackedInline):
+ model = Token
-def get_stub_account(obj):
- return obj.profile.stub_account
+class UserProfileInline(admin.StackedInline):
+ model = UserProfile
-get_stub_account.short_description = "Stub Account?"
+class CustomUserChangeForm(UserChangeForm):
-class TokenInline(admin.StackedInline):
- model = Token
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ # Ensure user_permissions field uses an optimized queryset
+ if "user_permissions" in self.fields:
+ self.fields["user_permissions"].queryset = (
+ Permission.objects.select_related("content_type")
+ )
-class UserProfileInline(admin.StackedInline):
- model = UserProfile
+# Replace the normal User admin with our better one.
+admin.site.unregister(User)
+@admin.register(User)
class UserAdmin(admin.ModelAdmin, AdminTweaksMixin):
+ form = CustomUserChangeForm # optimize queryset for user_permissions field
+ change_form_template = "admin/user_change_form.html"
inlines = (
UserProfileInline,
DonationInline,
@@ -57,8 +66,16 @@ class UserAdmin(admin.ModelAdmin, AdminTweaksMixin):
)
list_display = (
"username",
- get_email_confirmed,
- get_stub_account,
+ "get_email_confirmed",
+ "get_stub_account",
+ )
+ list_filter = (
+ "is_superuser",
+ "profile__email_confirmed",
+ "profile__stub_account",
+ )
+ search_help_text = (
+ "Search Users by username, first name, last name, or email."
)
search_fields = (
"username",
@@ -67,6 +84,35 @@ class UserAdmin(admin.ModelAdmin, AdminTweaksMixin):
"email",
)
+ def change_view(self, request, object_id, form_url="", extra_context=None):
+ """Add links to related event admin pages filtered by user/profile."""
+ extra_context = extra_context or {}
+ user = self.get_object(request, object_id)
+
+ extra_context["proxy_events_url"] = build_admin_url(
+ UserProxyEvent,
+ {"pgh_obj": object_id},
+ )
+
+ if user and hasattr(user, "profile"):
+ profile_id = user.profile.pk
+ extra_context["profile_events_url"] = build_admin_url(
+ UserProfileEvent,
+ {"pgh_obj": profile_id},
+ )
+
+ return super().change_view(
+ request, object_id, form_url, extra_context=extra_context
+ )
+
+ @admin.display(description="Email Confirmed?")
+ def get_email_confirmed(self, obj):
+ return obj.profile.email_confirmed
+
+ @admin.display(description="Stub Account?")
+ def get_stub_account(self, obj):
+ return obj.profile.stub_account
+
@admin.register(EmailFlag)
class EmailFlagAdmin(admin.ModelAdmin):
@@ -117,8 +163,54 @@ class FailedEmailAdmin(admin.ModelAdmin):
raw_id_fields = ("stored_email",)
-# Replace the normal User admin with our better one.
-admin.site.unregister(User)
-admin.site.register(User, UserAdmin)
+class BaseUserEventAdmin(admin.ModelAdmin):
+ ordering = ("-pgh_created_at",)
+ # Define common attributes to be extended:
+ common_list_display = ("get_pgh_created", "get_pgh_label")
+ common_list_filters = ("pgh_created_at",)
+ common_search_fields = ("pgh_obj",)
+ # Default to common attributes:
+ list_display = common_list_display
+ list_filter = common_list_filters
+ search_fields = common_search_fields
+
+ @admin.display(ordering="pgh_created_at", description="Event triggered")
+ def get_pgh_created(self, obj):
+ return obj.pgh_created_at
+
+ @admin.display(ordering="pgh_label", description="Event label")
+ def get_pgh_label(self, obj):
+ return obj.pgh_label
+
+ def get_readonly_fields(self, request, obj=None):
+ return [field.name for field in self.model._meta.get_fields()]
+
+
+@admin.register(UserProxyEvent)
+class UserProxyEventAdmin(BaseUserEventAdmin):
+ search_help_text = "Search UserProxyEvents by pgh_obj, email, or username."
+ search_fields = BaseUserEventAdmin.common_search_fields + (
+ "email",
+ "username",
+ )
+ list_display = BaseUserEventAdmin.list_display + (
+ "email",
+ "username",
+ )
+
+
+@admin.register(UserProfileEvent)
+class UserProfileEventAdmin(BaseUserEventAdmin):
+ search_help_text = "Search UserProxyEvents by pgh_obj or username."
+ search_fields = BaseUserEventAdmin.common_search_fields + (
+ "user__username",
+ )
+ list_display = BaseUserEventAdmin.common_list_display + (
+ "user",
+ "email_confirmed",
+ )
+ list_filter = BaseUserEventAdmin.common_list_filters + ("email_confirmed",)
+
+
admin.site.register(BarMembership)
admin.site.register(Permission)
diff --git a/cl/users/api_views.py b/cl/users/api_views.py
index 865188911d..5ac1c87ca3 100644
--- a/cl/users/api_views.py
+++ b/cl/users/api_views.py
@@ -136,14 +136,26 @@ def test_webhook(self, request, *args, **kwargs):
{"endpoint_url": webhook.url, "webhook_version": version}
).strip()
case WebhookEventType.SEARCH_ALERT:
- event_template = loader.get_template(
- "includes/search_alert_webhook_dummy.txt"
+ event_template = (
+ loader.get_template(
+ "includes/search_alert_webhook_dummy.txt"
+ )
+ if version == WebhookVersions.v1
+ else loader.get_template(
+ "includes/search_alert_webhook_dummy_v2.txt"
+ )
)
event_dummy_content = event_template.render(
{"webhook_version": version}
).strip()
- event_curl_template = loader.get_template(
- "includes/search_alert_webhook_dummy_curl.txt"
+ event_curl_template = (
+ loader.get_template(
+ "includes/search_alert_webhook_dummy_curl.txt"
+ )
+ if version == WebhookVersions.v1
+ else loader.get_template(
+ "includes/search_alert_webhook_dummy_curl_v2.txt"
+ )
)
event_dummy_curl = event_curl_template.render(
{"endpoint_url": webhook.url, "webhook_version": version}
diff --git a/cl/users/templates/includes/search_alert_webhook_dummy_curl_v2.txt b/cl/users/templates/includes/search_alert_webhook_dummy_curl_v2.txt
new file mode 100644
index 0000000000..261cfac30b
--- /dev/null
+++ b/cl/users/templates/includes/search_alert_webhook_dummy_curl_v2.txt
@@ -0,0 +1,98 @@
+curl --request POST \
+ --url '{{ endpoint_url }}' \
+ --header 'Content-Type: application/json' \
+ --header 'Idempotency-Key: 59f1be59-e428-427a-a346-9cacded5c1d4' \
+ --data '{
+ "payload":{
+ "results":[
+ {
+ "meta":{
+ "timestamp":"2024-11-15T00:22:15.765337Z",
+ "date_created":"2024-11-15T00:22:15.765337Z"
+ },
+ "court":"Supreme Court of the United States",
+ "judge":"",
+ "source":"C",
+ "status":"Published",
+ "posture":"",
+ "scdb_id":"",
+ "attorney":"",
+ "caseName":"Obergefell v. Hodges",
+ "citation":[
+ "192 L. Ed. 2d 609",
+ "135 S. Ct. 2584",
+ "2015 U.S. LEXIS 4250",
+ "576 U.S. 644"
+ ],
+ "court_id":"scotus",
+ "opinions":[
+ {
+ "id":2812209,
+ "meta":{
+ "timestamp":"2024-11-15T00:22:15.765337Z",
+ "date_created":"2024-11-15T00:22:15.765337Z"
+ },
+ "sha1":"d7bcc865b883abd70d74d9af7578d256ae62a973",
+ "type":"combined-opinion",
+ "cites":[
+ 13
+ ],
+ "snippet":"Lorem dolor Obergefell sit amet, consectetur adipiscing elit.",
+ "author_id":17,
+ "local_path":"test/search/opinion_html.html",
+ "per_curiam":false,
+ "download_url":"http://www.supremecourt.gov/opinions/14pdf/14-556_3204.pdf",
+ "joined_by_ids":[
+
+ ]
+ }
+ ],
+ "syllabus":"",
+ "citeCount":432,
+ "dateFiled":"2015-06-26",
+ "docket_id":2668808,
+ "lexisCite":"2015 U.S. LEXIS 4250",
+ "panel_ids":[
+
+ ],
+ "cluster_id":2812209,
+ "dateArgued":null,
+ "suitNature":"",
+ "neutralCite":"10 Neutral 4",
+ "panel_names":[
+ "Kristine Pena Mendez Jr."
+ ],
+ "sibling_ids":[
+ 2812209
+ ],
+ "absolute_url":"/opinion/2812209/obergefell-v-hodges/",
+ "caseNameFull":"Todd Ballard, Julie Smith, Jeanne Peters, Ashley Jordan, and Andrew Thompson v. Johnson LLC, Walker-Bernard, Jones PLC, Bailey Group, and Lopez-Gonzales",
+ "dateReargued":null,
+ "docketNumber":"14-556",
+ "procedural_history":"",
+ "dateReargumentDenied":null,
+ "court_citation_string":"SCOTUS",
+ "non_participating_judge_ids":[
+
+ ]
+ }
+ ],
+ "alert":{
+ "id":1,
+ "name":"Obergefell v. Hodges",
+ "rate":"dly",
+ "user":1,
+ "query":"?q=Obergefell+v.+Hodges&type=o&order_by=score+desc&stat_Precedential=on&filed_after=06%2F01%2F2015&docket_number=14-556&",
+ "secret_key":"MV8yfOSXQvDQauQOCEg0orkgi4hBC4r9yg5Wk3ue",
+ "date_created":"2022-12-02T15:42:34.921805-08:00",
+ "date_last_hit":"2022-12-02T15:42:35.915737-08:00",
+ "date_modified":"2022-12-02T15:42:35.915880-08:00"
+ }
+ },
+ "webhook":{
+ "version":{{ webhook_version }},
+ "event_type":2,
+ "date_created":"2022-12-02T23:42:34.894411+00:00",
+ "deprecation_date":"None"
+ }
+}'
diff --git a/cl/users/templates/includes/search_alert_webhook_dummy_v2.txt b/cl/users/templates/includes/search_alert_webhook_dummy_v2.txt
new file mode 100644
index 0000000000..36b1b7eb87
--- /dev/null
+++ b/cl/users/templates/includes/search_alert_webhook_dummy_v2.txt
@@ -0,0 +1,94 @@
+{
+ "payload":{
+ "results":[
+ {
+ "meta":{
+ "timestamp":"2024-11-15T00:22:15.765337Z",
+ "date_created":"2024-11-15T00:22:15.765337Z"
+ },
+ "court":"Supreme Court of the United States",
+ "judge":"",
+ "source":"C",
+ "status":"Published",
+ "posture":"",
+ "scdb_id":"",
+ "attorney":"",
+ "caseName":"Obergefell v. Hodges",
+ "citation":[
+ "192 L. Ed. 2d 609",
+ "135 S. Ct. 2584",
+ "2015 U.S. LEXIS 4250",
+ "576 U.S. 644"
+ ],
+ "court_id":"scotus",
+ "opinions":[
+ {
+ "id":2812209,
+ "meta":{
+ "timestamp":"2024-11-15T00:22:15.765337Z",
+ "date_created":"2024-11-15T00:22:15.765337Z"
+ },
+ "sha1":"d7bcc865b883abd70d74d9af7578d256ae62a973",
+ "type":"combined-opinion",
+ "cites":[
+ 13
+ ],
+ "snippet":"Lorem dolor Obergefell sit amet, consectetur adipiscing elit.",
+ "author_id":17,
+ "local_path":"test/search/opinion_html.html",
+ "per_curiam":false,
+ "download_url":"http://www.supremecourt.gov/opinions/14pdf/14-556_3204.pdf",
+ "joined_by_ids":[
+
+ ]
+ }
+ ],
+ "syllabus":"",
+ "citeCount":432,
+ "dateFiled":"2015-06-26",
+ "docket_id":2668808,
+ "lexisCite":"2015 U.S. LEXIS 4250",
+ "panel_ids":[
+
+ ],
+ "cluster_id":2812209,
+ "dateArgued":null,
+ "suitNature":"",
+ "neutralCite":"10 Neutral 4",
+ "panel_names":[
+ "Kristine Pena Mendez Jr."
+ ],
+ "sibling_ids":[
+ 2812209
+ ],
+ "absolute_url":"/opinion/2812209/obergefell-v-hodges/",
+ "caseNameFull":"Todd Ballard, Julie Smith, Jeanne Peters, Ashley Jordan, and Andrew Thompson v. Johnson LLC, Walker-Bernard, Jones PLC, Bailey Group, and Lopez-Gonzales",
+ "dateReargued":null,
+ "docketNumber":"14-556",
+ "procedural_history":"",
+ "dateReargumentDenied":null,
+ "court_citation_string":"SCOTUS",
+ "non_participating_judge_ids":[
+
+ ]
+ }
+ ],
+ "alert":{
+ "id":1,
+ "name":"Obergefell v. Hodges",
+ "rate":"dly",
+ "user":1,
+ "query":"?q=Obergefell+v.+Hodges&type=o&order_by=score+desc&stat_Precedential=on&filed_after=06%2F01%2F2015&docket_number=14-556&",
+ "secret_key":"MV8yfOSXQvDQauQOCEg0orkgi4hBC4r9yg5Wk3ue",
+ "date_created":"2022-12-02T15:42:34.921805-08:00",
+ "date_last_hit":"2022-12-02T15:42:35.915737-08:00",
+ "date_modified":"2022-12-02T15:42:35.915880-08:00"
+ }
+ },
+ "webhook":{
+ "version":{{"webhook_version"}},
+ "event_type":2,
+ "date_created":"2022-12-02T23:42:34.894411+00:00",
+ "deprecation_date":"None"
+ }
+}
diff --git a/cl/visualizations/admin.py b/cl/visualizations/admin.py
index 5b8064d274..3287fb258c 100644
--- a/cl/visualizations/admin.py
+++ b/cl/visualizations/admin.py
@@ -72,6 +72,7 @@ class SCOTUSMapAdmin(admin.ModelAdmin):
"published",
"deleted",
)
+ autocomplete_fields = ("user",)
search_fields = (
"id",
"title",
diff --git a/poetry.lock b/poetry.lock
index 7413a0d8c1..3703770058 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -412,13 +412,13 @@ zstd = ["zstandard (==0.22.0)"]
[[package]]
name = "certifi"
-version = "2024.8.30"
+version = "2024.12.14"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
files = [
- {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
- {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
+ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"},
+ {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"},
]
[[package]]
@@ -992,18 +992,18 @@ dev = ["black", "flake8", "therapist", "tox", "twine"]
[[package]]
name = "django-cors-headers"
-version = "4.5.0"
+version = "4.6.0"
description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)."
optional = false
python-versions = ">=3.9"
files = [
- {file = "django_cors_headers-4.5.0-py3-none-any.whl", hash = "sha256:28c1ded847aa70208798de3e42422a782f427b8b720e8d7319d34b654b5978e6"},
- {file = "django_cors_headers-4.5.0.tar.gz", hash = "sha256:6c01a85cf1ec779a7bde621db853aa3ce5c065a5ba8e27df7a9f9e8dac310f4f"},
+ {file = "django_cors_headers-4.6.0-py3-none-any.whl", hash = "sha256:8edbc0497e611c24d5150e0055d3b178c6534b8ed826fb6f53b21c63f5d48ba3"},
+ {file = "django_cors_headers-4.6.0.tar.gz", hash = "sha256:14d76b4b4c8d39375baeddd89e4f08899051eeaf177cb02a29bd6eae8cf63aa8"},
]
[package.dependencies]
asgiref = ">=3.6"
-django = ">=3.2"
+django = ">=4.2"
[[package]]
name = "django-csp"
@@ -1088,17 +1088,17 @@ Django = ">=3.2"
[[package]]
name = "django-filter"
-version = "23.5"
+version = "24.3"
description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "django-filter-23.5.tar.gz", hash = "sha256:67583aa43b91fe8c49f74a832d95f4d8442be628fd4c6d65e9f811f5153a4e5c"},
- {file = "django_filter-23.5-py3-none-any.whl", hash = "sha256:99122a201d83860aef4fe77758b69dda913e874cc5e0eaa50a86b0b18d708400"},
+ {file = "django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64"},
+ {file = "django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3"},
]
[package.dependencies]
-Django = ">=3.2"
+Django = ">=4.2"
[[package]]
name = "django-hcaptcha"
@@ -1192,13 +1192,13 @@ django = ">=3.2"
[[package]]
name = "django-pghistory"
-version = "3.4.4"
+version = "3.5.1"
description = "History tracking for Django and Postgres"
optional = false
-python-versions = "<4,>=3.8.0"
+python-versions = "<4,>=3.9.0"
files = [
- {file = "django_pghistory-3.4.4-py3-none-any.whl", hash = "sha256:d0402a8ef5a40ce39283bbf68d9f3388dbabf183d7efc0925972d231cba89621"},
- {file = "django_pghistory-3.4.4.tar.gz", hash = "sha256:2e68d5ee3a9e811a0f0663eb9c35ab6b3b92c8d93a4fa865aeaef2359eef0a41"},
+ {file = "django_pghistory-3.5.1-py3-none-any.whl", hash = "sha256:900e5be084d20519528a1c66a354464a74b80ec0101abb7541ded61ff46759b7"},
+ {file = "django_pghistory-3.5.1.tar.gz", hash = "sha256:28a4238326651d60c33a22337c3c93edc0e657d26ef7faac412875c7b4d40d1c"},
]
[package.dependencies]
@@ -1903,13 +1903,13 @@ test = ["Cython (>=0.29.24)"]
[[package]]
name = "httpx"
-version = "0.27.2"
+version = "0.28.1"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
files = [
- {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
- {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
+ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
+ {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
]
[package.dependencies]
@@ -1918,7 +1918,6 @@ certifi = "*"
h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""}
httpcore = "==1.*"
idna = "*"
-sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
@@ -2320,13 +2319,13 @@ setuptools = "*"
[[package]]
name = "juriscraper"
-version = "2.6.47"
+version = "2.6.48"
description = "An API to scrape American court websites for metadata."
optional = false
python-versions = "*"
files = [
- {file = "juriscraper-2.6.47-py27-none-any.whl", hash = "sha256:6a471b971635ad3a78783a52608ec49a88cb58873731bc64510bc5a2d64431de"},
- {file = "juriscraper-2.6.47.tar.gz", hash = "sha256:a98281ebff859c434a7aa9ce9bca8036cb14d80c0a1e627b6c9374b65a91dd7b"},
+ {file = "juriscraper-2.6.48-py27-none-any.whl", hash = "sha256:f2e198cb66a5d3f1423ec4928fc76e1f25c13d0caafc2a6262a7d158c39eab8e"},
+ {file = "juriscraper-2.6.48.tar.gz", hash = "sha256:bc138e2c5776f55ef96c10f4a4185d0fec80d83e555e25d1f3fb4b384d399c53"},
]
[package.dependencies]
@@ -2932,13 +2931,13 @@ files = [
[[package]]
name = "openai"
-version = "1.52.0"
+version = "1.58.1"
description = "The official Python library for the openai API"
optional = false
-python-versions = ">=3.7.1"
+python-versions = ">=3.8"
files = [
- {file = "openai-1.52.0-py3-none-any.whl", hash = "sha256:0c249f20920183b0a2ca4f7dba7b0452df3ecd0fa7985eb1d91ad884bc3ced9c"},
- {file = "openai-1.52.0.tar.gz", hash = "sha256:95c65a5f77559641ab8f3e4c3a050804f7b51d278870e2ec1f7444080bfe565a"},
+ {file = "openai-1.58.1-py3-none-any.whl", hash = "sha256:e2910b1170a6b7f88ef491ac3a42c387f08bd3db533411f7ee391d166571d63c"},
+ {file = "openai-1.58.1.tar.gz", hash = "sha256:f5a035fd01e141fc743f4b0e02c41ca49be8fab0866d3b67f5f29b4f4d3c0973"},
]
[package.dependencies]
@@ -2953,6 +2952,7 @@ typing-extensions = ">=4.11,<5"
[package.extras]
datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
+realtime = ["websockets (>=13,<15)"]
[[package]]
name = "outcome"
@@ -3660,20 +3660,20 @@ testutils = ["gitpython (>3)"]
[[package]]
name = "pyopenssl"
-version = "24.2.1"
+version = "24.3.0"
description = "Python wrapper module around the OpenSSL library"
optional = false
python-versions = ">=3.7"
files = [
- {file = "pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d"},
- {file = "pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95"},
+ {file = "pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a"},
+ {file = "pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36"},
]
[package.dependencies]
-cryptography = ">=41.0.5,<44"
+cryptography = ">=41.0.5,<45"
[package.extras]
-docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"]
+docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"]
test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"]
[[package]]
@@ -4467,13 +4467,13 @@ websocket-client = ">=1.8,<2.0"
[[package]]
name = "sentry-sdk"
-version = "2.17.0"
+version = "2.19.2"
description = "Python client for Sentry (https://sentry.io)"
optional = false
python-versions = ">=3.6"
files = [
- {file = "sentry_sdk-2.17.0-py2.py3-none-any.whl", hash = "sha256:625955884b862cc58748920f9e21efdfb8e0d4f98cca4ab0d3918576d5b606ad"},
- {file = "sentry_sdk-2.17.0.tar.gz", hash = "sha256:dd0a05352b78ffeacced73a94e86f38b32e2eae15fff5f30ca5abb568a72eacf"},
+ {file = "sentry_sdk-2.19.2-py2.py3-none-any.whl", hash = "sha256:ebdc08228b4d131128e568d696c210d846e5b9d70aa0327dec6b1272d9d40b84"},
+ {file = "sentry_sdk-2.19.2.tar.gz", hash = "sha256:467df6e126ba242d39952375dd816fbee0f217d119bf454a8ce74cf1e7909e8d"},
]
[package.dependencies]
@@ -4501,14 +4501,16 @@ grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"]
http2 = ["httpcore[http2] (==1.*)"]
httpx = ["httpx (>=0.16.0)"]
huey = ["huey (>=2)"]
-huggingface-hub = ["huggingface-hub (>=0.22)"]
+huggingface-hub = ["huggingface_hub (>=0.22)"]
langchain = ["langchain (>=0.0.210)"]
+launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"]
litestar = ["litestar (>=2.0.0)"]
loguru = ["loguru (>=0.5)"]
openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"]
+openfeature = ["openfeature-sdk (>=0.7.1)"]
opentelemetry = ["opentelemetry-distro (>=0.35b0)"]
opentelemetry-experimental = ["opentelemetry-distro"]
-pure-eval = ["asttokens", "executing", "pure-eval"]
+pure-eval = ["asttokens", "executing", "pure_eval"]
pymongo = ["pymongo (>=3.1)"]
pyspark = ["pyspark (>=2.4.4)"]
quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
@@ -5286,20 +5288,20 @@ dev = ["black", "flake8", "isort", "mypy", "parserator", "pytest"]
[[package]]
name = "uvicorn"
-version = "0.32.0"
+version = "0.34.0"
description = "The lightning-fast ASGI server."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
files = [
- {file = "uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82"},
- {file = "uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e"},
+ {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"},
+ {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"},
]
[package.dependencies]
click = ">=7.0"
colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""}
h11 = ">=0.8"
-httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""}
+httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""}
python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""}
uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
@@ -5307,7 +5309,7 @@ watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standar
websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""}
[package.extras]
-standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
+standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
[[package]]
name = "uvloop"
@@ -5690,4 +5692,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.13, <3.14"
-content-hash = "638849e8b93312af48bcd4bae74b198fde89a59a666e21b546ada9dae6a656c4"
+content-hash = "8703160c5832be62299f5a926fa8670aed8715cb7c03dc7dd7be2d1a5c84fb2a"
diff --git a/pyproject.toml b/pyproject.toml
index 03f2c0b45c..ecff70afd5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,19 +30,19 @@ ada-url = "^1.15.0"
argparse = "*"
beautifulsoup4 = "==4.12.*"
celery = "^5.4.0"
-certifi = "^2024.8.30"
+certifi = "^2024.12.14"
courts-db = "*"
disposable-email-domains = "*"
Django = "^5.1.2"
django-cache-memoize = "==0.*"
-django-cors-headers = "^4.5.0"
+django-cors-headers = "^4.6.0"
django-csp = "^3.8"
django-extensions = "^3.2.3"
-django-filter = "^23.5"
+django-filter = "^24.3"
django-localflavor = "^4.0"
django-markdown-deux = "^1.0.6"
django-mathfilters = "*"
-django-pghistory = "^3.4.4"
+django-pghistory = "^3.5.1"
django-ratelimit = "^4.1.0"
django-storages = "^1.14.3"
djangorestframework = {git = "https://github.com/encode/django-rest-framework.git", rev = "cc3c89a11c7ee9cf7cfd732e0a329c318ace71b2"}
@@ -93,20 +93,20 @@ django-override-storage = "^0.3.2"
django-environ = "^0.11.2"
judge-pics = "^2.0.5"
django-admin-cursor-paginator = "^0.1.6"
-sentry-sdk = {extras = ["celery", "django"], version = "^2.17.0"}
+sentry-sdk = {extras = ["celery", "django"], version = "^2.19.2"}
selenium = "^4.25.0"
ipython = "^8.28.0"
time-machine = "^2.16.0"
dateparser = "1.2.0"
types-dateparser = "^1.2.0.20240420"
-uvicorn = {extras = ["standard"], version = "^0.32.0"}
+uvicorn = {extras = ["standard"], version = "^0.34.0"}
daphne = "^4.1.2"
-httpx = {extras = ["http2"], version = "^0.27.2"}
+httpx = {extras = ["http2"], version = "^0.28.1"}
django-model-utils = "^5.0.0"
django-permissions-policy = "^4.22.0"
tiktoken = "^0.8.0"
hyperscan = "^0.7.8"
-openai = "^1.52.0"
+openai = "^1.58.1"
seal-rookery = "^2.2.5"
types-pytz = "^2024.2.0.20241003"
psycopg = {version = "3.2.3", extras = ["binary", "pool"]}