Compare commits

..

33 Commits

Author SHA1 Message Date
rubenwardy
39d4cf362b Fix url_for crash on "home_page" 2019-11-21 19:38:32 +00:00
rubenwardy
b977a42738 Add celery beat and celery flower to docker compose 2019-11-18 22:44:37 +00:00
rubenwardy
ff2a74367f Fix download forgery 2019-11-18 21:42:56 +00:00
rubenwardy
3f666d2302 Fix exception on badly-formed query string 2019-11-17 21:40:55 +00:00
rubenwardy
a7d22973ff Fix user profile after blueprints commit 2019-11-16 00:05:59 +00:00
rubenwardy
20583784f5 Fix hardcoded progress bar in work queue, and related crash 2019-11-16 00:05:35 +00:00
rubenwardy
64f131ae27 Refactor endpoints to use blueprints instead 2019-11-15 23:51:42 +00:00
rubenwardy
015abe5a25 Indicate stuck releases in todo list and allow admins to delete them 2019-11-14 23:39:41 +00:00
rubenwardy
719a652235 Enable hot code reloading 2019-11-14 23:38:29 +00:00
rubenwardy
50892ce9fc Add debug warning to template 2019-11-14 23:38:11 +00:00
rubenwardy
2e14836ed6 Fix permission issues by not mounting source code 2019-11-14 23:02:36 +00:00
rubenwardy
35e1aba4ad Fix CDB running as root in docker container 2019-11-14 22:53:46 +00:00
rubenwardy
913537f96f Sort packages in approval queue by creation date 2019-11-14 22:43:05 +00:00
rubenwardy
b36a60d3a2 Fix worker start command in docker-compose.yml 2019-11-14 22:42:49 +00:00
rubenwardy
df247b021e Improve docker image and deployment scripts 2019-11-14 22:24:37 +00:00
rubenwardy
9f678d8fde Add issue templates 2019-11-12 22:49:47 +00:00
rubenwardy
d89442438f Add security policy 2019-11-12 22:46:42 +00:00
rubenwardy
08a9ae7b94 Make review threads public by default 2019-11-12 22:39:17 +00:00
rubenwardy
904e09f0dd Create utils folder 2019-11-12 22:36:30 +00:00
Alex
038ef5b739 Specify excessive horror 2019-10-22 21:18:56 +01:00
TumeniNodes
f8958ae1bc Fix error in thread privacy message 2019-10-22 21:17:08 +01:00
rubenwardy
03eccbd56a Fix text in emails 2019-09-15 18:43:22 +01:00
rubenwardy
fb31ea3c22 Fix git clone breaking when branch is None 2019-09-15 18:30:42 +01:00
rubenwardy
4082863b5a Add basic dependency resolution 2019-09-03 00:42:51 +01:00
rubenwardy
cc564af44e Fix broken reference based git import
Fixes #130
2019-08-31 22:09:19 +01:00
rubenwardy
655ed2255a Fix crash by truncating notification titles 2019-08-31 18:38:15 +01:00
rubenwardy
96b22744ec Fix crash on null release task_id 2019-08-31 18:32:59 +01:00
rubenwardy
130d0bc7a0 Fix wtfforms setting fields to empty string instead of None 2019-08-30 21:11:38 +01:00
rubenwardy
1469e37c38 Fix accidental limit on password length 2019-08-12 14:10:28 +01:00
rubenwardy
6ce495fcd3 Fix crash on reading mod.conf from Github 2019-08-09 11:27:54 +01:00
rubenwardy
776a3eff2a Fail gracefully when given a bad git reference 2019-08-09 11:25:19 +01:00
rubenwardy
04e8ae5bdd Fix unexpected crash on bad Github URL 2019-08-09 11:17:39 +01:00
rubenwardy
18b9fb3876 Fix typo in zip uploading 2019-08-09 11:10:45 +01:00
76 changed files with 713 additions and 485 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
.git
data
uploads

13
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,13 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: Unconfirmed Bug
assignees: ''
---
## Summary
Describe your problem here
##### Steps to reproduce
For bug reports or build issues, explain how the problem happened

View File

