Merge pull request #69 from simple-login/welcome-email-serie

Job system
This commit is contained in:
Son Nguyen Kim 2020-02-04 22:21:33 +07:00 committed by GitHub
commit 14e82ca7a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 200 additions and 5 deletions

View file

@ -54,11 +54,11 @@ def auth_login():
if user.enable_otp:
s = Signer(FLASK_SECRET)
ret["mfa_key"] = s.sign(str(user.id))
ret["api_key"] = ""
ret["api_key"] = None
else:
api_key = ApiKey.create(user.id, device)
db.session.commit()
ret["mfa_key"] = ""
ret["mfa_key"] = None
ret["api_key"] = api_key.code
return jsonify(**ret), 200

View file

@ -166,3 +166,7 @@ MFA_USER_ID = "mfa_user_id"
FLASK_PROFILER_PATH = os.environ.get("FLASK_PROFILER_PATH")
FLASK_PROFILER_PASSWORD = os.environ.get("FLASK_PROFILER_PASSWORD")
# Job names
JOB_ONBOARDING_1 = "onboarding-1"

View file

@ -10,7 +10,13 @@ from sqlalchemy import text, desc
from sqlalchemy_utils import ArrowType
from app import s3
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN, URL, AVATAR_URL_EXPIRATION
from app.config import (
EMAIL_DOMAIN,
MAX_NB_EMAIL_FREE_PLAN,
URL,
AVATAR_URL_EXPIRATION,
JOB_ONBOARDING_1,
)
from app.email_utils import get_email_name
from app.extensions import db
from app.log import LOG
@ -144,6 +150,14 @@ class User(db.Model, ModelMixin, UserMixin):
GenEmail.create_new(user.id, prefix="my-first-alias")
db.session.flush()
# Schedule onboarding emails
Job.create(
name=JOB_ONBOARDING_1,
payload={"user_id": user.id},
run_at=arrow.now().shift(days=1),
)
db.session.flush()
return user
def lifetime_or_active_subscription(self) -> bool:
@ -769,3 +783,17 @@ class Directory(db.Model, ModelMixin):
def __repr__(self):
return f"<Directory {self.name}>"
class Job(db.Model, ModelMixin):
"""Used to schedule one-time job in the future"""
name = db.Column(db.String(128), nullable=False)
payload = db.Column(db.JSON)
# whether the job has been taken by the job runner
taken = db.Column(db.Boolean, default=False, nullable=False)
run_at = db.Column(ArrowType)
def __repr__(self):
return f"<Job {self.id} {self.name} {self.payload}>"

79
job_runner.py Normal file
View file

@ -0,0 +1,79 @@
"""
Run scheduled jobs.
Not meant for running job at precise time (+- 1h)
"""
import time
import arrow
from app.config import JOB_ONBOARDING_1
from app.email_utils import (
send_email,
render,
)
from app.extensions import db
from app.log import LOG
from app.models import (
User,
Job,
)
from server import create_app
# fix the database connection leak issue
# use this method instead of create_app
def new_app():
app = create_app()
@app.teardown_appcontext
def shutdown_session(response_or_exc):
# same as shutdown_session() in flask-sqlalchemy but this is not enough
db.session.remove()
# dispose the engine too
db.engine.dispose()
return app
def onboarding_1(user):
if not user.notification:
LOG.d("User %s disable notification setting", user)
return
send_email(
user.email,
f"Do you know you can send emails to anyone from your alias?",
render("com/onboarding-1.txt", user=user),
render("com/onboarding-1.html", user=user),
)
if __name__ == "__main__":
while True:
# run a job 1h earlier or later is not a big deal ...
min_dt = arrow.now().shift(hours=-1)
max_dt = arrow.now().shift(hours=1)
app = new_app()
with app.app_context():
for job in Job.query.filter(
Job.taken == False, Job.run_at > min_dt, Job.run_at <= max_dt
).all():
LOG.d("Take job %s", job)
# mark the job as taken, whether it will be executed successfully or not
job.taken = True
db.session.commit()
if job.name == JOB_ONBOARDING_1:
user_id = job.payload.get("user_id")
user = User.get(user_id)
LOG.d("run onboarding_1 for user %s", user)
onboarding_1(user)
else:
LOG.error("Unknown job name %s", job.name)
time.sleep(10)

View file

@ -0,0 +1,38 @@
"""empty message
Revision ID: 9c976df9b9c4
Revises: 7c39ba4ec38d
Create Date: 2020-02-03 13:08:29.049797
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9c976df9b9c4'
down_revision = '7c39ba4ec38d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('job',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
sa.Column('name', sa.String(length=128), nullable=False),
sa.Column('payload', sa.JSON(), nullable=True),
sa.Column('taken', sa.Boolean(), nullable=False),
sa.Column('run_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('job')
# ### end Alembic commands ###

View file

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block content %}
{{ render_text("This email is sent to " + user.email + " and is part of our onboarding series.") }}
{{ render_text('Unsubscribe from our emails on <a href="https://app.simplelogin.io/dashboard/setting#notification">https://app.simplelogin.io/dashboard/setting#notification</a>') }}
{{ render_text("<hr>") }}
{{ render_text("Hi " + user.name) }}
{{ render_text("Do you know you can send emails to anyone from your alias?") }}
{{ render_text("This Youtube video quickly walks you through the steps:") }}
{{ render_text('<a href="https://youtu.be/VsypF-DBaow">https://youtu.be/VsypF-DBaow</a>') }}
{{ render_text("Here are the steps:") }}
{{ render_text("1. First click <b>Send Email</b> on your alias you want to send email from.") }}
{{ render_text("2. Enter your contact email, this will generate a <b>reverse-alias</b>.") }}
{{ render_text("3. Use this reverse-alias <b>instead of your contact email</b>.") }}
{{ render_text("4. Your contact will receive this email from your alias.") }}
{{ render_text("As usual, let me know if you have any question by replying to this email.") }}
{% endblock %}

View file

@ -0,0 +1,21 @@
This email is sent to {{ user.email }} and is part of our onboarding series.
Unsubscribe from our emails on https://app.simplelogin.io/dashboard/setting#notification
----------------
Hi {{user.name}}
Do you know you can send an email to anyone from your alias?
This below Youtube video walks you quickly through the steps:
https://youtu.be/VsypF-DBaow
Here are the steps:
1. First click "Send Email" on your alias you want to send email from
2. Enter your contact email, this will generate an "reverse-alias"
3. Use this reverse-alias instead of your contact email
4. Your contact will receive this email from your alias.
As usual, let me know if you have any question by replying to this email.
Best regards,
Son - SimpleLogin founder.

View file

@ -16,7 +16,7 @@ def test_auth_login_success_mfa_disabled(flask_client):
assert r.status_code == 200
assert r.json["api_key"]
assert r.json["mfa_enabled"] == False
assert r.json["mfa_key"] == ""
assert r.json["mfa_key"] is None
assert r.json["name"] == "Test User"
@ -36,7 +36,7 @@ def test_auth_login_success_mfa_enabled(flask_client):
)
assert r.status_code == 200
assert r.json["api_key"] == ""
assert r.json["api_key"] is None
assert r.json["mfa_enabled"] == True
assert r.json["mfa_key"]
assert r.json["name"] == "Test User"