From 9639cf04f1b54865884850d608f08b5df4afe169 Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Tue, 8 Jan 2019 17:37:33 +0000 Subject: Improve views subfoldering --- app/querybuilder.py | 16 +- app/views/__init__.py | 5 +- app/views/admin.py | 124 ---------------- app/views/admin/__init__.py | 18 +++ app/views/admin/admin.py | 124 ++++++++++++++++ app/views/admin/licenseseditor.py | 62 ++++++++ app/views/admin/tagseditor.py | 57 +++++++ app/views/admin/todo.py | 99 +++++++++++++ app/views/githublogin.py | 73 --------- app/views/licenseseditor.py | 62 -------- app/views/notifications.py | 33 ----- app/views/packages/__init__.py | 2 +- app/views/packages/todo.py | 99 ------------- app/views/tagseditor.py | 57 ------- app/views/users.py | 304 -------------------------------------- app/views/users/__init__.py | 18 +++ app/views/users/githublogin.py | 73 +++++++++ app/views/users/notifications.py | 33 +++++ app/views/users/users.py | 304 ++++++++++++++++++++++++++++++++++++++ 19 files changed, 799 insertions(+), 764 deletions(-) delete mode 100644 app/views/admin.py create mode 100644 app/views/admin/__init__.py create mode 100644 app/views/admin/admin.py create mode 100644 app/views/admin/licenseseditor.py create mode 100644 app/views/admin/tagseditor.py create mode 100644 app/views/admin/todo.py delete mode 100644 app/views/githublogin.py delete mode 100644 app/views/licenseseditor.py delete mode 100644 app/views/notifications.py delete mode 100644 app/views/packages/todo.py delete mode 100644 app/views/tagseditor.py delete mode 100644 app/views/users.py create mode 100644 app/views/users/__init__.py create mode 100644 app/views/users/githublogin.py create mode 100644 app/views/users/notifications.py create mode 100644 app/views/users/users.py diff --git a/app/querybuilder.py b/app/querybuilder.py index 9406406..a7f1323 100644 --- a/app/querybuilder.py +++ b/app/querybuilder.py @@ -28,13 +28,16 @@ class QueryBuilder: self.order_by = args.get("sort") or "score" self.order_dir = args.get("order") or "desc" + if self.search is not None and self.search.strip() == "": + self.search = None + def buildPackageQuery(self): query = Package.query.filter_by(soft_deleted=False, approved=True) if len(self.types) > 0: query = query.filter(Package.type.in_(self.types)) - if self.search is not None and self.search.strip() != "": + if self.search: query = query.filter(Package.title.ilike('%' + self.search + '%')) if self.random: @@ -69,17 +72,14 @@ class QueryBuilder: def buildTopicQuery(self): topics = ForumTopic.query \ .filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \ - .order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \ - .filter(ForumTopic.title.ilike('%' + self.search + '%')) + .order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title)) + + if self.search: + topics = topics.filter(ForumTopic.title.ilike('%' + self.search + '%')) if len(self.types) > 0: topics = topics.filter(ForumTopic.type.in_(self.types)) - if self.hide_nonfree: - topics = topics \ - .filter(Package.license.has(License.is_foss == True)) \ - .filter(Package.media_license.has(License.is_foss == True)) - if self.limit: topics = topics.limit(self.limit) diff --git a/app/views/__init__.py b/app/views/__init__.py index 7ca7620..303ed2a 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -56,9 +56,8 @@ def home_page(): return render_template("index.html", count=count, \ new=new, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam) -from . import users, githublogin, packages, meta, threads, api -from . import tasks, admin, notifications, tagseditor, licenseseditor -from . import sass, thumbnails +from . import users, packages, meta, threads, api +from . import sass, thumbnails, tasks, admin @menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' }) @app.route('//') diff --git a/app/views/admin.py b/app/views/admin.py deleted file mode 100644 index b2b615d..0000000 --- a/app/views/admin.py +++ /dev/null @@ -1,124 +0,0 @@ -# Content DB -# Copyright (C) 2018 rubenwardy -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -from flask import * -from flask_user import * -import flask_menu as menu -from app import app -from app.models import * -from celery import uuid -from app.tasks.importtasks import importRepoScreenshot, importAllDependencies, makeVCSRelease -from app.tasks.forumtasks import importTopicList, checkAllForumAccounts -from flask_wtf import FlaskForm -from wtforms import * -from app.utils import loginUser, rank_required, triggerNotif -import datetime - -@app.route("/admin/", methods=["GET", "POST"]) -@rank_required(UserRank.ADMIN) -def admin_page(): - if request.method == "POST": - action = request.form["action"] - if action == "importmodlist": - task = importTopicList.delay() - return redirect(url_for("check_task", id=task.id, r=url_for("todo_topics_page"))) - elif action == "checkusers": - task = checkAllForumAccounts.delay() - return redirect(url_for("check_task", id=task.id, r=url_for("admin_page"))) - elif action == "importscreenshots": - packages = Package.query \ - .filter_by(soft_deleted=False) \ - .outerjoin(PackageScreenshot, Package.id==PackageScreenshot.package_id) \ - .filter(PackageScreenshot.id==None) \ - .all() - for package in packages: - importRepoScreenshot.delay(package.id) - - return redirect(url_for("admin_page")) - elif action == "restore": - package = Package.query.get(request.form["package"]) - if package is None: - flash("Unknown package", "error") - else: - package.soft_deleted = False - db.session.commit() - return redirect(url_for("admin_page")) - elif action == "importdepends": - task = importAllDependencies.delay() - return redirect(url_for("check_task", id=task.id, r=url_for("admin_page"))) - elif action == "modprovides": - packages = Package.query.filter_by(type=PackageType.MOD).all() - mpackage_cache = {} - for p in packages: - if len(p.provides) == 0: - p.provides.append(MetaPackage.GetOrCreate(p.name, mpackage_cache)) - - db.session.commit() - return redirect(url_for("admin_page")) - elif action == "recalcscores": - for p in Package.query.all(): - p.recalcScore() - - db.session.commit() - return redirect(url_for("admin_page")) - elif action == "vcsrelease": - for package in Package.query.filter(Package.repo.isnot(None)).all(): - if package.releases.count() != 0: - continue - - rel = PackageRelease() - rel.package = package - rel.title = datetime.date.today().isoformat() - rel.url = "" - rel.task_id = uuid() - rel.approved = True - db.session.add(rel) - db.session.commit() - - makeVCSRelease.apply_async((rel.id, "master"), task_id=rel.task_id) - - msg = "{}: Release {} created".format(package.title, rel.title) - triggerNotif(package.author, current_user, msg, rel.getEditURL()) - db.session.commit() - - else: - flash("Unknown action: " + action, "error") - - deleted_packages = Package.query.filter_by(soft_deleted=True).all() - return render_template("admin/list.html", deleted_packages=deleted_packages) - -class SwitchUserForm(FlaskForm): - username = StringField("Username") - submit = SubmitField("Switch") - - -@app.route("/admin/switchuser/", methods=["GET", "POST"]) -@rank_required(UserRank.ADMIN) -def switch_user_page(): - form = SwitchUserForm(formdata=request.form) - if request.method == "POST" and form.validate(): - user = User.query.filter_by(username=form["username"].data).first() - if user is None: - flash("Unable to find user", "error") - elif loginUser(user): - return redirect(url_for("user_profile_page", username=current_user.username)) - else: - flash("Unable to login as user", "error") - - - # Process GET or invalid POST - return render_template("admin/switch_user_page.html", form=form) diff --git a/app/views/admin/__init__.py b/app/views/admin/__init__.py new file mode 100644 index 0000000..b4b4f99 --- /dev/null +++ b/app/views/admin/__init__.py @@ -0,0 +1,18 @@ +# Content DB +# Copyright (C) 2018 rubenwardy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from . import admin, licenseseditor, tagseditor, todo diff --git a/app/views/admin/admin.py b/app/views/admin/admin.py new file mode 100644 index 0000000..b2b615d --- /dev/null +++ b/app/views/admin/admin.py @@ -0,0 +1,124 @@ +# Content DB +# Copyright (C) 2018 rubenwardy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from flask import * +from flask_user import * +import flask_menu as menu +from app import app +from app.models import * +from celery import uuid +from app.tasks.importtasks import importRepoScreenshot, importAllDependencies, makeVCSRelease +from app.tasks.forumtasks import importTopicList, checkAllForumAccounts +from flask_wtf import FlaskForm +from wtforms import * +from app.utils import loginUser, rank_required, triggerNotif +import datetime + +@app.route("/admin/", methods=["GET", "POST"]) +@rank_required(UserRank.ADMIN) +def admin_page(): + if request.method == "POST": + action = request.form["action"] + if action == "importmodlist": + task = importTopicList.delay() + return redirect(url_for("check_task", id=task.id, r=url_for("todo_topics_page"))) + elif action == "checkusers": + task = checkAllForumAccounts.delay() + return redirect(url_for("check_task", id=task.id, r=url_for("admin_page"))) + elif action == "importscreenshots": + packages = Package.query \ + .filter_by(soft_deleted=False) \ + .outerjoin(PackageScreenshot, Package.id==PackageScreenshot.package_id) \ + .filter(PackageScreenshot.id==None) \ + .all() + for package in packages: + importRepoScreenshot.delay(package.id) + + return redirect(url_for("admin_page")) + elif action == "restore": + package = Package.query.get(request.form["package"]) + if package is None: + flash("Unknown package", "error") + else: + package.soft_deleted = False + db.session.commit() + return redirect(url_for("admin_page")) + elif action == "importdepends": + task = importAllDependencies.delay() + return redirect(url_for("check_task", id=task.id, r=url_for("admin_page"))) + elif action == "modprovides": + packages = Package.query.filter_by(type=PackageType.MOD).all() + mpackage_cache = {} + for p in packages: + if len(p.provides) == 0: + p.provides.append(MetaPackage.GetOrCreate(p.name, mpackage_cache)) + + db.session.commit() + return redirect(url_for("admin_page")) + elif action == "recalcscores": + for p in Package.query.all(): + p.recalcScore() + + db.session.commit() + return redirect(url_for("admin_page")) + elif action == "vcsrelease": + for package in Package.query.filter(Package.repo.isnot(None)).all(): + if package.releases.count() != 0: + continue + + rel = PackageRelease() + rel.package = package + rel.title = datetime.date.today().isoformat() + rel.url = "" + rel.task_id = uuid() + rel.approved = True + db.session.add(rel) + db.session.commit() + + makeVCSRelease.apply_async((rel.id, "master"), task_id=rel.task_id) + + msg = "{}: Release {} created".format(package.title, rel.title) + triggerNotif(package.author, current_user, msg, rel.getEditURL()) + db.session.commit() + + else: + flash("Unknown action: " + action, "error") + + deleted_packages = Package.query.filter_by(soft_deleted=True).all() + return render_template("admin/list.html", deleted_packages=deleted_packages) + +class SwitchUserForm(FlaskForm): + username = StringField("Username") + submit = SubmitField("Switch") + + +@app.route("/admin/switchuser/", methods=["GET", "POST"]) +@rank_required(UserRank.ADMIN) +def switch_user_page(): + form = SwitchUserForm(formdata=request.form) + if request.method == "POST" and form.validate(): + user = User.query.filter_by(username=form["username"].data).first() + if user is None: + flash("Unable to find user", "error") + elif loginUser(user): + return redirect(url_for("user_profile_page", username=current_user.username)) + else: + flash("Unable to login as user", "error") + + + # Process GET or invalid POST + return render_template("admin/switch_user_page.html", form=form) diff --git a/app/views/admin/licenseseditor.py b/app/views/admin/licenseseditor.py new file mode 100644 index 0000000..343f4ee --- /dev/null +++ b/app/views/admin/licenseseditor.py @@ -0,0 +1,62 @@ +# Content DB +# Copyright (C) 2018 rubenwardy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from flask import * +from flask_user import * +from app import app +from app.models import * +from flask_wtf import FlaskForm +from wtforms import * +from wtforms.validators import * +from app.utils import rank_required + +@app.route("/licenses/") +@rank_required(UserRank.MODERATOR) +def license_list_page(): + return render_template("admin/licenses/list.html", licenses=License.query.order_by(db.asc(License.name)).all()) + +class LicenseForm(FlaskForm): + name = StringField("Name", [InputRequired(), Length(3,100)]) + is_foss = BooleanField("Is FOSS") + submit = SubmitField("Save") + +@app.route("/licenses/new/", methods=["GET", "POST"]) +@app.route("/licenses//edit/", methods=["GET", "POST"]) +@rank_required(UserRank.MODERATOR) +def createedit_license_page(name=None): + license = None + if name is not None: + license = License.query.filter_by(name=name).first() + if license is None: + abort(404) + + form = LicenseForm(formdata=request.form, obj=license) + if request.method == "GET" and license is None: + form.is_foss.data = True + elif request.method == "POST" and form.validate(): + if license is None: + license = License(form.name.data) + db.session.add(license) + flash("Created license " + form.name.data, "success") + else: + flash("Updated license " + form.name.data, "success") + + form.populate_obj(license) + db.session.commit() + return redirect(url_for("license_list_page")) + + return render_template("admin/licenses/edit.html", license=license, form=form) diff --git a/app/views/admin/tagseditor.py b/app/views/admin/tagseditor.py new file mode 100644 index 0000000..7d88f28 --- /dev/null +++ b/app/views/admin/tagseditor.py @@ -0,0 +1,57 @@ +# Content DB +# Copyright (C) 2018 rubenwardy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from flask import * +from flask_user import * +from app import app +from app.models import * +from flask_wtf import FlaskForm +from wtforms import * +from wtforms.validators import * +from app.utils import rank_required + +@app.route("/tags/") +@rank_required(UserRank.MODERATOR) +def tag_list_page(): + return render_template("admin/tags/list.html", tags=Tag.query.order_by(db.asc(Tag.title)).all()) + +class TagForm(FlaskForm): + title = StringField("Title", [InputRequired(), Length(3,100)]) + name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")]) + submit = SubmitField("Save") + +@app.route("/tags/new/", methods=["GET", "POST"]) +@app.route("/tags//edit/", methods=["GET", "POST"]) +@rank_required(UserRank.MODERATOR) +def createedit_tag_page(name=None): + tag = None + if name is not None: + tag = Tag.query.filter_by(name=name).first() + if tag is None: + abort(404) + + form = TagForm(formdata=request.form, obj=tag) + if request.method == "POST" and form.validate(): + if tag is None: + tag = Tag(form.title.data) + db.session.add(tag) + else: + form.populate_obj(tag) + db.session.commit() + return redirect(url_for("createedit_tag_page", name=tag.name)) + + return render_template("admin/tags/edit.html", tag=tag, form=form) diff --git a/app/views/admin/todo.py b/app/views/admin/todo.py new file mode 100644 index 0000000..47b8cb5 --- /dev/null +++ b/app/views/admin/todo.py @@ -0,0 +1,99 @@ +# Content DB +# Copyright (C) 2018 rubenwardy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from flask import * +from flask_user import * +import flask_menu as menu +from app import app +from app.models import * + +@app.route("/todo/") +@login_required +def todo_page(): + canApproveNew = Permission.APPROVE_NEW.check(current_user) + canApproveRel = Permission.APPROVE_RELEASE.check(current_user) + canApproveScn = Permission.APPROVE_SCREENSHOT.check(current_user) + + packages = None + if canApproveNew: + packages = Package.query.filter_by(approved=False, soft_deleted=False).all() + + releases = None + if canApproveRel: + releases = PackageRelease.query.filter_by(approved=False).all() + + screenshots = None + if canApproveScn: + screenshots = PackageScreenshot.query.filter_by(approved=False).all() + + + topics_to_add = ForumTopic.query \ + .filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \ + .filter_by(discarded=False) \ + .count() + + return render_template("todo/list.html", title="Reports and Work Queue", + packages=packages, releases=releases, screenshots=screenshots, + canApproveNew=canApproveNew, canApproveRel=canApproveRel, canApproveScn=canApproveScn, + topics_to_add=topics_to_add) + + +@app.route("/todo/topics/") +@login_required +def todo_topics_page(): + query = ForumTopic.query + + show_discarded = request.args.get("show_discarded") == "True" + if not show_discarded: + query = query.filter_by(discarded=False) + + total = query.count() + + query = query.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \ + + sort_by = request.args.get("sort") + if sort_by == "name": + query = query.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title)) + elif sort_by == "views": + query = query.order_by(db.desc(ForumTopic.views)) + elif sort_by is None or sort_by == "date": + query = query.order_by(db.asc(ForumTopic.created_at)) + sort_by = "date" + + topic_count = query.count() + + search = request.args.get("q") + if search is not None and search.strip() != "": + query = query.filter(ForumTopic.title.ilike('%' + search + '%')) + + page = int(request.args.get("page") or 1) + num = int(request.args.get("n") or 100) + if num > 100 and not current_user.rank.atLeast(UserRank.EDITOR): + num = 100 + + query = query.paginate(page, num, True) + next_url = url_for("todo_topics_page", page=query.next_num, query=search, \ + show_discarded=show_discarded, n=num, sort=sort_by) \ + if query.has_next else None + prev_url = url_for("todo_topics_page", page=query.prev_num, query=search, \ + show_discarded=show_discarded, n=num, sort=sort_by) \ + if query.has_prev else None + + return render_template("todo/topics.html", topics=query.items, total=total, \ + topic_count=topic_count, query=search, show_discarded=show_discarded, \ + next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, \ + n=num, sort_by=sort_by) diff --git a/app/views/githublogin.py b/app/views/githublogin.py deleted file mode 100644 index 9ea2584..0000000 --- a/app/views/githublogin.py +++ /dev/null @@ -1,73 +0,0 @@ -# Content DB -# Copyright (C) 2018 rubenwardy -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -from flask import * -from flask_user import * -from flask_login import login_user, logout_user -from sqlalchemy import func -import flask_menu as menu -from flask_github import GitHub -from app import app, github -from app.models import * -from app.utils import loginUser - -@app.route("/user/github/start/") -def github_signin_page(): - return github.authorize("") - -@app.route("/user/github/callback/") -@github.authorized_handler -def github_authorized(oauth_token): - next_url = request.args.get("next") - if oauth_token is None: - flash("Authorization failed [err=gh-oauth-login-failed]", "danger") - return redirect(url_for("user.login")) - - import requests - - # Get Github username - url = "https://api.github.com/user" - r = requests.get(url, headers={"Authorization": "token " + oauth_token}) - username = r.json()["login"] - - # Get user by github username - userByGithub = User.query.filter(func.lower(User.github_username) == func.lower(username)).first() - - # If logged in, connect - if current_user and current_user.is_authenticated: - if userByGithub is None: - current_user.github_username = username - db.session.commit() - flash("Linked github to account", "success") - return redirect(url_for("home_page")) - else: - flash("Github account is already associated with another user", "danger") - return redirect(url_for("home_page")) - - # If not logged in, log in - else: - if userByGithub is None: - flash("Unable to find an account for that Github user", "error") - return redirect(url_for("user_claim_page")) - elif loginUser(userByGithub): - if current_user.password is None: - return redirect(next_url or url_for("set_password_page", optional=True)) - else: - return redirect(next_url or url_for("home_page")) - else: - flash("Authorization failed [err=gh-login-failed]", "danger") - return redirect(url_for("user.login")) diff --git a/app/views/licenseseditor.py b/app/views/licenseseditor.py deleted file mode 100644 index 343f4ee..0000000 --- a/app/views/licenseseditor.py +++ /dev/null @@ -1,62 +0,0 @@ -# Content DB -# Copyright (C) 2018 rubenwardy -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -from flask import * -from flask_user import * -from app import app -from app.models import * -from flask_wtf import FlaskForm -from wtforms import * -from wtforms.validators import * -from app.utils import rank_required - -@app.route("/licenses/") -@rank_required(UserRank.MODERATOR) -def license_list_page(): - return render_template("admin/licenses/list.html", licenses=License.query.order_by(db.asc(License.name)).all()) - -class LicenseForm(FlaskForm): - name = StringField("Name", [InputRequired(), Length(3,100)]) - is_foss = BooleanField("Is FOSS") - submit = SubmitField("Save") - -@app.route("/licenses/new/", methods=["GET", "POST"]) -@app.route("/licenses//edit/", methods=["GET", "POST"]) -@rank_required(UserRank.MODERATOR) -def createedit_license_page(name=None): - license = None - if name is not None: - license = License.query.filter_by(name=name).first() - if license is None: - abort(404) - - form = LicenseForm(formdata=request.form, obj=license) - if request.method == "GET" and license is None: - form.is_foss.data = True - elif request.method == "POST" and form.validate(): - if license is None: - license = License(form.name.data) - db.session.add(license) - flash("Created license " + form.name.data, "success") - else: - flash("Updated license " + form.name.data, "success") - - form.populate_obj(license) - db.session.commit() - return redirect(url_for("license_list_page")) - - return render_template("admin/licenses/edit.html", license=license, form=form) diff --git a/app/views/notifications.py b/app/views/notifications.py deleted file mode 100644 index 23dbb31..0000000 --- a/app/views/notifications.py +++ /dev/null @@ -1,33 +0,0 @@ -# Content DB -# Copyright (C) 2018 rubenwardy -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -from flask import * -from flask_user import current_user, login_required -from app import app -from app.models import * - -@app.route("/notifications/") -@login_required -def notifications_page(): - return render_template("notifications/list.html") - -@app.route("/notifications/clear/", methods=["POST"]) -@login_required -def clear_notifications_page(): - current_user.notifications.clear() - db.session.commit() - return redirect(url_for("notifications_page")) diff --git a/app/views/packages/__init__.py b/app/views/packages/__init__.py index 8bb6c1a..5df5376 100644 --- a/app/views/packages/__init__.py +++ b/app/views/packages/__init__.py @@ -15,4 +15,4 @@ # along with this program. If not, see . -from . import packages, todo, screenshots, releases +from . import packages, screenshots, releases diff --git a/app/views/packages/todo.py b/app/views/packages/todo.py deleted file mode 100644 index 47b8cb5..0000000 --- a/app/views/packages/todo.py +++ /dev/null @@ -1,99 +0,0 @@ -# Content DB -# Copyright (C) 2018 rubenwardy -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -from flask import * -from flask_user import * -import flask_menu as menu -from app import app -from app.models import * - -@app.route("/todo/") -@login_required -def todo_page(): - canApproveNew = Permission.APPROVE_NEW.check(current_user) - canApproveRel = Permission.APPROVE_RELEASE.check(current_user) - canApproveScn = Permission.APPROVE_SCREENSHOT.check(current_user) - - packages = None - if canApproveNew: - packages = Package.query.filter_by(approved=False, soft_deleted=False).all() - - releases = None - if canApproveRel: - releases = PackageRelease.query.filter_by(approved=False).all() - - screenshots = None - if canApproveScn: - screenshots = PackageScreenshot.query.filter_by(approved=False).all() - - - topics_to_add = ForumTopic.query \ - .filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \ - .filter_by(discarded=False) \ - .count() - - return render_template("todo/list.html", title="Reports and Work Queue", - packages=packages, releases=releases, screenshots=screenshots, - canApproveNew=canApproveNew, canApproveRel=canApproveRel, canApproveScn=canApproveScn, - topics_to_add=topics_to_add) - - -@app.route("/todo/topics/") -@login_required -def todo_topics_page(): - query = ForumTopic.query - - show_discarded = request.args.get("show_discarded") == "True" - if not show_discarded: - query = query.filter_by(discarded=False) - - total = query.count() - - query = query.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \ - - sort_by = request.args.get("sort") - if sort_by == "name": - query = query.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title)) - elif sort_by == "views": - query = query.order_by(db.desc(ForumTopic.views)) - elif sort_by is None or sort_by == "date": - query = query.order_by(db.asc(ForumTopic.created_at)) - sort_by = "date" - - topic_count = query.count() - - search = request.args.get("q") - if search is not None and search.strip() != "": - query = query.filter(ForumTopic.title.ilike('%' + search + '%')) - - page = int(request.args.get("page") or 1) - num = int(request.args.get("n") or 100) - if num > 100 and not current_user.rank.atLeast(UserRank.EDITOR): - num = 100 - - query = query.paginate(page, num, True) - next_url = url_for("todo_topics_page", page=query.next_num, query=search, \ - show_discarded=show_discarded, n=num, sort=sort_by) \ - if query.has_next else None - prev_url = url_for("todo_topics_page", page=query.prev_num, query=search, \ - show_discarded=show_discarded, n=num, sort=sort_by) \ - if query.has_prev else None - - return render_template("todo/topics.html", topics=query.items, total=total, \ - topic_count=topic_count, query=search, show_discarded=show_discarded, \ - next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, \ - n=num, sort_by=sort_by) diff --git a/app/views/tagseditor.py b/app/views/tagseditor.py deleted file mode 100644 index 7d88f28..0000000 --- a/app/views/tagseditor.py +++ /dev/null @@ -1,57 +0,0 @@ -# Content DB -# Copyright (C) 2018 rubenwardy -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -from flask import * -from flask_user import * -from app import app -from app.models import * -from flask_wtf import FlaskForm -from wtforms import * -from wtforms.validators import * -from app.utils import rank_required - -@app.route("/tags/") -@rank_required(UserRank.MODERATOR) -def tag_list_page(): - return render_template("admin/tags/list.html", tags=Tag.query.order_by(db.asc(Tag.title)).all()) - -class TagForm(FlaskForm): - title = StringField("Title", [InputRequired(), Length(3,100)]) - name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")]) - submit = SubmitField("Save") - -@app.route("/tags/new/", methods=["GET", "POST"]) -@app.route("/tags//edit/", methods=["GET", "POST"]) -@rank_required(UserRank.MODERATOR) -def createedit_tag_page(name=None): - tag = None - if name is not None: - tag = Tag.query.filter_by(name=name).first() - if tag is None: - abort(404) - - form = TagForm(formdata=request.form, obj=tag) - if request.method == "POST" and form.validate(): - if tag is None: - tag = Tag(form.title.data) - db.session.add(tag) - else: - form.populate_obj(tag) - db.session.commit() - return redirect(url_for("createedit_tag_page", name=tag.name)) - - return render_template("admin/tags/edit.html", tag=tag, form=form) diff --git a/app/views/users.py b/app/views/users.py deleted file mode 100644 index 6317d6b..0000000 --- a/app/views/users.py +++ /dev/null @@ -1,304 +0,0 @@ -# Content DB -# Copyright (C) 2018 rubenwardy -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -from flask import * -from flask_user import * -from flask_login import login_user, logout_user -from app import app, markdown -from app.models import * -from flask_wtf import FlaskForm -from wtforms import * -from wtforms.validators import * -from app.utils import randomString, loginUser, rank_required -from app.tasks.forumtasks import checkForumAccount -from app.tasks.emails import sendVerifyEmail, sendEmailRaw -from app.tasks.phpbbparser import getProfile - -# Define the User profile form -class UserProfileForm(FlaskForm): - display_name = StringField("Display name", [Optional(), Length(2, 20)]) - email = StringField("Email", [Optional(), Email()]) - rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER) - submit = SubmitField("Save") - -@app.route("/users/", methods=["GET"]) -@login_required -def user_list_page(): - users = User.query.order_by(db.desc(User.rank), db.asc(User.display_name)).all() - return render_template("users/list.html", users=users) - - -@app.route("/users//", methods=["GET", "POST"]) -def user_profile_page(username): - user = User.query.filter_by(username=username).first() - if not user: - abort(404) - - form = None - if user.checkPerm(current_user, Permission.CHANGE_DNAME) or \ - user.checkPerm(current_user, Permission.CHANGE_EMAIL) or \ - user.checkPerm(current_user, Permission.CHANGE_RANK): - # Initialize form - form = UserProfileForm(formdata=request.form, obj=user) - - # Process valid POST - if request.method=="POST" and form.validate(): - # Copy form fields to user_profile fields - if user.checkPerm(current_user, Permission.CHANGE_DNAME): - user.display_name = form["display_name"].data - - if user.checkPerm(current_user, Permission.CHANGE_RANK): - newRank = form["rank"].data - if current_user.rank.atLeast(newRank): - user.rank = form["rank"].data - else: - flash("Can't promote a user to a rank higher than yourself!", "error") - - if user.checkPerm(current_user, Permission.CHANGE_EMAIL): - newEmail = form["email"].data - if newEmail != user.email and newEmail.strip() != "": - token = randomString(32) - - 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("check_task", id=task.id, r=url_for("user_profile_page", username=username))) - - # Save user_profile - db.session.commit() - - # Redirect to home page - return redirect(url_for("user_profile_page", username=username)) - - packages = user.packages.filter_by(soft_deleted=False) - if not current_user.is_authenticated or (user != current_user and not current_user.canAccessTodoList()): - packages = packages.filter_by(approved=True) - packages = packages.order_by(db.asc(Package.title)) - - topics_to_add = None - if current_user == user or user.checkPerm(current_user, Permission.CHANGE_AUTHOR): - topics_to_add = ForumTopic.query \ - .filter_by(author_id=user.id) \ - .filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \ - .order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \ - .all() - - # Process GET or invalid POST - return render_template("users/user_profile_page.html", - user=user, form=form, packages=packages, topics_to_add=topics_to_add) - - -@app.route("/users//check/", methods=["POST"]) -@login_required -def user_check(username): - user = User.query.filter_by(username=username).first() - if user is None: - abort(404) - - if current_user != user and not current_user.rank.atLeast(UserRank.MODERATOR): - abort(403) - - if user.forums_username is None: - abort(404) - - task = checkForumAccount.delay(user.forums_username) - next_url = url_for("user_profile_page", username=username) - - return redirect(url_for("check_task", id=task.id, r=next_url)) - - -class SendEmailForm(FlaskForm): - subject = StringField("Subject", [InputRequired(), Length(1, 300)]) - text = TextAreaField("Message", [InputRequired()]) - submit = SubmitField("Send") - - -@app.route("/users//email/", methods=["GET", "POST"]) -@rank_required(UserRank.MODERATOR) -def send_email_page(username): - user = User.query.filter_by(username=username).first() - if user is None: - abort(404) - - next_url = url_for("user_profile_page", username=user.username) - - if user.email is None: - flash("User has no email address!", "error") - return redirect(next_url) - - form = SendEmailForm(request.form) - if form.validate_on_submit(): - text = form.text.data - html = markdown(text) - task = sendEmailRaw.delay([user.email], form.subject.data, text, html) - return redirect(url_for("check_task", id=task.id, r=next_url)) - - return render_template("users/send_email.html", form=form) - - - -class SetPasswordForm(FlaskForm): - email = StringField("Email", [Optional(), Email()]) - password = PasswordField("New password", [InputRequired(), Length(2, 20)]) - password2 = PasswordField("Verify password", [InputRequired(), Length(2, 20)]) - submit = SubmitField("Save") - -@app.route("/user/set-password/", methods=["GET", "POST"]) -@login_required -def set_password_page(): - if current_user.password is not None: - return redirect(url_for("user.change_password")) - - form = SetPasswordForm(request.form) - if current_user.email == None: - form.email.validators = [InputRequired(), Email()] - - if request.method == "POST" and form.validate(): - one = form.password.data - two = form.password2.data - if one == two: - # Hash password - hashed_password = user_manager.hash_password(form.password.data) - - # Change password - user_manager.update_password(current_user, hashed_password) - - # Send 'password_changed' email - if user_manager.enable_email and user_manager.send_password_changed_email and current_user.email: - emails.send_password_changed_email(current_user) - - # Send password_changed signal - signals.user_changed_password.send(current_app._get_current_object(), user=current_user) - - # Prepare one-time system message - flash('Your password has been changed successfully.', 'success') - - newEmail = form["email"].data - if newEmail != current_user.email and newEmail.strip() != "": - token = randomString(32) - - ver = UserEmailVerification() - ver.user = current_user - ver.token = token - ver.email = newEmail - db.session.add(ver) - db.session.commit() - - task = sendVerifyEmail.delay(newEmail, token) - return redirect(url_for("check_task", id=task.id, r=url_for("user_profile_page", username=current_user.username))) - else: - return redirect(url_for("user_profile_page", username=current_user.username)) - else: - flash("Passwords do not match", "error") - - return render_template("users/set_password.html", form=form, optional=request.args.get("optional")) - - -@app.route("/user/claim/", methods=["GET", "POST"]) -def user_claim_page(): - username = request.args.get("username") - if username is None: - username = "" - else: - method = request.args.get("method") - user = User.query.filter_by(forums_username=username).first() - if user and user.rank.atLeast(UserRank.NEW_MEMBER): - flash("User has already been claimed", "error") - return redirect(url_for("user_claim_page")) - elif user is None and method == "github": - flash("Unable to get Github username for user", "error") - return redirect(url_for("user_claim_page")) - elif user is None: - flash("Unable to find that user", "error") - return redirect(url_for("user_claim_page")) - - if user is not None and method == "github": - return redirect(url_for("github_signin_page")) - - token = None - if "forum_token" in session: - token = session["forum_token"] - else: - token = randomString(32) - session["forum_token"] = token - - if request.method == "POST": - ctype = request.form.get("claim_type") - username = request.form.get("username") - - if username is None or len(username.strip()) < 2: - flash("Invalid username", "error") - elif ctype == "github": - task = checkForumAccount.delay(username) - return redirect(url_for("check_task", id=task.id, r=url_for("user_claim_page", username=username, method="github"))) - elif ctype == "forum": - user = User.query.filter_by(forums_username=username).first() - if user is not None and user.rank.atLeast(UserRank.NEW_MEMBER): - flash("That user has already been claimed!", "error") - return redirect(url_for("user_claim_page")) - - # Get signature - sig = None - try: - profile = getProfile("https://forum.minetest.net", username) - sig = profile.signature - except IOError: - flash("Unable to get forum signature - does the user exist?", "error") - return redirect(url_for("user_claim_page", username=username)) - - # Look for key - if token in sig: - if user is None: - user = User(username) - user.forums_username = username - db.session.add(user) - db.session.commit() - - if loginUser(user): - return redirect(url_for("set_password_page")) - else: - flash("Unable to login as user", "error") - return redirect(url_for("user_claim_page", username=username)) - - else: - flash("Could not find the key in your signature!", "error") - return redirect(url_for("user_claim_page", username=username)) - else: - flash("Unknown claim type", "error") - - return render_template("users/claim.html", username=username, key=token) - -@app.route("/users/verify/") -def verify_email_page(): - token = request.args.get("token") - ver = UserEmailVerification.query.filter_by(token=token).first() - if ver is None: - flash("Unknown verification token!", "error") - else: - ver.user.email = ver.email - db.session.delete(ver) - db.session.commit() - - if current_user.is_authenticated: - return redirect(url_for("user_profile_page", username=current_user.username)) - else: - return redirect(url_for("home_page")) diff --git a/app/views/users/__init__.py b/app/views/users/__init__.py new file mode 100644 index 0000000..45af431 --- /dev/null +++ b/app/views/users/__init__.py @@ -0,0 +1,18 @@ +# Content DB +# Copyright (C) 2018 rubenwardy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from . import users, githublogin, notifications diff --git a/app/views/users/githublogin.py b/app/views/users/githublogin.py new file mode 100644 index 0000000..9ea2584 --- /dev/null +++ b/app/views/users/githublogin.py @@ -0,0 +1,73 @@ +# Content DB +# Copyright (C) 2018 rubenwardy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from flask import * +from flask_user import * +from flask_login import login_user, logout_user +from sqlalchemy import func +import flask_menu as menu +from flask_github import GitHub +from app import app, github +from app.models import * +from app.utils import loginUser + +@app.route("/user/github/start/") +def github_signin_page(): + return github.authorize("") + +@app.route("/user/github/callback/") +@github.authorized_handler +def github_authorized(oauth_token): + next_url = request.args.get("next") + if oauth_token is None: + flash("Authorization failed [err=gh-oauth-login-failed]", "danger") + return redirect(url_for("user.login")) + + import requests + + # Get Github username + url = "https://api.github.com/user" + r = requests.get(url, headers={"Authorization": "token " + oauth_token}) + username = r.json()["login"] + + # Get user by github username + userByGithub = User.query.filter(func.lower(User.github_username) == func.lower(username)).first() + + # If logged in, connect + if current_user and current_user.is_authenticated: + if userByGithub is None: + current_user.github_username = username + db.session.commit() + flash("Linked github to account", "success") + return redirect(url_for("home_page")) + else: + flash("Github account is already associated with another user", "danger") + return redirect(url_for("home_page")) + + # If not logged in, log in + else: + if userByGithub is None: + flash("Unable to find an account for that Github user", "error") + return redirect(url_for("user_claim_page")) + elif loginUser(userByGithub): + if current_user.password is None: + return redirect(next_url or url_for("set_password_page", optional=True)) + else: + return redirect(next_url or url_for("home_page")) + else: + flash("Authorization failed [err=gh-login-failed]", "danger") + return redirect(url_for("user.login")) diff --git a/app/views/users/notifications.py b/app/views/users/notifications.py new file mode 100644 index 0000000..23dbb31 --- /dev/null +++ b/app/views/users/notifications.py @@ -0,0 +1,33 @@ +# Content DB +# Copyright (C) 2018 rubenwardy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from flask import * +from flask_user import current_user, login_required +from app import app +from app.models import * + +@app.route("/notifications/") +@login_required +def notifications_page(): + return render_template("notifications/list.html") + +@app.route("/notifications/clear/", methods=["POST"]) +@login_required +def clear_notifications_page(): + current_user.notifications.clear() + db.session.commit() + return redirect(url_for("notifications_page")) diff --git a/app/views/users/users.py b/app/views/users/users.py new file mode 100644 index 0000000..6317d6b --- /dev/null +++ b/app/views/users/users.py @@ -0,0 +1,304 @@ +# Content DB +# Copyright (C) 2018 rubenwardy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from flask import * +from flask_user import * +from flask_login import login_user, logout_user +from app import app, markdown +from app.models import * +from flask_wtf import FlaskForm +from wtforms import * +from wtforms.validators import * +from app.utils import randomString, loginUser, rank_required +from app.tasks.forumtasks import checkForumAccount +from app.tasks.emails import sendVerifyEmail, sendEmailRaw +from app.tasks.phpbbparser import getProfile + +# Define the User profile form +class UserProfileForm(FlaskForm): + display_name = StringField("Display name", [Optional(), Length(2, 20)]) + email = StringField("Email", [Optional(), Email()]) + rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER) + submit = SubmitField("Save") + +@app.route("/users/", methods=["GET"]) +@login_required +def user_list_page(): + users = User.query.order_by(db.desc(User.rank), db.asc(User.display_name)).all() + return render_template("users/list.html", users=users) + + +@app.route("/users//", methods=["GET", "POST"]) +def user_profile_page(username): + user = User.query.filter_by(username=username).first() + if not user: + abort(404) + + form = None + if user.checkPerm(current_user, Permission.CHANGE_DNAME) or \ + user.checkPerm(current_user, Permission.CHANGE_EMAIL) or \ + user.checkPerm(current_user, Permission.CHANGE_RANK): + # Initialize form + form = UserProfileForm(formdata=request.form, obj=user) + + # Process valid POST + if request.method=="POST" and form.validate(): + # Copy form fields to user_profile fields + if user.checkPerm(current_user, Permission.CHANGE_DNAME): + user.display_name = form["display_name"].data + + if user.checkPerm(current_user, Permission.CHANGE_RANK): + newRank = form["rank"].data + if current_user.rank.atLeast(newRank): + user.rank = form["rank"].data + else: + flash("Can't promote a user to a rank higher than yourself!", "error") + + if user.checkPerm(current_user, Permission.CHANGE_EMAIL): + newEmail = form["email"].data + if newEmail != user.email and newEmail.strip() != "": + token = randomString(32) + + 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("check_task", id=task.id, r=url_for("user_profile_page", username=username))) + + # Save user_profile + db.session.commit() + + # Redirect to home page + return redirect(url_for("user_profile_page", username=username)) + + packages = user.packages.filter_by(soft_deleted=False) + if not current_user.is_authenticated or (user != current_user and not current_user.canAccessTodoList()): + packages = packages.filter_by(approved=True) + packages = packages.order_by(db.asc(Package.title)) + + topics_to_add = None + if current_user == user or user.checkPerm(current_user, Permission.CHANGE_AUTHOR): + topics_to_add = ForumTopic.query \ + .filter_by(author_id=user.id) \ + .filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \ + .order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \ + .all() + + # Process GET or invalid POST + return render_template("users/user_profile_page.html", + user=user, form=form, packages=packages, topics_to_add=topics_to_add) + + +@app.route("/users//check/", methods=["POST"]) +@login_required +def user_check(username): + user = User.query.filter_by(username=username).first() + if user is None: + abort(404) + + if current_user != user and not current_user.rank.atLeast(UserRank.MODERATOR): + abort(403) + + if user.forums_username is None: + abort(404) + + task = checkForumAccount.delay(user.forums_username) + next_url = url_for("user_profile_page", username=username) + + return redirect(url_for("check_task", id=task.id, r=next_url)) + + +class SendEmailForm(FlaskForm): + subject = StringField("Subject", [InputRequired(), Length(1, 300)]) + text = TextAreaField("Message", [InputRequired()]) + submit = SubmitField("Send") + + +@app.route("/users//email/", methods=["GET", "POST"]) +@rank_required(UserRank.MODERATOR) +def send_email_page(username): + user = User.query.filter_by(username=username).first() + if user is None: + abort(404) + + next_url = url_for("user_profile_page", username=user.username) + + if user.email is None: + flash("User has no email address!", "error") + return redirect(next_url) + + form = SendEmailForm(request.form) + if form.validate_on_submit(): + text = form.text.data + html = markdown(text) + task = sendEmailRaw.delay([user.email], form.subject.data, text, html) + return redirect(url_for("check_task", id=task.id, r=next_url)) + + return render_template("users/send_email.html", form=form) + + + +class SetPasswordForm(FlaskForm): + email = StringField("Email", [Optional(), Email()]) + password = PasswordField("New password", [InputRequired(), Length(2, 20)]) + password2 = PasswordField("Verify password", [InputRequired(), Length(2, 20)]) + submit = SubmitField("Save") + +@app.route("/user/set-password/", methods=["GET", "POST"]) +@login_required +def set_password_page(): + if current_user.password is not None: + return redirect(url_for("user.change_password")) + + form = SetPasswordForm(request.form) + if current_user.email == None: + form.email.validators = [InputRequired(), Email()] + + if request.method == "POST" and form.validate(): + one = form.password.data + two = form.password2.data + if one == two: + # Hash password + hashed_password = user_manager.hash_password(form.password.data) + + # Change password + user_manager.update_password(current_user, hashed_password) + + # Send 'password_changed' email + if user_manager.enable_email and user_manager.send_password_changed_email and current_user.email: + emails.send_password_changed_email(current_user) + + # Send password_changed signal + signals.user_changed_password.send(current_app._get_current_object(), user=current_user) + + # Prepare one-time system message + flash('Your password has been changed successfully.', 'success') + + newEmail = form["email"].data + if newEmail != current_user.email and newEmail.strip() != "": + token = randomString(32) + + ver = UserEmailVerification() + ver.user = current_user + ver.token = token + ver.email = newEmail + db.session.add(ver) + db.session.commit() + + task = sendVerifyEmail.delay(newEmail, token) + return redirect(url_for("check_task", id=task.id, r=url_for("user_profile_page", username=current_user.username))) + else: + return redirect(url_for("user_profile_page", username=current_user.username)) + else: + flash("Passwords do not match", "error") + + return render_template("users/set_password.html", form=form, optional=request.args.get("optional")) + + +@app.route("/user/claim/", methods=["GET", "POST"]) +def user_claim_page(): + username = request.args.get("username") + if username is None: + username = "" + else: + method = request.args.get("method") + user = User.query.filter_by(forums_username=username).first() + if user and user.rank.atLeast(UserRank.NEW_MEMBER): + flash("User has already been claimed", "error") + return redirect(url_for("user_claim_page")) + elif user is None and method == "github": + flash("Unable to get Github username for user", "error") + return redirect(url_for("user_claim_page")) + elif user is None: + flash("Unable to find that user", "error") + return redirect(url_for("user_claim_page")) + + if user is not None and method == "github": + return redirect(url_for("github_signin_page")) + + token = None + if "forum_token" in session: + token = session["forum_token"] + else: + token = randomString(32) + session["forum_token"] = token + + if request.method == "POST": + ctype = request.form.get("claim_type") + username = request.form.get("username") + + if username is None or len(username.strip()) < 2: + flash("Invalid username", "error") + elif ctype == "github": + task = checkForumAccount.delay(username) + return redirect(url_for("check_task", id=task.id, r=url_for("user_claim_page", username=username, method="github"))) + elif ctype == "forum": + user = User.query.filter_by(forums_username=username).first() + if user is not None and user.rank.atLeast(UserRank.NEW_MEMBER): + flash("That user has already been claimed!", "error") + return redirect(url_for("user_claim_page")) + + # Get signature + sig = None + try: + profile = getProfile("https://forum.minetest.net", username) + sig = profile.signature + except IOError: + flash("Unable to get forum signature - does the user exist?", "error") + return redirect(url_for("user_claim_page", username=username)) + + # Look for key + if token in sig: + if user is None: + user = User(username) + user.forums_username = username + db.session.add(user) + db.session.commit() + + if loginUser(user): + return redirect(url_for("set_password_page")) + else: + flash("Unable to login as user", "error") + return redirect(url_for("user_claim_page", username=username)) + + else: + flash("Could not find the key in your signature!", "error") + return redirect(url_for("user_claim_page", username=username)) + else: + flash("Unknown claim type", "error") + + return render_template("users/claim.html", username=username, key=token) + +@app.route("/users/verify/") +def verify_email_page(): + token = request.args.get("token") + ver = UserEmailVerification.query.filter_by(token=token).first() + if ver is None: + flash("Unknown verification token!", "error") + else: + ver.user.email = ver.email + db.session.delete(ver) + db.session.commit() + + if current_user.is_authenticated: + return redirect(url_for("user_profile_page", username=current_user.username)) + else: + return redirect(url_for("home_page")) -- cgit v1.2.3