Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c2c8c21f1 | ||
|
|
e40b247a97 | ||
|
|
a79cc758ed | ||
|
|
bafd426eaf | ||
|
|
36f9572cbb | ||
|
|
2586a11bcf | ||
|
|
d36138d5e1 | ||
|
|
7810bb54e0 | ||
|
|
2844773e4d | ||
|
|
23c406bff9 | ||
|
|
0f3adda592 | ||
|
|
441ed3beeb | ||
|
|
d1f5585fda | ||
|
|
0fd3ed8f6b | ||
|
|
0e5c1f83ff | ||
|
|
f112756b04 | ||
|
|
f822027ec5 | ||
|
|
034315d421 | ||
|
|
5cd8b35d1f | ||
|
|
84b996c489 | ||
|
|
d77403c0be | ||
|
|
e9fe936aa9 | ||
|
|
8afe17b984 | ||
|
|
2691105513 | ||
|
|
5f7efd4f31 | ||
|
|
7d52931a20 | ||
|
|
a45df0e173 | ||
|
|
0db49efe4a | ||
|
|
9639cf04f1 | ||
|
|
9866e43b4b | ||
|
|
014370ea06 | ||
|
|
fbf374ff5d | ||
|
|
a68ac9cb4d |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
config.cfg
|
config.cfg
|
||||||
config.prod.cfg
|
*.env
|
||||||
*.sqlite
|
*.sqlite
|
||||||
custom.css
|
custom.css
|
||||||
tmp
|
tmp
|
||||||
@@ -8,6 +8,7 @@ log.txt
|
|||||||
uploads
|
uploads
|
||||||
thumbnails
|
thumbnails
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
|
/data
|
||||||
|
|
||||||
# Created by https://www.gitignore.io/api/linux,macos,python,windows
|
# Created by https://www.gitignore.io/api/linux,macos,python,windows
|
||||||
|
|
||||||
|
|||||||
17
Dockerfile
Normal file
17
Dockerfile
Normal 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
|
||||||
51
README.md
51
README.md
@@ -1,57 +1,17 @@
|
|||||||
# Content Database
|
# Content Database
|
||||||
|
|
||||||
## Setup
|
Content database for Minetest mods, games, and more.
|
||||||
|
|
||||||
First create a Python virtual env:
|
Developed by rubenwardy, license GPLv3.0+.
|
||||||
|
|
||||||
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/
|
|
||||||
|
|
||||||
## How-tos
|
## 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
|
```sh
|
||||||
|
# Run celery worker
|
||||||
FLASK_CONFIG=../config.cfg celery -A app.tasks.celery worker
|
FLASK_CONFIG=../config.cfg celery -A app.tasks.celery worker
|
||||||
```
|
|
||||||
|
|
||||||
### Create migration
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# if sqlite
|
# if sqlite
|
||||||
python setup.py -t
|
python setup.py -t
|
||||||
rm db.sqlite && python setup.py -t && FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db stamp head
|
rm db.sqlite && python setup.py -t && FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db stamp head
|
||||||
@@ -61,4 +21,7 @@ FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db migrate
|
|||||||
|
|
||||||
# Run migration
|
# Run migration
|
||||||
FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db upgrade
|
FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db upgrade
|
||||||
|
|
||||||
|
# Enter docker
|
||||||
|
docker exec -it contentdb_app_1 bash
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ app.config["FLATPAGES_EXTENSION"] = ".md"
|
|||||||
app.config.from_pyfile(os.environ["FLASK_CONFIG"])
|
app.config.from_pyfile(os.environ["FLASK_CONFIG"])
|
||||||
|
|
||||||
menu.Menu(app=app)
|
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)
|
github = GitHub(app)
|
||||||
csrf = CsrfProtect(app)
|
csrf = CsrfProtect(app)
|
||||||
mail = Mail(app)
|
mail = Mail(app)
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ title: Help
|
|||||||
|
|
||||||
* [Package Tags](package_tags)
|
* [Package Tags](package_tags)
|
||||||
* [Ranks and Permissions](ranks_permissions)
|
* [Ranks and Permissions](ranks_permissions)
|
||||||
|
* [Content Ratings and Flags](content_flags)
|
||||||
* [Reporting Content](reporting)
|
* [Reporting Content](reporting)
|
||||||
|
|||||||
26
app/flatpages/help/content_flags.md
Normal file
26
app/flatpages/help/content_flags.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
title: Content Flags
|
||||||
|
|
||||||
|
Content flags allow you to hide content based on your preferences.
|
||||||
|
The filtering is done server-side, which means that you don't need to update
|
||||||
|
your client to use new flags.
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
* `nonfree` - can be used to hide packages which do not qualify as
|
||||||
|
'free software', as defined by the Free Software Foundation.
|
||||||
|
* A content rating, given below.
|
||||||
|
|
||||||
|
|
||||||
|
## Ratings
|
||||||
|
|
||||||
|
Content ratings aren't currently supported by ContentDB.
|
||||||
|
Instead, mature content isn't allowed at all for now.
|
||||||
|
|
||||||
|
In the future, more mature content will be allowed but labelled with
|
||||||
|
content ratings which may contain the following:
|
||||||
|
|
||||||
|
* android_default - meta-rating which includes gore and drugs.
|
||||||
|
* desktop_default - meta-rating which won't include anything for now.
|
||||||
|
* gore - more than just blood
|
||||||
|
* drugs
|
||||||
|
* swearing
|
||||||
@@ -5,4 +5,4 @@ laws.
|
|||||||
|
|
||||||
We take copyright violation and other offenses very seriously.
|
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>
|
||||||
|
|||||||
131
app/models.py
131
app/models.py
@@ -15,19 +15,29 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from flask import Flask, url_for
|
import enum, datetime
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
from flask_migrate import Migrate
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
from app import app, gravatar
|
from app import app, gravatar
|
||||||
from datetime import datetime
|
from urllib.parse import urlparse
|
||||||
from sqlalchemy.orm import validates
|
|
||||||
|
from flask import Flask, url_for
|
||||||
|
from flask_sqlalchemy import SQLAlchemy, BaseQuery
|
||||||
|
from flask_migrate import Migrate
|
||||||
from flask_user import login_required, UserManager, UserMixin, SQLAlchemyAdapter
|
from 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
|
# Initialise database
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
migrate = Migrate(app, db)
|
migrate = Migrate(app, db)
|
||||||
|
make_searchable(db.metadata)
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleQuery(BaseQuery, SearchQueryMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UserRank(enum.Enum):
|
class UserRank(enum.Enum):
|
||||||
@@ -129,8 +139,6 @@ class User(db.Model, UserMixin):
|
|||||||
replies = db.relationship("ThreadReply", backref="author", lazy="dynamic")
|
replies = db.relationship("ThreadReply", backref="author", lazy="dynamic")
|
||||||
|
|
||||||
def __init__(self, username, active=False, email=None, password=None):
|
def __init__(self, username, active=False, email=None, password=None):
|
||||||
import datetime
|
|
||||||
|
|
||||||
self.username = username
|
self.username = username
|
||||||
self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
|
self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
|
||||||
self.display_name = username
|
self.display_name = username
|
||||||
@@ -172,6 +180,16 @@ class User(db.Model, UserMixin):
|
|||||||
else:
|
else:
|
||||||
raise Exception("Permission {} is not related to users".format(perm.name))
|
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):
|
class UserEmailVerification(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||||
@@ -239,7 +257,7 @@ class PackageType(enum.Enum):
|
|||||||
class PackagePropertyKey(enum.Enum):
|
class PackagePropertyKey(enum.Enum):
|
||||||
name = "Name"
|
name = "Name"
|
||||||
title = "Title"
|
title = "Title"
|
||||||
shortDesc = "Short Description"
|
short_desc = "Short Description"
|
||||||
desc = "Description"
|
desc = "Description"
|
||||||
type = "Type"
|
type = "Type"
|
||||||
license = "License"
|
license = "License"
|
||||||
@@ -336,18 +354,21 @@ class Dependency(db.Model):
|
|||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Package(db.Model):
|
class Package(db.Model):
|
||||||
|
query_class = ArticleQuery
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
# Basic details
|
# Basic details
|
||||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||||
name = db.Column(db.String(100), nullable=False)
|
name = db.Column(db.String(100), nullable=False)
|
||||||
title = db.Column(db.String(100), nullable=False)
|
title = db.Column(db.Unicode(100), nullable=False)
|
||||||
shortDesc = db.Column(db.String(200), nullable=False)
|
short_desc = db.Column(db.Unicode(200), nullable=False)
|
||||||
desc = db.Column(db.Text, nullable=True)
|
desc = db.Column(db.UnicodeText, nullable=True)
|
||||||
type = db.Column(db.Enum(PackageType))
|
type = db.Column(db.Enum(PackageType))
|
||||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
|
search_vector = db.Column(TSVectorType("title", "short_desc", "desc"))
|
||||||
|
|
||||||
license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
|
license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
|
||||||
license = db.relationship("License", foreign_keys=[license_id])
|
license = db.relationship("License", foreign_keys=[license_id])
|
||||||
@@ -380,7 +401,7 @@ class Package(db.Model):
|
|||||||
lazy="dynamic", order_by=db.desc("package_release_releaseDate"))
|
lazy="dynamic", order_by=db.desc("package_release_releaseDate"))
|
||||||
|
|
||||||
screenshots = db.relationship("PackageScreenshot", backref="package",
|
screenshots = db.relationship("PackageScreenshot", backref="package",
|
||||||
lazy="dynamic")
|
lazy="dynamic", order_by=db.asc("package_screenshot_id"))
|
||||||
|
|
||||||
requests = db.relationship("EditRequest", backref="package",
|
requests = db.relationship("EditRequest", backref="package",
|
||||||
lazy="dynamic")
|
lazy="dynamic")
|
||||||
@@ -396,26 +417,26 @@ class Package(db.Model):
|
|||||||
for e in PackagePropertyKey:
|
for e in PackagePropertyKey:
|
||||||
setattr(self, e.name, getattr(package, e.name))
|
setattr(self, e.name, getattr(package, e.name))
|
||||||
|
|
||||||
def getAsDictionaryShort(self, base_url):
|
def getAsDictionaryShort(self, base_url, protonum=None):
|
||||||
tnurl = self.getThumbnailURL(1)
|
tnurl = self.getThumbnailURL(1)
|
||||||
return {
|
return {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"author": self.author.display_name,
|
"author": self.author.display_name,
|
||||||
"short_description": self.shortDesc,
|
"short_description": self.short_desc,
|
||||||
"type": self.type.toName(),
|
"type": self.type.toName(),
|
||||||
"release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
|
"release": self.getDownloadRelease(protonum).id if self.getDownloadRelease(protonum) is not None else None,
|
||||||
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
|
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
|
||||||
"score": round(self.score * 10) / 10
|
"score": round(self.score * 10) / 10
|
||||||
}
|
}
|
||||||
|
|
||||||
def getAsDictionary(self, base_url):
|
def getAsDictionary(self, base_url, protonum=None):
|
||||||
tnurl = self.getThumbnailURL(1)
|
tnurl = self.getThumbnailURL(1)
|
||||||
return {
|
return {
|
||||||
"author": self.author.display_name,
|
"author": self.author.display_name,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"short_description": self.shortDesc,
|
"short_description": self.short_desc,
|
||||||
"desc": self.desc,
|
"desc": self.desc,
|
||||||
"type": self.type.toName(),
|
"type": self.type.toName(),
|
||||||
"created_at": self.created_at,
|
"created_at": self.created_at,
|
||||||
@@ -433,17 +454,17 @@ class Package(db.Model):
|
|||||||
"screenshots": [base_url + ss.url for ss in self.screenshots],
|
"screenshots": [base_url + ss.url for ss in self.screenshots],
|
||||||
|
|
||||||
"url": base_url + self.getDownloadURL(),
|
"url": base_url + self.getDownloadURL(),
|
||||||
"release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
|
"release": self.getDownloadRelease(protonum).id if self.getDownloadRelease(protonum) is not None else None,
|
||||||
|
|
||||||
"score": round(self.score * 10) / 10
|
"score": round(self.score * 10) / 10
|
||||||
}
|
}
|
||||||
|
|
||||||
def getThumbnailURL(self, level=2):
|
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
|
return screenshot.getThumbnailURL(level) if screenshot is not None else None
|
||||||
|
|
||||||
def getMainScreenshotURL(self):
|
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
|
return screenshot.url if screenshot is not None else None
|
||||||
|
|
||||||
def getDetailsURL(self):
|
def getDetailsURL(self):
|
||||||
@@ -474,17 +495,38 @@ class Package(db.Model):
|
|||||||
return url_for("create_edit_editrequest_page",
|
return url_for("create_edit_editrequest_page",
|
||||||
author=self.author.username, name=self.name)
|
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):
|
def getDownloadURL(self):
|
||||||
return url_for("package_download_page",
|
return url_for("package_download_page",
|
||||||
author=self.author.username, name=self.name)
|
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:
|
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 rel
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def getDownloadCount(self):
|
||||||
|
counter = 0
|
||||||
|
for release in self.releases:
|
||||||
|
counter += release.downloads
|
||||||
|
return counter
|
||||||
|
|
||||||
def checkPerm(self, user, perm):
|
def checkPerm(self, user, perm):
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
@@ -496,8 +538,11 @@ class Package(db.Model):
|
|||||||
|
|
||||||
isOwner = user == self.author
|
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
|
# 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)
|
return isOwner or user.rank.atLeast(UserRank.EDITOR)
|
||||||
|
|
||||||
if perm == Permission.EDIT_PACKAGE or perm == Permission.APPROVE_CHANGES:
|
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))
|
raise Exception("Permission {} is not related to packages".format(perm.name))
|
||||||
|
|
||||||
def recalcScore(self):
|
def recalcScore(self):
|
||||||
import datetime
|
|
||||||
|
|
||||||
self.score = 10
|
self.score = 10
|
||||||
|
|
||||||
if self.forums is not None:
|
if self.forums is not None:
|
||||||
@@ -604,16 +647,36 @@ class Tag(db.Model):
|
|||||||
regex = re.compile("[^a-z_]")
|
regex = re.compile("[^a-z_]")
|
||||||
self.name = regex.sub("", self.title.lower().replace(" ", "_"))
|
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):
|
class PackageRelease(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
|
package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
|
||||||
title = db.Column(db.String(100), nullable=False)
|
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)
|
url = db.Column(db.String(200), nullable=False)
|
||||||
approved = db.Column(db.Boolean, nullable=False, default=False)
|
approved = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
task_id = db.Column(db.String(37), nullable=True)
|
task_id = db.Column(db.String(37), nullable=True)
|
||||||
commit_hash = db.Column(db.String(41), nullable=True, default=None)
|
commit_hash = db.Column(db.String(41), nullable=True, default=None)
|
||||||
|
downloads = db.Column(db.Integer, nullable=False, default=0)
|
||||||
|
|
||||||
|
min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
|
||||||
|
min_rel = 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):
|
def getEditURL(self):
|
||||||
@@ -630,7 +693,7 @@ class PackageRelease(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.releaseDate = datetime.now()
|
self.releaseDate = datetime.datetime.now()
|
||||||
|
|
||||||
|
|
||||||
class PackageReview(db.Model):
|
class PackageReview(db.Model):
|
||||||
@@ -762,7 +825,7 @@ class Thread(db.Model):
|
|||||||
title = db.Column(db.String(100), nullable=False)
|
title = db.Column(db.String(100), nullable=False)
|
||||||
private = db.Column(db.Boolean, server_default="0")
|
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")
|
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)
|
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
|
||||||
comment = db.Column(db.String(500), nullable=False)
|
comment = db.Column(db.String(500), nullable=False)
|
||||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), 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", \
|
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \
|
||||||
@@ -824,7 +887,7 @@ class ForumTopic(db.Model):
|
|||||||
posts = db.Column(db.Integer, nullable=False)
|
posts = db.Column(db.Integer, nullable=False)
|
||||||
views = 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):
|
def getRepoURL(self):
|
||||||
if self.link is None:
|
if self.link is None:
|
||||||
|
|||||||
9
app/public/static/opensearch.xml
Normal file
9
app/public/static/opensearch.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
||||||
|
<ShortName>ContentDB</ShortName>
|
||||||
|
<LongName>ContentDB</LongName>
|
||||||
|
<InputEncoding>UTF-8</InputEncoding>
|
||||||
|
<Description>Search mods, games, and textures for Minetest.</Description>
|
||||||
|
<Tags>Minetest Mod Game Subgame Search</Tags>
|
||||||
|
<Url type="text/html" method="get" template="https://content.minetest.net/packages?q={searchTerms}"/>
|
||||||
|
</OpenSearchDescription>
|
||||||
@@ -35,10 +35,10 @@ $(function() {
|
|||||||
setField("#repo", result.repo || repoURL);
|
setField("#repo", result.repo || repoURL);
|
||||||
setField("#issueTracker", result.issueTracker);
|
setField("#issueTracker", result.issueTracker);
|
||||||
setField("#desc", result.description);
|
setField("#desc", result.description);
|
||||||
setField("#shortDesc", result.short_description);
|
setField("#short_desc", result.short_description);
|
||||||
setField("#harddep_str", result.depends);
|
setField("#harddep_str", result.depends);
|
||||||
setField("#softdep_str", result.optional_depends);
|
setField("#softdep_str", result.optional_depends);
|
||||||
setField("#shortDesc", result.short_description);
|
setField("#short_desc", result.short_description);
|
||||||
setField("#forums", result.forumId);
|
setField("#forums", result.forumId);
|
||||||
if (result.type && result.type.length > 2) {
|
if (result.type && result.type.length > 2) {
|
||||||
$("#type").val(result.type);
|
$("#type").val(result.type);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ $(function() {
|
|||||||
It's obvious that this adds something to Minetest,
|
It's obvious that this adds something to Minetest,
|
||||||
there's no need to use phrases such as \"adds X to the game\".`
|
there's no need to use phrases such as \"adds X to the game\".`
|
||||||
|
|
||||||
$("#shortDesc").on("change paste keyup", function() {
|
$("#short_desc").on("change paste keyup", function() {
|
||||||
var val = $(this).val().toLowerCase();
|
var val = $(this).val().toLowerCase();
|
||||||
if (val.indexOf("minetest") >= 0 || val.indexOf("mod") >= 0 ||
|
if (val.indexOf("minetest") >= 0 || val.indexOf("mod") >= 0 ||
|
||||||
val.indexOf("modpack") >= 0 || val.indexOf("mod pack") >= 0) {
|
val.indexOf("modpack") >= 0 || val.indexOf("mod pack") >= 0) {
|
||||||
|
|||||||
18
app/public/static/release_minmax.js
Normal file
18
app/public/static/release_minmax.js
Normal 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
102
app/querybuilder.py
Normal 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
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from flask import *
|
from flask import render_template
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
from app import mail
|
from app import mail
|
||||||
from app.tasks import celery
|
from app.tasks import celery
|
||||||
@@ -32,8 +32,10 @@ def sendVerifyEmail(newEmail, token):
|
|||||||
def sendEmailRaw(to, subject, text, html):
|
def sendEmailRaw(to, subject, text, html):
|
||||||
from flask_mail import Message
|
from flask_mail import Message
|
||||||
msg = Message(subject, recipients=to)
|
msg = Message(subject, recipients=to)
|
||||||
|
|
||||||
if text:
|
if text:
|
||||||
msg.body = 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)
|
mail.send(msg)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Licenses
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url_for('createedit_license_page') }}">New Tag</a>
|
<a href="{{ url_for('createedit_license_page') }}">New License</a>
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for l in licenses %}
|
{% for l in licenses %}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<li><a href="{{ url_for('user_list_page') }}">User list</a></li>
|
<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('tag_list_page') }}">Tag Editor</a></li>
|
||||||
<li><a href="{{ url_for('license_list_page') }}">License Editor</a></li>
|
<li><a href="{{ url_for('license_list_page') }}">License Editor</a></li>
|
||||||
|
<li><a href="{{ url_for('version_list_page') }}">Version Editor</a></li>
|
||||||
<li><a href="{{ url_for('switch_user_page') }}">Sign in as another user</a></li>
|
<li><a href="{{ url_for('switch_user_page') }}">Sign in as another user</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|||||||
25
app/templates/admin/versions/edit.html
Normal file
25
app/templates/admin/versions/edit.html
Normal 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 %}
|
||||||
16
app/templates/admin/versions/list.html
Normal file
16
app/templates/admin/versions/list.html
Normal 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 %}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
|
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
|
||||||
<link rel="stylesheet" type="text/css" href="/static/bootstrap.css">
|
<link rel="stylesheet" type="text/css" href="/static/bootstrap.css">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=6">
|
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=6">
|
||||||
|
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
|
||||||
{% block headextra %}{% endblock %}
|
{% block headextra %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -138,7 +139,7 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="/static/easymde.min.css">
|
<link rel="stylesheet" type="text/css" href="/static/easymde.min.css">
|
||||||
<script>
|
<script>
|
||||||
$("textarea.markdown").each(function() {
|
$("textarea.markdown").each(function() {
|
||||||
new EasyMDE({ element: this, hideIcons: ["image"] });
|
new EasyMDE({ element: this, hideIcons: ["image"], forceSync: true });
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
{% block scriptextra %}{% endblock %}
|
{% block scriptextra %}{% endblock %}
|
||||||
|
|||||||
64
app/templates/emails/base.html
Normal file
64
app/templates/emails/base.html
Normal 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 © rubenwardy
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
<h1>Hello!</h1>
|
{% extends "emails/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2 style="margin-top: 0;">Hello!</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
This email has been sent to you because someone (hopefully you)
|
This email has been sent to you because someone (hopefully you)
|
||||||
@@ -6,12 +9,19 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
If this was you, then please click this link to verify the address:
|
If it wasn't you, then just delete this email.
|
||||||
<a href="{{ url_for('verify_email_page', token=token, _external=True) }}">
|
|
||||||
{{ url_for('verify_email_page', token=token, _external=True) }}
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<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>
|
</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 %}
|
||||||
|
|||||||
@@ -4,6 +4,21 @@
|
|||||||
Welcome
|
Welcome
|
||||||
{% endblock %}
|
{% 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 %}
|
{% block content %}
|
||||||
<!-- <header class="jumbotron">
|
<!-- <header class="jumbotron">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -49,5 +64,10 @@ Welcome
|
|||||||
<h2 class="my-3">Top Texture Packs</h2>
|
<h2 class="my-3">Top Texture Packs</h2>
|
||||||
{{ render_pkggrid(pop_txp) }}
|
{{ render_pkggrid(pop_txp) }}
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<small>
|
||||||
|
CDB has {{ count }} packages, with a total of {{ downloads }} downloads.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
<!-- </main> -->
|
<!-- </main> -->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
|
|
||||||
{% macro render_checkbox_field(field, label=None) -%}
|
{% macro render_checkbox_field(field, label=None) -%}
|
||||||
{% if not label %}{% set label=field.label.text %}{% endif %}
|
{% if not label %}{% set label=field.label.text %}{% endif %}
|
||||||
<div class="checkbox">
|
<div class="checkbox {{ kwargs.pop('class_', '') }}">
|
||||||
<label>
|
<label>
|
||||||
{{ field(type='checkbox', **kwargs) }} {{ label }}
|
{{ field(type='checkbox', **kwargs) }} {{ label }}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{ package.shortDesc }}
|
{{ package.short_desc }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,11 +42,18 @@
|
|||||||
<a name="reply"></a>
|
<a name="reply"></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="{{ url_for('thread_page', id=thread.id)}}" class="card-body">
|
{% if current_user.canCommentRL() %}
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<form method="post" action="{{ url_for('thread_page', id=thread.id)}}" class="card-body">
|
||||||
<textarea class="form-control markdown" required maxlength=500 name="comment"></textarea><br />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<input class="btn btn-primary" type="submit" value="Comment" />
|
<textarea class="form-control markdown" required maxlength=500 name="comment"></textarea><br />
|
||||||
</form>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
{{ render_field(form.title, class_="pkg_meta col-sm-7") }}
|
{{ render_field(form.title, class_="pkg_meta col-sm-7") }}
|
||||||
{{ render_field(form.name, class_="pkg_meta col-sm-3") }}
|
{{ render_field(form.name, class_="pkg_meta col-sm-3") }}
|
||||||
</div>
|
</div>
|
||||||
{{ render_field(form.shortDesc, class_="pkg_meta") }}
|
{{ render_field(form.short_desc, class_="pkg_meta") }}
|
||||||
{{ render_multiselect_field(form.tags, class_="pkg_meta") }}
|
{{ render_multiselect_field(form.tags, class_="pkg_meta") }}
|
||||||
<div class="pkg_meta row">
|
<div class="pkg_meta row">
|
||||||
{{ render_field(form.license, class_="not_txp col-sm-6") }}
|
{{ render_field(form.license, class_="not_txp col-sm-6") }}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
{{ render_field(form.type) }}
|
{{ render_field(form.type) }}
|
||||||
{{ render_field(form.name) }}
|
{{ render_field(form.name) }}
|
||||||
{{ render_field(form.title) }}
|
{{ render_field(form.title) }}
|
||||||
{{ render_field(form.shortDesc) }}
|
{{ render_field(form.short_desc) }}
|
||||||
{{ render_field(form.desc) }}
|
{{ render_field(form.desc) }}
|
||||||
{{ render_multiselect_field(form.tags) }}
|
{{ render_multiselect_field(form.tags) }}
|
||||||
|
|
||||||
|
|||||||
67
app/templates/packages/release_bulk_change.html
Normal file
67
app/templates/packages/release_bulk_change.html
Normal 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 %}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
Create a release | {{ package.title }}
|
Edit release | {{ package.title }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -40,6 +40,27 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% 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) }}
|
{{ render_submit_field(form.submit) }}
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scriptextra %}
|
||||||
|
<script src="/static/release_minmax.js?v=1"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -17,7 +17,42 @@
|
|||||||
{{ render_field(form.vcsLabel, class_="mt-3") }}
|
{{ render_field(form.vcsLabel, class_="mt-3") }}
|
||||||
{% endif %}
|
{% 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) }}
|
{{ render_submit_field(form.submit) }}
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% 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 %}
|
||||||
|
|||||||
@@ -19,11 +19,12 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
{{ package.shortDesc }}
|
{{ package.short_desc }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="row" style="margin-top: 2rem;">
|
<div class="row" style="margin-top: 2rem;">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
{{ package.getDownloadCount() }} downloads
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group-horizontal col-md-auto">
|
<div class="btn-group-horizontal col-md-auto">
|
||||||
{% if package.repo %}<a class="btn btn-secondary" href="{{ package.repo }}">View Source</a>{% endif %}
|
{% if package.repo %}<a class="btn btn-secondary" href="{{ package.repo }}">View Source</a>{% endif %}
|
||||||
@@ -111,11 +112,22 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<aside class="float-right ml-4" style="width: 18rem;">
|
<aside class="float-right ml-4" style="width: 18rem;">
|
||||||
{% if package.getDownloadRelease() %}
|
{% set release = package.getDownloadRelease() %}
|
||||||
<a class="btn btn-download btn-lg btn-block"
|
{% if release %}
|
||||||
|
<a class="btn btn-download btn-lg btn-block" rel="nofollow"
|
||||||
href="{{ package.getDownloadURL() }}" class="btn_green">
|
href="{{ package.getDownloadURL() }}" class="btn_green">
|
||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<p class="text-center m-2" style="font-size: 80%;">
|
||||||
|
{% if release.min_rel and release.max_rel %}
|
||||||
|
Minetest {{ release.min_rel.name }} - {{ release.max_rel.name }}
|
||||||
|
{% elif release.min_rel %}
|
||||||
|
Supports Minetest {{ release.min_rel.name }} and above.
|
||||||
|
{% elif release.max_rel %}
|
||||||
|
Supports Minetest {{ release.max_rel.name }} and below.
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
No download available.
|
No download available.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -237,10 +249,13 @@
|
|||||||
<div class="card my-4">
|
<div class="card my-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Releases
|
Releases
|
||||||
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
|
<div class="float-right">
|
||||||
<a class="float-right"
|
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
|
||||||
href="{{ package.getCreateReleaseURL() }}">+</a>
|
<a href="{{ package.getBulkReleaseURL() }}">bulk</a>
|
||||||
{% endif %}
|
|
|
||||||
|
<a href="{{ package.getCreateReleaseURL() }}">+</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
{% for rel in releases %}
|
{% for rel in releases %}
|
||||||
@@ -257,10 +272,28 @@
|
|||||||
|
|
||||||
{% if not rel.approved %}<i>{% endif %}
|
{% if not rel.approved %}<i>{% endif %}
|
||||||
|
|
||||||
<a href="{{ rel.getDownloadURL() }}">{{ rel.title }}</a>{% if rel.commit_hash %}
|
<a href="{{ rel.getDownloadURL() }}" rel="nofollow">{{ rel.title }}</a>
|
||||||
[{{ rel.commit_hash | truncate(5, end='') }}]{% endif %}<br>
|
|
||||||
<small>created {{ rel.releaseDate | datetime }}.</small>
|
<span style="color:#ddd;">
|
||||||
{% if rel.task_id %}
|
{% if rel.min_rel and rel.max_rel %}
|
||||||
|
[MT {{ rel.min_rel.name }}-{{ rel.max_rel.name }}]
|
||||||
|
{% elif rel.min_rel %}
|
||||||
|
[MT {{ rel.min_rel.name }}+]
|
||||||
|
{% elif rel.max_rel %}
|
||||||
|
[MT ≤{{ rel.max_rel.name }}]
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<small style="color:#999;">
|
||||||
|
{% if rel.commit_hash %}
|
||||||
|
[{{ rel.commit_hash | truncate(5, end='') }}]
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
created {{ rel.releaseDate | date }}.
|
||||||
|
</small>
|
||||||
|
{% if (package.checkPerm(current_user, "MAKE_RELEASE") or package.checkPerm(current_user, "APPROVE_RELEASE")) and rel.task_id %}
|
||||||
<a href="{{ url_for('check_task', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
|
<a href="{{ url_for('check_task', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
|
||||||
{% elif not rel.approved %}
|
{% elif not rel.approved %}
|
||||||
Waiting for approval.
|
Waiting for approval.
|
||||||
@@ -289,6 +322,13 @@
|
|||||||
{{ render_threadlist(threads, list_group=True) }}
|
{{ render_threadlist(threads, list_group=True) }}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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>
|
</aside>
|
||||||
|
|
||||||
{% if not package.approved and (package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW")) %}
|
{% if not package.approved and (package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW")) %}
|
||||||
|
|||||||
@@ -6,6 +6,15 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% 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 %}
|
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}
|
||||||
<form method="POST" action="" enctype="multipart/form-data">
|
<form method="POST" action="" enctype="multipart/form-data">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|||||||
18
app/templates/users/send_email.html
Normal file
18
app/templates/users/send_email.html
Normal 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 %}
|
||||||
@@ -65,6 +65,25 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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 %}
|
{% if user == current_user %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Profile Picture:</td>
|
<td>Profile Picture:</td>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from app.models import *
|
|||||||
import flask_menu as menu
|
import flask_menu as menu
|
||||||
from werkzeug.contrib.cache import SimpleCache
|
from werkzeug.contrib.cache import SimpleCache
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
from sqlalchemy.sql.expression import func
|
||||||
cache = SimpleCache()
|
cache = SimpleCache()
|
||||||
|
|
||||||
@app.template_filter()
|
@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_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_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()
|
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)
|
new=new, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam)
|
||||||
|
|
||||||
from . import users, githublogin, packages, meta, threads, api
|
from . import users, packages, meta, threads, api
|
||||||
from . import tasks, admin, notifications, tagseditor, licenseseditor
|
from . import sass, thumbnails, tasks, admin
|
||||||
from . import sass, thumbnails
|
|
||||||
|
|
||||||
@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
|
@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
|
||||||
@app.route('/<path:path>/')
|
@app.route('/<path:path>/')
|
||||||
|
|||||||
18
app/views/admin/__init__.py
Normal file
18
app/views/admin/__init__.py
Normal 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
|
||||||
60
app/views/admin/versioneditor.py
Normal file
60
app/views/admin/versioneditor.py
Normal 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)
|
||||||
@@ -19,15 +19,15 @@ from flask import *
|
|||||||
from flask_user import *
|
from flask_user import *
|
||||||
from app import app
|
from app import app
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app.utils import is_package_page, rank_required
|
from app.utils import is_package_page
|
||||||
from .packages import QueryBuilder
|
from app.querybuilder import QueryBuilder
|
||||||
|
|
||||||
@app.route("/api/packages/")
|
@app.route("/api/packages/")
|
||||||
def api_packages_page():
|
def api_packages_page():
|
||||||
qb = QueryBuilder()
|
qb = QueryBuilder(request.args)
|
||||||
query = qb.buildPackageQuery()
|
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]
|
for package in query.all() if package.getDownloadRelease() is not None]
|
||||||
return jsonify(pkgs)
|
return jsonify(pkgs)
|
||||||
|
|
||||||
@@ -61,3 +61,9 @@ def topic_set_discard():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify(topic.getAsDictionary())
|
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])
|
||||||
|
|||||||
@@ -15,431 +15,4 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from flask import *
|
from . import packages, screenshots, releases
|
||||||
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
|
|
||||||
|
|||||||
360
app/views/packages/packages.py
Normal file
360
app/views/packages/packages.py
Normal 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)
|
||||||
@@ -27,12 +27,28 @@ from celery import uuid
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import *
|
from wtforms import *
|
||||||
from wtforms.validators import *
|
from wtforms.validators import *
|
||||||
|
from 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):
|
class CreatePackageReleaseForm(FlaskForm):
|
||||||
title = StringField("Title", [InputRequired(), Length(1, 30)])
|
title = StringField("Title", [InputRequired(), Length(1, 30)])
|
||||||
uploadOpt = RadioField ("Method", choices=[("upload", "File Upload")], default="upload")
|
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")
|
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")
|
submit = SubmitField("Save")
|
||||||
|
|
||||||
class EditPackageReleaseForm(FlaskForm):
|
class EditPackageReleaseForm(FlaskForm):
|
||||||
@@ -40,6 +56,10 @@ class EditPackageReleaseForm(FlaskForm):
|
|||||||
url = StringField("URL", [URL])
|
url = StringField("URL", [URL])
|
||||||
task_id = StringField("Task ID")
|
task_id = StringField("Task ID")
|
||||||
approved = BooleanField("Is Approved")
|
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")
|
submit = SubmitField("Save")
|
||||||
|
|
||||||
@app.route("/packages/<author>/<name>/releases/new/", methods=["GET", "POST"])
|
@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.title = form["title"].data
|
||||||
rel.url = ""
|
rel.url = ""
|
||||||
rel.task_id = uuid()
|
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.add(rel)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -80,6 +103,9 @@ def create_release_page(package):
|
|||||||
rel.package = package
|
rel.package = package
|
||||||
rel.title = form["title"].data
|
rel.title = form["title"].data
|
||||||
rel.url = uploadedPath
|
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.add(rel)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -105,6 +131,11 @@ def download_release_page(package, id):
|
|||||||
flash("No download available.", "error")
|
flash("No download available.", "error")
|
||||||
return redirect(package.getDetailsURL())
|
return redirect(package.getDetailsURL())
|
||||||
else:
|
else:
|
||||||
|
PackageRelease.query.filter_by(id=release.id).update({
|
||||||
|
"downloads": PackageRelease.downloads + 1
|
||||||
|
})
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
return redirect(release.url, code=300)
|
return redirect(release.url, code=300)
|
||||||
|
|
||||||
@app.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
|
@app.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
|
||||||
@@ -128,6 +159,8 @@ def edit_release_page(package, id):
|
|||||||
wasApproved = release.approved
|
wasApproved = release.approved
|
||||||
if canEdit:
|
if canEdit:
|
||||||
release.title = form["title"].data
|
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):
|
if package.checkPerm(current_user, Permission.CHANGE_RELEASE_URL):
|
||||||
release.url = form["url"].data
|
release.url = form["url"].data
|
||||||
@@ -144,3 +177,43 @@ def edit_release_page(package, id):
|
|||||||
return redirect(package.getDetailsURL())
|
return redirect(package.getDetailsURL())
|
||||||
|
|
||||||
return render_template("packages/release_edit.html", package=package, release=release, form=form)
|
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)
|
||||||
|
|||||||
@@ -53,9 +53,10 @@ def create_screenshot_page(package, id=None):
|
|||||||
"a PNG or JPG image file")
|
"a PNG or JPG image file")
|
||||||
if uploadedPath is not None:
|
if uploadedPath is not None:
|
||||||
ss = PackageScreenshot()
|
ss = PackageScreenshot()
|
||||||
ss.package = package
|
ss.package = package
|
||||||
ss.title = form["title"].data or "Untitled"
|
ss.title = form["title"].data or "Untitled"
|
||||||
ss.url = uploadedPath
|
ss.url = uploadedPath
|
||||||
|
ss.approved = package.checkPerm(current_user, Permission.APPROVE_SCREENSHOT)
|
||||||
db.session.add(ss)
|
db.session.add(ss)
|
||||||
|
|
||||||
msg = "{}: Screenshot added {}" \
|
msg = "{}: Screenshot added {}" \
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ from app import app
|
|||||||
from app.models import *
|
from app.models import *
|
||||||
from app.utils import triggerNotif, clearNotifications
|
from app.utils import triggerNotif, clearNotifications
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import *
|
from wtforms import *
|
||||||
from wtforms.validators import *
|
from wtforms.validators import *
|
||||||
@@ -78,6 +80,13 @@ def thread_page(id):
|
|||||||
if current_user.is_authenticated and request.method == "POST":
|
if current_user.is_authenticated and request.method == "POST":
|
||||||
comment = request.form["comment"]
|
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:
|
if len(comment) <= 500 and len(comment) > 3:
|
||||||
reply = ThreadReply()
|
reply = ThreadReply()
|
||||||
reply.author = current_user
|
reply.author = current_user
|
||||||
@@ -126,15 +135,15 @@ def new_thread_page():
|
|||||||
if package is None:
|
if package is None:
|
||||||
flash("Unable to find that package!", "error")
|
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:
|
if package is None:
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
def_is_private = request.args.get("private") or False
|
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
|
def_is_private = True
|
||||||
allow_change = package.approved
|
allow_change = package and package.approved
|
||||||
is_review_thread = package is not None and not package.approved
|
is_review_thread = package and not package.approved
|
||||||
|
|
||||||
# Check that user can make the thread
|
# Check that user can make the thread
|
||||||
if not package.checkPerm(current_user, Permission.CREATE_THREAD):
|
if not package.checkPerm(current_user, Permission.CREATE_THREAD):
|
||||||
@@ -144,8 +153,15 @@ def new_thread_page():
|
|||||||
# Only allow creating one thread when not approved
|
# Only allow creating one thread when not approved
|
||||||
elif is_review_thread and package.review_thread is not None:
|
elif is_review_thread and package.review_thread is not None:
|
||||||
flash("A review thread already exists!", "error")
|
flash("A review thread already exists!", "error")
|
||||||
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
|
# Set default values
|
||||||
elif request.method == "GET":
|
elif request.method == "GET":
|
||||||
@@ -178,13 +194,19 @@ def new_thread_page():
|
|||||||
if is_review_thread:
|
if is_review_thread:
|
||||||
package.review_thread = thread
|
package.review_thread = thread
|
||||||
|
|
||||||
|
notif_msg = None
|
||||||
if package is not None:
|
if package is not None:
|
||||||
triggerNotif(package.author, current_user,
|
notif_msg = "New thread '{}' on package {}".format(thread.title, package.title)
|
||||||
"New thread '{}' on package {}".format(thread.title, package.title), url_for("thread_page", id=thread.id))
|
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()
|
db.session.commit()
|
||||||
|
|
||||||
return redirect(url_for("thread_page", id=thread.id))
|
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)
|
||||||
|
|||||||
18
app/views/users/__init__.py
Normal file
18
app/views/users/__init__.py
Normal 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
|
||||||
@@ -18,14 +18,14 @@
|
|||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
from flask_login import login_user, logout_user
|
from flask_login import login_user, logout_user
|
||||||
from app import app
|
from app import app, markdown
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import *
|
from wtforms import *
|
||||||
from wtforms.validators import *
|
from wtforms.validators import *
|
||||||
from app.utils import randomString, loginUser
|
from app.utils import randomString, loginUser, rank_required
|
||||||
from app.tasks.forumtasks import checkForumAccount
|
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
|
from app.tasks.phpbbparser import getProfile
|
||||||
|
|
||||||
# Define the User profile form
|
# 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))
|
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):
|
class SetPasswordForm(FlaskForm):
|
||||||
email = StringField("Email", [Optional(), Email()])
|
email = StringField("Email", [Optional(), Email()])
|
||||||
password = PasswordField("New password", [InputRequired(), Length(2, 20)])
|
password = PasswordField("New password", [InputRequired(), Length(2, 20)])
|
||||||
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal 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
|
||||||
36
migrations/versions/2f3c3597c78d_.py
Normal file
36
migrations/versions/2f3c3597c78d_.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 2f3c3597c78d
|
||||||
|
Revises: 9ec17b558413
|
||||||
|
Create Date: 2019-01-29 02:43:08.865695
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
from sqlalchemy_utils.types import TSVectorType
|
||||||
|
from sqlalchemy_searchable import sync_trigger
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2f3c3597c78d'
|
||||||
|
down_revision = '9ec17b558413'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('package', 'shortDesc', nullable=False, new_column_name='short_desc')
|
||||||
|
op.add_column('package', sa.Column('search_vector', TSVectorType("title", "short_desc", "desc"), nullable=True))
|
||||||
|
op.create_index('ix_package_search_vector', 'package', ['search_vector'], unique=False, postgresql_using='gin')
|
||||||
|
|
||||||
|
conn = op.get_bind()
|
||||||
|
sync_trigger(conn, 'package', 'search_vector', ["title", "short_desc", "desc"])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index('ix_package_search_vector', table_name='package')
|
||||||
|
op.drop_column('package', 'search_vector')
|
||||||
|
# ### end Alembic commands ###
|
||||||
41
migrations/versions/7def3e843d04_.py
Normal file
41
migrations/versions/7def3e843d04_.py
Normal 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 ###
|
||||||
249
migrations/versions/7ff57806ffd5_.py
Normal file
249
migrations/versions/7ff57806ffd5_.py
Normal 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 ###
|
||||||
28
migrations/versions/97a9c461bc2d_.py
Normal file
28
migrations/versions/97a9c461bc2d_.py
Normal 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 ###
|
||||||
28
migrations/versions/9ec17b558413_.py
Normal file
28
migrations/versions/9ec17b558413_.py
Normal 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 ###
|
||||||
@@ -8,6 +8,7 @@ Flask-Migrate~=2.3
|
|||||||
Flask-SQLAlchemy~=2.3
|
Flask-SQLAlchemy~=2.3
|
||||||
Flask-User~=0.6
|
Flask-User~=0.6
|
||||||
GitHub-Flask~=3.2
|
GitHub-Flask~=3.2
|
||||||
|
SQLAlchemy-Searchable==1.0.3
|
||||||
|
|
||||||
beautifulsoup4~=4.6
|
beautifulsoup4~=4.6
|
||||||
celery~=4.2
|
celery~=4.2
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/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
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/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
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/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
|
||||||
|
|||||||
31
setup.py
31
setup.py
@@ -20,7 +20,9 @@ import os, sys, datetime
|
|||||||
if not "FLASK_CONFIG" in os.environ:
|
if not "FLASK_CONFIG" in os.environ:
|
||||||
os.environ["FLASK_CONFIG"] = "../config.cfg"
|
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.models import *
|
||||||
from app.utils import make_flask_user_password
|
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.repo = "https://github.com/ezhh/other_worlds"
|
||||||
mod.issueTracker = "https://github.com/ezhh/other_worlds/issues"
|
mod.issueTracker = "https://github.com/ezhh/other_worlds/issues"
|
||||||
mod.forums = 16015
|
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"
|
mod.desc = "This is the long desc"
|
||||||
db.session.add(mod)
|
db.session.add(mod)
|
||||||
|
|
||||||
@@ -75,7 +77,7 @@ def defineDummyData(licenses, tags, ruben):
|
|||||||
mod1.repo = "https://github.com/rubenwardy/awards"
|
mod1.repo = "https://github.com/rubenwardy/awards"
|
||||||
mod1.issueTracker = "https://github.com/rubenwardy/awards/issues"
|
mod1.issueTracker = "https://github.com/rubenwardy/awards/issues"
|
||||||
mod1.forums = 4870
|
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 = """
|
mod1.desc = """
|
||||||
Majority of awards are back ported from Calinou's old fork in Carbone, under same license.
|
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.repo = "https://github.com/minetest-mods/mesecons/"
|
||||||
mod2.issueTracker = "https://github.com/minetest-mods/mesecons/issues"
|
mod2.issueTracker = "https://github.com/minetest-mods/mesecons/issues"
|
||||||
mod2.forums = 628
|
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 = """
|
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.repo = "https://github.com/ezhh/handholds"
|
||||||
mod.issueTracker = "https://github.com/ezhh/handholds/issues"
|
mod.issueTracker = "https://github.com/ezhh/handholds/issues"
|
||||||
mod.forums = 17069
|
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"
|
mod.desc = "This is the long desc"
|
||||||
db.session.add(mod)
|
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.repo = "https://github.com/ezhh/other_worlds"
|
||||||
mod.issueTracker = "https://github.com/ezhh/other_worlds/issues"
|
mod.issueTracker = "https://github.com/ezhh/other_worlds/issues"
|
||||||
mod.forums = 16015
|
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"
|
mod.desc = "This is the long desc"
|
||||||
db.session.add(mod)
|
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.repo = "https://github.com/rubenwardy/food/"
|
||||||
mod.issueTracker = "https://github.com/rubenwardy/food/issues/"
|
mod.issueTracker = "https://github.com/rubenwardy/food/issues/"
|
||||||
mod.forums = 2960
|
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"
|
mod.desc = "This is the long desc"
|
||||||
food = mod
|
food = mod
|
||||||
db.session.add(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.repo = "https://github.com/rubenwardy/food_sweet/"
|
||||||
mod.issueTracker = "https://github.com/rubenwardy/food_sweet/issues/"
|
mod.issueTracker = "https://github.com/rubenwardy/food_sweet/issues/"
|
||||||
mod.forums = 9039
|
mod.forums = 9039
|
||||||
mod.shortDesc = "Adds sweet food"
|
mod.short_desc = "Adds sweet food"
|
||||||
mod.desc = "This is the long desc"
|
mod.desc = "This is the long desc"
|
||||||
food_sweet = mod
|
food_sweet = mod
|
||||||
db.session.add(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.repo = "https://github.com/rubenwardy/capturetheflag"
|
||||||
game1.issueTracker = "https://github.com/rubenwardy/capturetheflag/issues"
|
game1.issueTracker = "https://github.com/rubenwardy/capturetheflag/issues"
|
||||||
game1.forums = 12835
|
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 = """
|
game1.desc = """
|
||||||
As seen on the Capture the Flag server (minetest.rubenwardy.com:30000)
|
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.type = PackageType.TXP
|
||||||
mod.author = ruben
|
mod.author = ruben
|
||||||
mod.forums = 14132
|
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"
|
mod.desc = "This is the long desc"
|
||||||
db.session.add(mod)
|
db.session.add(mod)
|
||||||
|
|
||||||
@@ -333,13 +335,14 @@ Uses the CTF PvP Engine.
|
|||||||
db.session.add(dep)
|
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"):
|
if delete_db and os.path.isfile("db.sqlite"):
|
||||||
os.remove("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...")
|
print("Filling database...")
|
||||||
|
|
||||||
ruben = User("rubenwardy")
|
ruben = User("rubenwardy")
|
||||||
|
|||||||
Reference in New Issue
Block a user