Compare commits
65 Commits
06-30-2025
...
1bf55e7fa3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bf55e7fa3 | ||
|
|
591231c084 | ||
|
|
a5753cb20a | ||
|
|
128ca06270 | ||
|
|
710631312f | ||
|
|
f5d5aa0bf9 | ||
|
|
6f31bc14dc | ||
|
|
f849799bc4 | ||
|
|
f940d1fde6 | ||
|
|
ecc8057b30 | ||
|
|
3e0ff8d353 | ||
|
|
6b2d783b37 | ||
|
|
9cc4bbb5ce | ||
|
|
405b47a02e | ||
|
|
53a3143a8f | ||
|
|
3b1d9689b4 | ||
|
|
793e51a3a2 | ||
|
|
9a2c6d1111 | ||
|
|
0267a82687 | ||
|
|
41b925982e | ||
|
|
bce4671803 | ||
|
|
0faa32c268 | ||
|
|
fc54cfd941 | ||
|
|
51dcdca047 | ||
|
|
d1c423a112 | ||
|
|
ffc6d6c5b7 | ||
|
|
a691f5c0dd | ||
|
|
3f42dc343b | ||
|
|
823593893d | ||
|
|
f07c196734 | ||
|
|
ad824bbc1b | ||
|
|
ea5e1f6d8c | ||
|
|
cf6f134cc1 | ||
|
|
5d1e1caf24 | ||
|
|
cc30337d6c | ||
|
|
377d24357f | ||
|
|
bf75705175 | ||
|
|
3130b1399c | ||
|
|
fa9e45fbcd | ||
|
|
ee2f777904 | ||
|
|
9d9fa1c394 | ||
|
|
8a41de8f07 | ||
|
|
b73b4ac73e | ||
|
|
e69cd8267d | ||
|
|
2cec15f097 | ||
|
|
a0c87e0773 | ||
|
|
b13657373e | ||
|
|
232d76369f | ||
|
|
ddaa68c1de | ||
|
|
84bbc328b4 | ||
|
|
2b72e5df60 | ||
|
|
47d388ba29 | ||
|
|
f5501e364e | ||
|
|
3e2fe8be04 | ||
|
|
f968aece17 | ||
|
|
7002dfaba7 | ||
|
|
bee667a5e9 | ||
|
|
7447a0080c | ||
|
|
ac226f5ad8 | ||
|
|
afbdb2aa9a | ||
|
|
4e31738fd8 | ||
|
|
25d76d1350 | ||
|
|
3f58647dc9 | ||
|
|
07bdad4134 | ||
|
|
23ab1dd6c2 |
16
Makefile
@@ -159,22 +159,6 @@ clean:
|
||||
upload-static:
|
||||
aws s3 cp --region us-west-2 --recursive --cache-control max-age=315360000,immutable ./app/static s3://${DNS_NAME}-static
|
||||
|
||||
# .PHONY: cf-failwhale-deployed
|
||||
# cf-failwhale-deployed:
|
||||
# @cf app notify-admin-failwhale --guid || (echo "notify-admin-failwhale is not deployed on ${CF_SPACE}" && exit 1)
|
||||
|
||||
# .PHONY: enable-failwhale
|
||||
# enable-failwhale: cf-target cf-failwhale-deployed ## Enable the failwhale app and disable admin
|
||||
# @cf map-route notify-admin-failwhale ${DNS_NAME} --hostname www
|
||||
# @cf unmap-route notify-admin ${DNS_NAME} --hostname www
|
||||
# @echo "Failwhale is enabled"
|
||||
|
||||
# .PHONY: disable-failwhale
|
||||
# disable-failwhale: cf-target cf-failwhale-deployed ## Disable the failwhale app and enable admin
|
||||
# @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
|
||||
|
||||
@@ -359,6 +359,11 @@ def init_app(application):
|
||||
def _attach_current_user():
|
||||
return {"current_user": current_user}
|
||||
|
||||
@application.context_processor
|
||||
def _attach_enums():
|
||||
from app.enums import ServicePermission
|
||||
return {"ServicePermission": ServicePermission}
|
||||
|
||||
@application.context_processor
|
||||
def _nav_selected():
|
||||
return navigation
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 425.2 428" style="enable-background:new 0 0 425.2 428;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#1D70B8;}
|
||||
.st1{fill:#0B0C0C;}
|
||||
.st2{fill:#FFDD00;}
|
||||
.st3{opacity:0.2;fill:#1D2F3A;}
|
||||
.st4{opacity:0.5;fill:#F4F4F4;}
|
||||
.st5{clip-path:url(#SVGID_2_);fill:#FFDD00;}
|
||||
.st6{opacity:0.2;clip-path:url(#SVGID_2_);fill:#1D2F3A;}
|
||||
.st7{opacity:0.5;clip-path:url(#SVGID_2_);}
|
||||
.st8{fill:#F4F4F4;}
|
||||
.st9{fill:#206FAB;}
|
||||
</style>
|
||||
<g id="Layer_1">
|
||||
<circle class="st0" cx="212.6" cy="212.6" r="212.6"/>
|
||||
</g>
|
||||
<g id="Layer_2">
|
||||
|
||||
<rect x="91.9" y="248.6" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -164.6926 181.3616)" class="st1" width="89.3" height="81.7"/>
|
||||
|
||||
<rect x="76.9" y="258.5" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -150.6822 163.09)" class="st2" width="89.3" height="9.9"/>
|
||||
|
||||
<rect x="90.6" y="272.2" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -156.3567 176.7894)" class="st2" width="89.3" height="9.9"/>
|
||||
|
||||
<rect x="104.3" y="285.9" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -162.0312 190.4889)" class="st2" width="89.3" height="9.9"/>
|
||||
|
||||
<rect x="118" y="299.6" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -167.7057 204.1884)" class="st2" width="89.3" height="9.9"/>
|
||||
|
||||
<rect x="131.9" y="227.1" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -143.1309 190.2927)" class="st3" width="52.5" height="81.7"/>
|
||||
</g>
|
||||
<g id="Layer_5">
|
||||
<polygon class="st4" points="116.8,275.2 108.5,283.4 103,274.9 111.3,266.7 "/>
|
||||
<polygon class="st4" points="127.6,291.8 119.3,300 113.8,291.5 122.1,283.3 "/>
|
||||
<polygon class="st4" points="138.3,308.4 130.1,316.7 124.6,308.2 132.8,299.9 "/>
|
||||
<polygon class="st4" points="149.1,325 140.9,333.3 135.4,324.8 143.6,316.6 "/>
|
||||
</g>
|
||||
<g id="Layer_6">
|
||||
</g>
|
||||
<g id="Layer_4">
|
||||
<defs>
|
||||
|
||||
<ellipse id="SVGID_1_" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -22.5211 240.2509)" cx="278.7" cy="147.3" rx="23.7" ry="23.7"/>
|
||||
</defs>
|
||||
<clipPath id="SVGID_2_">
|
||||
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
|
||||
</clipPath>
|
||||
|
||||
<ellipse transform="matrix(0.7071 -0.7071 0.7071 0.7071 -22.5211 240.2509)" class="st5" cx="278.7" cy="147.3" rx="23.7" ry="23.7"/>
|
||||
|
||||
<ellipse transform="matrix(0.7071 -0.7071 0.7071 0.7071 -22.5211 240.2509)" class="st6" cx="278.7" cy="147.3" rx="23.7" ry="23.7"/>
|
||||
<path class="st5" d="M264.8,127.7c15.8,2.7,30.8,17.7,33.5,33.5s-7.9,26.4-23.7,23.7s-30.8-17.7-33.5-33.5
|
||||
C238.4,135.6,249,125,264.8,127.7z"/>
|
||||
<g class="st7">
|
||||
|
||||
<rect x="260.6" y="117.2" transform="matrix(0.842 -0.5395 0.5395 0.842 -44.6945 168.5803)" class="st8" width="9.8" height="86.7"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Layer_9">
|
||||
|
||||
<ellipse transform="matrix(0.7071 -0.7071 0.7071 0.7071 -22.5211 240.2509)" class="st9" cx="278.7" cy="147.3" rx="15.8" ry="15.8"/>
|
||||
</g>
|
||||
<g id="Layer_3">
|
||||
<path class="st1" d="M278,74.5l-168,168l73.6,73.6l168-168L278,74.5z M262,164c-9.2-9.2-9.2-24.2,0-33.5s24.2-9.2,33.5,0
|
||||
s9.2,24.2,0,33.5S271.3,173.3,262,164z"/>
|
||||
</g>
|
||||
<g id="Layer_8">
|
||||
</g>
|
||||
<g id="Layer_10">
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -65,7 +65,7 @@
|
||||
|
||||
return $(`<div class="selection-footer${stickyClass} margin-top-2">
|
||||
<button
|
||||
class="govuk-button govuk-button--secondary selection-footer__button usa-button usa-button--outline"
|
||||
class="usa-button usa-button--outline selection-footer__button"
|
||||
aria-expanded="${expanded ? 'true' : 'false'}"
|
||||
aria-controls="${this.fieldsetId}">
|
||||
${buttonContent}
|
||||
@@ -214,7 +214,7 @@
|
||||
CollapsibleCheckboxes.prototype.bindEvents = function() {
|
||||
const self = this;
|
||||
|
||||
this.$formGroup.on('click', '.govuk-button', this.handleClick.bind(this));
|
||||
this.$formGroup.on('click', '.usa-button', this.handleClick.bind(this));
|
||||
this.$checkboxes.on('click', this.handleSelection.bind(this));
|
||||
|
||||
this.summary.bindEvents(this);
|
||||
|
||||
@@ -80,14 +80,14 @@
|
||||
$('.copy-to-clipboard__value', component)[0], () =>
|
||||
$component
|
||||
.html(states.valueCopied(stateOptions))
|
||||
.find('.govuk-button').focus()
|
||||
.find('.usa-button').focus()
|
||||
)
|
||||
)
|
||||
.on(
|
||||
'click', '.copy-to-clipboard__button--show', () =>
|
||||
$component
|
||||
.html(states.valueVisible(stateOptions))
|
||||
.find('.govuk-button').focus()
|
||||
.find('.usa-button').focus()
|
||||
);
|
||||
|
||||
if ('stickAtBottomWhenScrolling' in GOVUK) {
|
||||
|
||||
@@ -37,14 +37,14 @@
|
||||
' {{{sharedAttributes}}}' +
|
||||
'/>' +
|
||||
'{{#button}}' +
|
||||
'<button type="button" class="govuk-button govuk-button--secondary input-list__button--remove">' +
|
||||
'<button type="button" class="usa-button input-list__button--remove">' +
|
||||
'Remove<span class="usa-sr-only"> {{listItemName}} number {{number}}</span>' +
|
||||
'</button>' +
|
||||
'{{/button}}' +
|
||||
'</div>'
|
||||
);
|
||||
ListEntry.prototype.addButtonTemplate = Hogan.compile(
|
||||
'<button type="button" class="govuk-button govuk-button--secondary input-list__button--add">Add another {{listItemName}} ({{entriesLeft}} remaining)</button>'
|
||||
'<button type="button" class="usa-button input-list__button--add">Add another {{listItemName}} ({{entriesLeft}} remaining)</button>'
|
||||
);
|
||||
ListEntry.prototype.getSharedAttributes = function () {
|
||||
var $inputs = this.$wrapper.find('input'),
|
||||
|
||||
@@ -108,9 +108,6 @@ p,
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.multiple-choice [type='radio']:focus + label::before {
|
||||
box-shadow: 0 0 0 3px color('blue-40v');
|
||||
}
|
||||
|
||||
.usa-dark-background .pill-item__container {
|
||||
color: #fff;
|
||||
|
||||
@@ -34,12 +34,6 @@ $path: '/static/images/';
|
||||
// Dependencies from GOV.UK Frontend, packaged to be specific to this application
|
||||
@import './govuk-frontend/all';
|
||||
|
||||
// Custom overrides
|
||||
.govuk-link {
|
||||
font-weight: bold;
|
||||
color: #005ea5;
|
||||
}
|
||||
|
||||
// Specific to this application
|
||||
@import 'local/typography';
|
||||
@import 'grids';
|
||||
|
||||
60
app/enums.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class NotificationStatus(StrEnum):
|
||||
CREATED = "created"
|
||||
PENDING = "pending"
|
||||
SENDING = "sending"
|
||||
|
||||
DELIVERED = "delivered"
|
||||
SENT = "sent"
|
||||
|
||||
FAILED = "failed"
|
||||
TEMPORARY_FAILURE = "temporary-failure"
|
||||
PERMANENT_FAILURE = "permanent-failure"
|
||||
TECHNICAL_FAILURE = "technical-failure"
|
||||
VALIDATION_FAILED = "validation-failed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class ApiKeyType(StrEnum):
|
||||
NORMAL = "normal"
|
||||
TEAM = "team"
|
||||
TEST = "test"
|
||||
|
||||
|
||||
class JobStatus(StrEnum):
|
||||
PENDING = "pending"
|
||||
IN_PROGRESS = "in progress"
|
||||
FINISHED = "finished"
|
||||
SENDING_LIMITS_EXCEEDED = "sending limits exceeded"
|
||||
SCHEDULED = "scheduled"
|
||||
CANCELLED = "cancelled"
|
||||
READY_TO_SEND = "ready to send"
|
||||
SENT_TO_DVLA = "sent to dvla"
|
||||
|
||||
|
||||
class ServicePermission(StrEnum):
|
||||
SEND_MESSAGES = "send_messages"
|
||||
MANAGE_SERVICE = "manage_service"
|
||||
|
||||
|
||||
class InvitedUserStatus(StrEnum):
|
||||
ACCEPTED = "accepted"
|
||||
CANCELLED = "cancelled"
|
||||
EXPIRED = "expired"
|
||||
|
||||
|
||||
class InvitedOrgUserStatus(StrEnum):
|
||||
ACCEPTED = "accepted"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class VerificationStatus(StrEnum):
|
||||
PENDING = "pending"
|
||||
SUCCESS = "success"
|
||||
|
||||
|
||||
class AuthType(StrEnum):
|
||||
EMAIL_AUTH = "email_auth"
|
||||
SMS_AUTH = "sms_auth"
|
||||
@@ -655,7 +655,7 @@ class RegisterUserFromOrgInviteForm(StripWhitespaceForm):
|
||||
auth_type = HiddenField("auth_type", validators=[DataRequired()])
|
||||
|
||||
|
||||
def govuk_checkbox_field_widget(self, field, param_extensions=None, **kwargs):
|
||||
def uswds_checkbox_field_widget(self, field, param_extensions=None, **kwargs):
|
||||
# error messages
|
||||
error_message = None
|
||||
if field.errors:
|
||||
@@ -695,7 +695,7 @@ def govuk_checkbox_field_widget(self, field, param_extensions=None, **kwargs):
|
||||
) # nosec
|
||||
|
||||
|
||||
def govuk_checkboxes_field_widget(
|
||||
def uswds_checkboxes_field_widget(
|
||||
self, field, wrap_in_collapsible=False, param_extensions=None, **kwargs
|
||||
):
|
||||
def _wrap_in_collapsible(field_label, checkboxes_string):
|
||||
@@ -812,9 +812,9 @@ def govuk_radios_field_widget(self, field, param_extensions=None, **kwargs):
|
||||
) # nosec
|
||||
|
||||
|
||||
class GovukCheckboxField(BooleanField):
|
||||
class USWDSCheckboxField(BooleanField):
|
||||
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
|
||||
super(GovukCheckboxField, self).__init__(
|
||||
super(USWDSCheckboxField, self).__init__(
|
||||
label, validators, false_values=None, **kwargs
|
||||
)
|
||||
self.param_extensions = param_extensions
|
||||
@@ -824,17 +824,17 @@ class GovukCheckboxField(BooleanField):
|
||||
# 2. calls field.widget
|
||||
# this bypasses that by making self.widget a method with the same interface as widget.__call__
|
||||
def widget(self, field, param_extensions=None, **kwargs):
|
||||
return govuk_checkbox_field_widget(
|
||||
return uswds_checkbox_field_widget(
|
||||
self, field, param_extensions=param_extensions, **kwargs
|
||||
)
|
||||
|
||||
|
||||
# based on work done by @richardjpope: https://github.com/richardjpope/recourse/blob/master/recourse/forms.py#L6
|
||||
class GovukCheckboxesField(SelectMultipleField):
|
||||
class USWDSCheckboxesField(SelectMultipleField):
|
||||
render_as_list = False
|
||||
|
||||
def __init__(self, label="", validators=None, param_extensions=None, **kwargs):
|
||||
super(GovukCheckboxesField, self).__init__(label, validators, **kwargs)
|
||||
super(USWDSCheckboxesField, self).__init__(label, validators, **kwargs)
|
||||
self.param_extensions = param_extensions
|
||||
|
||||
def get_item_from_option(self, option):
|
||||
@@ -854,32 +854,32 @@ class GovukCheckboxesField(SelectMultipleField):
|
||||
# 2. calls field.widget
|
||||
# this bypasses that by making self.widget a method with the same interface as widget.__call__
|
||||
def widget(self, field, param_extensions=None, **kwargs):
|
||||
return govuk_checkboxes_field_widget(
|
||||
return uswds_checkboxes_field_widget(
|
||||
self, field, param_extensions=param_extensions, **kwargs
|
||||
)
|
||||
|
||||
|
||||
# Wraps checkboxes rendering in HTML needed by the collapsible JS
|
||||
class GovukCollapsibleCheckboxesField(GovukCheckboxesField):
|
||||
class USWDSCollapsibleCheckboxesField(USWDSCheckboxesField):
|
||||
def __init__(
|
||||
self, label="", validators=None, field_label="", param_extensions=None, **kwargs
|
||||
):
|
||||
super(GovukCollapsibleCheckboxesField, self).__init__(
|
||||
super(USWDSCollapsibleCheckboxesField, self).__init__(
|
||||
label, validators, param_extensions, **kwargs
|
||||
)
|
||||
self.field_label = field_label
|
||||
|
||||
def widget(self, field, **kwargs):
|
||||
return govuk_checkboxes_field_widget(
|
||||
return uswds_checkboxes_field_widget(
|
||||
self, field, wrap_in_collapsible=True, param_extensions=None, **kwargs
|
||||
)
|
||||
|
||||
|
||||
# GovukCollapsibleCheckboxesField adds an ARIA live-region to the hint and wraps the render in HTML needed by the
|
||||
# USWDSCollapsibleCheckboxesField adds an ARIA live-region to the hint and wraps the render in HTML needed by the
|
||||
# collapsible JS
|
||||
# NestedFieldMixin puts the items into a tree hierarchy, pre-rendering the sub-trees of the top-level items
|
||||
class GovukCollapsibleNestedCheckboxesField(
|
||||
NestedFieldMixin, GovukCollapsibleCheckboxesField
|
||||
class USWDSCollapsibleNestedCheckboxesField(
|
||||
NestedFieldMixin, USWDSCollapsibleCheckboxesField
|
||||
):
|
||||
NONE_OPTION_VALUE = None
|
||||
render_as_list = True
|
||||
@@ -998,7 +998,7 @@ class BasePermissionsForm(StripWhitespaceForm):
|
||||
for item in ([{"name": "Templates", "id": None}] + all_template_folders)
|
||||
]
|
||||
|
||||
folder_permissions = GovukCollapsibleNestedCheckboxesField(
|
||||
folder_permissions = USWDSCollapsibleNestedCheckboxesField(
|
||||
"Folders this team member can see", field_label="folder"
|
||||
)
|
||||
|
||||
@@ -1012,7 +1012,7 @@ class BasePermissionsForm(StripWhitespaceForm):
|
||||
validators=[DataRequired()],
|
||||
)
|
||||
|
||||
permissions_field = GovukCheckboxesField(
|
||||
permissions_field = USWDSCheckboxesField(
|
||||
"Permissions",
|
||||
filters=[filter_by_permissions],
|
||||
choices=[(value, label) for value, label in permission_options],
|
||||
@@ -1422,7 +1422,7 @@ class ServiceContactDetailsForm(StripWhitespaceForm):
|
||||
|
||||
class ServiceReplyToEmailForm(StripWhitespaceForm):
|
||||
email_address = email_address(label="Reply-to email address", gov_user=False)
|
||||
is_default = GovukCheckboxField("Make this email address the default")
|
||||
is_default = USWDSCheckboxField("Make this email address the default")
|
||||
|
||||
|
||||
class ServiceSmsSenderForm(StripWhitespaceForm):
|
||||
@@ -1436,11 +1436,11 @@ class ServiceSmsSenderForm(StripWhitespaceForm):
|
||||
DoesNotStartWithDoubleZero(),
|
||||
],
|
||||
)
|
||||
is_default = GovukCheckboxField("Make this text message sender the default")
|
||||
is_default = USWDSCheckboxField("Make this text message sender the default")
|
||||
|
||||
|
||||
class ServiceEditInboundNumberForm(StripWhitespaceForm):
|
||||
is_default = GovukCheckboxField("Make this text message sender the default")
|
||||
is_default = USWDSCheckboxField("Make this text message sender the default")
|
||||
|
||||
|
||||
class AdminNotesForm(StripWhitespaceForm):
|
||||
@@ -1528,7 +1528,7 @@ class GuestList(StripWhitespaceForm):
|
||||
class DateFilterForm(StripWhitespaceForm):
|
||||
start_date = GovukDateField("Start Date", [validators.optional()])
|
||||
end_date = GovukDateField("End Date", [validators.optional()])
|
||||
include_from_test_key = GovukCheckboxField("Include test keys")
|
||||
include_from_test_key = USWDSCheckboxField("Include test keys")
|
||||
|
||||
|
||||
class RequiredDateFilterForm(StripWhitespaceForm):
|
||||
@@ -1732,7 +1732,7 @@ class TemplateFolderForm(StripWhitespaceForm):
|
||||
(item.id, item.name) for item in all_service_users
|
||||
]
|
||||
|
||||
users_with_permission = GovukCollapsibleCheckboxesField(
|
||||
users_with_permission = USWDSCollapsibleCheckboxesField(
|
||||
"Team members who can see this folder", field_label="team member"
|
||||
)
|
||||
name = GovukTextInputField(
|
||||
@@ -1851,7 +1851,7 @@ class TemplateAndFoldersSelectionForm(Form):
|
||||
return self.move_to_new_folder_name.data
|
||||
return None
|
||||
|
||||
templates_and_folders = GovukCheckboxesField(
|
||||
templates_and_folders = USWDSCheckboxesField(
|
||||
"Choose templates or folders",
|
||||
validators=[required_for_ops("move-to-new-folder", "move-to-existing-folder")],
|
||||
choices=[], # added to keep order of arguments, added properly in __init__
|
||||
@@ -1883,21 +1883,10 @@ class TemplateAndFoldersSelectionForm(Form):
|
||||
|
||||
|
||||
class AdminClearCacheForm(StripWhitespaceForm):
|
||||
model_type = GovukCheckboxesField(
|
||||
model_type = USWDSCheckboxesField(
|
||||
"What do you want to clear today",
|
||||
)
|
||||
|
||||
def validate_model_type(self, field):
|
||||
if not field.data:
|
||||
raise ValidationError("Select at least one option")
|
||||
|
||||
|
||||
class ChangeSecurityKeyNameForm(StripWhitespaceForm):
|
||||
security_key_name = GovukTextInputField(
|
||||
"Name of key",
|
||||
validators=[
|
||||
DataRequired(message="Cannot be empty"),
|
||||
MustContainAlphanumericCharacters(),
|
||||
Length(max=255, message="Name of key must be 255 characters or fewer"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from flask import abort, render_template, request, url_for
|
||||
|
||||
from app import current_service, job_api_client
|
||||
from app.enums import NotificationStatus
|
||||
from app.formatters import get_time_left
|
||||
from app.main import main
|
||||
from app.utils.pagination import (
|
||||
@@ -78,10 +79,6 @@ def handle_pagination(jobs, service_id, page):
|
||||
return prev_page, next_page, pagination
|
||||
|
||||
|
||||
JOB_STATUS_DELIVERED = "delivered"
|
||||
JOB_STATUS_FAILED = "failed"
|
||||
|
||||
|
||||
def get_job_statistics(job, status):
|
||||
statistics = job.get("statistics", [])
|
||||
for stat in statistics:
|
||||
@@ -109,8 +106,8 @@ def create_job_dict_entry(job):
|
||||
"activity_time": activity_time,
|
||||
"created_by": job.get("created_by"),
|
||||
"template_name": job.get("template_name"),
|
||||
"delivered_count": get_job_statistics(job, JOB_STATUS_DELIVERED),
|
||||
"failed_count": get_job_statistics(job, JOB_STATUS_FAILED),
|
||||
"delivered_count": get_job_statistics(job, NotificationStatus.DELIVERED),
|
||||
"failed_count": get_job_statistics(job, NotificationStatus.FAILED),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from flask import jsonify, redirect, render_template, session, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import current_service, notification_api_client, service_api_client
|
||||
from app.enums import ServicePermission
|
||||
from app.main import main
|
||||
from app.main.forms import SearchByNameForm
|
||||
from app.models.template_list import TemplateList
|
||||
@@ -45,7 +46,7 @@ def conversation_updates(service_id, notification_id):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/conversation/<uuid:notification_id>/reply-with/from-folder/<uuid:from_folder>"
|
||||
)
|
||||
@user_has_permissions("send_messages")
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES)
|
||||
def conversation_reply(
|
||||
service_id,
|
||||
notification_id,
|
||||
@@ -69,7 +70,7 @@ def conversation_reply(
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/conversation/<uuid:notification_id>/reply-with/<uuid:template_id>"
|
||||
)
|
||||
@user_has_permissions("send_messages")
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES)
|
||||
def conversation_reply_with_template(
|
||||
service_id,
|
||||
notification_id,
|
||||
|
||||
@@ -14,6 +14,7 @@ from app import (
|
||||
service_api_client,
|
||||
template_statistics_client,
|
||||
)
|
||||
from app.enums import ServicePermission
|
||||
from app.main import main
|
||||
from app.main.views.user_profile import set_timezone
|
||||
from app.statistics_utils import get_formatted_percentage
|
||||
@@ -23,7 +24,7 @@ from app.utils.user import user_has_permissions
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/dashboard")
|
||||
@user_has_permissions("view_activity", "send_messages")
|
||||
@user_has_permissions("view_activity", ServicePermission.SEND_MESSAGES)
|
||||
def old_service_dashboard(service_id):
|
||||
return redirect(url_for(".service_dashboard", service_id=service_id))
|
||||
|
||||
@@ -202,7 +203,7 @@ def template_usage(service_id):
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/usage")
|
||||
@user_has_permissions("manage_service", allow_org_user=True)
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE, allow_org_user=True)
|
||||
def usage(service_id):
|
||||
year, current_financial_year = requested_and_current_financial_year(request)
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ from operator import attrgetter
|
||||
from flask import render_template, request
|
||||
|
||||
from app import current_service, format_date_numeric
|
||||
from app.enums import ServicePermission
|
||||
from app.main import main
|
||||
from app.models.event import APIKeyEvent, APIKeyEvents, ServiceEvents
|
||||
from app.utils.user import user_has_permissions
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/history")
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def history(service_id):
|
||||
events = _get_events(current_service.id, request.args.get("selected"))
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from flask import abort, flash, redirect, render_template, session, url_for
|
||||
from flask_login import current_user
|
||||
from markupsafe import Markup
|
||||
|
||||
from app.enums import InvitedOrgUserStatus, InvitedUserStatus
|
||||
from app.main import main
|
||||
from app.models.organization import Organization
|
||||
from app.models.service import Service
|
||||
@@ -29,14 +30,14 @@ def accept_invite(token):
|
||||
|
||||
abort(403)
|
||||
|
||||
if invited_user.status == "cancelled":
|
||||
if invited_user.status == InvitedUserStatus.CANCELLED:
|
||||
service = Service.from_id(invited_user.service)
|
||||
return render_template(
|
||||
"views/cancelled-invitation.html",
|
||||
from_user=invited_user.from_user.name,
|
||||
service_name=service.name,
|
||||
)
|
||||
if invited_user.status == "accepted":
|
||||
if invited_user.status == InvitedUserStatus.ACCEPTED:
|
||||
session.pop("invited_user_id", None)
|
||||
service = Service.from_id(invited_user.service)
|
||||
return redirect(
|
||||
@@ -104,7 +105,7 @@ def accept_org_invite(token):
|
||||
|
||||
abort(403)
|
||||
|
||||
if invited_org_user.status == "cancelled":
|
||||
if invited_org_user.status == InvitedOrgUserStatus.CANCELLED:
|
||||
organization = Organization.from_id(invited_org_user.organization)
|
||||
return render_template(
|
||||
"views/cancelled-invitation.html",
|
||||
@@ -112,7 +113,7 @@ def accept_org_invite(token):
|
||||
organization_name=organization.name,
|
||||
)
|
||||
|
||||
if invited_org_user.status == "accepted":
|
||||
if invited_org_user.status == InvitedOrgUserStatus.ACCEPTED:
|
||||
session.pop("invited_org_user_id", None)
|
||||
return redirect(
|
||||
url_for("main.organization_dashboard", org_id=invited_org_user.organization)
|
||||
|
||||
@@ -23,6 +23,7 @@ from app import (
|
||||
notification_api_client,
|
||||
service_api_client,
|
||||
)
|
||||
from app.enums import JobStatus, ServicePermission
|
||||
from app.formatters import get_time_left, message_count_noun
|
||||
from app.main import main
|
||||
from app.main.forms import SearchNotificationsForm
|
||||
@@ -105,7 +106,7 @@ def view_job_csv(service_id, job_id):
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>", methods=["POST"])
|
||||
@user_has_permissions("send_messages")
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES)
|
||||
def cancel_job(service_id, job_id):
|
||||
Job.from_id(job_id, service_id=service_id).cancel()
|
||||
return redirect(url_for("main.service_dashboard", service_id=service_id))
|
||||
@@ -398,7 +399,7 @@ def get_job_partials(job):
|
||||
counts=_get_job_counts(job),
|
||||
status=filter_args["status"],
|
||||
notifications_deleted=(
|
||||
job.status == "finished" and not notifications["notifications"]
|
||||
job.status == JobStatus.FINISHED and not notifications["notifications"]
|
||||
),
|
||||
)
|
||||
service_data_retention_days = current_service.get_days_of_retention(
|
||||
|
||||
@@ -2,6 +2,7 @@ from flask import abort, flash, redirect, render_template, request, session, url
|
||||
from flask_login import current_user
|
||||
|
||||
from app import current_service, service_api_client
|
||||
from app.enums import ServicePermission
|
||||
from app.event_handlers import (
|
||||
create_cancel_user_invite_to_service_event,
|
||||
create_email_change_event,
|
||||
@@ -43,7 +44,7 @@ def manage_users(service_id):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/users/invite/<uuid:user_id>", methods=["GET", "POST"]
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def invite_user(service_id, user_id=None):
|
||||
form_class = InviteUserForm
|
||||
form = form_class(
|
||||
@@ -113,7 +114,7 @@ def invite_user(service_id, user_id=None):
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>", methods=["GET", "POST"])
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def edit_user_permissions(service_id, user_id):
|
||||
service_has_email_auth = current_service.has_permission("email_auth")
|
||||
user = current_service.get_team_member(user_id)
|
||||
@@ -164,7 +165,7 @@ def edit_user_permissions(service_id, user_id):
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/users/<uuid:user_id>/delete", methods=["POST"])
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def remove_user_from_service(service_id, user_id):
|
||||
try:
|
||||
service_api_client.remove_user_from_service(service_id, user_id)
|
||||
@@ -187,7 +188,7 @@ def remove_user_from_service(service_id, user_id):
|
||||
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-email",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def edit_user_email(service_id, user_id):
|
||||
user = current_service.get_team_member(user_id)
|
||||
user_email = user.email_address
|
||||
@@ -220,7 +221,7 @@ def edit_user_email(service_id, user_id):
|
||||
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-email/confirm",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def confirm_edit_user_email(service_id, user_id):
|
||||
user = current_service.get_team_member(user_id)
|
||||
session_key = "team_member_email_change-{}".format(user_id)
|
||||
@@ -258,7 +259,7 @@ def confirm_edit_user_email(service_id, user_id):
|
||||
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-mobile-number",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def edit_user_mobile_number(service_id, user_id):
|
||||
user = current_service.get_team_member(user_id)
|
||||
user_mobile_number = redact_mobile_number(user.mobile_number)
|
||||
@@ -288,7 +289,7 @@ def edit_user_mobile_number(service_id, user_id):
|
||||
"/services/<uuid:service_id>/users/<uuid:user_id>/edit-mobile-number/confirm",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def confirm_edit_user_mobile_number(service_id, user_id):
|
||||
user = current_service.get_team_member(user_id)
|
||||
if "team_member_mobile_change" in session:
|
||||
@@ -326,7 +327,7 @@ def confirm_edit_user_mobile_number(service_id, user_id):
|
||||
"/services/<uuid:service_id>/cancel-invited-user/<uuid:invited_user_id>",
|
||||
methods=["GET"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def cancel_invited_user(service_id, invited_user_id):
|
||||
current_service.cancel_invite(invited_user_id)
|
||||
|
||||
@@ -345,7 +346,7 @@ def cancel_invited_user(service_id, invited_user_id):
|
||||
"/services/<uuid:service_id>/resend-invite/<uuid:invited_user_id>",
|
||||
methods=["GET"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def resend_invite(service_id, invited_user_id):
|
||||
current_service.resend_invite(invited_user_id)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from flask import (
|
||||
)
|
||||
|
||||
from app import current_service, job_api_client, notification_api_client
|
||||
from app.enums import ServicePermission
|
||||
from app.main import main
|
||||
from app.notify_client.api_key_api_client import KEY_TYPE_TEST
|
||||
from app.utils import (
|
||||
@@ -27,7 +28,7 @@ from app.utils.user import user_has_permissions
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/notification/<uuid:notification_id>")
|
||||
@user_has_permissions("view_activity", "send_messages")
|
||||
@user_has_permissions("view_activity", ServicePermission.SEND_MESSAGES)
|
||||
def view_notification(service_id, notification_id, error_message=None):
|
||||
if error_message:
|
||||
flash(error_message)
|
||||
@@ -100,7 +101,7 @@ def view_notification(service_id, notification_id, error_message=None):
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/notification/<uuid:notification_id>.json")
|
||||
@user_has_permissions("view_activity", "send_messages")
|
||||
@user_has_permissions("view_activity", ServicePermission.SEND_MESSAGES)
|
||||
def view_notification_updates(service_id, notification_id):
|
||||
return jsonify(
|
||||
**get_single_notification_partials(
|
||||
|
||||
@@ -15,6 +15,7 @@ from flask import (
|
||||
)
|
||||
|
||||
from app import redis_client, user_api_client
|
||||
from app.enums import InvitedUserStatus
|
||||
from app.main import main
|
||||
from app.main.forms import (
|
||||
RegisterUserFromOrgInviteForm,
|
||||
@@ -254,14 +255,14 @@ def get_invited_user_email_address(invited_user_id):
|
||||
def invited_user_accept_invite(invited_user_id):
|
||||
invited_user = InvitedUser.by_id(invited_user_id)
|
||||
|
||||
if invited_user.status == "expired":
|
||||
if invited_user.status == InvitedUserStatus.EXPIRED:
|
||||
current_app.logger.error("User invitation has expired")
|
||||
flash(
|
||||
"Your invitation has expired; please contact the person who invited you for additional help."
|
||||
)
|
||||
abort(401)
|
||||
|
||||
if invited_user.status == "cancelled":
|
||||
if invited_user.status == InvitedUserStatus.CANCELLED:
|
||||
current_app.logger.error("User invitation has been cancelled")
|
||||
flash(
|
||||
"Your invitation is no longer valid; please contact the person who invited you for additional help."
|
||||
|
||||
@@ -27,6 +27,7 @@ from app import (
|
||||
notification_api_client,
|
||||
service_api_client,
|
||||
)
|
||||
from app.enums import ServicePermission
|
||||
from app.main import main
|
||||
from app.main.forms import (
|
||||
ChooseTimeForm,
|
||||
@@ -94,7 +95,7 @@ def get_example_csv_rows(template, use_example_as_example=True, submitted_fields
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/send/<uuid:template_id>/csv", methods=["GET", "POST"]
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
|
||||
def send_messages(service_id, template_id):
|
||||
notification_count = service_api_client.get_notification_count(service_id)
|
||||
remaining_messages = current_service.message_limit - notification_count
|
||||
@@ -189,7 +190,7 @@ def send_messages(service_id, template_id):
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/send/<uuid:template_id>.csv", methods=["GET"])
|
||||
@user_has_permissions("send_messages", "manage_templates")
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, "manage_templates")
|
||||
def get_example_csv(service_id, template_id):
|
||||
template = get_template(
|
||||
service_api_client.get_service_template(service_id, template_id)["data"],
|
||||
@@ -214,7 +215,7 @@ def get_example_csv(service_id, template_id):
|
||||
"/services/<uuid:service_id>/send/<uuid:template_id>/set-sender",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
|
||||
def set_sender(service_id, template_id):
|
||||
session["sender_id"] = None
|
||||
redirect_to_one_off = redirect(
|
||||
@@ -343,7 +344,7 @@ def get_sender_details(service_id, template_type):
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/send/<uuid:template_id>/one-off")
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
|
||||
def send_one_off(service_id, template_id):
|
||||
session["recipient"] = None
|
||||
session["placeholders"] = {}
|
||||
@@ -387,7 +388,7 @@ def get_notification_check_endpoint(service_id, template):
|
||||
"/services/<uuid:service_id>/send/<uuid:template_id>/one-off/step-<int:step_index>",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
|
||||
def send_one_off_step(service_id, template_id, step_index):
|
||||
if {"recipient", "placeholders"} - set(session.keys()):
|
||||
return redirect(
|
||||
@@ -647,7 +648,7 @@ def _check_messages(service_id, template_id, upload_id, preview_row, **kwargs):
|
||||
"/services/<uuid:service_id>/<uuid:template_id>/check/<uuid:upload_id>/row-<int:row_index>",
|
||||
methods=["GET"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
|
||||
def check_messages(service_id, template_id, upload_id, row_index=2):
|
||||
data = _check_messages(service_id, template_id, upload_id, row_index)
|
||||
data["allowed_file_extensions"] = Spreadsheet.ALLOWED_FILE_EXTENSIONS
|
||||
@@ -691,7 +692,7 @@ def check_messages(service_id, template_id, upload_id, row_index=2):
|
||||
"/services/<uuid:service_id>/<uuid:template_id>/check/<uuid:upload_id>/preview/row-<int:row_index>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
|
||||
def preview_job(service_id, template_id, upload_id, row_index=2):
|
||||
session["scheduled_for"] = request.form.get("scheduled_for", "")
|
||||
data = _check_messages(
|
||||
@@ -706,7 +707,7 @@ def preview_job(service_id, template_id, upload_id, row_index=2):
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/start-job/<uuid:upload_id>", methods=["POST"])
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
|
||||
def start_job(service_id, upload_id):
|
||||
scheduled_for = session.pop("scheduled_for", None)
|
||||
job_api_client.create_job(
|
||||
@@ -843,7 +844,9 @@ def get_skip_link(step_index, template):
|
||||
and step_index == 0
|
||||
and template.template_type in ("sms", "email")
|
||||
and not (template.template_type == "sms" and current_user.mobile_number is None)
|
||||
and current_user.has_permissions("manage_templates", "manage_service")
|
||||
and current_user.has_permissions(
|
||||
"manage_templates", ServicePermission.MANAGE_SERVICE
|
||||
)
|
||||
):
|
||||
return (
|
||||
"Use my {}".format(first_column_headings[template.template_type][0]),
|
||||
@@ -859,7 +862,7 @@ def get_skip_link(step_index, template):
|
||||
"/services/<uuid:service_id>/template/<uuid:template_id>/one-off/send-to-myself",
|
||||
methods=["GET"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
|
||||
def send_one_off_to_myself(service_id, template_id):
|
||||
current_app.logger.info("Send one off to myself")
|
||||
try:
|
||||
@@ -896,7 +899,7 @@ def send_one_off_to_myself(service_id, template_id):
|
||||
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check",
|
||||
methods=["GET"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
|
||||
def check_notification(service_id, template_id):
|
||||
return render_template(
|
||||
"views/notifications/check.html",
|
||||
@@ -978,7 +981,7 @@ def get_template_error_dict(exception):
|
||||
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check/preview",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
|
||||
def preview_notification(service_id, template_id):
|
||||
recipient = get_recipient()
|
||||
if not recipient:
|
||||
@@ -1010,7 +1013,7 @@ def preview_notification(service_id, template_id):
|
||||
"/services/<uuid:service_id>/template/<uuid:template_id>/notification/check",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
|
||||
def send_notification(service_id, template_id):
|
||||
recipient = get_recipient()
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from app import (
|
||||
organizations_client,
|
||||
service_api_client,
|
||||
)
|
||||
from app.enums import ServicePermission, VerificationStatus
|
||||
from app.event_handlers import (
|
||||
create_archive_service_event,
|
||||
create_resume_service_event,
|
||||
@@ -69,7 +70,7 @@ PLATFORM_ADMIN_SERVICE_PERMISSIONS = OrderedDict(
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/service-settings")
|
||||
@user_has_permissions("manage_service", "manage_api_keys")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys")
|
||||
def service_settings(service_id):
|
||||
return render_template(
|
||||
"views/service-settings.html",
|
||||
@@ -80,7 +81,7 @@ def service_settings(service_id):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/name", methods=["GET", "POST"]
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_name_change(service_id):
|
||||
form = RenameServiceForm(name=current_service.name)
|
||||
|
||||
@@ -194,7 +195,7 @@ def service_set_permission(service_id, permission):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/archive", methods=["GET", "POST"]
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def archive_service(service_id):
|
||||
if not current_service.active or not (
|
||||
current_service.trial_mode or current_user.platform_admin
|
||||
@@ -267,7 +268,7 @@ def resume_service(service_id):
|
||||
"/services/<uuid:service_id>/service-settings/send-files-by-email",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def send_files_by_email_contact_details(service_id):
|
||||
form = ServiceContactDetailsForm()
|
||||
contact_details = None
|
||||
@@ -297,7 +298,7 @@ def send_files_by_email_contact_details(service_id):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/set-reply-to-email", methods=["GET"]
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_set_reply_to_email(service_id):
|
||||
return redirect(url_for(".service_email_reply_to", service_id=service_id))
|
||||
|
||||
@@ -305,7 +306,7 @@ def service_set_reply_to_email(service_id):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/email-reply-to", methods=["GET"]
|
||||
)
|
||||
@user_has_permissions("manage_service", "manage_api_keys")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys")
|
||||
def service_email_reply_to(service_id):
|
||||
return render_template("views/service-settings/email_reply_to.html")
|
||||
|
||||
@@ -314,7 +315,7 @@ def service_email_reply_to(service_id):
|
||||
"/services/<uuid:service_id>/service-settings/email-reply-to/add",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_add_email_reply_to(service_id):
|
||||
form = ServiceReplyToEmailForm()
|
||||
first_email_address = current_service.count_email_reply_to_addresses == 0
|
||||
@@ -358,7 +359,7 @@ def service_add_email_reply_to(service_id):
|
||||
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:notification_id>/verify",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_verify_reply_to_address(service_id, notification_id):
|
||||
replace = request.args.get("replace", False)
|
||||
is_default = request.args.get("is_default", False)
|
||||
@@ -378,7 +379,7 @@ def service_verify_reply_to_address(service_id, notification_id):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:notification_id>/verify.json"
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_verify_reply_to_address_updates(service_id, notification_id):
|
||||
return jsonify(
|
||||
**get_service_verify_reply_to_address_partials(service_id, notification_id)
|
||||
@@ -397,10 +398,10 @@ def get_service_verify_reply_to_address_partials(service_id, notification_id):
|
||||
if replace:
|
||||
existing = current_service.get_email_reply_to_address(replace)
|
||||
existing_is_default = existing["is_default"]
|
||||
verification_status = "pending"
|
||||
verification_status = VerificationStatus.PENDING
|
||||
is_default = True if (request.args.get("is_default", False) == "True") else False
|
||||
if notification["status"] in DELIVERED_STATUSES:
|
||||
verification_status = "success"
|
||||
verification_status = VerificationStatus.SUCCESS
|
||||
if notification["to"] not in [
|
||||
i["email_address"] for i in current_service.email_reply_to_addresses
|
||||
]:
|
||||
@@ -441,7 +442,7 @@ def get_service_verify_reply_to_address_partials(service_id, notification_id):
|
||||
first_email_address=first_email_address,
|
||||
replace=replace,
|
||||
),
|
||||
"stop": 0 if verification_status == "pending" else 1,
|
||||
"stop": 0 if verification_status == VerificationStatus.PENDING else 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -455,7 +456,7 @@ def get_service_verify_reply_to_address_partials(service_id, notification_id):
|
||||
methods=["GET"],
|
||||
endpoint="service_confirm_delete_email_reply_to",
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_edit_email_reply_to(service_id, reply_to_email_id):
|
||||
form = ServiceReplyToEmailForm()
|
||||
reply_to_email_address = current_service.get_email_reply_to_address(
|
||||
@@ -524,7 +525,7 @@ def service_edit_email_reply_to(service_id, reply_to_email_id):
|
||||
"/services/<uuid:service_id>/service-settings/email-reply-to/<uuid:reply_to_email_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_delete_email_reply_to(service_id, reply_to_email_id):
|
||||
service_api_client.delete_reply_to_email_address(
|
||||
service_id=current_service.id,
|
||||
@@ -537,7 +538,7 @@ def service_delete_email_reply_to(service_id, reply_to_email_id):
|
||||
"/services/<uuid:service_id>/service-settings/set-inbound-number",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_set_inbound_number(service_id):
|
||||
available_inbound_numbers = (
|
||||
inbound_number_client.get_available_inbound_sms_numbers()
|
||||
@@ -570,7 +571,7 @@ def service_set_inbound_number(service_id):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/sms-prefix", methods=["GET", "POST"]
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_set_sms_prefix(service_id):
|
||||
form = SMSPrefixForm(enabled=current_service.prefix_sms)
|
||||
|
||||
@@ -589,7 +590,7 @@ def service_set_sms_prefix(service_id):
|
||||
"/services/<uuid:service_id>/service-settings/set-international-sms",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_set_international_sms(service_id):
|
||||
form = ServiceOnOffSettingForm(
|
||||
"Send text messages to international phone numbers",
|
||||
@@ -610,7 +611,7 @@ def service_set_international_sms(service_id):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/set-inbound-sms", methods=["GET"]
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_set_inbound_sms(service_id):
|
||||
return render_template(
|
||||
"views/service-settings/set-inbound-sms.html",
|
||||
@@ -621,7 +622,7 @@ def service_set_inbound_sms(service_id):
|
||||
"/services/<uuid:service_id>/service-settings/set-<channel>",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_set_channel(service_id, channel):
|
||||
if channel not in {"email", "sms"}:
|
||||
abort(404)
|
||||
@@ -646,7 +647,7 @@ def service_set_channel(service_id, channel):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/service-settings/set-auth-type", methods=["GET"]
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_set_auth_type(service_id):
|
||||
return render_template(
|
||||
"views/service-settings/set-auth-type.html",
|
||||
@@ -654,7 +655,7 @@ def service_set_auth_type(service_id):
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/service-settings/sms-sender", methods=["GET"])
|
||||
@user_has_permissions("manage_service", "manage_api_keys")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE, "manage_api_keys")
|
||||
def service_sms_senders(service_id):
|
||||
return render_template(
|
||||
"views/service-settings/sms-senders.html",
|
||||
@@ -665,7 +666,7 @@ def service_sms_senders(service_id):
|
||||
"/services/<uuid:service_id>/service-settings/sms-sender/add",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_add_sms_sender(service_id):
|
||||
form = ServiceSmsSenderForm()
|
||||
first_sms_sender = current_service.count_sms_senders == 0
|
||||
@@ -693,7 +694,7 @@ def service_add_sms_sender(service_id):
|
||||
methods=["GET"],
|
||||
endpoint="service_confirm_delete_sms_sender",
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_edit_sms_sender(service_id, sms_sender_id):
|
||||
sms_sender = current_service.get_sms_sender(sms_sender_id)
|
||||
is_inbound_number = sms_sender["inbound_number_id"]
|
||||
@@ -731,7 +732,7 @@ def service_edit_sms_sender(service_id, sms_sender_id):
|
||||
"/services/<uuid:service_id>/service-settings/sms-sender/<uuid:sms_sender_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@user_has_permissions("manage_service")
|
||||
@user_has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
def service_delete_sms_sender(service_id, sms_sender_id):
|
||||
service_api_client.delete_sms_sender(
|
||||
service_id=current_service.id,
|
||||
|
||||
@@ -12,6 +12,7 @@ from app import (
|
||||
template_folder_api_client,
|
||||
template_statistics_client,
|
||||
)
|
||||
from app.enums import ServicePermission
|
||||
from app.formatters import character_count, message_count
|
||||
from app.main import main
|
||||
from app.main.forms import (
|
||||
@@ -117,7 +118,7 @@ def choose_template(service_id, template_type="all", template_folder_id=None):
|
||||
single_notification_channel = notification_channels[0]
|
||||
|
||||
if request.method == "POST" and templates_and_folders_form.validate_on_submit():
|
||||
if not current_user.has_permissions("manage_templates"):
|
||||
if not current_user.has_permissions(ServicePermission.MANAGE_TEMPLATES):
|
||||
abort(403)
|
||||
try:
|
||||
return process_folder_management_form(
|
||||
@@ -266,7 +267,7 @@ def _add_template_by_type(template_type, template_folder_id):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/templates/copy/from-service/<uuid:from_service>/from-folder/<uuid:from_folder>"
|
||||
)
|
||||
@user_has_permissions("manage_templates")
|
||||
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
|
||||
def choose_template_to_copy(
|
||||
service_id,
|
||||
from_service=None,
|
||||
@@ -298,7 +299,7 @@ def choose_template_to_copy(
|
||||
"/services/<uuid:service_id>/templates/copy/<uuid:template_id>",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_templates")
|
||||
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
|
||||
def copy_template(service_id, template_id):
|
||||
from_service = request.args.get("from_service")
|
||||
|
||||
@@ -355,7 +356,7 @@ def _get_template_copy_name(template, existing_templates):
|
||||
"<template_type:notification_type>/<return_to>/<uuid:template_id>"
|
||||
)
|
||||
)
|
||||
@user_has_permissions("manage_templates")
|
||||
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
|
||||
def action_blocked(service_id, notification_type, return_to, template_id=None):
|
||||
back_link = {
|
||||
"add_new_template": partial(
|
||||
@@ -387,7 +388,7 @@ def action_blocked(service_id, notification_type, return_to, template_id=None):
|
||||
"/services/<uuid:service_id>/templates/folders/<uuid:template_folder_id>/manage",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_templates")
|
||||
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
|
||||
def manage_template_folder(service_id, template_folder_id):
|
||||
template_folder = current_service.get_template_folder_with_user_permission_or_403(
|
||||
template_folder_id, current_user
|
||||
@@ -401,7 +402,7 @@ def manage_template_folder(service_id, template_folder_id):
|
||||
)
|
||||
if form.validate_on_submit():
|
||||
if (
|
||||
current_user.has_permissions("manage_service")
|
||||
current_user.has_permissions(ServicePermission.MANAGE_SERVICE)
|
||||
and form.users_with_permission.all_service_users
|
||||
):
|
||||
users_with_permission = form.users_with_permission.data + [current_user.id]
|
||||
@@ -437,7 +438,7 @@ def manage_template_folder(service_id, template_folder_id):
|
||||
"/services/<uuid:service_id>/templates/folders/<uuid:template_folder_id>/delete",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_templates")
|
||||
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
|
||||
def delete_template_folder(service_id, template_folder_id):
|
||||
template_folder = current_service.get_template_folder_with_user_permission_or_403(
|
||||
template_folder_id, current_user
|
||||
@@ -502,7 +503,7 @@ def delete_template_folder(service_id, template_folder_id):
|
||||
"/services/<uuid:service_id>/templates/folders/<uuid:template_folder_id>/add-<template_type:template_type>",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_templates")
|
||||
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
|
||||
def add_service_template(service_id, template_type, template_folder_id=None):
|
||||
if template_type not in current_service.available_template_types:
|
||||
return redirect(
|
||||
@@ -566,7 +567,7 @@ def abort_403_if_not_admin_user():
|
||||
"/services/<uuid:service_id>/templates/<uuid:template_id>/edit",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_templates")
|
||||
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
|
||||
def edit_service_template(service_id, template_id):
|
||||
template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
@@ -724,7 +725,7 @@ def _get_content_count_error_and_message_for_template(template):
|
||||
"/services/<uuid:service_id>/templates/<uuid:template_id>/delete",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_templates")
|
||||
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
|
||||
def delete_service_template(service_id, template_id):
|
||||
template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
@@ -782,7 +783,7 @@ def delete_service_template(service_id, template_id):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/templates/<uuid:template_id>/redact", methods=["GET"]
|
||||
)
|
||||
@user_has_permissions("manage_templates")
|
||||
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
|
||||
def confirm_redact_template(service_id, template_id):
|
||||
template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
@@ -803,7 +804,7 @@ def confirm_redact_template(service_id, template_id):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/templates/<uuid:template_id>/redact", methods=["POST"]
|
||||
)
|
||||
@user_has_permissions("manage_templates")
|
||||
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
|
||||
def redact_template(service_id, template_id):
|
||||
service_api_client.redact_service_template(service_id, template_id)
|
||||
|
||||
@@ -842,7 +843,7 @@ def view_template_versions(service_id, template_id):
|
||||
"/services/<uuid:service_id>/templates/<uuid:template_id>/set-template-sender",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("manage_templates")
|
||||
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
|
||||
def set_template_sender(service_id, template_id):
|
||||
template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from flask import abort, redirect, render_template, session
|
||||
|
||||
from app import current_service, current_user, service_api_client, url_for
|
||||
from app.enums import ServicePermission
|
||||
from app.main import main
|
||||
from app.main.views.send import (
|
||||
all_placeholders_in_session,
|
||||
@@ -14,7 +15,7 @@ from app.utils.user import user_has_permissions
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/tour/<uuid:template_id>")
|
||||
@user_has_permissions("send_messages")
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES)
|
||||
def begin_tour(service_id, template_id):
|
||||
db_template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
@@ -47,7 +48,7 @@ def begin_tour(service_id, template_id):
|
||||
"/services/<uuid:service_id>/tour/<uuid:template_id>/step-<int:step_index>",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
|
||||
def tour_step(service_id, template_id, step_index):
|
||||
db_template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
@@ -166,7 +167,7 @@ def _get_tour_step_back_link(service_id, template_id, step_index):
|
||||
@main.route(
|
||||
"/services/<uuid:service_id>/tour/<uuid:template_id>/check", methods=["GET"]
|
||||
)
|
||||
@user_has_permissions("send_messages", restrict_admin_usage=True)
|
||||
@user_has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True)
|
||||
def check_tour_notification(service_id, template_id):
|
||||
db_template = current_service.get_template_with_user_permission_or_403(
|
||||
template_id, current_user
|
||||
@@ -223,7 +224,7 @@ def check_tour_notification(service_id, template_id):
|
||||
|
||||
|
||||
@main.route("/services/<uuid:service_id>/end-tour/<uuid:example_template_id>")
|
||||
@user_has_permissions("manage_templates")
|
||||
@user_has_permissions(ServicePermission.MANAGE_TEMPLATES)
|
||||
def go_to_dashboard_after_tour(service_id, example_template_id):
|
||||
service_api_client.delete_service_template(service_id, example_template_id)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from app.enums import JobStatus, NotificationStatus
|
||||
from app.models import JSONModel, ModelList, PaginatedModelList
|
||||
from app.notify_client.job_api_client import job_api_client
|
||||
from app.notify_client.notification_api_client import notification_api_client
|
||||
@@ -32,11 +33,11 @@ class Job(JSONModel):
|
||||
|
||||
@property
|
||||
def cancelled(self):
|
||||
return self.status == "cancelled"
|
||||
return self.status == JobStatus.CANCELLED
|
||||
|
||||
@property
|
||||
def scheduled(self):
|
||||
return self.status == "scheduled"
|
||||
return self.status == JobStatus.SCHEDULED
|
||||
|
||||
@property
|
||||
def scheduled_for(self):
|
||||
@@ -61,16 +62,18 @@ class Job(JSONModel):
|
||||
|
||||
@property
|
||||
def notifications_delivered(self):
|
||||
return self._aggregate_statistics("delivered", "sent")
|
||||
return self._aggregate_statistics(
|
||||
NotificationStatus.DELIVERED, NotificationStatus.SENT
|
||||
)
|
||||
|
||||
@property
|
||||
def notifications_failed(self):
|
||||
return self._aggregate_statistics(
|
||||
"failed",
|
||||
"technical-failure",
|
||||
"temporary-failure",
|
||||
"permanent-failure",
|
||||
"cancelled",
|
||||
NotificationStatus.FAILED,
|
||||
NotificationStatus.TECHNICAL_FAILURE,
|
||||
NotificationStatus.TEMPORARY_FAILURE,
|
||||
NotificationStatus.PERMANENT_FAILURE,
|
||||
NotificationStatus.CANCELLED,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -95,7 +98,7 @@ class Job(JSONModel):
|
||||
|
||||
@property
|
||||
def still_processing(self):
|
||||
return self.status != "finished" or self.percentage_complete < 100
|
||||
return self.status != JobStatus.FINISHED or self.percentage_complete < 100
|
||||
|
||||
@cached_property
|
||||
def finished_processing(self):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from flask import abort, current_app
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from app.enums import ServicePermission
|
||||
from app.models import JSONModel, SortByNameMixin
|
||||
from app.models.job import ImmediateJobs, PaginatedJobs, PaginatedUploads, ScheduledJobs
|
||||
from app.models.organization import Organization
|
||||
@@ -175,7 +176,7 @@ class Service(JSONModel, SortByNameMixin):
|
||||
[
|
||||
user
|
||||
for user in self.team_members
|
||||
if user.has_permission_for_service(self.id, "manage_service")
|
||||
if user.has_permission_for_service(self.id, ServicePermission.MANAGE_SERVICE)
|
||||
]
|
||||
)
|
||||
> 1
|
||||
|
||||
@@ -5,6 +5,7 @@ from flask import abort, current_app, request, session
|
||||
from flask_login import AnonymousUserMixin, UserMixin, login_user, logout_user
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from app.enums import InvitedUserStatus
|
||||
from app.event_handlers import (
|
||||
create_add_user_to_service_event,
|
||||
create_set_user_permissions_event,
|
||||
@@ -566,7 +567,7 @@ class InvitedUser(JSONModel):
|
||||
# current_app.logger.warning(
|
||||
# f"Checking invited user {self.id} for permissions: {permissions}"
|
||||
# )
|
||||
if self.status == "cancelled":
|
||||
if self.status == InvitedUserStatus.CANCELLED:
|
||||
return False
|
||||
return set(self.permissions) > set(permissions)
|
||||
|
||||
@@ -574,7 +575,7 @@ class InvitedUser(JSONModel):
|
||||
# current_app.logger.warn(
|
||||
# f"Checking invited user {self.id} for permission: {permission} on service {service_id}"
|
||||
# )
|
||||
if self.status == "cancelled":
|
||||
if self.status == InvitedUserStatus.CANCELLED:
|
||||
return False
|
||||
return self.service == service_id and permission in self.permissions
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ from itertools import chain
|
||||
|
||||
from flask import request
|
||||
|
||||
from app.enums import ServicePermission
|
||||
|
||||
|
||||
class Navigation:
|
||||
mapping = {}
|
||||
@@ -74,7 +76,7 @@ class HeaderNavigation(Navigation):
|
||||
"delete_service_template",
|
||||
"edit_service_template",
|
||||
"manage_template_folder",
|
||||
"send_messages",
|
||||
ServicePermission.SEND_MESSAGES,
|
||||
"send_one_off",
|
||||
"send_one_off_step",
|
||||
"send_one_off_to_myself",
|
||||
@@ -177,7 +179,7 @@ class MainNavigation(Navigation):
|
||||
"delete_service_template",
|
||||
"edit_service_template",
|
||||
"manage_template_folder",
|
||||
"send_messages",
|
||||
ServicePermission.SEND_MESSAGES,
|
||||
"send_one_off",
|
||||
"send_one_off_step",
|
||||
"send_one_off_to_myself",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from app.enums import ApiKeyType
|
||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
|
||||
|
||||
# must match key types in notifications-api/app/models.py
|
||||
KEY_TYPE_NORMAL = "normal"
|
||||
KEY_TYPE_TEAM = "team"
|
||||
KEY_TYPE_TEST = "test"
|
||||
KEY_TYPE_NORMAL = ApiKeyType.NORMAL
|
||||
KEY_TYPE_TEAM = ApiKeyType.TEAM
|
||||
KEY_TYPE_TEST = ApiKeyType.TEST
|
||||
|
||||
|
||||
class ApiKeyApiClient(NotifyAdminAPIClient):
|
||||
|
||||
@@ -5,6 +5,7 @@ from urllib.parse import unquote
|
||||
from flask import current_app, request
|
||||
|
||||
from app import redis_client
|
||||
from app.enums import InvitedUserStatus
|
||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
|
||||
from app.utils.user_permissions import (
|
||||
all_ui_permissions,
|
||||
@@ -94,7 +95,7 @@ class InviteApiClient(NotifyAdminAPIClient):
|
||||
return self.get(url=f"/invite/service/check/{token}")["data"]
|
||||
|
||||
def cancel_invited_user(self, service_id, invited_user_id):
|
||||
data = {"status": "cancelled"}
|
||||
data = {"status": InvitedUserStatus.CANCELLED}
|
||||
data = _attach_current_user(data)
|
||||
self.post(url=f"/service/{service_id}/invite/{invited_user_id}", data=data)
|
||||
|
||||
@@ -131,7 +132,7 @@ class InviteApiClient(NotifyAdminAPIClient):
|
||||
@cache.delete("service-{service_id}")
|
||||
@cache.delete("user-{invited_user_id}")
|
||||
def accept_invite(self, service_id, invited_user_id):
|
||||
data = {"status": "accepted"}
|
||||
data = {"status": InvitedUserStatus.ACCEPTED}
|
||||
self.post(url=f"/service/{service_id}/invite/{invited_user_id}", data=data)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from app.enums import JobStatus
|
||||
from app.extensions import redis_client
|
||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
|
||||
from app.utils.csv import get_user_preferred_timezone
|
||||
@@ -8,17 +9,17 @@ from app.utils.csv import get_user_preferred_timezone
|
||||
|
||||
class JobApiClient(NotifyAdminAPIClient):
|
||||
JOB_STATUSES = {
|
||||
"scheduled",
|
||||
"pending",
|
||||
"in progress",
|
||||
"finished",
|
||||
"cancelled",
|
||||
"sending limits exceeded",
|
||||
"ready to send",
|
||||
"sent to dvla",
|
||||
JobStatus.SCHEDULED,
|
||||
JobStatus.PENDING,
|
||||
JobStatus.IN_PROGRESS,
|
||||
JobStatus.FINISHED,
|
||||
JobStatus.CANCELLED,
|
||||
JobStatus.SENDING_LIMITS_EXCEEDED,
|
||||
JobStatus.READY_TO_SEND,
|
||||
JobStatus.SENT_TO_DVLA,
|
||||
}
|
||||
SCHEDULED_JOB_STATUS = "scheduled"
|
||||
CANCELLED_JOB_STATUS = "cancelled"
|
||||
SCHEDULED_JOB_STATUS = JobStatus.SCHEDULED
|
||||
CANCELLED_JOB_STATUS = JobStatus.CANCELLED
|
||||
NON_CANCELLED_JOB_STATUSES = JOB_STATUSES - {CANCELLED_JOB_STATUS}
|
||||
NON_SCHEDULED_JOB_STATUSES = JOB_STATUSES - {
|
||||
SCHEDULED_JOB_STATUS,
|
||||
@@ -57,7 +58,7 @@ class JobApiClient(NotifyAdminAPIClient):
|
||||
job["original_file_name"],
|
||||
)
|
||||
for job in self.get_jobs(service_id, limit_days=0)["data"]
|
||||
if job["job_status"] != "cancelled"
|
||||
if job["job_status"] != JobStatus.CANCELLED
|
||||
)
|
||||
|
||||
def get_page_of_jobs(self, service_id, *, page, statuses=None, limit_days=None):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from app.enums import InvitedOrgUserStatus
|
||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user
|
||||
|
||||
|
||||
@@ -33,7 +34,7 @@ class OrgInviteApiClient(NotifyAdminAPIClient):
|
||||
return resp["data"]
|
||||
|
||||
def cancel_invited_user(self, org_id, invited_user_id):
|
||||
data = {"status": "cancelled"}
|
||||
data = {"status": InvitedOrgUserStatus.CANCELLED}
|
||||
data = _attach_current_user(data)
|
||||
self.post(
|
||||
url="/organization/{0}/invite/{1}".format(org_id, invited_user_id),
|
||||
@@ -41,7 +42,7 @@ class OrgInviteApiClient(NotifyAdminAPIClient):
|
||||
)
|
||||
|
||||
def accept_invite(self, org_id, invited_user_id):
|
||||
data = {"status": "accepted"}
|
||||
data = {"status": InvitedOrgUserStatus.ACCEPTED}
|
||||
self.post(
|
||||
url="/organization/{0}/invite/{1}".format(org_id, invited_user_id),
|
||||
data=data,
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
visuallyHiddenText: params.errorMessage.visuallyHiddenText
|
||||
}) | indent(2) | trim }}
|
||||
{% endif %}
|
||||
<div class="govuk-checkboxes {%- if params.classes %} {{ params.classes }}{% endif %}"
|
||||
<div class="usa-checkbox-group {%- if params.classes %} {{ params.classes }}{% endif %}"
|
||||
{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %}
|
||||
{%- if isConditional %} data-module="checkboxes"{% endif -%}>
|
||||
{% for item in params.items %}
|
||||
@@ -60,8 +60,8 @@
|
||||
{% set itemHintId = id + "-item-hint" if hasHint else "" %}
|
||||
{% set itemDescribedBy = describedBy if not hasFieldset else "" %}
|
||||
{% set itemDescribedBy = (itemDescribedBy + " " + itemHintId) | trim %}
|
||||
<div class="govuk-checkboxes__item">
|
||||
<input class="govuk-checkboxes__input" id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ item.value }}"
|
||||
<div class="usa-checkbox">
|
||||
<input class="usa-checkbox__input" id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ item.value }}"
|
||||
{{-" checked" if item.checked }}
|
||||
{{-" disabled" if item.disabled }}
|
||||
{%- if item.conditional %} data-aria-controls="{{ conditionalId }}"{% endif -%}
|
||||
@@ -70,14 +70,14 @@
|
||||
{{ usaLabel({
|
||||
html: item.html,
|
||||
text: item.text,
|
||||
classes: 'govuk-checkboxes__label' + (' ' + item.label.classes if item.label.classes),
|
||||
classes: 'usa-checkbox__label' + (' ' + item.label.classes if item.label.classes),
|
||||
attributes: item.label.attributes,
|
||||
for: id
|
||||
}) | indent(6) | trim }}
|
||||
{%- if hasHint %}
|
||||
{{ usaHint({
|
||||
id: itemHintId,
|
||||
classes: 'govuk-checkboxes__hint',
|
||||
classes: 'usa-checkbox__label-description',
|
||||
attributes: item.hint.attributes,
|
||||
html: item.hint.html,
|
||||
text: item.hint.text
|
||||
@@ -85,7 +85,7 @@
|
||||
{%- endif %}
|
||||
</div>
|
||||
{% if item.conditional %}
|
||||
<div class="govuk-checkboxes__conditional{% if not item.checked %} govuk-checkboxes__conditional--hidden{% endif %}" id="{{ conditionalId }}">
|
||||
<div class="usa-checkbox__conditional{% if not item.checked %} display-none{% endif %}" id="{{ conditionalId }}">
|
||||
{{ item.conditional.html | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if current_service %}
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
|
||||
{% set secondaryNavigation = [
|
||||
{"href": url_for('main.service_settings', service_id=current_service.id), "text": "Settings", "active": secondary_navigation.is_selected('settings')},
|
||||
{"href": url_for('main.sign_out'), "text": "Sign out"}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
|
||||
{% set button_data = {"text": button_text} %}
|
||||
{% if destructive %}{% set _ = button_data.update({"classes": "govuk-button--warning page-footer__button"}) %}{% endif %}
|
||||
{% if destructive %}{% set _ = button_data.update({"classes": "usa-button usa-button--secondary page-footer__button"}) %}{% endif %}
|
||||
{% if centered_button %}{% set _ = button_data.update({"classes": "page-footer__button--centred"}) %}{% endif %}
|
||||
{% if button_name %}{% set _ = button_data.update({"name": button_name}) %}{% endif %}
|
||||
{% if button_value %}{% set _ = button_data.update({"value": button_value}) %}{% endif %}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class="radio-select" data-module="radio-select" data-categories="{{ field.categories|join(',') }}" data-show-now-as-default="{{ show_now_as_default|string|lower }}">
|
||||
<div class="radio-select-column">
|
||||
{% for option in field %}
|
||||
<div class="multiple-choice">
|
||||
<div class="usa-radio">
|
||||
{{ option }}
|
||||
<label for="{{ option.id }}">
|
||||
{{ option.label.text }}
|
||||
|
||||
@@ -9,18 +9,19 @@
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro select_list(options, child_map, disable=[], option_hints={}, input="radio") %}
|
||||
<ul>
|
||||
{% macro select_list(options, child_map, disable=[], option_hints={}, input="radio", indent_level=0) %}
|
||||
{% for option in options %}
|
||||
{% if child_map[option.data] %}
|
||||
{% call select_input(option, disable, option_hints, as_list_item=True, input=input) %}
|
||||
{{ select_list(child_map[option.data], child_map, disable, option_hints, input=input) }}
|
||||
{% endcall %}
|
||||
<div class="{% if indent_level > 0 %}margin-left-{{ indent_level * 4 }}{% endif %}">
|
||||
{{ select_input(option, disable, option_hints, as_list_item=False, input=input) }}
|
||||
</div>
|
||||
{{ select_list(child_map[option.data], child_map, disable, option_hints, input=input, indent_level=indent_level + 1) }}
|
||||
{% else %}
|
||||
{{ select_input(option, disable, option_hints, as_list_item=True, input=input) }}
|
||||
<div class="{% if indent_level > 0 %}margin-left-{{ indent_level * 4 }}{% endif %}">
|
||||
{{ select_input(option, disable, option_hints, as_list_item=False, input=input) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
@@ -63,11 +64,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro select_input(option, disable=[], option_hints={}, data_target=None, as_list_item=False, input="radio") %}
|
||||
{% if as_list_item %}
|
||||
<li class="multiple-choice test-one" {% if data_target %}data-target="{{ data_target }}"{% endif %}>
|
||||
{% else %}
|
||||
<div class="usa-radio" {% if data_target %}data-target="{{ data_target }}"{% endif %}>
|
||||
{% endif %}
|
||||
<input
|
||||
id="{{ option.id }}" class="usa-radio__input" name="{{ option.name }}" type="{{ input }}" value="{{ option.data }}"
|
||||
{% if option.data in disable %}
|
||||
@@ -80,17 +77,13 @@
|
||||
<label class="usa-radio__label" for="{{ option.id }}">
|
||||
{{ option.label.text }}
|
||||
{% if option_hints[option.data] %}
|
||||
<div class="block-label-hint">
|
||||
<span class="usa-hint">
|
||||
{{ option_hints[option.data] }}
|
||||
</div>
|
||||
</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{% if caller %}
|
||||
{{ caller() }}
|
||||
{% endif %}
|
||||
{% if as_list_item %}
|
||||
</li>
|
||||
{% else %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{% from "components/components/error-message/macro.njk" import usaErrorMessage -%}
|
||||
{% from "components/components/fieldset/macro.njk" import govukFieldset %}
|
||||
{% from "components/components/hint/macro.njk" import usaHint %}
|
||||
{% from "components/components/radio-label/macro.njk" import usaRadioLabel %}
|
||||
{% from "components/components/label/macro.njk" import usaLabel %}
|
||||
|
||||
|
||||
{#- Copied from https://github.com/alphagov/govuk-frontend/blob/v2.13.0/src/components/checkboxes/template.njk
|
||||
Changes:
|
||||
- `formGroup` option to control whether or not the checkboxes are wrapped with a `usa-form-group` class
|
||||
- `classes` option added to `item` allow custom classes on the `.govuk-checkboxes__item` element
|
||||
- `classes` option added to `item.hint` allow custom classes on the `.usa-hint` element (added to GOVUK Frontend in v3.5.0 - remove when we update)
|
||||
- `asList` option added the root `params` object to allow setting of the `.govuk-checkboxes` and `.govuk-checkboxes__item` element types
|
||||
- `children` option added to `item` allowing the sending in of prerendered child checkboxes (allowing the creation of tree structures through recursion) -#}
|
||||
{#- Updated from GOV.UK components to USWDS patterns
|
||||
Changed from original GOV.UK template:
|
||||
- Uses USWDS checkbox classes (usa-checkbox, usa-checkbox__input, usa-checkbox__label)
|
||||
- Conditional content uses display-none utility class instead of govuk-specific classes
|
||||
- Label uses usaLabel macro instead of radio label
|
||||
- Hint text uses usa-checkbox__label-description class
|
||||
- Maintains all original functionality including conditional reveals and nested checkboxes -#}
|
||||
{#- If an id 'prefix' is not passed, fall back to using the name attribute
|
||||
instead. We need this for error messages and hints as well -#}
|
||||
{% set idPrefix = params.idPrefix if params.idPrefix else params.name %}
|
||||
@@ -92,17 +92,17 @@
|
||||
{%- if item.conditional %} data-aria-controls="{{ conditionalId }}"{% endif -%}
|
||||
{%- if itemDescribedBy %} aria-describedby="{{ itemDescribedBy }}"{% endif -%}
|
||||
{%- for attribute, value in item.attributes %} {{ attribute }}="{{ value }}"{% endfor -%}>
|
||||
{{ usaRadioLabel({
|
||||
{{ usaLabel({
|
||||
html: item.html,
|
||||
text: item.text,
|
||||
classes: (' ' + item.label.classes if item.label.classes),
|
||||
classes: 'usa-checkbox__label' + (' ' + item.label.classes if item.label.classes),
|
||||
attributes: item.label.attributes,
|
||||
for: id
|
||||
}) | indent(6) | trim }}
|
||||
{%- if hasHint %}
|
||||
{{ usaHint({
|
||||
id: itemHintId,
|
||||
classes: 'govuk-checkboxes__hint' + (' ' + item.hint.classes if item.hint.classes),
|
||||
classes: 'usa-checkbox__label-description' + (' ' + item.hint.classes if item.hint.classes),
|
||||
attributes: item.hint.attributes,
|
||||
html: item.hint.html,
|
||||
text: item.hint.text
|
||||
@@ -112,14 +112,14 @@
|
||||
{{ item.children | safe }}
|
||||
{%- endif %}
|
||||
{% if params.asList and item.conditional %}
|
||||
<div class="govuk-checkboxes__conditional{% if not item.checked %} govuk-checkboxes__conditional--hidden{% endif %}" id="{{ conditionalId }}">
|
||||
<div class="usa-checkbox__conditional{% if not item.checked %} display-none{% endif %}" id="{{ conditionalId }}">
|
||||
{{ item.conditional.html | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- if item.after %}{{ item.after }}{% endif -%}
|
||||
</{{ groupItemElement }}>
|
||||
{% if not params.asList and item.conditional %}
|
||||
<div class="govuk-checkboxes__conditional{% if not item.checked %} govuk-checkboxes__conditional--hidden{% endif %}" id="{{ conditionalId }}">
|
||||
<div class="usa-checkbox__conditional{% if not item.checked %} display-none{% endif %}" id="{{ conditionalId }}">
|
||||
{{ item.conditional.html | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"element": "a",
|
||||
"text": "Create an API key",
|
||||
"href": url_for('.create_api_key', service_id=current_service.id),
|
||||
"classes": "govuk-button--secondary"
|
||||
"classes": "usa-button"
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
'messages',
|
||||
) }}
|
||||
|
||||
{% if current_user.has_permissions('send_messages') %}
|
||||
{% if current_user.has_permissions(ServicePermission.SEND_MESSAGES) %}
|
||||
<p class="sms-message-reply-link">
|
||||
<a class="usa-link" href="{{ url_for('.conversation_reply', service_id=current_service.id, notification_id=notification_id) }}">Send a text message to this phone number</a>
|
||||
</p>
|
||||
|
||||
@@ -43,7 +43,7 @@ Dashboard
|
||||
<div id="totalMessageTable" class="margin-0"></div>
|
||||
{% include 'views/dashboard/activity-table.html' %}
|
||||
|
||||
{% if current_user.has_permissions('manage_service') %}{% endif %}
|
||||
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}{% endif %}
|
||||
|
||||
{% include 'views/dashboard/most-used-templates.html' %}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<h1 class="font-body-2xl margin-0">
|
||||
Team members
|
||||
</h1>
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
|
||||
{{ usaButton({
|
||||
"element": "a",
|
||||
"text": "Invite a team member",
|
||||
@@ -108,7 +108,7 @@
|
||||
|
||||
|
||||
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
|
||||
{% if user.status == 'pending' or user.status == 'expired' %}
|
||||
<a class="user-list-edit-link usa-link padding-x-2" href="{{ url_for('.cancel_invited_user', service_id=current_service.id, invited_user_id=user.id) }}">
|
||||
Cancel invitation<span class="usa-sr-only"> for {{ user.email_address }}</span>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
{{ ajax_block(partials, updates_url, 'status', finished=finished) }}
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.has_permissions('send_messages') and current_user.has_permissions('view_activity') and template.template_type == 'sms' and can_receive_inbound %}
|
||||
{% if current_user.has_permissions(ServicePermission.SEND_MESSAGES) and current_user.has_permissions('view_activity') and template.template_type == 'sms' and can_receive_inbound %}
|
||||
<p>
|
||||
<a class="usa-link" href="{{ url_for('.conversation', service_id=current_service.id, notification_id=notification_id, _anchor='n{}'.format(notification_id)) }}">See all text messages sent to this phone number</a>
|
||||
</p>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"element": "a",
|
||||
"text": "New organization",
|
||||
"href": url_for('main.add_organization'),
|
||||
"classes": "govuk-button--secondary"
|
||||
"classes": "usa-button"
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"element": "a",
|
||||
"text": "Invite team member",
|
||||
"href": url_for('.invite_org_user', org_id=current_org.id),
|
||||
"classes": "govuk-button--secondary"
|
||||
"classes": "usa-button"
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="govuk-heading-m">
|
||||
<h2 class="font-body-lg">
|
||||
Messages sent since May 2023
|
||||
</h2>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="govuk-heading-m">
|
||||
<h2 class="font-body-lg">
|
||||
Messages sent within 10 seconds
|
||||
</h2>
|
||||
<div class="grid-row">
|
||||
@@ -106,7 +106,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="govuk-heading-m">
|
||||
<h2 class="font-body-lg">
|
||||
Organizations using Notify
|
||||
</h2>
|
||||
<div class="grid-row bottom-gutter">
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
{{ edit_field(
|
||||
'Change',
|
||||
url_for('.service_name_change', service_id=current_service.id),
|
||||
permissions=['manage_service'],
|
||||
permissions=[ServicePermission.MANAGE_SERVICE],
|
||||
suffix='service name',
|
||||
)
|
||||
}}
|
||||
@@ -46,7 +46,7 @@
|
||||
service_id=current_service.id,
|
||||
channel='sms'
|
||||
),
|
||||
permissions=['manage_service'],
|
||||
permissions=[ServicePermission.MANAGE_SERVICE],
|
||||
suffix='your settings for sending text messages',
|
||||
)}} #}
|
||||
{% endcall %}
|
||||
@@ -65,7 +65,7 @@
|
||||
{{ edit_field(
|
||||
'Manage',
|
||||
url_for('.service_sms_senders', service_id=current_service.id),
|
||||
permissions=['manage_service','manage_api_keys'],
|
||||
permissions=[ServicePermission.MANAGE_SERVICE,'manage_api_keys'],
|
||||
suffix='text message senders',
|
||||
)
|
||||
}}
|
||||
@@ -78,7 +78,7 @@
|
||||
{{ edit_field(
|
||||
'Change',
|
||||
url_for('.service_set_sms_prefix', service_id=current_service.id),
|
||||
permissions=['manage_service'],
|
||||
permissions=[ServicePermission.MANAGE_SERVICE],
|
||||
suffix='your settings for starting text messages with service name',
|
||||
)
|
||||
}}
|
||||
@@ -91,7 +91,7 @@
|
||||
{{ edit_field(
|
||||
'Change',
|
||||
url_for('.service_set_international_sms', service_id=current_service.id),
|
||||
permissions=['manage_service'],
|
||||
permissions=[ServicePermission.MANAGE_SERVICE],
|
||||
suffix='your settings for sending international text messages',
|
||||
)
|
||||
}}
|
||||
@@ -104,7 +104,7 @@
|
||||
{{ edit_field(
|
||||
'Change',
|
||||
url_for('.service_set_inbound_sms', service_id=current_service.id),
|
||||
permissions=['manage_service'],
|
||||
permissions=[ServicePermission.MANAGE_SERVICE],
|
||||
suffix='your settings for receiving text messages',
|
||||
)
|
||||
}}
|
||||
@@ -128,7 +128,7 @@
|
||||
channel='email',
|
||||
service_id=current_service.id
|
||||
),
|
||||
permissions=['manage_service'],
|
||||
permissions=[ServicePermission.MANAGE_SERVICE],
|
||||
suffix='your settings for sending emails',
|
||||
)}}
|
||||
{% endcall %}
|
||||
@@ -146,7 +146,7 @@
|
||||
{{ edit_field(
|
||||
'Manage',
|
||||
url_for('.service_email_reply_to', service_id=current_service.id),
|
||||
permissions=['manage_service','manage_api_keys'],
|
||||
permissions=[ServicePermission.MANAGE_SERVICE,'manage_api_keys'],
|
||||
suffix='reply-to email addresses',
|
||||
)
|
||||
}}
|
||||
@@ -158,7 +158,7 @@
|
||||
{{ edit_field(
|
||||
'Manage',
|
||||
url_for('.send_files_by_email_contact_details', service_id=current_service.id),
|
||||
permissions=['manage_service'],
|
||||
permissions=[ServicePermission.MANAGE_SERVICE],
|
||||
suffix='sending files by email',
|
||||
)}}
|
||||
{% endcall %}
|
||||
@@ -178,7 +178,7 @@
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
|
||||
To remove these restrictions, you can send us a
|
||||
<a class="usa-link" href="mailto:notify-support@gsa.gov">request to go live</a>.
|
||||
{% else %}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</h2>
|
||||
</div>
|
||||
<div class="grid-col-3">
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
|
||||
<a class="usa-link user-list-edit-link" href="{{ url_for('.service_edit_email_reply_to', service_id =current_service.id, reply_to_email_id = item.id) }}">
|
||||
Change<span class="usa-sr-only"> {{ item.email_address }}</span>
|
||||
</a>
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-12">
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
|
||||
<div class="js-stick-at-bottom-when-scrolling">
|
||||
{{ usaButton({
|
||||
"element": "a",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</h2>
|
||||
</div>
|
||||
<div class="grid-col">
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
|
||||
<a class="usa-link user-list-edit-link" href="{{ url_for('.service_edit_sms_sender', service_id=current_service.id, sms_sender_id = item.id) }}">
|
||||
Change<span class="usa-sr-only"> {{ item.sms_sender }}</span>
|
||||
</a>
|
||||
@@ -52,7 +52,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if current_user.has_permissions('manage_service') %}
|
||||
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) %}
|
||||
<div class="grid-row bottom-gutter">
|
||||
<div class="grid-col-12">
|
||||
{{ usaButton({
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<p class="hint">
|
||||
This template was deleted {{ template._template.updated_at|format_datetime_relative }}.
|
||||
</p>
|
||||
{% elif not current_user.has_permissions('send_messages', 'manage_api_keys', 'manage_templates', 'manage_service') or not user_has_template_permission %}
|
||||
{% elif not current_user.has_permissions(ServicePermission.SEND_MESSAGES, 'manage_api_keys', 'manage_templates', ServicePermission.MANAGE_SERVICE) or not user_has_template_permission %}
|
||||
<p class="top-gutter-1-3 {% if template.template_type != 'sms' %}bottom-gutter{% endif %}">
|
||||
If you need to send this
|
||||
{{ 1|message_count_label(template.template_type, suffix='') }}
|
||||
@@ -17,7 +17,7 @@
|
||||
{% else %}
|
||||
<div class="grid-row margin-bottom-5">
|
||||
<div class="usa-button-group">
|
||||
{% if current_user.has_permissions('send_messages', restrict_admin_usage=True) %}
|
||||
{% if current_user.has_permissions(ServicePermission.SEND_MESSAGES, restrict_admin_usage=True) %}
|
||||
<a href="{{ url_for(".set_sender", service_id=current_service.id, template_id=template.id) }}" class="usa-button usa-pill-separate-item">
|
||||
Use this template
|
||||
</a>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
{{ form.name(param_extensions={
|
||||
"classes": ""
|
||||
}) }}
|
||||
{% if current_user.has_permissions("manage_service") and form.users_with_permission.all_service_users %}
|
||||
{% if current_user.has_permissions(ServicePermission.MANAGE_SERVICE) and form.users_with_permission.all_service_users %}
|
||||
{{ form.users_with_permission }}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/components/button/macro.njk" import usaButton %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
{% from "components/webauthn-api-check.html" import webauthn_api_check %}
|
||||
{% from "components/components/error-message/macro.njk" import usaErrorMessage %}
|
||||
|
||||
{% set page_title = 'Get your security key' %}
|
||||
|
||||
{% block extra_javascripts_before_body %}
|
||||
{{ webauthn_api_check() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{{ page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": url_for('.user_profile') }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{{ usaErrorMessage({
|
||||
"classes": "banner-dangerous display-none",
|
||||
"html": (
|
||||
'There’s a problem with your security key' +
|
||||
'<p>Check you have the right key and try again. ' +
|
||||
'If this does not work, ' +
|
||||
'<a class="usa-link" href=' + url_for('main.support') + ">contact us</a>." +
|
||||
'</p>'
|
||||
),
|
||||
"attributes": {
|
||||
"aria-live": "polite",
|
||||
"tabindex": '-1'
|
||||
}
|
||||
}) }}
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-6">
|
||||
{{ page_header(page_title) }}
|
||||
|
||||
<p>
|
||||
You need to have your security key to sign in.
|
||||
</p>
|
||||
|
||||
{{ usaButton({
|
||||
"element": "button",
|
||||
"text": "Check security key",
|
||||
"classes": "govuk-button--secondary webauthn__api-required",
|
||||
"attributes": {
|
||||
"data-module": "authenticate-security-key",
|
||||
"data-csrf-token": csrf_token(),
|
||||
}
|
||||
}) }}
|
||||
|
||||
{{ usaErrorMessage({
|
||||
"classes": "webauthn__api-missing",
|
||||
"text": "Your browser does not support security keys. Try signing in to Notify using a different browser."
|
||||
}) }}
|
||||
|
||||
{{ usaErrorMessage({
|
||||
"classes": "webauthn__no-js",
|
||||
"text": "JavaScript is not available for this page. Security keys need JavaScript to work."
|
||||
}) }}
|
||||
</div>
|
||||
<div class="grid-col-3">
|
||||
<img src="{{ asset_url('images/security-key.svg') }}" alt="" class="webauthn-illustration" width="149" height="150">
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,34 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
|
||||
{% set page_title = 'Manage ' + '‘' + security_key.name + '’' %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{{ page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": url_for('.user_profile_security_keys') }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
{{ page_header(page_title) }}
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-9">
|
||||
{% call form_wrapper(autocomplete=True) %}
|
||||
{{ form.security_key_name }}
|
||||
{{ page_footer(
|
||||
'Save',
|
||||
delete_link=url_for(
|
||||
'.user_profile_confirm_delete_security_key', key_id=security_key.id),
|
||||
delete_link_text='Delete'
|
||||
) }}
|
||||
{% endcall %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,107 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/page-header.html" import page_header %}
|
||||
{% from "components/components/button/macro.njk" import usaButton %}
|
||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||
{% from "components/table.html" import edit_field, mapping_table, row, field, row_heading %}
|
||||
{% from "components/webauthn-api-check.html" import webauthn_api_check %}
|
||||
{% from "components/components/error-message/macro.njk" import usaErrorMessage %}
|
||||
|
||||
{% set page_title = 'Security keys' %}
|
||||
{% set credentials = current_user.webauthn_credentials %}
|
||||
|
||||
{% block extra_javascripts_before_body %}
|
||||
{{ webauthn_api_check() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block per_page_title %}
|
||||
{{ page_title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block backLink %}
|
||||
{{ usaBackLink({ "href": url_for('.user_profile') }) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincolumn_content %}
|
||||
|
||||
{% set webauthn_button %}
|
||||
{{ usaButton({
|
||||
"element": "button",
|
||||
"text": "Register a key",
|
||||
"classes": "govuk-button--secondary webauthn__api-required",
|
||||
"attributes": {
|
||||
"data-module": "register-security-key",
|
||||
"data-csrf-token": csrf_token(),
|
||||
}
|
||||
}) }}
|
||||
|
||||
{{ usaErrorMessage({
|
||||
"classes": "webauthn__api-missing",
|
||||
"text": "Your browser does not support security keys. Try signing in to Notify using a different browser."
|
||||
}) }}
|
||||
|
||||
{{ usaErrorMessage({
|
||||
"classes": "webauthn__no-js",
|
||||
"text": "JavaScript is not available for this page. Security keys need JavaScript to work."
|
||||
}) }}
|
||||
{% endset %}
|
||||
|
||||
<div class="grid-row">
|
||||
|
||||
{{ usaErrorMessage({
|
||||
"classes": "banner-dangerous display-none",
|
||||
"html": (
|
||||
'There’s a problem with your security key' +
|
||||
'<p>Check you have the right key and try again. ' +
|
||||
'If this does not work, ' +
|
||||
'<a class="usa-link" href=' + url_for('main.support') + ">contact us</a>." +
|
||||
'</p>'
|
||||
),
|
||||
"attributes": {
|
||||
"aria-live": "polite",
|
||||
"tabindex": '-1'
|
||||
}
|
||||
}) }}
|
||||
|
||||
{% if credentials %}
|
||||
<div class="grid-col-10">
|
||||
{{ page_header(page_title) }}
|
||||
<div class="body-copy-table">
|
||||
{% call mapping_table(
|
||||
caption=page_title,
|
||||
field_headings=['Security key details', 'Action'],
|
||||
field_headings_visible=False,
|
||||
caption_visible=False,
|
||||
) %}
|
||||
{% for credential in credentials %}
|
||||
{% call row() %}
|
||||
{% call field() %}
|
||||
<div class="usa-body">{{ credential.name }}</div>
|
||||
<div class="usa-body hint">Registered {{ credential.created_at|format_delta }}</div>
|
||||
{% endcall %}
|
||||
{{ edit_field('Manage', url_for('.user_profile_manage_security_key', key_id=credential.id)) }}
|
||||
{% endcall %}
|
||||
{% endfor %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
{{ webauthn_button }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid-col-6">
|
||||
{{ page_header(page_title) }}
|
||||
<p>
|
||||
Security keys are an alternative way of signing in to Notify,
|
||||
instead of getting a code in a text message
|
||||
</p>
|
||||
<p>
|
||||
You can buy any key that’s compatible with the WebAuthn
|
||||
standard.
|
||||
</p>
|
||||
{{ webauthn_button }}
|
||||
</div>
|
||||
<div class="grid-col-3">
|
||||
<img src="{{ asset_url('images/security-key.svg') }}" alt="" class="webauthn-illustration" width="149" height="150">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -7,16 +7,21 @@ from ordered_set import OrderedSet
|
||||
from werkzeug.datastructures import MultiDict
|
||||
from werkzeug.routing import RequestRedirect
|
||||
|
||||
from app.enums import NotificationStatus, ServicePermission
|
||||
from notifications_utils.field import Field
|
||||
|
||||
SENDING_STATUSES = ["created", "pending", "sending"]
|
||||
DELIVERED_STATUSES = ["delivered", "sent"]
|
||||
SENDING_STATUSES = [
|
||||
NotificationStatus.CREATED,
|
||||
NotificationStatus.PENDING,
|
||||
NotificationStatus.SENDING,
|
||||
]
|
||||
DELIVERED_STATUSES = [NotificationStatus.DELIVERED, NotificationStatus.SENT]
|
||||
FAILURE_STATUSES = [
|
||||
"failed",
|
||||
"temporary-failure",
|
||||
"permanent-failure",
|
||||
"technical-failure",
|
||||
"validation-failed",
|
||||
NotificationStatus.FAILED,
|
||||
NotificationStatus.TEMPORARY_FAILURE,
|
||||
NotificationStatus.PERMANENT_FAILURE,
|
||||
NotificationStatus.TECHNICAL_FAILURE,
|
||||
NotificationStatus.VALIDATION_FAILED,
|
||||
]
|
||||
REQUESTED_STATUSES = SENDING_STATUSES + DELIVERED_STATUSES + FAILURE_STATUSES
|
||||
|
||||
@@ -78,7 +83,7 @@ def unicode_truncate(s, length):
|
||||
|
||||
def should_skip_template_page(db_template):
|
||||
return (
|
||||
current_user.has_permissions("send_messages")
|
||||
current_user.has_permissions(ServicePermission.SEND_MESSAGES)
|
||||
and not current_user.has_permissions("manage_templates", "manage_api_keys")
|
||||
and not db_template["archived"]
|
||||
)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from itertools import chain
|
||||
|
||||
from app.enums import ServicePermission
|
||||
|
||||
permission_mappings = {
|
||||
# TODO: consider turning off email-sending permissions during SMS pilot
|
||||
"send_messages": ["send_texts", "send_emails"],
|
||||
ServicePermission.SEND_MESSAGES: ["send_texts", "send_emails"],
|
||||
"manage_templates": ["manage_templates"],
|
||||
"manage_service": ["manage_users", "manage_settings"],
|
||||
ServicePermission.MANAGE_SERVICE: ["manage_users", "manage_settings"],
|
||||
"manage_api_keys": ["manage_api_keys"],
|
||||
"view_activity": ["view_activity"],
|
||||
}
|
||||
@@ -14,9 +16,9 @@ all_db_permissions = set(chain(*permission_mappings.values()))
|
||||
|
||||
permission_options = (
|
||||
("view_activity", "See dashboard"),
|
||||
("send_messages", "Send messages"),
|
||||
(ServicePermission.SEND_MESSAGES, "Send messages"),
|
||||
("manage_templates", "Add and edit templates"),
|
||||
("manage_service", "Manage settings, team and usage"),
|
||||
(ServicePermission.MANAGE_SERVICE, "Manage settings, team and usage"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 325 KiB |
|
After Width: | Height: | Size: 505 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 358 KiB After Width: | Height: | Size: 362 KiB |
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 532 KiB After Width: | Height: | Size: 532 KiB |
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
Before Width: | Height: | Size: 935 KiB After Width: | Height: | Size: 938 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 472 KiB After Width: | Height: | Size: 476 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 379 KiB After Width: | Height: | Size: 381 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 454 KiB |
|
Before Width: | Height: | Size: 466 KiB After Width: | Height: | Size: 468 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 451 KiB After Width: | Height: | Size: 452 KiB |
@@ -403,7 +403,7 @@ class SMSPreviewTemplate(BaseSMSTemplate):
|
||||
.then(nl2br)
|
||||
.then(
|
||||
autolink_urls,
|
||||
classes="govuk-link govuk-link--no-visited-state",
|
||||
classes="usa-link usa-link--external",
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
### What is it?
|
||||
|
||||
This is a simple static error page to present to users in case of a (planned) downtime.
|
||||
It is deployed as an individual app and remains dormant until a route is assigned to it.
|
||||
|
||||
|
||||
### How do I use it?
|
||||
|
||||
It should already be deployed, but if not you can deploy it by running
|
||||
|
||||
cf push notify-admin-failwhale
|
||||
|
||||
To enable it you need to run
|
||||
|
||||
make <environment> enable-failwhale
|
||||
|
||||
and to disable it
|
||||
|
||||
make <environment> disable-failwhale
|
||||
|
||||
|
||||
Where `<environment>` is any of
|
||||
|
||||
- preview
|
||||
- staging
|
||||
- production
|
||||
@@ -1,130 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>
|
||||
Sorry, the service is unavailable – Notify.gov
|
||||
</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#0b0c0c" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
|
||||
<link rel="shortcut icon" sizes="16x16 32x32 48x48"
|
||||
href="/static_503/images/favicon.ico?de7abc5226925203ac10b0a4a94af949" type="image/x-icon" />
|
||||
<link rel="mask-icon" href="/static_503/images/usa-mask-icon.svg?2afd125b1db96109388d4a0727312f3e" color="#0b0c0c">
|
||||
<link rel="apple-touch-icon" sizes="180x180"
|
||||
href="/static_503/images/apple-touch-icon.png?a0f7e1b728a42016b247dc54ee40d055">
|
||||
|
||||
<link rel="stylesheet" media="screen" href="/static_503/stylesheets/main.css?d077f86473501ab244de0b30600536ee" />
|
||||
<link rel="stylesheet" media="print" href="/static_503/stylesheets/print.css?28010888ca5719dc83d7c2c80f6ed2b2" />
|
||||
<style>
|
||||
.govuk-header__container {
|
||||
border-color: #005EA5
|
||||
}
|
||||
</style>
|
||||
|
||||
<meta property="og:image" content="/static_503/images/govuk-opengraph-image.png">
|
||||
</head>
|
||||
|
||||
<body class="govuk-template__body ">
|
||||
<a href="#main-content" class="govuk-skip-link">Skip to main content</a>
|
||||
<header class="govuk-header " role="banner" data-module="header">
|
||||
<div class="govuk-header__container govuk-width-container">
|
||||
|
||||
<div class="govuk-header__logo">
|
||||
<a href="/accounts-or-dashboard" class="govuk-header__link govuk-header__link--homepage">
|
||||
<span class="govuk-header__logotype">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="33"
|
||||
viewBox="0 0 7410 3900">
|
||||
<rect width="7410" height="3900" fill="#b22234" />
|
||||
<path d="M0,450H7410m0,600H0m0,600H7410m0,600H0m0,600H7410m0,600H0" stroke="#fff" stroke-width="300" />
|
||||
<rect width="2964" height="2100" fill="#3c3b6e" />
|
||||
<g fill="#fff">
|
||||
<g id="s18">
|
||||
<g id="s9">
|
||||
<g id="s5">
|
||||
<g id="s4">
|
||||
<path id="s"
|
||||
d="M247,90 317.534230,307.082039 132.873218,172.917961H361.126782L176.465770,307.082039z" />
|
||||
<use xlink:href="#s" y="420" />
|
||||
<use xlink:href="#s" y="840" />
|
||||
<use xlink:href="#s" y="1260" />
|
||||
</g>
|
||||
<use xlink:href="#s" y="1680" />
|
||||
</g>
|
||||
<use xlink:href="#s4" x="247" y="210" />
|
||||
</g>
|
||||
<use xlink:href="#s9" x="494" />
|
||||
</g>
|
||||
<use xlink:href="#s18" x="988" />
|
||||
<use xlink:href="#s9" x="1976" />
|
||||
<use xlink:href="#s5" x="2470" />
|
||||
</g>
|
||||
</svg>
|
||||
<span class="govuk-header__logotype-text">US</span>
|
||||
</span>
|
||||
|
||||
<span class="govuk-header__product-name">Notify</span>
|
||||
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="govuk-width-container">
|
||||
<main class="govuk-main-wrapper" id="main-content" role="main">
|
||||
<div class="grid-row">
|
||||
<div class="grid-col-12">
|
||||
<h1 class="govuk-heading-l">
|
||||
Sorry, the service is unavailable
|
||||
</h1>
|
||||
<p class="usa-body">
|
||||
We’re performing some essential updates. Notify will be back shortly.
|
||||
</p>
|
||||
<p class="usa-body">
|
||||
Please check the <a class="usa-link"
|
||||
href="https://status.notifications.service.gov.uk/">status page</a> for updates.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer class="usa-footer" role="contentinfo">
|
||||
<div>
|
||||
<div class="usa-footer__meta">
|
||||
<div class="usa-footer__meta-item usa-footer__meta-item--grow">
|
||||
|
||||
<h2 class="usa-sr-only">Support links</h2>
|
||||
<div class="usa-footer__meta-custom">
|
||||
Built by the <a href="https://www.gsa.gov/about-us/organization/federal-acquisition-service/technology-transformation-services/tts-solutions"
|
||||
class="usa-link">Technology Transformation Services</a>
|
||||
</div>
|
||||
|
||||
<svg role="presentation" focusable="false" class="usa-footer__licence-logo"
|
||||
xmlns="http://www.w3.org/2000/svg" viewbox="0 0 483.2 195.7" height="17" width="41">
|
||||
<path fill="currentColor"
|
||||
d="M421.5 142.8V.1l-50.7 32.3v161.1h112.4v-50.7zm-122.3-9.6A47.12 47.12 0 0 1 221 97.8c0-26 21.1-47.1 47.1-47.1 16.7 0 31.4 8.7 39.7 21.8l42.7-27.2A97.63 97.63 0 0 0 268.1 0c-36.5 0-68.3 20.1-85.1 49.7A98 98 0 0 0 97.8 0C43.9 0 0 43.9 0 97.8s43.9 97.8 97.8 97.8c36.5 0 68.3-20.1 85.1-49.7a97.76 97.76 0 0 0 149.6 25.4l19.4 22.2h3v-87.8h-80l24.3 27.5zM97.8 145c-26 0-47.1-21.1-47.1-47.1s21.1-47.1 47.1-47.1 47.2 21 47.2 47S123.8 145 97.8 145" />
|
||||
</svg>
|
||||
<span class="usa-footer__licence-description">
|
||||
All content is available under the
|
||||
<a class="usa-link"
|
||||
href="https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" rel="license">Open
|
||||
Government Licence v3.0</a>, except where otherwise stated
|
||||
</span>
|
||||
</div>
|
||||
<!-- <div class="usa-footer__meta-item">
|
||||
<a class="usa-footer__link usa-footer__copyright-logo"
|
||||
href="https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/">©
|
||||
US copyright</a>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||