@@ -0,0 +1,25 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: Feature
assignees: ''
---
## Problem
A clear and concise description of what the problem is.
ie: Why is this needed?
Ex. I'm always frustrated when [...]
## Solutions
A clear and concise description of what you want to happen.
## Alternatives
A clear and concise description of any alternative solutions or features you've considered.
## Additional context
Add any other context or screenshots about the feature request here.

7
.github/ISSUE_TEMPLATE/policy.md vendored Normal file
View File

@@ -0,0 +1,7 @@
---
name: Policy suggestion
about: Suggest a change to the guidelines
title: ''
labels: Policy
assignees: ''
---

19
.github/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,19 @@
# Security Policy
## Supported Versions
We only support the latest production version, deployed to <https://content.minetest.net>.
See the [releases page](https://github.com/minetest/contentdb/releases).
## Reporting a Vulnerability
We ask that you report vulnerabilities privately, by contacting rubenwardy,
to give us time to fix them. You can do that by using one of the methods outlined in the following link:
* https://rubenwardy.com/contact/
Depending on severity, we will either create a private issue for the vulnerability
and release a security update, or give you permission to file the issue publicly.
For more information on the justification of this policy, see
[Responsible Disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure).

4
.gitignore vendored
View File

@@ -6,8 +6,8 @@ custom.css
tmp
log.txt
*.rdb
uploads
thumbnails
app/public/uploads
app/public/thumbnails
celerybeat-schedule
/data

View File

@@ -1,17 +1,19 @@
FROM python:3.6
RUN groupadd -g 5123 cdb && \
useradd -r -u 5123 -g cdb cdb
WORKDIR /home/cdb
COPY requirements.txt requirements.txt
RUN pip install -r ./requirements.txt
RUN pip install gunicorn
RUN pip install psycopg2
COPY runprodguni.sh ./
COPY rundebug.sh ./
RUN chmod +x runprodguni.sh
COPY setup.py ./setup.py
COPY app app
COPY migrations migrations
COPY utils utils
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

View File

@@ -13,7 +13,7 @@ Note: you should first read one of the guides on the [Github repo wiki](https://
FLASK_CONFIG=../config.cfg celery -A app.tasks.celery worker
# if sqlite
python setup.py -t
python utils/setup.py -t
rm db.sqlite && python setup.py -t && FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db stamp head
# Create migration

View File

@@ -25,13 +25,15 @@ from flask_github import GitHub
from flask_wtf.csrf import CsrfProtect
from flask_flatpages import FlatPages
from flask_babel import Babel
import os
import os, redis
app = Flask(__name__, static_folder="public/static")
app.config["FLATPAGES_ROOT"] = "flatpages"
app.config["FLATPAGES_EXTENSION"] = ".md"
app.config.from_pyfile(os.environ["FLASK_CONFIG"])
r = redis.Redis.from_url(app.config["REDIS_URL"])
menu.Menu(app=app)
markdown = Markdown(app, extensions=["fenced_code"], safe_mode=True, output_format="html5")
github = GitHub(app)
@@ -48,15 +50,44 @@ gravatar = Gravatar(app,
use_ssl=True,
base_url=None)
if not app.debug:
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 . import models, tasks
from .views import *
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)
@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()

View File

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

View File

@@ -15,4 +15,8 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from . import users, githublogin, notifications
from flask import Blueprint
bp = Blueprint("admin", __name__)
from . import admin, licenseseditor, tagseditor, versioneditor

View File

@@ -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,17 +28,21 @@ 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":
action = request.form["action"]
if action == "importmodlist":
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("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) \
@@ -48,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:
@@ -56,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 = {}
@@ -68,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:
@@ -106,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)

View File

@@ -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/<name>/edit/", methods=["GET", "POST"])
@bp.route("/licenses/new/", methods=["GET", "POST"])
@bp.route("/licenses/<name>/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)

View File

@@ -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/<name>/edit/", methods=["GET", "POST"])
@bp.route("/tags/new/", methods=["GET", "POST"])
@bp.route("/tags/<name>/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)

View File

@@ -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/<name>/edit/", methods=["GET", "POST"])
@bp.route("/versions/new/", methods=["GET", "POST"])
@bp.route("/versions/<name>/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)

View File

@@ -17,35 +17,66 @@
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/<author>/<name>/")
@bp.route("/api/packages/<author>/<name>/")
@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/topics/")
def api_topics_page():
@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()])
@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")
@@ -63,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])

