Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39d4cf362b | ||
|
|
b977a42738 | ||
|
|
ff2a74367f | ||
|
|
3f666d2302 | ||
|
|
a7d22973ff | ||
|
|
20583784f5 | ||
|
|
64f131ae27 | ||
|
|
015abe5a25 | ||
|
|
719a652235 | ||
|
|
50892ce9fc | ||
|
|
2e14836ed6 | ||
|
|
35e1aba4ad | ||
|
|
913537f96f | ||
|
|
b36a60d3a2 | ||
|
|
df247b021e | ||
|
|
9f678d8fde | ||
|
|
d89442438f | ||
|
|
08a9ae7b94 | ||
|
|
904e09f0dd | ||
|
|
038ef5b739 | ||
|
|
f8958ae1bc | ||
|
|
03eccbd56a | ||
|
|
fb31ea3c22 | ||
|
|
4082863b5a | ||
|
|
cc564af44e | ||
|
|
655ed2255a | ||
|
|
96b22744ec | ||
|
|
130d0bc7a0 | ||
|
|
1469e37c38 | ||
|
|
6ce495fcd3 | ||
|
|
776a3eff2a | ||
|
|
04e8ae5bdd | ||
|
|
18b9fb3876 | ||
|
|
1da86f27a7 | ||
|
|
85340a2fe9 | ||
|
|
c4a4d9c116 | ||
|
|
87a184595c | ||
|
|
b3b1e421f2 | ||
|
|
60483ef542 | ||
|
|
3c8a8b8988 | ||
|
|
2f8bdd8f0f | ||
|
|
e87db8b87f | ||
|
|
b36273a848 | ||
|
|
7b087158d7 | ||
|
|
2fbc44bd54 | ||
|
|
950512c2a7 | ||
|
|
f4010d498f | ||
|
|
f04d4ff3cd | ||
|
|
f8b290fc45 | ||
|
|
7e4eb29db7 | ||
|
|
93a74b7681 | ||
|
|
2677e088a8 | ||
|
|
0fd4984e5a | ||
|
|
896a65fd99 | ||
|
|
885209a614 | ||
|
|
4c109d6bd3 | ||
|
|
9c2c8c21f1 | ||
|
|
e40b247a97 | ||
|
|
a79cc758ed | ||
|
|
bafd426eaf | ||
|
|
36f9572cbb | ||
|
|
2586a11bcf | ||
|
|
d36138d5e1 | ||
|
|
7810bb54e0 | ||
|
|
2844773e4d | ||
|
|
23c406bff9 |
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.git
|
||||||
|
data
|
||||||
|
uploads
|
||||||
13
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
|
||||||
25
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
@@ -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
@@ -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).
|
||||||
5
.gitignore
vendored
@@ -1,12 +1,13 @@
|
|||||||
config.cfg
|
config.cfg
|
||||||
*.env
|
*.env
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
.vscode
|
||||||
custom.css
|
custom.css
|
||||||
tmp
|
tmp
|
||||||
log.txt
|
log.txt
|
||||||
*.rdb
|
*.rdb
|
||||||
uploads
|
app/public/uploads
|
||||||
thumbnails
|
app/public/thumbnails
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
/data
|
/data
|
||||||
|
|
||||||
|
|||||||
18
Dockerfile
@@ -1,17 +1,19 @@
|
|||||||
FROM python:3.6
|
FROM python:3.6
|
||||||
|
|
||||||
|
RUN groupadd -g 5123 cdb && \
|
||||||
|
useradd -r -u 5123 -g cdb cdb
|
||||||
|
|
||||||
WORKDIR /home/cdb
|
WORKDIR /home/cdb
|
||||||
|
|
||||||
COPY requirements.txt requirements.txt
|
COPY requirements.txt requirements.txt
|
||||||
RUN pip install -r ./requirements.txt
|
RUN pip install -r ./requirements.txt
|
||||||
RUN pip install gunicorn
|
RUN pip install gunicorn
|
||||||
RUN pip install psycopg2
|
|
||||||
|
|
||||||
COPY runprodguni.sh ./
|
COPY utils utils
|
||||||
COPY rundebug.sh ./
|
|
||||||
RUN chmod +x runprodguni.sh
|
|
||||||
|
|
||||||
COPY setup.py ./setup.py
|
|
||||||
COPY app app
|
|
||||||
COPY migrations migrations
|
|
||||||
COPY config.cfg ./config.cfg
|
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
|
||||||
|
|||||||
@@ -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
|
FLASK_CONFIG=../config.cfg celery -A app.tasks.celery worker
|
||||||
|
|
||||||
# if sqlite
|
# 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
|
rm db.sqlite && python setup.py -t && FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db stamp head
|
||||||
|
|
||||||
# Create migration
|
# Create migration
|
||||||
|
|||||||
@@ -24,19 +24,23 @@ from flaskext.markdown import Markdown
|
|||||||
from flask_github import GitHub
|
from flask_github import GitHub
|
||||||
from flask_wtf.csrf import CsrfProtect
|
from flask_wtf.csrf import CsrfProtect
|
||||||
from flask_flatpages import FlatPages
|
from flask_flatpages import FlatPages
|
||||||
import os
|
from flask_babel import Babel
|
||||||
|
import os, redis
|
||||||
|
|
||||||
app = Flask(__name__, static_folder="public/static")
|
app = Flask(__name__, static_folder="public/static")
|
||||||
app.config["FLATPAGES_ROOT"] = "flatpages"
|
app.config["FLATPAGES_ROOT"] = "flatpages"
|
||||||
app.config["FLATPAGES_EXTENSION"] = ".md"
|
app.config["FLATPAGES_EXTENSION"] = ".md"
|
||||||
app.config.from_pyfile(os.environ["FLASK_CONFIG"])
|
app.config.from_pyfile(os.environ["FLASK_CONFIG"])
|
||||||
|
|
||||||
|
r = redis.Redis.from_url(app.config["REDIS_URL"])
|
||||||
|
|
||||||
menu.Menu(app=app)
|
menu.Menu(app=app)
|
||||||
markdown = Markdown(app, extensions=["fenced_code"], safe_mode=True, output_format="html5")
|
markdown = Markdown(app, extensions=["fenced_code"], safe_mode=True, output_format="html5")
|
||||||
github = GitHub(app)
|
github = GitHub(app)
|
||||||
csrf = CsrfProtect(app)
|
csrf = CsrfProtect(app)
|
||||||
mail = Mail(app)
|
mail = Mail(app)
|
||||||
pages = FlatPages(app)
|
pages = FlatPages(app)
|
||||||
|
babel = Babel(app)
|
||||||
gravatar = Gravatar(app,
|
gravatar = Gravatar(app,
|
||||||
size=58,
|
size=58,
|
||||||
rating='g',
|
rating='g',
|
||||||
@@ -46,9 +50,44 @@ gravatar = Gravatar(app,
|
|||||||
use_ssl=True,
|
use_ssl=True,
|
||||||
base_url=None)
|
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
|
from .maillogger import register_mail_error_handler
|
||||||
register_mail_error_handler(app, mail)
|
register_mail_error_handler(app, mail)
|
||||||
|
|
||||||
from . import models, tasks
|
|
||||||
from .views import *
|
@babel.localeselector
|
||||||
|
def get_locale():
|
||||||
|
return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
|
||||||
|
|
||||||
|
from . import models, tasks, template_filters
|
||||||
|
|
||||||
|
from .blueprints import create_blueprints
|
||||||
|
create_blueprints(app)
|
||||||
|
|
||||||
|
from flask_login import logout_user
|
||||||
|
|
||||||
|
@app.route("/uploads/<path:path>")
|
||||||
|
def send_upload(path):
|
||||||
|
return send_from_directory("public/uploads", path)
|
||||||
|
|
||||||
|
@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
|
||||||
|
@app.route('/<path:path>/')
|
||||||
|
def flatpage(path):
|
||||||
|
page = pages.get_or_404(path)
|
||||||
|
template = page.meta.get('template', 'flatpage.html')
|
||||||
|
return render_template(template, page=page)
|
||||||
|
|
||||||
|
@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()
|
||||||
|
|||||||
10
app/blueprints/__init__.py
Normal 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)
|
||||||
@@ -15,4 +15,8 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# 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
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
import flask_menu as menu
|
import flask_menu as menu
|
||||||
from app import app
|
from . import bp
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from celery import uuid
|
from celery import uuid
|
||||||
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies, makeVCSRelease
|
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies, makeVCSRelease
|
||||||
@@ -28,17 +28,21 @@ from wtforms import *
|
|||||||
from app.utils import loginUser, rank_required, triggerNotif
|
from app.utils import loginUser, rank_required, triggerNotif
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
@app.route("/admin/", methods=["GET", "POST"])
|
@bp.route("/admin/", methods=["GET", "POST"])
|
||||||
@rank_required(UserRank.ADMIN)
|
@rank_required(UserRank.ADMIN)
|
||||||
def admin_page():
|
def admin_page():
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
action = request.form["action"]
|
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()
|
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":
|
elif action == "checkusers":
|
||||||
task = checkAllForumAccounts.delay()
|
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":
|
elif action == "importscreenshots":
|
||||||
packages = Package.query \
|
packages = Package.query \
|
||||||
.filter_by(soft_deleted=False) \
|
.filter_by(soft_deleted=False) \
|
||||||
@@ -48,7 +52,7 @@ def admin_page():
|
|||||||
for package in packages:
|
for package in packages:
|
||||||
importRepoScreenshot.delay(package.id)
|
importRepoScreenshot.delay(package.id)
|
||||||
|
|
||||||
return redirect(url_for("admin_page"))
|
return redirect(url_for("admin.admin_page"))
|
||||||
elif action == "restore":
|
elif action == "restore":
|
||||||
package = Package.query.get(request.form["package"])
|
package = Package.query.get(request.form["package"])
|
||||||
if package is None:
|
if package is None:
|
||||||
@@ -56,10 +60,10 @@ def admin_page():
|
|||||||
else:
|
else:
|
||||||
package.soft_deleted = False
|
package.soft_deleted = False
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for("admin_page"))
|
return redirect(url_for("admin.admin_page"))
|
||||||
elif action == "importdepends":
|
elif action == "importdepends":
|
||||||
task = importAllDependencies.delay()
|
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":
|
elif action == "modprovides":
|
||||||
packages = Package.query.filter_by(type=PackageType.MOD).all()
|
packages = Package.query.filter_by(type=PackageType.MOD).all()
|
||||||
mpackage_cache = {}
|
mpackage_cache = {}
|
||||||
@@ -68,13 +72,13 @@ def admin_page():
|
|||||||
p.provides.append(MetaPackage.GetOrCreate(p.name, mpackage_cache))
|
p.provides.append(MetaPackage.GetOrCreate(p.name, mpackage_cache))
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for("admin_page"))
|
return redirect(url_for("admin.admin_page"))
|
||||||
elif action == "recalcscores":
|
elif action == "recalcscores":
|
||||||
for p in Package.query.all():
|
for p in Package.query.all():
|
||||||
p.recalcScore()
|
p.recalcScore()
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for("admin_page"))
|
return redirect(url_for("admin.admin_page"))
|
||||||
elif action == "vcsrelease":
|
elif action == "vcsrelease":
|
||||||
for package in Package.query.filter(Package.repo.isnot(None)).all():
|
for package in Package.query.filter(Package.repo.isnot(None)).all():
|
||||||
if package.releases.count() != 0:
|
if package.releases.count() != 0:
|
||||||
@@ -106,19 +110,19 @@ class SwitchUserForm(FlaskForm):
|
|||||||
submit = SubmitField("Switch")
|
submit = SubmitField("Switch")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/admin/switchuser/", methods=["GET", "POST"])
|
@bp.route("/admin/switchuser/", methods=["GET", "POST"])
|
||||||
@rank_required(UserRank.ADMIN)
|
@rank_required(UserRank.ADMIN)
|
||||||
def switch_user_page():
|
def switch_user():
|
||||||
form = SwitchUserForm(formdata=request.form)
|
form = SwitchUserForm(formdata=request.form)
|
||||||
if request.method == "POST" and form.validate():
|
if request.method == "POST" and form.validate():
|
||||||
user = User.query.filter_by(username=form["username"].data).first()
|
user = User.query.filter_by(username=form["username"].data).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
flash("Unable to find user", "error")
|
flash("Unable to find user", "error")
|
||||||
elif loginUser(user):
|
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:
|
else:
|
||||||
flash("Unable to login as user", "error")
|
flash("Unable to login as user", "error")
|
||||||
|
|
||||||
|
|
||||||
# Process GET or invalid POST
|
# Process GET or invalid POST
|
||||||
return render_template("admin/switch_user_page.html", form=form)
|
return render_template("admin/switch_user.html", form=form)
|
||||||
@@ -17,16 +17,16 @@
|
|||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
from app import app
|
from . import bp
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import *
|
from wtforms import *
|
||||||
from wtforms.validators import *
|
from wtforms.validators import *
|
||||||
from app.utils import rank_required
|
from app.utils import rank_required
|
||||||
|
|
||||||
@app.route("/licenses/")
|
@bp.route("/licenses/")
|
||||||
@rank_required(UserRank.MODERATOR)
|
@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())
|
return render_template("admin/licenses/list.html", licenses=License.query.order_by(db.asc(License.name)).all())
|
||||||
|
|
||||||
class LicenseForm(FlaskForm):
|
class LicenseForm(FlaskForm):
|
||||||
@@ -34,10 +34,10 @@ class LicenseForm(FlaskForm):
|
|||||||
is_foss = BooleanField("Is FOSS")
|
is_foss = BooleanField("Is FOSS")
|
||||||
submit = SubmitField("Save")
|
submit = SubmitField("Save")
|
||||||
|
|
||||||
@app.route("/licenses/new/", methods=["GET", "POST"])
|
@bp.route("/licenses/new/", methods=["GET", "POST"])
|
||||||
@app.route("/licenses/<name>/edit/", methods=["GET", "POST"])
|
@bp.route("/licenses/<name>/edit/", methods=["GET", "POST"])
|
||||||
@rank_required(UserRank.MODERATOR)
|
@rank_required(UserRank.MODERATOR)
|
||||||
def createedit_license_page(name=None):
|
def create_edit_license(name=None):
|
||||||
license = None
|
license = None
|
||||||
if name is not None:
|
if name is not None:
|
||||||
license = License.query.filter_by(name=name).first()
|
license = License.query.filter_by(name=name).first()
|
||||||
@@ -57,6 +57,6 @@ def createedit_license_page(name=None):
|
|||||||
|
|
||||||
form.populate_obj(license)
|
form.populate_obj(license)
|
||||||
db.session.commit()
|
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)
|
return render_template("admin/licenses/edit.html", license=license, form=form)
|
||||||
@@ -17,16 +17,16 @@
|
|||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
from app import app
|
from . import bp
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import *
|
from wtforms import *
|
||||||
from wtforms.validators import *
|
from wtforms.validators import *
|
||||||
from app.utils import rank_required
|
from app.utils import rank_required
|
||||||
|
|
||||||
@app.route("/tags/")
|
@bp.route("/tags/")
|
||||||
@rank_required(UserRank.MODERATOR)
|
@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())
|
return render_template("admin/tags/list.html", tags=Tag.query.order_by(db.asc(Tag.title)).all())
|
||||||
|
|
||||||
class TagForm(FlaskForm):
|
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")])
|
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")
|
submit = SubmitField("Save")
|
||||||
|
|
||||||
@app.route("/tags/new/", methods=["GET", "POST"])
|
@bp.route("/tags/new/", methods=["GET", "POST"])
|
||||||
@app.route("/tags/<name>/edit/", methods=["GET", "POST"])
|
@bp.route("/tags/<name>/edit/", methods=["GET", "POST"])
|
||||||
@rank_required(UserRank.MODERATOR)
|
@rank_required(UserRank.MODERATOR)
|
||||||
def createedit_tag_page(name=None):
|
def create_edit_tag(name=None):
|
||||||
tag = None
|
tag = None
|
||||||
if name is not None:
|
if name is not None:
|
||||||
tag = Tag.query.filter_by(name=name).first()
|
tag = Tag.query.filter_by(name=name).first()
|
||||||
@@ -52,6 +52,6 @@ def createedit_tag_page(name=None):
|
|||||||
else:
|
else:
|
||||||
form.populate_obj(tag)
|
form.populate_obj(tag)
|
||||||
db.session.commit()
|
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)
|
return render_template("admin/tags/edit.html", tag=tag, form=form)
|
||||||
@@ -17,16 +17,16 @@
|
|||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
from app import app
|
from . import bp
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import *
|
from wtforms import *
|
||||||
from wtforms.validators import *
|
from wtforms.validators import *
|
||||||
from app.utils import rank_required
|
from app.utils import rank_required
|
||||||
|
|
||||||
@app.route("/versions/")
|
@bp.route("/versions/")
|
||||||
@rank_required(UserRank.MODERATOR)
|
@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())
|
return render_template("admin/versions/list.html", versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all())
|
||||||
|
|
||||||
class VersionForm(FlaskForm):
|
class VersionForm(FlaskForm):
|
||||||
@@ -34,10 +34,10 @@ class VersionForm(FlaskForm):
|
|||||||
protocol = IntegerField("Protocol")
|
protocol = IntegerField("Protocol")
|
||||||
submit = SubmitField("Save")
|
submit = SubmitField("Save")
|
||||||
|
|
||||||
@app.route("/versions/new/", methods=["GET", "POST"])
|
@bp.route("/versions/new/", methods=["GET", "POST"])
|
||||||
@app.route("/versions/<name>/edit/", methods=["GET", "POST"])
|
@bp.route("/versions/<name>/edit/", methods=["GET", "POST"])
|
||||||
@rank_required(UserRank.MODERATOR)
|
@rank_required(UserRank.MODERATOR)
|
||||||
def createedit_version_page(name=None):
|
def create_edit_version(name=None):
|
||||||
version = None
|
version = None
|
||||||
if name is not None:
|
if name is not None:
|
||||||
version = MinetestRelease.query.filter_by(name=name).first()
|
version = MinetestRelease.query.filter_by(name=name).first()
|
||||||
@@ -55,6 +55,6 @@ def createedit_version_page(name=None):
|
|||||||
|
|
||||||
form.populate_obj(version)
|
form.populate_obj(version)
|
||||||
db.session.commit()
|
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)
|
return render_template("admin/versions/edit.html", version=version, form=form)
|
||||||
@@ -17,35 +17,66 @@
|
|||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
from app import app
|
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app.utils import is_package_page
|
from app.utils import is_package_page
|
||||||
from app.querybuilder import QueryBuilder
|
from app.querybuilder import QueryBuilder
|
||||||
|
|
||||||
@app.route("/api/packages/")
|
bp = Blueprint("api", __name__)
|
||||||
def api_packages_page():
|
|
||||||
|
@bp.route("/api/packages/")
|
||||||
|
def packages():
|
||||||
qb = QueryBuilder(request.args)
|
qb = QueryBuilder(request.args)
|
||||||
query = qb.buildPackageQuery()
|
query = qb.buildPackageQuery()
|
||||||
|
ver = qb.getMinetestVersion()
|
||||||
|
|
||||||
pkgs = [package.getAsDictionaryShort(app.config["BASE_URL"]) \
|
pkgs = [package.getAsDictionaryShort(current_app.config["BASE_URL"], version=ver) \
|
||||||
for package in query.all() if package.getDownloadRelease() is not None]
|
for package in query.all()]
|
||||||
return jsonify(pkgs)
|
return jsonify(pkgs)
|
||||||
|
|
||||||
@app.route("/api/packages/<author>/<name>/")
|
|
||||||
|
@bp.route("/api/packages/<author>/<name>/")
|
||||||
@is_package_page
|
@is_package_page
|
||||||
def api_package_page(package):
|
def package(package):
|
||||||
return jsonify(package.getAsDictionary(app.config["BASE_URL"]))
|
return jsonify(package.getAsDictionary(current_app.config["BASE_URL"]))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/topics/")
|
@bp.route("/api/packages/<author>/<name>/dependencies/")
|
||||||
def api_topics_page():
|
@is_package_page
|
||||||
query = ForumTopic.query \
|
def package_dependencies(package):
|
||||||
.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title))
|
ret = []
|
||||||
pkgs = [t.getAsDictionary() for t in query.all()]
|
|
||||||
return jsonify(pkgs)
|
for dep in package.dependencies:
|
||||||
|
name = None
|
||||||
|
fulfilled_by = None
|
||||||
|
|
||||||
|
if dep.package:
|
||||||
|
name = dep.package.name
|
||||||
|
fulfilled_by = [ dep.package.getAsDictionaryKey() ]
|
||||||
|
|
||||||
|
elif dep.meta_package:
|
||||||
|
name = dep.meta_package.name
|
||||||
|
fulfilled_by = [ pkg.getAsDictionaryKey() for pkg in dep.meta_package.packages]
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise "Malformed dependency"
|
||||||
|
|
||||||
|
ret.append({
|
||||||
|
"name": name,
|
||||||
|
"is_optional": dep.optional,
|
||||||
|
"packages": fulfilled_by
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify(ret)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/topic_discard/", methods=["POST"])
|
@bp.route("/api/topics/")
|
||||||
|
def topics():
|
||||||
|
qb = QueryBuilder(request.args)
|
||||||
|
query = qb.buildTopicQuery(show_added=True)
|
||||||
|
return jsonify([t.getAsDictionary() for t in query.all()])
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/api/topic_discard/", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def topic_set_discard():
|
def topic_set_discard():
|
||||||
tid = request.args.get("tid")
|
tid = request.args.get("tid")
|
||||||
@@ -63,7 +94,7 @@ def topic_set_discard():
|
|||||||
return jsonify(topic.getAsDictionary())
|
return jsonify(topic.getAsDictionary())
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/minetest_versions/")
|
@bp.route("/api/minetest_versions/")
|
||||||
def api_minetest_versions_page():
|
def versions():
|
||||||
return jsonify([{ "name": rel.name, "protocol_version": rel.protocol }\
|
return jsonify([{ "name": rel.name, "protocol_version": rel.protocol }\
|
||||||
for rel in MinetestRelease.query.all() if rel.getActual() is not None])
|
for rel in MinetestRelease.query.all() if rel.getActual() is not None])
|
||||||
20
app/blueprints/homepage/__init__.py
Normal 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)
|
||||||
@@ -16,17 +16,19 @@
|
|||||||
|
|
||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
|
|
||||||
|
bp = Blueprint("metapackages", __name__)
|
||||||
|
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
from app import app
|
|
||||||
from app.models import *
|
from app.models import *
|
||||||
|
|
||||||
@app.route("/metapackages/")
|
@bp.route("/metapackages/")
|
||||||
def meta_package_list_page():
|
def list_all():
|
||||||
mpackages = MetaPackage.query.order_by(db.asc(MetaPackage.name)).all()
|
mpackages = MetaPackage.query.order_by(db.asc(MetaPackage.name)).all()
|
||||||
return render_template("meta/list.html", mpackages=mpackages)
|
return render_template("meta/list.html", mpackages=mpackages)
|
||||||
|
|
||||||
@app.route("/metapackages/<name>/")
|
@bp.route("/metapackages/<name>/")
|
||||||
def meta_package_page(name):
|
def view(name):
|
||||||
mpackage = MetaPackage.query.filter_by(name=name).first()
|
mpackage = MetaPackage.query.filter_by(name=name).first()
|
||||||
if mpackage is None:
|
if mpackage is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
@@ -15,19 +15,20 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# 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 flask_user import current_user, login_required
|
||||||
from app import app
|
from app.models import db
|
||||||
from app.models import *
|
|
||||||
|
|
||||||
@app.route("/notifications/")
|
bp = Blueprint("notifications", __name__)
|
||||||
|
|
||||||
|
@bp.route("/notifications/")
|
||||||
@login_required
|
@login_required
|
||||||
def notifications_page():
|
def list_all():
|
||||||
return render_template("notifications/list.html")
|
return render_template("notifications/list.html")
|
||||||
|
|
||||||
@app.route("/notifications/clear/", methods=["POST"])
|
@bp.route("/notifications/clear/", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def clear_notifications_page():
|
def clear():
|
||||||
current_user.notifications.clear()
|
current_user.notifications.clear()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for("notifications_page"))
|
return redirect(url_for("notifications.list_all"))
|
||||||
@@ -14,5 +14,8 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# 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
|
from . import packages, screenshots, releases
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
from app import app
|
from app import app
|
||||||
@@ -18,11 +18,14 @@
|
|||||||
from flask import render_template, abort, request, redirect, url_for, flash
|
from flask import render_template, abort, request, redirect, url_for, flash
|
||||||
from flask_user import current_user
|
from flask_user import current_user
|
||||||
import flask_menu as menu
|
import flask_menu as menu
|
||||||
from app import app
|
|
||||||
|
from . import bp
|
||||||
|
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app.querybuilder import QueryBuilder
|
from app.querybuilder import QueryBuilder
|
||||||
from app.tasks.importtasks import importRepoScreenshot
|
from app.tasks.importtasks import importRepoScreenshot
|
||||||
from app.utils import *
|
from app.utils import *
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import *
|
from wtforms import *
|
||||||
from wtforms.validators import *
|
from wtforms.validators import *
|
||||||
@@ -30,12 +33,12 @@ from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleF
|
|||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
|
|
||||||
|
|
||||||
@menu.register_menu(app, ".mods", "Mods", order=11, endpoint_arguments_constructor=lambda: { 'type': 'mod' })
|
@menu.register_menu(bp, ".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(bp, ".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(bp, ".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' })
|
@menu.register_menu(bp, ".random", "Random", order=14, endpoint_arguments_constructor=lambda: { 'random': '1' })
|
||||||
@app.route("/packages/")
|
@bp.route("/packages/")
|
||||||
def packages_page():
|
def list_all():
|
||||||
qb = QueryBuilder(request.args)
|
qb = QueryBuilder(request.args)
|
||||||
query = qb.buildPackageQuery()
|
query = qb.buildPackageQuery()
|
||||||
title = qb.title
|
title = qb.title
|
||||||
@@ -49,16 +52,16 @@ def packages_page():
|
|||||||
if qb.search and topic:
|
if qb.search and topic:
|
||||||
return redirect("https://forum.minetest.net/viewtopic.php?t=" + str(topic.topic_id))
|
return redirect("https://forum.minetest.net/viewtopic.php?t=" + str(topic.topic_id))
|
||||||
|
|
||||||
page = int(request.args.get("page") or 1)
|
page = get_int_or_abort(request.args.get("page"), 1)
|
||||||
num = min(40, int(request.args.get("n") or 100))
|
num = min(40, get_int_or_abort(request.args.get("n"), 100))
|
||||||
query = query.paginate(page, num, True)
|
query = query.paginate(page, num, True)
|
||||||
|
|
||||||
search = request.args.get("q")
|
search = request.args.get("q")
|
||||||
type_name = request.args.get("type")
|
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
|
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
|
if query.has_prev else None
|
||||||
|
|
||||||
topics = None
|
topics = None
|
||||||
@@ -74,14 +77,14 @@ def packages_page():
|
|||||||
|
|
||||||
def getReleases(package):
|
def getReleases(package):
|
||||||
if package.checkPerm(current_user, Permission.MAKE_RELEASE):
|
if package.checkPerm(current_user, Permission.MAKE_RELEASE):
|
||||||
return package.releases
|
return package.releases.limit(5)
|
||||||
else:
|
else:
|
||||||
return [rel for rel in package.releases if rel.approved]
|
return package.releases.filter_by(approved=True).limit(5)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/packages/<author>/<name>/")
|
@bp.route("/packages/<author>/<name>/")
|
||||||
@is_package_page
|
@is_package_page
|
||||||
def package_page(package):
|
def view(package):
|
||||||
clearNotifications(package.getDetailsURL())
|
clearNotifications(package.getDetailsURL())
|
||||||
|
|
||||||
alternatives = None
|
alternatives = None
|
||||||
@@ -147,9 +150,9 @@ def package_page(package):
|
|||||||
threads=threads.all())
|
threads=threads.all())
|
||||||
|
|
||||||
|
|
||||||
@app.route("/packages/<author>/<name>/download/")
|
@bp.route("/packages/<author>/<name>/download/")
|
||||||
@is_package_page
|
@is_package_page
|
||||||
def package_download_page(package):
|
def download(package):
|
||||||
release = package.getDownloadRelease()
|
release = package.getDownloadRelease()
|
||||||
|
|
||||||
if release is None:
|
if release is None:
|
||||||
@@ -160,13 +163,18 @@ def package_download_page(package):
|
|||||||
flash("No download available.", "error")
|
flash("No download available.", "error")
|
||||||
return redirect(package.getDetailsURL())
|
return redirect(package.getDetailsURL())
|
||||||
else:
|
else:
|
||||||
|
PackageRelease.query.filter_by(id=release.id).update({
|
||||||
|
"downloads": PackageRelease.downloads + 1
|
||||||
|
})
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
return redirect(release.url, code=302)
|
return redirect(release.url, code=302)
|
||||||
|
|
||||||
|
|
||||||
class PackageForm(FlaskForm):
|
class PackageForm(FlaskForm):
|
||||||
name = StringField("Name (Technical)", [InputRequired(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
|
name = StringField("Name (Technical)", [InputRequired(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
|
||||||
title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 50)])
|
title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 50)])
|
||||||
shortDesc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
|
short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
|
||||||
desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
|
desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
|
||||||
type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
|
type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
|
||||||
license = QuerySelectField("License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
license = QuerySelectField("License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||||
@@ -175,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)
|
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()])
|
harddep_str = StringField("Hard Dependencies", [Optional()])
|
||||||
softdep_str = StringField("Soft Dependencies", [Optional()])
|
softdep_str = StringField("Soft Dependencies", [Optional()])
|
||||||
repo = StringField("VCS Repository URL", [Optional(), URL()])
|
repo = StringField("VCS Repository URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||||
website = StringField("Website URL", [Optional(), URL()])
|
website = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||||
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()])
|
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||||
forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
|
forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
|
||||||
submit = SubmitField("Save")
|
submit = SubmitField("Save")
|
||||||
|
|
||||||
@app.route("/packages/new/", methods=["GET", "POST"])
|
@bp.route("/packages/new/", methods=["GET", "POST"])
|
||||||
@app.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
|
@bp.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def create_edit_package_page(author=None, name=None):
|
def create_edit(author=None, name=None):
|
||||||
package = None
|
package = None
|
||||||
form = None
|
form = None
|
||||||
if author is None:
|
if author is None:
|
||||||
@@ -196,11 +204,11 @@ def create_edit_package_page(author=None, name=None):
|
|||||||
author = User.query.filter_by(username=author).first()
|
author = User.query.filter_by(username=author).first()
|
||||||
if author is None:
|
if author is None:
|
||||||
flash("Unable to find that user", "error")
|
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):
|
if not author.checkPerm(current_user, Permission.CHANGE_AUTHOR):
|
||||||
flash("Permission denied", "error")
|
flash("Permission denied", "error")
|
||||||
return redirect(url_for("create_edit_package_page"))
|
return redirect(url_for("packages.create_edit"))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
package = getPackageByInfo(author, name)
|
package = getPackageByInfo(author, name)
|
||||||
@@ -233,17 +241,26 @@ def create_edit_package_page(author=None, name=None):
|
|||||||
Package.query.filter_by(name=form["name"].data, author_id=author.id).delete()
|
Package.query.filter_by(name=form["name"].data, author_id=author.id).delete()
|
||||||
else:
|
else:
|
||||||
flash("Package already exists!", "error")
|
flash("Package already exists!", "error")
|
||||||
return redirect(url_for("create_edit_package_page"))
|
return redirect(url_for("packages.create_edit"))
|
||||||
|
|
||||||
package = Package()
|
package = Package()
|
||||||
package.author = author
|
package.author = author
|
||||||
wasNew = True
|
wasNew = True
|
||||||
|
|
||||||
|
elif package.approved and package.name != form.name.data and \
|
||||||
|
not package.checkPerm(current_user, Permission.CHANGE_NAME):
|
||||||
|
flash("Unable to change package name", "danger")
|
||||||
|
return redirect(url_for("packages.create_edit", author=author, name=name))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
triggerNotif(package.author, current_user,
|
triggerNotif(package.author, current_user,
|
||||||
"{} edited".format(package.title), package.getDetailsURL())
|
"{} edited".format(package.title), package.getDetailsURL())
|
||||||
|
|
||||||
form.populate_obj(package) # copy to row
|
form.populate_obj(package) # copy to row
|
||||||
|
|
||||||
|
if package.type== PackageType.TXP:
|
||||||
|
package.license = package.media_license
|
||||||
|
|
||||||
mpackage_cache = {}
|
mpackage_cache = {}
|
||||||
package.provides.clear()
|
package.provides.clear()
|
||||||
mpackages = MetaPackage.SpecToList(form.provides_str.data, mpackage_cache)
|
mpackages = MetaPackage.SpecToList(form.provides_str.data, mpackage_cache)
|
||||||
@@ -274,7 +291,7 @@ def create_edit_package_page(author=None, name=None):
|
|||||||
next_url = package.getDetailsURL()
|
next_url = package.getDetailsURL()
|
||||||
if wasNew and package.repo is not None:
|
if wasNew and package.repo is not None:
|
||||||
task = importRepoScreenshot.delay(package.id)
|
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):
|
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)
|
next_url = url_for("flatpage", path="help/wtfpl", r=next_url)
|
||||||
@@ -291,10 +308,10 @@ def create_edit_package_page(author=None, name=None):
|
|||||||
packages=package_query.all(), \
|
packages=package_query.all(), \
|
||||||
mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).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
|
@login_required
|
||||||
@is_package_page
|
@is_package_page
|
||||||
def approve_package_page(package):
|
def approve(package):
|
||||||
if not package.checkPerm(current_user, Permission.APPROVE_NEW):
|
if not package.checkPerm(current_user, Permission.APPROVE_NEW):
|
||||||
flash("You don't have permission to do that.", "error")
|
flash("You don't have permission to do that.", "error")
|
||||||
|
|
||||||
@@ -315,10 +332,10 @@ def approve_package_page(package):
|
|||||||
return redirect(package.getDetailsURL())
|
return redirect(package.getDetailsURL())
|
||||||
|
|
||||||
|
|
||||||
@app.route("/packages/<author>/<name>/remove/", methods=["GET", "POST"])
|
@bp.route("/packages/<author>/<name>/remove/", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@is_package_page
|
@is_package_page
|
||||||
def remove_package_page(package):
|
def remove(package):
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return render_template("packages/remove.html", package=package)
|
return render_template("packages/remove.html", package=package)
|
||||||
|
|
||||||
@@ -329,7 +346,7 @@ def remove_package_page(package):
|
|||||||
|
|
||||||
package.soft_deleted = True
|
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,
|
triggerNotif(package.author, current_user,
|
||||||
"{} deleted".format(package.title), url)
|
"{} deleted".format(package.title), url)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@@ -17,10 +17,12 @@
|
|||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
from flask_user 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.models import *
|
||||||
from app.tasks.importtasks import makeVCSRelease
|
from app.tasks.importtasks import makeVCSRelease
|
||||||
|
|
||||||
from app.utils import *
|
from app.utils import *
|
||||||
|
|
||||||
from celery import uuid
|
from celery import uuid
|
||||||
@@ -54,7 +56,7 @@ class CreatePackageReleaseForm(FlaskForm):
|
|||||||
class EditPackageReleaseForm(FlaskForm):
|
class EditPackageReleaseForm(FlaskForm):
|
||||||
title = StringField("Title", [InputRequired(), Length(1, 30)])
|
title = StringField("Title", [InputRequired(), Length(1, 30)])
|
||||||
url = StringField("URL", [URL])
|
url = StringField("URL", [URL])
|
||||||
task_id = StringField("Task ID")
|
task_id = StringField("Task ID", filters = [lambda x: x or None])
|
||||||
approved = BooleanField("Is Approved")
|
approved = BooleanField("Is Approved")
|
||||||
min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
|
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)
|
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)
|
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||||
submit = SubmitField("Save")
|
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
|
@login_required
|
||||||
@is_package_page
|
@is_package_page
|
||||||
def create_release_page(package):
|
def create_release(package):
|
||||||
if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
|
if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
|
||||||
return redirect(package.getDetailsURL())
|
return redirect(package.getDetailsURL())
|
||||||
|
|
||||||
@@ -94,9 +96,9 @@ def create_release_page(package):
|
|||||||
triggerNotif(package.author, current_user, msg, rel.getEditURL())
|
triggerNotif(package.author, current_user, msg, rel.getEditURL())
|
||||||
db.session.commit()
|
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:
|
else:
|
||||||
uploadedPath = doFileUpload(form.fileUpload.data, ["zip"], "a zip file")
|
uploadedPath = doFileUpload(form.fileUpload.data, "zip", "a zip file")
|
||||||
if uploadedPath is not None:
|
if uploadedPath is not None:
|
||||||
rel = PackageRelease()
|
rel = PackageRelease()
|
||||||
rel.package = package
|
rel.package = package
|
||||||
@@ -104,6 +106,7 @@ def create_release_page(package):
|
|||||||
rel.url = uploadedPath
|
rel.url = uploadedPath
|
||||||
rel.min_rel = form["min_rel"].data.getActual()
|
rel.min_rel = form["min_rel"].data.getActual()
|
||||||
rel.max_rel = form["max_rel"].data.getActual()
|
rel.max_rel = form["max_rel"].data.getActual()
|
||||||
|
rel.approve(current_user)
|
||||||
db.session.add(rel)
|
db.session.add(rel)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -114,27 +117,30 @@ def create_release_page(package):
|
|||||||
|
|
||||||
return render_template("packages/release_new.html", package=package, form=form)
|
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
|
@is_package_page
|
||||||
def download_release_page(package, id):
|
def download_release(package, id):
|
||||||
release = PackageRelease.query.get(id)
|
release = PackageRelease.query.get(id)
|
||||||
if release is None or release.package != package:
|
if release is None or release.package != package:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
if release is None:
|
ip = request.headers.get("X-Forwarded-For") or request.remote_addr
|
||||||
if "application/zip" in request.accept_mimetypes and \
|
if ip is not None:
|
||||||
not "text/html" in request.accept_mimetypes:
|
key = make_download_key(ip, release.package)
|
||||||
return "", 204
|
if not has_key(key):
|
||||||
else:
|
set_key(key, "true")
|
||||||
flash("No download available.", "error")
|
|
||||||
return redirect(package.getDetailsURL())
|
|
||||||
else:
|
|
||||||
return redirect(release.url, code=300)
|
|
||||||
|
|
||||||
@app.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
|
PackageRelease.query.filter_by(id=release.id).update({
|
||||||
|
"downloads": PackageRelease.downloads + 1
|
||||||
|
})
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return redirect(release.url, code=300)
|
||||||
|
|
||||||
|
@bp.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@is_package_page
|
@is_package_page
|
||||||
def edit_release_page(package, id):
|
def edit_release(package, id):
|
||||||
release = PackageRelease.query.get(id)
|
release = PackageRelease.query.get(id)
|
||||||
if release is None or release.package != package:
|
if release is None or release.package != package:
|
||||||
abort(404)
|
abort(404)
|
||||||
@@ -158,7 +164,7 @@ def edit_release_page(package, id):
|
|||||||
if package.checkPerm(current_user, Permission.CHANGE_RELEASE_URL):
|
if package.checkPerm(current_user, Permission.CHANGE_RELEASE_URL):
|
||||||
release.url = form["url"].data
|
release.url = form["url"].data
|
||||||
release.task_id = form["task_id"].data
|
release.task_id = form["task_id"].data
|
||||||
if release.task_id.strip() == "":
|
if release.task_id is not None:
|
||||||
release.task_id = None
|
release.task_id = None
|
||||||
|
|
||||||
if canApprove:
|
if canApprove:
|
||||||
@@ -184,10 +190,10 @@ class BulkReleaseForm(FlaskForm):
|
|||||||
submit = SubmitField("Update")
|
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
|
@login_required
|
||||||
@is_package_page
|
@is_package_page
|
||||||
def bulk_change_release_page(package):
|
def bulk_change_release(package):
|
||||||
if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
|
if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
|
||||||
return redirect(package.getDetailsURL())
|
return redirect(package.getDetailsURL())
|
||||||
|
|
||||||
@@ -17,9 +17,10 @@
|
|||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
from flask_user 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 app.utils import *
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
@@ -39,23 +40,24 @@ class EditScreenshotForm(FlaskForm):
|
|||||||
delete = BooleanField("Delete")
|
delete = BooleanField("Delete")
|
||||||
submit = SubmitField("Save")
|
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
|
@login_required
|
||||||
@is_package_page
|
@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):
|
if not package.checkPerm(current_user, Permission.ADD_SCREENSHOTS):
|
||||||
return redirect(package.getDetailsURL())
|
return redirect(package.getDetailsURL())
|
||||||
|
|
||||||
# Initial form class from post data and default data
|
# Initial form class from post data and default data
|
||||||
form = CreateScreenshotForm()
|
form = CreateScreenshotForm()
|
||||||
if request.method == "POST" and form.validate():
|
if request.method == "POST" and form.validate():
|
||||||
uploadedPath = doFileUpload(form.fileUpload.data, ["png", "jpg", "jpeg"],
|
uploadedPath = doFileUpload(form.fileUpload.data, "image",
|
||||||
"a PNG or JPG image file")
|
"a PNG or JPG image file")
|
||||||
if uploadedPath is not None:
|
if uploadedPath is not None:
|
||||||
ss = PackageScreenshot()
|
ss = PackageScreenshot()
|
||||||
ss.package = package
|
ss.package = package
|
||||||
ss.title = form["title"].data or "Untitled"
|
ss.title = form["title"].data or "Untitled"
|
||||||
ss.url = uploadedPath
|
ss.url = uploadedPath
|
||||||
|
ss.approved = package.checkPerm(current_user, Permission.APPROVE_SCREENSHOT)
|
||||||
db.session.add(ss)
|
db.session.add(ss)
|
||||||
|
|
||||||
msg = "{}: Screenshot added {}" \
|
msg = "{}: Screenshot added {}" \
|
||||||
@@ -66,10 +68,10 @@ def create_screenshot_page(package, id=None):
|
|||||||
|
|
||||||
return render_template("packages/screenshot_new.html", package=package, form=form)
|
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
|
@login_required
|
||||||
@is_package_page
|
@is_package_page
|
||||||
def edit_screenshot_page(package, id):
|
def edit_screenshot(package, id):
|
||||||
screenshot = PackageScreenshot.query.get(id)
|
screenshot = PackageScreenshot.query.get(id)
|
||||||
if screenshot is None or screenshot.package != package:
|
if screenshot is None or screenshot.package != package:
|
||||||
abort(404)
|
abort(404)
|
||||||
@@ -18,28 +18,28 @@
|
|||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
import flask_menu as menu
|
import flask_menu as menu
|
||||||
from app import app, csrf
|
from app import csrf
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app.tasks import celery, TaskError
|
from app.tasks import celery, TaskError
|
||||||
from app.tasks.importtasks import getMeta
|
from app.tasks.importtasks import getMeta
|
||||||
from app.utils import shouldReturnJson
|
from app.utils import shouldReturnJson
|
||||||
# from celery.result import AsyncResult
|
|
||||||
|
|
||||||
from app.utils import *
|
from app.utils import *
|
||||||
|
|
||||||
|
bp = Blueprint("tasks", __name__)
|
||||||
|
|
||||||
@csrf.exempt
|
@csrf.exempt
|
||||||
@app.route("/tasks/getmeta/new/", methods=["POST"])
|
@bp.route("/tasks/getmeta/new/", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def new_getmeta_page():
|
def start_getmeta():
|
||||||
author = request.args.get("author")
|
author = request.args.get("author")
|
||||||
author = current_user.forums_username if author is None else author
|
author = current_user.forums_username if author is None else author
|
||||||
aresult = getMeta.delay(request.args.get("url"), author)
|
aresult = getMeta.delay(request.args.get("url"), author)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"poll_url": url_for("check_task", id=aresult.id),
|
"poll_url": url_for("tasks.check", id=aresult.id),
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.route("/tasks/<id>/")
|
@bp.route("/tasks/<id>/")
|
||||||
def check_task(id):
|
def check(id):
|
||||||
result = celery.AsyncResult(id)
|
result = celery.AsyncResult(id)
|
||||||
status = result.status
|
status = result.status
|
||||||
traceback = result.traceback
|
traceback = result.traceback
|
||||||
@@ -16,8 +16,10 @@
|
|||||||
|
|
||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
|
|
||||||
|
bp = Blueprint("threads", __name__)
|
||||||
|
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
from app import app
|
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app.utils import triggerNotif, clearNotifications
|
from app.utils import triggerNotif, clearNotifications
|
||||||
|
|
||||||
@@ -27,17 +29,17 @@ from flask_wtf import FlaskForm
|
|||||||
from wtforms import *
|
from wtforms import *
|
||||||
from wtforms.validators import *
|
from wtforms.validators import *
|
||||||
|
|
||||||
@app.route("/threads/")
|
@bp.route("/threads/")
|
||||||
def threads_page():
|
def list_all():
|
||||||
query = Thread.query
|
query = Thread.query
|
||||||
if not Permission.SEE_THREAD.check(current_user):
|
if not Permission.SEE_THREAD.check(current_user):
|
||||||
query = query.filter_by(private=False)
|
query = query.filter_by(private=False)
|
||||||
return render_template("threads/list.html", threads=query.all())
|
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
|
@login_required
|
||||||
def thread_subscribe_page(id):
|
def subscribe(id):
|
||||||
thread = Thread.query.get(id)
|
thread = Thread.query.get(id)
|
||||||
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||||
abort(404)
|
abort(404)
|
||||||
@@ -49,12 +51,12 @@ def thread_subscribe_page(id):
|
|||||||
thread.watchers.append(current_user)
|
thread.watchers.append(current_user)
|
||||||
db.session.commit()
|
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
|
@login_required
|
||||||
def thread_unsubscribe_page(id):
|
def unsubscribe(id):
|
||||||
thread = Thread.query.get(id)
|
thread = Thread.query.get(id)
|
||||||
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||||
abort(404)
|
abort(404)
|
||||||
@@ -66,12 +68,12 @@ def thread_unsubscribe_page(id):
|
|||||||
else:
|
else:
|
||||||
flash("Not subscribed to thread", "success")
|
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"])
|
@bp.route("/threads/<int:id>/", methods=["GET", "POST"])
|
||||||
def thread_page(id):
|
def view(id):
|
||||||
clearNotifications(url_for("thread_page", id=id))
|
clearNotifications(url_for("threads.view", id=id))
|
||||||
|
|
||||||
thread = Thread.query.get(id)
|
thread = Thread.query.get(id)
|
||||||
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||||
@@ -85,7 +87,7 @@ def thread_page(id):
|
|||||||
if package:
|
if package:
|
||||||
return redirect(package.getDetailsURL())
|
return redirect(package.getDetailsURL())
|
||||||
else:
|
else:
|
||||||
return redirect(url_for("home_page"))
|
return redirect(url_for("homepage.home"))
|
||||||
|
|
||||||
if len(comment) <= 500 and len(comment) > 3:
|
if len(comment) <= 500 and len(comment) > 3:
|
||||||
reply = ThreadReply()
|
reply = ThreadReply()
|
||||||
@@ -106,11 +108,11 @@ def thread_page(id):
|
|||||||
|
|
||||||
for user in thread.watchers:
|
for user in thread.watchers:
|
||||||
if user != current_user:
|
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()
|
db.session.commit()
|
||||||
|
|
||||||
return redirect(url_for("thread_page", id=id))
|
return redirect(url_for("threads.view", id=id))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
flash("Comment needs to be between 3 and 500 characters.")
|
flash("Comment needs to be between 3 and 500 characters.")
|
||||||
@@ -124,9 +126,9 @@ class ThreadForm(FlaskForm):
|
|||||||
private = BooleanField("Private")
|
private = BooleanField("Private")
|
||||||
submit = SubmitField("Open Thread")
|
submit = SubmitField("Open Thread")
|
||||||
|
|
||||||
@app.route("/threads/new/", methods=["GET", "POST"])
|
@bp.route("/threads/new/", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def new_thread_page():
|
def new():
|
||||||
form = ThreadForm(formdata=request.form)
|
form = ThreadForm(formdata=request.form)
|
||||||
|
|
||||||
package = None
|
package = None
|
||||||
@@ -140,7 +142,7 @@ def new_thread_page():
|
|||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
def_is_private = request.args.get("private") or False
|
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
|
def_is_private = True
|
||||||
allow_change = package and package.approved
|
allow_change = package and package.approved
|
||||||
is_review_thread = package and not 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
|
# Check that user can make the thread
|
||||||
if not package.checkPerm(current_user, Permission.CREATE_THREAD):
|
if not package.checkPerm(current_user, Permission.CREATE_THREAD):
|
||||||
flash("Unable to create thread!", "error")
|
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
|
# Only allow creating one thread when not approved
|
||||||
elif is_review_thread and package.review_thread is not None:
|
elif is_review_thread and package.review_thread is not None:
|
||||||
flash("A review thread already exists!", "error")
|
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():
|
elif not current_user.canOpenThreadRL():
|
||||||
flash("Please wait before opening another thread", "danger")
|
flash("Please wait before opening another thread", "danger")
|
||||||
@@ -161,7 +163,7 @@ def new_thread_page():
|
|||||||
if package:
|
if package:
|
||||||
return redirect(package.getDetailsURL())
|
return redirect(package.getDetailsURL())
|
||||||
else:
|
else:
|
||||||
return redirect(url_for("home_page"))
|
return redirect(url_for("homepage.home"))
|
||||||
|
|
||||||
# Set default values
|
# Set default values
|
||||||
elif request.method == "GET":
|
elif request.method == "GET":
|
||||||
@@ -197,16 +199,16 @@ def new_thread_page():
|
|||||||
notif_msg = None
|
notif_msg = None
|
||||||
if package is not None:
|
if package is not None:
|
||||||
notif_msg = "New thread '{}' on package {}".format(thread.title, package.title)
|
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:
|
else:
|
||||||
notif_msg = "New thread '{}'".format(thread.title)
|
notif_msg = "New thread '{}'".format(thread.title)
|
||||||
|
|
||||||
for user in User.query.filter(User.rank >= UserRank.EDITOR).all():
|
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()
|
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)
|
return render_template("threads/new.html", form=form, allow_private_change=allow_change, package=package)
|
||||||
@@ -16,7 +16,8 @@
|
|||||||
|
|
||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
from app import app
|
|
||||||
|
bp = Blueprint("thumbnails", __name__)
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -57,7 +58,7 @@ def resize_and_crop(img_path, modified_path, size):
|
|||||||
img.save(modified_path)
|
img.save(modified_path)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/thumbnails/<int:level>/<img>")
|
@bp.route("/thumbnails/<int:level>/<img>")
|
||||||
def make_thumbnail(img, level):
|
def make_thumbnail(img, level):
|
||||||
if level > len(ALLOWED_RESOLUTIONS) or level <= 0:
|
if level > len(ALLOWED_RESOLUTIONS) or level <= 0:
|
||||||
abort(403)
|
abort(403)
|
||||||
@@ -14,23 +14,25 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
import flask_menu as menu
|
import flask_menu as menu
|
||||||
from app import app
|
|
||||||
from app.models import *
|
from app.models import *
|
||||||
|
from app.querybuilder import QueryBuilder
|
||||||
|
from app.utils import get_int_or_abort
|
||||||
|
|
||||||
@app.route("/todo/")
|
bp = Blueprint("todo", __name__)
|
||||||
|
|
||||||
|
@bp.route("/todo/", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def todo_page():
|
def view():
|
||||||
canApproveNew = Permission.APPROVE_NEW.check(current_user)
|
canApproveNew = Permission.APPROVE_NEW.check(current_user)
|
||||||
canApproveRel = Permission.APPROVE_RELEASE.check(current_user)
|
canApproveRel = Permission.APPROVE_RELEASE.check(current_user)
|
||||||
canApproveScn = Permission.APPROVE_SCREENSHOT.check(current_user)
|
canApproveScn = Permission.APPROVE_SCREENSHOT.check(current_user)
|
||||||
|
|
||||||
packages = None
|
packages = None
|
||||||
if canApproveNew:
|
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
|
releases = None
|
||||||
if canApproveRel:
|
if canApproveRel:
|
||||||
@@ -40,60 +42,61 @@ def todo_page():
|
|||||||
if canApproveScn:
|
if canApproveScn:
|
||||||
screenshots = PackageScreenshot.query.filter_by(approved=False).all()
|
screenshots = PackageScreenshot.query.filter_by(approved=False).all()
|
||||||
|
|
||||||
|
if not canApproveNew and not canApproveRel and not canApproveScn:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
topics_to_add = ForumTopic.query \
|
if request.method == "POST":
|
||||||
|
if request.form["action"] == "screenshots_approve_all":
|
||||||
|
if not canApproveScn:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
PackageScreenshot.query.update({ "approved": True })
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for("todo.view"))
|
||||||
|
else:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
topic_query = ForumTopic.query \
|
||||||
|
.filter_by(discarded=False)
|
||||||
|
|
||||||
|
total_topics = topic_query.count()
|
||||||
|
topics_to_add = topic_query \
|
||||||
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
|
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
|
||||||
.filter_by(discarded=False) \
|
|
||||||
.count()
|
.count()
|
||||||
|
|
||||||
return render_template("todo/list.html", title="Reports and Work Queue",
|
return render_template("todo/list.html", title="Reports and Work Queue",
|
||||||
packages=packages, releases=releases, screenshots=screenshots,
|
packages=packages, releases=releases, screenshots=screenshots,
|
||||||
canApproveNew=canApproveNew, canApproveRel=canApproveRel, canApproveScn=canApproveScn,
|
canApproveNew=canApproveNew, canApproveRel=canApproveRel, canApproveScn=canApproveScn,
|
||||||
topics_to_add=topics_to_add)
|
topics_to_add=topics_to_add, total_topics=total_topics)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/todo/topics/")
|
@bp.route("/todo/topics/")
|
||||||
@login_required
|
@login_required
|
||||||
def todo_topics_page():
|
def topics():
|
||||||
query = ForumTopic.query
|
qb = QueryBuilder(request.args)
|
||||||
|
qb.setSortIfNone("date")
|
||||||
show_discarded = request.args.get("show_discarded") == "True"
|
query = qb.buildTopicQuery()
|
||||||
if not show_discarded:
|
|
||||||
query = query.filter_by(discarded=False)
|
|
||||||
|
|
||||||
total = query.count()
|
|
||||||
|
|
||||||
query = query.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
|
|
||||||
|
|
||||||
sort_by = request.args.get("sort")
|
|
||||||
if sort_by == "name":
|
|
||||||
query = query.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title))
|
|
||||||
elif sort_by == "views":
|
|
||||||
query = query.order_by(db.desc(ForumTopic.views))
|
|
||||||
elif sort_by is None or sort_by == "date":
|
|
||||||
query = query.order_by(db.asc(ForumTopic.created_at))
|
|
||||||
sort_by = "date"
|
|
||||||
|
|
||||||
|
tmp_q = ForumTopic.query
|
||||||
|
if not qb.show_discarded:
|
||||||
|
tmp_q = tmp_q.filter_by(discarded=False)
|
||||||
|
total = tmp_q.count()
|
||||||
topic_count = query.count()
|
topic_count = query.count()
|
||||||
|
|
||||||
search = request.args.get("q")
|
page = get_int_or_abort(request.args.get("page"), 1)
|
||||||
if search is not None and search.strip() != "":
|
num = get_int_or_abort(request.args.get("n"), 100)
|
||||||
query = query.filter(ForumTopic.title.ilike('%' + search + '%'))
|
|
||||||
|
|
||||||
page = int(request.args.get("page") or 1)
|
|
||||||
num = int(request.args.get("n") or 100)
|
|
||||||
if num > 100 and not current_user.rank.atLeast(UserRank.EDITOR):
|
if num > 100 and not current_user.rank.atLeast(UserRank.EDITOR):
|
||||||
num = 100
|
num = 100
|
||||||
|
|
||||||
query = query.paginate(page, num, True)
|
query = query.paginate(page, num, True)
|
||||||
next_url = url_for("todo_topics_page", page=query.next_num, query=search, \
|
next_url = url_for("todo.topics", page=query.next_num, query=qb.search, \
|
||||||
show_discarded=show_discarded, n=num, sort=sort_by) \
|
show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
|
||||||
if query.has_next else None
|
if query.has_next else None
|
||||||
prev_url = url_for("todo_topics_page", page=query.prev_num, query=search, \
|
prev_url = url_for("todo.topics", page=query.prev_num, query=qb.search, \
|
||||||
show_discarded=show_discarded, n=num, sort=sort_by) \
|
show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
|
||||||
if query.has_prev else None
|
if query.has_prev else None
|
||||||
|
|
||||||
return render_template("todo/topics.html", topics=query.items, total=total, \
|
return render_template("todo/topics.html", topics=query.items, total=total, \
|
||||||
topic_count=topic_count, query=search, show_discarded=show_discarded, \
|
topic_count=topic_count, query=qb.search, show_discarded=qb.show_discarded, \
|
||||||
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, \
|
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, \
|
||||||
n=num, sort_by=sort_by)
|
n=num, sort_by=qb.order_by)
|
||||||
5
app/blueprints/users/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint("users", __name__)
|
||||||
|
|
||||||
|
from . import githublogin, profile
|
||||||
@@ -21,15 +21,16 @@ from flask_login import login_user, logout_user
|
|||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
import flask_menu as menu
|
import flask_menu as menu
|
||||||
from flask_github import GitHub
|
from flask_github import GitHub
|
||||||
from app import app, github
|
from . import bp
|
||||||
|
from app import github
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app.utils import loginUser
|
from app.utils import loginUser
|
||||||
|
|
||||||
@app.route("/user/github/start/")
|
@bp.route("/user/github/start/")
|
||||||
def github_signin_page():
|
def github_signin():
|
||||||
return github.authorize("")
|
return github.authorize("")
|
||||||
|
|
||||||
@app.route("/user/github/callback/")
|
@bp.route("/user/github/callback/")
|
||||||
@github.authorized_handler
|
@github.authorized_handler
|
||||||
def github_authorized(oauth_token):
|
def github_authorized(oauth_token):
|
||||||
next_url = request.args.get("next")
|
next_url = request.args.get("next")
|
||||||
@@ -53,21 +54,21 @@ def github_authorized(oauth_token):
|
|||||||
current_user.github_username = username
|
current_user.github_username = username
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Linked github to account", "success")
|
flash("Linked github to account", "success")
|
||||||
return redirect(url_for("home_page"))
|
return redirect(url_for("homepage.home"))
|
||||||
else:
|
else:
|
||||||
flash("Github account is already associated with another user", "danger")
|
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
|
# If not logged in, log in
|
||||||
else:
|
else:
|
||||||
if userByGithub is None:
|
if userByGithub is None:
|
||||||
flash("Unable to find an account for that Github user", "error")
|
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):
|
elif loginUser(userByGithub):
|
||||||
if current_user.password is None:
|
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:
|
else:
|
||||||
return redirect(next_url or url_for("home_page"))
|
return redirect(next_url or url_for("homepage.home"))
|
||||||
else:
|
else:
|
||||||
flash("Authorization failed [err=gh-login-failed]", "danger")
|
flash("Authorization failed [err=gh-login-failed]", "danger")
|
||||||
return redirect(url_for("user.login"))
|
return redirect(url_for("user.login"))
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
from flask_login import login_user, logout_user
|
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 app.models import *
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import *
|
from wtforms import *
|
||||||
@@ -31,19 +32,21 @@ from app.tasks.phpbbparser import getProfile
|
|||||||
# Define the User profile form
|
# Define the User profile form
|
||||||
class UserProfileForm(FlaskForm):
|
class UserProfileForm(FlaskForm):
|
||||||
display_name = StringField("Display name", [Optional(), Length(2, 20)])
|
display_name = StringField("Display name", [Optional(), Length(2, 20)])
|
||||||
email = StringField("Email", [Optional(), Email()])
|
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)
|
rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER)
|
||||||
submit = SubmitField("Save")
|
submit = SubmitField("Save")
|
||||||
|
|
||||||
@app.route("/users/", methods=["GET"])
|
|
||||||
@login_required
|
@bp.route("/users/", methods=["GET"])
|
||||||
def user_list_page():
|
def list_all():
|
||||||
users = User.query.order_by(db.desc(User.rank), db.asc(User.display_name)).all()
|
users = User.query.order_by(db.desc(User.rank), db.asc(User.display_name)).all()
|
||||||
return render_template("users/list.html", users=users)
|
return render_template("users/list.html", users=users)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/users/<username>/", methods=["GET", "POST"])
|
@bp.route("/users/<username>/", methods=["GET", "POST"])
|
||||||
def user_profile_page(username):
|
def profile(username):
|
||||||
user = User.query.filter_by(username=username).first()
|
user = User.query.filter_by(username=username).first()
|
||||||
if not user:
|
if not user:
|
||||||
abort(404)
|
abort(404)
|
||||||
@@ -60,6 +63,8 @@ def user_profile_page(username):
|
|||||||
# Copy form fields to user_profile fields
|
# Copy form fields to user_profile fields
|
||||||
if user.checkPerm(current_user, Permission.CHANGE_DNAME):
|
if user.checkPerm(current_user, Permission.CHANGE_DNAME):
|
||||||
user.display_name = form["display_name"].data
|
user.display_name = form["display_name"].data
|
||||||
|
user.website_url = form["website_url"].data
|
||||||
|
user.donate_url = form["donate_url"].data
|
||||||
|
|
||||||
if user.checkPerm(current_user, Permission.CHANGE_RANK):
|
if user.checkPerm(current_user, Permission.CHANGE_RANK):
|
||||||
newRank = form["rank"].data
|
newRank = form["rank"].data
|
||||||
@@ -74,20 +79,20 @@ def user_profile_page(username):
|
|||||||
token = randomString(32)
|
token = randomString(32)
|
||||||
|
|
||||||
ver = UserEmailVerification()
|
ver = UserEmailVerification()
|
||||||
ver.user = user
|
ver.user = user
|
||||||
ver.token = token
|
ver.token = token
|
||||||
ver.email = newEmail
|
ver.email = newEmail
|
||||||
db.session.add(ver)
|
db.session.add(ver)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
task = sendVerifyEmail.delay(newEmail, token)
|
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
|
# Save user_profile
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# Redirect to home page
|
# 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)
|
packages = user.packages.filter_by(soft_deleted=False)
|
||||||
if not current_user.is_authenticated or (user != current_user and not current_user.canAccessTodoList()):
|
if not current_user.is_authenticated or (user != current_user and not current_user.canAccessTodoList()):
|
||||||
@@ -103,11 +108,11 @@ def user_profile_page(username):
|
|||||||
.all()
|
.all()
|
||||||
|
|
||||||
# Process GET or invalid POST
|
# 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)
|
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
|
@login_required
|
||||||
def user_check(username):
|
def user_check(username):
|
||||||
user = User.query.filter_by(username=username).first()
|
user = User.query.filter_by(username=username).first()
|
||||||
@@ -121,9 +126,9 @@ def user_check(username):
|
|||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
task = checkForumAccount.delay(user.forums_username)
|
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):
|
class SendEmailForm(FlaskForm):
|
||||||
@@ -132,14 +137,14 @@ class SendEmailForm(FlaskForm):
|
|||||||
submit = SubmitField("Send")
|
submit = SubmitField("Send")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/users/<username>/email/", methods=["GET", "POST"])
|
@bp.route("/users/<username>/email/", methods=["GET", "POST"])
|
||||||
@rank_required(UserRank.MODERATOR)
|
@rank_required(UserRank.MODERATOR)
|
||||||
def send_email_page(username):
|
def send_email(username):
|
||||||
user = User.query.filter_by(username=username).first()
|
user = User.query.filter_by(username=username).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
abort(404)
|
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:
|
if user.email is None:
|
||||||
flash("User has no email address!", "error")
|
flash("User has no email address!", "error")
|
||||||
@@ -150,7 +155,7 @@ def send_email_page(username):
|
|||||||
text = form.text.data
|
text = form.text.data
|
||||||
html = markdown(text)
|
html = markdown(text)
|
||||||
task = sendEmailRaw.delay([user.email], form.subject.data, text, html)
|
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)
|
return render_template("users/send_email.html", form=form)
|
||||||
|
|
||||||
@@ -158,13 +163,13 @@ def send_email_page(username):
|
|||||||
|
|
||||||
class SetPasswordForm(FlaskForm):
|
class SetPasswordForm(FlaskForm):
|
||||||
email = StringField("Email", [Optional(), Email()])
|
email = StringField("Email", [Optional(), Email()])
|
||||||
password = PasswordField("New password", [InputRequired(), Length(2, 20)])
|
password = PasswordField("New password", [InputRequired(), Length(2, 100)])
|
||||||
password2 = PasswordField("Verify password", [InputRequired(), Length(2, 20)])
|
password2 = PasswordField("Verify password", [InputRequired(), Length(2, 100)])
|
||||||
submit = SubmitField("Save")
|
submit = SubmitField("Save")
|
||||||
|
|
||||||
@app.route("/user/set-password/", methods=["GET", "POST"])
|
@bp.route("/user/set-password/", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def set_password_page():
|
def set_password():
|
||||||
if current_user.password is not None:
|
if current_user.password is not None:
|
||||||
return redirect(url_for("user.change_password"))
|
return redirect(url_for("user.change_password"))
|
||||||
|
|
||||||
@@ -204,17 +209,17 @@ def set_password_page():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
task = sendVerifyEmail.delay(newEmail, token)
|
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:
|
else:
|
||||||
return redirect(url_for("user_profile_page", username=current_user.username))
|
return redirect(url_for("users.profile", username=current_user.username))
|
||||||
else:
|
else:
|
||||||
flash("Passwords do not match", "error")
|
flash("Passwords do not match", "error")
|
||||||
|
|
||||||
return render_template("users/set_password.html", form=form, optional=request.args.get("optional"))
|
return render_template("users/set_password.html", form=form, optional=request.args.get("optional"))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/user/claim/", methods=["GET", "POST"])
|
@bp.route("/user/claim/", methods=["GET", "POST"])
|
||||||
def user_claim_page():
|
def claim():
|
||||||
username = request.args.get("username")
|
username = request.args.get("username")
|
||||||
if username is None:
|
if username is None:
|
||||||
username = ""
|
username = ""
|
||||||
@@ -223,16 +228,16 @@ def user_claim_page():
|
|||||||
user = User.query.filter_by(forums_username=username).first()
|
user = User.query.filter_by(forums_username=username).first()
|
||||||
if user and user.rank.atLeast(UserRank.NEW_MEMBER):
|
if user and user.rank.atLeast(UserRank.NEW_MEMBER):
|
||||||
flash("User has already been claimed", "error")
|
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":
|
elif user is None and method == "github":
|
||||||
flash("Unable to get Github username for user", "error")
|
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:
|
elif user is None:
|
||||||
flash("Unable to find that user", "error")
|
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":
|
if user is not None and method == "github":
|
||||||
return redirect(url_for("github_signin_page"))
|
return redirect(url_for("users.github_signin"))
|
||||||
|
|
||||||
token = None
|
token = None
|
||||||
if "forum_token" in session:
|
if "forum_token" in session:
|
||||||
@@ -249,12 +254,12 @@ def user_claim_page():
|
|||||||
flash("Invalid username", "error")
|
flash("Invalid username", "error")
|
||||||
elif ctype == "github":
|
elif ctype == "github":
|
||||||
task = checkForumAccount.delay(username)
|
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":
|
elif ctype == "forum":
|
||||||
user = User.query.filter_by(forums_username=username).first()
|
user = User.query.filter_by(forums_username=username).first()
|
||||||
if user is not None and user.rank.atLeast(UserRank.NEW_MEMBER):
|
if user is not None and user.rank.atLeast(UserRank.NEW_MEMBER):
|
||||||
flash("That user has already been claimed!", "error")
|
flash("That user has already been claimed!", "error")
|
||||||
return redirect(url_for("user_claim_page"))
|
return redirect(url_for("users.claim"))
|
||||||
|
|
||||||
# Get signature
|
# Get signature
|
||||||
sig = None
|
sig = None
|
||||||
@@ -263,7 +268,7 @@ def user_claim_page():
|
|||||||
sig = profile.signature
|
sig = profile.signature
|
||||||
except IOError:
|
except IOError:
|
||||||
flash("Unable to get forum signature - does the user exist?", "error")
|
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
|
# Look for key
|
||||||
if token in sig:
|
if token in sig:
|
||||||
@@ -274,21 +279,21 @@ def user_claim_page():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
if loginUser(user):
|
if loginUser(user):
|
||||||
return redirect(url_for("set_password_page"))
|
return redirect(url_for("users.set_password"))
|
||||||
else:
|
else:
|
||||||
flash("Unable to login as user", "error")
|
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:
|
else:
|
||||||
flash("Could not find the key in your signature!", "error")
|
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:
|
else:
|
||||||
flash("Unknown claim type", "error")
|
flash("Unknown claim type", "error")
|
||||||
|
|
||||||
return render_template("users/claim.html", username=username, key=token)
|
return render_template("users/claim.html", username=username, key=token)
|
||||||
|
|
||||||
@app.route("/users/verify/")
|
@bp.route("/users/verify/")
|
||||||
def verify_email_page():
|
def verify_email():
|
||||||
token = request.args.get("token")
|
token = request.args.get("token")
|
||||||
ver = UserEmailVerification.query.filter_by(token=token).first()
|
ver = UserEmailVerification.query.filter_by(token=token).first()
|
||||||
if ver is None:
|
if ver is None:
|
||||||
@@ -299,6 +304,6 @@ def verify_email_page():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
if current_user.is_authenticated:
|
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:
|
else:
|
||||||
return redirect(url_for("home_page"))
|
return redirect(url_for("homepage.home"))
|
||||||
@@ -2,4 +2,5 @@ title: Help
|
|||||||
|
|
||||||
* [Package Tags](package_tags)
|
* [Package Tags](package_tags)
|
||||||
* [Ranks and Permissions](ranks_permissions)
|
* [Ranks and Permissions](ranks_permissions)
|
||||||
|
* [Content Ratings and Flags](content_flags)
|
||||||
* [Reporting Content](reporting)
|
* [Reporting Content](reporting)
|
||||||
|
|||||||
26
app/flatpages/help/content_flags.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
title: Content Flags
|
||||||
|
|
||||||
|
Content flags allow you to hide content based on your preferences.
|
||||||
|
The filtering is done server-side, which means that you don't need to update
|
||||||
|
your client to use new flags.
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
* `nonfree` - can be used to hide packages which do not qualify as
|
||||||
|
'free software', as defined by the Free Software Foundation.
|
||||||
|
* A content rating, given below.
|
||||||
|
|
||||||
|
|
||||||
|
## Ratings
|
||||||
|
|
||||||
|
Content ratings aren't currently supported by ContentDB.
|
||||||
|
Instead, mature content isn't allowed at all for now.
|
||||||
|
|
||||||
|
In the future, more mature content will be allowed but labelled with
|
||||||
|
content ratings which may contain the following:
|
||||||
|
|
||||||
|
* android_default - meta-rating which includes gore and drugs.
|
||||||
|
* desktop_default - meta-rating which won't include anything for now.
|
||||||
|
* gore - more than just blood
|
||||||
|
* drugs
|
||||||
|
* swearing
|
||||||
@@ -10,36 +10,45 @@ ContentDB is for the community, and as such listings should be useful to the
|
|||||||
community. To help with this, there are a few rules to improve the quality of
|
community. To help with this, there are a few rules to improve the quality of
|
||||||
the listings and to combat abuse.
|
the listings and to combat abuse.
|
||||||
|
|
||||||
* No inappropriate content.
|
* No inappropriate content. <sup>2.1</sup>
|
||||||
* Content must be playable/useful, but not necessarily finished.
|
* Content must be playable/useful, but not necessarily finished. <sup>2.2</sup>
|
||||||
* Don't use the name of another mod unless your mod is a fork or reimplementation.
|
* Don't use the name of another mod unless your mod is a fork or reimplementation. <sup>3</sup>
|
||||||
* Licenses must allow derivatives, redistribution, and must not discriminate.
|
* Licenses must allow derivatives, redistribution, and must not discriminate. <sup>4</sup>
|
||||||
* Don't put promotions are advertisements in package listings, except for
|
* Don't put promotions or advertisements in package listings, except for
|
||||||
donation and personal website links which are permitted in the long description.
|
donation and personal website links which are permitted in the
|
||||||
|
long description. <sup>5</sup>
|
||||||
|
* The ContentDB admin reserves the right to remove packages for any reason,
|
||||||
|
including ones not covered by this document, and to ban users who abuse
|
||||||
|
this service. <sup>1</sup>
|
||||||
|
|
||||||
|
|
||||||
## 1. General
|
## 1. General
|
||||||
|
|
||||||
It is not permitted to submit abusive, obscene, vulgar, slanderous, hateful,
|
|
||||||
threatening, sexually-orientated or any material that may violate any laws be
|
|
||||||
it of your country, the country where "Content DB” is hosted or International Law.
|
|
||||||
|
|
||||||
The ContentDB admin reserves the right to remove packages for any reason,
|
The ContentDB admin reserves the right to remove packages for any reason,
|
||||||
including ones not covered by this document, and to ban users who abuse this service.
|
including ones not covered by this document, and to ban users who abuse this service.
|
||||||
|
|
||||||
Also see the [help page on tags](/help/package_tags/).
|
Also see the [help page on tags](/help/package_tags/).
|
||||||
|
|
||||||
|
|
||||||
## 2. Accepted Content and State of Completion
|
## 2. Accepted Content
|
||||||
|
|
||||||
|
### 2.1. Acceptable Content
|
||||||
|
|
||||||
|
Sexually-orientated content is not permitted.
|
||||||
|
|
||||||
|
Mature content, including that relating to drugs, excessive gore, violence, or
|
||||||
|
excessive horror, is not currently permitted - but will be in the future.
|
||||||
|
|
||||||
The submission of malware is strictly prohibited. This includes software which
|
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
|
does not do as it advertises, for example if it posts telemetry without stating
|
||||||
clearly that it does in the package meta.
|
clearly that it does in the package meta.
|
||||||
|
|
||||||
|
### 2.2. State of Completion
|
||||||
|
|
||||||
ContentDB should only currently contain playable content - content which is
|
ContentDB should only currently contain playable content - content which is
|
||||||
sufficiently complete to be useful to end users. It's fine to add stuff which
|
sufficiently complete to be useful to end users. It's fine to add stuff which
|
||||||
is still a work in progress (WIP) as long as it adds sufficient value -
|
is still a work in progress (WIP) as long as it adds sufficient value -
|
||||||
Mineclone 2 is a good example of a WIP package which may break between releases
|
MineClone 2 is a good example of a WIP package which may break between releases
|
||||||
but still has value. Note that this doesn't mean that you should add a thing
|
but still has value. Note that this doesn't mean that you should add a thing
|
||||||
you started working on yesterday, it's worth adding all the basic stuff to
|
you started working on yesterday, it's worth adding all the basic stuff to
|
||||||
make your package useful.
|
make your package useful.
|
||||||
@@ -116,15 +125,15 @@ Public domain is not a valid license in many countries, please use CC0 or MIT in
|
|||||||
|
|
||||||
## 5. Promotions and Advertisements (inc. asking for donations)
|
## 5. Promotions and Advertisements (inc. asking for donations)
|
||||||
|
|
||||||
Any information other than the long description - including screenshots - must
|
You may note place any promotions or advertisements in any meta data including
|
||||||
not contain any promotions or advertisements. This includes asking for donations,
|
screensthos. This includes asking for donations, promoting online shops,
|
||||||
promoting online shops, or linking to personal websites and social media.
|
or linking to personal websites and social media. Please instead use the
|
||||||
|
fields provided on your user profile page to place links to websites and
|
||||||
|
donation pages.
|
||||||
|
|
||||||
ContentDB is for the community. We may remove any promotions if we feel that
|
ContentDB is for the community. We may remove any promotions if we feel that
|
||||||
they're inappropriate.
|
they're inappropriate.
|
||||||
|
|
||||||
Paid promotions are not allowed at all, anywhere.
|
|
||||||
|
|
||||||
|
|
||||||
## 6. Reporting Violations
|
## 6. Reporting Violations
|
||||||
|
|
||||||
|
|||||||
146
app/models.py
@@ -15,18 +15,29 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from flask import Flask, url_for
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
from flask_migrate import Migrate
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
from app import app, gravatar
|
|
||||||
from sqlalchemy.orm import validates
|
|
||||||
from flask_user import login_required, UserManager, UserMixin, SQLAlchemyAdapter
|
|
||||||
import enum, datetime
|
import enum, datetime
|
||||||
|
|
||||||
|
from app import app, gravatar
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from flask import Flask, url_for
|
||||||
|
from flask_sqlalchemy import SQLAlchemy, BaseQuery
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
from flask_user import login_required, UserManager, UserMixin, SQLAlchemyAdapter
|
||||||
|
from sqlalchemy.orm import validates
|
||||||
|
from sqlalchemy_searchable import SearchQueryMixin
|
||||||
|
from sqlalchemy_utils.types import TSVectorType
|
||||||
|
from sqlalchemy_searchable import make_searchable
|
||||||
|
|
||||||
|
|
||||||
# Initialise database
|
# Initialise database
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
migrate = Migrate(app, db)
|
migrate = Migrate(app, db)
|
||||||
|
make_searchable(db.metadata)
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleQuery(BaseQuery, SearchQueryMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UserRank(enum.Enum):
|
class UserRank(enum.Enum):
|
||||||
@@ -65,6 +76,7 @@ class Permission(enum.Enum):
|
|||||||
APPROVE_CHANGES = "APPROVE_CHANGES"
|
APPROVE_CHANGES = "APPROVE_CHANGES"
|
||||||
DELETE_PACKAGE = "DELETE_PACKAGE"
|
DELETE_PACKAGE = "DELETE_PACKAGE"
|
||||||
CHANGE_AUTHOR = "CHANGE_AUTHOR"
|
CHANGE_AUTHOR = "CHANGE_AUTHOR"
|
||||||
|
CHANGE_NAME = "CHANGE_NAME"
|
||||||
MAKE_RELEASE = "MAKE_RELEASE"
|
MAKE_RELEASE = "MAKE_RELEASE"
|
||||||
ADD_SCREENSHOTS = "ADD_SCREENSHOTS"
|
ADD_SCREENSHOTS = "ADD_SCREENSHOTS"
|
||||||
APPROVE_SCREENSHOT = "APPROVE_SCREENSHOT"
|
APPROVE_SCREENSHOT = "APPROVE_SCREENSHOT"
|
||||||
@@ -118,6 +130,10 @@ class User(db.Model, UserMixin):
|
|||||||
active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
|
active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
|
||||||
display_name = db.Column(db.String(100), nullable=False, server_default="")
|
display_name = db.Column(db.String(100), nullable=False, server_default="")
|
||||||
|
|
||||||
|
# Links
|
||||||
|
website_url = db.Column(db.String(255), nullable=True, default=None)
|
||||||
|
donate_url = db.Column(db.String(255), nullable=True, default=None)
|
||||||
|
|
||||||
# Content
|
# Content
|
||||||
notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id")
|
notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id")
|
||||||
|
|
||||||
@@ -197,6 +213,9 @@ class Notification(db.Model):
|
|||||||
url = db.Column(db.String(200), nullable=True)
|
url = db.Column(db.String(200), nullable=True)
|
||||||
|
|
||||||
def __init__(self, us, cau, titl, ur):
|
def __init__(self, us, cau, titl, ur):
|
||||||
|
if len(titl) > 100:
|
||||||
|
title = title[:99] + "…"
|
||||||
|
|
||||||
self.user = us
|
self.user = us
|
||||||
self.causer = cau
|
self.causer = cau
|
||||||
self.title = titl
|
self.title = titl
|
||||||
@@ -246,7 +265,7 @@ class PackageType(enum.Enum):
|
|||||||
class PackagePropertyKey(enum.Enum):
|
class PackagePropertyKey(enum.Enum):
|
||||||
name = "Name"
|
name = "Name"
|
||||||
title = "Title"
|
title = "Title"
|
||||||
shortDesc = "Short Description"
|
short_desc = "Short Description"
|
||||||
desc = "Description"
|
desc = "Description"
|
||||||
type = "Type"
|
type = "Type"
|
||||||
license = "License"
|
license = "License"
|
||||||
@@ -343,19 +362,22 @@ class Dependency(db.Model):
|
|||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Package(db.Model):
|
class Package(db.Model):
|
||||||
|
query_class = ArticleQuery
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
# Basic details
|
# Basic details
|
||||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||||
name = db.Column(db.String(100), nullable=False)
|
name = db.Column(db.String(100), nullable=False)
|
||||||
title = db.Column(db.String(100), nullable=False)
|
title = db.Column(db.Unicode(100), nullable=False)
|
||||||
shortDesc = db.Column(db.String(200), nullable=False)
|
short_desc = db.Column(db.Unicode(200), nullable=False)
|
||||||
desc = db.Column(db.Text, nullable=True)
|
desc = db.Column(db.UnicodeText, nullable=True)
|
||||||
type = db.Column(db.Enum(PackageType))
|
type = db.Column(db.Enum(PackageType))
|
||||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
|
search_vector = db.Column(TSVectorType("title", "short_desc", "desc"))
|
||||||
|
|
||||||
license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
|
license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
|
||||||
license = db.relationship("License", foreign_keys=[license_id])
|
license = db.relationship("License", foreign_keys=[license_id])
|
||||||
media_license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
|
media_license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
|
||||||
@@ -376,7 +398,7 @@ class Package(db.Model):
|
|||||||
forums = db.Column(db.Integer, nullable=True)
|
forums = db.Column(db.Integer, nullable=True)
|
||||||
|
|
||||||
provides = db.relationship("MetaPackage", secondary=provides, lazy="subquery",
|
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])
|
dependencies = db.relationship("Dependency", backref="depender", lazy="dynamic", foreign_keys=[Dependency.depender_id])
|
||||||
|
|
||||||
@@ -403,26 +425,51 @@ class Package(db.Model):
|
|||||||
for e in PackagePropertyKey:
|
for e in PackagePropertyKey:
|
||||||
setattr(self, e.name, getattr(package, e.name))
|
setattr(self, e.name, getattr(package, e.name))
|
||||||
|
|
||||||
def getAsDictionaryShort(self, base_url):
|
def getState(self):
|
||||||
|
if self.approved:
|
||||||
|
return "approved"
|
||||||
|
elif self.review_thread_id:
|
||||||
|
return "thread"
|
||||||
|
elif (self.type == PackageType.GAME or \
|
||||||
|
self.type == PackageType.TXP) and \
|
||||||
|
self.screenshots.count() == 0:
|
||||||
|
return "wip"
|
||||||
|
elif not self.getDownloadRelease():
|
||||||
|
return "wip"
|
||||||
|
elif "Other" in self.license.name or "Other" in self.media_license.name:
|
||||||
|
return "license"
|
||||||
|
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)
|
tnurl = self.getThumbnailURL(1)
|
||||||
|
release = self.getDownloadRelease(version=version, protonum=protonum)
|
||||||
return {
|
return {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"author": self.author.display_name,
|
"author": self.author.display_name,
|
||||||
"short_description": self.shortDesc,
|
"short_description": self.short_desc,
|
||||||
"type": self.type.toName(),
|
"type": self.type.toName(),
|
||||||
"release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
|
"release": release and release.id,
|
||||||
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
|
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
|
||||||
"score": round(self.score * 10) / 10
|
"score": round(self.score * 10) / 10
|
||||||
}
|
}
|
||||||
|
|
||||||
def getAsDictionary(self, base_url):
|
def getAsDictionary(self, base_url, version=None, protonum=None):
|
||||||
tnurl = self.getThumbnailURL(1)
|
tnurl = self.getThumbnailURL(1)
|
||||||
|
release = self.getDownloadRelease(version=version, protonum=protonum)
|
||||||
return {
|
return {
|
||||||
"author": self.author.display_name,
|
"author": self.author.display_name,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"short_description": self.shortDesc,
|
"short_description": self.short_desc,
|
||||||
"desc": self.desc,
|
"desc": self.desc,
|
||||||
"type": self.type.toName(),
|
"type": self.type.toName(),
|
||||||
"created_at": self.created_at,
|
"created_at": self.created_at,
|
||||||
@@ -440,7 +487,7 @@ class Package(db.Model):
|
|||||||
"screenshots": [base_url + ss.url for ss in self.screenshots],
|
"screenshots": [base_url + ss.url for ss in self.screenshots],
|
||||||
|
|
||||||
"url": base_url + self.getDownloadURL(),
|
"url": base_url + self.getDownloadURL(),
|
||||||
"release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
|
"release": release and release.id,
|
||||||
|
|
||||||
"score": round(self.score * 10) / 10
|
"score": round(self.score * 10) / 10
|
||||||
}
|
}
|
||||||
@@ -454,27 +501,27 @@ class Package(db.Model):
|
|||||||
return screenshot.url if screenshot is not None else None
|
return screenshot.url if screenshot is not None else None
|
||||||
|
|
||||||
def getDetailsURL(self):
|
def getDetailsURL(self):
|
||||||
return url_for("package_page",
|
return url_for("packages.view",
|
||||||
author=self.author.username, name=self.name)
|
author=self.author.username, name=self.name)
|
||||||
|
|
||||||
def getEditURL(self):
|
def getEditURL(self):
|
||||||
return url_for("create_edit_package_page",
|
return url_for("packages.create_edit",
|
||||||
author=self.author.username, name=self.name)
|
author=self.author.username, name=self.name)
|
||||||
|
|
||||||
def getApproveURL(self):
|
def getApproveURL(self):
|
||||||
return url_for("approve_package_page",
|
return url_for("packages.approve",
|
||||||
author=self.author.username, name=self.name)
|
author=self.author.username, name=self.name)
|
||||||
|
|
||||||
def getRemoveURL(self):
|
def getRemoveURL(self):
|
||||||
return url_for("remove_package_page",
|
return url_for("packages.remove",
|
||||||
author=self.author.username, name=self.name)
|
author=self.author.username, name=self.name)
|
||||||
|
|
||||||
def getNewScreenshotURL(self):
|
def getNewScreenshotURL(self):
|
||||||
return url_for("create_screenshot_page",
|
return url_for("packages.create_screenshot",
|
||||||
author=self.author.username, name=self.name)
|
author=self.author.username, name=self.name)
|
||||||
|
|
||||||
def getCreateReleaseURL(self):
|
def getCreateReleaseURL(self):
|
||||||
return url_for("create_release_page",
|
return url_for("packages.create_release",
|
||||||
author=self.author.username, name=self.name)
|
author=self.author.username, name=self.name)
|
||||||
|
|
||||||
def getCreateEditRequestURL(self):
|
def getCreateEditRequestURL(self):
|
||||||
@@ -482,20 +529,36 @@ class Package(db.Model):
|
|||||||
author=self.author.username, name=self.name)
|
author=self.author.username, name=self.name)
|
||||||
|
|
||||||
def getBulkReleaseURL(self):
|
def getBulkReleaseURL(self):
|
||||||
return url_for("bulk_change_release_page",
|
return url_for("packages.bulk_change_release",
|
||||||
author=self.author.username, name=self.name)
|
author=self.author.username, name=self.name)
|
||||||
|
|
||||||
def getDownloadURL(self):
|
def getDownloadURL(self):
|
||||||
return url_for("package_download_page",
|
return url_for("packages.download",
|
||||||
author=self.author.username, name=self.name)
|
author=self.author.username, name=self.name)
|
||||||
|
|
||||||
def getDownloadRelease(self):
|
def getDownloadRelease(self, version=None, protonum=None):
|
||||||
|
if version is None and protonum is not None:
|
||||||
|
version = MinetestRelease.query.filter(MinetestRelease.protocol >= int(protonum)).first()
|
||||||
|
if version is not None:
|
||||||
|
version = version.id
|
||||||
|
else:
|
||||||
|
version = 10000000
|
||||||
|
|
||||||
|
|
||||||
for rel in self.releases:
|
for rel in self.releases:
|
||||||
if rel.approved:
|
if rel.approved and (version is None or
|
||||||
|
((rel.min_rel is None or rel.min_rel_id <= version) and \
|
||||||
|
(rel.max_rel is None or rel.max_rel_id >= version))):
|
||||||
return rel
|
return rel
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def getDownloadCount(self):
|
||||||
|
counter = 0
|
||||||
|
for release in self.releases:
|
||||||
|
counter += release.downloads
|
||||||
|
return counter
|
||||||
|
|
||||||
def checkPerm(self, user, perm):
|
def checkPerm(self, user, perm):
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
@@ -520,6 +583,10 @@ class Package(db.Model):
|
|||||||
else:
|
else:
|
||||||
return user.rank.atLeast(UserRank.EDITOR)
|
return user.rank.atLeast(UserRank.EDITOR)
|
||||||
|
|
||||||
|
# Anyone can change the package name when not approved, but only editors when approved
|
||||||
|
elif perm == Permission.CHANGE_NAME:
|
||||||
|
return not self.approved or user.rank.atLeast(UserRank.EDITOR)
|
||||||
|
|
||||||
# Editors can change authors and approve new packages
|
# Editors can change authors and approve new packages
|
||||||
elif perm == Permission.APPROVE_NEW or perm == Permission.CHANGE_AUTHOR:
|
elif perm == Permission.APPROVE_NEW or perm == Permission.CHANGE_AUTHOR:
|
||||||
return user.rank.atLeast(UserRank.EDITOR)
|
return user.rank.atLeast(UserRank.EDITOR)
|
||||||
@@ -639,6 +706,7 @@ class PackageRelease(db.Model):
|
|||||||
approved = db.Column(db.Boolean, nullable=False, default=False)
|
approved = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
task_id = db.Column(db.String(37), nullable=True)
|
task_id = db.Column(db.String(37), nullable=True)
|
||||||
commit_hash = db.Column(db.String(41), nullable=True, default=None)
|
commit_hash = db.Column(db.String(41), nullable=True, default=None)
|
||||||
|
downloads = db.Column(db.Integer, nullable=False, default=0)
|
||||||
|
|
||||||
min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
|
min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
|
||||||
min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])
|
min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])
|
||||||
@@ -648,13 +716,13 @@ class PackageRelease(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
def getEditURL(self):
|
def getEditURL(self):
|
||||||
return url_for("edit_release_page",
|
return url_for("packages.edit_release",
|
||||||
author=self.package.author.username,
|
author=self.package.author.username,
|
||||||
name=self.package.name,
|
name=self.package.name,
|
||||||
id=self.id)
|
id=self.id)
|
||||||
|
|
||||||
def getDownloadURL(self):
|
def getDownloadURL(self):
|
||||||
return url_for("download_release_page",
|
return url_for("packages.download_release",
|
||||||
author=self.package.author.username,
|
author=self.package.author.username,
|
||||||
name=self.package.name,
|
name=self.package.name,
|
||||||
id=self.id)
|
id=self.id)
|
||||||
@@ -663,6 +731,16 @@ class PackageRelease(db.Model):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.releaseDate = datetime.datetime.now()
|
self.releaseDate = datetime.datetime.now()
|
||||||
|
|
||||||
|
def approve(self, user):
|
||||||
|
if self.package.approved and \
|
||||||
|
not self.package.checkPerm(user, Permission.APPROVE_RELEASE):
|
||||||
|
return False
|
||||||
|
|
||||||
|
assert(self.task_id is None and self.url is not None and self.url != "")
|
||||||
|
|
||||||
|
self.approved = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class PackageReview(db.Model):
|
class PackageReview(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -680,7 +758,7 @@ class PackageScreenshot(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
def getEditURL(self):
|
def getEditURL(self):
|
||||||
return url_for("edit_screenshot_page",
|
return url_for("packages.edit_screenshot",
|
||||||
author=self.package.author.username,
|
author=self.package.author.username,
|
||||||
name=self.package.name,
|
name=self.package.name,
|
||||||
id=self.id)
|
id=self.id)
|
||||||
@@ -802,11 +880,11 @@ class Thread(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
def getSubscribeURL(self):
|
def getSubscribeURL(self):
|
||||||
return url_for("thread_subscribe_page",
|
return url_for("threads.subscribe",
|
||||||
id=self.id)
|
id=self.id)
|
||||||
|
|
||||||
def getUnsubscribeURL(self):
|
def getUnsubscribeURL(self):
|
||||||
return url_for("thread_unsubscribe_page",
|
return url_for("threads.unsubscribe",
|
||||||
id=self.id)
|
id=self.id)
|
||||||
|
|
||||||
def checkPerm(self, user, perm):
|
def checkPerm(self, user, perm):
|
||||||
|
|||||||
BIN
app/public/favicon-128.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
app/public/favicon-16.png
Normal file
|
After Width: | Height: | Size: 846 B |
BIN
app/public/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 260 B After Width: | Height: | Size: 159 B |
|
Before Width: | Height: | Size: 342 B After Width: | Height: | Size: 232 B |
|
Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 276 B After Width: | Height: | Size: 165 B |
|
Before Width: | Height: | Size: 275 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 340 B After Width: | Height: | Size: 231 B |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 4.0 KiB |
9
app/public/static/opensearch.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
||||||
|
<ShortName>ContentDB</ShortName>
|
||||||
|
<LongName>ContentDB</LongName>
|
||||||
|
<InputEncoding>UTF-8</InputEncoding>
|
||||||
|
<Description>Search mods, games, and textures for Minetest.</Description>
|
||||||
|
<Tags>Minetest Mod Game Subgame Search</Tags>
|
||||||
|
<Url type="text/html" method="get" template="https://content.minetest.net/packages?q={searchTerms}"/>
|
||||||
|
</OpenSearchDescription>
|
||||||
@@ -35,10 +35,10 @@ $(function() {
|
|||||||
setField("#repo", result.repo || repoURL);
|
setField("#repo", result.repo || repoURL);
|
||||||
setField("#issueTracker", result.issueTracker);
|
setField("#issueTracker", result.issueTracker);
|
||||||
setField("#desc", result.description);
|
setField("#desc", result.description);
|
||||||
setField("#shortDesc", result.short_description);
|
setField("#short_desc", result.short_description);
|
||||||
setField("#harddep_str", result.depends);
|
setField("#harddep_str", result.depends);
|
||||||
setField("#softdep_str", result.optional_depends);
|
setField("#softdep_str", result.optional_depends);
|
||||||
setField("#shortDesc", result.short_description);
|
setField("#short_desc", result.short_description);
|
||||||
setField("#forums", result.forumId);
|
setField("#forums", result.forumId);
|
||||||
if (result.type && result.type.length > 2) {
|
if (result.type && result.type.length > 2) {
|
||||||
$("#type").val(result.type);
|
$("#type").val(result.type);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ $(function() {
|
|||||||
It's obvious that this adds something to Minetest,
|
It's obvious that this adds something to Minetest,
|
||||||
there's no need to use phrases such as \"adds X to the game\".`
|
there's no need to use phrases such as \"adds X to the game\".`
|
||||||
|
|
||||||
$("#shortDesc").on("change paste keyup", function() {
|
$("#short_desc").on("change paste keyup", function() {
|
||||||
var val = $(this).val().toLowerCase();
|
var val = $(this).val().toLowerCase();
|
||||||
if (val.indexOf("minetest") >= 0 || val.indexOf("mod") >= 0 ||
|
if (val.indexOf("minetest") >= 0 || val.indexOf("mod") >= 0 ||
|
||||||
val.indexOf("modpack") >= 0 || val.indexOf("mod pack") >= 0) {
|
val.indexOf("modpack") >= 0 || val.indexOf("mod pack") >= 0) {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 4.6 KiB |
@@ -1,5 +1,5 @@
|
|||||||
from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease
|
from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease
|
||||||
from .utils import isNo
|
from .utils import isNo, isYes
|
||||||
from sqlalchemy.sql.expression import func
|
from sqlalchemy.sql.expression import func
|
||||||
from flask import abort
|
from flask import abort
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
@@ -19,20 +19,42 @@ class QueryBuilder:
|
|||||||
if len(types) > 0:
|
if len(types) > 0:
|
||||||
title = ", ".join([type.value + "s" for type in types])
|
title = ", ".join([type.value + "s" for type in types])
|
||||||
|
|
||||||
|
hide_flags = args.getlist("hide")
|
||||||
|
|
||||||
self.title = title
|
self.title = title
|
||||||
self.types = types
|
self.types = types
|
||||||
self.search = args.get("q")
|
self.search = args.get("q")
|
||||||
self.random = "random" in args
|
self.random = "random" in args
|
||||||
self.lucky = self.random or "lucky" in args
|
self.lucky = self.random or "lucky" in args
|
||||||
self.hide_nonfree = isNo(args.get("nonfree"))
|
self.hide_nonfree = "nonfree" in hide_flags
|
||||||
self.limit = 1 if self.lucky else None
|
self.limit = 1 if self.lucky else None
|
||||||
self.order_by = args.get("sort") or "score"
|
self.order_by = args.get("sort")
|
||||||
self.order_dir = args.get("order") or "desc"
|
self.order_dir = args.get("order") or "desc"
|
||||||
self.protocol_version = args.get("protocol_version")
|
self.protocol_version = args.get("protocol_version")
|
||||||
|
|
||||||
|
self.show_discarded = isYes(args.get("show_discarded"))
|
||||||
|
self.show_added = args.get("show_added")
|
||||||
|
if self.show_added is not None:
|
||||||
|
self.show_added = isYes(self.show_added)
|
||||||
|
|
||||||
if self.search is not None and self.search.strip() == "":
|
if self.search is not None and self.search.strip() == "":
|
||||||
self.search = None
|
self.search = None
|
||||||
|
|
||||||
|
def setSortIfNone(self, name):
|
||||||
|
if self.order_by is None:
|
||||||
|
self.order_by = name
|
||||||
|
|
||||||
|
def getMinetestVersion(self):
|
||||||
|
if not self.protocol_version:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.protocol_version = int(self.protocol_version)
|
||||||
|
version = MinetestRelease.query.filter(MinetestRelease.protocol>=self.protocol_version).first()
|
||||||
|
if version is not None:
|
||||||
|
return version.id
|
||||||
|
else:
|
||||||
|
return 10000000
|
||||||
|
|
||||||
def buildPackageQuery(self):
|
def buildPackageQuery(self):
|
||||||
query = Package.query.filter_by(soft_deleted=False, approved=True)
|
query = Package.query.filter_by(soft_deleted=False, approved=True)
|
||||||
|
|
||||||
@@ -40,13 +62,13 @@ class QueryBuilder:
|
|||||||
query = query.filter(Package.type.in_(self.types))
|
query = query.filter(Package.type.in_(self.types))
|
||||||
|
|
||||||
if self.search:
|
if self.search:
|
||||||
query = query.filter(Package.title.ilike('%' + self.search + '%'))
|
query = query.search(self.search)
|
||||||
|
|
||||||
if self.random:
|
if self.random:
|
||||||
query = query.order_by(func.random())
|
query = query.order_by(func.random())
|
||||||
else:
|
else:
|
||||||
to_order = None
|
to_order = None
|
||||||
if self.order_by == "score":
|
if self.order_by is None or self.order_by == "score":
|
||||||
to_order = Package.score
|
to_order = Package.score
|
||||||
elif self.order_by == "created_at":
|
elif self.order_by == "created_at":
|
||||||
to_order = Package.created_at
|
to_order = Package.created_at
|
||||||
@@ -67,14 +89,9 @@ class QueryBuilder:
|
|||||||
query = query.filter(Package.media_license.has(License.is_foss == True))
|
query = query.filter(Package.media_license.has(License.is_foss == True))
|
||||||
|
|
||||||
if self.protocol_version:
|
if self.protocol_version:
|
||||||
self.protocol_version = int(self.protocol_version)
|
version = self.getMinetestVersion()
|
||||||
version = MinetestRelease.query.filter(MinetestRelease.protocol>=self.protocol_version).first()
|
|
||||||
if version is not None:
|
|
||||||
version = version.id
|
|
||||||
else:
|
|
||||||
version = 10000000
|
|
||||||
|
|
||||||
query = query.join(Package.releases) \
|
query = query.join(Package.releases) \
|
||||||
|
.filter(PackageRelease.approved==True) \
|
||||||
.filter(or_(PackageRelease.min_rel_id==None, PackageRelease.min_rel_id<=version)) \
|
.filter(or_(PackageRelease.min_rel_id==None, PackageRelease.min_rel_id<=version)) \
|
||||||
.filter(or_(PackageRelease.max_rel_id==None, PackageRelease.max_rel_id>=version))
|
.filter(or_(PackageRelease.max_rel_id==None, PackageRelease.max_rel_id>=version))
|
||||||
|
|
||||||
@@ -83,18 +100,31 @@ class QueryBuilder:
|
|||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def buildTopicQuery(self):
|
def buildTopicQuery(self, show_added=False):
|
||||||
topics = ForumTopic.query \
|
query = ForumTopic.query
|
||||||
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
|
|
||||||
.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title))
|
if not self.show_discarded:
|
||||||
|
query = query.filter_by(discarded=False)
|
||||||
|
|
||||||
|
show_added = self.show_added == True or (self.show_added is None and show_added)
|
||||||
|
if not show_added:
|
||||||
|
query = query.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id))
|
||||||
|
|
||||||
|
if self.order_by is None or self.order_by == "name":
|
||||||
|
query = query.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title))
|
||||||
|
elif self.order_by == "views":
|
||||||
|
query = query.order_by(db.desc(ForumTopic.views))
|
||||||
|
elif self.order_by == "date":
|
||||||
|
query = query.order_by(db.asc(ForumTopic.created_at))
|
||||||
|
sort_by = "date"
|
||||||
|
|
||||||
if self.search:
|
if self.search:
|
||||||
topics = topics.filter(ForumTopic.title.ilike('%' + self.search + '%'))
|
query = query.filter(ForumTopic.title.ilike('%' + self.search + '%'))
|
||||||
|
|
||||||
if len(self.types) > 0:
|
if len(self.types) > 0:
|
||||||
topics = topics.filter(ForumTopic.type.in_(self.types))
|
query = query.filter(ForumTopic.type.in_(self.types))
|
||||||
|
|
||||||
if self.limit:
|
if self.limit:
|
||||||
topics = topics.limit(self.limit)
|
query = query.limit(self.limit)
|
||||||
|
|
||||||
return topics
|
return query
|
||||||
|
|||||||
13
app/rediscache.py
Normal 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)
|
||||||
@@ -15,8 +15,6 @@ import codecs
|
|||||||
from flask import *
|
from flask import *
|
||||||
from scss import Scss
|
from scss import Scss
|
||||||
|
|
||||||
from app import app
|
|
||||||
|
|
||||||
def _convert(dir, src, dst):
|
def _convert(dir, src, dst):
|
||||||
original_wd = os.getcwd()
|
original_wd = os.getcwd()
|
||||||
os.chdir(dir)
|
os.chdir(dir)
|
||||||
@@ -31,7 +29,7 @@ def _convert(dir, src, dst):
|
|||||||
outfile.write(output)
|
outfile.write(output)
|
||||||
outfile.close()
|
outfile.close()
|
||||||
|
|
||||||
def _getDirPath(originalPath, create=False):
|
def _getDirPath(app, originalPath, create=False):
|
||||||
path = originalPath
|
path = originalPath
|
||||||
|
|
||||||
if not os.path.isdir(path):
|
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"):
|
def sass(app, inputDir='scss', outputPath='static', force=False, cacheDir="public/static"):
|
||||||
static_url_path = app.static_url_path
|
static_url_path = app.static_url_path
|
||||||
inputDir = _getDirPath(inputDir)
|
inputDir = _getDirPath(app, inputDir)
|
||||||
cacheDir = _getDirPath(cacheDir or outputPath, True)
|
cacheDir = _getDirPath(app, cacheDir or outputPath, True)
|
||||||
|
|
||||||
def _sass(filepath):
|
def _sass(filepath):
|
||||||
sassfile = "%s/%s.scss" % (inputDir, 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")
|
return send_from_directory(cacheDir, filepath + ".css")
|
||||||
|
|
||||||
app.add_url_rule("/%s/<path:filepath>.css" % (outputPath), 'sass', _sass)
|
app.add_url_rule("/%s/<path:filepath>.css" % (outputPath), 'sass', _sass)
|
||||||
|
|
||||||
sass(app)
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# 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 flask_mail import Message
|
||||||
from app import mail
|
from app import mail
|
||||||
from app.tasks import celery
|
from app.tasks import celery
|
||||||
@@ -24,7 +24,18 @@ from app.tasks import celery
|
|||||||
def sendVerifyEmail(newEmail, token):
|
def sendVerifyEmail(newEmail, token):
|
||||||
print("Sending verify email!")
|
print("Sending verify email!")
|
||||||
msg = Message("Verify email address", recipients=[newEmail])
|
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)
|
msg.html = render_template("emails/verify.html", token=token)
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
@@ -33,9 +44,7 @@ def sendEmailRaw(to, subject, text, html):
|
|||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
msg = Message(subject, recipients=to)
|
msg = Message(subject, recipients=to)
|
||||||
|
|
||||||
if text:
|
msg.body = text or html
|
||||||
msg.body = text
|
|
||||||
|
|
||||||
html = html or text
|
html = html or text
|
||||||
msg.html = render_template("emails/base.html", subject=subject, content=html)
|
msg.html = render_template("emails/base.html", subject=subject, content=html)
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|||||||
@@ -99,11 +99,19 @@ def parseTitle(title):
|
|||||||
def getLinksFromModSearch():
|
def getLinksFromModSearch():
|
||||||
links = {}
|
links = {}
|
||||||
|
|
||||||
contents = urllib.request.urlopen("https://krock-works.uk.to/minetest/modList.php").read().decode("utf-8")
|
try:
|
||||||
for x in json.loads(contents):
|
contents = urllib.request.urlopen("https://krock-works.uk.to/minetest/modList.php").read().decode("utf-8")
|
||||||
link = x.get("link")
|
for x in json.loads(contents):
|
||||||
if link is not None:
|
try:
|
||||||
links[int(x["topicId"])] = link
|
link = x.get("link")
|
||||||
|
if link is not None:
|
||||||
|
links[int(x["topicId"])] = link
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except urllib.error.URLError:
|
||||||
|
print("Unable to open krocks mod search!")
|
||||||
|
return links
|
||||||
|
|
||||||
return links
|
return links
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# 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 git import GitCommandError
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from urllib.error import HTTPError
|
from urllib.error import HTTPError
|
||||||
@@ -29,6 +29,10 @@ from app.utils import randomString
|
|||||||
|
|
||||||
class GithubURLMaker:
|
class GithubURLMaker:
|
||||||
def __init__(self, url):
|
def __init__(self, url):
|
||||||
|
self.baseUrl = None
|
||||||
|
self.user = None
|
||||||
|
self.repo = None
|
||||||
|
|
||||||
# Rewrite path
|
# Rewrite path
|
||||||
import re
|
import re
|
||||||
m = re.search("^\/([^\/]+)\/([^\/]+)\/?$", url.path)
|
m = re.search("^\/([^\/]+)\/([^\/]+)\/?$", url.path)
|
||||||
@@ -51,6 +55,9 @@ class GithubURLMaker:
|
|||||||
def getScreenshotURL(self):
|
def getScreenshotURL(self):
|
||||||
return self.baseUrl + "/screenshot.png"
|
return self.baseUrl + "/screenshot.png"
|
||||||
|
|
||||||
|
def getModConfURL(self):
|
||||||
|
return self.baseUrl + "/mod.conf"
|
||||||
|
|
||||||
def getCommitsURL(self, branch):
|
def getCommitsURL(self, branch):
|
||||||
return "https://api.github.com/repos/{}/{}/commits?sha={}" \
|
return "https://api.github.com/repos/{}/{}/commits?sha={}" \
|
||||||
.format(self.user, self.repo, urllib.parse.quote_plus(branch))
|
.format(self.user, self.repo, urllib.parse.quote_plus(branch))
|
||||||
@@ -291,16 +298,23 @@ def cloneRepo(urlstr, ref=None, recursive=False):
|
|||||||
try:
|
try:
|
||||||
gitUrl = generateGitURL(urlstr)
|
gitUrl = generateGitURL(urlstr)
|
||||||
print("Cloning from " + gitUrl)
|
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:
|
if ref is None:
|
||||||
repo.create_head("myhead", ref).checkout()
|
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
|
return gitDir, repo
|
||||||
|
|
||||||
except GitCommandError as e:
|
except GitCommandError as e:
|
||||||
# This is needed to stop the backtrace being weird
|
# This is needed to stop the backtrace being weird
|
||||||
err = e.stderr
|
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: ", "") \
|
raise TaskError(err.replace("stderr: ", "") \
|
||||||
.replace("Cloning into '" + gitDir + "'...", "") \
|
.replace("Cloning into '" + gitDir + "'...", "") \
|
||||||
.strip())
|
.strip())
|
||||||
@@ -339,8 +353,11 @@ def makeVCSReleaseFromGithub(id, branch, release, url):
|
|||||||
raise TaskError("Invalid github repo URL")
|
raise TaskError("Invalid github repo URL")
|
||||||
|
|
||||||
commitsURL = urlmaker.getCommitsURL(branch)
|
commitsURL = urlmaker.getCommitsURL(branch)
|
||||||
contents = urllib.request.urlopen(commitsURL).read().decode("utf-8")
|
try:
|
||||||
commits = json.loads(contents)
|
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]:
|
if len(commits) == 0 or not "sha" in commits[0]:
|
||||||
raise TaskError("No commits found")
|
raise TaskError("No commits found")
|
||||||
@@ -348,7 +365,7 @@ def makeVCSReleaseFromGithub(id, branch, release, url):
|
|||||||
release.url = urlmaker.getCommitDownload(commits[0]["sha"])
|
release.url = urlmaker.getCommitDownload(commits[0]["sha"])
|
||||||
release.task_id = None
|
release.task_id = None
|
||||||
release.commit_hash = commits[0]["sha"]
|
release.commit_hash = commits[0]["sha"]
|
||||||
print(release.url)
|
release.approve(release.package.author)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return release.url
|
return release.url
|
||||||
@@ -379,6 +396,7 @@ def makeVCSRelease(id, branch):
|
|||||||
release.url = "/uploads/" + filename
|
release.url = "/uploads/" + filename
|
||||||
release.task_id = None
|
release.task_id = None
|
||||||
release.commit_hash = repo.head.object.hexsha
|
release.commit_hash = repo.head.object.hexsha
|
||||||
|
release.approve(release.package.author)
|
||||||
print(release.url)
|
print(release.url)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|||||||
22
app/template_filters.py
Normal 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"
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url_for('license_list_page') }}">Back to list</a> |
|
<a href="{{ url_for('admin.license_list') }}">Back to list</a> |
|
||||||
<a href="{{ url_for('createedit_license_page') }}">New License</a>
|
<a href="{{ url_for('admin.create_edit_license') }}">New License</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ Licenses
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url_for('createedit_license_page') }}">New License</a>
|
<a href="{{ url_for('admin.create_edit_license') }}">New License</a>
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for l in licenses %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="{{ url_for('user_list_page') }}">User list</a></li>
|
<li><a href="{{ url_for('users.list_all') }}">User list</a></li>
|
||||||
<li><a href="{{ url_for('tag_list_page') }}">Tag Editor</a></li>
|
<li><a href="{{ url_for('admin.tag_list') }}">Tag Editor</a></li>
|
||||||
<li><a href="{{ url_for('license_list_page') }}">License Editor</a></li>
|
<li><a href="{{ url_for('admin.license_list') }}">License Editor</a></li>
|
||||||
<li><a href="{{ url_for('version_list_page') }}">Version Editor</a></li>
|
<li><a href="{{ url_for('admin.version_list') }}">Version Editor</a></li>
|
||||||
<li><a href="{{ url_for('switch_user_page') }}">Sign in as another user</a></li>
|
<li><a href="{{ url_for('admin.switch_user') }}">Sign in as another user</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="card my-4">
|
<div class="card my-4">
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
<form method="post" action="" class="card-body">
|
<form method="post" action="" class="card-body">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<select name="action">
|
<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="recalcscores">Recalculate package scores</option>
|
||||||
<option value="checkusers">Check forum users</option>
|
<option value="checkusers">Check forum users</option>
|
||||||
<option value="importscreenshots">Import screenshots from VCS</option>
|
<option value="importscreenshots">Import screenshots from VCS</option>
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url_for('tag_list_page') }}">Back to list</a> |
|
<a href="{{ url_for('admin.tag_list') }}">Back to list</a> |
|
||||||
<a href="{{ url_for('createedit_tag_page') }}">New Tag</a>
|
<a href="{{ url_for('admin.create_edit_tag') }}">New Tag</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ Tags
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url_for('createedit_tag_page') }}">New Tag</a>
|
<a href="{{ url_for('admin.create_edit_tag') }}">New Tag</a>
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for t in tags %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url_for('version_list_page') }}">Back to list</a> |
|
<a href="{{ url_for('admin.version_list') }}">Back to list</a> |
|
||||||
<a href="{{ url_for('createedit_version_page') }}">New Version</a>
|
<a href="{{ url_for('admin.create_edit_version') }}">New Version</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ Minetest Versions
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url_for('createedit_version_page') }}">New Version</a>
|
<a href="{{ url_for('admin.create_edit_version') }}">New Version</a>
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for v in versions %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -7,7 +7,11 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
|
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
|
||||||
<link rel="stylesheet" type="text/css" href="/static/bootstrap.css">
|
<link rel="stylesheet" type="text/css" href="/static/bootstrap.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=6">
|
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=7">
|
||||||
|
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
|
||||||
|
<link rel="shortcut icon" href="/favicon-16.png" sizes="16x16">
|
||||||
|
<link rel="icon" href="/favicon-128.png" sizes="128x128">
|
||||||
|
<link rel="icon" href="/favicon-32.png" sizes="32x32">
|
||||||
{% block headextra %}{% endblock %}
|
{% block headextra %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -56,10 +60,10 @@
|
|||||||
</form>
|
</form>
|
||||||
<ul class="navbar-nav ml-auto">
|
<ul class="navbar-nav ml-auto">
|
||||||
{% if current_user.is_authenticated %}
|
{% 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" />
|
<img src="/static/notification{% if current_user.notifications %}_alert{% endif %}.svg" />
|
||||||
</a></li>
|
</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">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle"
|
<a class="nav-link dropdown-toggle"
|
||||||
data-toggle="dropdown"
|
data-toggle="dropdown"
|
||||||
@@ -69,30 +73,30 @@
|
|||||||
|
|
||||||
<ul class="dropdown-menu" role="menu">
|
<ul class="dropdown-menu" role="menu">
|
||||||
<li class="nav-item">
|
<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>
|
||||||
<li class="nav-item">
|
<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>
|
</li>
|
||||||
{% if current_user.canAccessTodoList() %}
|
{% 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('todo.view') }}">{{ _("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('users.list_all') }}">{{ _("User list") }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="nav-item">
|
<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>
|
</li>
|
||||||
{% if current_user.rank == current_user.rank.ADMIN %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% if current_user.rank == current_user.rank.MODERATOR %}
|
{% 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('admin.tag_list') }}">{{ _("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.license_list') }}">{{ _("License Editor") }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('user.logout') }}">Sign out</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('user.logout') }}">{{ _("Sign out") }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a class="nav-link" href="{{ url_for('user.login') }}">Sign in</a></li>
|
<li><a class="nav-link" href="{{ url_for('user.login') }}">{{ _("Sign in") }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,10 +129,18 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<footer class="container footer-copyright my-5 page-footer font-small text-center">
|
<footer class="container footer-copyright my-5 page-footer font-small text-center">
|
||||||
ContentDB © 2018 to <a href="https://rubenwardy.com/">rubenwardy</a> |
|
ContentDB © 2018-9 to <a href="https://rubenwardy.com/">rubenwardy</a> |
|
||||||
<a href="https://github.com/minetest/contentdb">GitHub</a> |
|
<a href="https://github.com/minetest/contentdb">GitHub</a> |
|
||||||
<a href="{{ url_for('flatpage', path='help') }}">Help</a> |
|
<a href="{{ url_for('flatpage', path='help') }}">{{ _("Help") }}</a> |
|
||||||
<a href="{{ url_for('flatpage', path='help/reporting') }}">Report / DMCA</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('users.list_all') }}">{{ _("User List") }}</a>
|
||||||
|
|
||||||
|
{% if debug %}
|
||||||
|
<p style="color: red">
|
||||||
|
DEBUG MODE ENABLED
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="/static/jquery.min.js"></script>
|
<script src="/static/jquery.min.js"></script>
|
||||||
|
|||||||
@@ -16,12 +16,12 @@
|
|||||||
If this was you, then please click this link to verify the address:
|
If this was you, then please click this link to verify the address:
|
||||||
</p>
|
</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
|
Confirm Email Address
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<p style="font-size: 80%;">
|
<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>
|
<p>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ Sign in
|
|||||||
{% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %}
|
{% 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>
|
<h2 class="card-header">{%trans%}Sign in with Github{%endtrans%}</h2>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<a class="btn btn-primary" href="{{ url_for('github_signin_page') }}">GitHub</a>
|
<a class="btn btn-primary" href="{{ url_for('users.github_signin') }}">GitHub</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,7 +72,7 @@ Sign in
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>Create an account using your forum account or email.</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
Welcome
|
{{ _("Welcome") }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scriptextra %}
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
"url": "https://content.minetest.net/",
|
||||||
|
"potentialAction": {
|
||||||
|
"@type": "SearchAction",
|
||||||
|
"target": "https://content.minetest.net/packages?q={search_term_string}",
|
||||||
|
"query-input": "required name=search_term_string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -22,36 +37,36 @@ Welcome
|
|||||||
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
{% 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
|
{{ _("See more") }}
|
||||||
</a>
|
</a>
|
||||||
<h2 class="my-3">Recently Added</h2>
|
<h2 class="my-3">{{ _("Recently Added") }}</h2>
|
||||||
{{ render_pkggrid(new) }}
|
{{ 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
|
{{ _("See more") }}
|
||||||
</a>
|
</a>
|
||||||
<h2 class="my-3">Top Mods</h2>
|
<h2 class="my-3">{{ _("Top Mods") }}</h2>
|
||||||
{{ render_pkggrid(pop_mod) }}
|
{{ 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
|
{{ _("See more") }}
|
||||||
</a>
|
</a>
|
||||||
<h2 class="my-3">Top Games</h2>
|
<h2 class="my-3">{{ _("Top Games") }}</h2>
|
||||||
{{ render_pkggrid(pop_gam) }}
|
{{ 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
|
{{ _("See more") }}
|
||||||
</a>
|
</a>
|
||||||
<h2 class="my-3">Top Texture Packs</h2>
|
<h2 class="my-3">{{ _("Top Texture Packs") }}</h2>
|
||||||
{{ render_pkggrid(pop_txp) }}
|
{{ render_pkggrid(pop_txp) }}
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<small>
|
<small>
|
||||||
CDB has {{ count }} packages available to download.
|
{{ _("CDB has %(count)d packages, with a total of %(downloads)d downloads.", count=count, downloads=downloads) }}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<!-- </main> -->
|
<!-- </main> -->
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{ package.shortDesc }}
|
{{ package.short_desc }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{% for r in thread.replies %}
|
{% for r in thread.replies %}
|
||||||
<li class="row my-2 mx-0">
|
<li class="row my-2 mx-0">
|
||||||
<div class="col-md-1 p-1">
|
<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() }}">
|
<img class="img-responsive user-photo img-thumbnail img-thumbnail-1" src="{{ r.author.getProfilePicURL() }}">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,11 +12,11 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<a class="author {{ r.author.rank.name }}"
|
<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 }}
|
{{ r.author.display_name }}
|
||||||
</a>
|
</a>
|
||||||
<a name="reply-{{ r.id }}" class="text-muted float-right"
|
<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 }}
|
{{ r.created_at | datetime }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user.canCommentRL() %}
|
{% 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() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<textarea class="form-control markdown" required maxlength=500 name="comment"></textarea><br />
|
<textarea class="form-control markdown" required maxlength=500 name="comment"></textarea><br />
|
||||||
<input class="btn btn-primary" type="submit" value="Comment" />
|
<input class="btn btn-primary" type="submit" value="Comment" />
|
||||||
@@ -65,14 +65,14 @@
|
|||||||
{% for t in threads %}
|
{% for t in threads %}
|
||||||
<li {% if list_group %}class="list-group-item"{% endif %}>
|
<li {% if list_group %}class="list-group-item"{% endif %}>
|
||||||
{% if list_group %}
|
{% if list_group %}
|
||||||
<a href="{{ url_for('thread_page', id=t.id) }}">
|
<a href="{{ url_for('threads.view', id=t.id) }}">
|
||||||
{% if t.private %}🔒 {% endif %}
|
{% if t.private %}🔒 {% endif %}
|
||||||
{{ t.title }}
|
{{ t.title }}
|
||||||
by {{ t.author.display_name }}
|
by {{ t.author.display_name }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if t.private %}🔒 {% endif %}
|
{% if t.private %}🔒 {% endif %}
|
||||||
<a href="{{ url_for('thread_page', id=t.id) }}">{{ t.title }}</a>
|
<a href="{{ url_for('threads.view', id=t.id) }}">{{ t.title }}</a>
|
||||||
by {{ t.author.display_name }}
|
by {{ t.author.display_name }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -18,14 +18,14 @@
|
|||||||
{% if topic.wip %}[WIP]{% endif %}
|
{% if topic.wip %}[WIP]{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% if show_author %}
|
{% 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 %}
|
{% endif %}
|
||||||
<td>{{ topic.name or ""}}</td>
|
<td>{{ topic.name or ""}}</td>
|
||||||
<td>{{ topic.created_at | date }}</td>
|
<td>{{ topic.created_at | date }}</td>
|
||||||
<td class="btn-group">
|
<td class="btn-group">
|
||||||
{% if current_user == topic.author or topic.author.checkPerm(current_user, "CHANGE_AUTHOR") %}
|
{% if current_user == topic.author or topic.author.checkPerm(current_user, "CHANGE_AUTHOR") %}
|
||||||
<a class="btn btn-primary"
|
<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
|
Create
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -56,10 +56,10 @@
|
|||||||
{% if topic.wip %}[WIP]{% endif %}
|
{% if topic.wip %}[WIP]{% endif %}
|
||||||
{% if topic.name %}[{{ topic.name }}]{% endif %}
|
{% if topic.name %}[{{ topic.name }}]{% endif %}
|
||||||
{% if show_author %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% if topic.author == current_user or topic.author.checkPerm(current_user, "CHANGE_AUTHOR") %}
|
{% 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 %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Meta Packages
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for meta in mpackages %}
|
{% 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 %}
|
{% else %}
|
||||||
<li><i>No meta packages found.</i></li>
|
<li><i>No meta packages found.</i></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Notifications
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if current_user.notifications %}
|
{% 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="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<input type="submit" value="Clear All" />
|
<input type="submit" value="Clear All" />
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -20,19 +20,19 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Create Package</h1>
|
<h1>{{ _("Create Package") }}</h1>
|
||||||
|
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<a class="float-right btn btn-sm btn-default" href="{{ url_for('flatpage', path='policy_and_guidance') }}">View</a>
|
<a class="float-right btn btn-sm btn-default" href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("View") }}</a>
|
||||||
|
|
||||||
Have you read the Package Inclusion Policy and Guidance yet?
|
{{ _("Have you read the Package Inclusion Policy and Guidance yet?") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<noscript>
|
<noscript>
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
Javascript is needed to improve the user interface, and is needed for features
|
{{ _("Javascript is needed to improve the user interface, and is needed for features
|
||||||
such as finding metadata from git, and autocompletion.<br />
|
such as finding metadata from git, and autocompletion.") }}<br />
|
||||||
Whilst disabled Javascript may work, it is not officially supported.
|
{{ _("Whilst disabled Javascript may work, it is not officially supported.") }}
|
||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
@@ -42,24 +42,32 @@
|
|||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Package</legend>
|
<legend>{{ _("Package") }}</legend>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{ render_field(form.type, class_="pkg_meta col-sm-2") }}
|
{{ render_field(form.type, class_="pkg_meta col-sm-2") }}
|
||||||
{{ render_field(form.title, class_="pkg_meta col-sm-7") }}
|
{{ render_field(form.title, class_="pkg_meta col-sm-7") }}
|
||||||
{{ render_field(form.name, class_="pkg_meta col-sm-3") }}
|
{% if package and package.approved and not package.checkPerm(current_user, "CHANGE_NAME") %}
|
||||||
|
{{ render_field(form.name, class_="pkg_meta col-sm-3", readonly=True) }}
|
||||||
|
{% else %}
|
||||||
|
{{ render_field(form.name, class_="pkg_meta col-sm-3") }}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{{ render_field(form.shortDesc, class_="pkg_meta") }}
|
{{ render_field(form.short_desc, class_="pkg_meta") }}
|
||||||
{{ render_multiselect_field(form.tags, class_="pkg_meta") }}
|
{{ render_multiselect_field(form.tags, class_="pkg_meta") }}
|
||||||
<div class="pkg_meta row">
|
<div class="pkg_meta row">
|
||||||
{{ render_field(form.license, class_="not_txp col-sm-6") }}
|
{{ render_field(form.license, class_="not_txp col-sm-6") }}
|
||||||
{{ render_field(form.media_license, class_="col-sm-6") }}
|
{{ render_field(form.media_license, class_="col-sm-6") }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pkg_meta row">
|
||||||
|
<div class="not_txp col-sm-6"></div>
|
||||||
|
<div class="not_txp col-sm-6">{{ _("If there is no media, set the Media License to the same as the License.") }}</div>
|
||||||
|
</div>
|
||||||
{{ render_field(form.desc, class_="pkg_meta", fieldclass="form-control markdown") }}
|
{{ render_field(form.desc, class_="pkg_meta", fieldclass="form-control markdown") }}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="pkg_meta">
|
<fieldset class="pkg_meta">
|
||||||
<legend class="not_txp">Dependencies</legend>
|
<legend class="not_txp">{{ _("Dependencies") }}</legend>
|
||||||
|
|
||||||
{{ render_mpackage_field(form.provides_str, class_="not_txp", placeholder="Comma separated list") }}
|
{{ render_mpackage_field(form.provides_str, class_="not_txp", placeholder="Comma separated list") }}
|
||||||
{{ render_deps_field(form.harddep_str, class_="not_txp not_game", placeholder="Comma separated list") }}
|
{{ render_deps_field(form.harddep_str, class_="not_txp not_game", placeholder="Comma separated list") }}
|
||||||
@@ -67,30 +75,29 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend class="pkg_meta">Repository and Links</legend>
|
<legend class="pkg_meta">{{ _("Repository and Links") }}</legend>
|
||||||
|
|
||||||
<div class="pkg_wiz_1">
|
<div class="pkg_wiz_1">
|
||||||
<p>Enter the repo URL for the package.
|
<p>{{ _("Enter the repo URL for the package.
|
||||||
If the repo uses git then the metadata will be automatically imported.</p>
|
If the repo uses git then the metadata will be automatically imported.") }}</p>
|
||||||
|
|
||||||
<p>Leave blank if you don't have a repo. Click skip if the import fails.</p>
|
<p>{{ _("Leave blank if you don't have a repo. Click skip if the import fails.") }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ render_field(form.repo, class_="pkg_repo") }}
|
{{ render_field(form.repo, class_="pkg_repo") }}
|
||||||
|
|
||||||
|
|
||||||
<div class="pkg_wiz_1">
|
<div class="pkg_wiz_1">
|
||||||
<a id="pkg_wiz_1_next" class="btn btn-primary">Next (Autoimport)</a>
|
<a id="pkg_wiz_1_next" class="btn btn-primary">{{ _("Next (Autoimport)") }}</a>
|
||||||
<a id="pkg_wiz_1_skip" class="btn btn-default">Skip Autoimport</a>
|
<a id="pkg_wiz_1_skip" class="btn btn-default">{{ _("Skip Autoimport") }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pkg_wiz_2">
|
<div class="pkg_wiz_2">
|
||||||
Importing... (This may take a while)
|
{{ _("Importing... (This may take a while)") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ render_field(form.website, class_="pkg_meta") }}
|
{{ render_field(form.website, class_="pkg_meta") }}
|
||||||
{{ render_field(form.issueTracker, class_="pkg_meta") }}
|
{{ render_field(form.issueTracker, class_="pkg_meta") }}
|
||||||
{{ render_field(form.forums, class_="pkg_meta", placeholder="Tip: paste in a forum topic URL") }}
|
{{ render_field(form.forums, class_="pkg_meta", placeholder=_("Tip: paste in a forum topic URL")) }}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="pkg_meta">{{ render_submit_field(form.submit) }}</div>
|
<div class="pkg_meta">{{ render_submit_field(form.submit) }}</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
{{ render_field(form.type) }}
|
{{ render_field(form.type) }}
|
||||||
{{ render_field(form.name) }}
|
{{ render_field(form.name) }}
|
||||||
{{ render_field(form.title) }}
|
{{ render_field(form.title) }}
|
||||||
{{ render_field(form.shortDesc) }}
|
{{ render_field(form.short_desc) }}
|
||||||
{{ render_field(form.desc) }}
|
{{ render_field(form.desc) }}
|
||||||
{{ render_multiselect_field(form.tags) }}
|
{{ render_multiselect_field(form.tags) }}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
{% for n in range(1, page_max+1) %}
|
{% for n in range(1, page_max+1) %}
|
||||||
<li class="page-item {% if n == page %}active{% endif %}">
|
<li class="page-item {% if n == page %}active{% endif %}">
|
||||||
<a class="page-link"
|
<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 }}
|
{{ n }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if release.task_id %}
|
{% 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") %}
|
{% if package.checkPerm(current_user, "CHANGE_RELEASE_URL") %}
|
||||||
{{ render_field(form.task_id) }}
|
{{ render_field(form.task_id) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
{{ render_field(form.vcsLabel, class_="mt-3") }}
|
{{ render_field(form.vcsLabel, class_="mt-3") }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ render_field(form.fileUpload, fieldclass="form-control-file", class_="mt-3") }}
|
{{ render_field(form.fileUpload, fieldclass="form-control-file", class_="mt-3", accept=".zip") }}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{ render_field(form.min_rel, class_="col-sm-6") }}
|
{{ render_field(form.min_rel, class_="col-sm-6") }}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
{{ render_field(form.title) }}
|
{{ render_field(form.title) }}
|
||||||
{{ render_field(form.fileUpload, fieldclass="form-control-file") }}
|
{{ render_field(form.fileUpload, fieldclass="form-control-file", accept="image/png,image/jpeg") }}
|
||||||
{{ render_submit_field(form.submit) }}
|
{{ render_submit_field(form.submit) }}
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -19,11 +19,12 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
{{ package.shortDesc }}
|
{{ package.short_desc }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="row" style="margin-top: 2rem;">
|
<div class="row" style="margin-top: 2rem;">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
{{ package.getDownloadCount() }} downloads
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group-horizontal col-md-auto">
|
<div class="btn-group-horizontal col-md-auto">
|
||||||
{% if package.repo %}<a class="btn btn-secondary" href="{{ package.repo }}">View Source</a>{% endif %}
|
{% if package.repo %}<a class="btn btn-secondary" href="{{ package.repo }}">View Source</a>{% endif %}
|
||||||
@@ -102,7 +103,7 @@
|
|||||||
|
|
||||||
{% if not review_thread and (package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW")) %}
|
{% if not review_thread and (package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW")) %}
|
||||||
<div class="alert alert-info">
|
<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
|
Privately ask a question or give feedback
|
||||||
<div style="clear:both;"></div>
|
<div style="clear:both;"></div>
|
||||||
@@ -111,11 +112,22 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<aside class="float-right ml-4" style="width: 18rem;">
|
<aside class="float-right ml-4" style="width: 18rem;">
|
||||||
{% if package.getDownloadRelease() %}
|
{% set release = package.getDownloadRelease() %}
|
||||||
<a class="btn btn-download btn-lg btn-block"
|
{% if release %}
|
||||||
|
<a class="btn btn-download btn-lg btn-block" rel="nofollow"
|
||||||
href="{{ package.getDownloadURL() }}" class="btn_green">
|
href="{{ package.getDownloadURL() }}" class="btn_green">
|
||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<p class="text-center m-2" style="font-size: 80%;">
|
||||||
|
{% if release.min_rel and release.max_rel %}
|
||||||
|
Minetest {{ release.min_rel.name }} - {{ release.max_rel.name }}
|
||||||
|
{% elif release.min_rel %}
|
||||||
|
Supports Minetest {{ release.min_rel.name }} and above.
|
||||||
|
{% elif release.max_rel %}
|
||||||
|
Supports Minetest {{ release.max_rel.name }} and below.
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
No download available.
|
No download available.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -160,14 +172,14 @@
|
|||||||
<td>Provides</td>
|
<td>Provides</td>
|
||||||
<td>{% for meta in package.provides %}
|
<td>{% for meta in package.provides %}
|
||||||
<a class="badge badge-primary"
|
<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>
|
{% endfor %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Author</td>
|
<td>Author</td>
|
||||||
<td class="{{ package.author.rank }}">
|
<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 }}
|
{{ package.author.display_name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -205,6 +217,13 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if package.author.donate_url %}
|
||||||
|
<div class="alert alert-secondary">
|
||||||
|
Like {{ package.author.display_name }}'s work?
|
||||||
|
<a href="{{ package.author.donate_url }}" rel="nofollow">Donate now!</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if package.type == package.type.MOD %}
|
{% if package.type == package.type.MOD %}
|
||||||
<div class="card my-4">
|
<div class="card my-4">
|
||||||
<div class="card-header">Dependencies</div>
|
<div class="card-header">Dependencies</div>
|
||||||
@@ -222,7 +241,7 @@
|
|||||||
{{ dep.package.title }} by {{ dep.package.author.display_name }}
|
{{ dep.package.title }} by {{ dep.package.author.display_name }}
|
||||||
{% elif dep.meta_package %}
|
{% elif dep.meta_package %}
|
||||||
<a class="badge badge-{{ color }}"
|
<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 }}
|
{{ dep.meta_package.name }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ "Excepted package or meta_package in dep!" | throw }}
|
{{ "Excepted package or meta_package in dep!" | throw }}
|
||||||
@@ -260,11 +279,29 @@
|
|||||||
|
|
||||||
{% if not rel.approved %}<i>{% endif %}
|
{% if not rel.approved %}<i>{% endif %}
|
||||||
|
|
||||||
<a href="{{ rel.getDownloadURL() }}">{{ rel.title }}</a>{% if rel.commit_hash %}
|
<a href="{{ rel.getDownloadURL() }}" rel="nofollow">{{ rel.title }}</a>
|
||||||
[{{ rel.commit_hash | truncate(5, end='') }}]{% endif %}<br>
|
|
||||||
<small>created {{ rel.releaseDate | datetime }}.</small>
|
<span style="color:#ddd;">
|
||||||
{% if rel.task_id %}
|
{% if rel.min_rel and rel.max_rel %}
|
||||||
<a href="{{ url_for('check_task', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
|
[MT {{ rel.min_rel.name }}-{{ rel.max_rel.name }}]
|
||||||
|
{% elif rel.min_rel %}
|
||||||
|
[MT {{ rel.min_rel.name }}+]
|
||||||
|
{% elif rel.max_rel %}
|
||||||
|
[MT ≤{{ rel.max_rel.name }}]
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<small style="color:#999;">
|
||||||
|
{% if rel.commit_hash %}
|
||||||
|
[{{ rel.commit_hash | truncate(5, end='') }}]
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
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('tasks.check', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
|
||||||
{% elif not rel.approved %}
|
{% elif not rel.approved %}
|
||||||
Waiting for approval.
|
Waiting for approval.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -283,7 +320,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %}
|
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %}
|
||||||
<a class="float-right"
|
<a class="float-right"
|
||||||
href="{{ url_for('new_thread_page', pid=package.id) }}">+</a>
|
href="{{ url_for('threads.new', pid=package.id) }}">+</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
Threads
|
Threads
|
||||||
</div>
|
</div>
|
||||||
@@ -295,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) %}
|
{% 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"
|
<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
|
Report a problem with this listing
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -344,7 +381,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="{{ r.getURL() }}">{{ r.title }}</a>
|
<a href="{{ r.getURL() }}">{{ r.title }}</a>
|
||||||
by
|
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>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li>No edit requests have been made.</li>
|
<li>No edit requests have been made.</li>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Working
|
|||||||
<script>
|
<script>
|
||||||
// @author rubenwardy
|
// @author rubenwardy
|
||||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
// @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() })
|
.then(function() { location.reload() })
|
||||||
.catch(function() { location.reload() })
|
.catch(function() { location.reload() })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,13 +6,11 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% if package and current_user != package.author and not current_user.rank.atLeast(current_user.rank.EDITOR) %}
|
{% if package and current_user != package.author and not current_user.rank.atLeast(current_user.rank.EDITOR) and package.issueTracker %}
|
||||||
{% if package.issueTracker %}
|
<div class="alert alert-warning">
|
||||||
<div class="alert alert-warning">
|
Found a bug? Post on the <a href="{{ package.issueTracker }}">issue tracker</a> instead.<br />
|
||||||
Found a bug? Post on the <a href="{{ package.issue_tracker }}">issue tracker</a> instead.<br />
|
|
||||||
If the package shouldn't be on CDB - for example, if it doesn't work at all - then please let us know here.
|
If the package shouldn't be on CDB - for example, if it doesn't work at all - then please let us know here.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}
|
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}
|
||||||
@@ -39,7 +37,7 @@
|
|||||||
|
|
||||||
{{ render_checkbox_field(form.private, class_="my-3") }}
|
{{ render_checkbox_field(form.private, class_="my-3") }}
|
||||||
<p>
|
<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.
|
and above can read private threads.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -5,53 +5,92 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Awaiting Approval</h2>
|
<h2 class="mb-4">Approval Queue</h2>
|
||||||
|
|
||||||
{% if canApproveNew and packages %}
|
<div class="row">
|
||||||
<h3>Packages</h3>
|
{% if canApproveNew and packages %}
|
||||||
<ul>
|
<div class="col-sm-6">
|
||||||
{% for p in packages %}
|
<div class="card">
|
||||||
<li><a href="{{ p.getDetailsURL() }}">
|
<h3 class="card-header">Packages</h3>
|
||||||
{{ p.title }} by {{ p.author.display_name }}
|
<div class="list-group list-group-flush">
|
||||||
</a></li>
|
{% for p in packages %}
|
||||||
{% else %}
|
<a href="{{ p.getDetailsURL() }}" class="list-group-item list-group-item-action">
|
||||||
<li><i>No packages need reviewing.</i></ul>
|
{% if p.getState() == "thread" %}
|
||||||
{% endfor %}
|
<span class="mr-2 badge badge-danger">Thread</span>
|
||||||
</ul>
|
{% elif p.getState() == "ready" %}
|
||||||
{% endif %}
|
<span class="mr-2 badge badge-success">Ready</span>
|
||||||
|
{% elif p.getState() == "wip" %}
|
||||||
|
<span class="mr-2 badge badge-warning">WIP</span>
|
||||||
|
{% elif p.getState() == "license" %}
|
||||||
|
<span class="mr-2 badge badge-info">WIP</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{ p.title }} by {{ p.author.display_name }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<li class="list-group-item"><i>No packages need reviewing.</i></li>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if canApproveRel and releases %}
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="card">
|
||||||
|
<h3 class="card-header">Releases</h3>
|
||||||
|
<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() }}">
|
||||||
|
{{ r.package.title }} by {{ r.package.author.display_name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="list-group-item"><i>No releases need reviewing.</i></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if canApproveScn and screenshots %}
|
{% if canApproveScn and screenshots %}
|
||||||
<h3>Screenshots</h3>
|
<div class="card my-4">
|
||||||
<ul>
|
<h3 class="card-header">Screenshots
|
||||||
{% for s in screenshots %}
|
<form class="float-right" method="post" action="{{ url_for('todo.view') }}">
|
||||||
<li>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<a href="{{ s.getEditURL() }}">{{ s.title }}</a>
|
<input type="hidden" name="action" value="screenshots_approve_all" />
|
||||||
on
|
<input class="btn btn-sm btn-primary" type="submit" value="Approve All" />
|
||||||
<a href="{{ s.package.getDetailsURL() }}">
|
</form>
|
||||||
{{ s.package.title }} by {{ s.package.author.display_name }}
|
</h3>
|
||||||
</a>
|
<ul class="card-body d-flex p-0 flex-row flex-wrap justify-content-start align-content-start p-4">
|
||||||
</li>
|
{% for s in screenshots %}
|
||||||
{% else %}
|
<li class="packagetile flex-fill"><a href="{{ s.getEditURL() }}"
|
||||||
<li><i>No screenshots need reviewing.</i></ul>
|
style="background-image: url({{ s.getThumbnailURL(3) or '/static/placeholder.png' }});">
|
||||||
{% endfor %}
|
<div class="packagegridscrub"></div>
|
||||||
</ul>
|
<div class="packagegridinfo">
|
||||||
{% endif %}
|
<h3>
|
||||||
|
{{ s.title }}
|
||||||
{% if canApproveRel and releases %}
|
<br />
|
||||||
<h3>Releases</h3>
|
<small>{{ s.package.title }} by {{ s.package.author.display_name }}</small>
|
||||||
<ul>
|
</h3>
|
||||||
{% for r in releases %}
|
<p></p>
|
||||||
<li>
|
</div>
|
||||||
<a href="{{ r.getEditURL() }}">{{ r.title }}</a>
|
</a></li>
|
||||||
on
|
{% else %}
|
||||||
<a href="{{ r.package.getDetailsURL() }}">
|
<li><i>No screenshots need reviewing.</i></li>
|
||||||
{{ r.package.title }} by {{ r.package.author.display_name }}
|
{% endfor %}
|
||||||
</a>
|
{% for i in range(4) %}
|
||||||
</li>
|
<li class="packagetile flex-fill"></li>
|
||||||
{% else %}
|
{% endfor %}
|
||||||
<li><i>No releases need reviewing.</i></ul>
|
</ul>
|
||||||
{% endfor %}
|
</div>
|
||||||
</ul>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not (packages or screenshots or releases) %}
|
{% if not (packages or screenshots or releases) %}
|
||||||
@@ -60,11 +99,25 @@
|
|||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h2>Unadded Topic List</h2>
|
<h2 class="mt-4">Unadded Topic List</h2>
|
||||||
|
|
||||||
|
{% 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 = 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') }}">View Unadded Topic List</a>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
The forum topic crawler needs to run at least once for this section to work.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<p>
|
|
||||||
There are
|
|
||||||
<a href="{{ url_for('todo_topics_page') }}">{{ topics_to_add }} packages</a>
|
|
||||||
to be added to cdb, based on cdb's forum parser.
|
|
||||||
</p>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ Topics to be Added
|
|||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a class="btn btn-primary {% if sort_by=='date' %}active{% endif %}"
|
<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
|
Sort by date
|
||||||
</a>
|
</a>
|
||||||
<a class="btn btn-primary {% if sort_by=='name' %}active{% endif %}"
|
<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
|
Sort by name
|
||||||
</a>
|
</a>
|
||||||
<a class="btn btn-primary {% if sort_by=='views' %}active{% endif %}"
|
<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
|
Sort by views
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,18 +26,18 @@ Topics to be Added
|
|||||||
{% if current_user.rank.atLeast(current_user.rank.EDITOR) %}
|
{% if current_user.rank.atLeast(current_user.rank.EDITOR) %}
|
||||||
{% if n >= 10000 %}
|
{% if n >= 10000 %}
|
||||||
<a class="btn btn-primary"
|
<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
|
Paginated list
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="btn btn-primary"
|
<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
|
Unlimited list
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% 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 %}
|
{% if not show_discarded %}
|
||||||
Show
|
Show
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -51,17 +51,23 @@ Topics to be Added
|
|||||||
|
|
||||||
<h1>Topics to be Added</h1>
|
<h1>Topics to be Added</h1>
|
||||||
|
|
||||||
<p>
|
{% if topic_count > 0 %}
|
||||||
{{ total - topic_count }} / {{ total }} topics have been added as packages to CDB.
|
<p>
|
||||||
{{ topic_count }} remaining.
|
{{ total - topic_count }} / {{ total }} topics have been added as packages to CDB.
|
||||||
</p>
|
{{ topic_count }} remaining.
|
||||||
<div class="progress">
|
</p>
|
||||||
{% set perc = 100 * (total - topic_count) / total %}
|
<div class="progress">
|
||||||
<div class="progress-bar bg-success" role="progressbar"
|
{% set perc = 100 * (total - topic_count) / total %}
|
||||||
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
|
<div class="progress-bar bg-success" role="progressbar"
|
||||||
</div>
|
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="show_discarded" value={{ show_discarded and "True" or "False" }} />
|
||||||
<input type="hidden" name="n" value={{ n }} />
|
<input type="hidden" name="n" value={{ n }} />
|
||||||
<input type="hidden" name="sort" value={{ sort_by or "date" }} />
|
<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) %}
|
{% for i in range(1, page_max+1) %}
|
||||||
<li class="page-item {% if i == page %}active{% endif %}">
|
<li class="page-item {% if i == page %}active{% endif %}">
|
||||||
<a class="page-link"
|
<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 }}
|
{{ i }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Creating an Account
|
|||||||
Please log out to continue.
|
Please log out to continue.
|
||||||
</p>
|
</p>
|
||||||
<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>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>
|
<p>
|
||||||
@@ -44,7 +44,7 @@ Creating an Account
|
|||||||
Use GitHub field in forum profile
|
Use GitHub field in forum profile
|
||||||
</div>
|
</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="claim_type" value="github">
|
||||||
<input class="form-control" type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input class="form-control" type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ Creating an Account
|
|||||||
Verification token
|
Verification token
|
||||||
</div>
|
</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="claim_type" value="forum">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,13 @@
|
|||||||
<ul class="userlist">
|
<ul class="userlist">
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<li class="{{ user.rank }}">
|
<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 }}
|
{{ user.display_name }}
|
||||||
</a> -
|
</a> -
|
||||||
{{ user.rank.getTitle() }} -
|
{{ user.rank.getTitle() }}
|
||||||
{{ user.packages.count() }} packages.
|
{% if current_user.is_authenticated %}
|
||||||
|
- {{ user.packages.count() }} packages.
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% if not current_user.is_authenticated and user.rank == user.rank.NOT_JOINED and user.forums_username %}
|
{% if not current_user.is_authenticated and user.rank == user.rank.NOT_JOINED and user.forums_username %}
|
||||||
<div class="alert alert-info alert alert-info">
|
<div class="alert alert-info">
|
||||||
<a class="float-right btn btn-default btn-sm"
|
<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!
|
Is this you? Claim your account now!
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Accounts:</td>
|
<td>Links:</td>
|
||||||
<td>
|
<td>
|
||||||
{% if user.forums_username %}
|
{% if user.forums_username %}
|
||||||
<a href="https://forum.minetest.net/memberlist.php?mode=viewprofile&un={{ user.forums_username }}">
|
<a href="https://forum.minetest.net/memberlist.php?mode=viewprofile&un={{ user.forums_username }}">
|
||||||
@@ -50,18 +50,26 @@
|
|||||||
No forum account
|
No forum account
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if (user.forums_username and user.github_username) or user == current_user %}
|
{% if user.github_username or user == current_user %}
|
||||||
|
|
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user.github_username %}
|
{% if user.github_username %}
|
||||||
<a href="https://github.com/{{ user.github_username }}">GitHub</a>
|
<a href="https://github.com/{{ user.github_username }}">GitHub</a>
|
||||||
{% elif user == current_user %}
|
{% 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 %}
|
||||||
|
| <a href="{{ user.website_url }}" rel="nofollow">Website</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user == current_user %}
|
{% if user == current_user %}
|
||||||
🌎
|
<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
<span style="padding-right: 5px;">🌎</span>
|
||||||
|
Visible to everyone
|
||||||
|
</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -70,7 +78,7 @@
|
|||||||
<td>Admin</td>
|
<td>Admin</td>
|
||||||
<td>
|
<td>
|
||||||
{% if user.email %}
|
{% 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
|
Email
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -89,7 +97,7 @@
|
|||||||
<td>Profile Picture:</td>
|
<td>Profile Picture:</td>
|
||||||
<td>
|
<td>
|
||||||
{% if user.forums_username %}
|
{% 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="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<input type="submit" class="btn btn-primary" value="Sync with Forums" />
|
<input type="submit" class="btn btn-primary" value="Sync with Forums" />
|
||||||
</form>
|
</form>
|
||||||
@@ -114,7 +122,7 @@
|
|||||||
{% if user.password %}
|
{% if user.password %}
|
||||||
Set | <a href="{{ url_for('user.change_password') }}">Change</a>
|
Set | <a href="{{ url_for('user.change_password') }}">Change</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
Not set | <a href="{{ url_for('set_password_page') }}">Set</a>
|
Not set | <a href="{{ url_for('users.set_password') }}">Set</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -136,6 +144,8 @@
|
|||||||
|
|
||||||
{% if user.checkPerm(current_user, "CHANGE_DNAME") %}
|
{% if user.checkPerm(current_user, "CHANGE_DNAME") %}
|
||||||
{{ render_field(form.display_name, tabindex=230) }}
|
{{ render_field(form.display_name, tabindex=230) }}
|
||||||
|
{{ render_field(form.website_url, tabindex=232) }}
|
||||||
|
{{ render_field(form.donate_url, tabindex=233) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user.checkPerm(current_user, "CHANGE_EMAIL") %}
|
{% if user.checkPerm(current_user, "CHANGE_EMAIL") %}
|
||||||
@@ -158,6 +168,13 @@
|
|||||||
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
||||||
{{ render_pkggrid(packages, show_author=False) }}
|
{{ render_pkggrid(packages, show_author=False) }}
|
||||||
|
|
||||||
|
{% if user.donate_url %}
|
||||||
|
<div class="alert alert-secondary">
|
||||||
|
Like {{ user.display_name }}'s work?
|
||||||
|
<a href="{{ user.donate_url }}" rel="nofollow">Donate now!</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if current_user == user or (current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.EDITOR)) %}
|
{% if current_user == user or (current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.EDITOR)) %}
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<a name="unadded-topics"></a>
|
<a name="unadded-topics"></a>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="alert alert-primary">
|
<div class="alert alert-primary">
|
||||||
It is recommended that you set a password for your account.
|
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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
32
app/utils.py
@@ -20,7 +20,13 @@ from flask_user import *
|
|||||||
from flask_login import login_user, logout_user
|
from flask_login import login_user, logout_user
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app import app
|
from app import app
|
||||||
import random, string, os
|
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):
|
def getExtension(filename):
|
||||||
return filename.rsplit(".", 1)[1].lower() if "." in filename else None
|
return filename.rsplit(".", 1)[1].lower() if "." in filename else None
|
||||||
@@ -28,6 +34,10 @@ def getExtension(filename):
|
|||||||
def isFilenameAllowed(filename, exts):
|
def isFilenameAllowed(filename, exts):
|
||||||
return getExtension(filename) in exts
|
return getExtension(filename) in exts
|
||||||
|
|
||||||
|
ALLOWED_IMAGES = set(["jpeg", "png"])
|
||||||
|
def isAllowedImage(data):
|
||||||
|
return imghdr.what(None, data) in ALLOWED_IMAGES
|
||||||
|
|
||||||
def shouldReturnJson():
|
def shouldReturnJson():
|
||||||
return "application/json" in request.accept_mimetypes and \
|
return "application/json" in request.accept_mimetypes and \
|
||||||
not "text/html" in request.accept_mimetypes
|
not "text/html" in request.accept_mimetypes
|
||||||
@@ -36,16 +46,32 @@ def randomString(n):
|
|||||||
return ''.join(random.choice(string.ascii_lowercase + \
|
return ''.join(random.choice(string.ascii_lowercase + \
|
||||||
string.ascii_uppercase + string.digits) for _ in range(n))
|
string.ascii_uppercase + string.digits) for _ in range(n))
|
||||||
|
|
||||||
def doFileUpload(file, allowedExtensions, fileTypeName):
|
def doFileUpload(file, fileType, fileTypeDesc):
|
||||||
if not file or file is None or file.filename == "":
|
if not file or file is None or file.filename == "":
|
||||||
flash("No selected file", "error")
|
flash("No selected file", "error")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
allowedExtensions = []
|
||||||
|
isImage = False
|
||||||
|
if fileType == "image":
|
||||||
|
allowedExtensions = ["jpg", "jpeg", "png"]
|
||||||
|
isImage = True
|
||||||
|
elif fileType == "zip":
|
||||||
|
allowedExtensions = ["zip"]
|
||||||
|
else:
|
||||||
|
raise Exception("Invalid fileType")
|
||||||
|
|
||||||
ext = getExtension(file.filename)
|
ext = getExtension(file.filename)
|
||||||
if ext is None or not ext in allowedExtensions:
|
if ext is None or not ext in allowedExtensions:
|
||||||
flash("Please upload load " + fileTypeName, "error")
|
flash("Please upload load " + fileTypeDesc, "danger")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if isImage and not isAllowedImage(file.stream.read()):
|
||||||
|
flash("Uploaded image isn't actually an image", "danger")
|
||||||
|
return None
|
||||||
|
|
||||||
|
file.stream.seek(0)
|
||||||
|
|
||||||
filename = randomString(10) + "." + ext
|
filename = randomString(10) + "." + ext
|
||||||
file.save(os.path.join("app/public/uploads", filename))
|
file.save(os.path.join("app/public/uploads", filename))
|
||||||
return "/uploads/" + filename
|
return "/uploads/" + filename
|
||||||
|
|||||||
@@ -1,78 +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
|
|
||||||
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()
|
|
||||||
return render_template("index.html", count=count, \
|
|
||||||
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()
|
|
||||||
@@ -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
|
|
||||||
3
babel.cfg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[python: app/**.py]
|
||||||
|
[jinja2: app/templates/**.html]
|
||||||
|
extensions=jinja2.ext.autoescape,jinja2.ext.with_
|
||||||
@@ -10,8 +10,9 @@ SQLALCHEMY_DATABASE_URI = "sqlite:///../db.sqlite"
|
|||||||
GITHUB_CLIENT_ID = ""
|
GITHUB_CLIENT_ID = ""
|
||||||
GITHUB_CLIENT_SECRET = ""
|
GITHUB_CLIENT_SECRET = ""
|
||||||
|
|
||||||
CELERY_BROKER_URL='redis://localhost:6379'
|
REDIS_URL='redis://redis:6379'
|
||||||
CELERY_RESULT_BACKEND='redis://localhost:6379'
|
CELERY_BROKER_URL='redis://redis:6379'
|
||||||
|
CELERY_RESULT_BACKEND='redis://redis:6379'
|
||||||
|
|
||||||
USER_ENABLE_REGISTER = False
|
USER_ENABLE_REGISTER = False
|
||||||
USER_ENABLE_CHANGE_USERNAME = False
|
USER_ENABLE_CHANGE_USERNAME = False
|
||||||
@@ -23,3 +24,7 @@ MAIL_SERVER=""
|
|||||||
MAIL_PORT=587
|
MAIL_PORT=587
|
||||||
MAIL_USE_TLS=True
|
MAIL_USE_TLS=True
|
||||||
MAIL_UTILS_ERROR_SEND_TO=[""]
|
MAIL_UTILS_ERROR_SEND_TO=[""]
|
||||||
|
|
||||||
|
LANGUAGES = {
|
||||||
|
'en': 'English',
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ services:
|
|||||||
|
|
||||||
app:
|
app:
|
||||||
build: .
|
build: .
|
||||||
command: ./rundebug.sh
|
command: ./utils/run.sh
|
||||||
|
env_file:
|
||||||
|
- config.env
|
||||||
ports:
|
ports:
|
||||||
- 5123:5123
|
- 5123:5123
|
||||||
volumes:
|
volumes:
|
||||||
- "./data/uploads:/home/cdb/app/public/uploads"
|
- "./data/uploads:/home/cdb/app/public/uploads"
|
||||||
- "./app:/home/cdb/app"
|
- "./app:/home/cdb/appsrc"
|
||||||
- "./migrations:/home/cdb/migrations"
|
- "./migrations:/home/cdb/migrations"
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
@@ -31,7 +33,29 @@ services:
|
|||||||
command: celery -A app.tasks.celery worker
|
command: celery -A app.tasks.celery worker
|
||||||
env_file:
|
env_file:
|
||||||
- config.env
|
- config.env
|
||||||
|
environment:
|
||||||
|
- FLASK_CONFIG=../config.cfg
|
||||||
volumes:
|
volumes:
|
||||||
- "./data/uploads:/home/cdb/app/public/uploads"
|
- "./data/uploads:/home/cdb/app/public/uploads"
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- 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
|
||||||
|
|||||||
36
migrations/versions/2f3c3597c78d_.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 2f3c3597c78d
|
||||||
|
Revises: 9ec17b558413
|
||||||
|
Create Date: 2019-01-29 02:43:08.865695
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
from sqlalchemy_utils.types import TSVectorType
|
||||||
|
from sqlalchemy_searchable import sync_trigger
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2f3c3597c78d'
|
||||||
|
down_revision = '9ec17b558413'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('package', 'shortDesc', nullable=False, new_column_name='short_desc')
|
||||||
|
op.add_column('package', sa.Column('search_vector', TSVectorType("title", "short_desc", "desc"), nullable=True))
|
||||||
|
op.create_index('ix_package_search_vector', 'package', ['search_vector'], unique=False, postgresql_using='gin')
|
||||||
|
|
||||||
|
conn = op.get_bind()
|
||||||
|
sync_trigger(conn, 'package', 'search_vector', ["title", "short_desc", "desc"])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index('ix_package_search_vector', table_name='package')
|
||||||
|
op.drop_column('package', 'search_vector')
|
||||||
|
# ### end Alembic commands ###
|
||||||