From 64f131ae27a7332245b5a4eb8e1e4879d7d99578 Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Fri, 15 Nov 2019 23:51:42 +0000 Subject: [PATCH] Refactor endpoints to use blueprints instead --- .gitignore | 4 +- Dockerfile | 6 +- app/__init__.py | 35 +++++++- app/blueprints/__init__.py | 10 +++ .../users => blueprints/admin}/__init__.py | 6 +- app/{views => blueprints}/admin/admin.py | 28 +++---- .../admin/licenseseditor.py | 14 ++-- app/{views => blueprints}/admin/tagseditor.py | 14 ++-- .../admin/versioneditor.py | 14 ++-- .../api.py => blueprints/api/__init__.py} | 29 +++---- app/blueprints/homepage/__init__.py | 20 +++++ .../metapackages/__init__.py} | 12 +-- .../notifications/__init__.py} | 17 ++-- .../packages/__init__.py | 3 + .../packages/editrequests.py | 1 - .../packages/packages.py | 55 ++++++------ .../packages/releases.py | 23 ++--- .../packages/screenshots.py | 13 +-- .../tasks.py => blueprints/tasks/__init__.py} | 16 ++-- .../threads/__init__.py} | 42 +++++----- .../thumbnails/__init__.py} | 5 +- .../todo.py => blueprints/todo/__init__.py} | 18 ++-- app/blueprints/users/__init__.py | 5 ++ .../users/githublogin.py | 13 +-- .../users.py => blueprints/users/profile.py} | 69 +++++++-------- app/models.py | 26 +++--- app/{views => }/sass.py | 10 +-- app/tasks/emails.py | 2 +- app/tasks/importtasks.py | 2 +- app/template_filters.py | 22 +++++ app/templates/admin/licenses/edit.html | 4 +- app/templates/admin/licenses/list.html | 4 +- app/templates/admin/list.html | 10 +-- ...switch_user_page.html => switch_user.html} | 0 app/templates/admin/tags/edit.html | 4 +- app/templates/admin/tags/list.html | 4 +- app/templates/admin/versions/edit.html | 4 +- app/templates/admin/versions/list.html | 4 +- app/templates/base.html | 22 ++--- app/templates/emails/verify.html | 4 +- app/templates/flask_user/login.html | 4 +- app/templates/index.html | 8 +- app/templates/macros/threads.html | 12 +-- app/templates/macros/topics.html | 8 +- app/templates/meta/list.html | 2 +- app/templates/notifications/list.html | 2 +- app/templates/packages/list.html | 2 +- app/templates/packages/release_edit.html | 2 +- app/templates/packages/view.html | 16 ++-- app/templates/tasks/view.html | 2 +- app/templates/todo/list.html | 4 +- app/templates/todo/topics.html | 16 ++-- app/templates/users/claim.html | 6 +- app/templates/users/list.html | 2 +- app/templates/users/user_profile_page.html | 10 +-- app/views/__init__.py | 84 ------------------- app/views/admin/__init__.py | 18 ---- 57 files changed, 396 insertions(+), 396 deletions(-) create mode 100644 app/blueprints/__init__.py rename app/{views/users => blueprints/admin}/__init__.py (84%) rename app/{views => blueprints}/admin/admin.py (82%) rename app/{views => blueprints}/admin/licenseseditor.py (87%) rename app/{views => blueprints}/admin/tagseditor.py (86%) rename app/{views => blueprints}/admin/versioneditor.py (86%) rename app/{views/api.py => blueprints/api/__init__.py} (79%) create mode 100644 app/blueprints/homepage/__init__.py rename app/{views/meta.py => blueprints/metapackages/__init__.py} (87%) rename app/{views/users/notifications.py => blueprints/notifications/__init__.py} (77%) rename app/{views => blueprints}/packages/__init__.py (91%) rename app/{views => blueprints}/packages/editrequests.py (99%) rename app/{views => blueprints}/packages/packages.py (88%) rename app/{views => blueprints}/packages/releases.py (93%) rename app/{views => blueprints}/packages/screenshots.py (92%) rename app/{views/tasks.py => blueprints/tasks/__init__.py} (88%) rename app/{views/threads.py => blueprints/threads/__init__.py} (85%) rename app/{views/thumbnails.py => blueprints/thumbnails/__init__.py} (96%) rename app/{views/admin/todo.py => blueprints/todo/__init__.py} (90%) create mode 100644 app/blueprints/users/__init__.py rename app/{views => blueprints}/users/githublogin.py (90%) rename app/{views/users/users.py => blueprints/users/profile.py} (82%) rename app/{views => }/sass.py (90%) create mode 100644 app/template_filters.py rename app/templates/admin/{switch_user_page.html => switch_user.html} (100%) delete mode 100644 app/views/__init__.py delete mode 100644 app/views/admin/__init__.py diff --git a/.gitignore b/.gitignore index 7ab19d3..c8dd729 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,8 @@ custom.css tmp log.txt *.rdb -uploads -thumbnails +app/public/uploads +app/public/thumbnails celerybeat-schedule /data diff --git a/Dockerfile b/Dockerfile index 0bcaa9e..c88d0a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,10 +10,10 @@ RUN pip install -r ./requirements.txt 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 diff --git a/app/__init__.py b/app/__init__.py index c5d8000..a0b4ba5 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -48,6 +48,10 @@ gravatar = Gravatar(app, 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) @@ -55,8 +59,33 @@ if not app.debug and app.config["MAIL_UTILS_ERROR_SEND_TO"]: @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 . import models, tasks -from .views import * +from .blueprints import create_blueprints +create_blueprints(app) + +from flask_login import logout_user + +@app.route("/uploads/") +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('//') +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 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() diff --git a/app/blueprints/__init__.py b/app/blueprints/__init__.py new file mode 100644 index 0000000..74aa9ae --- /dev/null +++ b/app/blueprints/__init__.py @@ -0,0 +1,10 @@ +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) diff --git a/app/views/users/__init__.py b/app/blueprints/admin/__init__.py similarity index 84% rename from app/views/users/__init__.py rename to app/blueprints/admin/__init__.py index 45af431..66eb1ea 100644 --- a/app/views/users/__init__.py +++ b/app/blueprints/admin/__init__.py @@ -15,4 +15,8 @@ # along with this program. If not, see . -from . import users, githublogin, notifications +from flask import Blueprint + +bp = Blueprint("admin", __name__) + +from . import admin, licenseseditor, tagseditor, versioneditor diff --git a/app/views/admin/admin.py b/app/blueprints/admin/admin.py similarity index 82% rename from app/views/admin/admin.py rename to app/blueprints/admin/admin.py index b359700..2a2bace 100644 --- a/app/views/admin/admin.py +++ b/app/blueprints/admin/admin.py @@ -18,7 +18,7 @@ from flask import * from flask_user import * import flask_menu as menu -from app import app +from . import bp from app.models import * from celery import uuid from app.tasks.importtasks import importRepoScreenshot, importAllDependencies, makeVCSRelease @@ -28,7 +28,7 @@ from wtforms import * from app.utils import loginUser, rank_required, triggerNotif import datetime -@app.route("/admin/", methods=["GET", "POST"]) +@bp.route("/admin/", methods=["GET", "POST"]) @rank_required(UserRank.ADMIN) def admin_page(): if request.method == "POST": @@ -36,13 +36,13 @@ def admin_page(): if action == "delstuckreleases": PackageRelease.query.filter(PackageRelease.task_id != None).delete() db.session.commit() - return redirect(url_for("admin_page")) + return redirect(url_for("admin.admin_page")) elif action == "importmodlist": task = importTopicList.delay() - return redirect(url_for("check_task", id=task.id, r=url_for("todo_topics_page"))) + return redirect(url_for("tasks.check", id=task.id, r=url_for("todo.topics"))) elif action == "checkusers": task = checkAllForumAccounts.delay() - return redirect(url_for("check_task", id=task.id, r=url_for("admin_page"))) + 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) \ @@ -52,7 +52,7 @@ def admin_page(): for package in packages: importRepoScreenshot.delay(package.id) - return redirect(url_for("admin_page")) + return redirect(url_for("admin.admin_page")) elif action == "restore": package = Package.query.get(request.form["package"]) if package is None: @@ -60,10 +60,10 @@ def admin_page(): else: package.soft_deleted = False db.session.commit() - return redirect(url_for("admin_page")) + return redirect(url_for("admin.admin_page")) elif action == "importdepends": task = importAllDependencies.delay() - return redirect(url_for("check_task", id=task.id, r=url_for("admin_page"))) + 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 = {} @@ -72,13 +72,13 @@ def admin_page(): p.provides.append(MetaPackage.GetOrCreate(p.name, mpackage_cache)) db.session.commit() - return redirect(url_for("admin_page")) + 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_page")) + 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: @@ -110,19 +110,19 @@ class SwitchUserForm(FlaskForm): submit = SubmitField("Switch") -@app.route("/admin/switchuser/", methods=["GET", "POST"]) +@bp.route("/admin/switchuser/", methods=["GET", "POST"]) @rank_required(UserRank.ADMIN) -def switch_user_page(): +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("user_profile_page", username=current_user.username)) + 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_page.html", form=form) + return render_template("admin/switch_user.html", form=form) diff --git a/app/views/admin/licenseseditor.py b/app/blueprints/admin/licenseseditor.py similarity index 87% rename from app/views/admin/licenseseditor.py rename to app/blueprints/admin/licenseseditor.py index 343f4ee..c6fca02 100644 --- a/app/views/admin/licenseseditor.py +++ b/app/blueprints/admin/licenseseditor.py @@ -17,16 +17,16 @@ from flask import * from flask_user import * -from app import app +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 -@app.route("/licenses/") +@bp.route("/licenses/") @rank_required(UserRank.MODERATOR) -def license_list_page(): +def license_list(): return render_template("admin/licenses/list.html", licenses=License.query.order_by(db.asc(License.name)).all()) class LicenseForm(FlaskForm): @@ -34,10 +34,10 @@ class LicenseForm(FlaskForm): is_foss = BooleanField("Is FOSS") submit = SubmitField("Save") -@app.route("/licenses/new/", methods=["GET", "POST"]) -@app.route("/licenses//edit/", methods=["GET", "POST"]) +@bp.route("/licenses/new/", methods=["GET", "POST"]) +@bp.route("/licenses//edit/", methods=["GET", "POST"]) @rank_required(UserRank.MODERATOR) -def createedit_license_page(name=None): +def create_edit_license(name=None): license = None if name is not None: license = License.query.filter_by(name=name).first() @@ -57,6 +57,6 @@ def createedit_license_page(name=None): form.populate_obj(license) db.session.commit() - return redirect(url_for("license_list_page")) + return redirect(url_for("admin.license_list")) return render_template("admin/licenses/edit.html", license=license, form=form) diff --git a/app/views/admin/tagseditor.py b/app/blueprints/admin/tagseditor.py similarity index 86% rename from app/views/admin/tagseditor.py rename to app/blueprints/admin/tagseditor.py index 7d88f28..8fb89f4 100644 --- a/app/views/admin/tagseditor.py +++ b/app/blueprints/admin/tagseditor.py @@ -17,16 +17,16 @@ from flask import * from flask_user import * -from app import app +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 -@app.route("/tags/") +@bp.route("/tags/") @rank_required(UserRank.MODERATOR) -def tag_list_page(): +def tag_list(): return render_template("admin/tags/list.html", tags=Tag.query.order_by(db.asc(Tag.title)).all()) class TagForm(FlaskForm): @@ -34,10 +34,10 @@ class TagForm(FlaskForm): name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")]) submit = SubmitField("Save") -@app.route("/tags/new/", methods=["GET", "POST"]) -@app.route("/tags//edit/", methods=["GET", "POST"]) +@bp.route("/tags/new/", methods=["GET", "POST"]) +@bp.route("/tags//edit/", methods=["GET", "POST"]) @rank_required(UserRank.MODERATOR) -def createedit_tag_page(name=None): +def create_edit_tag(name=None): tag = None if name is not None: tag = Tag.query.filter_by(name=name).first() @@ -52,6 +52,6 @@ def createedit_tag_page(name=None): else: form.populate_obj(tag) db.session.commit() - return redirect(url_for("createedit_tag_page", name=tag.name)) + return redirect(url_for("admin.create_edit_tag", name=tag.name)) return render_template("admin/tags/edit.html", tag=tag, form=form) diff --git a/app/views/admin/versioneditor.py b/app/blueprints/admin/versioneditor.py similarity index 86% rename from app/views/admin/versioneditor.py rename to app/blueprints/admin/versioneditor.py index 6bcf93a..98a9a7c 100644 --- a/app/views/admin/versioneditor.py +++ b/app/blueprints/admin/versioneditor.py @@ -17,16 +17,16 @@ from flask import * from flask_user import * -from app import app +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 -@app.route("/versions/") +@bp.route("/versions/") @rank_required(UserRank.MODERATOR) -def version_list_page(): +def version_list(): return render_template("admin/versions/list.html", versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all()) class VersionForm(FlaskForm): @@ -34,10 +34,10 @@ class VersionForm(FlaskForm): protocol = IntegerField("Protocol") submit = SubmitField("Save") -@app.route("/versions/new/", methods=["GET", "POST"]) -@app.route("/versions//edit/", methods=["GET", "POST"]) +@bp.route("/versions/new/", methods=["GET", "POST"]) +@bp.route("/versions//edit/", methods=["GET", "POST"]) @rank_required(UserRank.MODERATOR) -def createedit_version_page(name=None): +def create_edit_version(name=None): version = None if name is not None: version = MinetestRelease.query.filter_by(name=name).first() @@ -55,6 +55,6 @@ def createedit_version_page(name=None): form.populate_obj(version) db.session.commit() - return redirect(url_for("version_list_page")) + return redirect(url_for("admin.version_list")) return render_template("admin/versions/edit.html", version=version, form=form) diff --git a/app/views/api.py b/app/blueprints/api/__init__.py similarity index 79% rename from app/views/api.py rename to app/blueprints/api/__init__.py index ba42aca..5092f21 100644 --- a/app/views/api.py +++ b/app/blueprints/api/__init__.py @@ -17,31 +17,32 @@ 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(): +bp = Blueprint("api", __name__) + +@bp.route("/api/packages/") +def packages(): qb = QueryBuilder(request.args) query = qb.buildPackageQuery() ver = qb.getMinetestVersion() - pkgs = [package.getAsDictionaryShort(app.config["BASE_URL"], version=ver) \ + pkgs = [package.getAsDictionaryShort(current_app.config["BASE_URL"], version=ver) \ for package in query.all()] return jsonify(pkgs) -@app.route("/api/packages///") +@bp.route("/api/packages///") @is_package_page -def api_package_page(package): - return jsonify(package.getAsDictionary(app.config["BASE_URL"])) +def package(package): + return jsonify(package.getAsDictionary(current_app.config["BASE_URL"])) -@app.route("/api/packages///dependencies/") +@bp.route("/api/packages///dependencies/") @is_package_page -def api_package_deps_page(package): +def package_dependencies(package): ret = [] for dep in package.dependencies: @@ -68,14 +69,14 @@ def api_package_deps_page(package): return jsonify(ret) -@app.route("/api/topics/") -def api_topics_page(): +@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()]) -@app.route("/api/topic_discard/", methods=["POST"]) +@bp.route("/api/topic_discard/", methods=["POST"]) @login_required def topic_set_discard(): tid = request.args.get("tid") @@ -93,7 +94,7 @@ def topic_set_discard(): return jsonify(topic.getAsDictionary()) -@app.route("/api/minetest_versions/") -def api_minetest_versions_page(): +@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]) diff --git a/app/blueprints/homepage/__init__.py b/app/blueprints/homepage/__init__.py new file mode 100644 index 0000000..0d50bbd --- /dev/null +++ b/app/blueprints/homepage/__init__.py @@ -0,0 +1,20 @@ +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) diff --git a/app/views/meta.py b/app/blueprints/metapackages/__init__.py similarity index 87% rename from app/views/meta.py rename to app/blueprints/metapackages/__init__.py index 9083289..ff54e6d 100644 --- a/app/views/meta.py +++ b/app/blueprints/metapackages/__init__.py @@ -16,17 +16,19 @@ from flask import * + +bp = Blueprint("metapackages", __name__) + from flask_user import * -from app import app from app.models import * -@app.route("/metapackages/") -def meta_package_list_page(): +@bp.route("/metapackages/") +def list_all(): mpackages = MetaPackage.query.order_by(db.asc(MetaPackage.name)).all() return render_template("meta/list.html", mpackages=mpackages) -@app.route("/metapackages//") -def meta_package_page(name): +@bp.route("/metapackages//") +def view(name): mpackage = MetaPackage.query.filter_by(name=name).first() if mpackage is None: abort(404) diff --git a/app/views/users/notifications.py b/app/blueprints/notifications/__init__.py similarity index 77% rename from app/views/users/notifications.py rename to app/blueprints/notifications/__init__.py index 23dbb31..77263e5 100644 --- a/app/views/users/notifications.py +++ b/app/blueprints/notifications/__init__.py @@ -15,19 +15,20 @@ # along with this program. If not, see . -from flask import * +from flask import Blueprint from flask_user import current_user, login_required -from app import app -from app.models import * +from app.models import db -@app.route("/notifications/") +bp = Blueprint("notifications", __name__) + +@bp.route("/notifications/") @login_required -def notifications_page(): +def list_all(): return render_template("notifications/list.html") -@app.route("/notifications/clear/", methods=["POST"]) +@bp.route("/notifications/clear/", methods=["POST"]) @login_required -def clear_notifications_page(): +def clear(): current_user.notifications.clear() db.session.commit() - return redirect(url_for("notifications_page")) + return redirect(url_for("notifications.list_all")) diff --git a/app/views/packages/__init__.py b/app/blueprints/packages/__init__.py similarity index 91% rename from app/views/packages/__init__.py rename to app/blueprints/packages/__init__.py index 5df5376..e4fc4f2 100644 --- a/app/views/packages/__init__.py +++ b/app/blueprints/packages/__init__.py @@ -14,5 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from flask import Blueprint + +bp = Blueprint("packages", __name__) from . import packages, screenshots, releases diff --git a/app/views/packages/editrequests.py b/app/blueprints/packages/editrequests.py similarity index 99% rename from app/views/packages/editrequests.py rename to app/blueprints/packages/editrequests.py index 7b52184..5ee9cd1 100644 --- a/app/views/packages/editrequests.py +++ b/app/blueprints/packages/editrequests.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from flask import * from flask_user import * from app import app diff --git a/app/views/packages/packages.py b/app/blueprints/packages/packages.py similarity index 88% rename from app/views/packages/packages.py rename to app/blueprints/packages/packages.py index 38aacbe..1cc2f26 100644 --- a/app/views/packages/packages.py +++ b/app/blueprints/packages/packages.py @@ -18,11 +18,14 @@ 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 . 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 * @@ -30,12 +33,12 @@ from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleF 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(): +@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 @@ -56,9 +59,9 @@ def packages_page(): 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) \ + 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_page", type=type_name, q=search, page=query.prev_num) \ + prev_url = url_for("packages.list_all", type=type_name, q=search, page=query.prev_num) \ if query.has_prev else None topics = None @@ -79,9 +82,9 @@ def getReleases(package): return package.releases.filter_by(approved=True).limit(5) -@app.route("/packages///") +@bp.route("/packages///") @is_package_page -def package_page(package): +def view(package): clearNotifications(package.getDetailsURL()) alternatives = None @@ -147,9 +150,9 @@ def package_page(package): threads=threads.all()) -@app.route("/packages///download/") +@bp.route("/packages///download/") @is_package_page -def package_download_page(package): +def download(package): release = package.getDownloadRelease() if release is None: @@ -186,10 +189,10 @@ class PackageForm(FlaskForm): forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)]) submit = SubmitField("Save") -@app.route("/packages/new/", methods=["GET", "POST"]) -@app.route("/packages///edit/", methods=["GET", "POST"]) +@bp.route("/packages/new/", methods=["GET", "POST"]) +@bp.route("/packages///edit/", methods=["GET", "POST"]) @login_required -def create_edit_package_page(author=None, name=None): +def create_edit(author=None, name=None): package = None form = None if author is None: @@ -201,11 +204,11 @@ def create_edit_package_page(author=None, name=None): 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")) + return redirect(url_for("packages.create_edit")) if not author.checkPerm(current_user, Permission.CHANGE_AUTHOR): flash("Permission denied", "error") - return redirect(url_for("create_edit_package_page")) + return redirect(url_for("packages.create_edit")) else: package = getPackageByInfo(author, name) @@ -238,7 +241,7 @@ def create_edit_package_page(author=None, name=None): 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")) + return redirect(url_for("packages.create_edit")) package = Package() package.author = author @@ -247,7 +250,7 @@ def create_edit_package_page(author=None, name=None): 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)) + return redirect(url_for("packages.create_edit", author=author, name=name)) else: triggerNotif(package.author, current_user, @@ -288,7 +291,7 @@ def create_edit_package_page(author=None, name=None): 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) + 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) @@ -305,10 +308,10 @@ def create_edit_package_page(author=None, name=None): packages=package_query.all(), \ mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all()) -@app.route("/packages///approve/", methods=["POST"]) +@bp.route("/packages///approve/", methods=["POST"]) @login_required @is_package_page -def approve_package_page(package): +def approve(package): if not package.checkPerm(current_user, Permission.APPROVE_NEW): flash("You don't have permission to do that.", "error") @@ -329,10 +332,10 @@ def approve_package_page(package): return redirect(package.getDetailsURL()) -@app.route("/packages///remove/", methods=["GET", "POST"]) +@bp.route("/packages///remove/", methods=["GET", "POST"]) @login_required @is_package_page -def remove_package_page(package): +def remove(package): if request.method == "GET": return render_template("packages/remove.html", package=package) @@ -343,7 +346,7 @@ def remove_package_page(package): package.soft_deleted = True - url = url_for("user_profile_page", username=package.author.username) + url = url_for("users.profile", username=package.author.username) triggerNotif(package.author, current_user, "{} deleted".format(package.title), url) db.session.commit() diff --git a/app/views/packages/releases.py b/app/blueprints/packages/releases.py similarity index 93% rename from app/views/packages/releases.py rename to app/blueprints/packages/releases.py index 963f903..89a9a00 100644 --- a/app/views/packages/releases.py +++ b/app/blueprints/packages/releases.py @@ -17,10 +17,11 @@ from flask import * from flask_user import * -from app import app + +from . import bp + from app.models import * from app.tasks.importtasks import makeVCSRelease - from app.utils import * from celery import uuid @@ -62,10 +63,10 @@ class EditPackageReleaseForm(FlaskForm): 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///releases/new/", methods=["GET", "POST"]) +@bp.route("/packages///releases/new/", methods=["GET", "POST"]) @login_required @is_package_page -def create_release_page(package): +def create_release(package): if not package.checkPerm(current_user, Permission.MAKE_RELEASE): return redirect(package.getDetailsURL()) @@ -94,7 +95,7 @@ def create_release_page(package): triggerNotif(package.author, current_user, msg, rel.getEditURL()) db.session.commit() - return redirect(url_for("check_task", id=rel.task_id, r=rel.getEditURL())) + 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: @@ -115,9 +116,9 @@ def create_release_page(package): return render_template("packages/release_new.html", package=package, form=form) -@app.route("/packages///releases//download/") +@bp.route("/packages///releases//download/") @is_package_page -def download_release_page(package, id): +def download_release(package, id): release = PackageRelease.query.get(id) if release is None or release.package != package: abort(404) @@ -137,10 +138,10 @@ def download_release_page(package, id): return redirect(release.url, code=300) -@app.route("/packages///releases//", methods=["GET", "POST"]) +@bp.route("/packages///releases//", methods=["GET", "POST"]) @login_required @is_package_page -def edit_release_page(package, id): +def edit_release(package, id): release = PackageRelease.query.get(id) if release is None or release.package != package: abort(404) @@ -190,10 +191,10 @@ class BulkReleaseForm(FlaskForm): submit = SubmitField("Update") -@app.route("/packages///releases/bulk_change/", methods=["GET", "POST"]) +@bp.route("/packages///releases/bulk_change/", methods=["GET", "POST"]) @login_required @is_package_page -def bulk_change_release_page(package): +def bulk_change_release(package): if not package.checkPerm(current_user, Permission.MAKE_RELEASE): return redirect(package.getDetailsURL()) diff --git a/app/views/packages/screenshots.py b/app/blueprints/packages/screenshots.py similarity index 92% rename from app/views/packages/screenshots.py rename to app/blueprints/packages/screenshots.py index dbb002b..c7fc7eb 100644 --- a/app/views/packages/screenshots.py +++ b/app/blueprints/packages/screenshots.py @@ -17,9 +17,10 @@ from flask import * from flask_user import * -from app import app -from app.models import * +from . import bp + +from app.models import * from app.utils import * from flask_wtf import FlaskForm @@ -39,10 +40,10 @@ class EditScreenshotForm(FlaskForm): delete = BooleanField("Delete") submit = SubmitField("Save") -@app.route("/packages///screenshots/new/", methods=["GET", "POST"]) +@bp.route("/packages///screenshots/new/", methods=["GET", "POST"]) @login_required @is_package_page -def create_screenshot_page(package, id=None): +def create_screenshot(package, id=None): if not package.checkPerm(current_user, Permission.ADD_SCREENSHOTS): return redirect(package.getDetailsURL()) @@ -67,10 +68,10 @@ def create_screenshot_page(package, id=None): return render_template("packages/screenshot_new.html", package=package, form=form) -@app.route("/packages///screenshots//edit/", methods=["GET", "POST"]) +@bp.route("/packages///screenshots//edit/", methods=["GET", "POST"]) @login_required @is_package_page -def edit_screenshot_page(package, id): +def edit_screenshot(package, id): screenshot = PackageScreenshot.query.get(id) if screenshot is None or screenshot.package != package: abort(404) diff --git a/app/views/tasks.py b/app/blueprints/tasks/__init__.py similarity index 88% rename from app/views/tasks.py rename to app/blueprints/tasks/__init__.py index 20eaef5..8d002db 100644 --- a/app/views/tasks.py +++ b/app/blueprints/tasks/__init__.py @@ -18,28 +18,28 @@ from flask import * from flask_user import * import flask_menu as menu -from app import app, csrf +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 celery.result import AsyncResult - from app.utils import * +bp = Blueprint("tasks", __name__) + @csrf.exempt -@app.route("/tasks/getmeta/new/", methods=["POST"]) +@bp.route("/tasks/getmeta/new/", methods=["POST"]) @login_required -def new_getmeta_page(): +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("check_task", id=aresult.id), + "poll_url": url_for("tasks.check", id=aresult.id), }) -@app.route("/tasks//") -def check_task(id): +@bp.route("/tasks//") +def check(id): result = celery.AsyncResult(id) status = result.status traceback = result.traceback diff --git a/app/views/threads.py b/app/blueprints/threads/__init__.py similarity index 85% rename from app/views/threads.py rename to app/blueprints/threads/__init__.py index e430577..0eee201 100644 --- a/app/views/threads.py +++ b/app/blueprints/threads/__init__.py @@ -16,8 +16,10 @@ from flask import * + +bp = Blueprint("threads", __name__) + from flask_user import * -from app import app from app.models import * from app.utils import triggerNotif, clearNotifications @@ -27,17 +29,17 @@ from flask_wtf import FlaskForm from wtforms import * from wtforms.validators import * -@app.route("/threads/") -def threads_page(): +@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()) -@app.route("/threads//subscribe/", methods=["POST"]) +@bp.route("/threads//subscribe/", methods=["POST"]) @login_required -def thread_subscribe_page(id): +def subscribe(id): thread = Thread.query.get(id) if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD): abort(404) @@ -49,12 +51,12 @@ def thread_subscribe_page(id): thread.watchers.append(current_user) db.session.commit() - return redirect(url_for("thread_page", id=id)) + return redirect(url_for("threads.view", id=id)) -@app.route("/threads//unsubscribe/", methods=["POST"]) +@bp.route("/threads//unsubscribe/", methods=["POST"]) @login_required -def thread_unsubscribe_page(id): +def unsubscribe(id): thread = Thread.query.get(id) if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD): abort(404) @@ -66,12 +68,12 @@ def thread_unsubscribe_page(id): else: flash("Not subscribed to thread", "success") - return redirect(url_for("thread_page", id=id)) + return redirect(url_for("threads.view", id=id)) -@app.route("/threads//", methods=["GET", "POST"]) -def thread_page(id): - clearNotifications(url_for("thread_page", id=id)) +@bp.route("/threads//", 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): @@ -106,11 +108,11 @@ def thread_page(id): for user in thread.watchers: if user != current_user: - triggerNotif(user, current_user, msg, url_for("thread_page", id=thread.id)) + triggerNotif(user, current_user, msg, url_for("threads.view", id=thread.id)) db.session.commit() - return redirect(url_for("thread_page", id=id)) + return redirect(url_for("threads.view", id=id)) else: flash("Comment needs to be between 3 and 500 characters.") @@ -124,9 +126,9 @@ class ThreadForm(FlaskForm): private = BooleanField("Private") submit = SubmitField("Open Thread") -@app.route("/threads/new/", methods=["GET", "POST"]) +@bp.route("/threads/new/", methods=["GET", "POST"]) @login_required -def new_thread_page(): +def new(): form = ThreadForm(formdata=request.form) package = None @@ -153,7 +155,7 @@ def new_thread_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)) + return redirect(url_for("threads.view", id=package.review_thread.id)) elif not current_user.canOpenThreadRL(): flash("Please wait before opening another thread", "danger") @@ -197,16 +199,16 @@ def new_thread_page(): 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)) + 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("thread_page", id=thread.id)) + triggerNotif(user, current_user, notif_msg, url_for("threads.view", id=thread.id)) db.session.commit() - return redirect(url_for("thread_page", id=thread.id)) + return redirect(url_for("threads.view", id=thread.id)) return render_template("threads/new.html", form=form, allow_private_change=allow_change, package=package) diff --git a/app/views/thumbnails.py b/app/blueprints/thumbnails/__init__.py similarity index 96% rename from app/views/thumbnails.py rename to app/blueprints/thumbnails/__init__.py index 8303067..1f46102 100644 --- a/app/views/thumbnails.py +++ b/app/blueprints/thumbnails/__init__.py @@ -16,7 +16,8 @@ from flask import * -from app import app + +bp = Blueprint("thumbnails", __name__) import os from PIL import Image @@ -57,7 +58,7 @@ def resize_and_crop(img_path, modified_path, size): img.save(modified_path) -@app.route("/thumbnails//") +@bp.route("/thumbnails//") def make_thumbnail(img, level): if level > len(ALLOWED_RESOLUTIONS) or level <= 0: abort(403) diff --git a/app/views/admin/todo.py b/app/blueprints/todo/__init__.py similarity index 90% rename from app/views/admin/todo.py rename to app/blueprints/todo/__init__.py index 9909eff..f4f818a 100644 --- a/app/views/admin/todo.py +++ b/app/blueprints/todo/__init__.py @@ -14,17 +14,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from flask import * from flask_user import * import flask_menu as menu -from app import app from app.models import * from app.querybuilder import QueryBuilder -@app.route("/todo/", methods=["GET", "POST"]) +bp = Blueprint("todo", __name__) + +@bp.route("/todo/", methods=["GET", "POST"]) @login_required -def todo_page(): +def view(): canApproveNew = Permission.APPROVE_NEW.check(current_user) canApproveRel = Permission.APPROVE_RELEASE.check(current_user) canApproveScn = Permission.APPROVE_SCREENSHOT.check(current_user) @@ -51,7 +51,7 @@ def todo_page(): PackageScreenshot.query.update({ "approved": True }) db.session.commit() - return redirect(url_for("todo_page")) + return redirect(url_for("todo.view")) else: abort(400) @@ -69,9 +69,9 @@ def todo_page(): topics_to_add=topics_to_add, total_topics=total_topics) -@app.route("/todo/topics/") +@bp.route("/todo/topics/") @login_required -def todo_topics_page(): +def topics(): qb = QueryBuilder(request.args) qb.setSortIfNone("date") query = qb.buildTopicQuery() @@ -88,10 +88,10 @@ def todo_topics_page(): num = 100 query = query.paginate(page, num, True) - next_url = url_for("todo_topics_page", page=query.next_num, query=qb.search, \ + 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", page=query.prev_num, query=qb.search, \ + 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 diff --git a/app/blueprints/users/__init__.py b/app/blueprints/users/__init__.py new file mode 100644 index 0000000..98cf34a --- /dev/null +++ b/app/blueprints/users/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint("users", __name__) + +from . import githublogin, profile diff --git a/app/views/users/githublogin.py b/app/blueprints/users/githublogin.py similarity index 90% rename from app/views/users/githublogin.py rename to app/blueprints/users/githublogin.py index 9ea2584..458c637 100644 --- a/app/views/users/githublogin.py +++ b/app/blueprints/users/githublogin.py @@ -21,15 +21,16 @@ 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 . import bp +from app import github from app.models import * from app.utils import loginUser -@app.route("/user/github/start/") -def github_signin_page(): +@bp.route("/user/github/start/") +def github_signin(): return github.authorize("") -@app.route("/user/github/callback/") +@bp.route("/user/github/callback/") @github.authorized_handler def github_authorized(oauth_token): next_url = request.args.get("next") @@ -62,10 +63,10 @@ def github_authorized(oauth_token): else: if userByGithub is None: flash("Unable to find an account for that Github user", "error") - return redirect(url_for("user_claim_page")) + return redirect(url_for("users.claim")) elif loginUser(userByGithub): if current_user.password is None: - return redirect(next_url or url_for("set_password_page", optional=True)) + return redirect(next_url or url_for("users.set_password", optional=True)) else: return redirect(next_url or url_for("home_page")) else: diff --git a/app/views/users/users.py b/app/blueprints/users/profile.py similarity index 82% rename from app/views/users/users.py rename to app/blueprints/users/profile.py index 1a81c7d..fd8d7d9 100644 --- a/app/views/users/users.py +++ b/app/blueprints/users/profile.py @@ -18,7 +18,8 @@ from flask import * from flask_user import * from flask_login import login_user, logout_user -from app import app, markdown +from app import markdown +from . import bp from app.models import * from flask_wtf import FlaskForm from wtforms import * @@ -38,14 +39,14 @@ class UserProfileForm(FlaskForm): submit = SubmitField("Save") -@app.route("/users/", methods=["GET"]) -def user_list_page(): +@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) -@app.route("/users//", methods=["GET", "POST"]) -def user_profile_page(username): +@bp.route("/users//", methods=["GET", "POST"]) +def profile(username): user = User.query.filter_by(username=username).first() if not user: abort(404) @@ -85,13 +86,13 @@ def user_profile_page(username): 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))) + 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("user_profile_page", username=username)) + 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()): @@ -107,11 +108,11 @@ def user_profile_page(username): .all() # Process GET or invalid POST - return render_template("users/user_profile_page.html", + return render_template("users/users.profile.html", user=user, form=form, packages=packages, topics_to_add=topics_to_add) -@app.route("/users//check/", methods=["POST"]) +@bp.route("/users//check/", methods=["POST"]) @login_required def user_check(username): user = User.query.filter_by(username=username).first() @@ -125,9 +126,9 @@ def user_check(username): abort(404) task = checkForumAccount.delay(user.forums_username) - next_url = url_for("user_profile_page", username=username) + next_url = url_for("users.profile", username=username) - return redirect(url_for("check_task", id=task.id, r=next_url)) + return redirect(url_for("tasks.check", id=task.id, r=next_url)) class SendEmailForm(FlaskForm): @@ -136,14 +137,14 @@ class SendEmailForm(FlaskForm): submit = SubmitField("Send") -@app.route("/users//email/", methods=["GET", "POST"]) +@bp.route("/users//email/", methods=["GET", "POST"]) @rank_required(UserRank.MODERATOR) -def send_email_page(username): +def send_email(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) + next_url = url_for("users.profile", username=user.username) if user.email is None: flash("User has no email address!", "error") @@ -154,7 +155,7 @@ def send_email_page(username): 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 redirect(url_for("tasks.check", id=task.id, r=next_url)) return render_template("users/send_email.html", form=form) @@ -166,9 +167,9 @@ class SetPasswordForm(FlaskForm): password2 = PasswordField("Verify password", [InputRequired(), Length(2, 100)]) submit = SubmitField("Save") -@app.route("/user/set-password/", methods=["GET", "POST"]) +@bp.route("/user/set-password/", methods=["GET", "POST"]) @login_required -def set_password_page(): +def set_password(): if current_user.password is not None: return redirect(url_for("user.change_password")) @@ -208,17 +209,17 @@ def set_password_page(): 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))) + return redirect(url_for("tasks.check", id=task.id, r=url_for("users.profile", username=current_user.username))) else: - return redirect(url_for("user_profile_page", username=current_user.username)) + 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")) -@app.route("/user/claim/", methods=["GET", "POST"]) -def user_claim_page(): +@bp.route("/user/claim/", methods=["GET", "POST"]) +def claim(): username = request.args.get("username") if username is None: username = "" @@ -227,16 +228,16 @@ def user_claim_page(): 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")) + 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("user_claim_page")) + return redirect(url_for("users.claim")) elif user is None: flash("Unable to find that user", "error") - return redirect(url_for("user_claim_page")) + return redirect(url_for("users.claim")) if user is not None and method == "github": - return redirect(url_for("github_signin_page")) + return redirect(url_for("users.github_signin")) token = None if "forum_token" in session: @@ -253,12 +254,12 @@ def user_claim_page(): 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"))) + 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("user_claim_page")) + return redirect(url_for("users.claim")) # Get signature sig = None @@ -267,7 +268,7 @@ def user_claim_page(): 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)) + return redirect(url_for("users.claim", username=username)) # Look for key if token in sig: @@ -278,21 +279,21 @@ def user_claim_page(): db.session.commit() if loginUser(user): - return redirect(url_for("set_password_page")) + return redirect(url_for("users.set_password")) else: flash("Unable to login as user", "error") - return redirect(url_for("user_claim_page", username=username)) + return redirect(url_for("users.claim", username=username)) else: flash("Could not find the key in your signature!", "error") - return redirect(url_for("user_claim_page", username=username)) + return redirect(url_for("users.claim", 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(): +@bp.route("/users/verify/") +def verify_email(): token = request.args.get("token") ver = UserEmailVerification.query.filter_by(token=token).first() if ver is None: @@ -303,6 +304,6 @@ def verify_email_page(): db.session.commit() if current_user.is_authenticated: - return redirect(url_for("user_profile_page", username=current_user.username)) + return redirect(url_for("users.profile", username=current_user.username)) else: return redirect(url_for("home_page")) diff --git a/app/models.py b/app/models.py index 9148f05..3632ebc 100644 --- a/app/models.py +++ b/app/models.py @@ -501,27 +501,27 @@ class Package(db.Model): 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): @@ -529,11 +529,11 @@ class Package(db.Model): 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): @@ -716,13 +716,13 @@ class PackageRelease(db.Model): 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) @@ -758,7 +758,7 @@ class PackageScreenshot(db.Model): 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) @@ -880,11 +880,11 @@ class Thread(db.Model): 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): diff --git a/app/views/sass.py b/app/sass.py similarity index 90% rename from app/views/sass.py rename to app/sass.py index 825f494..f4a272f 100644 --- a/app/views/sass.py +++ b/app/sass.py @@ -15,8 +15,6 @@ 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) @@ -31,7 +29,7 @@ def _convert(dir, src, dst): outfile.write(output) outfile.close() -def _getDirPath(originalPath, create=False): +def _getDirPath(app, originalPath, create=False): path = originalPath if not os.path.isdir(path): @@ -47,8 +45,8 @@ def _getDirPath(originalPath, create=False): 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) + inputDir = _getDirPath(app, inputDir) + cacheDir = _getDirPath(app, cacheDir or outputPath, True) def _sass(filepath): sassfile = "%s/%s.scss" % (inputDir, filepath) @@ -63,5 +61,3 @@ def sass(app, inputDir='scss', outputPath='static', force=False, cacheDir="publi return send_from_directory(cacheDir, filepath + ".css") app.add_url_rule("/%s/.css" % (outputPath), 'sass', _sass) - -sass(app) diff --git a/app/tasks/emails.py b/app/tasks/emails.py index 5eb915e..f81deaa 100644 --- a/app/tasks/emails.py +++ b/app/tasks/emails.py @@ -34,7 +34,7 @@ def sendVerifyEmail(newEmail, token): 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) diff --git a/app/tasks/importtasks.py b/app/tasks/importtasks.py index e53dbfa..ed43584 100644 --- a/app/tasks/importtasks.py +++ b/app/tasks/importtasks.py @@ -15,7 +15,7 @@ # along with this program. If not, see . -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 diff --git a/app/template_filters.py b/app/template_filters.py new file mode 100644 index 0000000..e535ce8 --- /dev/null +++ b/app/template_filters.py @@ -0,0 +1,22 @@ +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" diff --git a/app/templates/admin/licenses/edit.html b/app/templates/admin/licenses/edit.html index c68b17f..eabe782 100644 --- a/app/templates/admin/licenses/edit.html +++ b/app/templates/admin/licenses/edit.html @@ -10,8 +10,8 @@ {% block content %}

