diff --git a/cl/api/static/png/add-webhook-endpoint-v2.png b/cl/api/static/png/add-webhook-endpoint-v2.png new file mode 100644 index 0000000000..63098fc17a Binary files /dev/null and b/cl/api/static/png/add-webhook-endpoint-v2.png differ diff --git a/cl/api/static/png/add-webhook-endpoint.png b/cl/api/static/png/add-webhook-endpoint.png deleted file mode 100644 index a2a4ebf227..0000000000 Binary files a/cl/api/static/png/add-webhook-endpoint.png and /dev/null differ diff --git a/cl/api/static/png/re-enable-webhook-v2.png b/cl/api/static/png/re-enable-webhook-v2.png new file mode 100644 index 0000000000..7241a9efbe Binary files /dev/null and b/cl/api/static/png/re-enable-webhook-v2.png differ diff --git a/cl/api/static/png/re-enable-webhook.png b/cl/api/static/png/re-enable-webhook.png deleted file mode 100644 index 7e8cbb1e14..0000000000 Binary files a/cl/api/static/png/re-enable-webhook.png and /dev/null differ diff --git a/cl/api/static/png/test-curl-webhook-event-v2.png b/cl/api/static/png/test-curl-webhook-event-v2.png new file mode 100644 index 0000000000..484b150e50 Binary files /dev/null and b/cl/api/static/png/test-curl-webhook-event-v2.png differ diff --git a/cl/api/static/png/test-curl-webhook-event.png b/cl/api/static/png/test-curl-webhook-event.png deleted file mode 100644 index 543a371ccd..0000000000 Binary files a/cl/api/static/png/test-curl-webhook-event.png and /dev/null differ diff --git a/cl/api/static/png/test-json-webhook-event-v2.png b/cl/api/static/png/test-json-webhook-event-v2.png new file mode 100644 index 0000000000..184f4fd532 Binary files /dev/null and b/cl/api/static/png/test-json-webhook-event-v2.png differ diff --git a/cl/api/static/png/test-json-webhook-event.png b/cl/api/static/png/test-json-webhook-event.png deleted file mode 100644 index 6c7cf64fc5..0000000000 Binary files a/cl/api/static/png/test-json-webhook-event.png and /dev/null differ diff --git a/cl/api/static/png/webhook-disabled-v2.png b/cl/api/static/png/webhook-disabled-v2.png new file mode 100644 index 0000000000..2d77c55a24 Binary files /dev/null and b/cl/api/static/png/webhook-disabled-v2.png differ diff --git a/cl/api/static/png/webhook-disabled.png b/cl/api/static/png/webhook-disabled.png deleted file mode 100644 index dcb4d2f710..0000000000 Binary files a/cl/api/static/png/webhook-disabled.png and /dev/null differ diff --git a/cl/api/static/png/webhooks-panel-v2.png b/cl/api/static/png/webhooks-panel-v2.png new file mode 100644 index 0000000000..57dbaf1b54 Binary files /dev/null and b/cl/api/static/png/webhooks-panel-v2.png differ diff --git a/cl/api/static/png/webhooks-panel.png b/cl/api/static/png/webhooks-panel.png deleted file mode 100644 index 3644c234d6..0000000000 Binary files a/cl/api/static/png/webhooks-panel.png and /dev/null differ diff --git a/cl/api/templates/search-api-docs-vlatest.html b/cl/api/templates/search-api-docs-vlatest.html index 19d3c716be..89de45e3c6 100644 --- a/cl/api/templates/search-api-docs-vlatest.html +++ b/cl/api/templates/search-api-docs-vlatest.html @@ -154,7 +154,7 @@

Setting the Result type

o - Case law opinions + Case law opinion clusters with nested Opinion documents. 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.

- screenshot of how to re-enable a webhook endpointChange 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:

    - screenshot of the webhook panelSet Up a Webhook Endpoint in CourtListener

    Click the “Add webhook” button and the “Add webhook endpoint” modal pops up:

    - screenshot of how to add a webhook endpointSet 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:

    - screenshot of a disabled webhook endpointTesting 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:

    - screenshot of a disabled webhook endpointTesting 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.

    - screenshot of the webhook json test modalTesting 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.

    - screenshot of the webhook curl test modal 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"]}