04/08 fixed survey email issue
This commit is contained in:
@@ -319,102 +319,155 @@ def notify_assignment(ticket, assigned_by):
|
||||
def send_satisfaction_survey(ticket):
|
||||
"""Send a satisfaction survey email when a ticket is resolved.
|
||||
|
||||
Creates a TicketSatisfaction row with a unique survey token, then
|
||||
emails the ticket creator a link to rate their experience (1-5 stars).
|
||||
The link is token-authenticated so the employee does not need to be
|
||||
logged in to respond.
|
||||
|
||||
URL construction
|
||||
----------------
|
||||
All other notification functions in this module use APP_BASE_URL from
|
||||
config to build absolute URLs — NOT url_for(..., _external=True).
|
||||
This function follows the same pattern. Using url_for inside an f-string
|
||||
that is evaluated before the background thread starts causes a
|
||||
RuntimeError ("Working outside of request context") which silently
|
||||
swallows the entire function before the Thread is ever created.
|
||||
|
||||
Called from update_ticket() after commit, when status → Resolved.
|
||||
Every step is wrapped in explicit error handling and logged so failures
|
||||
are visible in the application log rather than swallowed silently.
|
||||
"""
|
||||
from app.models import TicketSatisfaction
|
||||
from app import mail
|
||||
from threading import Thread
|
||||
from flask_mail import Message
|
||||
|
||||
creator = ticket.creator
|
||||
if not creator or not creator.email_notif:
|
||||
logger.info(
|
||||
f'[SURVEY] send_satisfaction_survey called: '
|
||||
f'ticket_id={ticket.id} number={ticket.ticket_number}'
|
||||
)
|
||||
|
||||
# Guard: survey feature must be enabled in system settings
|
||||
from app.models import SystemSetting
|
||||
if not SystemSetting.get_bool('survey_enabled', default=True):
|
||||
logger.info('[SURVEY] Satisfaction survey is disabled in settings — skipping')
|
||||
return
|
||||
|
||||
# Guard: creator must exist and have email notifications enabled
|
||||
creator = ticket.creator
|
||||
if not creator:
|
||||
logger.warning(f'[SURVEY] No creator found for ticket_id={ticket.id} — skipping')
|
||||
return
|
||||
if not creator.email_notif:
|
||||
logger.info(
|
||||
f'[SURVEY] Creator user_id={creator.id} has email_notif=False — skipping'
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f'[SURVEY] Creator OK: user_id={creator.id} '
|
||||
f'email={creator.email} email_notif={creator.email_notif}'
|
||||
)
|
||||
|
||||
# Create the TicketSatisfaction row
|
||||
try:
|
||||
survey = TicketSatisfaction.create_for_ticket(ticket)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f'[SURVEY] create_for_ticket raised an exception for '
|
||||
f'ticket_id={ticket.id}: {exc}', exc_info=True
|
||||
)
|
||||
return
|
||||
|
||||
survey = TicketSatisfaction.create_for_ticket(ticket)
|
||||
if not survey:
|
||||
return # already sent for this ticket
|
||||
logger.info(
|
||||
f'[SURVEY] Survey already exists for ticket_id={ticket.id} — skipping'
|
||||
)
|
||||
return
|
||||
|
||||
db.session.commit()
|
||||
logger.info(f'[SURVEY CREATED] ticket_id={ticket.id} token={survey.survey_token[:8]}…')
|
||||
try:
|
||||
db.session.commit()
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
f'[SURVEY] db.session.commit() failed for ticket_id={ticket.id}: {exc}',
|
||||
exc_info=True
|
||||
)
|
||||
db.session.rollback()
|
||||
return
|
||||
|
||||
# Build absolute URLs using APP_BASE_URL — identical to every other
|
||||
# notification function in this file. url_for(_external=True) requires
|
||||
# an active request context which is not guaranteed here.
|
||||
logger.info(
|
||||
f'[SURVEY CREATED] ticket_id={ticket.id} '
|
||||
f'survey_id={survey.id} token={survey.survey_token[:8]}...'
|
||||
)
|
||||
|
||||
# Build absolute URLs using APP_BASE_URL — no url_for() which requires
|
||||
# an active request context not guaranteed in all calling paths
|
||||
base_url = current_app.config.get('APP_BASE_URL', '').rstrip('/')
|
||||
if not base_url:
|
||||
logger.warning(
|
||||
'[SURVEY] APP_BASE_URL is not set — survey links will be broken. '
|
||||
'Set APP_BASE_URL in your .env (e.g. https://tickets.ltservicesinc.com)'
|
||||
)
|
||||
survey_url = f"{base_url}/survey/{survey.survey_token}"
|
||||
ticket_url = f"{base_url}/tickets/{ticket.id}"
|
||||
|
||||
logger.info(f'[SURVEY] survey_url={survey_url}')
|
||||
|
||||
stars_html = ''.join(
|
||||
f'<a href="{survey_url}?rating={i}" '
|
||||
f'style="display:inline-block;margin:0 6px;font-size:38px;'
|
||||
f'<a href="{survey_url}?rating={i}" '
|
||||
f'style="display:inline-block;margin:0 4px;font-size:40px;'
|
||||
f'text-decoration:none;color:#f59e0b;" title="{i} star">★</a>'
|
||||
for i in range(1, 6)
|
||||
)
|
||||
|
||||
html = f"""
|
||||
<html><body style="font-family:Arial,sans-serif;background:#f4f4f4;padding:20px;">
|
||||
<div style="max-width:560px;margin:0 auto;background:#fff;border-radius:10px;
|
||||
overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,.08);">
|
||||
<div style="background:#1e293b;padding:24px 32px;">
|
||||
<h1 style="color:#fff;margin:0;font-size:20px;">⭐ How did we do?</h1>
|
||||
</div>
|
||||
<div style="padding:32px;">
|
||||
<p style="color:#334155;margin-top:0;">Hi {creator.full_name},</p>
|
||||
<p style="color:#334155;">
|
||||
Your ticket <strong>{ticket.ticket_number}</strong> —
|
||||
<em>{ticket.title}</em> — has been marked as resolved.
|
||||
We'd love to hear how we did!
|
||||
</p>
|
||||
<p style="color:#334155;font-weight:600;margin-bottom:6px;">
|
||||
Click a star to rate your experience:
|
||||
</p>
|
||||
<p style="text-align:center;margin:20px 0;line-height:1;">
|
||||
{stars_html}
|
||||
</p>
|
||||
<p style="text-align:center;">
|
||||
<a href="{survey_url}" style="color:#2563eb;font-size:13px;">
|
||||
Or leave a detailed comment
|
||||
</a>
|
||||
</p>
|
||||
<p style="color:#94a3b8;font-size:12px;margin-top:24px;">
|
||||
If you feel the issue is not fully resolved, you can
|
||||
<a href="{ticket_url}" style="color:#2563eb;">re-open your ticket</a>
|
||||
at any time.
|
||||
</p>
|
||||
</div>
|
||||
<div style="background:#f8fafc;padding:16px 32px;text-align:center;
|
||||
color:#94a3b8;font-size:11px;border-top:1px solid #e2e8f0;">
|
||||
TechDesk IT Helpdesk • This is an automated message.
|
||||
</div>
|
||||
</div>
|
||||
</body></html>"""
|
||||
html = (
|
||||
'<html><body style="font-family:Arial,sans-serif;background:#f4f4f4;padding:20px;">'
|
||||
'<div style="max-width:560px;margin:0 auto;background:#fff;border-radius:10px;'
|
||||
'overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,.08);">'
|
||||
'<div style="background:#1e293b;padding:24px 32px;">'
|
||||
'<h1 style="color:#fff;margin:0;font-size:20px;">⭐ How did we do?</h1>'
|
||||
'</div>'
|
||||
'<div style="padding:32px;">'
|
||||
f'<p style="color:#334155;margin-top:0;">Hi {creator.full_name},</p>'
|
||||
f'<p style="color:#334155;">Your ticket <strong>{ticket.ticket_number}</strong>'
|
||||
f' has been marked as resolved. We'd love to hear how we did!</p>'
|
||||
'<p style="color:#334155;font-weight:600;margin-bottom:6px;">'
|
||||
'How satisfied were you with the resolution?</p>'
|
||||
f'<p style="text-align:center;margin:24px 0;line-height:1;">{stars_html}</p>'
|
||||
f'<p style="text-align:center;margin-bottom:16px;">'
|
||||
f'<a href="{survey_url}" style="color:#2563eb;font-size:13px;">'
|
||||
'Or leave a written comment</a></p>'
|
||||
f'<p style="color:#94a3b8;font-size:12px;">If the issue is not resolved, '
|
||||
f'<a href="{ticket_url}" style="color:#2563eb;">re-open your ticket</a>.</p>'
|
||||
'</div>'
|
||||
'<div style="background:#f8fafc;padding:16px 32px;text-align:center;'
|
||||
'color:#94a3b8;font-size:11px;border-top:1px solid #e2e8f0;">'
|
||||
'TechDesk IT Helpdesk • This is an automated message.'
|
||||
'</div></div></body></html>'
|
||||
)
|
||||
|
||||
msg = Message(
|
||||
subject = f'[TechDesk] How did we do? — {ticket.ticket_number}',
|
||||
recipients = [creator.email],
|
||||
html = html,
|
||||
# Capture locals needed inside the thread before spawning.
|
||||
# Mirrors the pattern in send_email() exactly:
|
||||
# - current_app._get_current_object() resolves the proxy to the real
|
||||
# Flask app object so it is safe to reference inside a daemon thread
|
||||
# that has no active context of its own yet.
|
||||
# - The Message is built INSIDE the thread, within app.app_context(),
|
||||
# not before — flask_mail reads app config at construction time.
|
||||
app = current_app._get_current_object()
|
||||
ticket_num = ticket.ticket_number
|
||||
ticket_id = ticket.id
|
||||
creator_name = creator.full_name
|
||||
creator_email= creator.email
|
||||
|
||||
logger.info(
|
||||
f'[SURVEY] Spawning email thread: recipient={creator_email} '
|
||||
f'ticket_id={ticket_id}'
|
||||
)
|
||||
|
||||
def _send():
|
||||
with current_app.app_context():
|
||||
with app.app_context():
|
||||
try:
|
||||
msg = Message(
|
||||
subject = f'[TechDesk] How did we do? — {ticket_num}',
|
||||
recipients = [creator_email],
|
||||
html = html,
|
||||
)
|
||||
mail.send(msg)
|
||||
logger.info(f'[SURVEY EMAIL SENT] ticket_id={ticket.id} user_id={creator.id}')
|
||||
logger.info(
|
||||
f'[SURVEY EMAIL SENT] ticket_id={ticket_id} '
|
||||
f'recipient={creator_email}'
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error(f'[SURVEY EMAIL FAILED] ticket_id={ticket.id} {exc}')
|
||||
logger.error(
|
||||
f'[SURVEY EMAIL FAILED] ticket_id={ticket_id} '
|
||||
f'recipient={creator_email}: {exc}', exc_info=True
|
||||
)
|
||||
|
||||
Thread(target=_send, daemon=True).start()
|
||||
t = Thread(target=_send, daemon=True)
|
||||
t.start()
|
||||
logger.info(f'[SURVEY] Email thread started for ticket_id={ticket_id}')
|
||||
|
||||
Reference in New Issue
Block a user