- Back to list | - New License + Back to list | + New License

{% from "macros/forms.html" import render_field, render_submit_field %} diff --git a/app/templates/admin/licenses/list.html b/app/templates/admin/licenses/list.html index ff30805..869aac6 100644 --- a/app/templates/admin/licenses/list.html +++ b/app/templates/admin/licenses/list.html @@ -6,11 +6,11 @@ Licenses {% block content %}

- New License + New License

    {% for l in licenses %} -
  • {{ l.name }} [{{ l.is_foss and "Free" or "Non-free"}}]
  • +
  • {{ l.name }} [{{ l.is_foss and "Free" or "Non-free"}}]
  • {% endfor %}
{% endblock %} diff --git a/app/templates/admin/list.html b/app/templates/admin/list.html index ddfa30c..1048a88 100644 --- a/app/templates/admin/list.html +++ b/app/templates/admin/list.html @@ -6,11 +6,11 @@ {% block content %}
diff --git a/app/templates/admin/switch_user_page.html b/app/templates/admin/switch_user.html similarity index 100% rename from app/templates/admin/switch_user_page.html rename to app/templates/admin/switch_user.html diff --git a/app/templates/admin/tags/edit.html b/app/templates/admin/tags/edit.html index ccffa7f..5ffe2d0 100644 --- a/app/templates/admin/tags/edit.html +++ b/app/templates/admin/tags/edit.html @@ -10,8 +10,8 @@ {% block content %}

