Compare commits

..

33 Commits

Author SHA1 Message Date
rubenwardy
9c2c8c21f1 Add content flag support in the API 2019-02-03 13:03:30 +00:00
rubenwardy
e40b247a97 Add OpenSearch and Google site search support 2019-02-02 17:05:18 +00:00
rubenwardy
a79cc758ed Add placeholder content ratings page 2019-01-30 17:57:56 +00:00
rubenwardy
bafd426eaf Add automatic approval of releases and screenshots 2019-01-29 18:30:30 +00:00
rubenwardy
36f9572cbb Fix replace problem in migration 2019-01-29 03:02:46 +00:00
rubenwardy
2586a11bcf Add fulltext search support 2019-01-29 03:00:01 +00:00
rubenwardy
d36138d5e1 Add version information to package page 2019-01-29 02:03:10 +00:00
rubenwardy
7810bb54e0 Add download counter to home page 2019-01-29 01:43:21 +00:00
rubenwardy
2844773e4d Fix wrong release ID returned by API on explicit protocol version 2019-01-29 01:29:49 +00:00
rubenwardy
23c406bff9 Add download counting 2019-01-29 00:49:44 +00:00
rubenwardy
0f3adda592 Improve spacing on bulk change releases 2019-01-29 00:27:26 +00:00
rubenwardy
441ed3beeb Add option to only change None entries with bulk change releases 2019-01-29 00:24:59 +00:00
rubenwardy
d1f5585fda Fix typos in text 2019-01-29 00:18:49 +00:00
rubenwardy
0fd3ed8f6b Fix bulk change form 2019-01-28 23:54:00 +00:00
rubenwardy
0e5c1f83ff Add MinetestRelease editor 2019-01-28 23:49:27 +00:00
rubenwardy
f112756b04 Disable fields in bulk change on checkbox 2019-01-28 23:40:31 +00:00
rubenwardy
f822027ec5 Hide create release fields depending on radio buttons 2019-01-28 23:17:00 +00:00
rubenwardy
034315d421 Add notes about min/max, and hide invalid options 2019-01-28 22:28:47 +00:00
rubenwardy
5cd8b35d1f Add ability to bulk change releases 2019-01-28 21:49:29 +00:00
rubenwardy
84b996c489 Add Minetest version checking to packages API 2019-01-28 21:33:50 +00:00
rubenwardy
d77403c0be Add min and max Minetest version support 2019-01-28 20:48:07 +00:00
rubenwardy
e9fe936aa9 Increase visibility of thread creation 2019-01-28 19:41:24 +00:00
rubenwardy
8afe17b984 Add comment ratelimiting, allow any member to open threads 2019-01-28 19:01:37 +00:00
rubenwardy
2691105513 Remove limit on provides field 2019-01-09 22:44:23 +00:00
rubenwardy
5f7efd4f31 Reduce README size 2019-01-09 22:35:11 +00:00
rubenwardy
7d52931a20 Add celery support to docker config 2019-01-09 22:29:32 +00:00
rubenwardy
a45df0e173 Add Docker support 2019-01-09 21:58:11 +00:00
rubenwardy
0db49efe4a Fix weird ordering of screenshots 2019-01-08 21:35:46 +00:00
rubenwardy
9639cf04f1 Improve views subfoldering 2019-01-08 17:37:33 +00:00
rubenwardy
9866e43b4b Split up packages/__init__.py 2019-01-08 17:17:36 +00:00
rubenwardy
014370ea06 Add email template 2019-01-04 19:17:04 +00:00
rubenwardy
fbf374ff5d Add manual email support 2019-01-04 17:57:00 +00:00
rubenwardy
a68ac9cb4d Add number of packages to bottom of homepage 2018-12-31 14:01:19 +00:00
62 changed files with 1726 additions and 586 deletions

3
.gitignore vendored
View File

@@ -1,5 +1,5 @@
config.cfg
config.prod.cfg
*.env
*.sqlite
custom.css
tmp
@@ -8,6 +8,7 @@ log.txt
uploads
thumbnails
celerybeat-schedule
/data
# Created by https://www.gitignore.io/api/linux,macos,python,windows

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM python:3.6
WORKDIR /home/cdb
COPY requirements.txt requirements.txt
RUN pip install -r ./requirements.txt
RUN pip install gunicorn
RUN pip install psycopg2
COPY runprodguni.sh ./
COPY rundebug.sh ./
RUN chmod +x runprodguni.sh
COPY setup.py ./setup.py
COPY app app
COPY migrations migrations
COPY config.cfg ./config.cfg

View File

