mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-05 14:48:25 -04:00
Merge branch 'master' into email-registered-users
This commit is contained in:
@@ -12,7 +12,9 @@ from flask import (
|
||||
make_response,
|
||||
current_app,
|
||||
request,
|
||||
g)
|
||||
g,
|
||||
url_for
|
||||
)
|
||||
from flask._compat import string_types
|
||||
from flask.globals import _lookup_req_object
|
||||
from flask_login import LoginManager
|
||||
@@ -112,6 +114,7 @@ def create_app():
|
||||
application.add_template_filter(format_date_short)
|
||||
application.add_template_filter(format_notification_status)
|
||||
application.add_template_filter(format_notification_status_as_field_status)
|
||||
application.add_template_filter(format_notification_status_as_url)
|
||||
|
||||
application.after_request(useful_headers_after_request)
|
||||
application.after_request(save_service_after_request)
|
||||
@@ -235,16 +238,16 @@ def format_notification_status(status, template_type):
|
||||
'email': {
|
||||
'failed': 'Failed',
|
||||
'technical-failure': 'Technical failure',
|
||||
'temporary-failure': 'Temporary failure',
|
||||
'permanent-failure': 'Email address does not exist',
|
||||
'temporary-failure': 'Inbox not accepting messages right now',
|
||||
'permanent-failure': 'Email address doesn’t exist',
|
||||
'delivered': 'Delivered',
|
||||
'sending': 'Sending'
|
||||
},
|
||||
'sms': {
|
||||
'failed': 'Failed',
|
||||
'technical-failure': 'Technical failure',
|
||||
'temporary-failure': 'Temporary failure',
|
||||
'permanent-failure': 'Phone number does not exist',
|
||||
'temporary-failure': 'Phone not accepting messages right now',
|
||||
'permanent-failure': 'Phone number doesn’t exist',
|
||||
'delivered': 'Delivered',
|
||||
'sending': 'Sending'
|
||||
}
|
||||
@@ -262,6 +265,15 @@ def format_notification_status_as_field_status(status):
|
||||
}.get(status, 'error')
|
||||
|
||||
|
||||
def format_notification_status_as_url(status):
|
||||
url = partial(url_for, "main.delivery_and_failure")
|
||||
return {
|
||||
'technical-failure': url(_anchor='technical-failure'),
|
||||
'temporary-failure': url(_anchor='not-accepting-messages'),
|
||||
'permanent-failure': url(_anchor='does-not-exist')
|
||||
}.get(status)
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return user_api_client.get_user(user_id)
|
||||
|
||||
BIN
app/assets/images/cross-grey.png
Normal file
BIN
app/assets/images/cross-grey.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
46
app/assets/stylesheets/components/tick-cross.scss
Normal file
46
app/assets/stylesheets/components/tick-cross.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
%tick-cross {
|
||||
@include core-16;
|
||||
display: inline-block;
|
||||
background-size: 19px 19px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
padding: 1px 0 0 25px;
|
||||
}
|
||||
|
||||
.tick-cross {
|
||||
|
||||
&-tick {
|
||||
@extend %tick-cross;
|
||||
background-image: file-url('tick.png');
|
||||
}
|
||||
|
||||
&-cross {
|
||||
@extend %tick-cross;
|
||||
background-image: file-url('cross-grey.png');
|
||||
color: $secondary-text-colour;
|
||||
}
|
||||
|
||||
&-list {
|
||||
|
||||
@extend %grid-row;
|
||||
margin-top: 5px;
|
||||
|
||||
&-permissions {
|
||||
|
||||
@include grid-column(3/4);
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&-edit-link {
|
||||
@include grid-column(1/4);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -54,11 +54,13 @@ $path: '/static/images/';
|
||||
@import 'components/message';
|
||||
@import 'components/phone';
|
||||
@import 'components/research-mode';
|
||||
@import 'components/tick-cross';
|
||||
|
||||
@import 'views/job';
|
||||
@import 'views/edit-template';
|
||||
@import 'views/documenation';
|
||||
@import 'views/dashboard';
|
||||
@import 'views/users';
|
||||
|
||||
// TODO: break this up
|
||||
@import 'app';
|
||||
|
||||
16
app/assets/stylesheets/views/users.scss
Normal file
16
app/assets/stylesheets/views/users.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.user-list {
|
||||
|
||||
@include core-16;
|
||||
|
||||
&-item {
|
||||
|
||||
padding: $gutter-half 0;
|
||||
border-top: 1px solid $border-colour;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid $border-colour;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ from app.utils import (
|
||||
user_has_permissions,
|
||||
generate_notifications_csv)
|
||||
from app.statistics_utils import sum_of_statistics, statistics_by_state, add_rate_to_jobs
|
||||
from app.utils import get_help_argument
|
||||
|
||||
|
||||
def _parse_filter_args(filter_dict):
|
||||
@@ -98,7 +99,8 @@ def view_job(service_id, job_id):
|
||||
job_id=job['id'],
|
||||
status=request.args.get('status', '')
|
||||
),
|
||||
partials=get_job_partials(job)
|
||||
partials=get_job_partials(job),
|
||||
help=get_help_argument()
|
||||
)
|
||||
|
||||
|
||||
@@ -311,7 +313,8 @@ def get_job_partials(job):
|
||||
notifications=notification_api_client.get_notifications_for_service(
|
||||
job['service'], job['id'], status=filter_args.get('status')
|
||||
)['notifications'],
|
||||
status=request.args.get('status', '')
|
||||
status=request.args.get('status', ''),
|
||||
help=get_help_argument()
|
||||
),
|
||||
'status': render_template(
|
||||
'partials/jobs/status.html',
|
||||
|
||||
@@ -29,7 +29,7 @@ from app.main.uploader import (
|
||||
s3download
|
||||
)
|
||||
from app import job_api_client, service_api_client, current_service, user_api_client, statistics_api_client
|
||||
from app.utils import user_has_permissions, get_errors_for_csv, Spreadsheet
|
||||
from app.utils import user_has_permissions, get_errors_for_csv, Spreadsheet, get_help_argument
|
||||
|
||||
|
||||
def get_page_headings(template_type):
|
||||
@@ -181,7 +181,8 @@ def send_test(service_id, template_id):
|
||||
'views/send-test.html',
|
||||
template=template,
|
||||
recipient_column=first_column_heading[template.template_type],
|
||||
example=[get_example_csv_rows(template, use_example_as_example=False)]
|
||||
example=[get_example_csv_rows(template, use_example_as_example=False)],
|
||||
help=get_help_argument()
|
||||
)
|
||||
|
||||
|
||||
@@ -278,7 +279,8 @@ def check_messages(service_id, template_type, upload_id):
|
||||
upload_id=upload_id,
|
||||
form=CsvUploadForm(),
|
||||
statistics=statistics,
|
||||
back_link=back_link
|
||||
back_link=back_link,
|
||||
help=get_help_argument()
|
||||
)
|
||||
|
||||
|
||||
|
||||
15
app/templates/components/tick-cross.html
Normal file
15
app/templates/components/tick-cross.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% macro tick_cross(yes, label) %}
|
||||
<li>
|
||||
{% if yes %}
|
||||
<span class="tick-cross-tick">
|
||||
<span class="visually-hidden">Can</span>
|
||||
{{ label}}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="tick-cross-cross">
|
||||
<span class="visually-hidden">Can’t</span>
|
||||
{{ label}}
|
||||
</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endmacro %}
|
||||
@@ -1,9 +1,9 @@
|
||||
{% from "components/banner.html" import banner_wrapper %}
|
||||
|
||||
{% if request.args['help'] and request.args['help'] != '0' %}
|
||||
{% if help %}
|
||||
{% call banner_wrapper(type='tour') %}
|
||||
<p class="heading-medium">Try this example</p>
|
||||
<div class="grid-row bottom-gutter" {% if request.args['help'] != '1' %}style="opacity: 0.6"{% endif %}>
|
||||
<div class="grid-row bottom-gutter" {% if help != '1' %}style="opacity: 0.6"{% endif %}>
|
||||
<div class="column-one-sixth">
|
||||
<p class="heading-large" style="float: left;">1.</p>
|
||||
</div>
|
||||
@@ -13,7 +13,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-row bottom-gutter" {% if request.args['help'] != '2' %}style="opacity: 0.6"{% endif %}>
|
||||
<div class="grid-row bottom-gutter" {% if help != '2' %}style="opacity: 0.6"{% endif %}>
|
||||
<div class="column-one-sixth">
|
||||
<p class="heading-large">2.</p>
|
||||
</div>
|
||||
@@ -23,7 +23,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-row bottom-gutter" {% if request.args['help'] != '3' %}style="opacity: 0.6"{% endif %}>
|
||||
<div class="grid-row bottom-gutter" {% if help != '3' %}style="opacity: 0.6"{% endif %}>
|
||||
<div class="column-one-sixth">
|
||||
<p class="heading-large">3.</p>
|
||||
</div>
|
||||
@@ -31,7 +31,7 @@
|
||||
<p>
|
||||
Notify delivers the message
|
||||
</p>
|
||||
{% if request.args['help'] == '3' %}
|
||||
{% if help == '3' %}
|
||||
<a href='{{ url_for(".go_to_dashboard_after_tour", service_id=current_service.id, example_template_id=template.id) }}'>
|
||||
Now go to your dashboard
|
||||
</a>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="dashboard-table">
|
||||
{% endif %}
|
||||
|
||||
{% if notifications %}
|
||||
{% if notifications and not help %}
|
||||
<p class="bottom-gutter">
|
||||
<a href="{{ url_for('.view_job_csv', service_id=current_service.id, job_id=job.id, status=status) }}" download="download" class="heading-small">Download as a CSV file</a>
|
||||
 