- Back to list | - New Tag + Back to list | + New Tag

{% from "macros/forms.html" import render_field, render_submit_field %} diff --git a/app/templates/admin/tags/list.html b/app/templates/admin/tags/list.html index 355f62d..daae8e7 100644 --- a/app/templates/admin/tags/list.html +++ b/app/templates/admin/tags/list.html @@ -6,11 +6,11 @@ Tags {% block content %}

- New Tag + New Tag

    {% for t in tags %} -
  • {{ t.title }} [{{ t.packages | count }} packages]
  • +
  • {{ t.title }} [{{ t.packages | count }} packages]
  • {% endfor %}
{% endblock %} diff --git a/app/templates/admin/versions/edit.html b/app/templates/admin/versions/edit.html index ea84c11..f1042fa 100644 --- a/app/templates/admin/versions/edit.html +++ b/app/templates/admin/versions/edit.html @@ -10,8 +10,8 @@ {% block content %}

- Back to list | - New Version + Back to list | + New Version

{% from "macros/forms.html" import render_field, render_submit_field %} diff --git a/app/templates/admin/versions/list.html b/app/templates/admin/versions/list.html index 5a95efd..f3dd236 100644 --- a/app/templates/admin/versions/list.html +++ b/app/templates/admin/versions/list.html @@ -6,11 +6,11 @@ Minetest Versions {% block content %}

