mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-02-05 19:03:30 -05:00
Add log of notifications to API integration page
Now that we’ve removed simulated notifications from the dashboard and activity pages they’re not visible anywhere in the app. While they should’t be visible to non-technical users, developers have a real need for Notify to confirm that their code is doing what they expect. This is needed especially when they’re just getting started with Notify. There’s no way of seeing this info from the API either, because a key can only get notifications created with a key of that type. It doesn’t make sense to make this a ‘mode’ of the dashboard or activity because the information about notifications that developers need is also different. So this commit adds up to 50 of the most recent notifications sent via the API to the page that developers use as their ‘home’ page. This also lets us explain the 7 days thing to developers via the empty slate state of this area of the page.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import ago
|
||||
import os
|
||||
import re
|
||||
import urllib
|
||||
import dateutil
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import pytz
|
||||
from flask import (
|
||||
Flask,
|
||||
@@ -115,6 +117,7 @@ def create_app():
|
||||
application.add_template_filter(format_date)
|
||||
application.add_template_filter(format_date_normal)
|
||||
application.add_template_filter(format_date_short)
|
||||
application.add_template_filter(format_delta)
|
||||
application.add_template_filter(format_notification_status)
|
||||
application.add_template_filter(format_notification_status_as_time)
|
||||
application.add_template_filter(format_notification_status_as_field_status)
|
||||
@@ -251,6 +254,19 @@ def format_date_short(date):
|
||||
return gmt_timezones(date).strftime('%d %B').lstrip('0')
|
||||
|
||||
|
||||
def format_delta(date):
|
||||
return ago.human(
|
||||
(
|
||||
datetime.now(timezone.utc)
|
||||
) - (
|
||||
dateutil.parser.parse(date)
|
||||
),
|
||||
future_tense='{} from now', # No-one should ever see this
|
||||
past_tense='{} ago',
|
||||
precision=1
|
||||
)
|
||||
|
||||
|
||||
def valid_phone_number(phone_number):
|
||||
try:
|
||||
validate_phone_number(phone_number)
|
||||
|
||||
@@ -52,6 +52,12 @@
|
||||
margin-top: 45px;
|
||||
}
|
||||
|
||||
.align-with-heading-copy-right {
|
||||
display: block;
|
||||
margin-top: 35px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.global-cookie-message {
|
||||
p {
|
||||
@extend %site-width-container;
|
||||
|
||||
@@ -64,6 +64,7 @@ $path: '/static/images/';
|
||||
@import 'views/documenation';
|
||||
@import 'views/dashboard';
|
||||
@import 'views/users';
|
||||
@import 'views/api';
|
||||
|
||||
// TODO: break this up
|
||||
@import 'app';
|
||||
|
||||
48
app/assets/stylesheets/views/api.scss
Normal file
48
app/assets/stylesheets/views/api.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
.api-notifications {
|
||||
|
||||
font-family: monospace;
|
||||
border-bottom: 1px solid $border-colour;
|
||||
|
||||
&-item {
|
||||
|
||||
border-top: 1px solid $border-colour;
|
||||
padding: 10px 0 0 0;
|
||||
|
||||
&-title {
|
||||
color: $link-colour;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&-recipient {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&-meta {
|
||||
color: $secondary-text-colour;
|
||||
}
|
||||
|
||||
&-time {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&-key {
|
||||
display: inline-block;
|
||||
padding-left: 46px;
|
||||
}
|
||||
|
||||
&-data {
|
||||
|
||||
padding-left: 31px;
|
||||
color: $secondary-text-colour;
|
||||
|
||||
&-item {
|
||||
padding-bottom: 15px;
|
||||
color: $text-colour;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@ from flask import request, render_template, redirect, url_for, flash
|
||||
from flask_login import login_required
|
||||
from app.main import main
|
||||
from app.main.forms import CreateKeyForm, Whitelist
|
||||
from app import api_key_api_client, service_api_client, current_service
|
||||
from app import api_key_api_client, service_api_client, notification_api_client, current_service
|
||||
from app.utils import user_has_permissions
|
||||
from app.notify_client.api_key_api_client import KEY_TYPE_NORMAL, KEY_TYPE_TEST, KEY_TYPE_TEAM
|
||||
|
||||
@@ -12,7 +12,12 @@ from app.notify_client.api_key_api_client import KEY_TYPE_NORMAL, KEY_TYPE_TEST,
|
||||
@user_has_permissions('manage_api_keys')
|
||||
def api_integration(service_id):
|
||||
return render_template(
|
||||
'views/api/index.html'
|
||||
'views/api/index.html',
|
||||
api_notifications=notification_api_client.get_notifications_for_service(
|
||||
service_id=service_id,
|
||||
include_jobs=False,
|
||||
include_from_test_key=True
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
|
||||
<nav class="grid-row bottom-gutter-3-2">
|
||||
<nav class="grid-row bottom-gutter-1-2">
|
||||
{% if current_service.restricted %}
|
||||
<div class="column-one-third">
|
||||
<a class="pill-separate-item" href="{{ url_for('.api_keys', service_id=current_service.id) }}">API keys</a>
|
||||
@@ -43,4 +43,64 @@
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="column-half">
|
||||
<h2 class="heading-medium">
|
||||
Message log
|
||||
</h2>
|
||||
</div>
|
||||
<div class="column-half align-with-heading-copy-right">
|
||||
<a href="{{ url_for('.api_integration', service_id=current_service.id) }}">Refresh</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="api-notifications">
|
||||
{% if not api_notifications.notifications %}
|
||||
<div class="api-notifications-item">
|
||||
<p class="api-notifications-item-meta">
|
||||
When you send messages via the API they’ll appear here.
|
||||
</p>
|
||||
<p class="api-notifications-item-meta">
|
||||
Notify deletes messages after 7 days.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for notification in api_notifications.notifications %}
|
||||
<details class="api-notifications-item">
|
||||
<summary class="api-notifications-item-title">
|
||||
<h3 class="api-notifications-item-recipient">
|
||||
{{ notification.to }}
|
||||
</h3>
|
||||
<div class="grid-row api-notifications-item-meta">
|
||||
<div class="column-half api-notifications-item-key">
|
||||
{{notification.key_name}}
|
||||
</div>
|
||||
<div class="column-half api-notifications-item-time">
|
||||
{{ notification.created_at|format_delta }}
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<dl id="notification-{{ notification.id }}" class="api-notifications-item-data bottom-gutter-1-2">
|
||||
{% for key in [
|
||||
'id', 'notification_type', 'created_at', 'updated_at', 'sent_at', 'status'
|
||||
] %}
|
||||
<dt>{{ key }}:</dt>
|
||||
<dd class="api-notifications-item-data-item">{{ notification[key] }}</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
</details>
|
||||
{% endfor %}
|
||||
{% if api_notifications.notifications %}
|
||||
<div class="api-notifications-item">
|
||||
{% if api_notifications.links %}
|
||||
<p class="api-notifications-item-meta">
|
||||
Only showing the first 50 messages.
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="api-notifications-item-meta">
|
||||
Notify deletes messages after 7 days.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -208,7 +208,7 @@ def notification_json(
|
||||
if sent_at is None:
|
||||
sent_at = str(datetime.utcnow().time())
|
||||
if created_at is None:
|
||||
created_at = str(datetime.utcnow().time())
|
||||
created_at = datetime.now(timezone.utc).isoformat()
|
||||
if updated_at is None:
|
||||
updated_at = str((datetime.utcnow() + timedelta(minutes=1)).time())
|
||||
if status is None:
|
||||
|
||||
@@ -12,7 +12,8 @@ def test_should_show_api_page(
|
||||
mock_login,
|
||||
api_user_active,
|
||||
mock_get_service,
|
||||
mock_has_permissions
|
||||
mock_has_permissions,
|
||||
mock_get_notifications
|
||||
):
|
||||
with app_.test_request_context(), app_.test_client() as client:
|
||||
client.login(api_user_active)
|
||||
@@ -21,6 +22,42 @@ def test_should_show_api_page(
|
||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||
assert page.h1.string.strip() == 'API integration'
|
||||
assert 'Your service is in trial mode' in page.find('div', {'class': 'banner-warning'}).text
|
||||
rows = page.find_all('details')
|
||||
assert len(rows) == 5
|
||||
for index, row in enumerate(rows):
|
||||
assert row.find('h3').string.strip() == '07123456789'
|
||||
|
||||
|
||||
def test_should_show_api_page_with_lots_of_notifications(
|
||||
client,
|
||||
mock_login,
|
||||
api_user_active,
|
||||
mock_get_service,
|
||||
mock_has_permissions,
|
||||
mock_get_notifications_with_previous_next
|
||||
):
|
||||
client.login(api_user_active)
|
||||
response = client.get(url_for('main.api_integration', service_id=str(uuid.uuid4())))
|
||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||
rows = page.find_all('div', {'class': 'api-notifications-item'})
|
||||
assert ' '.join(rows[len(rows) - 1].text.split()) == (
|
||||
'Only showing the first 50 messages. Notify deletes messages after 7 days.'
|
||||
)
|
||||
|
||||
|
||||
def test_should_show_api_page_with_no_notifications(
|
||||
client,
|
||||
mock_login,
|
||||
api_user_active,
|
||||
mock_get_service,
|
||||
mock_has_permissions,
|
||||
mock_get_notifications_with_no_notifications
|
||||
):
|
||||
client.login(api_user_active)
|
||||
response = client.get(url_for('main.api_integration', service_id=str(uuid.uuid4())))
|
||||
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
|
||||
rows = page.find_all('div', {'class': 'api-notifications-item'})
|
||||
assert 'When you send messages via the API they’ll appear here.' in rows[len(rows) - 1].text.strip()
|
||||
|
||||
|
||||
def test_should_show_api_page_for_live_service(
|
||||
|
||||
@@ -976,7 +976,9 @@ def mock_get_notifications_with_previous_next(mocker):
|
||||
page=1,
|
||||
template_type=None,
|
||||
status=None,
|
||||
limit_days=None):
|
||||
limit_days=None,
|
||||
include_jobs=None,
|
||||
include_from_test_key=None):
|
||||
return notification_json(service_id, with_links=True)
|
||||
|
||||
return mocker.patch(
|
||||
@@ -985,6 +987,24 @@ def mock_get_notifications_with_previous_next(mocker):
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def mock_get_notifications_with_no_notifications(mocker):
|
||||
def _get_notifications(service_id,
|
||||
job_id=None,
|
||||
page=1,
|
||||
template_type=None,
|
||||
status=None,
|
||||
limit_days=None,
|
||||
include_jobs=None,
|
||||
include_from_test_key=None):
|
||||
return notification_json(service_id, rows=0)
|
||||
|
||||
return mocker.patch(
|
||||
'app.notification_api_client.get_notifications_for_service',
|
||||
side_effect=_get_notifications
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def mock_has_permissions(mocker):
|
||||
def _has_permission(permissions=None, any_=False, admin_override=False):
|
||||
|
||||
Reference in New Issue
Block a user