diff --git a/.ds.baseline b/.ds.baseline
index b84a53351..6d2034588 100644
--- a/.ds.baseline
+++ b/.ds.baseline
@@ -407,16 +407,6 @@
"is_secret": false
}
],
- "app/main/views/sign_in.py": [
- {
- "type": "Private Key",
- "filename": "app/main/views/sign_in.py",
- "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9",
- "is_verified": false,
- "line_number": 27,
- "is_secret": false
- }
- ],
"app/templates/new/components/head.html": [
{
"type": "Base64 High Entropy String",
@@ -702,5 +692,5 @@
}
]
},
- "generated_at": "2024-07-11T16:37:23Z"
+ "generated_at": "2024-07-24T14:13:02Z"
}
diff --git a/app/assets/javascripts/activityChart.js b/app/assets/javascripts/activityChart.js
index fd9af8fd6..56944ed25 100644
--- a/app/assets/javascripts/activityChart.js
+++ b/app/assets/javascripts/activityChart.js
@@ -190,12 +190,11 @@
var socketConnect = type === 'service' ? 'daily_stats_update' : 'daily_stats_by_user_update';
socket.on('connect', function () {
- const userId = ctx.getAttribute('data-service-id'); // Assuming user ID is the same as service ID
+ //const userId = ctx.getAttribute('data-service-id'); // Assuming user ID is the same as service ID
socket.emit(eventType);
});
socket.on(socketConnect, function(data) {
- console.log('Received data:', data); // Log the received data
var labels = [];
var deliveredData = [];
diff --git a/app/assets/javascripts/dataVisualization.js b/app/assets/javascripts/dataVisualization.js
deleted file mode 100644
index cdf50193a..000000000
--- a/app/assets/javascripts/dataVisualization.js
+++ /dev/null
@@ -1,71 +0,0 @@
-// (function (window) {
-
-// // Dummy data (replace with API data)
-// const data = {
-// messageStats: {
-// totalMessages: 1000,
-// delivered: 520,
-// pending: 280,
-// failed: 100
-// },
-// dailyUsage: {
-// dailyUsage: 20,
-// dailyUsageLimit: 100
-// },
-// yearlyUsage: {
-// yearlyUsage: 2000,
-// yearlyUsageLimit: 250000
-// }
-// };
-
-// // Update total messages progress bar percentages
-// const messageStats = data.messageStats;
-
-// const messageBars = ["delivered", "pending", "failed"];
-// for (const bar of messageBars) {
-// const percentage = messageStats[bar] / messageStats.totalMessages * 100;
-// const elementId = `${bar}-bar`;
-// const element = document.getElementById(elementId);
-// element.style.width = `${parseFloat(percentage)}%`;
-// }
-
-// // Update total messages progress bar percentages
-// const dailyUsage = data.dailyUsage;
-// // const dailyUsageLimit = dailyUsage.dailyUsageLimit;
-// const yearlyUsage = data.yearlyUsage;
-// // const yearlyUsageLimit = yearlyUsage.yearlyUsageLimit;
-
-// function updateUsageBar(elementId, dailyUsage, dailyUsageLimit, yearlyUsage, yearlyUsageLimit) {
-// // Ensure element exists
-// if (!elementId || !document.getElementById(elementId)) {
-// console.error(`Element with ID "${elementId}" not found`);
-// return;
-// }
-
-// // Calculate percentage
-// const percentage = dailyUsage / dailyUsageLimit * 100;
-
-// // Update bar width
-// const element = document.getElementById(elementId);
-// element.style.width = `${parseFloat(percentage)}%`;
-// }
-
-// // Update usage bars
-// // updateUsageBar("dailyUsage-bar", dailyUsage.dailyUsage, dailyUsageLimit);
-// // updateUsageBar("dailyUsageRemaining-bar", dailyUsageLimit - dailyUsage.dailyUsage, dailyUsageLimit);
-// // updateUsageBar("yearlyUsage-bar", yearlyUsage.yearlyUsage, yearlyUsageLimit);
-// // updateUsageBar("yearlyUsageRemaining-bar", yearlyUsageLimit - yearlyUsage.yearlyUsage, yearlyUsageLimit);
-
-// // Update total messages legend values
-// document.getElementById("total-value").innerText = `Total: ${messageStats.totalMessages} messages`;
-// document.getElementById("delivered-value").innerText = `Delivered: ${messageStats.delivered}`;
-// document.getElementById("pending-value").innerText = `Pending: ${messageStats.pending}`;
-// document.getElementById("failed-value").innerText = `Failed: ${messageStats.failed}`;
-
-// // Update usage legend values
-// // document.getElementById("daily-usage-value").innerText = `Daily usage: ${dailyUsage.dailyUsage}`;
-// // document.getElementById("daily-remaining-value").innerText = `Remaining messages: ${dailyUsage.dailyUsageLimit - dailyUsage.dailyUsage}`;
-// // document.getElementById("yearly-usage-value").innerText = `Yearly usage: ${yearlyUsage.yearlyUsage}`;
-// // document.getElementById("yearly-remaining-value").innerText = `Remaining messages: ${yearlyUsage.yearlyUsageLimit - yearlyUsage.yearlyUsage}`;
-
-// })(window);
diff --git a/app/assets/sass/uswds/_legacy-styles.scss b/app/assets/sass/uswds/_legacy-styles.scss
index 8daf10c9c..85da17321 100644
--- a/app/assets/sass/uswds/_legacy-styles.scss
+++ b/app/assets/sass/uswds/_legacy-styles.scss
@@ -344,11 +344,3 @@ h2.recipient-list {
}
}
}
-
-// .js-focus-style {
-// outline: 3px solid color("blue-40v");
-// box-shadow: 0 0 0 7px color("blue-40v");
-// *:focus {
-// outline: none;
-// }
-// }
diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss
index 638f90743..9606c5981 100644
--- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss
+++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss
@@ -332,6 +332,16 @@ td.table-empty-message {
bottom: 0;
}
+@media (max-width: units('desktop-lg')) {
+ .table-overflow-x-auto {
+ overflow-x: auto;
+ table {
+ min-width: 768px;
+ }
+ }
+}
+
+
// Dashboard
.dashboard {
@@ -381,9 +391,6 @@ td.table-empty-message {
background-image: url(../img/material-icons/description.svg);
}
}
- .table-wrapper {
- overflow-x: auto;
- }
}
.dashboard-table {
diff --git a/app/main/forms.py b/app/main/forms.py
index afec80f9f..90a4409fc 100644
--- a/app/main/forms.py
+++ b/app/main/forms.py
@@ -50,6 +50,7 @@ from app.main.validators import (
CommonlyUsedPassword,
CsvFileValidator,
DoesNotStartWithDoubleZero,
+ FieldCannotContainComma,
LettersNumbersSingleQuotesFullStopsAndUnderscoresOnly,
MustContainAlphanumericCharacters,
NoCommasInPlaceHolders,
@@ -1650,7 +1651,11 @@ def get_placeholder_form_instance(
) # TODO: replace with us_mobile_number
else:
field = GovukTextInputField(
- placeholder_name, validators=[DataRequired(message="Cannot be empty")]
+ placeholder_name,
+ validators=[
+ DataRequired(message="Cannot be empty"),
+ FieldCannotContainComma(),
+ ],
)
PlaceholderForm.placeholder_value = field
diff --git a/app/main/validators.py b/app/main/validators.py
index 4dd04017b..1dfc97c48 100644
--- a/app/main/validators.py
+++ b/app/main/validators.py
@@ -161,6 +161,15 @@ class DoesNotStartWithDoubleZero:
raise ValidationError(self.message)
+class FieldCannotContainComma:
+ def __init__(self, message="Cannot contain a comma"):
+ self.message = message
+
+ def __call__(self, form, field):
+ if field.data and "," in field.data:
+ raise ValidationError(self.message)
+
+
class MustContainAlphanumericCharacters:
regex = re.compile(r".*[a-zA-Z0-9].*[a-zA-Z0-9].*")
diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py
index 1021262b9..366182a82 100644
--- a/app/main/views/dashboard.py
+++ b/app/main/views/dashboard.py
@@ -1,5 +1,4 @@
import calendar
-from collections import defaultdict
from datetime import datetime
from functools import partial
from itertools import groupby
@@ -13,7 +12,6 @@ from app import (
billing_api_client,
current_service,
job_api_client,
- notification_api_client,
service_api_client,
socketio,
template_statistics_client,
@@ -94,18 +92,9 @@ def service_dashboard(service_id):
sms_allowance_remaining = usage_data["sms_allowance_remaining"]
job_response = job_api_client.get_jobs(service_id)["data"]
- notifications_response = notification_api_client.get_notifications_for_service(
- service_id
- )["notifications"]
service_data_retention_days = 7
- aggregate_notifications_by_job = defaultdict(list)
- for notification in notifications_response:
- job_id = notification.get("job", {}).get("id", None)
- if job_id:
- aggregate_notifications_by_job[job_id].append(notification)
-
- job_and_notifications = [
+ jobs = [
{
"job_id": job["id"],
"time_left": get_time_left(job["created_at"]),
@@ -116,20 +105,20 @@ def service_dashboard(service_id):
".view_job", service_id=current_service.id, job_id=job["id"]
),
"created_at": job["created_at"],
- "processing_finished": job["processing_finished"],
- "processing_started": job["processing_started"],
+ "processing_finished": job.get("processing_finished"),
+ "processing_started": job.get("processing_started"),
"notification_count": job["notification_count"],
"created_by": job["created_by"],
- "notifications": aggregate_notifications_by_job.get(job["id"], []),
+ "template_name": job["template_name"],
+ "original_file_name": job["original_file_name"],
}
for job in job_response
- if aggregate_notifications_by_job.get(job["id"], [])
]
return render_template(
"views/dashboard/dashboard.html",
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
partials=get_dashboard_partials(service_id),
- job_and_notifications=job_and_notifications,
+ jobs=jobs,
service_data_retention_days=service_data_retention_days,
sms_sent=sms_sent,
sms_allowance_remaining=sms_allowance_remaining,
diff --git a/app/main/views/register.py b/app/main/views/register.py
index 761582b3d..19187a47c 100644
--- a/app/main/views/register.py
+++ b/app/main/views/register.py
@@ -230,7 +230,12 @@ def set_up_your_profile():
debug_msg(
f"Added user {usr.email_address} to service {invite_data['service_id']}"
)
- return redirect(url_for("main.show_accounts_or_dashboard"))
+ # notify-admin-1766
+ # redirect new users to templates area of new service instead of dashboard
+ service_id = invite_data["service_id"]
+ url = url_for(".service_dashboard", service_id=service_id)
+ url = f"{url}/templates"
+ return redirect(url)
elif login_gov_error:
current_app.logger.error(f"login.gov error: {login_gov_error}")
diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py
index 9f089fe42..2440473fb 100644
--- a/app/main/views/sign_in.py
+++ b/app/main/views/sign_in.py
@@ -29,12 +29,18 @@ from notifications_utils.url_safe_token import generate_token
def _reformat_keystring(orig):
- new_keystring = orig.replace("-----BEGIN PRIVATE KEY-----", "")
- new_keystring = new_keystring.replace("-----END PRIVATE KEY-----", "")
+ private_key = "PRIVATE " # pragma: allowlist secret
+ private_key = f"{private_key} KEY"
+ new_keystring = orig.replace(f"-----BEGIN {private_key}-----", "")
+ new_keystring = new_keystring.replace(f"-----END {private_key}-----", "")
new_keystring = new_keystring.strip()
new_keystring = new_keystring.replace(" ", "\n")
new_keystring = "\n".join(
- ["-----BEGIN PRIVATE KEY-----", new_keystring, "-----END PRIVATE KEY-----"]
+ [
+ f"-----BEGIN {private_key}-----",
+ new_keystring,
+ f"-----END {private_key}-----",
+ ]
)
new_keystring = f"{new_keystring}\n"
return new_keystring
@@ -65,7 +71,9 @@ def _get_access_token(code, state):
response = requests.post(url, headers=headers)
if response.json().get("access_token") is None:
# Capture the response json here so it hopefully shows up in error reports
- current_app.logger.error(f"Error when getting access token {response.json()}")
+ current_app.logger.error(
+ f"Error when getting access token {response.json()} #notify-admin-1505"
+ )
raise KeyError(f"'access_token' {response.json()}")
access_token = response.json()["access_token"]
return access_token
@@ -90,7 +98,9 @@ def _do_login_dot_gov():
login_gov_error = request.args.get("error")
if login_gov_error:
- current_app.logger.error(f"login.gov error: {login_gov_error}")
+ current_app.logger.error(
+ f"login.gov error: {login_gov_error} #notify-admin-1505"
+ )
raise Exception(f"Could not login with login.gov {login_gov_error}")
elif code and state:
@@ -100,12 +110,15 @@ def _do_login_dot_gov():
user_email, user_uuid = _get_user_email_and_uuid(access_token)
if not is_gov_user(user_email):
current_app.logger.error(
- "invited user has a non-government email address."
+ "invited user has a non-government email address. #notify-admin-1505"
)
flash("You must use a government email address.")
abort(403)
redirect_url = request.args.get("next")
user = user_api_client.get_user_by_uuid_or_email(user_uuid, user_email)
+ current_app.logger.info(
+ f"Retrieved user {user['id']} from db #notify-admin-1505"
+ )
# Check if the email needs to be revalidated
is_fresh_email = is_less_than_days_ago(
@@ -115,9 +128,10 @@ def _do_login_dot_gov():
return verify_email(user, redirect_url)
usr = User.from_email_address(user["email_address"])
+ current_app.logger.info(f"activating user {usr.id} #notify-admin-1505")
activate_user(usr.id)
except BaseException as be: # noqa B036
- current_app.logger.error(be)
+ current_app.logger.error(f"Error signing in: {be} #notify-admin-1505 ")
error(401)
return redirect(url_for("main.show_accounts_or_dashboard", next=redirect_url))
diff --git a/app/main/views/verify.py b/app/main/views/verify.py
index fbe510643..be0a0532d 100644
--- a/app/main/views/verify.py
+++ b/app/main/views/verify.py
@@ -38,6 +38,7 @@ def verify_email(token):
current_app.config["EMAIL_EXPIRY_SECONDS"],
)
except SignatureExpired:
+ current_app.logger.error("Email link expired #notify-admin-1505")
flash(
"The link in the email we sent you has expired. We've sent you a new one."
)
@@ -50,6 +51,9 @@ def verify_email(token):
abort(404)
if user.is_active:
+ current_app.logger.error(
+ f"User is using an invite link but is already logged in {user.id} #notify-admin-1505"
+ )
flash("That verification link has expired.")
return redirect(url_for("main.sign_in"))
@@ -59,6 +63,7 @@ def verify_email(token):
user.send_verify_code()
session["user_details"] = {"email": user.email_address, "id": user.id}
+ current_app.logger.info(f"Email verified for user {user.id} #notify-admin-1505")
return redirect(url_for("main.verify"))
@@ -78,5 +83,7 @@ def activate_user(user_id):
return redirect(url_for("main.organization_dashboard", org_id=organization_id))
else:
activated_user = user.activate()
+ current_app.logger.info(f"Activated user {user.id} #notify-admin-1505")
activated_user.login()
+ current_app.logger.info(f"Logged in user {user.id} #notify-admin-1505")
return redirect(url_for("main.add_service", first="first"))
diff --git a/app/notify_client/__init__.py b/app/notify_client/__init__.py
index f78573090..5f65e85c6 100644
--- a/app/notify_client/__init__.py
+++ b/app/notify_client/__init__.py
@@ -58,6 +58,10 @@ class NotifyAdminAPIClient(BaseAPIClient):
def check_inactive_user(self, *args):
still_signing_in = False
+
+ # TODO clean up and add testing etc.
+ # We really should be checking for exact matches
+ # and we only want to check the first arg
for arg in args:
arg = str(arg)
if (
@@ -66,6 +70,7 @@ class NotifyAdminAPIClient(BaseAPIClient):
or "/activate" in arg
or "/email-code" in arg
or "/verify/code" in arg
+ or "/user" in arg
):
still_signing_in = True
diff --git a/app/templates/partials/jobs/notifications.html b/app/templates/partials/jobs/notifications.html
index 2b5cd8f77..22173e6bd 100644
--- a/app/templates/partials/jobs/notifications.html
+++ b/app/templates/partials/jobs/notifications.html
@@ -22,7 +22,7 @@
{% else %}
{% if notifications %}
-
+
{% endif %}
{% if job.still_processing %}
diff --git a/app/templates/partials/notifications/notifications.html b/app/templates/partials/notifications/notifications.html
index aae3e214f..e155fdb1a 100644
--- a/app/templates/partials/notifications/notifications.html
+++ b/app/templates/partials/notifications/notifications.html
@@ -1,8 +1,8 @@
{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading, notification_status_field %}
{% from "components/page-footer.html" import page_footer %}
-
-
+
+
{% call(item, row_number) list_table(
[notification],
diff --git a/app/templates/views/activity/notifications.html b/app/templates/views/activity/notifications.html
index 9ea9b3f81..f099e7f53 100644
--- a/app/templates/views/activity/notifications.html
+++ b/app/templates/views/activity/notifications.html
@@ -5,7 +5,7 @@
{% if notifications %}
-
+
{% endif %}
{% call(item, row_number) list_table(
diff --git a/app/templates/views/api/callbacks.html b/app/templates/views/api/callbacks.html
index 2f8cfbf2a..e0c83cc19 100644
--- a/app/templates/views/api/callbacks.html
+++ b/app/templates/views/api/callbacks.html
@@ -14,8 +14,8 @@
{% block maincolumn_content %}
{{ page_header('Callbacks') }}
-
-
+
+
{% call mapping_table(
caption='General',
field_headings=['Label', 'Value', 'Action'],
diff --git a/app/templates/views/check/preview.html b/app/templates/views/check/preview.html
index 32b158013..2664cbe27 100644
--- a/app/templates/views/check/preview.html
+++ b/app/templates/views/check/preview.html
@@ -41,7 +41,7 @@
-