|
||||
@@ -34,7 +34,13 @@
|
||||
align='right',
|
||||
status=item.status|format_notification_status_as_field_status
|
||||
) %}
|
||||
{% if item.status|format_notification_status_as_url %}
|
||||
<a href="{{ item.status|format_notification_status_as_url }}">
|
||||
{% endif %}
|
||||
{{ item.status|format_notification_status(item.template.template_type) }}
|
||||
{% if item.status|format_notification_status_as_url %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
{% else %}
|
||||
<form method="post" enctype="multipart/form-data" action="{{url_for('main.start_job', service_id=current_service.id, upload_id=upload_id)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="help" value="{{ '3' if request.args['help'] == '2' else 0 }}" />
|
||||
<input type="hidden" name="help" value="{{ '3' if help else 0 }}" />
|
||||
<input type="submit" class="button" value="Send {{ count_of_recipients }} {{ message_count_label(count_of_recipients, template.template_type, suffix='') }}" />
|
||||
<a href="{{ back_link }}" class="page-footer-back-link">Back</a>
|
||||
</form>
|
||||
|
||||
@@ -16,18 +16,17 @@ Delivery and failure – GOV.UK Notify
|
||||
<ul class="list list-bullet">
|
||||
<li><a href="#sending">Sending</a></li>
|
||||
<li><a href="#delivered">Delivered</a></li>
|
||||
<li><a href="#permanent-failure">Permanently failed</a></li>
|
||||
<li><a href="#temporary-failure">Temporarily failed</a></li>
|
||||
<li><a href="#does-not-exist">Phone number or email address does not exist</a></li>
|
||||
<li><a href="#inbox-not-accepting-messages">Inbox not accepting messages right now</a></li>
|
||||
<li><a href="#phone-not-accepting-messages">Phone not accepting messages right now</a></li>
|
||||
<li><a href="#technical-failure">Technical failure</a></li>
|
||||
</ul>
|
||||
|
||||
<p>Our delivery states are the same for both email and text message.</p>
|
||||
|
||||
<h2 id="sending" class="heading-medium">Sending</h2>
|
||||
|
||||
<p>All new messages start with the state ‘Sending’.</p>
|
||||
<p>All messages start in the ‘Sending’ state.</p>
|
||||
|
||||
<p>This means that we have accepted the message, the message is waiting in a queue to be sent to our email or text message delivery partners.</p>
|
||||
<p>This means that we have accepted the message. It’s waiting in a queue to be sent to our email or text message delivery partners.</p>
|
||||
|
||||
<h2 id="delivered" class="heading-medium">Delivered</h2>
|
||||
|
||||
@@ -35,23 +34,27 @@ Delivery and failure – GOV.UK Notify
|
||||
|
||||
<p>We can’t tell you if they’ve read it – to do so would require invasive and unreliable tracking techniques.</p>
|
||||
|
||||
<h2 id="permanent-failure" class="heading-medium">Permanently failed</h2>
|
||||
<h2 id="does-not-exist" class="heading-medium">Phone number or email address does not exist</h2>
|
||||
|
||||
<p>This means the email address or mobile number doesn’t exist or is blacklisted – also known as a ‘hard bounce’.</p>
|
||||
<p>You’re still billed for text messages to non-existant phone numbers.</p>
|
||||
|
||||
<p>You’re still billed for these text messages.</p>
|
||||
<p><strong>You need to remove these email addresses or phone numbers from your database.</strong></p>
|
||||
|
||||
<p><strong>You need to remove this email address or mobile number from your database.</strong></p>
|
||||
<a id="not-accepting-messages"></a>
|
||||
|
||||
<h2 id="temporary-failure" class="heading-medium">Temporarily failed</h2>
|
||||
<h2 id="inbox-not-accepting-messages" class="heading-medium">Inbox not accepting messages right now</h2>
|
||||
|
||||
<p>This means the email address or mobile number was full, or the mobile phone was switched off – also known as a ‘soft bounce’.</p>
|
||||
<p>This can happen for a number of reasons, eg the user’s inbox was full.</p>
|
||||
|
||||
<p>We mark messages as ‘Temporarily failed’.</p>
|
||||
<p><strong>You can choose to retry these messages later or not.</strong></p>
|
||||
|
||||
<h2 id="phone-not-accepting-messages" class="heading-medium">Phone not accepting messages right now</h2>
|
||||
|
||||
<p>This means the user’s phone was full or hasn’t been switched on in the last 72 hours.</p>
|
||||
|
||||
<p>You’re still billed for these messages.</p>
|
||||
|
||||
<p><strong>You can choose to retry this message later or not.</strong></p>
|
||||
<p><strong>You can choose to retry these messages later or not.</strong></p>
|
||||
|
||||
<h2 id="technical-failure" class="heading-medium">Technical failure</h2>
|
||||
|
||||
@@ -61,7 +64,7 @@ Delivery and failure – GOV.UK Notify
|
||||
|
||||
<p>You won’t be billed for these messages.</p>
|
||||
|
||||
<p><strong>You need to retry this message yourself later.</strong></p>
|
||||
<p><strong>You need to retry these messages yourself later.</strong></p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "withnav_template.html" %}
|
||||
{% from "components/table.html" import list_table, row, field, boolean_field, hidden_field_heading %}
|
||||
{% from "components/page-footer.html" import page_footer %}
|
||||
{% from "components/tick-cross.html" import tick_cross %}
|
||||
|
||||
{% set table_options = {
|
||||
'field_headings': [
|
||||
@@ -29,49 +30,84 @@ Manage users – GOV.UK Notify
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% call(item, row_number) list_table(
|
||||
users, caption='Active', **table_options
|
||||
) %}
|
||||
{% call field() %}
|
||||
{{ item.name }}
|
||||
{%- if item.email_address == current_user.email_address -%}
|
||||
 <span class="hint">(you)</span>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{{ boolean_field(item.has_permissions(permissions=['send_texts', 'send_emails', 'send_letters'])) }}
|
||||
{{ boolean_field(item.has_permissions(permissions=['manage_users', 'manage_templates', 'manage_settings'])) }}
|
||||
{{ boolean_field(item.has_permissions(permissions=['manage_api_keys'])) }}
|
||||
{% call field(align='right') %}
|
||||
{% if current_user.has_permissions(['manage_users']) %}
|
||||
{% if current_user.id != item.id %}
|
||||
<a href="{{ url_for('.edit_user_permissions', service_id=current_service.id, user_id=item.id)}}">Edit</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
<h2 class="heading-medium">
|
||||
Active
|
||||
</h2>
|
||||
<div class="user-list">
|
||||
{% for user in users %}
|
||||
<div class="user-list-item">
|
||||
<h3>
|
||||
{{ user.name }} <span class="hint">
|
||||
{%- if user.email_address == current_user.email_address -%}
|
||||
(you)
|
||||
{% else %}
|
||||
{{ user.email_address }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</h3>
|
||||
<ul class="tick-cross-list">
|
||||
<div class="tick-cross-list-permissions">
|
||||
{{ tick_cross(
|
||||
user.has_permissions(permissions=['send_texts', 'send_emails', 'send_letters']),
|
||||
'Send messages'
|
||||
) }}
|
||||
{{ tick_cross(
|
||||
user.has_permissions(permissions=['manage_users', 'manage_templates', 'manage_settings']),
|
||||
'Manage service'
|
||||
) }}
|
||||
{{ tick_cross(
|
||||
user.has_permissions(permissions=['manage_api_keys']),
|
||||
'Access API keys'
|
||||
) }}
|
||||
</div>
|
||||
{% if current_user.has_permissions(['manage_users']) %}
|
||||
{% if current_user.id != user.id %}
|
||||
<li class="tick-cross-list-edit-link">
|
||||
<a href="{{ url_for('.edit_user_permissions', service_id=current_service.id, user_id=user.id)}}">Edit permissions</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if invited_users %}
|
||||
{% call(item, row_number) list_table(
|
||||
invited_users, caption='Invited', **table_options
|
||||
) %}
|
||||
{% call field() %}
|
||||
{{ item.email_address }}
|
||||
{% endcall %}
|
||||
{{ boolean_field(item.has_permissions(permissions=['send_texts', 'send_emails', 'send_letters'])) }}
|
||||
{{ boolean_field(item.has_permissions(permissions=['manage_users', 'manage_templates', 'manage_settings'])) }}
|
||||
{{ boolean_field(item.has_permissions(permissions=['manage_api_keys'])) }}
|
||||
{% if item.status == 'pending' %}
|
||||
{% call field(align='right') %}
|
||||
{% if current_user.has_permissions(['manage_users']) %}
|
||||
<a href="{{ url_for('.cancel_invited_user', service_id=current_service.id, invited_user_id=item.id)}}">Cancel invitation</a>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
{% else %}
|
||||
{% call field() %}
|
||||
{{ item.status }}
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
<h2 class="heading-medium">
|
||||
Invited
|
||||
</h2>
|
||||
<div class="user-list">
|
||||
{% for user in invited_users %}
|
||||
<div class="user-list-item">
|
||||
<h3>
|
||||
{{ user.email_address }}
|
||||
</h3>
|
||||
<ul class="tick-cross-list">
|
||||
<div class="tick-cross-list-permissions">
|
||||
{{ tick_cross(
|
||||
user.has_permissions(permissions=['send_texts', 'send_emails', 'send_letters']),
|
||||
'Send messages'
|
||||
) }}
|
||||
{{ tick_cross(
|
||||
user.has_permissions(permissions=['manage_users', 'manage_templates', 'manage_settings']),
|
||||
'Manage service'
|
||||
) }}
|
||||
{{ tick_cross(
|
||||
user.has_permissions(permissions=['manage_api_keys']),
|
||||
'Access API keys'
|
||||
) }}
|
||||
</div>
|
||||
<li class="tick-cross-list-edit-link">
|
||||
{% if user.status == 'pending' and current_user.has_permissions(['manage_users']) %}
|
||||
<a href="{{ url_for('.cancel_invited_user', service_id=current_service.id, invited_user_id=user.id)}}">Cancel invitation</a>
|
||||
{% else %}
|
||||
{{ user.status|title }}
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -73,7 +73,13 @@
|
||||
) }}
|
||||
|
||||
{% call field(status=item.status|format_notification_status_as_field_status, align='right') %}
|
||||
{% if item.status|format_notification_status_as_url %}
|
||||
<a href="{{ item.status|format_notification_status_as_url }}">
|
||||
{% endif %}
|
||||
{{ item.status|format_notification_status(item.template.template_type) }}
|
||||
{% if item.status|format_notification_status_as_url %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
{% endcall %}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
{% endcall %}
|
||||
|
||||
{{ page_footer("Preview", back_link=(
|
||||
url_for('.choose_template', service_id=current_service.id, template_type=template.template_type)) if not request.args['help'] else None
|
||||
url_for('.send_messages', service_id=current_service.id, template_id=template.id)) if not help else None
|
||||
) }}
|
||||
</form>
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ def get_errors_for_csv(recipients, template_type):
|
||||
|
||||
|
||||
def generate_notifications_csv(json_list):
|
||||
from app import format_datetime
|
||||
from app import format_datetime, format_notification_status
|
||||
content = StringIO()
|
||||
retval = None
|
||||
with content as csvfile:
|
||||
@@ -102,7 +102,7 @@ def generate_notifications_csv(json_list):
|
||||
x['template']['name'],
|
||||
x['template']['template_type'],
|
||||
x['job']['original_file_name'] if x['job'] else '',
|
||||
x['status'],
|
||||
format_notification_status(x['status'], x['template']['template_type']),
|
||||
format_datetime(x['created_at'])])
|
||||
retval = content.getvalue()
|
||||
return retval
|
||||
@@ -186,3 +186,7 @@ class Spreadsheet():
|
||||
file_type=extension,
|
||||
file_content=file_content.getvalue()
|
||||
).to_array(), filename)
|
||||
|
||||
|
||||
def get_help_argument():
|
||||
return request.args.get('help') if request.args.get('help') in ('1', '2', '3') else None
|
||||
|
||||
Reference in New Issue
Block a user