diff --git a/app/__init__.py b/app/__init__.py index f39d4eaae..cac27c73d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -67,6 +67,7 @@ from app.notify_client.inbound_number_client import inbound_number_client from app.notify_client.billing_api_client import billing_api_client from app.notify_client.complaint_api_client import complaint_api_client from app.notify_client.platform_stats_api_client import platform_stats_api_client +from app.notify_client.template_folder_api_client import template_folder_api_client from app.commands import setup_commands from app.utils import get_cdn_domain, gmt_timezones, id_safe @@ -128,6 +129,7 @@ def create_app(application): billing_api_client.init_app(application) complaint_api_client.init_app(application) platform_stats_api_client.init_app(application) + template_folder_api_client.init_app(application) login_manager.init_app(application) login_manager.login_view = 'main.sign_in' diff --git a/app/main/forms.py b/app/main/forms.py index 5b1313c0e..eec7a5daf 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -857,21 +857,22 @@ class DateFilterForm(StripWhitespaceForm): class ChooseTemplateType(StripWhitespaceForm): template_type = RadioField( - 'What kind of template do you want to add?', + '', validators=[ DataRequired() ] ) - def __init__(self, include_letters=False, include_copy=False, *args, **kwargs): + def __init__(self, include_letters=False, include_copy=False, include_folder=False, *args, **kwargs): super().__init__(*args, **kwargs) self.template_type.choices = filter(None, [ - ('email', 'Email'), - ('sms', 'Text message'), - ('letter', 'Letter') if include_letters else None, + ('email', 'Email template'), + ('sms', 'Text message template'), + ('letter', 'Letter template') if include_letters else None, ('copy-existing', 'Copy of an existing template') if include_copy else None, + ('folder', 'Folder') if include_folder else None, ]) @@ -1106,3 +1107,7 @@ class ReturnedLettersForm(StripWhitespaceForm): DataRequired(message="Can’t be empty"), ] ) + + +class TemplateFolderForm(StripWhitespaceForm): + name = StringField('Folder name', validators=[DataRequired(message='Can’t be empty')]) diff --git a/app/main/views/conversation.py b/app/main/views/conversation.py index 540945afb..abe71fc3a 100644 --- a/app/main/views/conversation.py +++ b/app/main/views/conversation.py @@ -44,7 +44,7 @@ def conversation_reply( service_id, notification_id, ): - templates = current_service.templates_by_type('sms') + templates = current_service.get_templates('sms') return render_template( 'views/templates/choose-reply.html', templates=templates, diff --git a/app/main/views/templates.py b/app/main/views/templates.py index 849cb2e5e..b5b261a97 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -12,6 +12,7 @@ from notifications_utils.recipients import first_column_headings from app import ( current_service, service_api_client, + template_folder_api_client, template_statistics_client, user_api_client, ) @@ -23,6 +24,7 @@ from app.main.forms import ( SearchTemplatesForm, SetTemplateSenderForm, SMSTemplateForm, + TemplateFolderForm, ) from app.main.views.send import get_example_csv_rows, get_sender_details from app.models.service import Service @@ -98,13 +100,23 @@ def start_tour(service_id, template_id): @main.route("/services//templates") +@main.route("/services//templates/folders/") @main.route("/services//templates/") +@main.route("/services//templates//folders/") @login_required @user_has_permissions() -def choose_template(service_id, template_type='all'): +def choose_template(service_id, template_type='all', template_folder_id=None): template_nav_items = [ - (label, key, url_for('.choose_template', service_id=current_service.id, template_type=key), '') + ( + label, + key, + url_for( + '.choose_template', service_id=current_service.id, + template_type=key, template_folder_id=template_folder_id + ), + '' + ) for label, key in filter(None, [ ('All', 'all'), ('Text message', 'sms'), @@ -115,11 +127,14 @@ def choose_template(service_id, template_type='all'): return render_template( 'views/templates/choose.html', - templates=current_service.templates_by_type(template_type), - show_search_box=(len(current_service.templates_by_type(template_type)) > 7), + current_template_folder_id=template_folder_id, + template_folder_path=current_service.get_template_folder_path(template_folder_id), + template_folders=current_service.get_template_folders(template_folder_id), + templates=current_service.get_templates(template_type, template_folder_id), + show_search_box=(len(current_service.get_templates(template_type)) > 7), show_template_nav=( current_service.has_multiple_template_types - and (len(current_service.templates) > 2) + and (len(current_service.all_templates) > 2) ), template_nav_items=template_nav_items, template_type=template_type, @@ -173,9 +188,10 @@ def view_template_version_preview(service_id, template_id, version, filetype): @main.route("/services//templates/add", methods=['GET', 'POST']) +@main.route("/services//templates/folders//add", methods=['GET', 'POST']) @login_required @user_has_permissions('manage_templates') -def add_template_by_type(service_id): +def add_template_by_type(service_id, template_folder_id=None): form = ChooseTemplateType( include_letters=current_service.has_permission('letter'), @@ -183,6 +199,7 @@ def add_template_by_type(service_id): service_api_client.count_service_templates(service_id) > 0, len(user_api_client.get_service_ids_for_user(current_user)) > 1, )), + include_folder=current_service.has_permission('edit_folders') ) if form.validate_on_submit(): @@ -221,6 +238,7 @@ def add_template_by_type(service_id): '.add_service_template', service_id=service_id, template_type=form.template_type.data, + template_folder_id=template_folder_id, )) return render_template('views/templates/add.html', form=form) @@ -288,10 +306,34 @@ def action_blocked(service_id, notification_type, return_to, template_id): ) +@main.route("/services//templates/add-folder", methods=['GET', 'POST']) +@main.route("/services//templates/folders//add-folder", methods=['GET', 'POST']) +def add_template_folder(service_id, template_folder_id=None): + if not current_service.has_permission('edit_folders'): + abort(403) + + form = TemplateFolderForm() + + if form.validate_on_submit(): + template_folder_api_client.create_template_folder( + current_service.id, name=form.name.data, parent_id=template_folder_id + ) + return redirect( + url_for('.choose_template', service_id=service_id, template_folder_id=template_folder_id) + ) + + return render_template( + 'views/templates/add-template-folder.html', + form=form + ) + + @main.route("/services//templates/add-", methods=['GET', 'POST']) +@main.route("/services//templates/folders//add-", + methods=['GET', 'POST']) @login_required @user_has_permissions('manage_templates') -def add_service_template(service_id, template_type): +def add_service_template(service_id, template_type, template_folder_id=None): if template_type not in ['sms', 'email', 'letter']: abort(404) diff --git a/app/models/service.py b/app/models/service.py index 6e3bb9794..097e25579 100644 --- a/app/models/service.py +++ b/app/models/service.py @@ -7,6 +7,9 @@ from app.notify_client.inbound_number_client import inbound_number_client from app.notify_client.job_api_client import job_api_client from app.notify_client.organisations_api_client import organisations_client from app.notify_client.service_api_client import service_api_client +from app.notify_client.template_folder_api_client import ( + template_folder_api_client, +) from app.notify_client.user_api_client import user_api_client from app.utils import get_default_sms_sender @@ -88,7 +91,7 @@ class Service(): ) > 1 @cached_property - def templates(self): + def all_templates(self): templates = service_api_client.get_service_templates(self.id)['data'] @@ -97,12 +100,14 @@ class Service(): if template['template_type'] in self.available_template_types ] - def templates_by_type(self, template_type): + def get_templates(self, template_type='all', template_folder_id=None): if isinstance(template_type, str): template_type = [template_type] + return [ - template for template in self.templates - if set(template_type) & {'all', template['template_type']} + template for template in self.all_templates + if (set(template_type) & {'all', template['template_type']}) + and template.get('folder_id') == template_folder_id ] @property @@ -114,21 +119,21 @@ class Service(): @property def has_templates(self): - return len(self.templates) > 0 + return len(self.all_templates) > 0 @property def has_multiple_template_types(self): return len({ - template['template_type'] for template in self.templates + template['template_type'] for template in self.all_templates }) > 1 @property def has_email_templates(self): - return len(self.templates_by_type('email')) > 0 + return len(self.get_templates('email')) > 0 @property def has_sms_templates(self): - return len(self.templates_by_type('sms')) > 0 + return len(self.get_templates('sms')) > 0 @cached_property def email_reply_to_addresses(self): @@ -275,3 +280,28 @@ class Service(): @property def has_inbound_number(self): return bool(self.inbound_number) + + @cached_property + def all_template_folders(self): + return template_folder_api_client.get_template_folders(self.id) + + def get_template_folders(self, parent_folder_id=None): + return [ + folder for folder in self.all_template_folders + if folder['parent_id'] == parent_folder_id + ] + + def get_template_folder_path(self, template_folder_id): + if template_folder_id is None: + return [] + + id_to_folder = {folder['id']: folder for folder in self.all_template_folders} + + folder = id_to_folder[template_folder_id] + path = [folder] + + while folder['parent_id']: + folder = id_to_folder[folder['parent_id']] + path.append(folder) + + return list(reversed(path)) diff --git a/app/navigation.py b/app/navigation.py index cf893c5ca..af45c74ba 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -109,6 +109,7 @@ class HeaderNavigation(Navigation): 'add_service', 'add_service_template', 'add_template_by_type', + 'add_template_folder', 'agreement', 'api_callbacks', 'api_documentation', @@ -299,6 +300,7 @@ class MainNavigation(Navigation): 'action_blocked', 'add_service_template', 'add_template_by_type', + 'add_template_folder', 'check_messages', 'check_notification', 'choose_template', @@ -552,6 +554,7 @@ class CaseworkNavigation(Navigation): 'add_service', 'add_service_template', 'add_template_by_type', + 'add_template_folder', 'agreement', 'api_callbacks', 'api_documentation', @@ -787,6 +790,7 @@ class OrgNavigation(Navigation): 'add_service', 'add_service_template', 'add_template_by_type', + 'add_template_folder', 'agreement', 'api_callbacks', 'api_documentation', diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index 539fbbb73..78d03a563 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache diff --git a/app/notify_client/template_folder_api_client.py b/app/notify_client/template_folder_api_client.py new file mode 100644 index 000000000..c04a7bada --- /dev/null +++ b/app/notify_client/template_folder_api_client.py @@ -0,0 +1,28 @@ +from app.notify_client import NotifyAdminAPIClient, cache + + +class TemplateFolderAPIClient(NotifyAdminAPIClient): + # Fudge assert in the super __init__ so + # we can set those variables later. + def __init__(self): + super().__init__('a' * 73, 'b') + + @cache.delete('service-{service_id}-template-folders') + def create_template_folder( + self, + service_id, + name, + parent_id=None + ): + data = { + 'name': name, + 'parent_id': parent_id + } + return self.post('/service/{}/template-folder'.format(service_id), data)['data']['id'] + + @cache.set('service-{service_id}-template-folders') + def get_template_folders(self, service_id): + return self.get('/service/{}/template-folder'.format(service_id))['template_folders'] + + +template_folder_api_client = TemplateFolderAPIClient() diff --git a/app/templates/views/dashboard/dashboard.html b/app/templates/views/dashboard/dashboard.html index 3ddcc0f39..e480efe95 100644 --- a/app/templates/views/dashboard/dashboard.html +++ b/app/templates/views/dashboard/dashboard.html @@ -15,7 +15,7 @@

