mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-02-05 19:03:30 -05:00
Merge pull request #3225 from alphagov/fix-details
Add GOV.UK Frontend details component - second attempt
This commit is contained in:
@@ -743,6 +743,7 @@ def add_template_filters(application):
|
||||
format_phone_number_human_readable,
|
||||
format_thousands,
|
||||
id_safe,
|
||||
convert_to_boolean,
|
||||
]:
|
||||
application.add_template_filter(fn)
|
||||
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
// From
|
||||
// https://github.com/alphagov/govuk_elements/blob/4926897dc7734db2fc5e5ebb6acdc97f86e22e50/public/javascripts/vendor/details.polyfill.js
|
||||
//
|
||||
// ---
|
||||
//
|
||||
// <details> polyfill
|
||||
// http://caniuse.com/#feat=details
|
||||
|
||||
// FF Support for HTML5's <details> and <summary>
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=591737
|
||||
|
||||
// http://www.sitepoint.com/fixing-the-details-element/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var NATIVE_DETAILS = typeof document.createElement('details').open === 'boolean';
|
||||
|
||||
// Add event construct for modern browsers or IE
|
||||
// which fires the callback with a pre-converted target reference
|
||||
function addEvent(node, type, callback) {
|
||||
if (node.addEventListener) {
|
||||
node.addEventListener(type, function (e) {
|
||||
callback(e, e.target);
|
||||
}, false);
|
||||
} else if (node.attachEvent) {
|
||||
node.attachEvent('on' + type, function (e) {
|
||||
callback(e, e.srcElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cross-modal click events
|
||||
function addClickEvent(node, callback) {
|
||||
// Prevent space(32) from scrolling the page
|
||||
addEvent(node, 'keypress', function (e, target) {
|
||||
if (target.nodeName === 'SUMMARY') {
|
||||
if (e.keyCode === 32) {
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
e.returnValue = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// When the key comes up - check if it is enter(13) or space(32)
|
||||
addEvent(node, 'keyup', function (e, target) {
|
||||
if (e.keyCode === 13 || e.keyCode === 32) { callback(e, target); }
|
||||
});
|
||||
addEvent(node, 'mouseup', function (e, target) {
|
||||
callback(e, target);
|
||||
});
|
||||
}
|
||||
|
||||
// Get the nearest ancestor element of a node that matches a given tag name
|
||||
function getAncestor(node, match) {
|
||||
do {
|
||||
if (!node || node.nodeName.toLowerCase() === match) {
|
||||
break;
|
||||
}
|
||||
} while ((node = node.parentNode));
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
// Create a started flag so we can prevent the initialisation
|
||||
// function firing from both DOMContentLoaded and window.onload
|
||||
var started = false;
|
||||
|
||||
// Initialisation function
|
||||
function addDetailsPolyfill(list) {
|
||||
|
||||
// If this has already happened, just return
|
||||
// else set the flag so it doesn't happen again
|
||||
if (started) {
|
||||
return;
|
||||
}
|
||||
started = true;
|
||||
|
||||
// Get the collection of details elements, but if that's empty
|
||||
// then we don't need to bother with the rest of the scripting
|
||||
if ((list = document.getElementsByTagName('details')).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// else iterate through them to apply their initial state
|
||||
var n = list.length, i = 0;
|
||||
for (i; i < n; i++) {
|
||||
var details = list[i];
|
||||
|
||||
// Save shortcuts to the inner summary and content elements
|
||||
details.__summary = details.getElementsByTagName('summary').item(0);
|
||||
details.__content = details.getElementsByTagName('div').item(0);
|
||||
|
||||
// If the content doesn't have an ID, assign it one now
|
||||
// which we'll need for the summary's aria-controls assignment
|
||||
if (!details.__content.id) {
|
||||
details.__content.id = 'details-content-' + i;
|
||||
}
|
||||
|
||||
// Add ARIA role="group" to details
|
||||
details.setAttribute('role', 'group');
|
||||
|
||||
// Add role=button to summary
|
||||
details.__summary.setAttribute('role', 'button');
|
||||
|
||||
// Add aria-controls
|
||||
details.__summary.setAttribute('aria-controls', details.__content.id);
|
||||
|
||||
// Set tabIndex so the summary is keyboard accessible for non-native elements
|
||||
// http://www.saliences.com/browserBugs/tabIndex.html
|
||||
if (!NATIVE_DETAILS) {
|
||||
details.__summary.tabIndex = 0;
|
||||
}
|
||||
|
||||
// Detect initial open state
|
||||
var openAttr = details.getAttribute('open') !== null;
|
||||
if (openAttr === true) {
|
||||
details.__summary.setAttribute('aria-expanded', 'true');
|
||||
details.__content.setAttribute('aria-hidden', 'false');
|
||||
} else {
|
||||
details.__summary.setAttribute('aria-expanded', 'false');
|
||||
details.__content.setAttribute('aria-hidden', 'true');
|
||||
if (!NATIVE_DETAILS) {
|
||||
details.__content.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Create a circular reference from the summary back to its
|
||||
// parent details element, for convenience in the click handler
|
||||
details.__summary.__details = details;
|
||||
|
||||
// If this is not a native implementation, create an arrow
|
||||
// inside the summary
|
||||
|
||||
var twisty = document.createElement('i');
|
||||
|
||||
if (openAttr === true) {
|
||||
twisty.className = 'arrow arrow-open';
|
||||
twisty.appendChild(document.createTextNode('\u25bc'));
|
||||
} else {
|
||||
twisty.className = 'arrow arrow-closed';
|
||||
twisty.appendChild(document.createTextNode('\u25ba'));
|
||||
}
|
||||
|
||||
details.__summary.__twisty = details.__summary.insertBefore(twisty, details.__summary.firstChild);
|
||||
details.__summary.__twisty.setAttribute('aria-hidden', 'true');
|
||||
|
||||
}
|
||||
|
||||
// Define a statechange function that updates aria-expanded and style.display
|
||||
// Also update the arrow position
|
||||
function statechange(summary) {
|
||||
|
||||
var expanded = summary.__details.__summary.getAttribute('aria-expanded') === 'true';
|
||||
var hidden = summary.__details.__content.getAttribute('aria-hidden') === 'true';
|
||||
|
||||
summary.__details.__summary.setAttribute('aria-expanded', (expanded ? 'false' : 'true'));
|
||||
summary.__details.__content.setAttribute('aria-hidden', (hidden ? 'false' : 'true'));
|
||||
|
||||
if (!NATIVE_DETAILS) {
|
||||
summary.__details.__content.style.display = (expanded ? 'none' : '');
|
||||
|
||||
var hasOpenAttr = summary.__details.getAttribute('open') !== null;
|
||||
if (!hasOpenAttr) {
|
||||
summary.__details.setAttribute('open', 'open');
|
||||
} else {
|
||||
summary.__details.removeAttribute('open');
|
||||
}
|
||||
}
|
||||
|
||||
if (summary.__twisty) {
|
||||
summary.__twisty.firstChild.nodeValue = (expanded ? '\u25ba' : '\u25bc');
|
||||
summary.__twisty.setAttribute('class', (expanded ? 'arrow arrow-closed' : 'arrow arrow-open'));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bind a click event to handle summary elements
|
||||
addClickEvent(document, function(e, summary) {
|
||||
if (!(summary = getAncestor(summary, 'summary'))) {
|
||||
return true;
|
||||
}
|
||||
return statechange(summary);
|
||||
});
|
||||
}
|
||||
|
||||
// Bind two load events for modern and older browsers
|
||||
// If the first one fires it will set a flag to block the second one
|
||||
// but if it's not supported then the second one will fire
|
||||
addEvent(document, 'DOMContentLoaded', addDetailsPolyfill);
|
||||
addEvent(window, 'load', addDetailsPolyfill);
|
||||
|
||||
})();
|
||||
@@ -7,6 +7,21 @@
|
||||
// Exported items will be added to the window.GOVUK namespace.
|
||||
// For example, `export { Frontend }` will assign `Frontend` to `window.Frontend`
|
||||
import Header from 'govuk-frontend/components/header/header';
|
||||
import Details from 'govuk-frontend/components/details/details';
|
||||
|
||||
/**
|
||||
* TODO: Ideally this would be a NodeList.prototype.forEach polyfill
|
||||
* This seems to fail in IE8, requires more investigation.
|
||||
* See: https://github.com/imagitama/nodelist-foreach-polyfill
|
||||
*/
|
||||
function nodeListForEach (nodes, callback) {
|
||||
if (window.NodeList.prototype.forEach) {
|
||||
return nodes.forEach(callback)
|
||||
}
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
callback.call(window, nodes[i], i, nodes);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy of the initAll function from https://github.com/alphagov/govuk-frontend/blob/v2.13.0/src/all.js
|
||||
// except it only includes, and initialises, the components used by this application.
|
||||
@@ -18,6 +33,12 @@ function initAll (options) {
|
||||
// Defaults to the entire document if nothing is set.
|
||||
var scope = typeof options.scope !== 'undefined' ? options.scope : document
|
||||
|
||||
// Find all global details elements to enhance.
|
||||
var $details = scope.querySelectorAll('details')
|
||||
nodeListForEach($details, function ($detail) {
|
||||
new Details($detail).init()
|
||||
})
|
||||
|
||||
// Find first header module to enhance.
|
||||
var $toggleButton = scope.querySelector('[data-module="header"]')
|
||||
new Header($toggleButton).init()
|
||||
@@ -26,6 +47,7 @@ function initAll (options) {
|
||||
// Create separate namespace for GOVUK Frontend.
|
||||
var Frontend = {
|
||||
"Header": Header,
|
||||
"Details": Details,
|
||||
"initAll": initAll
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ $govuk-assets-path: "/static/";
|
||||
@import 'components/header/_header';
|
||||
@import 'components/footer/_footer';
|
||||
@import 'components/back-link/_back-link';
|
||||
@import 'components/details/_details';
|
||||
|
||||
@import "utilities/all";
|
||||
@import "overrides/all";
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.api-notifications {
|
||||
|
||||
font-family: monospace;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
border-bottom: 1px solid $border-colour;
|
||||
|
||||
&-item {
|
||||
@@ -8,38 +10,59 @@
|
||||
border-top: 1px solid $border-colour;
|
||||
padding: 10px 0 0 0;
|
||||
|
||||
&-title {
|
||||
color: $link-colour;
|
||||
&__heading,
|
||||
&__data,
|
||||
&__view {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
&__heading {
|
||||
display: block;
|
||||
margin-bottom: $gutter-half;
|
||||
|
||||
&::before {
|
||||
top: -1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
|
||||
display: block;
|
||||
color: $secondary-text-colour;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
|
||||
&-key,
|
||||
&-time {
|
||||
color: $secondary-text-colour;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@include govuk-media-query($from: tablet) {
|
||||
&-key,
|
||||
&-time {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
&-time {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&-recipient {
|
||||
display: inline;
|
||||
}
|
||||
&__data {
|
||||
|
||||
&-meta {
|
||||
display: block;
|
||||
color: $secondary-text-colour;
|
||||
}
|
||||
border-left: none;
|
||||
padding-left: 25px;
|
||||
|
||||
&-time {
|
||||
text-align: right;
|
||||
}
|
||||
&-name {
|
||||
color: $secondary-text-colour;
|
||||
}
|
||||
|
||||
&-key {
|
||||
display: inline-block;
|
||||
padding-left: 46px;
|
||||
}
|
||||
|
||||
&-data {
|
||||
|
||||
padding-left: 31px;
|
||||
color: $secondary-text-colour;
|
||||
|
||||
&-item {
|
||||
padding-bottom: 15px;
|
||||
&-value {
|
||||
color: $text-colour;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
.govuk-header__container { border-color: {{header_colour}} }
|
||||
</style>
|
||||
<meta name="google-site-verification" content="niWnSqImOWz6mVQTYqNb5tFK8HaKSB4b3ED4Z9gtUQ0" />
|
||||
{% block meta_format_detection %}
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
{% endblock %}
|
||||
{% block meta %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -39,43 +39,45 @@
|
||||
<div class="api-notifications">
|
||||
{% if not api_notifications.notifications %}
|
||||
<div class="api-notifications-item">
|
||||
<p class="api-notifications-item-meta">
|
||||
<p class="api-notifications-item__meta">
|
||||
When you send messages via the API they’ll appear here.
|
||||
</p>
|
||||
<p class="api-notifications-item-meta">
|
||||
<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">
|
||||
<details class="api-notifications-item govuk-details govuk-!-margin-bottom-0" data-module="govuk-details">
|
||||
<summary class="govuk-details__summary govuk-clearfix api-notifications-item__heading">
|
||||
<h3>
|
||||
<span class="govuk-details__summary-text">
|
||||
{{ notification.to }}
|
||||
</span>
|
||||
<span class="govuk-grid-row api-notifications-item__meta">
|
||||
<span class="govuk-grid-column-one-half api-notifications-item__meta-key">
|
||||
{{notification.key_name}}
|
||||
</span>
|
||||
<span class="govuk-grid-column-one-half api-notifications-item__meta-time">
|
||||
<time class="timeago" datetime="{{ notification.created_at }}">
|
||||
{{ notification.created_at|format_delta }}
|
||||
</time>
|
||||
</span>
|
||||
</span>
|
||||
</h3>
|
||||
<span class="grid-row api-notifications-item-meta">
|
||||
<span class="column-half api-notifications-item-key">
|
||||
{{notification.key_name}}
|
||||
</span>
|
||||
<span class="column-half api-notifications-item-time">
|
||||
<time class="timeago" datetime="{{ notification.created_at }}">
|
||||
{{ notification.created_at|format_delta }}
|
||||
</time>
|
||||
</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div>
|
||||
<dl id="notification-{{ notification.id }}" class="api-notifications-item-data bottom-gutter-1-2">
|
||||
<div class="govuk-details__text api-notifications-item__data govuk-!-padding-top-0">
|
||||
<dl id="notification-{{ notification.id }}">
|
||||
{% for key in [
|
||||
'id', 'client_reference', 'notification_type', 'created_at', 'updated_at', 'sent_at', 'status'
|
||||
] %}
|
||||
{% if notification[key] %}
|
||||
<dt>{{ key }}:</dt>
|
||||
<dd class="api-notifications-item-data-item">{{ notification[key] }}</dd>
|
||||
<dt class="api-notifications-item__data-name">{{ key }}:</dt>
|
||||
<dd class="api-notifications-item__data-value">{{ notification[key] }}</dd>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if notification.status not in ('pending-virus-check', 'virus-scan-failed') %}
|
||||
<a href="{{ url_for('.view_notification', service_id=current_service.id, notification_id=notification.id) }}">View {{ message_count_label(1, notification.template.template_type, suffix='') }}</a>
|
||||
<a class="api-notifications-item__view" href="{{ url_for('.view_notification', service_id=current_service.id, notification_id=notification.id) }}">View {{ message_count_label(1, notification.template.template_type, suffix='') }}</a>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
@@ -84,11 +86,11 @@
|
||||
{% if api_notifications.notifications %}
|
||||
<div class="api-notifications-item">
|
||||
{% if api_notifications.notifications|length == 50 %}
|
||||
<p class="api-notifications-item-meta">
|
||||
<p class="api-notifications-item__meta">
|
||||
Only showing the first 50 messages.
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="api-notifications-item-meta">
|
||||
<p class="api-notifications-item__meta">
|
||||
Notify deletes messages after 7 days.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "content_template.html" %}
|
||||
{% from "components/table.html" import mapping_table, row, text_field, edit_field, field with context %}
|
||||
{% from "components/sub-navigation.html" import sub_navigation %}
|
||||
{% from "components/details/macro.njk" import govukDetails %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Get started
|
||||
@@ -14,21 +15,22 @@
|
||||
<li class="get-started-list__item">
|
||||
<h2 class="get-started-list__heading">Check if GOV.UK Notify is right for you</h2>
|
||||
<p>Read about our <a href="{{ url_for('main.features') }}">features</a>, <a href="{{ url_for('.pricing') }}">pricing</a> and <a href="{{ url_for('main.roadmap') }}">roadmap</a>.</p>
|
||||
<details>
|
||||
<summary>Organisations that can use Notify</summary>
|
||||
<div id="eligible-organisations">
|
||||
<p>Notify is available to:</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>central government departments</li>
|
||||
<li>local authorities</li>
|
||||
<li>state-funded schools</li>
|
||||
<li>housing associations</li>
|
||||
<li>the NHS</li>
|
||||
<li>companies owned by local or central government that deliver services on their behalf</li>
|
||||
</ul>
|
||||
<p>Notify is not currently available to charities.</p>
|
||||
</div>
|
||||
</details>
|
||||
{{ govukDetails({
|
||||
"summaryText": "Organisations that can use Notify",
|
||||
"html": '''
|
||||
<div id="eligible-organisations">
|
||||
<p>Notify is available to:</p>
|
||||
<ul class="list list-bullet">
|
||||
<li>central government departments</li>
|
||||
<li>local authorities</li>
|
||||
<li>state-funded schools</li>
|
||||
<li>housing associations</li>
|
||||
<li>the NHS</li>
|
||||
<li>companies owned by local or central government that deliver services on their behalf</li>
|
||||
</ul>
|
||||
<p>Notify is not currently available to charities.</p>
|
||||
</div>'''
|
||||
}) }}
|
||||
</li>
|
||||
|
||||
<li class="get-started-list__item">
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
{% from "components/message-count-label.html" import message_count_label %}
|
||||
{% from "components/status-box.html" import status_box %}
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
{% from "components/details/macro.njk" import govukDetails %}
|
||||
|
||||
{% block per_page_title %}
|
||||
Platform admin
|
||||
@@ -14,15 +15,21 @@
|
||||
<h1 class="heading-large">
|
||||
Summary
|
||||
</h1>
|
||||
<details {% if form.errors %}open{% endif %}>
|
||||
<summary>Apply filters</summary>
|
||||
|
||||
{% set details_content %}
|
||||
{% call form_wrapper(method="get") %}
|
||||
{{ textbox(form.start_date, hint="Enter start date in format YYYY-MM-DD") }}
|
||||
{{ textbox(form.end_date, hint="Enter end date in format YYYY-MM-DD") }}
|
||||
</br>
|
||||
<button type="submit" class="button">Filter</button>
|
||||
{% endcall %}
|
||||
</details>
|
||||
{% endset %}
|
||||
|
||||
{{ govukDetails({
|
||||
"summaryText": "Apply filters",
|
||||
"html": details_content,
|
||||
"open": form.errors | convert_to_boolean
|
||||
}) }}
|
||||
|
||||
<div class="grid-row bottom-gutter">
|
||||
{% for noti_type in global_stats %}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
{% from "components/message-count-label.html" import message_count_label %}
|
||||
{% from "components/table.html" import mapping_table, field, stats_fields, row_group, row, right_aligned_field_heading, hidden_field_heading, text_field %}
|
||||
{% from "components/form.html" import form_wrapper %}
|
||||
{% from "components/details/macro.njk" import govukDetails %}
|
||||
|
||||
{% macro stats_fields(channel, data) -%}
|
||||
|
||||
@@ -101,16 +102,21 @@
|
||||
{{ page_title|capitalize }}
|
||||
</h1>
|
||||
|
||||
<details>
|
||||
<summary>Apply filters</summary>
|
||||
{% call form_wrapper(method="get") %}
|
||||
{{ textbox(form.start_date, hint="Enter start date in format YYYY-MM-DD") }}
|
||||
{{ textbox(form.end_date, hint="Enter end date in format YYYY-MM-DD") }}
|
||||
{{ checkbox(form.include_from_test_key) }}
|
||||
</br>
|
||||
<button type="submit" class="button">Filter</button>
|
||||
{% endcall %}
|
||||
</details>
|
||||
|
||||
{% set details_content %}
|
||||
{% call form_wrapper(method="get") %}
|
||||
{{ textbox(form.start_date, hint="Enter start date in format YYYY-MM-DD") }}
|
||||
{{ textbox(form.end_date, hint="Enter end date in format YYYY-MM-DD") }}
|
||||
{{ checkbox(form.include_from_test_key) }}
|
||||
</br>
|
||||
<button type="submit" class="button">Filter</button>
|
||||
{% endcall %}
|
||||
{% endset %}
|
||||
|
||||
{{ govukDetails({
|
||||
"summaryText": "Apply filters",
|
||||
"html": details_content
|
||||
}) }}
|
||||
|
||||
{% include "views/platform-admin/_global_stats.html" %}
|
||||
|
||||
|
||||
@@ -63,7 +63,8 @@ const copy = {
|
||||
'skip-link',
|
||||
'header',
|
||||
'footer',
|
||||
'back-link'
|
||||
'back-link',
|
||||
'details'
|
||||
];
|
||||
let done = 0;
|
||||
|
||||
@@ -143,7 +144,6 @@ const javascripts = () => {
|
||||
paths.src + 'javascripts/govuk/cookie-functions.js',
|
||||
paths.src + 'javascripts/cookieMessage.js',
|
||||
paths.src + 'javascripts/stick-to-window-when-scrolling.js',
|
||||
paths.src + 'javascripts/detailsPolyfill.js',
|
||||
paths.src + 'javascripts/apiKey.js',
|
||||
paths.src + 'javascripts/autofocus.js',
|
||||
paths.src + 'javascripts/enhancedTextbox.js',
|
||||
|
||||
@@ -37,7 +37,7 @@ def test_should_show_api_page(
|
||||
rows = page.find_all('details')
|
||||
assert len(rows) == 5
|
||||
for row in rows:
|
||||
assert row.find('h3').string.strip() == '07123456789'
|
||||
assert row.select('h3 .govuk-details__summary-text')[0].string.strip() == '07123456789'
|
||||
|
||||
|
||||
def test_should_show_api_page_with_lots_of_notifications(
|
||||
|
||||
Reference in New Issue
Block a user