- New Version + New Version

{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index e39097c..faa867a 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -60,10 +60,10 @@ @@ -134,7 +134,7 @@ {{ _("Help") }} | {{ _("Policy and Guidance") }} | {{ _("Report / DMCA") }} | - {{ _("User List") }} + {{ _("User List") }} {% if debug %}

diff --git a/app/templates/emails/verify.html b/app/templates/emails/verify.html index 38d488b..04a4bc5 100644 --- a/app/templates/emails/verify.html +++ b/app/templates/emails/verify.html @@ -16,12 +16,12 @@ If this was you, then please click this link to verify the address:

- + Confirm Email Address

- 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) }}

{% endblock %} diff --git a/app/templates/flask_user/login.html b/app/templates/flask_user/login.html index 642dc70..6214c8c 100644 --- a/app/templates/flask_user/login.html +++ b/app/templates/flask_user/login.html @@ -60,7 +60,7 @@ Sign in {% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %}

{%trans%}Sign in with Github{%endtrans%}

@@ -72,7 +72,7 @@ Sign in

Create an account using your forum account or email.

- {%trans%}Claim your account{%endtrans%} + {%trans%}Claim your account{%endtrans%}
diff --git a/app/templates/index.html b/app/templates/index.html index 0cb39dd..a7574d6 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -37,28 +37,28 @@ {% from "macros/packagegridtile.html" import render_pkggrid %} - + {{ _("See more") }}

