Add two-way messaging view

> Once an inbound message has been received, there should be a way to
> see the other messages in the system from the same service to the same
> number. Both in and outbound. Nice inbox/whatsapp stylee view or some
> such. This way the context of the reply is understood.
>
> Initially will only see the outbound template, not the actual message,
> but we’re going to change this for the rest (soon), so that you can
> always see the full message for all outbound.
This commit is contained in:
Chris Hill-Scott
2017-05-24 13:19:31 +01:00
parent 183c324f9a
commit f6d8e55579
11 changed files with 292 additions and 12 deletions

View File

@@ -255,13 +255,20 @@ def format_datetime_relative(date):
def get_human_day(time):
# Add 1 hour to get midnight today instead of midnight tomorrow
time = (gmt_timezones(time) - timedelta(hours=1)).strftime('%A')
if time == datetime.utcnow().strftime('%A'):
return 'today'
if time == (datetime.utcnow() + timedelta(days=1)).strftime('%A'):
time_as_day = (gmt_timezones(time) - timedelta(hours=1)).strftime('%A')
six_days_ago = gmt_timezones((datetime.utcnow() + timedelta(days=-6)).isoformat())
if gmt_timezones(time) < six_days_ago:
return format_date_short(time)
if time_as_day == (datetime.utcnow() + timedelta(days=1)).strftime('%A'):
return 'tomorrow'
return time
if time_as_day == datetime.utcnow().strftime('%A'):
return 'today'
if time_as_day == (datetime.utcnow() + timedelta(days=-1)).strftime('%A'):
return 'yesterday'
return format_date_short(time)
def format_time(date):

View File

@@ -19,7 +19,7 @@ $tail-angle: 20deg;
content: "";
display: block;
position: absolute;
bottom: -4px;
bottom: -5px;
right: -20px;
border: 10px solid transparent;
border-left-width: 13px;
@@ -31,8 +31,35 @@ $tail-angle: 20deg;
}
.sms-message-inbound {
.sms-message-wrapper {
&:after {
border-left-color: transparent;
border-bottom-color: $panel-colour;
border-right-color: $panel-colour;
right: auto;
left: -20px;
transform: rotate(-$tail-angle);
}
}
}
.sms-message-recipient {
@include copy-19;
color: $secondary-text-colour;
margin: 10px 0 0 0;
}
.sms-message-status {
@include core-16;
color: $secondary-text-colour;
margin: -20px $gutter-half 20px $gutter-half;
}
.sms-message-status-outbound {
text-align: right;
}

View File

@@ -27,5 +27,6 @@ from app.main.views import (
feedback,
providers,
platform_admin,
letter_jobs
letter_jobs,
conversation,
)

View File

@@ -0,0 +1,55 @@
from flask import (
render_template,
url_for,
)
from flask_login import login_required
from notifications_utils.recipients import format_phone_number_human_readable
from notifications_utils.template import SMSPreviewTemplate
from app.main import main
from app.utils import user_has_permissions
from app import notification_api_client, service_api_client
@main.route("/services/<service_id>/conversation/<notification_id>")
@login_required
@user_has_permissions('view_activity', admin_override=True)
def conversation(service_id, notification_id):
user_number = get_user_number(service_id, notification_id)
return render_template(
'views/conversations/conversation.html',
conversation=get_sms_thread(service_id, user_number=user_number),
user_number=user_number,
)
def get_user_number(service_id, notification_id):
try:
user_number = service_api_client.get_inbound_sms_by_id(service_id, notification_id)['user_number']
except Exception:
user_number = notification_api_client.get_notification(service_id, notification_id)['to']
return format_phone_number_human_readable(user_number)
def get_sms_thread(service_id, user_number):
return [
{
'inbound': ('notify_number' in notification),
'content': SMSPreviewTemplate(
{
'content': notification.get('content') or notification['template']['content']
}
),
'created_at': notification['created_at'],
'status': notification.get('status'),
'id': notification['id'],
}
for notification in sorted(
(
notification_api_client.get_notifications_for_service(service_id, to=user_number)['notifications'] +
service_api_client.get_inbound_sms(service_id, user_number=user_number)
),
key=lambda notification: notification['created_at'],
)
]

View File

@@ -54,3 +54,8 @@ class NotificationApiClient(NotifyAdminAPIClient):
url='/service/{}/notifications'.format(service_id),
params=params
)
def get_notification(self, service_id, notification_id):
return self.get(
url='/service/{}/notifications/{}'.format(service_id, notification_id)
)

