From f664243e42eb44fb057662da9376109618f09dcc Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Thu, 10 Sep 2020 20:14:55 +0200 Subject: [PATCH] add batch-import page --- app/config.py | 1 + app/dashboard/__init__.py | 1 + .../templates/dashboard/batch_import.html | 57 ++++++++++++++ .../templates/dashboard/setting.html | 29 ++++++-- app/dashboard/views/batch_import.py | 53 +++++++++++++ job_runner.py | 74 ++++++++++++++++++- static/batch_import_template.csv | 3 + 7 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 app/dashboard/templates/dashboard/batch_import.html create mode 100644 app/dashboard/views/batch_import.py create mode 100644 static/batch_import_template.csv diff --git a/app/config.py b/app/config.py index 31763cc3..c794ed60 100644 --- a/app/config.py +++ b/app/config.py @@ -225,6 +225,7 @@ JOB_ONBOARDING_1 = "onboarding-1" JOB_ONBOARDING_2 = "onboarding-2" JOB_ONBOARDING_3 = "onboarding-3" JOB_ONBOARDING_4 = "onboarding-4" +JOB_BATCH_IMPORT = "batch-import" # for pagination PAGE_LIMIT = 20 diff --git a/app/dashboard/__init__.py b/app/dashboard/__init__.py index 95ac461b..8363aa5d 100644 --- a/app/dashboard/__init__.py +++ b/app/dashboard/__init__.py @@ -24,4 +24,5 @@ from .views import ( recovery_code, contact_detail, setup_done, + batch_import, ) diff --git a/app/dashboard/templates/dashboard/batch_import.html b/app/dashboard/templates/dashboard/batch_import.html new file mode 100644 index 00000000..bccda6c4 --- /dev/null +++ b/app/dashboard/templates/dashboard/batch_import.html @@ -0,0 +1,57 @@ +{% extends 'default.html' %} +{% set active_page = "setting" %} +{% block title %} + Alias Batch Import +{% endblock %} + +{% block default_content %} +
+
+

Alias Batch Import

+

+ The import can take several minutes. + Please come back to this page to verify the import status.
+ Only aliases created with your verified domains can be imported.
+ If an alias already exists, it won't be imported. +

+ + Download CSV Template + +
+ +
+ +
+ +
+ + {% if batch_imports %} +
+

Batch imports