View File

@@ -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():
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)

View File

@@ -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/<name>/")
def meta_package_page(name):
@bp.route("/metapackages/<name>/")
def view(name):
mpackage = MetaPackage.query.filter_by(name=name).first()
if mpackage is None:
abort(404)

View File

@@ -15,19 +15,20 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
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"))

View File

@@ -14,5 +14,8 @@
# 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

View File

@@ -14,7 +14,6 @@
# 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

View File

@@ -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
@@ -49,16 +52,16 @@ def packages_page():
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))
page = get_int_or_abort(request.args.get("page"), 1)
num = min(40, get_int_or_abort(request.args.get("n"), 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) \
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/<author>/<name>/")
@bp.route("/packages/<author>/<name>/")
@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/<author>/<name>/download/")
@bp.route("/packages/<author>/<name>/download/")
@is_package_page
def package_download_page(package):
def download(package):
release = package.getDownloadRelease()
if release is None:
@@ -180,16 +183,16 @@ class PackageForm(FlaskForm):
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()])
website = StringField("Website URL", [Optional(), URL()])
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()])
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"])
@bp.route("/packages/new/", methods=["GET", "POST"])
@bp.route("/packages/<author>/<name>/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/<author>/<name>/approve/", methods=["POST"])
@bp.route("/packages/<author>/<name>/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/<author>/<name>/remove/", methods=["GET", "POST"])
@bp.route("/packages/<author>/<name>/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()

View File

@@ -17,10 +17,12 @@
from flask import *
from flask_user import *
from app import app
from . import bp
from app.rediscache import has_key, set_key, make_download_key
from app.models import *
from app.tasks.importtasks import makeVCSRelease
from app.utils import *
from celery import uuid
@@ -54,7 +56,7 @@ class CreatePackageReleaseForm(FlaskForm):
class EditPackageReleaseForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(1, 30)])
url = StringField("URL", [URL])
task_id = StringField("Task ID")
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)
@@ -62,10 +64,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/<author>/<name>/releases/new/", methods=["GET", "POST"])
@bp.route("/packages/<author>/<name>/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 +96,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,32 +117,30 @@ def create_release_page(package):
return render_template("packages/release_new.html", package=package, form=form)
@app.route("/packages/<author>/<name>/releases/<id>/download/")
@bp.route("/packages/<author>/<name>/releases/<id>/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)
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()
ip = request.headers.get("X-Forwarded-For") or request.remote_addr
if ip is not None:
key = make_download_key(ip, release.package)
if not has_key(key):
set_key(key, "true")
return redirect(release.url, code=300)
PackageRelease.query.filter_by(id=release.id).update({
"downloads": PackageRelease.downloads + 1
})
db.session.commit()
@app.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
return redirect(release.url, code=300)
@bp.route("/packages/<author>/<name>/releases/<id>/", 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)
@@ -164,7 +164,7 @@ def edit_release_page(package, id):
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.strip() == "":
if release.task_id is not None:
release.task_id = None
if canApprove:
@@ -190,10 +190,10 @@ class BulkReleaseForm(FlaskForm):
submit = SubmitField("Update")
@app.route("/packages/<author>/<name>/releases/bulk_change/", methods=["GET", "POST"])
@bp.route("/packages/<author>/<name>/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())

View File

@@ -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/<author>/<name>/screenshots/new/", methods=["GET", "POST"])
@bp.route("/packages/<author>/<name>/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/<author>/<name>/screenshots/<id>/edit/", methods=["GET", "POST"])
@bp.route("/packages/<author>/<name>/screenshots/<id>/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)

View File

@@ -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/<id>/")
def check_task(id):
@bp.route("/tasks/<id>/")
def check(id):
result = celery.AsyncResult(id)
status = result.status
traceback = result.traceback

View File

@@ -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/<int:id>/subscribe/", methods=["POST"])
@bp.route("/threads/<int:id>/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/<int:id>/unsubscribe/", methods=["POST"])
@bp.route("/threads/<int:id>/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/<int:id>/", methods=["GET", "POST"])
def thread_page(id):
clearNotifications(url_for("thread_page", 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):
@@ -85,7 +87,7 @@ def thread_page(id):
if package:
return redirect(package.getDetailsURL())
else:
return redirect(url_for("home_page"))
return redirect(url_for("homepage.home"))
if len(comment) <= 500 and len(comment) > 3:
reply = ThreadReply()
@@ -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
@@ -140,7 +142,7 @@ def new_thread_page():
abort(403)
def_is_private = request.args.get("private") or False
if package is None or not package.approved:
if package is None:
def_is_private = True
allow_change = package and package.approved
is_review_thread = package and not package.approved
@@ -148,12 +150,12 @@ def new_thread_page():
# 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"))
return redirect(url_for("homepage.home"))
# 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")
@@ -161,7 +163,7 @@ def new_thread_page():
if package:
return redirect(package.getDetailsURL())
else:
return redirect(url_for("home_page"))
return redirect(url_for("homepage.home"))
# Set default values
elif request.method == "GET":
@@ -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)

View File

@@ -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/<int:level>/<img>")
@bp.route("/thumbnails/<int:level>/<img>")
def make_thumbnail(img, level):
if level > len(ALLOWED_RESOLUTIONS) or level <= 0:
abort(403)

View File

@@ -14,24 +14,25 @@
# 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
from app.utils import get_int_or_abort
@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)
packages = None
if canApproveNew:
packages = Package.query.filter_by(approved=False, soft_deleted=False).all()
packages = Package.query.filter_by(approved=False, soft_deleted=False).order_by(db.desc(Package.created_at)).all()
releases = None
if canApproveRel:
@@ -51,7 +52,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 +70,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()
@@ -82,16 +83,16 @@ def todo_topics_page():
total = tmp_q.count()
topic_count = query.count()
page = int(request.args.get("page") or 1)
num = int(request.args.get("n") or 100)
page = get_int_or_abort(request.args.get("page"), 1)
num = get_int_or_abort(request.args.get("n"), 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, \
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

View File

@@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint("users", __name__)
from . import githublogin, profile

View File

@@ -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")
@@ -53,21 +54,21 @@ def github_authorized(oauth_token):
current_user.github_username = username
db.session.commit()
flash("Linked github to account", "success")
return redirect(url_for("home_page"))
return redirect(url_for("homepage.home"))
else:
flash("Github account is already associated with another user", "danger")
return redirect(url_for("home_page"))
return redirect(url_for("homepage.home"))
# 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"))
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"))
return redirect(next_url or url_for("homepage.home"))
else:
flash("Authorization failed [err=gh-login-failed]", "danger")
return redirect(url_for("user.login"))

View File

@@ -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 *
@@ -31,21 +32,21 @@ 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()])
website_url = StringField("Website URL", [Optional(), URL()])
donate_url = StringField("Donation URL", [Optional(), URL()])
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():
@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/<username>/", methods=["GET", "POST"])
def user_profile_page(username):
@bp.route("/users/<username>/", 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/profile.html",
user=user, form=form, packages=packages, topics_to_add=topics_to_add)
@app.route("/users/<username>/check/", methods=["POST"])
@bp.route("/users/<username>/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/<username>/email/", methods=["GET", "POST"])
@bp.route("/users/<username>/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)
@@ -162,13 +163,13 @@ def send_email_page(username):
class SetPasswordForm(FlaskForm):
email = StringField("Email", [Optional(), Email()])
password = PasswordField("New password", [InputRequired(), Length(2, 20)])
password2 = PasswordField("Verify password", [InputRequired(), Length(2, 20)])
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"])
@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"))
return redirect(url_for("homepage.home"))

View File

@@ -37,7 +37,7 @@ Also see the [help page on tags](/help/package_tags/).
Sexually-orientated content is not permitted.
Mature content, including that relating to drugs, excessive gore, violence, or
horror, is not currently permitted - but will be in the future.
excessive horror, is not currently permitted - but will be in the future.
The submission of malware is strictly prohibited. This includes software which
does not do as it advertises, for example if it posts telemetry without stating

View File

@@ -213,6 +213,9 @@ class Notification(db.Model):
url = db.Column(db.String(200), nullable=True)
def __init__(self, us, cau, titl, ur):
if len(titl) > 100:
title = title[:99] + ""
self.user = us
self.causer = cau
self.title = titl
@@ -395,7 +398,7 @@ class Package(db.Model):
forums = db.Column(db.Integer, nullable=True)
provides = db.relationship("MetaPackage", secondary=provides, lazy="subquery",
backref=db.backref("packages", lazy="dynamic"))
backref=db.backref("packages", lazy="dynamic", order_by=db.desc("score")))
dependencies = db.relationship("Dependency", backref="depender", lazy="dynamic", foreign_keys=[Dependency.depender_id])
@@ -438,6 +441,13 @@ class Package(db.Model):
else:
return "ready"
def getAsDictionaryKey(self):
return {
"name": self.name,
"author": self.author.display_name,
"type": self.type.toName(),
}
def getAsDictionaryShort(self, base_url, version=None, protonum=None):
tnurl = self.getThumbnailURL(1)
release = self.getDownloadRelease(version=version, protonum=protonum)
@@ -491,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):
@@ -519,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):
@@ -706,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)
@@ -748,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)
@@ -870,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):

