diff --git a/.ds.baseline b/.ds.baseline index 13299735d..edd26704b 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -161,7 +161,7 @@ "filename": "app/config.py", "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", "is_verified": false, - "line_number": 120, + "line_number": 121, "is_secret": false } ], @@ -634,5 +634,5 @@ } ] }, - "generated_at": "2025-10-01T14:58:39Z" + "generated_at": "2025-10-01T20:50:24Z" } diff --git a/app/__init__.py b/app/__init__.py index 9ff4e3b44..ba6b9dfa1 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -198,17 +198,17 @@ def create_app(application): # @application.context_processor # def inject_feature_flags(): - # this is where feature flags can be easily added as a dictionary within context - # feature_socket_enabled = application.config.get("FEATURE_SOCKET_ENABLED", True) + # this is where feature flags can be easily added as a dictionary within context + # feature_socket_enabled = application.config.get("FEATURE_SOCKET_ENABLED", True) - # current_app.logger.info( - # f"FEATURE_SOCKET_ENABLED value in __init__.py coming \ - # from config is {application.config.get('FEATURE_SOCKET_ENABLED')} and \ - # the ending value is {feature_socket_enabled}" - # ) - # return dict( - # FEATURE_SOCKET_ENABLED=feature_socket_enabled, - # ) + # current_app.logger.info( + # f"FEATURE_SOCKET_ENABLED value in __init__.py coming \ + # from config is {application.config.get('FEATURE_SOCKET_ENABLED')} and \ + # the ending value is {feature_socket_enabled}" + # ) + # return dict( + # FEATURE_SOCKET_ENABLED=feature_socket_enabled, + # ) @application.context_processor def inject_initial_signin_url(): diff --git a/app/assets/javascripts/job-polling.js b/app/assets/javascripts/job-polling.js index ac5e351d9..745e7f274 100644 --- a/app/assets/javascripts/job-polling.js +++ b/app/assets/javascripts/job-polling.js @@ -130,7 +130,9 @@ } loadNotificationsTable() { - const url = `${window.location.href.split('?')[0]}?_=${Date.now()}`; + const url = `/services/${this.serviceId}/jobs/${this.jobId}/notifications-table`; + + console.debug('Loading notifications table from:', url); fetch(url, { headers: { @@ -138,20 +140,28 @@ 'Pragma': 'no-cache' } }) - .then(response => response.text()) + .then(response => { + console.debug('Notifications table response status:', response.status); + if (response.status === 204) { + // No content yet, job still processing + console.debug('Job still processing, no notifications yet'); + return null; + } + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return response.text(); + }) .then(html => { - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - const notificationsTable = doc.querySelector('.job-status-table'); + if (!html) return; - if (notificationsTable) { - const insertPoint = document.querySelector('.notification-status'); - if (insertPoint) { - insertPoint.insertAdjacentElement('afterend', notificationsTable); - } else { - window.location.reload(); - } + console.debug('Received HTML length:', html.length); + const insertPoint = document.querySelector('[data-key="notifications"]'); + if (insertPoint) { + console.debug('Inserting notifications table'); + insertPoint.innerHTML = html; } else { + console.error('Could not find [data-key="notifications"], reloading page'); window.location.reload(); } }) diff --git a/app/config.py b/app/config.py index 9f332b242..718dfd88b 100644 --- a/app/config.py +++ b/app/config.py @@ -66,6 +66,7 @@ class Config(object): SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_NAME = "notify_admin_session" SESSION_COOKIE_SECURE = True + SESSION_COOKIE_SAMESITE = "Lax" # don't send back the cookie if it hasn't been modified by the request. this means that the expiry time won't be # updated unless the session is changed - but it's generally refreshed by `save_service_or_org_after_request` # every time anyway, except for specific endpoints (png/pdfs generally) where we've disabled that handler. diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py index 3010e5b53..adc56cec7 100644 --- a/app/main/views/jobs.py +++ b/app/main/views/jobs.py @@ -23,7 +23,7 @@ from app import ( service_api_client, ) from app.enums import NotificationStatus, ServicePermission -from app.formatters import message_count_noun +from app.formatters import get_time_left, message_count_noun from app.main import main from app.main.forms import SearchNotificationsForm from app.models.job import Job @@ -136,7 +136,9 @@ def view_job_status_poll(service_id, job_id): try: api_response = job_api_client.get_job_status(service_id, job_id) except HTTPError as e: - current_app.logger.error(f"API error fetching job status: {e.status_code} - {e.message}") + current_app.logger.error( + f"API error fetching job status: {e.status_code} - {e.message}" + ) if e.status_code == 404: abort(404) elif e.status_code >= 500: @@ -155,6 +157,44 @@ def view_job_status_poll(service_id, job_id): return jsonify(response_data) +@main.route("/services//jobs//notifications-table") +@user_has_permissions() +def view_job_notifications_table(service_id, job_id): + """Endpoint that returns only the notifications table HTML fragment.""" + job = Job.from_id(job_id, service_id=current_service.id) + + if job.cancelled or not job.finished_processing: + return "", 204 + + filter_args = parse_filter_args(request.args) + filter_args["status"] = set_status_filters(filter_args) + + notifications_data = job.get_notifications(status=filter_args["status"]) + notifications = list( + add_preview_of_content_to_notifications( + notifications_data.get("notifications", []) + ) + ) + more_than_one_page = bool(notifications_data.get("links", {}).get("next")) + + return render_template( + "partials/jobs/notifications.html", + job=job, + notifications=notifications, + more_than_one_page=more_than_one_page, + uploaded_file_name=job.original_file_name, + time_left=get_time_left(job.created_at), + service_data_retention_days=current_service.get_days_of_retention( + job.template_type, number_of_days="seven_day" + ), + download_link=url_for( + ".view_job_csv", + service_id=current_service.id, + job_id=job.id, + ), + ) + + @main.route("/services//notifications", methods=["GET", "POST"]) @main.route( "/services//notifications/", diff --git a/app/templates/views/activity/all-activity.html b/app/templates/views/activity/all-activity.html index 3e2608035..c536eb555 100644 --- a/app/templates/views/activity/all-activity.html +++ b/app/templates/views/activity/all-activity.html @@ -59,19 +59,6 @@ {% endif %} {% endset %} {% block maincolumn_content %} -

All activity

All activity

diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index 0f81c9b90..e0c90a08f 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -241,6 +241,7 @@ EXCLUDED_ENDPOINTS = tuple( "verify_email", "view_job", "view_job_csv", + "view_job_notifications_table", "view_job_status_poll", "view_jobs", "view_notification",