Merge branch 'main' into 2171-clean-up-titles

This commit is contained in:
Beverly Nguyen
2025-02-19 09:05:16 -08:00
29 changed files with 587 additions and 371 deletions

View File

@@ -1024,3 +1024,7 @@ nav.nav {
font-size: units(3);
font-weight: bold;
}
.form-control-error {
border: 4px solid #b10e1e
}

View File

@@ -401,7 +401,9 @@ def get_job_partials(job):
)
if request.referrer is not None:
session["arrived_from_preview_page"] = ("check" in request.referrer) or ("help=0" in request.referrer)
session["arrived_from_preview_page"] = ("check" in request.referrer) or (
"help=0" in request.referrer
)
else:
session["arrived_from_preview_page"] = False

View File

@@ -116,6 +116,70 @@ def download_all_users():
return response
@main.route("/platform-admin/get-redis-report")
@user_is_platform_admin
def get_redis_report():
memory_info = redis_client.info("memory")
memory_used = memory_info.get("used_memory_human", "N/A")
max_memory = memory_info.get("maxmemory_human", "N/A")
if max_memory == "0B":
max_memory = "No set limit"
mem_fragmentation = memory_info.get("mem_fragmentation_ratio", "N/A")
frag_quality = "Swapping (bad)"
if mem_fragmentation >= 1.0:
frag_quality = "Healthy"
if mem_fragmentation > 1.5:
frag_quality = "Problematic"
if mem_fragmentation > 2.0:
frag_quality = "Severe fragmentation"
frag_note = ""
if mem_fragmentation > 2.0:
frag_note = "Use MEMORY PURGE.\nReplace multiple small keys with hashes.\nAvoid long keys.\nSet max_memory."
elif mem_fragmentation < 1.0:
frag_note = "Allocate more RAM.\nSet max_memory."
keys = redis_client.keys("*")
key_details = []
for key in keys:
key_type = redis_client.type(key).decode("utf-8")
ttl = redis_client.ttl(key)
ttl_str = "No Expiry" if ttl == -1 else f"{ttl} seconds"
key_details.append(
{"Key": key.decode("utf-8"), "Type": key_type, "TTL": ttl_str}
)
output = StringIO()
writer = csv.writer(
output,
)
writer.writerow(["Redis Report"])
writer.writerow([])
writer.writerow(["Memory"])
writer.writerow(["", "Memory Used", memory_used])
writer.writerow(["", "Max Memory", max_memory])
writer.writerow(["", "Memory Fragmentation Ratio", mem_fragmentation])
writer.writerow(["", "Memory Fragmentation Quality", frag_quality, frag_note])
writer.writerow([])
writer.writerow(["Keys Overview"])
writer.writerow(["", "TTL", "Type", "Key"])
for key_detail in key_details:
writer.writerow(
["", key_detail["TTL"], key_detail["Type"], key_detail["Key"][0:50]]
)
csv_data = output.getvalue()
# Create a direct download response with the CSV data and appropriate headers
response = Response(csv_data, content_type="text/csv; charset=utf-8")
response.headers["Content-Disposition"] = "attachment; filename=redis.csv"
return response
def is_over_threshold(number, total, threshold):
percentage = number / total * 100 if total else 0
return percentage > threshold

View File

@@ -68,11 +68,12 @@ def _get_access_token(code): # pragma: no cover
id_token = get_id_token(response_json)
nonce = id_token["nonce"]
nonce_key = f"login-nonce-{unquote(nonce)}"
stored_nonce = redis_client.get(nonce_key).decode("utf8")
if not os.getenv("NOTIFY_ENVIRONMENT") == "development":
stored_nonce = redis_client.get(nonce_key).decode("utf8")
if nonce != stored_nonce:
current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}")
abort(403)
if nonce != stored_nonce:
current_app.logger.error(f"Nonce Error: {nonce} != {stored_nonce}")
abort(403)
try:
access_token = response_json["access_token"]
@@ -112,7 +113,7 @@ def _do_login_dot_gov(): # $ pragma: no cover
verify_key = f"login-verify_email-{unquote(state)}"
verify_path = bool(redis_client.get(verify_key))
if not verify_path:
if not verify_path and not os.getenv("NOTIFY_ENVIRONMENT") == "development":
state_key = f"login-state-{unquote(state)}"
stored_state = unquote(redis_client.get(state_key).decode("utf8"))
if state != stored_state:

