Compare commits

...

20 Commits

Author SHA1 Message Date
Alex Janousek
e958e097aa Merge pull request #2366 from GSA/dependabot/npm_and_yarn/rollup-4.34.9
Bump rollup from 4.34.8 to 4.34.9
2025-03-04 12:09:28 -05:00
dependabot[bot]
9ae1f40376 Bump rollup from 4.34.8 to 4.34.9
Bumps [rollup](https://github.com/rollup/rollup) from 4.34.8 to 4.34.9.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.34.8...v4.34.9)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 00:04:45 +00:00
Beverly Nguyen
0df04cfd89 Merge pull request #2325 from GSA/jb-add-accessibility-documentation
Create manual-a11y-checklist.md
2025-02-27 10:04:49 -08:00
Beverly Nguyen
34f7c6a4da Merge pull request #2361 from GSA/dependabot/npm_and_yarn/sass-embedded-1.85.1
Bump sass-embedded from 1.85.0 to 1.85.1
2025-02-27 10:04:10 -08:00
ccostino
cd78710753 Merge pull request #2359 from GSA/1543-review-and-update-total-messages-chart-on-dashboard-and-usagehtml-with-new-data-from-backend
Total message ratio update
2025-02-27 11:00:49 -05:00
Alex Janousek
102d005e0d Merge pull request #2362 from GSA/feat/fe-tz-change
Added local/utc time conversion for text metrics
2025-02-26 14:59:44 -05:00
Beverly Nguyen
3455235a8b fix test 2025-02-26 10:19:47 -08:00
Beverly Nguyen
c93d00dcbe fix test 2025-02-26 09:50:49 -08:00
alexjanousekGSA
a524ed0dc5 Fixed a test 2025-02-26 11:16:52 -05:00
alexjanousekGSA
c37bb44e0a Added unit tests for new utc conversion logic 2025-02-26 11:11:47 -05:00
dependabot[bot]
0b3177e3ff Bump sass-embedded from 1.85.0 to 1.85.1
Bumps [sass-embedded](https://github.com/sass/embedded-host-node) from 1.85.0 to 1.85.1.
- [Changelog](https://github.com/sass/embedded-host-node/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/embedded-host-node/compare/1.85.0...1.85.1)

---
updated-dependencies:
- dependency-name: sass-embedded
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-25 23:49:03 +00:00
Beverly Nguyen
baf9b69fbb fix pytest 2025-02-25 14:01:56 -08:00
Beverly Nguyen
df3e2264ec flake8 2025-02-25 13:19:59 -08:00
Beverly Nguyen
79192e29b6 fix jest testing 2025-02-25 13:12:55 -08:00
Beverly Nguyen
5cede78661 Merge branch 'main' into 1543-review-and-update-total-messages-chart-on-dashboard-and-usagehtml-with-new-data-from-backend 2025-02-24 14:49:28 -08:00
Beverly Nguyen
5c9d190021 total message ratio update 2025-02-24 14:44:43 -08:00
Beverly Nguyen
61fb59f1cb total message ratio update 2025-02-24 14:08:26 -08:00
Kenneth Kehl
02f39bd077 Merge pull request #2354 from GSA/fix_runaway_report_generation
fix runaway report generation
2025-02-21 10:13:37 -08:00
Kenneth Kehl
1eb12b93f1 fix runaway report generation 2025-02-21 09:22:29 -08:00
Jonathan Bobel
c1fa099c12 Create manual-a11y-checklist.md
Adding .md file for a11y documentation
2025-02-07 16:25:11 -05:00
16 changed files with 612 additions and 391 deletions

View File

@@ -527,7 +527,7 @@
"filename": "tests/app/main/views/test_accept_invite.py",
"hashed_secret": "07f0a6c13923fc3b5f0c57ffa2d29b715eb80d71",
"is_verified": false,
"line_number": 643,
"line_number": 631,
"is_secret": false
}
],
@@ -684,5 +684,5 @@
}
]
},
"generated_at": "2025-02-03T17:01:06Z"
"generated_at": "2025-02-26T18:19:37Z"
}

View File

@@ -162,3 +162,8 @@ upload-static:
# @cf map-route notify-admin ${DNS_NAME} --hostname www
# @cf unmap-route notify-admin-failwhale ${DNS_NAME} --hostname www
# @echo "Failwhale is disabled"
.PHONY: test-single
test-single: export NEW_RELIC_ENVIRONMENT=test
test-single: ## Run a single test file
poetry run pytest $(TEST_FILE)

View File

