From 9dec9ed62609168c4d2dde6e8e54f793595cccd6 Mon Sep 17 00:00:00 2001 From: Leo Hemsted Date: Wed, 14 Nov 2018 17:30:22 +0000 Subject: [PATCH 1/5] add new folder from the choose template page --- app/main/views/templates.py | 12 ++++++++++++ app/templates/views/templates/choose.html | 16 +++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/main/views/templates.py b/app/main/views/templates.py index 4b5c43f85..9506fc92a 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -117,6 +117,7 @@ def choose_template(service_id, template_type='all', template_folder_id=None): template_type=template_type, current_folder_id=template_folder_id, ) + add_folder_form = TemplateFolderForm() if is_valid_template_operation('move', templates_and_folders_form): current_service.move_to_folder( @@ -125,6 +126,16 @@ def choose_template(service_id, template_type='all', template_folder_id=None): ) return redirect(request.url) + if is_valid_template_operation('add_new_folder', add_folder_form): + template_folder_api_client.create_template_folder( + current_service.id, + name=add_folder_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/choose.html', current_template_folder_id=template_folder_id, @@ -140,6 +151,7 @@ def choose_template(service_id, template_type='all', template_folder_id=None): template_type=template_type, search_form=SearchTemplatesForm(), templates_and_folders_form=templates_and_folders_form, + add_folder_form=add_folder_form, ) diff --git a/app/templates/views/templates/choose.html b/app/templates/views/templates/choose.html index 40552d38e..e5f473ca3 100644 --- a/app/templates/views/templates/choose.html +++ b/app/templates/views/templates/choose.html @@ -3,6 +3,8 @@ {% from "components/message-count-label.html" import message_count_label %} {% from "components/textbox.html" import textbox %} {% from "components/live-search.html" import live_search %} +{% from "components/form.html" import form_wrapper %} +{% from "components/page-footer.html" import page_footer %} {% extends "withnav_template.html" %} @@ -76,10 +78,18 @@ {{ live_search(target_selector='#template-list .template-list-item', show=show_search_box, form=search_form) }} {% if can_manage_folders %} -
+ {% call form_wrapper() %} {% include 'views/templates/_template_list.html' %} - {% include 'views/templates/_move_to.html' %} -
+ {% with templates=templates, template_folders=template_folders, templates_and_folders_form=templates_and_folders_form %} + {% include 'views/templates/_move_to.html' %} + {% endwith %} + {% endcall %} + + {% call form_wrapper() %} + {{ textbox(add_folder_form.name) }} + {{ page_footer('New folder', button_name='operation', button_value='add_new_folder') }} + {% endcall %} + {% else %} {% include 'views/templates/_template_list.html' %} {% endif %} From 690585c017672752298f81b88ca4e3669a8a4a98 Mon Sep 17 00:00:00 2001 From: Leo Hemsted Date: Thu, 15 Nov 2018 17:31:07 +0000 Subject: [PATCH 2/5] move add new folder and move to folder into one dialog box this lets us combine the two actions to create "Move to a new folder". If the user hits enter rather than clicking a button, there is a hidden submit button that (for now) prevents them from taking any action. A future commit will try and understand what the user might be doing based on which fields are populated/selected. --- app/main/forms.py | 25 ++++++++++- app/main/views/templates.py | 49 +++++++++------------ app/templates/views/templates/_move_to.html | 6 ++- app/templates/views/templates/choose.html | 5 --- 4 files changed, 51 insertions(+), 34 deletions(-) diff --git a/app/main/forms.py b/app/main/forms.py index b4ddb4b6a..ed249e9eb 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from itertools import chain import pytz +from flask import request from flask_wtf import FlaskForm as Form from flask_wtf.file import FileAllowed from flask_wtf.file import FileField as FileField_wtf @@ -1154,11 +1155,33 @@ class TemplateAndFoldersSelectionForm(Form): self.templates_and_folders.choices = template_list.as_id_and_name + self.is_move_op = self.is_add_op = False + self.move_to.choices = [ (item['id'], item['name']) for item in ([self.ALL_TEMPLATES_FOLDER] + all_template_folders) if item['id'] != str(current_folder_id) ] + def validate(self): + op = request.form.get('operation') + + self.is_move_op = op in {'move_to_existing_folder', 'move_to_new_folder'} + self.is_add_op = op in {'add_new_folder', 'move_to_new_folder'} + + if not (self.is_add_op or self.is_move_op): + return False + + return super().validate() + + def validate_move_to(self, field): + if self.is_move_op and not field.data: + raise ValidationError('Can’t be empty') + + def validate_new_folder_name(self, field): + if self.is_add_op and not field.data: + raise ValidationError('Can’t be empty') + templates_and_folders = MultiCheckboxField('Choose templates or folders') - move_to = RadioFieldWithNoneOption('Choose a folder') + move_to = RadioFieldWithNoneOption('Choose a folder', validators=[Optional()]) + new_folder_name = StringField('Folder name') diff --git a/app/main/views/templates.py b/app/main/views/templates.py index 9506fc92a..02db81402 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -117,24 +117,9 @@ def choose_template(service_id, template_type='all', template_folder_id=None): template_type=template_type, current_folder_id=template_folder_id, ) - add_folder_form = TemplateFolderForm() - if is_valid_template_operation('move', templates_and_folders_form): - current_service.move_to_folder( - ids_to_move=templates_and_folders_form.templates_and_folders.data, - move_to=templates_and_folders_form.move_to.data, - ) - return redirect(request.url) - - if is_valid_template_operation('add_new_folder', add_folder_form): - template_folder_api_client.create_template_folder( - current_service.id, - name=add_folder_form.name.data, - parent_id=template_folder_id - ) - return redirect( - url_for('.choose_template', service_id=service_id, template_folder_id=template_folder_id) - ) + if request.method == 'POST' and can_manage_folders() and templates_and_folders_form.validate_on_submit(): + return process_folder_management_form(templates_and_folders_form, template_folder_id) return render_template( 'views/templates/choose.html', @@ -150,20 +135,30 @@ def choose_template(service_id, template_type='all', template_folder_id=None): template_nav_items=get_template_nav_items(template_folder_id), template_type=template_type, search_form=SearchTemplatesForm(), - templates_and_folders_form=templates_and_folders_form, - add_folder_form=add_folder_form, + templates_and_folders_form=templates_and_folders_form ) -def is_valid_template_operation(operation_name, form): +def process_folder_management_form(form, current_folder_id): + new_folder_id = None - if ( - can_manage_folders() and - request.method == 'POST' and - request.form.get('operation') == operation_name and - form.validate_on_submit() - ): - return True + if form.is_add_op: + new_folder_id = template_folder_api_client.create_template_folder( + current_service.id, + name=form.new_folder_name.data, + parent_id=current_folder_id + ) + + if form.is_move_op: + # if we've just made a folder, we also want to move there + move_to_id = new_folder_id or form.move_to.data + + current_service.move_to_folder( + ids_to_move=form.templates_and_folders.data, + move_to=move_to_id + ) + + return redirect(request.url) def get_template_nav_label(value): diff --git a/app/templates/views/templates/_move_to.html b/app/templates/views/templates/_move_to.html index 10ab965a2..f1789dd64 100644 --- a/app/templates/views/templates/_move_to.html +++ b/app/templates/views/templates/_move_to.html @@ -2,6 +2,10 @@ {% from "components/page-footer.html" import page_footer %} {% if templates_and_folders_form.move_to.choices and template_list.templates_to_show %} + {{ radios(templates_and_folders_form.move_to) }} - {{ page_footer('Move selected', button_name='operation', button_value='move') }} + {{ page_footer('Move', button_name='operation', button_value='move_to_existing_folder') }} + {{ textbox(templates_and_folders_form.new_folder_name) }} + {{ page_footer('Move to a new folder', button_name='operation', button_value='move_to_new_folder') }} + {{ page_footer('New folder', button_name='operation', button_value='add_new_folder') }} {% endif %} diff --git a/app/templates/views/templates/choose.html b/app/templates/views/templates/choose.html index e5f473ca3..8cba7139c 100644 --- a/app/templates/views/templates/choose.html +++ b/app/templates/views/templates/choose.html @@ -85,11 +85,6 @@ {% endwith %} {% endcall %} - {% call form_wrapper() %} - {{ textbox(add_folder_form.name) }} - {{ page_footer('New folder', button_name='operation', button_value='add_new_folder') }} - {% endcall %} - {% else %} {% include 'views/templates/_template_list.html' %} {% endif %} From 6743bd11fad029143ed0f41f7be4d4fea1ccbafe Mon Sep 17 00:00:00 2001 From: Leo Hemsted Date: Mon, 19 Nov 2018 17:33:33 +0000 Subject: [PATCH 3/5] moved existing template folder tests into test_template_folders also refactored slightly to work with new html, and separate succesful and bad-permissions test cases --- tests/app/main/views/test_template_folders.py | 267 +++++++++++++++++- tests/app/main/views/test_templates.py | 262 ----------------- .../test_template_folder_client.py | 12 +- 3 files changed, 270 insertions(+), 271 deletions(-) diff --git a/tests/app/main/views/test_template_folders.py b/tests/app/main/views/test_template_folders.py index 2e951a070..aca605794 100644 --- a/tests/app/main/views/test_template_folders.py +++ b/tests/app/main/views/test_template_folders.py @@ -3,7 +3,15 @@ import uuid import pytest from flask import url_for -from tests.conftest import SERVICE_ONE_ID, normalize_spaces +from tests import sample_uuid +from tests.conftest import ( + SERVICE_ONE_ID, + TEMPLATE_ONE_ID, + active_caseworking_user, + active_user_view_permissions, + active_user_with_permissions, + normalize_spaces, +) PARENT_FOLDER_ID = '7e979e79-d970-43a5-ac69-b625a8d147b0' CHILD_FOLDER_ID = '92ee1ee0-e4ee-4dcc-b1a7-a5da9ebcfa2b' @@ -316,7 +324,6 @@ def test_should_show_templates_folder_page( mock_has_no_jobs, service_one, mocker, - fake_uuid, expected_title_tag, expected_page_title, expected_parent_link_args, @@ -441,7 +448,6 @@ def test_add_template_by_type_should_redirect_to_view_template_for_letter( service_one, mock_get_service_templates, mock_get_organisations_and_services_for_user, - fake_uuid, mock_create_service_template ): service_one['permissions'] += ['edit_folders'] @@ -667,3 +673,258 @@ def test_delete_folder(client_request, service_one, mock_get_template_folders, m ) mock_delete.assert_called_once_with(service_one['id'], folder_id) + + +@pytest.mark.parametrize('user', [ + pytest.param( + active_user_with_permissions + ), + pytest.param( + active_user_view_permissions, + marks=pytest.mark.xfail(raises=AssertionError) + ), + pytest.param( + active_caseworking_user, + marks=pytest.mark.xfail(raises=AssertionError) + ), +]) +@pytest.mark.parametrize('extra_service_permissions', [ + pytest.param( + ['edit_folders'] + ), + pytest.param( + [], + marks=pytest.mark.xfail(raises=AssertionError) + ), +]) +def test_should_show_checkboxes_for_selecting_templates( + client_request, + mocker, + service_one, + mock_get_service_templates, + mock_get_template_folders, + mock_has_no_jobs, + fake_uuid, + user, + extra_service_permissions, +): + service_one['permissions'] += extra_service_permissions + client_request.login(user(fake_uuid)) + + page = client_request.get( + 'main.choose_template', + service_id=SERVICE_ONE_ID, + ) + checkboxes = page.select('input[name=templates_and_folders]') + + assert len(checkboxes) == 4 + + assert checkboxes[0]['value'] == TEMPLATE_ONE_ID + assert checkboxes[0]['id'] == 'templates-or-folder-{}'.format(TEMPLATE_ONE_ID) + + for index in (1, 2, 3): + assert checkboxes[index]['value'] != TEMPLATE_ONE_ID + assert TEMPLATE_ONE_ID not in checkboxes[index]['id'] + + +@pytest.mark.parametrize('user,extra_service_permissions,should_show_radio_buttons', [ + (active_user_with_permissions, ['edit_folders'], True), + (active_user_with_permissions, [], False), + (active_user_view_permissions, ['edit_folders'], False), + (active_caseworking_user, ['edit_folders'], False), +]) +def test_should_show_radios_and_buttons_for_move_destination_if_correct_permissions( + client_request, + mocker, + service_one, + mock_get_service_templates, + mock_get_template_folders, + mock_has_no_jobs, + fake_uuid, + user, + extra_service_permissions, + should_show_radio_buttons, +): + service_one['permissions'] += extra_service_permissions + + client_request.login(user(fake_uuid)) + + FOLDER_TWO_ID = str(uuid.uuid4()) + FOLDER_ONE_TWO_ID = str(uuid.uuid4()) + mock_get_template_folders.return_value = [ + {'id': PARENT_FOLDER_ID, 'name': 'folder_one', 'parent_id': None}, + {'id': FOLDER_TWO_ID, 'name': 'folder_two', 'parent_id': None}, + {'id': CHILD_FOLDER_ID, 'name': 'folder_one_one', 'parent_id': PARENT_FOLDER_ID}, + {'id': FOLDER_ONE_TWO_ID, 'name': 'folder_one_two', 'parent_id': PARENT_FOLDER_ID}, + ] + page = client_request.get( + 'main.choose_template', + service_id=SERVICE_ONE_ID, + ) + radios = page.select('input[type=radio]') + labels = page.select('label[for^=move_to]') + assert radios == page.select('input[name=move_to]') + + if should_show_radio_buttons: + assert [x['value'] for x in radios] == [ + PARENT_FOLDER_ID, CHILD_FOLDER_ID, FOLDER_ONE_TWO_ID, FOLDER_TWO_ID, + ] + assert [x.text.strip() for x in labels] == [ + 'folder_one', 'folder_one_one', 'folder_one_two', 'folder_two', + ] + assert set(x['value'] for x in page.find_all('button', {'name': 'operation'})) == { + 'unknown', + 'move_to_existing_folder', + 'move_to_new_folder', + 'add_new_folder' + } + else: + assert not radios + assert not labels + assert page.find_all('button', {'name': 'operation'}) == [] + + +def test_should_be_able_to_move_to_existing_folder( + client_request, + service_one, + mock_get_service_templates, + mock_get_template_folders, + mock_move_to_template_folder, +): + service_one['permissions'] += ['edit_folders'] + FOLDER_TWO_ID = str(uuid.uuid4()) + mock_get_template_folders.return_value = [ + {'id': PARENT_FOLDER_ID, 'name': 'folder_one', 'parent_id': None}, + {'id': FOLDER_TWO_ID, 'name': 'folder_two', 'parent_id': None}, + ] + client_request.post( + 'main.choose_template', + service_id=SERVICE_ONE_ID, + _data={ + 'operation': 'move_to_existing_folder', + 'move_to': PARENT_FOLDER_ID, + 'templates_and_folders': [ + FOLDER_TWO_ID, + TEMPLATE_ONE_ID, + ], + }, + _expected_status=302, + _expected_redirect=url_for( + 'main.choose_template', + service_id=SERVICE_ONE_ID, + _external=True, + ), + ) + mock_move_to_template_folder.assert_called_once_with( + service_id=SERVICE_ONE_ID, + folder_id=PARENT_FOLDER_ID, + folder_ids={FOLDER_TWO_ID}, + template_ids={TEMPLATE_ONE_ID}, + ) + + +@pytest.mark.parametrize('user,extra_service_permissions', [ + (active_user_view_permissions, ['edit_folders']), + (active_user_with_permissions, []) +]) +def test_should_not_be_able_to_move_to_existing_folder_if_dont_have_permission( + client_request, + service_one, + fake_uuid, + mock_get_service_templates, + mock_get_template_folders, + mock_move_to_template_folder, + user, + extra_service_permissions, +): + service_one['permissions'] += extra_service_permissions + client_request.login(user(fake_uuid)) + FOLDER_TWO_ID = str(uuid.uuid4()) + mock_get_template_folders.return_value = [ + {'id': PARENT_FOLDER_ID, 'name': 'folder_one', 'parent_id': None}, + {'id': FOLDER_TWO_ID, 'name': 'folder_two', 'parent_id': None}, + ] + client_request.post( + 'main.choose_template', + service_id=SERVICE_ONE_ID, + _data={ + 'operation': 'move_to_existing_folder', + 'move_to': PARENT_FOLDER_ID, + 'templates_and_folders': [ + FOLDER_TWO_ID, + TEMPLATE_ONE_ID, + ], + }, + # it's just returned the form. Should this have a 400 because there were errors? + _expected_status=200 + ) + assert mock_move_to_template_folder.called is False + + +def test_should_be_able_to_move_a_sub_item( + client_request, + service_one, + fake_uuid, + mock_get_service_templates, + mock_get_template_folders, + mock_move_to_template_folder, +): + service_one['permissions'] += ['edit_folders'] + GRANDCHILD_FOLDER_ID = str(uuid.uuid4()) + mock_get_template_folders.return_value = [ + {'id': PARENT_FOLDER_ID, 'name': 'folder_one', 'parent_id': None}, + {'id': CHILD_FOLDER_ID, 'name': 'folder_one_one', 'parent_id': PARENT_FOLDER_ID}, + {'id': GRANDCHILD_FOLDER_ID, 'name': 'folder_one_one_one', 'parent_id': CHILD_FOLDER_ID}, + ] + client_request.post( + 'main.choose_template', + service_id=SERVICE_ONE_ID, + template_folder_id=PARENT_FOLDER_ID, + _data={ + 'operation': 'move', + 'move_to': 'None', + 'templates_and_folders': [GRANDCHILD_FOLDER_ID], + }, + _expected_status=302, + ) + mock_move_to_template_folder.assert_called_once_with( + service_id=SERVICE_ONE_ID, + folder_id=None, + folder_ids={GRANDCHILD_FOLDER_ID}, + template_ids=set(), + ) + + +@pytest.mark.parametrize('thing_to_move', [ + PARENT_FOLDER_ID, # Can’t move a folder inside itself + CHILD_FOLDER_ID, # Can’t move a folder which doesn’t belong to the service +]) +def test_should_validate_illegal_moves( + client_request, + service_one, + mock_get_service_templates, + mock_get_template_folders, + mock_move_to_template_folder, + thing_to_move, +): + service_one['permissions'] += 'edit_folders' + + FOLDER_TWO_ID = str(uuid.uuid4()) + mock_get_template_folders.return_value = [ + {'id': PARENT_FOLDER_ID, 'name': 'folder_one', 'parent_id': None}, + {'id': FOLDER_TWO_ID, 'name': 'folder_two', 'parent_id': None}, + ] + client_request.post( + 'main.choose_template', + service_id=SERVICE_ONE_ID, + _data={ + 'operation': 'move', + 'move_to': PARENT_FOLDER_ID, + 'templates_and_folders': [ + thing_to_move, + ], + }, + _expected_status=200, + _expected_redirect=None, + ) + assert mock_move_to_template_folder.called is False diff --git a/tests/app/main/views/test_templates.py b/tests/app/main/views/test_templates.py index 28f623382..b47c989ee 100644 --- a/tests/app/main/views/test_templates.py +++ b/tests/app/main/views/test_templates.py @@ -1,4 +1,3 @@ -import uuid from datetime import datetime from unittest.mock import ANY, Mock @@ -28,8 +27,6 @@ from tests.conftest import ( TEMPLATE_ONE_ID, active_caseworking_user, active_user_view_permissions, - active_user_with_permissions, - fake_uuid, mock_get_service_email_template, mock_get_service_letter_template, mock_get_service_template, @@ -176,265 +173,6 @@ def test_should_show_page_for_choosing_a_template( mock_get_template_folders.assert_called_once_with(SERVICE_ONE_ID) -@pytest.mark.parametrize('user', [ - pytest.param( - active_user_with_permissions - ), - pytest.param( - active_user_view_permissions, - marks=pytest.mark.xfail(raises=AssertionError) - ), - pytest.param( - active_caseworking_user, - marks=pytest.mark.xfail(raises=AssertionError) - ), -]) -@pytest.mark.parametrize('extra_service_permissions', [ - pytest.param( - ['edit_folders'] - ), - pytest.param( - [], - marks=pytest.mark.xfail(raises=AssertionError) - ), -]) -def test_should_show_checkboxes_for_selecting_templates( - client_request, - mocker, - service_one, - mock_get_service_templates, - mock_get_template_folders, - mock_has_no_jobs, - fake_uuid, - user, - extra_service_permissions, -): - service_one['permissions'] += extra_service_permissions - client_request.login(user(fake_uuid)) - - page = client_request.get( - 'main.choose_template', - service_id=SERVICE_ONE_ID, - ) - checkboxes = page.select('input[name=templates_and_folders]') - - assert len(checkboxes) == 4 - - assert checkboxes[0]['value'] == TEMPLATE_ONE_ID - assert checkboxes[0]['id'] == 'templates-or-folder-{}'.format(TEMPLATE_ONE_ID) - - for index in (1, 2, 3): - assert checkboxes[index]['value'] != TEMPLATE_ONE_ID - assert TEMPLATE_ONE_ID not in checkboxes[index]['id'] - - -@pytest.mark.parametrize('user', [ - pytest.param( - active_user_with_permissions - ), - pytest.param( - active_user_view_permissions, - marks=pytest.mark.xfail(raises=AssertionError) - ), - pytest.param( - active_caseworking_user, - marks=pytest.mark.xfail(raises=AssertionError) - ), -]) -@pytest.mark.parametrize('extra_service_permissions', [ - pytest.param( - ['edit_folders'] - ), - pytest.param( - [], - marks=pytest.mark.xfail(raises=AssertionError) - ), -]) -@pytest.mark.parametrize('folder_id, expected_destinations', [ - (None, []), - (fake_uuid(), []), -]) -def test_should_show_radio_buttons_for_move_destination( - client_request, - mocker, - service_one, - mock_get_service_templates, - mock_get_template_folders, - mock_has_no_jobs, - fake_uuid, - user, - extra_service_permissions, - folder_id, - expected_destinations, -): - service_one['permissions'] += extra_service_permissions - client_request.login(user(fake_uuid)) - FOLDER_TWO_ID = str(uuid.uuid4()) - FOLDER_ONE_TWO_ID = str(uuid.uuid4()) - mock_get_template_folders.return_value = [ - {'id': PARENT_FOLDER_ID, 'name': 'folder_one', 'parent_id': None}, - {'id': FOLDER_TWO_ID, 'name': 'folder_two', 'parent_id': None}, - {'id': CHILD_FOLDER_ID, 'name': 'folder_one_one', 'parent_id': PARENT_FOLDER_ID}, - {'id': FOLDER_ONE_TWO_ID, 'name': 'folder_one_two', 'parent_id': PARENT_FOLDER_ID}, - ] - page = client_request.get( - 'main.choose_template', - service_id=SERVICE_ONE_ID, - ) - radios = page.select('input[type=radio]') - labels = page.select('label[for^=move_to]') - assert radios == page.select('input[name=move_to]') - - assert [x['value'] for x in radios] == [ - PARENT_FOLDER_ID, CHILD_FOLDER_ID, FOLDER_ONE_TWO_ID, FOLDER_TWO_ID, - ] - assert [x.text.strip() for x in labels] == [ - 'folder_one', 'folder_one_one', 'folder_one_two', 'folder_two', - ] - assert page.select_one('button[name=operation]')['value'] == 'move' - - -@pytest.mark.parametrize('user', [ - pytest.param( - active_user_with_permissions - ), - pytest.param( - active_user_view_permissions, - marks=pytest.mark.xfail(raises=AssertionError) - ), - pytest.param( - active_caseworking_user, - marks=pytest.mark.xfail(raises=AssertionError) - ), -]) -@pytest.mark.parametrize('extra_service_permissions', [ - pytest.param( - ['edit_folders'] - ), - pytest.param( - [], - marks=pytest.mark.xfail(raises=AssertionError) - ), -]) -@pytest.mark.parametrize('folder_id, expected_destinations', [ - (None, []), - (fake_uuid(), []), -]) -def test_should_post_move_to_api( - client_request, - service_one, - fake_uuid, - mock_get_service_templates, - mock_get_template_folders, - mock_move_to_template_folder, - user, - extra_service_permissions, - folder_id, - expected_destinations, -): - service_one['permissions'] += extra_service_permissions - client_request.login(user(fake_uuid)) - FOLDER_TWO_ID = str(uuid.uuid4()) - mock_get_template_folders.return_value = [ - {'id': PARENT_FOLDER_ID, 'name': 'folder_one', 'parent_id': None}, - {'id': FOLDER_TWO_ID, 'name': 'folder_two', 'parent_id': None}, - ] - client_request.post( - 'main.choose_template', - service_id=SERVICE_ONE_ID, - _data={ - 'operation': 'move', - 'move_to': PARENT_FOLDER_ID, - 'templates_and_folders': [ - FOLDER_TWO_ID, - TEMPLATE_ONE_ID, - ], - }, - _expected_status=302, - _expected_redirect=url_for( - 'main.choose_template', - service_id=SERVICE_ONE_ID, - _external=True, - ), - ) - mock_move_to_template_folder.assert_called_once_with( - service_id=SERVICE_ONE_ID, - folder_id=PARENT_FOLDER_ID, - folder_ids={FOLDER_TWO_ID}, - template_ids={TEMPLATE_ONE_ID}, - ) - - -def test_should_be_able_to_move_a_sub_item( - client_request, - service_one, - fake_uuid, - mock_get_service_templates, - mock_get_template_folders, - mock_move_to_template_folder, -): - service_one['permissions'] += ['edit_folders'] - GRANDCHILD_FOLDER_ID = str(uuid.uuid4()) - mock_get_template_folders.return_value = [ - {'id': PARENT_FOLDER_ID, 'name': 'folder_one', 'parent_id': None}, - {'id': CHILD_FOLDER_ID, 'name': 'folder_one_one', 'parent_id': PARENT_FOLDER_ID}, - {'id': GRANDCHILD_FOLDER_ID, 'name': 'folder_one_one_one', 'parent_id': CHILD_FOLDER_ID}, - ] - client_request.post( - 'main.choose_template', - service_id=SERVICE_ONE_ID, - template_folder_id=PARENT_FOLDER_ID, - _data={ - 'operation': 'move', - 'move_to': 'None', - 'templates_and_folders': [GRANDCHILD_FOLDER_ID], - }, - _expected_status=302, - ) - mock_move_to_template_folder.assert_called_once_with( - service_id=SERVICE_ONE_ID, - folder_id=None, - folder_ids={GRANDCHILD_FOLDER_ID}, - template_ids=set(), - ) - - -@pytest.mark.parametrize('thing_to_move', [ - PARENT_FOLDER_ID, # Can’t move a folder inside itself - CHILD_FOLDER_ID, # Can’t move a folder which doesn’t belong to the service -]) -def test_should_validate_illegal_moves( - client_request, - service_one, - fake_uuid, - mock_get_service_templates, - mock_get_template_folders, - mock_move_to_template_folder, - thing_to_move, -): - service_one['permissions'] += 'edit_folders' - - FOLDER_TWO_ID = str(uuid.uuid4()) - mock_get_template_folders.return_value = [ - {'id': PARENT_FOLDER_ID, 'name': 'folder_one', 'parent_id': None}, - {'id': FOLDER_TWO_ID, 'name': 'folder_two', 'parent_id': None}, - ] - client_request.post( - 'main.choose_template', - service_id=SERVICE_ONE_ID, - _data={ - 'operation': 'move', - 'move_to': PARENT_FOLDER_ID, - 'templates_and_folders': [ - thing_to_move, - ], - }, - _expected_status=200, - _expected_redirect=None, - ) - assert mock_move_to_template_folder.called is False - - def test_should_not_show_template_nav_if_only_one_type_of_template( client_request, mock_get_template_folders, diff --git a/tests/app/notify_client/test_template_folder_client.py b/tests/app/notify_client/test_template_folder_client.py index 4057797ca..3ab808930 100644 --- a/tests/app/notify_client/test_template_folder_client.py +++ b/tests/app/notify_client/test_template_folder_client.py @@ -8,7 +8,7 @@ from app.notify_client.template_folder_api_client import TemplateFolderAPIClient @pytest.mark.parametrize('parent_id', [uuid.uuid4(), None]) -def test_create_template_folder_calls_correct_api_endpoint(mocker, api_user_active, parent_id): +def test_create_template_folder_calls_correct_api_endpoint(mocker, parent_id): mock_redis_delete = mocker.patch('app.notify_client.RedisClient.delete') some_service_id = uuid.uuid4() @@ -25,7 +25,7 @@ def test_create_template_folder_calls_correct_api_endpoint(mocker, api_user_acti mock_redis_delete.assert_called_once_with('service-{}-template-folders'.format(some_service_id)) -def test_get_template_folders_calls_correct_api_endpoint(mocker, api_user_active): +def test_get_template_folders_calls_correct_api_endpoint(mocker): mock_redis_get = mocker.patch('app.notify_client.RedisClient.get', return_value=None) mock_redis_set = mocker.patch('app.notify_client.RedisClient.set') mock_api_get = mocker.patch( @@ -48,7 +48,7 @@ def test_get_template_folders_calls_correct_api_endpoint(mocker, api_user_active mock_redis_set.assert_called_once_with(redis_key, '{"a": "b"}', ex=604800) -def test_move_templates_and_folders(mocker, api_user_active): +def test_move_templates_and_folders(mocker): mock_redis_delete = mocker.patch('app.notify_client.RedisClient.delete') mock_api_post = mocker.patch('app.notify_client.NotifyAdminAPIClient.post') @@ -83,7 +83,7 @@ def test_move_templates_and_folders(mocker, api_user_active): ] -def test_move_templates_and_folders_to_root(mocker, api_user_active): +def test_move_templates_and_folders_to_root(mocker): mock_api_post = mocker.patch('app.notify_client.NotifyAdminAPIClient.post') @@ -105,7 +105,7 @@ def test_move_templates_and_folders_to_root(mocker, api_user_active): ) -def test_update_template_folder_calls_correct_api_endpoint(mocker, api_user_active): +def test_update_template_folder_calls_correct_api_endpoint(mocker): mock_redis_delete = mocker.patch('app.notify_client.RedisClient.delete') some_service_id = uuid.uuid4() @@ -123,7 +123,7 @@ def test_update_template_folder_calls_correct_api_endpoint(mocker, api_user_acti mock_redis_delete.assert_called_once_with('service-{}-template-folders'.format(some_service_id)) -def test_delete_template_folder_calls_correct_api_endpoint(mocker, api_user_active): +def test_delete_template_folder_calls_correct_api_endpoint(mocker): mock_redis_delete = mocker.patch('app.notify_client.RedisClient.delete') some_service_id = uuid.uuid4() From d07de58cd9a323c768b782bedd2f2031c9d36641 Mon Sep 17 00:00:00 2001 From: Leo Hemsted Date: Tue, 20 Nov 2018 18:03:57 +0000 Subject: [PATCH 4/5] split out new folder into two separate forms move_to_new_folder and add_new_folder are now two separate html fields and form items - so that we can more easily manipulate them on the front end --- app/main/forms.py | 44 ++-- app/main/views/templates.py | 6 +- app/templates/views/templates/_move_to.html | 18 +- pytest.ini | 3 + tests/app/main/views/test_template_folders.py | 205 ++++++++++++++---- tests/app/main/views/test_templates.py | 6 +- tests/conftest.py | 5 + 7 files changed, 215 insertions(+), 72 deletions(-) diff --git a/app/main/forms.py b/app/main/forms.py index ed249e9eb..29e3813ae 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -1135,6 +1135,18 @@ class TemplateFolderForm(StripWhitespaceForm): name = StringField('Folder name', validators=[DataRequired(message='Can’t be empty')]) +def required_for_ops(*operations): + operations = set(operations) + + def validate(form, field): + if form.op not in operations and field.data: + # super weird + raise ValidationError('Must be empty') + if form.op in operations and not field.data: + raise ValidationError('Can’t be empty') + return validate + + class TemplateAndFoldersSelectionForm(Form): ALL_TEMPLATES_FOLDER = { @@ -1155,6 +1167,7 @@ class TemplateAndFoldersSelectionForm(Form): self.templates_and_folders.choices = template_list.as_id_and_name + self.op = None self.is_move_op = self.is_add_op = False self.move_to.choices = [ @@ -1164,24 +1177,29 @@ class TemplateAndFoldersSelectionForm(Form): ] def validate(self): - op = request.form.get('operation') + self.op = request.form.get('operation') - self.is_move_op = op in {'move_to_existing_folder', 'move_to_new_folder'} - self.is_add_op = op in {'add_new_folder', 'move_to_new_folder'} + self.is_move_op = self.op in {'move_to_existing_folder', 'move_to_new_folder'} + self.is_add_op = self.op in {'add_new_folder', 'move_to_new_folder'} if not (self.is_add_op or self.is_move_op): return False return super().validate() - def validate_move_to(self, field): - if self.is_move_op and not field.data: - raise ValidationError('Can’t be empty') + def get_folder_name(self): + if self.op == 'add_new_folder': + return self.add_new_folder_name.data + elif self.op == 'move_to_new_folder': + return self.move_to_new_folder_name.data + return None - def validate_new_folder_name(self, field): - if self.is_add_op and not field.data: - raise ValidationError('Can’t be empty') - - templates_and_folders = MultiCheckboxField('Choose templates or folders') - move_to = RadioFieldWithNoneOption('Choose a folder', validators=[Optional()]) - new_folder_name = StringField('Folder name') + templates_and_folders = MultiCheckboxField('Choose templates or folders', validators=[ + required_for_ops('move_to_new_folder', 'move_to_existing_folder') + ]) + move_to = RadioFieldWithNoneOption('Choose a folder', validators=[ + Optional(), + required_for_ops('move_to_new_folder', 'move_to_existing_folder') + ]) + add_new_folder_name = StringField('Folder name', validators=[required_for_ops('add_new_folder')]) + move_to_new_folder_name = StringField('Folder name', validators=[required_for_ops('move_to_new_folder')]) diff --git a/app/main/views/templates.py b/app/main/views/templates.py index 02db81402..4a2854a9a 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -118,7 +118,9 @@ def choose_template(service_id, template_type='all', template_folder_id=None): current_folder_id=template_folder_id, ) - if request.method == 'POST' and can_manage_folders() and templates_and_folders_form.validate_on_submit(): + if request.method == 'POST' and templates_and_folders_form.validate_on_submit(): + if not can_manage_folders(): + abort(403) return process_folder_management_form(templates_and_folders_form, template_folder_id) return render_template( @@ -145,7 +147,7 @@ def process_folder_management_form(form, current_folder_id): if form.is_add_op: new_folder_id = template_folder_api_client.create_template_folder( current_service.id, - name=form.new_folder_name.data, + name=form.get_folder_name(), parent_id=current_folder_id ) diff --git a/app/templates/views/templates/_move_to.html b/app/templates/views/templates/_move_to.html index f1789dd64..871e26364 100644 --- a/app/templates/views/templates/_move_to.html +++ b/app/templates/views/templates/_move_to.html @@ -3,9 +3,17 @@ {% if templates_and_folders_form.move_to.choices and template_list.templates_to_show %} - {{ radios(templates_and_folders_form.move_to) }} - {{ page_footer('Move', button_name='operation', button_value='move_to_existing_folder') }} - {{ textbox(templates_and_folders_form.new_folder_name) }} - {{ page_footer('Move to a new folder', button_name='operation', button_value='move_to_new_folder') }} - {{ page_footer('New folder', button_name='operation', button_value='add_new_folder') }} +
+ {{ radios(templates_and_folders_form.move_to) }} + {{ page_footer('Move', button_name='operation', button_value='move_to_existing_folder') }} +
+
+ {{ textbox(templates_and_folders_form.move_to_new_folder_name) }} + {{ page_footer('Move to a new folder', button_name='operation', button_value='move_to_new_folder') }} +
+
+ {{ textbox(templates_and_folders_form.add_new_folder_name) }} + {{ page_footer('New folder', button_name='operation', button_value='add_new_folder') }} +
+ {% endif %} diff --git a/pytest.ini b/pytest.ini index a9b6c3bc3..2b0a34ba8 100644 --- a/pytest.ini +++ b/pytest.ini @@ -10,3 +10,6 @@ env = ZENDESK_API_KEY=test STATSD_PREFIX=stats-prefix REDIS_ENABLED=0 +# :thinking_face: +filterwarnings = + ignore::pytest.RemovedInPytest4Warning diff --git a/tests/app/main/views/test_template_folders.py b/tests/app/main/views/test_template_folders.py index aca605794..2943a6a6c 100644 --- a/tests/app/main/views/test_template_folders.py +++ b/tests/app/main/views/test_template_folders.py @@ -3,7 +3,6 @@ import uuid import pytest from flask import url_for -from tests import sample_uuid from tests.conftest import ( SERVICE_ONE_ID, TEMPLATE_ONE_ID, @@ -727,13 +726,12 @@ def test_should_show_checkboxes_for_selecting_templates( assert TEMPLATE_ONE_ID not in checkboxes[index]['id'] -@pytest.mark.parametrize('user,extra_service_permissions,should_show_radio_buttons', [ - (active_user_with_permissions, ['edit_folders'], True), - (active_user_with_permissions, [], False), - (active_user_view_permissions, ['edit_folders'], False), - (active_caseworking_user, ['edit_folders'], False), +@pytest.mark.parametrize('user,extra_service_permissions', [ + (active_user_with_permissions, []), + (active_user_view_permissions, ['edit_folders']), + (active_caseworking_user, ['edit_folders']), ]) -def test_should_show_radios_and_buttons_for_move_destination_if_correct_permissions( +def test_should_not_show_radios_and_buttons_for_move_destination_if_incorrect_permissions( client_request, mocker, service_one, @@ -743,12 +741,38 @@ def test_should_show_radios_and_buttons_for_move_destination_if_correct_permissi fake_uuid, user, extra_service_permissions, - should_show_radio_buttons, ): service_one['permissions'] += extra_service_permissions client_request.login(user(fake_uuid)) + page = client_request.get( + 'main.choose_template', + service_id=SERVICE_ONE_ID, + ) + radios = page.select('input[type=radio]') + radio_div = page.find('div', {'id': 'move_to_folder_radios'}) + assert radios == page.select('input[name=move_to]') + + assert not radios + assert not radio_div + assert page.find_all('button', {'name': 'operation'}) == [] + + +def test_should_show_radios_and_buttons_for_move_destination_if_correct_permissions( + client_request, + mocker, + service_one, + mock_get_service_templates, + mock_get_template_folders, + mock_has_no_jobs, + fake_uuid, + active_user_with_permissions +): + service_one['permissions'] += ['edit_folders'] + + client_request.login(active_user_with_permissions) + FOLDER_TWO_ID = str(uuid.uuid4()) FOLDER_ONE_TWO_ID = str(uuid.uuid4()) mock_get_template_folders.return_value = [ @@ -762,26 +786,21 @@ def test_should_show_radios_and_buttons_for_move_destination_if_correct_permissi service_id=SERVICE_ONE_ID, ) radios = page.select('input[type=radio]') - labels = page.select('label[for^=move_to]') + radio_div = page.find('div', {'id': 'move_to_folder_radios'}) assert radios == page.select('input[name=move_to]') - if should_show_radio_buttons: - assert [x['value'] for x in radios] == [ - PARENT_FOLDER_ID, CHILD_FOLDER_ID, FOLDER_ONE_TWO_ID, FOLDER_TWO_ID, - ] - assert [x.text.strip() for x in labels] == [ - 'folder_one', 'folder_one_one', 'folder_one_two', 'folder_two', - ] - assert set(x['value'] for x in page.find_all('button', {'name': 'operation'})) == { - 'unknown', - 'move_to_existing_folder', - 'move_to_new_folder', - 'add_new_folder' - } - else: - assert not radios - assert not labels - assert page.find_all('button', {'name': 'operation'}) == [] + assert [x['value'] for x in radios] == [ + PARENT_FOLDER_ID, CHILD_FOLDER_ID, FOLDER_ONE_TWO_ID, FOLDER_TWO_ID, + ] + assert [x.text.strip() for x in radio_div.select('label')] == [ + 'folder_one', 'folder_one_one', 'folder_one_two', 'folder_two', + ] + assert set(x['value'] for x in page.find_all('button', {'name': 'operation'})) == { + 'unknown', + 'move_to_existing_folder', + 'move_to_new_folder', + 'add_new_folder' + } def test_should_be_able_to_move_to_existing_folder( @@ -855,8 +874,7 @@ def test_should_not_be_able_to_move_to_existing_folder_if_dont_have_permission( TEMPLATE_ONE_ID, ], }, - # it's just returned the form. Should this have a 400 because there were errors? - _expected_status=200 + _expected_status=403 ) assert mock_move_to_template_folder.called is False @@ -881,7 +899,7 @@ def test_should_be_able_to_move_a_sub_item( service_id=SERVICE_ONE_ID, template_folder_id=PARENT_FOLDER_ID, _data={ - 'operation': 'move', + 'operation': 'move_to_existing_folder', 'move_to': 'None', 'templates_and_folders': [GRANDCHILD_FOLDER_ID], }, @@ -895,36 +913,129 @@ def test_should_be_able_to_move_a_sub_item( ) -@pytest.mark.parametrize('thing_to_move', [ - PARENT_FOLDER_ID, # Can’t move a folder inside itself - CHILD_FOLDER_ID, # Can’t move a folder which doesn’t belong to the service +@pytest.mark.parametrize('data', [ + # move to existing, but add new folder name given + { + 'operation': 'move_to_existing_folder', + 'templates_and_folders': [], + 'add_new_folder_name': 'foo', + 'move_to': PARENT_FOLDER_ID + }, + # move to existing, but move to new folder name given + { + 'operation': 'move_to_existing_folder', + 'templates_and_folders': [TEMPLATE_ONE_ID], + 'move_to_new_folder_name': 'foo', + 'move_to': PARENT_FOLDER_ID + }, + # move to new, but nothing selected to move + { + 'operation': 'move_to_new_folder', + 'templates_and_folders': [], + 'move_to_new_folder_name': 'foo', + 'move_to': None + } ]) -def test_should_validate_illegal_moves( +def test_no_action_if_user_fills_in_ambiguous_fields( client_request, service_one, mock_get_service_templates, mock_get_template_folders, mock_move_to_template_folder, - thing_to_move, + mock_create_template_folder, + data, ): - service_one['permissions'] += 'edit_folders' + service_one['permissions'] += ['edit_folders'] - FOLDER_TWO_ID = str(uuid.uuid4()) - mock_get_template_folders.return_value = [ - {'id': PARENT_FOLDER_ID, 'name': 'folder_one', 'parent_id': None}, - {'id': FOLDER_TWO_ID, 'name': 'folder_two', 'parent_id': None}, - ] client_request.post( 'main.choose_template', service_id=SERVICE_ONE_ID, - _data={ - 'operation': 'move', - 'move_to': PARENT_FOLDER_ID, - 'templates_and_folders': [ - thing_to_move, - ], - }, + _data=data, _expected_status=200, _expected_redirect=None, ) + assert mock_move_to_template_folder.called is False + assert mock_create_template_folder.called is False + + +def test_new_folder_is_created_if_only_new_folder_is_filled_out( + client_request, + service_one, + mock_get_service_templates, + mock_get_template_folders, + mock_move_to_template_folder, + mock_create_template_folder +): + data = { + 'move_to_new_folder_name': '', + 'add_new_folder_name': 'new folder', + 'operation': 'add_new_folder' + } + + service_one['permissions'] += ['edit_folders'] + + client_request.post( + 'main.choose_template', + service_id=SERVICE_ONE_ID, + _data=data, + _expected_status=302, + _expected_redirect=url_for( + 'main.choose_template', + service_id=service_one['id'], + template_folder_id=None, + _external=True, + ), + ) + + assert mock_move_to_template_folder.called is False + mock_create_template_folder.assert_called_once_with( + SERVICE_ONE_ID, + name='new folder', + parent_id=None + ) + + +def test_should_be_able_to_move_to_new_folder( + client_request, + service_one, + mock_get_service_templates, + mock_get_template_folders, + mock_move_to_template_folder, + mock_create_template_folder, +): + service_one['permissions'] += ['edit_folders'] + new_folder_id = mock_create_template_folder.return_value + FOLDER_TWO_ID = str(uuid.uuid4()) + mock_get_template_folders.return_value = [ + {'id': PARENT_FOLDER_ID, 'name': 'parent folder', 'parent_id': None}, + {'id': FOLDER_TWO_ID, 'name': 'folder_two', 'parent_id': None}, + ] + + client_request.post( + 'main.choose_template', + service_id=SERVICE_ONE_ID, + template_folder_id=None, + _data={ + 'operation': 'move_to_new_folder', + 'move_to_new_folder_name': 'new folder', + 'templates_and_folders': [ + FOLDER_TWO_ID, + TEMPLATE_ONE_ID, + ], + }, + _expected_status=302, + _expected_redirect=url_for('main.choose_template', service_id=SERVICE_ONE_ID, _external=True), + ) + + mock_create_template_folder.assert_called_once_with( + SERVICE_ONE_ID, + name='new folder', + parent_id=None + ) + mock_move_to_template_folder.assert_called_once_with( + service_id=SERVICE_ONE_ID, + folder_id=new_folder_id, + folder_ids={FOLDER_TWO_ID}, + template_ids={TEMPLATE_ONE_ID}, + ) diff --git a/tests/app/main/views/test_templates.py b/tests/app/main/views/test_templates.py index b47c989ee..be1a2c5e2 100644 --- a/tests/app/main/views/test_templates.py +++ b/tests/app/main/views/test_templates.py @@ -16,11 +16,7 @@ from tests import ( template_json, validate_route_permission, ) -from tests.app.main.views.test_template_folders import ( - CHILD_FOLDER_ID, - PARENT_FOLDER_ID, - _folder, -) +from tests.app.main.views.test_template_folders import PARENT_FOLDER_ID, _folder from tests.conftest import ( SERVICE_ONE_ID, SERVICE_TWO_ID, diff --git a/tests/conftest.py b/tests/conftest.py index 5c64914fc..0d964d14b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3255,3 +3255,8 @@ def mock_get_template_folders(mocker): @pytest.fixture def mock_move_to_template_folder(mocker): return mocker.patch('app.template_folder_api_client.move_to_folder') + + +@pytest.fixture +def mock_create_template_folder(mocker): + return mocker.patch('app.template_folder_api_client.create_template_folder', return_value=sample_uuid()) From 1bb7e98722aef02dd048939a10e05cea7cc9744c Mon Sep 17 00:00:00 2001 From: Leo Hemsted Date: Tue, 27 Nov 2018 14:33:50 +0000 Subject: [PATCH 5/5] add fieldsets to template/folder form and update docstrings also remove erroneous pytest.ini cfg and reset button --- app/main/forms.py | 15 +++++++++++++++ app/templates/views/templates/_move_to.html | 15 ++++++++++----- pytest.ini | 4 +--- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/app/main/forms.py b/app/main/forms.py index 29e3813ae..2a1b5b66f 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -1148,6 +1148,21 @@ def required_for_ops(*operations): class TemplateAndFoldersSelectionForm(Form): + """ + This form also expects the form data to include an operation, based on which submit button is clicked. + If enter is pressed, unknown will be sent by a hidden submit button at the top of the form. + The value of this operation affects which fields are required, expected to be empty, or optional. + + * unknown + currently not implemented, but in the future will try and work out if there are any obvious commands that can be + assumed based on which fields are empty vs populated. + * move_to_existing_folder + must have data for templates_and_folders checkboxes, and move_to radios + * move_to_new_folder + must have data for move_to_new_folder_name, cannot have data for move_to_existing_folder_name + * add_new_folder + must have data for move_to_existing_folder_name, cannot have data for move_to_new_folder_name + """ ALL_TEMPLATES_FOLDER = { 'name': 'All templates', diff --git a/app/templates/views/templates/_move_to.html b/app/templates/views/templates/_move_to.html index 871e26364..10c6b7341 100644 --- a/app/templates/views/templates/_move_to.html +++ b/app/templates/views/templates/_move_to.html @@ -8,12 +8,17 @@ {{ page_footer('Move', button_name='operation', button_value='move_to_existing_folder') }}
- {{ textbox(templates_and_folders_form.move_to_new_folder_name) }} - {{ page_footer('Move to a new folder', button_name='operation', button_value='move_to_new_folder') }} +
+ Move to a new folder + {{ textbox(templates_and_folders_form.move_to_new_folder_name) }} + {{ page_footer('Move to a new folder', button_name='operation', button_value='move_to_new_folder') }} +
- {{ textbox(templates_and_folders_form.add_new_folder_name) }} - {{ page_footer('New folder', button_name='operation', button_value='add_new_folder') }} +
+ Add a new folder + {{ textbox(templates_and_folders_form.add_new_folder_name) }} + {{ page_footer('New folder', button_name='operation', button_value='add_new_folder') }} +
- {% endif %} diff --git a/pytest.ini b/pytest.ini index 2b0a34ba8..cbc831544 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,5 @@ [pytest] +xfail_strict=true testpaths = tests log_level = 999 env = @@ -10,6 +11,3 @@ env = ZENDESK_API_KEY=test STATSD_PREFIX=stats-prefix REDIS_ENABLED=0 -# :thinking_face: -filterwarnings = - ignore::pytest.RemovedInPytest4Warning