Skip to content

Commit 761bb3e

Browse files
feat: Add session mails and notify endpoint (#7198)
1 parent f609453 commit 761bb3e

File tree

7 files changed

+98
-15
lines changed

7 files changed

+98
-15
lines changed

app/api/helpers/mail.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import base64
22
import logging
33
from datetime import datetime
4+
from typing import Dict
45

56
from flask import current_app
67

@@ -171,7 +172,7 @@ def send_email_new_session(email, event_name, link):
171172
)
172173

173174

174-
def send_email_session_state_change(email, session):
175+
def send_email_session_state_change(email, session, mail_override: Dict[str, str] = None):
175176
"""email for new session"""
176177
event = session.event
177178

@@ -195,6 +196,10 @@ def send_email_session_state_change(email, session):
195196

196197
try:
197198
mail = MAILS[SESSION_STATE_CHANGE][session.state]
199+
if mail_override:
200+
mail = mail.copy()
201+
mail['subject'] = mail_override.get('subject') or mail['subject']
202+
mail['message'] = mail_override.get('message') or mail['message']
198203
except KeyError:
199204
logger.error('No mail found for session state change: ' + session.state)
200205
return

app/api/helpers/permission_manager.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,17 +163,19 @@ def is_speaker_for_session(view, view_args, view_kwargs, *args, **kwargs):
163163
Allows admin and super admin access to any resource irrespective of id.
164164
Otherwise the user can only access his/her resource.
165165
"""
166+
not_found = NotFoundError({'parameter': 'id'}, 'Session not found.')
167+
try:
168+
session = Session.query.filter(Session.id == view_kwargs['id']).one()
169+
except NoResultFound:
170+
raise not_found
171+
166172
user = current_user
167-
if user.is_admin or user.is_super_admin:
168-
return view(*view_args, **view_kwargs)
169173

170174
if user.is_staff:
171175
return view(*view_args, **view_kwargs)
172176

173-
try:
174-
session = Session.query.filter(Session.id == view_kwargs['id']).one()
175-
except NoResultFound:
176-
raise NotFoundError({'parameter': 'id'}, 'Session not found.')
177+
if session.deleted_at is not None:
178+
raise not_found
177179

178180
if user.has_event_access(session.event_id):
179181
return view(*view_args, **view_kwargs)

app/api/routes.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,6 @@
165165
SessionListPost,
166166
SessionRelationshipOptional,
167167
SessionRelationshipRequired,
168-
get_session_states,
169168
)
170169
from app.api.settings import SettingDetail
171170
from app.api.social_links import (

app/api/schema/sessions.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import datetime
22

33
from flask_rest_jsonapi.exceptions import ObjectNotFound
4-
from marshmallow import validate, validates_schema
4+
from marshmallow import Schema, validate, validates_schema
55
from marshmallow_jsonapi import fields
66
from marshmallow_jsonapi.flask import Relationship
77
from sqlalchemy.orm.exc import NoResultFound
@@ -12,6 +12,7 @@
1212
from app.api.helpers.utilities import dasherize
1313
from app.api.helpers.validations import validate_complex_fields_json
1414
from app.api.schema.base import SoftDeletionSchema
15+
from app.models.helpers.versioning import clean_html
1516
from app.models.session import Session
1617
from utils.common import use_defaults
1718

@@ -176,3 +177,15 @@ def validate_fields(self, data, original_data):
176177
schema='UserSchemaPublic',
177178
type_='user',
178179
)
180+
181+
182+
# Used for customization of email notification subject and message body
183+
class SessionNotifySchema(Schema):
184+
subject = fields.Str(required=False, validate=validate.Length(max=250))
185+
message = fields.Str(required=False, validate=validate.Length(max=5000))
186+
187+
@validates_schema
188+
def validate_fields(self, data):
189+
if not data:
190+
return
191+
data['message'] = clean_html(data.get('message'), allow_link=True)

app/api/sessions.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Dict
2+
13
from flask import Blueprint, g, jsonify, request
24
from flask_jwt_extended import current_user
35
from flask_rest_jsonapi import ResourceDetail, ResourceList, ResourceRelationship
@@ -7,7 +9,7 @@
79
from app.api.events import Event
810
from app.api.helpers.custom_forms import validate_custom_form_constraints_request
911
from app.api.helpers.db import get_count, safe_query, safe_query_kwargs, save_to_db
10-
from app.api.helpers.errors import ForbiddenError
12+
from app.api.helpers.errors import ForbiddenError, UnprocessableEntityError
1113
from app.api.helpers.files import make_frontend_url
1214
from app.api.helpers.mail import send_email_new_session, send_email_session_state_change
1315
from app.api.helpers.notification import (
@@ -17,8 +19,9 @@
1719
from app.api.helpers.permission_manager import has_access
1820
from app.api.helpers.query import event_query
1921
from app.api.helpers.speaker import can_edit_after_cfs_ends
22+
from app.api.helpers.system_mails import MAILS, SESSION_STATE_CHANGE
2023
from app.api.helpers.utilities import require_relationship
21-
from app.api.schema.sessions import SessionSchema
24+
from app.api.schema.sessions import SessionNotifySchema, SessionSchema
2225
from app.models import db
2326
from app.models.microlocation import Microlocation
2427
from app.models.session import Session
@@ -221,6 +224,11 @@ def get_session_states():
221224
return jsonify(SESSION_STATE_DICT)
222225

223226

227+
@sessions_blueprint.route('/mails')
228+
def get_session_state_change_mails():
229+
return jsonify(MAILS[SESSION_STATE_CHANGE])
230+
231+
224232
class SessionDetail(ResourceDetail):
225233
"""
226234
Session detail by id
@@ -338,28 +346,44 @@ def after_update_object(self, session, data, view_kwargs):
338346
}
339347