View File

@@ -6,17 +6,36 @@ from app.notify_client import NotifyAdminAPIClient
class BillingAPIClient(NotifyAdminAPIClient):
def get_monthly_usage_for_service(self, service_id, year):
return self.get(
monthly_usage = redis_client.get(f"monthly-usage-summary-{service_id}-{year}")
if monthly_usage is not None:
return json.loads(monthly_usage.decode("utf-8"))
result = self.get(
"/service/{0}/billing/monthly-usage".format(service_id),
params=dict(year=year),
)
redis_client.set(
f"monthly-usage-summary-{service_id}-{year}",
json.dumps(result),
ex=30,
)
return result
def get_annual_usage_for_service(self, service_id, year=None):
return self.get(
annual_usage = redis_client.get(f"yearly-usage-summary-{service_id}-{year}")
if annual_usage is not None:
return json.loads(annual_usage.decode("utf-8"))
result = self.get(
"/service/{0}/billing/yearly-usage-summary".format(service_id),
params=dict(year=year),
)
redis_client.set(
f"yearly-usage-summary-{service_id}-{year}",
json.dumps(result),
ex=30,
)
return result
def get_free_sms_fragment_limit_for_year(self, service_id, year=None):
frag_limit = redis_client.get(f"free-sms-fragment-limit-{service_id}-{year}")
if frag_limit is not None:
@@ -48,13 +67,28 @@ class BillingAPIClient(NotifyAdminAPIClient):
)
def get_data_for_billing_report(self, start_date, end_date):
return self.get(
x_start_date = str(start_date)
x_start_date = x_start_date.replace(" ", "_")
x_end_date = str(end_date)
x_end_date = x_end_date.replace(" ", "_")
billing_data = redis_client.get(
f"get-data-for-billing-report-{x_start_date}-{x_end_date}"
)
if billing_data is not None:
return json.loads(billing_data.decode("utf-8"))
result = self.get(
url="/platform-stats/data-for-billing-report",
params={
"start_date": str(start_date),
"end_date": str(end_date),
},
)
redis_client.set(
f"get-data-for-billing-report-{x_start_date}-{x_end_date}",
json.dumps(result),
ex=30,
)
return result
def get_data_for_volumes_by_service_report(self, start_date, end_date):
return self.get(

View File

@@ -1,3 +1,6 @@
import json
from app.extensions import redis_client
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
@@ -41,7 +44,7 @@ class NotificationApiClient(NotifyAdminAPIClient):
if job_id:
return method(
url="/service/{}/job/{}/notifications".format(service_id, job_id),
**kwargs
**kwargs,
)
else:
if limit_days is not None:
@@ -96,9 +99,20 @@ class NotificationApiClient(NotifyAdminAPIClient):
)
def get_notification_count_for_job_id(self, *, service_id, job_id):
return self.get(
counts = redis_client.get(
f"notification-count-for-job-id-{service_id}-{job_id}"
)
if counts is not None:
return json.loads(counts.decode("utf-8"))
result = self.get(
url="/service/{}/job/{}/notification_count".format(service_id, job_id)
)["count"]
)
redis_client.set(
f"notification-count-for-job-id-{service_id}-{job_id}",
json.dumps(result["count"]),
ex=30,
)
return result["count"]
notification_api_client = NotificationApiClient()

View File

@@ -14,10 +14,8 @@
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
{% block bodyStart %}
{% block extra_javascripts_before_body %}
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WX5NGWF"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
{% endblock %}
{% endblock %}
@@ -145,10 +143,8 @@
{% block bodyEnd %}
{% block extra_javascripts %}
{% endblock %}
<!--[if gt IE 8]><!-->
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
<script type="text/javascript" src="{{ asset_url('js/uswds.min.js') }}"></script>
<!--<![endif]-->
{% endblock %}
</body>
</html>

View File

@@ -34,7 +34,7 @@
attributes: params.errorMessage.attributes,
html: params.errorMessage.html,
text: params.errorMessage.text,
visuallyHiddenText: params.errorMessage.visuallyHiddenText
visuallyHiddenText: params.errorMessage.visuallyHiddenText,
}) | indent(2) | trim }}
{% endif %}
<input class="usa-input {%- if params.classes %} {{ params.classes }}{% endif %} {%- if params.errorMessage %} usa-input--error{% endif %}" id="{{ params.id }}" name="{{ params.name }}" type="{{ params.type | default('text') }}"
@@ -42,5 +42,7 @@
{%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %}
{%- if params.autocomplete %} autocomplete="{{ params.autocomplete}}"{% endif %}
{%- if params.pattern %} pattern="{{ params.pattern }}"{% endif %}
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}>
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}
{%- if params.required %} required{% endif %}
/>
</div>

