Merge branch 'main' of https://github.com/GSA/notifications-admin into sms-allowance-dashboardbug

This commit is contained in:
Andrew Shumway
2024-02-07 09:38:43 -07:00
10 changed files with 379 additions and 81 deletions

View File

@@ -21,7 +21,6 @@ from flask_login import LoginManager, current_user
from flask_talisman import Talisman
from flask_wtf import CSRFProtect
from flask_wtf.csrf import CSRFError
from govuk_frontend_jinja.flask_ext import init_govuk_frontend
from itsdangerous import BadSignature
from notifications_python_client.errors import HTTPError
from notifications_utils import logging, request_helper
@@ -114,6 +113,7 @@ from app.notify_client.template_statistics_api_client import template_statistics
from app.notify_client.upload_api_client import upload_api_client
from app.notify_client.user_api_client import user_api_client
from app.url_converters import SimpleDateTypeConverter, TemplateTypeConverter
from app.utils.govuk_frontend_jinja.flask_ext import init_govuk_frontend
login_manager = LoginManager()
csrf = CSRFProtect()

View File

@@ -90,7 +90,7 @@ def view_notification(service_id, notification_id, error_message=None):
partials=get_single_notification_partials(notification),
created_by=notification.get("created_by"),
created_at=notification["created_at"],
updated_at=notification["updated_at"],
updated_at=notification["sent_at"],
help=get_help_argument(),
notification_id=notification["id"],
can_receive_inbound=(current_service.has_permission("inbound_sms")),

View File

@@ -172,7 +172,7 @@
<p class="status-hint margin-0 width-card ">
{{ notification.status|format_notification_status_as_time(
notification.created_at|format_datetime_short,
(notification.updated_at or notification.created_at)|format_datetime_short
(notification.sent_at or notification.created_at)|format_datetime_short
) }}
</p>
{% if displayed_on_single_line %}</span>{% endif %}

View File

@@ -0,0 +1 @@
from .templates import Environment # noqa

View File

@@ -0,0 +1,30 @@
from flask.templating import Environment as FlaskEnvironment
from jinja2 import select_autoescape
from app.utils.govuk_frontend_jinja.templates import Environment as NunjucksEnvironment
from app.utils.govuk_frontend_jinja.templates import (
NunjucksExtension,
NunjucksUndefined,
)
class Environment(NunjucksEnvironment, FlaskEnvironment):
pass
def init_govuk_frontend(app):
"""Use the govuk_frontend_jinja Jinja environment in a Flask app
>>> from flask import Flask
>>> app = Flask("cheeseshop_service")
>>> init_govuk_frontend(app)
"""
app.jinja_environment = Environment
app.select_jinja_autoescape = select_autoescape(
("html", "htm", "xml", "xhtml", "njk")
)
jinja_options = app.jinja_options.copy()
jinja_options["extensions"].append(NunjucksExtension)
jinja_options["undefined"] = NunjucksUndefined
app.jinja_options = jinja_options
return app

View File