@@ -1,57 +1,17 @@
# Content Database
## Setup
Content database for Minetest mods, games, and more.
First create a Python virtual env:
virtualenv env -ppython3
source env/bin/activate
then use pip:
pip3 install -r requirements.txt
### Development
* Copy config.example.cfg to config.cfg
* Fill SECRET_KEY and WTF_CSRF_SECRET_KEY in with a random string
* Make a Github OAuth Client at <https://github.com/settings/developers>:
* Homepage URL - `http://localhost:5000/`
* Authorization callback URL - `http://localhost:5000/user/github/callback/`
* Put client id and client secret in GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET
* Setup the database: python3 setup.py
## Running
### Development
You need to enter the virtual environment if you haven't yet in
the current session:
source env/bin/activate
If you need to, reset the db like so:
python3 setup.py -t
Then run the server:
./rundebug.py
Then view in your web browser: http://localhost:5000/
Developed by rubenwardy, license GPLv3.0+.
## How-tos
### Start celery worker
Note: you should first read one of the guides on the [Github repo wiki](https://github.com/minetest/contentdb/wiki)
```sh
# Run celery worker
FLASK_CONFIG=../config.cfg celery -A app.tasks.celery worker
```
### Create migration
```sh
# if sqlite
python setup.py -t
rm db.sqlite && python setup.py -t && FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db stamp head
@@ -61,4 +21,7 @@ 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
```

View File

@@ -32,7 +32,7 @@ app.config["FLATPAGES_EXTENSION"] = ".md"
app.config.from_pyfile(os.environ["FLASK_CONFIG"])
menu.Menu(app=app)
Markdown(app, extensions=["fenced_code"], safe_mode=True, output_format="html5")
markdown = Markdown(app, extensions=["fenced_code"], safe_mode=True, output_format="html5")
github = GitHub(app)
csrf = CsrfProtect(app)
mail = Mail(app)

View File

@@ -2,4 +2,5 @@ title: Help
* [Package Tags](package_tags)
* [Ranks and Permissions](ranks_permissions)
* [Content Ratings and Flags](content_flags)
* [Reporting Content](reporting)

View File

@@ -0,0 +1,26 @@
title: Content Flags
Content flags allow you to hide content based on your preferences.
The filtering is done server-side, which means that you don't need to update
your client to use new flags.
## Flags
* `nonfree` - can be used to hide packages which do not qualify as
'free software', as defined by the Free Software Foundation.
* A content rating, given below.
## Ratings
Content ratings aren't currently supported by ContentDB.
Instead, mature content isn't allowed at all for now.
In the future, more mature content will be allowed but labelled with
content ratings which may contain the following:
* android_default - meta-rating which includes gore and drugs.
* desktop_default - meta-rating which won't include anything for now.
* gore - more than just blood
* drugs
* swearing

View File

@@ -5,4 +5,4 @@ laws.
We take copyright violation and other offenses very seriously.
<a href="https://rubenwardy.com/contact/" class="button btn_green">Contact</a>
<a href="https://rubenwardy.com/contact/" class="btn btn-success">Contact</a>

View File

@@ -15,19 +15,29 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import Flask, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from urllib.parse import urlparse
import enum, datetime
from app import app, gravatar
from datetime import datetime
from sqlalchemy.orm import validates
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
import enum
from sqlalchemy.orm import validates
from sqlalchemy_searchable import SearchQueryMixin
from sqlalchemy_utils.types import TSVectorType
from sqlalchemy_searchable import make_searchable
# Initialise database
db = SQLAlchemy(app)
migrate = Migrate(app, db)
make_searchable(db.metadata)
class ArticleQuery(BaseQuery, SearchQueryMixin):
pass
class UserRank(enum.Enum):
@@ -129,8 +139,6 @@ class User(db.Model, UserMixin):
replies = db.relationship("ThreadReply", backref="author", lazy="dynamic")
def __init__(self, username, active=False, email=None, password=None):
import datetime
self.username = username
self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
self.display_name = username
@@ -172,6 +180,16 @@ class User(db.Model, UserMixin):
else:
raise Exception("Permission {} is not related to users".format(perm.name))
def canCommentRL(self):
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
return ThreadReply.query.filter_by(author=self) \
.filter(ThreadReply.created_at > hour_ago).count() < 4
def canOpenThreadRL(self):
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
return Thread.query.filter_by(author=self) \
.filter(Thread.created_at > hour_ago).count() < 2
class UserEmailVerification(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
@@ -239,7 +257,7 @@ class PackageType(enum.Enum):
class PackagePropertyKey(enum.Enum):
name = "Name"
title = "Title"
shortDesc = "Short Description"
short_desc = "Short Description"
desc = "Description"
type = "Type"
license = "License"
@@ -336,18 +354,21 @@ class Dependency(db.Model):
return retval
class Package(db.Model):
query_class = ArticleQuery
id = db.Column(db.Integer, primary_key=True)
# Basic details
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
name = db.Column(db.String(100), nullable=False)
title = db.Column(db.String(100), nullable=False)
shortDesc = db.Column(db.String(200), nullable=False)
desc = db.Column(db.Text, nullable=True)
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.utcnow)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
search_vector = db.Column(TSVectorType("title", "short_desc", "desc"))
license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
license = db.relationship("License", foreign_keys=[license_id])
@@ -380,7 +401,7 @@ class Package(db.Model):
lazy="dynamic", order_by=db.desc("package_release_releaseDate"))
screenshots = db.relationship("PackageScreenshot", backref="package",
lazy="dynamic")
lazy="dynamic", order_by=db.asc("package_screenshot_id"))
requests = db.relationship("EditRequest", backref="package",
lazy="dynamic")
@@ -396,26 +417,26 @@ class Package(db.Model):
for e in PackagePropertyKey:
setattr(self, e.name, getattr(package, e.name))
def getAsDictionaryShort(self, base_url):
def getAsDictionaryShort(self, base_url, protonum=None):
tnurl = self.getThumbnailURL(1)
return {
"name": self.name,
"title": self.title,
"author": self.author.display_name,
"short_description": self.shortDesc,
"short_description": self.short_desc,
"type": self.type.toName(),
"release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
"release": self.getDownloadRelease(protonum).id if self.getDownloadRelease(protonum) is not None else None,
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
"score": round(self.score * 10) / 10
}
def getAsDictionary(self, base_url):
def getAsDictionary(self, base_url, protonum=None):
tnurl = self.getThumbnailURL(1)
return {
"author": self.author.display_name,
"name": self.name,
"title": self.title,
"short_description": self.shortDesc,
"short_description": self.short_desc,
"desc": self.desc,
"type": self.type.toName(),
"created_at": self.created_at,
@@ -433,17 +454,17 @@ class Package(db.Model):
"screenshots": [base_url + ss.url for ss in self.screenshots],
"url": base_url + self.getDownloadURL(),
"release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
"release": self.getDownloadRelease(protonum).id if self.getDownloadRelease(protonum) is not None else None,
"score": round(self.score * 10) / 10
}
def getThumbnailURL(self, level=2):
screenshot = self.screenshots.filter_by(approved=True).first()
screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first()
return screenshot.getThumbnailURL(level) if screenshot is not None else None
def getMainScreenshotURL(self):
screenshot = self.screenshots.filter_by(approved=True).first()
screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first()
return screenshot.url if screenshot is not None else None
def getDetailsURL(self):
@@ -474,17 +495,38 @@ class Package(db.Model):
return url_for("create_edit_editrequest_page",
author=self.author.username, name=self.name)
def getBulkReleaseURL(self):
return url_for("bulk_change_release_page",
author=self.author.username, name=self.name)
def getDownloadURL(self):
return url_for("package_download_page",
author=self.author.username, name=self.name)
def getDownloadRelease(self):
def getDownloadRelease(self, protonum=None):
version = None
if protonum is not None:
version = MinetestRelease.query.filter(MinetestRelease.protocol >= int(protonum)).first()
if version is not None:
version = version.id
else:
version = 10000000
for rel in self.releases:
if rel.approved:
if rel.approved and (protonum is None or
((rel.min_rel is None or rel.min_rel_id <= version) and \
(rel.max_rel is None or rel.max_rel_id >= version))):
return rel
return None
def getDownloadCount(self):
counter = 0
for release in self.releases:
counter += release.downloads
return counter
def checkPerm(self, user, perm):
if not user.is_authenticated:
return False
@@ -496,8 +538,11 @@ class Package(db.Model):
isOwner = user == self.author
if perm == Permission.CREATE_THREAD:
return user.rank.atLeast(UserRank.MEMBER)
# Members can edit their own packages, and editors can edit any packages
if perm == Permission.MAKE_RELEASE or perm == Permission.ADD_SCREENSHOTS or perm == Permission.CREATE_THREAD:
if perm == Permission.MAKE_RELEASE or perm == Permission.ADD_SCREENSHOTS:
return isOwner or user.rank.atLeast(UserRank.EDITOR)
if perm == Permission.EDIT_PACKAGE or perm == Permission.APPROVE_CHANGES:
@@ -522,8 +567,6 @@ class Package(db.Model):
raise Exception("Permission {} is not related to packages".format(perm.name))
def recalcScore(self):
import datetime
self.score = 10
if self.forums is not None:
@@ -604,16 +647,36 @@ class Tag(db.Model):
regex = re.compile("[^a-z_]")
self.name = regex.sub("", self.title.lower().replace(" ", "_"))
class MinetestRelease(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
protocol = db.Column(db.Integer, nullable=False, default=0)
def __init__(self, name=None):
self.name = name
def getActual(self):
return None if self.name == "None" else self
class PackageRelease(db.Model):
id = db.Column(db.Integer, primary_key=True)
package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
title = db.Column(db.String(100), nullable=False)
releaseDate = db.Column(db.DateTime, nullable=False)
releaseDate = db.Column(db.DateTime, nullable=False)
url = db.Column(db.String(200), nullable=False)
approved = db.Column(db.Boolean, nullable=False, default=False)
task_id = db.Column(db.String(37), nullable=True)
commit_hash = db.Column(db.String(41), nullable=True, default=None)
downloads = db.Column(db.Integer, nullable=False, default=0)
min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])
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])
def getEditURL(self):
@@ -630,7 +693,7 @@ class PackageRelease(db.Model):
def __init__(self):
self.releaseDate = datetime.now()
self.releaseDate = datetime.datetime.now()
class PackageReview(db.Model):
@@ -762,7 +825,7 @@ class Thread(db.Model):
title = db.Column(db.String(100), nullable=False)
private = db.Column(db.Boolean, server_default="0")
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
replies = db.relationship("ThreadReply", backref="thread", lazy="dynamic")
@@ -800,7 +863,7 @@ class ThreadReply(db.Model):
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
comment = db.Column(db.String(500), nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \
@@ -824,7 +887,7 @@ class ForumTopic(db.Model):
posts = db.Column(db.Integer, nullable=False)
views = db.Column(db.Integer, nullable=False)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
def getRepoURL(self):
if self.link is None:

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>ContentDB</ShortName>
<LongName>ContentDB</LongName>
<InputEncoding>UTF-8</InputEncoding>
<Description>Search mods, games, and textures for Minetest.</Description>
<Tags>Minetest Mod Game Subgame Search</Tags>
<Url type="text/html" method="get" template="https://content.minetest.net/packages?q={searchTerms}"/>
</OpenSearchDescription>

View File

@@ -35,10 +35,10 @@ $(function() {
setField("#repo", result.repo || repoURL);
setField("#issueTracker", result.issueTracker);
setField("#desc", result.description);
setField("#shortDesc", result.short_description);
setField("#short_desc", result.short_description);
setField("#harddep_str", result.depends);
setField("#softdep_str", result.optional_depends);
setField("#shortDesc", result.short_description);
setField("#short_desc", result.short_description);
setField("#forums", result.forumId);
if (result.type && result.type.length > 2) {
$("#type").val(result.type);

View File

@@ -41,7 +41,7 @@ $(function() {
It's obvious that this adds something to Minetest,
there's no need to use phrases such as \"adds X to the game\".`
$("#shortDesc").on("change paste keyup", function() {
$("#short_desc").on("change paste keyup", function() {
var val = $(this).val().toLowerCase();
if (val.indexOf("minetest") >= 0 || val.indexOf("mod") >= 0 ||
val.indexOf("modpack") >= 0 || val.indexOf("mod pack") >= 0) {

View File

@@ -0,0 +1,18 @@
var min = $("#min_rel");
var max = $("#max_rel");
var none = $("#min_rel option:first-child").attr("value");
var warning = $("#minmax_warning");
function ver_check() {
var minv = min.val();
var maxv = max.val();
if (minv != none && maxv != none && minv > maxv) {
warning.show();
} else {
warning.hide();
}
}
min.change(ver_check);
max.change(ver_check);

102
app/querybuilder.py Normal file
View File

@@ -0,0 +1,102 @@
from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease
from .utils import isNo
from sqlalchemy.sql.expression import func
from flask import abort
from sqlalchemy import or_
class QueryBuilder:
title = None
types = None
search = None
def __init__(self, args):
title = "Packages"
# Get request types
types = args.getlist("type")
types = [PackageType.get(tname) for tname in types]
types = [type for type in types if type is not None]
if len(types) > 0:
title = ", ".join([type.value + "s" for type in types])
hide_flags = args.getlist("hide")
self.title = title
self.types = types
self.search = args.get("q")
self.random = "random" in args
self.lucky = self.random or "lucky" in args
self.hide_nonfree = "nonfree" in hide_flags
self.limit = 1 if self.lucky else None
self.order_by = args.get("sort") or "score"
self.order_dir = args.get("order") or "desc"
self.protocol_version = args.get("protocol_version")
if self.search is not None and self.search.strip() == "":
self.search = None
def buildPackageQuery(self):
query = Package.query.filter_by(soft_deleted=False, approved=True)
if len(self.types) > 0:
query = query.filter(Package.type.in_(self.types))
if self.search:
query = query.search(self.search)
if self.random:
query = query.order_by(func.random())
else:
to_order = None
if self.order_by == "score":
to_order = Package.score
elif self.order_by == "created_at":
to_order = Package.created_at
else:
abort(400)
if self.order_dir == "asc":
to_order = db.asc(to_order)
elif self.order_dir == "desc":
to_order = db.desc(to_order)
else:
abort(400)
query = query.order_by(to_order)
if self.hide_nonfree:
query = query.filter(Package.license.has(License.is_foss == True))
query = query.filter(Package.media_license.has(License.is_foss == True))
if self.protocol_version:
self.protocol_version = int(self.protocol_version)
version = MinetestRelease.query.filter(MinetestRelease.protocol>=self.protocol_version).first()
if version is not None:
version = version.id
else:
version = 10000000
query = query.join(Package.releases) \
.filter(or_(PackageRelease.min_rel_id==None, PackageRelease.min_rel_id<=version)) \
.filter(or_(PackageRelease.max_rel_id==None, PackageRelease.max_rel_id>=version))
if self.limit:
query = query.limit(self.limit)
return query
def buildTopicQuery(self):
topics = ForumTopic.query \
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title))
if self.search:
topics = topics.filter(ForumTopic.title.ilike('%' + self.search + '%'))
if len(self.types) > 0:
topics = topics.filter(ForumTopic.type.in_(self.types))
if self.limit:
topics = topics.limit(self.limit)
return topics

View File

@@ -15,7 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import *
from flask import render_template
from flask_mail import Message
from app import mail
from app.tasks import celery
@@ -32,8 +32,10 @@ def sendVerifyEmail(newEmail, token):
def sendEmailRaw(to, subject, text, html):
from flask_mail import Message
msg = Message(subject, recipients=to)
if text:
msg.body = text
if html:
msg.html = html
html = html or text
msg.html = render_template("emails/base.html", subject=subject, content=html)
mail.send(msg)

View File

@@ -6,7 +6,7 @@ Licenses
{% block content %}
<p>
<a href="{{ url_for('createedit_license_page') }}">New Tag</a>
<a href="{{ url_for('createedit_license_page') }}">New License</a>
</p>
<ul>
{% for l in licenses %}

View File

@@ -9,6 +9,7 @@
<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>
</ul>

View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}
{% if version %}
Edit {{ version.name }}
{% else %}
New Minetest Version
{% endif %}
{% endblock %}
{% block content %}
<p>
<a href="{{ url_for('version_list_page') }}">Back to list</a> |
<a href="{{ url_for('createedit_version_page') }}">New Version</a>
</p>
{% from "macros/forms.html" import render_field, render_submit_field %}
<form method="POST" action="" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{{ render_field(form.name) }}
{{ render_field(form.protocol) }}
{{ render_submit_field(form.submit) }}
</form>
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}
Minetest Versions
{% endblock %}
{% block content %}
<p>
<a href="{{ url_for('createedit_version_page') }}">New Version</a>
</p>
<ul>
{% for v in versions %}
<li><a href="{{ url_for('createedit_version_page', name=v.name) }}">{{ v.name }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -8,6 +8,7 @@
<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=6">
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
{% block headextra %}{% endblock %}
</head>
@@ -138,7 +139,7 @@
<link rel="stylesheet" type="text/css" href="/static/easymde.min.css">
<script>
$("textarea.markdown").each(function() {
new EasyMDE({ element: this, hideIcons: ["image"] });
new EasyMDE({ element: this, hideIcons: ["image"], forceSync: true });
})
</script>
{% block scriptextra %}{% endblock %}

View File

@@ -0,0 +1,64 @@
<!doctype html>
<html>
<head>
<style>
.btn {
display: inline-block !important;
color: #fff !important;
font-weight: 400;
text-align: center;
vertical-align: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: transparent;
border: 1px solid transparent;
border-top-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
padding: 0.375rem 0.75rem;
font-size: 0.9375rem;
line-height: 1.5;
border-radius: 0.25rem;
-webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
background-color: #2C3E50;
border-color: #2C3E50;
text-decoration: none;
}
.btn:hover {
color: #fff;
background-color: #1e2b37;
border-color: #1a252f;
}
.btn:focus {
-webkit-box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5);
box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5);
outline: 0;
}
</style>
</head>
<body>
<div style="font-family: 'Arial', 'sans-serif'; max-width: 700px; margin: auto; padding: 0;">
<div style="background: #2C3E50; padding: 1.2rem 1.2rem 1.2rem 2em; color: white;">
<h1 style="margin: 0; font-size: 120%; font-weight: normal;">ContentDB</h1>
</div>
<div style="padding: 2em; background: white;">
{% block content %}
<h2 style="margin-top: 0;">{{ subject }}</h2>
{{ content | safe }}
{% endblock %}
<div style="margin-top: 3em;font-size: 80%;color: #666;">
ContentDB &copy; rubenwardy
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,4 +1,7 @@
<h1>Hello!</h1>
{% extends "emails/base.html" %}
{% block content %}
<h2 style="margin-top: 0;">Hello!</h2>
<p>
This email has been sent to you because someone (hopefully you)
@@ -6,12 +9,19 @@
</p>
<p>
If this was you, then please click this link to verify the address:
<a href="{{ url_for('verify_email_page', token=token, _external=True) }}">
{{ url_for('verify_email_page', token=token, _external=True) }}
</a>
If it wasn't you, then just delete this email.
</p>
<p>
If it wasn't you, then just delete this email.
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) }}">
Confirm Email Address
</a>
<p style="font-size: 80%;">
Or paste this into your browser: {{ url_for('verify_email_page', token=token, _external=True) }}
<p>
{% endblock %}

View File

@@ -4,6 +4,21 @@
Welcome
{% endblock %}
{% block scriptextra %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"url": "https://content.minetest.net/",
"potentialAction": {
"@type": "SearchAction",
"target": "https://content.minetest.net/packages?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
{% endblock %}
{% block content %}
<!-- <header class="jumbotron">
<div class="container">
@@ -49,5 +64,10 @@ Welcome
<h2 class="my-3">Top Texture Packs</h2>
{{ render_pkggrid(pop_txp) }}
<div class="text-center">
<small>
CDB has {{ count }} packages, with a total of {{ downloads }} downloads.
</small>
</div>
<!-- </main> -->
{% endblock %}

View File

@@ -115,7 +115,7 @@
{% macro render_checkbox_field(field, label=None) -%}
{% if not label %}{% set label=field.label.text %}{% endif %}
<div class="checkbox">
<div class="checkbox {{ kwargs.pop('class_', '') }}">
<label>
{{ field(type='checkbox', **kwargs) }} {{ label }}
</label>

View File

@@ -12,7 +12,7 @@
</h3>
<p>
{{ package.shortDesc }}
{{ package.short_desc }}
</p>

View File

@@ -42,11 +42,18 @@
<a name="reply"></a>
</div>
<form method="post" action="{{ url_for('thread_page', 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" />
</form>
{% if current_user.canCommentRL() %}
<form method="post" action="{{ url_for('thread_page', 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" />
</form>
{% else %}
<div class="card-body">
<textarea class="form-control" readonly disabled>Please wait before commenting again.</textarea><br />
<input class="btn btn-primary" type="submit" disabled value="Comment" />
</div>
{% endif %}
</div>
</div>
</div>

View File

@@ -49,7 +49,7 @@
{{ render_field(form.title, class_="pkg_meta col-sm-7") }}
{{ render_field(form.name, class_="pkg_meta col-sm-3") }}
</div>
{{ render_field(form.shortDesc, class_="pkg_meta") }}
{{ render_field(form.short_desc, class_="pkg_meta") }}
{{ render_multiselect_field(form.tags, class_="pkg_meta") }}
<div class="pkg_meta row">
{{ render_field(form.license, class_="not_txp col-sm-6") }}

View File

@@ -17,7 +17,7 @@
{{ render_field(form.type) }}
{{ render_field(form.name) }}
{{ render_field(form.title) }}
{{ render_field(form.shortDesc) }}
{{ render_field(form.short_desc) }}
{{ render_field(form.desc) }}
{{ render_multiselect_field(form.tags) }}

View File

@@ -0,0 +1,67 @@
{% extends "base.html" %}
{% block title %}
Create a release | {{ package.title }}
{% endblock %}
{% block content %}
<h1>Bulk Change Releases</h1>
<p class="mb-5">
Use this page to set the min and max of all releases for your package.
</p>
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}
<form method="POST" action="">
{{ form.hidden_tag() }}
<div class="row">
{{ render_checkbox_field(form.set_min, class_="col-sm-2") }}
{{ render_field(form.min_rel, class_="col-sm-10") }}
</div>
<div class="row">
{{ render_checkbox_field(form.set_max, class_="col-sm-2") }}
{{ render_field(form.max_rel, class_="col-sm-10") }}
</div>
{{ render_checkbox_field(form.only_change_none) }}
<p id="minmax_warning" style="color:#f00; display: none;">
Maximum must be greater than or equal to the minimum!
</p>
<p class="mt-3">
Note: Min and max versions will be used to hide the package on
platforms not within the range.
You cannot select the oldest version for min or the newest version
for max as this does not make sense - you can't predict the future.<br />
Leave both as None if in doubt.
</p>
{{ render_submit_field(form.submit) }}
</form>
{% endblock %}
{% block scriptextra %}
<script src="/static/release_minmax.js?v=1"></script>
<script>
function setup_toggle(type) {
var toggle = $("#set_" + type);
function on_change() {
if (toggle.is(":checked")) {
// $("#" + type + "_rel").removeAttr("disabled");
$("#" + type + "_rel").parent().css("opacity", "1");
} else {
// $("#" + type + "_rel").attr("disabled", "disabled");
$("#" + type + "_rel").parent().css("opacity", "0.4");
$("#" + type + "_rel").val($("#" + type + "_rel option:first-child").attr("value"));
$("#" + type + "_rel").change();
}
}
toggle.change(on_change);
on_change();
}
setup_toggle("min");
setup_toggle("max");
</script>
{% endblock %}

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block title %}
Create a release | {{ package.title }}
Edit release | {{ package.title }}
{% endblock %}
{% block content %}
@@ -40,6 +40,27 @@
{% endif %}
{% endif %}
<div class="row">
{{ render_field(form.min_rel, class_="col-sm-6") }}
{{ render_field(form.max_rel, class_="col-sm-6") }}
</div>
<p id="minmax_warning" style="color:#f00; display: none;">
Maximum must be greater than or equal to the minimum!
</p>
<p>
Note: Min and max versions will be used to hide the package on
platforms not within the range.
You cannot select the oldest version for min or the newest version
for max as this does not make sense - you can't predict the future.<br />
Leave both as None if in doubt.
</p>
{{ render_submit_field(form.submit) }}
</form>
{% endblock %}
{% block scriptextra %}
<script src="/static/release_minmax.js?v=1"></script>
{% endblock %}

View File

@@ -17,7 +17,42 @@
{{ render_field(form.vcsLabel, class_="mt-3") }}
{% endif %}
{{ render_field(form.fileUpload, fieldclass="form-control-file") }}
{{ render_field(form.fileUpload, fieldclass="form-control-file", class_="mt-3") }}
<div class="row">
{{ render_field(form.min_rel, class_="col-sm-6") }}
{{ render_field(form.max_rel, class_="col-sm-6") }}
</div>
<p id="minmax_warning" style="color:#f00; display: none;">
Maximum must be greater than or equal to the minimum!
</p>
<p>
Note: Min and max versions will be used to hide the package on
platforms not within the range.
You cannot select the oldest version for min or the newest version
for max as this does not make sense - you can't predict the future.<br />
Leave both as None if in doubt.
</p>
{{ render_submit_field(form.submit) }}
</form>
{% endblock %}
{% block scriptextra %}
<script src="/static/release_minmax.js?v=1"></script>
<script>
function check_opt() {
if ($("input[name=uploadOpt]:checked").val() == "vcs") {
$("#fileUpload").parent().hide();
$("#vcsLabel").parent().show();
} else {
$("#fileUpload").parent().show();
$("#vcsLabel").parent().hide();
}
}
$("input[name=uploadOpt]").change(check_opt);
check_opt();
</script>
{% endblock %}

View File

@@ -19,11 +19,12 @@
</h1>
<p class="lead">
{{ package.shortDesc }}
{{ package.short_desc }}
</p>
<div class="row" style="margin-top: 2rem;">
<div class="col">
{{ 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 %}
@@ -111,11 +112,22 @@
{% endif %}
<aside class="float-right ml-4" style="width: 18rem;">
{% if package.getDownloadRelease() %}
<a class="btn btn-download btn-lg btn-block"
{% set release = package.getDownloadRelease() %}
{% if release %}
<a class="btn btn-download btn-lg btn-block" rel="nofollow"
href="{{ package.getDownloadURL() }}" class="btn_green">
Download
</a>
<p class="text-center m-2" style="font-size: 80%;">
{% if release.min_rel and release.max_rel %}
Minetest {{ release.min_rel.name }} - {{ release.max_rel.name }}
{% elif release.min_rel %}
Supports Minetest {{ release.min_rel.name }} and above.
{% elif release.max_rel %}
Supports Minetest {{ release.max_rel.name }} and below.
{% endif %}
</p>
{% else %}
No download available.
{% endif %}
@@ -237,10 +249,13 @@
<div class="card my-4">
<div class="card-header">
Releases
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
<a class="float-right"
href="{{ package.getCreateReleaseURL() }}">+</a>
{% endif %}
<div class="float-right">
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
<a href="{{ package.getBulkReleaseURL() }}">bulk</a>
|
<a href="{{ package.getCreateReleaseURL() }}">+</a>
{% endif %}
</div>
</div>
<ul class="list-group list-group-flush">
{% for rel in releases %}
@@ -257,10 +272,28 @@
{% if not rel.approved %}<i>{% endif %}
<a href="{{ rel.getDownloadURL() }}">{{ rel.title }}</a>{% if rel.commit_hash %}
[{{ rel.commit_hash | truncate(5, end='') }}]{% endif %}<br>
<small>created {{ rel.releaseDate | datetime }}.</small>
{% if rel.task_id %}
<a href="{{ rel.getDownloadURL() }}" rel="nofollow">{{ rel.title }}</a>
<span style="color:#ddd;">
{% if rel.min_rel and rel.max_rel %}
[MT {{ rel.min_rel.name }}-{{ rel.max_rel.name }}]
{% elif rel.min_rel %}
[MT {{ rel.min_rel.name }}+]
{% elif rel.max_rel %}
[MT &le;{{ rel.max_rel.name }}]
{% endif %}
</span>
<br>
<small style="color:#999;">
{% if rel.commit_hash %}
[{{ rel.commit_hash | truncate(5, end='') }}]
{% endif %}
created {{ rel.releaseDate | date }}.
</small>
{% if (package.checkPerm(current_user, "MAKE_RELEASE") or package.checkPerm(current_user, "APPROVE_RELEASE")) and rel.task_id %}
<a href="{{ url_for('check_task', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
{% elif not rel.approved %}
Waiting for approval.
@@ -289,6 +322,13 @@
{{ render_threadlist(threads, list_group=True) }}
</ul>
</div>
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") and current_user != package.author and not current_user.rank.atLeast(current_user.rank.EDITOR) %}
<a class="float-right"
href="{{ url_for('new_thread_page', pid=package.id) }}">
Report a problem with this listing
</a>
{% endif %}
</aside>
{% if not package.approved and (package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW")) %}

View File

@@ -6,6 +6,15 @@
{% block content %}
{% if package and current_user != package.author and not current_user.rank.atLeast(current_user.rank.EDITOR) %}
{% if package.issueTracker %}
<div class="alert alert-warning">
Found a bug? Post on the <a href="{{ package.issue_tracker }}">issue tracker</a> instead.<br />
If the package shouldn't be on CDB - for example, if it doesn't work at all - then please let us know here.
</div>
{% endif %}
{% endif %}
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}
<form method="POST" action="" enctype="multipart/form-data">
{{ form.hidden_tag() }}

View File

@@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block title %}
Send Email
{% endblock %}
{% block content %}
<h1>Send Email</h1>
{% from "macros/forms.html" import render_field, render_submit_field %}
<form action="" method="POST" class="form" role="form">
{{ form.hidden_tag() }}
{{ render_field(form.subject) }}
{{ render_field(form.text, fieldclass="form-control markdown") }}
{{ render_submit_field(form.submit) }}
</form>
{% endblock %}

View File

@@ -65,6 +65,25 @@
{% endif %}
</td>
</tr>
{% if current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.MODERATOR) %}
<tr>
<td>Admin</td>
<td>
{% if user.email %}
<a class="btn btn-primary" href="{{ url_for('send_email_page', username=user.username) }}">
Email
</a>
{% else %}
<a class="btn btn-primary disabled"
data-toggle="tooltip" data-placement="bottom"
title="No email address for user"
style="pointer-events: all;">
Email
</a>
{% endif %}
</td>
</tr>
{% endif %}
{% if user == current_user %}
<tr>
<td>Profile Picture:</td>

View File

@@ -22,6 +22,7 @@ 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()
@@ -53,12 +54,12 @@ def home_page():
pop_mod = query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score)).limit(8).all()
pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(4).all()
pop_txp = query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score)).limit(4).all()
return render_template("index.html", count=count, \
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, githublogin, packages, meta, threads, api
from . import tasks, admin, notifications, tagseditor, licenseseditor
from . import sass, thumbnails
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>/')

View File

@@ -0,0 +1,18 @@
# Content DB
# Copyright (C) 2018 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from . import admin, licenseseditor, tagseditor, versioneditor, todo

View File

@@ -0,0 +1,60 @@
# Content DB
# Copyright (C) 2018 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import *
from flask_user import *
from app import app
from app.models import *
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from app.utils import rank_required
@app.route("/versions/")
@rank_required(UserRank.MODERATOR)
def version_list_page():
return render_template("admin/versions/list.html", versions=MinetestRelease.query.order_by(db.asc(MinetestRelease.id)).all())
class VersionForm(FlaskForm):
name = StringField("Name", [InputRequired(), Length(3,100)])
protocol = IntegerField("Protocol")
submit = SubmitField("Save")
@app.route("/versions/new/", methods=["GET", "POST"])
@app.route("/versions/<name>/edit/", methods=["GET", "POST"])
@rank_required(UserRank.MODERATOR)
def createedit_version_page(name=None):
version = None
if name is not None:
version = MinetestRelease.query.filter_by(name=name).first()
if version is None:
abort(404)
form = VersionForm(formdata=request.form, obj=version)
if request.method == "POST" and form.validate():
if version is None:
version = MinetestRelease(form.name.data)
db.session.add(version)
flash("Created version " + form.name.data, "success")
else:
flash("Updated version " + form.name.data, "success")
form.populate_obj(version)
db.session.commit()
return redirect(url_for("version_list_page"))
return render_template("admin/versions/edit.html", version=version, form=form)

View File

@@ -19,15 +19,15 @@ from flask import *
from flask_user import *
from app import app
from app.models import *
from app.utils import is_package_page, rank_required
from .packages import QueryBuilder
from app.utils import is_package_page
from app.querybuilder import QueryBuilder
@app.route("/api/packages/")
def api_packages_page():
qb = QueryBuilder()
qb = QueryBuilder(request.args)
query = qb.buildPackageQuery()
pkgs = [package.getAsDictionaryShort(app.config["BASE_URL"]) \
pkgs = [package.getAsDictionaryShort(app.config["BASE_URL"], request.args.get("protocol_version")) \
for package in query.all() if package.getDownloadRelease() is not None]
return jsonify(pkgs)
@@ -61,3 +61,9 @@ def topic_set_discard():
db.session.commit()
return jsonify(topic.getAsDictionary())
@app.route("/api/minetest_versions/")
def api_minetest_versions_page():
return jsonify([{ "name": rel.name, "protocol_version": rel.protocol }\
for rel in MinetestRelease.query.all() if rel.getActual() is not None])

View File

@@ -15,431 +15,4 @@
# 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.tasks.importtasks import importRepoScreenshot, makeVCSRelease
from app.utils import *
from celery import uuid
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
from sqlalchemy import or_, any_
from sqlalchemy.sql.expression import func
class QueryBuilder:
title = None
types = None
search = None
def __init__(self):
title = "Packages"
# Get request types
types = request.args.getlist("type")
types = [PackageType.get(tname) for tname in types]
types = [type for type in types if type is not None]
if len(types) > 0:
title = ", ".join([type.value + "s" for type in types])
self.title = title
self.types = types
self.search = request.args.get("q")
self.random = "random" in request.args
self.lucky = self.random or "lucky" in request.args
self.hide_nonfree = isNo(request.args.get("nonfree"))
self.limit = 1 if self.lucky else None
self.order_by = request.args.get("sort") or "score"
self.order_dir = request.args.get("order") or "desc"
def buildPackageQuery(self):
query = Package.query.filter_by(soft_deleted=False, approved=True)
if len(self.types) > 0:
query = query.filter(Package.type.in_(self.types))
if self.search is not None and self.search.strip() != "":
query = query.filter(Package.title.ilike('%' + self.search + '%'))
if self.random:
query = query.order_by(func.random())
else:
to_order = None
if self.order_by == "score":
to_order = Package.score
elif self.order_by == "created_at":
to_order = Package.created_at
else:
abort(400)
if self.order_dir == "asc":
to_order = db.asc(to_order)
elif self.order_dir == "desc":
to_order = db.desc(to_order)
else:
abort(400)
query = query.order_by(to_order)
if self.hide_nonfree:
query = query.filter(Package.license.has(License.is_foss == True))
query = query.filter(Package.media_license.has(License.is_foss == True))
if self.limit:
query = query.limit(self.limit)
return query
def buildTopicQuery(self):
topics = ForumTopic.query \
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
.filter(ForumTopic.title.ilike('%' + self.search + '%'))
if len(self.types) > 0:
topics = topics.filter(ForumTopic.type.in_(self.types))
if self.hide_nonfree:
query = query.filter(Package.license.has(License.is_foss == True))
query = query.filter(Package.media_license.has(License.is_foss == True))
if self.limit:
topics = topics.limit(self.limit)
return topics
@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():
if shouldReturnJson():
return redirect(url_for("api_packages_page"))
qb = QueryBuilder()
query = qb.buildPackageQuery()
title = qb.title
if qb.lucky:
package = query.first()
if package:
return redirect(package.getDetailsURL())
topic = qb.buildTopicQuery().first()
if qb.search and topic:
return redirect("https://forum.minetest.net/viewtopic.php?t=" + str(topic.topic_id))
page = int(request.args.get("page") or 1)
num = min(40, int(request.args.get("n") or 100))
query = query.paginate(page, num, True)
search = request.args.get("q")
type_name = request.args.get("type")
next_url = url_for("packages_page", type=type_name, q=search, page=query.next_num) \
if query.has_next else None
prev_url = url_for("packages_page", type=type_name, q=search, page=query.prev_num) \
if query.has_prev else None
topics = None
if qb.search and not query.has_next:
topics = qb.buildTopicQuery().all()
tags = Tag.query.all()
return render_template("packages/list.html", \
title=title, packages=query.items, topics=topics, \
query=search, tags=tags, type=type_name, \
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, packages_count=query.total)
def getReleases(package):
if package.checkPerm(current_user, Permission.MAKE_RELEASE):
return package.releases
else:
return [rel for rel in package.releases if rel.approved]
@app.route("/packages/<author>/<name>/")
@is_package_page
def package_page(package):
clearNotifications(package.getDetailsURL())
alternatives = None
if package.type == PackageType.MOD:
alternatives = Package.query \
.filter_by(name=package.name, type=PackageType.MOD, soft_deleted=False) \
.filter(Package.id != package.id) \
.order_by(db.desc(Package.score)) \
.all()
show_similar_topics = current_user == package.author or \
package.checkPerm(current_user, Permission.APPROVE_NEW)
similar_topics = None if not show_similar_topics else \
ForumTopic.query \
.filter_by(name=package.name) \
.filter(ForumTopic.topic_id != package.forums) \
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
.all()
releases = getReleases(package)
requests = [r for r in package.requests if r.status == 0]
review_thread = package.review_thread
if review_thread is not None and not review_thread.checkPerm(current_user, Permission.SEE_THREAD):
review_thread = None
topic_error = None
topic_error_lvl = "warning"
if not package.approved and package.forums is not None:
errors = []
if Package.query.filter_by(forums=package.forums, soft_deleted=False).count() > 1:
errors.append("<b>Error: Another package already uses this forum topic!</b>")
topic_error_lvl = "danger"
topic = ForumTopic.query.get(package.forums)
if topic is not None:
if topic.author != package.author:
errors.append("<b>Error: Forum topic author doesn't match package author.</b>")
topic_error_lvl = "danger"
if topic.wip:
errors.append("Warning: Forum topic is in WIP section, make sure package meets playability standards.")
elif package.type != PackageType.TXP:
errors.append("Warning: Forum topic not found. This may happen if the topic has only just been created.")
topic_error = "<br />".join(errors)
threads = Thread.query.filter_by(package_id=package.id)
if not current_user.is_authenticated:
threads = threads.filter_by(private=False)
elif not current_user.rank.atLeast(UserRank.EDITOR) and not current_user == package.author:
threads = threads.filter(or_(Thread.private == False, Thread.author == current_user))
return render_template("packages/view.html", \
package=package, releases=releases, requests=requests, \
alternatives=alternatives, similar_topics=similar_topics, \
review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl, \
threads=threads.all())
@app.route("/packages/<author>/<name>/download/")
@is_package_page
def package_download_page(package):
release = package.getDownloadRelease()
if release is None:
if "application/zip" in request.accept_mimetypes and \
not "text/html" in request.accept_mimetypes:
return "", 204
else:
flash("No download available.", "error")
return redirect(package.getDetailsURL())
else:
return redirect(release.url, code=302)
class PackageForm(FlaskForm):
name = StringField("Name (Technical)", [InputRequired(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 50)])
shortDesc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
license = QuerySelectField("License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
media_license = QuerySelectField("Media License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
provides_str = StringField("Provides (mods included in package)", [Optional(), Length(0,1000)])
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()])
forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
submit = SubmitField("Save")
@app.route("/packages/new/", methods=["GET", "POST"])
@app.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
@login_required
def create_edit_package_page(author=None, name=None):
package = None
form = None
if author is None:
form = PackageForm(formdata=request.form)
author = request.args.get("author")
if author is None or author == current_user.username:
author = current_user
else:
author = User.query.filter_by(username=author).first()
if author is None:
flash("Unable to find that user", "error")
return redirect(url_for("create_edit_package_page"))
if not author.checkPerm(current_user, Permission.CHANGE_AUTHOR):
flash("Permission denied", "error")
return redirect(url_for("create_edit_package_page"))
else:
package = getPackageByInfo(author, name)
if not package.checkPerm(current_user, Permission.EDIT_PACKAGE):
return redirect(package.getDetailsURL())
author = package.author
form = PackageForm(formdata=request.form, obj=package)
# Initial form class from post data and default data
if request.method == "GET":
if package is None:
form.name.data = request.args.get("bname")
form.title.data = request.args.get("title")
form.repo.data = request.args.get("repo")
form.forums.data = request.args.get("forums")
else:
deps = package.dependencies
form.harddep_str.data = ",".join([str(x) for x in deps if not x.optional])
form.softdep_str.data = ",".join([str(x) for x in deps if x.optional])
form.provides_str.data = MetaPackage.ListToSpec(package.provides)
if request.method == "POST" and form.validate():
wasNew = False
if not package:
package = Package.query.filter_by(name=form["name"].data, author_id=author.id).first()
if package is not None:
if package.soft_deleted:
Package.query.filter_by(name=form["name"].data, author_id=author.id).delete()
else:
flash("Package already exists!", "error")
return redirect(url_for("create_edit_package_page"))
package = Package()
package.author = author
wasNew = True
else:
triggerNotif(package.author, current_user,
"{} edited".format(package.title), package.getDetailsURL())
form.populate_obj(package) # copy to row
mpackage_cache = {}
package.provides.clear()
mpackages = MetaPackage.SpecToList(form.provides_str.data, mpackage_cache)
for m in mpackages:
package.provides.append(m)
Dependency.query.filter_by(depender=package).delete()
deps = Dependency.SpecToList(package, form.harddep_str.data, mpackage_cache)
for dep in deps:
dep.optional = False
db.session.add(dep)
deps = Dependency.SpecToList(package, form.softdep_str.data, mpackage_cache)
for dep in deps:
dep.optional = True
db.session.add(dep)
if wasNew and package.type == PackageType.MOD and not package.name in mpackage_cache:
m = MetaPackage.GetOrCreate(package.name, mpackage_cache)
package.provides.append(m)
package.tags.clear()
for tag in form.tags.raw_data:
package.tags.append(Tag.query.get(tag))
db.session.commit() # save
next_url = package.getDetailsURL()
if wasNew and package.repo is not None:
task = importRepoScreenshot.delay(package.id)
next_url = url_for("check_task", id=task.id, r=next_url)
if wasNew and ("WTFPL" in package.license.name or "WTFPL" in package.media_license.name):
next_url = url_for("flatpage", path="help/wtfpl", r=next_url)
return redirect(next_url)
package_query = Package.query.filter_by(approved=True, soft_deleted=False)
if package is not None:
package_query = package_query.filter(Package.id != package.id)
enableWizard = name is None and request.method != "POST"
return render_template("packages/create_edit.html", package=package, \
form=form, author=author, enable_wizard=enableWizard, \
packages=package_query.all(), \
mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all())
@app.route("/packages/<author>/<name>/approve/", methods=["POST"])
@login_required
@is_package_page
def approve_package_page(package):
if not package.checkPerm(current_user, Permission.APPROVE_NEW):
flash("You don't have permission to do that.", "error")
elif package.approved:
flash("Package has already been approved", "error")
else:
package.approved = True
screenshots = PackageScreenshot.query.filter_by(package=package, approved=False).all()
for s in screenshots:
s.approved = True
triggerNotif(package.author, current_user,
"{} approved".format(package.title), package.getDetailsURL())
db.session.commit()
return redirect(package.getDetailsURL())
@app.route("/packages/<author>/<name>/remove/", methods=["GET", "POST"])
@login_required
@is_package_page
def remove_package_page(package):
if request.method == "GET":
return render_template("packages/remove.html", package=package)
if "delete" in request.form:
if not package.checkPerm(current_user, Permission.DELETE_PACKAGE):
flash("You don't have permission to do that.", "error")
return redirect(package.getDetailsURL())
package.soft_deleted = True
url = url_for("user_profile_page", username=package.author.username)
triggerNotif(package.author, current_user,
"{} deleted".format(package.title), url)
db.session.commit()
flash("Deleted package", "success")
return redirect(url)
elif "unapprove" in request.form:
if not package.checkPerm(current_user, Permission.UNAPPROVE_PACKAGE):
flash("You don't have permission to do that.", "error")
return redirect(package.getDetailsURL())
package.approved = False
triggerNotif(package.author, current_user,
"{} deleted".format(package.title), package.getDetailsURL())
db.session.commit()
flash("Unapproved package", "success")
return redirect(package.getDetailsURL())
else:
abort(400)
from . import todo, screenshots, releases
from . import packages, screenshots, releases

View File

@@ -0,0 +1,360 @@
# Content DB
# Copyright (C) 2018 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import render_template, abort, request, redirect, url_for, flash
from flask_user import current_user
import flask_menu as menu
from app import app
from app.models import *
from app.querybuilder import QueryBuilder
from app.tasks.importtasks import importRepoScreenshot
from app.utils import *
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
from sqlalchemy import or_
@menu.register_menu(app, ".mods", "Mods", order=11, endpoint_arguments_constructor=lambda: { 'type': 'mod' })
@menu.register_menu(app, ".games", "Games", order=12, endpoint_arguments_constructor=lambda: { 'type': 'game' })
@menu.register_menu(app, ".txp", "Texture Packs", order=13, endpoint_arguments_constructor=lambda: { 'type': 'txp' })
@menu.register_menu(app, ".random", "Random", order=14, endpoint_arguments_constructor=lambda: { 'random': '1' })
@app.route("/packages/")
def packages_page():
qb = QueryBuilder(request.args)
query = qb.buildPackageQuery()
title = qb.title
if qb.lucky:
package = query.first()
if package:
return redirect(package.getDetailsURL())
topic = qb.buildTopicQuery().first()
if qb.search and topic:
return redirect("https://forum.minetest.net/viewtopic.php?t=" + str(topic.topic_id))
page = int(request.args.get("page") or 1)
num = min(40, int(request.args.get("n") or 100))
query = query.paginate(page, num, True)
search = request.args.get("q")
type_name = request.args.get("type")
next_url = url_for("packages_page", type=type_name, q=search, page=query.next_num) \
if query.has_next else None
prev_url = url_for("packages_page", type=type_name, q=search, page=query.prev_num) \
if query.has_prev else None
topics = None
if qb.search and not query.has_next:
topics = qb.buildTopicQuery().all()
tags = Tag.query.all()
return render_template("packages/list.html", \
title=title, packages=query.items, topics=topics, \
query=search, tags=tags, type=type_name, \
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, packages_count=query.total)
def getReleases(package):
if package.checkPerm(current_user, Permission.MAKE_RELEASE):
return package.releases
else:
return [rel for rel in package.releases if rel.approved]
@app.route("/packages/<author>/<name>/")
@is_package_page
def package_page(package):
clearNotifications(package.getDetailsURL())
alternatives = None
if package.type == PackageType.MOD:
alternatives = Package.query \
.filter_by(name=package.name, type=PackageType.MOD, soft_deleted=False) \
.filter(Package.id != package.id) \
.order_by(db.desc(Package.score)) \
.all()
show_similar_topics = current_user == package.author or \
package.checkPerm(current_user, Permission.APPROVE_NEW)
similar_topics = None if not show_similar_topics else \
ForumTopic.query \
.filter_by(name=package.name) \
.filter(ForumTopic.topic_id != package.forums) \
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
.all()
releases = getReleases(package)
requests = [r for r in package.requests if r.status == 0]
review_thread = package.review_thread
if review_thread is not None and not review_thread.checkPerm(current_user, Permission.SEE_THREAD):
review_thread = None
topic_error = None
topic_error_lvl = "warning"
if not package.approved and package.forums is not None:
errors = []
if Package.query.filter_by(forums=package.forums, soft_deleted=False).count() > 1:
errors.append("<b>Error: Another package already uses this forum topic!</b>")
topic_error_lvl = "danger"
topic = ForumTopic.query.get(package.forums)
if topic is not None:
if topic.author != package.author:
errors.append("<b>Error: Forum topic author doesn't match package author.</b>")
topic_error_lvl = "danger"
if topic.wip:
errors.append("Warning: Forum topic is in WIP section, make sure package meets playability standards.")
elif package.type != PackageType.TXP:
errors.append("Warning: Forum topic not found. This may happen if the topic has only just been created.")
topic_error = "<br />".join(errors)
threads = Thread.query.filter_by(package_id=package.id)
if not current_user.is_authenticated:
threads = threads.filter_by(private=False)
elif not current_user.rank.atLeast(UserRank.EDITOR) and not current_user == package.author:
threads = threads.filter(or_(Thread.private == False, Thread.author == current_user))
return render_template("packages/view.html", \
package=package, releases=releases, requests=requests, \
alternatives=alternatives, similar_topics=similar_topics, \
review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl, \
threads=threads.all())
@app.route("/packages/<author>/<name>/download/")
@is_package_page
def package_download_page(package):
release = package.getDownloadRelease()
if release is None:
if "application/zip" in request.accept_mimetypes and \
not "text/html" in request.accept_mimetypes:
return "", 204
else:
flash("No download available.", "error")
return redirect(package.getDetailsURL())
else:
PackageRelease.query.filter_by(id=release.id).update({
"downloads": PackageRelease.downloads + 1
})
db.session.commit()
return redirect(release.url, code=302)
class PackageForm(FlaskForm):
name = StringField("Name (Technical)", [InputRequired(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 50)])
short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
license = QuerySelectField("License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
media_license = QuerySelectField("Media License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
provides_str = StringField("Provides (mods included in package)", [Optional()])
tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title)
harddep_str = StringField("Hard Dependencies", [Optional()])
softdep_str = StringField("Soft Dependencies", [Optional()])
repo = StringField("VCS Repository URL", [Optional(), URL()])
website = StringField("Website URL", [Optional(), URL()])
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()])
forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
submit = SubmitField("Save")
@app.route("/packages/new/", methods=["GET", "POST"])
@app.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
@login_required
def create_edit_package_page(author=None, name=None):
package = None
form = None
if author is None:
form = PackageForm(formdata=request.form)
author = request.args.get("author")
if author is None or author == current_user.username:
author = current_user
else:
author = User.query.filter_by(username=author).first()
if author is None:
flash("Unable to find that user", "error")
return redirect(url_for("create_edit_package_page"))
if not author.checkPerm(current_user, Permission.CHANGE_AUTHOR):
flash("Permission denied", "error")
return redirect(url_for("create_edit_package_page"))
else:
package = getPackageByInfo(author, name)
if not package.checkPerm(current_user, Permission.EDIT_PACKAGE):
return redirect(package.getDetailsURL())
author = package.author
form = PackageForm(formdata=request.form, obj=package)
# Initial form class from post data and default data
if request.method == "GET":
if package is None:
form.name.data = request.args.get("bname")
form.title.data = request.args.get("title")
form.repo.data = request.args.get("repo")
form.forums.data = request.args.get("forums")
else:
deps = package.dependencies
form.harddep_str.data = ",".join([str(x) for x in deps if not x.optional])
form.softdep_str.data = ",".join([str(x) for x in deps if x.optional])
form.provides_str.data = MetaPackage.ListToSpec(package.provides)
if request.method == "POST" and form.validate():
wasNew = False
if not package:
package = Package.query.filter_by(name=form["name"].data, author_id=author.id).first()
if package is not None:
if package.soft_deleted:
Package.query.filter_by(name=form["name"].data, author_id=author.id).delete()
else:
flash("Package already exists!", "error")
return redirect(url_for("create_edit_package_page"))
package = Package()
package.author = author
wasNew = True
else:
triggerNotif(package.author, current_user,
"{} edited".format(package.title), package.getDetailsURL())
form.populate_obj(package) # copy to row
mpackage_cache = {}
package.provides.clear()
mpackages = MetaPackage.SpecToList(form.provides_str.data, mpackage_cache)
for m in mpackages:
package.provides.append(m)
Dependency.query.filter_by(depender=package).delete()
deps = Dependency.SpecToList(package, form.harddep_str.data, mpackage_cache)
for dep in deps:
dep.optional = False
db.session.add(dep)
deps = Dependency.SpecToList(package, form.softdep_str.data, mpackage_cache)
for dep in deps:
dep.optional = True
db.session.add(dep)
if wasNew and package.type == PackageType.MOD and not package.name in mpackage_cache:
m = MetaPackage.GetOrCreate(package.name, mpackage_cache)
package.provides.append(m)
package.tags.clear()
for tag in form.tags.raw_data:
package.tags.append(Tag.query.get(tag))
db.session.commit() # save
next_url = package.getDetailsURL()
if wasNew and package.repo is not None:
task = importRepoScreenshot.delay(package.id)
next_url = url_for("check_task", id=task.id, r=next_url)
if wasNew and ("WTFPL" in package.license.name or "WTFPL" in package.media_license.name):
next_url = url_for("flatpage", path="help/wtfpl", r=next_url)
return redirect(next_url)
package_query = Package.query.filter_by(approved=True, soft_deleted=False)
if package is not None:
package_query = package_query.filter(Package.id != package.id)
enableWizard = name is None and request.method != "POST"
return render_template("packages/create_edit.html", package=package, \
form=form, author=author, enable_wizard=enableWizard, \
packages=package_query.all(), \
mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all())
@app.route("/packages/<author>/<name>/approve/", methods=["POST"])
@login_required
@is_package_page
def approve_package_page(package):
if not package.checkPerm(current_user, Permission.APPROVE_NEW):
flash("You don't have permission to do that.", "error")
elif package.approved:
flash("Package has already been approved", "error")
else:
package.approved = True
screenshots = PackageScreenshot.query.filter_by(package=package, approved=False).all()
for s in screenshots:
s.approved = True
triggerNotif(package.author, current_user,
"{} approved".format(package.title), package.getDetailsURL())
db.session.commit()
return redirect(package.getDetailsURL())
@app.route("/packages/<author>/<name>/remove/", methods=["GET", "POST"])
@login_required
@is_package_page
def remove_package_page(package):
if request.method == "GET":
return render_template("packages/remove.html", package=package)
if "delete" in request.form:
if not package.checkPerm(current_user, Permission.DELETE_PACKAGE):
flash("You don't have permission to do that.", "error")
return redirect(package.getDetailsURL())
package.soft_deleted = True
url = url_for("user_profile_page", username=package.author.username)
triggerNotif(package.author, current_user,
"{} deleted".format(package.title), url)
db.session.commit()
flash("Deleted package", "success")
return redirect(url)
elif "unapprove" in request.form:
if not package.checkPerm(current_user, Permission.UNAPPROVE_PACKAGE):
flash("You don't have permission to do that.", "error")
return redirect(package.getDetailsURL())
package.approved = False
triggerNotif(package.author, current_user,
"{} deleted".format(package.title), package.getDetailsURL())
db.session.commit()
flash("Unapproved package", "success")
return redirect(package.getDetailsURL())
else:
abort(400)

View File

@@ -27,12 +27,28 @@ from celery import uuid
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from wtforms.ext.sqlalchemy.fields import QuerySelectField
def get_mt_releases(is_max):
query = MinetestRelease.query.order_by(db.asc(MinetestRelease.id))
if is_max:
query = query.limit(query.count() - 1)
else:
query = query.filter(MinetestRelease.name != "0.4.17")
return query
class CreatePackageReleaseForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(1, 30)])
uploadOpt = RadioField ("Method", choices=[("upload", "File Upload")], default="upload")
vcsLabel = StringField("VCS Commit or Branch", default="master")
vcsLabel = StringField("VCS Commit Hash, Branch, or Tag", default="master")
fileUpload = FileField("File Upload")
min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
max_rel = QuerySelectField("Maximum Minetest Version", [InputRequired()],
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
submit = SubmitField("Save")
class EditPackageReleaseForm(FlaskForm):
@@ -40,6 +56,10 @@ class EditPackageReleaseForm(FlaskForm):
url = StringField("URL", [URL])
task_id = StringField("Task ID")
approved = BooleanField("Is Approved")
min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
max_rel = QuerySelectField("Maximum Minetest Version", [InputRequired()],
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
submit = SubmitField("Save")
@app.route("/packages/<author>/<name>/releases/new/", methods=["GET", "POST"])
@@ -63,6 +83,9 @@ def create_release_page(package):
rel.title = form["title"].data
rel.url = ""
rel.task_id = uuid()
rel.min_rel = form["min_rel"].data.getActual()
rel.max_rel = form["max_rel"].data.getActual()
rel.approved = package.checkPerm(current_user, Permission.APPROVE_RELEASE)
db.session.add(rel)
db.session.commit()
@@ -80,6 +103,9 @@ def create_release_page(package):
rel.package = package
rel.title = form["title"].data
rel.url = uploadedPath
rel.min_rel = form["min_rel"].data.getActual()
rel.max_rel = form["max_rel"].data.getActual()
rel.approved = package.checkPerm(current_user, Permission.APPROVE_RELEASE)
db.session.add(rel)
db.session.commit()
@@ -105,6 +131,11 @@ def download_release_page(package, id):
flash("No download available.", "error")
return redirect(package.getDetailsURL())
else:
PackageRelease.query.filter_by(id=release.id).update({
"downloads": PackageRelease.downloads + 1
})
db.session.commit()
return redirect(release.url, code=300)
@app.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
@@ -128,6 +159,8 @@ def edit_release_page(package, id):
wasApproved = release.approved
if canEdit:
release.title = form["title"].data
release.min_rel = form["min_rel"].data.getActual()
release.max_rel = form["max_rel"].data.getActual()
if package.checkPerm(current_user, Permission.CHANGE_RELEASE_URL):
release.url = form["url"].data
@@ -144,3 +177,43 @@ def edit_release_page(package, id):
return redirect(package.getDetailsURL())
return render_template("packages/release_edit.html", package=package, release=release, form=form)
class BulkReleaseForm(FlaskForm):
set_min = BooleanField("Set Min")
min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
set_max = BooleanField("Set Max")
max_rel = QuerySelectField("Maximum Minetest Version", [InputRequired()],
query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name)
only_change_none = BooleanField("Only change values previously set as none")
submit = SubmitField("Update")
@app.route("/packages/<author>/<name>/releases/bulk_change/", methods=["GET", "POST"])
@login_required
@is_package_page
def bulk_change_release_page(package):
if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
return redirect(package.getDetailsURL())
# Initial form class from post data and default data
form = BulkReleaseForm()
if request.method == "GET":
form.only_change_none.data = True
elif request.method == "POST" and form.validate():
only_change_none = form.only_change_none.data
for release in package.releases.all():
if form["set_min"].data and (not only_change_none or release.min_rel is None):
release.min_rel = form["min_rel"].data.getActual()
if form["set_max"].data and (not only_change_none or release.max_rel is None):
release.max_rel = form["max_rel"].data.getActual()
db.session.commit()
return redirect(package.getDetailsURL())
return render_template("packages/release_bulk_change.html", package=package, form=form)

View File

@@ -53,9 +53,10 @@ def create_screenshot_page(package, id=None):
"a PNG or JPG image file")
if uploadedPath is not None:
ss = PackageScreenshot()
ss.package = package
ss.title = form["title"].data or "Untitled"
ss.url = uploadedPath
ss.package = package
ss.title = form["title"].data or "Untitled"
ss.url = uploadedPath
ss.approved = package.checkPerm(current_user, Permission.APPROVE_SCREENSHOT)
db.session.add(ss)
msg = "{}: Screenshot added {}" \

View File

@@ -21,6 +21,8 @@ from app import app
from app.models import *
from app.utils import triggerNotif, clearNotifications
import datetime
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
@@ -78,6 +80,13 @@ def thread_page(id):
if current_user.is_authenticated and request.method == "POST":
comment = request.form["comment"]
if not current_user.canCommentRL():
flash("Please wait before commenting again", "danger")
if package:
return redirect(package.getDetailsURL())
else:
return redirect(url_for("home_page"))
if len(comment) <= 500 and len(comment) > 3:
reply = ThreadReply()
reply.author = current_user
@@ -126,15 +135,15 @@ def new_thread_page():
if package is None:
flash("Unable to find that package!", "error")
# Don't allow making threads on approved packages for now
# Don't allow making orphan threads on approved packages for now
if package is None:
abort(403)
def_is_private = request.args.get("private") or False
if not package.approved:
if package is None or not package.approved:
def_is_private = True
allow_change = package.approved
is_review_thread = package is not None and not package.approved
allow_change = package and package.approved
is_review_thread = package and not package.approved
# Check that user can make the thread
if not package.checkPerm(current_user, Permission.CREATE_THREAD):
@@ -144,8 +153,15 @@ def new_thread_page():
# Only allow creating one thread when not approved
elif is_review_thread and package.review_thread is not None:
flash("A review thread already exists!", "error")
if request.method == "GET":
return redirect(url_for("thread_page", id=package.review_thread.id))
return redirect(url_for("thread_page", id=package.review_thread.id))
elif not current_user.canOpenThreadRL():
flash("Please wait before opening another thread", "danger")
if package:
return redirect(package.getDetailsURL())
else:
return redirect(url_for("home_page"))
# Set default values
elif request.method == "GET":
@@ -178,13 +194,19 @@ def new_thread_page():
if is_review_thread:
package.review_thread = thread
notif_msg = None
if package is not None:
triggerNotif(package.author, current_user,
"New thread '{}' on package {}".format(thread.title, package.title), url_for("thread_page", id=thread.id))
notif_msg = "New thread '{}' on package {}".format(thread.title, package.title)
triggerNotif(package.author, current_user, notif_msg, url_for("thread_page", id=thread.id))
else:
notif_msg = "New thread '{}'".format(thread.title)
for user in User.query.filter(User.rank >= UserRank.EDITOR).all():
triggerNotif(user, current_user, notif_msg, url_for("thread_page", id=thread.id))
db.session.commit()
return redirect(url_for("thread_page", id=thread.id))
return render_template("threads/new.html", form=form, allow_private_change=allow_change)
return render_template("threads/new.html", form=form, allow_private_change=allow_change, package=package)

View File

@@ -0,0 +1,18 @@
# Content DB
# Copyright (C) 2018 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from . import users, githublogin, notifications

View File

@@ -18,14 +18,14 @@
from flask import *
from flask_user import *
from flask_login import login_user, logout_user
from app import app
from app import app, markdown
from app.models import *
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from app.utils import randomString, loginUser
from app.utils import randomString, loginUser, rank_required
from app.tasks.forumtasks import checkForumAccount
from app.tasks.emails import sendVerifyEmail
from app.tasks.emails import sendVerifyEmail, sendEmailRaw
from app.tasks.phpbbparser import getProfile
# Define the User profile form
@@ -126,6 +126,36 @@ def user_check(username):
return redirect(url_for("check_task", id=task.id, r=next_url))
class SendEmailForm(FlaskForm):
subject = StringField("Subject", [InputRequired(), Length(1, 300)])
text = TextAreaField("Message", [InputRequired()])
submit = SubmitField("Send")
@app.route("/users/<username>/email/", methods=["GET", "POST"])
@rank_required(UserRank.MODERATOR)
def send_email_page(username):
user = User.query.filter_by(username=username).first()
if user is None:
abort(404)
next_url = url_for("user_profile_page", username=user.username)
if user.email is None:
flash("User has no email address!", "error")
return redirect(next_url)
form = SendEmailForm(request.form)
if form.validate_on_submit():
text = form.text.data
html = markdown(text)
task = sendEmailRaw.delay([user.email], form.subject.data, text, html)
return redirect(url_for("check_task", id=task.id, r=next_url))
return render_template("users/send_email.html", form=form)
class SetPasswordForm(FlaskForm):
email = StringField("Email", [Optional(), Email()])
password = PasswordField("New password", [InputRequired(), Length(2, 20)])

37
docker-compose.yml Normal file
View File

@@ -0,0 +1,37 @@
version: '3'
services:
db:
image: "postgres:9.6.5"
volumes:
- "./data/db:/var/lib/postgresql/data"
env_file:
- config.env
redis:
image: 'redis:3.0-alpine'
command: redis-server
volumes:
- './data/redis:/data'
app:
build: .
command: ./rundebug.sh
ports:
- 5123:5123
volumes:
- "./data/uploads:/home/cdb/app/public/uploads"
- "./app:/home/cdb/app"
- "./migrations:/home/cdb/migrations"
depends_on:
- db
- redis
worker:
build: .
command: celery -A app.tasks.celery worker
env_file:
- config.env
volumes:
- "./data/uploads:/home/cdb/app/public/uploads"
depends_on:
- redis

View File

@@ -0,0 +1,36 @@
"""empty message
Revision ID: 2f3c3597c78d
Revises: 9ec17b558413
Create Date: 2019-01-29 02:43:08.865695
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from sqlalchemy_utils.types import TSVectorType
from sqlalchemy_searchable import sync_trigger
# revision identifiers, used by Alembic.
revision = '2f3c3597c78d'
down_revision = '9ec17b558413'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('package', 'shortDesc', nullable=False, new_column_name='short_desc')
op.add_column('package', sa.Column('search_vector', TSVectorType("title", "short_desc", "desc"), nullable=True))
op.create_index('ix_package_search_vector', 'package', ['search_vector'], unique=False, postgresql_using='gin')
conn = op.get_bind()
sync_trigger(conn, 'package', 'search_vector', ["title", "short_desc", "desc"])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_package_search_vector', table_name='package')
op.drop_column('package', 'search_vector')
# ### end Alembic commands ###

View File

@@ -0,0 +1,41 @@
"""empty message
Revision ID: 7def3e843d04
Revises: dce69ad1e4eb
Create Date: 2019-01-28 20:27:33.760232
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '7def3e843d04'
down_revision = 'dce69ad1e4eb'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('minetest_release',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.add_column('package_release', sa.Column('max_rel_id', sa.Integer(), nullable=True, server_default=None))
op.add_column('package_release', sa.Column('min_rel_id', sa.Integer(), nullable=True, server_default=None))
op.create_foreign_key(None, 'package_release', 'minetest_release', ['max_rel_id'], ['id'])
op.create_foreign_key(None, 'package_release', 'minetest_release', ['min_rel_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'package_release', type_='foreignkey')
op.drop_constraint(None, 'package_release', type_='foreignkey')
op.drop_column('package_release', 'min_rel_id')
op.drop_column('package_release', 'max_rel_id')
op.drop_table('minetest_release')
# ### end Alembic commands ###

View File

@@ -0,0 +1,249 @@
"""empty message
Revision ID: 7ff57806ffd5
Revises: 2f3c3597c78d
Create Date: 2019-01-29 02:57:50.279918
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '7ff57806ffd5'
down_revision = '2f3c3597c78d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.execute("""
DROP TYPE IF EXISTS tsq_state CASCADE;
CREATE TYPE tsq_state AS (
search_query text,
parentheses_stack int,
skip_for int,
current_token text,
current_index int,
current_char text,
previous_char text,
tokens text[]
);
CREATE OR REPLACE FUNCTION tsq_append_current_token(state tsq_state)
RETURNS tsq_state AS $$
BEGIN
IF state.current_token != '' THEN
state.tokens := array_append(state.tokens, state.current_token);
state.current_token := '';
END IF;
RETURN state;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE OR REPLACE FUNCTION tsq_tokenize_character(state tsq_state)
RETURNS tsq_state AS $$
BEGIN
IF state.current_char = '(' THEN
state.tokens := array_append(state.tokens, '(');
state.parentheses_stack := state.parentheses_stack + 1;
state := tsq_append_current_token(state);
ELSIF state.current_char = ')' THEN
IF (state.parentheses_stack > 0 AND state.current_token != '') THEN
state := tsq_append_current_token(state);
state.tokens := array_append(state.tokens, ')');
state.parentheses_stack := state.parentheses_stack - 1;
END IF;
ELSIF state.current_char = '"' THEN
state.skip_for := position('"' IN substring(
state.search_query FROM state.current_index + 1
));
IF state.skip_for > 1 THEN
state.tokens = array_append(
state.tokens,
substring(
state.search_query
FROM state.current_index FOR state.skip_for + 1
)
);
ELSIF state.skip_for = 0 THEN
state.current_token := state.current_token || state.current_char;
END IF;
ELSIF (
state.current_char = '-' AND
(state.current_index = 1 OR state.previous_char = ' ')
) THEN
state.tokens := array_append(state.tokens, '-');
ELSIF state.current_char = ' ' THEN
state := tsq_append_current_token(state);
IF substring(
state.search_query FROM state.current_index FOR 4
) = ' or ' THEN
state.skip_for := 2;
-- remove duplicate OR tokens
IF state.tokens[array_length(state.tokens, 1)] != ' | ' THEN
state.tokens := array_append(state.tokens, ' | ');
END IF;
END IF;
ELSE
state.current_token = state.current_token || state.current_char;
END IF;
RETURN state;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE OR REPLACE FUNCTION tsq_tokenize(search_query text) RETURNS text[] AS $$
DECLARE
state tsq_state;
BEGIN
SELECT
search_query::text AS search_query,
0::int AS parentheses_stack,
0 AS skip_for,
''::text AS current_token,
0 AS current_index,
''::text AS current_char,
''::text AS previous_char,
'{}'::text[] AS tokens
INTO state;
state.search_query := lower(trim(
regexp_replace(search_query, '""+', '""', 'g')
));
FOR state.current_index IN (
SELECT generate_series(1, length(state.search_query))
) LOOP
state.current_char := substring(
search_query FROM state.current_index FOR 1
);
IF state.skip_for > 0 THEN
state.skip_for := state.skip_for - 1;
CONTINUE;
END IF;
state := tsq_tokenize_character(state);
state.previous_char := state.current_char;
END LOOP;
state := tsq_append_current_token(state);
state.tokens := array_nremove(state.tokens, '(', -state.parentheses_stack);
RETURN state.tokens;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- Processes an array of text search tokens and returns a tsquery
CREATE OR REPLACE FUNCTION tsq_process_tokens(config regconfig, tokens text[])
RETURNS tsquery AS $$
DECLARE
result_query text;
previous_value text;
value text;
BEGIN
result_query := '';
FOREACH value IN ARRAY tokens LOOP
IF value = '"' THEN
CONTINUE;
END IF;
IF left(value, 1) = '"' AND right(value, 1) = '"' THEN
value := phraseto_tsquery(config, value);
ELSIF value NOT IN ('(', ' | ', ')', '-') THEN
value := quote_literal(value) || ':*';
END IF;
IF previous_value = '-' THEN
IF value = '(' THEN
value := '!' || value;
ELSE
value := '!(' || value || ')';
END IF;
END IF;
SELECT
CASE
WHEN result_query = '' THEN value
WHEN (
previous_value IN ('!(', '(', ' | ') OR
value IN (')', ' | ')
) THEN result_query || value
ELSE result_query || ' & ' || value
END
INTO result_query;
previous_value := value;
END LOOP;
RETURN to_tsquery(config, result_query);
END;
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE OR REPLACE FUNCTION tsq_process_tokens(tokens text[])
RETURNS tsquery AS $$
SELECT tsq_process_tokens(get_current_ts_config(), tokens);
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION tsq_parse(config regconfig, search_query text)
RETURNS tsquery AS $$
SELECT tsq_process_tokens(config, tsq_tokenize(search_query));
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION tsq_parse(config text, search_query text)
RETURNS tsquery AS $$
SELECT tsq_parse(config::regconfig, search_query);
$$ LANGUAGE SQL IMMUTABLE;
CREATE OR REPLACE FUNCTION tsq_parse(search_query text) RETURNS tsquery AS $$
SELECT tsq_parse(get_current_ts_config(), search_query);
$$ LANGUAGE SQL IMMUTABLE;
-- remove first N elements equal to the given value from the array (array
-- must be one-dimensional)
--
-- If negative value is given as the third argument the removal of elements
-- starts from the last array element.
CREATE OR REPLACE FUNCTION array_nremove(anyarray, anyelement, int)
RETURNS ANYARRAY AS $$
WITH replaced_positions AS (
SELECT UNNEST(
CASE
WHEN $2 IS NULL THEN
'{}'::int[]
WHEN $3 > 0 THEN
(array_positions($1, $2))[1:$3]
WHEN $3 < 0 THEN
(array_positions($1, $2))[
(cardinality(array_positions($1, $2)) + $3 + 1):
]
ELSE
'{}'::int[]
END
) AS position
)
SELECT COALESCE((
SELECT array_agg(value)
FROM unnest($1) WITH ORDINALITY AS t(value, index)
WHERE index NOT IN (SELECT position FROM replaced_positions)
), $1[1:0]);
$$ LANGUAGE SQL IMMUTABLE;
""")
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@@ -0,0 +1,28 @@
"""empty message
Revision ID: 97a9c461bc2d
Revises: 7def3e843d04
Create Date: 2019-01-28 20:49:41.831991
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '97a9c461bc2d'
down_revision = '7def3e843d04'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('minetest_release', sa.Column('protocol', sa.Integer(), nullable=False, server_default="0"))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('minetest_release', 'protocol')
# ### end Alembic commands ###

View File

@@ -0,0 +1,28 @@
"""empty message
Revision ID: 9ec17b558413
Revises: 97a9c461bc2d
Create Date: 2019-01-29 00:37:49.507631
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '9ec17b558413'
down_revision = '97a9c461bc2d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('package_release', sa.Column('downloads', sa.Integer(), nullable=False, server_default="0"))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('package_release', 'downloads')
# ### end Alembic commands ###

View File

@@ -8,6 +8,7 @@ Flask-Migrate~=2.3
Flask-SQLAlchemy~=2.3
Flask-User~=0.6
GitHub-Flask~=3.2
SQLAlchemy-Searchable==1.0.3
beautifulsoup4~=4.6
celery~=4.2

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,9 @@ import os, sys, datetime
if not "FLASK_CONFIG" in os.environ:
os.environ["FLASK_CONFIG"] = "../config.cfg"
test_data = len(sys.argv) >= 2 and sys.argv[1].strip() == "-t"
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
from app.models import *
from app.utils import make_flask_user_password
@@ -53,7 +55,7 @@ def defineDummyData(licenses, tags, ruben):
mod.repo = "https://github.com/ezhh/other_worlds"
mod.issueTracker = "https://github.com/ezhh/other_worlds/issues"
mod.forums = 16015
mod.shortDesc = "The content library should not be used yet as it is still in alpha"
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)
@@ -75,7 +77,7 @@ def defineDummyData(licenses, tags, ruben):
mod1.repo = "https://github.com/rubenwardy/awards"
mod1.issueTracker = "https://github.com/rubenwardy/awards/issues"
mod1.forums = 4870
mod1.shortDesc = "Adds achievements and an API to register new ones."
mod1.short_desc = "Adds achievements and an API to register new ones."
mod1.desc = """
Majority of awards are back ported from Calinou's old fork in Carbone, under same license.
@@ -110,7 +112,7 @@ awards.register_achievement("award_mesefind",{
mod2.repo = "https://github.com/minetest-mods/mesecons/"
mod2.issueTracker = "https://github.com/minetest-mods/mesecons/issues"
mod2.forums = 628
mod2.shortDesc = "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.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 = """
########################################################################
## __ __ _____ _____ _____ _____ _____ _ _ _____ ##
@@ -208,7 +210,7 @@ No warranty is provided, express or implied, for any part of the project.
mod.repo = "https://github.com/ezhh/handholds"
mod.issueTracker = "https://github.com/ezhh/handholds/issues"
mod.forums = 17069
mod.shortDesc = "Adds hand holds and climbing thingies"
mod.short_desc = "Adds hand holds and climbing thingies"
mod.desc = "This is the long desc"
db.session.add(mod)
@@ -231,7 +233,7 @@ No warranty is provided, express or implied, for any part of the project.
mod.repo = "https://github.com/ezhh/other_worlds"
mod.issueTracker = "https://github.com/ezhh/other_worlds/issues"
mod.forums = 16015
mod.shortDesc = "Adds space with asteroids and comets"
mod.short_desc = "Adds space with asteroids and comets"
mod.desc = "This is the long desc"
db.session.add(mod)
@@ -246,7 +248,7 @@ No warranty is provided, express or implied, for any part of the project.
mod.repo = "https://github.com/rubenwardy/food/"
mod.issueTracker = "https://github.com/rubenwardy/food/issues/"
mod.forums = 2960
mod.shortDesc = "Adds lots of food and an API to manage ingredients"
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)
@@ -262,7 +264,7 @@ No warranty is provided, express or implied, for any part of the project.
mod.repo = "https://github.com/rubenwardy/food_sweet/"
mod.issueTracker = "https://github.com/rubenwardy/food_sweet/issues/"
mod.forums = 9039
mod.shortDesc = "Adds sweet food"
mod.short_desc = "Adds sweet food"
mod.desc = "This is the long desc"
food_sweet = mod
db.session.add(mod)
@@ -280,7 +282,7 @@ No warranty is provided, express or implied, for any part of the project.
game1.repo = "https://github.com/rubenwardy/capturetheflag"
game1.issueTracker = "https://github.com/rubenwardy/capturetheflag/issues"
game1.forums = 12835
game1.shortDesc = "Two teams battle to snatch and return the enemy's flag, before the enemy takes their own!"
game1.short_desc = "Two teams battle to snatch and return the enemy's flag, before the enemy takes their own!"
game1.desc = """
As seen on the Capture the Flag server (minetest.rubenwardy.com:30000)
@@ -305,7 +307,7 @@ Uses the CTF PvP Engine.
mod.type = PackageType.TXP
mod.author = ruben
mod.forums = 14132
mod.shortDesc = "This is an update of the original PixelBOX texture pack by the brillant artist Gambit"
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)
@@ -333,13 +335,14 @@ Uses the CTF PvP Engine.
db.session.add(dep)
delete_db = len(sys.argv) >= 2 and sys.argv[1].strip() == "-d"
if delete_db and os.path.isfile("db.sqlite"):
os.remove("db.sqlite")
print("Creating database tables...")
db.create_all()
if create_db:
print("Creating database tables...")
db.create_all()
print("Filling database...")
ruben = User("rubenwardy")