13
app/rediscache.py Normal file
View File

@@ -0,0 +1,13 @@
from . import r
# This file acts as a facade between the releases code and redis,
# and also means that the releases code avoids knowing about `app`
def make_download_key(ip, package):
return ("{}/{}/{}").format(ip, package.author.username, package.name)
def set_key(key, v):
r.set(key, v)
def has_key(key):
return r.exists(key)

View File

@@ -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/<path:filepath>.css" % (outputPath), 'sass', _sass)
sass(app)

View File

@@ -15,7 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import render_template
from flask import render_template, url_for
from flask_mail import Message
from app import mail
from app.tasks import celery
@@ -24,7 +24,18 @@ from app.tasks import celery
def sendVerifyEmail(newEmail, token):
print("Sending verify email!")
msg = Message("Verify email address", recipients=[newEmail])
msg.body = "This is a verification email!"
msg.body = """
This email has been sent to you because someone (hopefully you)
has entered your email address as a user's email.
If it wasn't you, then just delete this email.
If this was you, then please click this link to verify the address:
{}
""".format(url_for('users.verify_email', token=token, _external=True))
msg.html = render_template("emails/verify.html", token=token)
mail.send(msg)
@@ -33,9 +44,7 @@ def sendEmailRaw(to, subject, text, html):
from flask_mail import Message
msg = Message(subject, recipients=to)
if text:
msg.body = text
msg.body = text or html
html = html or text
msg.html = render_template("emails/base.html", subject=subject, content=html)
mail.send(msg)