@@ -0,0 +1,292 @@
import builtins
import os.path as path
import re
from collections.abc import Sized
import jinja2
import jinja2.ext
from jinja2.lexer import Token
from markupsafe import Markup
def njk_to_j2(template):
# Some component templates (such as radios) use `items` as the key of
# an object element. However `items` is also the name of a dictionary
# method in Python, and Jinja2 will prefer to return this attribute
# over the dict item. Handle specially.
template = re.sub(r"\.items\b", ".items__njk", template)
# Some component templates (such as radios) append the loop index to a
# string. As the loop index is an integer this causes a TypeError in
# Python. Jinja2 has an operator `~` for string concatenation that
# converts integers to strings.
template = template.replace("+ loop.index", "~ loop.index")
# The Character Count component in version 3 concatenates the word count
# with the hint text. As the word count is an integer this causes a
# TypeError in Python. Jinja2 has an operator `~` for string
# concatenation that converts integers to strings.
template = template.replace(
"+ (params.maxlength or params.maxwords) +",
"~ (params.maxlength or params.maxwords) ~",
)
# Nunjucks uses elseif, Jinja uses elif
template = template.replace("elseif", "elif")
# Some component templates (such as input) call macros with params as
# an object which has unqoted keys. This causes Jinja to silently
# ignore the values.
template = re.sub(
r"""^([ ]*)([^ '"#\r\n:]+?)\s*:""", r"\1'\2':", template, flags=re.M
)
# govukFieldset can accept a call block argument, however the Jinja
# compiler does not detect this as the macro body is included from
# the template file. A workaround is to patch the declaration of the
# macro to include an explicit caller argument.
template = template.replace(
"macro govukFieldset(params)", "macro govukFieldset(params, caller=none)"
)
# Many components feature an attributes field, which is supposed to be
# a dictionary. In the template for these components, the keys and values
# are iterated. In Python, the default iterator for a dict is .keys(), but
# we want .items().
# This only works because our undefined implements .items()
# We've tested this explicitly with: govukInput, govukCheckbox, govukTable,
# govukSummaryList
template = re.sub(
r"for attribute, value in (params|item|cell|action).attributes",
r"for attribute, value in \1.attributes.items()",
template,
flags=re.M,
)
# Some templates try to set a variable in an outer block, which is not
# supported in Jinja. We create a namespace in those templates to get
# around this.
template = re.sub(
r"""^([ ]*)({% set describedBy =( params.*describedBy if params.*describedBy else)? "" %})""",
r"\1{%- set nonlocal = namespace() -%}\n\1\2",
template,
flags=re.M,
)
# Change any references to describedBy to be nonlocal.describedBy,
# unless describedBy is a dictionary key (i.e. quoted or dotted).
template = re.sub(r"""(?<!['".])describedBy""", r"nonlocal.describedBy", template)
# govukSummaryList
template = re.sub(
r"""^([ ]*)({% set anyRowHasActions = false %})""",
r"\1{%- set nonlocal = namespace() -%}\n\1\2",
template,
flags=re.M,
)
template = re.sub(
r"""(?<!['".])anyRowHasActions""", r"nonlocal.anyRowHasActions", template
)
# govukRadios and govukCheckboxes
# Since both of these templates set describedBy before isConditional, we can use
# the existing nonlocal.
template = re.sub(
r"""(?<!['".])isConditional""", r"nonlocal.isConditional", template
)
# Issue#16: some component templates test the length of an array by trying
# to get an attribute `.length`. We need to handle this specially because
# .length isn't a thing in python
template = re.sub(r"\.length\b", ".length__njk", template)
# see `indent_njk`
template = re.sub(re.escape("| indent") + r"\b", "| indent_njk", template)
return template
def indent_njk(s, width=4, first=False, blank=False, indentfirst=None):
"""Return a copy of the string with each line indented by 4 spaces."""
# Copied from
# https://github.com/pallets/jinja/blob/a2f5e2c7972c4d5148c1c75c724e24950d8605bc/jinja2/filters.py#L536-L580
# to include an unreleased fix for https://github.com/pallets/jinja/pull/826
# which causes the file upload component to escape HTML markup.
# TODO: Remove once jinja2 2.11 is released and in use.
if indentfirst is not None:
first = indentfirst
indention = " " * width
newline = "\n"
if isinstance(s, Markup):
indention = Markup(indention)
newline = Markup(newline)
s += newline # this quirk is necessary for splitlines method
if blank:
rv = (newline + indention).join(s.splitlines())
else:
lines = s.splitlines()
rv = lines.pop(0)
if lines:
rv += newline + newline.join(
indention + line if line else line for line in lines
)
if first:
rv = indention + rv
return rv
class NunjucksExtension(jinja2.ext.Extension):
def filter_stream(self, stream):
if stream.filename and stream.filename.endswith(".njk"):
return self.filter_njk_stream(stream)
else:
return stream
def filter_njk_stream(self, stream):
for token in stream:
# patch strict equality operator `===`
if token.test("eq:==") and stream.current.test("assign:="):
yield Token(token.lineno, "name", "is")
yield Token(token.lineno, "name", "sameas")
stream.skip(1)
else:
yield token
def preprocess(self, source, name, filename=None):
if filename and filename.endswith(".njk"):
return njk_to_j2(source)
else:
return source
class NunjucksUndefined(jinja2.runtime.Undefined):
__slots__ = ()
# copied from https://github.com/pallets/jinja/commit/19133d40593ced72eb28e230588abcc70d8b9f82
def __getattr__(self, _):
"""Make undefined that is chainable, where both
__getattr__ and __getitem__ return itself rather than
raising an :exc:`UndefinedError`:
>>> foo = ChainableUndefined(name='foo')
>>> str(foo.bar['baz'])
''
>>> foo.bar['baz'] + 42
Traceback (most recent call last):
...
jinja2.exceptions.UndefinedError: 'foo' is undefined
"""
return self
__getitem__ = __getattr__
# Allow treating undefined as an (empty) dictionary.
# This works because Undefined is an iterable.
def items(self):
return self
# Allow escaping with Markup. This is required when
# autoescape is enabled. Debugging this issue was
# annoying; the error messages were not clear as to
# the cause of the issue (see upstream pull request
# for info https://github.com/pallets/jinja/pull/1047)
def __html__(self):
return str(self)
# attempt to behave a bit like js's `undefined` when concatenation is attempted
def __add__(self, other):
if isinstance(other, str):
return "undefined" + other
return super().__add__(other)
def __radd__(self, other):
if isinstance(other, str):
return other + "undefined"
return super().__radd__(other)
class NunjucksCodeGenerator(jinja2.compiler.CodeGenerator):
def visit_CondExpr(self, node, frame):
if not (self.filename or "").endswith(".njk"):
return super().visit_CondExpr(node, frame)
# else our replacement, which is based on that in
# https://github.com/pallets/jinja/blob/c4c4088945a2c12535f539be7f5453b9ca94666c/jinja2/compiler.py#L1613
def write_expr2():
if node.expr2 is not None:
return self.visit(node.expr2, frame)
# rather than complaining about a missing else
# clause we just assume it to be the empty
# string for nunjucks compatibility
return self.write('""')
self.write("(")
self.visit(node.expr1, frame)
self.write(" if ")
self.visit(node.test, frame)
self.write(" else ")
write_expr2()
self.write(")")
_njk_signature = "__njk"
_builtin_function_or_method_type = type({}.keys)
class Environment(jinja2.Environment):
code_generator_class = NunjucksCodeGenerator
def __init__(self, *args, **kwargs):
kwargs.setdefault("extensions", [NunjucksExtension])
kwargs.setdefault("undefined", NunjucksUndefined)
super().__init__(*args, **kwargs)
self.filters["indent_njk"] = indent_njk
def join_path(self, template, parent):
"""Enable the use of relative paths in template import statements"""
if template.startswith(("./", "../")):
return path.normpath(path.join(path.dirname(parent), template))
else:
return template
def _handle_njk(method_name):
def inner(self, obj, argument):
if isinstance(argument, str) and argument.endswith(_njk_signature):
# a njk-originated access will always be assuming a dict lookup before an attr
final_method_name = "getitem"
final_argument = argument[: -len(_njk_signature)]
else:
final_argument = argument
final_method_name = method_name
# pleasantly surprised that super() works in this context
retval = builtins.getattr(super(), final_method_name)(obj, final_argument)
if (
argument == f"length{_njk_signature}"
and isinstance(retval, jinja2.runtime.Undefined)
and isinstance(obj, Sized)
):
return len(obj)
if (
isinstance(argument, str)
and argument.endswith(_njk_signature)
and isinstance(retval, _builtin_function_or_method_type)
):
# the lookup has probably gone looking for attributes and found a builtin method. because
# any njk-originated lookup will have been made to prefer dict lookups over attributes, we
# can be fairly sure there isn't a dict key matching this - so we should just call this a
# failure.
return self.undefined(obj=obj, name=final_argument)
return retval
return inner
getitem = _handle_njk("getitem")
getattr = _handle_njk("getattr")

