diff --git a/app/blueprints/api/tokens.py b/app/blueprints/api/tokens.py index 255be134..da743cfe 100644 --- a/app/blueprints/api/tokens.py +++ b/app/blueprints/api/tokens.py @@ -25,7 +25,7 @@ from wtforms.validators import * from app.models import db, User, APIToken, Package, Permission from app.utils import randomString from . import bp -from ..users.profile import get_setting_tabs +from ..users.settings import get_setting_tabs class CreateAPIToken(FlaskForm): diff --git a/app/blueprints/notifications/__init__.py b/app/blueprints/notifications/__init__.py index 3f87ca0a..f83d63f1 100644 --- a/app/blueprints/notifications/__init__.py +++ b/app/blueprints/notifications/__init__.py @@ -17,10 +17,7 @@ from flask import Blueprint, render_template, redirect, url_for from flask_login import current_user, login_required -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 +from app.models import db, Notification bp = Blueprint("notifications", __name__) @@ -37,45 +34,3 @@ 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, is_new=is_new, - tabs=get_setting_tabs(current_user), current_tab="notifications") diff --git a/app/blueprints/users/__init__.py b/app/blueprints/users/__init__.py index 3a2814ee..2b47d457 100644 --- a/app/blueprints/users/__init__.py +++ b/app/blueprints/users/__init__.py @@ -2,4 +2,4 @@ from flask import Blueprint bp = Blueprint("users", __name__) -from . import profile, claim, account +from . import profile, claim, account, settings diff --git a/app/blueprints/users/profile.py b/app/blueprints/users/profile.py index 6424c29f..c11c0b8d 100644 --- a/app/blueprints/users/profile.py +++ b/app/blueprints/users/profile.py @@ -24,24 +24,12 @@ from wtforms.validators import * from app.markdown import render_markdown from app.models import * -from app.tasks.emails import sendVerifyEmail, sendEmailRaw +from app.tasks.emails import sendEmailRaw from app.tasks.forumtasks import checkForumAccount -from app.utils import randomString, rank_required, nonEmptyOrNone, addAuditLog, make_flask_login_password +from app.utils import rank_required, addAuditLog from . import bp -# Define the User profile form -class UserProfileForm(FlaskForm): - display_name = StringField("Display name", [Optional(), Length(2, 100)]) - forums_username = StringField("Forums Username", [Optional(), Length(2, 50)]) - github_username = StringField("GitHub Username", [Optional(), Length(2, 50)]) - email = StringField("Email", [Optional(), Email()], filters = [lambda x: x or None]) - website_url = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None]) - donate_url = StringField("Donation URL", [Optional(), URL()], filters = [lambda x: x or None]) - rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER) - submit = SubmitField("Save") - - @bp.route("/users/", methods=["GET"]) def list_all(): users = db.session.query(User, func.count(Package.id)) \ @@ -76,93 +64,6 @@ def profile(username): user=user, packages=packages, topics_to_add=topics_to_add) -def get_setting_tabs(user): - return [ - { - "id": "edit_profile", - "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", - "url": url_for("api.list_tokens", username=user.username) - }, - ] - - -@bp.route("/users//edit/", methods=["GET", "POST"]) -@login_required -def profile_edit(username): - user : User = User.query.filter_by(username=username).first() - if not user: - abort(404) - - if not user.can_see_edit_profile(current_user): - flash("Permission denied", "danger") - return redirect(url_for("users.profile", username=username)) - - - form = UserProfileForm(formdata=request.form, obj=user) - - # Process valid POST - if request.method=="POST" and form.validate(): - severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION - addAuditLog(severity, current_user, "Edited {}'s profile".format(user.display_name), - url_for("users.profile", username=username)) - - # Copy form fields to user_profile fields - if user.checkPerm(current_user, Permission.CHANGE_USERNAMES): - user.display_name = form.display_name.data - user.forums_username = nonEmptyOrNone(form.forums_username.data) - user.github_username = nonEmptyOrNone(form.github_username.data) - - if user.checkPerm(current_user, Permission.CHANGE_PROFILE_URLS): - user.website_url = form["website_url"].data - user.donate_url = form["donate_url"].data - - if user.checkPerm(current_user, Permission.CHANGE_RANK): - newRank = form["rank"].data - if current_user.rank.atLeast(newRank): - if newRank != user.rank: - user.rank = form["rank"].data - msg = "Set rank of {} to {}".format(user.display_name, user.rank.getTitle()) - addAuditLog(AuditSeverity.MODERATION, current_user, msg, url_for("users.profile", username=username)) - else: - flash("Can't promote a user to a rank higher than yourself!", "danger") - - if user.checkPerm(current_user, Permission.CHANGE_EMAIL): - newEmail = form["email"].data - if newEmail and newEmail != user.email and newEmail.strip() != "": - token = randomString(32) - - msg = "Changed email of {}".format(user.display_name) - addAuditLog(severity, current_user, msg, url_for("users.profile", username=username)) - - ver = UserEmailVerification() - ver.user = user - ver.token = token - ver.email = newEmail - db.session.add(ver) - db.session.commit() - - task = sendVerifyEmail.delay(newEmail, token) - return redirect(url_for("tasks.check", id=task.id, r=url_for("users.profile", username=username))) - - # Save user_profile - db.session.commit() - - return redirect(url_for("users.profile", username=username)) - - # Process GET or invalid POST - return render_template("users/profile_edit.html", user=user, form=form, tabs=get_setting_tabs(user), current_tab="edit_profile") - - @bp.route("/users//check/", methods=["POST"]) @login_required def user_check(username): @@ -188,7 +89,7 @@ class SendEmailForm(FlaskForm): submit = SubmitField("Send") -@bp.route("/users//email/", methods=["GET", "POST"]) +@bp.route("/users//send-email/", methods=["GET", "POST"]) @rank_required(UserRank.MODERATOR) def send_email(username): user = User.query.filter_by(username=username).first() diff --git a/app/blueprints/users/settings.py b/app/blueprints/users/settings.py new file mode 100644 index 00000000..2fcd1185 --- /dev/null +++ b/app/blueprints/users/settings.py @@ -0,0 +1,165 @@ +from flask import * +from flask_login import current_user, login_required +from flask_wtf import FlaskForm +from wtforms import * +from wtforms.validators import * + +from app.models import * +from app.utils import nonEmptyOrNone, addAuditLog, randomString +from app.tasks.emails import sendVerifyEmail +from . import bp + + +def get_setting_tabs(user): + return [ + { + "id": "edit_profile", + "title": "Edit Profile", + "url": url_for("users.profile_edit", username=user.username) + }, + { + "id": "notifications", + "title": "Email and Notifications", + "url": url_for("users.email_notifications", username=user.username) + }, + { + "id": "api_tokens", + "title": "API Tokens", + "url": url_for("api.list_tokens", username=user.username) + }, + ] + + +# Define the User profile form +class UserProfileForm(FlaskForm): + display_name = StringField("Display name", [Optional(), Length(2, 100)]) + forums_username = StringField("Forums Username", [Optional(), Length(2, 50)]) + github_username = StringField("GitHub Username", [Optional(), Length(2, 50)]) + website_url = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None]) + donate_url = StringField("Donation URL", [Optional(), URL()], filters = [lambda x: x or None]) + rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER) + submit = SubmitField("Save") + + +@bp.route("/users//settings/profile/", methods=["GET", "POST"]) +@login_required +def profile_edit(username): + user : User = User.query.filter_by(username=username).first() + if not user: + abort(404) + + if not user.can_see_edit_profile(current_user): + flash("Permission denied", "danger") + return redirect(url_for("users.profile", username=username)) + + + form = UserProfileForm(formdata=request.form, obj=user) + + # Process valid POST + if request.method=="POST" and form.validate(): + severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION + addAuditLog(severity, current_user, "Edited {}'s profile".format(user.display_name), + url_for("users.profile", username=username)) + + # Copy form fields to user_profile fields + if user.checkPerm(current_user, Permission.CHANGE_USERNAMES): + user.display_name = form.display_name.data + user.forums_username = nonEmptyOrNone(form.forums_username.data) + user.github_username = nonEmptyOrNone(form.github_username.data) + + if user.checkPerm(current_user, Permission.CHANGE_PROFILE_URLS): + user.website_url = form["website_url"].data + user.donate_url = form["donate_url"].data + + if user.checkPerm(current_user, Permission.CHANGE_RANK): + newRank = form["rank"].data + if current_user.rank.atLeast(newRank): + if newRank != user.rank: + user.rank = form["rank"].data + msg = "Set rank of {} to {}".format(user.display_name, user.rank.getTitle()) + addAuditLog(AuditSeverity.MODERATION, current_user, msg, url_for("users.profile", username=username)) + else: + flash("Can't promote a user to a rank higher than yourself!", "danger") + + # Save user_profile + db.session.commit() + + return redirect(url_for("users.profile", username=username)) + + # Process GET or invalid POST + return render_template("users/profile_edit.html", user=user, form=form, tabs=get_setting_tabs(user), current_tab="edit_profile") + + + + +def make_settings_form(): + attrs = { + "email": StringField("Email", [Optional(), Email()]), + "submit": SubmitField("Save") + } + + for notificationType in NotificationType: + key = "pref_" + notificationType.toName() + attrs[key] = BooleanField("") + + return type("SettingsForm", (FlaskForm,), attrs) + +SettingsForm = make_settings_form() + + +@bp.route("/users//settings/email/", methods=["GET", "POST"]) +@login_required +def email_notifications(username): + user: User = User.query.filter_by(username=username).first() + if not user: + abort(404) + + is_new = False + prefs = user.notification_preferences + if prefs is None: + is_new = True + prefs = UserNotificationPreferences(user) + + data = {} + types = [] + for notificationType in NotificationType: + types.append(notificationType) + data["pref_" + notificationType.toName()] = prefs.get_can_email(notificationType) + + data["email"] = user.email + + form = SettingsForm(data=data) + if form.validate_on_submit(): + for notificationType in NotificationType: + field = getattr(form, "pref_" + notificationType.toName()) + prefs.set_can_email(notificationType, field.data) + + if is_new: + db.session.add(prefs) + + if user.checkPerm(current_user, Permission.CHANGE_EMAIL): + newEmail = form.email.data + if newEmail and newEmail != user.email and newEmail.strip() != "": + token = randomString(32) + + severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION + + msg = "Changed email of {}".format(user.display_name) + addAuditLog(severity, current_user, msg, url_for("users.profile", username=username)) + + ver = UserEmailVerification() + ver.user = user + ver.token = token + ver.email = newEmail + db.session.add(ver) + db.session.commit() + + task = sendVerifyEmail.delay(newEmail, token) + return redirect(url_for("tasks.check", id=task.id, r=url_for("users.profile", username=username))) + + db.session.commit() + return redirect(url_for("notifications.settings")) + + return render_template("users/settings_email.html", + form=form, user=user, types=types, is_new=is_new, + tabs=get_setting_tabs(current_user), current_tab="notifications") diff --git a/app/models.py b/app/models.py index 0dfaec5f..c9a1609e 100644 --- a/app/models.py +++ b/app/models.py @@ -414,6 +414,13 @@ class UserNotificationPreferences(db.Model): self.pref_editor_misc = 0 self.pref_other = 0 + def get_can_email(self, type): + return getattr(self, "pref_" + type.toName()) == 2 + + def set_can_email(self, type, value): + value = 2 if value else 0 + setattr(self, "pref_" + type.toName(), value) + class License(db.Model): id = db.Column(db.Integer, primary_key=True) diff --git a/app/templates/users/profile_edit.html b/app/templates/users/profile_edit.html index 80df0860..10a88819 100644 --- a/app/templates/users/profile_edit.html +++ b/app/templates/users/profile_edit.html @@ -139,11 +139,6 @@ {{ render_field(form.donate_url, tabindex=233) }} {% endif %} - {% if user.checkPerm(current_user, "CHANGE_EMAIL") %} - {{ render_field(form.email, tabindex=240) }} - We'll send you an email to verify it if changed. - {% endif %} - {% if user.checkPerm(current_user, "CHANGE_RANK") %} {{ render_field(form.rank, tabindex=250) }} {% endif %} diff --git a/app/templates/notifications/settings.html b/app/templates/users/settings_email.html similarity index 58% rename from app/templates/notifications/settings.html rename to app/templates/users/settings_email.html index e5a08dc0..235fcf49 100644 --- a/app/templates/notifications/settings.html +++ b/app/templates/users/settings_email.html @@ -7,16 +7,29 @@ {% block pane %}

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

- {% if is_new %} -

- {{ _("Email notifications are currently turned off. Click 'Save' to apply recommended settings.") }} -

- {% endif %} - {% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}
{{ form.hidden_tag() }} +

Email Address

+ + {{ render_field(form.email, tabindex=100) }} + +

+ Your email is needed to recover your account if you forget your + password, and to optionally send notifications. + Your email will never be shared to a third-party. +

+ + +

Notification Settings

+ + {% if is_new %} +

+ {{ _("Email notifications are currently turned off. Click 'Save' to apply recommended settings.") }} +

+ {% endif %} + @@ -32,7 +45,8 @@ {% endfor %}
Event
- - {{ render_submit_field(form.submit, tabindex=280) }} +

+ {{ render_submit_field(form.submit, tabindex=280) }} +

{% endblock %}