diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d7ec83be43..2203ff239c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -28,11 +28,11 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: python config-file: ./.github/codeql-config.yml setup-python-dependencies: true - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 89d4b1dc7a..8cac4f50ba 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -33,7 +33,7 @@ jobs: run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v3 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/flp-dependencies-pr.yml b/.github/workflows/flp-dependencies-pr.yml index 710082cdd2..a3d4fdf4ee 100644 --- a/.github/workflows/flp-dependencies-pr.yml +++ b/.github/workflows/flp-dependencies-pr.yml @@ -24,7 +24,7 @@ jobs: poetry update courts-db eyecite juriscraper reporters-db - name: Create Pull Request - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@v7 with: commit-message: Update freelawproject dependencies title: Update freelawproject dependencies diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 92d2426e66..38d8297458 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,7 +53,7 @@ jobs: # Build and cache docker images so tests are always run on the latest # dependencies - name: Set up docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 with: driver-opts: network=host - name: Prebuild docker images diff --git a/cl/alerts/views.py b/cl/alerts/views.py index e7a46f2670..aecf21a78e 100644 --- a/cl/alerts/views.py +++ b/cl/alerts/views.py @@ -274,7 +274,7 @@ async def new_docket_alert(request: AuthenticatedHttpRequest) -> HttpResponse: ).aearliest("date_created") title = f"New Docket Alert for {make_docket_title(docket)}" - has_alert = await user_has_alert(await request.auser(), docket) # type: ignore[attr-defined] + has_alert = await user_has_alert(await request.auser(), docket) # type: ignore[arg-type] return TemplateResponse( request, "docket_alert_new.html", diff --git a/cl/api/models.py b/cl/api/models.py index 0e28a15f50..2bffe2d9c0 100644 --- a/cl/api/models.py +++ b/cl/api/models.py @@ -26,28 +26,28 @@ class WebhookEventType(models.IntegerChoices): AfterUpdateOrDeleteSnapshot(), model_name="WebhookHistoryEvent" ) class Webhook(AbstractDateTimeModel): - user = models.ForeignKey( + user: models.ForeignKey = models.ForeignKey( User, help_text="The user that has provisioned the webhook.", related_name="webhooks", on_delete=models.CASCADE, ) - event_type = models.IntegerField( + event_type: models.IntegerField = models.IntegerField( help_text="The event type that triggers the webhook.", choices=WebhookEventType.choices, ) - url = models.URLField( + url: models.URLField = models.URLField( help_text="The URL that receives a POST request from the webhook.", max_length=2000, validators=[URLValidator(schemes=["https"])], ) - enabled = models.BooleanField( + enabled: models.BooleanField = models.BooleanField( help_text="An on/off switch for the webhook.", default=False ) - version = models.IntegerField( + version: models.IntegerField = models.IntegerField( help_text="The specific version of the webhook provisioned.", default=1 ) - failure_count = models.IntegerField( + failure_count: models.IntegerField = models.IntegerField( help_text="The number of failures (400+ status) responses the webhook " "has received.", default=0, @@ -75,51 +75,51 @@ class WEBHOOK_EVENT_STATUS: class WebhookEvent(AbstractDateTimeModel): - webhook = models.ForeignKey( + webhook: models.ForeignKey = models.ForeignKey( Webhook, help_text="The Webhook this event is associated with.", related_name="webhook_events", on_delete=models.CASCADE, ) - event_id = models.UUIDField( + event_id: models.UUIDField = models.UUIDField( help_text="Unique event identifier", default=uuid.uuid4, editable=False, ) - event_status = models.SmallIntegerField( + event_status: models.SmallIntegerField = models.SmallIntegerField( help_text="The webhook event status.", default=WEBHOOK_EVENT_STATUS.IN_PROGRESS, choices=WEBHOOK_EVENT_STATUS.STATUS, ) - content = models.JSONField( # type: ignore + content: models.JSONField = models.JSONField( help_text="The content of the outgoing body in the POST request.", blank=True, null=True, ) - next_retry_date = models.DateTimeField( + next_retry_date: models.DateTimeField = models.DateTimeField( help_text="The scheduled datetime to retry the webhook event.", blank=True, null=True, ) - error_message = models.TextField( + error_message: models.TextField = models.TextField( help_text="The error raised by a failed POST request.", blank=True, ) - response = models.TextField( + response: models.TextField = models.TextField( help_text="The response received from the POST request.", blank=True, ) - retry_counter = models.SmallIntegerField( + retry_counter: models.SmallIntegerField = models.SmallIntegerField( help_text="The retry counter for the exponential backoff event.", default=0, ) - status_code = models.SmallIntegerField( + status_code: models.SmallIntegerField = models.SmallIntegerField( help_text="The HTTP status code received from the POST request.", choices=HttpStatusCodes.choices, # type: ignore[attr-defined] blank=True, null=True, ) - debug = models.BooleanField( + debug: models.BooleanField = models.BooleanField( help_text="Enabled if this is a test event for debugging purposes.", default=False, ) diff --git a/cl/api/templates/bulk-data.html b/cl/api/templates/bulk-data.html index 84b590687b..0929b9df08 100644 --- a/cl/api/templates/bulk-data.html +++ b/cl/api/templates/bulk-data.html @@ -163,6 +163,12 @@

