diff --git a/app/config.py b/app/config.py index eabd7391f..4232e2411 100644 --- a/app/config.py +++ b/app/config.py @@ -87,6 +87,8 @@ class Config(object): r"cqc\.org\.uk", ] + LOGO_UPLOAD_BUCKET_NAME = 'public-logos-local' + class Development(Config): DEBUG = True @@ -94,6 +96,7 @@ class Development(Config): SESSION_PROTECTION = None STATSD_ENABLED = False CSV_UPLOAD_BUCKET_NAME = 'development-notifications-csv-upload' + LOGO_UPLOAD_BUCKET_NAME = 'public-logos-tools' class Test(Development): @@ -102,6 +105,7 @@ class Test(Development): STATSD_ENABLED = True WTF_CSRF_ENABLED = False CSV_UPLOAD_BUCKET_NAME = 'test-notifications-csv-upload' + LOGO_UPLOAD_BUCKET_NAME = 'public-logos-test' NOTIFY_ENVIRONMENT = 'test' TEMPLATE_PREVIEW_API_HOST = 'http://localhost:9999' @@ -111,6 +115,7 @@ class Preview(Config): HEADER_COLOUR = '#F499BE' # $baby-pink STATSD_ENABLED = True CSV_UPLOAD_BUCKET_NAME = 'preview-notifications-csv-upload' + LOGO_UPLOAD_BUCKET_NAME = 'public-logos-preview' NOTIFY_ENVIRONMENT = 'preview' @@ -120,6 +125,7 @@ class Staging(Config): HEADER_COLOUR = '#6F72AF' # $mauve STATSD_ENABLED = True CSV_UPLOAD_BUCKET_NAME = 'staging-notify-csv-upload' + LOGO_UPLOAD_BUCKET_NAME = 'public-logos-staging' NOTIFY_ENVIRONMENT = 'staging' @@ -129,6 +135,7 @@ class Live(Config): HTTP_PROTOCOL = 'https' STATSD_ENABLED = True CSV_UPLOAD_BUCKET_NAME = 'live-notifications-csv-upload' + LOGO_UPLOAD_BUCKET_NAME = 'public-logos-live' NOTIFY_ENVIRONMENT = 'live' @@ -142,6 +149,7 @@ class Sandbox(CloudFoundryConfig): HEADER_COLOUR = '#F499BE' # $baby-pink STATSD_ENABLED = True CSV_UPLOAD_BUCKET_NAME = 'cf-sandbox-notifications-csv-upload' + LOGO_UPLOAD_BUCKET_NAME = 'cf-sandbox-notifications-logo-upload' NOTIFY_ENVIRONMENT = 'sandbox' diff --git a/app/main/__init__.py b/app/main/__init__.py index 506d07858..84f2b1d0b 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -28,6 +28,7 @@ from app.main.views import ( providers, platform_admin, letter_jobs, + organisations, conversation, notifications ) diff --git a/app/main/forms.py b/app/main/forms.py index 5f6560716..bc334ab41 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -26,6 +26,7 @@ from wtforms import ( SelectField) from wtforms.fields.html5 import EmailField, TelField, SearchField from wtforms.validators import (DataRequired, Email, Length, Regexp, Optional) +from flask_wtf.file import FileField as FileField_wtf, FileAllowed from app.main.validators import (Blacklist, CsvFileValidator, ValidGovEmail, NoCommasInPlaceHolders, OnlyGSMCharacters) @@ -548,6 +549,28 @@ class ServiceBrandingOrg(Form): ) +class ServiceSelectOrg(Form): + + def __init__(self, organisations=[], *args, **kwargs): + self.organisation.choices = organisations + super(ServiceSelectOrg, self).__init__(*args, **kwargs) + + organisation = RadioField( + 'Organisation', + validators=[ + DataRequired() + ] + ) + + +class ServiceManageOrg(Form): + + name = StringField('Name') + + colour = StringField('Colour', render_kw={'onkeyup': 'update_colour_span()', 'onblur': 'update_colour_span()'}) + file = FileField_wtf('Upload a PNG logo', validators=[FileAllowed(['png'], 'PNG Images only!')]) + + class LetterBranding(Form): def __init__(self, choices=[], *args, **kwargs): diff --git a/app/main/s3_client.py b/app/main/s3_client.py new file mode 100644 index 000000000..75404ac5e --- /dev/null +++ b/app/main/s3_client.py @@ -0,0 +1,102 @@ +import uuid +import botocore +from boto3 import resource +from flask import current_app +from notifications_utils.s3 import s3upload as utils_s3upload + +FILE_LOCATION_STRUCTURE = 'service-{}-notify/{}.csv' +TEMP_TAG = 'temp-{user_id}_' +LOGO_LOCATION_STRUCTURE = '{temp}{unique_id}-{filename}' + + +def get_s3_object(bucket_name, filename): + s3 = resource('s3') + return s3.Object(bucket_name, filename) + + +def delete_s3_object(filename): + bucket_name = current_app.config['LOGO_UPLOAD_BUCKET_NAME'] + get_s3_object(bucket_name, filename).delete() + + +def rename_s3_object(old_name, new_name): + bucket_name = current_app.config['LOGO_UPLOAD_BUCKET_NAME'] + get_s3_object(bucket_name, new_name).copy_from( + CopySource='{}/{}'.format(bucket_name, old_name)) + delete_s3_object(old_name) + + +def get_s3_objects_filter_by_prefix(prefix): + bucket_name = current_app.config['LOGO_UPLOAD_BUCKET_NAME'] + s3 = resource('s3') + return s3.Bucket(bucket_name).objects.filter(Prefix=prefix) + + +def get_temp_truncated_filename(filename, user_id): + return filename[len(TEMP_TAG.format(user_id=user_id)):] + + +def s3upload(service_id, filedata, region): + upload_id = str(uuid.uuid4()) + upload_file_name = FILE_LOCATION_STRUCTURE.format(service_id, upload_id) + utils_s3upload(filedata=filedata['data'], + region=region, + bucket_name=current_app.config['CSV_UPLOAD_BUCKET_NAME'], + file_location=upload_file_name) + return upload_id + + +def s3download(service_id, upload_id): + contents = '' + try: + bucket_name = current_app.config['CSV_UPLOAD_BUCKET_NAME'] + upload_file_name = FILE_LOCATION_STRUCTURE.format(service_id, upload_id) + key = get_s3_object(bucket_name, upload_file_name) + contents = key.get()['Body'].read().decode('utf-8') + except botocore.exceptions.ClientError as e: + current_app.logger.error("Unable to download s3 file {}".format( + FILE_LOCATION_STRUCTURE.format(service_id, upload_id))) + raise e + return contents + + +def upload_logo(filename, filedata, region, user_id): + upload_file_name = LOGO_LOCATION_STRUCTURE.format( + temp=TEMP_TAG.format(user_id=user_id), + unique_id=str(uuid.uuid4()), + filename=filename + ) + bucket_name = current_app.config['LOGO_UPLOAD_BUCKET_NAME'] + utils_s3upload( + filedata=filedata, + region=region, + bucket_name=bucket_name, + file_location=upload_file_name, + content_type='image/png' + ) + + return upload_file_name + + +def persist_logo(filename, user_id): + if filename.startswith(TEMP_TAG.format(user_id=user_id)): + persisted_filename = get_temp_truncated_filename( + filename=filename, user_id=user_id) + else: + return filename + + rename_s3_object(filename, persisted_filename) + + return persisted_filename + + +def delete_temp_files_created_by(user_id): + for obj in get_s3_objects_filter_by_prefix(TEMP_TAG.format(user_id=user_id)): + delete_s3_object(obj.key) + + +def delete_temp_file(filename): + if not filename.startswith(TEMP_TAG[:5]): + raise ValueError('Not a temp file: {}'.format(filename)) + + delete_s3_object(filename) diff --git a/app/main/uploader.py b/app/main/uploader.py deleted file mode 100644 index 4c00fbce5..000000000 --- a/app/main/uploader.py +++ /dev/null @@ -1,32 +0,0 @@ -import uuid -import botocore -from boto3 import resource -from flask import current_app -from notifications_utils.s3 import s3upload as utils_s3upload - -FILE_LOCATION_STRUCTURE = 'service-{}-notify/{}.csv' - - -def s3upload(service_id, filedata, region): - upload_id = str(uuid.uuid4()) - upload_file_name = FILE_LOCATION_STRUCTURE.format(service_id, upload_id) - utils_s3upload(filedata=filedata['data'], - region=region, - bucket_name=current_app.config['CSV_UPLOAD_BUCKET_NAME'], - file_location=upload_file_name) - return upload_id - - -def s3download(service_id, upload_id): - contents = '' - try: - s3 = resource('s3') - bucket_name = current_app.config['CSV_UPLOAD_BUCKET_NAME'] - upload_file_name = FILE_LOCATION_STRUCTURE.format(service_id, upload_id) - key = s3.Object(bucket_name, upload_file_name) - contents = key.get()['Body'].read().decode('utf-8') - except botocore.exceptions.ClientError as e: - current_app.logger.error("Unable to download s3 file {}".format( - FILE_LOCATION_STRUCTURE.format(service_id, upload_id))) - raise e - return contents diff --git a/app/main/views/organisations.py b/app/main/views/organisations.py new file mode 100644 index 000000000..0fe976752 --- /dev/null +++ b/app/main/views/organisations.py @@ -0,0 +1,97 @@ +from flask import current_app, redirect, render_template, session, url_for, request +from flask_login import login_required + +from app import organisations_client +from app.main import main +from app.main.forms import ( + ServiceSelectOrg, + ServiceManageOrg) +from app.utils import user_has_permissions, get_cdn_domain +from app.main.s3_client import ( + TEMP_TAG, + upload_logo, + delete_temp_file, + delete_temp_files_created_by, + persist_logo +) +from app.main.views.service_settings import get_branding_as_value_and_label, get_branding_as_dict + + +@main.route("/organisations", methods=['GET', 'POST']) +@main.route("/organisations/", methods=['GET', 'POST']) +@login_required +@user_has_permissions(admin_override=True) +def organisations(organisation_id=None): + orgs = organisations_client.get_organisations() + + form = ServiceSelectOrg() + form.organisation.choices = get_branding_as_value_and_label(orgs) + [('None', 'Create a new organisation')] + + if form.validate_on_submit(): + if form.organisation.data != 'None': + session['organisation'] = [o for o in orgs if o['id'] == form.organisation.data][0] + elif session.get('organisation'): + del session['organisation'] + + return redirect(url_for('.manage_org')) + + form.organisation.data = organisation_id if organisation_id in [o['id'] for o in orgs] else 'None' + + return render_template( + 'views/organisations/select-org.html', + form=form, + branding_dict=get_branding_as_dict(orgs), + organisation_id=organisation_id + ) + + +@main.route("/organisations/manage", methods=['GET', 'POST']) +@main.route("/organisations/manage/", methods=['GET', 'POST']) +@login_required +@user_has_permissions(admin_override=True) +def manage_org(logo=None): + form = ServiceManageOrg() + + org = session.get("organisation") + + logo = logo if logo else org.get('logo') if org else None + + if form.validate_on_submit(): + if form.file.data: + upload_filename = upload_logo( + form.file.data.filename, + form.file.data, + current_app.config['AWS_REGION'], + user_id=session["user_id"] + ) + + if logo and logo.startswith(TEMP_TAG.format(user_id=session['user_id'])): + delete_temp_file(logo) + + return redirect( + url_for('.manage_org', logo=upload_filename)) + + logo = persist_logo(logo, session["user_id"]) + delete_temp_files_created_by(session["user_id"]) + + if org: + organisations_client.update_organisation( + org_id=org['id'], logo=logo, name=form.name.data, colour=form.colour.data) + org_id = org['id'] + else: + resp = organisations_client.create_organisation( + logo=logo, name=form.name.data, colour=form.colour.data) + org_id = resp['data']['id'] + + return redirect(url_for('.organisations', organisation_id=org_id)) + if org: + form.name.data = org['name'] + form.colour.data = org['colour'] + + return render_template( + 'views/organisations/manage-org.html', + form=form, + organisation=org, + cdn_url=get_cdn_domain(), + logo=logo + ) diff --git a/app/main/views/send.py b/app/main/views/send.py index 557faf0ba..354c5c8e2 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -33,7 +33,7 @@ from app.main.forms import ( ChooseTimeForm, get_placeholder_form_instance ) -from app.main.uploader import ( +from app.main.s3_client import ( s3upload, s3download ) diff --git a/app/notify_client/organisations_client.py b/app/notify_client/organisations_client.py index bf4717976..aae8d4c07 100644 --- a/app/notify_client/organisations_client.py +++ b/app/notify_client/organisations_client.py @@ -19,3 +19,19 @@ class OrganisationsClient(NotifyAdminAPIClient): def get_letter_organisations(self): return self.get(url='/dvla_organisations') + + def create_organisation(self, logo, name, colour): + data = { + "logo": logo, + "name": name, + "colour": colour + } + return self.post(url="/organisation", data=data) + + def update_organisation(self, org_id, logo, name, colour): + data = { + "logo": logo, + "name": name, + "colour": colour + } + return self.post(url="/organisation/{}".format(org_id), data=data) diff --git a/app/templates/components/page-footer.html b/app/templates/components/page-footer.html index 5dcc69e4f..74b847a0c 100644 --- a/app/templates/components/page-footer.html +++ b/app/templates/components/page-footer.html @@ -6,12 +6,13 @@ secondary_link=False, secondary_link_text=None, delete_link=False, - delete_link_text="delete" + delete_link_text="delete", + button_disabled=False ) %}