View File

@@ -15,7 +15,7 @@
# 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
@@ -29,6 +29,10 @@ from app.utils import randomString
class GithubURLMaker:
def __init__(self, url):
self.baseUrl = None
self.user = None
self.repo = None
# Rewrite path
import re
m = re.search("^\/([^\/]+)\/([^\/]+)\/?$", url.path)
@@ -51,6 +55,9 @@ class GithubURLMaker:
def getScreenshotURL(self):
return self.baseUrl + "/screenshot.png"
def getModConfURL(self):
return self.baseUrl + "/mod.conf"
def getCommitsURL(self, branch):
return "https://api.github.com/repos/{}/{}/commits?sha={}" \
.format(self.user, self.repo, urllib.parse.quote_plus(branch))
@@ -291,16 +298,23 @@ def cloneRepo(urlstr, ref=None, recursive=False):
try:
gitUrl = generateGitURL(urlstr)
print("Cloning from " + gitUrl)
repo = git.Repo.clone_from(gitUrl, gitDir, \
progress=None, env=None, depth=1, recursive=recursive, kill_after_timeout=15)
if ref is not None:
repo.create_head("myhead", ref).checkout()
if ref is None:
repo = git.Repo.clone_from(gitUrl, gitDir, \
progress=None, env=None, depth=1, recursive=recursive, kill_after_timeout=15)
else:
repo = git.Repo.clone_from(gitUrl, gitDir, \
progress=None, env=None, depth=1, recursive=recursive, kill_after_timeout=15, b=ref)
return gitDir, repo
except GitCommandError as e:
# This is needed to stop the backtrace being weird
err = e.stderr
except gitdb.exc.BadName as e:
err = "Unable to find the reference " + (ref or "?") + "\n" + e.stderr
raise TaskError(err.replace("stderr: ", "") \
.replace("Cloning into '" + gitDir + "'...", "") \
.strip())
@@ -339,8 +353,11 @@ def makeVCSReleaseFromGithub(id, branch, release, url):
raise TaskError("Invalid github repo URL")
commitsURL = urlmaker.getCommitsURL(branch)
contents = urllib.request.urlopen(commitsURL).read().decode("utf-8")
commits = json.loads(contents)
try:
contents = urllib.request.urlopen(commitsURL).read().decode("utf-8")
commits = json.loads(contents)
except HTTPError:
raise TaskError("Unable to get commits for Github repository. Either the repository or reference doesn't exist.")
if len(commits) == 0 or not "sha" in commits[0]:
raise TaskError("No commits found")
@@ -349,7 +366,6 @@ def makeVCSReleaseFromGithub(id, branch, release, url):
release.task_id = None
release.commit_hash = commits[0]["sha"]
release.approve(release.package.author)
print(release.url)
db.session.commit()
return release.url