View File

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
from flask import url_for
from app.utils import BrowsableItem
from app.notify_client import _attach_current_user, NotifyAdminAPIClient
from . import notification_api_client
class ServiceAPIClient(NotifyAdminAPIClient):
@@ -244,11 +245,22 @@ class ServiceAPIClient(NotifyAdminAPIClient):
params=dict(year=year)
)
def get_inbound_sms(self, service_id):
def get_inbound_sms(self, service_id, user_number=''):
return self.get(
'/service/{}/inbound-sms'.format(service_id)
'/service/{}/inbound-sms?user_number={}'.format(
service_id,
user_number,
)
)['data']
def get_inbound_sms_by_id(self, service_id, notification_id):
return self.get(
'/service/{}/inbound-sms/{}'.format(
service_id,
notification_id,
)
)
def get_inbound_sms_summary(self, service_id):
return self.get(
'/service/{}/inbound-sms/summary'.format(service_id)

View File

@@ -0,0 +1,50 @@
{% extends "withnav_template.html" %}
{% block service_page_title %}
Conversation
{% endblock %}
{% block maincolumn_content %}
<div class="dashboard">
<div class="bottom-gutter">
<h1 class="heading-large">
07900 900 123
</h1>
</div>
{% for message in conversation %}
<div class="grid-row" id="n{{ message.id }}">
{% if message.inbound %}
<div class="column-two-thirds sms-message-inbound">
{{ message.content | string }}
<div class="sms-message-status">
{{ message.created_at | format_datetime_relative }}
</div>
</div>
{% else %}
<div class="column-one-third">
&nbsp;
</div>
<div class="column-two-thirds">
{{ message.content | string }}
{% if message.status == 'delivered' %}
<div class="sms-message-status sms-message-status-outbound">
{{ message.created_at | format_datetime_relative }}
</div>
{% elif message.status in ['pending', 'sending', 'created'] %}
<div class="sms-message-status sms-message-status-outbound hint">
sending
</div>
{% else %}
<div class="sms-message-status sms-message-status-outbound table-field-error-label">
Failed (sent {{ message.created_at | format_datetime_relative }})
</div>
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -25,7 +25,12 @@
field_headings_visible=False
) %}
{% call field() %}
<span class="file-list-filename" href="#">{{ item.user_number | format_phone_number_human_readable }}</span>
<a
class="file-list-filename"
href="{{ url_for('.conversation', service_id=current_service.id, notification_id=item.id) }}#n{{ item.id }}"
>
{{ item.user_number | format_phone_number_human_readable }}
</a>
<span class="wide-left-hand-column">{{ item.content }}</span>
{% endcall %}
{% call field(align='right') %}

View File

@@ -248,7 +248,9 @@ def notification_json(
'template': {
'id': template['id'],
'name': template['name'],
'template_type': template['template_type']},
'template_type': template['template_type'],
'content': template['content'],
},
'job': job_payload,
'sent_at': sent_at,
'status': status,

View File

@@ -0,0 +1,98 @@
import pytest
from bs4 import BeautifulSoup
from flask import (
url_for,
)
from tests.conftest import (
SERVICE_ONE_ID,
)
from tests.app.test_utils import normalize_spaces
from freezegun import freeze_time
@pytest.mark.parametrize('index, expected', enumerate([
(
'message-8',
'Failed (sent yesterday at 2:59pm)',
),
(
'message-7',
'Failed (sent yesterday at 2:59pm)',
),
(
'message-6',
'Failed (sent yesterday at 4:59pm)',
),
(
'message-5',
'Failed (sent yesterday at 6:59pm)',
),
(
'message-4',
'Failed (sent yesterday at 8:59pm)',
),
(
'message-3',
'Failed (sent yesterday at 10:59pm)',
),
(
'message-2',
'Failed (sent yesterday at 10:59pm)',
),
(
'message-1',
'Failed (sent yesterday at 11:00pm)',
),
(
'template content',
'yesterday at midnight',
),
(
'template content',
'yesterday at midnight',
),
(
'template content',
'yesterday at midnight',
),
(
'template content',
'yesterday at midnight',
),
(
'template content',
'yesterday at midnight',
),
]))
@freeze_time("2012-01-01 00:00:00")
def test_view_conversation(
logged_in_client,
fake_uuid,
mock_get_notification,
mock_get_inbound_sms,
mock_get_notifications,
index,
expected,
):
print(index)
response = logged_in_client.get(url_for(
'main.conversation',
service_id=SERVICE_ONE_ID,
notification_id=fake_uuid,
))
assert response.status_code == 200
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
messages = page.select('.sms-message-wrapper')
statuses = page.select('.sms-message-status')
for elements in (messages, statuses):
assert len(elements) == 13
assert (
normalize_spaces(messages[index].text),
normalize_spaces(statuses[index].text),
) == expected

View File

@@ -1096,6 +1096,22 @@ def mock_get_notifications(mocker, api_user_active):
)
@pytest.fixture(scope='function')
def mock_get_notification(mocker, api_user_active):
def _get_notification(
service_id,
notification_id,
):
return single_notification_json(
service_id,
)
return mocker.patch(
'app.notification_api_client.get_notification',
side_effect=_get_notification
)
@pytest.fixture(scope='function')
def mock_get_notifications_with_previous_next(mocker):
def _get_notifications(service_id,
@@ -1138,11 +1154,13 @@ def mock_get_notifications_with_no_notifications(mocker):
def mock_get_inbound_sms(mocker):
def _get_inbound_sms(
service_id,
user_number=None,
):
return [{
'user_number': '0790090000' + str(i),
'content': 'message-{}'.format(index + 1),
'created_at': (datetime.utcnow() - timedelta(minutes=60 * (i + 1))).isoformat()
'created_at': (datetime.utcnow() - timedelta(minutes=60 * (i + 1), seconds=index)).isoformat(),
'id': sample_uuid(),
} for index, i in enumerate([0, 0, 0, 2, 4, 6, 8, 8])]
return mocker.patch(