diff --git a/app/blueprints/users/account.py b/app/blueprints/users/account.py index ff92795..bc6e338 100644 --- a/app/blueprints/users/account.py +++ b/app/blueprints/users/account.py @@ -23,7 +23,7 @@ from wtforms import * from wtforms.validators import * from app.models import * -from app.tasks.emails import sendVerifyEmail, send_anon_email, sendUnsubscribeVerifyEmail, send_user_email +from app.tasks.emails import send_verify_email, send_anon_email, send_unsubscribe_verify, send_user_email from app.utils import randomString, make_flask_login_password, is_safe_url, check_password_hash, addAuditLog from passlib.pwd import genphrase @@ -126,7 +126,7 @@ def handle_register(form): db.session.add(ver) db.session.commit() - sendVerifyEmail.delay(form.email.data, token) + send_verify_email.delay(form.email.data, token) flash("Check your email address to verify your account", "success") return redirect(url_for("homepage.home")) @@ -168,7 +168,7 @@ def forgot_password(): db.session.add(ver) db.session.commit() - sendVerifyEmail.delay(form.email.data, token) + send_verify_email.delay(form.email.data, token) else: send_anon_email.delay(email, "Unable to find account", """

@@ -335,7 +335,7 @@ def unsubscribe_verify(): sub.token = randomString(32) db.session.commit() - sendUnsubscribeVerifyEmail.delay(form.email.data) + send_unsubscribe_verify.delay(form.email.data) flash("Check your email address to continue the unsubscribe", "success") return redirect(url_for("homepage.home")) diff --git a/app/blueprints/users/settings.py b/app/blueprints/users/settings.py index 884020f..f52fc1c 100644 --- a/app/blueprints/users/settings.py +++ b/app/blueprints/users/settings.py @@ -6,7 +6,7 @@ from wtforms.validators import * from app.models import * from app.utils import nonEmptyOrNone, addAuditLog, randomString -from app.tasks.emails import sendVerifyEmail +from app.tasks.emails import send_verify_email from . import bp @@ -141,7 +141,7 @@ def handle_email_notifications(user, prefs: UserNotificationPreferences, is_new, flash("Check your email to confirm it", "success") - sendVerifyEmail.delay(newEmail, token) + send_verify_email.delay(newEmail, token) return redirect(url_for("homepage.home")) db.session.commit() diff --git a/app/models.py b/app/models.py index fa2c501..52a2ac2 100644 --- a/app/models.py +++ b/app/models.py @@ -348,10 +348,12 @@ class NotificationType(enum.Enum): else: return "" - def __str__(self): return self.name + def __lt__(self, other): + return self.value < other.value + @classmethod def choices(cls): return [(choice, choice.getTitle()) for choice in cls] @@ -397,6 +399,10 @@ class Notification(db.Model): prefs = self.user.notification_preferences return prefs and self.user.email and prefs.get_can_email(self.type) + def can_send_digest(self): + prefs = self.user.notification_preferences + return prefs and self.user.email and prefs.get_can_digest(self.type) + class UserNotificationPreferences(db.Model): id = db.Column(db.Integer, primary_key=True) diff --git a/app/tasks/__init__.py b/app/tasks/__init__.py index e9a01ce..4cd0048 100644 --- a/app/tasks/__init__.py +++ b/app/tasks/__init__.py @@ -73,8 +73,12 @@ CELERYBEAT_SCHEDULE = { 'schedule': crontab(minute=10, hour=1), }, 'send_pending_notifications': { - 'task': 'app.tasks.emails.sendPendingNotifications', + 'task': 'app.tasks.emails.send_pending_notifications', 'schedule': crontab(minute='*/5'), + }, + 'send_notification_digests': { + 'task': 'app.tasks.emails.send_pending_digests', + 'schedule': crontab(minute=0, hour=14), } } celery.conf.beat_schedule = CELERYBEAT_SCHEDULE diff --git a/app/tasks/emails.py b/app/tasks/emails.py index ff3cb93..f27fecd 100644 --- a/app/tasks/emails.py +++ b/app/tasks/emails.py @@ -18,7 +18,7 @@ from flask import render_template from flask_mail import Message from app import mail -from app.models import Notification, db, EmailSubscription +from app.models import Notification, db, EmailSubscription, User from app.tasks import celery from app.utils import abs_url_for, abs_url, randomString @@ -36,7 +36,7 @@ def get_email_subscription(email): @celery.task() -def sendVerifyEmail(email, token): +def send_verify_email(email, token): sub = get_email_subscription(email) if sub.blacklisted: return @@ -59,7 +59,7 @@ def sendVerifyEmail(email, token): @celery.task() -def sendUnsubscribeVerifyEmail(email): +def send_unsubscribe_verify(email): sub = get_email_subscription(email) if sub.blacklisted: return @@ -103,7 +103,7 @@ def send_anon_email(email: str, subject: str, text: str, html=None): "You are receiving this email because someone (hopefully you) entered your email address as a user's email.") -def sendNotificationEmail(notification): +def send_single_email(notification): sub = get_email_subscription(notification.user.email) if sub.blacklisted: return @@ -125,11 +125,55 @@ def sendNotificationEmail(notification): mail.send(msg) -@celery.task() -def sendPendingNotifications(): - for notification in Notification.query.filter_by(emailed=False).all(): - if notification.can_send_email(): - sendNotificationEmail(notification) +def send_notification_digest(notifications: [Notification]): + user = notifications[0].user + + sub = get_email_subscription(user.email) + if sub.blacklisted: + return + + msg = Message("{} new notifications".format(len(notifications)), recipients=[user.email]) + + msg.body = "".join(["<{}> {}\nView: {}\n\n".format(notification.causer.display_name, notification.title, abs_url(notification.url)) for notification in notifications]) + + msg.body += "Manage email settings: {}\nUnsubscribe: {}".format( + abs_url_for("users.email_notifications", username=user.username), + abs_url_for("users.unsubscribe", token=sub.token)) + + msg.html = render_template("emails/notification_digest.html", notifications=notifications, user=user, sub=sub) + mail.send(msg) + + +@celery.task() +def send_pending_digests(): + for user in User.query.filter(User.notifications.any(emailed=False)).all(): + to_send = [] + for notification in user.notifications: + if not notification.emailed and notification.can_send_digest(): + to_send.append(notification) + notification.emailed = True + + if len(to_send) > 0: + send_notification_digest(to_send) - notification.emailed = True db.session.commit() + + +@celery.task() +def send_pending_notifications(): + for user in User.query.filter(User.notifications.any(emailed=False)).all(): + to_send = [] + for notification in user.notifications: + if not notification.emailed: + if notification.can_send_email(): + to_send.append(notification) + notification.emailed = True + elif not notification.can_send_digest(): + notification.emailed = True + + db.session.commit() + + if len(to_send) > 1: + send_notification_digest(to_send) + elif len(to_send) > 0: + send_single_email(to_send[0]) diff --git a/app/templates/emails/notification_digest.html b/app/templates/emails/notification_digest.html new file mode 100644 index 0000000..5cf6d58 --- /dev/null +++ b/app/templates/emails/notification_digest.html @@ -0,0 +1,39 @@ +{% extends "emails/base.html" %} + +{% block content %} + +{% for type, group in notifications | groupby("package.title") %} +

+ {{ type or _("Other Notifications") }} +

+ + +{% endfor %} + +

+ + View Notifications + +

+ +{% endblock %} + +{% block footer %} + You are receiving this email because you are a registered user of ContentDB, + and have email notifications enabled.
+ + + {{ _("Manage your preferences") }} + + | + + {{ _("Unsubscribe") }} +
+{% endblock %} diff --git a/app/templates/users/settings_email.html b/app/templates/users/settings_email.html index 601aa49..4015c06 100644 --- a/app/templates/users/settings_email.html +++ b/app/templates/users/settings_email.html @@ -32,7 +32,6 @@

Configure whether certain types of notifications are sent immediately, or as part of a daily digest.
- Note: daily digests aren't implemented yet.