diff --git a/app/__init__.py b/app/__init__.py index ef10702ba..e685bac2d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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): diff --git a/app/assets/stylesheets/components/sms-message.scss b/app/assets/stylesheets/components/sms-message.scss index 96efbf308..41cad421e 100644 --- a/app/assets/stylesheets/components/sms-message.scss +++ b/app/assets/stylesheets/components/sms-message.scss @@ -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; +} diff --git a/app/main/__init__.py b/app/main/__init__.py index 0bf151fe3..c6b2161a9 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -27,5 +27,6 @@ from app.main.views import ( feedback, providers, platform_admin, - letter_jobs + letter_jobs, + conversation, ) diff --git a/app/main/views/conversation.py b/app/main/views/conversation.py new file mode 100644 index 000000000..783e244fd --- /dev/null +++ b/app/main/views/conversation.py @@ -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//conversation/") +@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'], + ) + ] diff --git a/app/notify_client/notification_api_client.py b/app/notify_client/notification_api_client.py index 4a4399723..88ee80e21 100644 --- a/app/notify_client/notification_api_client.py +++ b/app/notify_client/notification_api_client.py @@ -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) + ) diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index da9bb9d34..bbc4fdf81 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -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) diff --git a/app/templates/views/conversations/conversation.html b/app/templates/views/conversations/conversation.html new file mode 100644 index 000000000..6314de9ed --- /dev/null +++ b/app/templates/views/conversations/conversation.html @@ -0,0 +1,50 @@ +{% extends "withnav_template.html" %} + +{% block service_page_title %} + Conversation +{% endblock %} + +{% block maincolumn_content %} + +
+ +
+

+ 07900 900 123 +

+
+ {% for message in conversation %} +
+ {% if message.inbound %} +
+ {{ message.content | string }} +
+ {{ message.created_at | format_datetime_relative }} +
+
+ {% else %} +
+   +
+
+ {{ message.content | string }} + {% if message.status == 'delivered' %} +
+ {{ message.created_at | format_datetime_relative }} +
+ {% elif message.status in ['pending', 'sending', 'created'] %} +
+ sending +
+ {% else %} +
+ Failed (sent {{ message.created_at | format_datetime_relative }}) +
+ {% endif %} +
+ {% endif %} +
+ {% endfor %} +
+ +{% endblock %} diff --git a/app/templates/views/dashboard/inbox.html b/app/templates/views/dashboard/inbox.html index 6d5612bb4..fe343b1bc 100644 --- a/app/templates/views/dashboard/inbox.html +++ b/app/templates/views/dashboard/inbox.html @@ -25,7 +25,12 @@ field_headings_visible=False ) %} {% call field() %} - {{ item.user_number | format_phone_number_human_readable }} + + {{ item.user_number | format_phone_number_human_readable }} + {{ item.content }} {% endcall %} {% call field(align='right') %} diff --git a/tests/__init__.py b/tests/__init__.py index 19e724a8a..3b8ff9a8c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -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, diff --git a/tests/app/main/views/test_conversation.py b/tests/app/main/views/test_conversation.py new file mode 100644 index 000000000..a9ad71369 --- /dev/null +++ b/tests/app/main/views/test_conversation.py @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index e8dffef64..c763008e5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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(