+ + + + + + + + + + {% for batch_import in batch_imports %} + + + + + + {% endfor %} + +
UploadedNumber Alias ImportedStatus
{{ batch_import.created_at | dt }}{{ batch_import.nb_alias() }}{% if batch_import.processed %} Processed ✅ {% else %} Pending {% endif %}
+ {% endif %} + + +
+
+{% endblock %} diff --git a/app/dashboard/templates/dashboard/setting.html b/app/dashboard/templates/dashboard/setting.html index 7de883d4..11e630cf 100644 --- a/app/dashboard/templates/dashboard/setting.html +++ b/app/dashboard/templates/dashboard/setting.html @@ -8,11 +8,11 @@ {% block head %} {% endblock %} @@ -156,9 +156,9 @@ @@ -296,6 +296,19 @@ +
+
+
Import alias
+
+ You can import your aliases created on other platforms into SimpleLogin. +
+ + Batch Import + + +
+
+
Export Data
diff --git a/app/dashboard/views/batch_import.py b/app/dashboard/views/batch_import.py new file mode 100644 index 00000000..00e4a363 --- /dev/null +++ b/app/dashboard/views/batch_import.py @@ -0,0 +1,53 @@ +import arrow +from flask import render_template, flash, request, redirect, url_for +from flask_login import login_required, current_user +from flask_wtf import FlaskForm +from wtforms import StringField, validators + +from app import s3 +from app.config import JOB_BATCH_IMPORT +from app.dashboard.base import dashboard_bp +from app.extensions import db +from app.log import LOG +from app.models import CustomDomain, File, BatchImport, Job +from app.utils import random_string + + +@dashboard_bp.route("/batch_import", methods=["GET", "POST"]) +@login_required +def batch_import_route(): + # only for users who have custom domains + if not current_user.verified_custom_domains(): + flash("Alias batch import is only available for custom domains", "warning") + + batch_imports = BatchImport.query.filter_by(user_id=current_user.id).all() + + if request.method == "POST": + alias_file = request.files["alias-file"] + + file_path = random_string(20) + ".csv" + file = File.create(user_id=current_user.id, path=file_path) + s3.upload_from_bytesio(file_path, alias_file) + db.session.flush() + LOG.d("upload file %s to s3 at %s", file, file_path) + + bi = BatchImport.create(user_id=current_user.id, file_id=file.id) + db.session.flush() + LOG.debug("Add a batch import job %s for %s", bi, current_user) + + # Schedule batch import job + Job.create( + name=JOB_BATCH_IMPORT, + payload={"batch_import_id": bi.id}, + run_at=arrow.now(), + ) + db.session.commit() + + flash( + "The file has been uploaded successfully and the import will start shortly", + "success", + ) + + return redirect(url_for("dashboard.batch_import_route")) + + return render_template("dashboard/batch_import.html", batch_imports=batch_imports) diff --git a/job_runner.py b/job_runner.py index 70c580b5..c015731b 100644 --- a/job_runner.py +++ b/job_runner.py @@ -2,20 +2,36 @@ Run scheduled jobs. Not meant for running job at precise time (+- 1h) """ +import csv import time import arrow +import requests +from app import s3 from app.config import ( JOB_ONBOARDING_1, JOB_ONBOARDING_2, JOB_ONBOARDING_3, JOB_ONBOARDING_4, + JOB_BATCH_IMPORT, +) +from app.email_utils import ( + send_email, + render, + get_email_domain_part, ) -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 app.models import ( + User, + Job, + BatchImport, + Alias, + DeletedAlias, + DomainDeletedAlias, + CustomDomain, +) from server import create_app @@ -71,6 +87,55 @@ def onboarding_mailbox(user): ) +def handle_batch_import(batch_import: BatchImport): + user = batch_import.user + + batch_import.processed = True + db.session.commit() + + LOG.debug("Start batch import for %s %s", batch_import, user) + file_url = s3.get_url(batch_import.file.path) + + LOG.d("Download file %s from %s", batch_import.file, file_url) + r = requests.get(file_url) + lines = [l.decode() for l in r.iter_lines()] + reader = csv.DictReader(lines) + + for row in reader: + full_alias = row["alias"].lower().strip().replace(" ", "") + note = row["note"] + + alias_domain = get_email_domain_part(full_alias) + custom_domain = CustomDomain.get_by(domain=alias_domain) + + if ( + not custom_domain + or not custom_domain.verified + or custom_domain.user_id != user.id + ): + LOG.debug("domain %s can't be used %s", alias_domain, user) + continue + + if ( + Alias.get_by(email=full_alias) + or DeletedAlias.get_by(email=full_alias) + or DomainDeletedAlias.get_by(email=full_alias) + ): + LOG.d("alias already used %s", full_alias) + continue + + alias = Alias.create( + user_id=user.id, + email=full_alias, + note=note, + mailbox_id=user.default_mailbox_id, + custom_domain_id=custom_domain.id, + batch_import_id=batch_import.id, + ) + db.session.commit() + LOG.d("Create %s", alias) + + if __name__ == "__main__": while True: # run a job 1h earlier or later is not a big deal ... @@ -129,6 +194,11 @@ if __name__ == "__main__": LOG.d("send onboarding pgp email to user %s", user) onboarding_pgp(user) + elif job.name == JOB_BATCH_IMPORT: + batch_import_id = job.payload.get("batch_import_id") + batch_import = BatchImport.get(batch_import_id) + handle_batch_import(batch_import) + else: LOG.exception("Unknown job name %s", job.name) diff --git a/static/batch_import_template.csv b/static/batch_import_template.csv new file mode 100644 index 00000000..c9af19f0 --- /dev/null +++ b/static/batch_import_template.csv @@ -0,0 +1,3 @@ +"alias","note" +"ebay@my-domain.com","Used on eBay" +"facebook@my-domain.com","Used on Facebook, Instagram." \ No newline at end of file