View File

@@ -16,19 +16,9 @@
placeholder=''
) %}
<div
class="form-group{% if field.errors %} form-group-error{% endif %} {{ extra_form_group_classes }}"
class="usa-form-group{% if field.errors %} usa-form-group--error{% endif %} {{ extra_form_group_classes }}"
data-module="{% if autofocus %}autofocus{% elif colour_preview %}colour-preview{% endif %}"
>
{% if field.errors %}
<div class="usa-alert usa-alert--error edit-textbox-error-mt" role="alert">
<div class="usa-alert__body">
<h4 class="usa-alert__heading">Error message</h4>
<p class="usa-alert__text" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}">
{% if not safe_error_message %}{{ field.errors[0] }}{% else %}{{ field.errors[0]|safe }}{% endif %}
</p>
</div>
</div>
{% endif %}
<label class="usa-label" for="{{ field.name }}">
{% if label %}
{{ label }}
@@ -41,6 +31,12 @@
{{ hint }}
</div>
{% endif %}
{% if field.errors %}
<span id="{{ field.name}}-error" class="usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}" tabindex="-1" aria-live="assertive" role="alert">
<span class="usa-sr-only">Error:</span>
{% if not safe_error_message %}{{ field.errors[0] }}{% else %}{{ field.errors[0]|safe }}{% endif %}
</span>
{% endif %}
{%
if highlight_placeholders or autosize
%}
@@ -59,6 +55,8 @@
data_highlight_placeholders='true' if highlight_placeholders else 'false',
rows=rows|string,
placeholder=placeholder,
aria_describedby=field.name+"-error",
required='required' if required else None,
**kwargs
) }}
{% if suffix %}

View File

@@ -32,6 +32,8 @@
<div class="tablet:grid-col-9 mobile-lg:grid-col-12">
{{ form.name(param_extensions={
"extra_form_group_classes": "margin-bottom-2",
"id": "name",
"required": True,
"hint": {"text": "Your recipients will not see this"}
}) }}
{{ textbox(
@@ -41,7 +43,8 @@
hint=content_hint,
rows=5,
extra_form_group_classes='margin-bottom-1',
placeholder='Edit me! Check out the Personalization section below for details on cool ((stuff)) you can do with your messages!'
placeholder='Edit me! Check out the Personalization section below for details on cool ((stuff)) you can do with your messages!',
required=True
) }}
{% if current_user.platform_admin %}
{{ form.process_type }}

View File

@@ -34,5 +34,8 @@
<p>
<a class="usa-link" href="{{ url_for('main.download_all_users') }}">Download All Users</a>
</p>
<p>
<a class="usa-link" href="{{ url_for('main.get_redis_report') }}">Get Redis Report</a>
</p>
{% endblock %}

View File

@@ -37,8 +37,8 @@
data_kwargs={'force-focus': True}
) %}
<div class="grid-row">
<div class="grid-col-12 {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}" aria-live="polite" role="alert">
{{ form.placeholder_value(param_extensions={"classes": ""}) }}
<div class="grid-col-12 {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}">
{{ form.placeholder_value(param_extensions={"classes": "", "id": "phone-number"}) }}
</div>
{% if skip_link or link_to_upload %}
<div class="grid-col-12 margin-top-1">

View File

@@ -90,8 +90,4 @@
{% endif %}
</div>
<!--<div class="">
{{ copy_to_clipboard(template.id, name="Template ID", thing='template ID') }}
</div>-->
{% endblock %}