{{ _("Recently Added") }}

{{ render_pkggrid(new) }} - + {{ _("See more") }}

{{ _("Top Mods") }}

{{ render_pkggrid(pop_mod) }} - + {{ _("See more") }}

{{ _("Top Games") }}

{{ render_pkggrid(pop_gam) }} - + {{ _("See more") }}

{{ _("Top Texture Packs") }}

diff --git a/app/templates/macros/threads.html b/app/templates/macros/threads.html index fd7b648..16b67a0 100644 --- a/app/templates/macros/threads.html +++ b/app/templates/macros/threads.html @@ -4,7 +4,7 @@ {% for r in thread.replies %}
  • @@ -12,11 +12,11 @@ {% if current_user.canCommentRL() %} -
    +
    @@ -65,14 +65,14 @@ {% for t in threads %}
  • {% if list_group %} - + {% if t.private %}🔒 {% endif %} {{ t.title }} by {{ t.author.display_name }} {% else %} {% if t.private %}🔒 {% endif %} - {{ t.title }} + {{ t.title }} by {{ t.author.display_name }} {% endif %}
  • diff --git a/app/templates/macros/topics.html b/app/templates/macros/topics.html index a6d0a42..987f810 100644 --- a/app/templates/macros/topics.html +++ b/app/templates/macros/topics.html @@ -18,14 +18,14 @@ {% if topic.wip %}[WIP]{% endif %} {% if show_author %} - {{ topic.author.display_name}} + {{ topic.author.display_name}} {% endif %} {{ topic.name or ""}} {{ topic.created_at | date }} {% if current_user == topic.author or topic.author.checkPerm(current_user, "CHANGE_AUTHOR") %} + href="{{ url_for('packages.create_edit', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}"> Create {% endif %} @@ -56,10 +56,10 @@ {% if topic.wip %}[WIP]{% endif %} {% if topic.name %}[{{ topic.name }}]{% endif %} {% if show_author %} - by {{ topic.author.display_name }} + by {{ topic.author.display_name }} {% endif %} {% if topic.author == current_user or topic.author.checkPerm(current_user, "CHANGE_AUTHOR") %} - | Create + | Create {% endif %}
  • {% endfor %} diff --git a/app/templates/meta/list.html b/app/templates/meta/list.html index e6daf99..525bafd 100644 --- a/app/templates/meta/list.html +++ b/app/templates/meta/list.html @@ -7,7 +7,7 @@ Meta Packages {% block content %}
      {% for meta in mpackages %} -
    • {{ meta.name }} ({{ meta.packages.filter_by(soft_deleted=False, approved=True).all() | count }} packages)
    • +
    • {{ meta.name }} ({{ meta.packages.filter_by(soft_deleted=False, approved=True).all() | count }} packages)
    • {% else %}
    • No meta packages found.
    • {% endfor %} diff --git a/app/templates/notifications/list.html b/app/templates/notifications/list.html index d6de54e..7f09e5d 100644 --- a/app/templates/notifications/list.html +++ b/app/templates/notifications/list.html @@ -6,7 +6,7 @@ Notifications {% block content %} {% if current_user.notifications %} - + diff --git a/app/templates/packages/list.html b/app/templates/packages/list.html index cc0c6c9..4aea5a0 100644 --- a/app/templates/packages/list.html +++ b/app/templates/packages/list.html @@ -15,7 +15,7 @@ {% for n in range(1, page_max+1) %}
    • + href="{{ url_for('packages.list_all', type=type, q=query, page=n) }}"> {{ n }}
    • diff --git a/app/templates/packages/release_edit.html b/app/templates/packages/release_edit.html index 03f0d7a..37fc655 100644 --- a/app/templates/packages/release_edit.html +++ b/app/templates/packages/release_edit.html @@ -26,7 +26,7 @@ {% endif %} {% if release.task_id %} - Importing... view task
      + Importing... view task
      {% if package.checkPerm(current_user, "CHANGE_RELEASE_URL") %} {{ render_field(form.task_id) }} {% endif %} diff --git a/app/templates/packages/view.html b/app/templates/packages/view.html index 8216c71..5da8797 100644 --- a/app/templates/packages/view.html +++ b/app/templates/packages/view.html @@ -103,7 +103,7 @@ {% if not review_thread and (package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW")) %}
      - Open Thread + Open Thread Privately ask a question or give feedback
      @@ -172,14 +172,14 @@ Provides {% for meta in package.provides %} {{ meta.name }} + href="{{ url_for('metapackages.view', name=meta.name) }}">{{ meta.name }} {% endfor %} {% endif %} Author - + {{ package.author.display_name }} @@ -241,7 +241,7 @@ {{ dep.package.title }} by {{ dep.package.author.display_name }} {% elif dep.meta_package %} + href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}"> {{ dep.meta_package.name }} {% else %} {{ "Excepted package or meta_package in dep!" | throw }} @@ -301,7 +301,7 @@ created {{ rel.releaseDate | date }}. {% if (package.checkPerm(current_user, "MAKE_RELEASE") or package.checkPerm(current_user, "APPROVE_RELEASE")) and rel.task_id %} - Importing... + Importing... {% elif not rel.approved %} Waiting for approval. {% endif %} @@ -320,7 +320,7 @@
      {% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %} + + href="{{ url_for('threads.new', pid=package.id) }}">+ {% endif %} Threads
      @@ -332,7 +332,7 @@ {% 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) %} + href="{{ url_for('threads.new', pid=package.id) }}"> Report a problem with this listing {% endif %} @@ -381,7 +381,7 @@
    • {{ r.title }} by - {{ r.author.display_name }} + {{ r.author.display_name }}
    • {% else %}
    • No edit requests have been made.
    • diff --git a/app/templates/tasks/view.html b/app/templates/tasks/view.html index 97c3343..a348b1f 100644 --- a/app/templates/tasks/view.html +++ b/app/templates/tasks/view.html @@ -16,7 +16,7 @@ Working diff --git a/app/templates/todo/list.html b/app/templates/todo/list.html index 2e756af..2f09cb9 100644 --- a/app/templates/todo/list.html +++ b/app/templates/todo/list.html @@ -63,7 +63,7 @@ {% if canApproveScn and screenshots %}

      Screenshots -
      + @@ -112,6 +112,6 @@ style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100">

      - View Unadded Topic List + View Unadded Topic List {% endblock %} diff --git a/app/templates/todo/topics.html b/app/templates/todo/topics.html index f9774d1..8afa3b0 100644 --- a/app/templates/todo/topics.html +++ b/app/templates/todo/topics.html @@ -8,15 +8,15 @@ Topics to be Added - + @@ -79,7 +79,7 @@ Topics to be Added {% for i in range(1, page_max+1) %}
    • + href="{{ url_for('todo.topics', page=i, query=query, show_discarded=show_discarded, n=n, sort=sort_by) }}"> {{ i }}
    • diff --git a/app/templates/users/claim.html b/app/templates/users/claim.html index db00d3f..ab66349 100644 --- a/app/templates/users/claim.html +++ b/app/templates/users/claim.html @@ -19,7 +19,7 @@ Creating an Account Please log out to continue.

      - Logout + Logout

      {% else %}

      @@ -44,7 +44,7 @@ Creating an Account Use GitHub field in forum profile - + @@ -73,7 +73,7 @@ Creating an Account Verification token - + diff --git a/app/templates/users/list.html b/app/templates/users/list.html index 5ec5662..345a039 100644 --- a/app/templates/users/list.html +++ b/app/templates/users/list.html @@ -8,7 +8,7 @@

        {% for user in users %}
      • - + {{ user.display_name }} - {{ user.rank.getTitle() }} diff --git a/app/templates/users/user_profile_page.html b/app/templates/users/user_profile_page.html index fc197e8..d1edf54 100644 --- a/app/templates/users/user_profile_page.html +++ b/app/templates/users/user_profile_page.html @@ -9,7 +9,7 @@ {% if not current_user.is_authenticated and user.rank == user.rank.NOT_JOINED and user.forums_username %}
        Claim + href="{{ url_for('users.claim', username=user.forums_username) }}">Claim Is this you? Claim your account now!
        @@ -57,7 +57,7 @@ {% if user.github_username %} GitHub {% elif user == current_user %} - Link Github + Link Github {% endif %} {% if user.website_url %} @@ -78,7 +78,7 @@ Admin {% if user.email %} - + Email {% else %} @@ -97,7 +97,7 @@ Profile Picture: {% if user.forums_username %} - + @@ -122,7 +122,7 @@ {% if user.password %} Set | Change {% else %} - Not set | Set + Not set | Set {% endif %} diff --git a/app/views/__init__.py b/app/views/__init__.py deleted file mode 100644 index 3abb7ee..0000000 --- a/app/views/__init__.py +++ /dev/null @@ -1,84 +0,0 @@ -# Content DB -# Copyright (C) 2018 rubenwardy -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -from 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/") -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('//') -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() diff --git a/app/views/admin/__init__.py b/app/views/admin/__init__.py deleted file mode 100644 index 2e467da..0000000 --- a/app/views/admin/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# Content DB -# Copyright (C) 2018 rubenwardy -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -from . import admin, licenseseditor, tagseditor, versioneditor, todo