@@ -216,7 +216,13 @@
return;
}
var url = type === 'service' ? `/services/${currentServiceId}/daily-stats.json` : `/services/${currentServiceId}/daily-stats-by-user.json`;
var userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
var url = type === 'service'
? `/services/${currentServiceId}/daily-stats.json?timezone=${encodeURIComponent(userTimezone)}`
: `/services/${currentServiceId}/daily-stats-by-user.json`;
return fetch(url)
.then(response => {
if (!response.ok) {

View File

@@ -6,17 +6,17 @@
var chartTitle = document.getElementById('chartTitle').textContent;
// Access data attributes from the HTML
var sms_sent = parseInt(chartContainer.getAttribute('data-sms-sent'));
var sms_remaining_messages = parseInt(chartContainer.getAttribute('data-sms-allowance-remaining'));
var totalMessages = sms_sent + sms_remaining_messages;
var messagesSent = parseInt(chartContainer.getAttribute('data-messages-sent'));
var messagesRemaining = parseInt(chartContainer.getAttribute('data-messages-remaining'));
var totalMessages = messagesSent + messagesRemaining;
// Update the message below the chart
document.getElementById('message').innerText = `${sms_sent.toLocaleString()} sent / ${sms_remaining_messages.toLocaleString()} remaining`;
document.getElementById('message').innerText = `${messagesSent.toLocaleString()} sent / ${messagesRemaining.toLocaleString()} remaining`;
// Calculate minimum width for "Messages Sent" as 1% of the total chart width
var minSentPercentage = (sms_sent === 0) ? 0 : 0.02;
var minSentPercentage = (messagesSent === 0) ? 0 : 0.02;
var minSentValue = totalMessages * minSentPercentage;
var displaySent = Math.max(sms_sent, minSentValue);
var displaySent = Math.max(messagesSent, minSentValue);
var displayRemaining = totalMessages - displaySent;
var svg = d3.select("#totalMessageChart");
@@ -48,7 +48,7 @@
.attr("width", 0) // Start with width 0 for animation
.on('mouseover', function(event) {
tooltip.style('display', 'block')
.html(`Messages Sent: ${sms_sent.toLocaleString()}`);
.html(`Messages Sent: ${messagesSent.toLocaleString()}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
@@ -66,7 +66,7 @@
.attr("width", 0) // Start with width 0 for animation
.on('mouseover', function(event) {
tooltip.style('display', 'block')
.html(`Remaining: ${sms_remaining_messages.toLocaleString()}`);
.html(`Remaining: ${messagesRemaining.toLocaleString()}`);
})
.on('mousemove', function(event) {
tooltip.style('left', `${event.pageX + 10}px`)
@@ -115,9 +115,9 @@
var tbodyRow = document.createElement('tr');
var tdMessagesSent = document.createElement('td');
tdMessagesSent.textContent = sms_sent.toLocaleString(); // Value for Messages Sent
tdMessagesSent.textContent = messagesSent.toLocaleString(); // Value for Messages Sent
var tdRemaining = document.createElement('td');
tdRemaining.textContent = sms_remaining_messages.toLocaleString(); // Value for Remaining
tdRemaining.textContent = messagesRemaining.toLocaleString(); // Value for Remaining
tbodyRow.appendChild(tdMessagesSent);
tbodyRow.appendChild(tdRemaining);

View File

@@ -1,7 +1,8 @@
import calendar
from datetime import datetime
from datetime import datetime, timedelta
from functools import partial
from itertools import groupby
from zoneinfo import ZoneInfo
from flask import Response, abort, jsonify, render_template, request, session, url_for
from flask_login import current_user
@@ -48,38 +49,27 @@ def service_dashboard(service_id):
if not current_user.has_permissions("view_activity"):
return redirect(url_for("main.choose_template", service_id=service_id))
yearly_usage = billing_api_client.get_annual_usage_for_service(
service_id,
get_current_financial_year(),
)
free_sms_allowance = billing_api_client.get_free_sms_fragment_limit_for_year(
current_service.id,
)
usage_data = get_annual_usage_breakdown(yearly_usage, free_sms_allowance)
sms_sent = usage_data["sms_sent"]
sms_allowance_remaining = usage_data["sms_allowance_remaining"]
job_response = job_api_client.get_jobs(service_id)["data"]
service_data_retention_days = 7
active_jobs = [job for job in job_response if job["job_status"] != "cancelled"]
sorted_jobs = sorted(active_jobs, key=lambda job: job["created_at"], reverse=True)
job_lists = [
{
**job_dict,
"finished_processing": job_is_finished(job_dict)
}
{**job_dict, "finished_processing": job_is_finished(job_dict)}
for job_dict in sorted_jobs
]
total_messages = service_api_client.get_service_message_ratio(service_id)
messages_remaining = total_messages.get("messages_remaining", 0)
messages_sent = total_messages.get("messages_sent", 0)
return render_template(
"views/dashboard/dashboard.html",
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
partials=get_dashboard_partials(service_id),
jobs=job_lists,
service_data_retention_days=service_data_retention_days,
sms_sent=sms_sent,
sms_allowance_remaining=sms_allowance_remaining,
messages_remaining=messages_remaining,
messages_sent=messages_sent,
)
@@ -91,7 +81,7 @@ def job_is_finished(job_dict):
"technical-failure",
"temporary-failure",
"permanent-failure",
"cancelled"
"cancelled",
]
processed_count = sum(
@@ -106,10 +96,47 @@ def job_is_finished(job_dict):
@user_has_permissions()
def get_daily_stats(service_id):
date_range = get_stats_date_range()
stats = service_api_client.get_service_notification_statistics_by_day(
service_id, start_date=date_range["start_date"], days=date_range["days"]
days = date_range["days"]
user_timezone = request.args.get("timezone", "UTC")
stats_utc = service_api_client.get_service_notification_statistics_by_day(
service_id,
start_date=date_range["start_date"],
days=days,
)
return jsonify(stats)
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days)
return jsonify(local_stats)
def get_local_daily_stats_for_last_x_days(stats_utc, user_timezone, days):
tz = ZoneInfo(user_timezone)
today_local = datetime.now(tz).date()
start_local = today_local - timedelta(days=days - 1)
# Generate exactly days local dates, each with zeroed stats
days_list = [
(start_local + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(days)
]
aggregator = {
d: {
"sms": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
}
for d in days_list
}
# Convert each UTC timestamp to local date and iterate
for utc_ts, data in stats_utc.items():
utc_dt = datetime.strptime(utc_ts, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=ZoneInfo("UTC"))
local_day = utc_dt.astimezone(tz).strftime("%Y-%m-%d")
if local_day in aggregator:
for msg_type in ["sms", "email"]:
for status in ["delivered", "failure", "pending", "requested"]:
aggregator[local_day][msg_type][status] += data[msg_type][status]
return aggregator
@main.route("/services/<uuid:service_id>/daily-stats-by-user.json")

View File

@@ -537,6 +537,11 @@ class ServiceAPIClient(NotifyAdminAPIClient):
"""
return self.get("/service/invite/redis/{0}".format(redis_key))
def get_service_message_ratio(self, service_id):
return self.get(
url="service/get-service-message-ratio?service_id={0}".format(service_id),
)
service_api_client = ServiceAPIClient()

View File

@@ -26,7 +26,7 @@
{{ ajax_block(partials, updates_url, 'inbox') }}
<div id="totalMessageChartContainer" data-sms-sent="{{ sms_sent }}" data-sms-allowance-remaining="{{ sms_allowance_remaining }}">
<div id="totalMessageChartContainer" data-messages-sent="{{ messages_sent }}" data-messages-remaining="{{ messages_remaining }}">
<h2 id="chartTitle">Total messages</h2>
<svg id="totalMessageChart"></svg>
<div id="message"></div>

View File

@@ -128,6 +128,13 @@ def generate_notifications_csv(**kwargs):
notifications_resp = notification_api_client.get_notifications_for_service(
**kwargs
)
# Stop if we are finished
if (
notifications_resp.get("notifications") is None
or len(notifications_resp["notifications"]) == 0
):
return
for notification in notifications_resp["notifications"]:
preferred_tz_created_at = convert_report_date_to_preferred_timezone(
notification["created_at"]

View File

@@ -0,0 +1,136 @@
# Manual Accessibility Testing Checklist
## 1. Structure and Semantics
### Headings:
- Verify that headings are used logically (`<h1>` to `<h6>`) to create a clear content hierarchy.
- Ensure there is only one `<h1>` per page (unless its a valid use case, like within `<section>` landmarks).
### Landmarks:
- Confirm the presence of ARIA landmarks (e.g., `<header>`, `<main>`, `<footer>`) for navigation.
- Use tools (e.g., Accessibility Insights, Axe) to ensure proper landmark roles.
### HTML Validity:
- Check for semantic HTML usage (e.g., `<button>` for buttons, `<a>` for links).
- Avoid using `<div>` or `<span>` for interactive elements.
## 2. Navigation and Focus
### Keyboard Navigation:
- Ensure all functionality is accessible via the keyboard (e.g., Tab, Enter, Space, Arrow Keys).
- Test for logical tab order that follows the visual reading order.
### Focus States:
- Ensure focus is visible and distinct on all interactive elements.
- Check that focus is not trapped in modal dialogs or components.
### Skip Links:
- Verify that “Skip to content” links are present and functional.
## 3. Forms and Inputs
### Labels:
- Confirm all form fields have accessible, descriptive labels (`<label>` or `aria-label`/`aria-labelledby`).
- Ensure placeholder text is not used as a label substitute.
### Error Messages:
- Check that error messages are programmatically associated with inputs and conveyed to assistive technologies.
- Verify that error messages are specific and provide actionable guidance.
### Fieldset and Legend:
- Group related inputs with `<fieldset>` and `<legend>` where appropriate.
## 4. Media and Non-Text Content
### Images:
- Ensure all meaningful images have appropriate alt text.
- Decorative images should have `alt=""` or be hidden with `role="presentation"`.
### Videos:
- Verify captions are available and accurate for all video content.
- Provide audio descriptions for videos with critical visual information.
### Audio:
- Confirm there is a way to stop, pause, or adjust the volume of any audio that plays automatically.
## 5. Color and Contrast
### Color Contrast:
- Use a contrast checker to ensure text and interactive elements meet WCAG AA requirements (4.5:1 for text, 3:1 for large text).
### Color Independence:
- Verify that color is not the sole means of conveying information (e.g., “errors in red”).
### High Contrast Modes:
- Test with browser or OS high-contrast modes to ensure proper readability.
---
## 6. Dynamic Content and Interactions
### ARIA Live Regions:
- Verify that dynamic updates are announced using `aria-live` (e.g., success messages).
### Modals and Dialogs:
- Ensure modals have proper focus management (focus should move to the modal on open and back to the trigger on close).
- Test that modals are announced properly by screen readers.
### Tooltips and Popovers:
- Ensure tooltips are accessible via keyboard and announced by assistive technologies.
## 7. Assistive Technology Compatibility
### Screen Reader Testing:
- Test the site with a screen reader (e.g., NVDA, JAWS, VoiceOver) to ensure content is announced logically.
- Verify that dynamic content (e.g., dropdowns, modals) is announced appropriately.
### Zoom and Magnification:
- Ensure the site is usable at 200% zoom without loss of functionality or content.
### Responsive Design:
- Test on different devices and orientations to verify responsive behavior is accessible.
## 8. Performance and Usability
### Loading Indicators:
- Confirm that loading indicators are announced (e.g., using `aria-live`).
### Page Titles:
- Ensure page titles are unique and descriptive of the pages content.
### Time Limits:
- Check if users can extend or disable time limits where applicable.
## 9. WCAG Success Criteria Coverage
### Review WCAG 2.1 (or 2.2 if applicable) Success Criteria:
- **Level A**: Must-have minimum requirements.
- **Level AA**: Commonly required for legal compliance.
- **Level AAA**: Optional for enhanced accessibility.
## Tools to Assist Manual Testing
### Browser Extensions:
- [Axe DevTools](https://www.deque.com/axe/)
- [WAVE](https://wave.webaim.org/)
- [Lighthouse](https://developers.google.com/web/tools/lighthouse)
### Screen Readers:
- **NVDA** (Windows)
- **VoiceOver** (macOS/iOS)
- **TalkBack** (Android)
### Contrast Checkers:
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- [Color Contrast Analyzer](https://developer.paciellogroup.com/resources/contrastanalyser/)
### Keyboard Navigation:
- Test tabbing through the site manually.

328
package-lock.json generated
View File

@@ -24,7 +24,7 @@
"playwright": "^1.50.1",
"python": "^0.0.4",
"query-command-supported": "1.0.0",
"sass-embedded": "^1.85.0",
"sass-embedded": "^1.85.1",
"textarea-caret": "3.1.0",
"timeago": "1.6.7",
"vinyl-buffer": "^1.0.1",
@@ -50,7 +50,7 @@
"jest-environment-jsdom": "^29.2.2",
"jshint": "2.13.6",
"jshint-stylish": "2.2.1",
"rollup": "^4.34.8",
"rollup": "^4.34.9",
"rollup-plugin-commonjs": "10.1.0",
"rollup-plugin-node-resolve": "5.2.0"
},
@@ -2340,9 +2340,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
"integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz",
"integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==",
"cpu": [
"arm"
],
@@ -2353,9 +2353,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz",
"integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz",
"integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==",
"cpu": [
"arm64"
],
@@ -2366,9 +2366,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz",
"integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz",
"integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==",
"cpu": [
"arm64"
],
@@ -2379,9 +2379,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz",
"integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz",
"integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==",
"cpu": [
"x64"
],
@@ -2392,9 +2392,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz",
"integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz",
"integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==",
"cpu": [
"arm64"
],
@@ -2405,9 +2405,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz",
"integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz",
"integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==",
"cpu": [
"x64"
],
@@ -2418,9 +2418,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz",
"integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz",
"integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==",
"cpu": [
"arm"
],
@@ -2431,9 +2431,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz",
"integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz",
"integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==",
"cpu": [
"arm"
],
@@ -2444,9 +2444,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz",
"integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz",
"integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==",
"cpu": [
"arm64"
],
@@ -2457,9 +2457,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz",
"integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz",
"integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==",
"cpu": [
"arm64"
],
@@ -2470,9 +2470,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz",
"integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz",
"integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==",
"cpu": [
"loong64"
],
@@ -2483,9 +2483,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz",
"integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz",
"integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==",
"cpu": [
"ppc64"
],
@@ -2496,9 +2496,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz",
"integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz",
"integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==",
"cpu": [
"riscv64"
],
@@ -2509,9 +2509,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz",
"integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz",
"integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==",
"cpu": [
"s390x"
],
@@ -2522,9 +2522,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz",
"integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz",
"integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==",
"cpu": [
"x64"
],
@@ -2535,9 +2535,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz",
"integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz",
"integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==",
"cpu": [
"x64"
],
@@ -2548,9 +2548,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz",
"integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz",
"integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==",
"cpu": [
"arm64"
],
@@ -2561,9 +2561,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz",
"integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz",
"integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==",
"cpu": [
"ia32"
],
@@ -2574,9 +2574,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz",
"integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz",
"integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==",
"cpu": [
"x64"
],
@@ -11566,9 +11566,9 @@
"license": "Unlicense"
},
"node_modules/rollup": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz",
"integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==",
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz",
"integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
@@ -11581,25 +11581,25 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.34.8",
"@rollup/rollup-android-arm64": "4.34.8",
"@rollup/rollup-darwin-arm64": "4.34.8",
"@rollup/rollup-darwin-x64": "4.34.8",
"@rollup/rollup-freebsd-arm64": "4.34.8",
"@rollup/rollup-freebsd-x64": "4.34.8",
"@rollup/rollup-linux-arm-gnueabihf": "4.34.8",
"@rollup/rollup-linux-arm-musleabihf": "4.34.8",
"@rollup/rollup-linux-arm64-gnu": "4.34.8",
"@rollup/rollup-linux-arm64-musl": "4.34.8",
"@rollup/rollup-linux-loongarch64-gnu": "4.34.8",
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.8",
"@rollup/rollup-linux-riscv64-gnu": "4.34.8",
"@rollup/rollup-linux-s390x-gnu": "4.34.8",
"@rollup/rollup-linux-x64-gnu": "4.34.8",
"@rollup/rollup-linux-x64-musl": "4.34.8",
"@rollup/rollup-win32-arm64-msvc": "4.34.8",
"@rollup/rollup-win32-ia32-msvc": "4.34.8",
"@rollup/rollup-win32-x64-msvc": "4.34.8",
"@rollup/rollup-android-arm-eabi": "4.34.9",
"@rollup/rollup-android-arm64": "4.34.9",
"@rollup/rollup-darwin-arm64": "4.34.9",
"@rollup/rollup-darwin-x64": "4.34.9",
"@rollup/rollup-freebsd-arm64": "4.34.9",
"@rollup/rollup-freebsd-x64": "4.34.9",
"@rollup/rollup-linux-arm-gnueabihf": "4.34.9",
"@rollup/rollup-linux-arm-musleabihf": "4.34.9",
"@rollup/rollup-linux-arm64-gnu": "4.34.9",
"@rollup/rollup-linux-arm64-musl": "4.34.9",
"@rollup/rollup-linux-loongarch64-gnu": "4.34.9",
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.9",
"@rollup/rollup-linux-riscv64-gnu": "4.34.9",
"@rollup/rollup-linux-s390x-gnu": "4.34.9",
"@rollup/rollup-linux-x64-gnu": "4.34.9",
"@rollup/rollup-linux-x64-musl": "4.34.9",
"@rollup/rollup-win32-arm64-msvc": "4.34.9",
"@rollup/rollup-win32-ia32-msvc": "4.34.9",
"@rollup/rollup-win32-x64-msvc": "4.34.9",
"fsevents": "~2.3.2"
}
},
@@ -11708,9 +11708,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sass-embedded": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.85.0.tgz",
"integrity": "sha512-x3Vv54g0jv1aPSW8OTA/0GzQCs/HMQOjIkLtZJ3Xsn/I4vnyjKbVTQmFTax9bQjldqLEEkdbvy6ES/cOOnYNwA==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.85.1.tgz",
"integrity": "sha512-0i+3h2Df/c71afluxC1SXqyyMmJlnKWfu9ZGdzwuKRM1OftEa2XM2myt5tR36CF3PanYrMjFKtRIj8PfSf838w==",
"license": "MIT",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
@@ -11729,32 +11729,32 @@
"node": ">=16.0.0"
},
"optionalDependencies": {
"sass-embedded-android-arm": "1.85.0",
"sass-embedded-android-arm64": "1.85.0",
"sass-embedded-android-ia32": "1.85.0",
"sass-embedded-android-riscv64": "1.85.0",
"sass-embedded-android-x64": "1.85.0",
"sass-embedded-darwin-arm64": "1.85.0",
"sass-embedded-darwin-x64": "1.85.0",
"sass-embedded-linux-arm": "1.85.0",
"sass-embedded-linux-arm64": "1.85.0",
"sass-embedded-linux-ia32": "1.85.0",
"sass-embedded-linux-musl-arm": "1.85.0",
"sass-embedded-linux-musl-arm64": "1.85.0",
"sass-embedded-linux-musl-ia32": "1.85.0",
"sass-embedded-linux-musl-riscv64": "1.85.0",
"sass-embedded-linux-musl-x64": "1.85.0",
"sass-embedded-linux-riscv64": "1.85.0",
"sass-embedded-linux-x64": "1.85.0",
"sass-embedded-win32-arm64": "1.85.0",
"sass-embedded-win32-ia32": "1.85.0",
"sass-embedded-win32-x64": "1.85.0"
"sass-embedded-android-arm": "1.85.1",
"sass-embedded-android-arm64": "1.85.1",
"sass-embedded-android-ia32": "1.85.1",
"sass-embedded-android-riscv64": "1.85.1",
"sass-embedded-android-x64": "1.85.1",
"sass-embedded-darwin-arm64": "1.85.1",
"sass-embedded-darwin-x64": "1.85.1",
"sass-embedded-linux-arm": "1.85.1",
"sass-embedded-linux-arm64": "1.85.1",
"sass-embedded-linux-ia32": "1.85.1",
"sass-embedded-linux-musl-arm": "1.85.1",
"sass-embedded-linux-musl-arm64": "1.85.1",
"sass-embedded-linux-musl-ia32": "1.85.1",
"sass-embedded-linux-musl-riscv64": "1.85.1",
"sass-embedded-linux-musl-x64": "1.85.1",
"sass-embedded-linux-riscv64": "1.85.1",
"sass-embedded-linux-x64": "1.85.1",
"sass-embedded-win32-arm64": "1.85.1",
"sass-embedded-win32-ia32": "1.85.1",
"sass-embedded-win32-x64": "1.85.1"
}
},
"node_modules/sass-embedded-android-arm": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.85.0.tgz",
"integrity": "sha512-pPBT7Ad6G8Mlao8ypVNXW2ya7I/Bhcny+RYZ/EmrunEXfhzCNp4PWV2VAweitPO9RnPIJwvUTkLc8Fu6K3nVmw==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.85.1.tgz",
"integrity": "sha512-GkcgUGMZtEF9gheuE1dxCU0ZSAifuaFXi/aX7ZXvjtdwmTl9Zc/OHR9oiUJkc8IW9UI7H8TuwlTAA8+SwgwIeQ==",
"cpu": [
"arm"
],
@@ -11768,9 +11768,9 @@
}
},
"node_modules/sass-embedded-android-arm64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.85.0.tgz",
"integrity": "sha512-4itDzRwezwrW8+YzMLIwHtMeH+qrBNdBsRn9lTVI15K+cNLC8z5JWJi6UCZ8TNNZr9LDBfsh5jUdjSub0yF7jg==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.85.1.tgz",
"integrity": "sha512-27oRheqNA3SJM2hAxpVbs7mCKUwKPWmEEhyiNFpBINb5ELVLg+Ck5RsGg+SJmo130ul5YX0vinmVB5uPWc8X5w==",
"cpu": [
"arm64"
],
@@ -11784,9 +11784,9 @@
}
},
"node_modules/sass-embedded-android-ia32": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.85.0.tgz",
"integrity": "sha512-bwqKq95hzbGbMTeXCMQhH7yEdc2xJVwIXj7rGdD3McvyFWbED6362XRFFPI5YyjfD2wRJd9yWLh/hn+6VyjcYA==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.85.1.tgz",
"integrity": "sha512-f3x16NyRgtXFksIaO/xXKrUhttUBv8V0XsAR2Dhdb/yz4yrDrhzw9Wh8fmw7PlQqECcQvFaoDr3XIIM6lKzasw==",
"cpu": [
"ia32"
],
@@ -11800,9 +11800,9 @@
}
},
"node_modules/sass-embedded-android-riscv64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.85.0.tgz",
"integrity": "sha512-Fgkgay+5EePJXZFHR5Vlkutnsmox2V6nX4U3mfGbSN1xjLRm8F5ST72V2s5Z0mnIFpGvEu/v7hfptgViqMvaxg==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.85.1.tgz",
"integrity": "sha512-IP6OijpJ8Mqo7XqCe0LsuZVbAxEFVboa0kXqqR5K55LebEplsTIA2GnmRyMay3Yr/2FVGsZbCb6Wlgkw23eCiA==",
"cpu": [
"riscv64"
],
@@ -11816,9 +11816,9 @@
}
},
"node_modules/sass-embedded-android-x64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.85.0.tgz",
"integrity": "sha512-/bG3JgTn3eoIDHCiJNVkLeJgUesat4ghxqYmKMZUJx++4e6iKCDj8XwQTJAgm+QDrsPKXHBacHEANJ9LEAuTqg==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.85.1.tgz",
"integrity": "sha512-Mh7CA53wR3ADvXAYipFc/R3vV4PVOzoKwWzPxmq+7i8UZrtsVjKONxGtqWe9JG1mna0C9CRZAx0sv/BzbOJxWg==",
"cpu": [
"x64"
],
@@ -11832,9 +11832,9 @@
}
},
"node_modules/sass-embedded-darwin-arm64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.85.0.tgz",
"integrity": "sha512-plp8TyMz97YFBCB3ndftEvoW29vyfsSBJILM5U84cGzr06SvLh/Npjj8psfUeRw+upEk1zkFtw5u61sRCdgwIw==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.85.1.tgz",
"integrity": "sha512-msWxzhvcP9hqGVegxVePVEfv9mVNTlUgGr6k7O7Ihji702mbtrH/lKwF4aRkkt4g1j7tv10+JtQXmTNi/pi9kA==",
"cpu": [
"arm64"
],
@@ -11848,9 +11848,9 @@
}
},
"node_modules/sass-embedded-darwin-x64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.85.0.tgz",
"integrity": "sha512-LP8Zv8DG57Gn6PmSwWzC0gEZUsGdg36Ps3m0i1fVTOelql7N3HZIrlPYRjJvidL8ZlB3ISxNANebTREUHn/wkQ==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.85.1.tgz",
"integrity": "sha512-J4UFHUiyI9Z+mwYMwz11Ky9TYr3hY1fCxeQddjNGL/+ovldtb0yAIHvoVM0BGprQDm5JqhtUk8KyJ3RMJqpaAA==",
"cpu": [
"x64"
],
@@ -11864,9 +11864,9 @@
}
},
"node_modules/sass-embedded-linux-arm": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.85.0.tgz",
"integrity": "sha512-18xOAEfazJt1MMVS2TRHV94n81VyMnywOoJ7/S7I79qno/zx26OoqqP4XvH107xu8+mZ9Gg54LrUH6ZcgHk08g==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.85.1.tgz",
"integrity": "sha512-X0fDh95nNSw1wfRlnkE4oscoEA5Au4nnk785s9jghPFkTBg+A+5uB6trCjf0fM22+Iw6kiP4YYmDdw3BqxAKLQ==",
"cpu": [
"arm"
],
@@ -11880,9 +11880,9 @@
}
},
"node_modules/sass-embedded-linux-arm64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.85.0.tgz",
"integrity": "sha512-JRIRKVOY5Y8M1zlUOv9AQGju4P6lj8i5vLJZsVYVN/uY8Cd2dDJZPC8EOhjntp+IpF8AOGIHqCeCkHBceIyIjA==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.85.1.tgz",
"integrity": "sha512-jGadetB03BMFG2rq3OXub/uvC/lGpbQOiLGEz3NLb2nRZWyauRhzDtvZqkr6BEhxgIWtMtz2020yD8ZJSw/r2w==",
"cpu": [
"arm64"
],
@@ -11896,9 +11896,9 @@
}
},
"node_modules/sass-embedded-linux-ia32": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.85.0.tgz",
"integrity": "sha512-4JH+h+gLt9So22nNPQtsKojEsLzjld9ol3zWcOtMGclv+HojZGbCuhJUrLUcK72F8adXYsULmWhJPKROLIwYMA==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.85.1.tgz",
"integrity": "sha512-7HlYY90d9mitDtNi5s+S+5wYZrTVbkBH2/kf7ixrzh2BFfT0YM81UHLJRnGX93y9aOMBL6DSZAIfkt1RsV9bkQ==",
"cpu": [
"ia32"
],
@@ -11912,9 +11912,9 @@
}
},
"node_modules/sass-embedded-linux-musl-arm": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.85.0.tgz",
"integrity": "sha512-Z1j4ageDVFihqNUBnm89fxY46pY0zD/Clp1D3ZdI7S+D280+AEpbm5vMoH8LLhBQfQLf2w7H++SZGpQwrisudQ==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.85.1.tgz",
"integrity": "sha512-5vcdEqE8QZnu6i6shZo7x2N36V7YUoFotWj2rGekII5ty7Nkaj+VtZhUEOp9tAzEOlaFuDp5CyO1kUCvweT64A==",
"cpu": [
"arm"
],
@@ -11928,9 +11928,9 @@
}
},
"node_modules/sass-embedded-linux-musl-arm64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.85.0.tgz",
"integrity": "sha512-aoQjUjK28bvdw9XKTjQeayn8oWQ2QqvoTD11myklGd3IHH7Jj0nwXUstI4NxDueCKt3wghuZoIQkjOheReQxlg==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.85.1.tgz",
"integrity": "sha512-FLkIT0p18XOkR6wryJ13LqGBDsrYev2dRk9dtiU18NCpNXruKsdBQ1ZnWHVKB3h1dA9lFyEEisC0sooKdNfeOQ==",
"cpu": [
"arm64"
],
@@ -11944,9 +11944,9 @@
}
},
"node_modules/sass-embedded-linux-musl-ia32": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.85.0.tgz",
"integrity": "sha512-/cJCSXOfXmQFH8deE+3U9x+BSz8i0d1Tt9gKV/Gat1Xm43Oumw8pmZgno+cDuGjYQInr9ryW5121pTMlj/PBXQ==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.85.1.tgz",
"integrity": "sha512-N1093T84zQJor1yyIAdYScB5eAuQarGK1tKgZ4uTnxVlgA7Xi1lXV8Eh7ox9sDqKCaWkVQ3MjqU26vYRBeRWyw==",
"cpu": [
"ia32"
],
@@ -11960,9 +11960,9 @@
}
},
"node_modules/sass-embedded-linux-musl-riscv64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.85.0.tgz",
"integrity": "sha512-l+FJxMXkmg42RZq5RFKXg4InX0IA7yEiPHe4kVSdrczP7z3NLxk+W9wVkPnoRKYIMe1qZPPQ25y0TgI4HNWouA==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.85.1.tgz",
"integrity": "sha512-WRsZS/7qlfYXsa93FBpSruieuURIu7ySfFhzYfF1IbKrNAGwmbduutkHZh2ddm5/vQMvQ0Rdosgv+CslaQHMcw==",
"cpu": [
"riscv64"
],
@@ -11976,9 +11976,9 @@
}
},
"node_modules/sass-embedded-linux-musl-x64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.85.0.tgz",
"integrity": "sha512-M9ffjcYfFcRvkFA6V3DpOS955AyvmpvPAhL/xNK45d/ma1n1ehTWpd24tVeKiNK5CZkNjjMEfyw2fHa6MpqmEA==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.85.1.tgz",
"integrity": "sha512-+OlLIilA5TnP0YEqTQ8yZtkW+bJIQYvzoGoNLUEskeyeGuOiIyn2CwL6G4JQB4xZQFaxPHb7JD3EueFkQbH0Pw==",
"cpu": [
"x64"
],
@@ -11992,9 +11992,9 @@
}
},
"node_modules/sass-embedded-linux-riscv64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.85.0.tgz",
"integrity": "sha512-yqPXQWfM+qiIPkfn++48GOlbmSvUZIyL9nwFstBk0k4x40UhbhilfknqeTUpxoHfQzylTGVhrm5JE7MjM+LNZA==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.85.1.tgz",
"integrity": "sha512-mKKlOwMGLN7yP1p0gB5yG/HX4fYLnpWaqstNuOOXH+fOzTaNg0+1hALg0H0CDIqypPO74M5MS9T6FAJZGdT6dQ==",
"cpu": [
"riscv64"
],
@@ -12008,9 +12008,9 @@
}
},
"node_modules/sass-embedded-linux-x64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.85.0.tgz",
"integrity": "sha512-NTDeQFZcuVR7COoaRy8pZD6/+QznwBR8kVFsj7NpmvX9aJ7TX/q+OQZHX7Bfb3tsfKXhf1YZozegPuYxRnMKAQ==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.85.1.tgz",
"integrity": "sha512-uKRTv0z8NgtHV7xSren78+yoWB79sNi7TMqI7Bxd8fcRNIgHQSA8QBdF8led2ETC004hr8h71BrY60RPO+SSvA==",
"cpu": [
"x64"
],
@@ -12024,9 +12024,9 @@
}
},
"node_modules/sass-embedded-win32-arm64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.85.0.tgz",
"integrity": "sha512-gO0VAuxC4AdV+uZYJESRWVVHQWCGzNs0C3OKCAdH4r1vGRugooMi7J/5wbwUdXDA1MV9ICfhlKsph2n3GiPdqA==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.85.1.tgz",
"integrity": "sha512-/GMiZXBOc6AEMBC3g25Rp+x8fq9Z6Ql7037l5rajBPhZ+DdFwtdHY0Ou3oIU6XuWUwD06U3ii4XufXVFhsP6PA==",
"cpu": [
"arm64"
],
@@ -12040,9 +12040,9 @@
}
},
"node_modules/sass-embedded-win32-ia32": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.85.0.tgz",
"integrity": "sha512-PCyn6xeFIBUgBceNypuf73/5DWF2VWPlPqPuBprPsTvpZOMUJeBtP+Lf4mnu3dNy1z76mYVnpaCnQmzZ0zHZaA==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.85.1.tgz",
"integrity": "sha512-L+4BWkKKBGFOKVQ2PQ5HwFfkM5FvTf1Xx2VSRvEWt9HxPXp6SPDho6zC8fqNQ3hSjoaoASEIJcSvgfdQYO0gdg==",
"cpu": [
"ia32"
],
@@ -12056,9 +12056,9 @@
}
},
"node_modules/sass-embedded-win32-x64": {
"version": "1.85.0",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.85.0.tgz",
"integrity": "sha512-AknE2jLp6OBwrR5hQ8pDsG94KhJCeSheFJ2xgbnk8RUjZX909JiNbgh2sNt9LG+RXf4xZa55dDL537gZoCx/iw==",
"version": "1.85.1",
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.85.1.tgz",
"integrity": "sha512-/FO0AGKWxVfCk4GKsC0yXWBpUZdySe3YAAbQQL0lL6xUd1OiUY8Kow6g4Kc1TB/+z0iuQKKTqI/acJMEYl4iTQ==",
"cpu": [
"x64"
],

View File

@@ -40,7 +40,7 @@
"playwright": "^1.50.1",
"python": "^0.0.4",
"query-command-supported": "1.0.0",
"sass-embedded": "^1.85.0",
"sass-embedded": "^1.85.1",
"textarea-caret": "3.1.0",
"timeago": "1.6.7",
"vinyl-buffer": "^1.0.1",
@@ -66,7 +66,7 @@
"jest-environment-jsdom": "^29.2.2",
"jshint": "2.13.6",
"jshint-stylish": "2.2.1",
"rollup": "^4.34.8",
"rollup": "^4.34.9",
"rollup-plugin-commonjs": "10.1.0",
"rollup-plugin-node-resolve": "5.2.0"
}

View File

@@ -300,25 +300,13 @@ def test_accepting_invite_removes_invite_from_session(
client_request.login(user)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
date_range = {"start_date": "2024-01-01", "days": 7}
mocker.patch(
"app.main.views.dashboard.get_daily_stats",
"app.service_api_client.get_service_message_ratio",
return_value={
date_range["start_date"]: {
"email": {"delivered": 0, "failure": 0, "requested": 0},
"sms": {"delivered": 0, "failure": 1, "requested": 1},
},
},
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value={
date_range["start_date"]: {
"email": {"delivered": 1, "failure": 0, "requested": 1},
"sms": {"delivered": 1, "failure": 0, "requested": 1},
},
"messages_remaining": 71919,
"messages_sent": 28081,
"total_message_limit": 100000,
},
)
page = client_request.get(

View File

@@ -1,6 +1,8 @@
import copy
import json
from datetime import datetime
from unittest.mock import patch
from zoneinfo import ZoneInfo
import pytest
from flask import url_for
@@ -12,6 +14,7 @@ from app.main.views.dashboard import (
aggregate_template_usage,
format_monthly_stats_to_list,
get_dashboard_totals,
get_local_daily_stats_for_last_x_days,
get_tuples_of_financial_years,
)
from tests import (
@@ -193,6 +196,12 @@ mock_daily_stats_by_user = {
},
}
mock_service_message_ratio = {
"messages_remaining": 71919,
"messages_sent": 28081,
"total_message_limit": 100000,
}
@pytest.mark.parametrize(
"user",
@@ -252,12 +261,8 @@ def test_get_started(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get(
"main.service_dashboard",
@@ -286,12 +291,8 @@ def test_get_started_is_hidden_once_templates_exist(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get(
"main.service_dashboard",
@@ -317,12 +318,8 @@ def test_inbound_messages_not_visible_to_service_without_permissions(
service_one["permissions"] = []
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get(
"main.service_dashboard",
@@ -348,12 +345,8 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_messages(
):
service_one["permissions"] = ["inbound_sms"]
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get(
"main.service_dashboard",
@@ -384,12 +377,8 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_no_messages(
):
service_one["permissions"] = ["inbound_sms"]
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get(
"main.service_dashboard",
@@ -638,12 +627,8 @@ def test_should_show_recent_templates_on_dashboard(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get(
"main.service_dashboard",
@@ -690,12 +675,8 @@ def test_should_not_show_recent_templates_on_dashboard_if_only_one_template_used
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
main = page.select_one("main").text
@@ -849,12 +830,8 @@ def test_should_show_upcoming_jobs_on_dashboard(
mock_get_inbound_sms_summary,
):
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get(
"main.service_dashboard",
@@ -896,12 +873,8 @@ def test_should_not_show_upcoming_jobs_on_dashboard_if_count_is_0(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get(
"main.service_dashboard",
@@ -926,12 +899,8 @@ def test_should_not_show_upcoming_jobs_on_dashboard_if_service_has_no_jobs(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get(
"main.service_dashboard",
@@ -1016,12 +985,8 @@ def test_should_not_show_jobs_on_dashboard_for_users_with_uploads_page(
mock_get_inbound_sms_summary,
):
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get(
"main.service_dashboard",
@@ -1284,12 +1249,8 @@ def test_menu_send_messages(
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = _test_dashboard_menu(
client_request,
@@ -1325,12 +1286,8 @@ def test_menu_manage_service(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = _test_dashboard_menu(
client_request,
@@ -1366,12 +1323,8 @@ def test_menu_main_settings(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = _test_settings_menu(
client_request,
@@ -1406,12 +1359,8 @@ def test_menu_manage_api_keys(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = _test_dashboard_menu(
client_request,
@@ -1447,12 +1396,8 @@ def test_menu_all_services_for_platform_admin_user(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = _test_dashboard_menu(
client_request, mocker, platform_admin_user, service_one, []
@@ -1490,12 +1435,8 @@ def test_route_for_service_permissions(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
validate_route_permission(
mocker,
@@ -1639,12 +1580,8 @@ def test_org_breadcrumbs_do_not_show_if_service_has_no_org(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
@@ -1710,12 +1647,8 @@ def test_org_breadcrumbs_show_if_user_is_a_member_of_the_services_org(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
@@ -1750,12 +1683,8 @@ def test_org_breadcrumbs_do_not_show_if_user_is_a_member_of_the_services_org_but
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
@@ -1792,12 +1721,8 @@ def test_org_breadcrumbs_show_if_user_is_platform_admin(
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
client_request.login(platform_admin_user, service_one_json)
@@ -1829,14 +1754,9 @@ def test_breadcrumb_shows_if_service_is_suspended(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
@@ -1869,12 +1789,8 @@ def test_service_dashboard_shows_usage(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
service_one["permissions"] = permissions
@@ -1909,12 +1825,8 @@ def test_service_dashboard_shows_free_allowance(
)
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
@@ -1930,12 +1842,8 @@ def test_service_dashboard_shows_batched_jobs(
):
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
mocker.patch(
"app.main.views.dashboard.get_daily_stats", return_value=mock_daily_stats
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value=mock_daily_stats_by_user,
"app.service_api_client.get_service_message_ratio",
return_value=mock_service_message_ratio,
)
page = client_request.get("main.service_dashboard", service_id=SERVICE_ONE_ID)
@@ -1945,3 +1853,155 @@ def test_service_dashboard_shows_batched_jobs(
assert len(rows) == 1
assert job_table_body is not None
@patch("app.main.views.dashboard.datetime", wraps=datetime)
def test_simple_local_conversion(mock_dt):
stats_utc = {
"2025-02-24T15:00:00Z": {
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
},
"2025-02-25T07:00:00Z": {
"sms": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
},
}
# Mock today's date in local time: 2025-02-25 at 10:00
mock_dt.now.return_value = datetime(2025, 2, 25, 10, 0, 0, tzinfo=ZoneInfo("America/New_York"))
local_stats = get_local_daily_stats_for_last_x_days(
stats_utc,
"America/New_York",
days=2
)
assert len(local_stats) == 2
assert "2025-02-24" in local_stats
assert "2025-02-25" in local_stats
assert local_stats["2025-02-24"]["sms"]["delivered"] == 1
assert local_stats["2025-02-24"]["sms"]["requested"] == 1
assert local_stats["2025-02-25"]["sms"]["delivered"] == 2
assert local_stats["2025-02-25"]["sms"]["requested"] == 2
def test_no_timestamps_returns_zeroed_days():
stats_utc = {}
class MockDateTime(datetime):
@classmethod
def now(cls, tz=None):
return cls(2025, 2, 26, 8, 0, 0, tzinfo=tz)
with patch("app.main.views.dashboard.datetime", MockDateTime):
local_stats = get_local_daily_stats_for_last_x_days(
stats_utc, "America/New_York", days=3
)
assert list(local_stats.keys()) == ["2025-02-24", "2025-02-25", "2025-02-26"]
for day in local_stats:
for msg_type in ["sms", "email"]:
for status in ["delivered", "failure", "pending", "requested"]:
assert local_stats[day][msg_type][status] == 0
def test_timestamp_in_future_time_zone():
stats_utc = {
"2025-02-25T01:00:00Z": {
"sms": {"delivered": 5, "failure": 0, "pending": 0, "requested": 5},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
}
}
class MockDateTime(datetime):
@classmethod
def now(cls, tz=None):
return cls(2025, 2, 25, 10, 0, 0, tzinfo=tz)
with patch("app.main.views.dashboard.datetime", MockDateTime):
local_stats = get_local_daily_stats_for_last_x_days(
stats_utc, "Asia/Shanghai", days=1
)
assert list(local_stats.keys()) == ["2025-02-25"]
assert local_stats["2025-02-25"]["sms"]["delivered"] == 5
def test_many_timestamps_one_local_day():
stats_utc = {
"2025-02-24T05:00:00Z": {
"sms": {"delivered": 2, "failure": 1, "pending": 0, "requested": 3},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
},
"2025-02-24T09:30:00Z": {
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
"email": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
},
"2025-02-24T16:59:59Z": {
"sms": {"delivered": 4, "failure": 0, "pending": 0, "requested": 4},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
},
}
class MockDateTime(datetime):
@classmethod
def now(cls, tz=None):
return cls(2025, 2, 24, 20, 0, 0, tzinfo=tz)
with patch("app.main.views.dashboard.datetime", MockDateTime):
local_stats = get_local_daily_stats_for_last_x_days(
stats_utc, "America/New_York", days=1
)
assert list(local_stats.keys()) == ["2025-02-24"]
assert local_stats["2025-02-24"]["sms"]["delivered"] == 7
assert local_stats["2025-02-24"]["sms"]["requested"] == 8
assert local_stats["2025-02-24"]["email"]["delivered"] == 2
assert local_stats["2025-02-24"]["email"]["requested"] == 2
def test_local_conversion_phoenix():
"""Test aggregator logic in Mountain Time, no DST (America/Phoenix)."""
stats_utc = {
"2025-02-25T01:00:00Z": {
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
},
"2025-02-25T12:00:00Z": {
"sms": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
"email": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
},
}
with patch("app.main.views.dashboard.datetime", wraps=datetime) as mock_dt:
mock_dt.now.return_value = datetime(2025, 2, 25, 12, 0, 0, tzinfo=ZoneInfo("America/Phoenix"))
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, "America/Phoenix", days=1)
assert len(local_stats) == 1
(day_key,) = local_stats.keys()
sms_delivered = local_stats[day_key]["sms"]["delivered"]
assert sms_delivered in (2, 3, 4)
def test_local_conversion_honolulu():
"""Test aggregator logic in Hawaii (Pacific/Honolulu)."""
stats_utc = {
"2025-02-25T03:00:00Z": {
"sms": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
"email": {"delivered": 0, "failure": 0, "pending": 0, "requested": 0},
},
"2025-02-25T21:00:00Z": {
"sms": {"delivered": 1, "failure": 0, "pending": 0, "requested": 1},
"email": {"delivered": 2, "failure": 0, "pending": 0, "requested": 2},
},
}
with patch("app.main.views.dashboard.datetime", wraps=datetime) as mock_dt:
mock_dt.now.return_value = datetime(2025, 2, 25, 12, 0, 0, tzinfo=ZoneInfo("Pacific/Honolulu"))
local_stats = get_local_daily_stats_for_last_x_days(stats_utc, "Pacific/Honolulu", days=1)
assert len(local_stats) == 1
(day_key,) = local_stats.keys()
total_requested = local_stats[day_key]["sms"]["requested"]
assert total_requested in (3, 1, 4)

View File

@@ -128,25 +128,12 @@ def test_sign_out_user(
# Check we are logged in
mocker.patch("app.job_api_client.get_jobs", return_value=MOCK_JOBS)
date_range = {"start_date": "2024-01-01", "days": 7}
mocker.patch(
"app.main.views.dashboard.get_daily_stats",
"app.service_api_client.get_service_message_ratio",
return_value={
date_range["start_date"]: {
"email": {"delivered": 0, "failure": 0, "requested": 0},
"sms": {"delivered": 0, "failure": 1, "requested": 1},
},
},
)
mocker.patch(
"app.main.views.dashboard.get_daily_stats_by_user",
return_value={
date_range["start_date"]: {
"email": {"delivered": 1, "failure": 0, "requested": 1},
"sms": {"delivered": 1, "failure": 0, "requested": 1},
},
"messages_remaining": 71919,
"messages_sent": 28081,
"total_message_limit": 100000,
},
)

View File

@@ -199,7 +199,7 @@ test('Fetches data and creates chart and table correctly', async () => {
const data = await fetchData('service');
expect(global.fetch).toHaveBeenCalledWith(`/services/${currentServiceId}/daily-stats.json`);
expect(global.fetch).toHaveBeenCalledWith(`/services/${currentServiceId}/daily-stats.json?timezone=UTC`);
expect(data).toEqual(mockResponse);
const labels = Object.keys(mockResponse).map(dateString => {

View File

@@ -22,7 +22,7 @@ Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
beforeEach(() => {
// Set up the DOM with the D3 script included
document.body.innerHTML = `
<div id="totalMessageChartContainer" data-sms-sent="100" data-sms-allowance-remaining="249900" style="width: 600px;">
<div id="totalMessageChartContainer" data-messages-sent="28081" data-messages-remaining="71919" style="width: 600px;">
<h1 id="chartTitle">Total Messages</h1>
<svg id="totalMessageChart"></svg>
</div>
@@ -76,8 +76,8 @@ test('Populates the accessible table correctly', () => {
expect(headers[1].textContent).toBe('Remaining');
const firstRowCells = rows[1].getElementsByTagName('td');
expect(firstRowCells[0].textContent).toBe('100');
expect(firstRowCells[1].textContent).toBe('249,900');
expect(firstRowCells[0].textContent).toBe('28,081');
expect(firstRowCells[1].textContent).toBe('71,919');
});
// Test to check if the chart title is correctly set
@@ -114,7 +114,7 @@ test('Chart resizes correctly on window resize', done => {
// Testing the tooltip
test('Tooltip displays on hover', () => {
document.body.innerHTML = `
<div id="totalMessageChartContainer" data-sms-sent="100" data-sms-allowance-remaining="249900" style="width: 600px;">
<div id="totalMessageChartContainer" data-messages-sent="100" data-messages-remaining="249900" style="width: 600px;">
<h1 id="chartTitle">Total Messages</h1>
<svg id="totalMessageChart"></svg>
</div>