From febd037bed9d7d0b06681e45b1d3f8ac6f28b13e Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 18 Jul 2024 15:57:29 -0700 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 97ae46d57b9cb6d489edacdd4d0caebd0bd09149 Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Thu, 1 Aug 2024 09:34:57 -0700 Subject: [PATCH 7/7] 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;