22
app/template_filters.py Normal file
View File

@@ -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"

View File

@@ -10,8 +10,8 @@
{% 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 %}

View File

@@ -6,11 +6,11 @@ Licenses
{% 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 %}

View File

@@ -6,11 +6,11 @@
{% 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">
@@ -19,7 +19,8 @@
<form method="post" action="" class="card-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<select name="action">
<option value="importmodlist" selected>Import forum topics</option>
<option value="delstuckreleases" selected>Delete stuck releases</option>
<option value="importmodlist">Import forum topics</option>
<option value="recalcscores">Recalculate package scores</option>
<option value="checkusers">Check forum users</option>
<option value="importscreenshots">Import screenshots from VCS</option>

View File

@@ -10,8 +10,8 @@
{% 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 %}

View File

@@ -6,11 +6,11 @@ Tags
{% 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 %}

View File

@@ -10,8 +10,8 @@
{% 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 %}

View File

@@ -6,11 +6,11 @@ Minetest Versions
{% 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 %}

View File

@@ -60,10 +60,10 @@
</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"
@@ -73,24 +73,24 @@
<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>
@@ -134,7 +134,13 @@
<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">
DEBUG MODE ENABLED
</p>
{% endif %}
</footer>
<script src="/static/jquery.min.js"></script>

View File

@@ -16,12 +16,12 @@
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 %}

View File

