04/08 fixed survey email issue

This commit is contained in:
2026-04-08 10:38:50 -04:00
parent 60db5c3f9a
commit cc81e0cca3
6 changed files with 210 additions and 76 deletions
+124 -71
View File
@@ -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">&#9733;</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;">&#11088; 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> &mdash;
<em>{ticket.title}</em> &mdash; 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 &bull; 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;">&#11088; 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&#39;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 &bull; 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}')