def process_precompiled_letter_notifications(*, letter_data, api_key, template, reply_to_text): try: status = NOTIFICATION_PENDING_VIRUS_CHECK letter_content = base64.b64decode(letter_data['content']) pages = pdf_page_count(io.BytesIO(letter_content)) except ValueError: raise BadRequestError(message='Cannot decode letter content (invalid base64 encoding)', status_code=400) except PdfReadError: current_app.logger.exception(msg='Invalid PDF received') raise BadRequestError(message='Letter content is not a valid PDF', status_code=400) notification = create_letter_notification(letter_data=letter_data, template=template, api_key=api_key, status=status, reply_to_text=reply_to_text) filename = upload_letter_pdf(notification, letter_content, precompiled=True) pages_per_sheet = 2 notification.billable_units = math.ceil(pages / pages_per_sheet) dao_update_notification(notification) current_app.logger.info('Calling task scan-file for {}'.format(filename)) # call task to add the filename to anti virus queue notify_celery.send_task( name=TaskNames.SCAN_FILE, kwargs={'filename': filename}, queue=QueueNames.ANTIVIRUS, ) return notification
def sanitise_file_contents(encoded_string, *, allow_international_letters, filename=None): """ Given a PDF, returns a new PDF that has been sanitised and dvla approved đź‘Ť * makes sure letter meets DVLA's printable boundaries and page dimensions requirements * re-writes address block (to ensure it's in arial in the right location) * adds NOTIFY tag if not present """ try: file_data = BytesIO(encoded_string) page_count = pdf_page_count(file_data) if is_letter_too_long(page_count): message = "letter-too-long" raise ValidationFailed(message, page_count=page_count) message, invalid_pages = get_invalid_pages_with_message(file_data) if message: raise ValidationFailed(message, invalid_pages, page_count=page_count) file_data, recipient_address, redaction_failed_message = rewrite_pdf( file_data, page_count=page_count, allow_international_letters=allow_international_letters, filename=filename) return { "recipient_address": recipient_address, "page_count": page_count, "message": None, "invalid_pages": None, "redaction_failed_message": redaction_failed_message, "file": base64.b64encode(file_data.read()).decode('utf-8') } except Exception as error: if isinstance(error, ValidationFailed): current_app.logger.warning( 'Validation Failed for precompiled pdf: {} for file name: {}'. format(repr(error), filename)) else: current_app.logger.exception( 'Unhandled exception with precompiled pdf: {} for file name: {}' .format(repr(error), filename)) return { "page_count": getattr(error, 'page_count', None), "recipient_address": None, "message": getattr(error, 'message', 'unable-to-read-the-file'), "invalid_pages": getattr(error, 'invalid_pages', None), "file": None }
def view_notification(service_id, notification_id): notification = notification_api_client.get_notification( service_id, str(notification_id)) notification['template'].update( {'reply_to_text': notification['reply_to_text']}) if notification['template']['is_precompiled_letter']: file_contents = view_letter_notification_as_preview( service_id, notification_id, "pdf") page_count = pdf_page_count(io.BytesIO(file_contents)) else: page_count = get_page_count_for_letter(notification['template']) template = get_template( notification['template'], current_service, letter_preview_url=url_for( '.view_letter_notification_as_preview', service_id=service_id, notification_id=notification_id, filetype='png', ), page_count=page_count, show_recipient=True, redact_missing_personalisation=True, ) template.values = get_all_personalisation_from_notification(notification) if notification['job']: job = job_api_client.get_job(service_id, notification['job']['id'])['data'] else: job = None return render_template( 'views/notifications/notification.html', finished=(notification['status'] in (DELIVERED_STATUSES + FAILURE_STATUSES)), uploaded_file_name='Report', template=template, job=job, updates_url=url_for(".view_notification_updates", service_id=service_id, notification_id=notification['id'], status=request.args.get('status'), help=get_help_argument()), partials=get_single_notification_partials(notification), created_by=notification.get('created_by'), created_at=notification['created_at'], help=get_help_argument(), estimated_letter_delivery_date=get_letter_timings( notification['created_at']).earliest_delivery, notification_id=notification['id'], can_receive_inbound=(current_service.has_permission('inbound_sms')), is_precompiled_letter=notification['template'] ['is_precompiled_letter'])
def test_replace_first_page_of_pdf_with_new_content(): x2 = A4_WIDTH * mm y2 = A4_HEIGHT * mm assert _extract_text_from_first_page_of_pdf(BytesIO(single_sample_page), x1=0, y1=0, x2=x2, y2=y2) == 'Z' assert _extract_text_from_first_page_of_pdf(BytesIO(sample_pages), x1=0, y1=0, x2=x2, y2=y2) == 'A' modified_pdf = replace_first_page_of_pdf_with_new_content( BytesIO(sample_pages), BytesIO(single_sample_page)) assert _extract_text_from_first_page_of_pdf(modified_pdf, x1=0, y1=0, x2=x2, y2=y2) == 'Z' assert pdf_page_count(modified_pdf) == 3
def view_notification(service_id, notification_id): notification = notification_api_client.get_notification( service_id, str(notification_id)) notification['template'].update( {'reply_to_text': notification['reply_to_text']}) personalisation = get_all_personalisation_from_notification(notification) if notification['template']['is_precompiled_letter']: try: file_contents = view_letter_notification_as_preview( service_id, notification_id, "pdf") page_count = pdf_page_count(io.BytesIO(file_contents)) except PdfReadError: return render_template( 'views/notifications/invalid_precompiled_letter.html', created_at=notification['created_at']) else: page_count = get_page_count_for_letter(notification['template'], values=personalisation) if notification.get('postage'): notification['template']['postage'] = notification['postage'] template = get_template( notification['template'], current_service, letter_preview_url=url_for( '.view_letter_notification_as_preview', service_id=service_id, notification_id=notification_id, filetype='png', ), page_count=page_count, show_recipient=True, redact_missing_personalisation=True, ) template.values = personalisation if notification['job']: job = job_api_client.get_job(service_id, notification['job']['id'])['data'] else: job = None letter_print_day = get_letter_printing_statement( notification['status'], notification['created_at']) notification_created = parser.parse( notification['created_at']).replace(tzinfo=None) show_cancel_button = notification['notification_type'] == 'letter' and \ letter_can_be_cancelled(notification['status'], notification_created) if get_help_argument() or request.args.get('help') == '0': # help=0 is set when you’ve just sent a notification. We # only want to show the back link when you’ve navigated to a # notification, not when you’ve just sent it. back_link = None elif request.args.get('from_job'): back_link = url_for( 'main.view_job', service_id=current_service.id, job_id=request.args.get('from_job'), ) else: back_link = url_for( 'main.view_notifications', service_id=current_service.id, message_type=template.template_type, status='sending,delivered,failed', ) return render_template( 'views/notifications/notification.html', finished=(notification['status'] in (DELIVERED_STATUSES + FAILURE_STATUSES)), notification_status=notification['status'], uploaded_file_name='Report', template=template, job=job, updates_url=url_for(".view_notification_updates", service_id=service_id, notification_id=notification['id'], status=request.args.get('status'), help=get_help_argument()), partials=get_single_notification_partials(notification), created_by=notification.get('created_by'), created_at=notification['created_at'], updated_at=notification['updated_at'], help=get_help_argument(), estimated_letter_delivery_date=get_letter_timings( notification['created_at'], postage=notification['postage']).earliest_delivery, notification_id=notification['id'], postage=notification['postage'], can_receive_inbound=(current_service.has_permission('inbound_sms')), is_precompiled_letter=notification['template'] ['is_precompiled_letter'], letter_print_day=letter_print_day, show_cancel_button=show_cancel_button, sent_with_test_key=(notification.get('key_type') == KEY_TYPE_TEST), back_link=back_link, )
def get_page_count(pdf): return pdf_page_count(io.BytesIO(pdf))
def get_page_count(pdf): pages = pdf_page_count(io.BytesIO(pdf)) pages_per_sheet = 2 billable_units = math.ceil(pages / pages_per_sheet) return billable_units
def upload_letter(service_id): form = PDFUploadForm() error = {} if form.validate_on_submit(): pdf_file_bytes = form.file.data.read() original_filename = form.file.data.filename if current_app.config['ANTIVIRUS_ENABLED']: virus_free = antivirus_client.scan(BytesIO(pdf_file_bytes)) if not virus_free: return invalid_upload_error('Your file contains a virus') if len(pdf_file_bytes) > MAX_FILE_UPLOAD_SIZE: 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( "There’s a problem with your file", 'Notify cannot read this PDF.<br>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) try: response = sanitise_letter( BytesIO(pdf_file_bytes), allow_international_letters=current_service.has_permission( 'international_letters'), ) 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, message=validation_failed_message, invalid_pages=invalid_pages) else: raise ex else: response = response.json() recipient = response['recipient_address'] status = 'valid' file_contents = base64.b64decode(response['file'].encode()) upload_letter_to_s3(file_contents, file_location=file_location, status=status, page_count=page_count, filename=original_filename, recipient=recipient) return redirect( url_for( 'main.uploaded_letter_preview', service_id=current_service.id, file_id=upload_id, )) 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 test_pdf_page_count_src_pdf_not_a_pdf(): with pytest.raises(PdfReadError): file_data = base64.b64decode(not_pdf) pdf_page_count(BytesIO(file_data))
def test_pdf_page_count_src_pdf_has_multiple_pages(): file_data = base64.b64decode(multi_page_pdf) num = pdf_page_count(BytesIO(file_data)) assert num == 10
def test_pdf_page_count_src_pdf_has_one_page(): file_data = base64.b64decode(one_page_pdf) num = pdf_page_count(BytesIO(file_data)) assert num == 1
def test_pdf_page_count_src_pdf_is_null(): with pytest.raises(PdfReadError): pdf_page_count(None)
def view_notification(service_id, notification_id): notification = notification_api_client.get_notification(service_id, str(notification_id)) notification["template"].update({"reply_to_text": notification["reply_to_text"]}) personalisation = get_all_personalisation_from_notification(notification) if notification["template"]["is_precompiled_letter"]: try: file_contents = view_letter_notification_as_preview(service_id, notification_id, "pdf") page_count = pdf_page_count(io.BytesIO(file_contents)) except PdfReadError: return render_template( "views/notifications/invalid_precompiled_letter.html", created_at=notification["created_at"], ) else: page_count = get_page_count_for_letter(notification["template"], values=personalisation) if notification.get("postage"): notification["template"]["postage"] = notification["postage"] template = get_template( notification["template"], current_service, letter_preview_url=url_for( ".view_letter_notification_as_preview", service_id=service_id, notification_id=notification_id, filetype="png", ), page_count=page_count, show_recipient=True, redact_missing_personalisation=True, ) template.values = personalisation if notification["job"]: job = job_api_client.get_job(service_id, notification["job"]["id"])["data"] else: job = None letter_print_day = get_letter_printing_statement(notification["status"], notification["created_at"]) notification_created = parser.parse(notification["created_at"]).replace(tzinfo=None) show_cancel_button = notification["notification_type"] == "letter" and letter_can_be_cancelled( notification["status"], notification_created ) if get_help_argument() or request.args.get("help") == "0": # help=0 is set when you’ve just sent a notification. We # only want to show the back link when you’ve navigated to a # notification, not when you’ve just sent it. back_link = None elif request.args.get("from_job"): back_link = url_for( "main.view_job", service_id=current_service.id, job_id=request.args.get("from_job"), ) else: back_link = url_for( "main.view_notifications", service_id=current_service.id, message_type=template.template_type, status="sending,delivered,failed", ) return render_template( "views/notifications/notification.html", finished=(notification["status"] in (DELIVERED_STATUSES + FAILURE_STATUSES)), notification_status=notification["status"], uploaded_file_name="Report", template=template, job=job, updates_url=url_for( ".view_notification_updates", service_id=service_id, notification_id=notification["id"], status=request.args.get("status"), help=get_help_argument(), ), partials=get_single_notification_partials(notification), created_by=notification.get("created_by"), created_at=notification["created_at"], updated_at=notification["updated_at"], help=get_help_argument(), estimated_letter_delivery_date=get_letter_timings( notification["created_at"], postage=notification["postage"] ).earliest_delivery, notification_id=notification["id"], postage=notification["postage"], can_receive_inbound=(current_service.has_permission("inbound_sms")), is_precompiled_letter=notification["template"]["is_precompiled_letter"], letter_print_day=letter_print_day, show_cancel_button=show_cancel_button, sent_with_test_key=(notification.get("key_type") == KEY_TYPE_TEST), back_link=back_link, just_sent=request.args.get("just_sent"), attachments=get_attachments(notification, "attach").values(), )