mirror of
https://github.com/GSA/notifications-api.git
synced 2026-07-25 10:29:34 -04:00
Merge pull request #2199 from alphagov/template-folder-crud
add template folder CRUD
This commit is contained in:
@@ -114,6 +114,7 @@ def register_blueprint(application):
|
||||
from app.organisation.invite_rest import organisation_invite_blueprint
|
||||
from app.complaint.complaint_rest import complaint_blueprint
|
||||
from app.platform_stats.rest import platform_stats_blueprint
|
||||
from app.template_folder.rest import template_folder_blueprint
|
||||
|
||||
service_blueprint.before_request(requires_admin_auth)
|
||||
application.register_blueprint(service_blueprint, url_prefix='/service')
|
||||
@@ -196,6 +197,9 @@ def register_blueprint(application):
|
||||
platform_stats_blueprint.before_request(requires_admin_auth)
|
||||
application.register_blueprint(platform_stats_blueprint, url_prefix='/platform-stats')
|
||||
|
||||
template_folder_blueprint.before_request(requires_admin_auth)
|
||||
application.register_blueprint(template_folder_blueprint)
|
||||
|
||||
|
||||
def register_v2_blueprints(application):
|
||||
from app.v2.inbound_sms.get_inbound_sms import v2_inbound_sms_blueprint as get_inbound_sms
|
||||
|
||||
22
app/dao/template_folder_dao.py
Normal file
22
app/dao/template_folder_dao.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from app import db
|
||||
from app.dao.dao_utils import transactional
|
||||
from app.models import TemplateFolder
|
||||
|
||||
|
||||
def dao_get_template_folder_by_id(template_folder_id):
|
||||
return TemplateFolder.query.filter(TemplateFolder.id == template_folder_id).one()
|
||||
|
||||
|
||||
@transactional
|
||||
def dao_create_template_folder(template_folder):
|
||||
db.session.add(template_folder)
|
||||
|
||||
|
||||
@transactional
|
||||
def dao_update_template_folder(template_folder):
|
||||
db.session.add(template_folder)
|
||||
|
||||
|
||||
@transactional
|
||||
def dao_delete_template_folder(template_folder):
|
||||
db.session.delete(template_folder)
|
||||
@@ -714,8 +714,16 @@ class TemplateFolder(db.Model):
|
||||
name = db.Column(db.String, nullable=False)
|
||||
parent_id = db.Column(UUID(as_uuid=True), db.ForeignKey('template_folder.id'), nullable=True)
|
||||
|
||||
service = db.relationship('Service')
|
||||
parent = db.relationship('TemplateFolder', remote_side=[id], backref='children')
|
||||
service = db.relationship('Service', backref='all_template_folders')
|
||||
parent = db.relationship('TemplateFolder', remote_side=[id], backref='subfolders')
|
||||
|
||||
def serialize(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'parent_id': self.parent_id,
|
||||
'service_id': self.service_id
|
||||
}
|
||||
|
||||
|
||||
template_folder_map = db.Table(
|
||||
@@ -864,7 +872,7 @@ class Template(TemplateBase):
|
||||
uselist=False,
|
||||
# eagerly load the folder whenever the template object is fetched
|
||||
lazy='joined',
|
||||
backref=db.backref('templates', lazy='dynamic')
|
||||
backref=db.backref('templates')
|
||||
)
|
||||
|
||||
def get_link(self):
|
||||
|
||||
@@ -11,6 +11,14 @@ uuid = {
|
||||
"link": "link to our error documentation not yet implemented"
|
||||
}
|
||||
|
||||
nullable_uuid = {
|
||||
"type": ["string", "null"],
|
||||
"format": "validate_uuid",
|
||||
"validationMessage": "is not a valid UUID",
|
||||
"code": "1001", # yet to be implemented
|
||||
"link": "link to our error documentation not yet implemented"
|
||||
}
|
||||
|
||||
|
||||
personalisation = {
|
||||
"type": "object",
|
||||
|
||||
0
app/template_folder/__init__.py
Normal file
0
app/template_folder/__init__.py
Normal file
84
app/template_folder/rest.py
Normal file
84
app/template_folder/rest.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from flask import Blueprint, jsonify, request
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.dao.template_folder_dao import (
|
||||
dao_create_template_folder,
|
||||
dao_get_template_folder_by_id,
|
||||
dao_update_template_folder,
|
||||
dao_delete_template_folder
|
||||
)
|
||||
from app.dao.services_dao import dao_fetch_service_by_id
|
||||
from app.errors import 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
|
||||
)
|
||||
from app.schema_validation import validate
|
||||
|
||||
template_folder_blueprint = Blueprint(
|
||||
'template_folder',
|
||||
__name__,
|
||||
url_prefix='/service/<uuid:service_id>/template-folder'
|
||||
)
|
||||
register_errors(template_folder_blueprint)
|
||||
|
||||
|
||||
@template_folder_blueprint.errorhandler(IntegrityError)
|
||||
def handle_integrity_error(exc):
|
||||
if 'template_folder_parent_id_fkey' in str(exc):
|
||||
return jsonify(result='error', message='parent_id not found'), 400
|
||||
|
||||
raise
|
||||
|
||||
|
||||
@template_folder_blueprint.route('', methods=['GET'])
|
||||
def get_template_folders_for_service(service_id):
|
||||
service = dao_fetch_service_by_id(service_id)
|
||||
|
||||
template_folders = [o.serialize() for o in service.all_template_folders]
|
||||
return jsonify(template_folders=template_folders)
|
||||
|
||||
|
||||
@template_folder_blueprint.route('', methods=['POST'])
|
||||
def create_template_folder(service_id):
|
||||
data = request.get_json()
|
||||
|
||||
validate(data, post_create_template_folder_schema)
|
||||
|
||||
template_folder = TemplateFolder(
|
||||
service_id=service_id,
|
||||
name=data['name'].strip(),
|
||||
parent_id=data['parent_id']
|
||||
)
|
||||
|
||||
dao_create_template_folder(template_folder)
|
||||
|
||||
return jsonify(data=template_folder.serialize()), 201
|
||||
|
||||
|
||||
@template_folder_blueprint.route('/<uuid:template_folder_id>/rename', methods=['POST'])
|
||||
def rename_template_folder(service_id, template_folder_id):
|
||||
data = request.get_json()
|
||||
|
||||
validate(data, post_rename_template_folder_schema)
|
||||
|
||||
template_folder = dao_get_template_folder_by_id(template_folder_id)
|
||||
template_folder.name = data['name']
|
||||
|
||||
dao_update_template_folder(template_folder)
|
||||
|
||||
return jsonify(data=template_folder.serialize()), 200
|
||||
|
||||
|
||||
@template_folder_blueprint.route('/<uuid:template_folder_id>', methods=['DELETE'])
|
||||
def delete_template_folder(service_id, template_folder_id):
|
||||
template_folder = dao_get_template_folder_by_id(template_folder_id)
|
||||
|
||||
# don't allow deleting if there's anything in the folder (even if it's just more empty subfolders)
|
||||
if template_folder.subfolders or template_folder.templates:
|
||||
return jsonify(result='error', message='Folder is not empty'), 400
|
||||
|
||||
dao_delete_template_folder(template_folder)
|
||||
|
||||
return '', 204
|
||||
22
app/template_folder/template_folder_schema.py
Normal file
22
app/template_folder/template_folder_schema.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from app.schema_validation.definitions import nullable_uuid
|
||||
|
||||
post_create_template_folder_schema = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "POST schema for getting template_folder",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "minLength": 1},
|
||||
"parent_id": nullable_uuid
|
||||
},
|
||||
"required": ["name", "parent_id"]
|
||||
}
|
||||
|
||||
post_rename_template_folder_schema = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "POST schema for renaming template_folder",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "minLength": 1},
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
@@ -48,7 +48,8 @@ from app.models import (
|
||||
FactBilling,
|
||||
FactNotificationStatus,
|
||||
Complaint,
|
||||
InvitedUser
|
||||
InvitedUser,
|
||||
TemplateFolder,
|
||||
)
|
||||
|
||||
|
||||
@@ -690,3 +691,10 @@ def create_invited_user(service=None,
|
||||
invited_user = InvitedUser(**data)
|
||||
save_invited_user(invited_user)
|
||||
return invited_user
|
||||
|
||||
|
||||
def create_template_folder(service, name='foo', parent=None):
|
||||
tf = TemplateFolder(name=name, service=service, parent=parent)
|
||||
db.session.add(tf)
|
||||
db.session.commit()
|
||||
return tf
|
||||
|
||||
178
tests/app/template_folder/test_template_folder_rest.py
Normal file
178
tests/app/template_folder/test_template_folder_rest.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from app.models import TemplateFolder
|
||||
|
||||
from tests.app.db import create_service, create_template_folder
|
||||
|
||||
|
||||
def test_get_folders_for_service(admin_request, notify_db_session):
|
||||
s1 = create_service(service_name='a')
|
||||
s2 = create_service(service_name='b')
|
||||
|
||||
tf1 = create_template_folder(s1)
|
||||
tf2 = create_template_folder(s1)
|
||||
|
||||
create_template_folder(s2)
|
||||
|
||||
resp = admin_request.get('template_folder.get_template_folders_for_service', service_id=s1.id)
|
||||
assert set(resp.keys()) == {'template_folders'}
|
||||
assert sorted(resp['template_folders'], key=lambda x: x['id']) == sorted([
|
||||
{'id': str(tf1.id), 'name': 'foo', 'service_id': str(s1.id), 'parent_id': None},
|
||||
{'id': str(tf2.id), 'name': 'foo', 'service_id': str(s1.id), 'parent_id': None},
|
||||
], key=lambda x: x['id'])
|
||||
|
||||
|
||||
def test_get_folders_for_service_with_no_folders(sample_service, admin_request):
|
||||
resp = admin_request.get('template_folder.get_template_folders_for_service', service_id=sample_service.id)
|
||||
assert resp == {'template_folders': []}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('has_parent', [True, False])
|
||||
def test_create_template_folder(admin_request, sample_service, has_parent):
|
||||
existing_folder = create_template_folder(sample_service)
|
||||
|
||||
parent_id = str(existing_folder.id) if has_parent else None
|
||||
|
||||
resp = admin_request.post(
|
||||
'template_folder.create_template_folder',
|
||||
service_id=sample_service.id,
|
||||
_data={
|
||||
'name': 'foo',
|
||||
'parent_id': parent_id
|
||||
},
|
||||
_expected_status=201
|
||||
)
|
||||
|
||||
assert resp['data']['name'] == 'foo'
|
||||
assert resp['data']['service_id'] == str(sample_service.id)
|
||||
assert resp['data']['parent_id'] == parent_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize('missing_field', ['name', 'parent_id'])
|
||||
def test_create_template_folder_fails_if_missing_fields(admin_request, sample_service, missing_field):
|
||||
data = {
|
||||
'name': 'foo',
|
||||
'parent_id': None
|
||||
}
|
||||
data.pop(missing_field)
|
||||
|
||||
resp = admin_request.post(
|
||||
'template_folder.create_template_folder',
|
||||
service_id=sample_service.id,
|
||||
_data=data,
|
||||
_expected_status=400
|
||||
)
|
||||
|
||||
assert resp == {
|
||||
'status_code': 400,
|
||||
'errors': [
|
||||
{'error': 'ValidationError', 'message': '{} is a required property'.format(missing_field)}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_create_template_folder_fails_if_unknown_parent_id(admin_request, sample_service):
|
||||
# create existing folder
|
||||
create_template_folder(sample_service)
|
||||
|
||||
resp = admin_request.post(
|
||||
'template_folder.create_template_folder',
|
||||
service_id=sample_service.id,
|
||||
_data={'name': 'bar', 'parent_id': str(uuid.uuid4())},
|
||||
_expected_status=400
|
||||
)
|
||||
|
||||
assert resp['result'] == 'error'
|
||||
assert resp['message'] == 'parent_id not found'
|
||||
|
||||
|
||||
def test_rename_template_folder(admin_request, sample_service):
|
||||
existing_folder = create_template_folder(sample_service)
|
||||
|
||||
resp = admin_request.post(
|
||||
'template_folder.rename_template_folder',
|
||||
service_id=sample_service.id,
|
||||
template_folder_id=existing_folder.id,
|
||||
_data={
|
||||
'name': 'bar'
|
||||
}
|
||||
)
|
||||
|
||||
assert resp['data']['name'] == 'bar'
|
||||
assert existing_folder.name == 'bar'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('data, err', [
|
||||
({}, 'name is a required property'),
|
||||
({'name': None}, 'name None is not of type string'),
|
||||
({'name': ''}, 'name is too short'),
|
||||
])
|
||||
def test_rename_template_folder_fails_if_missing_name(admin_request, sample_service, data, err):
|
||||
existing_folder = create_template_folder(sample_service)
|
||||
|
||||
resp = admin_request.post(
|
||||
'template_folder.rename_template_folder',
|
||||
service_id=sample_service.id,
|
||||
template_folder_id=existing_folder.id,
|
||||
_data=data,
|
||||
_expected_status=400
|
||||
)
|
||||
|
||||
assert resp == {
|
||||
'status_code': 400,
|
||||
'errors': [
|
||||
{'error': 'ValidationError', 'message': err}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_delete_template_folder(admin_request, sample_service):
|
||||
existing_folder = create_template_folder(sample_service)
|
||||
|
||||
admin_request.delete(
|
||||
'template_folder.delete_template_folder',
|
||||
service_id=sample_service.id,
|
||||
template_folder_id=existing_folder.id,
|
||||
)
|
||||
|
||||
assert TemplateFolder.query.all() == []
|
||||
|
||||
|
||||
def test_delete_template_folder_fails_if_folder_has_subfolders(admin_request, sample_service):
|
||||
existing_folder = create_template_folder(sample_service)
|
||||
existing_subfolder = create_template_folder(sample_service, parent=existing_folder) # noqa
|
||||
|
||||
resp = admin_request.delete(
|
||||
'template_folder.delete_template_folder',
|
||||
service_id=sample_service.id,
|
||||
template_folder_id=existing_folder.id,
|
||||
_expected_status=400
|
||||
)
|
||||
|
||||
assert resp == {
|
||||
'result': 'error',
|
||||
'message': 'Folder is not empty'
|
||||
}
|
||||
|
||||
assert TemplateFolder.query.count() == 2
|
||||
|
||||
|
||||
def test_delete_template_folder_fails_if_folder_contains_templates(admin_request, sample_service, sample_template):
|
||||
existing_folder = create_template_folder(sample_service)
|
||||
sample_template.folder = existing_folder
|
||||
|
||||
resp = admin_request.delete(
|
||||
'template_folder.delete_template_folder',
|
||||
service_id=sample_service.id,
|
||||
template_folder_id=existing_folder.id,
|
||||
_expected_status=400
|
||||
)
|
||||
|
||||
assert resp == {
|
||||
'result': 'error',
|
||||
'message': 'Folder is not empty'
|
||||
}
|
||||
|
||||
assert TemplateFolder.query.count() == 1
|
||||
Reference in New Issue
Block a user