@@ -60,7 +60,7 @@ Sign in
{% 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>
@@ -72,7 +72,7 @@ Sign in
<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>

View File

@@ -37,28 +37,28 @@
{% 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>

View File

@@ -4,7 +4,7 @@
{% 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>
@@ -12,11 +12,11 @@
<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>
@@ -43,7 +43,7 @@
</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" />
@@ -65,14 +65,14 @@
{% 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 %}&#x1f512; {% endif %}
{{ t.title }}
by {{ t.author.display_name }}
</a>
{% else %}
{% if t.private %}&#x1f512; {% 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>

View File

@@ -18,14 +18,14 @@
{% 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 %}
@@ -56,10 +56,10 @@
{% 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 %}

View File

@@ -7,7 +7,7 @@ Meta Packages
{% 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 %}

View File

@@ -6,7 +6,7 @@ Notifications
{% 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>

View File

@@ -15,7 +15,7 @@
{% 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>

View File

@@ -26,7 +26,7 @@
{% 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 %}

View File

@@ -103,7 +103,7 @@
{% 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>
@@ -172,14 +172,14 @@
<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>
@@ -241,7 +241,7 @@
{{ 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 }}
@@ -301,7 +301,7 @@
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 %}
@@ -320,7 +320,7 @@
<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>
@@ -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) %}
<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 %}
@@ -381,7 +381,7 @@
<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>

View File

@@ -16,7 +16,7 @@ Working
<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>

View File

@@ -37,7 +37,7 @@
{{ render_checkbox_field(form.private, class_="my-3") }}
<p>
Only the you, the package author, and users of Editor rank
Only you, the package author, and users of Editor rank
and above can read private threads.
</p>

View File

@@ -42,6 +42,9 @@
<ul class="list-group list-group-flush">
{% for r in releases %}
<li class="list-group-item">
{% if r.task_id %}
<span class="mr-2 badge badge-warning">Importing</span>
{% endif %}
<a href="{{ r.getEditURL() }}">{{ r.title }}</a>
on
<a href="{{ r.package.getDetailsURL() }}">
@@ -60,7 +63,7 @@
{% 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" />
@@ -98,17 +101,23 @@
<h2 class="mt-4">Unadded Topic List</h2>
<p>
{{ total_topics - topics_to_add }} / {{ total_topics }} packages have been been added to cdb,
based on cdb's forum parser. {{ topics_to_add }} remaining.
</p>
{% if total_topics > 0 %}
<p>
{{ total_topics - topics_to_add }} / {{ total_topics }} packages have been been added to cdb,
based on cdb's forum parser. {{ topics_to_add }} remaining.
</p>
<div class="progress my-4">
{% set perc = 32 %}
<div class="progress-bar bg-success" role="progressbar"
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div class="progress my-4">
{% set perc = 100 * (total_topics - topics_to_add) / total_topics %}
<div class="progress-bar bg-success" role="progressbar"
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>
{% else %}
<p>
The forum topic crawler needs to run at least once for this section to work.
</p>
{% endif %}
{% endblock %}

View File

@@ -8,15 +8,15 @@ Topics to be Added
<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>
@@ -26,18 +26,18 @@ Topics to be Added
{% 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 %}
@@ -51,17 +51,23 @@ Topics to be Added
<h1>Topics to be Added</h1>
<p>
{{ total - topic_count }} / {{ total }} topics have been added as packages to CDB.
{{ topic_count }} remaining.
</p>
<div class="progress">
{% set perc = 100 * (total - topic_count) / total %}
<div class="progress-bar bg-success" role="progressbar"
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
{% if topic_count > 0 %}
<p>
{{ total - topic_count }} / {{ total }} topics have been added as packages to CDB.
{{ topic_count }} remaining.
</p>
<div class="progress">
{% set perc = 100 * (total - topic_count) / total %}
<div class="progress-bar bg-success" role="progressbar"
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
{% else %}
<p>
The forum topic crawler needs to run at least once for this section to work.
</p>
{% endif %}
<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" }} />
@@ -79,7 +85,7 @@ Topics to be Added
{% 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>

View File

@@ -19,7 +19,7 @@ Creating an Account
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>
@@ -44,7 +44,7 @@ Creating an Account
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() }}" />
@@ -73,7 +73,7 @@ Creating an Account
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() }}" />

View File

@@ -8,7 +8,7 @@
<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() }}

View File

