tmp
log.txt
*.rdb
-uploads
-thumbnails
+app/public/uploads
+app/public/thumbnails
celerybeat-schedule
/data
RUN pip install gunicorn
COPY utils utils
-COPY app app
-COPY migrations migrations
COPY config.cfg ./config.cfg
+COPY migrations migrations
+COPY app app
+RUN mkdir /home/cdb/app/public/uploads/
RUN chown cdb:cdb /home/cdb -R
-
USER cdb
use_ssl=True,
base_url=None)
+from .sass import sass
+sass(app)
+
+
if not app.debug and app.config["MAIL_UTILS_ERROR_SEND_TO"]:
from .maillogger import register_mail_error_handler
register_mail_error_handler(app, mail)
@babel.localeselector
def get_locale():
- return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
+ return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
+
+from . import models, tasks, template_filters
+
+from .blueprints import create_blueprints
+create_blueprints(app)
+
+from flask_login import logout_user
+
+@app.route("/uploads/<path:path>")
+def send_upload(path):
+ return send_from_directory("public/uploads", path)
+@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
+@app.route('/<path:path>/')
+def flatpage(path):
+ page = pages.get_or_404(path)
+ template = page.meta.get('template', 'flatpage.html')
+ return render_template(template, page=page)
-from . import models, tasks
-from .views import *
+@app.before_request
+def check_for_ban():
+ if current_user.is_authenticated:
+ if current_user.rank == models.UserRank.BANNED:
+ flash("You have been banned.", "error")
+ logout_user()
+ return redirect(url_for('user.login'))
+ elif current_user.rank == models.UserRank.NOT_JOINED:
+ current_user.rank = models.UserRank.MEMBER
+ models.db.session.commit()
--- /dev/null
+import os, importlib
+
+def create_blueprints(app):
+ dir = os.path.dirname(os.path.realpath(__file__))
+ modules = next(os.walk(dir))[1]
+
+ for modname in modules:
+ if all(c.islower() for c in modname):
+ module = importlib.import_module("." + modname, __name__)
+ app.register_blueprint(module.bp)
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import Blueprint
+
+bp = Blueprint("admin", __name__)
+
+from . import admin, licenseseditor, tagseditor, versioneditor
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import *
+from flask_user import *
+import flask_menu as menu
+from . import bp
+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
+
+@bp.route("/admin/", methods=["GET", "POST"])
+@rank_required(UserRank.ADMIN)
+def admin_page():
+ if request.method == "POST":
+ action = request.form["action"]
+ if action == "delstuckreleases":
+ PackageRelease.query.filter(PackageRelease.task_id != None).delete()
+ db.session.commit()
+ return redirect(url_for("admin.admin_page"))
+ elif action == "importmodlist":
+ task = importTopicList.delay()
+ return redirect(url_for("tasks.check", id=task.id, r=url_for("todo.topics")))
+ elif action == "checkusers":
+ task = checkAllForumAccounts.delay()
+ return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.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.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.admin_page"))
+ elif action == "importdepends":
+ task = importAllDependencies.delay()
+ return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.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.admin_page"))
+ elif action == "recalcscores":
+ for p in Package.query.all():
+ p.recalcScore()
+
+ db.session.commit()
+ return redirect(url_for("admin.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")
+
+
+@bp.route("/admin/switchuser/", methods=["GET", "POST"])
+@rank_required(UserRank.ADMIN)
+def switch_user():
+ 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("users.profile", username=current_user.username))
+ else:
+ flash("Unable to login as user", "error")
+
+
+ # Process GET or invalid POST
+ return render_template("admin/switch_user.html", form=form)
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import *
+from flask_user import *
+from . import bp
+from app.models import *
+from flask_wtf import FlaskForm
+from wtforms import *
+from wtforms.validators import *
+from app.utils import rank_required
+
+@bp.route("/licenses/")
+@rank_required(UserRank.MODERATOR)
+def license_list():
+ 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")
+
+@bp.route("/licenses/new/", methods=["GET", "POST"])
+@bp.route("/licenses/<name>/edit/", methods=["GET", "POST"])
+@rank_required(UserRank.MODERATOR)
+def create_edit_license(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("admin.license_list"))
+
+ return render_template("admin/licenses/edit.html", license=license, form=form)
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import *
+from flask_user import *
+from . import bp
+from app.models import *
+from flask_wtf import FlaskForm
+from wtforms import *
+from wtforms.validators import *
+from app.utils import rank_required
+
+@bp.route("/tags/")
+@rank_required(UserRank.MODERATOR)
+def tag_list():
+ 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")
+
+@bp.route("/tags/new/", methods=["GET", "POST"])
+@bp.route("/tags/<name>/edit/", methods=["GET", "POST"])
+@rank_required(UserRank.MODERATOR)
+def create_edit_tag(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("admin.create_edit_tag", name=tag.name))
+
+ return render_template("admin/tags/edit.html", tag=tag, form=form)
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import *
+from flask_user import *
+from . import bp
+from app.models import *
+from flask_wtf import FlaskForm
+from wtforms import *
+from wtforms.validators import *
+from app.utils import rank_required
+
+@bp.route("/versions/")
+@rank_required(UserRank.MODERATOR)
+def version_list():
+ return render_template("admin/versions/list.html", versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all())
+
+class VersionForm(FlaskForm):
+ name = StringField("Name", [InputRequired(), Length(3,100)])
+ protocol = IntegerField("Protocol")
+ submit = SubmitField("Save")
+
+@bp.route("/versions/new/", methods=["GET", "POST"])
+@bp.route("/versions/<name>/edit/", methods=["GET", "POST"])
+@rank_required(UserRank.MODERATOR)
+def create_edit_version(name=None):
+ version = None
+ if name is not None:
+ version = MinetestRelease.query.filter_by(name=name).first()
+ if version is None:
+ abort(404)
+
+ form = VersionForm(formdata=request.form, obj=version)
+ if request.method == "POST" and form.validate():
+ if version is None:
+ version = MinetestRelease(form.name.data)
+ db.session.add(version)
+ flash("Created version " + form.name.data, "success")
+ else:
+ flash("Updated version " + form.name.data, "success")
+
+ form.populate_obj(version)
+ db.session.commit()
+ return redirect(url_for("admin.version_list"))
+
+ return render_template("admin/versions/edit.html", version=version, form=form)
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import *
+from flask_user import *
+from app.models import *
+from app.utils import is_package_page
+from app.querybuilder import QueryBuilder
+
+bp = Blueprint("api", __name__)
+
+@bp.route("/api/packages/")
+def packages():
+ qb = QueryBuilder(request.args)
+ query = qb.buildPackageQuery()
+ ver = qb.getMinetestVersion()
+
+ pkgs = [package.getAsDictionaryShort(current_app.config["BASE_URL"], version=ver) \
+ for package in query.all()]
+ return jsonify(pkgs)
+
+
+@bp.route("/api/packages/<author>/<name>/")
+@is_package_page
+def package(package):
+ return jsonify(package.getAsDictionary(current_app.config["BASE_URL"]))
+
+
+@bp.route("/api/packages/<author>/<name>/dependencies/")
+@is_package_page
+def package_dependencies(package):
+ ret = []
+
+ for dep in package.dependencies:
+ name = None
+ fulfilled_by = None
+
+ if dep.package:
+ name = dep.package.name
+ fulfilled_by = [ dep.package.getAsDictionaryKey() ]
+
+ elif dep.meta_package:
+ name = dep.meta_package.name
+ fulfilled_by = [ pkg.getAsDictionaryKey() for pkg in dep.meta_package.packages]
+
+ else:
+ raise "Malformed dependency"
+
+ ret.append({
+ "name": name,
+ "is_optional": dep.optional,
+ "packages": fulfilled_by
+ })
+
+ return jsonify(ret)
+
+
+@bp.route("/api/topics/")
+def topics():
+ qb = QueryBuilder(request.args)
+ query = qb.buildTopicQuery(show_added=True)
+ return jsonify([t.getAsDictionary() for t in query.all()])
+
+
+@bp.route("/api/topic_discard/", methods=["POST"])
+@login_required
+def topic_set_discard():
+ tid = request.args.get("tid")
+ discard = request.args.get("discard")
+ if tid is None or discard is None:
+ abort(400)
+
+ topic = ForumTopic.query.get(tid)
+ if not topic.checkPerm(current_user, Permission.TOPIC_DISCARD):
+ abort(403)
+
+ topic.discarded = discard == "true"
+ db.session.commit()
+
+ return jsonify(topic.getAsDictionary())
+
+
+@bp.route("/api/minetest_versions/")
+def versions():
+ return jsonify([{ "name": rel.name, "protocol_version": rel.protocol }\
+ for rel in MinetestRelease.query.all() if rel.getActual() is not None])
--- /dev/null
+from flask import Blueprint, render_template
+
+bp = Blueprint("homepage", __name__)
+
+from app.models import *
+import flask_menu as menu
+from sqlalchemy.sql.expression import func
+
+@bp.route("/")
+@menu.register_menu(bp, ".", "Home")
+def home_page():
+ query = Package.query.filter_by(approved=True, soft_deleted=False)
+ count = query.count()
+ new = query.order_by(db.desc(Package.created_at)).limit(8).all()
+ pop_mod = query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score)).limit(8).all()
+ pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(4).all()
+ pop_txp = query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score)).limit(4).all()
+ downloads = db.session.query(func.sum(PackageRelease.downloads)).first()[0]
+ return render_template("index.html", count=count, downloads=downloads, \
+ new=new, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam)
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import *
+
+bp = Blueprint("metapackages", __name__)
+
+from flask_user import *
+from app.models import *
+
+@bp.route("/metapackages/")
+def list_all():
+ mpackages = MetaPackage.query.order_by(db.asc(MetaPackage.name)).all()
+ return render_template("meta/list.html", mpackages=mpackages)
+
+@bp.route("/metapackages/<name>/")
+def view(name):
+ mpackage = MetaPackage.query.filter_by(name=name).first()
+ if mpackage is None:
+ abort(404)
+
+ return render_template("meta/view.html", mpackage=mpackage)
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import Blueprint
+from flask_user import current_user, login_required
+from app.models import db
+
+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():
+ current_user.notifications.clear()
+ db.session.commit()
+ return redirect(url_for("notifications.list_all"))
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+from flask import Blueprint
+
+bp = Blueprint("packages", __name__)
+
+from . import packages, screenshots, releases
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+from flask import *
+from flask_user import *
+from app import app
+from app.models import *
+
+from app.utils import *
+
+from flask_wtf import FlaskForm
+from wtforms import *
+from wtforms.validators import *
+from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
+
+from . import PackageForm
+
+
+class EditRequestForm(PackageForm):
+ edit_title = StringField("Edit Title", [InputRequired(), Length(1, 100)])
+ edit_desc = TextField("Edit Description", [Optional()])
+
+@app.route("/packages/<author>/<name>/requests/new/", methods=["GET","POST"])
+@app.route("/packages/<author>/<name>/requests/<id>/edit/", methods=["GET","POST"])
+@login_required
+@is_package_page
+def create_edit_editrequest_page(package, id=None):
+ edited_package = package
+
+ erequest = None
+ if id is not None:
+ erequest = EditRequest.query.get(id)
+ if erequest.package != package:
+ abort(404)
+
+ if not erequest.checkPerm(current_user, Permission.EDIT_EDITREQUEST):
+ abort(403)
+
+ if erequest.status != 0:
+ flash("Can't edit EditRequest, it has already been merged or rejected", "error")
+ return redirect(erequest.getURL())
+
+ edited_package = Package(package)
+ erequest.applyAll(edited_package)
+
+ form = EditRequestForm(request.form, obj=edited_package)
+ if request.method == "GET":
+ deps = edited_package.dependencies
+ form.harddep_str.data = ",".join([str(x) for x in deps if not x.optional])
+ form.softdep_str.data = ",".join([str(x) for x in deps if x.optional])
+ form.provides_str.data = MetaPackage.ListToSpec(edited_package.provides)
+
+ if request.method == "POST" and form.validate():
+ if erequest is None:
+ erequest = EditRequest()
+ erequest.package = package
+ erequest.author = current_user
+
+ erequest.title = form["edit_title"].data
+ erequest.desc = form["edit_desc"].data
+ db.session.add(erequest)
+
+ EditRequestChange.query.filter_by(request=erequest).delete()
+
+ wasChangeMade = False
+ for e in PackagePropertyKey:
+ newValue = form[e.name].data
+ oldValue = getattr(package, e.name)
+
+ newValueComp = newValue
+ oldValueComp = oldValue
+ if type(newValue) is str:
+ newValue = newValue.replace("\r\n", "\n")
+ newValueComp = newValue.strip()
+ oldValueComp = "" if oldValue is None else oldValue.strip()
+
+ if newValueComp != oldValueComp:
+ change = EditRequestChange()
+ change.request = erequest
+ change.key = e
+ change.oldValue = e.convert(oldValue)
+ change.newValue = e.convert(newValue)
+ db.session.add(change)
+ wasChangeMade = True
+
+ if wasChangeMade:
+ msg = "{}: Edit request #{} {}" \
+ .format(package.title, erequest.id, "created" if id is None else "edited")
+ triggerNotif(package.author, current_user, msg, erequest.getURL())
+ triggerNotif(erequest.author, current_user, msg, erequest.getURL())
+ db.session.commit()
+ return redirect(erequest.getURL())
+ else:
+ flash("No changes detected", "warning")
+ elif erequest is not None:
+ form["edit_title"].data = erequest.title
+ form["edit_desc"].data = erequest.desc
+
+ return render_template("packages/editrequest_create_edit.html", package=package, form=form)
+
+
+@app.route("/packages/<author>/<name>/requests/<id>/")
+@is_package_page
+def view_editrequest_page(package, id):
+ erequest = EditRequest.query.get(id)
+ if erequest is None or erequest.package != package:
+ abort(404)
+
+ clearNotifications(erequest.getURL())
+ return render_template("packages/editrequest_view.html", package=package, request=erequest)
+
+
+@app.route("/packages/<author>/<name>/requests/<id>/approve/", methods=["POST"])
+@is_package_page
+def approve_editrequest_page(package, id):
+ if not package.checkPerm(current_user, Permission.APPROVE_CHANGES):
+ flash("You don't have permission to do that.", "error")
+ return redirect(package.getDetailsURL())
+
+ erequest = EditRequest.query.get(id)
+ if erequest is None or erequest.package != package:
+ abort(404)
+
+ if erequest.status != 0:
+ flash("Edit request has already been resolved", "error")
+
+ else:
+ erequest.status = 1
+ erequest.applyAll(package)
+
+ msg = "{}: Edit request #{} merged".format(package.title, erequest.id)
+ triggerNotif(erequest.author, current_user, msg, erequest.getURL())
+ triggerNotif(package.author, current_user, msg, erequest.getURL())
+ db.session.commit()
+
+ return redirect(package.getDetailsURL())
+
+@app.route("/packages/<author>/<name>/requests/<id>/reject/", methods=["POST"])
+@is_package_page
+def reject_editrequest_page(package, id):
+ if not package.checkPerm(current_user, Permission.APPROVE_CHANGES):
+ flash("You don't have permission to do that.", "error")
+ return redirect(package.getDetailsURL())
+
+ erequest = EditRequest.query.get(id)
+ if erequest is None or erequest.package != package:
+ abort(404)
+
+ if erequest.status != 0:
+ flash("Edit request has already been resolved", "error")
+
+ else:
+ erequest.status = 2
+
+ msg = "{}: Edit request #{} rejected".format(package.title, erequest.id)
+ triggerNotif(erequest.author, current_user, msg, erequest.getURL())
+ triggerNotif(package.author, current_user, msg, erequest.getURL())
+ db.session.commit()
+
+ return redirect(package.getDetailsURL())
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import render_template, abort, request, redirect, url_for, flash
+from flask_user import current_user
+import flask_menu as menu
+
+from . import bp
+
+from app.models import *
+from app.querybuilder import QueryBuilder
+from app.tasks.importtasks import importRepoScreenshot
+from app.utils import *
+
+from flask_wtf import FlaskForm
+from wtforms import *
+from wtforms.validators import *
+from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
+from sqlalchemy import or_
+
+
+@menu.register_menu(bp, ".mods", "Mods", order=11, endpoint_arguments_constructor=lambda: { 'type': 'mod' })
+@menu.register_menu(bp, ".games", "Games", order=12, endpoint_arguments_constructor=lambda: { 'type': 'game' })
+@menu.register_menu(bp, ".txp", "Texture Packs", order=13, endpoint_arguments_constructor=lambda: { 'type': 'txp' })
+@menu.register_menu(bp, ".random", "Random", order=14, endpoint_arguments_constructor=lambda: { 'random': '1' })
+@bp.route("/packages/")
+def list_all():
+ qb = QueryBuilder(request.args)
+ query = qb.buildPackageQuery()
+ title = qb.title
+
+ if qb.lucky:
+ package = query.first()
+ if package:
+ return redirect(package.getDetailsURL())
+
+ topic = qb.buildTopicQuery().first()
+ if qb.search and topic:
+ return redirect("https://forum.minetest.net/viewtopic.php?t=" + str(topic.topic_id))
+
+ page = int(request.args.get("page") or 1)
+ num = min(40, int(request.args.get("n") or 100))
+ query = query.paginate(page, num, True)
+
+ search = request.args.get("q")
+ type_name = request.args.get("type")
+
+ next_url = url_for("packages.list_all", type=type_name, q=search, page=query.next_num) \
+ if query.has_next else None
+ prev_url = url_for("packages.list_all", type=type_name, q=search, page=query.prev_num) \
+ if query.has_prev else None
+
+ topics = None
+ if qb.search and not query.has_next:
+ topics = qb.buildTopicQuery().all()
+
+ tags = Tag.query.all()
+ return render_template("packages/list.html", \
+ title=title, packages=query.items, topics=topics, \
+ query=search, tags=tags, type=type_name, \
+ next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, packages_count=query.total)
+
+
+def getReleases(package):
+ if package.checkPerm(current_user, Permission.MAKE_RELEASE):
+ return package.releases.limit(5)
+ else:
+ return package.releases.filter_by(approved=True).limit(5)
+
+
+@bp.route("/packages/<author>/<name>/")
+@is_package_page
+def view(package):
+ clearNotifications(package.getDetailsURL())
+
+ alternatives = None
+ if package.type == PackageType.MOD:
+ alternatives = Package.query \
+ .filter_by(name=package.name, type=PackageType.MOD, soft_deleted=False) \
+ .filter(Package.id != package.id) \
+ .order_by(db.desc(Package.score)) \
+ .all()
+
+
+ show_similar_topics = current_user == package.author or \
+ package.checkPerm(current_user, Permission.APPROVE_NEW)
+
+ similar_topics = None if not show_similar_topics else \
+ ForumTopic.query \
+ .filter_by(name=package.name) \
+ .filter(ForumTopic.topic_id != package.forums) \
+ .filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
+ .order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
+ .all()
+
+ releases = getReleases(package)
+ requests = [r for r in package.requests if r.status == 0]
+
+ review_thread = package.review_thread
+ if review_thread is not None and not review_thread.checkPerm(current_user, Permission.SEE_THREAD):
+ review_thread = None
+
+ topic_error = None
+ topic_error_lvl = "warning"
+ if not package.approved and package.forums is not None:
+ errors = []
+ if Package.query.filter_by(forums=package.forums, soft_deleted=False).count() > 1:
+ errors.append("<b>Error: Another package already uses this forum topic!</b>")
+ topic_error_lvl = "danger"
+
+ topic = ForumTopic.query.get(package.forums)
+ if topic is not None:
+ if topic.author != package.author:
+ errors.append("<b>Error: Forum topic author doesn't match package author.</b>")
+ topic_error_lvl = "danger"
+
+ if topic.wip:
+ errors.append("Warning: Forum topic is in WIP section, make sure package meets playability standards.")
+ elif package.type != PackageType.TXP:
+ errors.append("Warning: Forum topic not found. This may happen if the topic has only just been created.")
+
+ topic_error = "<br />".join(errors)
+
+
+ threads = Thread.query.filter_by(package_id=package.id)
+ if not current_user.is_authenticated:
+ threads = threads.filter_by(private=False)
+ elif not current_user.rank.atLeast(UserRank.EDITOR) and not current_user == package.author:
+ threads = threads.filter(or_(Thread.private == False, Thread.author == current_user))
+
+
+ return render_template("packages/view.html", \
+ package=package, releases=releases, requests=requests, \
+ alternatives=alternatives, similar_topics=similar_topics, \
+ review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl, \
+ threads=threads.all())
+
+
+@bp.route("/packages/<author>/<name>/download/")
+@is_package_page
+def download(package):
+ release = package.getDownloadRelease()
+
+ if release is None:
+ if "application/zip" in request.accept_mimetypes and \
+ not "text/html" in request.accept_mimetypes:
+ return "", 204
+ else:
+ flash("No download available.", "error")
+ return redirect(package.getDetailsURL())
+ else:
+ PackageRelease.query.filter_by(id=release.id).update({
+ "downloads": PackageRelease.downloads + 1
+ })
+ db.session.commit()
+
+ return redirect(release.url, code=302)
+
+
+class PackageForm(FlaskForm):
+ name = StringField("Name (Technical)", [InputRequired(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
+ title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 50)])
+ short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
+ desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
+ type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
+ license = QuerySelectField("License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
+ media_license = QuerySelectField("Media License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
+ provides_str = StringField("Provides (mods included in package)", [Optional()])
+ tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title)
+ harddep_str = StringField("Hard Dependencies", [Optional()])
+ softdep_str = StringField("Soft Dependencies", [Optional()])
+ repo = StringField("VCS Repository URL", [Optional(), URL()], filters = [lambda x: x or None])
+ website = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None])
+ issueTracker = StringField("Issue Tracker URL", [Optional(), URL()], filters = [lambda x: x or None])
+ forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
+ submit = SubmitField("Save")
+
+@bp.route("/packages/new/", methods=["GET", "POST"])
+@bp.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
+@login_required
+def create_edit(author=None, name=None):
+ package = None
+ form = None
+ if author is None:
+ form = PackageForm(formdata=request.form)
+ author = request.args.get("author")
+ if author is None or author == current_user.username:
+ author = current_user
+ else:
+ author = User.query.filter_by(username=author).first()
+ if author is None:
+ flash("Unable to find that user", "error")
+ return redirect(url_for("packages.create_edit"))
+
+ if not author.checkPerm(current_user, Permission.CHANGE_AUTHOR):
+ flash("Permission denied", "error")
+ return redirect(url_for("packages.create_edit"))
+
+ else:
+ package = getPackageByInfo(author, name)
+ if not package.checkPerm(current_user, Permission.EDIT_PACKAGE):
+ return redirect(package.getDetailsURL())
+
+ author = package.author
+
+ form = PackageForm(formdata=request.form, obj=package)
+
+ # Initial form class from post data and default data
+ if request.method == "GET":
+ if package is None:
+ form.name.data = request.args.get("bname")
+ form.title.data = request.args.get("title")
+ form.repo.data = request.args.get("repo")
+ form.forums.data = request.args.get("forums")
+ else:
+ deps = package.dependencies
+ form.harddep_str.data = ",".join([str(x) for x in deps if not x.optional])
+ form.softdep_str.data = ",".join([str(x) for x in deps if x.optional])
+ form.provides_str.data = MetaPackage.ListToSpec(package.provides)
+
+ if request.method == "POST" and form.validate():
+ wasNew = False
+ if not package:
+ package = Package.query.filter_by(name=form["name"].data, author_id=author.id).first()
+ if package is not None:
+ if package.soft_deleted:
+ Package.query.filter_by(name=form["name"].data, author_id=author.id).delete()
+ else:
+ flash("Package already exists!", "error")
+ return redirect(url_for("packages.create_edit"))
+
+ package = Package()
+ package.author = author
+ wasNew = True
+
+ elif package.approved and package.name != form.name.data and \
+ not package.checkPerm(current_user, Permission.CHANGE_NAME):
+ flash("Unable to change package name", "danger")
+ return redirect(url_for("packages.create_edit", author=author, name=name))
+
+ else:
+ triggerNotif(package.author, current_user,
+ "{} edited".format(package.title), package.getDetailsURL())
+
+ form.populate_obj(package) # copy to row
+
+ if package.type== PackageType.TXP:
+ package.license = package.media_license
+
+ mpackage_cache = {}
+ package.provides.clear()
+ mpackages = MetaPackage.SpecToList(form.provides_str.data, mpackage_cache)
+ for m in mpackages:
+ package.provides.append(m)
+
+ Dependency.query.filter_by(depender=package).delete()
+ deps = Dependency.SpecToList(package, form.harddep_str.data, mpackage_cache)
+ for dep in deps:
+ dep.optional = False
+ db.session.add(dep)
+
+ deps = Dependency.SpecToList(package, form.softdep_str.data, mpackage_cache)
+ for dep in deps:
+ dep.optional = True
+ db.session.add(dep)
+
+ if wasNew and package.type == PackageType.MOD and not package.name in mpackage_cache:
+ m = MetaPackage.GetOrCreate(package.name, mpackage_cache)
+ package.provides.append(m)
+
+ package.tags.clear()
+ for tag in form.tags.raw_data:
+ package.tags.append(Tag.query.get(tag))
+
+ db.session.commit() # save
+
+ next_url = package.getDetailsURL()
+ if wasNew and package.repo is not None:
+ task = importRepoScreenshot.delay(package.id)
+ next_url = url_for("tasks.check", id=task.id, r=next_url)
+
+ if wasNew and ("WTFPL" in package.license.name or "WTFPL" in package.media_license.name):
+ next_url = url_for("flatpage", path="help/wtfpl", r=next_url)
+
+ return redirect(next_url)
+
+ package_query = Package.query.filter_by(approved=True, soft_deleted=False)
+ if package is not None:
+ package_query = package_query.filter(Package.id != package.id)
+
+ enableWizard = name is None and request.method != "POST"
+ return render_template("packages/create_edit.html", package=package, \
+ form=form, author=author, enable_wizard=enableWizard, \
+ packages=package_query.all(), \
+ mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all())
+
+@bp.route("/packages/<author>/<name>/approve/", methods=["POST"])
+@login_required
+@is_package_page
+def approve(package):
+ if not package.checkPerm(current_user, Permission.APPROVE_NEW):
+ flash("You don't have permission to do that.", "error")
+
+ elif package.approved:
+ flash("Package has already been approved", "error")
+
+ else:
+ package.approved = True
+
+ screenshots = PackageScreenshot.query.filter_by(package=package, approved=False).all()
+ for s in screenshots:
+ s.approved = True
+
+ triggerNotif(package.author, current_user,
+ "{} approved".format(package.title), package.getDetailsURL())
+ db.session.commit()
+
+ return redirect(package.getDetailsURL())
+
+
+@bp.route("/packages/<author>/<name>/remove/", methods=["GET", "POST"])
+@login_required
+@is_package_page
+def remove(package):
+ if request.method == "GET":
+ return render_template("packages/remove.html", package=package)
+
+ if "delete" in request.form:
+ if not package.checkPerm(current_user, Permission.DELETE_PACKAGE):
+ flash("You don't have permission to do that.", "error")
+ return redirect(package.getDetailsURL())
+
+ package.soft_deleted = True
+
+ url = url_for("users.profile", username=package.author.username)
+ triggerNotif(package.author, current_user,
+ "{} deleted".format(package.title), url)
+ db.session.commit()
+
+ flash("Deleted package", "success")
+
+ return redirect(url)
+ elif "unapprove" in request.form:
+ if not package.checkPerm(current_user, Permission.UNAPPROVE_PACKAGE):
+ flash("You don't have permission to do that.", "error")
+ return redirect(package.getDetailsURL())
+
+ package.approved = False
+
+ triggerNotif(package.author, current_user,
+ "{} deleted".format(package.title), package.getDetailsURL())
+ db.session.commit()
+
+ flash("Unapproved package", "success")
+
+ return redirect(package.getDetailsURL())
+ else:
+ abort(400)
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import *
+from flask_user import *
+
+from . import bp
+
+from app.models import *
+from app.tasks.importtasks import makeVCSRelease
+from app.utils import *
+
+from celery import uuid
+from flask_wtf import FlaskForm
+from wtforms import *
+from wtforms.validators import *
+from wtforms.ext.sqlalchemy.fields import QuerySelectField
+
+
+def get_mt_releases(is_max):
+ query = MinetestRelease.query.order_by(db.asc(MinetestRelease.id))
+ if is_max:
+ query = query.limit(query.count() - 1)
+ else:
+ query = query.filter(MinetestRelease.name != "0.4.17")
+
+ return query
+
+
+class CreatePackageReleaseForm(FlaskForm):
+ title = StringField("Title", [InputRequired(), Length(1, 30)])
+ uploadOpt = RadioField ("Method", choices=[("upload", "File Upload")], default="upload")
+ vcsLabel = StringField("VCS Commit Hash, Branch, or Tag", default="master")
+ fileUpload = FileField("File Upload")
+ min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
+ query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
+ max_rel = QuerySelectField("Maximum Minetest Version", [InputRequired()],
+ query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
+ submit = SubmitField("Save")
+
+class EditPackageReleaseForm(FlaskForm):
+ title = StringField("Title", [InputRequired(), Length(1, 30)])
+ url = StringField("URL", [URL])
+ task_id = StringField("Task ID", filters = [lambda x: x or None])
+ approved = BooleanField("Is Approved")
+ min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
+ query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
+ max_rel = QuerySelectField("Maximum Minetest Version", [InputRequired()],
+ query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
+ submit = SubmitField("Save")
+
+@bp.route("/packages/<author>/<name>/releases/new/", methods=["GET", "POST"])
+@login_required
+@is_package_page
+def create_release(package):
+ if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
+ return redirect(package.getDetailsURL())
+
+ # Initial form class from post data and default data
+ form = CreatePackageReleaseForm()
+ if package.repo is not None:
+ form["uploadOpt"].choices = [("vcs", "From Git Commit or Branch"), ("upload", "File Upload")]
+ if request.method != "POST":
+ form["uploadOpt"].data = "vcs"
+
+ if request.method == "POST" and form.validate():
+ if form["uploadOpt"].data == "vcs":
+ rel = PackageRelease()
+ rel.package = package
+ rel.title = form["title"].data
+ rel.url = ""
+ rel.task_id = uuid()
+ rel.min_rel = form["min_rel"].data.getActual()
+ rel.max_rel = form["max_rel"].data.getActual()
+ db.session.add(rel)
+ db.session.commit()
+
+ makeVCSRelease.apply_async((rel.id, form["vcsLabel"].data), task_id=rel.task_id)
+
+ msg = "{}: Release {} created".format(package.title, rel.title)
+ triggerNotif(package.author, current_user, msg, rel.getEditURL())
+ db.session.commit()
+
+ return redirect(url_for("tasks.check", id=rel.task_id, r=rel.getEditURL()))
+ else:
+ uploadedPath = doFileUpload(form.fileUpload.data, "zip", "a zip file")
+ if uploadedPath is not None:
+ rel = PackageRelease()
+ rel.package = package
+ rel.title = form["title"].data
+ rel.url = uploadedPath
+ rel.min_rel = form["min_rel"].data.getActual()
+ rel.max_rel = form["max_rel"].data.getActual()
+ rel.approve(current_user)
+ db.session.add(rel)
+ db.session.commit()
+
+ msg = "{}: Release {} created".format(package.title, rel.title)
+ triggerNotif(package.author, current_user, msg, rel.getEditURL())
+ db.session.commit()
+ return redirect(package.getDetailsURL())
+
+ return render_template("packages/release_new.html", package=package, form=form)
+
+@bp.route("/packages/<author>/<name>/releases/<id>/download/")
+@is_package_page
+def download_release(package, id):
+ release = PackageRelease.query.get(id)
+ if release is None or release.package != package:
+ abort(404)
+
+ if release is None:
+ if "application/zip" in request.accept_mimetypes and \
+ not "text/html" in request.accept_mimetypes:
+ return "", 204
+ else:
+ flash("No download available.", "error")
+ return redirect(package.getDetailsURL())
+ else:
+ PackageRelease.query.filter_by(id=release.id).update({
+ "downloads": PackageRelease.downloads + 1
+ })
+ db.session.commit()
+
+ return redirect(release.url, code=300)
+
+@bp.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
+@login_required
+@is_package_page
+def edit_release(package, id):
+ release = PackageRelease.query.get(id)
+ if release is None or release.package != package:
+ abort(404)
+
+ clearNotifications(release.getEditURL())
+
+ canEdit = package.checkPerm(current_user, Permission.MAKE_RELEASE)
+ canApprove = package.checkPerm(current_user, Permission.APPROVE_RELEASE)
+ if not (canEdit or canApprove):
+ return redirect(package.getDetailsURL())
+
+ # Initial form class from post data and default data
+ form = EditPackageReleaseForm(formdata=request.form, obj=release)
+ if request.method == "POST" and form.validate():
+ wasApproved = release.approved
+ if canEdit:
+ release.title = form["title"].data
+ release.min_rel = form["min_rel"].data.getActual()
+ release.max_rel = form["max_rel"].data.getActual()
+
+ if package.checkPerm(current_user, Permission.CHANGE_RELEASE_URL):
+ release.url = form["url"].data
+ release.task_id = form["task_id"].data
+ if release.task_id is not None:
+ release.task_id = None
+
+ if canApprove:
+ release.approved = form["approved"].data
+ else:
+ release.approved = wasApproved
+
+ db.session.commit()
+ return redirect(package.getDetailsURL())
+
+ return render_template("packages/release_edit.html", package=package, release=release, form=form)
+
+
+
+class BulkReleaseForm(FlaskForm):
+ set_min = BooleanField("Set Min")
+ min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
+ query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
+ set_max = BooleanField("Set Max")
+ max_rel = QuerySelectField("Maximum Minetest Version", [InputRequired()],
+ query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
+ only_change_none = BooleanField("Only change values previously set as none")
+ submit = SubmitField("Update")
+
+
+@bp.route("/packages/<author>/<name>/releases/bulk_change/", methods=["GET", "POST"])
+@login_required
+@is_package_page
+def bulk_change_release(package):
+ if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
+ return redirect(package.getDetailsURL())
+
+ # Initial form class from post data and default data
+ form = BulkReleaseForm()
+
+ if request.method == "GET":
+ form.only_change_none.data = True
+ elif request.method == "POST" and form.validate():
+ only_change_none = form.only_change_none.data
+
+ for release in package.releases.all():
+ if form["set_min"].data and (not only_change_none or release.min_rel is None):
+ release.min_rel = form["min_rel"].data.getActual()
+ if form["set_max"].data and (not only_change_none or release.max_rel is None):
+ release.max_rel = form["max_rel"].data.getActual()
+
+ db.session.commit()
+
+ return redirect(package.getDetailsURL())
+
+ return render_template("packages/release_bulk_change.html", package=package, form=form)
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import *
+from flask_user import *
+
+from . import bp
+
+from app.models import *
+from app.utils import *
+
+from flask_wtf import FlaskForm
+from wtforms import *
+from wtforms.validators import *
+
+
+class CreateScreenshotForm(FlaskForm):
+ title = StringField("Title/Caption", [Optional()])
+ fileUpload = FileField("File Upload", [InputRequired()])
+ submit = SubmitField("Save")
+
+
+class EditScreenshotForm(FlaskForm):
+ title = StringField("Title/Caption", [Optional()])
+ approved = BooleanField("Is Approved")
+ delete = BooleanField("Delete")
+ submit = SubmitField("Save")
+
+@bp.route("/packages/<author>/<name>/screenshots/new/", methods=["GET", "POST"])
+@login_required
+@is_package_page
+def create_screenshot(package, id=None):
+ if not package.checkPerm(current_user, Permission.ADD_SCREENSHOTS):
+ return redirect(package.getDetailsURL())
+
+ # Initial form class from post data and default data
+ form = CreateScreenshotForm()
+ if request.method == "POST" and form.validate():
+ uploadedPath = doFileUpload(form.fileUpload.data, "image",
+ "a PNG or JPG image file")
+ if uploadedPath is not None:
+ ss = PackageScreenshot()
+ ss.package = package
+ ss.title = form["title"].data or "Untitled"
+ ss.url = uploadedPath
+ ss.approved = package.checkPerm(current_user, Permission.APPROVE_SCREENSHOT)
+ db.session.add(ss)
+
+ msg = "{}: Screenshot added {}" \
+ .format(package.title, ss.title)
+ triggerNotif(package.author, current_user, msg, package.getDetailsURL())
+ db.session.commit()
+ return redirect(package.getDetailsURL())
+
+ return render_template("packages/screenshot_new.html", package=package, form=form)
+
+@bp.route("/packages/<author>/<name>/screenshots/<id>/edit/", methods=["GET", "POST"])
+@login_required
+@is_package_page
+def edit_screenshot(package, id):
+ screenshot = PackageScreenshot.query.get(id)
+ if screenshot is None or screenshot.package != package:
+ abort(404)
+
+ canEdit = package.checkPerm(current_user, Permission.ADD_SCREENSHOTS)
+ canApprove = package.checkPerm(current_user, Permission.APPROVE_SCREENSHOT)
+ if not (canEdit or canApprove):
+ return redirect(package.getDetailsURL())
+
+ clearNotifications(screenshot.getEditURL())
+
+ # Initial form class from post data and default data
+ form = EditScreenshotForm(formdata=request.form, obj=screenshot)
+ if request.method == "POST" and form.validate():
+ if canEdit and form["delete"].data:
+ PackageScreenshot.query.filter_by(id=id).delete()
+
+ else:
+ wasApproved = screenshot.approved
+
+ if canEdit:
+ screenshot.title = form["title"].data or "Untitled"
+
+ if canApprove:
+ screenshot.approved = form["approved"].data
+ else:
+ screenshot.approved = wasApproved
+
+ db.session.commit()
+ return redirect(package.getDetailsURL())
+
+ return render_template("packages/screenshot_edit.html", package=package, screenshot=screenshot, form=form)
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import *
+from flask_user import *
+import flask_menu as menu
+from app import csrf
+from app.models import *
+from app.tasks import celery, TaskError
+from app.tasks.importtasks import getMeta
+from app.utils import shouldReturnJson
+from app.utils import *
+
+bp = Blueprint("tasks", __name__)
+
+@csrf.exempt
+@bp.route("/tasks/getmeta/new/", methods=["POST"])
+@login_required
+def start_getmeta():
+ author = request.args.get("author")
+ author = current_user.forums_username if author is None else author
+ aresult = getMeta.delay(request.args.get("url"), author)
+ return jsonify({
+ "poll_url": url_for("tasks.check", id=aresult.id),
+ })
+
+@bp.route("/tasks/<id>/")
+def check(id):
+ result = celery.AsyncResult(id)
+ status = result.status
+ traceback = result.traceback
+ result = result.result
+
+ info = None
+ if isinstance(result, Exception):
+ info = {
+ 'id': id,
+ 'status': status,
+ }
+
+ if current_user.is_authenticated and current_user.rank.atLeast(UserRank.ADMIN):
+ info["error"] = str(traceback)
+ elif str(result)[1:12] == "TaskError: ":
+ info["error"] = str(result)[12:-1]
+ else:
+ info["error"] = "Unknown server error"
+ else:
+ info = {
+ 'id': id,
+ 'status': status,
+ 'result': result,
+ }
+
+ if shouldReturnJson():
+ return jsonify(info)
+ else:
+ r = request.args.get("r")
+ if r is not None and status == "SUCCESS":
+ return redirect(r)
+ else:
+ return render_template("tasks/view.html", info=info)
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import *
+
+bp = Blueprint("threads", __name__)
+
+from flask_user import *
+from app.models import *
+from app.utils import triggerNotif, clearNotifications
+
+import datetime
+
+from flask_wtf import FlaskForm
+from wtforms import *
+from wtforms.validators import *
+
+@bp.route("/threads/")
+def list_all():
+ query = Thread.query
+ if not Permission.SEE_THREAD.check(current_user):
+ query = query.filter_by(private=False)
+ return render_template("threads/list.html", threads=query.all())
+
+
+@bp.route("/threads/<int:id>/subscribe/", methods=["POST"])
+@login_required
+def subscribe(id):
+ thread = Thread.query.get(id)
+ if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
+ abort(404)
+
+ if current_user in thread.watchers:
+ flash("Already subscribed!", "success")
+ else:
+ flash("Subscribed to thread", "success")
+ thread.watchers.append(current_user)
+ db.session.commit()
+
+ return redirect(url_for("threads.view", id=id))
+
+
+@bp.route("/threads/<int:id>/unsubscribe/", methods=["POST"])
+@login_required
+def unsubscribe(id):
+ thread = Thread.query.get(id)
+ if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
+ abort(404)
+
+ if current_user in thread.watchers:
+ flash("Unsubscribed!", "success")
+ thread.watchers.remove(current_user)
+ db.session.commit()
+ else:
+ flash("Not subscribed to thread", "success")
+
+ return redirect(url_for("threads.view", id=id))
+
+
+@bp.route("/threads/<int:id>/", methods=["GET", "POST"])
+def view(id):
+ clearNotifications(url_for("threads.view", id=id))
+
+ thread = Thread.query.get(id)
+ if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
+ abort(404)
+
+ if current_user.is_authenticated and request.method == "POST":
+ comment = request.form["comment"]
+
+ if not current_user.canCommentRL():
+ flash("Please wait before commenting again", "danger")
+ if package:
+ return redirect(package.getDetailsURL())
+ else:
+ return redirect(url_for("home_page"))
+
+ if len(comment) <= 500 and len(comment) > 3:
+ reply = ThreadReply()
+ reply.author = current_user
+ reply.comment = comment
+ db.session.add(reply)
+
+ thread.replies.append(reply)
+ if not current_user in thread.watchers:
+ thread.watchers.append(current_user)
+
+ msg = None
+ if thread.package is None:
+ msg = "New comment on '{}'".format(thread.title)
+ else:
+ msg = "New comment on '{}' on package {}".format(thread.title, thread.package.title)
+
+
+ for user in thread.watchers:
+ if user != current_user:
+ triggerNotif(user, current_user, msg, url_for("threads.view", id=thread.id))
+
+ db.session.commit()
+
+ return redirect(url_for("threads.view", id=id))
+
+ else:
+ flash("Comment needs to be between 3 and 500 characters.")
+
+ return render_template("threads/view.html", thread=thread)
+
+
+class ThreadForm(FlaskForm):
+ title = StringField("Title", [InputRequired(), Length(3,100)])
+ comment = TextAreaField("Comment", [InputRequired(), Length(10, 500)])
+ private = BooleanField("Private")
+ submit = SubmitField("Open Thread")
+
+@bp.route("/threads/new/", methods=["GET", "POST"])
+@login_required
+def new():
+ form = ThreadForm(formdata=request.form)
+
+ package = None
+ if "pid" in request.args:
+ package = Package.query.get(int(request.args.get("pid")))
+ if package is None:
+ flash("Unable to find that package!", "error")
+
+ # Don't allow making orphan threads on approved packages for now
+ if package is None:
+ abort(403)
+
+ def_is_private = request.args.get("private") or False
+ if package is None:
+ def_is_private = True
+ allow_change = package and package.approved
+ is_review_thread = package and not package.approved
+
+ # Check that user can make the thread
+ if not package.checkPerm(current_user, Permission.CREATE_THREAD):
+ flash("Unable to create thread!", "error")
+ return redirect(url_for("home_page"))
+
+ # Only allow creating one thread when not approved
+ elif is_review_thread and package.review_thread is not None:
+ flash("A review thread already exists!", "error")
+ return redirect(url_for("threads.view", id=package.review_thread.id))
+
+ elif not current_user.canOpenThreadRL():
+ flash("Please wait before opening another thread", "danger")
+
+ if package:
+ return redirect(package.getDetailsURL())
+ else:
+ return redirect(url_for("home_page"))
+
+ # Set default values
+ elif request.method == "GET":
+ form.private.data = def_is_private
+ form.title.data = request.args.get("title") or ""
+
+ # Validate and submit
+ elif request.method == "POST" and form.validate():
+ thread = Thread()
+ thread.author = current_user
+ thread.title = form.title.data
+ thread.private = form.private.data if allow_change else def_is_private
+ thread.package = package
+ db.session.add(thread)
+
+ thread.watchers.append(current_user)
+ if package is not None and package.author != current_user:
+ thread.watchers.append(package.author)
+
+ reply = ThreadReply()
+ reply.thread = thread
+ reply.author = current_user
+ reply.comment = form.comment.data
+ db.session.add(reply)
+
+ thread.replies.append(reply)
+
+ db.session.commit()
+
+ if is_review_thread:
+ package.review_thread = thread
+
+ notif_msg = None
+ if package is not None:
+ notif_msg = "New thread '{}' on package {}".format(thread.title, package.title)
+ triggerNotif(package.author, current_user, notif_msg, url_for("threads.view", id=thread.id))
+ else:
+ notif_msg = "New thread '{}'".format(thread.title)
+
+ for user in User.query.filter(User.rank >= UserRank.EDITOR).all():
+ triggerNotif(user, current_user, notif_msg, url_for("threads.view", id=thread.id))
+
+ db.session.commit()
+
+ return redirect(url_for("threads.view", id=thread.id))
+
+
+ return render_template("threads/new.html", form=form, allow_private_change=allow_change, package=package)
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import *
+
+bp = Blueprint("thumbnails", __name__)
+
+import os
+from PIL import Image
+
+ALLOWED_RESOLUTIONS=[(100,67), (270,180), (350,233)]
+
+def mkdir(path):
+ if not os.path.isdir(path):
+ os.mkdir(path)
+
+mkdir("app/public/thumbnails/")
+
+def resize_and_crop(img_path, modified_path, size):
+ img = Image.open(img_path)
+
+ # Get current and desired ratio for the images
+ img_ratio = img.size[0] / float(img.size[1])
+ ratio = size[0] / float(size[1])
+
+ # Is more portrait than target, scale and crop
+ if ratio > img_ratio:
+ img = img.resize((int(size[0]), int(size[0] * img.size[1] / img.size[0])),
+ Image.BICUBIC)
+ box = (0, (img.size[1] - size[1]) / 2, img.size[0], (img.size[1] + size[1]) / 2)
+ img = img.crop(box)
+
+ # Is more landscape than target, scale and crop
+ elif ratio < img_ratio:
+ img = img.resize((int(size[1] * img.size[0] / img.size[1]), int(size[1])),
+ Image.BICUBIC)
+ box = ((img.size[0] - size[0]) / 2, 0, (img.size[0] + size[0]) / 2, img.size[1])
+ img = img.crop(box)
+
+ # Is exactly the same ratio as target
+ else:
+ img = img.resize(size, Image.BICUBIC)
+
+ img.save(modified_path)
+
+
+@bp.route("/thumbnails/<int:level>/<img>")
+def make_thumbnail(img, level):
+ if level > len(ALLOWED_RESOLUTIONS) or level <= 0:
+ abort(403)
+
+ w, h = ALLOWED_RESOLUTIONS[level - 1]
+
+ mkdir("app/public/thumbnails/{:d}/".format(level))
+
+ cache_filepath = "public/thumbnails/{:d}/{}".format(level, img)
+ source_filepath = "public/uploads/" + img
+
+ resize_and_crop("app/" + source_filepath, "app/" + cache_filepath, (w, h))
+ return send_file(cache_filepath)
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+from flask import *
+from flask_user import *
+import flask_menu as menu
+from app.models import *
+from app.querybuilder import QueryBuilder
+
+bp = Blueprint("todo", __name__)
+
+@bp.route("/todo/", methods=["GET", "POST"])
+@login_required
+def view():
+ 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).order_by(db.desc(Package.created_at)).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()
+
+ if not canApproveNew and not canApproveRel and not canApproveScn:
+ abort(403)
+
+ if request.method == "POST":
+ if request.form["action"] == "screenshots_approve_all":
+ if not canApproveScn:
+ abort(403)
+
+ PackageScreenshot.query.update({ "approved": True })
+ db.session.commit()
+ return redirect(url_for("todo.view"))
+ else:
+ abort(400)
+
+ topic_query = ForumTopic.query \
+ .filter_by(discarded=False)
+
+ total_topics = topic_query.count()
+ topics_to_add = topic_query \
+ .filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
+ .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, total_topics=total_topics)
+
+
+@bp.route("/todo/topics/")
+@login_required
+def topics():
+ qb = QueryBuilder(request.args)
+ qb.setSortIfNone("date")
+ query = qb.buildTopicQuery()
+
+ tmp_q = ForumTopic.query
+ if not qb.show_discarded:
+ tmp_q = tmp_q.filter_by(discarded=False)
+ total = tmp_q.count()
+ topic_count = query.count()
+
+ 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=query.next_num, query=qb.search, \
+ show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
+ if query.has_next else None
+ prev_url = url_for("todo.topics", page=query.prev_num, query=qb.search, \
+ show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
+ if query.has_prev else None
+
+ return render_template("todo/topics.html", topics=query.items, total=total, \
+ topic_count=topic_count, query=qb.search, show_discarded=qb.show_discarded, \
+ next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, \
+ n=num, sort_by=qb.order_by)
--- /dev/null
+from flask import Blueprint
+
+bp = Blueprint("users", __name__)
+
+from . import githublogin, profile
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+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 . import bp
+from app import github
+from app.models import *
+from app.utils import loginUser
+
+@bp.route("/user/github/start/")
+def github_signin():
+ return github.authorize("")
+
+@bp.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("users.claim"))
+ elif loginUser(userByGithub):
+ if current_user.password is None:
+ return redirect(next_url or url_for("users.set_password", 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"))
--- /dev/null
+# 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 <https://www.gnu.org/licenses/>.
+
+
+from flask import *
+from flask_user import *
+from flask_login import login_user, logout_user
+from app import markdown
+from . import bp
+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()], 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 = User.query.order_by(db.desc(User.rank), db.asc(User.display_name)).all()
+ return render_template("users/list.html", users=users)
+
+
+@bp.route("/users/<username>/", methods=["GET", "POST"])
+def profile(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
+ 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):
+ 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("tasks.check", id=task.id, r=url_for("users.profile", username=username)))
+
+ # Save user_profile
+ db.session.commit()
+
+ # Redirect to home page
+ return redirect(url_for("users.profile", 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/users.profile.html",
+ user=user, form=form, packages=packages, topics_to_add=topics_to_add)
+
+
+@bp.route("/users/<username>/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("users.profile", username=username)
+
+ return redirect(url_for("tasks.check", id=task.id, r=next_url))
+
+
+class SendEmailForm(FlaskForm):
+ subject = StringField("Subject", [InputRequired(), Length(1, 300)])
+ text = TextAreaField("Message", [InputRequired()])
+ submit = SubmitField("Send")
+
+
+@bp.route("/users/<username>/email/", methods=["GET", "POST"])
+@rank_required(UserRank.MODERATOR)
+def send_email(username):
+ user = User.query.filter_by(username=username).first()
+ if user is None:
+ abort(404)
+
+ next_url = url_for("users.profile", 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("tasks.check", 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, 100)])
+ password2 = PasswordField("Verify password", [InputRequired(), Length(2, 100)])
+ submit = SubmitField("Save")
+
+@bp.route("/user/set-password/", methods=["GET", "POST"])
+@login_required
+def set_password():
+ 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("tasks.check", id=task.id, r=url_for("users.profile", username=current_user.username)))
+ else:
+ return redirect(url_for("users.profile", 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"))
+
+
+@bp.route("/user/claim/", methods=["GET", "POST"])
+def claim():
+ 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("users.claim"))
+ elif user is None and method == "github":
+ flash("Unable to get Github username for user", "error")
+ return redirect(url_for("users.claim"))
+ elif user is None:
+ flash("Unable to find that user", "error")
+ return redirect(url_for("users.claim"))
+
+ if user is not None and method == "github":
+ return redirect(url_for("users.github_signin"))
+
+ 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("tasks.check", id=task.id, r=url_for("users.claim", 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("users.claim"))
+
+ # 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("users.claim", 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("users.set_password"))
+ else:
+ flash("Unable to login as user", "error")
+ return redirect(url_for("users.claim", username=username))
+
+ else:
+ flash("Could not find the key in your signature!", "error")
+ return redirect(url_for("users.claim", username=username))
+ else:
+ flash("Unknown claim type", "error")
+
+ return render_template("users/claim.html", username=username, key=token)
+
+@bp.route("/users/verify/")
+def verify_email():
+ 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("users.profile", username=current_user.username))
+ else:
+ return redirect(url_for("home_page"))
return screenshot.url if screenshot is not None else None
def getDetailsURL(self):
- return url_for("package_page",
+ return url_for("packages.view",
author=self.author.username, name=self.name)
def getEditURL(self):
- return url_for("create_edit_package_page",
+ return url_for("packages.create_edit",
author=self.author.username, name=self.name)
def getApproveURL(self):
- return url_for("approve_package_page",
+ return url_for("packages.approve",
author=self.author.username, name=self.name)
def getRemoveURL(self):
- return url_for("remove_package_page",
+ return url_for("packages.remove",
author=self.author.username, name=self.name)
def getNewScreenshotURL(self):
- return url_for("create_screenshot_page",
+ return url_for("packages.create_screenshot",
author=self.author.username, name=self.name)
def getCreateReleaseURL(self):
- return url_for("create_release_page",
+ return url_for("packages.create_release",
author=self.author.username, name=self.name)
def getCreateEditRequestURL(self):
author=self.author.username, name=self.name)
def getBulkReleaseURL(self):
- return url_for("bulk_change_release_page",
+ return url_for("packages.bulk_change_release",
author=self.author.username, name=self.name)
def getDownloadURL(self):
- return url_for("package_download_page",
+ return url_for("packages.download",
author=self.author.username, name=self.name)
def getDownloadRelease(self, version=None, protonum=None):
def getEditURL(self):
- return url_for("edit_release_page",
+ return url_for("packages.edit_release",
author=self.package.author.username,
name=self.package.name,
id=self.id)
def getDownloadURL(self):
- return url_for("download_release_page",
+ return url_for("packages.download_release",
author=self.package.author.username,
name=self.package.name,
id=self.id)
def getEditURL(self):
- return url_for("edit_screenshot_page",
+ return url_for("packages.edit_screenshot",
author=self.package.author.username,
name=self.package.name,
id=self.id)
def getSubscribeURL(self):
- return url_for("thread_subscribe_page",
+ return url_for("threads.subscribe",
id=self.id)
def getUnsubscribeURL(self):
- return url_for("thread_unsubscribe_page",
+ return url_for("threads.unsubscribe",
id=self.id)
def checkPerm(self, user, perm):
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+A small Flask extension that makes it easy to use Sass (SCSS) with your
+Flask application.
+
+Code unabashedly adapted from https://github.com/weapp/flask-coffee2js
+
+:copyright: (c) 2012 by Ivan Miric.
+:license: MIT, see LICENSE for more details.
+"""
+
+import os
+import os.path
+import codecs
+from flask import *
+from scss import Scss
+
+def _convert(dir, src, dst):
+ original_wd = os.getcwd()
+ os.chdir(dir)
+
+ css = Scss()
+ source = codecs.open(src, 'r', encoding='utf-8').read()
+ output = css.compile(source)
+
+ os.chdir(original_wd)
+
+ outfile = codecs.open(dst, 'w', encoding='utf-8')
+ outfile.write(output)
+ outfile.close()
+
+def _getDirPath(app, originalPath, create=False):
+ path = originalPath
+
+ if not os.path.isdir(path):
+ path = os.path.join(app.root_path, path)
+
+ if not os.path.isdir(path):
+ if create:
+ os.mkdir(path)
+ else:
+ raise IOError("Unable to find " + originalPath)
+
+ return path
+
+def sass(app, inputDir='scss', outputPath='static', force=False, cacheDir="public/static"):
+ static_url_path = app.static_url_path
+ inputDir = _getDirPath(app, inputDir)
+ cacheDir = _getDirPath(app, cacheDir or outputPath, True)
+
+ def _sass(filepath):
+ sassfile = "%s/%s.scss" % (inputDir, filepath)
+ cacheFile = "%s/%s.css" % (cacheDir, filepath)
+
+ # Source file exists, and needs regenerating
+ if os.path.isfile(sassfile) and (force or not os.path.isfile(cacheFile) or \
+ os.path.getmtime(sassfile) > os.path.getmtime(cacheFile)):
+ _convert(inputDir, sassfile, cacheFile)
+ app.logger.debug('Compiled %s into %s' % (sassfile, cacheFile))
+
+ return send_from_directory(cacheDir, filepath + ".css")
+
+ app.add_url_rule("/%s/<path:filepath>.css" % (outputPath), 'sass', _sass)
If this was you, then please click this link to verify the address:
{}
- """.format(url_for('verify_email_page', token=token, _external=True))
+ """.format(url_for('users.verify_email', token=token, _external=True))
msg.html = render_template("emails/verify.html", token=token)
mail.send(msg)
# along with this program. If not, see <https://www.gnu.org/licenses/>.
-import flask, json, os, git, tempfile, shutil
+import flask, json, os, git, tempfile, shutil, gitdb
from git import GitCommandError
from flask_sqlalchemy import SQLAlchemy
from urllib.error import HTTPError
--- /dev/null
+from . import app
+from urllib.parse import urlparse
+
+@app.context_processor
+def inject_debug():
+ return dict(debug=app.debug)
+
+@app.template_filter()
+def throw(err):
+ raise Exception(err)
+
+@app.template_filter()
+def domain(url):
+ return urlparse(url).netloc
+
+@app.template_filter()
+def date(value):
+ return value.strftime("%Y-%m-%d")
+
+@app.template_filter()
+def datetime(value):
+ return value.strftime("%Y-%m-%d %H:%M") + " UTC"
{% block content %}
<p>
- <a href="{{ url_for('license_list_page') }}">Back to list</a> |
- <a href="{{ url_for('createedit_license_page') }}">New License</a>
+ <a href="{{ url_for('admin.license_list') }}">Back to list</a> |
+ <a href="{{ url_for('admin.create_edit_license') }}">New License</a>
</p>
{% from "macros/forms.html" import render_field, render_submit_field %}
{% block content %}
<p>
- <a href="{{ url_for('createedit_license_page') }}">New License</a>
+ <a href="{{ url_for('admin.create_edit_license') }}">New License</a>
</p>
<ul>
{% for l in licenses %}
- <li><a href="{{ url_for('createedit_license_page', name=l.name) }}">{{ l.name }}</a> [{{ l.is_foss and "Free" or "Non-free"}}]</li>
+ <li><a href="{{ url_for('admin.create_edit_license', name=l.name) }}">{{ l.name }}</a> [{{ l.is_foss and "Free" or "Non-free"}}]</li>
{% endfor %}
</ul>
{% endblock %}
{% block content %}
<ul>
- <li><a href="{{ url_for('user_list_page') }}">User list</a></li>
- <li><a href="{{ url_for('tag_list_page') }}">Tag Editor</a></li>
- <li><a href="{{ url_for('license_list_page') }}">License Editor</a></li>
- <li><a href="{{ url_for('version_list_page') }}">Version Editor</a></li>
- <li><a href="{{ url_for('switch_user_page') }}">Sign in as another user</a></li>
+ <li><a href="{{ url_for('users.list_all') }}">User list</a></li>
+ <li><a href="{{ url_for('admin.tag_list') }}">Tag Editor</a></li>
+ <li><a href="{{ url_for('admin.license_list') }}">License Editor</a></li>
+ <li><a href="{{ url_for('admin.version_list') }}">Version Editor</a></li>
+ <li><a href="{{ url_for('admin.switch_user') }}">Sign in as another user</a></li>
</ul>
<div class="card my-4">
--- /dev/null
+{% extends "base.html" %}
+
+{% block title %}
+ Switch User
+{% endblock %}
+
+{% block content %}
+ <h2>Log in as another user</h2>
+
+ {% from "macros/forms.html" import render_field, render_submit_field %}
+ <form method="POST" action="">
+ {{ form.hidden_tag() }}
+
+ {{ render_field(form.username) }}
+ {{ render_submit_field(form.submit) }}
+ </form>
+{% endblock %}
+++ /dev/null
-{% extends "base.html" %}
-
-{% block title %}
- Switch User
-{% endblock %}
-
-{% block content %}
- <h2>Log in as another user</h2>
-
- {% from "macros/forms.html" import render_field, render_submit_field %}
- <form method="POST" action="">
- {{ form.hidden_tag() }}
-
- {{ render_field(form.username) }}
- {{ render_submit_field(form.submit) }}
- </form>
-{% endblock %}
{% block content %}
<p>
- <a href="{{ url_for('tag_list_page') }}">Back to list</a> |
- <a href="{{ url_for('createedit_tag_page') }}">New Tag</a>
+ <a href="{{ url_for('admin.tag_list') }}">Back to list</a> |
+ <a href="{{ url_for('admin.create_edit_tag') }}">New Tag</a>
</p>
{% from "macros/forms.html" import render_field, render_submit_field %}
{% block content %}
<p>
- <a href="{{ url_for('createedit_tag_page') }}">New Tag</a>
+ <a href="{{ url_for('admin.create_edit_tag') }}">New Tag</a>
</p>
<ul>
{% for t in tags %}
- <li><a href="{{ url_for('createedit_tag_page', name=t.name) }}">{{ t.title }}</a> [{{ t.packages | count }} packages]</li>
+ <li><a href="{{ url_for('admin.create_edit_tag', name=t.name) }}">{{ t.title }}</a> [{{ t.packages | count }} packages]</li>
{% endfor %}
</ul>
{% endblock %}
{% block content %}
<p>
- <a href="{{ url_for('version_list_page') }}">Back to list</a> |
- <a href="{{ url_for('createedit_version_page') }}">New Version</a>
+ <a href="{{ url_for('admin.version_list') }}">Back to list</a> |
+ <a href="{{ url_for('admin.create_edit_version') }}">New Version</a>
</p>
{% from "macros/forms.html" import render_field, render_submit_field %}
{% block content %}
<p>
- <a href="{{ url_for('createedit_version_page') }}">New Version</a>
+ <a href="{{ url_for('admin.create_edit_version') }}">New Version</a>
</p>
<ul>
{% for v in versions %}
- <li><a href="{{ url_for('createedit_version_page', name=v.name) }}">{{ v.name }}</a></li>
+ <li><a href="{{ url_for('admin.create_edit_version', name=v.name) }}">{{ v.name }}</a></li>
{% endfor %}
</ul>
{% endblock %}
</form>
<ul class="navbar-nav ml-auto">
{% if current_user.is_authenticated %}
- <li class="nav-item"><a class="nav-link" href="{{ url_for('notifications_page') }}">
+ <li class="nav-item"><a class="nav-link" href="{{ url_for('notifications.list_all') }}">
<img src="/static/notification{% if current_user.notifications %}_alert{% endif %}.svg" />
</a></li>
- <li class="nav-item"><a class="nav-link" href="{{ url_for('create_edit_package_page') }}">+</a></li>
+ <li class="nav-item"><a class="nav-link" href="{{ url_for('packages.create_edit') }}">+</a></li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle"
data-toggle="dropdown"
<ul class="dropdown-menu" role="menu">
<li class="nav-item">
- <a class="nav-link" href="{{ url_for('user_profile_page', username=current_user.username) }}">Profile</a>
+ <a class="nav-link" href="{{ url_for('users.profile', username=current_user.username) }}">Profile</a>
</li>
<li class="nav-item">
- <a class="nav-link" href="{{ url_for('user_profile_page', username=current_user.username) }}#unadded-topics">Your unadded topics</a>
+ <a class="nav-link" href="{{ url_for('users.profile', username=current_user.username) }}#unadded-topics">Your unadded topics</a>
</li>
{% if current_user.canAccessTodoList() %}
- <li class="nav-item"><a class="nav-link" href="{{ url_for('todo_page') }}">{{ _("Work Queue") }}</a></li>
- <li class="nav-item"><a class="nav-link" href="{{ url_for('user_list_page') }}">{{ _("User list") }}</a></li>
+ <li class="nav-item"><a class="nav-link" href="{{ url_for('todo.view') }}">{{ _("Work Queue") }}</a></li>
+ <li class="nav-item"><a class="nav-link" href="{{ url_for('users.list_all') }}">{{ _("User list") }}</a></li>
{% endif %}
<li class="nav-item">
- <a class="nav-link" href="{{ url_for('todo_topics_page') }}">{{ _("All unadded topics") }}</a>
+ <a class="nav-link" href="{{ url_for('todo.topics') }}">{{ _("All unadded topics") }}</a>
</li>
{% if current_user.rank == current_user.rank.ADMIN %}
- <li class="nav-item"><a class="nav-link" href="{{ url_for('admin_page') }}">{{ _("Admin") }}</a></li>
+ <li class="nav-item"><a class="nav-link" href="{{ url_for('admin.admin_page') }}">{{ _("Admin") }}</a></li>
{% endif %}
{% if current_user.rank == current_user.rank.MODERATOR %}
- <li class="nav-item"><a class="nav-link" href="{{ url_for('tag_list_page') }}">{{ _("Tag Editor") }}</a></li>
- <li class="nav-item"><a class="nav-link" href="{{ url_for('license_list_page') }}">{{ _("License Editor") }}</a></li>
+ <li class="nav-item"><a class="nav-link" href="{{ url_for('admin.tag_list') }}">{{ _("Tag Editor") }}</a></li>
+ <li class="nav-item"><a class="nav-link" href="{{ url_for('admin.license_list') }}">{{ _("License Editor") }}</a></li>
{% endif %}
<li class="nav-item"><a class="nav-link" href="{{ url_for('user.logout') }}">{{ _("Sign out") }}</a></li>
</ul>
<a href="{{ url_for('flatpage', path='help') }}">{{ _("Help") }}</a> |
<a href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("Policy and Guidance") }}</a> |
<a href="{{ url_for('flatpage', path='help/reporting') }}">{{ _("Report / DMCA") }}</a> |
- <a href="{{ url_for('user_list_page') }}">{{ _("User List") }}</a>
+ <a href="{{ url_for('users.list_all') }}">{{ _("User List") }}</a>
{% if debug %}
<p style="color: red">
If this was you, then please click this link to verify the address:
</p>
-<a class="btn" href="{{ url_for('verify_email_page', token=token, _external=True) }}">
+<a class="btn" href="{{ url_for('users.verify_email', token=token, _external=True) }}">
Confirm Email Address
</a>
<p style="font-size: 80%;">
- Or paste this into your browser: {{ url_for('verify_email_page', token=token, _external=True) }}
+ Or paste this into your browser: {{ url_for('users.verify_email', token=token, _external=True) }}
<p>
{% endblock %}
{% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %}
<h2 class="card-header">{%trans%}Sign in with Github{%endtrans%}</h2>
<div class="card-body">
- <a class="btn btn-primary" href="{{ url_for('github_signin_page') }}">GitHub</a>
+ <a class="btn btn-primary" href="{{ url_for('users.github_signin') }}">GitHub</a>
</div>
</div>
</div>
<div class="card-body">
<p>Create an account using your forum account or email.</p>
- <a href="{{ url_for('user_claim_page') }}" class="btn btn-primary">{%trans%}Claim your account{%endtrans%}</a>
+ <a href="{{ url_for('users.claim') }}" class="btn btn-primary">{%trans%}Claim your account{%endtrans%}</a>
</div>
</div>
</aside>
{% from "macros/packagegridtile.html" import render_pkggrid %}
- <a href="{{ url_for('packages_page', sort='created_at', order='desc') }}" class="btn btn-secondary float-right">
+ <a href="{{ url_for('packages.list_all', sort='created_at', order='desc') }}" class="btn btn-secondary float-right">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Recently Added") }}</h2>
{{ render_pkggrid(new) }}
- <a href="{{ url_for('packages_page', type='mod', sort='score', order='desc') }}" class="btn btn-secondary float-right">
+ <a href="{{ url_for('packages.list_all', type='mod', sort='score', order='desc') }}" class="btn btn-secondary float-right">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Top Mods") }}</h2>
{{ render_pkggrid(pop_mod) }}
- <a href="{{ url_for('packages_page', type='game', sort='score', order='desc') }}" class="btn btn-secondary float-right">
+ <a href="{{ url_for('packages.list_all', type='game', sort='score', order='desc') }}" class="btn btn-secondary float-right">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Top Games") }}</h2>
{{ render_pkggrid(pop_gam) }}
- <a href="{{ url_for('packages_page', type='txp', sort='score', order='desc') }}" class="btn btn-secondary float-right">
+ <a href="{{ url_for('packages.list_all', type='txp', sort='score', order='desc') }}" class="btn btn-secondary float-right">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Top Texture Packs") }}</h2>
{% for r in thread.replies %}
<li class="row my-2 mx-0">
<div class="col-md-1 p-1">
- <a href="{{ url_for('user_profile_page', username=r.author.username) }}">
+ <a href="{{ url_for('users.profile', username=r.author.username) }}">
<img class="img-responsive user-photo img-thumbnail img-thumbnail-1" src="{{ r.author.getProfilePicURL() }}">
</a>
</div>
<div class="card">
<div class="card-header">
<a class="author {{ r.author.rank.name }}"
- href="{{ url_for('user_profile_page', username=r.author.username) }}">
+ href="{{ url_for('users.profile', username=r.author.username) }}">
{{ r.author.display_name }}
</a>
<a name="reply-{{ r.id }}" class="text-muted float-right"
- href="{{ url_for('thread_page', id=thread.id) }}#reply-{{ r.id }}">
+ href="{{ url_for('threads.view', id=thread.id) }}#reply-{{ r.id }}">
{{ r.created_at | datetime }}
</a>
</div>
</div>
{% if current_user.canCommentRL() %}
- <form method="post" action="{{ url_for('thread_page', id=thread.id)}}" class="card-body">
+ <form method="post" action="{{ url_for('threads.view', id=thread.id)}}" class="card-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<textarea class="form-control markdown" required maxlength=500 name="comment"></textarea><br />
<input class="btn btn-primary" type="submit" value="Comment" />
{% for t in threads %}
<li {% if list_group %}class="list-group-item"{% endif %}>
{% if list_group %}
- <a href="{{ url_for('thread_page', id=t.id) }}">
+ <a href="{{ url_for('threads.view', id=t.id) }}">
{% if t.private %}🔒 {% endif %}
{{ t.title }}
by {{ t.author.display_name }}
</a>
{% else %}
{% if t.private %}🔒 {% endif %}
- <a href="{{ url_for('thread_page', id=t.id) }}">{{ t.title }}</a>
+ <a href="{{ url_for('threads.view', id=t.id) }}">{{ t.title }}</a>
by {{ t.author.display_name }}
{% endif %}
</li>
{% if topic.wip %}[WIP]{% endif %}
</td>
{% if show_author %}
- <td><a href="{{ url_for('user_profile_page', username=topic.author.username) }}">{{ topic.author.display_name}}</a></td>
+ <td><a href="{{ url_for('users.profile', username=topic.author.username) }}">{{ topic.author.display_name}}</a></td>
{% endif %}
<td>{{ topic.name or ""}}</td>
<td>{{ topic.created_at | date }}</td>
<td class="btn-group">
{% if current_user == topic.author or topic.author.checkPerm(current_user, "CHANGE_AUTHOR") %}
<a class="btn btn-primary"
- href="{{ url_for('create_edit_package_page', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">
+ href="{{ url_for('packages.create_edit', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">
Create
</a>
{% endif %}
{% if topic.wip %}[WIP]{% endif %}
{% if topic.name %}[{{ topic.name }}]{% endif %}
{% if show_author %}
- by <a href="{{ url_for('user_profile_page', username=topic.author.username) }}">{{ topic.author.display_name }}</a>
+ by <a href="{{ url_for('users.profile', username=topic.author.username) }}">{{ topic.author.display_name }}</a>
{% endif %}
{% if topic.author == current_user or topic.author.checkPerm(current_user, "CHANGE_AUTHOR") %}
- | <a href="{{ url_for('create_edit_package_page', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">Create</a>
+ | <a href="{{ url_for('packages.create_edit', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">Create</a>
{% endif %}
</li>
{% endfor %}
{% block content %}
<ul>
{% for meta in mpackages %}
- <li><a href="{{ url_for('meta_package_page', name=meta.name) }}">{{ meta.name }}</a> ({{ meta.packages.filter_by(soft_deleted=False, approved=True).all() | count }} packages)</li>
+ <li><a href="{{ url_for('metapackages.view', name=meta.name) }}">{{ meta.name }}</a> ({{ meta.packages.filter_by(soft_deleted=False, approved=True).all() | count }} packages)</li>
{% else %}
<li><i>No meta packages found.</i></li>
{% endfor %}
{% block content %}
{% if current_user.notifications %}
- <form method="post" action="{{ url_for('clear_notifications_page') }}">
+ <form method="post" action="{{ url_for('notifications.clear') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" value="Clear All" />
</form>
{% for n in range(1, page_max+1) %}
<li class="page-item {% if n == page %}active{% endif %}">
<a class="page-link"
- href="{{ url_for('packages_page', type=type, q=query, page=n) }}">
+ href="{{ url_for('packages.list_all', type=type, q=query, page=n) }}">
{{ n }}
</a>
</li>
{% endif %}
{% if release.task_id %}
- Importing... <a href="{{ url_for('check_task', id=release.task_id, r=release.getEditURL()) }}">view task</a><br />
+ Importing... <a href="{{ url_for('tasks.check', id=release.task_id, r=release.getEditURL()) }}">view task</a><br />
{% if package.checkPerm(current_user, "CHANGE_RELEASE_URL") %}
{{ render_field(form.task_id) }}
{% endif %}
{% if not review_thread and (package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW")) %}
<div class="alert alert-info">
- <a class="float-right btn btn-sm btn-info" href="{{ url_for('new_thread_page', pid=package.id, title='Package approval comments') }}">Open Thread</a>
+ <a class="float-right btn btn-sm btn-info" href="{{ url_for('threads.new', pid=package.id, title='Package approval comments') }}">Open Thread</a>
Privately ask a question or give feedback
<div style="clear:both;"></div>
<td>Provides</td>
<td>{% for meta in package.provides %}
<a class="badge badge-primary"
- href="{{ url_for('meta_package_page', name=meta.name) }}">{{ meta.name }}</a>
+ href="{{ url_for('metapackages.view', name=meta.name) }}">{{ meta.name }}</a>
{% endfor %}</td>
</tr>
{% endif %}
<tr>
<td>Author</td>
<td class="{{ package.author.rank }}">
- <a href="{{ url_for('user_profile_page', username=package.author.username) }}">
+ <a href="{{ url_for('users.profile', username=package.author.username) }}">
{{ package.author.display_name }}
</a>
</td>
{{ dep.package.title }} by {{ dep.package.author.display_name }}
{% elif dep.meta_package %}
<a class="badge badge-{{ color }}"
- href="{{ url_for('meta_package_page', name=dep.meta_package.name) }}">
+ href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}">
{{ dep.meta_package.name }}
{% else %}
{{ "Excepted package or meta_package in dep!" | throw }}
created {{ rel.releaseDate | date }}.
</small>
{% if (package.checkPerm(current_user, "MAKE_RELEASE") or package.checkPerm(current_user, "APPROVE_RELEASE")) and rel.task_id %}
- <a href="{{ url_for('check_task', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
+ <a href="{{ url_for('tasks.check', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
{% elif not rel.approved %}
Waiting for approval.
{% endif %}
<div class="card-header">
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %}
<a class="float-right"
- href="{{ url_for('new_thread_page', pid=package.id) }}">+</a>
+ href="{{ url_for('threads.new', pid=package.id) }}">+</a>
{% endif %}
Threads
</div>
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") and current_user != package.author and not current_user.rank.atLeast(current_user.rank.EDITOR) %}
<a class="float-right"
- href="{{ url_for('new_thread_page', pid=package.id) }}">
+ href="{{ url_for('threads.new', pid=package.id) }}">
Report a problem with this listing
</a>
{% endif %}
<li>
<a href="{{ r.getURL() }}">{{ r.title }}</a>
by
- <a href="{{ url_for('user_profile_page', username=r.author.username) }}">{{ r.author.display_name }}</a>
+ <a href="{{ url_for('users.profile', username=r.author.username) }}">{{ r.author.display_name }}</a>
</li>
{% else %}
<li>No edit requests have been made.</li>
<script>
// @author rubenwardy
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
- pollTask("{{ url_for('check_task', id=info.id) }}", true)
+ pollTask("{{ url_for('tasks.check', id=info.id) }}", true)
.then(function() { location.reload() })
.catch(function() { location.reload() })
</script>
{% if canApproveScn and screenshots %}
<div class="card my-4">
<h3 class="card-header">Screenshots
- <form class="float-right" method="post" action="{{ url_for('todo_page') }}">
+ <form class="float-right" method="post" action="{{ url_for('todo.view') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="action" value="screenshots_approve_all" />
<input class="btn btn-sm btn-primary" type="submit" value="Approve All" />
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
- <a class="btn btn-primary" href="{{ url_for('todo_topics_page') }}">View Unadded Topic List</a>
+ <a class="btn btn-primary" href="{{ url_for('todo.topics') }}">View Unadded Topic List</a>
{% endblock %}
<div class="float-right">
<div class="btn-group">
<a class="btn btn-primary {% if sort_by=='date' %}active{% endif %}"
- href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=n, sort='date') }}">
+ href="{{ url_for('todo.topics', q=query, show_discarded=show_discarded, n=n, sort='date') }}">
Sort by date
</a>
<a class="btn btn-primary {% if sort_by=='name' %}active{% endif %}"
- href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=n, sort='name') }}">
+ href="{{ url_for('todo.topics', q=query, show_discarded=show_discarded, n=n, sort='name') }}">
Sort by name
</a>
<a class="btn btn-primary {% if sort_by=='views' %}active{% endif %}"
- href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=n, sort='views') }}">
+ href="{{ url_for('todo.topics', q=query, show_discarded=show_discarded, n=n, sort='views') }}">
Sort by views
</a>
</div>
{% if current_user.rank.atLeast(current_user.rank.EDITOR) %}
{% if n >= 10000 %}
<a class="btn btn-primary"
- href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=100, sort=sort_by) }}">
+ href="{{ url_for('todo.topics', q=query, show_discarded=show_discarded, n=100, sort=sort_by) }}">
Paginated list
</a>
{% else %}
<a class="btn btn-primary"
- href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=10000, sort=sort_by) }}">
+ href="{{ url_for('todo.topics', q=query, show_discarded=show_discarded, n=10000, sort=sort_by) }}">
Unlimited list
</a>
{% endif %}
{% endif %}
- <a class="btn btn-primary" href="{{ url_for('todo_topics_page', q=query, show_discarded=not show_discarded, n=n, sort=sort_by) }}">
+ <a class="btn btn-primary" href="{{ url_for('todo.topics', q=query, show_discarded=not show_discarded, n=n, sort=sort_by) }}">
{% if not show_discarded %}
Show
{% else %}
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
- <form method="GET" action="{{ url_for('todo_topics_page') }}" class="my-4">
+ <form method="GET" action="{{ url_for('todo.topics') }}" class="my-4">
<input type="hidden" name="show_discarded" value={{ show_discarded and "True" or "False" }} />
<input type="hidden" name="n" value={{ n }} />
<input type="hidden" name="sort" value={{ sort_by or "date" }} />
{% for i in range(1, page_max+1) %}
<li class="page-item {% if i == page %}active{% endif %}">
<a class="page-link"
- href="{{ url_for('todo_topics_page', page=i, query=query, show_discarded=show_discarded, n=n, sort=sort_by) }}">
+ href="{{ url_for('todo.topics', page=i, query=query, show_discarded=show_discarded, n=n, sort=sort_by) }}">
{{ i }}
</a>
</li>
Please log out to continue.
</p>
<p>
- <a href="{{ url_for('user.logout', next=url_for('user_claim_page')) }}" class="btn">Logout</a>
+ <a href="{{ url_for('user.logout', next=url_for('users.claim')) }}" class="btn">Logout</a>
</p>
{% else %}
<p>
Use GitHub field in forum profile
</div>
- <form method="post" class="card-body" action="{{ url_for('user_claim_page') }}">
+ <form method="post" class="card-body" action="{{ url_for('users.claim') }}">
<input class="form-control" type="hidden" name="claim_type" value="github">
<input class="form-control" type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
Verification token
</div>
- <form method="post" class="card-body" action="{{ url_for('user_claim_page') }}">
+ <form method="post" class="card-body" action="{{ url_for('users.claim') }}">
<input type="hidden" name="claim_type" value="forum">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<ul class="userlist">
{% for user in users %}
<li class="{{ user.rank }}">
- <a href="{{ url_for('user_profile_page', username=user.username) }}">
+ <a href="{{ url_for('users.profile', username=user.username) }}">
{{ user.display_name }}
</a> -
{{ user.rank.getTitle() }}
{% if not current_user.is_authenticated and user.rank == user.rank.NOT_JOINED and user.forums_username %}
<div class="alert alert-info">
<a class="float-right btn btn-default btn-sm"
- href="{{ url_for('user_claim_page', username=user.forums_username) }}">Claim</a>
+ href="{{ url_for('users.claim', username=user.forums_username) }}">Claim</a>
Is this you? Claim your account now!
</div>
{% if user.github_username %}
<a href="https://github.com/{{ user.github_username }}">GitHub</a>
{% elif user == current_user %}
- <a href="{{ url_for('github_signin_page') }}">Link Github</a>
+ <a href="{{ url_for('users.github_signin') }}">Link Github</a>
{% endif %}
{% if user.website_url %}
<td>Admin</td>
<td>
{% if user.email %}
- <a class="btn btn-primary" href="{{ url_for('send_email_page', username=user.username) }}">
+ <a class="btn btn-primary" href="{{ url_for('users.send_email', username=user.username) }}">
Email
</a>
{% else %}
<td>Profile Picture:</td>
<td>
{% if user.forums_username %}
- <form method="post" action="{{ url_for('user_check', username=user.username) }}" class="" style="display:inline-block;">
+ <form method="post" action="{{ url_for('users.user_check', username=user.username) }}" class="" style="display:inline-block;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" class="btn btn-primary" value="Sync with Forums" />
</form>
{% if user.password %}
Set | <a href="{{ url_for('user.change_password') }}">Change</a>
{% else %}
- Not set | <a href="{{ url_for('set_password_page') }}">Set</a>
+ Not set | <a href="{{ url_for('users.set_password') }}">Set</a>
{% endif %}
</td>
</tr>
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from app import app, pages
-from flask import *
-from flask_user import *
-from app.models import *
-import flask_menu as menu
-from werkzeug.contrib.cache import SimpleCache
-from urllib.parse import urlparse
-from sqlalchemy.sql.expression import func
-cache = SimpleCache()
-
-@app.context_processor
-def inject_debug():
- return dict(debug=app.debug)
-
-@app.template_filter()
-def throw(err):
- raise Exception(err)
-
-@app.template_filter()
-def domain(url):
- return urlparse(url).netloc
-
-@app.template_filter()
-def date(value):
- return value.strftime("%Y-%m-%d")
-
-@app.template_filter()
-def datetime(value):
- return value.strftime("%Y-%m-%d %H:%M") + " UTC"
-
-@app.route("/uploads/<path:path>")
-def send_upload(path):
- return send_from_directory("public/uploads", path)
-
-@app.route("/")
-@menu.register_menu(app, ".", "Home")
-def home_page():
- query = Package.query.filter_by(approved=True, soft_deleted=False)
- count = query.count()
- new = query.order_by(db.desc(Package.created_at)).limit(8).all()
- pop_mod = query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score)).limit(8).all()
- pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(4).all()
- pop_txp = query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score)).limit(4).all()
- downloads = db.session.query(func.sum(PackageRelease.downloads)).first()[0]
- return render_template("index.html", count=count, downloads=downloads, \
- new=new, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam)
-
-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('/<path:path>/')
-def flatpage(path):
- page = pages.get_or_404(path)
- template = page.meta.get('template', 'flatpage.html')
- return render_template(template, page=page)
-
-@app.before_request
-def do_something_whenever_a_request_comes_in():
- if current_user.is_authenticated:
- if current_user.rank == UserRank.BANNED:
- flash("You have been banned.", "error")
- logout_user()
- return redirect(url_for('user.login'))
- elif current_user.rank == UserRank.NOT_JOINED:
- current_user.rank = UserRank.MEMBER
- db.session.commit()
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from . import admin, licenseseditor, tagseditor, versioneditor, todo
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-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 == "delstuckreleases":
- PackageRelease.query.filter(PackageRelease.task_id != None).delete()
- db.session.commit()
- return redirect(url_for("admin_page"))
- elif 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)
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-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/<name>/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)
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-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/<name>/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)
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from flask import *
-from flask_user import *
-import flask_menu as menu
-from app import app
-from app.models import *
-from app.querybuilder import QueryBuilder
-
-@app.route("/todo/", methods=["GET", "POST"])
-@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).order_by(db.desc(Package.created_at)).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()
-
- if not canApproveNew and not canApproveRel and not canApproveScn:
- abort(403)
-
- if request.method == "POST":
- if request.form["action"] == "screenshots_approve_all":
- if not canApproveScn:
- abort(403)
-
- PackageScreenshot.query.update({ "approved": True })
- db.session.commit()
- return redirect(url_for("todo_page"))
- else:
- abort(400)
-
- topic_query = ForumTopic.query \
- .filter_by(discarded=False)
-
- total_topics = topic_query.count()
- topics_to_add = topic_query \
- .filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
- .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, total_topics=total_topics)
-
-
-@app.route("/todo/topics/")
-@login_required
-def todo_topics_page():
- qb = QueryBuilder(request.args)
- qb.setSortIfNone("date")
- query = qb.buildTopicQuery()
-
- tmp_q = ForumTopic.query
- if not qb.show_discarded:
- tmp_q = tmp_q.filter_by(discarded=False)
- total = tmp_q.count()
- topic_count = query.count()
-
- 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=qb.search, \
- show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
- if query.has_next else None
- prev_url = url_for("todo_topics_page", page=query.prev_num, query=qb.search, \
- show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
- if query.has_prev else None
-
- return render_template("todo/topics.html", topics=query.items, total=total, \
- topic_count=topic_count, query=qb.search, show_discarded=qb.show_discarded, \
- next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, \
- n=num, sort_by=qb.order_by)
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-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("/versions/")
-@rank_required(UserRank.MODERATOR)
-def version_list_page():
- return render_template("admin/versions/list.html", versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all())
-
-class VersionForm(FlaskForm):
- name = StringField("Name", [InputRequired(), Length(3,100)])
- protocol = IntegerField("Protocol")
- submit = SubmitField("Save")
-
-@app.route("/versions/new/", methods=["GET", "POST"])
-@app.route("/versions/<name>/edit/", methods=["GET", "POST"])
-@rank_required(UserRank.MODERATOR)
-def createedit_version_page(name=None):
- version = None
- if name is not None:
- version = MinetestRelease.query.filter_by(name=name).first()
- if version is None:
- abort(404)
-
- form = VersionForm(formdata=request.form, obj=version)
- if request.method == "POST" and form.validate():
- if version is None:
- version = MinetestRelease(form.name.data)
- db.session.add(version)
- flash("Created version " + form.name.data, "success")
- else:
- flash("Updated version " + form.name.data, "success")
-
- form.populate_obj(version)
- db.session.commit()
- return redirect(url_for("version_list_page"))
-
- return render_template("admin/versions/edit.html", version=version, form=form)
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from flask import *
-from flask_user import *
-from app import app
-from app.models import *
-from app.utils import is_package_page
-from app.querybuilder import QueryBuilder
-
-@app.route("/api/packages/")
-def api_packages_page():
- qb = QueryBuilder(request.args)
- query = qb.buildPackageQuery()
- ver = qb.getMinetestVersion()
-
- pkgs = [package.getAsDictionaryShort(app.config["BASE_URL"], version=ver) \
- for package in query.all()]
- return jsonify(pkgs)
-
-
-@app.route("/api/packages/<author>/<name>/")
-@is_package_page
-def api_package_page(package):
- return jsonify(package.getAsDictionary(app.config["BASE_URL"]))
-
-
-@app.route("/api/packages/<author>/<name>/dependencies/")
-@is_package_page
-def api_package_deps_page(package):
- ret = []
-
- for dep in package.dependencies:
- name = None
- fulfilled_by = None
-
- if dep.package:
- name = dep.package.name
- fulfilled_by = [ dep.package.getAsDictionaryKey() ]
-
- elif dep.meta_package:
- name = dep.meta_package.name
- fulfilled_by = [ pkg.getAsDictionaryKey() for pkg in dep.meta_package.packages]
-
- else:
- raise "Malformed dependency"
-
- ret.append({
- "name": name,
- "is_optional": dep.optional,
- "packages": fulfilled_by
- })
-
- return jsonify(ret)
-
-
-@app.route("/api/topics/")
-def api_topics_page():
- qb = QueryBuilder(request.args)
- query = qb.buildTopicQuery(show_added=True)
- return jsonify([t.getAsDictionary() for t in query.all()])
-
-
-@app.route("/api/topic_discard/", methods=["POST"])
-@login_required
-def topic_set_discard():
- tid = request.args.get("tid")
- discard = request.args.get("discard")
- if tid is None or discard is None:
- abort(400)
-
- topic = ForumTopic.query.get(tid)
- if not topic.checkPerm(current_user, Permission.TOPIC_DISCARD):
- abort(403)
-
- topic.discarded = discard == "true"
- db.session.commit()
-
- return jsonify(topic.getAsDictionary())
-
-
-@app.route("/api/minetest_versions/")
-def api_minetest_versions_page():
- return jsonify([{ "name": rel.name, "protocol_version": rel.protocol }\
- for rel in MinetestRelease.query.all() if rel.getActual() is not None])
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from flask import *
-from flask_user import *
-from app import app
-from app.models import *
-
-@app.route("/metapackages/")
-def meta_package_list_page():
- mpackages = MetaPackage.query.order_by(db.asc(MetaPackage.name)).all()
- return render_template("meta/list.html", mpackages=mpackages)
-
-@app.route("/metapackages/<name>/")
-def meta_package_page(name):
- mpackage = MetaPackage.query.filter_by(name=name).first()
- if mpackage is None:
- abort(404)
-
- return render_template("meta/view.html", mpackage=mpackage)
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from . import packages, screenshots, releases
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from flask import *
-from flask_user import *
-from app import app
-from app.models import *
-
-from app.utils import *
-
-from flask_wtf import FlaskForm
-from wtforms import *
-from wtforms.validators import *
-from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
-
-from . import PackageForm
-
-
-class EditRequestForm(PackageForm):
- edit_title = StringField("Edit Title", [InputRequired(), Length(1, 100)])
- edit_desc = TextField("Edit Description", [Optional()])
-
-@app.route("/packages/<author>/<name>/requests/new/", methods=["GET","POST"])
-@app.route("/packages/<author>/<name>/requests/<id>/edit/", methods=["GET","POST"])
-@login_required
-@is_package_page
-def create_edit_editrequest_page(package, id=None):
- edited_package = package
-
- erequest = None
- if id is not None:
- erequest = EditRequest.query.get(id)
- if erequest.package != package:
- abort(404)
-
- if not erequest.checkPerm(current_user, Permission.EDIT_EDITREQUEST):
- abort(403)
-
- if erequest.status != 0:
- flash("Can't edit EditRequest, it has already been merged or rejected", "error")
- return redirect(erequest.getURL())
-
- edited_package = Package(package)
- erequest.applyAll(edited_package)
-
- form = EditRequestForm(request.form, obj=edited_package)
- if request.method == "GET":
- deps = edited_package.dependencies
- form.harddep_str.data = ",".join([str(x) for x in deps if not x.optional])
- form.softdep_str.data = ",".join([str(x) for x in deps if x.optional])
- form.provides_str.data = MetaPackage.ListToSpec(edited_package.provides)
-
- if request.method == "POST" and form.validate():
- if erequest is None:
- erequest = EditRequest()
- erequest.package = package
- erequest.author = current_user
-
- erequest.title = form["edit_title"].data
- erequest.desc = form["edit_desc"].data
- db.session.add(erequest)
-
- EditRequestChange.query.filter_by(request=erequest).delete()
-
- wasChangeMade = False
- for e in PackagePropertyKey:
- newValue = form[e.name].data
- oldValue = getattr(package, e.name)
-
- newValueComp = newValue
- oldValueComp = oldValue
- if type(newValue) is str:
- newValue = newValue.replace("\r\n", "\n")
- newValueComp = newValue.strip()
- oldValueComp = "" if oldValue is None else oldValue.strip()
-
- if newValueComp != oldValueComp:
- change = EditRequestChange()
- change.request = erequest
- change.key = e
- change.oldValue = e.convert(oldValue)
- change.newValue = e.convert(newValue)
- db.session.add(change)
- wasChangeMade = True
-
- if wasChangeMade:
- msg = "{}: Edit request #{} {}" \
- .format(package.title, erequest.id, "created" if id is None else "edited")
- triggerNotif(package.author, current_user, msg, erequest.getURL())
- triggerNotif(erequest.author, current_user, msg, erequest.getURL())
- db.session.commit()
- return redirect(erequest.getURL())
- else:
- flash("No changes detected", "warning")
- elif erequest is not None:
- form["edit_title"].data = erequest.title
- form["edit_desc"].data = erequest.desc
-
- return render_template("packages/editrequest_create_edit.html", package=package, form=form)
-
-
-@app.route("/packages/<author>/<name>/requests/<id>/")
-@is_package_page
-def view_editrequest_page(package, id):
- erequest = EditRequest.query.get(id)
- if erequest is None or erequest.package != package:
- abort(404)
-
- clearNotifications(erequest.getURL())
- return render_template("packages/editrequest_view.html", package=package, request=erequest)
-
-
-@app.route("/packages/<author>/<name>/requests/<id>/approve/", methods=["POST"])
-@is_package_page
-def approve_editrequest_page(package, id):
- if not package.checkPerm(current_user, Permission.APPROVE_CHANGES):
- flash("You don't have permission to do that.", "error")
- return redirect(package.getDetailsURL())
-
- erequest = EditRequest.query.get(id)
- if erequest is None or erequest.package != package:
- abort(404)
-
- if erequest.status != 0:
- flash("Edit request has already been resolved", "error")
-
- else:
- erequest.status = 1
- erequest.applyAll(package)
-
- msg = "{}: Edit request #{} merged".format(package.title, erequest.id)
- triggerNotif(erequest.author, current_user, msg, erequest.getURL())
- triggerNotif(package.author, current_user, msg, erequest.getURL())
- db.session.commit()
-
- return redirect(package.getDetailsURL())
-
-@app.route("/packages/<author>/<name>/requests/<id>/reject/", methods=["POST"])
-@is_package_page
-def reject_editrequest_page(package, id):
- if not package.checkPerm(current_user, Permission.APPROVE_CHANGES):
- flash("You don't have permission to do that.", "error")
- return redirect(package.getDetailsURL())
-
- erequest = EditRequest.query.get(id)
- if erequest is None or erequest.package != package:
- abort(404)
-
- if erequest.status != 0:
- flash("Edit request has already been resolved", "error")
-
- else:
- erequest.status = 2
-
- msg = "{}: Edit request #{} rejected".format(package.title, erequest.id)
- triggerNotif(erequest.author, current_user, msg, erequest.getURL())
- triggerNotif(package.author, current_user, msg, erequest.getURL())
- db.session.commit()
-
- return redirect(package.getDetailsURL())
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from flask import render_template, abort, request, redirect, url_for, flash
-from flask_user import current_user
-import flask_menu as menu
-from app import app
-from app.models import *
-from app.querybuilder import QueryBuilder
-from app.tasks.importtasks import importRepoScreenshot
-from app.utils import *
-from flask_wtf import FlaskForm
-from wtforms import *
-from wtforms.validators import *
-from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
-from sqlalchemy import or_
-
-
-@menu.register_menu(app, ".mods", "Mods", order=11, endpoint_arguments_constructor=lambda: { 'type': 'mod' })
-@menu.register_menu(app, ".games", "Games", order=12, endpoint_arguments_constructor=lambda: { 'type': 'game' })
-@menu.register_menu(app, ".txp", "Texture Packs", order=13, endpoint_arguments_constructor=lambda: { 'type': 'txp' })
-@menu.register_menu(app, ".random", "Random", order=14, endpoint_arguments_constructor=lambda: { 'random': '1' })
-@app.route("/packages/")
-def packages_page():
- qb = QueryBuilder(request.args)
- query = qb.buildPackageQuery()
- title = qb.title
-
- if qb.lucky:
- package = query.first()
- if package:
- return redirect(package.getDetailsURL())
-
- topic = qb.buildTopicQuery().first()
- if qb.search and topic:
- return redirect("https://forum.minetest.net/viewtopic.php?t=" + str(topic.topic_id))
-
- page = int(request.args.get("page") or 1)
- num = min(40, int(request.args.get("n") or 100))
- query = query.paginate(page, num, True)
-
- search = request.args.get("q")
- type_name = request.args.get("type")
-
- next_url = url_for("packages_page", type=type_name, q=search, page=query.next_num) \
- if query.has_next else None
- prev_url = url_for("packages_page", type=type_name, q=search, page=query.prev_num) \
- if query.has_prev else None
-
- topics = None
- if qb.search and not query.has_next:
- topics = qb.buildTopicQuery().all()
-
- tags = Tag.query.all()
- return render_template("packages/list.html", \
- title=title, packages=query.items, topics=topics, \
- query=search, tags=tags, type=type_name, \
- next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, packages_count=query.total)
-
-
-def getReleases(package):
- if package.checkPerm(current_user, Permission.MAKE_RELEASE):
- return package.releases.limit(5)
- else:
- return package.releases.filter_by(approved=True).limit(5)
-
-
-@app.route("/packages/<author>/<name>/")
-@is_package_page
-def package_page(package):
- clearNotifications(package.getDetailsURL())
-
- alternatives = None
- if package.type == PackageType.MOD:
- alternatives = Package.query \
- .filter_by(name=package.name, type=PackageType.MOD, soft_deleted=False) \
- .filter(Package.id != package.id) \
- .order_by(db.desc(Package.score)) \
- .all()
-
-
- show_similar_topics = current_user == package.author or \
- package.checkPerm(current_user, Permission.APPROVE_NEW)
-
- similar_topics = None if not show_similar_topics else \
- ForumTopic.query \
- .filter_by(name=package.name) \
- .filter(ForumTopic.topic_id != package.forums) \
- .filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
- .order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
- .all()
-
- releases = getReleases(package)
- requests = [r for r in package.requests if r.status == 0]
-
- review_thread = package.review_thread
- if review_thread is not None and not review_thread.checkPerm(current_user, Permission.SEE_THREAD):
- review_thread = None
-
- topic_error = None
- topic_error_lvl = "warning"
- if not package.approved and package.forums is not None:
- errors = []
- if Package.query.filter_by(forums=package.forums, soft_deleted=False).count() > 1:
- errors.append("<b>Error: Another package already uses this forum topic!</b>")
- topic_error_lvl = "danger"
-
- topic = ForumTopic.query.get(package.forums)
- if topic is not None:
- if topic.author != package.author:
- errors.append("<b>Error: Forum topic author doesn't match package author.</b>")
- topic_error_lvl = "danger"
-
- if topic.wip:
- errors.append("Warning: Forum topic is in WIP section, make sure package meets playability standards.")
- elif package.type != PackageType.TXP:
- errors.append("Warning: Forum topic not found. This may happen if the topic has only just been created.")
-
- topic_error = "<br />".join(errors)
-
-
- threads = Thread.query.filter_by(package_id=package.id)
- if not current_user.is_authenticated:
- threads = threads.filter_by(private=False)
- elif not current_user.rank.atLeast(UserRank.EDITOR) and not current_user == package.author:
- threads = threads.filter(or_(Thread.private == False, Thread.author == current_user))
-
-
- return render_template("packages/view.html", \
- package=package, releases=releases, requests=requests, \
- alternatives=alternatives, similar_topics=similar_topics, \
- review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl, \
- threads=threads.all())
-
-
-@app.route("/packages/<author>/<name>/download/")
-@is_package_page
-def package_download_page(package):
- release = package.getDownloadRelease()
-
- if release is None:
- if "application/zip" in request.accept_mimetypes and \
- not "text/html" in request.accept_mimetypes:
- return "", 204
- else:
- flash("No download available.", "error")
- return redirect(package.getDetailsURL())
- else:
- PackageRelease.query.filter_by(id=release.id).update({
- "downloads": PackageRelease.downloads + 1
- })
- db.session.commit()
-
- return redirect(release.url, code=302)
-
-
-class PackageForm(FlaskForm):
- name = StringField("Name (Technical)", [InputRequired(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
- title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 50)])
- short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
- desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
- type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
- license = QuerySelectField("License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
- media_license = QuerySelectField("Media License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
- provides_str = StringField("Provides (mods included in package)", [Optional()])
- tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title)
- harddep_str = StringField("Hard Dependencies", [Optional()])
- softdep_str = StringField("Soft Dependencies", [Optional()])
- repo = StringField("VCS Repository URL", [Optional(), URL()], filters = [lambda x: x or None])
- website = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None])
- issueTracker = StringField("Issue Tracker URL", [Optional(), URL()], filters = [lambda x: x or None])
- forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
- submit = SubmitField("Save")
-
-@app.route("/packages/new/", methods=["GET", "POST"])
-@app.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
-@login_required
-def create_edit_package_page(author=None, name=None):
- package = None
- form = None
- if author is None:
- form = PackageForm(formdata=request.form)
- author = request.args.get("author")
- if author is None or author == current_user.username:
- author = current_user
- else:
- author = User.query.filter_by(username=author).first()
- if author is None:
- flash("Unable to find that user", "error")
- return redirect(url_for("create_edit_package_page"))
-
- if not author.checkPerm(current_user, Permission.CHANGE_AUTHOR):
- flash("Permission denied", "error")
- return redirect(url_for("create_edit_package_page"))
-
- else:
- package = getPackageByInfo(author, name)
- if not package.checkPerm(current_user, Permission.EDIT_PACKAGE):
- return redirect(package.getDetailsURL())
-
- author = package.author
-
- form = PackageForm(formdata=request.form, obj=package)
-
- # Initial form class from post data and default data
- if request.method == "GET":
- if package is None:
- form.name.data = request.args.get("bname")
- form.title.data = request.args.get("title")
- form.repo.data = request.args.get("repo")
- form.forums.data = request.args.get("forums")
- else:
- deps = package.dependencies
- form.harddep_str.data = ",".join([str(x) for x in deps if not x.optional])
- form.softdep_str.data = ",".join([str(x) for x in deps if x.optional])
- form.provides_str.data = MetaPackage.ListToSpec(package.provides)
-
- if request.method == "POST" and form.validate():
- wasNew = False
- if not package:
- package = Package.query.filter_by(name=form["name"].data, author_id=author.id).first()
- if package is not None:
- if package.soft_deleted:
- Package.query.filter_by(name=form["name"].data, author_id=author.id).delete()
- else:
- flash("Package already exists!", "error")
- return redirect(url_for("create_edit_package_page"))
-
- package = Package()
- package.author = author
- wasNew = True
-
- elif package.approved and package.name != form.name.data and \
- not package.checkPerm(current_user, Permission.CHANGE_NAME):
- flash("Unable to change package name", "danger")
- return redirect(url_for("create_edit_package_page", author=author, name=name))
-
- else:
- triggerNotif(package.author, current_user,
- "{} edited".format(package.title), package.getDetailsURL())
-
- form.populate_obj(package) # copy to row
-
- if package.type== PackageType.TXP:
- package.license = package.media_license
-
- mpackage_cache = {}
- package.provides.clear()
- mpackages = MetaPackage.SpecToList(form.provides_str.data, mpackage_cache)
- for m in mpackages:
- package.provides.append(m)
-
- Dependency.query.filter_by(depender=package).delete()
- deps = Dependency.SpecToList(package, form.harddep_str.data, mpackage_cache)
- for dep in deps:
- dep.optional = False
- db.session.add(dep)
-
- deps = Dependency.SpecToList(package, form.softdep_str.data, mpackage_cache)
- for dep in deps:
- dep.optional = True
- db.session.add(dep)
-
- if wasNew and package.type == PackageType.MOD and not package.name in mpackage_cache:
- m = MetaPackage.GetOrCreate(package.name, mpackage_cache)
- package.provides.append(m)
-
- package.tags.clear()
- for tag in form.tags.raw_data:
- package.tags.append(Tag.query.get(tag))
-
- db.session.commit() # save
-
- next_url = package.getDetailsURL()
- if wasNew and package.repo is not None:
- task = importRepoScreenshot.delay(package.id)
- next_url = url_for("check_task", id=task.id, r=next_url)
-
- if wasNew and ("WTFPL" in package.license.name or "WTFPL" in package.media_license.name):
- next_url = url_for("flatpage", path="help/wtfpl", r=next_url)
-
- return redirect(next_url)
-
- package_query = Package.query.filter_by(approved=True, soft_deleted=False)
- if package is not None:
- package_query = package_query.filter(Package.id != package.id)
-
- enableWizard = name is None and request.method != "POST"
- return render_template("packages/create_edit.html", package=package, \
- form=form, author=author, enable_wizard=enableWizard, \
- packages=package_query.all(), \
- mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all())
-
-@app.route("/packages/<author>/<name>/approve/", methods=["POST"])
-@login_required
-@is_package_page
-def approve_package_page(package):
- if not package.checkPerm(current_user, Permission.APPROVE_NEW):
- flash("You don't have permission to do that.", "error")
-
- elif package.approved:
- flash("Package has already been approved", "error")
-
- else:
- package.approved = True
-
- screenshots = PackageScreenshot.query.filter_by(package=package, approved=False).all()
- for s in screenshots:
- s.approved = True
-
- triggerNotif(package.author, current_user,
- "{} approved".format(package.title), package.getDetailsURL())
- db.session.commit()
-
- return redirect(package.getDetailsURL())
-
-
-@app.route("/packages/<author>/<name>/remove/", methods=["GET", "POST"])
-@login_required
-@is_package_page
-def remove_package_page(package):
- if request.method == "GET":
- return render_template("packages/remove.html", package=package)
-
- if "delete" in request.form:
- if not package.checkPerm(current_user, Permission.DELETE_PACKAGE):
- flash("You don't have permission to do that.", "error")
- return redirect(package.getDetailsURL())
-
- package.soft_deleted = True
-
- url = url_for("user_profile_page", username=package.author.username)
- triggerNotif(package.author, current_user,
- "{} deleted".format(package.title), url)
- db.session.commit()
-
- flash("Deleted package", "success")
-
- return redirect(url)
- elif "unapprove" in request.form:
- if not package.checkPerm(current_user, Permission.UNAPPROVE_PACKAGE):
- flash("You don't have permission to do that.", "error")
- return redirect(package.getDetailsURL())
-
- package.approved = False
-
- triggerNotif(package.author, current_user,
- "{} deleted".format(package.title), package.getDetailsURL())
- db.session.commit()
-
- flash("Unapproved package", "success")
-
- return redirect(package.getDetailsURL())
- else:
- abort(400)
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from flask import *
-from flask_user import *
-from app import app
-from app.models import *
-from app.tasks.importtasks import makeVCSRelease
-
-from app.utils import *
-
-from celery import uuid
-from flask_wtf import FlaskForm
-from wtforms import *
-from wtforms.validators import *
-from wtforms.ext.sqlalchemy.fields import QuerySelectField
-
-
-def get_mt_releases(is_max):
- query = MinetestRelease.query.order_by(db.asc(MinetestRelease.id))
- if is_max:
- query = query.limit(query.count() - 1)
- else:
- query = query.filter(MinetestRelease.name != "0.4.17")
-
- return query
-
-
-class CreatePackageReleaseForm(FlaskForm):
- title = StringField("Title", [InputRequired(), Length(1, 30)])
- uploadOpt = RadioField ("Method", choices=[("upload", "File Upload")], default="upload")
- vcsLabel = StringField("VCS Commit Hash, Branch, or Tag", default="master")
- fileUpload = FileField("File Upload")
- min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
- query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
- max_rel = QuerySelectField("Maximum Minetest Version", [InputRequired()],
- query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
- submit = SubmitField("Save")
-
-class EditPackageReleaseForm(FlaskForm):
- title = StringField("Title", [InputRequired(), Length(1, 30)])
- url = StringField("URL", [URL])
- task_id = StringField("Task ID", filters = [lambda x: x or None])
- approved = BooleanField("Is Approved")
- min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
- query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
- max_rel = QuerySelectField("Maximum Minetest Version", [InputRequired()],
- query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
- submit = SubmitField("Save")
-
-@app.route("/packages/<author>/<name>/releases/new/", methods=["GET", "POST"])
-@login_required
-@is_package_page
-def create_release_page(package):
- if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
- return redirect(package.getDetailsURL())
-
- # Initial form class from post data and default data
- form = CreatePackageReleaseForm()
- if package.repo is not None:
- form["uploadOpt"].choices = [("vcs", "From Git Commit or Branch"), ("upload", "File Upload")]
- if request.method != "POST":
- form["uploadOpt"].data = "vcs"
-
- if request.method == "POST" and form.validate():
- if form["uploadOpt"].data == "vcs":
- rel = PackageRelease()
- rel.package = package
- rel.title = form["title"].data
- rel.url = ""
- rel.task_id = uuid()
- rel.min_rel = form["min_rel"].data.getActual()
- rel.max_rel = form["max_rel"].data.getActual()
- db.session.add(rel)
- db.session.commit()
-
- makeVCSRelease.apply_async((rel.id, form["vcsLabel"].data), task_id=rel.task_id)
-
- msg = "{}: Release {} created".format(package.title, rel.title)
- triggerNotif(package.author, current_user, msg, rel.getEditURL())
- db.session.commit()
-
- return redirect(url_for("check_task", id=rel.task_id, r=rel.getEditURL()))
- else:
- uploadedPath = doFileUpload(form.fileUpload.data, "zip", "a zip file")
- if uploadedPath is not None:
- rel = PackageRelease()
- rel.package = package
- rel.title = form["title"].data
- rel.url = uploadedPath
- rel.min_rel = form["min_rel"].data.getActual()
- rel.max_rel = form["max_rel"].data.getActual()
- rel.approve(current_user)
- db.session.add(rel)
- db.session.commit()
-
- msg = "{}: Release {} created".format(package.title, rel.title)
- triggerNotif(package.author, current_user, msg, rel.getEditURL())
- db.session.commit()
- return redirect(package.getDetailsURL())
-
- return render_template("packages/release_new.html", package=package, form=form)
-
-@app.route("/packages/<author>/<name>/releases/<id>/download/")
-@is_package_page
-def download_release_page(package, id):
- release = PackageRelease.query.get(id)
- if release is None or release.package != package:
- abort(404)
-
- if release is None:
- if "application/zip" in request.accept_mimetypes and \
- not "text/html" in request.accept_mimetypes:
- return "", 204
- else:
- flash("No download available.", "error")
- return redirect(package.getDetailsURL())
- else:
- PackageRelease.query.filter_by(id=release.id).update({
- "downloads": PackageRelease.downloads + 1
- })
- db.session.commit()
-
- return redirect(release.url, code=300)
-
-@app.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
-@login_required
-@is_package_page
-def edit_release_page(package, id):
- release = PackageRelease.query.get(id)
- if release is None or release.package != package:
- abort(404)
-
- clearNotifications(release.getEditURL())
-
- canEdit = package.checkPerm(current_user, Permission.MAKE_RELEASE)
- canApprove = package.checkPerm(current_user, Permission.APPROVE_RELEASE)
- if not (canEdit or canApprove):
- return redirect(package.getDetailsURL())
-
- # Initial form class from post data and default data
- form = EditPackageReleaseForm(formdata=request.form, obj=release)
- if request.method == "POST" and form.validate():
- wasApproved = release.approved
- if canEdit:
- release.title = form["title"].data
- release.min_rel = form["min_rel"].data.getActual()
- release.max_rel = form["max_rel"].data.getActual()
-
- if package.checkPerm(current_user, Permission.CHANGE_RELEASE_URL):
- release.url = form["url"].data
- release.task_id = form["task_id"].data
- if release.task_id is not None:
- release.task_id = None
-
- if canApprove:
- release.approved = form["approved"].data
- else:
- release.approved = wasApproved
-
- db.session.commit()
- return redirect(package.getDetailsURL())
-
- return render_template("packages/release_edit.html", package=package, release=release, form=form)
-
-
-
-class BulkReleaseForm(FlaskForm):
- set_min = BooleanField("Set Min")
- min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
- query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
- set_max = BooleanField("Set Max")
- max_rel = QuerySelectField("Maximum Minetest Version", [InputRequired()],
- query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
- only_change_none = BooleanField("Only change values previously set as none")
- submit = SubmitField("Update")
-
-
-@app.route("/packages/<author>/<name>/releases/bulk_change/", methods=["GET", "POST"])
-@login_required
-@is_package_page
-def bulk_change_release_page(package):
- if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
- return redirect(package.getDetailsURL())
-
- # Initial form class from post data and default data
- form = BulkReleaseForm()
-
- if request.method == "GET":
- form.only_change_none.data = True
- elif request.method == "POST" and form.validate():
- only_change_none = form.only_change_none.data
-
- for release in package.releases.all():
- if form["set_min"].data and (not only_change_none or release.min_rel is None):
- release.min_rel = form["min_rel"].data.getActual()
- if form["set_max"].data and (not only_change_none or release.max_rel is None):
- release.max_rel = form["max_rel"].data.getActual()
-
- db.session.commit()
-
- return redirect(package.getDetailsURL())
-
- return render_template("packages/release_bulk_change.html", package=package, form=form)
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from flask import *
-from flask_user import *
-from app import app
-from app.models import *
-
-from app.utils import *
-
-from flask_wtf import FlaskForm
-from wtforms import *
-from wtforms.validators import *
-
-
-class CreateScreenshotForm(FlaskForm):
- title = StringField("Title/Caption", [Optional()])
- fileUpload = FileField("File Upload", [InputRequired()])
- submit = SubmitField("Save")
-
-
-class EditScreenshotForm(FlaskForm):
- title = StringField("Title/Caption", [Optional()])
- approved = BooleanField("Is Approved")
- delete = BooleanField("Delete")
- submit = SubmitField("Save")
-
-@app.route("/packages/<author>/<name>/screenshots/new/", methods=["GET", "POST"])
-@login_required
-@is_package_page
-def create_screenshot_page(package, id=None):
- if not package.checkPerm(current_user, Permission.ADD_SCREENSHOTS):
- return redirect(package.getDetailsURL())
-
- # Initial form class from post data and default data
- form = CreateScreenshotForm()
- if request.method == "POST" and form.validate():
- uploadedPath = doFileUpload(form.fileUpload.data, "image",
- "a PNG or JPG image file")
- if uploadedPath is not None:
- ss = PackageScreenshot()
- ss.package = package
- ss.title = form["title"].data or "Untitled"
- ss.url = uploadedPath
- ss.approved = package.checkPerm(current_user, Permission.APPROVE_SCREENSHOT)
- db.session.add(ss)
-
- msg = "{}: Screenshot added {}" \
- .format(package.title, ss.title)
- triggerNotif(package.author, current_user, msg, package.getDetailsURL())
- db.session.commit()
- return redirect(package.getDetailsURL())
-
- return render_template("packages/screenshot_new.html", package=package, form=form)
-
-@app.route("/packages/<author>/<name>/screenshots/<id>/edit/", methods=["GET", "POST"])
-@login_required
-@is_package_page
-def edit_screenshot_page(package, id):
- screenshot = PackageScreenshot.query.get(id)
- if screenshot is None or screenshot.package != package:
- abort(404)
-
- canEdit = package.checkPerm(current_user, Permission.ADD_SCREENSHOTS)
- canApprove = package.checkPerm(current_user, Permission.APPROVE_SCREENSHOT)
- if not (canEdit or canApprove):
- return redirect(package.getDetailsURL())
-
- clearNotifications(screenshot.getEditURL())
-
- # Initial form class from post data and default data
- form = EditScreenshotForm(formdata=request.form, obj=screenshot)
- if request.method == "POST" and form.validate():
- if canEdit and form["delete"].data:
- PackageScreenshot.query.filter_by(id=id).delete()
-
- else:
- wasApproved = screenshot.approved
-
- if canEdit:
- screenshot.title = form["title"].data or "Untitled"
-
- if canApprove:
- screenshot.approved = form["approved"].data
- else:
- screenshot.approved = wasApproved
-
- db.session.commit()
- return redirect(package.getDetailsURL())
-
- return render_template("packages/screenshot_edit.html", package=package, screenshot=screenshot, form=form)
+++ /dev/null
-# -*- coding: utf-8 -*-
-"""
-A small Flask extension that makes it easy to use Sass (SCSS) with your
-Flask application.
-
-Code unabashedly adapted from https://github.com/weapp/flask-coffee2js
-
-:copyright: (c) 2012 by Ivan Miric.
-:license: MIT, see LICENSE for more details.
-"""
-
-import os
-import os.path
-import codecs
-from flask import *
-from scss import Scss
-
-from app import app
-
-def _convert(dir, src, dst):
- original_wd = os.getcwd()
- os.chdir(dir)
-
- css = Scss()
- source = codecs.open(src, 'r', encoding='utf-8').read()
- output = css.compile(source)
-
- os.chdir(original_wd)
-
- outfile = codecs.open(dst, 'w', encoding='utf-8')
- outfile.write(output)
- outfile.close()
-
-def _getDirPath(originalPath, create=False):
- path = originalPath
-
- if not os.path.isdir(path):
- path = os.path.join(app.root_path, path)
-
- if not os.path.isdir(path):
- if create:
- os.mkdir(path)
- else:
- raise IOError("Unable to find " + originalPath)
-
- return path
-
-def sass(app, inputDir='scss', outputPath='static', force=False, cacheDir="public/static"):
- static_url_path = app.static_url_path
- inputDir = _getDirPath(inputDir)
- cacheDir = _getDirPath(cacheDir or outputPath, True)
-
- def _sass(filepath):
- sassfile = "%s/%s.scss" % (inputDir, filepath)
- cacheFile = "%s/%s.css" % (cacheDir, filepath)
-
- # Source file exists, and needs regenerating
- if os.path.isfile(sassfile) and (force or not os.path.isfile(cacheFile) or \
- os.path.getmtime(sassfile) > os.path.getmtime(cacheFile)):
- _convert(inputDir, sassfile, cacheFile)
- app.logger.debug('Compiled %s into %s' % (sassfile, cacheFile))
-
- return send_from_directory(cacheDir, filepath + ".css")
-
- app.add_url_rule("/%s/<path:filepath>.css" % (outputPath), 'sass', _sass)
-
-sass(app)
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from flask import *
-from flask_user import *
-import flask_menu as menu
-from app import app, csrf
-from app.models import *
-from app.tasks import celery, TaskError
-from app.tasks.importtasks import getMeta
-from app.utils import shouldReturnJson
-# from celery.result import AsyncResult
-
-from app.utils import *
-
-@csrf.exempt
-@app.route("/tasks/getmeta/new/", methods=["POST"])
-@login_required
-def new_getmeta_page():
- author = request.args.get("author")
- author = current_user.forums_username if author is None else author
- aresult = getMeta.delay(request.args.get("url"), author)
- return jsonify({
- "poll_url": url_for("check_task", id=aresult.id),
- })
-
-@app.route("/tasks/<id>/")
-def check_task(id):
- result = celery.AsyncResult(id)
- status = result.status
- traceback = result.traceback
- result = result.result
-
- info = None
- if isinstance(result, Exception):
- info = {
- 'id': id,
- 'status': status,
- }
-
- if current_user.is_authenticated and current_user.rank.atLeast(UserRank.ADMIN):
- info["error"] = str(traceback)
- elif str(result)[1:12] == "TaskError: ":
- info["error"] = str(result)[12:-1]
- else:
- info["error"] = "Unknown server error"
- else:
- info = {
- 'id': id,
- 'status': status,
- 'result': result,
- }
-
- if shouldReturnJson():
- return jsonify(info)
- else:
- r = request.args.get("r")
- if r is not None and status == "SUCCESS":
- return redirect(r)
- else:
- return render_template("tasks/view.html", info=info)
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from flask import *
-from flask_user import *
-from app import app
-from app.models import *
-from app.utils import triggerNotif, clearNotifications
-
-import datetime
-
-from flask_wtf import FlaskForm
-from wtforms import *
-from wtforms.validators import *
-
-@app.route("/threads/")
-def threads_page():
- query = Thread.query
- if not Permission.SEE_THREAD.check(current_user):
- query = query.filter_by(private=False)
- return render_template("threads/list.html", threads=query.all())
-
-
-@app.route("/threads/<int:id>/subscribe/", methods=["POST"])
-@login_required
-def thread_subscribe_page(id):
- thread = Thread.query.get(id)
- if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
- abort(404)
-
- if current_user in thread.watchers:
- flash("Already subscribed!", "success")
- else:
- flash("Subscribed to thread", "success")
- thread.watchers.append(current_user)
- db.session.commit()
-
- return redirect(url_for("thread_page", id=id))
-
-
-@app.route("/threads/<int:id>/unsubscribe/", methods=["POST"])
-@login_required
-def thread_unsubscribe_page(id):
- thread = Thread.query.get(id)
- if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
- abort(404)
-
- if current_user in thread.watchers:
- flash("Unsubscribed!", "success")
- thread.watchers.remove(current_user)
- db.session.commit()
- else:
- flash("Not subscribed to thread", "success")
-
- return redirect(url_for("thread_page", id=id))
-
-
-@app.route("/threads/<int:id>/", methods=["GET", "POST"])
-def thread_page(id):
- clearNotifications(url_for("thread_page", id=id))
-
- thread = Thread.query.get(id)
- if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
- abort(404)
-
- if current_user.is_authenticated and request.method == "POST":
- comment = request.form["comment"]
-
- if not current_user.canCommentRL():
- flash("Please wait before commenting again", "danger")
- if package:
- return redirect(package.getDetailsURL())
- else:
- return redirect(url_for("home_page"))
-
- if len(comment) <= 500 and len(comment) > 3:
- reply = ThreadReply()
- reply.author = current_user
- reply.comment = comment
- db.session.add(reply)
-
- thread.replies.append(reply)
- if not current_user in thread.watchers:
- thread.watchers.append(current_user)
-
- msg = None
- if thread.package is None:
- msg = "New comment on '{}'".format(thread.title)
- else:
- msg = "New comment on '{}' on package {}".format(thread.title, thread.package.title)
-
-
- for user in thread.watchers:
- if user != current_user:
- triggerNotif(user, current_user, msg, url_for("thread_page", id=thread.id))
-
- db.session.commit()
-
- return redirect(url_for("thread_page", id=id))
-
- else:
- flash("Comment needs to be between 3 and 500 characters.")
-
- return render_template("threads/view.html", thread=thread)
-
-
-class ThreadForm(FlaskForm):
- title = StringField("Title", [InputRequired(), Length(3,100)])
- comment = TextAreaField("Comment", [InputRequired(), Length(10, 500)])
- private = BooleanField("Private")
- submit = SubmitField("Open Thread")
-
-@app.route("/threads/new/", methods=["GET", "POST"])
-@login_required
-def new_thread_page():
- form = ThreadForm(formdata=request.form)
-
- package = None
- if "pid" in request.args:
- package = Package.query.get(int(request.args.get("pid")))
- if package is None:
- flash("Unable to find that package!", "error")
-
- # Don't allow making orphan threads on approved packages for now
- if package is None:
- abort(403)
-
- def_is_private = request.args.get("private") or False
- if package is None:
- def_is_private = True
- allow_change = package and package.approved
- is_review_thread = package and not package.approved
-
- # Check that user can make the thread
- if not package.checkPerm(current_user, Permission.CREATE_THREAD):
- flash("Unable to create thread!", "error")
- return redirect(url_for("home_page"))
-
- # Only allow creating one thread when not approved
- elif is_review_thread and package.review_thread is not None:
- flash("A review thread already exists!", "error")
- return redirect(url_for("thread_page", id=package.review_thread.id))
-
- elif not current_user.canOpenThreadRL():
- flash("Please wait before opening another thread", "danger")
-
- if package:
- return redirect(package.getDetailsURL())
- else:
- return redirect(url_for("home_page"))
-
- # Set default values
- elif request.method == "GET":
- form.private.data = def_is_private
- form.title.data = request.args.get("title") or ""
-
- # Validate and submit
- elif request.method == "POST" and form.validate():
- thread = Thread()
- thread.author = current_user
- thread.title = form.title.data
- thread.private = form.private.data if allow_change else def_is_private
- thread.package = package
- db.session.add(thread)
-
- thread.watchers.append(current_user)
- if package is not None and package.author != current_user:
- thread.watchers.append(package.author)
-
- reply = ThreadReply()
- reply.thread = thread
- reply.author = current_user
- reply.comment = form.comment.data
- db.session.add(reply)
-
- thread.replies.append(reply)
-
- db.session.commit()
-
- if is_review_thread:
- package.review_thread = thread
-
- notif_msg = None
- if package is not None:
- notif_msg = "New thread '{}' on package {}".format(thread.title, package.title)
- triggerNotif(package.author, current_user, notif_msg, url_for("thread_page", id=thread.id))
- else:
- notif_msg = "New thread '{}'".format(thread.title)
-
- for user in User.query.filter(User.rank >= UserRank.EDITOR).all():
- triggerNotif(user, current_user, notif_msg, url_for("thread_page", id=thread.id))
-
- db.session.commit()
-
- return redirect(url_for("thread_page", id=thread.id))
-
-
- return render_template("threads/new.html", form=form, allow_private_change=allow_change, package=package)
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from flask import *
-from app import app
-
-import os
-from PIL import Image
-
-ALLOWED_RESOLUTIONS=[(100,67), (270,180), (350,233)]
-
-def mkdir(path):
- if not os.path.isdir(path):
- os.mkdir(path)
-
-mkdir("app/public/thumbnails/")
-
-def resize_and_crop(img_path, modified_path, size):
- img = Image.open(img_path)
-
- # Get current and desired ratio for the images
- img_ratio = img.size[0] / float(img.size[1])
- ratio = size[0] / float(size[1])
-
- # Is more portrait than target, scale and crop
- if ratio > img_ratio:
- img = img.resize((int(size[0]), int(size[0] * img.size[1] / img.size[0])),
- Image.BICUBIC)
- box = (0, (img.size[1] - size[1]) / 2, img.size[0], (img.size[1] + size[1]) / 2)
- img = img.crop(box)
-
- # Is more landscape than target, scale and crop
- elif ratio < img_ratio:
- img = img.resize((int(size[1] * img.size[0] / img.size[1]), int(size[1])),
- Image.BICUBIC)
- box = ((img.size[0] - size[0]) / 2, 0, (img.size[0] + size[0]) / 2, img.size[1])
- img = img.crop(box)
-
- # Is exactly the same ratio as target
- else:
- img = img.resize(size, Image.BICUBIC)
-
- img.save(modified_path)
-
-
-@app.route("/thumbnails/<int:level>/<img>")
-def make_thumbnail(img, level):
- if level > len(ALLOWED_RESOLUTIONS) or level <= 0:
- abort(403)
-
- w, h = ALLOWED_RESOLUTIONS[level - 1]
-
- mkdir("app/public/thumbnails/{:d}/".format(level))
-
- cache_filepath = "public/thumbnails/{:d}/{}".format(level, img)
- source_filepath = "public/uploads/" + img
-
- resize_and_crop("app/" + source_filepath, "app/" + cache_filepath, (w, h))
- return send_file(cache_filepath)
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-from . import users, githublogin, notifications
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-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"))
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-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"))
+++ /dev/null
-# 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 <https://www.gnu.org/licenses/>.
-
-
-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()], 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")
-
-
-@app.route("/users/", methods=["GET"])
-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/<username>/", 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
- 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):
- 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/<username>/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/<username>/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, 100)])
- password2 = PasswordField("Verify password", [InputRequired(), Length(2, 100)])
- 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"))