Adding Features and Fixing Bugs

Release Notes

+

+ 2024-08-07: Added filepath_pdf_harvard field to OpinionCluster data in bulk exports. This field contains the path to the PDF file from the Harvard Caselaw Access Project for the given case. +

+

+ 2024-08-02: Add new fields to the bulk data files for the Docket object: federal_dn_case_type, federal_dn_office_code, federal_dn_judge_initials_assigned, federal_dn_judge_initials_referred, federal_defendant_number, parent_docket_id. +

2023-09-26: Bulk script refactored to make it easier to maintain. Courthouse table added to bulk script. Court appeals_to through table added to bulk script. Bulk script now automatically generates a shell script to load bulk data and stream the script to S3.

diff --git a/cl/api/templates/citation-api-docs-vlatest.html b/cl/api/templates/citation-api-docs-vlatest.html index 0b529578eb..0f27031f38 100644 --- a/cl/api/templates/citation-api-docs-vlatest.html +++ b/cl/api/templates/citation-api-docs-vlatest.html @@ -80,7 +80,7 @@

Opinions Cited/Citing API
{% url "opinio } }

To understand RelatedFilters, see our filtering documentation.

-

These filters allow you to filter to the opinions that an opinion cites (it's "Authorities") or the opinions that cite it. +

These filters allow you to filter to the opinions that an opinion cites (its "Authorities" or backward citations) or the later opinions that cite it (forward citations).

For example, opinion 2812209 is the decision in Obergefell v. Hodges. To see what it cites:

curl -v \
diff --git a/cl/api/tests.py b/cl/api/tests.py
index f03422c339..f550de3b4a 100644
--- a/cl/api/tests.py
+++ b/cl/api/tests.py
@@ -59,6 +59,8 @@
     SchoolViewSet,
     SourceViewSet,
 )
+from cl.people_db.factories import PartyFactory, PartyTypeFactory
+from cl.people_db.models import Attorney
 from cl.recap.factories import ProcessingQueueFactory
 from cl.recap.views import (
     EmailProcessingQueueViewSet,
@@ -759,6 +761,21 @@ async def test_attorney_filters(self) -> None:
         self.q = {"parties_represented__name__contains": "Honker-Nope"}
         await self.assertCountInResults(0)
 
+        # Adds extra role to the existing attorney
+        docket = await Docket.objects.afirst()
+        attorney = await Attorney.objects.afirst()
+        await sync_to_async(PartyTypeFactory.create)(
+            party=await sync_to_async(PartyFactory)(
+                docket=docket,
+                attorneys=[attorney],
+            ),
+            docket=docket,
+        )
+        self.q = {"docket__date_created__range": "2017-04-14,2017-04-15"}
+        await self.assertCountInResults(1)
+        self.q = {"docket__date_created__range": "2017-04-15,2017-04-16"}
+        await self.assertCountInResults(0)
+
     async def test_party_filters(self) -> None:
         self.path = reverse("party-list", kwargs={"version": "v3"})
 
diff --git a/cl/api/webhooks.py b/cl/api/webhooks.py
index cf6e3f6cab..f6ca97d9e3 100644
--- a/cl/api/webhooks.py
+++ b/cl/api/webhooks.py
@@ -1,4 +1,5 @@
 import json
+import random
 
 import requests
 from django.conf import settings
@@ -38,7 +39,7 @@ def send_webhook_event(
     the webhook is sent.
     """
     proxy_server = {
-        "http": settings.EGRESS_PROXY_HOST,  # type: ignore
+        "http": random.choice(settings.EGRESS_PROXY_HOSTS),  # type: ignore
     }
     headers = {
         "Content-type": "application/json",
diff --git a/cl/assets/static-global/css/override.css b/cl/assets/static-global/css/override.css
index cb62236644..7a27e9f08f 100644
--- a/cl/assets/static-global/css/override.css
+++ b/cl/assets/static-global/css/override.css
@@ -745,7 +745,6 @@ div.shown ul {
   padding: 3px 0 5px 0;
 }
 
-
 #summaries ul {
   padding-inline-start: 15px;
   border-bottom: 1pt solid #DDD;
@@ -893,6 +892,20 @@ input.court-checkbox, input.status-checkbox {
   border-bottom: 1px solid #dddddd;
 }
 
+.description-header,
+.export-csv {
+  padding: 0px;
+}
+
+#export-csv-error {
+  padding: 5px 0px;
+}
+
+@media (min-width: 767px) {
+  .export-csv {
+    padding: 5px 10px;
+  }
+}
 #docket-entry-table .recap-documents.row {
   padding-top: 0px;
   border-bottom: none;
@@ -1681,3 +1694,32 @@ rect.series-segment {
 .ml-1 {
   margin-left: 0.25rem !important;
 }
+
+.htmx-indicator {
+  opacity: 0;
+  transition: opacity 200ms ease-in;
+}
+
+.htmx-hidden-indicator {
+    display:none;
+}
+.htmx-request .htmx-hidden-indicator {
+    display:inline;
+}
+.htmx-request.htmx-hidden-indicator {
+    display:inline;
+}
+
+.turbo-progress-bar {
+  position: fixed;
+  display: block;
+  top: 0;
+  left: 0;
+  height: 3px;
+  background: #B53C2C;
+  z-index: 2147483647;
+  transition:
+    width 300ms ease-out,
+    opacity 150ms 150ms ease-in;
+  transform: translate3d(0, 0, 0);
+}
diff --git a/cl/assets/static-global/js/base.js b/cl/assets/static-global/js/base.js
index ea71c270b5..99355aa207 100644
--- a/cl/assets/static-global/js/base.js
+++ b/cl/assets/static-global/js/base.js
@@ -103,6 +103,8 @@ $(document).ready(function () {
         .val(el.val())
         .appendTo('#search-form');
     });
+    installProgressBar();
+    disableAllSubmitButtons();
     document.location = '/?' + $('#search-form').serialize();
   }
 
diff --git a/cl/assets/static-global/js/export-csv.js b/cl/assets/static-global/js/export-csv.js
new file mode 100644
index 0000000000..90fd2206a3
--- /dev/null
+++ b/cl/assets/static-global/js/export-csv.js
@@ -0,0 +1,55 @@
+document.addEventListener('htmx:beforeRequest', function () {
+  // If the mobile error message is currently visible, adds the 'hidden' class
+  // to hide it.
+  let mobileErrorMessage = document.getElementById('mobile-export-csv-error');
+  if (!mobileErrorMessage.classList.contains('hidden')) {
+    mobileErrorMessage.classList.add('hidden');
+  }
+
+  // If the desktop error message is currently visible, adds the 'hidden' class
+  // to hide it.
+  let desktopErrorMessage = document.getElementById('export-csv-error');
+  if (!desktopErrorMessage.classList.contains('hidden')) {
+    desktopErrorMessage.classList.add('hidden');
+  }
+});
+
+document.addEventListener('htmx:beforeOnLoad', function (event) {
+  // Get the XMLHttpRequest object from the event details
+  const xhr = event.detail.xhr;
+  if (xhr.status == 200) {
+    const response = xhr.response;
+    // Extract the filename from the Content-Disposition header and get
+    //the MIME type from the Content-Type header
+    const filename = xhr.getResponseHeader('Content-Disposition').split('=')[1];
+    const mimetype = xhr.getResponseHeader('Content-Type');
+
+    // Prepare a link element for a file download
+    // Create a hidden link element for download
+    const link = document.createElement('a');
+    link.style.display = 'none';
+
+    // Create a Blob object containing the response data with the correct
+    // MIME type and generate a temporary URL for it
+    const blob = new Blob([response], { type: mimetype });
+    const url = window.URL.createObjectURL(blob);
+
+    // Set the link's attributes for download
+    link.href = url;
+    link.download = filename.replaceAll('"', '');
+
+    // It needs to be added to the DOM so it can be clicked
+    document.body.appendChild(link);
+    link.click();
+
+    // Release the temporary URL after download (for memory management)
+    window.URL.revokeObjectURL(url);
+  } else {
+    // If the request failed, show the error messages
+    let mobileErrorMessage = document.getElementById('mobile-export-csv-error');
+    mobileErrorMessage.classList.remove('hidden');
+
+    let desktopErrorMessage = document.getElementById('export-csv-error');
+    desktopErrorMessage.classList.remove('hidden');
+  }
+});
diff --git a/cl/assets/static-global/js/progress-bar.js b/cl/assets/static-global/js/progress-bar.js
new file mode 100644
index 0000000000..f7115da79f
--- /dev/null
+++ b/cl/assets/static-global/js/progress-bar.js
@@ -0,0 +1,32 @@
+let trickleInterval = null;
+
+function updateProgressBar() {
+  let progressElement = document.getElementById('progress-bar');
+  let oldValue = 0;
+  if ('value' in progressElement.dataset) {
+    oldValue = parseFloat(progressElement.dataset.value);
+  }
+  let newValue = oldValue + Math.random() / 10;
+  if (newValue >= 1) newValue = 1;
+  progressElement.style.width = `${10 + newValue * 75}%`;
+  progressElement.dataset.value = newValue;
+}
+
+function installProgressBar() {
+  let progressElement = document.createElement('div');
+  progressElement.id = 'progress-bar';
+  progressElement.classList.add('turbo-progress-bar');
+  progressElement.style.width = '0';
+  progressElement.style.opacity = '1';
+  document.body.prepend(progressElement);
+  trickleInterval = window.setInterval(updateProgressBar, 300);
+}
+
+function disableAllSubmitButtons() {
+  // Get all submit buttons on the page
+  const submitButtons = document.querySelectorAll('input[type="submit"],button[type="submit"]');
+  // Disable each element
+  submitButtons.forEach((button) => {
+    button.disabled = true;
+  });
+}
diff --git a/cl/assets/templates/base.html b/cl/assets/templates/base.html
index f6ff99f3b7..33181f52e1 100644
--- a/cl/assets/templates/base.html
+++ b/cl/assets/templates/base.html
@@ -87,8 +87,6 @@ 

You did not supply the "private" variable to your template. {% include 'includes/dismissible_nav_banner.html' with link="https://free.law/2024/01/18/new-recap-archive-search-is-live" text="A year in the making, today we are launching a huge new search engine for the RECAP Archive" emoji="🎁" cookie_name="no_banner"%} {% endif %} - {% include 'includes/dismissible_nav_banner.html' with link="https://free.law/2024/07/05/new-branding-rip-flip" text="CourtListener, RECAP, and Free Law Project are getting some new logos and new branding!" emoji="🔔" cookie_name="no_branding_banner"%} - {% if EMAIL_BAN_REASON %}