From 761bb3e1cbff3bbb379f5ad09a263ba7ac48bcf5 Mon Sep 17 00:00:00 2001 From: Areeb Jamal Date: Mon, 10 Aug 2020 00:00:03 +0530 Subject: [PATCH] feat: Add session mails and notify endpoint (#7198) --- app/api/helpers/mail.py | 7 ++++- app/api/helpers/permission_manager.py | 14 +++++---- app/api/routes.py | 1 - app/api/schema/sessions.py | 15 +++++++++- app/api/sessions.py | 34 ++++++++++++++++++---- app/models/helpers/versioning.py | 6 +++- docs/api/blueprint/session/sessions.apib | 36 ++++++++++++++++++++++++ 7 files changed, 98 insertions(+), 15 deletions(-) diff --git a/app/api/helpers/mail.py b/app/api/helpers/mail.py index dab4a7bec8..a684f3b37c 100644 --- a/app/api/helpers/mail.py +++ b/app/api/helpers/mail.py @@ -1,6 +1,7 @@ import base64 import logging from datetime import datetime +from typing import Dict from flask import current_app @@ -171,7 +172,7 @@ def send_email_new_session(email, event_name, link): ) -def send_email_session_state_change(email, session): +def send_email_session_state_change(email, session, mail_override: Dict[str, str] = None): """email for new session""" event = session.event @@ -195,6 +196,10 @@ def send_email_session_state_change(email, session): try: mail = MAILS[SESSION_STATE_CHANGE][session.state] + if mail_override: + mail = mail.copy() + mail['subject'] = mail_override.get('subject') or mail['subject'] + mail['message'] = mail_override.get('message') or mail['message'] except KeyError: logger.error('No mail found for session state change: ' + session.state) return diff --git a/app/api/helpers/permission_manager.py b/app/api/helpers/permission_manager.py index bea5bc55cc..17548e09f3 100644 --- a/app/api/helpers/permission_manager.py +++ b/app/api/helpers/permission_manager.py @@ -163,17 +163,19 @@ def is_speaker_for_session(view, view_args, view_kwargs, *args, **kwargs): Allows admin and super admin access to any resource irrespective of id. Otherwise the user can only access his/her resource. """ + not_found = NotFoundError({'parameter': 'id'}, 'Session not found.') + try: + session = Session.query.filter(Session.id == view_kwargs['id']).one() + except NoResultFound: + raise not_found + user = current_user - if user.is_admin or user.is_super_admin: - return view(*view_args, **view_kwargs) if user.is_staff: return view(*view_args, **view_kwargs) - try: - session = Session.query.filter(Session.id == view_kwargs['id']).one() - except NoResultFound: - raise NotFoundError({'parameter': 'id'}, 'Session not found.') + if session.deleted_at is not None: + raise not_found if user.has_event_access(session.event_id): return view(*view_args, **view_kwargs) diff --git a/app/api/routes.py b/app/api/routes.py index bd28dcd29b..748c5cef0e 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -165,7 +165,6 @@ SessionListPost, SessionRelationshipOptional, SessionRelationshipRequired, - get_session_states, ) from app.api.settings import SettingDetail from app.api.social_links import ( diff --git a/app/api/schema/sessions.py b/app/api/schema/sessions.py index 703d116b56..fbc0e4dc24 100644 --- a/app/api/schema/sessions.py +++ b/app/api/schema/sessions.py @@ -1,7 +1,7 @@ from datetime import datetime from flask_rest_jsonapi.exceptions import ObjectNotFound -from marshmallow import validate, validates_schema +from marshmallow import Schema, validate, validates_schema from marshmallow_jsonapi import fields from marshmallow_jsonapi.flask import Relationship from sqlalchemy.orm.exc import NoResultFound @@ -12,6 +12,7 @@ from app.api.helpers.utilities import dasherize from app.api.helpers.validations import validate_complex_fields_json from app.api.schema.base import SoftDeletionSchema +from app.models.helpers.versioning import clean_html from app.models.session import Session from utils.common import use_defaults @@ -176,3 +177,15 @@ def validate_fields(self, data, original_data): schema='UserSchemaPublic', type_='user', ) + + +# Used for customization of email notification subject and message body +class SessionNotifySchema(Schema): + subject = fields.Str(required=False, validate=validate.Length(max=250)) + message = fields.Str(required=False, validate=validate.Length(max=5000)) + + @validates_schema + def validate_fields(self, data): + if not data: + return + data['message'] = clean_html(data.get('message'), allow_link=True) diff --git a/app/api/sessions.py b/app/api/sessions.py index 10247bb364..640b0f45ad 100644 --- a/app/api/sessions.py +++ b/app/api/sessions.py @@ -1,3 +1,5 @@ +from typing import Dict + from flask import Blueprint, g, jsonify, request from flask_jwt_extended import current_user from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship @@ -7,7 +9,7 @@ from app.api.events import Event from app.api.helpers.custom_forms import validate_custom_form_constraints_request from app.api.helpers.db import get_count, safe_query, safe_query_kwargs, save_to_db -from app.api.helpers.errors import ForbiddenError +from app.api.helpers.errors import ForbiddenError, UnprocessableEntityError from app.api.helpers.files import make_frontend_url from app.api.helpers.mail import send_email_new_session, send_email_session_state_change from app.api.helpers.notification import ( @@ -17,8 +19,9 @@ from app.api.helpers.permission_manager import has_access from app.api.helpers.query import event_query from app.api.helpers.speaker import can_edit_after_cfs_ends +from app.api.helpers.system_mails import MAILS, SESSION_STATE_CHANGE from app.api.helpers.utilities import require_relationship -from app.api.schema.sessions import SessionSchema +from app.api.schema.sessions import SessionNotifySchema, SessionSchema from app.models import db from app.models.microlocation import Microlocation from app.models.session import Session @@ -221,6 +224,11 @@ def get_session_states(): return jsonify(SESSION_STATE_DICT) +@sessions_blueprint.route('/mails') +def get_session_state_change_mails(): + return jsonify(MAILS[SESSION_STATE_CHANGE]) + + class SessionDetail(ResourceDetail): """ Session detail by id @@ -338,7 +346,7 @@ def after_update_object(self, session, data, view_kwargs): } -def notify_for_session(session): +def notify_for_session(session, mail_override: Dict[str, str] = None): event = session.event frontend_url = get_settings()['frontend_url'] link = "{}/events/{}/sessions/{}".format(frontend_url, event.identifier, session.id) @@ -346,7 +354,7 @@ def notify_for_session(session): speakers = session.speakers for speaker in speakers: if not speaker.is_email_overridden: - send_email_session_state_change(speaker.email, session) + send_email_session_state_change(speaker.email, session, mail_override) send_notif_session_state_change( speaker.user, session.title, session.state, link, session.id ) @@ -354,12 +362,28 @@ def notify_for_session(session): # Email for owner if session.event.get_owner(): owner = session.event.get_owner() - send_email_session_state_change(owner.email, session) + send_email_session_state_change(owner.email, session, mail_override) send_notif_session_state_change( owner, session.title, session.state, link, session.id ) +@sessions_blueprint.route('//notify', methods=['POST']) +@api.has_permission('is_speaker_for_session', methods="POST") +def notify_session(id): + session = Session.query.filter_by(deleted_at=None, id=id).first_or_404() + + data, errors = SessionNotifySchema().load(request.json) + if errors: + raise UnprocessableEntityError( + {'pointer': '/data', 'errors': errors}, 'Data in incorrect format' + ) + + notify_for_session(session, data) + + return jsonify({'success': True}) + + class SessionRelationshipRequired(ResourceRelationship): """ Session Relationship diff --git a/app/models/helpers/versioning.py b/app/models/helpers/versioning.py index 95eb1d69be..65a49c6a94 100644 --- a/app/models/helpers/versioning.py +++ b/app/models/helpers/versioning.py @@ -21,7 +21,7 @@ def clean_up_string(target_string): return target_string -def clean_html(html): +def clean_html(html, allow_link=False): if html is None: return None tags = [ @@ -39,8 +39,12 @@ def clean_html(html): 'ol', 'li', 'strike', + 'br', ] attrs = {'*': ['style']} + if allow_link: + tags.append('a') + attrs['a'] = ['href'] styles = ['text-align', 'font-weight', 'text-decoration'] cleaned = bleach.clean(html, tags=tags, attributes=attrs, styles=styles, strip=True) return bleach.linkify( diff --git a/docs/api/blueprint/session/sessions.apib b/docs/api/blueprint/session/sessions.apib index 5ad903e270..b8667f64e1 100644 --- a/docs/api/blueprint/session/sessions.apib +++ b/docs/api/blueprint/session/sessions.apib @@ -920,3 +920,39 @@ Get possible session state transitions. **(Public)** "withdrawn": {} } } + + +## Sessions State Change Mails [/v1/sessions/mails] + +### Sessions State Change Mails [GET] +Get mail subject and message sent on changing session state. **(Public)** + ++ Response 200 (application/json) + + { + "accepted": { + "message": "Hello,

This is an automatic message from {app_name}.

Your session status for the submission {session_name} for {event_name} was changed to \"Accepted\". Congratulations!

Your proposal will be scheduled by the event organizers and review team. Please (re)confirm your participation with the organizers of the event, if required.

You can also check the status and details of your submission on the session page {session_link}. You need to be logged in to view it.

More details about the event are on the event page at {event_link}.

Thank you.
{app_name}", + "subject": "Accepted! Congratulations Your submission for {event_name} titled {session_name} has been Accepted" + }, + "canceled": { + "message": "Hello,

This is an automatic message from {app_name}.

Your session status for the submission {session_name} for {event_name} was changed to \"Canceled\".

The status change was done by event organizers. If there are questions about this change please contact the organizers.

You can also check the status and details of your submission on the session page {session_link}. You need to be logged in to view it.

More details about the event are on the event page at {event_link}.

Thank you.
{app_name}", + "subject": "Canceled! Your submission for {event_name} titled {session_name} has been Canceled" + }, + "confirmed": { + "message": "Hello,

This is an automatic message from {app_name}.

Your session status for the submission {session_name} for {event_name} was changed to \"Confirmed\". Congratulations!

Your proposal will be scheduled by the event organizers and review team. Please inform the event organizers in case there are any changes to your participation.

You can also check the status and details of your submission on the session page {session_link}. You need to be logged in to view it.

More details about the event are on the event page at {event_link}.

Thank you.
{app_name}", + "subject": "Confirmed! Congratulations Your submission for {event_name} titled {session_name} has been Confirmed" + }, + "pending": { + "message": "Hello,

This is an automatic message from {app_name}.

We have received your submission {session_name} for {event_name}

Your proposal will be reviewed by the event organizers and review team. The current status of your session is now \"Pending\".

You can also check the status and details of your submission on the session page {session_link}. You need to be logged in to view it.

More details about the event are on the event page at {event_link}.

Thank you.
{app_name}", + "subject": "Your speaker submission for {event_name} titled {session_name}" + }, + "recipient": "Speaker", + "rejected": { + "message": "Hello,

This is an automatic message from {app_name}.

Unfortunately your submission {session_name} for {event_name} was not accepted. Your session status was changed to \"Rejected\".

The status change was done by event organizers. If there are questions about this change please contact the organizers.

You can also check the status and details of your submission on the session page {session_link}. You need to be logged in to view it.

More details about the event are on the event page at {event_link}.

Thank you.
{app_name}", + "subject": "Not Accepted. Your submission for {event_name} titled {session_name} was not accepted" + }, + "withdrawn": { + "message": "Hello,

This is an automatic message from {app_name}.

Your session status for the submission {session_name} for {event_name} was changed to \"Withdrawn\".

The status change was done by event organizers. If there are questions about this change please contact the organizers.

You can also check the status and details of your submission on the session page {session_link}. You need to be logged in to view it.

More details about the event are on the event page at {event_link}.

Thank you.
{app_name}", + "subject": "Withdrawn! Your submission for {event_name} titled {session_name} has been Withdrawn" + } + }