From c8e93a9f528059e6a9ca1d0798eb37ff5cfa3a1e Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Sat, 5 Dec 2020 05:24:27 +0000 Subject: [PATCH] Add notification settings --- app/blueprints/notifications/__init__.py | 49 ++++++++++++++++- app/blueprints/users/profile.py | 5 ++ app/models.py | 67 ++++++++++++++++++++++- app/templates/notifications/settings.html | 32 +++++++++++ migrations/versions/5d7233cf8a00_.py | 42 ++++++++++++++ 5 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 app/templates/notifications/settings.html create mode 100644 migrations/versions/5d7233cf8a00_.py diff --git a/app/blueprints/notifications/__init__.py b/app/blueprints/notifications/__init__.py index 6e8cce4..93acf53 100644 --- a/app/blueprints/notifications/__init__.py +++ b/app/blueprints/notifications/__init__.py @@ -17,18 +17,65 @@ from flask import Blueprint, render_template, redirect, url_for from flask_login import current_user, login_required -from app.models import db, Notification +from flask_wtf import FlaskForm +from wtforms import BooleanField, SubmitField +from app.blueprints.users.profile import get_setting_tabs +from app.models import db, Notification, UserNotificationPreferences, NotificationType bp = Blueprint("notifications", __name__) + @bp.route("/notifications/") @login_required def list_all(): return render_template("notifications/list.html") + @bp.route("/notifications/clear/", methods=["POST"]) @login_required def clear(): Notification.query.filter_by(user=current_user).delete() db.session.commit() return redirect(url_for("notifications.list_all")) + + +@bp.route("/notifications/settings/", methods=["GET", "POST"]) +@login_required +def settings(): + is_new = False + prefs = current_user.notification_preferences + if prefs is None: + is_new = True + prefs = UserNotificationPreferences(current_user) + + attrs = { + "submit": SubmitField("Save") + } + + data = {} + types = [] + for notificationType in NotificationType: + key = "pref_" + notificationType.toName() + types.append(notificationType) + attrs[key] = BooleanField("") + data[key] = getattr(prefs, key) == 2 + + SettingsForm = type("SettingsForm", (FlaskForm,), attrs) + + form = SettingsForm(data=data) + if form.validate_on_submit(): + for notificationType in NotificationType: + key = "pref_" + notificationType.toName() + field = getattr(form, key) + value = 2 if field.data else 0 + setattr(prefs, key, value) + + if is_new: + db.session.add(prefs) + + db.session.commit() + return redirect(url_for("notifications.settings")) + + return render_template("notifications/settings.html", + form=form, user=current_user, types=types, + tabs=get_setting_tabs(current_user), current_tab="notifications") diff --git a/app/blueprints/users/profile.py b/app/blueprints/users/profile.py index 31bbf27..6424c29 100644 --- a/app/blueprints/users/profile.py +++ b/app/blueprints/users/profile.py @@ -83,6 +83,11 @@ def get_setting_tabs(user): "title": "Edit Profile", "url": url_for("users.profile_edit", username=user.username) }, + { + "id": "notifications", + "title": "Emails and Notifications", + "url": url_for("notifications.settings") + }, { "id": "api_tokens", "title": "API Tokens", diff --git a/app/models.py b/app/models.py index 75e022d..0dfaec5 100644 --- a/app/models.py +++ b/app/models.py @@ -166,6 +166,8 @@ class User(db.Model, UserMixin): notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id", order_by="Notification.created_at") + notification_preferences = db.relationship("UserNotificationPreferences", uselist=False, back_populates="user") + packages = db.relationship("Package", backref=db.backref("author", lazy="joined"), lazy="dynamic") requests = db.relationship("EditRequest", backref="author", lazy="dynamic") threads = db.relationship("Thread", backref="author", lazy="dynamic") @@ -275,9 +277,6 @@ class UserEmailVerification(db.Model): class NotificationType(enum.Enum): - # Any other - OTHER = 0 - # Package / release / etc PACKAGE_EDIT = 1 @@ -302,6 +301,9 @@ class NotificationType(enum.Enum): # Editor misc EDITOR_MISC = 8 + # Any other + OTHER = 0 + def getTitle(self): return self.name.replace("_", " ").title() @@ -309,6 +311,29 @@ class NotificationType(enum.Enum): def toName(self): return self.name.lower() + def get_description(self): + if self == NotificationType.OTHER: + return "Minor notifications not important enough for a dedicated category." + elif self == NotificationType.PACKAGE_EDIT: + return "When another user edits your packages, releases, etc." + elif self == NotificationType.PACKAGE_APPROVAL: + return "Notifications from editors related to the package approval process." + elif self == NotificationType.NEW_THREAD: + return "When a thread is created on your package." + elif self == NotificationType.NEW_REVIEW: + return "When a user posts a review." + elif self == NotificationType.THREAD_REPLY: + return "When someone replies to a thread you're watching." + elif self == NotificationType.MAINTAINER: + return "When your package's maintainers change." + elif self == NotificationType.EDITOR_ALERT: + return "Important editor alerts." + elif self == NotificationType.EDITOR_MISC: + return "Miscellaneous editor alerts." + else: + return "" + + def __str__(self): return self.name @@ -353,6 +378,42 @@ class Notification(db.Model): self.url = url self.package = package + def can_send_email(self): + prefs = self.user.notification_preferences + return prefs and getattr(prefs, "pref_" + self.type.toName()) == 2 + + +class UserNotificationPreferences(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + user = db.relationship("User", back_populates="notification_preferences") + + # 2 = immediate emails + # 1 = daily digest emails + # 0 = no emails + + pref_package_edit = db.Column(db.Integer, nullable=False) + pref_package_approval = db.Column(db.Integer, nullable=False) + pref_new_thread = db.Column(db.Integer, nullable=False) + pref_new_review = db.Column(db.Integer, nullable=False) + pref_thread_reply = db.Column(db.Integer, nullable=False) + pref_maintainer = db.Column(db.Integer, nullable=False) + pref_editor_alert = db.Column(db.Integer, nullable=False) + pref_editor_misc = db.Column(db.Integer, nullable=False) + pref_other = db.Column(db.Integer, nullable=False) + + def __init__(self, user): + self.user = user + self.pref_package_edit = 1 + self.pref_package_approval = 2 + self.pref_new_thread = 2 + self.pref_new_review = 1 + self.pref_thread_reply = 2 + self.pref_maintainer = 2 + self.pref_editor_alert = 2 + self.pref_editor_misc = 0 + self.pref_other = 0 + class License(db.Model): id = db.Column(db.Integer, primary_key=True) diff --git a/app/templates/notifications/settings.html b/app/templates/notifications/settings.html new file mode 100644 index 0000000..bdacccb --- /dev/null +++ b/app/templates/notifications/settings.html @@ -0,0 +1,32 @@ +{% extends "users/settings_base.html" %} + +{% block title %} + {{ _("Email and Notifications | %(username)s", username=user.username) }} +{% endblock %} + +{% block pane %} +

{{ _("Email and Notifications") }}

+ + {% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %} +
+ {{ form.hidden_tag() }} + + + + + + + + {% for type in types %} + + + + + + {% endfor %} +
EventDescriptionEmails?
{{ type.getTitle() }}{{ type.get_description() }}{{ render_checkbox_field(form["pref_" + type.toName()]) }}
+ + + {{ render_submit_field(form.submit, tabindex=280) }} +
+{% endblock %} diff --git a/migrations/versions/5d7233cf8a00_.py b/migrations/versions/5d7233cf8a00_.py new file mode 100644 index 0000000..6fdcafd --- /dev/null +++ b/migrations/versions/5d7233cf8a00_.py @@ -0,0 +1,42 @@ +"""empty message + +Revision ID: 5d7233cf8a00 +Revises: 81de25b72f66 +Create Date: 2020-12-05 03:50:18.843494 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '5d7233cf8a00' +down_revision = '81de25b72f66' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user_notification_preferences', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('pref_other', sa.Integer(), nullable=False), + sa.Column('pref_package_edit', sa.Integer(), nullable=False), + sa.Column('pref_package_approval', sa.Integer(), nullable=False), + sa.Column('pref_new_thread', sa.Integer(), nullable=False), + sa.Column('pref_new_review', sa.Integer(), nullable=False), + sa.Column('pref_thread_reply', sa.Integer(), nullable=False), + sa.Column('pref_maintainer', sa.Integer(), nullable=False), + sa.Column('pref_editor_alert', sa.Integer(), nullable=False), + sa.Column('pref_editor_misc', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('user_notification_preferences') + # ### end Alembic commands ###