diff --git a/app/__init__.py b/app/__init__.py index e9f1bc698..e040868a2 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,7 +3,6 @@ import os import urllib from datetime import datetime, timedelta, timezone from functools import partial -from numbers import Number from time import monotonic import ago @@ -84,7 +83,7 @@ from app.notify_client.template_statistics_api_client import ( template_statistics_client, ) from app.notify_client.user_api_client import user_api_client -from app.utils import get_logo_cdn_domain, id_safe +from app.utils import format_thousands, get_logo_cdn_domain, id_safe login_manager = LoginManager() csrf = CSRFProtect() @@ -298,12 +297,16 @@ def get_human_day(time): # Add 1 minute to transform 00:00 into ‘midnight today’ instead of ‘midnight tomorrow’ date = (utc_string_to_aware_gmt_datetime(time) - timedelta(minutes=1)).date() - if date == (datetime.utcnow() + timedelta(days=1)).date(): + now = datetime.utcnow() + + if date == (now + timedelta(days=1)).date(): return 'tomorrow' - if date == datetime.utcnow().date(): + if date == now.date(): return 'today' - if date == (datetime.utcnow() - timedelta(days=1)).date(): + if date == (now - timedelta(days=1)).date(): return 'yesterday' + if date.strftime('%Y') != now.strftime('%Y'): + return '{} {}'.format(_format_datetime_short(date), date.strftime('%Y')) return _format_datetime_short(date) @@ -329,6 +332,10 @@ def format_date_short(date): return _format_datetime_short(utc_string_to_aware_gmt_datetime(date)) +def format_date_human(date): + return get_human_day(date) + + def _format_datetime_short(datetime): return datetime.strftime('%d %B').lstrip('0') @@ -351,14 +358,6 @@ def format_delta(date): ) -def format_thousands(value): - if isinstance(value, Number): - return '{:,.0f}'.format(value) - if value is None: - return '' - return value - - def valid_phone_number(phone_number): try: validate_phone_number(phone_number) @@ -690,6 +689,7 @@ def add_template_filters(application): valid_phone_number, linkable_name, format_date, + format_date_human, format_date_normal, format_date_short, format_datetime_relative, diff --git a/app/assets/javascripts/previewPane.js b/app/assets/javascripts/previewPane.js index 7a8172861..514b02274 100644 --- a/app/assets/javascripts/previewPane.js +++ b/app/assets/javascripts/previewPane.js @@ -1,9 +1,8 @@ -(function () { +(function (global) { 'use strict'; - const root = this, - $ = this.jQuery; + $ = global.jQuery; let branding_style = $('.multiple-choice input[name="branding_style"]:checked'); @@ -34,4 +33,4 @@ $form.find('button[type="submit"]').text('Save'); $('fieldset').on('change', 'input[name="branding_style"]', setPreviewPane); -})(); +})(window); diff --git a/app/assets/javascripts/updateContent.js b/app/assets/javascripts/updateContent.js index 204e1c9ce..6297e3854 100644 --- a/app/assets/javascripts/updateContent.js +++ b/app/assets/javascripts/updateContent.js @@ -1,8 +1,8 @@ -(function(Modules) { +(function(global) { "use strict"; var queues = {}; - var dd = new diffDOM(); + var dd = new global.diffDOM(); var getRenderer = $component => response => dd.apply( $component.get(0), @@ -43,7 +43,7 @@ ); }; - Modules.UpdateContent = function() { + global.GOVUK.Modules.UpdateContent = function() { this.start = component => poll( getRenderer($(component)), @@ -55,4 +55,4 @@ }; -})(window.GOVUK.Modules); +})(window); diff --git a/app/assets/stylesheets/components/textbox.scss b/app/assets/stylesheets/components/textbox.scss index 91c1bba42..4e6c4e484 100644 --- a/app/assets/stylesheets/components/textbox.scss +++ b/app/assets/stylesheets/components/textbox.scss @@ -39,7 +39,8 @@ padding-bottom: $gutter-half; z-index: 10; - .placeholder { + .placeholder, + .placeholder-conditional { color: transparent; } diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index 466b6c4a2..3571fe020 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -76,6 +76,7 @@ $path: '/static/images/'; @import 'views/notification'; @import 'views/send'; @import 'views/get_started'; +@import 'views/history'; // TODO: break this up @import 'app'; diff --git a/app/assets/stylesheets/views/history.scss b/app/assets/stylesheets/views/history.scss new file mode 100644 index 000000000..b971a1726 --- /dev/null +++ b/app/assets/stylesheets/views/history.scss @@ -0,0 +1,29 @@ +$item-top-padding: $gutter-half; + +.history-list { + + @include core-19; + margin-bottom: $gutter; + + &-item { + + padding: $item-top-padding 0 $gutter-half 0; + border-top: 1px solid $border-colour; + position: relative; + + &:last-child { + border-bottom: 1px solid $border-colour; + } + + } + + &-user { + display: block; + } + + &-time { + display: block; + color: $secondary-text-colour; + } + +} diff --git a/app/main/__init__.py b/app/main/__init__.py index f0bd8c4a8..4d3e4e2aa 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -15,6 +15,7 @@ from app.main.views import ( # noqa isort:skip find_services, find_users, forgot_password, + history, inbound_number, index, invites, diff --git a/app/main/forms.py b/app/main/forms.py index ebec282f8..65877e997 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -1111,8 +1111,8 @@ class PDFUploadForm(StripWhitespaceForm): file = FileField_wtf( 'Upload a letter in PDF format', validators=[ - FileAllowed(['pdf'], 'Letters must be saved as a PDF'), - DataRequired(message="You need to upload a file to submit") + FileAllowed(['pdf'], 'Save your letter as a PDF and try again.'), + DataRequired(message="You need to choose a file to upload") ] ) diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 499610b91..b7e64997c 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -38,19 +38,6 @@ from app.utils import ( ) -# This is a placeholder view method to be replaced -# when product team makes decision about how/what/when -# to view history -@main.route("/services//history") -@user_has_permissions() -def temp_service_history(service_id): - data = service_api_client.get_service_history(service_id)['data'] - return render_template('views/temp-history.html', - services=data['service_history'], - api_keys=data['api_key_history'], - events=data['events']) - - @main.route("/services//dashboard") @user_has_permissions('view_activity', 'send_messages') def old_service_dashboard(service_id): diff --git a/app/main/views/history.py b/app/main/views/history.py new file mode 100644 index 000000000..ca6dd60fb --- /dev/null +++ b/app/main/views/history.py @@ -0,0 +1,43 @@ +from collections import defaultdict +from operator import attrgetter + +from flask import render_template, request + +from app import current_service, format_date_numeric +from app.main import main +from app.models.event import APIKeyEvent, APIKeyEvents, ServiceEvents +from app.utils import user_has_permissions + + +@main.route("/services//history") +@user_has_permissions('manage_service') +def history(service_id): + + events = _get_events(current_service.id, request.args.get('selected')) + + return render_template( + 'views/temp-history.html', + days=_chunk_events_by_day(events), + show_navigation=request.args.get('selected') or any( + isinstance(event, APIKeyEvent) for event in events + ), + user_getter=current_service.active_users.get_name_from_id, + ) + + +def _get_events(service_id, selected): + if selected == 'api': + return APIKeyEvents(service_id) + if selected == 'service': + return ServiceEvents(service_id) + return APIKeyEvents(service_id) + ServiceEvents(service_id) + + +def _chunk_events_by_day(events): + + days = defaultdict(list) + + for event in sorted(events, key=attrgetter('time'), reverse=True): + days[format_date_numeric(event.time)].append(event) + + return sorted(days.items(), reverse=True) diff --git a/app/main/views/send.py b/app/main/views/send.py index c6d720be1..c3415c78b 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -15,8 +15,9 @@ from flask import ( ) from flask_login import current_user from notifications_python_client.errors import HTTPError -from notifications_utils import SMS_CHAR_COUNT_LIMIT +from notifications_utils import LETTER_MAX_PAGE_COUNT, SMS_CHAR_COUNT_LIMIT from notifications_utils.columns import Columns +from notifications_utils.pdf import is_letter_too_long from notifications_utils.recipients import ( RecipientCSV, first_column_headings, @@ -522,6 +523,7 @@ def _check_messages(service_id, template_id, upload_id, preview_row, letters_as_ email_reply_to = get_email_reply_to_address_from_session() elif db_template['template_type'] == 'sms': sms_sender = get_sms_sender_from_session() + template = get_template( db_template, current_service, @@ -567,6 +569,8 @@ def _check_messages(service_id, template_id, upload_id, preview_row, letters_as_ elif preview_row > 2: abort(404) + page_count = get_page_count_for_letter(db_template, template.values) + return dict( recipients=recipients, template=template, @@ -589,7 +593,10 @@ def _check_messages(service_id, template_id, upload_id, preview_row, letters_as_ preview_row=preview_row, sent_previously=job_api_client.has_sent_previously( service_id, template.id, db_template['version'], request.args.get('original_file_name', '') - ) + ), + letter_too_long=is_letter_too_long(page_count), + letter_max_pages=LETTER_MAX_PAGE_COUNT, + page_count=page_count ) @@ -601,12 +608,12 @@ def check_messages(service_id, template_id, upload_id, row_index=2): data = _check_messages(service_id, template_id, upload_id, row_index) if ( - data['recipients'].too_many_rows or - not data['count_of_recipients'] or - not data['recipients'].has_recipient_columns or - data['recipients'].duplicate_recipient_column_headers or - data['recipients'].missing_column_headers or - data['sent_previously'] + data['recipients'].too_many_rows + or not data['count_of_recipients'] + or not data['recipients'].has_recipient_columns + or data['recipients'].duplicate_recipient_column_headers + or data['recipients'].missing_column_headers + or data['sent_previously'] ): return render_template('views/check/column-errors.html', **data) @@ -614,8 +621,8 @@ def check_messages(service_id, template_id, upload_id, row_index=2): return render_template('views/check/row-errors.html', **data) if ( - data['errors'] or - data['trying_to_send_letters_in_trial_mode'] + data['errors'] + or data['trying_to_send_letters_in_trial_mode'] ): return render_template('views/check/column-errors.html', **data) @@ -877,10 +884,14 @@ def _check_notification(service_id, template_id, exception=None): raise PermanentRedirect(back_link) template.values = get_recipient_and_placeholders_from_session(template.template_type) + page_count = get_page_count_for_letter(db_template, template.values) return dict( template=template, back_link=back_link, help=get_help_argument(), + letter_too_long=is_letter_too_long(page_count), + letter_max_pages=LETTER_MAX_PAGE_COUNT, + page_count=page_count, **(get_template_error_dict(exception) if exception else {}), ) diff --git a/app/main/views/templates.py b/app/main/views/templates.py index 7a5923e13..7e8fc6019 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -6,7 +6,9 @@ from flask import abort, flash, redirect, render_template, request, url_for from flask_login import current_user from markupsafe import Markup from notifications_python_client.errors import HTTPError +from notifications_utils import LETTER_MAX_PAGE_COUNT from notifications_utils.formatters import nl2br +from notifications_utils.pdf import is_letter_too_long from notifications_utils.recipients import first_column_headings from app import ( @@ -58,6 +60,8 @@ def view_template(service_id, template_id): '.send_one_off', service_id=service_id, template_id=template_id )) + page_count = get_page_count_for_letter(template) + return render_template( 'views/templates/template.html', template=get_template( @@ -74,6 +78,9 @@ def view_template(service_id, template_id): ), template_postage=template["postage"], user_has_template_permission=user_has_template_permission, + letter_too_long=is_letter_too_long(page_count), + letter_max_pages=LETTER_MAX_PAGE_COUNT, + page_count=page_count ) diff --git a/app/main/views/uploads.py b/app/main/views/uploads.py index 31d3a4f46..665fce5a5 100644 --- a/app/main/views/uploads.py +++ b/app/main/views/uploads.py @@ -1,11 +1,11 @@ import base64 +import json import uuid from io import BytesIO from flask import ( abort, current_app, - flash, redirect, render_template, request, @@ -19,6 +19,7 @@ from app import current_service, notification_api_client, service_api_client from app.extensions import antivirus_client from app.main import main from app.main.forms import PDFUploadForm +from app.main.views.jobs import view_jobs from app.s3_client.s3_letter_upload_client import ( get_letter_metadata, get_letter_pdf_and_metadata, @@ -26,21 +27,26 @@ from app.s3_client.s3_letter_upload_client import ( upload_letter_to_s3, ) from app.template_previews import TemplatePreview, sanitise_letter -from app.utils import get_template, user_has_permissions +from app.utils import ( + get_letter_validation_error, + get_template, + user_has_permissions, +) MAX_FILE_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB @main.route("/services//uploads") -@user_has_permissions('send_messages') +@user_has_permissions() def uploads(service_id): - return render_template('views/uploads/index.html') + return view_jobs(service_id) @main.route("/services//upload-letter", methods=['GET', 'POST']) @user_has_permissions('send_messages') def upload_letter(service_id): form = PDFUploadForm() + error = {} if form.validate_on_submit(): pdf_file_bytes = form.file.data.read() @@ -48,17 +54,20 @@ def upload_letter(service_id): virus_free = antivirus_client.scan(BytesIO(pdf_file_bytes)) if not virus_free: - return invalid_upload_error('Your file has failed the virus check') + return invalid_upload_error('Your file contains a virus') if len(pdf_file_bytes) > MAX_FILE_UPLOAD_SIZE: - return invalid_upload_error('Your file must be smaller than 2MB') + return invalid_upload_error('Your file is too big', 'Files must be smaller than 2MB.') try: # TODO: get page count from the sanitise response once template preview handles malformed files nicely page_count = pdf_page_count(BytesIO(pdf_file_bytes)) except PdfReadError: current_app.logger.info('Invalid PDF uploaded for service_id: {}'.format(service_id)) - return invalid_upload_error('Your file must be a valid PDF') + return invalid_upload_error( + "There’s a problem with your file", + 'Notify cannot read this PDF.
Save a new copy of your file and try again.' + ) upload_id = uuid.uuid4() file_location = get_transient_letter_file_location(service_id, upload_id) @@ -68,13 +77,18 @@ def upload_letter(service_id): response.raise_for_status() except RequestException as ex: if ex.response is not None and ex.response.status_code == 400: + validation_failed_message = response.json().get('message') + invalid_pages = response.json().get('invalid_pages') + status = 'invalid' upload_letter_to_s3( pdf_file_bytes, file_location=file_location, status=status, page_count=page_count, - filename=original_filename) + filename=original_filename, + message=validation_failed_message, + invalid_pages=invalid_pages) else: raise ex else: @@ -95,12 +109,33 @@ def upload_letter(service_id): ) ) - return render_template('views/uploads/choose-file.html', form=form) + if form.file.errors: + error = _get_error_from_upload_form(form.file.errors[0]) + + return render_template( + 'views/uploads/choose-file.html', + error=error, + form=form + ) -def invalid_upload_error(message): - flash(message, 'dangerous') - return render_template('views/uploads/choose-file.html', form=PDFUploadForm()), 400 +def invalid_upload_error(error_title, error_detail=None): + return render_template( + 'views/uploads/choose-file.html', + error={'title': error_title, 'detail': error_detail}, + form=PDFUploadForm() + ), 400 + + +def _get_error_from_upload_form(form_errors): + error = {} + if 'PDF' in form_errors: + error['title'] = 'Wrong file type' + error['detail'] = form_errors + else: # No file was uploaded error + error['title'] = form_errors + + return error @main.route("/services//preview-letter/") @@ -110,7 +145,13 @@ def uploaded_letter_preview(service_id, file_id): original_filename = metadata.get('filename') page_count = metadata.get('page_count') status = metadata.get('status') + error_message = metadata.get('message') + invalid_pages = metadata.get('invalid_pages') + if invalid_pages: + invalid_pages = json.loads(invalid_pages) + + error = get_letter_validation_error(error_message, invalid_pages, page_count) template_dict = service_api_client.get_precompiled_template(service_id) template = get_template( @@ -130,6 +171,7 @@ def uploaded_letter_preview(service_id, file_id): template=template, status=status, file_id=file_id, + error=error, ) @@ -140,7 +182,7 @@ def view_letter_upload_as_preview(service_id, file_id): page = request.args.get('page') - if metadata['status'] == 'invalid': + if metadata.get('message') == 'content-outside-printable-area': return TemplatePreview.from_invalid_pdf_file(pdf_file, page) else: return TemplatePreview.from_valid_pdf_file(pdf_file, page) diff --git a/app/models/__init__.py b/app/models/__init__.py index 2574495e2..def264a98 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -63,8 +63,8 @@ class ModelList(ABC, Sequence): def model(): pass - def __init__(self): - self.items = self.client() + def __init__(self, *args): + self.items = self.client(*args) def __getitem__(self, index): return self.model(self.items[index]) diff --git a/app/models/event.py b/app/models/event.py new file mode 100644 index 000000000..43dfba627 --- /dev/null +++ b/app/models/event.py @@ -0,0 +1,191 @@ +from abc import ABC, abstractmethod + +from notifications_utils.formatters import formatted_list + +from app.models import ModelList +from app.notify_client.service_api_client import service_api_client +from app.utils import format_thousands + + +class Event(ABC): + + def __init__( + self, + item, + key=None, + value_from=None, + value_to=None, + ): + self.item = item + self.time = item['updated_at'] or item['created_at'] + self.user_id = item['created_by_id'] + self.key = key + self.value_from = value_from + self.value_to = value_to + + @abstractmethod + def __str__(self): + pass + + @property + @abstractmethod + def relevant(self): + pass + + +class ServiceCreationEvent(Event): + + relevant = True + + def __str__(self): + return 'Created this service and called it ‘{}’'.format( + self.item['name'] + ) + + +class ServiceEvent(Event): + + @property + def relevant(self): + return self.value_from != self.value_to and bool(self._formatter) + + def __str__(self): + return self._formatter() + + @property + def _formatter(self): + return getattr(self, 'format_{}'.format(self.key), None) + + def format_restricted(self): + if self.value_to is False: + return 'Made this service live' + if self.value_to is True: + return 'Put this service back into trial mode' + + def format_active(self): + if self.value_to is False: + return 'Deleted this service' + if self.value_to is True: + return 'Unsuspended this service' + + def format_contact_link(self): + return 'Set the contact details for this service to ‘{}’'.format( + self.value_to + ) + + def format_email_branding(self): + return 'Updated this service’s email branding' + + def format_inbound_api(self): + return 'Updated the callback for received text messages' + + def format_letter_branding(self): + if self.value_to is None: + return 'Removed the logo from this service’s letters' + return 'Updated the logo on this service’s letters' + + def format_letter_contact_block(self): + return 'Updated the default letter contact block for this service' + + def format_message_limit(self): + return ( + '{} this service’s daily message limit from {} to {}' + ).format( + 'Reduced' if self.value_from > self.value_to else 'Increased', + format_thousands(self.value_from), + format_thousands(self.value_to), + ) + + def format_name(self): + return ( + 'Renamed this service from ‘{}’ to ‘{}’' + ).format( + self.value_from, self.value_to + ) + + def format_permissions(self): + added = list(sorted(set(self.value_to) - set(self.value_from))) + removed = list(sorted(set(self.value_from) - set(self.value_to))) + if removed and added: + return 'Removed {} from this service’s permissions, added {}'.format( + formatted_list(removed), + formatted_list(added), + ) + if added: + return 'Added {} to this service’s permissions'.format( + formatted_list(added) + ) + if removed: + return 'Removed {} from this service’s permissions'.format( + formatted_list(removed) + ) + + def format_prefix_sms(self): + if self.value_to is True: + return 'Set text messages to start with the name of this service' + else: + return 'Set text messages to not start with the name of this service' + + def format_research_mode(self): + if self.value_to is True: + return 'Put this service into research mode' + else: + return 'Took this service out of research mode' + + def format_service_callback_api(self): + return 'Updated the callback for delivery receipts' + + def format_go_live_user(self): + return 'Requested for this service to go live' + + +class APIKeyEvent(Event): + + relevant = True + + def __str__(self): + if self.item['updated_at']: + return ( + 'Revoked the ‘{}’ API key' + ).format(self.item['name']) + else: + return ( + 'Created an API key called ‘{}’' + ).format(self.item['name']) + + +class APIKeyEvents(ModelList): + + model = APIKeyEvent + client = service_api_client.get_service_api_key_history + + +class ServiceEvents(ModelList): + + client = service_api_client.get_service_service_history + + @property + def model(self): + return lambda x: x + + @staticmethod + def splat(events): + for index, item in enumerate(sorted( + events, + key=lambda event: event['updated_at'] or event['created_at'] + )): + if index == 0: + yield ServiceCreationEvent(item) + else: + for key in sorted(item.keys()): + yield ServiceEvent( + item, + key, + events[index - 1][key], + events[index][key], + ) + + def __init__(self, service_id): + self.items = [ + event for event in self.splat(self.client(service_id)) if event.relevant + ] diff --git a/app/models/service.py b/app/models/service.py index 3e3e93fd6..bf989cfeb 100644 --- a/app/models/service.py +++ b/app/models/service.py @@ -462,6 +462,10 @@ class Service(JSONModel): key=lambda folder: folder['name'].lower(), ) + @property + def can_upload_letters(self): + return self.has_permission('letter') and self.has_permission('upload_letters') + @cached_property def all_template_folder_ids(self): return {folder['id'] for folder in self.all_template_folders} diff --git a/app/models/user.py b/app/models/user.py index 8810719bc..2c9cf5fce 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -610,6 +610,12 @@ class Users(ModelList): def __init__(self, service_id): self.items = self.client(service_id) + def get_name_from_id(self, id): + for user in self: + if user.id == id: + return user.name + return 'Unknown' + class OrganisationUsers(Users): client = user_api_client.get_users_for_organisation diff --git a/app/navigation.py b/app/navigation.py index 8138afab6..d4faadcdf 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -192,6 +192,7 @@ class HeaderNavigation(Navigation): 'get_notifications_as_json', 'get_started', 'go_to_dashboard_after_tour', + 'history', 'inbound_sms_admin', 'inbox', 'inbox_download', @@ -302,7 +303,6 @@ class HeaderNavigation(Navigation): 'start_tour', 'styleguide', 'submit_request_to_go_live', - 'temp_service_history', 'template_history', 'template_usage', 'trial_mode', @@ -507,6 +507,7 @@ class MainNavigation(Navigation): 'get_notifications_as_json', 'get_started', 'go_to_dashboard_after_tour', + 'history', 'how_to_pay', 'inbound_sms_admin', 'inbox_download', @@ -579,7 +580,6 @@ class MainNavigation(Navigation): 'styleguide', 'support', 'suspend_service', - 'temp_service_history', 'template_history', 'terms', 'thanks', @@ -635,9 +635,12 @@ class CaseworkNavigation(Navigation): 'view_notifications', 'view_notification', }, - 'uploaded-files': { + 'uploads': { 'view_jobs', 'view_job', + 'upload_letter', + 'uploaded_letter_preview', + 'uploads', }, } @@ -732,6 +735,7 @@ class CaseworkNavigation(Navigation): 'get_notifications_as_json', 'get_started', 'go_to_dashboard_after_tour', + 'history', 'how_to_pay', 'inbound_sms_admin', 'inbox_download', @@ -857,7 +861,6 @@ class CaseworkNavigation(Navigation): 'submit_request_to_go_live', 'support', 'suspend_service', - 'temp_service_history', 'template_history', 'template_usage', 'terms', @@ -871,9 +874,6 @@ class CaseworkNavigation(Navigation): 'two_factor_email_sent', 'update_email_branding', 'update_letter_branding', - 'upload_letter', - 'uploaded_letter_preview', - 'uploads', 'usage', 'usage_for_all_services', 'user_information', @@ -1020,6 +1020,7 @@ class OrgNavigation(Navigation): 'get_notifications_as_json', 'get_started', 'go_to_dashboard_after_tour', + 'history', 'how_to_pay', 'inbound_sms_admin', 'inbox', @@ -1141,7 +1142,6 @@ class OrgNavigation(Navigation): 'submit_request_to_go_live', 'support', 'suspend_service', - 'temp_service_history', 'template_history', 'template_usage', 'terms', diff --git a/app/notify_client/notification_api_client.py b/app/notify_client/notification_api_client.py index 341d10234..793af02ef 100644 --- a/app/notify_client/notification_api_client.py +++ b/app/notify_client/notification_api_client.py @@ -63,6 +63,7 @@ class NotificationApiClient(NotifyAdminAPIClient): data = { 'filename': filename, 'file_id': file_id, + 'postage': 'second', } data = _attach_current_user(data) return self.post(url='/service/{}/send-pdf-letter'.format(service_id), data=data) diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index f247597be..8c45fb1b3 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -311,7 +311,13 @@ class ServiceAPIClient(NotifyAdminAPIClient): # Temp access of service history data. Includes service and api key history def get_service_history(self, service_id): - return self.get('/service/{0}/history'.format(service_id)) + return self.get('/service/{0}/history'.format(service_id))['data'] + + def get_service_service_history(self, service_id): + return self.get_service_history(service_id)['service_history'] + + def get_service_api_key_history(self, service_id): + return self.get_service_history(service_id)['api_key_history'] def get_monthly_notification_stats(self, service_id, year): return self.get(url='/service/{}/notifications/monthly?year={}'.format(service_id, year)) diff --git a/app/s3_client/s3_letter_upload_client.py b/app/s3_client/s3_letter_upload_client.py index 8bdeece7b..1a5bb4611 100644 --- a/app/s3_client/s3_letter_upload_client.py +++ b/app/s3_client/s3_letter_upload_client.py @@ -1,3 +1,5 @@ +import json + from boto3 import resource from flask import current_app from notifications_utils.s3 import s3upload as utils_s3upload @@ -7,17 +9,23 @@ def get_transient_letter_file_location(service_id, upload_id): return 'service-{}/{}.pdf'.format(service_id, upload_id) -def upload_letter_to_s3(data, *, file_location, status, page_count, filename): +def upload_letter_to_s3(data, *, file_location, status, page_count, filename, message=None, invalid_pages=None): + metadata = { + 'status': status, + 'page_count': str(page_count), + 'filename': filename, + } + if message: + metadata['message'] = message + if invalid_pages: + metadata['invalid_pages'] = json.dumps(invalid_pages) + utils_s3upload( filedata=data, region=current_app.config['AWS_REGION'], bucket_name=current_app.config['TRANSIENT_UPLOADED_LETTERS'], file_location=file_location, - metadata={ - 'status': status, - 'page_count': str(page_count), - 'filename': filename, - } + metadata=metadata, ) diff --git a/app/templates/components/banner.html b/app/templates/components/banner.html index 3b30cf1cb..1a272fa55 100644 --- a/app/templates/components/banner.html +++ b/app/templates/components/banner.html @@ -1,12 +1,15 @@ {% from "components/form.html" import form_wrapper %} -{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None, context=None, action=None) %} +{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None, context=None, action=None, id=None) %}
{% if subhead -%}

{{ subhead }}

@@ -26,6 +29,6 @@
{% endmacro %} -{% macro banner_wrapper(type=None, with_tick=False, delete_button=None, subhead=None, action=None) %} - {{ banner(caller()|safe, type=type, with_tick=with_tick, delete_button=delete_button, subhead=subhead, action=action) }} +{% macro banner_wrapper(type=None, with_tick=False, delete_button=None, subhead=None, action=None, id=None) %} + {{ banner(caller()|safe, type=type, with_tick=with_tick, delete_button=delete_button, subhead=subhead, action=action, id=id) }} {% endmacro %} diff --git a/app/templates/components/file-upload.html b/app/templates/components/file-upload.html index 78598ca8a..b7acf0c56 100644 --- a/app/templates/components/file-upload.html +++ b/app/templates/components/file-upload.html @@ -4,10 +4,11 @@ button_text="Choose file", alternate_link=None, alternate_link_text=None, - hint=None + hint=None, + show_errors=True ) %} -
+