Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b68a1d7ab9 | ||
|
|
2ef90902aa | ||
|
|
e115b0678c | ||
|
|
0bda16de6d | ||
|
|
fd6ba459f9 | ||
|
|
d503908a65 | ||
|
|
215839c423 | ||
|
|
783bc86aaf | ||
|
|
6e626c0f89 | ||
|
|
facdd35b11 | ||
|
|
ec8a88a7a8 | ||
|
|
1b1c94ffa0 | ||
|
|
bcd003685e | ||
|
|
59039a14a5 | ||
|
|
0d6e217405 | ||
|
|
64e1805b53 | ||
|
|
22d02edbd8 | ||
|
|
5a496f6858 | ||
|
|
f4209d7a67 | ||
|
|
077bdeb01c | ||
|
|
095494f96f | ||
|
|
6f230ee4b2 | ||
|
|
311e0218af | ||
|
|
3fee369dc1 | ||
|
|
e57f2dfe7d | ||
|
|
dd5de1787f | ||
|
|
62f1aecfaf | ||
|
|
4ce388c8aa | ||
|
|
cb5451fe5d | ||
|
|
5466a2d64d | ||
|
|
77f8a79c51 | ||
|
|
33b2b38308 | ||
|
|
94426e97aa | ||
|
|
5b68e494db | ||
|
|
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 |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.git
|
||||
data
|
||||
uploads
|
||||
*.pyc
|
||||
__pycache__
|
||||
13
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
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
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
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
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).
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,13 +1,13 @@
|
||||
config.cfg
|
||||
*.env
|
||||
/config.cfg
|
||||
/*.env
|
||||
*.sqlite
|
||||
.vscode
|
||||
custom.css
|
||||
tmp
|
||||
log.txt
|
||||
*.rdb
|
||||
uploads
|
||||
thumbnails
|
||||
app/public/uploads
|
||||
app/public/thumbnails
|
||||
celerybeat-schedule
|
||||
/data
|
||||
|
||||
|
||||
22
.gitlab-ci.yml
Normal file
22
.gitlab-ci.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
image: docker/compose
|
||||
services:
|
||||
- docker:dind
|
||||
cache:
|
||||
key: "$CI_COMMIT_REF_SLUG"
|
||||
paths:
|
||||
- /var/lib/docker
|
||||
|
||||
# build:
|
||||
# stage: build
|
||||
# script:
|
||||
# - cp utils/gitlabci/* .
|
||||
# - docker-compose build
|
||||
|
||||
UI_Test:
|
||||
stage: test
|
||||
script:
|
||||
- cp utils/gitlabci/* .
|
||||
- docker-compose up -d
|
||||
- ./utils/run_migrations.sh
|
||||
- ./utils/tests_cov.sh
|
||||
- docker-compose down
|
||||
23
Dockerfile
23
Dockerfile
@@ -1,17 +1,22 @@
|
||||
FROM python:3.6
|
||||
|
||||
RUN groupadd -g 5123 cdb && \
|
||||
useradd -r -u 5123 -g cdb cdb
|
||||
|
||||
WORKDIR /home/cdb
|
||||
|
||||
RUN mkdir /var/cdb
|
||||
RUN chown -R cdb:cdb /var/cdb
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip install -r ./requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
RUN pip install gunicorn
|
||||
RUN pip install psycopg2
|
||||
|
||||
COPY runprodguni.sh ./
|
||||
COPY rundebug.sh ./
|
||||
RUN chmod +x runprodguni.sh
|
||||
|
||||
COPY setup.py ./setup.py
|
||||
COPY app app
|
||||
COPY utils utils
|
||||
COPY config.cfg config.cfg
|
||||
COPY migrations migrations
|
||||
COPY config.cfg ./config.cfg
|
||||
COPY app app
|
||||
|
||||
RUN chown -R cdb:cdb /home/cdb
|
||||
|
||||
USER cdb
|
||||
|
||||
40
README.md
40
README.md
@@ -13,15 +13,51 @@ Note: you should first read one of the guides on the [Github repo wiki](https://
|
||||
FLASK_CONFIG=../config.cfg celery -A app.tasks.celery worker
|
||||
|
||||
# if sqlite
|
||||
python setup.py -t
|
||||
python utils/setup.py -t
|
||||
rm db.sqlite && python setup.py -t && FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db stamp head
|
||||
|
||||
# Create migration
|
||||
FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db migrate
|
||||
|
||||
# Run migration
|
||||
FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db upgrade
|
||||
|
||||
# Enter docker
|
||||
docker exec -it contentdb_app_1 bash
|
||||
|
||||
# Hot/live reload (only works with FLASK_DEBUG=1)
|
||||
./utils/reload.sh
|
||||
|
||||
# Cold update a running version of CDB with minimal downtime
|
||||
./utils/update.sh
|
||||
```
|
||||
|
||||
## Database
|
||||
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
|
||||
User "1" --> "*" Package
|
||||
User --> UserEmailVerification
|
||||
User "1" --> "*" Notification
|
||||
Package "1" --> "*" Release
|
||||
Package "1" --> "*" Dependency
|
||||
Package "1" --> "*" Tag
|
||||
Package "1" --> "*" MetaPackage : provides
|
||||
Release --> MinetestVersion
|
||||
Package --> License
|
||||
Dependency --> Package
|
||||
Dependency --> MetaPackage
|
||||
MetaPackage "1" --> "*" Package
|
||||
Package "1" --> "*" Screenshot
|
||||
Package "1" --> "*" Thread
|
||||
Thread "1" --> "*" Reply
|
||||
Thread "1" --> "*" User : watchers
|
||||
User "1" --> "*" Thread
|
||||
User "1" --> "*" Reply
|
||||
User "1" --> "*" ForumTopic
|
||||
|
||||
User --> "0..1" EmailPreferences
|
||||
User "1" --> "*" APIToken
|
||||
APIToken --> Package
|
||||
```
|
||||
|
||||
@@ -25,13 +25,15 @@ from flask_github import GitHub
|
||||
from flask_wtf.csrf import CsrfProtect
|
||||
from flask_flatpages import FlatPages
|
||||
from flask_babel import Babel
|
||||
import os
|
||||
import os, redis
|
||||
|
||||
app = Flask(__name__, static_folder="public/static")
|
||||
app.config["FLATPAGES_ROOT"] = "flatpages"
|
||||
app.config["FLATPAGES_EXTENSION"] = ".md"
|
||||
app.config.from_pyfile(os.environ["FLASK_CONFIG"])
|
||||
|
||||
r = redis.Redis.from_url(app.config["REDIS_URL"])
|
||||
|
||||
menu.Menu(app=app)
|
||||
markdown = Markdown(app, extensions=["fenced_code"], safe_mode=True, output_format="html5")
|
||||
github = GitHub(app)
|
||||
@@ -48,15 +50,44 @@ gravatar = Gravatar(app,
|
||||
use_ssl=True,
|
||||
base_url=None)
|
||||
|
||||
if not app.debug:
|
||||
from .sass import sass
|
||||
sass(app)
|
||||
|
||||
|
||||
if not app.debug and app.config["MAIL_UTILS_ERROR_SEND_TO"]:
|
||||
from .maillogger import register_mail_error_handler
|
||||
register_mail_error_handler(app, mail)
|
||||
|
||||
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
|
||||
return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
|
||||
|
||||
from . import models, tasks, template_filters
|
||||
|
||||
from . import models, tasks
|
||||
from .views import *
|
||||
from .blueprints import create_blueprints
|
||||
create_blueprints(app)
|
||||
|
||||
from flask_login import logout_user
|
||||
|
||||
@app.route("/uploads/<path:path>")
|
||||
def send_upload(path):
|
||||
return send_from_directory(app.config['UPLOAD_DIR'], 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
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/>.
|
||||
|
||||
|
||||
from . import admin, licenseseditor, tagseditor, versioneditor, todo
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint("admin", __name__)
|
||||
|
||||
from . import admin, licenseseditor, tagseditor, versioneditor
|
||||
@@ -18,27 +18,46 @@
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
import flask_menu as menu
|
||||
from app import app
|
||||
from . import bp
|
||||
from app.models import *
|
||||
from celery import uuid
|
||||
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies, makeVCSRelease
|
||||
from celery import uuid, group
|
||||
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies, makeVCSRelease, checkZipRelease
|
||||
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from app.utils import loginUser, rank_required, triggerNotif
|
||||
import datetime
|
||||
|
||||
@app.route("/admin/", methods=["GET", "POST"])
|
||||
@bp.route("/admin/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.ADMIN)
|
||||
def admin_page():
|
||||
if request.method == "POST":
|
||||
action = request.form["action"]
|
||||
if action == "importmodlist":
|
||||
if action == "delstuckreleases":
|
||||
PackageRelease.query.filter(PackageRelease.task_id != None).delete()
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
elif action == "checkreleases":
|
||||
releases = PackageRelease.query.filter(PackageRelease.url.like("/uploads/%")).all()
|
||||
|
||||
tasks = []
|
||||
for release in releases:
|
||||
zippath = release.url.replace("/uploads/", app.config["UPLOAD_DIR"])
|
||||
tasks.append(checkZipRelease.s(release.id, zippath))
|
||||
|
||||
result = group(tasks).apply_async()
|
||||
|
||||
while not result.ready():
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
|
||||
return redirect(url_for("todo.view"))
|
||||
elif action == "importmodlist":
|
||||
task = importTopicList.delay()
|
||||
return redirect(url_for("check_task", id=task.id, r=url_for("todo_topics_page")))
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("todo.topics")))
|
||||
elif action == "checkusers":
|
||||
task = checkAllForumAccounts.delay()
|
||||
return redirect(url_for("check_task", id=task.id, r=url_for("admin_page")))
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
|
||||
elif action == "importscreenshots":
|
||||
packages = Package.query \
|
||||
.filter_by(soft_deleted=False) \
|
||||
@@ -48,7 +67,7 @@ def admin_page():
|
||||
for package in packages:
|
||||
importRepoScreenshot.delay(package.id)
|
||||
|
||||
return redirect(url_for("admin_page"))
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
elif action == "restore":
|
||||
package = Package.query.get(request.form["package"])
|
||||
if package is None:
|
||||
@@ -56,10 +75,10 @@ def admin_page():
|
||||
else:
|
||||
package.soft_deleted = False
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin_page"))
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
elif action == "importdepends":
|
||||
task = importAllDependencies.delay()
|
||||
return redirect(url_for("check_task", id=task.id, r=url_for("admin_page")))
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
|
||||
elif action == "modprovides":
|
||||
packages = Package.query.filter_by(type=PackageType.MOD).all()
|
||||
mpackage_cache = {}
|
||||
@@ -68,13 +87,13 @@ def admin_page():
|
||||
p.provides.append(MetaPackage.GetOrCreate(p.name, mpackage_cache))
|
||||
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin_page"))
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
elif action == "recalcscores":
|
||||
for p in Package.query.all():
|
||||
p.recalcScore()
|
||||
p.setStartScore()
|
||||
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin_page"))
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
elif action == "vcsrelease":
|
||||
for package in Package.query.filter(Package.repo.isnot(None)).all():
|
||||
if package.releases.count() != 0:
|
||||
@@ -106,19 +125,19 @@ class SwitchUserForm(FlaskForm):
|
||||
submit = SubmitField("Switch")
|
||||
|
||||
|
||||
@app.route("/admin/switchuser/", methods=["GET", "POST"])
|
||||
@bp.route("/admin/switchuser/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.ADMIN)
|
||||
def switch_user_page():
|
||||
def switch_user():
|
||||
form = SwitchUserForm(formdata=request.form)
|
||||
if request.method == "POST" and form.validate():
|
||||
user = User.query.filter_by(username=form["username"].data).first()
|
||||
if user is None:
|
||||
flash("Unable to find user", "error")
|
||||
elif loginUser(user):
|
||||
return redirect(url_for("user_profile_page", username=current_user.username))
|
||||
return redirect(url_for("users.profile", username=current_user.username))
|
||||
else:
|
||||
flash("Unable to login as user", "error")
|
||||
|
||||
|
||||
# Process GET or invalid POST
|
||||
return render_template("admin/switch_user_page.html", form=form)
|
||||
return render_template("admin/switch_user.html", form=form)
|
||||
@@ -17,16 +17,16 @@
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from app import app
|
||||
from . import bp
|
||||
from app.models import *
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
from app.utils import rank_required
|
||||
|
||||
@app.route("/licenses/")
|
||||
@bp.route("/licenses/")
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def license_list_page():
|
||||
def license_list():
|
||||
return render_template("admin/licenses/list.html", licenses=License.query.order_by(db.asc(License.name)).all())
|
||||
|
||||
class LicenseForm(FlaskForm):
|
||||
@@ -34,10 +34,10 @@ class LicenseForm(FlaskForm):
|
||||
is_foss = BooleanField("Is FOSS")
|
||||
submit = SubmitField("Save")
|
||||
|
||||
@app.route("/licenses/new/", methods=["GET", "POST"])
|
||||
@app.route("/licenses/<name>/edit/", methods=["GET", "POST"])
|
||||
@bp.route("/licenses/new/", methods=["GET", "POST"])
|
||||
@bp.route("/licenses/<name>/edit/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def createedit_license_page(name=None):
|
||||
def create_edit_license(name=None):
|
||||
license = None
|
||||
if name is not None:
|
||||
license = License.query.filter_by(name=name).first()
|
||||
@@ -57,6 +57,6 @@ def createedit_license_page(name=None):
|
||||
|
||||
form.populate_obj(license)
|
||||
db.session.commit()
|
||||
return redirect(url_for("license_list_page"))
|
||||
return redirect(url_for("admin.license_list"))
|
||||
|
||||
return render_template("admin/licenses/edit.html", license=license, form=form)
|
||||
@@ -17,16 +17,16 @@
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from app import app
|
||||
from . import bp
|
||||
from app.models import *
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
from app.utils import rank_required
|
||||
|
||||
@app.route("/tags/")
|
||||
@bp.route("/tags/")
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def tag_list_page():
|
||||
def tag_list():
|
||||
return render_template("admin/tags/list.html", tags=Tag.query.order_by(db.asc(Tag.title)).all())
|
||||
|
||||
class TagForm(FlaskForm):
|
||||
@@ -34,10 +34,10 @@ class TagForm(FlaskForm):
|
||||
name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
|
||||
submit = SubmitField("Save")
|
||||
|
||||
@app.route("/tags/new/", methods=["GET", "POST"])
|
||||
@app.route("/tags/<name>/edit/", methods=["GET", "POST"])
|
||||
@bp.route("/tags/new/", methods=["GET", "POST"])
|
||||
@bp.route("/tags/<name>/edit/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def createedit_tag_page(name=None):
|
||||
def create_edit_tag(name=None):
|
||||
tag = None
|
||||
if name is not None:
|
||||
tag = Tag.query.filter_by(name=name).first()
|
||||
@@ -52,6 +52,6 @@ def createedit_tag_page(name=None):
|
||||
else:
|
||||
form.populate_obj(tag)
|
||||
db.session.commit()
|
||||
return redirect(url_for("createedit_tag_page", name=tag.name))
|
||||
return redirect(url_for("admin.create_edit_tag", name=tag.name))
|
||||
|
||||
return render_template("admin/tags/edit.html", tag=tag, form=form)
|
||||
@@ -17,16 +17,16 @@
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from app import app
|
||||
from . import bp
|
||||
from app.models import *
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
from app.utils import rank_required
|
||||
|
||||
@app.route("/versions/")
|
||||
@bp.route("/versions/")
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def version_list_page():
|
||||
def version_list():
|
||||
return render_template("admin/versions/list.html", versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all())
|
||||
|
||||
class VersionForm(FlaskForm):
|
||||
@@ -34,10 +34,10 @@ class VersionForm(FlaskForm):
|
||||
protocol = IntegerField("Protocol")
|
||||
submit = SubmitField("Save")
|
||||
|
||||
@app.route("/versions/new/", methods=["GET", "POST"])
|
||||
@app.route("/versions/<name>/edit/", methods=["GET", "POST"])
|
||||
@bp.route("/versions/new/", methods=["GET", "POST"])
|
||||
@bp.route("/versions/<name>/edit/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def createedit_version_page(name=None):
|
||||
def create_edit_version(name=None):
|
||||
version = None
|
||||
if name is not None:
|
||||
version = MinetestRelease.query.filter_by(name=name).first()
|
||||
@@ -55,6 +55,6 @@ def createedit_version_page(name=None):
|
||||
|
||||
form.populate_obj(version)
|
||||
db.session.commit()
|
||||
return redirect(url_for("version_list_page"))
|
||||
return redirect(url_for("admin.version_list"))
|
||||
|
||||
return render_template("admin/versions/edit.html", version=version, form=form)
|
||||
@@ -14,5 +14,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
from . import users, githublogin, notifications
|
||||
bp = Blueprint("api", __name__)
|
||||
|
||||
from . import tokens, endpoints
|
||||
42
app/blueprints/api/auth.py
Normal file
42
app/blueprints/api/auth.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2019 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import request, make_response, jsonify, abort
|
||||
from app.models import APIToken
|
||||
from functools import wraps
|
||||
|
||||
def is_api_authd(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
token = None
|
||||
|
||||
value = request.headers.get("authorization")
|
||||
if value is None:
|
||||
pass
|
||||
elif value[0:7].lower() == "bearer ":
|
||||
access_token = value[7:]
|
||||
if len(access_token) < 10:
|
||||
abort(400)
|
||||
|
||||
token = APIToken.query.filter_by(access_token=access_token).first()
|
||||
if token is None:
|
||||
abort(403)
|
||||
else:
|
||||
abort(403)
|
||||
|
||||
return f(token=token, *args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
@@ -17,35 +17,66 @@
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from app import app
|
||||
from . import bp
|
||||
from .auth import is_api_authd
|
||||
from app.models import *
|
||||
from app.utils import is_package_page
|
||||
from app.querybuilder import QueryBuilder
|
||||
|
||||
@app.route("/api/packages/")
|
||||
def api_packages_page():
|
||||
@bp.route("/api/packages/")
|
||||
def packages():
|
||||
qb = QueryBuilder(request.args)
|
||||
query = qb.buildPackageQuery()
|
||||
ver = qb.getMinetestVersion()
|
||||
|
||||
pkgs = [package.getAsDictionaryShort(app.config["BASE_URL"], version=ver) \
|
||||
pkgs = [package.getAsDictionaryShort(current_app.config["BASE_URL"], version=ver) \
|
||||
for package in query.all()]
|
||||
return jsonify(pkgs)
|
||||
|
||||
@app.route("/api/packages/<author>/<name>/")
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/")
|
||||
@is_package_page
|
||||
def api_package_page(package):
|
||||
return jsonify(package.getAsDictionary(app.config["BASE_URL"]))
|
||||
def package(package):
|
||||
return jsonify(package.getAsDictionary(current_app.config["BASE_URL"]))
|
||||
|
||||
|
||||
@app.route("/api/topics/")
|
||||
def api_topics_page():
|
||||
@bp.route("/api/packages/<author>/<name>/dependencies/")
|
||||
@is_package_page
|
||||
def package_dependencies(package):
|
||||
ret = []
|
||||
|
||||
for dep in package.dependencies:
|
||||
name = None
|
||||
fulfilled_by = None
|
||||
|
||||
if dep.package:
|
||||
name = dep.package.name
|
||||
fulfilled_by = [ dep.package.getAsDictionaryKey() ]
|
||||
|
||||
elif dep.meta_package:
|
||||
name = dep.meta_package.name
|
||||
fulfilled_by = [ pkg.getAsDictionaryKey() for pkg in dep.meta_package.packages]
|
||||
|
||||
else:
|
||||
raise "Malformed dependency"
|
||||
|
||||
ret.append({
|
||||
"name": name,
|
||||
"is_optional": dep.optional,
|
||||
"packages": fulfilled_by
|
||||
})
|
||||
|
||||
return jsonify(ret)
|
||||
|
||||
|
||||
@bp.route("/api/topics/")
|
||||
def topics():
|
||||
qb = QueryBuilder(request.args)
|
||||
query = qb.buildTopicQuery(show_added=True)
|
||||
return jsonify([t.getAsDictionary() for t in query.all()])
|
||||
|
||||
|
||||
@app.route("/api/topic_discard/", methods=["POST"])
|
||||
@bp.route("/api/topic_discard/", methods=["POST"])
|
||||
@login_required
|
||||
def topic_set_discard():
|
||||
tid = request.args.get("tid")
|
||||
@@ -63,7 +94,16 @@ def topic_set_discard():
|
||||
return jsonify(topic.getAsDictionary())
|
||||
|
||||
|
||||
@app.route("/api/minetest_versions/")
|
||||
def api_minetest_versions_page():
|
||||
@bp.route("/api/minetest_versions/")
|
||||
def versions():
|
||||
return jsonify([{ "name": rel.name, "protocol_version": rel.protocol }\
|
||||
for rel in MinetestRelease.query.all() if rel.getActual() is not None])
|
||||
|
||||
|
||||
@bp.route("/api/whoami/")
|
||||
@is_api_authd
|
||||
def whoami(token):
|
||||
if token is None:
|
||||
return jsonify({ "is_authenticated": False, "username": None })
|
||||
else:
|
||||
return jsonify({ "is_authenticated": True, "username": token.owner.username })
|
||||
141
app/blueprints/api/tokens.py
Normal file
141
app/blueprints/api/tokens.py
Normal file
@@ -0,0 +1,141 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import render_template, redirect, request, session, url_for, abort
|
||||
from flask_user import login_required, current_user
|
||||
from . import bp
|
||||
from app.models import db, User, APIToken, Package, Permission
|
||||
from app.utils import randomString
|
||||
from app.querybuilder import QueryBuilder
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
from wtforms.ext.sqlalchemy.fields import QuerySelectField
|
||||
|
||||
class CreateAPIToken(FlaskForm):
|
||||
name = StringField("Name", [InputRequired(), Length(1, 30)])
|
||||
submit = SubmitField("Save")
|
||||
|
||||
|
||||
@bp.route("/users/<username>/tokens/")
|
||||
@login_required
|
||||
def list_tokens(username):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
abort(404)
|
||||
|
||||
if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
|
||||
abort(403)
|
||||
|
||||
return render_template("api/list_tokens.html", user=user)
|
||||
|
||||
|
||||
@bp.route("/users/<username>/tokens/new/", methods=["GET", "POST"])
|
||||
@bp.route("/users/<username>/tokens/<int:id>/edit/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create_edit_token(username, id=None):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
abort(404)
|
||||
|
||||
if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
|
||||
abort(403)
|
||||
|
||||
is_new = id is None
|
||||
|
||||
token = None
|
||||
access_token = None
|
||||
if not is_new:
|
||||
token = APIToken.query.get(id)
|
||||
if token is None:
|
||||
abort(404)
|
||||
elif token.owner != user:
|
||||
abort(403)
|
||||
|
||||
access_token = session.pop("token_" + str(id), None)
|
||||
|
||||
form = CreateAPIToken(formdata=request.form, obj=token)
|
||||
if request.method == "POST" and form.validate():
|
||||
if is_new:
|
||||
token = APIToken()
|
||||
token.owner = user
|
||||
token.access_token = randomString(32)
|
||||
|
||||
form.populate_obj(token)
|
||||
db.session.add(token)
|
||||
|
||||
db.session.commit() # save
|
||||
|
||||
# Store token so it can be shown in the edit page
|
||||
session["token_" + str(token.id)] = token.access_token
|
||||
|
||||
return redirect(url_for("api.create_edit_token", username=username, id=token.id))
|
||||
|
||||
return render_template("api/create_edit_token.html", user=user, form=form, token=token, access_token=access_token)
|
||||
|
||||
|
||||
@bp.route("/users/<username>/tokens/<int:id>/reset/", methods=["POST"])
|
||||
@login_required
|
||||
def reset_token(username, id):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
abort(404)
|
||||
|
||||
if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
|
||||
abort(403)
|
||||
|
||||
is_new = id is None
|
||||
|
||||
token = APIToken.query.get(id)
|
||||
if token is None:
|
||||
abort(404)
|
||||
elif token.owner != user:
|
||||
abort(403)
|
||||
|
||||
token.access_token = randomString(32)
|
||||
|
||||
db.session.commit() # save
|
||||
|
||||
# Store token so it can be shown in the edit page
|
||||
session["token_" + str(token.id)] = token.access_token
|
||||
|
||||
return redirect(url_for("api.create_edit_token", username=username, id=token.id))
|
||||
|
||||
|
||||
@bp.route("/users/<username>/tokens/<int:id>/delete/", methods=["POST"])
|
||||
@login_required
|
||||
def delete_token(username, id):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
abort(404)
|
||||
|
||||
if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
|
||||
abort(403)
|
||||
|
||||
is_new = id is None
|
||||
|
||||
token = APIToken.query.get(id)
|
||||
if token is None:
|
||||
abort(404)
|
||||
elif token.owner != user:
|
||||
abort(403)
|
||||
|
||||
db.session.delete(token)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("api.list_tokens", username=username))
|
||||
21
app/blueprints/homepage/__init__.py
Normal file
21
app/blueprints/homepage/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
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_result = db.session.query(func.sum(PackageRelease.downloads)).one_or_none()
|
||||
downloads = 0 if not downloads_result or not downloads_result[0] else downloads_result[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 *
|
||||
|
||||
bp = Blueprint("metapackages", __name__)
|
||||
|
||||
from flask_user import *
|
||||
from app import app
|
||||
from app.models import *
|
||||
|
||||
@app.route("/metapackages/")
|
||||
def meta_package_list_page():
|
||||
@bp.route("/metapackages/")
|
||||
def list_all():
|
||||
mpackages = MetaPackage.query.order_by(db.asc(MetaPackage.name)).all()
|
||||
return render_template("meta/list.html", mpackages=mpackages)
|
||||
|
||||
@app.route("/metapackages/<name>/")
|
||||
def meta_package_page(name):
|
||||
@bp.route("/metapackages/<name>/")
|
||||
def view(name):
|
||||
mpackage = MetaPackage.query.filter_by(name=name).first()
|
||||
if mpackage is None:
|
||||
abort(404)
|
||||
@@ -15,19 +15,20 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask import Blueprint, render_template, redirect, url_for
|
||||
from flask_user import current_user, login_required
|
||||
from app import app
|
||||
from app.models import *
|
||||
from app.models import db
|
||||
|
||||
@app.route("/notifications/")
|
||||
bp = Blueprint("notifications", __name__)
|
||||
|
||||
@bp.route("/notifications/")
|
||||
@login_required
|
||||
def notifications_page():
|
||||
def list_all():
|
||||
return render_template("notifications/list.html")
|
||||
|
||||
@app.route("/notifications/clear/", methods=["POST"])
|
||||
@bp.route("/notifications/clear/", methods=["POST"])
|
||||
@login_required
|
||||
def clear_notifications_page():
|
||||
def clear():
|
||||
current_user.notifications.clear()
|
||||
db.session.commit()
|
||||
return redirect(url_for("notifications_page"))
|
||||
return redirect(url_for("notifications.list_all"))
|
||||
@@ -14,5 +14,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint("packages", __name__)
|
||||
|
||||
from . import packages, screenshots, releases
|
||||
@@ -14,7 +14,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from app import app
|
||||
@@ -18,11 +18,14 @@
|
||||
from flask import render_template, abort, request, redirect, url_for, flash
|
||||
from flask_user import current_user
|
||||
import flask_menu as menu
|
||||
from app import app
|
||||
|
||||
from . import bp
|
||||
|
||||
from app.models import *
|
||||
from app.querybuilder import QueryBuilder
|
||||
from app.tasks.importtasks import importRepoScreenshot
|
||||
from app.utils import *
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
@@ -30,12 +33,12 @@ from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleF
|
||||
from sqlalchemy import or_
|
||||
|
||||
|
||||
@menu.register_menu(app, ".mods", "Mods", order=11, endpoint_arguments_constructor=lambda: { 'type': 'mod' })
|
||||
@menu.register_menu(app, ".games", "Games", order=12, endpoint_arguments_constructor=lambda: { 'type': 'game' })
|
||||
@menu.register_menu(app, ".txp", "Texture Packs", order=13, endpoint_arguments_constructor=lambda: { 'type': 'txp' })
|
||||
@menu.register_menu(app, ".random", "Random", order=14, endpoint_arguments_constructor=lambda: { 'random': '1' })
|
||||
@app.route("/packages/")
|
||||
def packages_page():
|
||||
@menu.register_menu(bp, ".mods", "Mods", order=11, endpoint_arguments_constructor=lambda: { 'type': 'mod' })
|
||||
@menu.register_menu(bp, ".games", "Games", order=12, endpoint_arguments_constructor=lambda: { 'type': 'game' })
|
||||
@menu.register_menu(bp, ".txp", "Texture Packs", order=13, endpoint_arguments_constructor=lambda: { 'type': 'txp' })
|
||||
@menu.register_menu(bp, ".random", "Random", order=14, endpoint_arguments_constructor=lambda: { 'random': '1', 'lucky': '1' })
|
||||
@bp.route("/packages/")
|
||||
def list_all():
|
||||
qb = QueryBuilder(request.args)
|
||||
query = qb.buildPackageQuery()
|
||||
title = qb.title
|
||||
@@ -49,16 +52,16 @@ def packages_page():
|
||||
if qb.search and topic:
|
||||
return redirect("https://forum.minetest.net/viewtopic.php?t=" + str(topic.topic_id))
|
||||
|
||||
page = int(request.args.get("page") or 1)
|
||||
num = min(40, int(request.args.get("n") or 100))
|
||||
page = get_int_or_abort(request.args.get("page"), 1)
|
||||
num = min(40, get_int_or_abort(request.args.get("n"), 100))
|
||||
query = query.paginate(page, num, True)
|
||||
|
||||
search = request.args.get("q")
|
||||
type_name = request.args.get("type")
|
||||
|
||||
next_url = url_for("packages_page", type=type_name, q=search, page=query.next_num) \
|
||||
next_url = url_for("packages.list_all", type=type_name, q=search, page=query.next_num) \
|
||||
if query.has_next else None
|
||||
prev_url = url_for("packages_page", type=type_name, q=search, page=query.prev_num) \
|
||||
prev_url = url_for("packages.list_all", type=type_name, q=search, page=query.prev_num) \
|
||||
if query.has_prev else None
|
||||
|
||||
topics = None
|
||||
@@ -79,9 +82,9 @@ def getReleases(package):
|
||||
return package.releases.filter_by(approved=True).limit(5)
|
||||
|
||||
|
||||
@app.route("/packages/<author>/<name>/")
|
||||
@bp.route("/packages/<author>/<name>/")
|
||||
@is_package_page
|
||||
def package_page(package):
|
||||
def view(package):
|
||||
clearNotifications(package.getDetailsURL())
|
||||
|
||||
alternatives = None
|
||||
@@ -147,9 +150,9 @@ def package_page(package):
|
||||
threads=threads.all())
|
||||
|
||||
|
||||
@app.route("/packages/<author>/<name>/download/")
|
||||
@bp.route("/packages/<author>/<name>/download/")
|
||||
@is_package_page
|
||||
def package_download_page(package):
|
||||
def download(package):
|
||||
release = package.getDownloadRelease()
|
||||
|
||||
if release is None:
|
||||
@@ -180,16 +183,16 @@ class PackageForm(FlaskForm):
|
||||
tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title)
|
||||
harddep_str = StringField("Hard Dependencies", [Optional()])
|
||||
softdep_str = StringField("Soft Dependencies", [Optional()])
|
||||
repo = StringField("VCS Repository URL", [Optional(), URL()])
|
||||
website = StringField("Website URL", [Optional(), URL()])
|
||||
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()])
|
||||
repo = StringField("VCS Repository URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
website = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
|
||||
submit = SubmitField("Save")
|
||||
|
||||
@app.route("/packages/new/", methods=["GET", "POST"])
|
||||
@app.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
|
||||
@bp.route("/packages/new/", methods=["GET", "POST"])
|
||||
@bp.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create_edit_package_page(author=None, name=None):
|
||||
def create_edit(author=None, name=None):
|
||||
package = None
|
||||
form = None
|
||||
if author is None:
|
||||
@@ -201,11 +204,11 @@ def create_edit_package_page(author=None, name=None):
|
||||
author = User.query.filter_by(username=author).first()
|
||||
if author is None:
|
||||
flash("Unable to find that user", "error")
|
||||
return redirect(url_for("create_edit_package_page"))
|
||||
return redirect(url_for("packages.create_edit"))
|
||||
|
||||
if not author.checkPerm(current_user, Permission.CHANGE_AUTHOR):
|
||||
flash("Permission denied", "error")
|
||||
return redirect(url_for("create_edit_package_page"))
|
||||
return redirect(url_for("packages.create_edit"))
|
||||
|
||||
else:
|
||||
package = getPackageByInfo(author, name)
|
||||
@@ -238,7 +241,7 @@ def create_edit_package_page(author=None, name=None):
|
||||
Package.query.filter_by(name=form["name"].data, author_id=author.id).delete()
|
||||
else:
|
||||
flash("Package already exists!", "error")
|
||||
return redirect(url_for("create_edit_package_page"))
|
||||
return redirect(url_for("packages.create_edit"))
|
||||
|
||||
package = Package()
|
||||
package.author = author
|
||||
@@ -247,7 +250,7 @@ def create_edit_package_page(author=None, name=None):
|
||||
elif package.approved and package.name != form.name.data and \
|
||||
not package.checkPerm(current_user, Permission.CHANGE_NAME):
|
||||
flash("Unable to change package name", "danger")
|
||||
return redirect(url_for("create_edit_package_page", author=author, name=name))
|
||||
return redirect(url_for("packages.create_edit", author=author, name=name))
|
||||
|
||||
else:
|
||||
triggerNotif(package.author, current_user,
|
||||
@@ -288,7 +291,7 @@ def create_edit_package_page(author=None, name=None):
|
||||
next_url = package.getDetailsURL()
|
||||
if wasNew and package.repo is not None:
|
||||
task = importRepoScreenshot.delay(package.id)
|
||||
next_url = url_for("check_task", id=task.id, r=next_url)
|
||||
next_url = url_for("tasks.check", id=task.id, r=next_url)
|
||||
|
||||
if wasNew and ("WTFPL" in package.license.name or "WTFPL" in package.media_license.name):
|
||||
next_url = url_for("flatpage", path="help/wtfpl", r=next_url)
|
||||
@@ -305,10 +308,10 @@ def create_edit_package_page(author=None, name=None):
|
||||
packages=package_query.all(), \
|
||||
mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all())
|
||||
|
||||
@app.route("/packages/<author>/<name>/approve/", methods=["POST"])
|
||||
@bp.route("/packages/<author>/<name>/approve/", methods=["POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def approve_package_page(package):
|
||||
def approve(package):
|
||||
if not package.checkPerm(current_user, Permission.APPROVE_NEW):
|
||||
flash("You don't have permission to do that.", "error")
|
||||
|
||||
@@ -329,10 +332,10 @@ def approve_package_page(package):
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
|
||||
@app.route("/packages/<author>/<name>/remove/", methods=["GET", "POST"])
|
||||
@bp.route("/packages/<author>/<name>/remove/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def remove_package_page(package):
|
||||
def remove(package):
|
||||
if request.method == "GET":
|
||||
return render_template("packages/remove.html", package=package)
|
||||
|
||||
@@ -343,7 +346,7 @@ def remove_package_page(package):
|
||||
|
||||
package.soft_deleted = True
|
||||
|
||||
url = url_for("user_profile_page", username=package.author.username)
|
||||
url = url_for("users.profile", username=package.author.username)
|
||||
triggerNotif(package.author, current_user,
|
||||
"{} deleted".format(package.title), url)
|
||||
db.session.commit()
|
||||
@@ -17,10 +17,12 @@
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from app import app
|
||||
from app.models import *
|
||||
from app.tasks.importtasks import makeVCSRelease
|
||||
|
||||
from . import bp
|
||||
|
||||
from app.rediscache import has_key, set_key, make_download_key
|
||||
from app.models import *
|
||||
from app.tasks.importtasks import makeVCSRelease, checkZipRelease
|
||||
from app.utils import *
|
||||
|
||||
from celery import uuid
|
||||
@@ -54,7 +56,7 @@ class CreatePackageReleaseForm(FlaskForm):
|
||||
class EditPackageReleaseForm(FlaskForm):
|
||||
title = StringField("Title", [InputRequired(), Length(1, 30)])
|
||||
url = StringField("URL", [URL])
|
||||
task_id = StringField("Task ID")
|
||||
task_id = StringField("Task ID", filters = [lambda x: x or None])
|
||||
approved = BooleanField("Is Approved")
|
||||
min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
|
||||
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||
@@ -62,10 +64,10 @@ class EditPackageReleaseForm(FlaskForm):
|
||||
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||
submit = SubmitField("Save")
|
||||
|
||||
@app.route("/packages/<author>/<name>/releases/new/", methods=["GET", "POST"])
|
||||
@bp.route("/packages/<author>/<name>/releases/new/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def create_release_page(package):
|
||||
def create_release(package):
|
||||
if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
@@ -94,53 +96,63 @@ def create_release_page(package):
|
||||
triggerNotif(package.author, current_user, msg, rel.getEditURL())
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("check_task", id=rel.task_id, r=rel.getEditURL()))
|
||||
return redirect(url_for("tasks.check", id=rel.task_id, r=rel.getEditURL()))
|
||||
else:
|
||||
uploadedPath = doFileUpload(form.fileUpload.data, "zip", "a zip file")
|
||||
if uploadedPath is not None:
|
||||
uploadedUrl, uploadedPath = doFileUpload(form.fileUpload.data, "zip", "a zip file")
|
||||
if uploadedUrl is not None:
|
||||
rel = PackageRelease()
|
||||
rel.package = package
|
||||
rel.title = form["title"].data
|
||||
rel.url = uploadedPath
|
||||
rel.url = uploadedUrl
|
||||
rel.task_id = uuid()
|
||||
rel.min_rel = form["min_rel"].data.getActual()
|
||||
rel.max_rel = form["max_rel"].data.getActual()
|
||||
rel.approve(current_user)
|
||||
db.session.add(rel)
|
||||
db.session.commit()
|
||||
|
||||
checkZipRelease.apply_async((rel.id, uploadedPath), task_id=rel.task_id)
|
||||
|
||||
msg = "{}: Release {} created".format(package.title, rel.title)
|
||||
triggerNotif(package.author, current_user, msg, rel.getEditURL())
|
||||
db.session.commit()
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
return redirect(url_for("tasks.check", id=rel.task_id, r=rel.getEditURL()))
|
||||
|
||||
return render_template("packages/release_new.html", package=package, form=form)
|
||||
|
||||
@app.route("/packages/<author>/<name>/releases/<id>/download/")
|
||||
@bp.route("/packages/<author>/<name>/releases/<id>/download/")
|
||||
@is_package_page
|
||||
def download_release_page(package, id):
|
||||
def download_release(package, id):
|
||||
release = PackageRelease.query.get(id)
|
||||
if release is None or release.package != package:
|
||||
abort(404)
|
||||
|
||||
if release is None:
|
||||
if "application/zip" in request.accept_mimetypes and \
|
||||
not "text/html" in request.accept_mimetypes:
|
||||
return "", 204
|
||||
else:
|
||||
flash("No download available.", "error")
|
||||
return redirect(package.getDetailsURL())
|
||||
else:
|
||||
PackageRelease.query.filter_by(id=release.id).update({
|
||||
"downloads": PackageRelease.downloads + 1
|
||||
})
|
||||
db.session.commit()
|
||||
ip = request.headers.get("X-Forwarded-For") or request.remote_addr
|
||||
if ip is not None:
|
||||
key = make_download_key(ip, release.package)
|
||||
if not has_key(key):
|
||||
set_key(key, "true")
|
||||
|
||||
return redirect(release.url, code=300)
|
||||
bonus = 1
|
||||
if not package.getIsFOSS():
|
||||
bonus *= 0.1
|
||||
|
||||
@app.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
|
||||
PackageRelease.query.filter_by(id=release.id).update({
|
||||
"downloads": PackageRelease.downloads + 1
|
||||
})
|
||||
|
||||
Package.query.filter_by(id=package.id).update({
|
||||
"score": Package.score + bonus
|
||||
})
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(release.url, code=300)
|
||||
|
||||
@bp.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def edit_release_page(package, id):
|
||||
def edit_release(package, id):
|
||||
release = PackageRelease.query.get(id)
|
||||
if release is None or release.package != package:
|
||||
abort(404)
|
||||
@@ -154,6 +166,11 @@ def edit_release_page(package, id):
|
||||
|
||||
# Initial form class from post data and default data
|
||||
form = EditPackageReleaseForm(formdata=request.form, obj=release)
|
||||
|
||||
# HACK: fix bug in wtforms
|
||||
if request.method == "GET":
|
||||
form.approved.data = release.approved
|
||||
|
||||
if request.method == "POST" and form.validate():
|
||||
wasApproved = release.approved
|
||||
if canEdit:
|
||||
@@ -164,7 +181,7 @@ def edit_release_page(package, id):
|
||||
if package.checkPerm(current_user, Permission.CHANGE_RELEASE_URL):
|
||||
release.url = form["url"].data
|
||||
release.task_id = form["task_id"].data
|
||||
if release.task_id.strip() == "":
|
||||
if release.task_id is not None:
|
||||
release.task_id = None
|
||||
|
||||
if canApprove:
|
||||
@@ -190,10 +207,10 @@ class BulkReleaseForm(FlaskForm):
|
||||
submit = SubmitField("Update")
|
||||
|
||||
|
||||
@app.route("/packages/<author>/<name>/releases/bulk_change/", methods=["GET", "POST"])
|
||||
@bp.route("/packages/<author>/<name>/releases/bulk_change/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def bulk_change_release_page(package):
|
||||
def bulk_change_release(package):
|
||||
if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
@@ -216,3 +233,20 @@ def bulk_change_release_page(package):
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
return render_template("packages/release_bulk_change.html", package=package, form=form)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/releases/<id>/delete/", methods=["POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def delete_release(package, id):
|
||||
release = PackageRelease.query.get(id)
|
||||
if release is None or release.package != package:
|
||||
abort(404)
|
||||
|
||||
if not release.checkPerm(current_user, Permission.DELETE_RELEASE):
|
||||
return redirect(release.getEditURL())
|
||||
|
||||
db.session.delete(release)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(package.getDetailsURL())
|
||||
@@ -17,9 +17,10 @@
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from app import app
|
||||
from app.models import *
|
||||
|
||||
from . import bp
|
||||
|
||||
from app.models import *
|
||||
from app.utils import *
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
@@ -39,23 +40,23 @@ class EditScreenshotForm(FlaskForm):
|
||||
delete = BooleanField("Delete")
|
||||
submit = SubmitField("Save")
|
||||
|
||||
@app.route("/packages/<author>/<name>/screenshots/new/", methods=["GET", "POST"])
|
||||
@bp.route("/packages/<author>/<name>/screenshots/new/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def create_screenshot_page(package, id=None):
|
||||
def create_screenshot(package, id=None):
|
||||
if not package.checkPerm(current_user, Permission.ADD_SCREENSHOTS):
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
# Initial form class from post data and default data
|
||||
form = CreateScreenshotForm()
|
||||
if request.method == "POST" and form.validate():
|
||||
uploadedPath = doFileUpload(form.fileUpload.data, "image",
|
||||
uploadedUrl, uploadedPath = doFileUpload(form.fileUpload.data, "image",
|
||||
"a PNG or JPG image file")
|
||||
if uploadedPath is not None:
|
||||
if uploadedUrl is not None:
|
||||
ss = PackageScreenshot()
|
||||
ss.package = package
|
||||
ss.title = form["title"].data or "Untitled"
|
||||
ss.url = uploadedPath
|
||||
ss.url = uploadedUrl
|
||||
ss.approved = package.checkPerm(current_user, Permission.APPROVE_SCREENSHOT)
|
||||
db.session.add(ss)
|
||||
|
||||
@@ -67,10 +68,10 @@ def create_screenshot_page(package, id=None):
|
||||
|
||||
return render_template("packages/screenshot_new.html", package=package, form=form)
|
||||
|
||||
@app.route("/packages/<author>/<name>/screenshots/<id>/edit/", methods=["GET", "POST"])
|
||||
@bp.route("/packages/<author>/<name>/screenshots/<id>/edit/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def edit_screenshot_page(package, id):
|
||||
def edit_screenshot(package, id):
|
||||
screenshot = PackageScreenshot.query.get(id)
|
||||
if screenshot is None or screenshot.package != package:
|
||||
abort(404)
|
||||
@@ -18,28 +18,28 @@
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
import flask_menu as menu
|
||||
from app import app, csrf
|
||||
from app import csrf
|
||||
from app.models import *
|
||||
from app.tasks import celery, TaskError
|
||||
from app.tasks.importtasks import getMeta
|
||||
from app.utils import shouldReturnJson
|
||||
# from celery.result import AsyncResult
|
||||
|
||||
from app.utils import *
|
||||
|
||||
bp = Blueprint("tasks", __name__)
|
||||
|
||||
@csrf.exempt
|
||||
@app.route("/tasks/getmeta/new/", methods=["POST"])
|
||||
@bp.route("/tasks/getmeta/new/", methods=["POST"])
|
||||
@login_required
|
||||
def new_getmeta_page():
|
||||
def start_getmeta():
|
||||
author = request.args.get("author")
|
||||
author = current_user.forums_username if author is None else author
|
||||
aresult = getMeta.delay(request.args.get("url"), author)
|
||||
return jsonify({
|
||||
"poll_url": url_for("check_task", id=aresult.id),
|
||||
"poll_url": url_for("tasks.check", id=aresult.id),
|
||||
})
|
||||
|
||||
@app.route("/tasks/<id>/")
|
||||
def check_task(id):
|
||||
@bp.route("/tasks/<id>/")
|
||||
def check(id):
|
||||
result = celery.AsyncResult(id)
|
||||
status = result.status
|
||||
traceback = result.traceback
|
||||
@@ -16,8 +16,10 @@
|
||||
|
||||
|
||||
from flask import *
|
||||
|
||||
bp = Blueprint("threads", __name__)
|
||||
|
||||
from flask_user import *
|
||||
from app import app
|
||||
from app.models import *
|
||||
from app.utils import triggerNotif, clearNotifications
|
||||
|
||||
@@ -27,17 +29,17 @@ from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
|
||||
@app.route("/threads/")
|
||||
def threads_page():
|
||||
@bp.route("/threads/")
|
||||
def list_all():
|
||||
query = Thread.query
|
||||
if not Permission.SEE_THREAD.check(current_user):
|
||||
query = query.filter_by(private=False)
|
||||
return render_template("threads/list.html", threads=query.all())
|
||||
|
||||
|
||||
@app.route("/threads/<int:id>/subscribe/", methods=["POST"])
|
||||
@bp.route("/threads/<int:id>/subscribe/", methods=["POST"])
|
||||
@login_required
|
||||
def thread_subscribe_page(id):
|
||||
def subscribe(id):
|
||||
thread = Thread.query.get(id)
|
||||
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||
abort(404)
|
||||
@@ -49,12 +51,12 @@ def thread_subscribe_page(id):
|
||||
thread.watchers.append(current_user)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("thread_page", id=id))
|
||||
return redirect(url_for("threads.view", id=id))
|
||||
|
||||
|
||||
@app.route("/threads/<int:id>/unsubscribe/", methods=["POST"])
|
||||
@bp.route("/threads/<int:id>/unsubscribe/", methods=["POST"])
|
||||
@login_required
|
||||
def thread_unsubscribe_page(id):
|
||||
def unsubscribe(id):
|
||||
thread = Thread.query.get(id)
|
||||
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||
abort(404)
|
||||
@@ -66,12 +68,12 @@ def thread_unsubscribe_page(id):
|
||||
else:
|
||||
flash("Not subscribed to thread", "success")
|
||||
|
||||
return redirect(url_for("thread_page", id=id))
|
||||
return redirect(url_for("threads.view", id=id))
|
||||
|
||||
|
||||
@app.route("/threads/<int:id>/", methods=["GET", "POST"])
|
||||
def thread_page(id):
|
||||
clearNotifications(url_for("thread_page", id=id))
|
||||
@bp.route("/threads/<int:id>/", methods=["GET", "POST"])
|
||||
def view(id):
|
||||
clearNotifications(url_for("threads.view", id=id))
|
||||
|
||||
thread = Thread.query.get(id)
|
||||
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||
@@ -85,7 +87,7 @@ def thread_page(id):
|
||||
if package:
|
||||
return redirect(package.getDetailsURL())
|
||||
else:
|
||||
return redirect(url_for("home_page"))
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
if len(comment) <= 500 and len(comment) > 3:
|
||||
reply = ThreadReply()
|
||||
@@ -106,11 +108,11 @@ def thread_page(id):
|
||||
|
||||
for user in thread.watchers:
|
||||
if user != current_user:
|
||||
triggerNotif(user, current_user, msg, url_for("thread_page", id=thread.id))
|
||||
triggerNotif(user, current_user, msg, url_for("threads.view", id=thread.id))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("thread_page", id=id))
|
||||
return redirect(url_for("threads.view", id=id))
|
||||
|
||||
else:
|
||||
flash("Comment needs to be between 3 and 500 characters.")
|
||||
@@ -124,9 +126,9 @@ class ThreadForm(FlaskForm):
|
||||
private = BooleanField("Private")
|
||||
submit = SubmitField("Open Thread")
|
||||
|
||||
@app.route("/threads/new/", methods=["GET", "POST"])
|
||||
@bp.route("/threads/new/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def new_thread_page():
|
||||
def new():
|
||||
form = ThreadForm(formdata=request.form)
|
||||
|
||||
package = None
|
||||
@@ -140,7 +142,7 @@ def new_thread_page():
|
||||
abort(403)
|
||||
|
||||
def_is_private = request.args.get("private") or False
|
||||
if package is None or not package.approved:
|
||||
if package is None:
|
||||
def_is_private = True
|
||||
allow_change = package and package.approved
|
||||
is_review_thread = package and not package.approved
|
||||
@@ -148,12 +150,12 @@ def new_thread_page():
|
||||
# Check that user can make the thread
|
||||
if not package.checkPerm(current_user, Permission.CREATE_THREAD):
|
||||
flash("Unable to create thread!", "error")
|
||||
return redirect(url_for("home_page"))
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
# Only allow creating one thread when not approved
|
||||
elif is_review_thread and package.review_thread is not None:
|
||||
flash("A review thread already exists!", "error")
|
||||
return redirect(url_for("thread_page", id=package.review_thread.id))
|
||||
return redirect(url_for("threads.view", id=package.review_thread.id))
|
||||
|
||||
elif not current_user.canOpenThreadRL():
|
||||
flash("Please wait before opening another thread", "danger")
|
||||
@@ -161,7 +163,7 @@ def new_thread_page():
|
||||
if package:
|
||||
return redirect(package.getDetailsURL())
|
||||
else:
|
||||
return redirect(url_for("home_page"))
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
# Set default values
|
||||
elif request.method == "GET":
|
||||
@@ -197,16 +199,16 @@ def new_thread_page():
|
||||
notif_msg = None
|
||||
if package is not None:
|
||||
notif_msg = "New thread '{}' on package {}".format(thread.title, package.title)
|
||||
triggerNotif(package.author, current_user, notif_msg, url_for("thread_page", id=thread.id))
|
||||
triggerNotif(package.author, current_user, notif_msg, url_for("threads.view", id=thread.id))
|
||||
else:
|
||||
notif_msg = "New thread '{}'".format(thread.title)
|
||||
|
||||
for user in User.query.filter(User.rank >= UserRank.EDITOR).all():
|
||||
triggerNotif(user, current_user, notif_msg, url_for("thread_page", id=thread.id))
|
||||
triggerNotif(user, current_user, notif_msg, url_for("threads.view", id=thread.id))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("thread_page", id=thread.id))
|
||||
return redirect(url_for("threads.view", id=thread.id))
|
||||
|
||||
|
||||
return render_template("threads/new.html", form=form, allow_private_change=allow_change, package=package)
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
|
||||
from flask import *
|
||||
from app import app
|
||||
|
||||
bp = Blueprint("thumbnails", __name__)
|
||||
|
||||
import os
|
||||
from PIL import Image
|
||||
@@ -24,10 +25,10 @@ from PIL import Image
|
||||
ALLOWED_RESOLUTIONS=[(100,67), (270,180), (350,233)]
|
||||
|
||||
def mkdir(path):
|
||||
assert path != "" and path is not None
|
||||
if not os.path.isdir(path):
|
||||
os.mkdir(path)
|
||||
|
||||
mkdir("app/public/thumbnails/")
|
||||
|
||||
def resize_and_crop(img_path, modified_path, size):
|
||||
img = Image.open(img_path)
|
||||
@@ -57,17 +58,22 @@ def resize_and_crop(img_path, modified_path, size):
|
||||
img.save(modified_path)
|
||||
|
||||
|
||||
@app.route("/thumbnails/<int:level>/<img>")
|
||||
@bp.route("/thumbnails/<int:level>/<img>")
|
||||
def make_thumbnail(img, level):
|
||||
if level > len(ALLOWED_RESOLUTIONS) or level <= 0:
|
||||
abort(403)
|
||||
|
||||
w, h = ALLOWED_RESOLUTIONS[level - 1]
|
||||
|
||||
mkdir("app/public/thumbnails/{:d}/".format(level))
|
||||
upload_dir = current_app.config["UPLOAD_DIR"]
|
||||
thumbnail_dir = current_app.config["THUMBNAIL_DIR"]
|
||||
mkdir(thumbnail_dir)
|
||||
|
||||
cache_filepath = "public/thumbnails/{:d}/{}".format(level, img)
|
||||
source_filepath = "public/uploads/" + img
|
||||
output_dir = os.path.join(thumbnail_dir, str(level))
|
||||
mkdir(output_dir)
|
||||
|
||||
resize_and_crop("app/" + source_filepath, "app/" + cache_filepath, (w, h))
|
||||
cache_filepath = os.path.join(output_dir, img)
|
||||
source_filepath = os.path.join(upload_dir, img)
|
||||
|
||||
resize_and_crop(source_filepath, cache_filepath, (w, h))
|
||||
return send_file(cache_filepath)
|
||||
@@ -14,24 +14,25 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
import flask_menu as menu
|
||||
from app import app
|
||||
from app.models import *
|
||||
from app.querybuilder import QueryBuilder
|
||||
from app.utils import get_int_or_abort
|
||||
|
||||
@app.route("/todo/", methods=["GET", "POST"])
|
||||
bp = Blueprint("todo", __name__)
|
||||
|
||||
@bp.route("/todo/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def todo_page():
|
||||
def view():
|
||||
canApproveNew = Permission.APPROVE_NEW.check(current_user)
|
||||
canApproveRel = Permission.APPROVE_RELEASE.check(current_user)
|
||||
canApproveScn = Permission.APPROVE_SCREENSHOT.check(current_user)
|
||||
|
||||
packages = None
|
||||
if canApproveNew:
|
||||
packages = Package.query.filter_by(approved=False, soft_deleted=False).all()
|
||||
packages = Package.query.filter_by(approved=False, soft_deleted=False).order_by(db.desc(Package.created_at)).all()
|
||||
|
||||
releases = None
|
||||
if canApproveRel:
|
||||
@@ -51,7 +52,7 @@ def todo_page():
|
||||
|
||||
PackageScreenshot.query.update({ "approved": True })
|
||||
db.session.commit()
|
||||
return redirect(url_for("todo_page"))
|
||||
return redirect(url_for("todo.view"))
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
@@ -69,9 +70,9 @@ def todo_page():
|
||||
topics_to_add=topics_to_add, total_topics=total_topics)
|
||||
|
||||
|
||||
@app.route("/todo/topics/")
|
||||
@bp.route("/todo/topics/")
|
||||
@login_required
|
||||
def todo_topics_page():
|
||||
def topics():
|
||||
qb = QueryBuilder(request.args)
|
||||
qb.setSortIfNone("date")
|
||||
query = qb.buildTopicQuery()
|
||||
@@ -82,16 +83,16 @@ def todo_topics_page():
|
||||
total = tmp_q.count()
|
||||
topic_count = query.count()
|
||||
|
||||
page = int(request.args.get("page") or 1)
|
||||
num = int(request.args.get("n") or 100)
|
||||
page = get_int_or_abort(request.args.get("page"), 1)
|
||||
num = get_int_or_abort(request.args.get("n"), 100)
|
||||
if num > 100 and not current_user.rank.atLeast(UserRank.EDITOR):
|
||||
num = 100
|
||||
|
||||
query = query.paginate(page, num, True)
|
||||
next_url = url_for("todo_topics_page", page=query.next_num, query=qb.search, \
|
||||
next_url = url_for("todo.topics", page=query.next_num, query=qb.search, \
|
||||
show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
|
||||
if query.has_next else None
|
||||
prev_url = url_for("todo_topics_page", page=query.prev_num, query=qb.search, \
|
||||
prev_url = url_for("todo.topics", page=query.prev_num, query=qb.search, \
|
||||
show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
|
||||
if query.has_prev else None
|
||||
|
||||
5
app/blueprints/users/__init__.py
Normal file
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
|
||||
import flask_menu as menu
|
||||
from flask_github import GitHub
|
||||
from app import app, github
|
||||
from . import bp
|
||||
from app import github
|
||||
from app.models import *
|
||||
from app.utils import loginUser
|
||||
|
||||
@app.route("/user/github/start/")
|
||||
def github_signin_page():
|
||||
@bp.route("/user/github/start/")
|
||||
def github_signin():
|
||||
return github.authorize("")
|
||||
|
||||
@app.route("/user/github/callback/")
|
||||
@bp.route("/user/github/callback/")
|
||||
@github.authorized_handler
|
||||
def github_authorized(oauth_token):
|
||||
next_url = request.args.get("next")
|
||||
@@ -53,21 +54,21 @@ def github_authorized(oauth_token):
|
||||
current_user.github_username = username
|
||||
db.session.commit()
|
||||
flash("Linked github to account", "success")
|
||||
return redirect(url_for("home_page"))
|
||||
return redirect(url_for("homepage.home"))
|
||||
else:
|
||||
flash("Github account is already associated with another user", "danger")
|
||||
return redirect(url_for("home_page"))
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
# If not logged in, log in
|
||||
else:
|
||||
if userByGithub is None:
|
||||
flash("Unable to find an account for that Github user", "error")
|
||||
return redirect(url_for("user_claim_page"))
|
||||
return redirect(url_for("users.claim"))
|
||||
elif loginUser(userByGithub):
|
||||
if current_user.password is None:
|
||||
return redirect(next_url or url_for("set_password_page", optional=True))
|
||||
if not current_user.hasPassword():
|
||||
return redirect(next_url or url_for("users.set_password", optional=True))
|
||||
else:
|
||||
return redirect(next_url or url_for("home_page"))
|
||||
return redirect(next_url or url_for("homepage.home"))
|
||||
else:
|
||||
flash("Authorization failed [err=gh-login-failed]", "danger")
|
||||
return redirect(url_for("user.login"))
|
||||
@@ -18,7 +18,8 @@
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from flask_login import login_user, logout_user
|
||||
from app import app, markdown
|
||||
from app import markdown
|
||||
from . import bp
|
||||
from app.models import *
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
@@ -31,21 +32,21 @@ from app.tasks.phpbbparser import getProfile
|
||||
# Define the User profile form
|
||||
class UserProfileForm(FlaskForm):
|
||||
display_name = StringField("Display name", [Optional(), Length(2, 20)])
|
||||
email = StringField("Email", [Optional(), Email()])
|
||||
website_url = StringField("Website URL", [Optional(), URL()])
|
||||
donate_url = StringField("Donation URL", [Optional(), URL()])
|
||||
email = StringField("Email", [Optional(), Email()], filters = [lambda x: x or None])
|
||||
website_url = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
donate_url = StringField("Donation URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER)
|
||||
submit = SubmitField("Save")
|
||||
|
||||
|
||||
@app.route("/users/", methods=["GET"])
|
||||
def user_list_page():
|
||||
@bp.route("/users/", methods=["GET"])
|
||||
def list_all():
|
||||
users = User.query.order_by(db.desc(User.rank), db.asc(User.display_name)).all()
|
||||
return render_template("users/list.html", users=users)
|
||||
|
||||
|
||||
@app.route("/users/<username>/", methods=["GET", "POST"])
|
||||
def user_profile_page(username):
|
||||
@bp.route("/users/<username>/", methods=["GET", "POST"])
|
||||
def profile(username):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
@@ -85,13 +86,13 @@ def user_profile_page(username):
|
||||
db.session.commit()
|
||||
|
||||
task = sendVerifyEmail.delay(newEmail, token)
|
||||
return redirect(url_for("check_task", id=task.id, r=url_for("user_profile_page", username=username)))
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("users.profile", username=username)))
|
||||
|
||||
# Save user_profile
|
||||
db.session.commit()
|
||||
|
||||
# Redirect to home page
|
||||
return redirect(url_for("user_profile_page", username=username))
|
||||
return redirect(url_for("users.profile", username=username))
|
||||
|
||||
packages = user.packages.filter_by(soft_deleted=False)
|
||||
if not current_user.is_authenticated or (user != current_user and not current_user.canAccessTodoList()):
|
||||
@@ -107,11 +108,11 @@ def user_profile_page(username):
|
||||
.all()
|
||||
|
||||
# Process GET or invalid POST
|
||||
return render_template("users/user_profile_page.html",
|
||||
return render_template("users/profile.html",
|
||||
user=user, form=form, packages=packages, topics_to_add=topics_to_add)
|
||||
|
||||
|
||||
@app.route("/users/<username>/check/", methods=["POST"])
|
||||
@bp.route("/users/<username>/check/", methods=["POST"])
|
||||
@login_required
|
||||
def user_check(username):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
@@ -125,9 +126,9 @@ def user_check(username):
|
||||
abort(404)
|
||||
|
||||
task = checkForumAccount.delay(user.forums_username)
|
||||
next_url = url_for("user_profile_page", username=username)
|
||||
next_url = url_for("users.profile", username=username)
|
||||
|
||||
return redirect(url_for("check_task", id=task.id, r=next_url))
|
||||
return redirect(url_for("tasks.check", id=task.id, r=next_url))
|
||||
|
||||
|
||||
class SendEmailForm(FlaskForm):
|
||||
@@ -136,14 +137,14 @@ class SendEmailForm(FlaskForm):
|
||||
submit = SubmitField("Send")
|
||||
|
||||
|
||||
@app.route("/users/<username>/email/", methods=["GET", "POST"])
|
||||
@bp.route("/users/<username>/email/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def send_email_page(username):
|
||||
def send_email(username):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
abort(404)
|
||||
|
||||
next_url = url_for("user_profile_page", username=user.username)
|
||||
next_url = url_for("users.profile", username=user.username)
|
||||
|
||||
if user.email is None:
|
||||
flash("User has no email address!", "error")
|
||||
@@ -154,7 +155,7 @@ def send_email_page(username):
|
||||
text = form.text.data
|
||||
html = markdown(text)
|
||||
task = sendEmailRaw.delay([user.email], form.subject.data, text, html)
|
||||
return redirect(url_for("check_task", id=task.id, r=next_url))
|
||||
return redirect(url_for("tasks.check", id=task.id, r=next_url))
|
||||
|
||||
return render_template("users/send_email.html", form=form)
|
||||
|
||||
@@ -162,14 +163,14 @@ def send_email_page(username):
|
||||
|
||||
class SetPasswordForm(FlaskForm):
|
||||
email = StringField("Email", [Optional(), Email()])
|
||||
password = PasswordField("New password", [InputRequired(), Length(2, 20)])
|
||||
password2 = PasswordField("Verify password", [InputRequired(), Length(2, 20)])
|
||||
password = PasswordField("New password", [InputRequired(), Length(2, 100)])
|
||||
password2 = PasswordField("Verify password", [InputRequired(), Length(2, 100)])
|
||||
submit = SubmitField("Save")
|
||||
|
||||
@app.route("/user/set-password/", methods=["GET", "POST"])
|
||||
@bp.route("/user/set-password/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def set_password_page():
|
||||
if current_user.password is not None:
|
||||
def set_password():
|
||||
if current_user.hasPassword():
|
||||
return redirect(url_for("user.change_password"))
|
||||
|
||||
form = SetPasswordForm(request.form)
|
||||
@@ -184,10 +185,11 @@ def set_password_page():
|
||||
hashed_password = user_manager.hash_password(form.password.data)
|
||||
|
||||
# Change password
|
||||
user_manager.update_password(current_user, hashed_password)
|
||||
current_user.password = hashed_password
|
||||
db.session.commit()
|
||||
|
||||
# Send 'password_changed' email
|
||||
if user_manager.enable_email and user_manager.send_password_changed_email and current_user.email:
|
||||
if user_manager.USER_ENABLE_EMAIL and current_user.email:
|
||||
emails.send_password_changed_email(current_user)
|
||||
|
||||
# Send password_changed signal
|
||||
@@ -208,17 +210,17 @@ def set_password_page():
|
||||
db.session.commit()
|
||||
|
||||
task = sendVerifyEmail.delay(newEmail, token)
|
||||
return redirect(url_for("check_task", id=task.id, r=url_for("user_profile_page", username=current_user.username)))
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("users.profile", username=current_user.username)))
|
||||
else:
|
||||
return redirect(url_for("user_profile_page", username=current_user.username))
|
||||
return redirect(url_for("user.login"))
|
||||
else:
|
||||
flash("Passwords do not match", "error")
|
||||
|
||||
return render_template("users/set_password.html", form=form, optional=request.args.get("optional"))
|
||||
|
||||
|
||||
@app.route("/user/claim/", methods=["GET", "POST"])
|
||||
def user_claim_page():
|
||||
@bp.route("/user/claim/", methods=["GET", "POST"])
|
||||
def claim():
|
||||
username = request.args.get("username")
|
||||
if username is None:
|
||||
username = ""
|
||||
@@ -227,16 +229,16 @@ def user_claim_page():
|
||||
user = User.query.filter_by(forums_username=username).first()
|
||||
if user and user.rank.atLeast(UserRank.NEW_MEMBER):
|
||||
flash("User has already been claimed", "error")
|
||||
return redirect(url_for("user_claim_page"))
|
||||
return redirect(url_for("users.claim"))
|
||||
elif user is None and method == "github":
|
||||
flash("Unable to get Github username for user", "error")
|
||||
return redirect(url_for("user_claim_page"))
|
||||
return redirect(url_for("users.claim"))
|
||||
elif user is None:
|
||||
flash("Unable to find that user", "error")
|
||||
return redirect(url_for("user_claim_page"))
|
||||
return redirect(url_for("users.claim"))
|
||||
|
||||
if user is not None and method == "github":
|
||||
return redirect(url_for("github_signin_page"))
|
||||
return redirect(url_for("users.github_signin"))
|
||||
|
||||
token = None
|
||||
if "forum_token" in session:
|
||||
@@ -253,12 +255,12 @@ def user_claim_page():
|
||||
flash("Invalid username", "error")
|
||||
elif ctype == "github":
|
||||
task = checkForumAccount.delay(username)
|
||||
return redirect(url_for("check_task", id=task.id, r=url_for("user_claim_page", username=username, method="github")))
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("users.claim", username=username, method="github")))
|
||||
elif ctype == "forum":
|
||||
user = User.query.filter_by(forums_username=username).first()
|
||||
if user is not None and user.rank.atLeast(UserRank.NEW_MEMBER):
|
||||
flash("That user has already been claimed!", "error")
|
||||
return redirect(url_for("user_claim_page"))
|
||||
return redirect(url_for("users.claim"))
|
||||
|
||||
# Get signature
|
||||
sig = None
|
||||
@@ -267,7 +269,7 @@ def user_claim_page():
|
||||
sig = profile.signature
|
||||
except IOError:
|
||||
flash("Unable to get forum signature - does the user exist?", "error")
|
||||
return redirect(url_for("user_claim_page", username=username))
|
||||
return redirect(url_for("users.claim", username=username))
|
||||
|
||||
# Look for key
|
||||
if token in sig:
|
||||
@@ -278,21 +280,21 @@ def user_claim_page():
|
||||
db.session.commit()
|
||||
|
||||
if loginUser(user):
|
||||
return redirect(url_for("set_password_page"))
|
||||
return redirect(url_for("users.set_password"))
|
||||
else:
|
||||
flash("Unable to login as user", "error")
|
||||
return redirect(url_for("user_claim_page", username=username))
|
||||
return redirect(url_for("users.claim", username=username))
|
||||
|
||||
else:
|
||||
flash("Could not find the key in your signature!", "error")
|
||||
return redirect(url_for("user_claim_page", username=username))
|
||||
return redirect(url_for("users.claim", username=username))
|
||||
else:
|
||||
flash("Unknown claim type", "error")
|
||||
|
||||
return render_template("users/claim.html", username=username, key=token)
|
||||
|
||||
@app.route("/users/verify/")
|
||||
def verify_email_page():
|
||||
@bp.route("/users/verify/")
|
||||
def verify_email():
|
||||
token = request.args.get("token")
|
||||
ver = UserEmailVerification.query.filter_by(token=token).first()
|
||||
if ver is None:
|
||||
@@ -303,6 +305,6 @@ def verify_email_page():
|
||||
db.session.commit()
|
||||
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("user_profile_page", username=current_user.username))
|
||||
return redirect(url_for("users.profile", username=current_user.username))
|
||||
else:
|
||||
return redirect(url_for("home_page"))
|
||||
return redirect(url_for("homepage.home"))
|
||||
@@ -1,46 +1,65 @@
|
||||
# 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 .models import *
|
||||
from .utils import make_flask_user_password
|
||||
|
||||
|
||||
import os, sys, datetime
|
||||
def populate(session):
|
||||
admin_user = User("rubenwardy")
|
||||
admin_user.active = True
|
||||
admin_user.password = make_flask_user_password("tuckfrump")
|
||||
admin_user.github_username = "rubenwardy"
|
||||
admin_user.forums_username = "rubenwardy"
|
||||
admin_user.rank = UserRank.ADMIN
|
||||
session.add(admin_user)
|
||||
|
||||
if not "FLASK_CONFIG" in os.environ:
|
||||
os.environ["FLASK_CONFIG"] = "../config.cfg"
|
||||
session.add(MinetestRelease("None", 0))
|
||||
session.add(MinetestRelease("0.4.16/17", 32))
|
||||
session.add(MinetestRelease("5.0", 37))
|
||||
session.add(MinetestRelease("5.1", 38))
|
||||
|
||||
delete_db = len(sys.argv) >= 2 and sys.argv[1].strip() == "-d"
|
||||
create_db = not (len(sys.argv) >= 2 and sys.argv[1].strip() == "-o")
|
||||
test_data = len(sys.argv) >= 2 and sys.argv[1].strip() == "-t" or not create_db
|
||||
tags = {}
|
||||
for tag in ["Inventory", "Mapgen", "Building", \
|
||||
"Mobs and NPCs", "Tools", "Player effects", \
|
||||
"Environment", "Transport", "Maintenance", "Plants and farming", \
|
||||
"PvP", "PvE", "Survival", "Creative", "Puzzle", "Multiplayer", "Singleplayer"]:
|
||||
row = Tag(tag)
|
||||
tags[row.name] = row
|
||||
session.add(row)
|
||||
|
||||
from app.models import *
|
||||
from app.utils import make_flask_user_password
|
||||
licenses = {}
|
||||
for license in ["GPLv2.1", "GPLv3", "LGPLv2.1", "LGPLv3", "AGPLv2.1", "AGPLv3",
|
||||
"Apache", "BSD 3-Clause", "BSD 2-Clause", "CC0", "CC-BY-SA",
|
||||
"CC-BY", "MIT", "ZLib", "Other (Free)"]:
|
||||
row = License(license)
|
||||
licenses[row.name] = row
|
||||
session.add(row)
|
||||
|
||||
for license in ["CC-BY-NC-SA", "Other (Non-free)"]:
|
||||
row = License(license, False)
|
||||
licenses[row.name] = row
|
||||
session.add(row)
|
||||
|
||||
|
||||
def populate_test_data(session):
|
||||
licenses = { x.name : x for x in License.query.all() }
|
||||
tags = { x.name : x for x in Tag.query.all() }
|
||||
admin_user = User.query.filter_by(rank=UserRank.ADMIN).first()
|
||||
v4 = MinetestRelease.query.filter_by(protocol=32).first()
|
||||
v50 = MinetestRelease.query.filter_by(protocol=37).first()
|
||||
v51 = MinetestRelease.query.filter_by(protocol=38).first()
|
||||
|
||||
def defineDummyData(licenses, tags, ruben):
|
||||
ez = User("Shara")
|
||||
ez.github_username = "Ezhh"
|
||||
ez.forums_username = "Shara"
|
||||
ez.rank = UserRank.EDITOR
|
||||
db.session.add(ez)
|
||||
session.add(ez)
|
||||
|
||||
not1 = Notification(ruben, ez, "Awards approved", "/packages/rubenwardy/awards/")
|
||||
db.session.add(not1)
|
||||
not1 = Notification(admin_user, ez, "Awards approved", "/packages/rubenwardy/awards/")
|
||||
session.add(not1)
|
||||
|
||||
jeija = User("Jeija")
|
||||
jeija.github_username = "Jeija"
|
||||
jeija.forums_username = "Jeija"
|
||||
db.session.add(jeija)
|
||||
session.add(jeija)
|
||||
|
||||
|
||||
mod = Package()
|
||||
@@ -48,8 +67,9 @@ def defineDummyData(licenses, tags, ruben):
|
||||
mod.name = "alpha"
|
||||
mod.title = "Alpha Test"
|
||||
mod.license = licenses["MIT"]
|
||||
mod.media_license = licenses["MIT"]
|
||||
mod.type = PackageType.MOD
|
||||
mod.author = ruben
|
||||
mod.author = admin_user
|
||||
mod.tags.append(tags["mapgen"])
|
||||
mod.tags.append(tags["environment"])
|
||||
mod.repo = "https://github.com/ezhh/other_worlds"
|
||||
@@ -57,22 +77,23 @@ def defineDummyData(licenses, tags, ruben):
|
||||
mod.forums = 16015
|
||||
mod.short_desc = "The content library should not be used yet as it is still in alpha"
|
||||
mod.desc = "This is the long desc"
|
||||
db.session.add(mod)
|
||||
session.add(mod)
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = mod
|
||||
rel.title = "v1.0.0"
|
||||
rel.url = "https://github.com/ezhh/handholds/archive/master.zip"
|
||||
rel.approved = True
|
||||
db.session.add(rel)
|
||||
session.add(rel)
|
||||
|
||||
mod1 = Package()
|
||||
mod1.approved = True
|
||||
mod1.name = "awards"
|
||||
mod1.title = "Awards"
|
||||
mod1.license = licenses["LGPLv2.1"]
|
||||
mod1.media_license = licenses["MIT"]
|
||||
mod1.type = PackageType.MOD
|
||||
mod1.author = ruben
|
||||
mod1.author = admin_user
|
||||
mod1.tags.append(tags["player_effects"])
|
||||
mod1.repo = "https://github.com/rubenwardy/awards"
|
||||
mod1.issueTracker = "https://github.com/rubenwardy/awards/issues"
|
||||
@@ -96,10 +117,11 @@ awards.register_achievement("award_mesefind",{
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = mod1
|
||||
rel.min_rel = v51
|
||||
rel.title = "v1.0.0"
|
||||
rel.url = "https://github.com/rubenwardy/awards/archive/master.zip"
|
||||
rel.approved = True
|
||||
db.session.add(rel)
|
||||
session.add(rel)
|
||||
|
||||
mod2 = Package()
|
||||
mod2.approved = True
|
||||
@@ -108,22 +130,13 @@ awards.register_achievement("award_mesefind",{
|
||||
mod2.tags.append(tags["tools"])
|
||||
mod2.type = PackageType.MOD
|
||||
mod2.license = licenses["LGPLv3"]
|
||||
mod2.media_license = licenses["MIT"]
|
||||
mod2.author = jeija
|
||||
mod2.repo = "https://github.com/minetest-mods/mesecons/"
|
||||
mod2.issueTracker = "https://github.com/minetest-mods/mesecons/issues"
|
||||
mod2.forums = 628
|
||||
mod2.short_desc = "Mesecons adds everything digital, from all kinds of sensors, switches, solar panels, detectors, pistons, lamps, sound blocks to advanced digital circuitry like logic gates and programmable blocks."
|
||||
mod2.desc = """
|
||||
########################################################################
|
||||
## __ __ _____ _____ _____ _____ _____ _ _ _____ ##
|
||||
## | \ / | | ___| | ___| | ___| | ___| | _ | | \ | | | ___| ##
|
||||
## | \/ | | |___ | |___ | |___ | | | | | | | \| | | |___ ##
|
||||
## | |\__/| | | ___| |___ | | ___| | | | | | | | | |___ | ##
|
||||
## | | | | | |___ ___| | | |___ | |___ | |_| | | |\ | ___| | ##
|
||||
## |_| |_| |_____| |_____| |_____| |_____| |_____| |_| \_| |_____| ##
|
||||
## ##
|
||||
########################################################################
|
||||
|
||||
MESECONS by Jeija and contributors
|
||||
|
||||
Mezzee-what?
|
||||
@@ -196,14 +209,15 @@ No warranty is provided, express or implied, for any part of the project.
|
||||
|
||||
"""
|
||||
|
||||
db.session.add(mod1)
|
||||
db.session.add(mod2)
|
||||
session.add(mod1)
|
||||
session.add(mod2)
|
||||
|
||||
mod = Package()
|
||||
mod.approved = True
|
||||
mod.name = "handholds"
|
||||
mod.title = "Handholds"
|
||||
mod.license = licenses["MIT"]
|
||||
mod.media_license = licenses["MIT"]
|
||||
mod.type = PackageType.MOD
|
||||
mod.author = ez
|
||||
mod.tags.append(tags["player_effects"])
|
||||
@@ -212,20 +226,22 @@ No warranty is provided, express or implied, for any part of the project.
|
||||
mod.forums = 17069
|
||||
mod.short_desc = "Adds hand holds and climbing thingies"
|
||||
mod.desc = "This is the long desc"
|
||||
db.session.add(mod)
|
||||
session.add(mod)
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = mod
|
||||
rel.title = "v1.0.0"
|
||||
rel.max_rel = v4
|
||||
rel.url = "https://github.com/ezhh/handholds/archive/master.zip"
|
||||
rel.approved = True
|
||||
db.session.add(rel)
|
||||
session.add(rel)
|
||||
|
||||
mod = Package()
|
||||
mod.approved = True
|
||||
mod.name = "other_worlds"
|
||||
mod.title = "Other Worlds"
|
||||
mod.license = licenses["MIT"]
|
||||
mod.media_license = licenses["MIT"]
|
||||
mod.type = PackageType.MOD
|
||||
mod.author = ez
|
||||
mod.tags.append(tags["mapgen"])
|
||||
@@ -235,31 +251,32 @@ No warranty is provided, express or implied, for any part of the project.
|
||||
mod.forums = 16015
|
||||
mod.short_desc = "Adds space with asteroids and comets"
|
||||
mod.desc = "This is the long desc"
|
||||
db.session.add(mod)
|
||||
session.add(mod)
|
||||
|
||||
mod = Package()
|
||||
mod.approved = True
|
||||
mod.name = "food"
|
||||
mod.title = "Food"
|
||||
mod.license = licenses["LGPLv2.1"]
|
||||
mod.media_license = licenses["MIT"]
|
||||
mod.type = PackageType.MOD
|
||||
mod.author = ruben
|
||||
mod.author = admin_user
|
||||
mod.tags.append(tags["player_effects"])
|
||||
mod.repo = "https://github.com/rubenwardy/food/"
|
||||
mod.issueTracker = "https://github.com/rubenwardy/food/issues/"
|
||||
mod.forums = 2960
|
||||
mod.short_desc = "Adds lots of food and an API to manage ingredients"
|
||||
mod.desc = "This is the long desc"
|
||||
food = mod
|
||||
db.session.add(mod)
|
||||
session.add(mod)
|
||||
|
||||
mod = Package()
|
||||
mod.approved = True
|
||||
mod.name = "food_sweet"
|
||||
mod.title = "Sweet Foods"
|
||||
mod.license = licenses["CC0"]
|
||||
mod.media_license = licenses["MIT"]
|
||||
mod.type = PackageType.MOD
|
||||
mod.author = ruben
|
||||
mod.author = admin_user
|
||||
mod.tags.append(tags["player_effects"])
|
||||
mod.repo = "https://github.com/rubenwardy/food_sweet/"
|
||||
mod.issueTracker = "https://github.com/rubenwardy/food_sweet/issues/"
|
||||
@@ -267,7 +284,7 @@ No warranty is provided, express or implied, for any part of the project.
|
||||
mod.short_desc = "Adds sweet food"
|
||||
mod.desc = "This is the long desc"
|
||||
food_sweet = mod
|
||||
db.session.add(mod)
|
||||
session.add(mod)
|
||||
|
||||
game1 = Package()
|
||||
game1.approved = True
|
||||
@@ -275,7 +292,8 @@ No warranty is provided, express or implied, for any part of the project.
|
||||
game1.title = "Capture The Flag"
|
||||
game1.type = PackageType.GAME
|
||||
game1.license = licenses["LGPLv2.1"]
|
||||
game1.author = ruben
|
||||
game1.media_license = licenses["MIT"]
|
||||
game1.author = admin_user
|
||||
game1.tags.append(tags["pvp"])
|
||||
game1.tags.append(tags["survival"])
|
||||
game1.tags.append(tags["multiplayer"])
|
||||
@@ -289,14 +307,14 @@ As seen on the Capture the Flag server (minetest.rubenwardy.com:30000)
|
||||
Uses the CTF PvP Engine.
|
||||
"""
|
||||
|
||||
db.session.add(game1)
|
||||
session.add(game1)
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = game1
|
||||
rel.title = "v1.0.0"
|
||||
rel.url = "https://github.com/rubenwardy/capturetheflag/archive/master.zip"
|
||||
rel.approved = True
|
||||
db.session.add(rel)
|
||||
session.add(rel)
|
||||
|
||||
|
||||
mod = Package()
|
||||
@@ -304,21 +322,22 @@ Uses the CTF PvP Engine.
|
||||
mod.name = "pixelbox"
|
||||
mod.title = "PixelBOX Reloaded"
|
||||
mod.license = licenses["CC0"]
|
||||
mod.media_license = licenses["MIT"]
|
||||
mod.type = PackageType.TXP
|
||||
mod.author = ruben
|
||||
mod.author = admin_user
|
||||
mod.forums = 14132
|
||||
mod.short_desc = "This is an update of the original PixelBOX texture pack by the brillant artist Gambit"
|
||||
mod.desc = "This is the long desc"
|
||||
db.session.add(mod)
|
||||
session.add(mod)
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = mod
|
||||
rel.title = "v1.0.0"
|
||||
rel.url = "http://mamadou3.free.fr/Minetest/PixelBOX.zip"
|
||||
rel.approved = True
|
||||
db.session.add(rel)
|
||||
session.add(rel)
|
||||
|
||||
db.session.commit()
|
||||
session.commit()
|
||||
|
||||
metas = {}
|
||||
for package in Package.query.filter_by(type=PackageType.MOD).all():
|
||||
@@ -327,55 +346,9 @@ Uses the CTF PvP Engine.
|
||||
meta = metas[package.name]
|
||||
except KeyError:
|
||||
meta = MetaPackage(package.name)
|
||||
db.session.add(meta)
|
||||
session.add(meta)
|
||||
metas[package.name] = meta
|
||||
package.provides.append(meta)
|
||||
|
||||
dep = Dependency(food_sweet, meta=metas["food"])
|
||||
db.session.add(dep)
|
||||
|
||||
|
||||
if delete_db and os.path.isfile("db.sqlite"):
|
||||
os.remove("db.sqlite")
|
||||
|
||||
|
||||
if create_db:
|
||||
print("Creating database tables...")
|
||||
db.create_all()
|
||||
|
||||
print("Filling database...")
|
||||
|
||||
ruben = User("rubenwardy")
|
||||
ruben.active = True
|
||||
ruben.password = make_flask_user_password("tuckfrump")
|
||||
ruben.github_username = "rubenwardy"
|
||||
ruben.forums_username = "rubenwardy"
|
||||
ruben.rank = UserRank.ADMIN
|
||||
db.session.add(ruben)
|
||||
|
||||
tags = {}
|
||||
for tag in ["Inventory", "Mapgen", "Building", \
|
||||
"Mobs and NPCs", "Tools", "Player effects", \
|
||||
"Environment", "Transport", "Maintenance", "Plants and farming", \
|
||||
"PvP", "PvE", "Survival", "Creative", "Puzzle", "Multiplayer", "Singleplayer"]:
|
||||
row = Tag(tag)
|
||||
tags[row.name] = row
|
||||
db.session.add(row)
|
||||
|
||||
licenses = {}
|
||||
for license in ["GPLv2.1", "GPLv3", "LGPLv2.1", "LGPLv3", "AGPLv2.1", "AGPLv3",
|
||||
"Apache", "BSD 3-Clause", "BSD 2-Clause", "CC0", "CC-BY-SA",
|
||||
"CC-BY", "MIT", "ZLib", "Other (Free)"]:
|
||||
row = License(license)
|
||||
licenses[row.name] = row
|
||||
db.session.add(row)
|
||||
|
||||
for license in ["CC-BY-NC-SA", "Other (Non-free)"]:
|
||||
row = License(license, False)
|
||||
licenses[row.name] = row
|
||||
db.session.add(row)
|
||||
|
||||
if test_data:
|
||||
defineDummyData(licenses, tags, ruben)
|
||||
|
||||
db.session.commit()
|
||||
session.add(dep)
|
||||
@@ -4,3 +4,4 @@ title: Help
|
||||
* [Ranks and Permissions](ranks_permissions)
|
||||
* [Content Ratings and Flags](content_flags)
|
||||
* [Reporting Content](reporting)
|
||||
* [API](api)
|
||||
|
||||
51
app/flatpages/help/api.md
Normal file
51
app/flatpages/help/api.md
Normal file
@@ -0,0 +1,51 @@
|
||||
title: API
|
||||
|
||||
## Authentication
|
||||
|
||||
Not all endpoints require authentication.
|
||||
Authentication is done using Bearer tokens:
|
||||
|
||||
Authorization: Bearer YOURTOKEN
|
||||
|
||||
You can use the `/api/whoami` to check authentication.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Misc
|
||||
|
||||
* GET `/api/whoami/` - Json dictionary with the following keys:
|
||||
* `is_authenticated` - True on successful API authentication
|
||||
* `username` - Username of the user authenticated as, null otherwise.
|
||||
* 403 will be thrown on unsupported authentication type, invalid access token, or other errors.
|
||||
|
||||
### Packages
|
||||
|
||||
* GET `/api/packages/` - See [Package Queries](#package-queries)
|
||||
* GET `/api/packages/<username>/<name>/`
|
||||
|
||||
### Topics
|
||||
|
||||
* GET `/api/topics/` - Supports [Package Queries](#package-queries), and the following two options:
|
||||
* `show_added` - Show topics which exist as packages, default true.
|
||||
* `show_discarded` - Show topics which have been marked as outdated, default false.
|
||||
|
||||
### Minetest
|
||||
|
||||
* GET `/api/minetest_versions/`
|
||||
|
||||
|
||||
## Package Queries
|
||||
|
||||
Example:
|
||||
|
||||
/api/packages/?type=mod&type=game&q=mobs+fun&hide=nonfree&hide=gore
|
||||
|
||||
Supported query parameters:
|
||||
|
||||
* `type` - Package types (`mod`, `game`, `txp`).
|
||||
* `q` - Query string
|
||||
* `random` - When present, enable random ordering and ignore `sort`.
|
||||
* `hide` - Hide content based on [Content Flags](content_flags).
|
||||
* `sort` - Sort by (`name`, `views`, `date`, `score`).
|
||||
* `order` - Sort ascending (`Asc`) or descending (`desc`).
|
||||
* `protocol_version` - Only show packages supported by this Minetest protocol version.
|
||||
@@ -219,6 +219,21 @@ title: Ranks and Permissions
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Create Token</td>
|
||||
<th></th> <!-- new -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- editor -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- moderator -->
|
||||
<th>✓<sup>2</sup></th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Set Rank</td>
|
||||
<th></th> <!-- new -->
|
||||
|
||||
@@ -37,7 +37,7 @@ Also see the [help page on tags](/help/package_tags/).
|
||||
Sexually-orientated content is not permitted.
|
||||
|
||||
Mature content, including that relating to drugs, excessive gore, violence, or
|
||||
horror, is not currently permitted - but will be in the future.
|
||||
excessive horror, is not currently permitted - but will be in the future.
|
||||
|
||||
The submission of malware is strictly prohibited. This includes software which
|
||||
does not do as it advertises, for example if it posts telemetry without stating
|
||||
|
||||
172
app/models.py
172
app/models.py
@@ -23,8 +23,8 @@ 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 flask_user import login_required, UserManager, UserMixin
|
||||
from sqlalchemy import func, CheckConstraint
|
||||
from sqlalchemy_searchable import SearchQueryMixin
|
||||
from sqlalchemy_utils.types import TSVectorType
|
||||
from sqlalchemy_searchable import make_searchable
|
||||
@@ -78,6 +78,7 @@ class Permission(enum.Enum):
|
||||
CHANGE_AUTHOR = "CHANGE_AUTHOR"
|
||||
CHANGE_NAME = "CHANGE_NAME"
|
||||
MAKE_RELEASE = "MAKE_RELEASE"
|
||||
DELETE_RELEASE = "DELETE_RELEASE"
|
||||
ADD_SCREENSHOTS = "ADD_SCREENSHOTS"
|
||||
APPROVE_SCREENSHOT = "APPROVE_SCREENSHOT"
|
||||
APPROVE_RELEASE = "APPROVE_RELEASE"
|
||||
@@ -91,6 +92,7 @@ class Permission(enum.Enum):
|
||||
CREATE_THREAD = "CREATE_THREAD"
|
||||
UNAPPROVE_PACKAGE = "UNAPPROVE_PACKAGE"
|
||||
TOPIC_DISCARD = "TOPIC_DISCARD"
|
||||
CREATE_TOKEN = "CREATE_TOKEN"
|
||||
|
||||
# Only return true if the permission is valid for *all* contexts
|
||||
# See Package.checkPerm for package-specific contexts
|
||||
@@ -112,7 +114,7 @@ class User(db.Model, UserMixin):
|
||||
|
||||
# User authentication information
|
||||
username = db.Column(db.String(50, collation="NOCASE"), nullable=False, unique=True, index=True)
|
||||
password = db.Column(db.String(255), nullable=True)
|
||||
password = db.Column(db.String(255), nullable=False, server_default="")
|
||||
reset_password_token = db.Column(db.String(100), nullable=False, server_default="")
|
||||
|
||||
rank = db.Column(db.Enum(UserRank))
|
||||
@@ -123,7 +125,7 @@ class User(db.Model, UserMixin):
|
||||
|
||||
# User email information
|
||||
email = db.Column(db.String(255), nullable=True, unique=True)
|
||||
confirmed_at = db.Column(db.DateTime())
|
||||
email_confirmed_at = db.Column(db.DateTime())
|
||||
|
||||
# User information
|
||||
profile_pic = db.Column(db.String(255), nullable=True, server_default=None)
|
||||
@@ -141,17 +143,21 @@ class User(db.Model, UserMixin):
|
||||
packages = db.relationship("Package", backref="author", lazy="dynamic")
|
||||
requests = db.relationship("EditRequest", backref="author", lazy="dynamic")
|
||||
threads = db.relationship("Thread", backref="author", lazy="dynamic")
|
||||
tokens = db.relationship("APIToken", backref="owner", lazy="dynamic")
|
||||
replies = db.relationship("ThreadReply", backref="author", lazy="dynamic")
|
||||
|
||||
def __init__(self, username, active=False, email=None, password=None):
|
||||
def __init__(self, username, active=False, email=None, password=""):
|
||||
self.username = username
|
||||
self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
|
||||
self.email_confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
|
||||
self.display_name = username
|
||||
self.active = active
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.rank = UserRank.NOT_JOINED
|
||||
|
||||
def hasPassword(self):
|
||||
return self.password != ""
|
||||
|
||||
def canAccessTodoList(self):
|
||||
return Permission.APPROVE_NEW.check(self) or \
|
||||
Permission.APPROVE_RELEASE.check(self) or \
|
||||
@@ -182,6 +188,11 @@ class User(db.Model, UserMixin):
|
||||
return user.rank.atLeast(UserRank.MODERATOR)
|
||||
elif perm == Permission.CHANGE_EMAIL:
|
||||
return user == self or (user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank))
|
||||
elif perm == Permission.CREATE_TOKEN:
|
||||
if user == self:
|
||||
return user.rank.atLeast(UserRank.MEMBER)
|
||||
else:
|
||||
return user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank)
|
||||
else:
|
||||
raise Exception("Permission {} is not related to users".format(perm.name))
|
||||
|
||||
@@ -195,6 +206,13 @@ class User(db.Model, UserMixin):
|
||||
return Thread.query.filter_by(author=self) \
|
||||
.filter(Thread.created_at > hour_ago).count() < 2
|
||||
|
||||
def __eq__(self, other):
|
||||
if not self.is_authenticated or not other.is_authenticated:
|
||||
return False
|
||||
|
||||
assert self.id > 0
|
||||
return self.id == other.id
|
||||
|
||||
class UserEmailVerification(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
@@ -213,6 +231,9 @@ class Notification(db.Model):
|
||||
url = db.Column(db.String(200), nullable=True)
|
||||
|
||||
def __init__(self, us, cau, titl, ur):
|
||||
if len(titl) > 100:
|
||||
title = title[:99] + "…"
|
||||
|
||||
self.user = us
|
||||
self.causer = cau
|
||||
self.title = titl
|
||||
@@ -299,7 +320,7 @@ class Dependency(db.Model):
|
||||
package = db.relationship("Package", foreign_keys=[package_id])
|
||||
meta_package_id = db.Column(db.Integer, db.ForeignKey("meta_package.id"), nullable=True)
|
||||
optional = db.Column(db.Boolean, nullable=False, default=False)
|
||||
__table_args__ = (db.UniqueConstraint('depender_id', 'package_id', 'meta_package_id', name='_dependency_uc'), )
|
||||
__table_args__ = (db.UniqueConstraint("depender_id", "package_id", "meta_package_id", name="_dependency_uc"), )
|
||||
|
||||
def __init__(self, depender=None, package=None, meta=None):
|
||||
if depender is None:
|
||||
@@ -366,14 +387,17 @@ class Package(db.Model):
|
||||
|
||||
# Basic details
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
name = db.Column(db.Unicode(100), nullable=False)
|
||||
title = db.Column(db.Unicode(100), nullable=False)
|
||||
short_desc = db.Column(db.Unicode(200), nullable=False)
|
||||
desc = db.Column(db.UnicodeText, nullable=True)
|
||||
type = db.Column(db.Enum(PackageType))
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
search_vector = db.Column(TSVectorType("title", "short_desc", "desc"))
|
||||
name_valid = db.CheckConstraint("name ~* '^[a-z0-9_]+$'")
|
||||
|
||||
search_vector = db.Column(TSVectorType("name", "title", "short_desc", "desc", \
|
||||
weights={ "name": "A", "title": "B", "short_desc": "C", "desc": "D" }))
|
||||
|
||||
license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
|
||||
license = db.relationship("License", foreign_keys=[license_id])
|
||||
@@ -395,7 +419,7 @@ class Package(db.Model):
|
||||
forums = db.Column(db.Integer, nullable=True)
|
||||
|
||||
provides = db.relationship("MetaPackage", secondary=provides, lazy="subquery",
|
||||
backref=db.backref("packages", lazy="dynamic"))
|
||||
backref=db.backref("packages", lazy="dynamic", order_by=db.desc("score")))
|
||||
|
||||
dependencies = db.relationship("Dependency", backref="depender", lazy="dynamic", foreign_keys=[Dependency.depender_id])
|
||||
|
||||
@@ -422,6 +446,9 @@ class Package(db.Model):
|
||||
for e in PackagePropertyKey:
|
||||
setattr(self, e.name, getattr(package, e.name))
|
||||
|
||||
def getIsFOSS(self):
|
||||
return self.license.is_foss and self.media_license.is_foss
|
||||
|
||||
def getState(self):
|
||||
if self.approved:
|
||||
return "approved"
|
||||
@@ -438,6 +465,13 @@ class Package(db.Model):
|
||||
else:
|
||||
return "ready"
|
||||
|
||||
def getAsDictionaryKey(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"author": self.author.display_name,
|
||||
"type": self.type.toName(),
|
||||
}
|
||||
|
||||
def getAsDictionaryShort(self, base_url, version=None, protonum=None):
|
||||
tnurl = self.getThumbnailURL(1)
|
||||
release = self.getDownloadRelease(version=version, protonum=protonum)
|
||||
@@ -448,8 +482,7 @@ class Package(db.Model):
|
||||
"short_description": self.short_desc,
|
||||
"type": self.type.toName(),
|
||||
"release": release and release.id,
|
||||
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
|
||||
"score": round(self.score * 10) / 10
|
||||
"thumbnail": (base_url + tnurl) if tnurl is not None else None
|
||||
}
|
||||
|
||||
def getAsDictionary(self, base_url, version=None, protonum=None):
|
||||
@@ -491,27 +524,27 @@ class Package(db.Model):
|
||||
return screenshot.url if screenshot is not None else None
|
||||
|
||||
def getDetailsURL(self):
|
||||
return url_for("package_page",
|
||||
return url_for("packages.view",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getEditURL(self):
|
||||
return url_for("create_edit_package_page",
|
||||
return url_for("packages.create_edit",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getApproveURL(self):
|
||||
return url_for("approve_package_page",
|
||||
return url_for("packages.approve",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getRemoveURL(self):
|
||||
return url_for("remove_package_page",
|
||||
return url_for("packages.remove",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getNewScreenshotURL(self):
|
||||
return url_for("create_screenshot_page",
|
||||
return url_for("packages.create_screenshot",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getCreateReleaseURL(self):
|
||||
return url_for("create_release_page",
|
||||
return url_for("packages.create_release",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getCreateEditRequestURL(self):
|
||||
@@ -519,11 +552,11 @@ class Package(db.Model):
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getBulkReleaseURL(self):
|
||||
return url_for("bulk_change_release_page",
|
||||
return url_for("packages.bulk_change_release",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getDownloadURL(self):
|
||||
return url_for("package_download_page",
|
||||
return url_for("packages.download",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getDownloadRelease(self, version=None, protonum=None):
|
||||
@@ -592,16 +625,20 @@ class Package(db.Model):
|
||||
else:
|
||||
raise Exception("Permission {} is not related to packages".format(perm.name))
|
||||
|
||||
def recalcScore(self):
|
||||
self.score = 10
|
||||
def setStartScore(self):
|
||||
downloads = db.session.query(func.sum(PackageRelease.downloads)). \
|
||||
filter(PackageRelease.package_id == self.id).scalar() or 0
|
||||
|
||||
if self.forums is not None:
|
||||
topic = ForumTopic.query.get(self.forums)
|
||||
if topic:
|
||||
days = (datetime.datetime.now() - topic.created_at).days
|
||||
months = days / 30
|
||||
years = days / 365
|
||||
self.score = topic.views / max(years, 0.0416) + 80*min(max(months, 0.5), 6)
|
||||
forum_score = 0
|
||||
forum_bonus = 0
|
||||
topic = self.forums and ForumTopic.query.get(self.forums)
|
||||
if topic:
|
||||
months = (datetime.datetime.now() - topic.created_at).days / 30
|
||||
years = months / 12
|
||||
forum_score = topic.views / max(years, 0.0416) + 80*min(max(months, 0.5), 6)
|
||||
forum_bonus = topic.views + topic.posts
|
||||
|
||||
self.score = max(downloads, forum_score * 0.6) + forum_bonus
|
||||
|
||||
if self.getMainScreenshotURL() is None:
|
||||
self.score *= 0.8
|
||||
@@ -609,6 +646,7 @@ class Package(db.Model):
|
||||
if not self.license.is_foss or not self.media_license.is_foss:
|
||||
self.score *= 0.1
|
||||
|
||||
|
||||
class MetaPackage(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||
@@ -679,8 +717,9 @@ class MinetestRelease(db.Model):
|
||||
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||
protocol = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
def __init__(self, name=None):
|
||||
def __init__(self, name=None, protocol=0):
|
||||
self.name = name
|
||||
self.protocol = protocol
|
||||
|
||||
def getActual(self):
|
||||
return None if self.name == "None" else self
|
||||
@@ -704,15 +743,23 @@ class PackageRelease(db.Model):
|
||||
max_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
|
||||
max_rel = db.relationship("MinetestRelease", foreign_keys=[max_rel_id])
|
||||
|
||||
# If the release is approved, then the task_id must be null and the url must be present
|
||||
CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)")
|
||||
|
||||
def getEditURL(self):
|
||||
return url_for("edit_release_page",
|
||||
return url_for("packages.edit_release",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name,
|
||||
id=self.id)
|
||||
|
||||
def getDeleteURL(self):
|
||||
return url_for("packages.delete_release",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name,
|
||||
id=self.id)
|
||||
|
||||
def getDownloadURL(self):
|
||||
return url_for("download_release_page",
|
||||
return url_for("packages.download_release",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name,
|
||||
id=self.id)
|
||||
@@ -726,17 +773,47 @@ class PackageRelease(db.Model):
|
||||
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 != "")
|
||||
assert self.task_id is None and self.url is not None and self.url != ""
|
||||
|
||||
self.approved = True
|
||||
return True
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
class PackageReview(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
|
||||
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
|
||||
recommend = db.Column(db.Boolean, nullable=False, default=True)
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to PackageRelease.checkPerm()")
|
||||
|
||||
isOwner = user == self.package.author
|
||||
|
||||
if perm == Permission.DELETE_RELEASE:
|
||||
if user.rank.atLeast(UserRank.ADMIN):
|
||||
return True
|
||||
|
||||
if not (isOwner or user.rank.atLeast(UserRank.EDITOR)):
|
||||
return False
|
||||
|
||||
if not self.package.approved or self.task_id is not None:
|
||||
return True
|
||||
|
||||
count = PackageRelease.query \
|
||||
.filter_by(package_id=self.package_id) \
|
||||
.filter(PackageRelease.id > self.id) \
|
||||
.count()
|
||||
|
||||
return count > 0
|
||||
else:
|
||||
raise Exception("Permission {} is not related to releases".format(perm.name))
|
||||
|
||||
|
||||
# class PackageReview(db.Model):
|
||||
# id = db.Column(db.Integer, primary_key=True)
|
||||
# package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
|
||||
# thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
|
||||
# recommend = db.Column(db.Boolean, nullable=False, default=True)
|
||||
|
||||
|
||||
class PackageScreenshot(db.Model):
|
||||
@@ -748,7 +825,7 @@ class PackageScreenshot(db.Model):
|
||||
|
||||
|
||||
def getEditURL(self):
|
||||
return url_for("edit_screenshot_page",
|
||||
return url_for("packages.edit_screenshot",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name,
|
||||
id=self.id)
|
||||
@@ -757,6 +834,16 @@ class PackageScreenshot(db.Model):
|
||||
return self.url.replace("/uploads/", ("/thumbnails/{:d}/").format(level))
|
||||
|
||||
|
||||
class APIToken(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
access_token = db.Column(db.String(34), unique=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
owner_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
def canOperateOnPackage(self, package):
|
||||
return packages.count() == 0 or package in packages
|
||||
|
||||
|
||||
class EditRequest(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -870,11 +957,11 @@ class Thread(db.Model):
|
||||
|
||||
|
||||
def getSubscribeURL(self):
|
||||
return url_for("thread_subscribe_page",
|
||||
return url_for("threads.subscribe",
|
||||
id=self.id)
|
||||
|
||||
def getUnsubscribeURL(self):
|
||||
return url_for("thread_unsubscribe_page",
|
||||
return url_for("threads.unsubscribe",
|
||||
id=self.id)
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
@@ -967,5 +1054,4 @@ class ForumTopic(db.Model):
|
||||
|
||||
|
||||
# Setup Flask-User
|
||||
db_adapter = SQLAlchemyAdapter(db, User) # Register the User model
|
||||
user_manager = UserManager(db_adapter, app) # Initialize Flask-User
|
||||
user_manager = UserManager(app, db, User)
|
||||
|
||||
@@ -25,7 +25,7 @@ class QueryBuilder:
|
||||
self.types = types
|
||||
self.search = args.get("q")
|
||||
self.random = "random" in args
|
||||
self.lucky = self.random or "lucky" in args
|
||||
self.lucky = "lucky" in args
|
||||
self.hide_nonfree = "nonfree" in hide_flags
|
||||
self.limit = 1 if self.lucky else None
|
||||
self.order_by = args.get("sort")
|
||||
@@ -62,7 +62,7 @@ class QueryBuilder:
|
||||
query = query.filter(Package.type.in_(self.types))
|
||||
|
||||
if self.search:
|
||||
query = query.search(self.search)
|
||||
query = query.search(self.search, sort=True)
|
||||
|
||||
if self.random:
|
||||
query = query.order_by(func.random())
|
||||
|
||||
13
app/rediscache.py
Normal file
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 scss import Scss
|
||||
|
||||
from app import app
|
||||
|
||||
def _convert(dir, src, dst):
|
||||
original_wd = os.getcwd()
|
||||
os.chdir(dir)
|
||||
@@ -31,7 +29,7 @@ def _convert(dir, src, dst):
|
||||
outfile.write(output)
|
||||
outfile.close()
|
||||
|
||||
def _getDirPath(originalPath, create=False):
|
||||
def _getDirPath(app, originalPath, create=False):
|
||||
path = originalPath
|
||||
|
||||
if not os.path.isdir(path):
|
||||
@@ -47,8 +45,8 @@ def _getDirPath(originalPath, create=False):
|
||||
|
||||
def sass(app, inputDir='scss', outputPath='static', force=False, cacheDir="public/static"):
|
||||
static_url_path = app.static_url_path
|
||||
inputDir = _getDirPath(inputDir)
|
||||
cacheDir = _getDirPath(cacheDir or outputPath, True)
|
||||
inputDir = _getDirPath(app, inputDir)
|
||||
cacheDir = _getDirPath(app, cacheDir or outputPath, True)
|
||||
|
||||
def _sass(filepath):
|
||||
sassfile = "%s/%s.scss" % (inputDir, filepath)
|
||||
@@ -63,5 +61,3 @@ def sass(app, inputDir='scss', outputPath='static', force=False, cacheDir="publi
|
||||
return send_from_directory(cacheDir, filepath + ".css")
|
||||
|
||||
app.add_url_rule("/%s/<path:filepath>.css" % (outputPath), 'sass', _sass)
|
||||
|
||||
sass(app)
|
||||
@@ -4,7 +4,7 @@
|
||||
@import "comments.scss";
|
||||
|
||||
.dropdown-menu {
|
||||
margin-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.dropdown:hover .dropdown-menu {
|
||||
@@ -16,42 +16,48 @@
|
||||
}
|
||||
|
||||
#alerts {
|
||||
display: block;
|
||||
list-style: none;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left:0;
|
||||
right:0;
|
||||
margin: 0;
|
||||
padding:0;
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
list-style: none;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left:0;
|
||||
right:0;
|
||||
margin: 0;
|
||||
padding:0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#alerts li {
|
||||
list-style: none;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.jumbotron {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
||||
.btn-outline-secondary {
|
||||
color: #eee;
|
||||
border-color: #666;
|
||||
background: rgba(102, 102, 102, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.alert .btn {
|
||||
text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.card .table {
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.btn-download {
|
||||
color: #fff;
|
||||
background-color: #00b05c;
|
||||
border-color: #00b05c;
|
||||
color: #fff;
|
||||
background-color: #00b05c;
|
||||
border-color: #00b05c;
|
||||
}
|
||||
|
||||
.btn-download:focus, .btn-download.focus {
|
||||
-webkit-box-shadow: 0 0 0 0.2rem rgba(231, 76, 60, 0.5);
|
||||
box-shadow: 0 0 0 0.2rem rgba(231, 76, 60, 0.5);
|
||||
-webkit-box-shadow: 0 0 0 0.2rem rgba(231, 76, 60, 0.5);
|
||||
box-shadow: 0 0 0 0.2rem rgba(231, 76, 60, 0.5);
|
||||
}
|
||||
|
||||
@@ -69,8 +69,12 @@ CELERYBEAT_SCHEDULE = {
|
||||
'topic_list_import': {
|
||||
'task': 'app.tasks.forumtasks.importTopicList',
|
||||
'schedule': crontab(minute=1, hour=1),
|
||||
},
|
||||
'package_score_update': {
|
||||
'task': 'app.tasks.pkgtasks.updatePackageScores',
|
||||
'schedule': crontab(minute=10, hour=1),
|
||||
}
|
||||
}
|
||||
celery.conf.beat_schedule = CELERYBEAT_SCHEDULE
|
||||
|
||||
from . import importtasks, forumtasks, emails
|
||||
from . import importtasks, forumtasks, emails, pkgtasks
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import render_template
|
||||
from flask import render_template, url_for
|
||||
from flask_mail import Message
|
||||
from app import mail
|
||||
from app.tasks import celery
|
||||
@@ -24,7 +24,18 @@ from app.tasks import celery
|
||||
def sendVerifyEmail(newEmail, token):
|
||||
print("Sending verify email!")
|
||||
msg = Message("Verify email address", recipients=[newEmail])
|
||||
msg.body = "This is a verification email!"
|
||||
|
||||
msg.body = """
|
||||
This email has been sent to you because someone (hopefully you)
|
||||
has entered your email address as a user's email.
|
||||
|
||||
If it wasn't you, then just delete this email.
|
||||
|
||||
If this was you, then please click this link to verify the address:
|
||||
|
||||
{}
|
||||
""".format(url_for('users.verify_email', token=token, _external=True))
|
||||
|
||||
msg.html = render_template("emails/verify.html", token=token)
|
||||
mail.send(msg)
|
||||
|
||||
@@ -33,9 +44,7 @@ def sendEmailRaw(to, subject, text, html):
|
||||
from flask_mail import Message
|
||||
msg = Message(subject, recipients=to)
|
||||
|
||||
if text:
|
||||
msg.body = text
|
||||
|
||||
msg.body = text or html
|
||||
html = html or text
|
||||
msg.html = render_template("emails/base.html", subject=subject, content=html)
|
||||
mail.send(msg)
|
||||
|
||||
@@ -171,7 +171,4 @@ def importTopicList():
|
||||
topic.views = int(info["views"])
|
||||
topic.created_at = info["date"]
|
||||
|
||||
for p in Package.query.all():
|
||||
p.recalcScore()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
@@ -15,20 +15,28 @@
|
||||
# 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_archive_all import GitArchiver
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from urllib.error import HTTPError
|
||||
import urllib.request
|
||||
from urllib.parse import urlparse, quote_plus, urlsplit
|
||||
from zipfile import ZipFile
|
||||
|
||||
from app import app
|
||||
from app.models import *
|
||||
from app.tasks import celery, TaskError
|
||||
from app.utils import randomString
|
||||
|
||||
from .minetestcheck import build_tree, MinetestCheckError, ContentType
|
||||
from .minetestcheck.config import parse_conf
|
||||
|
||||
class GithubURLMaker:
|
||||
def __init__(self, url):
|
||||
self.baseUrl = None
|
||||
self.user = None
|
||||
self.repo = None
|
||||
|
||||
# Rewrite path
|
||||
import re
|
||||
m = re.search("^\/([^\/]+)\/([^\/]+)\/?$", url.path)
|
||||
@@ -51,6 +59,9 @@ class GithubURLMaker:
|
||||
def getScreenshotURL(self):
|
||||
return self.baseUrl + "/screenshot.png"
|
||||
|
||||
def getModConfURL(self):
|
||||
return self.baseUrl + "/mod.conf"
|
||||
|
||||
def getCommitsURL(self, branch):
|
||||
return "https://api.github.com/repos/{}/{}/commits?sha={}" \
|
||||
.format(self.user, self.repo, urllib.parse.quote_plus(branch))
|
||||
@@ -119,188 +130,44 @@ def findModInfo(author, name, link):
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parseConf(string):
|
||||
retval = {}
|
||||
for line in string.split("\n"):
|
||||
idx = line.find("=")
|
||||
if idx > 0:
|
||||
key = line[:idx].strip()
|
||||
value = line[idx+1:].strip()
|
||||
retval[key] = value
|
||||
|
||||
return retval
|
||||
|
||||
|
||||
class PackageTreeNode:
|
||||
def __init__(self, baseDir, author=None, repo=None, name=None):
|
||||
print("Scanning " + baseDir)
|
||||
self.baseDir = baseDir
|
||||
self.author = author
|
||||
self.name = name
|
||||
self.repo = repo
|
||||
self.meta = None
|
||||
self.children = []
|
||||
|
||||
# Detect type
|
||||
type = None
|
||||
is_modpack = False
|
||||
if os.path.isfile(baseDir + "/game.conf"):
|
||||
type = PackageType.GAME
|
||||
elif os.path.isfile(baseDir + "/init.lua"):
|
||||
type = PackageType.MOD
|
||||
elif os.path.isfile(baseDir + "/modpack.txt") or \
|
||||
os.path.isfile(baseDir + "/modpack.conf"):
|
||||
type = PackageType.MOD
|
||||
is_modpack = True
|
||||
elif os.path.isdir(baseDir + "/mods"):
|
||||
type = PackageType.GAME
|
||||
elif os.listdir(baseDir) == []:
|
||||
# probably a submodule
|
||||
return
|
||||
else:
|
||||
raise TaskError("Unable to detect package type!")
|
||||
|
||||
self.type = type
|
||||
self.readMetaFiles()
|
||||
|
||||
if self.type == PackageType.GAME:
|
||||
self.addChildrenFromModDir(baseDir + "/mods")
|
||||
elif is_modpack:
|
||||
self.addChildrenFromModDir(baseDir)
|
||||
|
||||
|
||||
def readMetaFiles(self):
|
||||
result = {}
|
||||
|
||||
# .conf file
|
||||
try:
|
||||
with open(self.baseDir + "/mod.conf", "r") as myfile:
|
||||
conf = parseConf(myfile.read())
|
||||
for key in ["name", "description", "title", "depends", "optional_depends"]:
|
||||
try:
|
||||
result[key] = conf[key]
|
||||
except KeyError:
|
||||
pass
|
||||
except IOError:
|
||||
print("description.txt does not exist!")
|
||||
|
||||
# description.txt
|
||||
if not "description" in result:
|
||||
try:
|
||||
with open(self.baseDir + "/description.txt", "r") as myfile:
|
||||
result["description"] = myfile.read()
|
||||
except IOError:
|
||||
print("description.txt does not exist!")
|
||||
|
||||
# depends.txt
|
||||
import re
|
||||
pattern = re.compile("^([a-z0-9_]+)\??$")
|
||||
if not "depends" in result and not "optional_depends" in result:
|
||||
try:
|
||||
with open(self.baseDir + "/depends.txt", "r") as myfile:
|
||||
contents = myfile.read()
|
||||
soft = []
|
||||
hard = []
|
||||
for line in contents.split("\n"):
|
||||
line = line.strip()
|
||||
if pattern.match(line):
|
||||
if line[len(line) - 1] == "?":
|
||||
soft.append( line[:-1])
|
||||
else:
|
||||
hard.append(line)
|
||||
|
||||
result["depends"] = hard
|
||||
result["optional_depends"] = soft
|
||||
|
||||
except IOError:
|
||||
print("depends.txt does not exist!")
|
||||
|
||||
else:
|
||||
if "depends" in result:
|
||||
result["depends"] = [x.strip() for x in result["depends"].split(",")]
|
||||
if "optional_depends" in result:
|
||||
result["optional_depends"] = [x.strip() for x in result["optional_depends"].split(",")]
|
||||
|
||||
|
||||
# Calculate Title
|
||||
if "name" in result and not "title" in result:
|
||||
result["title"] = result["name"].replace("_", " ").title()
|
||||
|
||||
# Calculate short description
|
||||
if "description" in result:
|
||||
desc = result["description"]
|
||||
idx = desc.find(".") + 1
|
||||
cutIdx = min(len(desc), 200 if idx < 5 else idx)
|
||||
result["short_description"] = desc[:cutIdx]
|
||||
|
||||
# Get forum ID
|
||||
info = findModInfo(self.author, result.get("name"), self.repo)
|
||||
if info is not None:
|
||||
result["forumId"] = info.get("topicId")
|
||||
|
||||
if "name" in result:
|
||||
self.name = result["name"]
|
||||
del result["name"]
|
||||
|
||||
self.meta = result
|
||||
|
||||
def addChildrenFromModDir(self, dir):
|
||||
for entry in next(os.walk(dir))[1]:
|
||||
path = dir + "/" + entry
|
||||
if not entry.startswith('.') and os.path.isdir(path):
|
||||
self.children.append(PackageTreeNode(path, name=entry))
|
||||
|
||||
|
||||
def fold(self, attr, key=None, acc=None):
|
||||
if acc is None:
|
||||
acc = set()
|
||||
|
||||
if self.meta is None:
|
||||
return acc
|
||||
|
||||
at = getattr(self, attr)
|
||||
value = at if key is None else at.get(key)
|
||||
|
||||
if isinstance(value, list):
|
||||
acc |= set(value)
|
||||
elif value is not None:
|
||||
acc.add(value)
|
||||
|
||||
for child in self.children:
|
||||
child.fold(attr, key, acc)
|
||||
|
||||
return acc
|
||||
|
||||
def get(self, key):
|
||||
return self.meta.get(key)
|
||||
|
||||
def generateGitURL(urlstr):
|
||||
scheme, netloc, path, query, frag = urlsplit(urlstr)
|
||||
|
||||
return "http://:@" + netloc + path + query
|
||||
|
||||
|
||||
def getTempDir():
|
||||
return os.path.join(tempfile.gettempdir(), randomString(10))
|
||||
|
||||
|
||||
# Clones a repo from an unvalidated URL.
|
||||
# Returns a tuple of path and repo on sucess.
|
||||
# Throws `TaskError` on failure.
|
||||
# Caller is responsible for deleting returned directory.
|
||||
def cloneRepo(urlstr, ref=None, recursive=False):
|
||||
gitDir = tempfile.gettempdir() + "/" + randomString(10)
|
||||
gitDir = getTempDir()
|
||||
|
||||
err = None
|
||||
try:
|
||||
gitUrl = generateGitURL(urlstr)
|
||||
print("Cloning from " + gitUrl)
|
||||
repo = git.Repo.clone_from(gitUrl, gitDir, \
|
||||
progress=None, env=None, depth=1, recursive=recursive, kill_after_timeout=15)
|
||||
|
||||
if ref is not None:
|
||||
repo.create_head("myhead", ref).checkout()
|
||||
if ref is None:
|
||||
repo = git.Repo.clone_from(gitUrl, gitDir, \
|
||||
progress=None, env=None, depth=1, recursive=recursive, kill_after_timeout=15)
|
||||
else:
|
||||
repo = git.Repo.clone_from(gitUrl, gitDir, \
|
||||
progress=None, env=None, depth=1, recursive=recursive, kill_after_timeout=15, b=ref)
|
||||
|
||||
return gitDir, repo
|
||||
|
||||
except GitCommandError as e:
|
||||
# This is needed to stop the backtrace being weird
|
||||
err = e.stderr
|
||||
|
||||
except gitdb.exc.BadName as e:
|
||||
err = "Unable to find the reference " + (ref or "?") + "\n" + e.stderr
|
||||
|
||||
raise TaskError(err.replace("stderr: ", "") \
|
||||
.replace("Cloning into '" + gitDir + "'...", "") \
|
||||
.strip())
|
||||
@@ -308,7 +175,12 @@ def cloneRepo(urlstr, ref=None, recursive=False):
|
||||
@celery.task()
|
||||
def getMeta(urlstr, author):
|
||||
gitDir, _ = cloneRepo(urlstr, recursive=True)
|
||||
tree = PackageTreeNode(gitDir, author=author, repo=urlstr)
|
||||
|
||||
try:
|
||||
tree = build_tree(gitDir, author=author, repo=urlstr)
|
||||
except MinetestCheckError as err:
|
||||
raise TaskError(str(err))
|
||||
|
||||
shutil.rmtree(gitDir)
|
||||
|
||||
result = {}
|
||||
@@ -339,8 +211,11 @@ def makeVCSReleaseFromGithub(id, branch, release, url):
|
||||
raise TaskError("Invalid github repo URL")
|
||||
|
||||
commitsURL = urlmaker.getCommitsURL(branch)
|
||||
contents = urllib.request.urlopen(commitsURL).read().decode("utf-8")
|
||||
commits = json.loads(contents)
|
||||
try:
|
||||
contents = urllib.request.urlopen(commitsURL).read().decode("utf-8")
|
||||
commits = json.loads(contents)
|
||||
except HTTPError:
|
||||
raise TaskError("Unable to get commits for Github repository. Either the repository or reference doesn't exist.")
|
||||
|
||||
if len(commits) == 0 or not "sha" in commits[0]:
|
||||
raise TaskError("No commits found")
|
||||
@@ -349,12 +224,44 @@ def makeVCSReleaseFromGithub(id, branch, release, url):
|
||||
release.task_id = None
|
||||
release.commit_hash = commits[0]["sha"]
|
||||
release.approve(release.package.author)
|
||||
print(release.url)
|
||||
db.session.commit()
|
||||
|
||||
return release.url
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def checkZipRelease(self, id, path):
|
||||
release = PackageRelease.query.get(id)
|
||||
if release is None:
|
||||
raise TaskError("No such release!")
|
||||
elif release.package is None:
|
||||
raise TaskError("No package attached to release")
|
||||
|
||||
temp = getTempDir()
|
||||
try:
|
||||
with ZipFile(path, 'r') as zip_ref:
|
||||
zip_ref.extractall(temp)
|
||||
|
||||
try:
|
||||
tree = build_tree(temp, expected_type=ContentType[release.package.type.name], \
|
||||
author=release.package.author.username, name=release.package.name)
|
||||
except MinetestCheckError as err:
|
||||
if "Fails validation" not in release.title:
|
||||
release.title += " (Fails validation)"
|
||||
|
||||
release.task_id = self.request.id
|
||||
release.approved = False
|
||||
db.session.commit()
|
||||
|
||||
raise TaskError(str(err))
|
||||
|
||||
release.task_id = None
|
||||
release.approve(release.package.author)
|
||||
db.session.commit()
|
||||
|
||||
finally:
|
||||
shutil.rmtree(temp)
|
||||
|
||||
|
||||
@celery.task()
|
||||
def makeVCSRelease(id, branch):
|
||||
@@ -364,29 +271,37 @@ def makeVCSRelease(id, branch):
|
||||
elif release.package is None:
|
||||
raise TaskError("No package attached to release")
|
||||
|
||||
urlmaker = None
|
||||
url = urlparse(release.package.repo)
|
||||
if url.netloc == "github.com":
|
||||
return makeVCSReleaseFromGithub(id, branch, release, url)
|
||||
else:
|
||||
gitDir, repo = cloneRepo(release.package.repo, ref=branch, recursive=True)
|
||||
# url = urlparse(release.package.repo)
|
||||
# if url.netloc == "github.com":
|
||||
# return makeVCSReleaseFromGithub(id, branch, release, url)
|
||||
|
||||
try:
|
||||
filename = randomString(10) + ".zip"
|
||||
destPath = os.path.join("app/public/uploads", filename)
|
||||
with open(destPath, "wb") as fp:
|
||||
repo.archive(fp, format="zip")
|
||||
gitDir, repo = cloneRepo(release.package.repo, ref=branch, recursive=True)
|
||||
|
||||
release.url = "/uploads/" + filename
|
||||
release.task_id = None
|
||||
release.commit_hash = repo.head.object.hexsha
|
||||
release.approve(release.package.author)
|
||||
print(release.url)
|
||||
db.session.commit()
|
||||
try:
|
||||
tree = build_tree(gitDir, expected_type=ContentType[release.package.type.name], \
|
||||
author=release.package.author.username, name=release.package.name)
|
||||
except MinetestCheckError as err:
|
||||
raise TaskError(str(err))
|
||||
|
||||
return release.url
|
||||
finally:
|
||||
shutil.rmtree(gitDir)
|
||||
try:
|
||||
filename = randomString(10) + ".zip"
|
||||
destPath = os.path.join(app.config["UPLOAD_DIR"], filename)
|
||||
|
||||
assert(not os.path.isfile(destPath))
|
||||
archiver = GitArchiver(force_sub=True, main_repo_abspath=gitDir)
|
||||
archiver.create(destPath)
|
||||
assert(os.path.isfile(destPath))
|
||||
|
||||
release.url = "/uploads/" + filename
|
||||
release.task_id = None
|
||||
release.commit_hash = repo.head.object.hexsha
|
||||
release.approve(release.package.author)
|
||||
print(release.url)
|
||||
db.session.commit()
|
||||
|
||||
return release.url
|
||||
finally:
|
||||
shutil.rmtree(gitDir)
|
||||
|
||||
@celery.task()
|
||||
def importRepoScreenshot(id):
|
||||
@@ -408,7 +323,7 @@ def importRepoScreenshot(id):
|
||||
sourcePath = gitDir + "/screenshot." + ext
|
||||
if os.path.isfile(sourcePath):
|
||||
filename = randomString(10) + "." + ext
|
||||
destPath = os.path.join("app/public/uploads", filename)
|
||||
destPath = os.path.join(app.config["UPLOAD_DIR"], filename)
|
||||
shutil.copyfile(sourcePath, destPath)
|
||||
|
||||
ss = PackageScreenshot()
|
||||
@@ -445,7 +360,7 @@ def getDepends(package):
|
||||
#
|
||||
try:
|
||||
contents = urllib.request.urlopen(urlmaker.getModConfURL()).read().decode("utf-8")
|
||||
conf = parseConf(contents)
|
||||
conf = parse_conf(contents)
|
||||
for key in ["depends", "optional_depends"]:
|
||||
try:
|
||||
result[key] = conf[key]
|
||||
|
||||
48
app/tasks/minetestcheck/__init__.py
Normal file
48
app/tasks/minetestcheck/__init__.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from enum import Enum
|
||||
|
||||
class MinetestCheckError(Exception):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def __str__(self):
|
||||
return repr("Error validating package: " + self.value)
|
||||
|
||||
class ContentType(Enum):
|
||||
UNKNOWN = "unknown"
|
||||
MOD = "mod"
|
||||
MODPACK = "modpack"
|
||||
GAME = "game"
|
||||
TXP = "texture pack"
|
||||
|
||||
def isModLike(self):
|
||||
return self == ContentType.MOD or self == ContentType.MODPACK
|
||||
|
||||
def validate_same(self, other):
|
||||
"""
|
||||
Whether or not `other` is an acceptable type for this
|
||||
"""
|
||||
assert(other)
|
||||
|
||||
if self == ContentType.MOD:
|
||||
if not other.isModLike():
|
||||
raise MinetestCheckError("expected a mod or modpack, found " + other.value)
|
||||
|
||||
elif self == ContentType.TXP:
|
||||
if other != ContentType.UNKNOWN and other != ContentType.TXP:
|
||||
raise MinetestCheckError("expected a " + self.value + ", found a " + other.value)
|
||||
|
||||
elif other != self:
|
||||
raise MinetestCheckError("expected a " + self.value + ", found a " + other.value)
|
||||
|
||||
|
||||
from .tree import PackageTreeNode, get_base_dir
|
||||
|
||||
def build_tree(path, expected_type=None, author=None, repo=None, name=None):
|
||||
path = get_base_dir(path)
|
||||
|
||||
root = PackageTreeNode(path, "/", author=author, repo=repo, name=name)
|
||||
assert(root)
|
||||
|
||||
if expected_type:
|
||||
expected_type.validate_same(root.type)
|
||||
|
||||
return root
|
||||
10
app/tasks/minetestcheck/config.py
Normal file
10
app/tasks/minetestcheck/config.py
Normal file
@@ -0,0 +1,10 @@
|
||||
def parse_conf(string):
|
||||
retval = {}
|
||||
for line in string.split("\n"):
|
||||
idx = line.find("=")
|
||||
if idx > 0:
|
||||
key = line[:idx].strip()
|
||||
value = line[idx+1:].strip()
|
||||
retval[key] = value
|
||||
|
||||
return retval
|
||||
162
app/tasks/minetestcheck/tree.py
Normal file
162
app/tasks/minetestcheck/tree.py
Normal file
@@ -0,0 +1,162 @@
|
||||
import os
|
||||
from . import MinetestCheckError, ContentType
|
||||
from .config import parse_conf
|
||||
|
||||
def get_base_dir(path):
|
||||
if not os.path.isdir(path):
|
||||
raise IOError("Expected dir")
|
||||
|
||||
root, subdirs, files = next(os.walk(path))
|
||||
if len(subdirs) == 1 and len(files) == 0:
|
||||
return get_base_dir(path + "/" + subdirs[0])
|
||||
else:
|
||||
return path
|
||||
|
||||
|
||||
def detect_type(path):
|
||||
if os.path.isfile(path + "/game.conf"):
|
||||
return ContentType.GAME
|
||||
elif os.path.isfile(path + "/init.lua"):
|
||||
return ContentType.MOD
|
||||
elif os.path.isfile(path + "/modpack.txt") or \
|
||||
os.path.isfile(path + "/modpack.conf"):
|
||||
return ContentType.MODPACK
|
||||
elif os.path.isdir(path + "/mods"):
|
||||
return ContentType.GAME
|
||||
elif os.path.isfile(path + "/texture_pack.conf"):
|
||||
return ContentType.TXP
|
||||
else:
|
||||
return ContentType.UNKNOWN
|
||||
|
||||
|
||||
class PackageTreeNode:
|
||||
def __init__(self, baseDir, relative, author=None, repo=None, name=None):
|
||||
print(baseDir)
|
||||
self.baseDir = baseDir
|
||||
self.relative = relative
|
||||
self.author = author
|
||||
self.name = name
|
||||
self.repo = repo
|
||||
self.meta = None
|
||||
self.children = []
|
||||
|
||||
# Detect type
|
||||
self.type = detect_type(baseDir)
|
||||
self.read_meta()
|
||||
|
||||
if self.type == ContentType.GAME:
|
||||
if not os.path.isdir(baseDir + "/mods"):
|
||||
raise MinetestCheckError(("game at {} does not have a mods/ folder").format(self.relative))
|
||||
self.add_children_from_mod_dir(baseDir + "/mods")
|
||||
elif self.type == ContentType.MODPACK:
|
||||
self.add_children_from_mod_dir(baseDir)
|
||||
|
||||
|
||||
def read_meta(self):
|
||||
result = {}
|
||||
|
||||
# .conf file
|
||||
try:
|
||||
with open(self.baseDir + "/mod.conf", "r") as myfile:
|
||||
conf = parse_conf(myfile.read())
|
||||
for key in ["name", "description", "title", "depends", "optional_depends"]:
|
||||
try:
|
||||
result[key] = conf[key]
|
||||
except KeyError:
|
||||
pass
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
# description.txt
|
||||
if not "description" in result:
|
||||
try:
|
||||
with open(self.baseDir + "/description.txt", "r") as myfile:
|
||||
result["description"] = myfile.read()
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
# depends.txt
|
||||
import re
|
||||
pattern = re.compile("^([a-z0-9_]+)\??$")
|
||||
if not "depends" in result and not "optional_depends" in result:
|
||||
try:
|
||||
with open(self.baseDir + "/depends.txt", "r") as myfile:
|
||||
contents = myfile.read()
|
||||
soft = []
|
||||
hard = []
|
||||
for line in contents.split("\n"):
|
||||
line = line.strip()
|
||||
if pattern.match(line):
|
||||
if line[len(line) - 1] == "?":
|
||||
soft.append( line[:-1])
|
||||
else:
|
||||
hard.append(line)
|
||||
|
||||
result["depends"] = hard
|
||||
result["optional_depends"] = soft
|
||||
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
else:
|
||||
if "depends" in result:
|
||||
result["depends"] = [x.strip() for x in result["depends"].split(",")]
|
||||
if "optional_depends" in result:
|
||||
result["optional_depends"] = [x.strip() for x in result["optional_depends"].split(",")]
|
||||
|
||||
|
||||
# Calculate Title
|
||||
if "name" in result and not "title" in result:
|
||||
result["title"] = result["name"].replace("_", " ").title()
|
||||
|
||||
# Calculate short description
|
||||
if "description" in result:
|
||||
desc = result["description"]
|
||||
idx = desc.find(".") + 1
|
||||
cutIdx = min(len(desc), 200 if idx < 5 else idx)
|
||||
result["short_description"] = desc[:cutIdx]
|
||||
|
||||
if "name" in result:
|
||||
self.name = result["name"]
|
||||
del result["name"]
|
||||
|
||||
self.meta = result
|
||||
|
||||
def add_children_from_mod_dir(self, dir):
|
||||
for entry in next(os.walk(dir))[1]:
|
||||
path = os.path.join(dir, entry)
|
||||
if not entry.startswith('.') and os.path.isdir(path):
|
||||
child = PackageTreeNode(path, self.relative + entry + "/", name=entry)
|
||||
if not child.type.isModLike():
|
||||
raise MinetestCheckError(("Expecting mod or modpack, found {} at {} inside {}") \
|
||||
.format(child.type.value, child.relative, self.type.value))
|
||||
|
||||
self.children.append(child)
|
||||
|
||||
|
||||
def fold(self, attr, key=None, acc=None):
|
||||
if acc is None:
|
||||
acc = set()
|
||||
|
||||
if self.meta is None:
|
||||
return acc
|
||||
|
||||
at = getattr(self, attr)
|
||||
value = at if key is None else at.get(key)
|
||||
|
||||
if isinstance(value, list):
|
||||
acc |= set(value)
|
||||
elif value is not None:
|
||||
acc.add(value)
|
||||
|
||||
for child in self.children:
|
||||
child.fold(attr, key, acc)
|
||||
|
||||
return acc
|
||||
|
||||
def get(self, key):
|
||||
return self.meta.get(key)
|
||||
|
||||
def validate(self):
|
||||
for child in self.children:
|
||||
child.validate()
|
||||
@@ -121,7 +121,7 @@ def parseForumListPage(id, page, out, extra=None):
|
||||
|
||||
if id in out:
|
||||
print(" - got {} again, title: {}".format(id, title))
|
||||
assert(title == out[id]['title'])
|
||||
assert title == out[id]['title']
|
||||
return False
|
||||
|
||||
row = {
|
||||
|
||||
23
app/tasks/pkgtasks.py
Normal file
23
app/tasks/pkgtasks.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# 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.models import Package
|
||||
from app.tasks import celery
|
||||
|
||||
@celery.task()
|
||||
def updatePackageScores():
|
||||
Package.query.update({ "score": Package.score * 0.8 })
|
||||
22
app/template_filters.py
Normal file
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 %}
|
||||
<p>
|
||||
<a href="{{ url_for('license_list_page') }}">Back to list</a> |
|
||||
<a href="{{ url_for('createedit_license_page') }}">New License</a>
|
||||
<a href="{{ url_for('admin.license_list') }}">Back to list</a> |
|
||||
<a href="{{ url_for('admin.create_edit_license') }}">New License</a>
|
||||
</p>
|
||||
|
||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||
|
||||
@@ -6,11 +6,11 @@ Licenses
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
<a href="{{ url_for('createedit_license_page') }}">New License</a>
|
||||
<a href="{{ url_for('admin.create_edit_license') }}">New License</a>
|
||||
</p>
|
||||
<ul>
|
||||
{% for l in licenses %}
|
||||
<li><a href="{{ url_for('createedit_license_page', name=l.name) }}">{{ l.name }}</a> [{{ l.is_foss and "Free" or "Non-free"}}]</li>
|
||||
<li><a href="{{ url_for('admin.create_edit_license', name=l.name) }}">{{ l.name }}</a> [{{ l.is_foss and "Free" or "Non-free"}}]</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
{% block content %}
|
||||
<ul>
|
||||
<li><a href="{{ url_for('user_list_page') }}">User list</a></li>
|
||||
<li><a href="{{ url_for('tag_list_page') }}">Tag Editor</a></li>
|
||||
<li><a href="{{ url_for('license_list_page') }}">License Editor</a></li>
|
||||
<li><a href="{{ url_for('version_list_page') }}">Version Editor</a></li>
|
||||
<li><a href="{{ url_for('switch_user_page') }}">Sign in as another user</a></li>
|
||||
<li><a href="{{ url_for('users.list_all') }}">User list</a></li>
|
||||
<li><a href="{{ url_for('admin.tag_list') }}">Tag Editor</a></li>
|
||||
<li><a href="{{ url_for('admin.license_list') }}">License Editor</a></li>
|
||||
<li><a href="{{ url_for('admin.version_list') }}">Version Editor</a></li>
|
||||
<li><a href="{{ url_for('admin.switch_user') }}">Sign in as another user</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="card my-4">
|
||||
@@ -19,7 +19,9 @@
|
||||
<form method="post" action="" class="card-body">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<select name="action">
|
||||
<option value="importmodlist" selected>Import forum topics</option>
|
||||
<option value="delstuckreleases" selected>Delete stuck releases</option>
|
||||
<option value="checkreleases">Validate all Zip releases</option>
|
||||
<option value="importmodlist">Import forum topics</option>
|
||||
<option value="recalcscores">Recalculate package scores</option>
|
||||
<option value="checkusers">Check forum users</option>
|
||||
<option value="importscreenshots">Import screenshots from VCS</option>
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
<a href="{{ url_for('tag_list_page') }}">Back to list</a> |
|
||||
<a href="{{ url_for('createedit_tag_page') }}">New Tag</a>
|
||||
<a href="{{ url_for('admin.tag_list') }}">Back to list</a> |
|
||||
<a href="{{ url_for('admin.create_edit_tag') }}">New Tag</a>
|
||||
</p>
|
||||
|
||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||
|
||||
@@ -6,11 +6,11 @@ Tags
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
<a href="{{ url_for('createedit_tag_page') }}">New Tag</a>
|
||||
<a href="{{ url_for('admin.create_edit_tag') }}">New Tag</a>
|
||||
</p>
|
||||
<ul>
|
||||
{% for t in tags %}
|
||||
<li><a href="{{ url_for('createedit_tag_page', name=t.name) }}">{{ t.title }}</a> [{{ t.packages | count }} packages]</li>
|
||||
<li><a href="{{ url_for('admin.create_edit_tag', name=t.name) }}">{{ t.title }}</a> [{{ t.packages | count }} packages]</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
<a href="{{ url_for('version_list_page') }}">Back to list</a> |
|
||||
<a href="{{ url_for('createedit_version_page') }}">New Version</a>
|
||||
<a href="{{ url_for('admin.version_list') }}">Back to list</a> |
|
||||
<a href="{{ url_for('admin.create_edit_version') }}">New Version</a>
|
||||
</p>
|
||||
|
||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||
|
||||
@@ -6,11 +6,11 @@ Minetest Versions
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
<a href="{{ url_for('createedit_version_page') }}">New Version</a>
|
||||
<a href="{{ url_for('admin.create_edit_version') }}">New Version</a>
|
||||
</p>
|
||||
<ul>
|
||||
{% for v in versions %}
|
||||
<li><a href="{{ url_for('createedit_version_page', name=v.name) }}">{{ v.name }}</a></li>
|
||||
<li><a href="{{ url_for('admin.create_edit_version', name=v.name) }}">{{ v.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
53
app/templates/api/create_edit_token.html
Normal file
53
app/templates/api/create_edit_token.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% if token %}
|
||||
{{ _("Edit - %(name)s", name=token.name) }}
|
||||
{% else %}
|
||||
{{ _("Create API Token") }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% from "macros/forms.html" import render_field, render_submit_field, render_radio_field %}
|
||||
|
||||
{% block content %}
|
||||
{% if token %}
|
||||
<form class="float-right" method="POST" action="{{ url_for('api.delete_token', username=token.owner.username, id=token.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input class="btn btn-danger" type="submit" value="Delete">
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<h1 class="mt-0">{{ self.title() }}</h1>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
{{ _("Use carefully, as you may be held responsible for any damage caused by rogue scripts") }}
|
||||
</div>
|
||||
|
||||
{% if token %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">{{ _("Access Token") }}</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
For security reasons, access tokens will only be shown once.
|
||||
Reset the token if it is lost.
|
||||
</p>
|
||||
{% if access_token %}
|
||||
<input class="form-control my-3" type="text" readonly value="{{ access_token }}" class="form-control">
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('api.reset_token', username=token.owner.username, id=token.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input class="btn btn-primary" type="submit" value="Reset">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{{ render_field(form.name, placeholder="Human readable") }}
|
||||
|
||||
{{ render_submit_field(form.submit) }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
23
app/templates/api/list_tokens.html
Normal file
23
app/templates/api/list_tokens.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
{{ _("List tokens for %(username)s", username=user.username) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<a class="btn btn-primary float-right" href="{{ url_for('api.create_edit_token', username=user.username) }}">Create</a>
|
||||
<h1 class="mt-0">{{ self.title() }}</h1>
|
||||
|
||||
<ul>
|
||||
{% for token in user.tokens %}
|
||||
<li>
|
||||
<a href="{{ url_for('api.create_edit_token', username=user.username, id=token.id) }}">{{ token.name }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<i>No tokens created</i>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
@@ -7,7 +7,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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/custom.css?v=7">
|
||||
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=8">
|
||||
<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">
|
||||
@@ -60,10 +60,10 @@
|
||||
</form>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('notifications_page') }}">
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('notifications.list_all') }}">
|
||||
<img src="/static/notification{% if current_user.notifications %}_alert{% endif %}.svg" />
|
||||
</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('create_edit_package_page') }}">+</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('packages.create_edit') }}">+</a></li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
@@ -73,24 +73,24 @@
|
||||
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('user_profile_page', username=current_user.username) }}">Profile</a>
|
||||
<a class="nav-link" href="{{ url_for('users.profile', username=current_user.username) }}">Profile</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('user_profile_page', username=current_user.username) }}#unadded-topics">Your unadded topics</a>
|
||||
<a class="nav-link" href="{{ url_for('users.profile', username=current_user.username) }}#unadded-topics">Your unadded topics</a>
|
||||
</li>
|
||||
{% if current_user.canAccessTodoList() %}
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('todo_page') }}">{{ _("Work Queue") }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('user_list_page') }}">{{ _("User list") }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('todo.view') }}">{{ _("Work Queue") }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('users.list_all') }}">{{ _("User list") }}</a></li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('todo_topics_page') }}">{{ _("All unadded topics") }}</a>
|
||||
<a class="nav-link" href="{{ url_for('todo.topics') }}">{{ _("All unadded topics") }}</a>
|
||||
</li>
|
||||
{% if current_user.rank == current_user.rank.ADMIN %}
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin_page') }}">{{ _("Admin") }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.admin_page') }}">{{ _("Admin") }}</a></li>
|
||||
{% endif %}
|
||||
{% if current_user.rank == current_user.rank.MODERATOR %}
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('tag_list_page') }}">{{ _("Tag Editor") }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('license_list_page') }}">{{ _("License Editor") }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.tag_list') }}">{{ _("Tag Editor") }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.license_list') }}">{{ _("License Editor") }}</a></li>
|
||||
{% endif %}
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('user.logout') }}">{{ _("Sign out") }}</a></li>
|
||||
</ul>
|
||||
@@ -134,7 +134,13 @@
|
||||
<a href="{{ url_for('flatpage', path='help') }}">{{ _("Help") }}</a> |
|
||||
<a href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("Policy and Guidance") }}</a> |
|
||||
<a href="{{ url_for('flatpage', path='help/reporting') }}">{{ _("Report / DMCA") }}</a> |
|
||||
<a href="{{ url_for('user_list_page') }}">{{ _("User List") }}</a>
|
||||
<a href="{{ url_for('users.list_all') }}">{{ _("User List") }}</a>
|
||||
|
||||
{% if debug %}
|
||||
<p style="color: red">
|
||||
DEBUG MODE ENABLED
|
||||
</p>
|
||||
{% endif %}
|
||||
</footer>
|
||||
|
||||
<script src="/static/jquery.min.js"></script>
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
If this was you, then please click this link to verify the address:
|
||||
</p>
|
||||
|
||||
<a class="btn" href="{{ url_for('verify_email_page', token=token, _external=True) }}">
|
||||
<a class="btn" href="{{ url_for('users.verify_email', token=token, _external=True) }}">
|
||||
Confirm Email Address
|
||||
</a>
|
||||
|
||||
<p style="font-size: 80%;">
|
||||
Or paste this into your browser: {{ url_for('verify_email_page', token=token, _external=True) }}
|
||||
Or paste this into your browser: {{ url_for('users.verify_email', token=token, _external=True) }}
|
||||
<p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,7 +15,7 @@ Sign in
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{# Username or Email field #}
|
||||
{% set field = form.username if user_manager.enable_username else form.email %}
|
||||
{% set field = form.username if user_manager.USER_ENABLE_USERNAME else form.email %}
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{# Label on left, "New here? Register." on right #}
|
||||
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}</label>
|
||||
@@ -31,7 +31,7 @@ Sign in
|
||||
{% set field = form.password %}
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}
|
||||
{% if user_manager.enable_forgot_password %}
|
||||
{% if user_manager.USER_ENABLE_FORGOT_PASSWORD %}
|
||||
<a href="{{ url_for('user.forgot_password') }}" tabindex='195'>
|
||||
[{%trans%}Forgot My Password{%endtrans%}]</a>
|
||||
{% endif %}
|
||||
@@ -45,7 +45,7 @@ Sign in
|
||||
</div>
|
||||
|
||||
{# Remember me #}
|
||||
{% if user_manager.enable_remember_me %}
|
||||
{% if user_manager.USER_ENABLE_REMEMBER_ME %}
|
||||
{{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
|
||||
{% endif %}
|
||||
|
||||
@@ -60,7 +60,7 @@ Sign in
|
||||
{% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %}
|
||||
<h2 class="card-header">{%trans%}Sign in with Github{%endtrans%}</h2>
|
||||
<div class="card-body">
|
||||
<a class="btn btn-primary" href="{{ url_for('github_signin_page') }}">GitHub</a>
|
||||
<a class="btn btn-primary" href="{{ url_for('users.github_signin') }}">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,7 +72,7 @@ Sign in
|
||||
<div class="card-body">
|
||||
<p>Create an account using your forum account or email.</p>
|
||||
|
||||
<a href="{{ url_for('user_claim_page') }}" class="btn btn-primary">{%trans%}Claim your account{%endtrans%}</a>
|
||||
<a href="{{ url_for('users.claim') }}" class="btn btn-primary">{%trans%}Claim your account{%endtrans%}</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -37,28 +37,28 @@
|
||||
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
||||
|
||||
|
||||
<a href="{{ url_for('packages_page', sort='created_at', order='desc') }}" class="btn btn-secondary float-right">
|
||||
<a href="{{ url_for('packages.list_all', sort='created_at', order='desc') }}" class="btn btn-secondary float-right">
|
||||
{{ _("See more") }}
|
||||
</a>
|
||||
<h2 class="my-3">{{ _("Recently Added") }}</h2>
|
||||
{{ render_pkggrid(new) }}
|
||||
|
||||
|
||||
<a href="{{ url_for('packages_page', type='mod', sort='score', order='desc') }}" class="btn btn-secondary float-right">
|
||||
<a href="{{ url_for('packages.list_all', type='mod', sort='score', order='desc') }}" class="btn btn-secondary float-right">
|
||||
{{ _("See more") }}
|
||||
</a>
|
||||
<h2 class="my-3">{{ _("Top Mods") }}</h2>
|
||||
{{ render_pkggrid(pop_mod) }}
|
||||
|
||||
|
||||
<a href="{{ url_for('packages_page', type='game', sort='score', order='desc') }}" class="btn btn-secondary float-right">
|
||||
<a href="{{ url_for('packages.list_all', type='game', sort='score', order='desc') }}" class="btn btn-secondary float-right">
|
||||
{{ _("See more") }}
|
||||
</a>
|
||||
<h2 class="my-3">{{ _("Top Games") }}</h2>
|
||||
{{ render_pkggrid(pop_gam) }}
|
||||
|
||||
|
||||
<a href="{{ url_for('packages_page', type='txp', sort='score', order='desc') }}" class="btn btn-secondary float-right">
|
||||
<a href="{{ url_for('packages.list_all', type='txp', sort='score', order='desc') }}" class="btn btn-secondary float-right">
|
||||
{{ _("See more") }}
|
||||
</a>
|
||||
<h2 class="my-3">{{ _("Top Texture Packs") }}</h2>
|
||||
|
||||
@@ -40,8 +40,10 @@
|
||||
{% else %}
|
||||
<li><i>No packages available</i></ul>
|
||||
{% endfor %}
|
||||
{% for i in range(4) %}
|
||||
<li class="packagetile flex-fill"></li>
|
||||
{% endfor %}
|
||||
{% if packages %}
|
||||
{% for i in range(4) %}
|
||||
<li class="packagetile flex-fill"></li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% for r in thread.replies %}
|
||||
<li class="row my-2 mx-0">
|
||||
<div class="col-md-1 p-1">
|
||||
<a href="{{ url_for('user_profile_page', username=r.author.username) }}">
|
||||
<a href="{{ url_for('users.profile', username=r.author.username) }}">
|
||||
<img class="img-responsive user-photo img-thumbnail img-thumbnail-1" src="{{ r.author.getProfilePicURL() }}">
|
||||
</a>
|
||||
</div>
|
||||
@@ -12,11 +12,11 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<a class="author {{ r.author.rank.name }}"
|
||||
href="{{ url_for('user_profile_page', username=r.author.username) }}">
|
||||
href="{{ url_for('users.profile', username=r.author.username) }}">
|
||||
{{ r.author.display_name }}
|
||||
</a>
|
||||
<a name="reply-{{ r.id }}" class="text-muted float-right"
|
||||
href="{{ url_for('thread_page', id=thread.id) }}#reply-{{ r.id }}">
|
||||
href="{{ url_for('threads.view', id=thread.id) }}#reply-{{ r.id }}">
|
||||
{{ r.created_at | datetime }}
|
||||
</a>
|
||||
</div>
|
||||
@@ -43,7 +43,7 @@
|
||||
</div>
|
||||
|
||||
{% if current_user.canCommentRL() %}
|
||||
<form method="post" action="{{ url_for('thread_page', id=thread.id)}}" class="card-body">
|
||||
<form method="post" action="{{ url_for('threads.view', id=thread.id)}}" class="card-body">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<textarea class="form-control markdown" required maxlength=500 name="comment"></textarea><br />
|
||||
<input class="btn btn-primary" type="submit" value="Comment" />
|
||||
@@ -65,14 +65,14 @@
|
||||
{% for t in threads %}
|
||||
<li {% if list_group %}class="list-group-item"{% endif %}>
|
||||
{% if list_group %}
|
||||
<a href="{{ url_for('thread_page', id=t.id) }}">
|
||||
<a href="{{ url_for('threads.view', id=t.id) }}">
|
||||
{% if t.private %}🔒 {% endif %}
|
||||
{{ t.title }}
|
||||
by {{ t.author.display_name }}
|
||||
</a>
|
||||
{% else %}
|
||||
{% if t.private %}🔒 {% endif %}
|
||||
<a href="{{ url_for('thread_page', id=t.id) }}">{{ t.title }}</a>
|
||||
<a href="{{ url_for('threads.view', id=t.id) }}">{{ t.title }}</a>
|
||||
by {{ t.author.display_name }}
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
@@ -18,14 +18,14 @@
|
||||
{% if topic.wip %}[WIP]{% endif %}
|
||||
</td>
|
||||
{% if show_author %}
|
||||
<td><a href="{{ url_for('user_profile_page', username=topic.author.username) }}">{{ topic.author.display_name}}</a></td>
|
||||
<td><a href="{{ url_for('users.profile', username=topic.author.username) }}">{{ topic.author.display_name}}</a></td>
|
||||
{% endif %}
|
||||
<td>{{ topic.name or ""}}</td>
|
||||
<td>{{ topic.created_at | date }}</td>
|
||||
<td class="btn-group">
|
||||
{% if current_user == topic.author or topic.author.checkPerm(current_user, "CHANGE_AUTHOR") %}
|
||||
<a class="btn btn-primary"
|
||||
href="{{ url_for('create_edit_package_page', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">
|
||||
href="{{ url_for('packages.create_edit', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">
|
||||
Create
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -56,10 +56,10 @@
|
||||
{% if topic.wip %}[WIP]{% endif %}
|
||||
{% if topic.name %}[{{ topic.name }}]{% endif %}
|
||||
{% if show_author %}
|
||||
by <a href="{{ url_for('user_profile_page', username=topic.author.username) }}">{{ topic.author.display_name }}</a>
|
||||
by <a href="{{ url_for('users.profile', username=topic.author.username) }}">{{ topic.author.display_name }}</a>
|
||||
{% endif %}
|
||||
{% if topic.author == current_user or topic.author.checkPerm(current_user, "CHANGE_AUTHOR") %}
|
||||
| <a href="{{ url_for('create_edit_package_page', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">Create</a>
|
||||
| <a href="{{ url_for('packages.create_edit', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">Create</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -7,7 +7,7 @@ Meta Packages
|
||||
{% block content %}
|
||||
<ul>
|
||||
{% for meta in mpackages %}
|
||||
<li><a href="{{ url_for('meta_package_page', name=meta.name) }}">{{ meta.name }}</a> ({{ meta.packages.filter_by(soft_deleted=False, approved=True).all() | count }} packages)</li>
|
||||
<li><a href="{{ url_for('metapackages.view', name=meta.name) }}">{{ meta.name }}</a> ({{ meta.packages.filter_by(soft_deleted=False, approved=True).all() | count }} packages)</li>
|
||||
{% else %}
|
||||
<li><i>No meta packages found.</i></li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -6,7 +6,7 @@ Notifications
|
||||
|
||||
{% block content %}
|
||||
{% if current_user.notifications %}
|
||||
<form method="post" action="{{ url_for('clear_notifications_page') }}">
|
||||
<form method="post" action="{{ url_for('notifications.clear') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="submit" value="Clear All" />
|
||||
</form>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{% for n in range(1, page_max+1) %}
|
||||
<li class="page-item {% if n == page %}active{% endif %}">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('packages_page', type=type, q=query, page=n) }}">
|
||||
href="{{ url_for('packages.list_all', type=type, q=query, page=n) }}">
|
||||
{{ n }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{{ _("Edit Release") }}</h2>
|
||||
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}
|
||||
<form method="POST" action="">
|
||||
{{ form.hidden_tag() }}
|
||||
@@ -26,7 +27,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if release.task_id %}
|
||||
Importing... <a href="{{ url_for('check_task', id=release.task_id, r=release.getEditURL()) }}">view task</a><br />
|
||||
Importing... <a href="{{ url_for('tasks.check', id=release.task_id, r=release.getEditURL()) }}">view task</a><br />
|
||||
{% if package.checkPerm(current_user, "CHANGE_RELEASE_URL") %}
|
||||
{{ render_field(form.task_id) }}
|
||||
{% endif %}
|
||||
@@ -59,6 +60,22 @@
|
||||
|
||||
{{ render_submit_field(form.submit) }}
|
||||
</form>
|
||||
|
||||
<h2 class="mt-5">{{ _("Delete Release") }}</h2>
|
||||
|
||||
{% if release.checkPerm(current_user, "DELETE_RELEASE") %}
|
||||
<form method="POST" action="{{ release.getDeleteURL() }}" class="alert alert-secondary mb-5">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input class="btn btn-sm btn-danger float-right" type="submit" value="{{ _('Delete') }}">
|
||||
<b>{{ _("This is permanent.") }}</b>
|
||||
{{ _("Any associated uploads will not be deleted immediately, but the release will no longer be listed.") }}
|
||||
<div style="clear:both;"></div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="alert alert-secondary mb-5">
|
||||
{{ _("You cannot delete the latest release; please create a newer one first.") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scriptextra %}
|
||||
|
||||
@@ -23,14 +23,14 @@
|
||||
</p>
|
||||
|
||||
<div class="row" style="margin-top: 2rem;">
|
||||
<div class="col">
|
||||
<div class="col text-secondary">
|
||||
{{ package.getDownloadCount() }} downloads
|
||||
</div>
|
||||
<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.forums %}<a class="btn btn-secondary" href="https://forum.minetest.net/viewtopic.php?t={{ package.forums }}">Forums</a>{% endif %}
|
||||
{% if package.issueTracker %}<a class="btn btn-secondary" href="{{ package.issueTracker }}">Issue Tracker</a>{% endif %}
|
||||
{% if package.website %}<a class="btn btn-secondary" href="{{ package.website }}">Website</a>{% endif %}
|
||||
{% if package.repo %}<a class="btn btn-outline-secondary" href="{{ package.repo }}">View Source</a>{% endif %}
|
||||
{% if package.forums %}<a class="btn btn-outline-secondary" href="https://forum.minetest.net/viewtopic.php?t={{ package.forums }}">Forums</a>{% endif %}
|
||||
{% if package.issueTracker %}<a class="btn btn-outline-secondary" href="{{ package.issueTracker }}">Issue Tracker</a>{% endif %}
|
||||
{% if package.website %}<a class="btn btn-outline-secondary" href="{{ package.website }}">Website</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
{% if not review_thread and (package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW")) %}
|
||||
<div class="alert alert-info">
|
||||
<a class="float-right btn btn-sm btn-info" href="{{ url_for('new_thread_page', pid=package.id, title='Package approval comments') }}">Open Thread</a>
|
||||
<a class="float-right btn btn-sm btn-info" href="{{ url_for('threads.new', pid=package.id, title='Package approval comments') }}">Open Thread</a>
|
||||
|
||||
Privately ask a question or give feedback
|
||||
<div style="clear:both;"></div>
|
||||
@@ -172,14 +172,14 @@
|
||||
<td>Provides</td>
|
||||
<td>{% for meta in package.provides %}
|
||||
<a class="badge badge-primary"
|
||||
href="{{ url_for('meta_package_page', name=meta.name) }}">{{ meta.name }}</a>
|
||||
href="{{ url_for('metapackages.view', name=meta.name) }}">{{ meta.name }}</a>
|
||||
{% endfor %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Author</td>
|
||||
<td class="{{ package.author.rank }}">
|
||||
<a href="{{ url_for('user_profile_page', username=package.author.username) }}">
|
||||
<a href="{{ url_for('users.profile', username=package.author.username) }}">
|
||||
{{ package.author.display_name }}
|
||||
</a>
|
||||
</td>
|
||||
@@ -241,7 +241,7 @@
|
||||
{{ dep.package.title }} by {{ dep.package.author.display_name }}
|
||||
{% elif dep.meta_package %}
|
||||
<a class="badge badge-{{ color }}"
|
||||
href="{{ url_for('meta_package_page', name=dep.meta_package.name) }}">
|
||||
href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}">
|
||||
{{ dep.meta_package.name }}
|
||||
{% else %}
|
||||
{{ "Excepted package or meta_package in dep!" | throw }}
|
||||
@@ -301,7 +301,7 @@
|
||||
created {{ rel.releaseDate | date }}.
|
||||
</small>
|
||||
{% if (package.checkPerm(current_user, "MAKE_RELEASE") or package.checkPerm(current_user, "APPROVE_RELEASE")) and rel.task_id %}
|
||||
<a href="{{ url_for('check_task', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
|
||||
<a href="{{ url_for('tasks.check', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
|
||||
{% elif not rel.approved %}
|
||||
Waiting for approval.
|
||||
{% endif %}
|
||||
@@ -320,7 +320,7 @@
|
||||
<div class="card-header">
|
||||
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %}
|
||||
<a class="float-right"
|
||||
href="{{ url_for('new_thread_page', pid=package.id) }}">+</a>
|
||||
href="{{ url_for('threads.new', pid=package.id) }}">+</a>
|
||||
{% endif %}
|
||||
Threads
|
||||
</div>
|
||||
@@ -332,7 +332,7 @@
|
||||
|
||||
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") and current_user != package.author and not current_user.rank.atLeast(current_user.rank.EDITOR) %}
|
||||
<a class="float-right"
|
||||
href="{{ url_for('new_thread_page', pid=package.id) }}">
|
||||
href="{{ url_for('threads.new', pid=package.id) }}">
|
||||
Report a problem with this listing
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -381,7 +381,7 @@
|
||||
<li>
|
||||
<a href="{{ r.getURL() }}">{{ r.title }}</a>
|
||||
by
|
||||
<a href="{{ url_for('user_profile_page', username=r.author.username) }}">{{ r.author.display_name }}</a>
|
||||
<a href="{{ url_for('users.profile', username=r.author.username) }}">{{ r.author.display_name }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>No edit requests have been made.</li>
|
||||
|
||||
@@ -16,7 +16,7 @@ Working
|
||||
<script>
|
||||
// @author rubenwardy
|
||||
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||
pollTask("{{ url_for('check_task', id=info.id) }}", true)
|
||||
pollTask("{{ url_for('tasks.check', id=info.id) }}", true)
|
||||
.then(function() { location.reload() })
|
||||
.catch(function() { location.reload() })
|
||||
</script>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
{{ render_checkbox_field(form.private, class_="my-3") }}
|
||||
<p>
|
||||
Only the you, the package author, and users of Editor rank
|
||||
Only you, the package author, and users of Editor rank
|
||||
and above can read private threads.
|
||||
</p>
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for r in releases %}
|
||||
<li class="list-group-item">
|
||||
{% if r.task_id %}
|
||||
<span class="mr-2 badge badge-warning">Importing</span>
|
||||
{% endif %}
|
||||
<a href="{{ r.getEditURL() }}">{{ r.title }}</a>
|
||||
on
|
||||
<a href="{{ r.package.getDetailsURL() }}">
|
||||
@@ -60,7 +63,7 @@
|
||||
{% if canApproveScn and screenshots %}
|
||||
<div class="card my-4">
|
||||
<h3 class="card-header">Screenshots
|
||||
<form class="float-right" method="post" action="{{ url_for('todo_page') }}">
|
||||
<form class="float-right" method="post" action="{{ url_for('todo.view') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="action" value="screenshots_approve_all" />
|
||||
<input class="btn btn-sm btn-primary" type="submit" value="Approve All" />
|
||||
@@ -98,17 +101,23 @@
|
||||
|
||||
<h2 class="mt-4">Unadded Topic List</h2>
|
||||
|
||||
<p>
|
||||
{{ total_topics - topics_to_add }} / {{ total_topics }} packages have been been added to cdb,
|
||||
based on cdb's forum parser. {{ topics_to_add }} remaining.
|
||||
</p>
|
||||
{% if total_topics > 0 %}
|
||||
<p>
|
||||
{{ total_topics - topics_to_add }} / {{ total_topics }} packages have been been added to cdb,
|
||||
based on cdb's forum parser. {{ topics_to_add }} remaining.
|
||||
</p>
|
||||
|
||||
<div class="progress my-4">
|
||||
{% set perc = 32 %}
|
||||
<div class="progress-bar bg-success" role="progressbar"
|
||||
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<div class="progress my-4">
|
||||
{% set perc = 100 * (total_topics - topics_to_add) / total_topics %}
|
||||
<div class="progress-bar bg-success" role="progressbar"
|
||||
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
|
||||
<a class="btn btn-primary" href="{{ url_for('todo_topics_page') }}">View Unadded Topic List</a>
|
||||
<a class="btn btn-primary" href="{{ url_for('todo.topics') }}">View Unadded Topic List</a>
|
||||
{% else %}
|
||||
<p>
|
||||
The forum topic crawler needs to run at least once for this section to work.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -8,15 +8,15 @@ Topics to be Added
|
||||
<div class="float-right">
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-primary {% if sort_by=='date' %}active{% endif %}"
|
||||
href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=n, sort='date') }}">
|
||||
href="{{ url_for('todo.topics', q=query, show_discarded=show_discarded, n=n, sort='date') }}">
|
||||
Sort by date
|
||||
</a>
|
||||
<a class="btn btn-primary {% if sort_by=='name' %}active{% endif %}"
|
||||
href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=n, sort='name') }}">
|
||||
href="{{ url_for('todo.topics', q=query, show_discarded=show_discarded, n=n, sort='name') }}">
|
||||
Sort by name
|
||||
</a>
|
||||
<a class="btn btn-primary {% if sort_by=='views' %}active{% endif %}"
|
||||
href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=n, sort='views') }}">
|
||||
href="{{ url_for('todo.topics', q=query, show_discarded=show_discarded, n=n, sort='views') }}">
|
||||
Sort by views
|
||||
</a>
|
||||
</div>
|
||||
@@ -26,18 +26,18 @@ Topics to be Added
|
||||
{% if current_user.rank.atLeast(current_user.rank.EDITOR) %}
|
||||
{% if n >= 10000 %}
|
||||
<a class="btn btn-primary"
|
||||
href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=100, sort=sort_by) }}">
|
||||
href="{{ url_for('todo.topics', q=query, show_discarded=show_discarded, n=100, sort=sort_by) }}">
|
||||
Paginated list
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="btn btn-primary"
|
||||
href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=10000, sort=sort_by) }}">
|
||||
href="{{ url_for('todo.topics', q=query, show_discarded=show_discarded, n=10000, sort=sort_by) }}">
|
||||
Unlimited list
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<a class="btn btn-primary" href="{{ url_for('todo_topics_page', q=query, show_discarded=not show_discarded, n=n, sort=sort_by) }}">
|
||||
<a class="btn btn-primary" href="{{ url_for('todo.topics', q=query, show_discarded=not show_discarded, n=n, sort=sort_by) }}">
|
||||
{% if not show_discarded %}
|
||||
Show
|
||||
{% else %}
|
||||
@@ -51,17 +51,23 @@ Topics to be Added
|
||||
|
||||
<h1>Topics to be Added</h1>
|
||||
|
||||
<p>
|
||||
{{ total - topic_count }} / {{ total }} topics have been added as packages to CDB.
|
||||
{{ topic_count }} remaining.
|
||||
</p>
|
||||
<div class="progress">
|
||||
{% set perc = 100 * (total - topic_count) / total %}
|
||||
<div class="progress-bar bg-success" role="progressbar"
|
||||
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
{% if topic_count > 0 %}
|
||||
<p>
|
||||
{{ total - topic_count }} / {{ total }} topics have been added as packages to CDB.
|
||||
{{ topic_count }} remaining.
|
||||
</p>
|
||||
<div class="progress">
|
||||
{% set perc = 100 * (total - topic_count) / total %}
|
||||
<div class="progress-bar bg-success" role="progressbar"
|
||||
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
The forum topic crawler needs to run at least once for this section to work.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="GET" action="{{ url_for('todo_topics_page') }}" class="my-4">
|
||||
<form method="GET" action="{{ url_for('todo.topics') }}" class="my-4">
|
||||
<input type="hidden" name="show_discarded" value={{ show_discarded and "True" or "False" }} />
|
||||
<input type="hidden" name="n" value={{ n }} />
|
||||
<input type="hidden" name="sort" value={{ sort_by or "date" }} />
|
||||
@@ -79,7 +85,7 @@ Topics to be Added
|
||||
{% for i in range(1, page_max+1) %}
|
||||
<li class="page-item {% if i == page %}active{% endif %}">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('todo_topics_page', page=i, query=query, show_discarded=show_discarded, n=n, sort=sort_by) }}">
|
||||
href="{{ url_for('todo.topics', page=i, query=query, show_discarded=show_discarded, n=n, sort=sort_by) }}">
|
||||
{{ i }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -19,7 +19,7 @@ Creating an Account
|
||||
Please log out to continue.
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url_for('user.logout', next=url_for('user_claim_page')) }}" class="btn">Logout</a>
|
||||
<a href="{{ url_for('user.logout', next=url_for('users.claim')) }}" class="btn">Logout</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
@@ -44,7 +44,7 @@ Creating an Account
|
||||
Use GitHub field in forum profile
|
||||
</div>
|
||||
|
||||
<form method="post" class="card-body" action="{{ url_for('user_claim_page') }}">
|
||||
<form method="post" class="card-body" action="{{ url_for('users.claim') }}">
|
||||
<input class="form-control" type="hidden" name="claim_type" value="github">
|
||||
<input class="form-control" type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
|
||||
@@ -73,7 +73,7 @@ Creating an Account
|
||||
Verification token
|
||||
</div>
|
||||
|
||||
<form method="post" class="card-body" action="{{ url_for('user_claim_page') }}">
|
||||
<form method="post" class="card-body" action="{{ url_for('users.claim') }}">
|
||||
<input type="hidden" name="claim_type" value="forum">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<ul class="userlist">
|
||||
{% for user in users %}
|
||||
<li class="{{ user.rank }}">
|
||||
<a href="{{ url_for('user_profile_page', username=user.username) }}">
|
||||
<a href="{{ url_for('users.profile', username=user.username) }}">
|
||||
{{ user.display_name }}
|
||||
</a> -
|
||||
{{ user.rank.getTitle() }}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{% if not current_user.is_authenticated and user.rank == user.rank.NOT_JOINED and user.forums_username %}
|
||||
<div class="alert alert-info">
|
||||
<a class="float-right btn btn-default btn-sm"
|
||||
href="{{ url_for('user_claim_page', username=user.forums_username) }}">Claim</a>
|
||||
href="{{ url_for('users.claim', username=user.forums_username) }}">Claim</a>
|
||||
|
||||
Is this you? Claim your account now!
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@
|
||||
{% if user.github_username %}
|
||||
<a href="https://github.com/{{ user.github_username }}">GitHub</a>
|
||||
{% elif user == current_user %}
|
||||
<a href="{{ url_for('github_signin_page') }}">Link Github</a>
|
||||
<a href="{{ url_for('users.github_signin') }}">Link Github</a>
|
||||
{% endif %}
|
||||
|
||||
{% if user.website_url %}
|
||||
@@ -78,7 +78,7 @@
|
||||
<td>Admin</td>
|
||||
<td>
|
||||
{% if user.email %}
|
||||
<a class="btn btn-primary" href="{{ url_for('send_email_page', username=user.username) }}">
|
||||
<a class="btn btn-primary" href="{{ url_for('users.send_email', username=user.username) }}">
|
||||
Email
|
||||
</a>
|
||||
{% else %}
|
||||
@@ -97,7 +97,7 @@
|
||||
<td>Profile Picture:</td>
|
||||
<td>
|
||||
{% if user.forums_username %}
|
||||
<form method="post" action="{{ url_for('user_check', username=user.username) }}" class="" style="display:inline-block;">
|
||||
<form method="post" action="{{ url_for('users.user_check', username=user.username) }}" class="" style="display:inline-block;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="submit" class="btn btn-primary" value="Sync with Forums" />
|
||||
</form>
|
||||
@@ -122,11 +122,20 @@
|
||||
{% if user.password %}
|
||||
Set | <a href="{{ url_for('user.change_password') }}">Change</a>
|
||||
{% else %}
|
||||
Not set | <a href="{{ url_for('set_password_page') }}">Set</a>
|
||||
Not set | <a href="{{ url_for('users.set_password') }}">Set</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if user.checkPerm(current_user, "CREATE_TOKEN") %}
|
||||
<tr>
|
||||
<td>API Tokens:</td>
|
||||
<td>
|
||||
<a href="{{ url_for('api.list_tokens', username=user.username) }}">Manage</a>
|
||||
<span class="badge badge-primary">{{ user.tokens.count() }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="alert alert-primary">
|
||||
It is recommended that you set a password for your account.
|
||||
|
||||
<a class="alert_right button" href="{{ url_for('home_page') }}">Skip</a>
|
||||
<a class="alert_right button" href="{{ url_for('homepage.home') }}">Skip</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
105
app/tests/test_api.py
Normal file
105
app/tests/test_api.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import pytest
|
||||
from app import app
|
||||
from app.default_data import populate_test_data
|
||||
from app.models import db, License, Tag, User, UserRank, Package
|
||||
from utils import client, recreate_db, parse_json
|
||||
from utils import is_str, is_int, is_optional
|
||||
|
||||
def validate_package_list(packages, strict=False):
|
||||
valid_keys = {
|
||||
"author", "name", "release",
|
||||
"short_description", "thumbnail",
|
||||
"title", "type"
|
||||
}
|
||||
|
||||
for package in packages:
|
||||
assert set(package.keys()).issubset(valid_keys)
|
||||
|
||||
assert is_str(package.get("author"))
|
||||
assert is_str(package.get("name"))
|
||||
if strict:
|
||||
assert is_int(package.get("release"))
|
||||
else:
|
||||
assert is_optional(int, package.get("release"))
|
||||
assert is_str(package.get("short_description"))
|
||||
assert is_optional(str, package.get("thumbnail"))
|
||||
assert is_str(package.get("title"))
|
||||
assert is_str(package.get("type"))
|
||||
|
||||
|
||||
def test_packages_empty(client):
|
||||
"""Start with a blank database."""
|
||||
|
||||
rv = client.get("/api/packages/")
|
||||
assert parse_json(rv.data) == []
|
||||
|
||||
|
||||
def test_packages_with_contents(client):
|
||||
"""Start with a test database."""
|
||||
|
||||
|
||||
populate_test_data(db.session)
|
||||
db.session.commit()
|
||||
|
||||
rv = client.get("/api/packages/")
|
||||
|
||||
packages = parse_json(rv.data)
|
||||
|
||||
assert len(packages) > 0
|
||||
assert len(packages) == Package.query.filter_by(approved=True).count()
|
||||
|
||||
validate_package_list(packages)
|
||||
|
||||
|
||||
def test_packages_with_query(client):
|
||||
"""Start with a test database."""
|
||||
|
||||
populate_test_data(db.session)
|
||||
db.session.commit()
|
||||
|
||||
rv = client.get("/api/packages/?q=food")
|
||||
|
||||
packages = parse_json(rv.data)
|
||||
|
||||
assert len(packages) == 2
|
||||
|
||||
validate_package_list(packages)
|
||||
|
||||
assert (packages[0]["name"] == "food" and packages[1]["name"] == "food_sweet") or \
|
||||
(packages[1]["name"] == "food" and packages[0]["name"] == "food_sweet")
|
||||
|
||||
|
||||
def test_packages_with_protocol_high(client):
|
||||
"""Start with a test database."""
|
||||
|
||||
populate_test_data(db.session)
|
||||
db.session.commit()
|
||||
|
||||
rv = client.get("/api/packages/?protocol_version=40")
|
||||
|
||||
packages = parse_json(rv.data)
|
||||
|
||||
assert len(packages) == 4
|
||||
|
||||
for package in packages:
|
||||
assert package["name"] != "mesecons"
|
||||
|
||||
validate_package_list(packages, True)
|
||||
|
||||
|
||||
def test_packages_with_protocol_low(client):
|
||||
"""Start with a test database."""
|
||||
|
||||
populate_test_data(db.session)
|
||||
db.session.commit()
|
||||
|
||||
rv = client.get("/api/packages/?protocol_version=20")
|
||||
|
||||
packages = parse_json(rv.data)
|
||||
|
||||
assert len(packages) == 4
|
||||
|
||||
for package in packages:
|
||||
assert package["name"] != "awards"
|
||||
|
||||
validate_package_list(packages, True)
|
||||
22
app/tests/test_homepage.py
Normal file
22
app/tests/test_homepage.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import pytest
|
||||
from app import app
|
||||
from app.default_data import populate_test_data
|
||||
from app.models import db, License, Tag, User, UserRank
|
||||
from utils import client, recreate_db
|
||||
|
||||
def test_homepage_empty(client):
|
||||
"""Start with a blank database."""
|
||||
|
||||
rv = client.get("/")
|
||||
assert b"No packages available" in rv.data and b"packagetile" not in rv.data
|
||||
|
||||
|
||||
def test_homepage_with_contents(client):
|
||||
"""Start with a test database."""
|
||||
|
||||
populate_test_data(db.session)
|
||||
db.session.commit()
|
||||
|
||||
rv = client.get("/")
|
||||
|
||||
assert b"No packages available" not in rv.data and b"packagetile" in rv.data
|
||||
45
app/tests/utils.py
Normal file
45
app/tests/utils.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import pytest, json
|
||||
from app import app
|
||||
from app.models import db, User
|
||||
from app.default_data import populate
|
||||
|
||||
def clear_data(session):
|
||||
meta = db.metadata
|
||||
for table in reversed(meta.sorted_tables):
|
||||
session.execute(f'ALTER TABLE "{table.name}" DISABLE TRIGGER ALL;')
|
||||
session.execute(table.delete())
|
||||
session.execute(f'ALTER TABLE "{table.name}" ENABLE TRIGGER ALL;')
|
||||
#session.execute(table.delete())
|
||||
|
||||
def recreate_db():
|
||||
clear_data(db.session)
|
||||
populate(db.session)
|
||||
db.session.commit()
|
||||
|
||||
def parse_json(b):
|
||||
return json.loads(b.decode("utf8"))
|
||||
|
||||
def is_type(t, v):
|
||||
return v and isinstance(v, t)
|
||||
|
||||
def is_optional(t, v):
|
||||
return not v or isinstance(v, t)
|
||||
|
||||
def is_str(v):
|
||||
return is_type(str, v)
|
||||
|
||||
def is_int(v):
|
||||
return is_type(int, v)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app.config["TESTING"] = True
|
||||
|
||||
recreate_db()
|
||||
assert User.query.count() == 1
|
||||
|
||||
with app.test_client() as client:
|
||||
yield client
|
||||
|
||||
app.config["TESTING"] = False
|
||||
40
app/utils.py
40
app/utils.py
@@ -22,6 +22,12 @@ from app.models import *
|
||||
from app import app
|
||||
import random, string, os, imghdr
|
||||
|
||||
def get_int_or_abort(v, default):
|
||||
try:
|
||||
return int(v or default)
|
||||
except ValueError:
|
||||
abort(400)
|
||||
|
||||
def getExtension(filename):
|
||||
return filename.rsplit(".", 1)[1].lower() if "." in filename else None
|
||||
|
||||
@@ -43,14 +49,16 @@ def randomString(n):
|
||||
def doFileUpload(file, fileType, fileTypeDesc):
|
||||
if not file or file is None or file.filename == "":
|
||||
flash("No selected file", "error")
|
||||
return None
|
||||
return None, None
|
||||
|
||||
assert os.path.isdir(app.config["UPLOAD_DIR"]), "UPLOAD_DIR must exist"
|
||||
|
||||
allowedExtensions = []
|
||||
isImage = False
|
||||
if fileType == "image":
|
||||
allowedExtensions = ["jpg", "jpeg", "png"]
|
||||
isImage = True
|
||||
elif filetype == "zip":
|
||||
elif fileType == "zip":
|
||||
allowedExtensions = ["zip"]
|
||||
else:
|
||||
raise Exception("Invalid fileType")
|
||||
@@ -58,17 +66,18 @@ def doFileUpload(file, fileType, fileTypeDesc):
|
||||
ext = getExtension(file.filename)
|
||||
if ext is None or not ext in allowedExtensions:
|
||||
flash("Please upload load " + fileTypeDesc, "danger")
|
||||
return None
|
||||
return None, None
|
||||
|
||||
if isImage and not isAllowedImage(file.stream.read()):
|
||||
flash("Uploaded image isn't actually an image", "danger")
|
||||
return None
|
||||
return None, None
|
||||
|
||||
file.stream.seek(0)
|
||||
|
||||
filename = randomString(10) + "." + ext
|
||||
file.save(os.path.join("app/public/uploads", filename))
|
||||
return "/uploads/" + filename
|
||||
filepath = os.path.join(app.config["UPLOAD_DIR"], filename)
|
||||
file.save(filepath)
|
||||
return "/uploads/" + filename, filepath
|
||||
|
||||
def make_flask_user_password(plaintext_str):
|
||||
# http://passlib.readthedocs.io/en/stable/modular_crypt_format.html
|
||||
@@ -93,7 +102,7 @@ def make_flask_user_password(plaintext_str):
|
||||
else:
|
||||
return password.decode("UTF-8")
|
||||
|
||||
def _do_login_user(user, remember_me=False):
|
||||
def loginUser(user):
|
||||
def _call_or_get(v):
|
||||
if callable(v):
|
||||
return v()
|
||||
@@ -119,29 +128,14 @@ def _do_login_user(user, remember_me=False):
|
||||
flash("Your account has not been enabled.", "error")
|
||||
return False
|
||||
|
||||
# Check if user has a confirmed email address
|
||||
user_manager = current_app.user_manager
|
||||
if user_manager.enable_email and user_manager.enable_confirm_email \
|
||||
and not current_app.user_manager.enable_login_without_confirm_email \
|
||||
and not user.has_confirmed_email():
|
||||
url = url_for("user.resend_confirm_email")
|
||||
flash("Your email address has not yet been confirmed", "error")
|
||||
return False
|
||||
|
||||
# Use Flask-Login to sign in user
|
||||
login_user(user, remember=remember_me)
|
||||
login_user(user, remember=True)
|
||||
signals.user_logged_in.send(current_app._get_current_object(), user=user)
|
||||
|
||||
flash("You have signed in successfully.", "success")
|
||||
|
||||
return True
|
||||
|
||||
def loginUser(user):
|
||||
user_mixin = None
|
||||
if user_manager.enable_username:
|
||||
user_mixin = user_manager.find_user_by_username(user.username)
|
||||
|
||||
return _do_login_user(user_mixin, True)
|
||||
|
||||
def rank_required(rank):
|
||||
def decorator(f):
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from app import app, pages
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from app.models import *
|
||||
import flask_menu as menu
|
||||
from werkzeug.contrib.cache import SimpleCache
|
||||
from urllib.parse import urlparse
|
||||
from sqlalchemy.sql.expression import func
|
||||
cache = SimpleCache()
|
||||
|
||||
@app.template_filter()
|
||||
def throw(err):
|
||||
raise Exception(err)
|
||||
|
||||
@app.template_filter()
|
||||
def domain(url):
|
||||
return urlparse(url).netloc
|
||||
|
||||
@app.template_filter()
|
||||
def date(value):
|
||||
return value.strftime("%Y-%m-%d")
|
||||
|
||||
@app.template_filter()
|
||||
def datetime(value):
|
||||
return value.strftime("%Y-%m-%d %H:%M") + " UTC"
|
||||
|
||||
@app.route("/uploads/<path:path>")
|
||||
def send_upload(path):
|
||||
return send_from_directory("public/uploads", path)
|
||||
|
||||
@app.route("/")
|
||||
@menu.register_menu(app, ".", "Home")
|
||||
def home_page():
|
||||
query = Package.query.filter_by(approved=True, soft_deleted=False)
|
||||
count = query.count()
|
||||
new = query.order_by(db.desc(Package.created_at)).limit(8).all()
|
||||
pop_mod = query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score)).limit(8).all()
|
||||
pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(4).all()
|
||||
pop_txp = query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score)).limit(4).all()
|
||||
downloads = db.session.query(func.sum(PackageRelease.downloads)).first()[0]
|
||||
return render_template("index.html", count=count, downloads=downloads, \
|
||||
new=new, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam)
|
||||
|
||||
from . import users, packages, meta, threads, api
|
||||
from . import sass, thumbnails, tasks, admin
|
||||
|
||||
@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
|
||||
@app.route('/<path:path>/')
|
||||
def flatpage(path):
|
||||
page = pages.get_or_404(path)
|
||||
template = page.meta.get('template', 'flatpage.html')
|
||||
return render_template(template, page=page)
|
||||
|
||||
@app.before_request
|
||||
def do_something_whenever_a_request_comes_in():
|
||||
if current_user.is_authenticated:
|
||||
if current_user.rank == UserRank.BANNED:
|
||||
flash("You have been banned.", "error")
|
||||
logout_user()
|
||||
return redirect(url_for('user.login'))
|
||||
elif current_user.rank == UserRank.NOT_JOINED:
|
||||
current_user.rank = UserRank.MEMBER
|
||||
db.session.commit()
|
||||
@@ -1,28 +1,35 @@
|
||||
USER_APP_NAME="Content DB"
|
||||
SERVER_NAME="content.minetest.net"
|
||||
BASE_URL="http://" + SERVER_NAME
|
||||
USER_APP_NAME = "Content DB"
|
||||
SERVER_NAME = "content.minetest.net"
|
||||
BASE_URL = "http://" + SERVER_NAME
|
||||
|
||||
SECRET_KEY=""
|
||||
WTF_CSRF_SECRET_KEY=""
|
||||
SECRET_KEY = ""
|
||||
WTF_CSRF_SECRET_KEY = ""
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:///../db.sqlite"
|
||||
|
||||
GITHUB_CLIENT_ID = ""
|
||||
GITHUB_CLIENT_SECRET = ""
|
||||
|
||||
CELERY_BROKER_URL='redis://localhost:6379'
|
||||
CELERY_RESULT_BACKEND='redis://localhost:6379'
|
||||
REDIS_URL = 'redis://redis:6379'
|
||||
CELERY_BROKER_URL = 'redis://redis:6379'
|
||||
CELERY_RESULT_BACKEND = 'redis://redis:6379'
|
||||
|
||||
USER_ENABLE_USERNAME = True
|
||||
USER_ENABLE_REGISTER = False
|
||||
USER_ENABLE_CHANGE_USERNAME = False
|
||||
|
||||
MAIL_USERNAME=""
|
||||
MAIL_PASSWORD=""
|
||||
MAIL_DEFAULT_SENDER=""
|
||||
MAIL_SERVER=""
|
||||
MAIL_PORT=587
|
||||
MAIL_USE_TLS=True
|
||||
MAIL_UTILS_ERROR_SEND_TO=[""]
|
||||
MAIL_USERNAME = ""
|
||||
MAIL_PASSWORD = ""
|
||||
USER_EMAIL_SENDER_NAME = ""
|
||||
USER_EMAIL_SENDER_EMAIL = ""
|
||||
MAIL_DEFAULT_SENDER = ""
|
||||
MAIL_SERVER = ""
|
||||
MAIL_PORT = 587
|
||||
MAIL_USE_TLS = True
|
||||
MAIL_UTILS_ERROR_SEND_TO = [""]
|
||||
|
||||
UPLOAD_DIR = "/var/cdb/uploads/"
|
||||
THUMBNAIL_DIR = "/var/cdb/thumbnails/"
|
||||
|
||||
LANGUAGES = {
|
||||
'en': 'English',
|
||||
|
||||
@@ -15,13 +15,15 @@ services:
|
||||
|
||||
app:
|
||||
build: .
|
||||
command: ./rundebug.sh
|
||||
command: ./utils/entrypoint.sh
|
||||
env_file:
|
||||
- config.env
|
||||
ports:
|
||||
- 5123:5123
|
||||
volumes:
|
||||
- "./data/uploads:/home/cdb/app/public/uploads"
|
||||
- "./app:/home/cdb/app"
|
||||
- "./migrations:/home/cdb/migrations"
|
||||
- "./data/uploads:/var/cdb/uploads"
|
||||
- "./app:/source/app"
|
||||
- "./migrations:/source/migrations"
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
@@ -31,7 +33,30 @@ services:
|
||||
command: celery -A app.tasks.celery worker
|
||||
env_file:
|
||||
- config.env
|
||||
environment:
|
||||
- FLASK_CONFIG=../config.cfg
|
||||
volumes:
|
||||
- "./data/uploads:/home/cdb/app/public/uploads"
|
||||
- "./data/uploads:/var/cdb/uploads"
|
||||
- "./app:/home/cdb/app"
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
beat:
|
||||
build: .
|
||||
command: celery -A app.tasks.celery beat
|
||||
env_file:
|
||||
- config.env
|
||||
environment:
|
||||
- FLASK_CONFIG=../config.cfg
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
flower:
|
||||
image: mher/flower
|
||||
command: ["flower", "--broker=redis://redis:6379/0", "--port=5124"]
|
||||
env_file:
|
||||
- config.env
|
||||
ports:
|
||||
- 5124:5124
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
26
migrations/versions/306ce331a2a7_.py
Normal file
26
migrations/versions/306ce331a2a7_.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 306ce331a2a7
|
||||
Revises: 6dca6eceb04d
|
||||
Create Date: 2020-01-18 23:00:40.487425
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '306ce331a2a7'
|
||||
down_revision = '6dca6eceb04d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
op.create_check_constraint("CK_approval_valid", "package_release", "not approved OR (task_id IS NULL AND NOT url = '')")
|
||||
|
||||
|
||||
def downgrade():
|
||||
conn = op.get_bind()
|
||||
op.drop_constraint("CK_approval_valid", "package_release", type_="check")
|
||||
24
migrations/versions/64fee8e5ab34_.py
Normal file
24
migrations/versions/64fee8e5ab34_.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 64fee8e5ab34
|
||||
Revises: 306ce331a2a7
|
||||
Create Date: 2020-01-19 02:28:05.432244
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '64fee8e5ab34'
|
||||
down_revision = '306ce331a2a7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.alter_column('user', 'confirmed_at', nullable=False, new_column_name='email_confirmed_at')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.alter_column('user', 'email_confirmed_at', nullable=False, new_column_name='confirmed_at')
|
||||
30
migrations/versions/6dca6eceb04d_.py
Normal file
30
migrations/versions/6dca6eceb04d_.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 6dca6eceb04d
|
||||
Revises: fd25bf3e57c3
|
||||
Create Date: 2020-01-18 17:32:21.885068
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
from sqlalchemy_searchable import sync_trigger
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6dca6eceb04d'
|
||||
down_revision = 'fd25bf3e57c3'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
sync_trigger(conn, 'package', 'search_vector', ["name", "title", "short_desc", "desc"])
|
||||
op.create_check_constraint("name_valid", "package", "name ~* '^[a-z0-9_]+$'")
|
||||
|
||||
|
||||
def downgrade():
|
||||
conn = op.get_bind()
|
||||
sync_trigger(conn, 'package', 'search_vector', ["title", "short_desc", "desc"])
|
||||
op.drop_constraint("name_valid", "package", type_="check")
|
||||
31
migrations/versions/a0f6c8743362_.py
Normal file
31
migrations/versions/a0f6c8743362_.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: a0f6c8743362
|
||||
Revises: 64fee8e5ab34
|
||||
Create Date: 2020-01-19 19:12:39.402679
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a0f6c8743362'
|
||||
down_revision = '64fee8e5ab34'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.alter_column('user', 'password',
|
||||
existing_type=sa.VARCHAR(length=255),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text("''::character varying"),
|
||||
server_default='')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.alter_column('user', 'password',
|
||||
existing_type=sa.VARCHAR(length=255),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text("''::character varying"))
|
||||
37
migrations/versions/fd25bf3e57c3_.py
Normal file
37
migrations/versions/fd25bf3e57c3_.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: fd25bf3e57c3
|
||||
Revises: d6ae9682c45f
|
||||
Create Date: 2019-11-26 23:43:47.476346
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'fd25bf3e57c3'
|
||||
down_revision = 'd6ae9682c45f'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('api_token',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('access_token', sa.String(length=34), nullable=True),
|
||||
sa.Column('name', sa.String(length=100), nullable=False),
|
||||
sa.Column('owner_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('access_token')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('api_token')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,4 +1,4 @@
|
||||
Flask~=1.0
|
||||
Flask~=1.1
|
||||
Flask-FlatPages~=0.7
|
||||
Flask-Gravatar~=0.5
|
||||
Flask-Login~=0.4.1
|
||||
@@ -6,16 +6,21 @@ Flask-Markdown~=0.3
|
||||
Flask-Menu~=0.7
|
||||
Flask-Migrate~=2.3
|
||||
Flask-SQLAlchemy~=2.3
|
||||
Flask-User~=0.6
|
||||
Flask-User~=1.0
|
||||
Flask-Babel
|
||||
GitHub-Flask~=3.2
|
||||
SQLAlchemy-Searchable==1.0.3
|
||||
SQLAlchemy-Searchable~=1.1
|
||||
|
||||
beautifulsoup4~=4.6
|
||||
celery==4.1.1
|
||||
kombu==4.2.0
|
||||
GitPython~=2.1
|
||||
celery~=4.4
|
||||
kombu~=4.6
|
||||
GitPython~=3.0
|
||||
git-archive-all~=1.20
|
||||
lxml~=4.2
|
||||
pillow~=5.3
|
||||
pillow~=7.0
|
||||
pyScss~=1.3
|
||||
redis~=3.0
|
||||
redis~=3.3
|
||||
psycopg2~=2.7
|
||||
|
||||
pytest ~= 5.3
|
||||
pytest-cov ~= 2.8
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
FLASK_APP=app/__init__.py FLASK_CONFIG=../config.cfg FLASK_DEBUG=1 python3 -m flask run -h 0.0.0.0 -p 5123
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user