mirror of
https://github.com/GSA/notifications-api.git
synced 2026-05-22 17:51:39 -04:00
move folders and templates to other folders
new endpoints: /services/<service_id>/move-to-folder /services/<service_id>/move-to-folder/<target_template_folder_id> * takes in a dict containing lists of `templates` and `folders` uuids. * sets parent of templates and folders to the folder specified in the URL. Or None, if there was no id specified. * if any template or folder has a differen service id, then the whole update fails * if any folder is an ancestor of the target folder, then the whole update fails (as that would cause a cyclical folder structure). * the whole function is wrapped in a single `transactional` decorator, so in case of error nothing will be saved.
This commit is contained in:
@@ -725,6 +725,13 @@ class TemplateFolder(db.Model):
|
||||
'service_id': self.service_id
|
||||
}
|
||||
|
||||
def is_parent_of(self, other):
|
||||
while other.parent is not None:
|
||||
if other.parent == self:
|
||||
return True
|
||||
other = other.parent
|
||||
return False
|
||||
|
||||
|
||||
template_folder_map = db.Table(
|
||||
'template_folder_map',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from flask import Blueprint, jsonify, request
|
||||
from flask import Blueprint, jsonify, request, abort, current_app
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
from app.dao.dao_utils import transactional
|
||||
from app.dao.templates_dao import dao_get_template_by_id_and_service_id
|
||||
from app.dao.template_folder_dao import (
|
||||
dao_create_template_folder,
|
||||
dao_get_template_folder_by_id_and_service_id,
|
||||
@@ -13,7 +15,8 @@ from app.errors import InvalidRequest, register_errors
|
||||
from app.models import TemplateFolder
|
||||
from app.template_folder.template_folder_schema import (
|
||||
post_create_template_folder_schema,
|
||||
post_rename_template_folder_schema
|
||||
post_rename_template_folder_schema,
|
||||
post_move_template_folder_schema,
|
||||
)
|
||||
from app.schema_validation import validate
|
||||
|
||||
@@ -89,3 +92,54 @@ def delete_template_folder(service_id, template_folder_id):
|
||||
dao_delete_template_folder(template_folder)
|
||||
|
||||
return '', 204
|
||||
|
||||
|
||||
@template_folder_blueprint.route('/move-to-folder', methods=['POST'])
|
||||
@template_folder_blueprint.route('/move-to-folder/<uuid:target_template_folder_id>', methods=['POST'])
|
||||
@transactional
|
||||
def move_to_template_folder(service_id, target_template_folder_id=None):
|
||||
data = request.get_json()
|
||||
|
||||
validate(data, post_move_template_folder_schema)
|
||||
|
||||
if target_template_folder_id:
|
||||
target_template_folder = dao_get_template_folder_by_id_and_service_id(target_template_folder_id, service_id)
|
||||
else:
|
||||
target_template_folder = None
|
||||
|
||||
for template_folder_id in data['folders']:
|
||||
try:
|
||||
template_folder = dao_get_template_folder_by_id_and_service_id(template_folder_id, service_id)
|
||||
except NoResultFound:
|
||||
current_app.logger.error('Could not move to folder: No folder found with id {} for service {}'.format(
|
||||
template_folder_id,
|
||||
service_id
|
||||
))
|
||||
abort(400)
|
||||
|
||||
if target_template_folder and template_folder.is_parent_of(target_template_folder):
|
||||
current_app.logger.error('Could not move to folder: {} is an ancestor of target folder {}'.format(
|
||||
template_folder_id,
|
||||
target_template_folder_id
|
||||
))
|
||||
abort(400)
|
||||
|
||||
template_folder.parent = target_template_folder
|
||||
|
||||
for template_id in data['templates']:
|
||||
try:
|
||||
template = dao_get_template_by_id_and_service_id(template_id, service_id)
|
||||
except NoResultFound:
|
||||
current_app.logger.error('Could not move to folder: No template found with id {} for service {}'.format(
|
||||
template_id,
|
||||
service_id
|
||||
))
|
||||
abort(400)
|
||||
|
||||
if template.archived:
|
||||
current_app.logger.error('Could not move to folder: Template {} is archived. (Skipping)'.format(
|
||||
template_id
|
||||
))
|
||||
else:
|
||||
template.folder = target_template_folder
|
||||
return '', 200
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from app.schema_validation.definitions import nullable_uuid
|
||||
from app.schema_validation.definitions import nullable_uuid, uuid
|
||||
|
||||
post_create_template_folder_schema = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
@@ -20,3 +20,14 @@ post_rename_template_folder_schema = {
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
|
||||
post_move_template_folder_schema = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "POST schema for renaming template_folder",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"templates": {"type": "array", "items": uuid},
|
||||
"folders": {"type": "array", "items": uuid},
|
||||
},
|
||||
"required": ["templates", "folders"]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ from app.dao.service_inbound_api_dao import save_service_inbound_api
|
||||
from app.dao.service_permissions_dao import dao_add_service_permission
|
||||
from app.dao.service_sms_sender_dao import update_existing_sms_sender_with_inbound_number, dao_update_service_sms_sender
|
||||
from app.dao.services_dao import dao_create_service
|
||||
from app.dao.templates_dao import dao_create_template
|
||||
from app.dao.templates_dao import dao_create_template, dao_update_template
|
||||
from app.dao.users_dao import save_model_user
|
||||
from app.models import (
|
||||
ApiKey,
|
||||
@@ -137,7 +137,9 @@ def create_template(
|
||||
subject='Template subject',
|
||||
content='Dear Sir/Madam, Hello. Yours Truly, The Government.',
|
||||
reply_to=None,
|
||||
hidden=False
|
||||
hidden=False,
|
||||
archived=False,
|
||||
folder=None,
|
||||
):
|
||||
data = {
|
||||
'name': template_name or '{} Template Name'.format(template_type),
|
||||
@@ -146,12 +148,18 @@ def create_template(
|
||||
'service': service,
|
||||
'created_by': service.created_by,
|
||||
'reply_to': reply_to,
|
||||
'hidden': hidden
|
||||
'hidden': hidden,
|
||||
'folder': folder,
|
||||
}
|
||||
if template_type != SMS_TYPE:
|
||||
data['subject'] = subject
|
||||
template = Template(**data)
|
||||
dao_create_template(template)
|
||||
|
||||
if archived:
|
||||
template.archived = archived
|
||||
dao_update_template(template)
|
||||
|
||||
return template
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
|
||||
from app.models import TemplateFolder
|
||||
|
||||
from tests.app.db import create_service, create_template_folder
|
||||
from tests.app.db import create_service, create_template_folder, create_template
|
||||
|
||||
|
||||
def test_get_folders_for_service(admin_request, notify_db_session):
|
||||
@@ -188,3 +188,157 @@ def test_delete_template_folder_fails_if_folder_contains_templates(admin_request
|
||||
}
|
||||
|
||||
assert TemplateFolder.query.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize('data', [
|
||||
{},
|
||||
{'templates': None, 'folders': []},
|
||||
{'folders': []},
|
||||
{'templates': [], 'folders': [None]},
|
||||
{'templates': [], 'folders': ['not a uuid']},
|
||||
])
|
||||
def test_move_to_folder_validates_schema(data, admin_request, notify_db_session):
|
||||
admin_request.post(
|
||||
'template_folder.move_to_template_folder',
|
||||
service_id=uuid.uuid4(),
|
||||
target_template_folder_id=uuid.uuid4(),
|
||||
_data=data,
|
||||
_expected_status=400
|
||||
)
|
||||
|
||||
|
||||
def test_move_to_folder_moves_folders_and_templates(admin_request, sample_service):
|
||||
target_folder = create_template_folder(sample_service, name='target')
|
||||
f1 = create_template_folder(sample_service, name='f1')
|
||||
f2 = create_template_folder(sample_service, name='f2')
|
||||
|
||||
t1 = create_template(sample_service, template_name='t1', folder=f1)
|
||||
t2 = create_template(sample_service, template_name='t2', folder=f1)
|
||||
t3 = create_template(sample_service, template_name='t3', folder=f2)
|
||||
t4 = create_template(sample_service, template_name='t4', folder=target_folder)
|
||||
|
||||
admin_request.post(
|
||||
'template_folder.move_to_template_folder',
|
||||
service_id=sample_service.id,
|
||||
target_template_folder_id=target_folder.id,
|
||||
_data={
|
||||
'templates': [str(t1.id)],
|
||||
'folders': [str(f1.id)]
|
||||
}
|
||||
)
|
||||
|
||||
assert target_folder.parent is None
|
||||
assert f1.parent == target_folder
|
||||
assert f2.parent is None # unchanged
|
||||
|
||||
assert t1.folder == target_folder # moved out of f1, even though f1 is also being moved
|
||||
assert t2.folder == f1 # stays in f1, though f1 has moved
|
||||
assert t3.folder == f2 # unchanged
|
||||
assert t4.folder == target_folder # unchanged
|
||||
|
||||
# versions are all unchanged
|
||||
assert t1.version == 1
|
||||
assert t2.version == 1
|
||||
assert t3.version == 1
|
||||
assert t4.version == 1
|
||||
|
||||
|
||||
def test_move_to_folder_moves_folders_and_templates_to_top_level_if_no_target(admin_request, sample_service):
|
||||
f1 = create_template_folder(sample_service, name='f1')
|
||||
f2 = create_template_folder(sample_service, name='f2')
|
||||
|
||||
t1 = create_template(sample_service, template_name='t1', folder=f1)
|
||||
t2 = create_template(sample_service, template_name='t2', folder=f1)
|
||||
t3 = create_template(sample_service, template_name='t3', folder=f2)
|
||||
|
||||
admin_request.post(
|
||||
'template_folder.move_to_template_folder',
|
||||
service_id=sample_service.id,
|
||||
target_template_folder_id=None,
|
||||
_data={
|
||||
'templates': [str(t1.id)],
|
||||
'folders': [str(f1.id)]
|
||||
}
|
||||
)
|
||||
|
||||
assert f1.parent is None
|
||||
assert f2.parent is None # unchanged
|
||||
|
||||
assert t1.folder is None # moved out of f1, even though f1 is also being moved
|
||||
assert t2.folder == f1 # stays in f1, though f1 has moved
|
||||
assert t3.folder == f2 # unchanged
|
||||
|
||||
|
||||
def test_move_to_folder_rejects_folder_from_other_service(admin_request, notify_db_session):
|
||||
s1 = create_service(service_name='s1')
|
||||
s2 = create_service(service_name='s2')
|
||||
|
||||
f2 = create_template_folder(s2)
|
||||
|
||||
admin_request.post(
|
||||
'template_folder.move_to_template_folder',
|
||||
service_id=s1.id,
|
||||
target_template_folder_id=None,
|
||||
_data={
|
||||
'templates': [],
|
||||
'folders': [str(f2.id)]
|
||||
},
|
||||
_expected_status=400
|
||||
)
|
||||
|
||||
|
||||
def test_move_to_folder_rejects_template_from_other_service(admin_request, notify_db_session):
|
||||
s1 = create_service(service_name='s1')
|
||||
s2 = create_service(service_name='s2')
|
||||
|
||||
t2 = create_template(s2)
|
||||
|
||||
admin_request.post(
|
||||
'template_folder.move_to_template_folder',
|
||||
service_id=s1.id,
|
||||
target_template_folder_id=None,
|
||||
_data={
|
||||
'templates': [],
|
||||
'folders': [str(t2.id)]
|
||||
},
|
||||
_expected_status=400
|
||||
)
|
||||
|
||||
|
||||
def test_move_to_folder_rejects_if_it_would_cause_folder_loop(admin_request, sample_service):
|
||||
f1 = create_template_folder(sample_service, name='f1')
|
||||
target_folder = create_template_folder(sample_service, name='target', parent=f1)
|
||||
|
||||
admin_request.post(
|
||||
'template_folder.move_to_template_folder',
|
||||
service_id=sample_service.id,
|
||||
target_template_folder_id=target_folder.id,
|
||||
_data={
|
||||
'folders': [str(f1.id)]
|
||||
},
|
||||
_expected_status=400
|
||||
)
|
||||
|
||||
|
||||
def test_move_to_folder_skips_archived_templates(admin_request, sample_service):
|
||||
target_folder = create_template_folder(sample_service)
|
||||
other_folder = create_template_folder(sample_service)
|
||||
|
||||
archived_template = create_template(sample_service, archived=True, folder=None)
|
||||
unarchived_template = create_template(sample_service, archived=False, folder=other_folder)
|
||||
|
||||
archived_timestamp = archived_template.updated_at
|
||||
|
||||
admin_request.post(
|
||||
'template_folder.move_to_template_folder',
|
||||
service_id=sample_service.id,
|
||||
target_template_folder_id=target_folder.id,
|
||||
_data={
|
||||
'templates': [str(archived_template.id), str(unarchived_template.id)],
|
||||
'folders': []
|
||||
}
|
||||
)
|
||||
|
||||
assert archived_template.updated_at == archived_timestamp
|
||||
assert archived_template.folder is None
|
||||
assert unarchived_template.folder == target_folder
|
||||
|
||||
@@ -28,7 +28,8 @@ from tests.app.db import (
|
||||
create_inbound_number,
|
||||
create_reply_to_email,
|
||||
create_letter_contact,
|
||||
create_template
|
||||
create_template,
|
||||
create_template_folder
|
||||
)
|
||||
|
||||
|
||||
@@ -329,3 +330,17 @@ def test_is_precompiled_letter_hidden_true_not_name(sample_letter_template):
|
||||
def test_is_precompiled_letter_name_correct_not_hidden(sample_letter_template):
|
||||
sample_letter_template.name = PRECOMPILED_TEMPLATE_NAME
|
||||
assert not sample_letter_template.is_precompiled_letter
|
||||
|
||||
|
||||
def test_template_folder_is_parent(sample_service):
|
||||
x = None
|
||||
folders = []
|
||||
for i in range(5):
|
||||
x = create_template_folder(sample_service, name=str(i), parent=x)
|
||||
folders.append(x)
|
||||
|
||||
assert folders[0].is_parent_of(folders[1])
|
||||
assert folders[0].is_parent_of(folders[2])
|
||||
assert folders[0].is_parent_of(folders[4])
|
||||
assert folders[1].is_parent_of(folders[2])
|
||||
assert not folders[1].is_parent_of(folders[0])
|
||||
|
||||
Reference in New Issue
Block a user