diff --git a/app/config.py b/app/config.py index 77a3e0781..d9f82ad6a 100644 --- a/app/config.py +++ b/app/config.py @@ -96,7 +96,7 @@ class Development(Config): SESSION_PROTECTION = None STATSD_ENABLED = False CSV_UPLOAD_BUCKET_NAME = 'development-notifications-csv-upload' - LOGO_UPLOAD_BUCKET_NAME = 'development-notifications-logo-upload' + LOGO_UPLOAD_BUCKET_NAME = 'public-logos-tools' class Test(Development): 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 0b92466dd..1709a7171 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -25,6 +25,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) @@ -559,6 +560,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..93764348d --- /dev/null +++ b/app/main/s3_client.py @@ -0,0 +1,95 @@ +import uuid +import botocore +from boto3 import resource, client +from flask import current_app +from notifications_utils.s3 import s3upload as utils_s3upload + +FILE_LOCATION_STRUCTURE = 'service-{}-notify/{}.csv' +TEMP_TAG = 'temp-{}_' +LOGO_LOCATION_STRUCTURE = '{}{}-{}' + + +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 + + +def upload_logo(filename, filedata, region, user_id): + upload_id = str(uuid.uuid4()) + upload_file_name = LOGO_LOCATION_STRUCTURE.format(TEMP_TAG.format(user_id), upload_id, filename) + utils_s3upload(filedata=filedata, + region=region, + bucket_name=current_app.config['LOGO_UPLOAD_BUCKET_NAME'], + file_location=upload_file_name, + content_type='image/png') + return upload_file_name + + +def persist_logo(filename, user_id): + try: + if filename.startswith(TEMP_TAG.format(user_id)): + persisted_filename = filename[len(TEMP_TAG.format(user_id)):] + else: + return filename + + s3 = resource('s3') + bucket_name = current_app.config['LOGO_UPLOAD_BUCKET_NAME'] + + s3.Object(bucket_name, persisted_filename).copy_from(CopySource='{}/{}'.format(bucket_name, filename)) + s3.Object(bucket_name, filename).delete() + + return persisted_filename + except botocore.exceptions.ClientError as e: + current_app.logger.error("Unable to get s3 bucket contents {}".format( + bucket_name)) + raise e + + +def delete_temp_files_created_by(user_id): + try: + s3 = resource('s3') + bucket_name = current_app.config['LOGO_UPLOAD_BUCKET_NAME'] + + for obj in s3.Bucket(bucket_name).objects.filter(Prefix=TEMP_TAG.format(user_id)): + s3.Object(bucket_name, obj.key).delete() + + except botocore.exceptions.ClientError as e: + current_app.logger.error("Unable to delete s3 bucket temp files created by {} from {}".format( + user_id, bucket_name)) + raise e + + +def delete_temp_file(filename): + try: + if not filename.startswith(TEMP_TAG): + raise ValueError('Not a temp file') + + s3 = resource('s3') + bucket_name = current_app.config['LOGO_UPLOAD_BUCKET_NAME'] + + s3.Object(bucket_name, filename).delete() + + except botocore.exceptions.ClientError as e: + current_app.logger.error("Unable to delete s3 bucket file {} from {}".format( + filename, bucket_name)) + raise e 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..b6a2c7127 --- /dev/null +++ b/app/main/views/organisations.py @@ -0,0 +1,98 @@ +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(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..f75719a42 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("/organisation", data) + + def update_organisation(self, org_id, logo, name, colour): + data = { + "logo": logo, + "name": name, + "colour": colour + } + return self.post("/organisation/{}".format(org_id), data) diff --git a/app/templates/admin_template.html b/app/templates/admin_template.html index 215563caa..5be2db3b7 100644 --- a/app/templates/admin_template.html +++ b/app/templates/admin_template.html @@ -49,6 +49,18 @@
  • Platform admin
  • +<<<<<<< HEAD +======= +
  • + Providers +
  • +
  • + Organisations +
  • +
  • + Letter jobs +
  • +>>>>>>> Add org select and manage pages {% endif %}
  • Sign out 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 ) %}