Dashboard

- {% if current_user.has_permissions('manage_templates') and not current_service.templates %} + {% if current_user.has_permissions('manage_templates') and not current_service.all_templates %} {% include 'views/dashboard/write-first-messages.html' %} {% endif %} diff --git a/app/templates/views/service-settings/name.html b/app/templates/views/service-settings/name.html index 33694395d..182af6fc8 100644 --- a/app/templates/views/service-settings/name.html +++ b/app/templates/views/service-settings/name.html @@ -1,3 +1,4 @@ + {% extends "withnav_template.html" %} {% from "components/textbox.html" import textbox %} {% from "components/page-footer.html" import page_footer %} diff --git a/app/templates/views/templates/add-template-folder.html b/app/templates/views/templates/add-template-folder.html new file mode 100644 index 000000000..81a816918 --- /dev/null +++ b/app/templates/views/templates/add-template-folder.html @@ -0,0 +1,20 @@ + +{% extends "withnav_template.html" %} +{% from "components/textbox.html" import textbox %} +{% from "components/page-footer.html" import page_footer %} +{% from "components/form.html" import form_wrapper %} + +{% block service_page_title %} + Add a folder +{% endblock %} + +{% block maincolumn_content %} + +

Add a folder

+ + {% call form_wrapper() %} + {{ textbox(form.name) }} + {{ page_footer('Save') }} + {% endcall %} + +{% endblock %} diff --git a/app/templates/views/templates/add.html b/app/templates/views/templates/add.html index c46f73da4..923acfb63 100644 --- a/app/templates/views/templates/add.html +++ b/app/templates/views/templates/add.html @@ -5,12 +5,12 @@ {% extends "withnav_template.html" %} {% block service_page_title %} - Add new template + Add new template or folder {% endblock %} {% block maincolumn_content %} -

