diff --git a/app/history_meta.py b/app/history_meta.py index c89fe06ca..ff44255f9 100644 --- a/app/history_meta.py +++ b/app/history_meta.py @@ -219,6 +219,8 @@ def create_history(obj, history_cls=None): data['created_at'] = obj.created_at for key, value in data.items(): + if not hasattr(history_cls, key): + raise AttributeError("{} has no attribute '{}'".format(history_cls.__name__, key)) setattr(history, key, value) return history diff --git a/app/models.py b/app/models.py index f921f2aca..a1322cf75 100644 --- a/app/models.py +++ b/app/models.py @@ -4,6 +4,7 @@ import uuid import datetime from flask import url_for, current_app +from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.dialects.postgresql import ( UUID, @@ -522,50 +523,67 @@ class TemplateProcessTypes(db.Model): name = db.Column(db.String(255), primary_key=True) -class Template(db.Model): - __tablename__ = 'templates' +class TemplateBase(db.Model): + __abstract__ = True + + def __init__(self, **kwargs): + if 'template_type' in kwargs: + self.template_type = kwargs.pop('template_type') + + super().__init__(**kwargs) id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name = db.Column(db.String(255), nullable=False) template_type = db.Column(template_types, nullable=False) - created_at = db.Column( - db.DateTime, - index=False, - unique=False, - nullable=False, - default=datetime.datetime.utcnow) - updated_at = db.Column( - db.DateTime, - index=False, - unique=False, - nullable=True, - onupdate=datetime.datetime.utcnow) - content = db.Column(db.Text, index=False, unique=False, nullable=False) - archived = db.Column(db.Boolean, index=False, nullable=False, default=False) - service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, unique=False, nullable=False) - service = db.relationship('Service', backref='templates') - subject = db.Column(db.Text, index=False, unique=False, nullable=True) - created_by_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), index=True, nullable=False) - created_by = db.relationship('User') - version = db.Column(db.Integer, default=0, nullable=False) - process_type = db.Column( - db.String(255), - db.ForeignKey('template_process_type.name'), - index=True, - nullable=False, - default=NORMAL - ) + created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow) + updated_at = db.Column(db.DateTime, onupdate=datetime.datetime.utcnow) + content = db.Column(db.Text, nullable=False) + archived = db.Column(db.Boolean, nullable=False, default=False) + subject = db.Column(db.Text) + + @declared_attr + def service_id(cls): + return db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, nullable=False) + + @declared_attr + def created_by_id(cls): + return db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), index=True, nullable=False) + + @declared_attr + def created_by(cls): + return db.relationship('User') + + @declared_attr + def process_type(cls): + return db.Column( + db.String(255), + db.ForeignKey('template_process_type.name'), + index=True, + nullable=False, + default=NORMAL + ) redact_personalisation = association_proxy('template_redacted', 'redact_personalisation') - def get_link(self): - # TODO: use "/v2/" route once available - return url_for( - "template.get_template_by_id_and_service_id", - service_id=self.service_id, - template_id=self.id, - _external=True - ) + @declared_attr + def service_letter_contact_id(cls): + return db.Column(UUID(as_uuid=True), db.ForeignKey('service_letter_contacts.id'), nullable=True) + + @property + def reply_to(self): + if self.template_type == LETTER_TYPE: + return self.service_letter_contact_id + else: + return None + + @reply_to.setter + def reply_to(self, value): + if self.template_type == LETTER_TYPE: + self.service_letter_contact_id = value + elif value is None: + pass + else: + raise ValueError('Unable to set sender for {} template'.format(self.template_type)) def _as_utils_template(self): if self.template_type == EMAIL_TYPE: @@ -605,6 +623,22 @@ class Template(db.Model): return serialized +class Template(TemplateBase): + __tablename__ = 'templates' + + service = db.relationship('Service', backref='templates') + version = db.Column(db.Integer, default=0, nullable=False) + + def get_link(self): + # TODO: use "/v2/" route once available + return url_for( + "template.get_template_by_id_and_service_id", + service_id=self.service_id, + template_id=self.id, + _external=True + ) + + class TemplateRedacted(db.Model): __tablename__ = 'template_redacted' @@ -618,32 +652,16 @@ class TemplateRedacted(db.Model): template = db.relationship('Template', uselist=False, backref=db.backref('template_redacted', uselist=False)) -class TemplateHistory(db.Model): +class TemplateHistory(TemplateBase): __tablename__ = 'templates_history' - id = db.Column(UUID(as_uuid=True), primary_key=True) - name = db.Column(db.String(255), nullable=False) - template_type = db.Column(template_types, nullable=False) - created_at = db.Column(db.DateTime, nullable=False) - updated_at = db.Column(db.DateTime) - content = db.Column(db.Text, nullable=False) - archived = db.Column(db.Boolean, nullable=False, default=False) - service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), index=True, nullable=False) service = db.relationship('Service') - subject = db.Column(db.Text) - created_by_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), index=True, nullable=False) - created_by = db.relationship('User') version = db.Column(db.Integer, primary_key=True, nullable=False) - process_type = db.Column(db.String(255), - db.ForeignKey('template_process_type.name'), - index=True, - nullable=False, - default=NORMAL) - template_redacted = db.relationship('TemplateRedacted', foreign_keys=[id], - primaryjoin='TemplateRedacted.template_id == TemplateHistory.id') - - redact_personalisation = association_proxy('template_redacted', 'redact_personalisation') + @declared_attr + def template_redacted(cls): + return db.relationship('TemplateRedacted', foreign_keys=[cls.id], + primaryjoin='TemplateRedacted.template_id == TemplateHistory.id') def get_link(self): return url_for( @@ -653,12 +671,6 @@ class TemplateHistory(db.Model): _external=True ) - def _as_utils_template(self): - return Template._as_utils_template(self) - - def serialize(self): - return Template.serialize(self) - MMG_PROVIDER = "mmg" FIRETEXT_PROVIDER = "firetext" diff --git a/app/schemas.py b/app/schemas.py index 5e020ddf1..b4d34ab62 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -314,9 +314,14 @@ class NotificationModelSchema(BaseSchema): class BaseTemplateSchema(BaseSchema): + reply_to = fields.Method("get_reply_to", allow_none=True) + + def get_reply_to(self, template): + return template.reply_to + class Meta: model = models.Template - exclude = ("service_id", "jobs") + exclude = ("service_id", "jobs", "service_letter_contact_id") strict = True @@ -339,9 +344,14 @@ class TemplateSchema(BaseTemplateSchema): class TemplateHistorySchema(BaseSchema): + reply_to = fields.Method("get_reply_to", allow_none=True) + created_by = fields.Nested(UserSchema, only=['id', 'name', 'email_address'], dump_only=True) created_at = field_for(models.Template, 'created_at', format='%Y-%m-%d %H:%M:%S.%f') + def get_reply_to(self, template): + return template.reply_to + class Meta: model = models.TemplateHistory diff --git a/app/template/rest.py b/app/template/rest.py index 9a30aab4b..de42e723d 100644 --- a/app/template/rest.py +++ b/app/template/rest.py @@ -155,7 +155,7 @@ def get_template_versions(service_id, template_id): def _template_has_not_changed(current_data, updated_template): return all( current_data[key] == updated_template[key] - for key in ('name', 'content', 'subject', 'archived', 'process_type') + for key in ('name', 'content', 'subject', 'archived', 'process_type', 'reply_to') ) diff --git a/migrations/versions/0144_template_service_letter.py b/migrations/versions/0144_template_service_letter.py new file mode 100644 index 000000000..31bc017cb --- /dev/null +++ b/migrations/versions/0144_template_service_letter.py @@ -0,0 +1,33 @@ +""" + +Revision ID: 0144_template_service_letter +Revises: 0143_remove_reply_to +Create Date: 2017-11-17 15:42:16.401229 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +revision = '0144_template_service_letter' +down_revision = '0143_remove_reply_to' + + +def upgrade(): + op.add_column('templates', + sa.Column('service_letter_contact_id', postgresql.UUID(as_uuid=True), nullable=True)) + op.create_foreign_key('templates_service_letter_contact_id_fkey', 'templates', + 'service_letter_contacts', ['service_letter_contact_id'], ['id']) + + op.add_column('templates_history', + sa.Column('service_letter_contact_id', postgresql.UUID(as_uuid=True), nullable=True)) + op.create_foreign_key('templates_history_service_letter_contact_id_fkey', 'templates_history', + 'service_letter_contacts', ['service_letter_contact_id'], ['id']) + + +def downgrade(): + op.drop_constraint('templates_service_letter_contact_id_fkey', 'templates', type_='foreignkey') + op.drop_column('templates', 'service_letter_contact_id') + + op.drop_constraint('templates_history_service_letter_contact_id_fkey', 'templates_history', type_='foreignkey') + op.drop_column('templates_history', 'service_letter_contact_id') diff --git a/tests/app/dao/test_templates_dao.py b/tests/app/dao/test_templates_dao.py index 43cc88fd7..607330097 100644 --- a/tests/app/dao/test_templates_dao.py +++ b/tests/app/dao/test_templates_dao.py @@ -15,7 +15,7 @@ from app.dao.templates_dao import ( from app.models import Template, TemplateHistory, TemplateRedacted from tests.app.conftest import sample_template as create_sample_template -from tests.app.db import create_template +from tests.app.db import create_template, create_letter_contact @pytest.mark.parametrize('template_type, subject', [ @@ -53,6 +53,23 @@ def test_create_template_creates_redact_entry(sample_service): assert redacted.updated_by_id == sample_service.created_by_id +def test_create_template_with_reply_to(sample_service, sample_user): + letter_contact = create_letter_contact(sample_service, 'Edinburgh, ED1 1AA') + + data = { + 'name': 'Sample Template', + 'template_type': "letter", + 'content': "Template content", + 'service': sample_service, + 'created_by': sample_user, + 'reply_to': letter_contact.id, + } + template = Template(**data) + dao_create_template(template) + + assert dao_get_all_templates_for_service(sample_service.id)[0].reply_to == letter_contact.id + + def test_update_template(sample_service, sample_user): data = { 'name': 'Sample Template', @@ -71,6 +88,29 @@ def test_update_template(sample_service, sample_user): assert dao_get_all_templates_for_service(sample_service.id)[0].name == 'new name' +def test_update_template_reply_to(sample_service, sample_user): + letter_contact = create_letter_contact(sample_service, 'Edinburgh, ED1 1AA') + + data = { + 'name': 'Sample Template', + 'template_type': "letter", + 'content': "Template content", + 'service': sample_service, + 'created_by': sample_user, + } + template = Template(**data) + dao_create_template(template) + created = dao_get_all_templates_for_service(sample_service.id)[0] + assert created.reply_to is None + + created.reply_to = letter_contact.id + dao_update_template(created) + assert dao_get_all_templates_for_service(sample_service.id)[0].reply_to == letter_contact.id + + template_history = TemplateHistory.query.filter_by(id=created.id, version=2).one() + assert template_history.service_letter_contact_id + + def test_redact_template(sample_template): redacted = TemplateRedacted.query.one() assert redacted.template_id == sample_template.id diff --git a/tests/app/template/test_rest.py b/tests/app/template/test_rest.py index 2c0f8ea96..da72c3dff 100644 --- a/tests/app/template/test_rest.py +++ b/tests/app/template/test_rest.py @@ -17,9 +17,7 @@ from tests.app.conftest import ( sample_template_without_letter_permission, sample_template_without_sms_permission, ) -from tests.app.db import create_service - -from app.dao.templates_dao import dao_get_template_by_id +from tests.app.db import create_service, create_letter_contact @pytest.mark.parametrize('template_type, subject', [ @@ -618,6 +616,39 @@ def test_update_set_process_type_on_template(client, sample_template): assert template.process_type == 'priority' +def test_get_template_reply_to(client, sample_letter_template): + auth_header = create_authorization_header() + letter_contact = create_letter_contact(sample_letter_template.service, "Edinburgh, ED1 1AA") + sample_letter_template.reply_to = str(letter_contact.id) + + resp = client.get('/service/{}/template/{}'.format(sample_letter_template.service_id, sample_letter_template.id), + headers=[auth_header]) + + assert resp.status_code == 200, resp.get_data(as_text=True) + json_resp = json.loads(resp.get_data(as_text=True)) + + assert 'service_letter_contact_id' not in json_resp['data'] + assert json_resp['data']['reply_to'] == str(letter_contact.id) + + +def test_update_template_reply_to(client, sample_letter_template): + auth_header = create_authorization_header() + letter_contact = create_letter_contact(sample_letter_template.service, "Edinburgh, ED1 1AA") + + data = { + 'reply_to': str(letter_contact.id), + } + + resp = client.post('/service/{}/template/{}'.format(sample_letter_template.service_id, sample_letter_template.id), + data=json.dumps(data), + headers=[('Content-Type', 'application/json'), auth_header]) + + assert resp.status_code == 200, resp.get_data(as_text=True) + + template = dao_get_template_by_id(sample_letter_template.id) + assert template.reply_to == letter_contact.id + + def test_update_redact_template(admin_request, sample_template): assert sample_template.redact_personalisation is False diff --git a/tests/app/template/test_rest_history.py b/tests/app/template/test_rest_history.py index 482b04aad..c26c4abc8 100644 --- a/tests/app/template/test_rest_history.py +++ b/tests/app/template/test_rest_history.py @@ -3,6 +3,7 @@ from datetime import (datetime, date) from flask import url_for from app.dao.templates_dao import dao_update_template from tests import create_authorization_header +from tests.app.db import create_letter_contact def test_template_history_version(notify_api, sample_user, sample_template): @@ -93,3 +94,21 @@ def test_all_versions_of_template(notify_api, sample_template): assert json_resp['data'][1]['content'] == newer_content assert json_resp['data'][1]['updated_at'] assert json_resp['data'][2]['content'] == old_content + + +def test_update_template_reply_to_updates_history(client, sample_letter_template): + auth_header = create_authorization_header() + letter_contact = create_letter_contact(sample_letter_template.service, "Edinburgh, ED1 1AA") + + sample_letter_template.reply_to = letter_contact.id + dao_update_template(sample_letter_template) + + resp = client.get( + '/service/{}/template/{}/version/2'.format(sample_letter_template.service_id, sample_letter_template.id), + headers=[auth_header] + ) + assert resp.status_code == 200 + + hist_json_resp = json.loads(resp.get_data(as_text=True)) + assert 'service_letter_contact_id' not in hist_json_resp['data'] + assert hist_json_resp['data']['reply_to'] == str(letter_contact.id)