106
poetry.lock generated
View File

@@ -529,47 +529,56 @@ toml = ["tomli"]
[[package]]
name = "cryptography"
version = "41.0.7"
version = "42.0.2"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
{file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"},
{file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"},
{file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"},
{file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"},
{file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"},
{file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"},
{file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"},
{file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"},
{file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"},
{file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"},
{file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"},
{file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"},
{file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"},
{file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"},
{file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"},
{file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"},
{file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"},
{file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"},
{file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"},
{file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"},
{file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"},
{file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"},
{file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"},
{file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"},
{file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"},
{file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"},
{file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"},
{file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"},
{file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"},
{file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"},
{file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"},
]
[package.dependencies]
cffi = ">=1.12"
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
nox = ["nox"]
pep8test = ["black", "check-sdist", "mypy", "ruff"]
pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
[[package]]
@@ -929,28 +938,6 @@ files = [
[package.dependencies]
requests = "*"
[[package]]
name = "govuk-frontend-jinja"
version = "0.5.8-alpha"
description = "Tools to use the GOV.UK Design System with Jinja-powered Python apps"
optional = false
python-versions = "*"
files = []
develop = false
[package.dependencies]
jinja2 = "*"
[package.extras]
dev = ["pytest", "pytest-flakes", "pytest-helpers-namespace"]
flask = ["Flask"]
[package.source]
type = "git"
url = "https://github.com/alphagov/govuk-frontend-jinja.git"
reference = "v0.5.8-alpha"
resolved_reference = "15845e4cca3a05df72c6e13ec6a7e35acc682f52"
[[package]]
name = "greenlet"
version = "3.0.1"
@@ -1428,16 +1415,6 @@ files = [
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
@@ -1700,7 +1677,7 @@ requests = ">=2.0.0"
[[package]]
name = "notifications-utils"
version = "0.2.7"
version = "0.2.8"
description = ""
optional = false
python-versions = ">=3.9,<3.12"
@@ -1718,7 +1695,7 @@ certifi = "^2023.7.22"
cffi = "^1.16.0"
charset-normalizer = "^3.1.0"
click = "^8.1.3"
cryptography = "^41.0.6"
cryptography = "^42.0.0"
flask = "^2.3.2"
flask-redis = "^0.4.0"
geojson = "^3.0.1"
@@ -1752,7 +1729,7 @@ werkzeug = "^3.0.1"
type = "git"
url = "https://github.com/GSA/notifications-utils.git"
reference = "HEAD"
resolved_reference = "b6cee72f45dbcd48b59447fa08bbac59e15a7b98"
resolved_reference = "a48171e865eb83cf29c75751be7369f396cbe3e2"
[[package]]
name = "numpy"
@@ -2476,7 +2453,6 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@@ -3088,4 +3064,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<3.12"
content-hash = "94e6fe2e143f12eaa31f01b5a9206012a6a48908d98daa9fa2f531e986e4a6d3"
content-hash = "be46f8e90d6213db5c54d62c3a364c0a2a7e50f5c8e5007ad662067a6191c51f"

View File

@@ -17,7 +17,6 @@ flask-login = "^0.6"
flask-talisman = "*"
flask-wtf = "^1.2"
govuk-bank-holidays = "==0.13"
govuk-frontend-jinja = {git = "https://github.com/alphagov/govuk-frontend-jinja.git", tag = "v0.5.8-alpha"}
gunicorn = {version = "==21.2.0", extras = ["eventlet"]}
humanize = "~=4.9"
itsdangerous = "~=2.1"

View File

@@ -160,8 +160,8 @@ def test_can_show_notifications(
assert normalize_spaces(
first_row.select_one(".table-field-right-aligned .align-with-message-body").text
) in [
"Delivered 1 January at 02:01 US/Eastern",
"Delivered 1 January at 01:01 US/Eastern",
"Delivered 1 January at 02:00 US/Eastern",
"Delivered 1 January at 01:00 US/Eastern",
]
assert page_title in page.h1.text.strip()
@@ -661,31 +661,31 @@ def test_redacts_templates_that_should_be_redacted(
(
"email",
"temporary-failure",
"Inbox not accepting messages right now 27 September at 08:31 US/Eastern",
"Inbox not accepting messages right now 27 September at 08:30 US/Eastern",
False,
),
(
"email",
"permanent-failure",
"Email address does not exist 27 September at 08:31 US/Eastern",
"Email address does not exist 27 September at 08:30 US/Eastern",
False,
),
("email", "delivered", "Delivered 27 September at 08:31 US/Eastern", True),
("email", "delivered", "Delivered 27 September at 08:30 US/Eastern", True),
("sms", "created", "Sending since 27 September at 08:30 US/Eastern", True),
("sms", "sending", "Sending since 27 September at 08:30 US/Eastern", True),
(
"sms",
"temporary-failure",
"Phone not accepting messages right now 27 September at 08:31 US/Eastern",
"Phone not accepting messages right now 27 September at 08:30 US/Eastern",
False,
),
(
"sms",
"permanent-failure",
"Not delivered 27 September at 08:31 US/Eastern",
"Not delivered 27 September at 08:30 US/Eastern",
False,
),
("sms", "delivered", "Delivered 27 September at 08:31 US/Eastern", True),
("sms", "delivered", "Delivered 27 September at 08:30 US/Eastern", True),
],
)
def test_sending_status_hint_displays_correctly_on_notifications_page(

View File

@@ -90,7 +90,7 @@ def test_should_show_page_for_one_job(
assert page.h1.text.strip() == "thisisatest.csv"
assert " ".join(page.find("tbody").find("tr").text.split()) == (
"2021234567 template content Delivered 1 January at 06:10 US/Eastern"
"2021234567 template content Delivered 1 January at 06:09 US/Eastern"
)
assert page.find("div", {"data-key": "notifications"})["data-resource"] == url_for(
"main.view_job_updates",
@@ -109,7 +109,7 @@ def test_should_show_page_for_one_job(
assert page.find("span", {"id": "time-left"}).text == "Data available for 7 days"
assert normalize_spaces(page.select_one("tbody tr").text) == normalize_spaces(
"2021234567 " "template content " "Delivered 1 January at 06:10 US/Eastern"
"2021234567 " "template content " "Delivered 1 January at 06:09 US/Eastern"
)
assert page.select_one("tbody tr a")["href"] == url_for(
"main.view_notification",
@@ -424,7 +424,7 @@ def test_should_show_updates_for_one_job_as_json(
assert "2021234567" in content["notifications"]
assert "Status" in content["notifications"]
assert "Delivered" in content["notifications"]
assert "00:01" in content["notifications"]
assert "00:00" in content["notifications"]
assert "Sent by Test User on 1 January at 00:00" in content["status"]
@@ -466,7 +466,7 @@ def test_should_show_updates_for_scheduled_job_as_json(
assert "2021234567" in content["notifications"]
assert "Status" in content["notifications"]
assert "Delivered" in content["notifications"]
assert "00:01" in content["notifications"]
assert "00:00" in content["notifications"]
assert "Sent by Test User on 1 June at 16:00" in content["status"]