@@ -9,7 +9,7 @@
{% 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>
@@ -57,7 +57,7 @@
{% 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 %}
@@ -78,7 +78,7 @@
<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 %}
@@ -97,7 +97,7 @@
<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>
@@ -122,7 +122,7 @@
{% 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>

View File

@@ -10,7 +10,7 @@
<div class="alert alert-primary">
It is recommended that you set a password for your account.
<a class="alert_right button" href="{{ url_for('home_page') }}">Skip</a>
<a class="alert_right button" href="{{ url_for('homepage.home') }}">Skip</a>
</div>
{% endif %}

View File

@@ -22,6 +22,12 @@ from app.models import *
from app import app
import random, string, os, imghdr
def get_int_or_abort(v, default):
try:
return int(v or default)
except ValueError:
abort(400)
def getExtension(filename):
return filename.rsplit(".", 1)[1].lower() if "." in filename else None
@@ -50,7 +56,7 @@ def doFileUpload(file, fileType, fileTypeDesc):
if fileType == "image":
allowedExtensions = ["jpg", "jpeg", "png"]
isImage = True
elif filetype == "zip":
elif fileType == "zip":
allowedExtensions = ["zip"]
else:
raise Exception("Invalid fileType")

View File

@@ -1,80 +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 <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.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()

View File

@@ -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 <https://www.gnu.org/licenses/>.
from . import admin, licenseseditor, tagseditor, versioneditor, todo

View File

@@ -10,8 +10,9 @@ SQLALCHEMY_DATABASE_URI = "sqlite:///../db.sqlite"
GITHUB_CLIENT_ID = ""
GITHUB_CLIENT_SECRET = ""
CELERY_BROKER_URL='redis://localhost:6379'
CELERY_RESULT_BACKEND='redis://localhost:6379'
REDIS_URL='redis://redis:6379'
CELERY_BROKER_URL='redis://redis:6379'
CELERY_RESULT_BACKEND='redis://redis:6379'
USER_ENABLE_REGISTER = False
USER_ENABLE_CHANGE_USERNAME = False

View File

@@ -15,12 +15,14 @@ services:
app:
build: .
command: ./rundebug.sh
command: ./utils/run.sh
env_file:
- config.env
ports:
- 5123:5123
volumes:
- "./data/uploads:/home/cdb/app/public/uploads"
- "./app:/home/cdb/app"
- "./app:/home/cdb/appsrc"
- "./migrations:/home/cdb/migrations"
depends_on:
- db
@@ -31,7 +33,29 @@ services:
command: celery -A app.tasks.celery worker
env_file:
- config.env
environment:
- FLASK_CONFIG=../config.cfg
volumes:
- "./data/uploads:/home/cdb/app/public/uploads"
depends_on:
- redis
beat:
build: .
command: celery -A app.tasks.celery beat
env_file:
- config.env
environment:
- FLASK_CONFIG=../config.cfg
depends_on:
- redis
flower:
image: mher/flower
command: ["flower", "--broker=redis://redis:6379/0", "--port=5124"]
env_file:
- config.env
ports:
- 5124:5124
depends_on:
- redis

View File

@@ -16,6 +16,7 @@ celery==4.1.1
kombu==4.2.0
GitPython~=2.1
lxml~=4.2
pillow~=5.3
pillow~=6.2
pyScss~=1.3
redis~=3.0
redis==2.10.6
psycopg2~=2.7

View File

@@ -1,3 +0,0 @@
#!/bin/bash
FLASK_APP=app/__init__.py FLASK_CONFIG=../config.cfg FLASK_DEBUG=1 python3 -m flask run -h 0.0.0.0 -p 5123

View File

@@ -1,3 +0,0 @@
#!/bin/bash
FLASK_APP=app/__init__.py FLASK_CONFIG=../config.cfg FLASK_DEBUG=0 python3 -m flask run -h 0.0.0.0 -p 5123

View File

@@ -1,3 +0,0 @@
#!/bin/bash
gunicorn -w 4 -b :5123 -e FLASK_APP=app/__init__.py -e FLASK_CONFIG=../config.cfg -e FLASK_DEBUG=0 app:app

21
utils/run.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
# Debug
# FLASK_APP=app/__init__.py FLASK_CONFIG=../config.cfg FLASK_DEBUG=1 python3 -m flask run -h 0.0.0.0 -p 5123
if [ -z "$FLASK_DEBUG" ]; then
echo "FLASK_DEBUG is required in config.env"
exit 1
fi
ENV="-e FLASK_APP=app/__init__.py -e FLASK_CONFIG=../config.cfg -e FLASK_DEBUG=$FLASK_DEBUG"
if [ "$FLASK_DEBUG" -eq "1" ]; then
EXTRA="--reload"
fi
echo "Running gunicorn with:"
echo " - env: $ENV"
echo " - extra: $EXTRA"
gunicorn -w 4 -b :5123 $ENV $EXTRA app:app

View File

@@ -15,7 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import os, sys, datetime
import os, sys, datetime, inspect
if not "FLASK_CONFIG" in os.environ:
os.environ["FLASK_CONFIG"] = "../config.cfg"
@@ -24,6 +24,11 @@ delete_db = len(sys.argv) >= 2 and sys.argv[1].strip() == "-d"
create_db = not (len(sys.argv) >= 2 and sys.argv[1].strip() == "-o")
test_data = len(sys.argv) >= 2 and sys.argv[1].strip() == "-t" or not create_db
# Allow finding the `app` module
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
parentdir = os.path.dirname(currentdir)
sys.path.insert(0,parentdir)
from app.models import *
from app.utils import make_flask_user_password