Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ce495fcd3 | ||
|
|
776a3eff2a | ||
|
|
04e8ae5bdd | ||
|
|
18b9fb3876 | ||
|
|
1da86f27a7 | ||
|
|
85340a2fe9 | ||
|
|
c4a4d9c116 | ||
|
|
87a184595c | ||
|
|
b3b1e421f2 | ||
|
|
60483ef542 | ||
|
|
3c8a8b8988 | ||
|
|
2f8bdd8f0f | ||
|
|
e87db8b87f | ||
|
|
b36273a848 | ||
|
|
7b087158d7 | ||
|
|
2fbc44bd54 | ||
|
|
950512c2a7 | ||
|
|
f4010d498f | ||
|
|
f04d4ff3cd | ||
|
|
f8b290fc45 | ||
|
|
7e4eb29db7 | ||
|
|
93a74b7681 | ||
|
|
2677e088a8 | ||
|
|
0fd4984e5a | ||
|
|
896a65fd99 | ||
|
|
885209a614 | ||
|
|
4c109d6bd3 | ||
|
|
9c2c8c21f1 | ||
|
|
e40b247a97 | ||
|
|
a79cc758ed | ||
|
|
bafd426eaf | ||
|
|
36f9572cbb | ||
|
|
2586a11bcf | ||
|
|
d36138d5e1 | ||
|
|
7810bb54e0 | ||
|
|
2844773e4d | ||
|
|
23c406bff9 | ||
|
|
0f3adda592 | ||
|
|
441ed3beeb | ||
|
|
d1f5585fda | ||
|
|
0fd3ed8f6b | ||
|
|
0e5c1f83ff | ||
|
|
f112756b04 | ||
|
|
f822027ec5 | ||
|
|
034315d421 | ||
|
|
5cd8b35d1f | ||
|
|
84b996c489 | ||
|
|
d77403c0be | ||
|
|
e9fe936aa9 | ||
|
|
8afe17b984 | ||
|
|
2691105513 | ||
|
|
5f7efd4f31 | ||
|
|
7d52931a20 | ||
|
|
a45df0e173 | ||
|
|
0db49efe4a | ||
|
|
9639cf04f1 | ||
|
|
9866e43b4b | ||
|
|
014370ea06 | ||
|
|
fbf374ff5d | ||
|
|
a68ac9cb4d | ||
|
|
7943598528 | ||
|
|
4bc8b58af7 | ||
|
|
ec0e89c21d | ||
|
|
2975f94d9e | ||
|
|
a9a045eefd | ||
|
|
d09ede00fb | ||
|
|
515248eb8b | ||
|
|
66ee706a6c | ||
|
|
d44178cb0c | ||
|
|
c926a812d3 | ||
|
|
0b83d2f2b5 | ||
|
|
21960f2404 | ||
|
|
f94885a58f | ||
|
|
f7d4b4bf6d | ||
|
|
d04e060854 | ||
|
|
7801be3d39 | ||
|
|
b10660030a | ||
|
|
f5744f5188 | ||
|
|
272be09ba1 | ||
|
|
09150a4dbb | ||
|
|
c726f56b3e | ||
|
|
daded6d193 | ||
|
|
b0a5980833 | ||
|
|
1eaed55bc6 | ||
|
|
c2265313d8 | ||
|
|
49d5a123e5 | ||
|
|
c79c970171 | ||
|
|
fa0506f58a | ||
|
|
50889ccca5 | ||
|
|
b8ca5d24c5 | ||
|
|
63969529ad | ||
|
|
08434300d8 | ||
|
|
86566bcd39 | ||
|
|
a7fcce4448 | ||
|
|
366ed9913e | ||
|
|
79f4e16286 | ||
|
|
137a6928bc | ||
|
|
de9135f44f | ||
|
|
31f57e1f12 | ||
|
|
89cae279cd | ||
|
|
fd901726b0 | ||
|
|
5f40d68441 | ||
|
|
8eedbf64a4 | ||
|
|
c551201f79 | ||
|
|
a21a5c24d8 | ||
|
|
0a969e597b | ||
|
|
a1700b5f7e | ||
|
|
d61f77a805 | ||
|
|
f6384e2e15 | ||
|
|
09a201759b | ||
|
|
5dcff01436 | ||
|
|
f355721cdb | ||
|
|
a25f77ce3c | ||
|
|
692628653c | ||
|
|
35f798c862 | ||
|
|
3a0e0377f9 | ||
|
|
c6a26786ec | ||
|
|
e5cb7a3721 | ||
|
|
03a155c17b | ||
|
|
266d579e9d | ||
|
|
c97eefc7b2 | ||
|
|
9da6b45cc3 | ||
|
|
c9bf7a3245 | ||
|
|
dd368d87aa | ||
|
|
e5b279d013 | ||
|
|
8ca3437689 | ||
|
|
aeafb8247f | ||
|
|
75bab28d82 | ||
|
|
328d05bdf6 | ||
|
|
2229b32c90 | ||
|
|
ed409df323 | ||
|
|
b8decafd75 | ||
|
|
5aaee010c1 | ||
|
|
a01fe4043e | ||
|
|
e0ef0e018d | ||
|
|
0210a3e601 | ||
|
|
36000b1592 | ||
|
|
b296b9b299 | ||
|
|
dd6257a0a0 | ||
|
|
23b324cc9c | ||
|
|
f61f9e8654 |
7
.gitignore
vendored
@@ -1,12 +1,15 @@
|
|||||||
config.cfg
|
config.cfg
|
||||||
config.prod.cfg
|
*.env
|
||||||
*.sqlite
|
*.sqlite
|
||||||
main.css
|
.vscode
|
||||||
|
custom.css
|
||||||
tmp
|
tmp
|
||||||
log.txt
|
log.txt
|
||||||
*.rdb
|
*.rdb
|
||||||
uploads
|
uploads
|
||||||
thumbnails
|
thumbnails
|
||||||
|
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
@@ -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
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -17,12 +17,14 @@
|
|||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
|
from flask_gravatar import Gravatar
|
||||||
import flask_menu as menu
|
import flask_menu as menu
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
from flask.ext import markdown
|
from flaskext.markdown import Markdown
|
||||||
from flask_github import GitHub
|
from flask_github import GitHub
|
||||||
from flask_wtf.csrf import CsrfProtect
|
from flask_wtf.csrf import CsrfProtect
|
||||||
from flask_flatpages import FlatPages
|
from flask_flatpages import FlatPages
|
||||||
|
from flask_babel import Babel
|
||||||
import os
|
import os
|
||||||
|
|
||||||
app = Flask(__name__, static_folder="public/static")
|
app = Flask(__name__, static_folder="public/static")
|
||||||
@@ -31,15 +33,30 @@ 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.Markdown(app, extensions=["fenced_code"], safe_mode=True, output_format="html5")
|
markdown = Markdown(app, extensions=["fenced_code"], safe_mode=True, output_format="html5")
|
||||||
github = GitHub(app)
|
github = GitHub(app)
|
||||||
csrf = CsrfProtect(app)
|
csrf = CsrfProtect(app)
|
||||||
mail = Mail(app)
|
mail = Mail(app)
|
||||||
pages = FlatPages(app)
|
pages = FlatPages(app)
|
||||||
|
babel = Babel(app)
|
||||||
|
gravatar = Gravatar(app,
|
||||||
|
size=58,
|
||||||
|
rating='g',
|
||||||
|
default='mp',
|
||||||
|
force_default=False,
|
||||||
|
force_lower=False,
|
||||||
|
use_ssl=True,
|
||||||
|
base_url=None)
|
||||||
|
|
||||||
if not app.debug:
|
if not app.debug:
|
||||||
from .maillogger import register_mail_error_handler
|
from .maillogger import register_mail_error_handler
|
||||||
register_mail_error_handler(app, mail)
|
register_mail_error_handler(app, mail)
|
||||||
|
|
||||||
|
|
||||||
|
@babel.localeselector
|
||||||
|
def get_locale():
|
||||||
|
return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
|
||||||
|
|
||||||
|
|
||||||
from . import models, tasks
|
from . import models, tasks
|
||||||
from .views import *
|
from .views import *
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ title: Help
|
|||||||
|
|
||||||
* [Package Tags](package_tags)
|
* [Package Tags](package_tags)
|
||||||
* [Ranks and Permissions](ranks_permissions)
|
* [Ranks and Permissions](ranks_permissions)
|
||||||
|
* [Content Ratings and Flags](content_flags)
|
||||||
* [Reporting Content](reporting)
|
* [Reporting Content](reporting)
|
||||||
|
|||||||
26
app/flatpages/help/content_flags.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
title: Content Flags
|
||||||
|
|
||||||
|
Content flags allow you to hide content based on your preferences.
|
||||||
|
The filtering is done server-side, which means that you don't need to update
|
||||||
|
your client to use new flags.
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
* `nonfree` - can be used to hide packages which do not qualify as
|
||||||
|
'free software', as defined by the Free Software Foundation.
|
||||||
|
* A content rating, given below.
|
||||||
|
|
||||||
|
|
||||||
|
## Ratings
|
||||||
|
|
||||||
|
Content ratings aren't currently supported by ContentDB.
|
||||||
|
Instead, mature content isn't allowed at all for now.
|
||||||
|
|
||||||
|
In the future, more mature content will be allowed but labelled with
|
||||||
|
content ratings which may contain the following:
|
||||||
|
|
||||||
|
* android_default - meta-rating which includes gore and drugs.
|
||||||
|
* desktop_default - meta-rating which won't include anything for now.
|
||||||
|
* gore - more than just blood
|
||||||
|
* drugs
|
||||||
|
* swearing
|
||||||
@@ -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>
|
||||||
|
|||||||
42
app/flatpages/help/wtfpl.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
title: WTFPL is a terrible license
|
||||||
|
no_h1: true
|
||||||
|
|
||||||
|
<div id="warning" class="alert alert-warning">
|
||||||
|
<span class="icon_message"></span>
|
||||||
|
|
||||||
|
Please reconsider the choice of WTFPL as a license.
|
||||||
|
|
||||||
|
<script src="/static/jquery.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// @author rubenwardy
|
||||||
|
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
|
||||||
|
|
||||||
|
var params = new URLSearchParams(location.search);
|
||||||
|
var r = params.get("r");
|
||||||
|
if (r)
|
||||||
|
document.write("<a class='alert_right button' href='" + r + "'>Okay</a>");
|
||||||
|
else
|
||||||
|
$("#warning").hide();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# WTFPL is a terrible license
|
||||||
|
|
||||||
|
The use of WTFPL as a license is discouraged for multiple reasons.
|
||||||
|
|
||||||
|
* **No Warranty disclaimer:** This could open you up to being sued.<sup>[1]</sup>
|
||||||
|
* **Swearing:** This prevents settings like schools from using your content.
|
||||||
|
* **Not OSI Approved:** Same as public domain?
|
||||||
|
|
||||||
|
The Open Source Initiative chose not to approve the license as an open-source
|
||||||
|
license, saying:<sup>[3]</sup>
|
||||||
|
|
||||||
|
> It's no different from dedication to the public domain.
|
||||||
|
> Author has submitted license approval request – author is free to make public domain dedication.
|
||||||
|
> Although he agrees with the recommendation, Mr. Michlmayr notes that public domain doesn't exist in Europe. Recommend: Reject.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
1. [WTFPL is harmful to software developers](https://cubicspot.blogspot.com/2017/04/wtfpl-is-harmful-to-software-developers.html)
|
||||||
|
2. [FSF](https://www.gnu.org/licenses/license-list.en.html)
|
||||||
|
3. [OSI](https://opensource.org/minutes20090304)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
title: Package Inclusion Policy and Guidance
|
title: Package Inclusion Policy and Guidance
|
||||||
|
|
||||||
<div class="box box_grey alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<b>Note:</b> This is a draft
|
<b>Note:</b> This is a draft
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -10,36 +10,45 @@ ContentDB is for the community, and as such listings should be useful to the
|
|||||||
community. To help with this, there are a few rules to improve the quality of
|
community. To help with this, there are a few rules to improve the quality of
|
||||||
the listings and to combat abuse.
|
the listings and to combat abuse.
|
||||||
|
|
||||||
* No inappropriate content.
|
* No inappropriate content. <sup>2.1</sup>
|
||||||
* Content must be playable/useful, but not necessarily finished.
|
* Content must be playable/useful, but not necessarily finished. <sup>2.2</sup>
|
||||||
* Don't use the name of another mod unless your mod is a fork or reimplementation.
|
* Don't use the name of another mod unless your mod is a fork or reimplementation. <sup>3</sup>
|
||||||
* Licenses must allow derivatives, redistribution, and must not discriminate.
|
* Licenses must allow derivatives, redistribution, and must not discriminate. <sup>4</sup>
|
||||||
* Don't put promotions are advertisements in package listings, except for
|
* Don't put promotions or advertisements in package listings, except for
|
||||||
donation and personal website links which are permitted in the long description.
|
donation and personal website links which are permitted in the
|
||||||
|
long description. <sup>5</sup>
|
||||||
|
* The ContentDB admin reserves the right to remove packages for any reason,
|
||||||
|
including ones not covered by this document, and to ban users who abuse
|
||||||
|
this service. <sup>1</sup>
|
||||||
|
|
||||||
|
|
||||||
## 1. General
|
## 1. General
|
||||||
|
|
||||||
It is not permitted to submit abusive, obscene, vulgar, slanderous, hateful,
|
|
||||||
threatening, sexually-orientated or any material that may violate any laws be
|
|
||||||
it of your country, the country where "Content DB” is hosted or International Law.
|
|
||||||
|
|
||||||
The ContentDB admin reserves the right to remove packages for any reason,
|
The ContentDB admin reserves the right to remove packages for any reason,
|
||||||
including ones not covered by this document, and to ban users who abuse this service.
|
including ones not covered by this document, and to ban users who abuse this service.
|
||||||
|
|
||||||
Also see the [help page on tags](/help/package_tags/).
|
Also see the [help page on tags](/help/package_tags/).
|
||||||
|
|
||||||
|
|
||||||
## 2. Accepted Content and State of Completion
|
## 2. Accepted Content
|
||||||
|
|
||||||
|
### 2.1. Acceptable Content
|
||||||
|
|
||||||
|
Sexually-orientated content is not permitted.
|
||||||
|
|
||||||
|
Mature content, including that relating to drugs, excessive gore, violence, or
|
||||||
|
horror, is not currently permitted - but will be in the future.
|
||||||
|
|
||||||
The submission of malware is strictly prohibited. This includes software which
|
The submission of malware is strictly prohibited. This includes software which
|
||||||
does not do as it advertises, for example if it posts telemetry without stating
|
does not do as it advertises, for example if it posts telemetry without stating
|
||||||
clearly that it does in the package meta.
|
clearly that it does in the package meta.
|
||||||
|
|
||||||
|
### 2.2. State of Completion
|
||||||
|
|
||||||
ContentDB should only currently contain playable content - content which is
|
ContentDB should only currently contain playable content - content which is
|
||||||
sufficiently complete to be useful to end users. It's fine to add stuff which
|
sufficiently complete to be useful to end users. It's fine to add stuff which
|
||||||
is still a work in progress (WIP) as long as it adds sufficient value -
|
is still a work in progress (WIP) as long as it adds sufficient value -
|
||||||
Mineclone 2 is a good example of a WIP package which may break between releases
|
MineClone 2 is a good example of a WIP package which may break between releases
|
||||||
but still has value. Note that this doesn't mean that you should add a thing
|
but still has value. Note that this doesn't mean that you should add a thing
|
||||||
you started working on yesterday, it's worth adding all the basic stuff to
|
you started working on yesterday, it's worth adding all the basic stuff to
|
||||||
make your package useful.
|
make your package useful.
|
||||||
@@ -65,8 +74,7 @@ package and give the name to the correct one.
|
|||||||
If you submit a package where you don't have the right to the name you will be asked
|
If you submit a package where you don't have the right to the name you will be asked
|
||||||
to change the name of the package, or your package won't be accepted.
|
to change the name of the package, or your package won't be accepted.
|
||||||
|
|
||||||
We reserve the right to issue exceptions for this where we feel necessary, however
|
We reserve the right to issue exceptions for this where we feel necessary.
|
||||||
this will be done rarely and usually only for packages created before CDB was created.
|
|
||||||
|
|
||||||
### 3.2 Mod Forks and Reimplementations
|
### 3.2 Mod Forks and Reimplementations
|
||||||
|
|
||||||
@@ -108,23 +116,24 @@ It is recommended that you use a proper license for code with a warranty
|
|||||||
disclaimer, such as the (L)GPL or MIT. You should also use a proper media license
|
disclaimer, such as the (L)GPL or MIT. You should also use a proper media license
|
||||||
for media, such as a Creative Commons license.
|
for media, such as a Creative Commons license.
|
||||||
|
|
||||||
The use of WTFPL is discouraged as it doesn't contain a valid warranty disclaimer,
|
The use of WTFPL is discouraged as it doesn't contain a [valid warranty disclaimer](https://cubicspot.blogspot.com/2017/04/wtfpl-is-harmful-to-software-developers.html),
|
||||||
and also includes swearing which dissuades teachers from using your content.
|
and also includes swearing which prevents settings like schools from using your content.
|
||||||
|
[Read more](/help/wtfpl/).
|
||||||
|
|
||||||
Public domain is not a valid license in many countries, please use CC0 or MIT instead.
|
Public domain is not a valid license in many countries, please use CC0 or MIT instead.
|
||||||
|
|
||||||
|
|
||||||
## 5. Promotions and Advertisements (inc. asking for donations)
|
## 5. Promotions and Advertisements (inc. asking for donations)
|
||||||
|
|
||||||
Any information other than the long description - including screenshots - must
|
You may note place any promotions or advertisements in any meta data including
|
||||||
not contain any promotions or advertisements. This includes asking for donations,
|
screensthos. This includes asking for donations, promoting online shops,
|
||||||
promoting online shops, or linking to personal websites and social media.
|
or linking to personal websites and social media. Please instead use the
|
||||||
|
fields provided on your user profile page to place links to websites and
|
||||||
|
donation pages.
|
||||||
|
|
||||||
ContentDB is for the community. We may remove any promotions if we feel that
|
ContentDB is for the community. We may remove any promotions if we feel that
|
||||||
they're inappropriate.
|
they're inappropriate.
|
||||||
|
|
||||||
Paid promotions are not allowed at all, anywhere.
|
|
||||||
|
|
||||||
|
|
||||||
## 6. Reporting Violations
|
## 6. Reporting Violations
|
||||||
|
|
||||||
|
|||||||
256
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 app import app, gravatar
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from app import app
|
|
||||||
from datetime import datetime
|
from flask import Flask, url_for
|
||||||
from sqlalchemy.orm import validates
|
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):
|
||||||
@@ -66,6 +76,7 @@ class Permission(enum.Enum):
|
|||||||
APPROVE_CHANGES = "APPROVE_CHANGES"
|
APPROVE_CHANGES = "APPROVE_CHANGES"
|
||||||
DELETE_PACKAGE = "DELETE_PACKAGE"
|
DELETE_PACKAGE = "DELETE_PACKAGE"
|
||||||
CHANGE_AUTHOR = "CHANGE_AUTHOR"
|
CHANGE_AUTHOR = "CHANGE_AUTHOR"
|
||||||
|
CHANGE_NAME = "CHANGE_NAME"
|
||||||
MAKE_RELEASE = "MAKE_RELEASE"
|
MAKE_RELEASE = "MAKE_RELEASE"
|
||||||
ADD_SCREENSHOTS = "ADD_SCREENSHOTS"
|
ADD_SCREENSHOTS = "ADD_SCREENSHOTS"
|
||||||
APPROVE_SCREENSHOT = "APPROVE_SCREENSHOT"
|
APPROVE_SCREENSHOT = "APPROVE_SCREENSHOT"
|
||||||
@@ -78,6 +89,8 @@ class Permission(enum.Enum):
|
|||||||
EDIT_EDITREQUEST = "EDIT_EDITREQUEST"
|
EDIT_EDITREQUEST = "EDIT_EDITREQUEST"
|
||||||
SEE_THREAD = "SEE_THREAD"
|
SEE_THREAD = "SEE_THREAD"
|
||||||
CREATE_THREAD = "CREATE_THREAD"
|
CREATE_THREAD = "CREATE_THREAD"
|
||||||
|
UNAPPROVE_PACKAGE = "UNAPPROVE_PACKAGE"
|
||||||
|
TOPIC_DISCARD = "TOPIC_DISCARD"
|
||||||
|
|
||||||
# Only return true if the permission is valid for *all* contexts
|
# Only return true if the permission is valid for *all* contexts
|
||||||
# See Package.checkPerm for package-specific contexts
|
# See Package.checkPerm for package-specific contexts
|
||||||
@@ -95,26 +108,31 @@ class Permission(enum.Enum):
|
|||||||
raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
|
raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
|
||||||
|
|
||||||
class User(db.Model, UserMixin):
|
class User(db.Model, UserMixin):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
# User authentication information
|
# User authentication information
|
||||||
username = db.Column(db.String(50, collation="NOCASE"), nullable=False, unique=True, index=True)
|
username = db.Column(db.String(50, collation="NOCASE"), nullable=False, unique=True, index=True)
|
||||||
password = db.Column(db.String(255), nullable=True)
|
password = db.Column(db.String(255), nullable=True)
|
||||||
reset_password_token = db.Column(db.String(100), nullable=False, server_default="")
|
reset_password_token = db.Column(db.String(100), nullable=False, server_default="")
|
||||||
|
|
||||||
rank = db.Column(db.Enum(UserRank))
|
rank = db.Column(db.Enum(UserRank))
|
||||||
|
|
||||||
# Account linking
|
# Account linking
|
||||||
github_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
|
github_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
|
||||||
forums_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
|
forums_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
|
||||||
|
|
||||||
# User email information
|
# User email information
|
||||||
email = db.Column(db.String(255), nullable=True, unique=True)
|
email = db.Column(db.String(255), nullable=True, unique=True)
|
||||||
confirmed_at = db.Column(db.DateTime())
|
confirmed_at = db.Column(db.DateTime())
|
||||||
|
|
||||||
# User information
|
# User information
|
||||||
active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
|
profile_pic = db.Column(db.String(255), nullable=True, server_default=None)
|
||||||
display_name = db.Column(db.String(100), nullable=False, server_default="")
|
active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
|
||||||
|
display_name = db.Column(db.String(100), nullable=False, server_default="")
|
||||||
|
|
||||||
|
# Links
|
||||||
|
website_url = db.Column(db.String(255), nullable=True, default=None)
|
||||||
|
donate_url = db.Column(db.String(255), nullable=True, default=None)
|
||||||
|
|
||||||
# Content
|
# Content
|
||||||
notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id")
|
notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id")
|
||||||
@@ -126,8 +144,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
|
||||||
@@ -144,6 +160,12 @@ class User(db.Model, UserMixin):
|
|||||||
def isClaimed(self):
|
def isClaimed(self):
|
||||||
return self.rank.atLeast(UserRank.NEW_MEMBER)
|
return self.rank.atLeast(UserRank.NEW_MEMBER)
|
||||||
|
|
||||||
|
def getProfilePicURL(self):
|
||||||
|
if self.profile_pic:
|
||||||
|
return self.profile_pic
|
||||||
|
else:
|
||||||
|
return gravatar(self.email or "")
|
||||||
|
|
||||||
def checkPerm(self, user, perm):
|
def checkPerm(self, user, perm):
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
@@ -163,6 +185,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"))
|
||||||
@@ -230,7 +262,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"
|
||||||
@@ -327,18 +359,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])
|
||||||
@@ -371,7 +406,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")
|
||||||
@@ -387,36 +422,54 @@ class Package(db.Model):
|
|||||||
for e in PackagePropertyKey:
|
for e in PackagePropertyKey:
|
||||||
setattr(self, e.name, getattr(package, e.name))
|
setattr(self, e.name, getattr(package, e.name))
|
||||||
|
|
||||||
def getAsDictionaryShort(self, base_url):
|
def getState(self):
|
||||||
tnurl = self.getThumbnailURL()
|
if self.approved:
|
||||||
|
return "approved"
|
||||||
|
elif self.review_thread_id:
|
||||||
|
return "thread"
|
||||||
|
elif (self.type == PackageType.GAME or \
|
||||||
|
self.type == PackageType.TXP) and \
|
||||||
|
self.screenshots.count() == 0:
|
||||||
|
return "wip"
|
||||||
|
elif not self.getDownloadRelease():
|
||||||
|
return "wip"
|
||||||
|
elif "Other" in self.license.name or "Other" in self.media_license.name:
|
||||||
|
return "license"
|
||||||
|
else:
|
||||||
|
return "ready"
|
||||||
|
|
||||||
|
def getAsDictionaryShort(self, base_url, version=None, protonum=None):
|
||||||
|
tnurl = self.getThumbnailURL(1)
|
||||||
|
release = self.getDownloadRelease(version=version, protonum=protonum)
|
||||||
return {
|
return {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"author": self.author.display_name,
|
"author": self.author.display_name,
|
||||||
"shortDesc": self.shortDesc,
|
"short_description": self.short_desc,
|
||||||
"type": self.type.toName(),
|
"type": self.type.toName(),
|
||||||
"release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
|
"release": release and release.id,
|
||||||
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
|
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
|
||||||
"score": round(self.score * 10) / 10
|
"score": round(self.score * 10) / 10
|
||||||
}
|
}
|
||||||
|
|
||||||
def getAsDictionary(self, base_url):
|
def getAsDictionary(self, base_url, version=None, protonum=None):
|
||||||
tnurl = self.getThumbnailURL()
|
tnurl = self.getThumbnailURL(1)
|
||||||
|
release = self.getDownloadRelease(version=version, protonum=protonum)
|
||||||
return {
|
return {
|
||||||
"author": self.author.display_name,
|
"author": self.author.display_name,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"shortDesc": self.shortDesc,
|
"short_description": self.short_desc,
|
||||||
"desc": self.desc,
|
"desc": self.desc,
|
||||||
"type": self.type.toName(),
|
"type": self.type.toName(),
|
||||||
"createdAt": self.created_at,
|
"created_at": self.created_at,
|
||||||
|
|
||||||
"license": self.license.name,
|
"license": self.license.name,
|
||||||
"mediaLicense": self.media_license.name,
|
"media_license": self.media_license.name,
|
||||||
|
|
||||||
"repo": self.repo,
|
"repo": self.repo,
|
||||||
"website": self.website,
|
"website": self.website,
|
||||||
"issueTracker": self.issueTracker,
|
"issue_tracker": self.issueTracker,
|
||||||
"forums": self.forums,
|
"forums": self.forums,
|
||||||
|
|
||||||
"provides": [x.name for x in self.provides],
|
"provides": [x.name for x in self.provides],
|
||||||
@@ -424,17 +477,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": release and release.id,
|
||||||
|
|
||||||
"score": round(self.score * 10) / 10
|
"score": round(self.score * 10) / 10
|
||||||
}
|
}
|
||||||
|
|
||||||
def getThumbnailURL(self):
|
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() 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):
|
||||||
@@ -449,8 +502,8 @@ class Package(db.Model):
|
|||||||
return url_for("approve_package_page",
|
return url_for("approve_package_page",
|
||||||
author=self.author.username, name=self.name)
|
author=self.author.username, name=self.name)
|
||||||
|
|
||||||
def getDeleteURL(self):
|
def getRemoveURL(self):
|
||||||
return url_for("delete_package_page",
|
return url_for("remove_package_page",
|
||||||
author=self.author.username, name=self.name)
|
author=self.author.username, name=self.name)
|
||||||
|
|
||||||
def getNewScreenshotURL(self):
|
def getNewScreenshotURL(self):
|
||||||
@@ -465,17 +518,37 @@ 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, version=None, protonum=None):
|
||||||
|
if version is None and protonum is not None:
|
||||||
|
version = MinetestRelease.query.filter(MinetestRelease.protocol >= int(protonum)).first()
|
||||||
|
if version is not None:
|
||||||
|
version = version.id
|
||||||
|
else:
|
||||||
|
version = 10000000
|
||||||
|
|
||||||
|
|
||||||
for rel in self.releases:
|
for rel in self.releases:
|
||||||
if rel.approved:
|
if rel.approved and (version is None or
|
||||||
|
((rel.min_rel is None or rel.min_rel_id <= version) and \
|
||||||
|
(rel.max_rel is None or rel.max_rel_id >= version))):
|
||||||
return rel
|
return rel
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def getDownloadCount(self):
|
||||||
|
counter = 0
|
||||||
|
for release in self.releases:
|
||||||
|
counter += release.downloads
|
||||||
|
return counter
|
||||||
|
|
||||||
def checkPerm(self, user, perm):
|
def checkPerm(self, user, perm):
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
@@ -487,8 +560,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:
|
||||||
@@ -497,6 +573,10 @@ class Package(db.Model):
|
|||||||
else:
|
else:
|
||||||
return user.rank.atLeast(UserRank.EDITOR)
|
return user.rank.atLeast(UserRank.EDITOR)
|
||||||
|
|
||||||
|
# Anyone can change the package name when not approved, but only editors when approved
|
||||||
|
elif perm == Permission.CHANGE_NAME:
|
||||||
|
return not self.approved or user.rank.atLeast(UserRank.EDITOR)
|
||||||
|
|
||||||
# Editors can change authors and approve new packages
|
# Editors can change authors and approve new packages
|
||||||
elif perm == Permission.APPROVE_NEW or perm == Permission.CHANGE_AUTHOR:
|
elif perm == Permission.APPROVE_NEW or perm == Permission.CHANGE_AUTHOR:
|
||||||
return user.rank.atLeast(UserRank.EDITOR)
|
return user.rank.atLeast(UserRank.EDITOR)
|
||||||
@@ -505,26 +585,29 @@ class Package(db.Model):
|
|||||||
return user.rank.atLeast(UserRank.TRUSTED_MEMBER if isOwner else UserRank.EDITOR)
|
return user.rank.atLeast(UserRank.TRUSTED_MEMBER if isOwner else UserRank.EDITOR)
|
||||||
|
|
||||||
# Moderators can delete packages
|
# Moderators can delete packages
|
||||||
elif perm == Permission.DELETE_PACKAGE or perm == Permission.CHANGE_RELEASE_URL:
|
elif perm == Permission.DELETE_PACKAGE or perm == Permission.UNAPPROVE_PACKAGE \
|
||||||
|
or perm == Permission.CHANGE_RELEASE_URL:
|
||||||
return user.rank.atLeast(UserRank.MODERATOR)
|
return user.rank.atLeast(UserRank.MODERATOR)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
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 = 0
|
if self.forums is not None:
|
||||||
|
topic = ForumTopic.query.get(self.forums)
|
||||||
|
if topic:
|
||||||
|
days = (datetime.datetime.now() - topic.created_at).days
|
||||||
|
months = days / 30
|
||||||
|
years = days / 365
|
||||||
|
self.score = topic.views / max(years, 0.0416) + 80*min(max(months, 0.5), 6)
|
||||||
|
|
||||||
if self.forums is None:
|
if self.getMainScreenshotURL() is None:
|
||||||
return
|
self.score *= 0.8
|
||||||
|
|
||||||
topic = ForumTopic.query.get(self.forums)
|
if not self.license.is_foss or not self.media_license.is_foss:
|
||||||
if topic:
|
self.score *= 0.1
|
||||||
days = (datetime.datetime.now() - topic.created_at).days
|
|
||||||
months = days / 30
|
|
||||||
years = days / 365
|
|
||||||
self.score = topic.views / years + 80*min(6, months)
|
|
||||||
|
|
||||||
class MetaPackage(db.Model):
|
class MetaPackage(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -590,16 +673,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):
|
||||||
@@ -616,7 +719,17 @@ class PackageRelease(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.releaseDate = datetime.now()
|
self.releaseDate = datetime.datetime.now()
|
||||||
|
|
||||||
|
def approve(self, user):
|
||||||
|
if self.package.approved and \
|
||||||
|
not self.package.checkPerm(user, Permission.APPROVE_RELEASE):
|
||||||
|
return False
|
||||||
|
|
||||||
|
assert(self.task_id is None and self.url is not None and self.url != "")
|
||||||
|
|
||||||
|
self.approved = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class PackageReview(db.Model):
|
class PackageReview(db.Model):
|
||||||
@@ -640,8 +753,10 @@ class PackageScreenshot(db.Model):
|
|||||||
name=self.package.name,
|
name=self.package.name,
|
||||||
id=self.id)
|
id=self.id)
|
||||||
|
|
||||||
def getThumbnailURL(self):
|
def getThumbnailURL(self, level=2):
|
||||||
return self.url.replace("/uploads/", "/thumbnails/350x233/")
|
return self.url.replace("/uploads/", ("/thumbnails/{:d}/").format(level))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class EditRequest(db.Model):
|
class EditRequest(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -746,7 +861,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")
|
||||||
|
|
||||||
@@ -784,7 +899,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", \
|
||||||
@@ -798,6 +913,7 @@ class ForumTopic(db.Model):
|
|||||||
author = db.relationship("User")
|
author = db.relationship("User")
|
||||||
|
|
||||||
wip = db.Column(db.Boolean, server_default="0")
|
wip = db.Column(db.Boolean, server_default="0")
|
||||||
|
discarded = db.Column(db.Boolean, server_default="0")
|
||||||
|
|
||||||
type = db.Column(db.Enum(PackageType), nullable=False)
|
type = db.Column(db.Enum(PackageType), nullable=False)
|
||||||
title = db.Column(db.String(200), nullable=False)
|
title = db.Column(db.String(200), nullable=False)
|
||||||
@@ -807,7 +923,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:
|
||||||
@@ -830,9 +946,25 @@ class ForumTopic(db.Model):
|
|||||||
"posts": self.posts,
|
"posts": self.posts,
|
||||||
"views": self.views,
|
"views": self.views,
|
||||||
"is_wip": self.wip,
|
"is_wip": self.wip,
|
||||||
|
"discarded": self.discarded,
|
||||||
"created_at": self.created_at.isoformat(),
|
"created_at": self.created_at.isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def checkPerm(self, user, perm):
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if type(perm) == str:
|
||||||
|
perm = Permission[perm]
|
||||||
|
elif type(perm) != Permission:
|
||||||
|
raise Exception("Unknown permission given to ForumTopic.checkPerm()")
|
||||||
|
|
||||||
|
if perm == Permission.TOPIC_DISCARD:
|
||||||
|
return self.author == user or user.rank.atLeast(UserRank.EDITOR)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("Permission {} is not related to topics".format(perm.name))
|
||||||
|
|
||||||
|
|
||||||
# Setup Flask-User
|
# Setup Flask-User
|
||||||
db_adapter = SQLAlchemyAdapter(db, User) # Register the User model
|
db_adapter = SQLAlchemyAdapter(db, User) # Register the User model
|
||||||
|
|||||||
BIN
app/public/favicon-128.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
app/public/favicon-16.png
Normal file
|
After Width: | Height: | Size: 846 B |
BIN
app/public/favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
9681
app/public/static/bootstrap.css
vendored
Normal file
7
app/public/static/bootstrap.min.js
vendored
Normal file
7
app/public/static/easymde.min.css
vendored
Normal file
7
app/public/static/easymde.min.js
vendored
Normal file
|
Before Width: | Height: | Size: 260 B After Width: | Height: | Size: 159 B |
|
Before Width: | Height: | Size: 342 B After Width: | Height: | Size: 232 B |
|
Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 276 B After Width: | Height: | Size: 165 B |
|
Before Width: | Height: | Size: 275 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 340 B After Width: | Height: | Size: 231 B |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 4.0 KiB |
9
app/public/static/opensearch.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
||||||
|
<ShortName>ContentDB</ShortName>
|
||||||
|
<LongName>ContentDB</LongName>
|
||||||
|
<InputEncoding>UTF-8</InputEncoding>
|
||||||
|
<Description>Search mods, games, and textures for Minetest.</Description>
|
||||||
|
<Tags>Minetest Mod Game Subgame Search</Tags>
|
||||||
|
<Url type="text/html" method="get" template="https://content.minetest.net/packages?q={searchTerms}"/>
|
||||||
|
</OpenSearchDescription>
|
||||||
@@ -11,6 +11,8 @@ $(function() {
|
|||||||
|
|
||||||
$(".pkg_meta").hide()
|
$(".pkg_meta").hide()
|
||||||
$(".pkg_wiz_1").show()
|
$(".pkg_wiz_1").show()
|
||||||
|
|
||||||
|
$("#pkg_wiz_1_skip").click(finish)
|
||||||
$("#pkg_wiz_1_next").click(function() {
|
$("#pkg_wiz_1_next").click(function() {
|
||||||
const repoURL = $("#repo").val();
|
const repoURL = $("#repo").val();
|
||||||
if (repoURL.trim() != "") {
|
if (repoURL.trim() != "") {
|
||||||
@@ -18,41 +20,38 @@ $(function() {
|
|||||||
$(".pkg_wiz_2").show()
|
$(".pkg_wiz_2").show()
|
||||||
$(".pkg_repo").hide()
|
$(".pkg_repo").hide()
|
||||||
|
|
||||||
function setSpecial(id, value) {
|
function setField(id, value) {
|
||||||
if (value != "") {
|
if (value && value != "") {
|
||||||
var ele = $(id);
|
var ele = $(id);
|
||||||
ele.val(value);
|
ele.val(value);
|
||||||
ele.trigger("change")
|
ele.trigger("change");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
performTask("/tasks/getmeta/new/?url=" + encodeURI(repoURL)).then(function(result) {
|
performTask("/tasks/getmeta/new/?url=" + encodeURI(repoURL)).then(function(result) {
|
||||||
$("#name").val(result.name)
|
setField("#name", result.name);
|
||||||
setSpecial("#provides_str", result.provides)
|
setField("#provides_str", result.provides);
|
||||||
$("#title").val(result.title)
|
setField("#title", result.title);
|
||||||
$("#repo").val(result.repo || repoURL)
|
setField("#repo", result.repo || repoURL);
|
||||||
$("#issueTracker").val(result.issueTracker)
|
setField("#issueTracker", result.issueTracker);
|
||||||
$("#desc").val(result.description)
|
setField("#desc", result.description);
|
||||||
$("#shortDesc").val(result.short_description)
|
setField("#short_desc", result.short_description);
|
||||||
setSpecial("#harddep_str", result.depends)
|
setField("#harddep_str", result.depends);
|
||||||
setSpecial("#softdep_str", result.optional_depends)
|
setField("#softdep_str", result.optional_depends);
|
||||||
$("#shortDesc").val(result.short_description)
|
setField("#short_desc", result.short_description);
|
||||||
if (result.forumId) {
|
setField("#forums", result.forumId);
|
||||||
$("#forums").val(result.forumId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.type && result.type.length > 2) {
|
if (result.type && result.type.length > 2) {
|
||||||
$("#type").val(result.type)
|
$("#type").val(result.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
finish()
|
finish();
|
||||||
}).catch(function(e) {
|
}).catch(function(e) {
|
||||||
alert(e)
|
alert(e);
|
||||||
$(".pkg_wiz_1").show()
|
$(".pkg_wiz_1").show();
|
||||||
$(".pkg_wiz_2").hide()
|
$(".pkg_wiz_2").hide();
|
||||||
$(".pkg_repo").show()
|
$(".pkg_repo").show();
|
||||||
// finish()
|
// finish()
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,57 @@ $(function() {
|
|||||||
})
|
})
|
||||||
$(".not_mod, .not_game, .not_txp").show()
|
$(".not_mod, .not_game, .not_txp").show()
|
||||||
$(".not_" + $("#type").val().toLowerCase()).hide()
|
$(".not_" + $("#type").val().toLowerCase()).hide()
|
||||||
|
|
||||||
|
$("#forums").on('paste', function(e) {
|
||||||
|
try {
|
||||||
|
var pasteData = e.originalEvent.clipboardData.getData('text')
|
||||||
|
var url = new URL(pasteData);
|
||||||
|
if (url.hostname == "forum.minetest.net") {
|
||||||
|
$(this).val(url.searchParams.get("t"));
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Not a URL");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let hint = null;
|
||||||
|
function showHint(ele, text) {
|
||||||
|
if (hint) {
|
||||||
|
hint.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
hint = ele.parent()
|
||||||
|
.append(`<div class="alert alert-warning my-3">${text}</div>`)
|
||||||
|
.find(".alert");
|
||||||
|
}
|
||||||
|
|
||||||
|
let hint_mtmods = `Tip:
|
||||||
|
Don't include <i>Minetest</i>, <i>mod</i>, or <i>modpack</i> anywhere in the short description.
|
||||||
|
It is unnecessary and wastes characters.`;
|
||||||
|
|
||||||
|
let hint_thegame = `Tip:
|
||||||
|
It's obvious that this adds something to Minetest,
|
||||||
|
there's no need to use phrases such as \"adds X to the game\".`
|
||||||
|
|
||||||
|
$("#short_desc").on("change paste keyup", function() {
|
||||||
|
var val = $(this).val().toLowerCase();
|
||||||
|
if (val.indexOf("minetest") >= 0 || val.indexOf("mod") >= 0 ||
|
||||||
|
val.indexOf("modpack") >= 0 || val.indexOf("mod pack") >= 0) {
|
||||||
|
showHint($(this), hint_mtmods);
|
||||||
|
} else if (val.indexOf("the game") >= 0) {
|
||||||
|
showHint($(this), hint_thegame);
|
||||||
|
} else if (hint) {
|
||||||
|
hint.remove();
|
||||||
|
hint = null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var btn = $("#forums").parent().find("label").append("<a class='ml-3 btn btn-sm btn-primary'>Open</a>");
|
||||||
|
btn.click(function() {
|
||||||
|
var id = $("#forums").val();
|
||||||
|
if (/^\d+$/.test(id)) {
|
||||||
|
window.open("https://forum.minetest.net/viewtopic.php?t=" + id, "_blank");
|
||||||
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 4.6 KiB |
5
app/public/static/popper.min.js
vendored
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);
|
||||||
@@ -5,13 +5,25 @@
|
|||||||
* https://petprojects.googlecode.com/svn/trunk/GPL-LICENSE.txt
|
* https://petprojects.googlecode.com/svn/trunk/GPL-LICENSE.txt
|
||||||
*/
|
*/
|
||||||
(function($) {
|
(function($) {
|
||||||
|
function hide_error(input) {
|
||||||
|
var err = input.parent().parent().find(".invalid-remaining");
|
||||||
|
err.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_error(input, msg) {
|
||||||
|
var err = input.parent().parent().find(".invalid-remaining");
|
||||||
|
console.log(err.length);
|
||||||
|
err.text(msg);
|
||||||
|
err.show();
|
||||||
|
}
|
||||||
|
|
||||||
$.fn.selectSelector = function(source, name, select) {
|
$.fn.selectSelector = function(source, name, select) {
|
||||||
return this.each(function() {
|
return this.each(function() {
|
||||||
var selector = $(this),
|
var selector = $(this),
|
||||||
input = $('input[type=text]', this);
|
input = $('input[type=text]', this);
|
||||||
|
|
||||||
selector.click(function() { input.focus(); })
|
selector.click(function() { input.focus(); })
|
||||||
.delegate('.tag a', 'click', function() {
|
.delegate('.badge a', 'click', function() {
|
||||||
var id = $(this).parent().data("id");
|
var id = $(this).parent().data("id");
|
||||||
for (var i = 0; i < source.length; i++) {
|
for (var i = 0; i < source.length; i++) {
|
||||||
if (source[i].id == id) {
|
if (source[i].id == id) {
|
||||||
@@ -23,13 +35,14 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function addTag(item) {
|
function addTag(item) {
|
||||||
var tag = $('<span class="tag"/>')
|
var tag = $('<span class="badge badge-pill badge-primary"/>')
|
||||||
.text(item.toString() + ' ')
|
.text(item.toString() + ' ')
|
||||||
.data("id", item.id)
|
.data("id", item.id)
|
||||||
.append('<a>x</a>')
|
.append('<a>x</a>')
|
||||||
.insertBefore(input);
|
.insertBefore(input);
|
||||||
input.attr("placeholder", null);
|
input.attr("placeholder", null);
|
||||||
select.find("option[value=" + item.id + "]").attr("selected", "selected")
|
select.find("option[value=" + item.id + "]").attr("selected", "selected")
|
||||||
|
hide_error(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
function recreate() {
|
function recreate() {
|
||||||
@@ -42,6 +55,13 @@
|
|||||||
}
|
}
|
||||||
recreate();
|
recreate();
|
||||||
|
|
||||||
|
input.focusout(function(e) {
|
||||||
|
var value = input.val().trim()
|
||||||
|
if (value != "") {
|
||||||
|
show_error(input, "Please select an existing tag, it;s not possible to add custom ones.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
input.keydown(function(e) {
|
input.keydown(function(e) {
|
||||||
if (e.keyCode === $.ui.keyCode.TAB && $(this).data('ui-autocomplete').menu.active)
|
if (e.keyCode === $.ui.keyCode.TAB && $(this).data('ui-autocomplete').menu.active)
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -92,7 +112,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
selector.click(function() { input.focus(); })
|
selector.click(function() { input.focus(); })
|
||||||
.delegate('.tag a', 'click', function() {
|
.delegate('.badge a', 'click', function() {
|
||||||
var id = $(this).parent().data("id");
|
var id = $(this).parent().data("id");
|
||||||
for (var i = 0; i < selected.length; i++) {
|
for (var i = 0; i < selected.length; i++) {
|
||||||
if (selected[i] == id) {
|
if (selected[i] == id) {
|
||||||
@@ -113,13 +133,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addTag(id, value) {
|
function addTag(id, value) {
|
||||||
var tag = $('<span class="tag"/>')
|
var tag = $('<span class="badge badge-pill badge-primary"/>')
|
||||||
.text(value)
|
.text(value)
|
||||||
.data("id", id)
|
.data("id", id)
|
||||||
.append(' <a>x</a>')
|
.append(' <a>x</a>')
|
||||||
.insertBefore(input);
|
.insertBefore(input);
|
||||||
|
|
||||||
input.attr("placeholder", null);
|
input.attr("placeholder", null);
|
||||||
|
hide_error(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
function recreate() {
|
function recreate() {
|
||||||
@@ -147,6 +168,18 @@
|
|||||||
|
|
||||||
result.change(readFromResult);
|
result.change(readFromResult);
|
||||||
|
|
||||||
|
input.focusout(function() {
|
||||||
|
var item = input.val();
|
||||||
|
if (item.length == 0) {
|
||||||
|
input.data("ui-autocomplete").search("");
|
||||||
|
} else if (item.match(/^([a-z0-9_]+)$/)) {
|
||||||
|
selectItem(item);
|
||||||
|
recreate();
|
||||||
|
input.val("");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
input.keydown(function(e) {
|
input.keydown(function(e) {
|
||||||
if (e.keyCode === $.ui.keyCode.TAB && $(this).data('ui-autocomplete').menu.active)
|
if (e.keyCode === $.ui.keyCode.TAB && $(this).data('ui-autocomplete').menu.active)
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -159,7 +192,7 @@
|
|||||||
recreate();
|
recreate();
|
||||||
input.val("");
|
input.val("");
|
||||||
} else {
|
} else {
|
||||||
alert("Only lowercase alphanumeric and number names allowed.");
|
show_error(input, "Only lowercase alphanumeric and number names allowed.");
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
29
app/public/static/topic_discard.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
$(".topic-discard").click(function() {
|
||||||
|
var ele = $(this);
|
||||||
|
var tid = ele.attr("data-tid");
|
||||||
|
var discard = !ele.parent().parent().hasClass("discardtopic");
|
||||||
|
fetch(new Request("/api/topic_discard/?tid=" + tid +
|
||||||
|
"&discard=" + (discard ? "true" : "false"), {
|
||||||
|
method: "post",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"X-CSRFToken": csrf_token,
|
||||||
|
},
|
||||||
|
})).then(function(response) {
|
||||||
|
response.text().then(function(txt) {
|
||||||
|
console.log(JSON.parse(txt));
|
||||||
|
if (JSON.parse(txt).discarded) {
|
||||||
|
ele.parent().parent().addClass("discardtopic");
|
||||||
|
ele.removeClass("btn-danger");
|
||||||
|
ele.addClass("btn-success");
|
||||||
|
ele.text("Show");
|
||||||
|
} else {
|
||||||
|
ele.parent().parent().removeClass("discardtopic");
|
||||||
|
ele.removeClass("btn-success");
|
||||||
|
ele.addClass("btn-danger");
|
||||||
|
ele.text("Discard");
|
||||||
|
}
|
||||||
|
}).catch(console.log)
|
||||||
|
}).catch(console.log)
|
||||||
|
});
|
||||||
130
app/querybuilder.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease
|
||||||
|
from .utils import isNo, isYes
|
||||||
|
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")
|
||||||
|
self.order_dir = args.get("order") or "desc"
|
||||||
|
self.protocol_version = args.get("protocol_version")
|
||||||
|
|
||||||
|
self.show_discarded = isYes(args.get("show_discarded"))
|
||||||
|
self.show_added = args.get("show_added")
|
||||||
|
if self.show_added is not None:
|
||||||
|
self.show_added = isYes(self.show_added)
|
||||||
|
|
||||||
|
if self.search is not None and self.search.strip() == "":
|
||||||
|
self.search = None
|
||||||
|
|
||||||
|
def setSortIfNone(self, name):
|
||||||
|
if self.order_by is None:
|
||||||
|
self.order_by = name
|
||||||
|
|
||||||
|
def getMinetestVersion(self):
|
||||||
|
if not self.protocol_version:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.protocol_version = int(self.protocol_version)
|
||||||
|
version = MinetestRelease.query.filter(MinetestRelease.protocol>=self.protocol_version).first()
|
||||||
|
if version is not None:
|
||||||
|
return version.id
|
||||||
|
else:
|
||||||
|
return 10000000
|
||||||
|
|
||||||
|
def buildPackageQuery(self):
|
||||||
|
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 is None or 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:
|
||||||
|
version = self.getMinetestVersion()
|
||||||
|
query = query.join(Package.releases) \
|
||||||
|
.filter(PackageRelease.approved==True) \
|
||||||
|
.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, show_added=False):
|
||||||
|
query = ForumTopic.query
|
||||||
|
|
||||||
|
if not self.show_discarded:
|
||||||
|
query = query.filter_by(discarded=False)
|
||||||
|
|
||||||
|
show_added = self.show_added == True or (self.show_added is None and show_added)
|
||||||
|
if not show_added:
|
||||||
|
query = query.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id))
|
||||||
|
|
||||||
|
if self.order_by is None or self.order_by == "name":
|
||||||
|
query = query.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title))
|
||||||
|
elif self.order_by == "views":
|
||||||
|
query = query.order_by(db.desc(ForumTopic.views))
|
||||||
|
elif self.order_by == "date":
|
||||||
|
query = query.order_by(db.asc(ForumTopic.created_at))
|
||||||
|
sort_by = "date"
|
||||||
|
|
||||||
|
if self.search:
|
||||||
|
query = query.filter(ForumTopic.title.ilike('%' + self.search + '%'))
|
||||||
|
|
||||||
|
if len(self.types) > 0:
|
||||||
|
query = query.filter(ForumTopic.type.in_(self.types))
|
||||||
|
|
||||||
|
if self.limit:
|
||||||
|
query = query.limit(self.limit)
|
||||||
|
|
||||||
|
return query
|
||||||
@@ -1,49 +1,29 @@
|
|||||||
.comments, .comments li {
|
.img-thumbnail-1 {
|
||||||
list-style: none;
|
padding: 0px;
|
||||||
padding: 0;
|
// width: 100%;
|
||||||
margin: 0;
|
background: white;
|
||||||
border: 1px solid #444;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comments {
|
.comments {
|
||||||
border-radius: 5px;
|
list-style: none;
|
||||||
margin: 15px 0;
|
padding: 0;
|
||||||
background: #333;
|
|
||||||
|
|
||||||
.info_strip, .msg {
|
.card {
|
||||||
display: block;
|
position:relative;
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info_strip {
|
.card-header:before {
|
||||||
padding: 0.2em 1em;
|
position: absolute;
|
||||||
border-bottom: 1px solid #444;
|
top: 11px;
|
||||||
}
|
right: 100%;
|
||||||
|
width: 0;
|
||||||
.msg {
|
height: 0;
|
||||||
padding: 1em;
|
display: block;
|
||||||
background: #222;
|
content:" ";
|
||||||
}
|
border-color: transparent;
|
||||||
|
border-style: solid solid outset;
|
||||||
.author {
|
pointer-events:none;
|
||||||
font-weight: bold;
|
border-right-color: #444;
|
||||||
float: left;
|
border-width: 14px;
|
||||||
display: inline-block;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.info_strip span {
|
|
||||||
float: right;
|
|
||||||
display: inline-block;
|
|
||||||
color: #bbb;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment_form {
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment_form textarea {
|
|
||||||
min-width: 60%;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0 0 1em 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,112 +1,3 @@
|
|||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2, h3 {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #0be;
|
|
||||||
font-weight: bold;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #0df;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Containers */
|
|
||||||
|
|
||||||
.box {
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 15px 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box > h2, .box > h3 {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.5em 0.5em 0.5em 15px;
|
|
||||||
border-bottom: 1px solid #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box .box-body {
|
|
||||||
padding: 1em !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// .box form {
|
|
||||||
// padding: 1em;
|
|
||||||
// }
|
|
||||||
|
|
||||||
.box_grey {
|
|
||||||
background: #333;
|
|
||||||
border: 1px solid #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ul_boxes {
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ul_boxes > li {
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box_link {
|
|
||||||
display: block;
|
|
||||||
color: #ddd;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box_link:hover{
|
|
||||||
background: #3a3a3a;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
buttonset
|
|
||||||
*/
|
|
||||||
|
|
||||||
.buttonset, .buttonset li {
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonset {
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button, .buttonset li a, input[type=submit] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button, .buttonset li a, input[type=submit], input[type=text],
|
|
||||||
input[type=password], textarea, select, .bulletselector {
|
|
||||||
text-align: center;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.4em 1em;
|
|
||||||
background: rgba(255,255,255,0.07);
|
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
|
||||||
color: #ddd;
|
|
||||||
border-radius: 5px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
select > * {
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=text], input[type=password], textarea, select, .bulletselector {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-autocomplete, ui-front {
|
.ui-autocomplete, ui-front {
|
||||||
position:absolute;
|
position:absolute;
|
||||||
cursor:default;
|
cursor:default;
|
||||||
@@ -133,74 +24,11 @@ input[type=text], input[type=password], textarea, select, .bulletselector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
.bulletselector {
|
||||||
min-width: 200px;
|
height: auto !important;
|
||||||
|
display: inline-block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
select:not([multiple]) {
|
|
||||||
background: linear-gradient(#444, #333);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
vertical-align: top;
|
|
||||||
padding: 0 8px 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input, .form-group textarea, .form-group .bulletselector {
|
|
||||||
display: block;
|
|
||||||
min-width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box .form-group input, .box .form-group textarea, .form-group .bulletselector {
|
|
||||||
min-width: 95%;
|
|
||||||
max-width: 95%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover, .buttonset li a:hover, input[type=submit]:hover {
|
|
||||||
background: rgba(255,255,255,0.13);
|
|
||||||
border: 1px solid rgba(255,255,255,0.17);
|
|
||||||
text-decoration: none;
|
|
||||||
color: #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn_green {
|
|
||||||
background: #363 !important;
|
|
||||||
border: 1px solid #473;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn_green:hover {
|
|
||||||
background: #474 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.linedbuttonset a {
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
margin: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.linedbuttonset {
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.linedbuttonset li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 10px 10px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.bulletselector input {
|
.bulletselector input {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@@ -215,166 +43,14 @@ select:not([multiple]) {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
.bulletselector .tag {
|
.bulletselector .badge {
|
||||||
background: #375D81;
|
|
||||||
border-radius: 3px;
|
|
||||||
-moz-border-radius: 3px;
|
|
||||||
color: #FFF;
|
|
||||||
float: left;
|
float: left;
|
||||||
height: 15px;
|
padding: 0.4em 0.8em;
|
||||||
padding: 0.1em 0.4em 0.5em;
|
|
||||||
margin-right: 0.3em;
|
margin-right: 0.3em;
|
||||||
margin-bottom: 0.3em;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
.bulletselector .tag a {
|
|
||||||
color: #FFF;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.bulletselector .tag a:hover {
|
|
||||||
color: #0099CC;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invalid-remaining {
|
||||||
/* Alerts */
|
display: none;
|
||||||
|
|
||||||
|
|
||||||
.alert .alert_right, .alert > form {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert {
|
|
||||||
padding: 10px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.alert_right:not(.button) {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert_right form {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
input, .button {
|
|
||||||
margin: 0;
|
|
||||||
background: 0;
|
|
||||||
border: 0;
|
|
||||||
border-left: 1px solid rgba(255,255,255,0.12);
|
|
||||||
border-radius: 0;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:hover, .button:hover {
|
|
||||||
border: 0;
|
|
||||||
border-left: 1px solid rgba(255,255,255,0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#alerts {
|
|
||||||
list-style: none;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 15px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#alerts .alert {
|
|
||||||
margin: 5px 0;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
#alerts .close {
|
|
||||||
float: right;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#alerts .close:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-error, .button-danger {
|
|
||||||
background: #933 !important;
|
|
||||||
border: 1px solid #c44 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-warning {
|
|
||||||
background: #963;
|
|
||||||
border: 1px solid #c96;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-primary {
|
|
||||||
background: #339;
|
|
||||||
border: 1px solid #66a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-success {
|
|
||||||
background: #161;
|
|
||||||
border: 1px solid #393;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.fancyTable {
|
|
||||||
font-family: "Arial Black", Gadget, sans-serif;
|
|
||||||
border: 2px solid #000000;
|
|
||||||
background-color: #4A4A4A;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
table.fancyTable td, table.fancyTable th {
|
|
||||||
border: 1px solid #4A4A4A;
|
|
||||||
padding: 3px 2px;
|
|
||||||
}
|
|
||||||
table.fancyTable tbody td {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #E6E6E6;
|
|
||||||
}
|
|
||||||
table.fancyTable tr:nth-child(even) {
|
|
||||||
background: #888888;
|
|
||||||
}
|
|
||||||
table.fancyTable thead {
|
|
||||||
background: #000000;
|
|
||||||
border-bottom: 3px solid #000000;
|
|
||||||
}
|
|
||||||
table.fancyTable thead th {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #E6E6E6;
|
|
||||||
text-align: center;
|
|
||||||
border-left: 2px solid #4A4A4A;
|
|
||||||
}
|
|
||||||
table.fancyTable thead th:first-child {
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.fancyTable tfoot {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #E6E6E6;
|
|
||||||
background: #000000;
|
|
||||||
background: -moz-linear-gradient(top, #404040 0%, #191919 66%, #000000 100%);
|
|
||||||
background: -webkit-linear-gradient(top, #404040 0%, #191919 66%, #000000 100%);
|
|
||||||
background: linear-gradient(to bottom, #404040 0%, #191919 66%, #000000 100%);
|
|
||||||
border-top: 1px solid #4A4A4A;
|
|
||||||
}
|
|
||||||
table.fancyTable tfoot td {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.t-mll tr td:not(:first-child) {
|
.t-mll tr td:not(:first-child) {
|
||||||
@@ -405,57 +81,44 @@ table.fancyTable tfoot td {
|
|||||||
color: #2c2;
|
color: #2c2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
.wiptopic a:not(.btn) {
|
||||||
Aside
|
|
||||||
*/
|
|
||||||
|
|
||||||
.asideright {
|
|
||||||
float: right;
|
|
||||||
margin: 0 0 0 15px;
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.outsidecontainer {
|
|
||||||
position: absolute;
|
|
||||||
right: 102%;
|
|
||||||
top: 0;
|
|
||||||
width: intrinsic; /* Safari/WebKit uses a non-standard name */
|
|
||||||
width: -moz-max-content; /* Firefox/Gecko */
|
|
||||||
width: -webkit-max-content; /* Chrome */
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1490px) {
|
|
||||||
.outsidecontainer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.flatlist, .flatlist li {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flatlist li {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flatlist a {
|
|
||||||
display: block;
|
|
||||||
padding: 0.5em 20px;
|
|
||||||
color: #ddd;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flatlist a:hover {
|
|
||||||
background: #444;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-topalign td {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wiptopic a {
|
|
||||||
color: #7ac;
|
color: #7ac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.discardtopic {
|
||||||
|
text-decoration: line-through;
|
||||||
|
a:not(.btn) {
|
||||||
|
color: #7ac;
|
||||||
|
}
|
||||||
|
filter: brightness(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar, .editor-toolbar.fullscreen {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
background-color: #444 !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0 !important;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.125) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar button {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar button.active, .editor-toolbar button:hover {
|
||||||
|
background: #375a7f !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar.fullscreen::before, .editor-toolbar.fullscreen::after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .CodeMirror {
|
||||||
|
// background-color: #222 !important;
|
||||||
|
// }
|
||||||
|
|
||||||
|
.editor-preview-side, .editor-preview {
|
||||||
|
background-color: #222 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|||||||
57
app/scss/custom.scss
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
@import "components.scss";
|
||||||
|
@import "packages.scss";
|
||||||
|
@import "packagegrid.scss";
|
||||||
|
@import "comments.scss";
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown:hover .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link > img {
|
||||||
|
max-height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#alerts {
|
||||||
|
display: block;
|
||||||
|
list-style: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left:0;
|
||||||
|
right:0;
|
||||||
|
margin: 0;
|
||||||
|
padding:0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#alerts li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jumbotron {
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert .btn {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .table {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-download {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #00b05c;
|
||||||
|
border-color: #00b05c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-download:focus, .btn-download.focus {
|
||||||
|
-webkit-box-shadow: 0 0 0 0.2rem rgba(231, 76, 60, 0.5);
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(231, 76, 60, 0.5);
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
@import "page.scss";
|
|
||||||
@import "components.scss";
|
|
||||||
@import "nav.scss";
|
|
||||||
@import "packages.scss";
|
|
||||||
@import "packagegrid.scss";
|
|
||||||
@import "comments.scss";
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
nav {
|
|
||||||
margin: 0 auto 0 auto;
|
|
||||||
list-style: none;
|
|
||||||
background: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav .navbar-left {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav .navbar-left li {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav .navbar-right {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul {
|
|
||||||
margin: 0 auto 0 auto;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav li {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav li a {
|
|
||||||
color: #ddd;
|
|
||||||
margin: 0;
|
|
||||||
padding: 1em 1em;
|
|
||||||
display: block;
|
|
||||||
border-left: 1px solid #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav li a:not([href]) {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul li:last-child a {
|
|
||||||
border-right: 1px solid #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a:hover {
|
|
||||||
color: #eee;
|
|
||||||
background: #444;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav img {
|
|
||||||
height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
li.dropdown {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
min-width:160px;
|
|
||||||
background: #333;
|
|
||||||
z-index: 1;
|
|
||||||
right: 0;
|
|
||||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown:hover ul {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown li {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown li a {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown li:last-child a {
|
|
||||||
border-bottom: 1px solid #444;
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,20 @@
|
|||||||
.packagegrid {
|
.packagetile {
|
||||||
display: flex;
|
list-style: none;
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0 -7px;
|
margin: 0 7px 7px 0;
|
||||||
|
min-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.packagegrid li {
|
li.d-flex {
|
||||||
flex: 1;
|
list-style: none;
|
||||||
display: block;
|
|
||||||
min-width: 300px;
|
|
||||||
min-height: 200px;
|
|
||||||
max-width: 332px;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 7px;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.packagegrid a {
|
.packagetile a {
|
||||||
display: block;
|
display: block;
|
||||||
padding-bottom: 66.66%;
|
padding-bottom: 66.66%;
|
||||||
border-radius: 7px;
|
border-radius: 3px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
@@ -29,10 +22,6 @@
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.packagegrid a:hover {
|
|
||||||
// box-shadow: 0px 0px 16px 6px rgba(0,0,0,0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.packagegridscrub {
|
.packagegridscrub {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -53,6 +42,14 @@
|
|||||||
|
|
||||||
.packagegridinfo h3 {
|
.packagegridinfo h3 {
|
||||||
color: white;
|
color: white;
|
||||||
|
font-size: 120%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packagegridinfo small {
|
||||||
|
color: #ddd;
|
||||||
|
font-size: 75%;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.packagegridinfo p {
|
.packagegridinfo p {
|
||||||
@@ -61,15 +58,15 @@
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.packagegrid a:hover .packagegridinfo {
|
.packagetile a:hover .packagegridinfo {
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.packagegrid a:hover p {
|
.packagetile a:hover p {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.packagegrid a:hover .packagegridscrub {
|
.packagetile a:hover .packagegridscrub {
|
||||||
top: 0;
|
top: 0;
|
||||||
background: rgba(0,0,0,0.8);
|
background: rgba(0,0,0,0.8);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,34 +37,6 @@
|
|||||||
left: 15px;
|
left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar_container {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar_container .right, .sidebar_container .left{
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
top: 10px;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar_container .right {
|
|
||||||
right: 0;
|
|
||||||
width: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar_container .left {
|
|
||||||
right: 295px;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar_container .right > *:first-child, .sidebar_container .left > *:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.package-short-large {
|
.package-short-large {
|
||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
html, body {
|
|
||||||
font-family: "Arial", sans-serif;
|
|
||||||
background: #222;
|
|
||||||
color: #ddd;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container, main, #alerts, footer {
|
|
||||||
width: 90%;
|
|
||||||
max-width: 1024px;
|
|
||||||
margin: auto;
|
|
||||||
padding: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 7px 0;
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clearboth {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1, header p, header form {
|
|
||||||
padding: 0 5px;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
padding: 30px;
|
|
||||||
background: #258;
|
|
||||||
}
|
|
||||||
|
|
||||||
header p {
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header input {
|
|
||||||
margin: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
color: #999;
|
|
||||||
padding: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer a {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer a:hover {
|
|
||||||
color: #bbb;
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,9 @@
|
|||||||
|
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
|
from celery.schedules import crontab
|
||||||
from app import app
|
from app import app
|
||||||
from app.models import *
|
from app.models import *
|
||||||
|
|
||||||
@@ -64,4 +65,12 @@ def make_celery(app):
|
|||||||
|
|
||||||
celery = make_celery(app)
|
celery = make_celery(app)
|
||||||
|
|
||||||
|
CELERYBEAT_SCHEDULE = {
|
||||||
|
'topic_list_import': {
|
||||||
|
'task': 'app.tasks.forumtasks.importTopicList',
|
||||||
|
'schedule': crontab(minute=1, hour=1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
celery.conf.beat_schedule = CELERYBEAT_SCHEDULE
|
||||||
|
|
||||||
from . import importtasks, forumtasks, emails
|
from . import importtasks, forumtasks, emails
|
||||||
|
|||||||
@@ -15,13 +15,14 @@
|
|||||||
# 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
|
||||||
|
|
||||||
@celery.task()
|
@celery.task()
|
||||||
def sendVerifyEmail(newEmail, token):
|
def sendVerifyEmail(newEmail, token):
|
||||||
|
print("Sending verify email!")
|
||||||
msg = Message("Verify email address", recipients=[newEmail])
|
msg = Message("Verify email address", recipients=[newEmail])
|
||||||
msg.body = "This is a verification email!"
|
msg.body = "This is a verification email!"
|
||||||
msg.html = render_template("emails/verify.html", token=token)
|
msg.html = render_template("emails/verify.html", token=token)
|
||||||
@@ -31,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)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import flask, json, re
|
import flask, json, re
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from app import app
|
from app import app
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app.tasks import celery
|
from app.tasks import celery
|
||||||
@@ -25,7 +25,8 @@ import urllib.request
|
|||||||
from urllib.parse import urlparse, quote_plus
|
from urllib.parse import urlparse, quote_plus
|
||||||
|
|
||||||
@celery.task()
|
@celery.task()
|
||||||
def checkForumAccount(username, token=None):
|
def checkForumAccount(username, forceNoSave=False):
|
||||||
|
print("Checking " + username)
|
||||||
try:
|
try:
|
||||||
profile = getProfile("https://forum.minetest.net", username)
|
profile = getProfile("https://forum.minetest.net", username)
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -47,10 +48,34 @@ def checkForumAccount(username, token=None):
|
|||||||
user.github_username = github_username
|
user.github_username = github_username
|
||||||
needsSaving = True
|
needsSaving = True
|
||||||
|
|
||||||
|
pic = profile.avatar
|
||||||
|
if pic and "http" in pic:
|
||||||
|
pic = None
|
||||||
|
|
||||||
|
needsSaving = needsSaving or pic != user.profile_pic
|
||||||
|
if pic:
|
||||||
|
user.profile_pic = "https://forum.minetest.net/" + pic
|
||||||
|
else:
|
||||||
|
user.profile_pic = None
|
||||||
|
|
||||||
# Save
|
# Save
|
||||||
if needsSaving:
|
if needsSaving and not forceNoSave:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
return needsSaving
|
||||||
|
|
||||||
|
@celery.task()
|
||||||
|
def checkAllForumAccounts(forceNoSave=False):
|
||||||
|
needsSaving = False
|
||||||
|
query = User.query.filter(User.forums_username.isnot(None))
|
||||||
|
for user in query.all():
|
||||||
|
needsSaving = checkForumAccount(user.username) or needsSaving
|
||||||
|
|
||||||
|
if needsSaving and not forceNoSave:
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return needsSaving
|
||||||
|
|
||||||
|
|
||||||
regex_tag = re.compile(r"\[([a-z0-9_]+)\]")
|
regex_tag = re.compile(r"\[([a-z0-9_]+)\]")
|
||||||
BANNED_NAMES = ["mod", "game", "old", "outdated", "wip", "api", "beta", "alpha", "git"]
|
BANNED_NAMES = ["mod", "game", "old", "outdated", "wip", "api", "beta", "alpha", "git"]
|
||||||
@@ -74,11 +99,19 @@ def parseTitle(title):
|
|||||||
def getLinksFromModSearch():
|
def getLinksFromModSearch():
|
||||||
links = {}
|
links = {}
|
||||||
|
|
||||||
contents = urllib.request.urlopen("http://krock-works.16mb.com/MTstuff/modList.php").read().decode("utf-8")
|
try:
|
||||||
for x in json.loads(contents):
|
contents = urllib.request.urlopen("https://krock-works.uk.to/minetest/modList.php").read().decode("utf-8")
|
||||||
link = x.get("link")
|
for x in json.loads(contents):
|
||||||
if link is not None:
|
try:
|
||||||
links[int(x["topicId"])] = link
|
link = x.get("link")
|
||||||
|
if link is not None:
|
||||||
|
links[int(x["topicId"])] = link
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except urllib.error.URLError:
|
||||||
|
print("Unable to open krocks mod search!")
|
||||||
|
return links
|
||||||
|
|
||||||
return links
|
return links
|
||||||
|
|
||||||
@@ -127,15 +160,18 @@ def importTopicList():
|
|||||||
link = links_by_id.get(id)
|
link = links_by_id.get(id)
|
||||||
|
|
||||||
# Fill row
|
# Fill row
|
||||||
topic.topic_id = id
|
topic.topic_id = int(id)
|
||||||
topic.author = user
|
topic.author = user
|
||||||
topic.type = info["type"]
|
topic.type = info["type"]
|
||||||
topic.title = title
|
topic.title = title
|
||||||
topic.name = name
|
topic.name = name
|
||||||
topic.link = link
|
topic.link = link
|
||||||
topic.wip = info["wip"]
|
topic.wip = info["wip"]
|
||||||
topic.posts = info["posts"]
|
topic.posts = int(info["posts"])
|
||||||
topic.views = info["views"]
|
topic.views = int(info["views"])
|
||||||
topic.created_at = info["date"]
|
topic.created_at = info["date"]
|
||||||
|
|
||||||
|
for p in Package.query.all():
|
||||||
|
p.recalcScore()
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
import flask, json, os, git, tempfile, shutil
|
import flask, json, os, git, tempfile, shutil
|
||||||
from git import GitCommandError
|
from git import GitCommandError
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from urllib.error import HTTPError
|
from urllib.error import HTTPError
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from urllib.parse import urlparse, quote_plus, urlsplit
|
from urllib.parse import urlparse, quote_plus, urlsplit
|
||||||
@@ -29,6 +29,10 @@ from app.utils import randomString
|
|||||||
|
|
||||||
class GithubURLMaker:
|
class GithubURLMaker:
|
||||||
def __init__(self, url):
|
def __init__(self, url):
|
||||||
|
self.baseUrl = None
|
||||||
|
self.user = None
|
||||||
|
self.repo = None
|
||||||
|
|
||||||
# Rewrite path
|
# Rewrite path
|
||||||
import re
|
import re
|
||||||
m = re.search("^\/([^\/]+)\/([^\/]+)\/?$", url.path)
|
m = re.search("^\/([^\/]+)\/([^\/]+)\/?$", url.path)
|
||||||
@@ -51,6 +55,9 @@ class GithubURLMaker:
|
|||||||
def getScreenshotURL(self):
|
def getScreenshotURL(self):
|
||||||
return self.baseUrl + "/screenshot.png"
|
return self.baseUrl + "/screenshot.png"
|
||||||
|
|
||||||
|
def getModConfURL(self):
|
||||||
|
return self.baseUrl + "/mod.conf"
|
||||||
|
|
||||||
def getCommitsURL(self, branch):
|
def getCommitsURL(self, branch):
|
||||||
return "https://api.github.com/repos/{}/{}/commits?sha={}" \
|
return "https://api.github.com/repos/{}/{}/commits?sha={}" \
|
||||||
.format(self.user, self.repo, urllib.parse.quote_plus(branch))
|
.format(self.user, self.repo, urllib.parse.quote_plus(branch))
|
||||||
@@ -66,7 +73,7 @@ def getKrockList():
|
|||||||
global krock_list_cache_by_name
|
global krock_list_cache_by_name
|
||||||
|
|
||||||
if krock_list_cache is None:
|
if krock_list_cache is None:
|
||||||
contents = urllib.request.urlopen("http://krock-works.16mb.com/MTstuff/modList.php").read().decode("utf-8")
|
contents = urllib.request.urlopen("https://krock-works.uk.to/minetest/modList.php").read().decode("utf-8")
|
||||||
list = json.loads(contents)
|
list = json.loads(contents)
|
||||||
|
|
||||||
def h(x):
|
def h(x):
|
||||||
@@ -149,7 +156,8 @@ class PackageTreeNode:
|
|||||||
type = PackageType.GAME
|
type = PackageType.GAME
|
||||||
elif os.path.isfile(baseDir + "/init.lua"):
|
elif os.path.isfile(baseDir + "/init.lua"):
|
||||||
type = PackageType.MOD
|
type = PackageType.MOD
|
||||||
elif os.path.isfile(baseDir + "/modpack.txt"):
|
elif os.path.isfile(baseDir + "/modpack.txt") or \
|
||||||
|
os.path.isfile(baseDir + "/modpack.conf"):
|
||||||
type = PackageType.MOD
|
type = PackageType.MOD
|
||||||
is_modpack = True
|
is_modpack = True
|
||||||
elif os.path.isdir(baseDir + "/mods"):
|
elif os.path.isdir(baseDir + "/mods"):
|
||||||
@@ -338,8 +346,11 @@ def makeVCSReleaseFromGithub(id, branch, release, url):
|
|||||||
raise TaskError("Invalid github repo URL")
|
raise TaskError("Invalid github repo URL")
|
||||||
|
|
||||||
commitsURL = urlmaker.getCommitsURL(branch)
|
commitsURL = urlmaker.getCommitsURL(branch)
|
||||||
contents = urllib.request.urlopen(commitsURL).read().decode("utf-8")
|
try:
|
||||||
commits = json.loads(contents)
|
contents = urllib.request.urlopen(commitsURL).read().decode("utf-8")
|
||||||
|
commits = json.loads(contents)
|
||||||
|
except HTTPError:
|
||||||
|
raise TaskError("Unable to get commits for Github repository. Either the repository or reference doesn't exist.")
|
||||||
|
|
||||||
if len(commits) == 0 or not "sha" in commits[0]:
|
if len(commits) == 0 or not "sha" in commits[0]:
|
||||||
raise TaskError("No commits found")
|
raise TaskError("No commits found")
|
||||||
@@ -347,7 +358,7 @@ def makeVCSReleaseFromGithub(id, branch, release, url):
|
|||||||
release.url = urlmaker.getCommitDownload(commits[0]["sha"])
|
release.url = urlmaker.getCommitDownload(commits[0]["sha"])
|
||||||
release.task_id = None
|
release.task_id = None
|
||||||
release.commit_hash = commits[0]["sha"]
|
release.commit_hash = commits[0]["sha"]
|
||||||
print(release.url)
|
release.approve(release.package.author)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return release.url
|
return release.url
|
||||||
@@ -378,6 +389,7 @@ def makeVCSRelease(id, branch):
|
|||||||
release.url = "/uploads/" + filename
|
release.url = "/uploads/" + filename
|
||||||
release.task_id = None
|
release.task_id = None
|
||||||
release.commit_hash = repo.head.object.hexsha
|
release.commit_hash = repo.head.object.hexsha
|
||||||
|
release.approve(release.package.author)
|
||||||
print(release.url)
|
print(release.url)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ def urlEncodeNonAscii(b):
|
|||||||
|
|
||||||
class Profile:
|
class Profile:
|
||||||
def __init__(self, username):
|
def __init__(self, username):
|
||||||
self.username = username
|
self.username = username
|
||||||
self.signature = ""
|
self.signature = ""
|
||||||
|
self.avatar = None
|
||||||
self.properties = {}
|
self.properties = {}
|
||||||
|
|
||||||
def set(self, key, value):
|
def set(self, key, value):
|
||||||
@@ -33,6 +34,11 @@ def __extract_properties(profile, soup):
|
|||||||
if el is None:
|
if el is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
res1 = el.find_all("dl")
|
||||||
|
imgs = res1[0].find_all("img")
|
||||||
|
if len(imgs) == 1:
|
||||||
|
profile.avatar = imgs[0]["src"]
|
||||||
|
|
||||||
res = el.find_all("dl", class_ = "left-box details")
|
res = el.find_all("dl", class_ = "left-box details")
|
||||||
if len(res) != 1:
|
if len(res) != 1:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -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,18 +9,20 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<div class="box box_grey">
|
<div class="card my-4">
|
||||||
<h2>Do action</h2>
|
<h2 class="card-header">Do action</h2>
|
||||||
|
|
||||||
<form method="post" action="" class="box-body">
|
<form method="post" action="" class="card-body">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<select name="action">
|
<select name="action">
|
||||||
<option value="importmodlist" selected>Import forum topics</option>
|
<option value="importmodlist" selected>Import forum topics</option>
|
||||||
<option value="recalcscores">Recalculate package scores</option>
|
<option value="recalcscores">Recalculate package scores</option>
|
||||||
<!-- <option value="importscreenshots">Import screenshots from VCS</option> -->
|
<option value="checkusers">Check forum users</option>
|
||||||
|
<option value="importscreenshots">Import screenshots from VCS</option>
|
||||||
<!-- <option value="importdepends">Import dependencies from downloads</option> -->
|
<!-- <option value="importdepends">Import dependencies from downloads</option> -->
|
||||||
<!-- <option value="modprovides">Set provides to mod name</option> -->
|
<!-- <option value="modprovides">Set provides to mod name</option> -->
|
||||||
<!-- <option value="vcsrelease">Create VCS releases</option> -->
|
<!-- <option value="vcsrelease">Create VCS releases</option> -->
|
||||||
@@ -29,10 +31,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box box_grey">
|
<div class="card my-4">
|
||||||
<h2>Restore Package</h2>
|
<h2 class="card-header">Restore Package</h2>
|
||||||
|
|
||||||
<form method="post" action="" class="box-body">
|
<form method="post" action="" class="card-body">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<input type="hidden" name="action" value="restore" />
|
<input type="hidden" name="action" value="restore" />
|
||||||
<select name="package">
|
<select name="package">
|
||||||
|
|||||||
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
@@ -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 %}
|
||||||
@@ -6,75 +6,100 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
|
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
|
||||||
<link rel="stylesheet" type="text/css" href="/static/main.css">
|
<link rel="stylesheet" type="text/css" href="/static/bootstrap.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=7">
|
||||||
|
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
|
||||||
|
<link rel="shortcut icon" href="/favicon-16.png" sizes="16x16">
|
||||||
|
<link rel="icon" href="/favicon-128.png" sizes="128x128">
|
||||||
|
<link rel="icon" href="/favicon-32.png" sizes="32x32">
|
||||||
{% block headextra %}{% endblock %}
|
{% block headextra %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<ul class="nav navbar-nav navbar-left">
|
<a class="navbar-brand" href="/">{{ config.USER_APP_NAME }}</a>
|
||||||
<li><a href="/">{{ config.USER_APP_NAME }}</a></li>
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
{% for item in current_menu.children recursive %}
|
<span class="navbar-toggler-icon"></span>
|
||||||
{% if item.visible %}
|
</button>
|
||||||
<li{% if item.children %} class="dropdown"{% endif %}>
|
|
||||||
<a href="{{ item.url }}"
|
|
||||||
{% if item.children %}
|
|
||||||
class="dropdown-toggle"
|
|
||||||
data-toggle="dropdown"
|
|
||||||
role="button"
|
|
||||||
aria-expanded="false"
|
|
||||||
{% endif %}>
|
|
||||||
{{ item.text }}
|
|
||||||
{% if item.children %}
|
|
||||||
<span class="caret"></span>
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
{% if item.children %}
|
|
||||||
<ul class="dropdown-menu" role="menu">
|
|
||||||
{{ loop(item.children) }}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
<ul class="nav navbar-nav navbar-right">
|
|
||||||
{% if current_user.is_authenticated %}
|
|
||||||
<li><a href="{{ url_for('notifications_page') }}">
|
|
||||||
<img src="/static/notification{% if current_user.notifications %}_alert{% endif %}.svg" />
|
|
||||||
</a></li>
|
|
||||||
<li><a href="{{ url_for('create_edit_package_page') }}">+</a></li>
|
|
||||||
<li class="dropdown">
|
|
||||||
<a class="dropdown-toggle"
|
|
||||||
data-toggle="dropdown"
|
|
||||||
role="button"
|
|
||||||
aria-expanded="false">{{ current_user.display_name }}
|
|
||||||
<span class="caret"></span></a>
|
|
||||||
|
|
||||||
<ul class="dropdown-menu" role="menu">
|
<div class="collapse navbar-collapse" id="navbarColor01">
|
||||||
<li>
|
<ul class="navbar-nav mr-auto">
|
||||||
<a href="{{ url_for('user_profile_page', username=current_user.username) }}">Profile</a>
|
{% for item in current_menu.children recursive %}
|
||||||
|
{% if item.visible %}
|
||||||
|
<li class="nav-item {% if item.children %} dropdown{% endif %}">
|
||||||
|
<a class="nav-link" href="{{ item.url }}"
|
||||||
|
{% if item.children %}
|
||||||
|
class="dropdown-toggle"
|
||||||
|
data-toggle="dropdown"
|
||||||
|
role="button"
|
||||||
|
aria-expanded="false"
|
||||||
|
{% endif %}>
|
||||||
|
{{ item.text }}
|
||||||
|
{% if item.children %}
|
||||||
|
<span class="caret"></span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% if item.children %}
|
||||||
|
<ul class="dropdown-menu" role="menu">
|
||||||
|
{{ loop(item.children) }}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% if current_user.canAccessTodoList() %}
|
{% endif %}
|
||||||
<li><a href="{{ url_for('todo_page') }}">Work Queue</a></li>
|
{% endfor %}
|
||||||
<li><a href="{{ url_for('user_list_page') }}">User list</a></li>
|
</ul>
|
||||||
{% endif %}
|
<form class="form-inline my-2 my-lg-0" method="GET" action="/packages/">
|
||||||
{% if current_user.rank == current_user.rank.ADMIN %}
|
{% if type %}<input type="hidden" name="type" value="{{ type }}" />{% endif %}
|
||||||
<li><a href="{{ url_for('admin_page') }}">Admin</a></li>
|
<input class="form-control mr-sm-2" name="q" type="text" placeholder="Search {{ title | lower or 'all packages' }}" value="{{ query or ''}}">
|
||||||
{% endif %}
|
<input class="btn btn-secondary my-2 my-sm-0 mr-sm-2" type="submit" value="Search" />
|
||||||
{% if current_user.rank == current_user.rank.MODERATOR %}
|
<!-- <input class="btn btn-secondary my-2 my-sm-0"
|
||||||
<li><a href="{{ url_for('tag_list_page') }}">Tag Editor</a></li>
|
data-toggle="tooltip" data-placement="bottom"
|
||||||
<li><a href="{{ url_for('license_list_page') }}">License Editor</a></li>
|
title="Go to the first found result for this query."
|
||||||
{% endif %}
|
type="submit" name="lucky" value="First" /> -->
|
||||||
<li><a href="{{ url_for('user.logout') }}">Sign out</a></li>
|
</form>
|
||||||
</ul>
|
<ul class="navbar-nav ml-auto">
|
||||||
</li>
|
{% if current_user.is_authenticated %}
|
||||||
{% else %}
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('notifications_page') }}">
|
||||||
<li><a href="{{ url_for('user.login') }}">Sign in</a></li>
|
<img src="/static/notification{% if current_user.notifications %}_alert{% endif %}.svg" />
|
||||||
{% endif %}
|
</a></li>
|
||||||
</ul>
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('create_edit_package_page') }}">+</a></li>
|
||||||
<div class="clearboth"></div>
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle"
|
||||||
|
data-toggle="dropdown"
|
||||||
|
role="button"
|
||||||
|
aria-expanded="false">{{ current_user.display_name }}
|
||||||
|
<span class="caret"></span></a>
|
||||||
|
|
||||||
|
<ul class="dropdown-menu" role="menu">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('user_profile_page', username=current_user.username) }}">Profile</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('user_profile_page', username=current_user.username) }}#unadded-topics">Your unadded topics</a>
|
||||||
|
</li>
|
||||||
|
{% if current_user.canAccessTodoList() %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('todo_page') }}">{{ _("Work Queue") }}</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('user_list_page') }}">{{ _("User list") }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('todo_topics_page') }}">{{ _("All unadded topics") }}</a>
|
||||||
|
</li>
|
||||||
|
{% if current_user.rank == current_user.rank.ADMIN %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin_page') }}">{{ _("Admin") }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.rank == current_user.rank.MODERATOR %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('tag_list_page') }}">{{ _("Tag Editor") }}</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('license_list_page') }}">{{ _("License Editor") }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('user.logout') }}">{{ _("Sign out") }}</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li><a class="nav-link" href="{{ url_for('user.login') }}">{{ _("Sign in") }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -83,7 +108,7 @@
|
|||||||
{% if messages %}
|
{% if messages %}
|
||||||
<ul id="alerts">
|
<ul id="alerts">
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
<li class="box box_grey alert alert-{{category}}">
|
<li class="alert alert-{{category}} container">
|
||||||
<span class="icon_message"></span>
|
<span class="icon_message"></span>
|
||||||
|
|
||||||
{{ message|safe }}
|
{{ message|safe }}
|
||||||
@@ -97,16 +122,31 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block container %}
|
{% block container %}
|
||||||
<main>
|
<main class="container mt-4">
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<footer>
|
<footer class="container footer-copyright my-5 page-footer font-small text-center">
|
||||||
ContentDB © 2018 to <a href="https://rubenwardy.com/">rubenwardy</a> |
|
ContentDB © 2018-9 to <a href="https://rubenwardy.com/">rubenwardy</a> |
|
||||||
<a href="https://github.com/minetest/contentdb">GitHub</a> |
|
<a href="https://github.com/minetest/contentdb">GitHub</a> |
|
||||||
<a href="{{ url_for('flatpage', path='help') }}">Help</a> |
|
<a href="{{ url_for('flatpage', path='help') }}">{{ _("Help") }}</a> |
|
||||||
<a href="{{ url_for('flatpage', path='help/reporting') }}">Report / DMCA</a>
|
<a href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("Policy and Guidance") }}</a> |
|
||||||
|
<a href="{{ url_for('flatpage', path='help/reporting') }}">{{ _("Report / DMCA") }}</a> |
|
||||||
|
<a href="{{ url_for('user_list_page') }}">{{ _("User List") }}</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<script src="/static/jquery.min.js"></script>
|
||||||
|
<script src="/static/popper.min.js"></script>
|
||||||
|
<script src="/static/bootstrap.min.js"></script>
|
||||||
|
<script src="/static/easymde.min.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/easymde.min.css">
|
||||||
|
<script>
|
||||||
|
$("textarea.markdown").each(function() {
|
||||||
|
new EasyMDE({ element: this, hideIcons: ["image"], forceSync: true });
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% block scriptextra %}{% endblock %}
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
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 %}
|
||||||
|
|||||||
@@ -5,76 +5,76 @@ Sign in
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="sidebar_container">
|
<div class="row">
|
||||||
<div class="left box box_grey">
|
<div class="col-sm-8">
|
||||||
{% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %}
|
<div class="card">
|
||||||
<h2>{%trans%}Sign in{%endtrans%}</h2>
|
{% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %}
|
||||||
|
<h2 class="card-header">{%trans%}Sign in{%endtrans%}</h2>
|
||||||
|
|
||||||
<form action="" method="POST" class="form box-body" role="form">
|
<form action="" method="POST" class="form card-body" role="form">
|
||||||
<h3>Sign in with username/password</h3>
|
{{ form.hidden_tag() }}
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
|
|
||||||
{# Username or Email field #}
|
{# Username or Email field #}
|
||||||
{% set field = form.username if user_manager.enable_username else form.email %}
|
{% set field = form.username if user_manager.enable_username else form.email %}
|
||||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||||
{# Label on left, "New here? Register." on right #}
|
{# Label on left, "New here? Register." on right #}
|
||||||
<div class="row">
|
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}</label>
|
||||||
<div class="col-xs-6">
|
{{ field(class_='form-control', tabindex=110) }}
|
||||||
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}</label>
|
{% if field.errors %}
|
||||||
</div>
|
{% for e in field.errors %}
|
||||||
|
<p class="help-block">{{ e }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{{ field(class_='form-control', tabindex=110) }}
|
|
||||||
{% if field.errors %}
|
|
||||||
{% for e in field.errors %}
|
|
||||||
<p class="help-block">{{ e }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Password field #}
|
{# Password field #}
|
||||||
{% set field = form.password %}
|
{% set field = form.password %}
|
||||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||||
<div class="row">
|
|
||||||
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}
|
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}
|
||||||
{% if user_manager.enable_forgot_password %}
|
{% if user_manager.enable_forgot_password %}
|
||||||
<a href="{{ url_for('user.forgot_password') }}" tabindex='195'>
|
<a href="{{ url_for('user.forgot_password') }}" tabindex='195'>
|
||||||
[{%trans%}Forgot My Password{%endtrans%}]</a>
|
[{%trans%}Forgot My Password{%endtrans%}]</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</label>
|
</label>
|
||||||
|
{{ field(class_='form-control', tabindex=120) }}
|
||||||
|
{% if field.errors %}
|
||||||
|
{% for e in field.errors %}
|
||||||
|
<p class="help-block">{{ e }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{{ field(class_='form-control', tabindex=120) }}
|
|
||||||
{% if field.errors %}
|
{# Remember me #}
|
||||||
{% for e in field.errors %}
|
{% if user_manager.enable_remember_me %}
|
||||||
<p class="help-block">{{ e }}</p>
|
{{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{# Submit button #}
|
||||||
|
<p>
|
||||||
|
{{ render_submit_field(form.submit, tabindex=180) }}
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-4">
|
||||||
|
{% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %}
|
||||||
|
<h2 class="card-header">{%trans%}Sign in with Github{%endtrans%}</h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<a class="btn btn-primary" href="{{ url_for('github_signin_page') }}">GitHub</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{# Remember me #}
|
|
||||||
{% if user_manager.enable_remember_me %}
|
|
||||||
{{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# Submit button #}
|
|
||||||
<p>
|
|
||||||
{{ render_submit_field(form.submit, tabindex=180) }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>Sign in with Github</h3>
|
|
||||||
<p><a class="button" href="{{ url_for('github_signin_page') }}">GitHub</a></p>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right">
|
<aside class="col-sm-4">
|
||||||
<aside class="box box_grey">
|
<div class="card">
|
||||||
<h2>New here?</h2>
|
{% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %}
|
||||||
|
<h2 class="card-header">{%trans%}New here?{%endtrans%}</h2>
|
||||||
<div class="box-body">
|
<div class="card-body">
|
||||||
<p>Create an account using your forum account or email.</p>
|
<p>Create an account using your forum account or email.</p>
|
||||||
|
|
||||||
<a href="{{ url_for('user_claim_page') }}" class="button">{%trans%}Claim your account{%endtrans%}</a>
|
<a href="{{ url_for('user_claim_page') }}" class="btn btn-primary">{%trans%}Claim your account{%endtrans%}</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</div>
|
||||||
</div>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block container %}
|
{% block container %}
|
||||||
<main>
|
<main class="container mt-4">
|
||||||
<div class="box box_grey">
|
<div class="card">
|
||||||
<!-- <h2>{{ self.title() }}</h2> -->
|
<!-- <h2 class="card-header">{{ self.title() }}</h2> -->
|
||||||
<div class="box-body">
|
<div class="card-body">
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{{ page['title'] }}</h1>
|
{% if not page["no_h1"] %}<h1>{{ page['title'] }}</h1>{% endif %}
|
||||||
|
|
||||||
{{ page.html | safe }}
|
{{ page.html | safe }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,38 +1,73 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
Welcome
|
{{ _("Welcome") }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block container %}
|
{% block scriptextra %}
|
||||||
<header>
|
<script type="application/ld+json">
|
||||||
<div class="container">
|
{
|
||||||
<h1>Content DB</h1>
|
"@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 %}
|
||||||
|
|
||||||
<p>
|
{% block content %}
|
||||||
|
<!-- <header class="jumbotron">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="display-3">{{ config.USER_APP_NAME }}</h1>
|
||||||
|
|
||||||
|
<p class="lead">
|
||||||
Minetest's official content repository.
|
Minetest's official content repository.
|
||||||
Browse {{ count }} packages,
|
Browse {{ count }} packages,
|
||||||
majority of which available under a free and open source
|
the majority of which are available under a free
|
||||||
license.
|
and open source license.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form method="get" action="/packages/">
|
|
||||||
<input type="text" name="q" value="{{ query or ''}}" />
|
|
||||||
<input type="submit" value="Search" />
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main class="container"> -->
|
||||||
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
||||||
|
|
||||||
<h2>Popular</h2>
|
|
||||||
{{ render_pkggrid(popular) }}
|
|
||||||
|
|
||||||
<a href="{{ url_for('packages_page') }}" class="button">Show More</a>
|
<a href="{{ url_for('packages_page', sort='created_at', order='desc') }}" class="btn btn-secondary float-right">
|
||||||
|
{{ _("See more") }}
|
||||||
<h2 style="margin-top:2em;">Newly Added</h2>
|
</a>
|
||||||
|
<h2 class="my-3">{{ _("Recently Added") }}</h2>
|
||||||
{{ render_pkggrid(new) }}
|
{{ render_pkggrid(new) }}
|
||||||
|
|
||||||
</main>
|
|
||||||
|
<a href="{{ url_for('packages_page', type='mod', sort='score', order='desc') }}" class="btn btn-secondary float-right">
|
||||||
|
{{ _("See more") }}
|
||||||
|
</a>
|
||||||
|
<h2 class="my-3">{{ _("Top Mods") }}</h2>
|
||||||
|
{{ render_pkggrid(pop_mod) }}
|
||||||
|
|
||||||
|
|
||||||
|
<a href="{{ url_for('packages_page', type='game', sort='score', order='desc') }}" class="btn btn-secondary float-right">
|
||||||
|
{{ _("See more") }}
|
||||||
|
</a>
|
||||||
|
<h2 class="my-3">{{ _("Top Games") }}</h2>
|
||||||
|
{{ render_pkggrid(pop_gam) }}
|
||||||
|
|
||||||
|
|
||||||
|
<a href="{{ url_for('packages_page', type='txp', sort='score', order='desc') }}" class="btn btn-secondary float-right">
|
||||||
|
{{ _("See more") }}
|
||||||
|
</a>
|
||||||
|
<h2 class="my-3">{{ _("Top Texture Packs") }}</h2>
|
||||||
|
{{ render_pkggrid(pop_txp) }}
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<small>
|
||||||
|
{{ _("CDB has %(count)d packages, with a total of %(downloads)d downloads.", count=count, downloads=downloads) }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<!-- </main> -->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{% macro render_field(field, label=None, label_visible=true, right_url=None, right_label=None) -%}
|
{% macro render_field(field, label=None, label_visible=true, right_url=None, right_label=None, fieldclass=None) -%}
|
||||||
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
|
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
|
||||||
{% if field.type != 'HiddenField' and label_visible %}
|
{% if field.type != 'HiddenField' and label_visible %}
|
||||||
{% if not label %}{% set label=field.label.text %}{% endif %}
|
{% if not label and label != "" %}{% set label=field.label.text %}{% endif %}
|
||||||
<label for="{{ field.id }}" class="control-label">{{ label|safe }}</label>
|
{% if label %}<label for="{{ field.id }}">{{ label|safe }}</label>{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ field(class_='form-control', **kwargs) }}
|
{{ field(class_=fieldclass or 'form-control', **kwargs) }}
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
{% for e in field.errors %}
|
{% for e in field.errors %}
|
||||||
<p class="help-block">{{ e }}</p>
|
<p class="help-block">{{ e }}</p>
|
||||||
@@ -13,9 +13,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro form_includes() -%}
|
{% macro form_scripts() -%}
|
||||||
<link href="/static/jquery-ui.min.css" rel="stylesheet" type="text/css">
|
<link href="/static/jquery-ui.min.css" rel="stylesheet" type="text/css">
|
||||||
<script src="/static/jquery.min.js"></script>
|
|
||||||
<script src="/static/jquery-ui.min.js"></script>
|
<script src="/static/jquery-ui.min.js"></script>
|
||||||
<script src="/static/tagselector.js"></script>
|
<script src="/static/tagselector.js"></script>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
@@ -58,16 +57,17 @@
|
|||||||
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
|
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
|
||||||
{% if field.type != 'HiddenField' and label_visible %}
|
{% if field.type != 'HiddenField' and label_visible %}
|
||||||
{% if not label %}{% set label=field.label.text %}{% endif %}
|
{% if not label %}{% set label=field.label.text %}{% endif %}
|
||||||
<label for="{{ field.id }}" class="control-label">{{ label|safe }}</label>
|
<label for="{{ field.id }}">{{ label|safe }}</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="multichoice_selector bulletselector">
|
<div class="multichoice_selector bulletselector form-control">
|
||||||
<input type="text" placeholder="Start typing to see suggestions">
|
<input type="text" placeholder="Start typing to see suggestions">
|
||||||
<div class="clearboth"></div>
|
<div class="clearboth"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="invalid-remaining invalid-feedback"></div>
|
||||||
{{ field(class_='form-control', **kwargs) }}
|
{{ field(class_='form-control', **kwargs) }}
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
{% for e in field.errors %}
|
{% for e in field.errors %}
|
||||||
<p class="help-block">{{ e }}</p>
|
<div class="invalid-feedback">{{ e }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -77,13 +77,14 @@
|
|||||||
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
|
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
|
||||||
{% if field.type != 'HiddenField' and label_visible %}
|
{% if field.type != 'HiddenField' and label_visible %}
|
||||||
{% if not label %}{% set label=field.label.text %}{% endif %}
|
{% if not label %}{% set label=field.label.text %}{% endif %}
|
||||||
<label for="{{ field.id }}" class="control-label">{{ label|safe }}</label>
|
<label for="{{ field.id }}">{{ label|safe }}</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="metapackage_selector bulletselector">
|
<div class="metapackage_selector bulletselector form-control">
|
||||||
<input type="text" placeholder="Comma-seperated values">
|
<input type="text" placeholder="Comma-seperated values">
|
||||||
<div class="clearboth"></div>
|
<div class="clearboth"></div>
|
||||||
</div>
|
</div>
|
||||||
{{ field(class_='form-control', **kwargs) }}
|
{{ field(class_='form-control', **kwargs) }}
|
||||||
|
<div class="invalid-remaining invalid-feedback"></div>
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
{% for e in field.errors %}
|
{% for e in field.errors %}
|
||||||
<p class="help-block">{{ e }}</p>
|
<p class="help-block">{{ e }}</p>
|
||||||
@@ -96,13 +97,14 @@
|
|||||||
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
|
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
|
||||||
{% if field.type != 'HiddenField' and label_visible %}
|
{% if field.type != 'HiddenField' and label_visible %}
|
||||||
{% if not label %}{% set label=field.label.text %}{% endif %}
|
{% if not label %}{% set label=field.label.text %}{% endif %}
|
||||||
<label for="{{ field.id }}" class="control-label">{{ label|safe }}</label>
|
<label for="{{ field.id }}">{{ label|safe }}</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="deps_selector bulletselector">
|
<div class="deps_selector bulletselector form-control">
|
||||||
<input type="text" placeholder="Comma-seperated values">
|
<input type="text" placeholder="Comma-seperated values">
|
||||||
<div class="clearboth"></div>
|
<div class="clearboth"></div>
|
||||||
</div>
|
</div>
|
||||||
{{ field(class_='form-control', **kwargs) }}
|
{{ field(class_='form-control', **kwargs) }}
|
||||||
|
<div class="invalid-remaining invalid-feedback"></div>
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
{% for e in field.errors %}
|
{% for e in field.errors %}
|
||||||
<p class="help-block">{{ e }}</p>
|
<p class="help-block">{{ e }}</p>
|
||||||
@@ -113,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>
|
||||||
@@ -122,9 +124,9 @@
|
|||||||
|
|
||||||
{% macro render_radio_field(field) -%}
|
{% macro render_radio_field(field) -%}
|
||||||
{% for value, label, checked in field.iter_choices() %}
|
{% for value, label, checked in field.iter_choices() %}
|
||||||
<div class="radio">
|
<div class="form-check my-1">
|
||||||
<label>
|
<label class="form-check-label">
|
||||||
<input type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}"{% if checked %} checked{% endif %}>
|
<input class="form-check-input" type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}"{% if checked %} checked{% endif %}>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +136,7 @@
|
|||||||
{% macro render_submit_field(field, label=None, tabindex=None) -%}
|
{% macro render_submit_field(field, label=None, tabindex=None) -%}
|
||||||
{% if not label %}{% set label=field.label.text %}{% endif %}
|
{% if not label %}{% set label=field.label.text %}{% endif %}
|
||||||
{#<button type="submit" class="form-control btn btn-default btn-primary">{{label}}</button>#}
|
{#<button type="submit" class="form-control btn btn-default btn-primary">{{label}}</button>#}
|
||||||
<input type="submit" value="{{label}}"
|
<input type="submit" value="{{label}}" class="btn btn-primary"
|
||||||
{% if tabindex %}tabindex="{{ tabindex }}"{% endif %}
|
{% if tabindex %}tabindex="{{ tabindex }}"{% endif %}
|
||||||
>
|
>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
{% macro render_pkgtile(package, show_author) -%}
|
{% macro render_pkgtile(package, show_author) -%}
|
||||||
<li><a href="{{ package.getDetailsURL() }}"
|
<li class="packagetile flex-fill"><a href="{{ package.getDetailsURL() }}"
|
||||||
style="background-image: url({{ package.getThumbnailURL() or '/static/placeholder.png' }});">
|
style="background-image: url({{ package.getThumbnailURL() or '/static/placeholder.png' }});">
|
||||||
<div class="packagegridscrub"></div>
|
<div class="packagegridscrub"></div>
|
||||||
<div class="packagegridinfo">
|
<div class="packagegridinfo">
|
||||||
<h3>
|
<h3>
|
||||||
{{ package.title }}
|
{{ package.title }}
|
||||||
|
|
||||||
{% if show_author %}
|
{% if show_author %}<br />
|
||||||
by {{ package.author.display_name }}
|
<small>{{ package.author.display_name }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{ package.shortDesc }}
|
{{ package.short_desc }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
@@ -34,11 +34,14 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro render_pkggrid(packages, show_author=True) -%}
|
{% macro render_pkggrid(packages, show_author=True) -%}
|
||||||
<ul class="packagegrid">
|
<ul class="d-flex p-0 flex-row flex-wrap justify-content-start align-content-start ">
|
||||||
{% for p in packages %}
|
{% for p in packages %}
|
||||||
{{ render_pkgtile(p, show_author) }}
|
{{ render_pkgtile(p, show_author) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><i>No packages available</i></ul>
|
<li><i>No packages available</i></ul>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% for i in range(4) %}
|
||||||
|
<li class="packagetile flex-fill"></li>
|
||||||
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|||||||
@@ -1,36 +1,83 @@
|
|||||||
{% macro render_thread(thread, current_user) -%}
|
{% macro render_thread(thread, current_user) -%}
|
||||||
<ul class="comments">
|
|
||||||
{% for r in thread.replies %}
|
<ul class="comments mt-4 mb-0">
|
||||||
<li>
|
{% for r in thread.replies %}
|
||||||
<div class="info_strip">
|
<li class="row my-2 mx-0">
|
||||||
|
<div class="col-md-1 p-1">
|
||||||
|
<a href="{{ url_for('user_profile_page', username=r.author.username) }}">
|
||||||
|
<img class="img-responsive user-photo img-thumbnail img-thumbnail-1" src="{{ r.author.getProfilePicURL() }}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
<a class="author {{ r.author.rank.name }}"
|
<a class="author {{ r.author.rank.name }}"
|
||||||
href="{{ url_for('user_profile_page', username=r.author.username) }}">
|
href="{{ url_for('user_profile_page', username=r.author.username) }}">
|
||||||
{{ r.author.display_name }}</a>
|
{{ r.author.display_name }}
|
||||||
<span>{{ r.created_at | datetime }}</span>
|
</a>
|
||||||
<div class="clearboth"></div>
|
<a name="reply-{{ r.id }}" class="text-muted float-right"
|
||||||
|
href="{{ url_for('thread_page', id=thread.id) }}#reply-{{ r.id }}">
|
||||||
|
{{ r.created_at | datetime }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="msg">
|
|
||||||
|
<div class="card-body">
|
||||||
{{ r.comment | markdown }}
|
{{ r.comment | markdown }}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
</ul>
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<form method="post" action="{{ url_for('thread_page', id=thread.id)}}" class="comment_form">
|
<div class="row mt-0 mb-4 comments mx-0">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<div class="col-md-1 p-1">
|
||||||
<textarea required maxlength=500 name="comment" placeholder="Markdown supported"></textarea><br />
|
<img class="img-responsive user-photo img-thumbnail img-thumbnail-1" src="{{ current_user.getProfilePicURL() }}">
|
||||||
<input type="submit" value="Comment" />
|
</div>
|
||||||
</form>
|
<div class="col">
|
||||||
{% endif %}
|
<div class="card">
|
||||||
|
<div class="card-header {{ current_user.rank.name }}">
|
||||||
|
{{ current_user.display_name }}
|
||||||
|
<a name="reply"></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if current_user.canCommentRL() %}
|
||||||
|
<form method="post" action="{{ url_for('thread_page', id=thread.id)}}" class="card-body">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<textarea class="form-control markdown" required maxlength=500 name="comment"></textarea><br />
|
||||||
|
<input class="btn btn-primary" type="submit" value="Comment" />
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<div class="card-body">
|
||||||
|
<textarea class="form-control" readonly disabled>Please wait before commenting again.</textarea><br />
|
||||||
|
<input class="btn btn-primary" type="submit" disabled value="Comment" />
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro render_threadlist(threads) -%}
|
{% macro render_threadlist(threads, list_group=False) -%}
|
||||||
<ul>
|
{% if not list_group %}<ul>{% endif %}
|
||||||
{% for t in threads %}
|
{% for t in threads %}
|
||||||
<li>{% if t.private %}🔒 {% endif %}<a href="{{ url_for('thread_page', id=t.id) }}">{{ t.title }}</a> by {{ t.author.display_name }}</li>
|
<li {% if list_group %}class="list-group-item"{% endif %}>
|
||||||
|
{% if list_group %}
|
||||||
|
<a href="{{ url_for('thread_page', id=t.id) }}">
|
||||||
|
{% if t.private %}🔒 {% endif %}
|
||||||
|
{{ t.title }}
|
||||||
|
by {{ t.author.display_name }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
{% if t.private %}🔒 {% endif %}
|
||||||
|
<a href="{{ url_for('thread_page', id=t.id) }}">{{ t.title }}</a>
|
||||||
|
by {{ t.author.display_name }}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><i>No threads found</i></li>
|
<li {% if list_group %}class="list-group-item"{% endif %}><i>No threads found</i></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
{% if not list_group %}</ul>{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|||||||
67
app/templates/macros/topics.html
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{% macro render_topics_table(topics, show_author=True, show_discard=False, current_user=current_user) -%}
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Title</th>
|
||||||
|
{% if show_author %}<th>Author</th>{% endif %}
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
{% for topic in topics %}
|
||||||
|
<tr class="{% if topic.wip %}wiptopic{% endif %}{% if topic.discarded %}discardtopic{% endif %}">
|
||||||
|
<td>
|
||||||
|
[{{ topic.type.value }}]
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a>
|
||||||
|
{% if topic.wip %}[WIP]{% endif %}
|
||||||
|
</td>
|
||||||
|
{% if show_author %}
|
||||||
|
<td><a href="{{ url_for('user_profile_page', username=topic.author.username) }}">{{ topic.author.display_name}}</a></td>
|
||||||
|
{% endif %}
|
||||||
|
<td>{{ topic.name or ""}}</td>
|
||||||
|
<td>{{ topic.created_at | date }}</td>
|
||||||
|
<td class="btn-group">
|
||||||
|
{% if current_user == topic.author or topic.author.checkPerm(current_user, "CHANGE_AUTHOR") %}
|
||||||
|
<a class="btn btn-primary"
|
||||||
|
href="{{ url_for('create_edit_package_page', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">
|
||||||
|
Create
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if show_discard and current_user.is_authenticated and topic.checkPerm(current_user, "TOPIC_DISCARD") %}
|
||||||
|
<a class="btn btn-{% if topic.discarded %}success{% else %}danger{% endif %} topic-discard" data-tid={{ topic.topic_id }}>
|
||||||
|
{% if topic.discarded %}
|
||||||
|
Show
|
||||||
|
{% else %}
|
||||||
|
Discard
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if topic.link %}
|
||||||
|
<a class="btn btn-info" href="{{ topic.link }}">{{ topic.link | domain | truncate(18) }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro render_topics(topics, current_user, show_author=True) -%}
|
||||||
|
<ul>
|
||||||
|
{% for topic in topics %}
|
||||||
|
<li{% if topic.wip %} class="wiptopic"{% endif %}>
|
||||||
|
<a href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a>
|
||||||
|
{% if topic.wip %}[WIP]{% endif %}
|
||||||
|
{% if topic.name %}[{{ topic.name }}]{% endif %}
|
||||||
|
{% if show_author %}
|
||||||
|
by <a href="{{ url_for('user_profile_page', username=topic.author.username) }}">{{ topic.author.display_name }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if topic.author == current_user or topic.author.checkPerm(current_user, "CHANGE_AUTHOR") %}
|
||||||
|
| <a href="{{ url_for('create_edit_package_page', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">Create</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endmacro %}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{% macro render_topictable(topics, show_author=True) -%}
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>Id</th>
|
|
||||||
<th></th>
|
|
||||||
<th>Title</th>
|
|
||||||
{% if show_author %}<th>Author</th>{% endif %}
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Link</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
{% for topic in topics %}
|
|
||||||
<tr{% if topic.wip %} class="wiptopic"{% endif %}>
|
|
||||||
<td>{{ topic.topic_id }}</td>
|
|
||||||
<td>
|
|
||||||
[{{ topic.type.value }}]
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a>
|
|
||||||
{% if topic.wip %}[WIP]{% endif %}
|
|
||||||
</td>
|
|
||||||
{% if show_author %}
|
|
||||||
<td><a href="{{ url_for('user_profile_page', username=topic.author.username) }}">{{ topic.author.display_name}}</a></td>
|
|
||||||
{% endif %}
|
|
||||||
<td>{{ topic.name or ""}}</td>
|
|
||||||
<td>{% if topic.link %}<a href="{{ topic.link }}">{{ topic.link | domain }}</a>{% endif %}</td>
|
|
||||||
<td>
|
|
||||||
<a href="{{ url_for('create_edit_package_page', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">Create</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% endmacro %}
|
|
||||||
@@ -7,79 +7,99 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% from "macros/forms.html" import render_field, render_submit_field, form_scripts, render_multiselect_field, render_mpackage_field, render_deps_field, package_lists %}
|
||||||
|
|
||||||
|
{% block scriptextra %}
|
||||||
|
{{ form_scripts() }}
|
||||||
|
{% if enable_wizard %}
|
||||||
|
<script src="/static/url.min.js"></script>
|
||||||
|
<script src="/static/polltask.js"></script>
|
||||||
|
<script src="/static/package_create.js?v=3"></script>
|
||||||
|
{% endif %}
|
||||||
|
<script src="/static/package_edit.js?v=3"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Create Package</h1>
|
<h1>{{ _("Create Package") }}</h1>
|
||||||
|
|
||||||
<div class="box box_grey alert alert-info">
|
<div class="alert alert-info">
|
||||||
Have you read the Package Inclusion Policy and Guidance yet?
|
<a class="float-right btn btn-sm btn-default" href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("View") }}</a>
|
||||||
|
|
||||||
<a class="alert_right button" href="{{ url_for('flatpage', path='policy_and_guidance') }}">View</a>
|
{{ _("Have you read the Package Inclusion Policy and Guidance yet?") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
{{ _("Javascript is needed to improve the user interface, and is needed for features
|
||||||
|
such as finding metadata from git, and autocompletion.") }}<br />
|
||||||
|
{{ _("Whilst disabled Javascript may work, it is not officially supported.") }}
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field, form_includes, render_multiselect_field, render_mpackage_field, render_deps_field, package_lists %}
|
|
||||||
{{ form_includes() }}
|
|
||||||
{{ package_lists() }}
|
{{ package_lists() }}
|
||||||
|
|
||||||
<form method="POST" action="" class="tableform">
|
<form method="POST" action="" class="tableform">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
<h2 class="pkg_meta">Package</h2>
|
<fieldset>
|
||||||
|
<legend>{{ _("Package") }}</legend>
|
||||||
|
|
||||||
{{ render_field(form.type, class_="pkg_meta") }}
|
<div class="row">
|
||||||
{{ render_field(form.name, class_="pkg_meta") }}
|
{{ render_field(form.type, class_="pkg_meta col-sm-2") }}
|
||||||
{{ render_field(form.title, class_="pkg_meta") }}
|
{{ render_field(form.title, class_="pkg_meta col-sm-7") }}
|
||||||
{{ render_field(form.shortDesc, class_="pkg_meta") }}
|
{% if package and package.approved and not package.checkPerm(current_user, "CHANGE_NAME") %}
|
||||||
{{ render_field(form.desc, class_="pkg_meta") }}
|
{{ render_field(form.name, class_="pkg_meta col-sm-3", readonly=True) }}
|
||||||
{{ render_multiselect_field(form.tags, class_="pkg_meta") }}
|
{% else %}
|
||||||
<div class="pkg_meta">
|
{{ render_field(form.name, class_="pkg_meta col-sm-3") }}
|
||||||
{{ render_field(form.license, class_="not_txp") }}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{{ render_field(form.media_license, class_="pkg_meta") }}
|
{{ render_field(form.short_desc, class_="pkg_meta") }}
|
||||||
|
{{ render_multiselect_field(form.tags, class_="pkg_meta") }}
|
||||||
|
<div class="pkg_meta row">
|
||||||
|
{{ render_field(form.license, class_="not_txp col-sm-6") }}
|
||||||
|
{{ render_field(form.media_license, class_="col-sm-6") }}
|
||||||
|
</div>
|
||||||
|
<div class="pkg_meta row">
|
||||||
|
<div class="not_txp col-sm-6"></div>
|
||||||
|
<div class="not_txp col-sm-6">{{ _("If there is no media, set the Media License to the same as the License.") }}</div>
|
||||||
|
</div>
|
||||||
|
{{ render_field(form.desc, class_="pkg_meta", fieldclass="form-control markdown") }}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<div class="pkg_meta">
|
<fieldset class="pkg_meta">
|
||||||
<h2 class="not_txp">Dependency Info</h2>
|
<legend class="not_txp">{{ _("Dependencies") }}</legend>
|
||||||
|
|
||||||
{{ render_mpackage_field(form.provides_str, class_="not_txp", placeholder="Comma separated list") }}
|
{{ render_mpackage_field(form.provides_str, class_="not_txp", placeholder="Comma separated list") }}
|
||||||
{{ render_deps_field(form.harddep_str, class_="not_txp not_game", placeholder="Comma separated list") }}
|
{{ render_deps_field(form.harddep_str, class_="not_txp not_game", placeholder="Comma separated list") }}
|
||||||
{{ render_deps_field(form.softdep_str, class_="not_txp not_game", placeholder="Comma separated list") }}
|
{{ render_deps_field(form.softdep_str, class_="not_txp not_game", placeholder="Comma separated list") }}
|
||||||
</div>
|
</fieldset>
|
||||||
|
|
||||||
<h2 class="pkg_meta">Repository and Links</h2>
|
<fieldset>
|
||||||
|
<legend class="pkg_meta">{{ _("Repository and Links") }}</legend>
|
||||||
|
|
||||||
<div class="pkg_wiz_1">
|
<div class="pkg_wiz_1">
|
||||||
<p>Enter the repo URL for the package.
|
<p>{{ _("Enter the repo URL for the package.
|
||||||
If the repo uses git then the metadata will be automatically imported.</p>
|
If the repo uses git then the metadata will be automatically imported.") }}</p>
|
||||||
|
|
||||||
<p>Leave blank if you don't have a repo.</p>
|
<p>{{ _("Leave blank if you don't have a repo. Click skip if the import fails.") }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ render_field(form.repo, class_="pkg_repo") }}
|
{{ render_field(form.repo, class_="pkg_repo") }}
|
||||||
|
|
||||||
<div class="pkg_wiz_1">
|
<div class="pkg_wiz_1">
|
||||||
<a id="pkg_wiz_1_next" class="button button-primary">Next</a>
|
<a id="pkg_wiz_1_next" class="btn btn-primary">{{ _("Next (Autoimport)") }}</a>
|
||||||
</div>
|
<a id="pkg_wiz_1_skip" class="btn btn-default">{{ _("Skip Autoimport") }}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pkg_wiz_2">
|
<div class="pkg_wiz_2">
|
||||||
Importing... (This may take a while)
|
{{ _("Importing... (This may take a while)") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{ render_field(form.website, class_="pkg_meta") }}
|
||||||
|
{{ render_field(form.issueTracker, class_="pkg_meta") }}
|
||||||
|
{{ render_field(form.forums, class_="pkg_meta", placeholder=_("Tip: paste in a forum topic URL")) }}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
{{ render_field(form.website, class_="pkg_meta") }}
|
|
||||||
{{ render_field(form.issueTracker, class_="pkg_meta") }}
|
|
||||||
{{ render_field(form.forums, class_="pkg_meta") }}
|
|
||||||
<div class="pkg_meta">{{ render_submit_field(form.submit) }}</div>
|
<div class="pkg_meta">{{ render_submit_field(form.submit) }}</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if enable_wizard %}
|
|
||||||
<script src="/static/url.min.js"></script>
|
|
||||||
<script src="/static/polltask.js"></script>
|
|
||||||
<script src="/static/package_create.js"></script>
|
|
||||||
<noscript>
|
|
||||||
<div class="box box_grey alert alert-warning">
|
|
||||||
<span class="icon_message"></span>
|
|
||||||
Javascript is needed to automatically import metadata from VCS.
|
|
||||||
</div>
|
|
||||||
</noscript>
|
|
||||||
{% endif %}
|
|
||||||
<script src="/static/package_edit.js"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -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) }}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
This edit request was merged.
|
This edit request was merged.
|
||||||
</div>
|
</div>
|
||||||
{% elif request.status == 2 %}
|
{% elif request.status == 2 %}
|
||||||
<div class="box box_grey alert alert-error">
|
<div class="box box_grey alert alert-danger">
|
||||||
This edit request was rejected.
|
This edit request was rejected.
|
||||||
</div>
|
</div>
|
||||||
{% elif package.checkPerm(current_user, "APPROVE_CHANGES") %}
|
{% elif package.checkPerm(current_user, "APPROVE_CHANGES") %}
|
||||||
|
|||||||
@@ -5,36 +5,31 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="get" action="" class="plsearchform">
|
|
||||||
{% if type %}<input type="hidden" name="type" value="{{ type }}" />{% endif %}
|
|
||||||
<input type="text" name="q" value="{{ query or ''}}" />
|
|
||||||
<input type="submit" value="Search" />
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Found {{ packages_count }} packages.
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!--<aside class="box box_grey outsidecontainer">
|
|
||||||
<h3>Tags</h3>
|
|
||||||
|
|
||||||
<ul class="flatlist">
|
|
||||||
{% for t in tags %}
|
|
||||||
<li><a href="{{ url_for('packages_page', q=(query or '')+' tag:'+t.name, type=type) }}">
|
|
||||||
{{ t.title }}
|
|
||||||
</a></li>
|
|
||||||
{% else %}
|
|
||||||
<li><i>No tags available</i></ul>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</aside> -->
|
|
||||||
|
|
||||||
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
||||||
{{ render_pkggrid(packages) }}
|
{{ render_pkggrid(packages) }}
|
||||||
|
|
||||||
<ul class="buttonset linedbuttonset">
|
<ul class="pagination mt-4">
|
||||||
{% if prev_url %}<li><a href="{{ prev_url }}">Previous</a></li>{% endif %}
|
<li class="page-item {% if not prev_url %}disabled{% endif %}">
|
||||||
<li>{{ page }} / {{ page_max }}</li>
|
<a class="page-link" {% if prev_url %}href="{{ prev_url }}"{% endif %}>«</a>
|
||||||
{% if next_url %}<li><a href="{{ next_url }}">Next</a></li> {% endif %}
|
</li>
|
||||||
|
{% for n in range(1, page_max+1) %}
|
||||||
|
<li class="page-item {% if n == page %}active{% endif %}">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{{ url_for('packages_page', type=type, q=query, page=n) }}">
|
||||||
|
{{ n }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
<li class="page-item {% if not next_url %}disabled{% endif %}">
|
||||||
|
<a class="page-link" {% if next_url %}href="{{ next_url }}"{% endif %}>»</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{% if topics %}
|
||||||
|
<h2 style="margin-top:2em;">More content from the forums</h2>
|
||||||
|
|
||||||
|
{% from "macros/topics.html" import render_topics %}
|
||||||
|
{{ render_topics(topics, current_user) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
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,11 +1,11 @@
|
|||||||
{% 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 %}
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}
|
||||||
<form method="POST" action="">
|
<form method="POST" action="">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
@@ -34,12 +34,33 @@
|
|||||||
<br />
|
<br />
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if package.checkPerm(current_user, "APPROVE_RELEASE") %}
|
{% if package.checkPerm(current_user, "APPROVE_RELEASE") %}
|
||||||
{{ render_field(form.approved) }}
|
{{ render_checkbox_field(form.approved, class_="my-3") }}
|
||||||
{% else %}
|
{% else %}
|
||||||
Approved: {{ release.approved }}
|
Approved: {{ release.approved }}
|
||||||
{% 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 %}
|
||||||
|
|||||||
@@ -5,16 +5,54 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
{% from "macros/forms.html" import render_field, render_submit_field, render_radio_field %}
|
||||||
<form method="POST" action="" enctype="multipart/form-data">
|
<form method="POST" action="" enctype="multipart/form-data">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
{{ render_field(form.title, placeholder="Human readable. Eg: 1.0.0 or 2018-05-28") }}
|
{{ render_field(form.title, placeholder="Human readable. Eg: 1.0.0 or 2018-05-28") }}
|
||||||
{{ render_field(form.uploadOpt) }}
|
<p class="mb-0">Method</p>
|
||||||
|
{{ render_radio_field(form.uploadOpt) }}
|
||||||
|
|
||||||
{% if package.repo %}
|
{% if package.repo %}
|
||||||
{{ render_field(form.vcsLabel) }}
|
{{ render_field(form.vcsLabel, class_="mt-3") }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ render_field(form.fileUpload) }}
|
|
||||||
|
{{ render_field(form.fileUpload, fieldclass="form-control-file", class_="mt-3", accept=".zip") }}
|
||||||
|
|
||||||
|
<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 %}
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="POST" action="" class="box box_grey ">
|
<form method="POST" action="" class="box box_grey ">
|
||||||
<h3>Delete Package</h3>
|
<h3>Remove Package</h3>
|
||||||
|
|
||||||
<div class="box-body">
|
<div class="box-body">
|
||||||
<p>This action can be undone by the admin, but he'll be very annoyed!</p>
|
<p>Deleting a package can be undone by the admin, but he'll be very annoyed!</p>
|
||||||
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<input type="submit" value="Delete" class="button-danger" />
|
<input type="submit" name="delete" value="Delete" class="btn btn-danger" />
|
||||||
|
<input type="submit" name="unapprove" value="Unapprove" class="btn btn-warning" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -9,15 +9,15 @@
|
|||||||
<img src="{{ screenshot.getThumbnailURL() }}" alt="{{ screenshot.title }}" />
|
<img src="{{ screenshot.getThumbnailURL() }}" alt="{{ screenshot.title }}" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% from "macros/forms.html" import render_field, render_submit_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() }}
|
||||||
|
|
||||||
{{ render_field(form.title) }}
|
{{ render_field(form.title) }}
|
||||||
{{ render_field(form.delete) }}
|
{{ render_checkbox_field(form.delete) }}
|
||||||
|
|
||||||
{% if package.checkPerm(current_user, "APPROVE_SCREENSHOT") %}
|
{% if package.checkPerm(current_user, "APPROVE_SCREENSHOT") %}
|
||||||
{{ render_field(form.approved) }}
|
{{ render_checkbox_field(form.approved) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>Approved: {{ screenshot.approved }}</p>
|
<p>Approved: {{ screenshot.approved }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
{{ render_field(form.title) }}
|
{{ render_field(form.title) }}
|
||||||
{{ render_field(form.fileUpload) }}
|
{{ render_field(form.fileUpload, fieldclass="form-control-file", accept="image/png,image/jpeg") }}
|
||||||
{{ render_submit_field(form.submit) }}
|
{{ render_submit_field(form.submit) }}
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,64 +1,344 @@
|
|||||||
|
{% set query=package.name %}
|
||||||
|
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{{ package.title }}
|
{{ package.title }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block container %}
|
||||||
{% if not package.approved %}
|
<header class="jumbotron pb-3"
|
||||||
<div class="box box_grey alert alert-warning">
|
style="background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url('{{ package.getMainScreenshotURL() }}');
|
||||||
<span class="icon_message"></span>
|
background-size: cover;
|
||||||
{% if package.releases.count() == 0 %}
|
background-repeat: no-repeat;
|
||||||
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
|
background-position: center;">
|
||||||
You need to create a release before this package can be approved.
|
<div class="container">
|
||||||
<p>
|
<h1 class="display-3">
|
||||||
A release is a single downloadable version of your {{ package.type.value | lower }}.
|
{{ package.title }}
|
||||||
You need to create releases even if you use a rolling release development cycle,
|
<small>by {{ package.author.display_name }}</small>
|
||||||
as Minetest needs them to check for updates.
|
</h1>
|
||||||
</p>
|
|
||||||
<a class="button" href="{{ package.getCreateReleaseURL() }}">Create Release</a>
|
|
||||||
{% else %}
|
|
||||||
A release is required before this package can be approved.
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% elif (package.type == package.type.GAME or package.type == package.type.TXP) and package.screenshots.count() == 0 %}
|
<p class="lead">
|
||||||
You need to add at least one screenshot.
|
{{ package.short_desc }}
|
||||||
|
</p>
|
||||||
|
|
||||||
{% elif topic_error_lvl == "error" %}
|
<div class="row" style="margin-top: 2rem;">
|
||||||
Please fix the below topic issue(s).
|
<div class="col">
|
||||||
|
{{ package.getDownloadCount() }} downloads
|
||||||
{% elif "Other" in package.license.name or "Other" in package.media_license.name %}
|
</div>
|
||||||
Please wait for the license to be added to CDB.
|
<div class="btn-group-horizontal col-md-auto">
|
||||||
|
{% if package.repo %}<a class="btn btn-secondary" href="{{ package.repo }}">View Source</a>{% endif %}
|
||||||
{% else %}
|
{% if package.forums %}<a class="btn btn-secondary" href="https://forum.minetest.net/viewtopic.php?t={{ package.forums }}">Forums</a>{% endif %}
|
||||||
{% if package.screenshots.count() == 0 %}
|
{% if package.issueTracker %}<a class="btn btn-secondary" href="{{ package.issueTracker }}">Issue Tracker</a>{% endif %}
|
||||||
<b>You should add at least one screenshot, but this isn't required.</b><br />
|
{% if package.website %}<a class="btn btn-secondary" href="{{ package.website }}">Website</a>{% endif %}
|
||||||
{% endif %}
|
</div>
|
||||||
|
</div>
|
||||||
{% if not package.getDownloadRelease() %}
|
|
||||||
Please wait for the release to be approved.
|
|
||||||
{% elif package.checkPerm(current_user, "APPROVE_NEW") %}
|
|
||||||
You can now approve this package if you're ready.
|
|
||||||
<form method="post" action="{{ package.getApproveURL() }}">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
<input type="submit" value="Approve" />
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
Please wait for the package to be approved.
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
<div style="clear: both;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
{% if topic_error %}
|
<main class="container mt-4">
|
||||||
<div class="box box_grey alert alert-{{ topic_error_lvl }}">
|
{% if not package.approved %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
<span class="icon_message"></span>
|
<span class="icon_message"></span>
|
||||||
{{ topic_error | safe }}
|
{% if package.releases.count() == 0 %}
|
||||||
|
<h4 class="alert-heading">Release Required</h4>
|
||||||
|
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
|
||||||
|
<p>You need to create a release before this package can be approved.</p>
|
||||||
|
<p>
|
||||||
|
A release is a single downloadable version of your {{ package.type.value | lower }}.
|
||||||
|
You need to create releases even if you use a rolling release development cycle,
|
||||||
|
as Minetest needs them to check for updates.
|
||||||
|
</p>
|
||||||
|
<a class="btn" href="{{ package.getCreateReleaseURL() }}">Create Release</a>
|
||||||
|
{% else %}
|
||||||
|
A release is required before this package can be approved.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% elif (package.type == package.type.GAME or package.type == package.type.TXP) and package.screenshots.count() == 0 %}
|
||||||
|
You need to add at least one screenshot.
|
||||||
|
|
||||||
|
{% elif topic_error_lvl == "danger" %}
|
||||||
|
Please fix the below topic issue(s).
|
||||||
|
|
||||||
|
{% elif "Other" in package.license.name or "Other" in package.media_license.name %}
|
||||||
|
Please wait for the license to be added to CDB.
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
{% if package.screenshots.count() == 0 %}
|
||||||
|
<b>You should add at least one screenshot, but this isn't required.</b><br />
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not package.getDownloadRelease() %}
|
||||||
|
Please wait for the release to be approved.
|
||||||
|
{% elif package.checkPerm(current_user, "APPROVE_NEW") %}
|
||||||
|
<form class="float-right" method="post" action="{{ package.getApproveURL() }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<input class="btn btn-sm btn-warning" type="submit" value="Approve" />
|
||||||
|
</form>
|
||||||
|
You can now approve this package if you're ready.
|
||||||
|
{% else %}
|
||||||
|
Please wait for the package to be approved.
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
<div style="clear: both;"></div>
|
<div style="clear: both;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if topic_error %}
|
||||||
|
<div class="alert alert-{{ topic_error_lvl }}">
|
||||||
|
<span class="icon_message"></span>
|
||||||
|
{{ topic_error | safe }}
|
||||||
|
<div style="clear: both;"></div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if similar_topics %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
Please make sure that this package has the right to
|
||||||
|
the name '{{ package.name }}'.
|
||||||
|
See the
|
||||||
|
<a href="/policy_and_guidance/">Inclusion Policy</a>
|
||||||
|
for more info.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not review_thread and (package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW")) %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<a class="float-right btn btn-sm btn-info" href="{{ url_for('new_thread_page', pid=package.id, title='Package approval comments') }}">Open Thread</a>
|
||||||
|
|
||||||
|
Privately ask a question or give feedback
|
||||||
|
<div style="clear:both;"></div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW") %}
|
<aside class="float-right ml-4" style="width: 18rem;">
|
||||||
|
{% set release = package.getDownloadRelease() %}
|
||||||
|
{% if release %}
|
||||||
|
<a class="btn btn-download btn-lg btn-block" rel="nofollow"
|
||||||
|
href="{{ package.getDownloadURL() }}" class="btn_green">
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p class="text-center m-2" style="font-size: 80%;">
|
||||||
|
{% if release.min_rel and release.max_rel %}
|
||||||
|
Minetest {{ release.min_rel.name }} - {{ release.max_rel.name }}
|
||||||
|
{% elif release.min_rel %}
|
||||||
|
Supports Minetest {{ release.min_rel.name }} and above.
|
||||||
|
{% elif release.max_rel %}
|
||||||
|
Supports Minetest {{ release.max_rel.name }} and below.
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
No download available.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card my-4">
|
||||||
|
<div class="card-header">
|
||||||
|
Details
|
||||||
|
<div class="btn-group float-right">
|
||||||
|
{% if package.checkPerm(current_user, "EDIT_PACKAGE") %}
|
||||||
|
<a class="btn btn-default btn-sm mx-1" href="{{ package.getEditURL() }}">Edit</a>
|
||||||
|
{% endif %}
|
||||||
|
{# {% if current_user.is_authenticated %}
|
||||||
|
<a class="btn btn-default btn-sm mx-1" href="{{ package.getCreateEditRequestURL() }}">Suggest Changes</a>
|
||||||
|
{% endif %} #}
|
||||||
|
{% if package.checkPerm(current_user, "DELETE_PACKAGE") or package.checkPerm(current_user, "UNAPPROVE_PACKAGE") %}
|
||||||
|
<a class="btn btn-danger btn-sm mx-1" href="{{ package.getRemoveURL() }}">Remove</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% if not package.license.is_foss and not package.media_license.is_foss and package.type != package.type.TXP %}
|
||||||
|
{% set package_warning="Non-free code and media." %}
|
||||||
|
{% elif not package.license.is_foss and package.type != package.type.TXP %}
|
||||||
|
{% set package_warning="Non-free code." %}
|
||||||
|
{% elif not package.media_license.is_foss %}
|
||||||
|
{% set package_warning="Non-free media." %}
|
||||||
|
{% endif %}
|
||||||
|
{% if package_warning %}
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<b>Warning:</b> {{ package_warning }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<td>Name</td>
|
||||||
|
<td>{{ package.name }}</td>
|
||||||
|
</tr>
|
||||||
|
{% if package.provides %}
|
||||||
|
<tr>
|
||||||
|
<td>Provides</td>
|
||||||
|
<td>{% for meta in package.provides %}
|
||||||
|
<a class="badge badge-primary"
|
||||||
|
href="{{ url_for('meta_package_page', name=meta.name) }}">{{ meta.name }}</a>
|
||||||
|
{% endfor %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td>Author</td>
|
||||||
|
<td class="{{ package.author.rank }}">
|
||||||
|
<a href="{{ url_for('user_profile_page', username=package.author.username) }}">
|
||||||
|
{{ package.author.display_name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Type</td>
|
||||||
|
<td>{{ package.type.value }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>License</td>
|
||||||
|
<td>
|
||||||
|
{% if package.license == package.media_license %}
|
||||||
|
{{ package.license.name }}
|
||||||
|
{% elif package.type == package.type.TXP %}
|
||||||
|
{{ package.media_license.name }}
|
||||||
|
{% else %}
|
||||||
|
{{ package.license.name }} for code,<br />
|
||||||
|
{{ package.media_license.name }} for media.
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Added</td>
|
||||||
|
<td>{{ package.created_at | datetime }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
{% for t in package.tags %}
|
||||||
|
<span class="badge badge-primary">{{ t.title }}</span>
|
||||||
|
{% else %}
|
||||||
|
<i>No tags.</i>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if package.author.donate_url %}
|
||||||
|
<div class="alert alert-secondary">
|
||||||
|
Like {{ package.author.display_name }}'s work?
|
||||||
|
<a href="{{ package.author.donate_url }}" rel="nofollow">Donate now!</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if package.type == package.type.MOD %}
|
||||||
|
<div class="card my-4">
|
||||||
|
<div class="card-header">Dependencies</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% for dep in package.dependencies %}
|
||||||
|
{% if dep.optional %}
|
||||||
|
{% set color="secondary" %}
|
||||||
|
{% else %}
|
||||||
|
{% set color="primary" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{%- if dep.package %}
|
||||||
|
<a class="badge badge-{{ color }}"
|
||||||
|
href="{{ dep.package.getDetailsURL() }}">
|
||||||
|
{{ dep.package.title }} by {{ dep.package.author.display_name }}
|
||||||
|
{% elif dep.meta_package %}
|
||||||
|
<a class="badge badge-{{ color }}"
|
||||||
|
href="{{ url_for('meta_package_page', name=dep.meta_package.name) }}">
|
||||||
|
{{ dep.meta_package.name }}
|
||||||
|
{% else %}
|
||||||
|
{{ "Excepted package or meta_package in dep!" | throw }}
|
||||||
|
{% endif %}</a>
|
||||||
|
{% else %}
|
||||||
|
<i>No dependencies</i>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card my-4">
|
||||||
|
<div class="card-header">
|
||||||
|
Releases
|
||||||
|
<div class="float-right">
|
||||||
|
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
|
||||||
|
<a href="{{ package.getBulkReleaseURL() }}">bulk</a>
|
||||||
|
|
|
||||||
|
<a href="{{ package.getCreateReleaseURL() }}">+</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for rel in releases %}
|
||||||
|
{% if rel.approved or package.checkPerm(current_user, "MAKE_RELEASE") or package.checkPerm(current_user, "APPROVE_RELEASE") %}
|
||||||
|
<li class="list-group-item">
|
||||||
|
|
||||||
|
{% if package.checkPerm(current_user, "MAKE_RELEASE") or package.checkPerm(current_user, "APPROVE_RELEASE") %}
|
||||||
|
<a class="btn btn-sm btn-primary float-right" href="{{ rel.getEditURL() }}">Edit
|
||||||
|
{% if not rel.task_id and not rel.approved and package.checkPerm(current_user, "APPROVE_RELEASE") %}
|
||||||
|
/ Approve
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not rel.approved %}<i>{% endif %}
|
||||||
|
|
||||||
|
<a href="{{ rel.getDownloadURL() }}" rel="nofollow">{{ rel.title }}</a>
|
||||||
|
|
||||||
|
<span style="color:#ddd;">
|
||||||
|
{% if rel.min_rel and rel.max_rel %}
|
||||||
|
[MT {{ rel.min_rel.name }}-{{ rel.max_rel.name }}]
|
||||||
|
{% elif rel.min_rel %}
|
||||||
|
[MT {{ rel.min_rel.name }}+]
|
||||||
|
{% elif rel.max_rel %}
|
||||||
|
[MT ≤{{ rel.max_rel.name }}]
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<small style="color:#999;">
|
||||||
|
{% if rel.commit_hash %}
|
||||||
|
[{{ rel.commit_hash | truncate(5, end='') }}]
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
created {{ rel.releaseDate | date }}.
|
||||||
|
</small>
|
||||||
|
{% if (package.checkPerm(current_user, "MAKE_RELEASE") or package.checkPerm(current_user, "APPROVE_RELEASE")) and rel.task_id %}
|
||||||
|
<a href="{{ url_for('check_task', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
|
||||||
|
{% elif not rel.approved %}
|
||||||
|
Waiting for approval.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not rel.approved %}</i>{% endif %}
|
||||||
|
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<li class="list-group-item">No releases available.</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card my-4"">
|
||||||
|
<div class="card-header">
|
||||||
|
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %}
|
||||||
|
<a class="float-right"
|
||||||
|
href="{{ url_for('new_thread_page', pid=package.id) }}">+</a>
|
||||||
|
{% endif %}
|
||||||
|
Threads
|
||||||
|
</div>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% from "macros/threads.html" import render_threadlist %}
|
||||||
|
{{ render_threadlist(threads, list_group=True) }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") and current_user != package.author and not current_user.rank.atLeast(current_user.rank.EDITOR) %}
|
||||||
|
<a class="float-right"
|
||||||
|
href="{{ url_for('new_thread_page', pid=package.id) }}">
|
||||||
|
Report a problem with this listing
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{% if not package.approved and (package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW")) %}
|
||||||
{% if review_thread %}
|
{% if review_thread %}
|
||||||
<h2>{% if review_thread.private %}🔒{% endif %} {{ review_thread.title }}</h2>
|
<h2>{% if review_thread.private %}🔒{% endif %} {{ review_thread.title }}</h2>
|
||||||
{% if review_thread.private %}
|
{% if review_thread.private %}
|
||||||
@@ -70,266 +350,66 @@
|
|||||||
|
|
||||||
{% from "macros/threads.html" import render_thread %}
|
{% from "macros/threads.html" import render_thread %}
|
||||||
{{ render_thread(review_thread, current_user) }}
|
{{ render_thread(review_thread, current_user) }}
|
||||||
{% else %}
|
|
||||||
<div class="box box_grey alert alert-info">
|
|
||||||
Privately ask a question or give feedback
|
|
||||||
|
|
||||||
<a class="alert_right button" href="{{ url_for('new_thread_page', pid=package.id, title='Package approval comments') }}">Open Thread</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h1>{{ package.title }} by {{ package.author.display_name }}</h1>
|
<ul class="screenshot_list mb-4">
|
||||||
|
{% if package.checkPerm(current_user, "EDIT_PACKAGE") %}
|
||||||
<ul class="screenshot_list">
|
<a class="btn btn-primary float-right" href="{{ package.getNewScreenshotURL() }}">Add screenshot</a>
|
||||||
{% for ss in package.screenshots %}
|
|
||||||
{% if ss.approved or package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
|
|
||||||
<li>
|
|
||||||
<a href="{% if package.checkPerm(current_user, 'ADD_SCREENSHOTS') %}{{ ss.getEditURL() }}{% else %}{{ ss.url }}{% endif %}">
|
|
||||||
<img src="{{ ss.getThumbnailURL() }}" alt="{{ ss.title }}" />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% for ss in package.screenshots %}
|
||||||
</ul>
|
{% if ss.approved or package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
|
||||||
|
<li>
|
||||||
<aside class="asideright box box_grey">
|
<a href="{% if package.checkPerm(current_user, 'ADD_SCREENSHOTS') %}{{ ss.getEditURL() }}{% else %}{{ ss.url }}{% endif %}">
|
||||||
<h3>Details</h3>
|
<img src="{{ ss.getThumbnailURL() }}" alt="{{ ss.title }}" />
|
||||||
|
|
||||||
<div class="box-body">
|
|
||||||
{% if not package.license.is_foss and not package.media_license.is_foss and package.type != package.type.TXP %}
|
|
||||||
<div class="box box_grey alert alert-error" style="margin-top: 0;">
|
|
||||||
<b>Warning:</b> Non-free code and media.
|
|
||||||
</div>
|
|
||||||
{% elif not package.license.is_foss and package.type != package.type.TXP %}
|
|
||||||
<div class="box box_grey alert alert-error" style="margin-top: 0;">
|
|
||||||
<b>Warning:</b> Non-free code.
|
|
||||||
</div>
|
|
||||||
{% elif not package.media_license.is_foss %}
|
|
||||||
<div class="box box_grey alert alert-error" style="margin-top: 0;">
|
|
||||||
<b>Warning:</b> Non-free media.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>Name</td>
|
|
||||||
<td>{{ package.name }}</td>
|
|
||||||
</tr>
|
|
||||||
{% if package.provides %}
|
|
||||||
<tr>
|
|
||||||
<td>Provides</td>
|
|
||||||
<td>{% for meta in package.provides %}
|
|
||||||
<a href="{{ url_for('meta_package_page', name=meta.name) }}">{{ meta.name }}</a>
|
|
||||||
{%- if not loop.last %}
|
|
||||||
,
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
|
||||||
<td>Author</td>
|
|
||||||
<td class="{{ package.author.rank }}">
|
|
||||||
<a href="{{ url_for('user_profile_page', username=package.author.username) }}">
|
|
||||||
{{ package.author.display_name }}
|
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</li>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Type</td>
|
|
||||||
<td>{{ package.type.value }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>License</td>
|
|
||||||
<td>
|
|
||||||
{% if package.license == package.media_license %}
|
|
||||||
{{ package.license.name }}
|
|
||||||
{% elif package.type == package.type.TXP %}
|
|
||||||
{{ package.media_license.name }}
|
|
||||||
{% else %}
|
|
||||||
{{ package.license.name }} for code,<br />
|
|
||||||
{{ package.media_license.name }} for media.
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Added</td>
|
|
||||||
<td>{{ package.created_at | datetime }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<ul class="buttonset linedbuttonset">
|
|
||||||
{% if package.getDownloadRelease() %}<li><a href="{{ package.getDownloadURL() }}" class="btn_green">Download</a></li>{% endif %}
|
|
||||||
{% if package.repo %}<li><a href="{{ package.repo }}">View Source</a></li>{% endif %}
|
|
||||||
{% if package.forums %}<li><a href="https://forum.minetest.net/viewtopic.php?t={{ package.forums }}">Forums</a></li>{% endif %}
|
|
||||||
{% if package.issueTracker %}<li><a href="{{ package.issueTracker }}">Issue Tracker</a></li>{% endif %}
|
|
||||||
{% if package.website %}<li><a href="{{ package.website }}">Website</a></li>{% endif %}
|
|
||||||
{% if package.checkPerm(current_user, "EDIT_PACKAGE") %}
|
|
||||||
<li><a href="{{ package.getEditURL() }}">Edit</a></li>
|
|
||||||
<li><a href="{{ package.getNewScreenshotURL() }}">Add screenshot</a></li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# {% if current_user.is_authenticated %}
|
{% endfor %}
|
||||||
<li><a href="{{ package.getCreateEditRequestURL() }}">Suggest Changes</a></li>
|
</ul>
|
||||||
{% endif %} #}
|
|
||||||
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
|
|
||||||
<li><a href="{{ package.getCreateReleaseURL() }}">Create Release</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %}
|
|
||||||
<li><a href="{{ url_for('new_thread_page', pid=package.id) }}">Open Thread</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if package.checkPerm(current_user, "DELETE_PACKAGE") %}
|
|
||||||
<li><a href="{{ package.getDeleteURL() }}">Delete</a></li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<p class="package-short-large">{{ package.shortDesc }}</p>
|
{{ package.desc | markdown }}
|
||||||
|
|
||||||
{{ package.desc | markdown }}
|
<div style="clear: both;"></div>
|
||||||
|
|
||||||
<h3>Releases</h3>
|
{#
|
||||||
|
{% if current_user.is_authenticated or requests %}
|
||||||
|
<h3>Edit Requests</h3>
|
||||||
|
|
||||||
<ul>
|
|
||||||
{% for rel in releases %}
|
|
||||||
{% if rel.approved or package.checkPerm(current_user, "MAKE_RELEASE") or package.checkPerm(current_user, "APPROVE_RELEASE") %}
|
|
||||||
<li>
|
|
||||||
{% if not rel.approved %}<i>{% endif %}
|
|
||||||
|
|
||||||
<a href="{{ rel.getDownloadURL() }}">{{ rel.title }}</a>{% if rel.commit_hash %}
|
|
||||||
[{{ rel.commit_hash | truncate(5, end='') }}]{% endif %},
|
|
||||||
created {{ rel.releaseDate | datetime }}.
|
|
||||||
{% if rel.task_id %}
|
|
||||||
<a href="{{ url_for('check_task', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
|
|
||||||
{% elif not rel.approved %}
|
|
||||||
Waiting for approval.
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if package.checkPerm(current_user, "MAKE_RELEASE") or package.checkPerm(current_user, "APPROVE_RELEASE") %}
|
|
||||||
<a href="{{ rel.getEditURL() }}">Edit
|
|
||||||
{% if not rel.task_id and not rel.approved and package.checkPerm(current_user, "APPROVE_RELEASE") %}
|
|
||||||
/ Approve
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if not rel.approved %}</i>{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<li>No releases available.</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>Tags</h3>
|
|
||||||
<ul>
|
|
||||||
{% for t in package.tags %}
|
|
||||||
<li>{{ t.title }}</li>
|
|
||||||
{% else %}
|
|
||||||
<li>No tags.</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- <table class="table-topalign">
|
|
||||||
<tr>
|
|
||||||
<td> -->
|
|
||||||
<h3>Dependencies</h3>
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for dep in package.dependencies %}
|
{% for r in requests %}
|
||||||
<li>
|
<li>
|
||||||
{%- if dep.package %}
|
<a href="{{ r.getURL() }}">{{ r.title }}</a>
|
||||||
<a href="{{ dep.package.getDetailsURL() }}">{{ dep.package.title }}</a> by {{ dep.package.author.display_name }}
|
by
|
||||||
{% elif dep.meta_package %}
|
<a href="{{ url_for('user_profile_page', username=r.author.username) }}">{{ r.author.display_name }}</a>
|
||||||
<a href="{{ url_for('meta_package_page', name=dep.meta_package.name) }}">{{ dep.meta_package.name }}</a>
|
|
||||||
{% else %}
|
|
||||||
{{ "Excepted package or meta_package in dep!" | throw }}
|
|
||||||
{% endif %}
|
|
||||||
{% if dep.optional %}
|
|
||||||
[optional]
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><i>No dependencies</i></li>
|
<li>No edit requests have been made.</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<!-- </td>
|
{% endif %}
|
||||||
<td>
|
#}
|
||||||
<h3>Required by</h3>
|
|
||||||
<ul>
|
|
||||||
{% for p in package.dependents %}
|
|
||||||
<li><a href="{{ p.getDetailsURL() }}">{{ p.title }}</a> by {{ p.author.display_name }}</li>
|
|
||||||
{% else %}
|
|
||||||
{% if not package.softdependents %}
|
|
||||||
<li>No dependents.</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% for p in package.softdependents %}
|
|
||||||
<li><a href="{{ p.getDetailsURL() }}">{{ p.title }}</a> by {{ p.author.display_name }} [optional]</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table> -->
|
|
||||||
|
|
||||||
{#
|
{% if alternatives %}
|
||||||
{% if current_user.is_authenticated or requests %}
|
<h3>Related</h3>
|
||||||
<h3>Edit Requests</h3>
|
|
||||||
|
|
||||||
|
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
||||||
|
{{ render_pkggrid(alternatives) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if similar_topics %}
|
||||||
|
<h3>Similar Forum Topics</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{% for r in requests %}
|
{% for t in similar_topics %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ r.getURL() }}">{{ r.title }}</a>
|
[{{ t.type.value }}]
|
||||||
by
|
<a href="https://forum.minetest.net/viewtopic.php?t={{ t.topic_id }}">
|
||||||
<a href="{{ url_for('user_profile_page', username=r.author.username) }}">{{ r.author.display_name }}</a>
|
{{ t.title }} by {{ t.author.display_name }}
|
||||||
|
</a>
|
||||||
|
{% if t.wip %}[WIP]{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
|
||||||
<li>No edit requests have been made.</li>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
#}
|
</main>
|
||||||
|
|
||||||
{% if alternatives %}
|
|
||||||
<h3>Alternatives</h3>
|
|
||||||
<ul>
|
|
||||||
{% for p in alternatives %}
|
|
||||||
<li><a href="{{ p.getDetailsURL() }}">{{ p.title }} by {{ p.author.display_name }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if similar_topics %}
|
|
||||||
<h3>Similar Forum Topics</h3>
|
|
||||||
{% if not package.approved and package.type == package.type.MOD %}
|
|
||||||
<div class="box box_grey alert alert-warning">
|
|
||||||
Please make sure that this package has the right to
|
|
||||||
the name '{{ package.name }}'.
|
|
||||||
See the
|
|
||||||
<a href="/policy_and_guidance/">Inclusion Policy</a>
|
|
||||||
for more info.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<ul>
|
|
||||||
{% for t in similar_topics %}
|
|
||||||
<li>
|
|
||||||
[{{ t.type.value }}]
|
|
||||||
<a href="https://forum.minetest.net/viewtopic.php?t={{ t.topic_id }}">
|
|
||||||
{{ t.title }} by {{ t.author.display_name }}
|
|
||||||
</a>
|
|
||||||
{% if t.wip %}[WIP]{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if threads %}
|
|
||||||
<h3>Threads</h3>
|
|
||||||
|
|
||||||
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %}
|
|
||||||
<p><a href="{{ url_for('new_thread_page', pid=package.id) }}">Open Thread</a></p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% from "macros/threads.html" import render_threadlist %}
|
|
||||||
{{ render_threadlist(threads) }}
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -5,15 +5,44 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
|
||||||
<form method="POST" action="" enctype="multipart/form-data">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
|
|
||||||
{{ render_field(form.title) }}
|
{% if package and current_user != package.author and not current_user.rank.atLeast(current_user.rank.EDITOR) and package.issueTracker %}
|
||||||
{{ render_field(form.comment) }}
|
<div class="alert alert-warning">
|
||||||
{{ render_field(form.private) }}
|
Found a bug? Post on the <a href="{{ package.issueTracker }}">issue tracker</a> instead.<br />
|
||||||
{{ render_submit_field(form.submit) }}
|
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 %}
|
||||||
|
|
||||||
|
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}
|
||||||
|
<form method="POST" action="" enctype="multipart/form-data">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{{ render_field(form.title) }}
|
||||||
|
|
||||||
|
<div class="row mt-0 mb-4 comments mx-0">
|
||||||
|
<div class="col-md-1 p-1">
|
||||||
|
<img class="img-responsive user-photo img-thumbnail img-thumbnail-1" src="{{ current_user.getProfilePicURL() }}">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header {{ current_user.rank.name }}">
|
||||||
|
{{ current_user.display_name }}
|
||||||
|
<a name="reply"></a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{ render_field(form.comment, label="", class_="m-0", fieldclass="form-control markdown") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ render_checkbox_field(form.private, class_="my-3") }}
|
||||||
|
<p>
|
||||||
|
Only the you, the package author, and users of Editor rank
|
||||||
|
and above can read private threads.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{ render_submit_field(form.submit) }}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
<p>Only the you, the package author, and users of Editor rank and above can read private threads.</p>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -5,25 +5,26 @@ Threads
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
{% if current_user in thread.watchers %}
|
||||||
|
<form method="post" action="{{ thread.getUnsubscribeURL() }}" class="float-right">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<input type="submit" class="btn btn-primary" value="Unsubscribe" />
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form method="post" action="{{ thread.getSubscribeURL() }}" class="float-right">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<input type="submit" class="btn btn-primary" value="Subscribe" />
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h1>{% if thread.private %}🔒 {% endif %}{{ thread.title }}</h1>
|
<h1>{% if thread.private %}🔒 {% endif %}{{ thread.title }}</h1>
|
||||||
|
|
||||||
{% if thread.package or current_user.is_authenticated %}
|
{% if thread.package or current_user.is_authenticated %}
|
||||||
{% if thread.package %}
|
{% if thread.package %}
|
||||||
<p>Package: <a href="{{ thread.package.getDetailsURL() }}">{{ thread.package.title }}</a></p>
|
<p>Package: <a href="{{ thread.package.getDetailsURL() }}">{{ thread.package.title }}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if current_user.is_authenticated %}
|
|
||||||
{% if current_user in thread.watchers %}
|
|
||||||
<form method="post" action="{{ thread.getUnsubscribeURL() }}">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
<input type="submit" value="Unsubscribe" />
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
<form method="post" action="{{ thread.getSubscribeURL() }}">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
|
||||||
<input type="submit" value="Subscribe" />
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if thread.private %}
|
{% if thread.private %}
|
||||||
|
|||||||
@@ -5,53 +5,89 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Awaiting Approval</h2>
|
<h2 class="mb-4">Approval Queue</h2>
|
||||||
|
|
||||||
{% if canApproveNew and packages %}
|
<div class="row">
|
||||||
<h3>Packages</h3>
|
{% if canApproveNew and packages %}
|
||||||
<ul>
|
<div class="col-sm-6">
|
||||||
{% for p in packages %}
|
<div class="card">
|
||||||
<li><a href="{{ p.getDetailsURL() }}">
|
<h3 class="card-header">Packages</h3>
|
||||||
{{ p.title }} by {{ p.author.display_name }}
|
<div class="list-group list-group-flush">
|
||||||
</a></li>
|
{% for p in packages %}
|
||||||
{% else %}
|
<a href="{{ p.getDetailsURL() }}" class="list-group-item list-group-item-action">
|
||||||
<li><i>No packages need reviewing.</i></ul>
|
{% if p.getState() == "thread" %}
|
||||||
{% endfor %}
|
<span class="mr-2 badge badge-danger">Thread</span>
|
||||||
</ul>
|
{% elif p.getState() == "ready" %}
|
||||||
{% endif %}
|
<span class="mr-2 badge badge-success">Ready</span>
|
||||||
|
{% elif p.getState() == "wip" %}
|
||||||
|
<span class="mr-2 badge badge-warning">WIP</span>
|
||||||
|
{% elif p.getState() == "license" %}
|
||||||
|
<span class="mr-2 badge badge-info">WIP</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{ p.title }} by {{ p.author.display_name }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<li class="list-group-item"><i>No packages need reviewing.</i></li>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if canApproveRel and releases %}
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="card">
|
||||||
|
<h3 class="card-header">Releases</h3>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for r in releases %}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<a href="{{ r.getEditURL() }}">{{ r.title }}</a>
|
||||||
|
on
|
||||||
|
<a href="{{ r.package.getDetailsURL() }}">
|
||||||
|
{{ r.package.title }} by {{ r.package.author.display_name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="list-group-item"><i>No releases need reviewing.</i></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if canApproveScn and screenshots %}
|
{% if canApproveScn and screenshots %}
|
||||||
<h3>Screenshots</h3>
|
<div class="card my-4">
|
||||||
<ul>
|
<h3 class="card-header">Screenshots
|
||||||
{% for s in screenshots %}
|
<form class="float-right" method="post" action="{{ url_for('todo_page') }}">
|
||||||
<li>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<a href="{{ s.getEditURL() }}">{{ s.title }}</a>
|
<input type="hidden" name="action" value="screenshots_approve_all" />
|
||||||
on
|
<input class="btn btn-sm btn-primary" type="submit" value="Approve All" />
|
||||||
<a href="{{ s.package.getDetailsURL() }}">
|
</form>
|
||||||
{{ s.package.title }} by {{ s.package.author.display_name }}
|
</h3>
|
||||||
</a>
|
<ul class="card-body d-flex p-0 flex-row flex-wrap justify-content-start align-content-start p-4">
|
||||||
</li>
|
{% for s in screenshots %}
|
||||||
{% else %}
|
<li class="packagetile flex-fill"><a href="{{ s.getEditURL() }}"
|
||||||
<li><i>No screenshots need reviewing.</i></ul>
|
style="background-image: url({{ s.getThumbnailURL(3) or '/static/placeholder.png' }});">
|
||||||
{% endfor %}
|
<div class="packagegridscrub"></div>
|
||||||
</ul>
|
<div class="packagegridinfo">
|
||||||
{% endif %}
|
<h3>
|
||||||
|
{{ s.title }}
|
||||||
{% if canApproveRel and releases %}
|
<br />
|
||||||
<h3>Releases</h3>
|
<small>{{ s.package.title }} by {{ s.package.author.display_name }}</small>
|
||||||
<ul>
|
</h3>
|
||||||
{% for r in releases %}
|
<p></p>
|
||||||
<li>
|
</div>
|
||||||
<a href="{{ r.getEditURL() }}">{{ r.title }}</a>
|
</a></li>
|
||||||
on
|
{% else %}
|
||||||
<a href="{{ r.package.getDetailsURL() }}">
|
<li><i>No screenshots need reviewing.</i></li>
|
||||||
{{ r.package.title }} by {{ r.package.author.display_name }}
|
{% endfor %}
|
||||||
</a>
|
{% for i in range(4) %}
|
||||||
</li>
|
<li class="packagetile flex-fill"></li>
|
||||||
{% else %}
|
{% endfor %}
|
||||||
<li><i>No releases need reviewing.</i></ul>
|
</ul>
|
||||||
{% endfor %}
|
</div>
|
||||||
</ul>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not (packages or screenshots or releases) %}
|
{% if not (packages or screenshots or releases) %}
|
||||||
@@ -60,11 +96,19 @@
|
|||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h2>Unadded Topic List</h2>
|
<h2 class="mt-4">Unadded Topic List</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
There are
|
{{ total_topics - topics_to_add }} / {{ total_topics }} packages have been been added to cdb,
|
||||||
<a href="{{ url_for('todo_topics_page') }}">{{ topics_to_add }} packages</a>
|
based on cdb's forum parser. {{ topics_to_add }} remaining.
|
||||||
to be added to cdb, based on cdb's forum parser.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div class="progress my-4">
|
||||||
|
{% set perc = 32 %}
|
||||||
|
<div class="progress-bar bg-success" role="progressbar"
|
||||||
|
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="btn btn-primary" href="{{ url_for('todo_topics_page') }}">View Unadded Topic List</a>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -5,13 +5,94 @@ Topics to be Added
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="float-right">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a class="btn btn-primary {% if sort_by=='date' %}active{% endif %}"
|
||||||
|
href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=n, sort='date') }}">
|
||||||
|
Sort by date
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-primary {% if sort_by=='name' %}active{% endif %}"
|
||||||
|
href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=n, sort='name') }}">
|
||||||
|
Sort by name
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-primary {% if sort_by=='views' %}active{% endif %}"
|
||||||
|
href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=n, sort='views') }}">
|
||||||
|
Sort by views
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="btn-group">
|
||||||
|
{% if current_user.rank.atLeast(current_user.rank.EDITOR) %}
|
||||||
|
{% if n >= 10000 %}
|
||||||
|
<a class="btn btn-primary"
|
||||||
|
href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=100, sort=sort_by) }}">
|
||||||
|
Paginated list
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-primary"
|
||||||
|
href="{{ url_for('todo_topics_page', q=query, show_discarded=show_discarded, n=10000, sort=sort_by) }}">
|
||||||
|
Unlimited list
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a class="btn btn-primary" href="{{ url_for('todo_topics_page', q=query, show_discarded=not show_discarded, n=n, sort=sort_by) }}">
|
||||||
|
{% if not show_discarded %}
|
||||||
|
Show
|
||||||
|
{% else %}
|
||||||
|
Hide
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
discarded topics
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1>Topics to be Added</h1>
|
<h1>Topics to be Added</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{ total - (topics | count) }} / {{ total }} packages have been added.
|
{{ total - topic_count }} / {{ total }} topics have been added as packages to CDB.
|
||||||
{{ topics | count }} remaining.
|
{{ topic_count }} remaining.
|
||||||
</p>
|
</p>
|
||||||
|
<div class="progress">
|
||||||
|
{% set perc = 100 * (total - topic_count) / total %}
|
||||||
|
<div class="progress-bar bg-success" role="progressbar"
|
||||||
|
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% from "macros/topictable.html" import render_topictable %}
|
<form method="GET" action="{{ url_for('todo_topics_page') }}" class="my-4">
|
||||||
{{ render_topictable(topics) }}
|
<input type="hidden" name="show_discarded" value={{ show_discarded and "True" or "False" }} />
|
||||||
|
<input type="hidden" name="n" value={{ n }} />
|
||||||
|
<input type="hidden" name="sort" value={{ sort_by or "date" }} />
|
||||||
|
<input name="q" type="text" placeholder="Search topics" value="{{ query or ''}}">
|
||||||
|
<input class="btn btn-secondary my-2 my-sm-0 mr-sm-2" type="submit" value="Search" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% from "macros/topics.html" import render_topics_table %}
|
||||||
|
{{ render_topics_table(topics, show_discard=True, current_user=current_user) }}
|
||||||
|
|
||||||
|
<ul class="pagination mt-4">
|
||||||
|
<li class="page-item {% if not prev_url %}disabled{% endif %}">
|
||||||
|
<a class="page-link" {% if prev_url %}href="{{ prev_url }}"{% endif %}>«</a>
|
||||||
|
</li>
|
||||||
|
{% for i in range(1, page_max+1) %}
|
||||||
|
<li class="page-item {% if i == page %}active{% endif %}">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{{ url_for('todo_topics_page', page=i, query=query, show_discarded=show_discarded, n=n, sort=sort_by) }}">
|
||||||
|
{{ i }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
<li class="page-item {% if not next_url %}disabled{% endif %}">
|
||||||
|
<a class="page-link" {% if next_url %}href="{{ next_url }}"{% endif %}>»</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scriptextra %}
|
||||||
|
<script>
|
||||||
|
var csrf_token = "{{ csrf_token() }}";
|
||||||
|
</script>
|
||||||
|
<script src="/static/topic_discard.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ Creating an Account
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="box box_grey">
|
<div class="card">
|
||||||
<h2>{{ self.title() }}</h2>
|
<h2 class="card-header">{{ self.title() }}</h2>
|
||||||
|
|
||||||
<div class="box-body">
|
<div class="card-body">
|
||||||
<p>
|
<p>
|
||||||
If you have a forum account, you'll need to prove that you own it
|
If you have a forum account, you'll need to prove that you own it
|
||||||
to get an account on ContentDB.
|
to get an account on ContentDB.
|
||||||
@@ -19,7 +19,7 @@ Creating an Account
|
|||||||
Please log out to continue.
|
Please log out to continue.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url_for('user.logout', next=url_for('user_claim_page')) }}" class="button">Logout</a>
|
<a href="{{ url_for('user.logout', next=url_for('user_claim_page')) }}" class="btn">Logout</a>
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>
|
<p>
|
||||||
@@ -28,7 +28,7 @@ Creating an Account
|
|||||||
out of the Minetest community.
|
out of the Minetest community.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a href="https://forum.minetest.net/ucp.php?mode=register">
|
<a class="btn btn-primary" href="https://forum.minetest.net/ucp.php?mode=register">
|
||||||
Create a Forum Account
|
Create a Forum Account
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -36,18 +36,23 @@ Creating an Account
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if not current_user.is_authenticated %}
|
{% if not current_user.is_authenticated %}
|
||||||
<div class="box box_grey">
|
<div class="row mt-4">
|
||||||
<h2>Option 1 - Use GitHub field in forum profile</h2>
|
<div class="col-sm-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="badge badge-pill badge-dark mr-2">Option 1</span>
|
||||||
|
Use GitHub field in forum profile
|
||||||
|
</div>
|
||||||
|
|
||||||
<form method="post" class="box-body" action="{{ url_for('user_claim_page') }}">
|
<form method="post" class="card-body" action="{{ url_for('user_claim_page') }}">
|
||||||
<input type="hidden" name="claim_type" value="github">
|
<input class="form-control" type="hidden" name="claim_type" value="github">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input class="form-control" type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Enter your forum username here:
|
Enter your forum username here:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<input type="text" name="username" value="{{ username }}" required placeholder="Forum username">
|
<input class="form-control my-4" type="text" name="username" value="{{ username }}" required placeholder="Forum username">
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You'll need to have the GitHub field in your forum profile
|
You'll need to have the GitHub field in your forum profile
|
||||||
@@ -56,14 +61,19 @@ Creating an Account
|
|||||||
do that here</a>.
|
do that here</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<input type="submit" value="Next: log in with GitHub">
|
<input class="btn btn-primary" type="submit" value="Next: log in with GitHub">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="box box_grey">
|
<div class="col-sm-4">
|
||||||
<h2>Option 2 - Paste verification token into signature</h2>
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="badge badge-pill badge-dark mr-2">Option 2</span>
|
||||||
|
Verification token
|
||||||
|
</div>
|
||||||
|
|
||||||
<form method="post" class="box-body" action="{{ url_for('user_claim_page') }}">
|
<form method="post" class="card-body" action="{{ url_for('user_claim_page') }}">
|
||||||
<input type="hidden" name="claim_type" value="forum">
|
<input type="hidden" name="claim_type" value="forum">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
|
||||||
@@ -71,7 +81,7 @@ Creating an Account
|
|||||||
Enter your forum username here:
|
Enter your forum username here:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<input type="text" name="username" value="{{ username }}" required placeholder="Forum username">
|
<input class="form-control my-3" type="text" name="username" value="{{ username }}" required placeholder="Forum username">
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Go to
|
Go to
|
||||||
@@ -79,11 +89,12 @@ Creating an Account
|
|||||||
User Control Panel > Profile > Edit signature
|
User Control Panel > Profile > Edit signature
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Paste this into your signature:
|
Paste this into your signature:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<input type="text" value="{{ key }}" readonly size=32>
|
<input class="form-control my-3" type="text" value="{{ key }}" readonly size=32>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Click next so we can check it.
|
Click next so we can check it.
|
||||||
@@ -92,15 +103,20 @@ Creating an Account
|
|||||||
Don't worry, you can remove it after this is done.
|
Don't worry, you can remove it after this is done.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<input type="submit" value="Next">
|
<input class="btn btn-primary" type="submit" value="Next">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="box box_grey">
|
<div class="col-sm-4">
|
||||||
<h2>Option 3 - Email/password sign up</h2>
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="badge badge-pill badge-dark mr-2">Option 3</span>
|
||||||
|
Email/password sign up
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="box-body">
|
<div class="card-body">
|
||||||
<p>
|
<p class="alert alert-danger">
|
||||||
<b>Only do this if you don't have a forum account!</b>
|
<b>Only do this if you don't have a forum account!</b>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
@@ -108,8 +124,10 @@ Creating an Account
|
|||||||
options.
|
options.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a class="button" href="{{ url_for('user.register') }}">Register</a>
|
<a class="btn btn-primary" href="{{ url_for('user.register') }}">Register</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@
|
|||||||
<a href="{{ url_for('user_profile_page', username=user.username) }}">
|
<a href="{{ url_for('user_profile_page', username=user.username) }}">
|
||||||
{{ user.display_name }}
|
{{ user.display_name }}
|
||||||
</a> -
|
</a> -
|
||||||
{{ user.rank.getTitle() }} -
|
{{ user.rank.getTitle() }}
|
||||||
{{ user.packages.count() }} packages.
|
{% if current_user.is_authenticated %}
|
||||||
|
- {{ user.packages.count() }} packages.
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
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 %}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% if optional %}
|
{% if optional %}
|
||||||
<div class="box box_grey alert alert-primary">
|
<div class="alert alert-primary">
|
||||||
It is recommended that you set a password for your account.
|
It is recommended that you set a password for your account.
|
||||||
|
|
||||||
<a class="alert_right button" href="{{ url_for('home_page') }}">Skip</a>
|
<a class="alert_right button" href="{{ url_for('home_page') }}">Skip</a>
|
||||||
|
|||||||
@@ -7,76 +7,145 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% if not current_user.is_authenticated and user.rank == user.rank.NOT_JOINED and user.forums_username %}
|
{% if not current_user.is_authenticated and user.rank == user.rank.NOT_JOINED and user.forums_username %}
|
||||||
<div class="box box_grey alert alert-info">
|
<div class="alert alert-info">
|
||||||
Is this you? Claim your account now!
|
<a class="float-right btn btn-default btn-sm"
|
||||||
|
href="{{ url_for('user_claim_page', username=user.forums_username) }}">Claim</a>
|
||||||
|
|
||||||
<a class="alert_right button" href="{{ url_for('user_claim_page', username=user.forums_username) }}">Claim</a>
|
Is this you? Claim your account now!
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="box box_grey">
|
<div class="row mb-3">
|
||||||
<h2>{{ user.display_name }}</h2>
|
<div class="col-sm-6">
|
||||||
|
<div class="card">
|
||||||
<table class="box-body">
|
<h2 class="card-header">{{ user.display_name }}</h2>
|
||||||
<tr>
|
<div class="card-body row">
|
||||||
<td>Rank:</td>
|
<div class="col-md-2">
|
||||||
<td>
|
{% if user.forums_username %}
|
||||||
{{ user.rank.getTitle() }}
|
<a href="https://forum.minetest.net/ucp.php?i=profile&mode=avatar">
|
||||||
</td>
|
{% elif user.email %}
|
||||||
</tr>
|
<a href="https://en.gravatar.com/">
|
||||||
<tr>
|
|
||||||
<td>Accounts:</td>
|
|
||||||
<td>
|
|
||||||
{% if user.forums_username %}
|
|
||||||
<a href="https://forum.minetest.net/memberlist.php?mode=viewprofile&un={{ user.forums_username }}">
|
|
||||||
Minetest Forum
|
|
||||||
</a>
|
|
||||||
{% elif user == current_user %}
|
|
||||||
No forum account
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if (user.forums_username and user.github_username) or user == current_user %}
|
|
||||||
|
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if user.github_username %}
|
|
||||||
<a href="https://github.com/{{ user.github_username }}">GitHub</a>
|
|
||||||
{% elif user == current_user %}
|
|
||||||
<a href="{{ url_for('github_signin_page') }}">Link Github</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if user == current_user %}
|
|
||||||
🌎
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% if user == current_user %}
|
|
||||||
<tr>
|
|
||||||
<td>Password:</td>
|
|
||||||
<td>
|
|
||||||
{% if user.password %}
|
|
||||||
Set | <a href="{{ url_for('user.change_password') }}">Change</a>
|
|
||||||
{% else %}
|
|
||||||
Not set | <a href="{{ url_for('set_password_page') }}">Set</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
<img class="img-responsive user-photo img-thumbnail img-thumbnail-1" src="{{ user.getProfilePicURL() }}">
|
||||||
</tr>
|
{% if user.forums_username or user.email %}
|
||||||
{% endif %}
|
</a>
|
||||||
</table>
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<td>Rank:</td>
|
||||||
|
<td>
|
||||||
|
{{ user.rank.getTitle() }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Links:</td>
|
||||||
|
<td>
|
||||||
|
{% if user.forums_username %}
|
||||||
|
<a href="https://forum.minetest.net/memberlist.php?mode=viewprofile&un={{ user.forums_username }}">
|
||||||
|
Minetest Forum
|
||||||
|
</a>
|
||||||
|
{% elif user == current_user %}
|
||||||
|
No forum account
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user.github_username or user == current_user %}
|
||||||
|
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user.github_username %}
|
||||||
|
<a href="https://github.com/{{ user.github_username }}">GitHub</a>
|
||||||
|
{% elif user == current_user %}
|
||||||
|
<a href="{{ url_for('github_signin_page') }}">Link Github</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user.website_url %}
|
||||||
|
| <a href="{{ user.website_url }}" rel="nofollow">Website</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user == current_user %}
|
||||||
|
<br>
|
||||||
|
<small class="text-muted">
|
||||||
|
<span style="padding-right: 5px;">🌎</span>
|
||||||
|
Visible to everyone
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.MODERATOR) %}
|
||||||
|
<tr>
|
||||||
|
<td>Admin</td>
|
||||||
|
<td>
|
||||||
|
{% if user.email %}
|
||||||
|
<a class="btn btn-primary" href="{{ url_for('send_email_page', username=user.username) }}">
|
||||||
|
Email
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-primary disabled"
|
||||||
|
data-toggle="tooltip" data-placement="bottom"
|
||||||
|
title="No email address for user"
|
||||||
|
style="pointer-events: all;">
|
||||||
|
Email
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if user == current_user %}
|
||||||
|
<tr>
|
||||||
|
<td>Profile Picture:</td>
|
||||||
|
<td>
|
||||||
|
{% if user.forums_username %}
|
||||||
|
<form method="post" action="{{ url_for('user_check', username=user.username) }}" class="" style="display:inline-block;">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<input type="submit" class="btn btn-primary" value="Sync with Forums" />
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.email %}
|
||||||
|
<a class="btn btn-primary" href="https://en.gravatar.com/">
|
||||||
|
Gravatar
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-primary disabled"
|
||||||
|
data-toggle="tooltip" data-placement="bottom"
|
||||||
|
title="Please add an email address to use Gravatar"
|
||||||
|
style="pointer-events: all;">
|
||||||
|
Gravatar
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Password:</td>
|
||||||
|
<td>
|
||||||
|
{% if user.password %}
|
||||||
|
Set | <a href="{{ url_for('user.change_password') }}">Change</a>
|
||||||
|
{% else %}
|
||||||
|
Not set | <a href="{{ url_for('set_password_page') }}">Set</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if form %}
|
{% if form %}
|
||||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||||
<div class="box box_grey">
|
<div class="col-sm-6">
|
||||||
<h2>Edit Details</h2>
|
<div class="card">
|
||||||
|
<div class="card-header">Edit Details</div>
|
||||||
<form action="" method="POST" class="form box-body" role="form">
|
<div class="card-body">
|
||||||
<div class="row">
|
<form action="" method="POST" class="form box-body" role="form">
|
||||||
<div class="col-sm-6 col-md-5 col-lg-4">
|
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
{% if user.checkPerm(current_user, "CHANGE_DNAME") %}
|
{% if user.checkPerm(current_user, "CHANGE_DNAME") %}
|
||||||
{{ render_field(form.display_name, tabindex=230) }}
|
{{ render_field(form.display_name, tabindex=230) }}
|
||||||
|
{{ render_field(form.website_url, tabindex=232) }}
|
||||||
|
{{ render_field(form.donate_url, tabindex=233) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user.checkPerm(current_user, "CHANGE_EMAIL") %}
|
{% if user.checkPerm(current_user, "CHANGE_EMAIL") %}
|
||||||
@@ -89,28 +158,48 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ render_submit_field(form.submit, tabindex=280) }}
|
{{ render_submit_field(form.submit, tabindex=280) }}
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
{% from "macros/packagegridtile.html" import render_pkggrid %}
|
||||||
{{ render_pkggrid(packages, show_author=False) }}
|
{{ render_pkggrid(packages, show_author=False) }}
|
||||||
|
|
||||||
{% if topics_to_add %}
|
{% if user.donate_url %}
|
||||||
<div class="box box_grey">
|
<div class="alert alert-secondary">
|
||||||
<h2>Unadded Packages</h2>
|
Like {{ user.display_name }}'s work?
|
||||||
|
<a href="{{ user.donate_url }}" rel="nofollow">Donate now!</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="box-body">
|
{% if current_user == user or (current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.EDITOR)) %}
|
||||||
<p>
|
<div class="card mt-3">
|
||||||
|
<a name="unadded-topics"></a>
|
||||||
|
<h2 class="card-header">Unadded topics</h2>
|
||||||
|
|
||||||
|
{% if topics_to_add %}
|
||||||
|
<p class="card-body">
|
||||||
List of your forum topics which do not have a matching package.
|
List of your forum topics which do not have a matching package.
|
||||||
|
Topics with a strikethrough have been marked as discarded.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% from "macros/topictable.html" import render_topictable %}
|
{% from "macros/topics.html" import render_topics_table %}
|
||||||
{{ render_topictable(topics_to_add, show_author=False) }}
|
{{ render_topics_table(topics_to_add, show_author=False, show_discard=True, current_user=current_user) }}
|
||||||
</div>
|
{% else %}
|
||||||
|
<p class="card-body">Congrats! You don't have any topics which aren't on CDB.</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block scriptextra %}
|
||||||
|
<script>
|
||||||
|
var csrf_token = "{{ csrf_token() }}";
|
||||||
|
</script>
|
||||||
|
<script src="/static/topic_discard.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
41
app/utils.py
@@ -20,7 +20,7 @@ from flask_user import *
|
|||||||
from flask_login import login_user, logout_user
|
from flask_login import login_user, logout_user
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app import app
|
from app import app
|
||||||
import random, string, os
|
import random, string, os, imghdr
|
||||||
|
|
||||||
def getExtension(filename):
|
def getExtension(filename):
|
||||||
return filename.rsplit(".", 1)[1].lower() if "." in filename else None
|
return filename.rsplit(".", 1)[1].lower() if "." in filename else None
|
||||||
@@ -28,6 +28,10 @@ def getExtension(filename):
|
|||||||
def isFilenameAllowed(filename, exts):
|
def isFilenameAllowed(filename, exts):
|
||||||
return getExtension(filename) in exts
|
return getExtension(filename) in exts
|
||||||
|
|
||||||
|
ALLOWED_IMAGES = set(["jpeg", "png"])
|
||||||
|
def isAllowedImage(data):
|
||||||
|
return imghdr.what(None, data) in ALLOWED_IMAGES
|
||||||
|
|
||||||
def shouldReturnJson():
|
def shouldReturnJson():
|
||||||
return "application/json" in request.accept_mimetypes and \
|
return "application/json" in request.accept_mimetypes and \
|
||||||
not "text/html" in request.accept_mimetypes
|
not "text/html" in request.accept_mimetypes
|
||||||
@@ -36,16 +40,32 @@ def randomString(n):
|
|||||||
return ''.join(random.choice(string.ascii_lowercase + \
|
return ''.join(random.choice(string.ascii_lowercase + \
|
||||||
string.ascii_uppercase + string.digits) for _ in range(n))
|
string.ascii_uppercase + string.digits) for _ in range(n))
|
||||||
|
|
||||||
def doFileUpload(file, allowedExtensions, fileTypeName):
|
def doFileUpload(file, fileType, fileTypeDesc):
|
||||||
if not file or file is None or file.filename == "":
|
if not file or file is None or file.filename == "":
|
||||||
flash("No selected file", "error")
|
flash("No selected file", "error")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
allowedExtensions = []
|
||||||
|
isImage = False
|
||||||
|
if fileType == "image":
|
||||||
|
allowedExtensions = ["jpg", "jpeg", "png"]
|
||||||
|
isImage = True
|
||||||
|
elif fileType == "zip":
|
||||||
|
allowedExtensions = ["zip"]
|
||||||
|
else:
|
||||||
|
raise Exception("Invalid fileType")
|
||||||
|
|
||||||
ext = getExtension(file.filename)
|
ext = getExtension(file.filename)
|
||||||
if ext is None or not ext in allowedExtensions:
|
if ext is None or not ext in allowedExtensions:
|
||||||
flash("Please upload load " + fileTypeName, "error")
|
flash("Please upload load " + fileTypeDesc, "danger")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if isImage and not isAllowedImage(file.stream.read()):
|
||||||
|
flash("Uploaded image isn't actually an image", "danger")
|
||||||
|
return None
|
||||||
|
|
||||||
|
file.stream.seek(0)
|
||||||
|
|
||||||
filename = randomString(10) + "." + ext
|
filename = randomString(10) + "." + ext
|
||||||
file.save(os.path.join("app/public/uploads", filename))
|
file.save(os.path.join("app/public/uploads", filename))
|
||||||
return "/uploads/" + filename
|
return "/uploads/" + filename
|
||||||
@@ -68,7 +88,10 @@ def make_flask_user_password(plaintext_str):
|
|||||||
import bcrypt
|
import bcrypt
|
||||||
plaintext = plaintext_str.encode("UTF-8")
|
plaintext = plaintext_str.encode("UTF-8")
|
||||||
password = bcrypt.hashpw(plaintext, bcrypt.gensalt())
|
password = bcrypt.hashpw(plaintext, bcrypt.gensalt())
|
||||||
return password.decode("UTF-8")
|
if isinstance(password, str):
|
||||||
|
return password
|
||||||
|
else:
|
||||||
|
return password.decode("UTF-8")
|
||||||
|
|
||||||
def _do_login_user(user, remember_me=False):
|
def _do_login_user(user, remember_me=False):
|
||||||
def _call_or_get(v):
|
def _call_or_get(v):
|
||||||
@@ -170,3 +193,13 @@ def clearNotifications(url):
|
|||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
Notification.query.filter_by(user=current_user, url=url).delete()
|
Notification.query.filter_by(user=current_user, url=url).delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
YESES = ["yes", "true", "1", "on"]
|
||||||
|
|
||||||
|
def isYes(val):
|
||||||
|
return val and val.lower() in YESES
|
||||||
|
|
||||||
|
|
||||||
|
def isNo(val):
|
||||||
|
return val and not isYes(val)
|
||||||
|
|||||||
@@ -18,13 +18,11 @@
|
|||||||
from app import app, pages
|
from app import app, pages
|
||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
from flask_login import login_user, logout_user
|
|
||||||
from app.models import *
|
from app.models import *
|
||||||
import flask_menu as menu
|
import flask_menu as menu
|
||||||
from flask.ext import markdown
|
|
||||||
from sqlalchemy import func
|
|
||||||
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()
|
||||||
@@ -35,6 +33,10 @@ def throw(err):
|
|||||||
def domain(url):
|
def domain(url):
|
||||||
return urlparse(url).netloc
|
return urlparse(url).netloc
|
||||||
|
|
||||||
|
@app.template_filter()
|
||||||
|
def date(value):
|
||||||
|
return value.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
@app.template_filter()
|
@app.template_filter()
|
||||||
def datetime(value):
|
def datetime(value):
|
||||||
return value.strftime("%Y-%m-%d %H:%M") + " UTC"
|
return value.strftime("%Y-%m-%d %H:%M") + " UTC"
|
||||||
@@ -48,13 +50,16 @@ def send_upload(path):
|
|||||||
def home_page():
|
def home_page():
|
||||||
query = Package.query.filter_by(approved=True, soft_deleted=False)
|
query = Package.query.filter_by(approved=True, soft_deleted=False)
|
||||||
count = query.count()
|
count = query.count()
|
||||||
new = query.order_by(db.desc(Package.created_at)).limit(15).all()
|
new = query.order_by(db.desc(Package.created_at)).limit(8).all()
|
||||||
popular = query.order_by(db.desc(Package.score)).limit(6).all()
|
pop_mod = query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score)).limit(8).all()
|
||||||
return render_template("index.html", new=new, popular=popular, count=count)
|
pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(4).all()
|
||||||
|
pop_txp = query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score)).limit(4).all()
|
||||||
|
downloads = db.session.query(func.sum(PackageRelease.downloads)).first()[0]
|
||||||
|
return render_template("index.html", count=count, downloads=downloads, \
|
||||||
|
new=new, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam)
|
||||||
|
|
||||||
from . import users, 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
@@ -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
|
||||||
@@ -17,12 +17,12 @@
|
|||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
from flask.ext import menu
|
import flask_menu as menu
|
||||||
from app import app
|
from app import app
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from celery import uuid
|
from celery import uuid
|
||||||
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies, makeVCSRelease
|
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies, makeVCSRelease
|
||||||
from app.tasks.forumtasks import importTopicList
|
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import *
|
from wtforms import *
|
||||||
from app.utils import loginUser, rank_required, triggerNotif
|
from app.utils import loginUser, rank_required, triggerNotif
|
||||||
@@ -36,6 +36,9 @@ def admin_page():
|
|||||||
if action == "importmodlist":
|
if action == "importmodlist":
|
||||||
task = importTopicList.delay()
|
task = importTopicList.delay()
|
||||||
return redirect(url_for("check_task", id=task.id, r=url_for("todo_topics_page")))
|
return redirect(url_for("check_task", id=task.id, r=url_for("todo_topics_page")))
|
||||||
|
elif action == "checkusers":
|
||||||
|
task = checkAllForumAccounts.delay()
|
||||||
|
return redirect(url_for("check_task", id=task.id, r=url_for("admin_page")))
|
||||||
elif action == "importscreenshots":
|
elif action == "importscreenshots":
|
||||||
packages = Package.query \
|
packages = Package.query \
|
||||||
.filter_by(soft_deleted=False) \
|
.filter_by(soft_deleted=False) \
|
||||||
@@ -27,7 +27,7 @@ from app.utils import rank_required
|
|||||||
@app.route("/tags/")
|
@app.route("/tags/")
|
||||||
@rank_required(UserRank.MODERATOR)
|
@rank_required(UserRank.MODERATOR)
|
||||||
def tag_list_page():
|
def tag_list_page():
|
||||||
return render_template("admin/tagslist.html", tags=Tag.query.order_by(db.asc(Tag.title)).all())
|
return render_template("admin/tags/list.html", tags=Tag.query.order_by(db.asc(Tag.title)).all())
|
||||||
|
|
||||||
class TagForm(FlaskForm):
|
class TagForm(FlaskForm):
|
||||||
title = StringField("Title", [InputRequired(), Length(3,100)])
|
title = StringField("Title", [InputRequired(), Length(3,100)])
|
||||||
101
app/views/admin/todo.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# 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 *
|
||||||
|
import flask_menu as menu
|
||||||
|
from app import app
|
||||||
|
from app.models import *
|
||||||
|
from app.querybuilder import QueryBuilder
|
||||||
|
|
||||||
|
@app.route("/todo/", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def todo_page():
|
||||||
|
canApproveNew = Permission.APPROVE_NEW.check(current_user)
|
||||||
|
canApproveRel = Permission.APPROVE_RELEASE.check(current_user)
|
||||||
|
canApproveScn = Permission.APPROVE_SCREENSHOT.check(current_user)
|
||||||
|
|
||||||
|
packages = None
|
||||||
|
if canApproveNew:
|
||||||
|
packages = Package.query.filter_by(approved=False, soft_deleted=False).all()
|
||||||
|
|
||||||
|
releases = None
|
||||||
|
if canApproveRel:
|
||||||
|
releases = PackageRelease.query.filter_by(approved=False).all()
|
||||||
|
|
||||||
|
screenshots = None
|
||||||
|
if canApproveScn:
|
||||||
|
screenshots = PackageScreenshot.query.filter_by(approved=False).all()
|
||||||
|
|
||||||
|
if not canApproveNew and not canApproveRel and not canApproveScn:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
if request.form["action"] == "screenshots_approve_all":
|
||||||
|
if not canApproveScn:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
PackageScreenshot.query.update({ "approved": True })
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for("todo_page"))
|
||||||
|
else:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
topic_query = ForumTopic.query \
|
||||||
|
.filter_by(discarded=False)
|
||||||
|
|
||||||
|
total_topics = topic_query.count()
|
||||||
|
topics_to_add = topic_query \
|
||||||
|
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
|
||||||
|
.count()
|
||||||
|
|
||||||
|
return render_template("todo/list.html", title="Reports and Work Queue",
|
||||||
|
packages=packages, releases=releases, screenshots=screenshots,
|
||||||
|
canApproveNew=canApproveNew, canApproveRel=canApproveRel, canApproveScn=canApproveScn,
|
||||||
|
topics_to_add=topics_to_add, total_topics=total_topics)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/todo/topics/")
|
||||||
|
@login_required
|
||||||
|
def todo_topics_page():
|
||||||
|
qb = QueryBuilder(request.args)
|
||||||
|
qb.setSortIfNone("date")
|
||||||
|
query = qb.buildTopicQuery()
|
||||||
|
|
||||||
|
tmp_q = ForumTopic.query
|
||||||
|
if not qb.show_discarded:
|
||||||
|
tmp_q = tmp_q.filter_by(discarded=False)
|
||||||
|
total = tmp_q.count()
|
||||||
|
topic_count = query.count()
|
||||||
|
|
||||||
|
page = int(request.args.get("page") or 1)
|
||||||
|
num = int(request.args.get("n") or 100)
|
||||||
|
if num > 100 and not current_user.rank.atLeast(UserRank.EDITOR):
|
||||||
|
num = 100
|
||||||
|
|
||||||
|
query = query.paginate(page, num, True)
|
||||||
|
next_url = url_for("todo_topics_page", page=query.next_num, query=qb.search, \
|
||||||
|
show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
|
||||||
|
if query.has_next else None
|
||||||
|
prev_url = url_for("todo_topics_page", page=query.prev_num, query=qb.search, \
|
||||||
|
show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
|
||||||
|
if query.has_prev else None
|
||||||
|
|
||||||
|
return render_template("todo/topics.html", topics=query.items, total=total, \
|
||||||
|
topic_count=topic_count, query=qb.search, show_discarded=qb.show_discarded, \
|
||||||
|
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, \
|
||||||
|
n=num, sort_by=qb.order_by)
|
||||||
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)
|
||||||
@@ -20,13 +20,16 @@ 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
|
from app.utils import is_package_page
|
||||||
from .packages import build_packages_query
|
from app.querybuilder import QueryBuilder
|
||||||
|
|
||||||
@app.route("/api/packages/")
|
@app.route("/api/packages/")
|
||||||
def api_packages_page():
|
def api_packages_page():
|
||||||
query, _ = build_packages_query()
|
qb = QueryBuilder(request.args)
|
||||||
pkgs = [package.getAsDictionaryShort(app.config["BASE_URL"]) \
|
query = qb.buildPackageQuery()
|
||||||
for package in query.all() if package.getDownloadRelease() is not None]
|
ver = qb.getMinetestVersion()
|
||||||
|
|
||||||
|
pkgs = [package.getAsDictionaryShort(app.config["BASE_URL"], version=ver) \
|
||||||
|
for package in query.all()]
|
||||||
return jsonify(pkgs)
|
return jsonify(pkgs)
|
||||||
|
|
||||||
@app.route("/api/packages/<author>/<name>/")
|
@app.route("/api/packages/<author>/<name>/")
|
||||||
@@ -37,7 +40,30 @@ def api_package_page(package):
|
|||||||
|
|
||||||
@app.route("/api/topics/")
|
@app.route("/api/topics/")
|
||||||
def api_topics_page():
|
def api_topics_page():
|
||||||
query = ForumTopic.query \
|
qb = QueryBuilder(request.args)
|
||||||
.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title))
|
query = qb.buildTopicQuery(show_added=True)
|
||||||
pkgs = [t.getAsDictionary() for t in query.all()]
|
return jsonify([t.getAsDictionary() for t in query.all()])
|
||||||
return jsonify(pkgs)
|
|
||||||
|
|
||||||
|
@app.route("/api/topic_discard/", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def topic_set_discard():
|
||||||
|
tid = request.args.get("tid")
|
||||||
|
discard = request.args.get("discard")
|
||||||
|
if tid is None or discard is None:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
topic = ForumTopic.query.get(tid)
|
||||||
|
if not topic.checkPerm(current_user, Permission.TOPIC_DISCARD):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
topic.discarded = discard == "true"
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify(topic.getAsDictionary())
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/minetest_versions/")
|
||||||
|
def api_minetest_versions_page():
|
||||||
|
return jsonify([{ "name": rel.name, "protocol_version": rel.protocol }\
|
||||||
|
for rel in MinetestRelease.query.all() if rel.getActual() is not None])
|
||||||
|
|||||||
@@ -15,329 +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 *
|
|
||||||
from flask.ext import 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_
|
|
||||||
|
|
||||||
def build_packages_query():
|
|
||||||
title = "Packages"
|
|
||||||
|
|
||||||
query = Package.query.filter_by(soft_deleted=False, approved=True)
|
|
||||||
|
|
||||||
# Filter by requested type(s)
|
|
||||||
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])
|
|
||||||
|
|
||||||
query = query.filter(Package.type.in_(types))
|
|
||||||
|
|
||||||
|
|
||||||
search = request.args.get("q")
|
|
||||||
if search is not None and search.strip() != "":
|
|
||||||
query = query.filter(Package.title.ilike('%' + search + '%'))
|
|
||||||
|
|
||||||
query = query.order_by(db.desc(Package.score))
|
|
||||||
|
|
||||||
return query, title
|
|
||||||
|
|
||||||
@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' })
|
|
||||||
@app.route("/packages/")
|
|
||||||
def packages_page():
|
|
||||||
if shouldReturnJson():
|
|
||||||
return redirect(url_for("api_packages_page"))
|
|
||||||
|
|
||||||
query, title = build_packages_query()
|
|
||||||
page = int(request.args.get("page") or 1)
|
|
||||||
num = min(42, 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
|
|
||||||
|
|
||||||
tags = Tag.query.all()
|
|
||||||
return render_template("packages/list.html", title=title, packages=query.items, \
|
|
||||||
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.asc(Package.title)) \
|
|
||||||
.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 = "error"
|
|
||||||
|
|
||||||
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 = "error"
|
|
||||||
|
|
||||||
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(), Length(0,1000)])
|
|
||||||
softdep_str = StringField("Soft Dependencies", [Optional(), Length(0,1000)])
|
|
||||||
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
|
|
||||||
|
|
||||||
if wasNew and package.repo is not None:
|
|
||||||
task = importRepoScreenshot.delay(package.id)
|
|
||||||
return redirect(url_for("check_task", id=task.id, r=package.getDetailsURL()))
|
|
||||||
|
|
||||||
return redirect(package.getDetailsURL())
|
|
||||||
|
|
||||||
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>/delete/", methods=["GET", "POST"])
|
|
||||||
@login_required
|
|
||||||
@is_package_page
|
|
||||||
def delete_package_page(package):
|
|
||||||
if request.method == "GET":
|
|
||||||
return render_template("packages/delete.html", package=package)
|
|
||||||
|
|
||||||
if not package.checkPerm(current_user, Permission.DELETE_PACKAGE):
|
|
||||||
flash("You don't have permission to do that.", "error")
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
from . import todo, screenshots, releases
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
from flask.ext import menu
|
|
||||||
from app import app
|
from app import app
|
||||||
from app.models import *
|
from app.models import *
|
||||||
|
|
||||||
|
|||||||
369
app/views/packages/packages.py
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
# 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.limit(5)
|
||||||
|
else:
|
||||||
|
return package.releases.filter_by(approved=True).limit(5)
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
elif package.approved and package.name != form.name.data and \
|
||||||
|
not package.checkPerm(current_user, Permission.CHANGE_NAME):
|
||||||
|
flash("Unable to change package name", "danger")
|
||||||
|
return redirect(url_for("create_edit_package_page", author=author, name=name))
|
||||||
|
|
||||||
|
else:
|
||||||
|
triggerNotif(package.author, current_user,
|
||||||
|
"{} edited".format(package.title), package.getDetailsURL())
|
||||||
|
|
||||||
|
form.populate_obj(package) # copy to row
|
||||||
|
|
||||||
|
if package.type== PackageType.TXP:
|
||||||
|
package.license = package.media_license
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
from flask import *
|
from flask import *
|
||||||
from flask_user import *
|
from flask_user import *
|
||||||
from flask.ext import menu
|
|
||||||
from app import app
|
from app import app
|
||||||
from app.models import *
|
from app.models import *
|
||||||
from app.tasks.importtasks import makeVCSRelease
|
from app.tasks.importtasks import makeVCSRelease
|
||||||
@@ -28,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):
|
||||||
@@ -41,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"])
|
||||||
@@ -64,6 +83,8 @@ 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()
|
||||||
db.session.add(rel)
|
db.session.add(rel)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -75,12 +96,15 @@ def create_release_page(package):
|
|||||||
|
|
||||||
return redirect(url_for("check_task", id=rel.task_id, r=rel.getEditURL()))
|
return redirect(url_for("check_task", id=rel.task_id, r=rel.getEditURL()))
|
||||||
else:
|
else:
|
||||||
uploadedPath = doFileUpload(form.fileUpload.data, ["zip"], "a zip file")
|
uploadedPath = doFileUpload(form.fileUpload.data, "zip", "a zip file")
|
||||||
if uploadedPath is not None:
|
if uploadedPath is not None:
|
||||||
rel = PackageRelease()
|
rel = PackageRelease()
|
||||||
rel.package = package
|
rel.package = package
|
||||||
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.approve(current_user)
|
||||||
db.session.add(rel)
|
db.session.add(rel)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -106,6 +130,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"])
|
||||||
@@ -129,6 +158,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
|
||||||
@@ -145,3 +176,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)
|
||||||
|
|||||||