Add new template

+

What do you want to add?

{% call form_wrapper() %} {{ radios(form.template_type) }} diff --git a/app/templates/views/templates/choose.html b/app/templates/views/templates/choose.html index e69ac226a..dfff6b425 100644 --- a/app/templates/views/templates/choose.html +++ b/app/templates/views/templates/choose.html @@ -9,13 +9,18 @@ {% block service_page_title %} {{ page_title }} + {% for folder in template_folder_path %} + / {{ folder.name }} + {% endfor %} {% endblock %} {% block maincolumn_content %} - {% if not templates %} + {% if (not templates) and (not template_folder_path) %} -

{{ page_title }}

+

+ {{ page_title }} +

{% if current_user.has_permissions('manage_templates') %}

You need a template before you can send @@ -41,11 +46,25 @@

-

{{ page_title }}

+

+ {% if template_folder_path %} + {{ page_title }} + {% else %} + {{ page_title }} + {% endif %} + {% for folder in template_folder_path %} + / + {% if loop.last %} + {{ folder.name }} + {% else %} + {{ folder.name }} + {% endif %} + {% endfor %} +

{% if current_user.has_permissions('manage_templates') %} {% endif %}
@@ -59,6 +78,16 @@ {{ live_search(target_selector='#template-list .column-whole', show=show_search_box, form=search_form) }}