Skip to content

Commit

Permalink
Add email_templates module (ethyca#1123)
Browse files Browse the repository at this point in the history
* Add email_templates module

* run isort

* Add unit tests

* Update ttl calculation

* Add ttl minutes test

* fix lint issues

* fix pylint issue

* fix pylint issue

* fix isort

* Update template constant

* Update changelog

* fix lints

* Add jinja to requirements.txt

* update templates directory

* update unit test

* Update imports

* fix issue template path

* Add templates to manifest

Co-authored-by: Paul Sanders <[email protected]>
  • Loading branch information
TheAndrewJackson and Paul Sanders authored Aug 23, 2022
1 parent 5e937ad commit b0eee6d
Show file tree
Hide file tree
Showing 14 changed files with 112 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ The types of changes are:
* SaaS Connector Configuration - Testing a Connection [#985](https://github.com/ethyca/fidesops/pull/1099)
* Add an endpoint for verifying the user's identity before queuing the privacy request. [#1111](https://github.com/ethyca/fidesops/pull/1111)
* Adds tests for email endpoints and service [#1112](https://github.com/ethyca/fidesops/pull/1112)
* Added email templates [#1123](https://github.com/ethyca/fidesops/pull/1123)
* Add Retry button back into the subject request detail view [#1128](https://github.com/ethyca/fidesops/pull/1131)

### Developer Experience
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ include versioneer.py
include src/fidesops/ops/alembic.ini
include src/fidesops/_version.py
include src/fidesops/py.typed
graft src/fidesops/ops/email_templates/templates
graft src/fidesops/ops/migrations
exclude src/fidesops/ops/migrations/README
exclude src/fidesops/ops/migrations/script.py.mako
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ fideslang==1.2.0
fideslib==3.0.3
fideslog==1.2.3
hvac==0.11.2
Jinja2==3.1.2
multidimensional_urlencode==0.0.4
pandas==1.4.3
passlib[bcrypt]==1.7.4
Expand Down
4 changes: 4 additions & 0 deletions src/fidesops/ops/common_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ class EmailDispatchException(FidesopsException):
"""Custom Exception - Email Dispatch Error"""


class EmailTemplateUnhandledActionType(FidesopsException):
"""Custom Exception - Email Template Unhandled ActionType Error"""


class OAuth2TokenException(FidesopsException):
"""Custom Exception - Unable to access or refresh OAuth2 tokens for SaaS connector"""

Expand Down
1 change: 1 addition & 0 deletions src/fidesops/ops/email_templates/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .get_email_template import get_email_template
29 changes: 29 additions & 0 deletions src/fidesops/ops/email_templates/get_email_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import logging
import pathlib

from jinja2 import Environment, FileSystemLoader, Template, select_autoescape

from fidesops.ops.common_exceptions import EmailTemplateUnhandledActionType
from fidesops.ops.email_templates.template_names import (
SUBJECT_IDENTITY_VERIFICATION_TEMPLATE,
)
from fidesops.ops.schemas.email.email import EmailActionType

pathlib.Path(__file__).parent.resolve()
logger = logging.getLogger(__name__)

abs_path_to_current_file_dir = pathlib.Path(__file__).parent.resolve()
template_env = Environment(
loader=FileSystemLoader(f"{abs_path_to_current_file_dir}/templates"),
autoescape=select_autoescape(),
)


def get_email_template(action_type: EmailActionType) -> Template:
if action_type == EmailActionType.SUBJECT_IDENTITY_VERIFICATION:
return template_env.get_template(SUBJECT_IDENTITY_VERIFICATION_TEMPLATE)

logger.error(f"No corresponding template linked to the {action_type}")
raise EmailTemplateUnhandledActionType(
f"No corresponding template linked to the {action_type}"
)
1 change: 1 addition & 0 deletions src/fidesops/ops/email_templates/template_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SUBJECT_IDENTITY_VERIFICATION_TEMPLATE = "subject_identity_verification.html"
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ID Code</title>
</head>
<body>
<main>
<p>
Your privacy request verification code is {{code}}.
Please return to the Privacy Center and enter the code to
continue. This code will expire in {{minutes}} minutes
</p>
</main>
</body>
</html>
11 changes: 9 additions & 2 deletions src/fidesops/ops/schemas/email/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,20 @@ class EmailActionType(Enum):
class EmailTemplateBodyParams(Enum):
"""Enum for all possible email template body params"""

ACCESS_CODE = "access_code"
VERIFICATION_CODE = "verification_code"


class SubjectIdentityVerificationBodyParams(BaseModel):
"""Body params required for subject identity verification email template"""

access_code: str
verification_code: str
verification_code_ttl_seconds: int

def get_verification_code_ttl_minutes(self) -> int:
"""returns verification_code_ttl_seconds in minutes"""
if self.verification_code_ttl_seconds < 60:
return 0
return self.verification_code_ttl_seconds // 60


class EmailForActionType(BaseModel):
Expand Down
10 changes: 8 additions & 2 deletions src/fidesops/ops/service/email/email_dispatch_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from sqlalchemy.orm import Session

from fidesops.ops.common_exceptions import EmailDispatchException
from fidesops.ops.email_templates import get_email_template
from fidesops.ops.models.email import EmailConfig
from fidesops.ops.schemas.email.email import (
EmailActionType,
Expand Down Expand Up @@ -54,10 +55,15 @@ def _build_email(
body_params: Union[SubjectIdentityVerificationBodyParams],
) -> EmailForActionType:
if action_type == EmailActionType.SUBJECT_IDENTITY_VERIFICATION:
template = get_email_template(action_type)
return EmailForActionType(
subject="Your one-time code",
# for 1st iteration, below will be replaced with actual template files
body=f"<html>Your one-time code is {body_params.access_code}. Hurry! It expires in 10 minutes.</html>",
body=template.render(
{
"code": body_params.verification_code,
"minutes": body_params.get_verification_code_ttl_minutes(),
}
),
)
logger.error(f"Email action type {action_type} is not implemented")
raise EmailDispatchException(f"Email action type {action_type} is not implemented")
Expand Down
Empty file.
18 changes: 18 additions & 0 deletions tests/ops/email_templates/test_get_email_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import pytest
from jinja2 import Template

from fidesops.ops.common_exceptions import EmailTemplateUnhandledActionType
from fidesops.ops.email_templates import get_email_template
from fidesops.ops.schemas.email.email import EmailActionType


def test_get_email_template_returns_template():
result = get_email_template(EmailActionType.SUBJECT_IDENTITY_VERIFICATION)
assert type(result) == Template


def test_get_email_template_exception():
fake_template = "templateThatDoesNotExist"
with pytest.raises(EmailTemplateUnhandledActionType) as e:
get_email_template(fake_template)
assert e.value == f"No corresponding template linked to the {fake_template}"
11 changes: 11 additions & 0 deletions tests/ops/schemas/email/email_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import pytest

from fidesops.ops.schemas.email.email import SubjectIdentityVerificationBodyParams


@pytest.mark.parametrize("ttl, expected", [(600, 10), (155, 2), (33, 0)])
def test_get_verification_code_ttl_minutes_calc(ttl, expected):
result = SubjectIdentityVerificationBodyParams(
verification_code="123123", verification_code_ttl_seconds=ttl
)
assert result.get_verification_code_ttl_minutes() == expected
18 changes: 12 additions & 6 deletions tests/ops/service/email/email_dispatch_service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,16 @@ def test_email_dispatch_mailgun_success(
db=db,
action_type=EmailActionType.SUBJECT_IDENTITY_VERIFICATION,
to_email="[email protected]",
email_body_params=SubjectIdentityVerificationBodyParams(access_code="2348"),
email_body_params=SubjectIdentityVerificationBodyParams(
verification_code="2348", verification_code_ttl_seconds=600
),
)

body = '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <title>ID Code</title>\n</head>\n<body>\n<main>\n <p>\n Your privacy request verification code is 2348.\n Please return to the Privacy Center and enter the code to\n continue. This code will expire in 10 minutes\n </p>\n</main>\n</body>\n</html>'
mock_mailgun_dispatcher.assert_called_with(
email_config=email_config,
email=EmailForActionType(
subject="Your one-time code",
body=f"<html>Your one-time code is 2348. Hurry! It expires in 10 minutes.</html>",
body=body,
),
to_email="[email protected]",
)
Expand All @@ -51,7 +53,9 @@ def test_email_dispatch_mailgun_config_not_found(
db=db,
action_type=EmailActionType.SUBJECT_IDENTITY_VERIFICATION,
to_email="[email protected]",
email_body_params=SubjectIdentityVerificationBodyParams(access_code="2348"),
email_body_params=SubjectIdentityVerificationBodyParams(
verification_code="2348", verification_code_ttl_seconds=600
),
)
assert exc.value.args[0] == "No email config found."

Expand Down Expand Up @@ -80,7 +84,9 @@ def test_email_dispatch_mailgun_config_no_secrets(
db=db,
action_type=EmailActionType.SUBJECT_IDENTITY_VERIFICATION,
to_email="[email protected]",
email_body_params=SubjectIdentityVerificationBodyParams(access_code="2348"),
email_body_params=SubjectIdentityVerificationBodyParams(
verification_code="2348", verification_code_ttl_seconds=600
),
)
assert (
exc.value.args[0]
Expand Down Expand Up @@ -108,7 +114,7 @@ def test_email_dispatch_mailgun_failed_email(db: Session, email_config) -> None:
action_type=EmailActionType.SUBJECT_IDENTITY_VERIFICATION,
to_email="[email protected]",
email_body_params=SubjectIdentityVerificationBodyParams(
access_code="2348"
verification_code="2348", verification_code_ttl_seconds=600
),
)
assert (
Expand Down

0 comments on commit b0eee6d

Please sign in to comment.