340348

341-
def notify_for_session(session):
349+
def notify_for_session(session, mail_override: Dict[str, str] = None):
342350
event = session.event
343351
frontend_url = get_settings()['frontend_url']
344352
link = "{}/events/{}/sessions/{}".format(frontend_url, event.identifier, session.id)
345353
# Email for speaker
346354
speakers = session.speakers
347355
for speaker in speakers:
348356
if not speaker.is_email_overridden:
349-
send_email_session_state_change(speaker.email, session)
357+
send_email_session_state_change(speaker.email, session, mail_override)
350358
send_notif_session_state_change(
351359
speaker.user, session.title, session.state, link, session.id
352360
)
353361

354362
# Email for owner
355363
if session.event.get_owner():
356364
owner = session.event.get_owner()
357-
send_email_session_state_change(owner.email, session)
365+
send_email_session_state_change(owner.email, session, mail_override)
358366
send_notif_session_state_change(
359367
owner, session.title, session.state, link, session.id
360368
)
361369

362370

371+
@sessions_blueprint.route('/<int:id>/notify', methods=['POST'])
372+
@api.has_permission('is_speaker_for_session', methods="POST")
373+
def notify_session(id):
374+
session = Session.query.filter_by(deleted_at=None, id=id).first_or_404()
375+
376+
data, errors = SessionNotifySchema().load(request.json)
377+
if errors:
378+
raise UnprocessableEntityError(
379+
{'pointer': '/data', 'errors': errors}, 'Data in incorrect format'
380+
)
381+
382+
notify_for_session(session, data)
383+
384+
return jsonify({'success': True})
385+
386+
363387
class SessionRelationshipRequired(ResourceRelationship):
364388
"""
365389
Session Relationship

app/models/helpers/versioning.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def clean_up_string(target_string):
2121
return target_string
2222

2323

24-
def clean_html(html):
24+
def clean_html(html, allow_link=False):
2525
if html is None:
2626
return None
2727
tags = [
@@ -39,8 +39,12 @@ def clean_html(html):
3939
'ol',
4040
'li',
4141
'strike',
42+
'br',
4243
]
4344
attrs = {'*': ['style']}
45+
if allow_link:
46+
tags.append('a')
47+
attrs['a'] = ['href']
4448
styles = ['text-align', 'font-weight', 'text-decoration']
4549
cleaned = bleach.clean(html, tags=tags, attributes=attrs, styles=styles, strip=True)
4650
return bleach.linkify(

docs/api/blueprint/session/sessions.apib

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,3 +920,39 @@ Get possible session state transitions. **(Public)**
920920
"withdrawn": {}
921921
}
922922
}
923+
924+
925+
## Sessions State Change Mails [/v1/sessions/mails]
926+
927+
### Sessions State Change Mails [GET]
928+
Get mail subject and message sent on changing session state. **(Public)**
929+
930+
+ Response 200 (application/json)
931+
932+
{
933+
"accepted": {
934+
"message": "Hello,<br/><br/>This is an automatic message from {app_name}.<br/><br/>Your session status for the submission {session_name} for {event_name} was changed to \"Accepted\". Congratulations!<br/><br/>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.<br/><br/>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.<br/><br/>More details about the event are on the event page at {event_link}.<br/><br/>Thank you.<br/><a href='{frontend_link}'>{app_name}</a>",
935+
"subject": "Accepted! Congratulations Your submission for {event_name} titled {session_name} has been Accepted"
936+
},
937+
"canceled": {
938+
"message": "Hello,<br/><br/>This is an automatic message from {app_name}.<br/><br/>Your session status for the submission {session_name} for {event_name} was changed to \"Canceled\".<br/><br/>The status change was done by event organizers. If there are questions about this change please contact the organizers.<br/><br/>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.<br/><br/>More details about the event are on the event page at {event_link}.<br/><br/>Thank you.<br/><a href='{frontend_link}'>{app_name}</a>",
939+
"subject": "Canceled! Your submission for {event_name} titled {session_name} has been Canceled"
940+
},
941+
"confirmed": {
942+
"message": "Hello,<br/><br/>This is an automatic message from {app_name}.<br/><br/>Your session status for the submission {session_name} for {event_name} was changed to \"Confirmed\". Congratulations!<br/><br/>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.<br/><br/>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.<br/><br/>More details about the event are on the event page at {event_link}.<br/><br/>Thank you.<br/><a href='{frontend_link}'>{app_name}</a>",
943+
"subject": "Confirmed! Congratulations Your submission for {event_name} titled {session_name} has been Confirmed"
944+
},
945+
"pending": {
946+
"message": "Hello,<br/><br/>This is an automatic message from {app_name}.<br/><br/>We have received your submission {session_name} for {event_name}<br/><br/>Your proposal will be reviewed by the event organizers and review team. The current status of your session is now \"Pending\".<br/><br/>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.<br/><br/>More details about the event are on the event page at {event_link}.<br/><br/>Thank you.<br/><a href='{frontend_link}'>{app_name}</a>",
947+
"subject": "Your speaker submission for {event_name} titled {session_name}"
948+
},
949+
"recipient": "Speaker",
950+
"rejected": {
951+
"message": "Hello,<br/><br/>This is an automatic message from {app_name}.<br/><br/>Unfortunately your submission {session_name} for {event_name} was not accepted. Your session status was changed to \"Rejected\".<br/><br/>The status change was done by event organizers. If there are questions about this change please contact the organizers.<br/><br/>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.<br/><br/>More details about the event are on the event page at {event_link}.<br/><br/>Thank you.<br/><a href='{frontend_link}'>{app_name}</a>",
952+
"subject": "Not Accepted. Your submission for {event_name} titled {session_name} was not accepted"
953+
},
954+
"withdrawn": {
955+
"message": "Hello,<br/><br/>This is an automatic message from {app_name}.<br/><br/>Your session status for the submission {session_name} for {event_name} was changed to \"Withdrawn\".<br/><br/>The status change was done by event organizers. If there are questions about this change please contact the organizers.<br/><br/>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.<br/><br/>More details about the event are on the event page at {event_link}.<br/><br/>Thank you.<br/><a href='{frontend_link}'>{app_name}</a>",
956+
"subject": "Withdrawn! Your submission for {event_name} titled {session_name} has been Withdrawn"
957+
}
958+
}

0 commit comments

Comments
 (0)