From febd037bed9d7d0b06681e45b1d3f8ac6f28b13e Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 18 Jul 2024 15:57:29 -0700 Subject: [PATCH 01/18] created new activity page --- app/main/__init__.py | 1 + app/main/views/activity.py | 68 ++++++++ app/navigation.py | 3 + app/templates/new/components/main_nav.html | 1 + .../views/activity/all-activity.html | 156 ++++++++++++++++++ app/utils/pagination.py | 17 ++ 6 files changed, 246 insertions(+) create mode 100644 app/main/views/activity.py create mode 100644 app/templates/views/activity/all-activity.html diff --git a/app/main/__init__.py b/app/main/__init__.py index 8626582f2..2375c8e58 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -3,6 +3,7 @@ from flask import Blueprint main = Blueprint("main", __name__) from app.main.views import ( # noqa isort:skip + activity, add_service, api_keys, choose_account, diff --git a/app/main/views/activity.py b/app/main/views/activity.py new file mode 100644 index 000000000..f8e0f8dba --- /dev/null +++ b/app/main/views/activity.py @@ -0,0 +1,68 @@ +from flask import abort, render_template, request, session, url_for +from flask_login import current_user +from werkzeug.utils import redirect + +from app import current_service, job_api_client +from app.formatters import get_time_left +from app.main import main +from app.utils.pagination import ( + generate_next_dict, + generate_pagination_pages, + generate_previous_dict, + get_page_from_request, +) +from app.utils.user import user_has_permissions + + +@main.route("/activity/services/") +@user_has_permissions() +def all_jobs_activity(service_id): + if session.get("invited_user_id"): + session.pop("invited_user_id", None) + session["service_id"] = service_id + + if not current_user.has_permissions("view_activity"): + return redirect(url_for("main.choose_template", service_id=service_id)) + service_data_retention_days = 7 + page = get_page_from_request() + jobs = job_api_client.get_page_of_jobs(service_id, page=page) + all_jobs_dict = generate_job_dict(jobs) + prev_page, next_page, pagination = handle_pagination(jobs, service_id, page) + + return render_template( + "views/activity/all-activity.html", + all_jobs_dict=all_jobs_dict, + service_data_retention_days=service_data_retention_days, + next_page=next_page, + prev_page=prev_page, + pagination=pagination + ) + + +def handle_pagination(jobs, service_id, page): + if page is None: + abort(404, "Invalid page argument ({}).".format(request.args.get("page"))) + + prev_page = generate_previous_dict("main.all_jobs_activity", service_id, page) if page > 1 else None + + next_page = generate_next_dict("main.all_jobs_activity", service_id, page) if jobs["links"].get("next") else None + + pagination = generate_pagination_pages(jobs["total"], jobs['page_size'], page) + + return prev_page, next_page, pagination + + +def generate_job_dict(jobs): + return [ + { + "job_id": job["id"], + "time_left": get_time_left(job["created_at"]), + "download_link": url_for(".view_job_csv", service_id=current_service.id, job_id=job["id"]), + "view_job_link": url_for(".view_job", service_id=current_service.id, job_id=job["id"]), + "created_at": job["created_at"], + "notification_count": job["notification_count"], + "created_by": job["created_by"], + "template_name": job["template_name"] + } + for job in jobs["data"] + ] diff --git a/app/navigation.py b/app/navigation.py index 3c79598cc..3ce3b6e62 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -153,6 +153,9 @@ class HeaderNavigation(Navigation): class MainNavigation(Navigation): mapping = { + "activity": { + "all_jobs_activity", + }, "dashboard": { "conversation", "inbox", diff --git a/app/templates/new/components/main_nav.html b/app/templates/new/components/main_nav.html index a3b02823e..d3ee0fd08 100644 --- a/app/templates/new/components/main_nav.html +++ b/app/templates/new/components/main_nav.html @@ -8,6 +8,7 @@ {% if current_user.has_permissions() %} {% if current_user.has_permissions('view_activity') %}
  • Dashboard
  • +
  • Activity
  • {% endif %} {% if not current_user.has_permissions('view_activity') %}
  • Sent messages
  • diff --git a/app/templates/views/activity/all-activity.html b/app/templates/views/activity/all-activity.html new file mode 100644 index 000000000..8a48b6050 --- /dev/null +++ b/app/templates/views/activity/all-activity.html @@ -0,0 +1,156 @@ +{% extends "withnav_template.html" %} + +{% from "components/ajax-block.html" import ajax_block %} +{% from "components/previous-next-navigation.html" import previous_next_navigation %} + +{% block service_page_title %} + All activity +{% endblock %} + +{% set show_pagination %} + {% if prev_page or next_page %} + + {% endif %} +{% endset %} + + +{% block maincolumn_content %} + +
    +

    All activity

    + {% if current_user.has_permissions('manage_templates') and not current_service.all_templates %} + + {% endif %} + + {{ ajax_block(partials, updates_url, 'upcoming') }} + +

    All activity

    + +

    Sent jobs

    +
    + + + + + + + + + + + + + + + {% if all_jobs_dict %} + {% for job in all_jobs_dict %} + {% if job.job_id %} + + + + + + + + + {% endif %} + {% endfor %} + {% else %} + + + + {% endif %} + +
    + Job ID# + + Template + + Status + + Sender + + Reports + + # of Recipients + + Message parts used + + Delivery rate +
    + + {{ job.job_id[-12:] if job and job.job_id else 'Manually entered number' }} + + + {{ job.template_name }} + + {{ job.created_at | format_datetime_table }} + + {{ job.created_by.name }} + + {% if job.time_left != "Data no longer available" %} + File Download Icon + {% elif job %} + {{ job.time_left }} n/a + {% endif %} + + {{ job.notification_count}} +
    No batched job messages found  (messages are kept for {{ service_data_retention_days }} days).
    +

    + Note: Report data is only available for 7 days after your message has been sent +

    +
    + {{show_pagination}} +
    + +{% endblock %} diff --git a/app/utils/pagination.py b/app/utils/pagination.py index a9ed28d3c..15858d73a 100644 --- a/app/utils/pagination.py +++ b/app/utils/pagination.py @@ -29,3 +29,20 @@ def generate_previous_next_dict(view, service_id, page, title, url_args): "title": title, "label": "page {}".format(page), } + + +def generate_pagination_pages(total_items, page_size, current_page): + total_pages = (total_items + page_size - 1) // page_size + pagination = { + 'current': current_page, + 'pages': [], + 'last': total_pages + } + if total_pages <= 4: + pagination['pages'] = list(range(1, total_pages + 1)) + else: + if current_page <= 3: + pagination['pages'] = [1, 2, 3, total_pages] + else: + pagination['pages'] = [1, current_page - 1, current_page, current_page + 1, total_pages] + return pagination From 5d287d236e451a8ba828ca92ba4107b40aa69683 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Tue, 23 Jul 2024 12:01:40 -0700 Subject: [PATCH 02/18] convert time to sort --- app/__init__.py | 1 + app/assets/javascripts/sortAlphanumeric.js | 14 +++++ app/formatters.py | 5 ++ app/main/views/activity.py | 11 +++- .../views/activity/all-activity.html | 56 ++++++++----------- gulpfile.js | 2 + 6 files changed, 55 insertions(+), 34 deletions(-) create mode 100644 app/assets/javascripts/sortAlphanumeric.js diff --git a/app/__init__.py b/app/__init__.py index 76f3664cd..2a22fb841 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -33,6 +33,7 @@ from app.asset_fingerprinter import asset_fingerprinter from app.config import configs from app.extensions import redis_client from app.formatters import ( + convert_time_unixtimestamp, convert_markdown_template, convert_to_boolean, format_auth_type, diff --git a/app/assets/javascripts/sortAlphanumeric.js b/app/assets/javascripts/sortAlphanumeric.js new file mode 100644 index 000000000..726f7b2cd --- /dev/null +++ b/app/assets/javascripts/sortAlphanumeric.js @@ -0,0 +1,14 @@ +(function (window) { + // document.addEventListener("DOMContentLoaded", function() { + // const rows = document.querySelectorAll('td.table-field.file-name'); + + // rows.forEach(row => { + // let sortValue = row.getAttribute('data-sort-value'); + // if (sortValue) { + // // Remove non-numeric characters to ensure numerical comparison + // sortValue = sortValue.replace(/\D/g, ''); + // row.setAttribute('data-sort-value', sortValue); + // } + // }); + // }); +})(window); diff --git a/app/formatters.py b/app/formatters.py index 5cb3feeaf..c427c2a9a 100644 --- a/app/formatters.py +++ b/app/formatters.py @@ -231,6 +231,11 @@ def naturaltime_without_indefinite_article(date): ) +def convert_time_unixtimestamp(date_string): + dt = datetime.fromisoformat(date_string) + return int(dt.timestamp()) + + def format_delta(date): # This method assumes that date is in UTC date = parse_naive_dt(date) diff --git a/app/main/views/activity.py b/app/main/views/activity.py index f8e0f8dba..a415dc4d5 100644 --- a/app/main/views/activity.py +++ b/app/main/views/activity.py @@ -3,7 +3,7 @@ from flask_login import current_user from werkzeug.utils import redirect from app import current_service, job_api_client -from app.formatters import get_time_left +from app.formatters import get_time_left, convert_time_unixtimestamp from app.main import main from app.utils.pagination import ( generate_next_dict, @@ -35,7 +35,8 @@ def all_jobs_activity(service_id): service_data_retention_days=service_data_retention_days, next_page=next_page, prev_page=prev_page, - pagination=pagination + pagination=pagination, + jobs=jobs ) @@ -56,10 +57,16 @@ def generate_job_dict(jobs): return [ { "job_id": job["id"], + "sort_value": hashlib.sha1(job["id"].encode("utf-8")).hexdigest(), + "job_sort_value": job["id"].replace("-", ""), "time_left": get_time_left(job["created_at"]), "download_link": url_for(".view_job_csv", service_id=current_service.id, job_id=job["id"]), "view_job_link": url_for(".view_job", service_id=current_service.id, job_id=job["id"]), "created_at": job["created_at"], + "time_sent_data_value": convert_time_unixtimestamp(job["processing_finished"] if job["processing_finished"] else job["processing_started"] + if job["processing_started"] else job["created_at"]), + "processing_finished": job["processing_finished"], + "processing_started": job["processing_started"], "notification_count": job["notification_count"], "created_by": job["created_by"], "template_name": job["template_name"] diff --git a/app/templates/views/activity/all-activity.html b/app/templates/views/activity/all-activity.html index 8a48b6050..ad98dd9c6 100644 --- a/app/templates/views/activity/all-activity.html +++ b/app/templates/views/activity/all-activity.html @@ -65,45 +65,36 @@ {% block maincolumn_content %} -

    All activity

    {% if current_user.has_permissions('manage_templates') and not current_service.all_templates %} {% endif %} - {{ ajax_block(partials, updates_url, 'upcoming') }} +

    All activity

    Sent jobs

    -
    - +
    +
    + - - - - @@ -111,17 +102,24 @@ {% if all_jobs_dict %} {% for job in all_jobs_dict %} {% if job.job_id %} - - + + + - - {% endif %} {% endfor %} {% else %} - - - + {% endif %}
    + Job ID# Template - Status + Time sent Sender - Reports - - # of Recipients - - Message parts used - - Delivery rate + Report
    +
    + + {{ job.job_id if job.job_id else 'Manually entered number' }} + {{ job.template_name }} - {{ job.created_at | format_datetime_table }} + + {{ (job.processing_finished if job.processing_finished else job.processing_started + if job.processing_started else job.created_at)|format_datetime_table }} {{ job.created_by.name }} @@ -130,25 +128,19 @@ {% if job.time_left != "Data no longer available" %} File Download Icon {% elif job %} - {{ job.time_left }} n/a + N/A {% endif %} - {{ job.notification_count}} -
    No batched job messages found  (messages are kept for {{ service_data_retention_days }} days).
    -

    - Note: Report data is only available for 7 days after your message has been sent -

    +
    +

    Note: Report data is only available for 7 days after your message has been sent

    {{show_pagination}}
    diff --git a/gulpfile.js b/gulpfile.js index 7c87a2b79..7f7d117da 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -127,6 +127,8 @@ const javascripts = () => { paths.src + 'javascripts/loginAlert.js', paths.src + 'javascripts/main.js', paths.src + 'javascripts/sampleChartDashboard.js', + paths.src + 'javascripts/sortAlphanumeric.js', + ]) .pipe(plugins.prettyerror()) .pipe(plugins.babel({ From 4e558ca0e7fa7da9de52fc1536598b518ca38b50 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Wed, 24 Jul 2024 14:29:05 -0700 Subject: [PATCH 03/18] cleaned code and added css spacing --- app/__init__.py | 1 - .../uswds/_uswds-theme-custom-styles.scss | 10 +++-- app/main/views/activity.py | 16 +++---- .../views/activity/all-activity.html | 43 +++++-------------- 4 files changed, 24 insertions(+), 46 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 45ce09319..f32a98b6c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -33,7 +33,6 @@ from app.asset_fingerprinter import asset_fingerprinter from app.config import configs from app.extensions import redis_client from app.formatters import ( - convert_time_unixtimestamp, convert_markdown_template, convert_to_boolean, format_auth_type, diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index efe86c763..8a81defd8 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -421,14 +421,18 @@ td.table-empty-message { width: 25%; overflow-wrap: anywhere; } + td.jobid { + width: 5%; + } td.template { - width: 20%; + width: 25%; } td.time-sent { - width: 20%; + width: 30%; } td.sender { - width: 15%; + width: 20%; + overflow-wrap: break-word; } td.count-of-recipients { width: 5%; diff --git a/app/main/views/activity.py b/app/main/views/activity.py index a415dc4d5..db7d576ef 100644 --- a/app/main/views/activity.py +++ b/app/main/views/activity.py @@ -3,7 +3,7 @@ from flask_login import current_user from werkzeug.utils import redirect from app import current_service, job_api_client -from app.formatters import get_time_left, convert_time_unixtimestamp +from app.formatters import convert_time_unixtimestamp, get_time_left from app.main import main from app.utils.pagination import ( generate_next_dict, @@ -43,13 +43,9 @@ def all_jobs_activity(service_id): def handle_pagination(jobs, service_id, page): if page is None: abort(404, "Invalid page argument ({}).".format(request.args.get("page"))) - prev_page = generate_previous_dict("main.all_jobs_activity", service_id, page) if page > 1 else None - next_page = generate_next_dict("main.all_jobs_activity", service_id, page) if jobs["links"].get("next") else None - pagination = generate_pagination_pages(jobs["total"], jobs['page_size'], page) - return prev_page, next_page, pagination @@ -57,17 +53,17 @@ def generate_job_dict(jobs): return [ { "job_id": job["id"], - "sort_value": hashlib.sha1(job["id"].encode("utf-8")).hexdigest(), - "job_sort_value": job["id"].replace("-", ""), "time_left": get_time_left(job["created_at"]), "download_link": url_for(".view_job_csv", service_id=current_service.id, job_id=job["id"]), "view_job_link": url_for(".view_job", service_id=current_service.id, job_id=job["id"]), "created_at": job["created_at"], - "time_sent_data_value": convert_time_unixtimestamp(job["processing_finished"] if job["processing_finished"] else job["processing_started"] - if job["processing_started"] else job["created_at"]), + "time_sent_data_value": convert_time_unixtimestamp( + job["processing_finished"] if job["processing_finished"] + else job["processing_started"] if job["processing_started"] + else job["created_at"] + ), "processing_finished": job["processing_finished"], "processing_started": job["processing_started"], - "notification_count": job["notification_count"], "created_by": job["created_by"], "template_name": job["template_name"] } diff --git a/app/templates/views/activity/all-activity.html b/app/templates/views/activity/all-activity.html index ad98dd9c6..aa26d28cf 100644 --- a/app/templates/views/activity/all-activity.html +++ b/app/templates/views/activity/all-activity.html @@ -1,8 +1,5 @@ {% extends "withnav_template.html" %} -{% from "components/ajax-block.html" import ajax_block %} -{% from "components/previous-next-navigation.html" import previous_next_navigation %} - {% block service_page_title %} All activity {% endblock %} @@ -54,7 +51,6 @@ > Next arrow - {% endif %} @@ -65,23 +61,19 @@ {% block maincolumn_content %} -
    +

    All activity

    {% if current_user.has_permissions('manage_templates') and not current_service.all_templates %} - + {% include 'views/dashboard/write-first-messages.html' %} {% endif %} - - -

    All activity

    -

    Sent jobs

    - +
    - {% if all_jobs_dict %} {% for job in all_jobs_dict %} - {% if job.job_id %} - - - + - - + - + - {% endif %} {% endfor %} {% else %} - {% endif %}
    + Job ID# @@ -101,29 +93,18 @@
    - - {{ job.job_id if job.job_id else 'Manually entered number' }} +
    + + {{ job.job_id[:8] if job.job_id else 'Manually entered number' }} - {{ job.template_name }} - {{ job.template_name }} {{ (job.processing_finished if job.processing_finished else job.processing_started if job.processing_started else job.created_at)|format_datetime_table }} - {{ job.created_by.name }} - {{ job.created_by.name }} {% if job.time_left != "Data no longer available" %} File Download Icon @@ -132,15 +113,13 @@ {% endif %}
    -

    Note: Report data is only available for 7 days after your message has been sent

    +

    Note:Report data is only available for 7 days after your message has been sent

    {{show_pagination}}
    From 68d58116c436dde143495fb5b1078e87b4f71f46 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 25 Jul 2024 00:30:31 -0700 Subject: [PATCH 04/18] updated css --- .../sass/uswds/_uswds-theme-custom-styles.scss | 14 +++++++++++--- app/templates/views/activity/all-activity.html | 10 +++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index 8a81defd8..0eca5ba63 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -439,13 +439,21 @@ td.table-empty-message { } td.report { width: 5%; + text-align: center; } + td.report img { + padding-top: 5px; + } th { padding: 0.5rem 1rem } - td { - padding: 0.5rem 1rem - } +} + +@media (max-width: 768px) { + .usa-table-container--scrollable-mobile { + margin: 0; + overflow-y:hidden; + } } #template-list { diff --git a/app/templates/views/activity/all-activity.html b/app/templates/views/activity/all-activity.html index aa26d28cf..0d4258f2d 100644 --- a/app/templates/views/activity/all-activity.html +++ b/app/templates/views/activity/all-activity.html @@ -68,8 +68,8 @@ {% endif %}

    All activity

    Sent jobs

    -
    - +
    +
    @@ -105,9 +105,9 @@ if job.processing_started else job.created_at)|format_datetime_table }} -
    {{ job.created_by.name }} + {% if job.time_left != "Data no longer available" %} - File Download Icon + File Download Icon {% elif job %} N/A {% endif %} @@ -119,7 +119,7 @@
    -

    Note:Report data is only available for 7 days after your message has been sent

    +

    Note: Report data is only available for 7 days after your message has been sent

    {{show_pagination}}
    From a8ce0be98bb7d659e30f2c20f68e1810a6501dc1 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 25 Jul 2024 11:59:42 -0700 Subject: [PATCH 05/18] test_nav --- app/assets/javascripts/sortAlphanumeric.js | 14 ------- app/main/views/activity.py | 47 +++++++++++++--------- app/utils/pagination.py | 18 +++++---- gulpfile.js | 2 - tests/app/test_navigation.py | 2 + 5 files changed, 40 insertions(+), 43 deletions(-) delete mode 100644 app/assets/javascripts/sortAlphanumeric.js diff --git a/app/assets/javascripts/sortAlphanumeric.js b/app/assets/javascripts/sortAlphanumeric.js deleted file mode 100644 index 726f7b2cd..000000000 --- a/app/assets/javascripts/sortAlphanumeric.js +++ /dev/null @@ -1,14 +0,0 @@ -(function (window) { - // document.addEventListener("DOMContentLoaded", function() { - // const rows = document.querySelectorAll('td.table-field.file-name'); - - // rows.forEach(row => { - // let sortValue = row.getAttribute('data-sort-value'); - // if (sortValue) { - // // Remove non-numeric characters to ensure numerical comparison - // sortValue = sortValue.replace(/\D/g, ''); - // row.setAttribute('data-sort-value', sortValue); - // } - // }); - // }); -})(window); diff --git a/app/main/views/activity.py b/app/main/views/activity.py index db7d576ef..f9b32e9db 100644 --- a/app/main/views/activity.py +++ b/app/main/views/activity.py @@ -1,6 +1,4 @@ -from flask import abort, render_template, request, session, url_for -from flask_login import current_user -from werkzeug.utils import redirect +from flask import abort, render_template, request, url_for from app import current_service, job_api_client from app.formatters import convert_time_unixtimestamp, get_time_left @@ -17,12 +15,6 @@ from app.utils.user import user_has_permissions @main.route("/activity/services/") @user_has_permissions() def all_jobs_activity(service_id): - if session.get("invited_user_id"): - session.pop("invited_user_id", None) - session["service_id"] = service_id - - if not current_user.has_permissions("view_activity"): - return redirect(url_for("main.choose_template", service_id=service_id)) service_data_retention_days = 7 page = get_page_from_request() jobs = job_api_client.get_page_of_jobs(service_id, page=page) @@ -36,16 +28,25 @@ def all_jobs_activity(service_id): next_page=next_page, prev_page=prev_page, pagination=pagination, - jobs=jobs ) def handle_pagination(jobs, service_id, page): if page is None: abort(404, "Invalid page argument ({}).".format(request.args.get("page"))) - prev_page = generate_previous_dict("main.all_jobs_activity", service_id, page) if page > 1 else None - next_page = generate_next_dict("main.all_jobs_activity", service_id, page) if jobs["links"].get("next") else None - pagination = generate_pagination_pages(jobs["total"], jobs['page_size'], page) + prev_page = ( + generate_previous_dict("main.all_jobs_activity", service_id, page) + if page > 1 + else None + ) + next_page = ( + generate_next_dict("main.all_jobs_activity", service_id, page) + if jobs.get("links", {}).get("next") + else None + ) + pagination = generate_pagination_pages( + jobs.get("total", {}), jobs.get("page_size", {}), page + ) return prev_page, next_page, pagination @@ -54,18 +55,26 @@ def generate_job_dict(jobs): { "job_id": job["id"], "time_left": get_time_left(job["created_at"]), - "download_link": url_for(".view_job_csv", service_id=current_service.id, job_id=job["id"]), - "view_job_link": url_for(".view_job", service_id=current_service.id, job_id=job["id"]), + "download_link": url_for( + ".view_job_csv", service_id=current_service.id, job_id=job["id"] + ), + "view_job_link": url_for( + ".view_job", service_id=current_service.id, job_id=job["id"] + ), "created_at": job["created_at"], "time_sent_data_value": convert_time_unixtimestamp( - job["processing_finished"] if job["processing_finished"] - else job["processing_started"] if job["processing_started"] - else job["created_at"] + job["processing_finished"] + if job["processing_finished"] + else ( + job["processing_started"] + if job["processing_started"] + else job["created_at"] + ) ), "processing_finished": job["processing_finished"], "processing_started": job["processing_started"], "created_by": job["created_by"], - "template_name": job["template_name"] + "template_name": job["template_name"], } for job in jobs["data"] ] diff --git a/app/utils/pagination.py b/app/utils/pagination.py index 15858d73a..50e6371be 100644 --- a/app/utils/pagination.py +++ b/app/utils/pagination.py @@ -33,16 +33,18 @@ def generate_previous_next_dict(view, service_id, page, title, url_args): def generate_pagination_pages(total_items, page_size, current_page): total_pages = (total_items + page_size - 1) // page_size - pagination = { - 'current': current_page, - 'pages': [], - 'last': total_pages - } + pagination = {"current": current_page, "pages": [], "last": total_pages} if total_pages <= 4: - pagination['pages'] = list(range(1, total_pages + 1)) + pagination["pages"] = list(range(1, total_pages + 1)) else: if current_page <= 3: - pagination['pages'] = [1, 2, 3, total_pages] + pagination["pages"] = [1, 2, 3, total_pages] else: - pagination['pages'] = [1, current_page - 1, current_page, current_page + 1, total_pages] + pagination["pages"] = [ + 1, + current_page - 1, + current_page, + current_page + 1, + total_pages, + ] return pagination diff --git a/gulpfile.js b/gulpfile.js index ef34d8ba6..3e7dee06e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -126,8 +126,6 @@ const javascripts = () => { paths.src + 'javascripts/loginAlert.js', paths.src + 'javascripts/main.js', paths.src + 'javascripts/sampleChartDashboard.js', - paths.src + 'javascripts/sortAlphanumeric.js', - ]) .pipe(plugins.prettyerror()) .pipe(plugins.babel({ diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index 6f1cf58eb..d346fc71a 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -25,6 +25,7 @@ EXCLUDED_ENDPOINTS = tuple( "add_organization", "add_service", "add_service_template", + "all_jobs_activity", "api_callbacks", "api_documentation", "api_integration", @@ -400,6 +401,7 @@ def test_navigation_urls( assert [a["href"] for a in page.select(".nav a")] == [ "/services/{}/templates".format(SERVICE_ONE_ID), "/services/{}".format(SERVICE_ONE_ID), + "/activity/services/{}".format(SERVICE_ONE_ID), # "/services/{}/usage".format(SERVICE_ONE_ID), # "/services/{}/users".format(SERVICE_ONE_ID), # "/services/{}/service-settings".format(SERVICE_ONE_ID), From 8461c6156b0b7d8a52e2d2323b493cca6ede8e46 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 25 Jul 2024 17:09:17 -0700 Subject: [PATCH 06/18] created testing for activity page --- .../views/activity/all-activity.html | 7 +- app/utils/pagination.py | 15 +- tests/app/main/views/test_jobs_activity.py | 179 ++++++++++++++++++ tests/app/utils/test_pagination.py | 25 ++- 4 files changed, 210 insertions(+), 16 deletions(-) create mode 100644 tests/app/main/views/test_jobs_activity.py diff --git a/app/templates/views/activity/all-activity.html b/app/templates/views/activity/all-activity.html index 0d4258f2d..7eb7cb3c5 100644 --- a/app/templates/views/activity/all-activity.html +++ b/app/templates/views/activity/all-activity.html @@ -59,13 +59,9 @@ {% endif %} {% endset %} - {% block maincolumn_content %}

    All activity

    - {% if current_user.has_permissions('manage_templates') and not current_service.all_templates %} - {% include 'views/dashboard/write-first-messages.html' %} - {% endif %}

    All activity

    Sent jobs

    @@ -115,6 +111,9 @@ {% endfor %} {% else %} + + No batched job messages found (messages are kept for {{ service_data_retention_days }} days). + {% endif %} diff --git a/app/utils/pagination.py b/app/utils/pagination.py index 50e6371be..7d3f89918 100644 --- a/app/utils/pagination.py +++ b/app/utils/pagination.py @@ -34,17 +34,10 @@ def generate_previous_next_dict(view, service_id, page, title, url_args): def generate_pagination_pages(total_items, page_size, current_page): total_pages = (total_items + page_size - 1) // page_size pagination = {"current": current_page, "pages": [], "last": total_pages} - if total_pages <= 4: + if total_pages <= 9: pagination["pages"] = list(range(1, total_pages + 1)) else: - if current_page <= 3: - pagination["pages"] = [1, 2, 3, total_pages] - else: - pagination["pages"] = [ - 1, - current_page - 1, - current_page, - current_page + 1, - total_pages, - ] + start_page = max(1, min(current_page - 4, total_pages - 8)) + end_page = min(start_page + 8, total_pages) + pagination["pages"] = list(range(start_page, end_page + 1)) return pagination diff --git a/tests/app/main/views/test_jobs_activity.py b/tests/app/main/views/test_jobs_activity.py new file mode 100644 index 000000000..8cb171d94 --- /dev/null +++ b/tests/app/main/views/test_jobs_activity.py @@ -0,0 +1,179 @@ +from bs4 import BeautifulSoup + +from app.utils.pagination import get_page_from_request +from tests.conftest import SERVICE_ONE_ID + +MOCK_JOBS = { + "data": [ + { + "archived": False, + "created_at": "2024-01-04T20:43:52+00:00", + "created_by": { + "id": "mocked_user_id", + "name": "mocked_user", + }, + "id": "55b242b5-9f62-4271-aff7-039e9c320578", + "job_status": "finished", + "notification_count": 1, + "original_file_name": "mocked_file.csv", + "processing_finished": "2024-01-25T23:02:25+00:00", + "processing_started": "2024-01-25T23:02:24+00:00", + "scheduled_for": None, + "service": "21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3", + "service_name": {"name": "Mock Texting Service"}, + "statistics": [{"count": 1, "status": "sending"}], + "template": "6a456418-498c-4c86-b0cd-9403c14a216c", + "template_name": "Mock Template Name", + "template_type": "sms", + "template_version": 3, + "updated_at": "2024-01-25T23:02:25+00:00", + } + ], + 'links': { + 'last': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=3', + 'next': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=3', + 'prev': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=1' + }, + 'page_size': 50, + 'total': 115 +} + + +def test_all_activity( + client_request, + mocker, +): + current_page = get_page_from_request() + mock_get_page_of_jobs = mocker.patch( + "app.job_api_client.get_page_of_jobs", return_value=MOCK_JOBS + ) + + response = client_request.get_response( + "main.all_jobs_activity", + service_id=SERVICE_ONE_ID, + page=current_page, + ) + assert response.status_code == 200, "Request failed" + assert response.data is not None, "Response data is None" + + assert "All activity" in response.text + mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page) + page = BeautifulSoup(response.data, 'html.parser') + table = page.find('table') + assert table is not None, "Table not found in the response" + + headers = [th.get_text(strip=True) for th in table.find_all('th')] + expected_headers = ["Job ID#", "Template", "Time sent", "Sender", "Report"] + + assert headers == expected_headers, f"Expected headers {expected_headers}, but got {headers}" + + rows = table.find('tbody').find_all('tr', class_='table-row') + assert len(rows) == 1, "Expected one job row in the table" + + job_row = rows[0] + cells = job_row.find_all('td') + assert len(cells) == 5, "Expected five columns in the job row" + + job_id_cell = cells[0].find('a').get_text(strip=True) + + assert job_id_cell == "55b242b5", f"Expected job ID '55b242b5', but got '{job_id_cell}'" + template_cell = cells[1].get_text(strip=True) + assert template_cell == "Mock Template Name", ( + f"Expected template 'Mock Template Name', but got '{template_cell}'" + ) + time_sent_cell = cells[2].get_text(strip=True) + assert time_sent_cell == "01-25-2024 at 06:02 PM", ( + f"Expected time sent '01-25-2024 at 06:02 PM', but got '{time_sent_cell}'" + ) + sender_cell = cells[3].get_text(strip=True) + assert sender_cell == "mocked_user", f"Expected sender 'mocked_user', but got '{sender_cell}'" + + report_cell = cells[4].find('span').get_text(strip=True) + assert report_cell == "N/A", f"Expected report 'N/A', but got '{report_cell}'" + + mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page) + + +def test_all_activity_no_jobs( + client_request, + mocker +): + current_page = get_page_from_request() + mock_get_page_of_jobs = mocker.patch( + "app.job_api_client.get_page_of_jobs", + return_value={ + "data": [], + 'links': { + 'last': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=1', + 'next': None, + 'prev': None + }, + 'page_size': 50, + 'total': 0 + } + ) + response = client_request.get_response( + "main.all_jobs_activity", + service_id=SERVICE_ONE_ID, + page=current_page, + ) + + assert response.status_code == 200, "Request failed" + + page = BeautifulSoup(response.data, 'html.parser') + + no_jobs_message_td = page.find('td', class_='table-empty-message') + assert no_jobs_message_td is not None, "No jobs message not found in the response" + + expected_message = "No batched job messages found (messages are kept for 7 days)." + actual_message = no_jobs_message_td.get_text(strip=True) + + assert expected_message == actual_message, ( + f"Expected message '{expected_message}', but got '{actual_message}'" + ) + mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page) + + +def test_all_activity_pagination(client_request, mocker): + current_page = get_page_from_request() + mock_get_page_of_jobs = mocker.patch( + "app.job_api_client.get_page_of_jobs", + return_value={ + "data": [ + { + "id": f"job-{i}", + "created_at": "2024-01-25T23:02:25+00:00", + "created_by": {"name": "mocked_user"}, + "processing_finished": "2024-01-25T23:02:25+00:00", + "processing_started": "2024-01-25T23:02:24+00:00", + "template_name": "Mock Template Name", + "original_file_name": "mocked_file.csv", + "notification_count": 1 + } for i in range(1, 101) + ], + 'links': { + 'last': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=2', + 'next': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=2', + 'prev': None + }, + 'page_size': 50, + 'total': 100 + } + ) + + response = client_request.get_response( + "main.all_jobs_activity", + service_id=SERVICE_ONE_ID, + page=current_page, + ) + mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page) + + page = BeautifulSoup(response.data, 'html.parser') + pagination_controls = page.find_all('li', class_='usa-pagination__item') + assert pagination_controls, "Pagination controls not found in the response" + + pagination_texts = [item.get_text(strip=True) for item in pagination_controls] + expected_pagination_texts = ['1', '2', 'Next'] + assert pagination_texts == expected_pagination_texts, ( + f"Expected pagination controls {expected_pagination_texts}, but got {pagination_texts}" + ) diff --git a/tests/app/utils/test_pagination.py b/tests/app/utils/test_pagination.py index 4a98a1913..a1264d8f5 100644 --- a/tests/app/utils/test_pagination.py +++ b/tests/app/utils/test_pagination.py @@ -1,4 +1,10 @@ -from app.utils.pagination import generate_next_dict, generate_previous_dict +import pytest + +from app.utils.pagination import ( + generate_next_dict, + generate_pagination_pages, + generate_previous_dict, +) def test_generate_previous_dict(client_request): @@ -20,3 +26,20 @@ def test_generate_previous_next_dict_adds_other_url_args(client_request): "main.view_notifications", "foo", 2, {"message_type": "blah"} ) assert "notifications/blah" in result["url"] + + +@pytest.mark.parametrize( + ("total_items", "page_size", "current_page", "expected"), + [ + (100, 50, 1, {"current": 1, "pages": [1, 2], "last": 2}), + (450, 50, 1, {"current": 1, "pages": [1, 2, 3, 4, 5, 6, 7, 8, 9], "last": 9}), + (500, 50, 1, {"current": 1, "pages": [1, 2, 3, 4, 5, 6, 7, 8, 9], "last": 10}), + (500, 50, 5, {"current": 5, "pages": [1, 2, 3, 4, 5, 6, 7, 8, 9], "last": 10}), + (500, 50, 6, {"current": 6, "pages": [2, 3, 4, 5, 6, 7, 8, 9, 10], "last": 10}), + (500, 50, 10, {"current": 10, "pages": [2, 3, 4, 5, 6, 7, 8, 9, 10], "last": 10}), + (950, 50, 15, {"current": 15, "pages": [11, 12, 13, 14, 15, 16, 17, 18, 19], "last": 19}), + ], +) +def test_generate_pagination_pages(total_items, page_size, current_page, expected): + result = generate_pagination_pages(total_items, page_size, current_page) + assert result == expected From 99493acb8687fab32a8c26768785f3bf368cf601 Mon Sep 17 00:00:00 2001 From: Jonathan Bobel Date: Mon, 29 Jul 2024 15:32:53 -0400 Subject: [PATCH 07/18] Updates to Job status page --- .../uswds/_uswds-theme-custom-styles.scss | 28 +++++++++++++++++++ .../partials/jobs/notifications.html | 7 +++-- app/templates/views/jobs/job.html | 6 ++-- poetry.lock | 6 +++- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index efe86c763..f82e63ad7 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -402,6 +402,30 @@ td.table-empty-message { } } +.job-status-table { + table-layout: fixed; + + thead tr th { + border-bottom: 0; + } + + thead, + tbody, + tr { + width: 100%; + } + + th:first-child, + td:first-child { + width: 75%; + } + + th:nth-child(2),R + td:nth-child(2) { + width: 25%; + } +} + .usage-table { ul { list-style: none; @@ -456,6 +480,10 @@ td.table-empty-message { } } +.usa-prose > p.max-width-full { + max-width: 100%; +} + // Tabs .tabs { diff --git a/app/templates/partials/jobs/notifications.html b/app/templates/partials/jobs/notifications.html index 2b5cd8f77..5787fb3d4 100644 --- a/app/templates/partials/jobs/notifications.html +++ b/app/templates/partials/jobs/notifications.html @@ -22,7 +22,7 @@ {% else %} {% if notifications %} -
    +
    {% endif %} {% if job.still_processing %}

    @@ -40,15 +40,16 @@ notifications, caption=uploaded_file_name, caption_visible=False, + border_visible=True, empty_message='No messages to show yet…' if job.awaiting_processing_or_recently_processed else 'These messages have been deleted because they were sent more than {} days ago'.format(service_data_retention_days), field_headings=[ 'Recipient', - 'Status' + 'Message status' ], field_headings_visible=False ) %} {% call row_heading() %} - {{ item.to }} + {{ item.to | format_phone_number_human_readable }}

    {{ item.preview_of_content }}

    diff --git a/app/templates/views/jobs/job.html b/app/templates/views/jobs/job.html index ce4e94b3d..0de8ccf83 100644 --- a/app/templates/views/jobs/job.html +++ b/app/templates/views/jobs/job.html @@ -10,7 +10,7 @@ {% block maincolumn_content %} - {{ page_header("Message status") }} + {{ page_header("Job status") }} {{ partials['status']|safe }} {% if not finished %}
    {% endif %} -

    - Messages will remain in pending state until carrier status is received, typically 5 minutes. +

    + Messages are sent immediately to the cell phone carrier, but will remain in "pending" status until we hear back from the carrier they have received it and attempted deliver. More information on delivery status.

    {% if not job.processing_finished %}
    Date: Tue, 30 Jul 2024 14:24:02 -0400 Subject: [PATCH 08/18] Adjusting tests --- app/templates/views/jobs/job.html | 2 +- tests/app/main/views/test_jobs.py | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/templates/views/jobs/job.html b/app/templates/views/jobs/job.html index 0de8ccf83..7014e1987 100644 --- a/app/templates/views/jobs/job.html +++ b/app/templates/views/jobs/job.html @@ -10,7 +10,7 @@ {% block maincolumn_content %} - {{ page_header("Job status") }} + {{ page_header("Message status") }} {{ partials['status']|safe }} {% if not finished %}
    Date: Tue, 30 Jul 2024 14:35:30 -0400 Subject: [PATCH 09/18] Needed to wrap this line --- tests/app/main/views/test_jobs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/app/main/views/test_jobs.py b/tests/app/main/views/test_jobs.py index 8a9ef5b58..dd55d7500 100644 --- a/tests/app/main/views/test_jobs.py +++ b/tests/app/main/views/test_jobs.py @@ -493,5 +493,6 @@ def test_should_show_message_note( ) assert normalize_spaces(page.select_one("main p.notification-status").text) == ( - 'Messages are sent immediately to the cell phone carrier, but will remain in "pending" status until we hear back from the carrier they have received it and attempted deliver. More information on delivery status.' + 'Messages are sent immediately to the cell phone carrier, but will remain in "pending" status until we hear ' + 'back from the carrier they have received it and attempted deliver. More information on delivery status.' ) From a1180747d5126ea7bf34444dc9a283ca4943bff7 Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 31 Jul 2024 08:17:29 -0700 Subject: [PATCH 10/18] remove uk bank holidays --- notifications_utils/letter_timings.py | 22 +++++----------------- poetry.lock | 16 +--------------- pyproject.toml | 1 - 3 files changed, 6 insertions(+), 33 deletions(-) diff --git a/notifications_utils/letter_timings.py b/notifications_utils/letter_timings.py index 62abf2c21..1072465fd 100644 --- a/notifications_utils/letter_timings.py +++ b/notifications_utils/letter_timings.py @@ -2,7 +2,6 @@ from collections import namedtuple from datetime import datetime, time, timedelta import pytz -from govuk_bank_holidays.bank_holidays import BankHolidays from notifications_utils.countries.data import Postage from notifications_utils.timezones import utc_string_to_aware_gmt_datetime @@ -18,16 +17,6 @@ CANCELLABLE_JOB_LETTER_STATUSES = [ ] -non_working_days_dvla = BankHolidays( - use_cached_holidays=True, - weekend=(5, 6), -) -non_working_days_royal_mail = BankHolidays( - use_cached_holidays=True, - weekend=(6,), # Only Sunday (day 6 of the week) is a non-working day -) - - def set_gmt_hour(day, hour): return ( day.astimezone(pytz.timezone("Europe/London")) @@ -36,28 +25,27 @@ def set_gmt_hour(day, hour): ) -def get_next_work_day(date, non_working_days): +def get_next_work_day(date, non_working_days=None): next_day = date + timedelta(days=1) - if non_working_days.is_work_day( + if non_working_days and non_working_days.is_work_day( date=next_day.date(), - division=BankHolidays.ENGLAND_AND_WALES, ): return next_day - return get_next_work_day(next_day, non_working_days) + return get_next_work_day(next_day) def get_next_dvla_working_day(date): """ Printing takes place monday to friday, excluding bank holidays """ - return get_next_work_day(date, non_working_days=non_working_days_dvla) + return get_next_work_day(date) def get_next_royal_mail_working_day(date): """ Royal mail deliver letters on monday to saturday """ - return get_next_work_day(date, non_working_days=non_working_days_royal_mail) + return get_next_work_day(date) def get_delivery_day(date, *, days_to_deliver): diff --git a/poetry.lock b/poetry.lock index e277224d6..a636a17e7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -966,20 +966,6 @@ files = [ {file = "geojson-3.1.0.tar.gz", hash = "sha256:58a7fa40727ea058efc28b0e9ff0099eadf6d0965e04690830208d3ef571adac"}, ] -[[package]] -name = "govuk-bank-holidays" -version = "0.14" -description = "Tool to load UK bank holidays from GOV.UK" -optional = false -python-versions = ">=3.6" -files = [ - {file = "govuk-bank-holidays-0.14.tar.gz", hash = "sha256:ce85102423b72908957d25981f616494729686515d5d66c09a1d35a354ce20a6"}, - {file = "govuk_bank_holidays-0.14-py3-none-any.whl", hash = "sha256:da485c4a40c6c874c925916e492e3f20b807cffba7eed5f07fb69327aef6b10b"}, -] - -[package.dependencies] -requests = "*" - [[package]] name = "greenlet" version = "3.0.3" @@ -3102,4 +3088,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "6420327e4cabd4b5e6b7903607f5c435c53f98b10b3da42da988a18da5fd1717" +content-hash = "b271104f669ce0a8e78fb09299b61cf0502cc81a18213dda00f77c759b6e0209" diff --git a/pyproject.toml b/pyproject.toml index 4abe5ac27..5a9dc8727 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,6 @@ flask-basicauth = "~=0.2" flask-login = "^0.6" flask-talisman = "*" flask-wtf = "^1.2" -govuk-bank-holidays = "^0.14" gunicorn = {version = "==22.0.0", extras = ["eventlet"]} humanize = "~=4.10" itsdangerous = "~=2.2" From 97ae46d57b9cb6d489edacdd4d0caebd0bd09149 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 1 Aug 2024 09:34:57 -0700 Subject: [PATCH 11/18] table th color changed --- app/assets/sass/uswds/_uswds-theme-custom-styles.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index 0eca5ba63..62a7052da 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -456,6 +456,10 @@ td.table-empty-message { } } +.usa-table th[data-sortable][aria-sort=ascending], .usa-table th[data-sortable][aria-sort=descending] { + background-color: #a1d3ff; +} + #template-list { max-height: 500px; overflow-y: auto; From 9471174f20c58b8e89d6823e40d64bf8c7518d0a Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 1 Aug 2024 15:44:05 -0700 Subject: [PATCH 12/18] removed extra template path in gulp --- gulpfile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index e9bec7a3d..278742cfe 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -27,7 +27,6 @@ plugins.uglify = require('gulp-uglify'); const paths = { src: 'app/assets/', dist: 'app/static/', - templates: 'app/templates/', npm: 'node_modules/', toolkit: 'node_modules/govuk_frontend_toolkit/', govuk_frontend: 'node_modules/govuk-frontend/' From 131d99a4df0f0a46fd32c6e75bcaf37c9651aa4c Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 2 Aug 2024 14:02:44 -0400 Subject: [PATCH 13/18] iAdding some logging for Bev. Signed-off-by: Cliff Hill --- app/notify_client/job_api_client.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/notify_client/job_api_client.py b/app/notify_client/job_api_client.py index 538bdd370..825097630 100644 --- a/app/notify_client/job_api_client.py +++ b/app/notify_client/job_api_client.py @@ -1,4 +1,5 @@ import datetime +from flask import current_app from zoneinfo import ZoneInfo from app.extensions import redis_client @@ -28,7 +29,7 @@ class JobApiClient(NotifyAdminAPIClient): def get_job(self, service_id, job_id): params = {} job = self.get( - url="/service/{}/job/{}".format(service_id, job_id), params=params + url=f"/service/{service_id}/job/{job_id}", params=params ) return job @@ -40,13 +41,16 @@ class JobApiClient(NotifyAdminAPIClient): if statuses is not None: params["statuses"] = ",".join(statuses) - return self.get(url="/service/{}/job".format(service_id), params=params) + job = self.get(url=f"/service/{service_id}/job", params=params) + from pprint import pformat + current_app.logger.info(pformat(job)) + return job def get_uploads(self, service_id, limit_days=None, page=1): params = {"page": page} if limit_days is not None: params["limit_days"] = limit_days - return self.get(url="/service/{}/upload".format(service_id), params=params) + return self.get(url=f"/service/{service_id}/upload", params=params) def has_sent_previously( self, service_id, template_id, template_version, original_file_name From 03466d01848cf53dc069d3d8b6cb4daf571ebbd2 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 2 Aug 2024 14:37:06 -0400 Subject: [PATCH 14/18] Removing pformat from the change. Signed-off-by: Cliff Hill --- app/notify_client/job_api_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/notify_client/job_api_client.py b/app/notify_client/job_api_client.py index 825097630..c88c97c64 100644 --- a/app/notify_client/job_api_client.py +++ b/app/notify_client/job_api_client.py @@ -42,8 +42,7 @@ class JobApiClient(NotifyAdminAPIClient): params["statuses"] = ",".join(statuses) job = self.get(url=f"/service/{service_id}/job", params=params) - from pprint import pformat - current_app.logger.info(pformat(job)) + current_app.logger.info(job) return job def get_uploads(self, service_id, limit_days=None, page=1): From 27ee039b2d98e81f8dec1addf800ac86ac635ff2 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 2 Aug 2024 14:42:29 -0400 Subject: [PATCH 15/18] iDumping object as a JSON. Signed-off-by: Cliff Hill --- app/notify_client/job_api_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/notify_client/job_api_client.py b/app/notify_client/job_api_client.py index c88c97c64..5b57edb08 100644 --- a/app/notify_client/job_api_client.py +++ b/app/notify_client/job_api_client.py @@ -42,7 +42,8 @@ class JobApiClient(NotifyAdminAPIClient): params["statuses"] = ",".join(statuses) job = self.get(url=f"/service/{service_id}/job", params=params) - current_app.logger.info(job) + from json import dumps + current_app.logger.info(dumps(job)) return job def get_uploads(self, service_id, limit_days=None, page=1): From 6b8be20ed216996e6413712da2eefc29c7a7e562 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 2 Aug 2024 14:50:52 -0400 Subject: [PATCH 16/18] Black & isort. Signed-off-by: Cliff Hill --- app/notify_client/job_api_client.py | 8 +- tests/app/main/views/test_dashboard.py | 2 +- tests/app/main/views/test_jobs.py | 2 +- tests/app/main/views/test_jobs_activity.py | 110 +++++++++++---------- tests/app/utils/test_pagination.py | 14 ++- 5 files changed, 75 insertions(+), 61 deletions(-) diff --git a/app/notify_client/job_api_client.py b/app/notify_client/job_api_client.py index 5b57edb08..cd15730df 100644 --- a/app/notify_client/job_api_client.py +++ b/app/notify_client/job_api_client.py @@ -1,7 +1,8 @@ import datetime -from flask import current_app from zoneinfo import ZoneInfo +from flask import current_app + from app.extensions import redis_client from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache from app.utils.csv import get_user_preferred_timezone @@ -28,9 +29,7 @@ class JobApiClient(NotifyAdminAPIClient): def get_job(self, service_id, job_id): params = {} - job = self.get( - url=f"/service/{service_id}/job/{job_id}", params=params - ) + job = self.get(url=f"/service/{service_id}/job/{job_id}", params=params) return job @@ -43,6 +42,7 @@ class JobApiClient(NotifyAdminAPIClient): job = self.get(url=f"/service/{service_id}/job", params=params) from json import dumps + current_app.logger.info(dumps(job)) return job diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index 613ac74d9..fe58dec7d 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -1362,7 +1362,7 @@ def test_menu_all_services_for_platform_admin_user( page = str(page) assert url_for("main.choose_template", service_id=service_one["id"]) in page assert url_for("main.service_settings", service_id=service_one["id"]) in page - assert url_for('main.api_keys', service_id=service_one['id']) not in page + assert url_for("main.api_keys", service_id=service_one["id"]) not in page def test_route_for_service_permissions( diff --git a/tests/app/main/views/test_jobs.py b/tests/app/main/views/test_jobs.py index dd55d7500..c4a756194 100644 --- a/tests/app/main/views/test_jobs.py +++ b/tests/app/main/views/test_jobs.py @@ -494,5 +494,5 @@ def test_should_show_message_note( assert normalize_spaces(page.select_one("main p.notification-status").text) == ( 'Messages are sent immediately to the cell phone carrier, but will remain in "pending" status until we hear ' - 'back from the carrier they have received it and attempted deliver. More information on delivery status.' + "back from the carrier they have received it and attempted deliver. More information on delivery status." ) diff --git a/tests/app/main/views/test_jobs_activity.py b/tests/app/main/views/test_jobs_activity.py index 8cb171d94..60502c957 100644 --- a/tests/app/main/views/test_jobs_activity.py +++ b/tests/app/main/views/test_jobs_activity.py @@ -29,13 +29,13 @@ MOCK_JOBS = { "updated_at": "2024-01-25T23:02:25+00:00", } ], - 'links': { - 'last': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=3', - 'next': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=3', - 'prev': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=1' + "links": { + "last": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=3", + "next": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=3", + "prev": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=1", }, - 'page_size': 50, - 'total': 115 + "page_size": 50, + "total": 115, } @@ -58,59 +58,62 @@ def test_all_activity( assert "All activity" in response.text mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page) - page = BeautifulSoup(response.data, 'html.parser') - table = page.find('table') + page = BeautifulSoup(response.data, "html.parser") + table = page.find("table") assert table is not None, "Table not found in the response" - headers = [th.get_text(strip=True) for th in table.find_all('th')] + headers = [th.get_text(strip=True) for th in table.find_all("th")] expected_headers = ["Job ID#", "Template", "Time sent", "Sender", "Report"] - assert headers == expected_headers, f"Expected headers {expected_headers}, but got {headers}" + assert ( + headers == expected_headers + ), f"Expected headers {expected_headers}, but got {headers}" - rows = table.find('tbody').find_all('tr', class_='table-row') + rows = table.find("tbody").find_all("tr", class_="table-row") assert len(rows) == 1, "Expected one job row in the table" job_row = rows[0] - cells = job_row.find_all('td') + cells = job_row.find_all("td") assert len(cells) == 5, "Expected five columns in the job row" - job_id_cell = cells[0].find('a').get_text(strip=True) + job_id_cell = cells[0].find("a").get_text(strip=True) - assert job_id_cell == "55b242b5", f"Expected job ID '55b242b5', but got '{job_id_cell}'" + assert ( + job_id_cell == "55b242b5" + ), f"Expected job ID '55b242b5', but got '{job_id_cell}'" template_cell = cells[1].get_text(strip=True) - assert template_cell == "Mock Template Name", ( - f"Expected template 'Mock Template Name', but got '{template_cell}'" - ) + assert ( + template_cell == "Mock Template Name" + ), f"Expected template 'Mock Template Name', but got '{template_cell}'" time_sent_cell = cells[2].get_text(strip=True) - assert time_sent_cell == "01-25-2024 at 06:02 PM", ( - f"Expected time sent '01-25-2024 at 06:02 PM', but got '{time_sent_cell}'" - ) + assert ( + time_sent_cell == "01-25-2024 at 06:02 PM" + ), f"Expected time sent '01-25-2024 at 06:02 PM', but got '{time_sent_cell}'" sender_cell = cells[3].get_text(strip=True) - assert sender_cell == "mocked_user", f"Expected sender 'mocked_user', but got '{sender_cell}'" + assert ( + sender_cell == "mocked_user" + ), f"Expected sender 'mocked_user', but got '{sender_cell}'" - report_cell = cells[4].find('span').get_text(strip=True) + report_cell = cells[4].find("span").get_text(strip=True) assert report_cell == "N/A", f"Expected report 'N/A', but got '{report_cell}'" mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page) -def test_all_activity_no_jobs( - client_request, - mocker -): +def test_all_activity_no_jobs(client_request, mocker): current_page = get_page_from_request() mock_get_page_of_jobs = mocker.patch( "app.job_api_client.get_page_of_jobs", return_value={ "data": [], - 'links': { - 'last': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=1', - 'next': None, - 'prev': None + "links": { + "last": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=1", + "next": None, + "prev": None, }, - 'page_size': 50, - 'total': 0 - } + "page_size": 50, + "total": 0, + }, ) response = client_request.get_response( "main.all_jobs_activity", @@ -120,17 +123,17 @@ def test_all_activity_no_jobs( assert response.status_code == 200, "Request failed" - page = BeautifulSoup(response.data, 'html.parser') + page = BeautifulSoup(response.data, "html.parser") - no_jobs_message_td = page.find('td', class_='table-empty-message') + no_jobs_message_td = page.find("td", class_="table-empty-message") assert no_jobs_message_td is not None, "No jobs message not found in the response" expected_message = "No batched job messages found (messages are kept for 7 days)." actual_message = no_jobs_message_td.get_text(strip=True) - assert expected_message == actual_message, ( - f"Expected message '{expected_message}', but got '{actual_message}'" - ) + assert ( + expected_message == actual_message + ), f"Expected message '{expected_message}', but got '{actual_message}'" mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page) @@ -148,17 +151,18 @@ def test_all_activity_pagination(client_request, mocker): "processing_started": "2024-01-25T23:02:24+00:00", "template_name": "Mock Template Name", "original_file_name": "mocked_file.csv", - "notification_count": 1 - } for i in range(1, 101) + "notification_count": 1, + } + for i in range(1, 101) ], - 'links': { - 'last': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=2', - 'next': '/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=2', - 'prev': None + "links": { + "last": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=2", + "next": "/service/21b3ee3d-1cb0-4666-bfa0-9c5ac26d3fe3/job?page=2", + "prev": None, }, - 'page_size': 50, - 'total': 100 - } + "page_size": 50, + "total": 100, + }, ) response = client_request.get_response( @@ -168,12 +172,12 @@ def test_all_activity_pagination(client_request, mocker): ) mock_get_page_of_jobs.assert_called_with(SERVICE_ONE_ID, page=current_page) - page = BeautifulSoup(response.data, 'html.parser') - pagination_controls = page.find_all('li', class_='usa-pagination__item') + page = BeautifulSoup(response.data, "html.parser") + pagination_controls = page.find_all("li", class_="usa-pagination__item") assert pagination_controls, "Pagination controls not found in the response" pagination_texts = [item.get_text(strip=True) for item in pagination_controls] - expected_pagination_texts = ['1', '2', 'Next'] - assert pagination_texts == expected_pagination_texts, ( - f"Expected pagination controls {expected_pagination_texts}, but got {pagination_texts}" - ) + expected_pagination_texts = ["1", "2", "Next"] + assert ( + pagination_texts == expected_pagination_texts + ), f"Expected pagination controls {expected_pagination_texts}, but got {pagination_texts}" diff --git a/tests/app/utils/test_pagination.py b/tests/app/utils/test_pagination.py index a1264d8f5..d1a3663c1 100644 --- a/tests/app/utils/test_pagination.py +++ b/tests/app/utils/test_pagination.py @@ -36,8 +36,18 @@ def test_generate_previous_next_dict_adds_other_url_args(client_request): (500, 50, 1, {"current": 1, "pages": [1, 2, 3, 4, 5, 6, 7, 8, 9], "last": 10}), (500, 50, 5, {"current": 5, "pages": [1, 2, 3, 4, 5, 6, 7, 8, 9], "last": 10}), (500, 50, 6, {"current": 6, "pages": [2, 3, 4, 5, 6, 7, 8, 9, 10], "last": 10}), - (500, 50, 10, {"current": 10, "pages": [2, 3, 4, 5, 6, 7, 8, 9, 10], "last": 10}), - (950, 50, 15, {"current": 15, "pages": [11, 12, 13, 14, 15, 16, 17, 18, 19], "last": 19}), + ( + 500, + 50, + 10, + {"current": 10, "pages": [2, 3, 4, 5, 6, 7, 8, 9, 10], "last": 10}, + ), + ( + 950, + 50, + 15, + {"current": 15, "pages": [11, 12, 13, 14, 15, 16, 17, 18, 19], "last": 19}, + ), ], ) def test_generate_pagination_pages(total_items, page_size, current_page, expected): From 65da623458da82c0df859cf5b3f7476f228e85a4 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 2 Aug 2024 15:01:55 -0400 Subject: [PATCH 17/18] Done stuff to make serialization work hopefully. Signed-off-by: Cliff Hill --- app/notify_client/job_api_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/notify_client/job_api_client.py b/app/notify_client/job_api_client.py index cd15730df..0ee90d464 100644 --- a/app/notify_client/job_api_client.py +++ b/app/notify_client/job_api_client.py @@ -42,8 +42,7 @@ class JobApiClient(NotifyAdminAPIClient): job = self.get(url=f"/service/{service_id}/job", params=params) from json import dumps - - current_app.logger.info(dumps(job)) + current_app.logger.info(dumps(job._dict)) return job def get_uploads(self, service_id, limit_days=None, page=1): From 0fe73f6973652724b89a7f1982ff968e18d19b2e Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Fri, 2 Aug 2024 15:14:17 -0400 Subject: [PATCH 18/18] Trying this now. Signed-off-by: Cliff Hill --- app/notify_client/job_api_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/notify_client/job_api_client.py b/app/notify_client/job_api_client.py index 0ee90d464..dfc4fe814 100644 --- a/app/notify_client/job_api_client.py +++ b/app/notify_client/job_api_client.py @@ -41,8 +41,8 @@ class JobApiClient(NotifyAdminAPIClient): params["statuses"] = ",".join(statuses) job = self.get(url=f"/service/{service_id}/job", params=params) - from json import dumps - current_app.logger.info(dumps(job._dict)) + from pprint import pformat + current_app.logger.info(pformat(job)) return job def get_uploads(self, service_id, limit_days=None, page=1):