Skip to content

Commit

Permalink
Changes for automated emails (#2138)
Browse files Browse the repository at this point in the history
* Changes to show all survey results to superusers

* removing hard coded values

* fixing linting

* splitting to seperate end points

* fixing auth check

* fixing linting

* merging method in service

* Handle no data error for graphs

* adding new nodata component

* adding new email for submission response

* fixing linting and testing

* Changes for automated emails

* fixing linting

* updating indent style
  • Loading branch information
VineetBala-AOT authored Sep 8, 2023
1 parent e217261 commit b17c8d0
Show file tree
Hide file tree
Showing 21 changed files with 376 additions and 122 deletions.
4 changes: 1 addition & 3 deletions met-api/src/met_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,7 @@ class _Config(): # pylint: disable=too-few-public-methods
'VERIFICATION_EMAIL_TEMPLATE_ID': os.getenv('VERIFICATION_EMAIL_TEMPLATE_ID'),
'VERIFICATION_EMAIL_SUBJECT': os.getenv('VERIFICATION_EMAIL_SUBJECT', '{engagement_name} - Survey link'),
'SUBSCRIBE_EMAIL_TEMPLATE_ID': os.getenv('SUBSCRIBE_EMAIL_TEMPLATE_ID'),
'SUBSCRIBE_EMAIL_SUBJECT': os.getenv(
'SUBSCRIBE_EMAIL_SUBJECT',
'Confirm your Subscription to {engagement_name}'),
'SUBSCRIBE_EMAIL_SUBJECT': os.getenv('SUBSCRIBE_EMAIL_SUBJECT', 'Confirm your subscription'),
'REJECTED_EMAIL_TEMPLATE_ID': os.getenv('REJECTED_EMAIL_TEMPLATE_ID'),
'REJECTED_EMAIL_SUBJECT': os.getenv('REJECTED_EMAIL_SUBJECT', '{engagement_name} - About your Comments'),
'SUBMISSION_RESPONSE_EMAIL_TEMPLATE_ID': os.getenv('SUBMISSION_RESPONSE_EMAIL_TEMPLATE_ID'),
Expand Down
10 changes: 9 additions & 1 deletion met-api/src/met_api/constants/subscription_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Constants for subscription."""
from enum import IntEnum
from enum import Enum, IntEnum


class SubscriptionType(IntEnum):
Expand All @@ -28,3 +28,11 @@ class SubscriptionType(IntEnum):
ENGAGEMENT = 1
PROJECT = 2
TENANT = 3


class SubscriptionTypes(Enum):
"""Enum of Subscription Type."""

ENGAGEMENT = 'ENGAGEMENT'
PROJECT = 'PROJECT'
TENANT = 'TENANT'
18 changes: 17 additions & 1 deletion met-api/src/met_api/models/engagement.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from __future__ import annotations

from datetime import datetime
from datetime import datetime, timedelta
from typing import List, Optional

from sqlalchemy import and_, asc, desc, or_
Expand Down Expand Up @@ -314,3 +314,19 @@ def get_assigned_engagements(user_id: int) -> List[Engagement]:
)) \
.all()
return engagements

@classmethod
def get_engagements_closing_soon(cls) -> List[Engagement]:
"""Get engagements that are closing within two days."""
now = local_datetime()
two_days_from_now = now + timedelta(days=2)
# Strip the time off the datetime object
date_due = datetime(two_days_from_now.year, two_days_from_now.month, two_days_from_now.day)
engagements = db.session.query(Engagement) \
.filter(
and_(
Engagement.status_id == Status.Published.value,
Engagement.end_date == date_due
)) \
.all()
return engagements
21 changes: 21 additions & 0 deletions met-api/src/met_api/resources/email_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,24 @@ def post():
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR
except ValueError as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR


@cors_preflight('POST, OPTIONS')
@API.route('/<string:subscription_type>/subscribe')
class SubscribeEmailVerifications(Resource):
"""Resource for managing email verifications."""

@staticmethod
# @TRACER.trace()
@cross_origin(origins=allowedorigins())
def post(subscription_type):
"""Create a new email verification."""
try:
requestjson = request.get_json()
email_verification = EmailVerificationSchema().load(requestjson)
created_email_verification = EmailVerificationService().create(email_verification, subscription_type)
return created_email_verification, HTTPStatus.OK
except KeyError as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR
except ValueError as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR
67 changes: 46 additions & 21 deletions met-api/src/met_api/services/email_verification_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
from flask import current_app
from met_api.constants.email_verification import INTERNAL_EMAIL_DOMAIN, EmailVerificationType

from met_api.constants.subscription_type import SubscriptionTypes
from met_api.exceptions.business_exception import BusinessException
from met_api.models import Engagement as EngagementModel
from met_api.models import EngagementSlug as EngagementSlugModel
from met_api.models import Survey as SurveyModel
from met_api.models import Tenant as TenantModel
from met_api.models.email_verification import EmailVerification
from met_api.models.engagement_metadata import EngagementMetadataModel
from met_api.schemas.email_verification import EmailVerificationSchema
from met_api.services.participant_service import ParticipantService
from met_api.utils import notification
Expand Down Expand Up @@ -42,7 +44,8 @@ def get_active(cls, verification_token):
return email_verification

@classmethod
def create(cls, email_verification: EmailVerificationSchema, session=None) -> EmailVerificationSchema:
def create(cls, email_verification: EmailVerificationSchema,
subscription_type='', session=None) -> EmailVerificationSchema:
"""Create an email verification."""
cls.validate_fields(email_verification)
email_address: str = email_verification.get('email_address')
Expand All @@ -66,7 +69,7 @@ def create(cls, email_verification: EmailVerificationSchema, session=None) -> Em

# TODO: remove this once email logic is brought over from submission service to here
if email_verification.get('type', None) != EmailVerificationType.RejectedComment:
cls._send_verification_email(email_verification)
cls._send_verification_email(email_verification, subscription_type)

return email_verification

Expand All @@ -93,11 +96,10 @@ def verify(cls, verification_token, survey_id, submission_id, session):
return email_verification

@staticmethod
def _send_verification_email(email_verification: dict) -> None:
def _send_verification_email(email_verification: dict, subscription_type) -> None:
"""Send an verification email.Throws error if fails."""
survey_id = email_verification.get('survey_id')
email_to = email_verification.get('email_address')
participant_id = email_verification.get('participant_id')
survey: SurveyModel = SurveyModel.get_open(survey_id)

if not survey:
Expand All @@ -106,7 +108,11 @@ def _send_verification_email(email_verification: dict) -> None:
raise ValueError('Engagement not found')

subject, body, args, template_id = EmailVerificationService._render_email_template(
survey, email_verification.get('verification_token'), email_verification.get('type'), participant_id)
survey,
email_verification.get('verification_token'),
email_verification.get('type'),
subscription_type
)
try:
# user hasn't been created yet.so create token using SA.
notification.send_email(
Expand All @@ -119,49 +125,52 @@ def _send_verification_email(email_verification: dict) -> None:
status_code=HTTPStatus.INTERNAL_SERVER_ERROR) from exc

@staticmethod
def _render_email_template(survey: SurveyModel, token, email_type: EmailVerificationType, participant_id):
def _render_email_template(survey: SurveyModel,
token,
email_type: EmailVerificationType,
subscription_type):
if email_type == EmailVerificationType.Subscribe:
return EmailVerificationService._render_subscribe_email_template(survey, token, participant_id)
return EmailVerificationService._render_subscribe_email_template(survey, token, subscription_type)
# if email_type == EmailVerificationType.RejectedComment:
# TODO: move reject comment email verification logic here
# return
return EmailVerificationService._render_survey_email_template(survey, token)

@staticmethod
# pylint: disable-msg=too-many-locals
def _render_subscribe_email_template(survey: SurveyModel, token, participant_id):
def _render_subscribe_email_template(survey: SurveyModel, token, subscription_type):
# url is origin url excluding context path
engagement: EngagementModel = EngagementModel.find_by_id(
survey.engagement_id)
engagement_name = engagement.name
tenant_name = EmailVerificationService._get_tenant_name(
engagement.tenant_id)
project_name = EmailVerificationService._get_project_name(
subscription_type, tenant_name, engagement)
is_subscribing_to_tenant = subscription_type == SubscriptionTypes.TENANT.value
is_subscribing_to_project = subscription_type != SubscriptionTypes.TENANT.value
template_id = get_gc_notify_config('SUBSCRIBE_EMAIL_TEMPLATE_ID')
template = Template.get_template('subscribe_email.html')
subject_template = get_gc_notify_config('SUBSCRIBE_EMAIL_SUBJECT')
confirm_path = current_app.config.get('SUBSCRIBE_PATH'). \
format(engagement_id=engagement.id, token=token)
unsubscribe_path = current_app.config.get('UNSUBSCRIBE_PATH'). \
format(engagement_id=engagement.id, participant_id=participant_id)
confirm_url = notification.get_tenant_site_url(
engagement.tenant_id, confirm_path)
unsubscribe_url = notification.get_tenant_site_url(
engagement.tenant_id, unsubscribe_path)
email_environment = get_gc_notify_config('EMAIL_ENVIRONMENT')
tenant_name = EmailVerificationService._get_tenant_name(
engagement.tenant_id)
args = {
'engagement_name': engagement_name,
'project_name': project_name,
'confirm_url': confirm_url,
'unsubscribe_url': unsubscribe_url,
'email_environment': email_environment,
'tenant_name': tenant_name,
'is_subscribing_to_tenant': is_subscribing_to_tenant,
'is_subscribing_to_project': is_subscribing_to_project,
}
subject = subject_template.format(engagement_name=engagement_name)
subject = get_gc_notify_config('SUBSCRIBE_EMAIL_SUBJECT')
body = template.render(
engagement_name=args.get('engagement_name'),
project_name=args.get('project_name'),
confirm_url=args.get('confirm_url'),
unsubscribe_url=args.get('unsubscribe_url'),
email_environment=args.get('email_environment'),
tenant_name=args.get('tenant_name'),
is_subscribing_to_tenant=args.get('is_subscribing_to_tenant'),
is_subscribing_to_project=args.get('is_subscribing_to_project'),
)
return subject, body, args, template_id

Expand Down Expand Up @@ -216,6 +225,22 @@ def _get_tenant_name(tenant_id):
tenant = TenantModel.find_by_id(tenant_id)
return tenant.name

@staticmethod
def _get_project_name(subscription_type, tenant_name, engagement):
metadata_model: EngagementMetadataModel = EngagementMetadataModel.find_by_id(engagement.id)
if subscription_type == SubscriptionTypes.TENANT.value:
return tenant_name

if subscription_type == SubscriptionTypes.PROJECT.value:
metadata_model: EngagementMetadataModel = EngagementMetadataModel.find_by_id(engagement.id)
project_name = metadata_model.project_metadata.get('project_name', None)
return project_name or engagement.name

if subscription_type == SubscriptionTypes.ENGAGEMENT.value:
return engagement.name

return None

@staticmethod
def validate_email_verification(email_verification: EmailVerificationSchema):
"""Validate an email verification."""
Expand Down
15 changes: 15 additions & 0 deletions met-api/templates/engagement_closing_soon.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<h1>The public comment period for {{ project_name }} will close on {{ end_date }} at midnight, Pacific time.</h1>
<br />
<p>If you would like to provide your feedback, please do so before the date noted above.</p>
<br />
<p>If you have already provided feedback, thank you! You may ignore this email.</p>
<br />
<p><a href="{{ survey_url }}">Provide Your Feedback</a></p>
<br />
<p>Thank you,</p>
<br />
<p>The {{ tenant_name }} Team</p>
<br />
<p>If you would no longer like to receive updates from us, you may <a href='{{ unsubscribe_url }}'>unsubscribe</a>.</p>
<br />
<p>{{email_environment}}</p>
15 changes: 9 additions & 6 deletions met-api/templates/publish_engagement.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<h1>Thank you for subscribing to our feedback tool.</h1>
<h1>A new engagement is now open {% if is_having_project %} for {% endif %} {% if is_not_having_project %} on {% endif %} {{ project_name }}.</h1>
<br />
<p>This is to update you that a new engagement, {{ engagement_name }}, has been published.</p>
<p>To learn more and provide your feedback during this public comment period, please click the link below:</p>
<br />
<p>Please visit the<p>
<a href='{{ link }}'>link</a>
<p>to learn more and provide your feedback.<p>
<p><a href="{{ survey_url }}">Provide Your Feedback</a></p>
<br />
<p>The public comment period ends on {{ end_date }}.</p>
<br />
<p>Thank you,</p>
<br />
<p>The Team at the Environmental Assessment Office</p>
<p>The {{ tenant_name }} Team</p>
<br />
<p>If you would no longer like to receive updates from us, you may <a href='{{ unsubscribe_url }}'>unsubscribe</a>.</p>
<br />
<p>{{email_environment}}</p>
7 changes: 4 additions & 3 deletions met-api/templates/subscribe_email.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<h1>Thank you for subscribing to {{ engagement_name }}.</h1>
<p>Please click the link below to confirm your subscription.</p>
<h1>Thank you for signing up to receive updates {% if is_subscribing_to_tenant %} on {% endif %} {% if is_subscribing_to_project %} for {% endif %} {{ project_name }}.</h1>
<br />
<p>To receive updates, you must confirm your subscription by clicking the link below:</p>
<br />
<a href='{{ confirm_url }}'>Confirm Subscription</a>
<br />
<p>Once you confirm your email we will send you updates about related engagements.<p>
<p>If you have received this in error you may ignore this email.<p>
<br />
<p>Thank you,</p>
<br />
Expand Down
8 changes: 7 additions & 1 deletion met-cron/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,18 @@ class _Config(): # pylint: disable=too-few-public-methods
# needed for publish emails for met api
ENGAGEMENT_VIEW_PATH = os.getenv('ENGAGEMENT_VIEW_PATH', '/engagements/{engagement_id}/view')
ENGAGEMENT_VIEW_PATH_SLUG = os.getenv('ENGAGEMENT_VIEW_PATH_SLUG', '/{slug}')
UNSUBSCRIBE_PATH = os.getenv('UNSUBSCRIBE_PATH', '/engagements/{engagement_id}/unsubscribe/{participant_id}')

# The GC notify email variables
# Publish Email Service
EMAIL_SECRET_KEY = os.getenv('EMAIL_SECRET_KEY', 'secret')
PUBLISH_ENGAGEMENT_EMAIL_TEMPLATE_ID = os.getenv('PUBLISH_ENGAGEMENT_EMAIL_TEMPLATE_ID')
PUBLISH_ENGAGEMENT_EMAIL_SUBJECT = os.getenv('PUBLISH_ENGAGEMENT_EMAIL_SUBJECT', 'New {engagement_name} published')
PUBLISH_ENGAGEMENT_EMAIL_SUBJECT = os.getenv('PUBLISH_ENGAGEMENT_EMAIL_SUBJECT', 'Share your feedback')

# Closing Soon Email Service
ENGAGEMENT_CLOSING_SOON_EMAIL_TEMPLATE_ID = os.getenv('ENGAGEMENT_CLOSING_SOON_EMAIL_TEMPLATE_ID')
ENGAGEMENT_CLOSING_SOON_EMAIL_SUBJECT = os.getenv('ENGAGEMENT_CLOSING_SOON_EMAIL_SUBJECT',
'Public comment period closes in 2 days')

# Email Service
ENGAGEMENT_CLOSEOUT_EMAIL_TEMPLATE_ID = os.getenv('ENGAGEMENT_CLOSEOUT_EMAIL_TEMPLATE_ID')
Expand Down
2 changes: 2 additions & 0 deletions met-cron/cron/crontab
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# ENGAGEMENT CLOSING SOON Runs At 16:00 on every day-of-week from Monday through Friday (UTC).
0 16 * * 1-5 default cd /met-cron && ./run_engagement_closing_soon.sh
# ANALYTICS ETL Runs At every hour (UTC).
0 * * * * default cd /met-cron && ./run_met_etl.sh
# ENGAGEMENT CLOSEOUT Runs At 17:00 on every day-of-week from Monday through Friday (UTC).
Expand Down
4 changes: 4 additions & 0 deletions met-cron/invoke_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def shell_context():


def run(job_name):
from tasks.closing_soon_mailer import EngagementClosingSoonMailer
from tasks.met_closeout import MetEngagementCloseout
from tasks.met_publish import MetEngagementPublish
from tasks.met_purge import MetPurge
Expand All @@ -83,6 +84,9 @@ def run(job_name):
elif job_name == 'PUBLISH_EMAIL':
SubscriptionMailer.do_email()
application.logger.info('<<<< Completed MET PUBLISH_EMAIL >>>>')
elif job_name == 'CLOSING_SOON_EMAIL':
EngagementClosingSoonMailer.do_email()
application.logger.info('<<<< Completed MET CLOSING_SOON_EMAIL >>>>')
else:
application.logger.debug('No valid args passed.Exiting job without running any ***************')

Expand Down
3 changes: 3 additions & 0 deletions met-cron/run_engagement_closing_soon.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#! /bin/sh
echo 'run invoke_jobs.py CLOSING_SOON_EMAIL'
python3 invoke_jobs.py CLOSING_SOON_EMAIL
26 changes: 26 additions & 0 deletions met-cron/src/met_cron/services/closing_soon_mail_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from flask import current_app
from met_api.models.engagement import Engagement as EngagementModel
from met_api.utils.template import Template
from met_cron.services.mail_service import EmailService


class ClosingSoonEmailService: # pylint: disable=too-few-public-methods
"""Mail for newly published engagements"""

@staticmethod
def do_mail():
"""Send mail by listening to the email_queue.
1. Get N number of unprocessed recoreds from the email_queue table
2. Process each mail and send it to subscribed users
"""
engagements_closing_soon = EngagementModel.get_engagements_closing_soon()
template_id = current_app.config.get('ENGAGEMENT_CLOSING_SOON_EMAIL_TEMPLATE_ID', None)
subject = current_app.config.get('ENGAGEMENT_CLOSING_SOON_EMAIL_SUBJECT')
template = Template.get_template('engagement_closing_soon.html')
for engagement in engagements_closing_soon:
# Process each mails.First set status as PROCESSING

EmailService._send_email_notification_for_subscription(engagement.id, template_id,
subject, template)
Loading

0 comments on commit b17c8d0

Please sign in to comment.