Sending Confirmation Emails with Flask, RQ (Working outside of application context.)

172 Views Asked by At

I am trying to send confirmation emails when a new user registers. When I send a confirmation email, everything works fine, however, when I try to run that task in the background I get the following error:

  File "/Users/martifont/Dev/bz/./FlaskApp/utils.py", line 22, in send_confirmation_email
    msg = Message()

I have tried running a different task in the background with no issues at all.

users.py

#imports
from .utils import send_reset_email, send_confirmation_email, example

users = Blueprint('users', __name__)

#SIGNUP
@users.route('/signup', methods=\['GET', 'POST'\])
def signup():
form = SignupForm()
if form.validate_on_submit():
user = User(email=form.email.data, username=form.username.data, confirmed=False, is_admin=False, password = generate_password_hash(form.password.data, method='sha256'))
db.session.add(user)
db.session.commit()

        user_id=user.id
        with app.app_context():
            job = q.enqueue(send_confirmation_email, user)
    
        login_user(user)
        flash('A confirmation email has been sent to your inbox.', 'is-success')
        return redirect(url_for('users.account', id=user.id))
    return render_template('signup.html', form=form)

utils.py

def send_confirmation_email(user):
    token = user.get_confirmation_token()
    msg = Message()
    msg.subject = ('Email Confirmation')
    msg.sender = 'MAIL_DEFAULT_SENDER'
    msg.recipients = [user.email]
    msg.body = f'''
Welcome { user.username }!,
Thanks for signing up. Please follow this link to activate your account:
{url_for('users.confirm_email', token=token, _external=True)}
Thanks!

'''
    mail.send(msg)

EDIT After following Sebastian's advice, I think the issue is solved, however, I am still getting the following error message.

File "/Users/martifont/Dev/bz/venv/lib/python3.10/site-packages/flask/app.py", line 2548, in __call__
    return self.wsgi_app(environ, start_response)
  File "/Users/martifont/Dev/bz/venv/lib/python3.10/site-packages/flask/app.py", line 2528, in wsgi_app
    response = self.handle_exception(e)
  File "/Users/martifont/Dev/bz/venv/lib/python3.10/site-packages/flask/app.py", line 2525, in wsgi_app
    response = self.full_dispatch_request()
  File "/Users/martifont/Dev/bz/venv/lib/python3.10/site-packages/flask/app.py", line 1822, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Users/martifont/Dev/bz/venv/lib/python3.10/site-packages/flask/app.py", line 1820, in full_dispatch_request
    rv = self.dispatch_request()
  File "/Users/martifont/Dev/bz/venv/lib/python3.10/site-packages/flask/app.py", line 1796, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
  File "/Users/martifont/Dev/bz/FlaskApp/users.py", line 30, in signup
    job = q.enqueue(send_confirmation_email, args=(app, user))
  File "/Users/martifont/Dev/bz/venv/lib/python3.10/site-packages/rq/queue.py", line 514, in enqueue
    return self.enqueue_call(
  File "/Users/martifont/Dev/bz/venv/lib/python3.10/site-packages/rq/queue.py", line 410, in enqueue_call
    return self.enqueue_job(job, pipeline=pipeline, at_front=at_front)
  File "/Users/martifont/Dev/bz/venv/lib/python3.10/site-packages/rq/queue.py", line 572, in enqueue_job
    job.save(pipeline=pipe)
  File "/Users/martifont/Dev/bz/venv/lib/python3.10/site-packages/rq/job.py", line 694, in save
    mapping = self.to_dict(include_meta=include_meta)
  File "/Users/martifont/Dev/bz/venv/lib/python3.10/site-packages/rq/job.py", line 629, in to_dict
    'data': zlib.compress(self.data),
  File "/Users/martifont/Dev/bz/venv/lib/python3.10/site-packages/rq/job.py", line 305, in data
    self._data = self.serializer.dumps(job_tuple)
1

There are 1 best solutions below

3
On

When you are in a view or inside the creat_app factory function there is no need to use the app_context() context manager.

The traceback defines where the error is been emited and that is when you instantiated the msg=Message() variable so instead of calling the app context inside the view i suggest you to refactor your view and send functions like so:

users.py

from flask import current_app

@users.route('/signup', methods=\['GET', 'POST'\])
def signup():
    form = SignupForm()
    if form.validate_on_submit():
        user = User(email=form.email.data, username=form.username.data, confirmed=False, is_admin=False, password = generate_password_hash(form.password.data, method='sha256'))
        db.session.add(user)
        db.session.commit()

        user_id=user.id
        # pass the current app object to the send_confirmation_email function.
        # Its important to pass the object and no the proxy
        app = current_app._get_current_object() 
        job = q.enqueue(send_confirmation_email, args=(app, user))
    
        login_user(user)
        flash('A confirmation email has been sent to your inbox.', 'is-success')
        return redirect(url_for('users.account', id=user.id))
    return render_template('signup.html', form=form)

utils.py

def send_confirmation_email(app, user):

    with app.app_context():
        token = user.get_confirmation_token()
        msg = Message()
        msg.subject = ('Email Confirmation')
        msg.sender = 'MAIL_DEFAULT_SENDER'
        msg.recipients = [user.email]
        msg.body = f'''
            Welcome { user.username }!,
            Thanks for signing up. Please follow this link to activate your account:
            {url_for('users.confirm_email', token=token, _external=True)}
            Thanks!'''
        mail.send(msg)