Compare commits

..

155 Commits

Author SHA1 Message Date
rubenwardy
7e4eb29db7 Limit releases on package view 2019-03-29 21:01:19 +00:00
rubenwardy
93a74b7681 Fix release auto-approval 2019-03-29 20:52:08 +00:00
rubenwardy
2677e088a8 Fix small style issue on todo page 2019-03-29 20:33:15 +00:00
rubenwardy
0fd4984e5a Redesign todo page, add ability to Approve All screenshots 2019-03-29 20:32:13 +00:00
rubenwardy
896a65fd99 Fix progress bar total 2019-03-29 20:02:10 +00:00
rubenwardy
885209a614 Add unified topic search in QueryBuilder 2019-03-29 19:48:21 +00:00
rubenwardy
4c109d6bd3 Fix release being null in API when release is unapproved
Fixes #129
2019-03-13 14:37:27 +00:00
rubenwardy
9c2c8c21f1 Add content flag support in the API 2019-02-03 13:03:30 +00:00
rubenwardy
e40b247a97 Add OpenSearch and Google site search support 2019-02-02 17:05:18 +00:00
rubenwardy
a79cc758ed Add placeholder content ratings page 2019-01-30 17:57:56 +00:00
rubenwardy
bafd426eaf Add automatic approval of releases and screenshots 2019-01-29 18:30:30 +00:00
rubenwardy
36f9572cbb Fix replace problem in migration 2019-01-29 03:02:46 +00:00
rubenwardy
2586a11bcf Add fulltext search support 2019-01-29 03:00:01 +00:00
rubenwardy
d36138d5e1 Add version information to package page 2019-01-29 02:03:10 +00:00
rubenwardy
7810bb54e0 Add download counter to home page 2019-01-29 01:43:21 +00:00
rubenwardy
2844773e4d Fix wrong release ID returned by API on explicit protocol version 2019-01-29 01:29:49 +00:00
rubenwardy
23c406bff9 Add download counting 2019-01-29 00:49:44 +00:00
rubenwardy
0f3adda592 Improve spacing on bulk change releases 2019-01-29 00:27:26 +00:00
rubenwardy
441ed3beeb Add option to only change None entries with bulk change releases 2019-01-29 00:24:59 +00:00
rubenwardy
d1f5585fda Fix typos in text 2019-01-29 00:18:49 +00:00
rubenwardy
0fd3ed8f6b Fix bulk change form 2019-01-28 23:54:00 +00:00
rubenwardy
0e5c1f83ff Add MinetestRelease editor 2019-01-28 23:49:27 +00:00
rubenwardy
f112756b04 Disable fields in bulk change on checkbox 2019-01-28 23:40:31 +00:00
rubenwardy
f822027ec5 Hide create release fields depending on radio buttons 2019-01-28 23:17:00 +00:00
rubenwardy
034315d421 Add notes about min/max, and hide invalid options 2019-01-28 22:28:47 +00:00
rubenwardy
5cd8b35d1f Add ability to bulk change releases 2019-01-28 21:49:29 +00:00
rubenwardy
84b996c489 Add Minetest version checking to packages API 2019-01-28 21:33:50 +00:00
rubenwardy
d77403c0be Add min and max Minetest version support 2019-01-28 20:48:07 +00:00
rubenwardy
e9fe936aa9 Increase visibility of thread creation 2019-01-28 19:41:24 +00:00
rubenwardy
8afe17b984 Add comment ratelimiting, allow any member to open threads 2019-01-28 19:01:37 +00:00
rubenwardy
2691105513 Remove limit on provides field 2019-01-09 22:44:23 +00:00
rubenwardy
5f7efd4f31 Reduce README size 2019-01-09 22:35:11 +00:00
rubenwardy
7d52931a20 Add celery support to docker config 2019-01-09 22:29:32 +00:00
rubenwardy
a45df0e173 Add Docker support 2019-01-09 21:58:11 +00:00
rubenwardy
0db49efe4a Fix weird ordering of screenshots 2019-01-08 21:35:46 +00:00
rubenwardy
9639cf04f1 Improve views subfoldering 2019-01-08 17:37:33 +00:00
rubenwardy
9866e43b4b Split up packages/__init__.py 2019-01-08 17:17:36 +00:00
rubenwardy
014370ea06 Add email template 2019-01-04 19:17:04 +00:00
rubenwardy
fbf374ff5d Add manual email support 2019-01-04 17:57:00 +00:00
rubenwardy
a68ac9cb4d Add number of packages to bottom of homepage 2018-12-31 14:01:19 +00:00
rubenwardy
7943598528 Improve package edit page layout 2018-12-29 19:06:17 +00:00
rubenwardy
4bc8b58af7 Remove limit on dependency size 2018-12-29 19:00:13 +00:00
rubenwardy
ec0e89c21d Fix bug in package_create.js 2018-12-29 18:41:09 +00:00
rubenwardy
2975f94d9e Add short description tips 2018-12-29 15:57:16 +00:00
rubenwardy
a9a045eefd Fix wizard deleting values from topic create 2018-12-28 14:32:11 +00:00
rubenwardy
d09ede00fb Add sort toggle bar to topics list 2018-12-27 15:32:15 +00:00
rubenwardy
515248eb8b Add open link to forum topic ID field 2018-12-27 00:03:16 +00:00
rubenwardy
66ee706a6c Fix profile picture bugs 2018-12-25 23:02:49 +00:00
rubenwardy
d44178cb0c Fix relative links again 2018-12-25 20:26:36 +00:00
rubenwardy
c926a812d3 Fix relative links 2018-12-25 20:25:17 +00:00
rubenwardy
0b83d2f2b5 Add task to bulk import avatars from forum 2018-12-25 19:49:17 +00:00
rubenwardy
21960f2404 Add support for using forum profile pictures 2018-12-25 19:28:32 +00:00
rubenwardy
f94885a58f Fix gravatar link being a button 2018-12-25 18:36:02 +00:00
rubenwardy
f7d4b4bf6d Show placeholder message in unadded topics profile section when empty 2018-12-25 18:34:38 +00:00
rubenwardy
d04e060854 Improve profile pic styling on user profile page 2018-12-25 18:25:25 +00:00
rubenwardy
7801be3d39 Fix create button show logic in topic list 2018-12-25 18:15:11 +00:00
rubenwardy
b10660030a Fix params in topic list being lost on page change 2018-12-25 18:12:25 +00:00
rubenwardy
f5744f5188 Fix non-editors seeing create buttons for topics 2018-12-25 17:58:44 +00:00
rubenwardy
272be09ba1 Add links to topic lists in user dropdown 2018-12-25 17:56:51 +00:00
rubenwardy
09150a4dbb Allow users to discard their own topics 2018-12-25 17:51:29 +00:00
rubenwardy
c726f56b3e Fix bugs in topic todo 2018-12-25 16:43:41 +00:00
rubenwardy
daded6d193 Add sort by option to topic list 2018-12-25 16:40:19 +00:00
rubenwardy
b0a5980833 Add unlimited results toggle in topics list 2018-12-25 15:20:58 +00:00
rubenwardy
1eaed55bc6 Add ability to unapprove package from GUI 2018-12-25 15:13:30 +00:00
rubenwardy
c2265313d8 Truncate long links in topic list 2018-12-24 00:37:52 +00:00
rubenwardy
49d5a123e5 Add progress bar to topics page 2018-12-24 00:27:55 +00:00
rubenwardy
c79c970171 Fix .wiptopic affecting buttons 2018-12-24 00:13:45 +00:00
rubenwardy
fa0506f58a Add create date to topic list 2018-12-24 00:11:15 +00:00
rubenwardy
50889ccca5 Add topic searching and topic discarding 2018-12-23 23:54:20 +00:00
rubenwardy
b8ca5d24c5 Add pagination and search to topics 2018-12-23 18:04:56 +00:00
rubenwardy
63969529ad Add random feature 2018-12-23 17:34:44 +00:00
rubenwardy
08434300d8 Rename "I'm feeling lucky" to "First" 2018-12-23 17:11:52 +00:00
rubenwardy
86566bcd39 Improve markdown editor style, switch to EasyMDE, add to comment reply fields 2018-12-23 17:02:02 +00:00
rubenwardy
a7fcce4448 Improve package grid style 2018-12-23 16:28:15 +00:00
rubenwardy
366ed9913e Update to Flask 1.0 2018-12-22 23:03:38 +00:00
rubenwardy
79f4e16286 Improve style of forms 2018-12-22 22:29:30 +00:00
rubenwardy
137a6928bc Replace Popular with Top Mods 2018-12-22 21:41:30 +00:00
rubenwardy
de9135f44f Decrease package tile rounding 2018-12-22 21:26:00 +00:00
rubenwardy
31f57e1f12 Add multi-level thumbnails 2018-12-22 21:20:25 +00:00
rubenwardy
89cae279cd Add top games to home page 2018-12-22 21:13:56 +00:00
rubenwardy
fd901726b0 Add sort and order query params to package list 2018-12-22 21:09:29 +00:00
rubenwardy
5f40d68441 Improve home page 2018-12-22 21:03:01 +00:00
rubenwardy
8eedbf64a4 Improve package grid 2018-12-22 20:49:19 +00:00
rubenwardy
c551201f79 Improve thread styling 2018-12-22 20:25:22 +00:00
rubenwardy
a21a5c24d8 Add pagination styling 2018-12-22 13:33:27 +00:00
rubenwardy
0a969e597b Fix flask_user template 2018-12-22 13:22:08 +00:00
rubenwardy
a1700b5f7e Fix small issues 2018-12-22 13:17:10 +00:00
rubenwardy
d61f77a805 Improve claim page 2018-12-22 13:14:08 +00:00
rubenwardy
f6384e2e15 Merge minetest/bootstrap into master 2018-12-22 12:39:35 +00:00
rubenwardy
09a201759b Improve card and user profile formatting 2018-12-22 12:38:03 +00:00
rubenwardy
5dcff01436 Improve button colours and position in package view 2018-12-22 12:20:26 +00:00
rubenwardy
f355721cdb Fix button style in policy alert 2018-12-22 12:10:34 +00:00
rubenwardy
a25f77ce3c Allow pasting of forum URLs in input box 2018-12-22 12:08:21 +00:00
rubenwardy
692628653c Improve package creation form 2018-12-22 12:00:20 +00:00
rubenwardy
35f798c862 Improve button layouts 2018-12-21 20:59:12 +00:00
rubenwardy
3a0e0377f9 Improve button placement 2018-12-21 17:00:16 +00:00
rubenwardy
c6a26786ec Improve package page style again 2018-12-21 16:55:22 +00:00
rubenwardy
e5cb7a3721 Improve jumbotron 2018-12-21 16:36:54 +00:00
rubenwardy
03a155c17b Improve package page style further 2018-12-21 16:06:52 +00:00
rubenwardy
266d579e9d Move cards to sidebar 2018-12-21 16:00:18 +00:00
rubenwardy
c97eefc7b2 Format package page 2018-12-21 15:58:43 +00:00
rubenwardy
9da6b45cc3 Add bootstrap, change base template 2018-12-21 14:45:54 +00:00
rubenwardy
c9bf7a3245 Add skip button to importer 2018-12-21 14:10:46 +00:00
rubenwardy
dd368d87aa Fix various issues 2018-12-21 14:02:57 +00:00
rubenwardy
e5b279d013 Fix capitalisation in API 2018-11-25 13:21:24 +00:00
rubenwardy
8ca3437689 Revert "Add flask-admin"
This reverts commit dd6257a0a0.
2018-11-14 00:56:28 +00:00
ClobberXD
aeafb8247f Fix grammar in jumbotron 2018-11-09 10:58:39 +00:00
rubenwardy
75bab28d82 Add celery beat for topic import 2018-10-09 21:49:26 +01:00
rubenwardy
328d05bdf6 Add option to hide non-free packages in API 2018-10-03 17:06:16 +01:00
rubenwardy
2229b32c90 Add SimpleMDE to edit markdown 2018-09-14 23:10:30 +01:00
rubenwardy
ed409df323 Update scoring algorithm to take licenses and screenshots into account 2018-09-03 01:50:53 +01:00
rubenwardy
b8decafd75 Add WTFPL warning on new packages 2018-09-03 01:40:48 +01:00
rubenwardy
5aaee010c1 Fix accidental regression in phpbbparser 2018-08-25 21:25:12 +01:00
rubenwardy
a01fe4043e Fix owner not seeing create link in 'more content' list 2018-08-25 19:12:42 +01:00
rubenwardy
e0ef0e018d Fix permissions check in 'more content' list 2018-08-25 19:10:11 +01:00
rubenwardy
0210a3e601 Add I'm feeling lucky 2018-08-25 18:50:05 +01:00
rubenwardy
36000b1592 Add list of relevant forum topics to last page of results 2018-08-25 18:20:45 +01:00
rubenwardy
b296b9b299 Fix two bugs 2018-07-30 00:42:11 +01:00
rubenwardy
dd6257a0a0 Add flask-admin 2018-07-30 00:16:22 +01:00
rubenwardy
23b324cc9c Update policy: remote too much detail about name exceptions 2018-07-29 17:34:06 +01:00
rubenwardy
f61f9e8654 Fix typo in template path for tags list 2018-07-28 19:20:49 +01:00
rubenwardy
286207ffa2 Add release specific download URL 2018-07-28 18:33:36 +01:00
rubenwardy
a3e82ad42f Add support for multiple types in packages list 2018-07-28 18:12:22 +01:00
rubenwardy
404200b8f0 Fix license editor setting is_foss to true on edit 2018-07-28 17:47:08 +01:00
rubenwardy
dfecf470fa Redirect to license list on save 2018-07-28 17:34:00 +01:00
rubenwardy
c737f58fc0 Update policy 2018-07-28 17:31:27 +01:00
rubenwardy
ab59b7f4ba Prevent approval of packages with an 'Other' license 2018-07-28 17:30:43 +01:00
rubenwardy
514a24e2c4 Add license editor 2018-07-28 17:26:28 +01:00
rubenwardy
742a327cbb Add warning on other license 2018-07-28 16:46:46 +01:00
rubenwardy
864e067412 Fix typo in running task link on edit release page 2018-07-28 16:06:23 +01:00
rubenwardy
1c7a192854 Add link to original screenshot in edit screenshot page 2018-07-28 16:05:09 +01:00
rubenwardy
c298f64295 Fix thumbnails
Fixes #97
2018-07-28 16:03:48 +01:00
rubenwardy
e82166f87e Add subscribe/unsubscribe button 2018-07-28 15:30:59 +01:00
rubenwardy
909a2b4ce9 Add support for post-approval threads 2018-07-28 15:19:30 +01:00
rubenwardy
df8d05f09d Add thread list to package view 2018-07-28 15:08:08 +01:00
rubenwardy
8c3b1c8c95 Add commit hash to releases 2018-07-28 14:48:03 +01:00
rubenwardy
ecdb755dd3 Remove unused release approval checklist 2018-07-28 14:29:40 +01:00
rubenwardy
901e115a21 Prevent trusted users from approving their own packages 2018-07-28 14:25:51 +01:00
rubenwardy
d4c2166019 Add default title to screenshots 2018-07-28 14:13:26 +01:00
rubenwardy
cbc98ef624 Enable markdown in comments 2018-07-28 14:07:29 +01:00
nOOb3167
794bc8a018 Add default password to admin user 2018-07-24 20:39:48 +01:00
nOOb3167
34900222dc Add upper version limit to Flask requirement 2018-07-24 20:39:29 +01:00
rubenwardy
f9a1d25c57 Fix unreadable dropdown text
Fixes #74
2018-07-24 20:33:26 +01:00
rubenwardy
8fe7bcfb71 Fix forum topic scanner only scanning one page 2018-07-24 20:11:48 +01:00
rubenwardy
28ee65809e Fix 2 filter_by bugs
Fixes #101
2018-07-13 21:28:11 +01:00
rubenwardy
1b42f3310a Add admin feature to bulk create releases 2018-07-08 17:28:39 +01:00
rubenwardy
8d2144895e Fix creation of corrupt zip files
Fixes #103
2018-07-08 17:10:38 +01:00
rubenwardy
13837ce88b Add forum topic validation 2018-07-07 00:28:27 +01:00
rubenwardy
73c65e3561 Add topics API 2018-07-07 00:01:56 +01:00
rubenwardy
67a229b8a3 Add WIP forum topic support 2018-07-06 23:17:56 +01:00
rubenwardy
9dd3570a52 Add email on Flask error 2018-07-06 22:55:55 +01:00
rubenwardy
a6c8b12cdd Reorder new and popular, change number of packages in each 2018-07-04 01:20:55 +01:00
rubenwardy
7813c766ac Add package scores and split homepage into new and popular 2018-07-04 01:08:34 +01:00
rubenwardy
9fc9826d30 Clarify home page on subject of free software 2018-07-04 00:42:46 +01:00
rubenwardy
19e1ed8b32 Implement forum parser to increase accuracy 2018-07-04 00:38:51 +01:00
115 changed files with 14142 additions and 2068 deletions

6
.gitignore vendored
View File

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

17
Dockerfile Normal file
View File

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

View File

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

View File

@@ -17,9 +17,10 @@
from flask import *
from flask_user import *
from flask_gravatar import Gravatar
import flask_menu as menu
from flask_mail import Mail
from flask.ext import markdown
from flaskext.markdown import Markdown
from flask_github import GitHub
from flask_wtf.csrf import CsrfProtect
from flask_flatpages import FlatPages
@@ -31,11 +32,23 @@ app.config["FLATPAGES_EXTENSION"] = ".md"
app.config.from_pyfile(os.environ["FLASK_CONFIG"])
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)
csrf = CsrfProtect(app)
mail = Mail(app)
pages = FlatPages(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:
from .maillogger import register_mail_error_handler
register_mail_error_handler(app, mail)
from . import models, tasks
from .views import *

View File

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

View File

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

View File

@@ -34,6 +34,8 @@ title: Ranks and Permissions
<th>N</th>
<th>Y</th>
<th>N</th>
<th>Y</th>
<th>N</th>
</tr>
</thead>
<tbody>
@@ -58,7 +60,7 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
@@ -101,7 +103,7 @@ title: Ranks and Permissions
<td>Approve Screenshot</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
@@ -187,6 +189,21 @@ title: Ranks and Permissions
<th></th> <!-- admin -->
<th></th>
</tr>
<tr>
<td>See Private Thread</td>
<th></th> <!-- new -->
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
<th></th>
<th></th> <!-- admin -->
<th></th>
</tr>
<tr>
<td>Set Email</td>
<th></th> <!-- new -->

View File

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

View File

@@ -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)

View File

@@ -1,9 +1,23 @@
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
</div>
## 0. Overview
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
the listings and to combat abuse.
* No inappropriate content.
* Content must be playable/useful, but not necessarily finished.
* Don't use the name of another mod unless your mod is a fork or reimplementation.
* Licenses must allow derivatives, redistribution, and must not discriminate.
* Don't put promotions are advertisements in package listings, except for
donation and personal website links which are permitted in the long description.
## 1. General
It is not permitted to submit abusive, obscene, vulgar, slanderous, hateful,
@@ -22,14 +36,17 @@ 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
clearly that it does in the package meta.
ContentDB should only currently contain playable content, ie: stuff that would
be in Mod Releases and Game Releases. Please don't upload any Work In Progress (WIP)
things. This will probably change in future if/when an "early access" feature is
added.
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
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
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
make your package useful.
Adding non-player facing mods, such as libraries and server tools, is perfectly fine.
ContentDB isn't just for player-facing things, and adding libraries allows them to be
installed when a mod depends on it.
Adding non-player facing mods, such as libraries and server tools, is perfectly fine
and encouraged. ContentDB isn't just for player-facing things, and adding
libraries allows them to be installed when a mod depends on it.
## 3. Technical Names
@@ -48,6 +65,8 @@ 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
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.
### 3.2 Mod Forks and Reimplementations
An exception to the above is that mods are allowed to have the same name as a
@@ -66,24 +85,31 @@ Please ensure that you correctly credit any resources (code, assets, or otherwis
that you have used in your package.
**The use of licenses which do not allow derivatives or redistribution is not
permitted. This includes CC-ND (No-Derivatives) and lots of closed source licenses.**
permitted. This includes CC-ND (No-Derivatives) and lots of closed source licenses.
The use of licenses which discriminate between groups of people or forbid the use
of the content on servers or singleplayer is also not permitted.**
However, closed sourced licenses are allowed if they allow the above.
If the license you use is not on the list then please choose the correct "Other"
option.
If the license you use is not on the list then please select "Other", and we'll
get around to adding it.
Please note that the definitions of "free" and "non-free" is the same as that
of the [Free Software Foundation](https://www.gnu.org/philosophy/free-sw.en.html).
### 4.2 Recommended Licenses
It is highly recommended that you use a free and open source software license.
FOSS licenses result in a sharing community and will increase the number of potential users your package has.
Using a closed source license will result in your package being massively penalised in the search results and package lists.
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
for media, such as a Creative Commons license.
The use of WTFPL is discouraged as it doesn't contain a valid warranty disclaimer,
and also includes swearing which dissuades teachers from using your content.
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 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.

109
app/maillogger.py Normal file
View File

@@ -0,0 +1,109 @@
import logging
from enum import Enum
from app.tasks.emails import sendEmailRaw
def _has_newline(line):
"""Used by has_bad_header to check for \\r or \\n"""
if line and ("\r" in line or "\n" in line):
return True
return False
def _is_bad_subject(subject):
"""Copied from: flask_mail.py class Message def has_bad_headers"""
if _has_newline(subject):
for linenum, line in enumerate(subject.split("\r\n")):
if not line:
return True
if linenum > 0 and line[0] not in "\t ":
return True
if _has_newline(line):
return True
if len(line.strip()) == 0:
return True
return False
class FlaskMailSubjectFormatter(logging.Formatter):
def format(self, record):
record.message = record.getMessage()
if self.usesTime():
record.asctime = self.formatTime(record, self.datefmt)
s = self.formatMessage(record)
return s
class FlaskMailTextFormatter(logging.Formatter):
pass
# TODO: hier nog niet tevreden over (vooral logger.error(..., exc_info, stack_info))
class FlaskMailHTMLFormatter(logging.Formatter):
pre_template = "<h1>%s</h1><pre>%s</pre>"
def formatException(self, exc_info):
formatted_exception = logging.Handler.formatException(self, exc_info)
return FlaskMailHTMLFormatter.pre_template % ("Exception information", formatted_exception)
def formatStack(self, stack_info):
return FlaskMailHTMLFormatter.pre_template % ("<h1>Stack information</h1><pre>%s</pre>", stack_info)
# see: https://github.com/python/cpython/blob/3.6/Lib/logging/__init__.py (class Handler)
class FlaskMailHandler(logging.Handler):
def __init__(self, mailer, subject_template, level=logging.NOTSET):
logging.Handler.__init__(self, level)
self.mailer = mailer
self.send_to = mailer.app.config["MAIL_UTILS_ERROR_SEND_TO"]
self.subject_template = subject_template
self.html_formatter = None
def setFormatter(self, text_fmt, html_fmt=None):
"""
Set the formatters for this handler. Provide at least one formatter.
When no text_fmt is provided, no text-part is created for the email body.
"""
assert (text_fmt, html_fmt) != (None, None), "At least one formatter should be provided"
if type(text_fmt)==str:
text_fmt = FlaskMailTextFormatter(text_fmt)
self.formatter = text_fmt
if type(html_fmt)==str:
html_fmt = FlaskMailHTMLFormatter(html_fmt)
self.html_formatter = html_fmt
def getSubject(self, record):
fmt = FlaskMailSubjectFormatter(self.subject_template)
subject = fmt.format(record)
#Since templates can cause header problems, and we rather have a incomplete email then an error, we fix this
if _is_bad_subject(subject):
subject="FlaskMailHandler log-entry from %s [original subject is replaced, because it would result in a bad header]" % self.mailer.app.name
return subject
def emit(self, record):
text = self.format(record) if self.formatter else None
html = self.html_formatter.format(record) if self.html_formatter else None
sendEmailRaw.delay(self.send_to, self.getSubject(record), text, html)
def register_mail_error_handler(app, mailer):
subject_template = "ContentDB crashed (%(module)s > %(funcName)s)"
text_template = """
Message type: %(levelname)s
Location: %(pathname)s:%(lineno)d
Module: %(module)s
Function: %(funcName)s
Time: %(asctime)s
Message:
%(message)s"""
html_template = """
<style>th { text-align: right}</style><table>
<tr><th>Message type:</th><td>%(levelname)s</td></tr>
<tr> <th>Location:</th><td>%(pathname)s:%(lineno)d</td></tr>
<tr> <th>Module:</th><td>%(module)s</td></tr>
<tr> <th>Function:</th><td>%(funcName)s</td></tr>
<tr> <th>Time:</th><td>%(asctime)s</td></tr>
</table>
<h2>Message</h2>
<pre>%(message)s</pre>"""
import logging
mail_handler = FlaskMailHandler(mailer, subject_template)
mail_handler.setLevel(logging.ERROR)
mail_handler.setFormatter(text_template, html_template)
app.logger.addHandler(mail_handler)

View File

@@ -15,19 +15,29 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import Flask, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import enum, datetime
from app import app, gravatar
from urllib.parse import urlparse
from app import app
from datetime import datetime
from sqlalchemy.orm import validates
from flask import Flask, url_for
from flask_sqlalchemy import SQLAlchemy, BaseQuery
from flask_migrate import Migrate
from flask_user import login_required, UserManager, UserMixin, SQLAlchemyAdapter
import enum
from sqlalchemy.orm import validates
from sqlalchemy_searchable import SearchQueryMixin
from sqlalchemy_utils.types import TSVectorType
from sqlalchemy_searchable import make_searchable
# Initialise database
db = SQLAlchemy(app)
migrate = Migrate(app, db)
make_searchable(db.metadata)
class ArticleQuery(BaseQuery, SearchQueryMixin):
pass
class UserRank(enum.Enum):
@@ -77,6 +87,9 @@ class Permission(enum.Enum):
CHANGE_EMAIL = "CHANGE_EMAIL"
EDIT_EDITREQUEST = "EDIT_EDITREQUEST"
SEE_THREAD = "SEE_THREAD"
CREATE_THREAD = "CREATE_THREAD"
UNAPPROVE_PACKAGE = "UNAPPROVE_PACKAGE"
TOPIC_DISCARD = "TOPIC_DISCARD"
# Only return true if the permission is valid for *all* contexts
# See Package.checkPerm for package-specific contexts
@@ -85,34 +98,36 @@ class Permission(enum.Enum):
return False
if self == Permission.APPROVE_NEW or \
self == Permission.APPROVE_CHANGES or \
self == Permission.APPROVE_RELEASE or \
self == Permission.APPROVE_SCREENSHOT:
self == Permission.APPROVE_CHANGES or \
self == Permission.APPROVE_RELEASE or \
self == Permission.APPROVE_SCREENSHOT or \
self == Permission.SEE_THREAD:
return user.rank.atLeast(UserRank.EDITOR)
else:
raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
id = db.Column(db.Integer, primary_key=True)
# User authentication information
username = db.Column(db.String(50, collation="NOCASE"), nullable=False, unique=True, index=True)
password = db.Column(db.String(255), nullable=True)
username = db.Column(db.String(50, collation="NOCASE"), nullable=False, unique=True, index=True)
password = db.Column(db.String(255), nullable=True)
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
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)
# User email information
email = db.Column(db.String(255), nullable=True, unique=True)
confirmed_at = db.Column(db.DateTime())
email = db.Column(db.String(255), nullable=True, unique=True)
confirmed_at = db.Column(db.DateTime())
# User information
active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
display_name = db.Column(db.String(100), nullable=False, server_default="")
profile_pic = db.Column(db.String(255), nullable=True, server_default=None)
active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
display_name = db.Column(db.String(100), nullable=False, server_default="")
# Content
notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id")
@@ -124,8 +139,6 @@ class User(db.Model, UserMixin):
replies = db.relationship("ThreadReply", backref="author", lazy="dynamic")
def __init__(self, username, active=False, email=None, password=None):
import datetime
self.username = username
self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
self.display_name = username
@@ -142,6 +155,12 @@ class User(db.Model, UserMixin):
def isClaimed(self):
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):
if not user.is_authenticated:
return False
@@ -161,6 +180,16 @@ class User(db.Model, UserMixin):
else:
raise Exception("Permission {} is not related to users".format(perm.name))
def canCommentRL(self):
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
return ThreadReply.query.filter_by(author=self) \
.filter(ThreadReply.created_at > hour_ago).count() < 4
def canOpenThreadRL(self):
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
return Thread.query.filter_by(author=self) \
.filter(Thread.created_at > hour_ago).count() < 2
class UserEmailVerification(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
@@ -209,6 +238,13 @@ class PackageType(enum.Enum):
def __str__(self):
return self.name
@classmethod
def get(cls, name):
try:
return PackageType[name.upper()]
except KeyError:
return None
@classmethod
def choices(cls):
return [(choice, choice.value) for choice in cls]
@@ -221,7 +257,7 @@ class PackageType(enum.Enum):
class PackagePropertyKey(enum.Enum):
name = "Name"
title = "Title"
shortDesc = "Short Description"
short_desc = "Short Description"
desc = "Description"
type = "Type"
license = "License"
@@ -318,18 +354,21 @@ class Dependency(db.Model):
return retval
class Package(db.Model):
query_class = ArticleQuery
id = db.Column(db.Integer, primary_key=True)
# Basic details
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
name = db.Column(db.String(100), nullable=False)
title = db.Column(db.String(100), nullable=False)
shortDesc = db.Column(db.String(200), nullable=False)
desc = db.Column(db.Text, nullable=True)
title = db.Column(db.Unicode(100), nullable=False)
short_desc = db.Column(db.Unicode(200), nullable=False)
desc = db.Column(db.UnicodeText, nullable=True)
type = db.Column(db.Enum(PackageType))
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
search_vector = db.Column(TSVectorType("title", "short_desc", "desc"))
license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
license = db.relationship("License", foreign_keys=[license_id])
@@ -339,6 +378,8 @@ class Package(db.Model):
approved = db.Column(db.Boolean, nullable=False, default=False)
soft_deleted = db.Column(db.Boolean, nullable=False, default=False)
score = db.Column(db.Float, nullable=False, default=0)
review_thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=True, default=None)
review_thread = db.relationship("Thread", foreign_keys=[review_thread_id])
@@ -360,7 +401,7 @@ class Package(db.Model):
lazy="dynamic", order_by=db.desc("package_release_releaseDate"))
screenshots = db.relationship("PackageScreenshot", backref="package",
lazy="dynamic")
lazy="dynamic", order_by=db.asc("package_screenshot_id"))
requests = db.relationship("EditRequest", backref="package",
lazy="dynamic")
@@ -376,35 +417,38 @@ class Package(db.Model):
for e in PackagePropertyKey:
setattr(self, e.name, getattr(package, e.name))
def getAsDictionaryShort(self, base_url):
tnurl = self.getThumbnailURL()
def getAsDictionaryShort(self, base_url, version=None, protonum=None):
tnurl = self.getThumbnailURL(1)
release = self.getDownloadRelease(version=version, protonum=protonum)
return {
"name": self.name,
"title": self.title,
"author": self.author.display_name,
"shortDesc": self.shortDesc,
"short_description": self.short_desc,
"type": self.type.toName(),
"release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
"thumbnail": (base_url + tnurl) if tnurl is not None else None
"release": release and release.id,
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
"score": round(self.score * 10) / 10
}
def getAsDictionary(self, base_url):
tnurl = self.getThumbnailURL()
def getAsDictionary(self, base_url, version=None, protonum=None):
tnurl = self.getThumbnailURL(1)
release = self.getDownloadRelease(version=version, protonum=protonum)
return {
"author": self.author.display_name,
"name": self.name,
"title": self.title,
"shortDesc": self.shortDesc,
"short_description": self.short_desc,
"desc": self.desc,
"type": self.type.toName(),
"createdAt": self.created_at,
"created_at": self.created_at,
"license": self.license.name,
"mediaLicense": self.media_license.name,
"media_license": self.media_license.name,
"repo": self.repo,
"website": self.website,
"issueTracker": self.issueTracker,
"issue_tracker": self.issueTracker,
"forums": self.forums,
"provides": [x.name for x in self.provides],
@@ -412,15 +456,17 @@ class Package(db.Model):
"screenshots": [base_url + ss.url for ss in self.screenshots],
"url": base_url + self.getDownloadURL(),
"release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None
"release": release and release.id,
"score": round(self.score * 10) / 10
}
def getThumbnailURL(self):
screenshot = self.screenshots.filter_by(approved=True).first()
return screenshot.getThumbnailURL() if screenshot is not None else None
def getThumbnailURL(self, level=2):
screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first()
return screenshot.getThumbnailURL(level) if screenshot is not None else None
def getMainScreenshotURL(self):
screenshot = self.screenshots.filter_by(approved=True).first()
screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first()
return screenshot.url if screenshot is not None else None
def getDetailsURL(self):
@@ -435,8 +481,8 @@ class Package(db.Model):
return url_for("approve_package_page",
author=self.author.username, name=self.name)
def getDeleteURL(self):
return url_for("delete_package_page",
def getRemoveURL(self):
return url_for("remove_package_page",
author=self.author.username, name=self.name)
def getNewScreenshotURL(self):
@@ -451,17 +497,37 @@ class Package(db.Model):
return url_for("create_edit_editrequest_page",
author=self.author.username, name=self.name)
def getBulkReleaseURL(self):
return url_for("bulk_change_release_page",
author=self.author.username, name=self.name)
def getDownloadURL(self):
return url_for("package_download_page",
author=self.author.username, name=self.name)
def getDownloadRelease(self):
def getDownloadRelease(self, 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:
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 None
def getDownloadCount(self):
counter = 0
for release in self.releases:
counter += release.downloads
return counter
def checkPerm(self, user, perm):
if not user.is_authenticated:
return False
@@ -473,6 +539,9 @@ class Package(db.Model):
isOwner = user == self.author
if perm == Permission.CREATE_THREAD:
return user.rank.atLeast(UserRank.MEMBER)
# Members can edit their own packages, and editors can edit any packages
if perm == Permission.MAKE_RELEASE or perm == Permission.ADD_SCREENSHOTS:
return isOwner or user.rank.atLeast(UserRank.EDITOR)
@@ -483,21 +552,38 @@ class Package(db.Model):
else:
return user.rank.atLeast(UserRank.EDITOR)
# Editors can change authors
elif perm == Permission.CHANGE_AUTHOR:
# Editors can change authors and approve new packages
elif perm == Permission.APPROVE_NEW or perm == Permission.CHANGE_AUTHOR:
return user.rank.atLeast(UserRank.EDITOR)
elif perm == Permission.APPROVE_NEW or perm == Permission.APPROVE_RELEASE \
or perm == Permission.APPROVE_SCREENSHOT:
elif perm == Permission.APPROVE_RELEASE or perm == Permission.APPROVE_SCREENSHOT:
return user.rank.atLeast(UserRank.TRUSTED_MEMBER if isOwner else UserRank.EDITOR)
# 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)
else:
raise Exception("Permission {} is not related to packages".format(perm.name))
def recalcScore(self):
self.score = 10
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.getMainScreenshotURL() is None:
self.score *= 0.8
if not self.license.is_foss or not self.media_license.is_foss:
self.score *= 0.1
class MetaPackage(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
@@ -562,15 +648,36 @@ class Tag(db.Model):
regex = re.compile("[^a-z_]")
self.name = regex.sub("", self.title.lower().replace(" ", "_"))
class MinetestRelease(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
protocol = db.Column(db.Integer, nullable=False, default=0)
def __init__(self, name=None):
self.name = name
def getActual(self):
return None if self.name == "None" else self
class PackageRelease(db.Model):
id = db.Column(db.Integer, primary_key=True)
package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
title = db.Column(db.String(100), nullable=False)
releaseDate = db.Column(db.DateTime, nullable=False)
releaseDate = db.Column(db.DateTime, nullable=False)
url = db.Column(db.String(200), nullable=False)
approved = db.Column(db.Boolean, nullable=False, default=False)
task_id = db.Column(db.String(37), nullable=True)
commit_hash = db.Column(db.String(41), nullable=True, default=None)
downloads = db.Column(db.Integer, nullable=False, default=0)
min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])
max_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
max_rel = db.relationship("MinetestRelease", foreign_keys=[max_rel_id])
def getEditURL(self):
@@ -579,8 +686,32 @@ class PackageRelease(db.Model):
name=self.package.name,
id=self.id)
def getDownloadURL(self):
return url_for("download_release_page",
author=self.package.author.username,
name=self.package.name,
id=self.id)
def __init__(self):
self.releaseDate = datetime.now()
self.releaseDate = datetime.datetime.now()
def approve(self, user):
if 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):
id = db.Column(db.Integer, primary_key=True)
package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
recommend = db.Column(db.Boolean, nullable=False, default=True)
class PackageScreenshot(db.Model):
id = db.Column(db.Integer, primary_key=True)
@@ -596,8 +727,10 @@ class PackageScreenshot(db.Model):
name=self.package.name,
id=self.id)
def getThumbnailURL(self):
return self.url.replace("/uploads/", "/thumbnails/350x233/")
def getThumbnailURL(self, level=2):
return self.url.replace("/uploads/", ("/thumbnails/{:d}/").format(level))
class EditRequest(db.Model):
id = db.Column(db.Integer, primary_key=True)
@@ -702,13 +835,22 @@ class Thread(db.Model):
title = db.Column(db.String(100), nullable=False)
private = db.Column(db.Boolean, server_default="0")
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
replies = db.relationship("ThreadReply", backref="thread", lazy="dynamic")
watchers = db.relationship("User", secondary=watchers, lazy="subquery", \
backref=db.backref("watching", lazy=True))
def getSubscribeURL(self):
return url_for("thread_subscribe_page",
id=self.id)
def getUnsubscribeURL(self):
return url_for("thread_unsubscribe_page",
id=self.id)
def checkPerm(self, user, perm):
if not user.is_authenticated:
return not self.private
@@ -731,11 +873,7 @@ class ThreadReply(db.Model):
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
comment = db.Column(db.String(500), nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \
@@ -743,29 +881,64 @@ REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \
"digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net", \
"imageshack.com", "imgur.com"]
class KrockForumTopic(db.Model):
class ForumTopic(db.Model):
topic_id = db.Column(db.Integer, primary_key=True, autoincrement=False)
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
author = db.relationship("User")
ttype = db.Column(db.Integer, nullable=False)
wip = db.Column(db.Boolean, server_default="0")
discarded = db.Column(db.Boolean, server_default="0")
type = db.Column(db.Enum(PackageType), nullable=False)
title = db.Column(db.String(200), nullable=False)
name = db.Column(db.String(30), nullable=True)
link = db.Column(db.String(200), nullable=True)
def getType(self):
if self.ttype == 1 or self.ttype == 2:
return PackageType.MOD
elif self.ttype == 6:
return PackageType.GAME
posts = db.Column(db.Integer, nullable=False)
views = db.Column(db.Integer, nullable=False)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
def getRepoURL(self):
if self.link is None:
return None
for item in REPO_BLACKLIST:
if item in self.link:
return None
return self.link.replace("repo.or.cz/w/", "repo.or.cz/")
def getAsDictionary(self):
return {
"author": self.author.username,
"name": self.name,
"type": self.type.toName(),
"title": self.title,
"id": self.topic_id,
"link": self.link,
"posts": self.posts,
"views": self.views,
"is_wip": self.wip,
"discarded": self.discarded,
"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
db_adapter = SQLAlchemyAdapter(db, User) # Register the User model

9681
app/public/static/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

7
app/public/static/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
app/public/static/easymde.min.css vendored Normal file

File diff suppressed because one or more lines are too long

7
app/public/static/easymde.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -11,6 +11,8 @@ $(function() {
$(".pkg_meta").hide()
$(".pkg_wiz_1").show()
$("#pkg_wiz_1_skip").click(finish)
$("#pkg_wiz_1_next").click(function() {
const repoURL = $("#repo").val();
if (repoURL.trim() != "") {
@@ -18,41 +20,38 @@ $(function() {
$(".pkg_wiz_2").show()
$(".pkg_repo").hide()
function setSpecial(id, value) {
if (value != "") {
function setField(id, value) {
if (value && value != "") {
var ele = $(id);
ele.val(value);
ele.trigger("change")
ele.trigger("change");
}
}
performTask("/tasks/getmeta/new/?url=" + encodeURI(repoURL)).then(function(result) {
$("#name").val(result.name)
setSpecial("#provides_str", result.provides)
$("#title").val(result.title)
$("#repo").val(result.repo || repoURL)
$("#issueTracker").val(result.issueTracker)
$("#desc").val(result.description)
$("#shortDesc").val(result.short_description)
setSpecial("#harddep_str", result.depends)
setSpecial("#softdep_str", result.optional_depends)
$("#shortDesc").val(result.short_description)
if (result.forumId) {
$("#forums").val(result.forumId)
}
setField("#name", result.name);
setField("#provides_str", result.provides);
setField("#title", result.title);
setField("#repo", result.repo || repoURL);
setField("#issueTracker", result.issueTracker);
setField("#desc", result.description);
setField("#short_desc", result.short_description);
setField("#harddep_str", result.depends);
setField("#softdep_str", result.optional_depends);
setField("#short_desc", result.short_description);
setField("#forums", result.forumId);
if (result.type && result.type.length > 2) {
$("#type").val(result.type)
$("#type").val(result.type);
}
finish()
finish();
}).catch(function(e) {
alert(e)
$(".pkg_wiz_1").show()
$(".pkg_wiz_2").hide()
$(".pkg_repo").show()
alert(e);
$(".pkg_wiz_1").show();
$(".pkg_wiz_2").hide();
$(".pkg_repo").show();
// finish()
})
});
} else {
finish()
}

View File

@@ -8,4 +8,57 @@ $(function() {
})
$(".not_mod, .not_game, .not_txp").show()
$(".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");
}
});
})

5
app/public/static/popper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -5,13 +5,25 @@
* https://petprojects.googlecode.com/svn/trunk/GPL-LICENSE.txt
*/
(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) {
return this.each(function() {
var selector = $(this),
input = $('input[type=text]', this);
selector.click(function() { input.focus(); })
.delegate('.tag a', 'click', function() {
.delegate('.badge a', 'click', function() {
var id = $(this).parent().data("id");
for (var i = 0; i < source.length; i++) {
if (source[i].id == id) {
@@ -23,13 +35,14 @@
});
function addTag(item) {
var tag = $('<span class="tag"/>')
var tag = $('<span class="badge badge-pill badge-primary"/>')
.text(item.toString() + ' ')
.data("id", item.id)
.append('<a>x</a>')
.insertBefore(input);
input.attr("placeholder", null);
select.find("option[value=" + item.id + "]").attr("selected", "selected")
hide_error(input);
}
function recreate() {
@@ -42,6 +55,13 @@
}
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) {
if (e.keyCode === $.ui.keyCode.TAB && $(this).data('ui-autocomplete').menu.active)
e.preventDefault();
@@ -92,7 +112,7 @@
}
selector.click(function() { input.focus(); })
.delegate('.tag a', 'click', function() {
.delegate('.badge a', 'click', function() {
var id = $(this).parent().data("id");
for (var i = 0; i < selected.length; i++) {
if (selected[i] == id) {
@@ -113,13 +133,14 @@
}
function addTag(id, value) {
var tag = $('<span class="tag"/>')
var tag = $('<span class="badge badge-pill badge-primary"/>')
.text(value)
.data("id", id)
.append(' <a>x</a>')
.insertBefore(input);
input.attr("placeholder", null);
hide_error(input);
}
function recreate() {
@@ -147,6 +168,18 @@
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) {
if (e.keyCode === $.ui.keyCode.TAB && $(this).data('ui-autocomplete').menu.active)
e.preventDefault();
@@ -159,7 +192,7 @@
recreate();
input.val("");
} else {
alert("Only lowercase alphanumeric and number names allowed.");
show_error(input, "Only lowercase alphanumeric and number names allowed.");
}
e.preventDefault();
return true;

View 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
View 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

View File

@@ -1,49 +1,29 @@
.comments, .comments li {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #444;
.img-thumbnail-1 {
padding: 0px;
// width: 100%;
background: white;
}
.comments {
border-radius: 5px;
margin: 15px 0;
background: #333;
list-style: none;
padding: 0;
.info_strip, .msg {
display: block;
margin: 0;
}
.card {
position:relative;
.info_strip {
padding: 0.2em 1em;
border-bottom: 1px solid #444;
}
.msg {
padding: 1em;
background: #222;
}
.author {
font-weight: bold;
float: left;
display: inline-block;
}
.info_strip span {
float: right;
display: inline-block;
color: #bbb;
.card-header:before {
position: absolute;
top: 11px;
right: 100%;
width: 0;
height: 0;
display: block;
content:" ";
border-color: transparent;
border-style: solid solid outset;
pointer-events:none;
border-right-color: #444;
border-width: 14px;
}
}
}
.comment_form {
margin: 1em 0;
}
.comment_form textarea {
min-width: 60%;
max-width: 100%;
margin: 0 0 1em 0;
}

View File

@@ -1,108 +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%;
}
input[type=text], input[type=password], textarea, select, .bulletselector {
text-align: left;
}
.ui-autocomplete, ui-front {
position:absolute;
cursor:default;
@@ -129,74 +24,11 @@ input[type=text], input[type=password], textarea, select, .bulletselector {
}
}
select {
min-width: 200px;
.bulletselector {
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 {
border: none;
border-radius: 0;
@@ -211,166 +43,14 @@ select:not([multiple]) {
white-space: nowrap;
background: transparent;
}
.bulletselector .tag {
background: #375D81;
border-radius: 3px;
-moz-border-radius: 3px;
color: #FFF;
.bulletselector .badge {
float: left;
height: 15px;
padding: 0.1em 0.4em 0.5em;
padding: 0.4em 0.8em;
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;
}
/* Alerts */
.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;
.invalid-remaining {
display: none;
}
.t-mll tr td:not(:first-child) {
@@ -401,53 +81,44 @@ table.fancyTable tfoot td {
color: #2c2;
}
/*
Aside
*/
.asideright {
float: right;
margin: 0 0 0 15px;
max-width: 300px;
.wiptopic a:not(.btn) {
color: #7ac;
}
.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;
.discardtopic {
text-decoration: line-through;
a:not(.btn) {
color: #7ac;
}
filter: brightness(0.5);
}
.flatlist, .flatlist li {
list-style: none;
padding: 0;
margin: 0;
.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;
}
.flatlist li {
display: block;
.editor-toolbar button {
color: white;
}
.flatlist a {
display: block;
padding: 0.5em 20px;
color: #ddd;
font-weight: normal;
.editor-toolbar button.active, .editor-toolbar button:hover {
background: #375a7f !important;
color: white !important;
}
.flatlist a:hover {
background: #444;
text-decoration: none;
.editor-toolbar.fullscreen::before, .editor-toolbar.fullscreen::after {
display: none !important;
}
.table-topalign td {
vertical-align: top;
// .CodeMirror {
// background-color: #222 !important;
// }
.editor-preview-side, .editor-preview {
background-color: #222 !important;
color: white !important;
}

57
app/scss/custom.scss Normal file
View 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);
}

View File

@@ -1,6 +0,0 @@
@import "page.scss";
@import "components.scss";
@import "nav.scss";
@import "packages.scss";
@import "packagegrid.scss";
@import "comments.scss";

View File

@@ -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;
}

View File

@@ -1,27 +1,20 @@
.packagegrid {
display: flex;
flex-wrap: wrap;
flex-direction: row;
flex-grow: 1;
flex-shrink: 1;
.packagetile {
list-style: none;
padding: 0;
margin: 0 -7px;
margin: 0 7px 7px 0;
min-width: 250px;
}
.packagegrid li {
flex: 1;
display: block;
min-width: 300px;
min-height: 200px;
max-width: 332px;
li.d-flex {
list-style: none;
padding: 0;
margin: 7px;
margin: 0;
}
.packagegrid a {
.packagetile a {
display: block;
padding-bottom: 66.66%;
border-radius: 7px;
border-radius: 3px;
position: relative;
overflow: hidden;
background-size: cover;
@@ -29,10 +22,6 @@
background-position: center;
}
.packagegrid a:hover {
// box-shadow: 0px 0px 16px 6px rgba(0,0,0,0.4);
}
.packagegridscrub {
position: absolute;
top: 50%;
@@ -53,6 +42,14 @@
.packagegridinfo h3 {
color: white;
font-size: 120%;
font-weight: bold;
}
.packagegridinfo small {
color: #ddd;
font-size: 75%;
font-weight: bold;
}
.packagegridinfo p {
@@ -61,15 +58,15 @@
font-weight: normal;
}
.packagegrid a:hover .packagegridinfo {
.packagetile a:hover .packagegridinfo {
top: 0;
}
.packagegrid a:hover p {
.packagetile a:hover p {
display: block;
}
.packagegrid a:hover .packagegridscrub {
.packagetile a:hover .packagegridscrub {
top: 0;
background: rgba(0,0,0,0.8);
}

View File

@@ -37,34 +37,6 @@
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 {
font-size: 120%;
}

View File

@@ -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;
}

View File

@@ -16,8 +16,9 @@
import flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask_sqlalchemy import SQLAlchemy
from celery import Celery
from celery.schedules import crontab
from app import app
from app.models import *
@@ -64,4 +65,12 @@ def 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

View File

@@ -15,14 +15,27 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import *
from flask import render_template
from flask_mail import Message
from app import mail
from app.tasks import celery
@celery.task()
def sendVerifyEmail(newEmail, token):
msg = Message("Verify email address", recipients=[newEmail])
msg.body = "This is a verification email!"
msg.html = render_template("emails/verify.html", token=token)
mail.send(msg)
print("Sending verify email!")
msg = Message("Verify email address", recipients=[newEmail])
msg.body = "This is a verification email!"
msg.html = render_template("emails/verify.html", token=token)
mail.send(msg)
@celery.task()
def sendEmailRaw(to, subject, text, html):
from flask_mail import Message
msg = Message(subject, recipients=to)
if text:
msg.body = text
html = html or text
msg.html = render_template("emails/base.html", subject=subject, content=html)
mail.send(msg)

View File

@@ -15,17 +15,18 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import flask, json
from flask.ext.sqlalchemy import SQLAlchemy
import flask, json, re
from flask_sqlalchemy import SQLAlchemy
from app import app
from app.models import *
from app.tasks import celery
from .phpbbparser import getProfile
from .phpbbparser import getProfile, getTopicsFromForum
import urllib.request
from urllib.parse import urlparse, quote_plus
@celery.task()
def checkForumAccount(username, token=None):
def checkForumAccount(username, forceNoSave=False):
print("Checking " + username)
try:
profile = getProfile("https://forum.minetest.net", username)
except OSError:
@@ -47,75 +48,122 @@ def checkForumAccount(username, token=None):
user.github_username = github_username
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
if needsSaving:
if needsSaving and not forceNoSave:
db.session.commit()
@celery.task()
def importUsersFromModList():
contents = urllib.request.urlopen("http://krock-works.16mb.com/MTstuff/modList.php").read().decode("utf-8")
list = json.loads(contents)
found = {}
imported = []
for user in User.query.all():
found[user.username] = True
if user.forums_username is not None:
found[user.forums_username] = True
for x in list:
author = x.get("author")
if author is not None and not author in found:
user = User(author)
user.forums_username = author
imported.append(author)
found[author] = True
db.session.add(user)
db.session.commit()
for author in found:
checkForumAccount.delay(author, None)
BANNED_NAMES = ["mod", "game", "old", "outdated", "wip", "api"]
ALLOWED_TYPES = [1, 2, 6]
return needsSaving
@celery.task()
def importKrocksModList():
contents = urllib.request.urlopen("http://krock-works.16mb.com/MTstuff/modList.php").read().decode("utf-8")
list = json.loads(contents)
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_]+)\]")
BANNED_NAMES = ["mod", "game", "old", "outdated", "wip", "api", "beta", "alpha", "git"]
def getNameFromTaglist(taglist):
for tag in reversed(regex_tag.findall(taglist)):
if len(tag) < 30 and not tag in BANNED_NAMES and \
not re.match(r"^[a-z]?[0-9]+$", tag):
return tag
return None
regex_title = re.compile(r"^((?:\[[^\]]+\] *)*)([^\[]+) *((?:\[[^\]]+\] *)*)[^\[]*$")
def parseTitle(title):
m = regex_title.match(title)
if m is None:
print("Invalid title format: " + title)
return title, getNameFromTaglist(title)
else:
return m.group(2).strip(), getNameFromTaglist(m.group(3))
def getLinksFromModSearch():
links = {}
contents = urllib.request.urlopen("https://krock-works.uk.to/minetest/modList.php").read().decode("utf-8")
for x in json.loads(contents):
link = x.get("link")
if link is not None:
links[int(x["topicId"])] = link
return links
@celery.task()
def importTopicList():
links_by_id = getLinksFromModSearch()
info_by_id = {}
getTopicsFromForum(11, out=info_by_id, extra={ 'type': PackageType.MOD, 'wip': False })
getTopicsFromForum(9, out=info_by_id, extra={ 'type': PackageType.MOD, 'wip': True })
getTopicsFromForum(15, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': False })
getTopicsFromForum(50, out=info_by_id, extra={ 'type': PackageType.GAME, 'wip': True })
# Caches
username_to_user = {}
topics_by_id = {}
for topic in ForumTopic.query.all():
topics_by_id[topic.topic_id] = topic
KrockForumTopic.query.delete()
# Create or update
for info in info_by_id.values():
id = int(info["id"])
for x in list:
type = int(x["type"])
if not type in ALLOWED_TYPES:
continue
username = x["author"]
# Get author
username = info["author"]
user = username_to_user.get(username)
if user is None:
user = User.query.filter_by(forums_username=username).first()
assert(user is not None)
if user is None:
print(username + " not found!")
user = User(username)
user.forums_username = username
db.session.add(user)
username_to_user[username] = user
import re
tags = re.findall("\[([a-z0-9_]+)\]", x["title"])
name = None
for tag in reversed(tags):
if len(tag) < 30 and not tag in BANNED_NAMES and \
not re.match("^([a-z][0-9]+)$", tag):
name = tag
break
# Get / add row
topic = topics_by_id.get(id)
if topic is None:
topic = ForumTopic()
db.session.add(topic)
topic = KrockForumTopic()
topic.topic_id = x["topicId"]
topic.author_id = user.id
topic.ttype = type
topic.title = x["title"]
topic.name = name
topic.link = x.get("link")
db.session.add(topic)
# Parse title
title, name = parseTitle(info["title"])
# Get link
link = links_by_id.get(id)
# Fill row
topic.topic_id = int(id)
topic.author = user
topic.type = info["type"]
topic.title = title
topic.name = name
topic.link = link
topic.wip = info["wip"]
topic.posts = int(info["posts"])
topic.views = int(info["views"])
topic.created_at = info["date"]
for p in Package.query.all():
p.recalcScore()
db.session.commit()

View File

@@ -17,7 +17,7 @@
import flask, json, os, git, tempfile, shutil
from git import GitCommandError
from flask.ext.sqlalchemy import SQLAlchemy
from flask_sqlalchemy import SQLAlchemy
from urllib.error import HTTPError
import urllib.request
from urllib.parse import urlparse, quote_plus, urlsplit
@@ -66,7 +66,7 @@ def getKrockList():
global krock_list_cache_by_name
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)
def h(x):
@@ -149,7 +149,8 @@ class PackageTreeNode:
type = PackageType.GAME
elif os.path.isfile(baseDir + "/init.lua"):
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
is_modpack = True
elif os.path.isdir(baseDir + "/mods"):
@@ -344,9 +345,11 @@ def makeVCSReleaseFromGithub(id, branch, release, url):
if len(commits) == 0 or not "sha" in commits[0]:
raise TaskError("No commits found")
release.url = urlmaker.getCommitDownload(commits[0]["sha"])
release.url = urlmaker.getCommitDownload(commits[0]["sha"])
release.task_id = None
release.commit_hash = commits[0]["sha"]
release.approve(release.package.author)
print(release.url)
release.task_id = None
db.session.commit()
return release.url
@@ -372,11 +375,13 @@ def makeVCSRelease(id, branch):
filename = randomString(10) + ".zip"
destPath = os.path.join("app/public/uploads", filename)
with open(destPath, "wb") as fp:
repo.archive(fp)
repo.archive(fp, format="zip")
release.url = "/uploads/" + filename
release.url = "/uploads/" + filename
release.task_id = None
release.commit_hash = repo.head.object.hexsha
release.approve(release.package.author)
print(release.url)
release.task_id = None
db.session.commit()
return release.url

View File

@@ -5,6 +5,7 @@
import urllib, socket
from bs4 import *
from urllib.parse import urljoin
from datetime import datetime
import urllib.request
import os.path
import time, re
@@ -14,8 +15,9 @@ def urlEncodeNonAscii(b):
class Profile:
def __init__(self, username):
self.username = username
self.signature = ""
self.username = username
self.signature = ""
self.avatar = None
self.properties = {}
def set(self, key, value):
@@ -32,6 +34,11 @@ def __extract_properties(profile, soup):
if el is 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")
if len(res) != 1:
return None
@@ -77,3 +84,72 @@ def getProfile(url, username):
__extract_properties(profile, soup)
return profile
regex_id = re.compile(r"^.*t=([0-9]+).*$")
def parseForumListPage(id, page, out, extra=None):
num_per_page = 30
start = page*num_per_page+1
print(" - Fetching page {} (topics {}-{})".format(page, start, start+num_per_page))
url = "https://forum.minetest.net/viewforum.php?f=" + str(id) + "&start=" + str(start)
r = urllib.request.urlopen(url).read().decode("utf-8")
soup = BeautifulSoup(r, "html.parser")
for row in soup.find_all("li", class_="row"):
classes = row.get("class")
if "sticky" in classes or "announce" in classes or "global-announce" in classes:
continue
topic = row.find("dl")
# Link info
link = topic.find(class_="topictitle")
id = regex_id.match(link.get("href")).group(1)
title = link.find(text=True)
# Date
left = topic.find("dt")
date = left.get_text().split("»")[1].strip()
date = datetime.strptime(date, "%a %b %d, %Y %H:%M")
author = left.find_all("a")[-1].get_text().strip()
# Get counts
posts = topic.find(class_="posts").find(text=True)
views = topic.find(class_="views").find(text=True)
if id in out:
print(" - got {} again, title: {}".format(id, title))
assert(title == out[id]['title'])
return False
row = {
"id" : id,
"title" : title,
"author": author,
"posts" : posts,
"views" : views,
"date" : date
}
if extra is not None:
for key, value in extra.items():
row[key] = value
out[id] = row
return True
def getTopicsFromForum(id, out={}, extra=None):
print("Fetching all topics from forum {}".format(id))
page = 0
while parseForumListPage(id, page, out, extra):
page = page + 1
return out
def dumpTitlesToFile(topics, path):
with open(path, "w") as out_file:
for topic in topics.values():
out_file.write(topic["title"] + "\n")

View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}
{% if license %}
Edit {{ license.name }}
{% else %}
New license
{% endif %}
{% endblock %}
{% block content %}
<p>
<a href="{{ url_for('license_list_page') }}">Back to list</a> |
<a href="{{ url_for('createedit_license_page') }}">New License</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.is_foss) }}
{{ render_submit_field(form.submit) }}
</form>
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}
Licenses
{% endblock %}
{% block content %}
<p>
<a href="{{ url_for('createedit_license_page') }}">New License</a>
</p>
<ul>
{% for l in licenses %}
<li><a href="{{ url_for('createedit_license_page', name=l.name) }}">{{ l.name }}</a> [{{ l.is_foss and "Free" or "Non-free"}}]</li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -8,29 +8,33 @@
<ul>
<li><a href="{{ url_for('user_list_page') }}">User list</a></li>
<li><a href="{{ url_for('tag_list_page') }}">Tag Editor</a></li>
<li><a href="{{ url_for('license_list_page') }}">License Editor</a></li>
<li><a href="{{ url_for('version_list_page') }}">Version Editor</a></li>
<li><a href="{{ url_for('switch_user_page') }}">Sign in as another user</a></li>
</ul>
<div class="box box_grey">
<h2>Do action</h2>
<div class="card my-4">
<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() }}" />
<select name="action">
<option value="importusers">Create users from mod list</option>
<option value="importmodlist">Import Krock's mod list</option>
<option value="importscreenshots" selected>Import screenshots from VCS</option>
<option value="importdepends">Import dependencies from downloads</option>
<option value="modprovides">Set provides to mod name</option>
<option value="importmodlist" selected>Import forum topics</option>
<option value="recalcscores">Recalculate package scores</option>
<option value="checkusers">Check forum users</option>
<option value="importscreenshots">Import screenshots from VCS</option>
<!-- <option value="importdepends">Import dependencies from downloads</option> -->
<!-- <option value="modprovides">Set provides to mod name</option> -->
<!-- <option value="vcsrelease">Create VCS releases</option> -->
</select>
<input type="submit" value="Perform" />
</form>
</div>
<div class="box box_grey">
<h2>Restore Package</h2>
<div class="card my-4">
<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="action" value="restore" />
<select name="package">

View File

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

View File

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

View File

@@ -6,74 +6,97 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
<link rel="stylesheet" type="text/css" href="/static/main.css">
<link rel="stylesheet" type="text/css" href="/static/bootstrap.css">
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=6">
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
{% block headextra %}{% endblock %}
</head>
<body>
<nav>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<ul class="nav navbar-nav navbar-left">
<li><a href="/">{{ config.USER_APP_NAME }}</a></li>
{% for item in current_menu.children recursive %}
{% if item.visible %}
<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>
<a class="navbar-brand" href="/">{{ config.USER_APP_NAME }}</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="{{ url_for('user_profile_page', username=current_user.username) }}">Profile</a>
<div class="collapse navbar-collapse" id="navbarColor01">
<ul class="navbar-nav mr-auto">
{% 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>
{% if current_user.canAccessTodoList() %}
<li><a href="{{ url_for('todo_page') }}">Work Queue</a></li>
<li><a href="{{ url_for('user_list_page') }}">User list</a></li>
{% endif %}
{% if current_user.rank == current_user.rank.ADMIN %}
<li><a href="{{ url_for('admin_page') }}">Admin</a></li>
{% endif %}
{% if current_user.rank == current_user.rank.MODERATOR %}
<li><a href="{{ url_for('tag_list_page') }}">Tag Editor</a></li>
{% endif %}
<li><a href="{{ url_for('user.logout') }}">Sign out</a></li>
</ul>
</li>
{% else %}
<li><a href="{{ url_for('user.login') }}">Sign in</a></li>
{% endif %}
</ul>
<div class="clearboth"></div>
{% endif %}
{% endfor %}
</ul>
<form class="form-inline my-2 my-lg-0" method="GET" action="/packages/">
{% if type %}<input type="hidden" name="type" value="{{ type }}" />{% endif %}
<input class="form-control mr-sm-2" name="q" type="text" placeholder="Search {{ title | lower or 'all packages' }}" value="{{ query or ''}}">
<input class="btn btn-secondary my-2 my-sm-0 mr-sm-2" type="submit" value="Search" />
<!-- <input class="btn btn-secondary my-2 my-sm-0"
data-toggle="tooltip" data-placement="bottom"
title="Go to the first found result for this query."
type="submit" name="lucky" value="First" /> -->
</form>
<ul class="navbar-nav ml-auto">
{% if current_user.is_authenticated %}
<li class="nav-item"><a class="nav-link" href="{{ url_for('notifications_page') }}">
<img src="/static/notification{% if current_user.notifications %}_alert{% endif %}.svg" />
</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('create_edit_package_page') }}">+</a></li>
<li class="nav-item 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>
</nav>
@@ -82,7 +105,7 @@
{% if messages %}
<ul id="alerts">
{% for category, message in messages %}
<li class="box box_grey alert alert-{{category}}">
<li class="alert alert-{{category}} container">
<span class="icon_message"></span>
{{ message|safe }}
@@ -96,16 +119,29 @@
{% endblock %}
{% block container %}
<main>
<main class="container mt-4">
{% block content %}
{% endblock %}
</main>
{% endblock %}
<footer>
<footer class="container footer-copyright my-5 page-footer font-small text-center">
ContentDB &copy; 2018 to <a href="https://rubenwardy.com/">rubenwardy</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/reporting') }}">Report / DMCA</a>
</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>

View File

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

View File

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

View File

@@ -5,76 +5,76 @@ Sign in
{% endblock %}
{% block content %}
<div class="sidebar_container">
<div class="left box box_grey">
{% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %}
<h2>{%trans%}Sign in{%endtrans%}</h2>
<div class="row">
<div class="col-sm-8">
<div class="card">
{% 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">
<h3>Sign in with username/password</h3>
{{ form.hidden_tag() }}
<form action="" method="POST" class="form card-body" role="form">
{{ form.hidden_tag() }}
{# Username or Email field #}
{% set field = form.username if user_manager.enable_username else form.email %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
{# Label on left, "New here? Register." on right #}
<div class="row">
<div class="col-xs-6">
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}</label>
</div>
{# Username or Email field #}
{% set field = form.username if user_manager.enable_username else form.email %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
{# Label on left, "New here? Register." on right #}
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}</label>
{{ field(class_='form-control', tabindex=110) }}
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{% endif %}
</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 #}
{% set field = form.password %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
<div class="row">
{# Password field #}
{% set field = form.password %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}
{% if user_manager.enable_forgot_password %}
<a href="{{ url_for('user.forgot_password') }}" tabindex='195'>
[{%trans%}Forgot My Password{%endtrans%}]</a>
{% endif %}
</label>
{{ field(class_='form-control', tabindex=120) }}
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{% endif %}
</div>
{{ field(class_='form-control', tabindex=120) }}
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{# 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>
</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>
{# 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="box box_grey">
<h2>New here?</h2>
<div class="box-body">
<aside class="col-sm-4">
<div class="card">
{% 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="card-body">
<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>
</aside>
</div>
</div>
</aside>
</div>
{% endblock %}

View File

@@ -1,10 +1,10 @@
{% extends "base.html" %}
{% block container %}
<main>
<div class="box box_grey">
<!-- <h2>{{ self.title() }}</h2> -->
<div class="box-body">
<main class="container mt-4">
<div class="card">
<!-- <h2 class="card-header">{{ self.title() }}</h2> -->
<div class="card-body">
{% block content %}
{% endblock %}
</div>

View File

@@ -5,7 +5,7 @@
{% endblock %}
{% block content %}
<h1>{{ page['title'] }}</h1>
{% if not page["no_h1"] %}<h1>{{ page['title'] }}</h1>{% endif %}
{{ page.html | safe }}
{% endblock %}

View File

@@ -4,27 +4,70 @@
Welcome
{% endblock %}
{% block container %}
<header>
<div class="container">
<h1>Content DB</h1>
{% block scriptextra %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"url": "https://content.minetest.net/",
"potentialAction": {
"@type": "SearchAction",
"target": "https://content.minetest.net/packages?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
{% endblock %}
<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.
Browse {{ count }} packages,
all available under a free and open source
license.
the majority of which are available under a free
and open source license.
</p>
<form method="get" action="/packages/">
<input type="text" name="q" value="{{ query or ''}}" />
<input type="submit" value="Search" />
</form>
</div>
</header>
<main>
<main class="container"> -->
{% from "macros/packagegridtile.html" import render_pkggrid %}
{{ render_pkggrid(packages) }}
</main>
<a href="{{ url_for('packages_page', sort='created_at', order='desc') }}" class="btn btn-secondary float-right">
See more
</a>
<h2 class="my-3">Recently Added</h2>
{{ render_pkggrid(new) }}
<a href="{{ url_for('packages_page', type='mod', sort='score', order='desc') }}" class="btn btn-secondary float-right">
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 }} packages, with a total of {{ downloads }} downloads.
</small>
</div>
<!-- </main> -->
{% endblock %}

View File

@@ -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_', '') }}">
{% if field.type != 'HiddenField' and label_visible %}
{% if not label %}{% set label=field.label.text %}{% endif %}
<label for="{{ field.id }}" class="control-label">{{ label|safe }}</label>
{% if not label and label != "" %}{% set label=field.label.text %}{% endif %}
{% if label %}<label for="{{ field.id }}">{{ label|safe }}</label>{% endif %}
{% endif %}
{{ field(class_='form-control', **kwargs) }}
{{ field(class_=fieldclass or 'form-control', **kwargs) }}
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
@@ -13,9 +13,8 @@
</div>
{%- endmacro %}
{% macro form_includes() -%}
{% macro form_scripts() -%}
<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/tagselector.js"></script>
{% endmacro %}
@@ -58,16 +57,17 @@
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' and label_visible %}
{% 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 %}
<div class="multichoice_selector bulletselector">
<div class="multichoice_selector bulletselector form-control">
<input type="text" placeholder="Start typing to see suggestions">
<div class="clearboth"></div>
</div>
<div class="invalid-remaining invalid-feedback"></div>
{{ field(class_='form-control', **kwargs) }}
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
<div class="invalid-feedback">{{ e }}</div>
{% endfor %}
{% endif %}
</div>
@@ -77,13 +77,14 @@
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' and label_visible %}
{% 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 %}
<div class="metapackage_selector bulletselector">
<div class="metapackage_selector bulletselector form-control">
<input type="text" placeholder="Comma-seperated values">
<div class="clearboth"></div>
</div>
{{ field(class_='form-control', **kwargs) }}
<div class="invalid-remaining invalid-feedback"></div>
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
@@ -96,13 +97,14 @@
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' and label_visible %}
{% 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 %}
<div class="deps_selector bulletselector">
<div class="deps_selector bulletselector form-control">
<input type="text" placeholder="Comma-seperated values">
<div class="clearboth"></div>
</div>
{{ field(class_='form-control', **kwargs) }}
<div class="invalid-remaining invalid-feedback"></div>
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
@@ -113,7 +115,7 @@
{% macro render_checkbox_field(field, label=None) -%}
{% if not label %}{% set label=field.label.text %}{% endif %}
<div class="checkbox">
<div class="checkbox {{ kwargs.pop('class_', '') }}">
<label>
{{ field(type='checkbox', **kwargs) }} {{ label }}
</label>
@@ -122,9 +124,9 @@
{% macro render_radio_field(field) -%}
{% for value, label, checked in field.iter_choices() %}
<div class="radio">
<label>
<input type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}"{% if checked %} checked{% endif %}>
<div class="form-check my-1">
<label class="form-check-label">
<input class="form-check-input" type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}"{% if checked %} checked{% endif %}>
{{ label }}
</label>
</div>
@@ -134,7 +136,7 @@
{% macro render_submit_field(field, label=None, tabindex=None) -%}
{% if not label %}{% set label=field.label.text %}{% endif %}
{#<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 %}
>
{%- endmacro %}

View File

@@ -1,18 +1,18 @@
{% 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' }});">
<div class="packagegridscrub"></div>
<div class="packagegridinfo">
<h3>
{{ package.title }}
{% if show_author %}
by {{ package.author.display_name }}
{% if show_author %}<br />
<small>{{ package.author.display_name }}</small>
{% endif %}
</h3>
<p>
{{ package.shortDesc }}
{{ package.short_desc }}
</p>
@@ -34,11 +34,14 @@
{% endmacro %}
{% 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 %}
{{ render_pkgtile(p, show_author) }}
{% else %}
<li><i>No packages available</i></ul>
{% endfor %}
{% for i in range(4) %}
<li class="packagetile flex-fill"></li>
{% endfor %}
</ul>
{% endmacro %}

View File

@@ -1,34 +1,83 @@
{% macro render_thread(thread, current_user) -%}
<ul class="comments">
{% for r in thread.replies %}
<li>
<div class="info_strip">
<ul class="comments mt-4 mb-0">
{% for r in thread.replies %}
<li class="row my-2 mx-0">
<div class="col-md-1 p-1">
<a href="{{ url_for('user_profile_page', username=r.author.username) }}">
<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 }}"
href="{{ url_for('user_profile_page', username=r.author.username) }}">
{{ r.author.display_name }}</a>
<span>{{ r.created_at | datetime }}</span>
<div class="clearboth"></div>
{{ r.author.display_name }}
</a>
<a name="reply-{{ r.id }}" class="text-muted float-right"
href="{{ url_for('thread_page', id=thread.id) }}#reply-{{ r.id }}">
{{ r.created_at | datetime }}
</a>
</div>
<div class="msg">
{{ r.comment }}
</div>
</li>
{% endfor %}
</ul>
{% if current_user.is_authenticated %}
<form method="post" action="{{ url_for('thread_page', id=thread.id)}}" class="comment_form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<textarea required maxlength=500 name="comment"></textarea><br />
<input type="submit" value="Comment" />
</form>
{% endif %}
<div class="card-body">
{{ r.comment | markdown }}
</div>
</div>
</div>
</li>
{% endfor %}
</ul>
{% if current_user.is_authenticated %}
<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>
{% 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 %}
{% macro render_threadlist(threads) -%}
<ul>
{% macro render_threadlist(threads, list_group=False) -%}
{% if not list_group %}<ul>{% endif %}
{% for t in threads %}
<li><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 %}&#x1f512; {% endif %}
{{ t.title }}
by {{ t.author.display_name }}
</a>
{% else %}
{% if t.private %}&#x1f512; {% endif %}
<a href="{{ url_for('thread_page', id=t.id) }}">{{ t.title }}</a>
by {{ t.author.display_name }}
{% endif %}
</li>
{% else %}
<li {% if list_group %}class="list-group-item"{% endif %}><i>No threads found</i></li>
{% endfor %}
</ul>
{% if not list_group %}</ul>{% endif %}
{% endmacro %}

View 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 %}

View File

@@ -1,26 +0,0 @@
{% macro render_topictable(topics, show_author=True) -%}
<table>
<tr>
<th>Id</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>
<td>{{ topic.topic_id }}</td>
<td>[{{ topic.getType().value }}] <a href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a></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><a href="{{ topic.link }}">{{ topic.link | domain }}</a></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 %}

View File

@@ -7,79 +7,92 @@
{% endif %}
{% 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 %}
<h1>Create Package</h1>
<div class="box box_grey alert alert-info">
Have you read the Package Inclusion Policy and Guidance yet?
<div class="alert alert-info">
<a class="float-right btn btn-sm btn-default" href="{{ url_for('flatpage', path='policy_and_guidance') }}">View</a>
<a class="alert_right button" href="{{ url_for('flatpage', path='policy_and_guidance') }}">View</a>
Have you read the Package Inclusion Policy and Guidance yet?
</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() }}
<form method="POST" action="" class="tableform">
{{ form.hidden_tag() }}
<h2 class="pkg_meta">Package</h2>
<fieldset>
<legend>Package</legend>
{{ render_field(form.type, class_="pkg_meta") }}
{{ render_field(form.name, class_="pkg_meta") }}
{{ render_field(form.title, class_="pkg_meta") }}
{{ render_field(form.shortDesc, class_="pkg_meta") }}
{{ render_field(form.desc, class_="pkg_meta") }}
{{ render_multiselect_field(form.tags, class_="pkg_meta") }}
<div class="pkg_meta">
{{ render_field(form.license, class_="not_txp") }}
</div>
{{ render_field(form.media_license, class_="pkg_meta") }}
<div class="row">
{{ render_field(form.type, class_="pkg_meta col-sm-2") }}
{{ render_field(form.title, class_="pkg_meta col-sm-7") }}
{{ render_field(form.name, class_="pkg_meta col-sm-3") }}
</div>
{{ 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>
{{ render_field(form.desc, class_="pkg_meta", fieldclass="form-control markdown") }}
</fieldset>
<div class="pkg_meta">
<h2 class="not_txp">Dependency Info</h2>
<fieldset class="pkg_meta">
<legend class="not_txp">Dependencies</legend>
{{ 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.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">
<p>Enter the repo URL for the package.
If the repo uses git then the metadata will be automatically imported.</p>
<div class="pkg_wiz_1">
<p>Enter the repo URL for the package.
If the repo uses git then the metadata will be automatically imported.</p>
<p>Leave blank if you don't have a repo.</p>
</div>
<p>Leave blank if you don't have a repo. Click skip if the import fails.</p>
</div>
{{ render_field(form.repo, class_="pkg_repo") }}
{{ render_field(form.repo, class_="pkg_repo") }}
<div class="pkg_wiz_1">
<a id="pkg_wiz_1_next" class="button button-primary">Next</a>
</div>
<div class="pkg_wiz_2">
Importing... (This may take a while)
</div>
<div class="pkg_wiz_1">
<a id="pkg_wiz_1_next" class="btn btn-primary">Next (Autoimport)</a>
<a id="pkg_wiz_1_skip" class="btn btn-default">Skip Autoimport</a>
</div>
<div class="pkg_wiz_2">
Importing... (This may take a while)
</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>
</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 %}

View File

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

View File

@@ -24,7 +24,7 @@
This edit request was merged.
</div>
{% 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.
</div>
{% elif package.checkPerm(current_user, "APPROVE_CHANGES") %}

View File

@@ -5,36 +5,31 @@
{% endblock %}
{% 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 %}
{{ render_pkggrid(packages) }}
<ul class="buttonset linedbuttonset">
{% if prev_url %}<li><a href="{{ prev_url }}">Previous</a></li>{% endif %}
<li>{{ page }} / {{ page_max }}</li>
{% if next_url %}<li><a href="{{ next_url }}">Next</a></li> {% endif %}
<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 %}>&laquo;</a>
</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 %}>&raquo;</a>
</li>
</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 %}

View File

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

View File

@@ -1,11 +1,11 @@
{% extends "base.html" %}
{% block title %}
Create a release | {{ package.title }}
Edit release | {{ package.title }}
{% endblock %}
{% 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.hidden_tag() }}
@@ -21,8 +21,12 @@
Url: <a href="{{ release.url }}">{{ release.url }}</a><br />
{% endif %}
{% if release.commit_hash %}
Commit Hash: {{ release.commit_hash }}<br />
{% endif %}
{% if release.task_id %}
Importing... <a href="{ url_for('check_task', id=release.task_id, r=release.getEditURL()) }}">view task</a><br />
Importing... <a href="{{ url_for('check_task', id=release.task_id, r=release.getEditURL()) }}">view task</a><br />
{% if package.checkPerm(current_user, "CHANGE_RELEASE_URL") %}
{{ render_field(form.task_id) }}
{% endif %}
@@ -30,31 +34,33 @@
<br />
{% else %}
{% if package.checkPerm(current_user, "APPROVE_RELEASE") %}
{{ render_field(form.approved) }}
{{ render_checkbox_field(form.approved, class_="my-3") }}
{% else %}
Approved: {{ release.approved }}
{% endif %}
{% endif %}
<div class="row">
{{ render_field(form.min_rel, class_="col-sm-6") }}
{{ render_field(form.max_rel, class_="col-sm-6") }}
</div>
<p id="minmax_warning" style="color:#f00; display: none;">
Maximum must be greater than or equal to the minimum!
</p>
<p>
Note: Min and max versions will be used to hide the package on
platforms not within the range.
You cannot select the oldest version for min or the newest version
for max as this does not make sense - you can't predict the future.<br />
Leave both as None if in doubt.
</p>
{{ render_submit_field(form.submit) }}
</form>
{% if package.checkPerm(current_user, "APPROVE_RELEASE") %}
<div class="box box_grey">
<h2>Approval Checklist</h2>
<ul>
<li>Link leads to a valid download, ie: is a zip file which
has either init.lua or modpack.txt if a mod, mods/ if a game, or textures if a texture pack.
It's okay if they're inside an immediate folder, like so:
<pre>
example.zip/
└── example
└── init.lua
</pre>
</li>
<li>There's no obfuscated code.</li>
</ul>
</div>
{% endif %}
{% endblock %}
{% block scriptextra %}
<script src="/static/release_minmax.js?v=1"></script>
{% endblock %}

View File

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

View File

@@ -6,13 +6,14 @@
{% block content %}
<form method="POST" action="" class="box box_grey ">
<h3>Delete Package</h3>
<h3>Remove Package</h3>
<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="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>
</form>
{% endblock %}

View File

@@ -5,17 +5,19 @@
{% endblock %}
{% block content %}
<img src="{{ screenshot.getThumbnailURL() }}" alt="{{ screenshot.title }}" />
<a href="{{ screenshot.url }}">
<img src="{{ screenshot.getThumbnailURL() }}" alt="{{ screenshot.title }}" />
</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.hidden_tag() }}
{{ render_field(form.title) }}
{{ render_field(form.delete) }}
{{ render_checkbox_field(form.delete) }}
{% if package.checkPerm(current_user, "APPROVE_SCREENSHOT") %}
{{ render_field(form.approved) }}
{{ render_checkbox_field(form.approved) }}
{% else %}
<p>Approved: {{ screenshot.approved }}</p>
{% endif %}

View File

@@ -10,7 +10,7 @@
{{ form.hidden_tag() }}
{{ render_field(form.title) }}
{{ render_field(form.fileUpload) }}
{{ render_field(form.fileUpload, fieldclass="form-control-file") }}
{{ render_submit_field(form.submit) }}
</form>
{% endblock %}

View File

@@ -1,303 +1,408 @@
{% set query=package.name %}
{% extends "base.html" %}
{% block title %}
{{ package.title }}
{% endblock %}
{% block content %}
{% if not package.approved %}
<div class="box box_grey alert alert-warning">
<span class="icon_message"></span>
{% if package.releases.count() == 0 %}
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
You need to create a release before this package can be approved.
<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="button" href="{{ package.getCreateReleaseURL() }}">Create Release</a>
{% else %}
A release is required before this package can be approved.
{% endif %}
{% block container %}
<header class="jumbotron pb-3"
style="background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), url('{{ package.getMainScreenshotURL() }}');
background-size: cover;
background-repeat: no-repeat;
background-position: center;">
<div class="container">
<h1 class="display-3">
{{ package.title }}
<small>by {{ package.author.display_name }}</small>
</h1>
{% 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.
<p class="lead">
{{ package.short_desc }}
</p>
{% 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") %}
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 class="row" style="margin-top: 2rem;">
<div class="col">
{{ package.getDownloadCount() }} downloads
</div>
<div class="btn-group-horizontal col-md-auto">
{% if package.repo %}<a class="btn btn-secondary" href="{{ package.repo }}">View Source</a>{% endif %}
{% if package.forums %}<a class="btn btn-secondary" href="https://forum.minetest.net/viewtopic.php?t={{ package.forums }}">Forums</a>{% endif %}
{% if package.issueTracker %}<a class="btn btn-secondary" href="{{ package.issueTracker }}">Issue Tracker</a>{% endif %}
{% if package.website %}<a class="btn btn-secondary" href="{{ package.website }}">Website</a>{% endif %}
</div>
</div>
</div>
</header>
{% if package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW") %}
<main class="container mt-4">
{% if not package.approved %}
<div class="alert alert-warning">
<span class="icon_message"></span>
{% 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>
{% 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 %}
<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.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 &le;{{ rel.max_rel.name }}]
{% endif %}
</span>
<br>
<small style="color:#999;">
{% if rel.commit_hash %}
[{{ rel.commit_hash | truncate(5, end='') }}]
{% endif %}
created {{ rel.releaseDate | date }}.
</small>
{% if (package.checkPerm(current_user, "MAKE_RELEASE") or package.checkPerm(current_user, "APPROVE_RELEASE")) and rel.task_id %}
<a href="{{ url_for('check_task', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
{% elif not rel.approved %}
Waiting for approval.
{% 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 %}
<h2>&#x1f512; {{ review_thread.title }}</h2>
<p><i>
This thread is only visible to the package owner and users of
Editor rank or above.
</i></p>
<h2>{% if review_thread.private %}&#x1f512;{% endif %} {{ review_thread.title }}</h2>
{% if review_thread.private %}
<p><i>
This thread is only visible to the package owner and users of
Editor rank or above.
</i></p>
{% endif %}
{% from "macros/threads.html" import render_thread %}
{{ 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 %}
<h1>{{ package.title }} by {{ package.author.display_name }}</h1>
<ul class="screenshot_list">
{% 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>
<ul class="screenshot_list mb-4">
{% if package.checkPerm(current_user, "EDIT_PACKAGE") %}
<a class="btn btn-primary float-right" href="{{ package.getNewScreenshotURL() }}">Add screenshot</a>
{% endif %}
{% endfor %}
</ul>
<aside class="asideright box box_grey">
<h3>Details</h3>
<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 }}
{% 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>
</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>
</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>
</li>
{% endif %}
{# {% if current_user.is_authenticated %}
<li><a href="{{ package.getCreateEditRequestURL() }}">Suggest Changes</a></li>
{% endif %} #}
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
<li><a href="{{ package.getCreateReleaseURL() }}">Create Release</a></li>
{% endif %}
{% if package.checkPerm(current_user, "DELETE_PACKAGE") %}
<li><a href="{{ package.getDeleteURL() }}">Delete</a></li>
{% endif %}
</ul>
</div>
</aside>
{% endfor %}
</ul>
<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.url }}">{{ rel.title }}</a>,
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>
{% for dep in package.dependencies %}
{% for r in requests %}
<li>
{%- if dep.package %}
<a href="{{ dep.package.getDetailsURL() }}">{{ dep.package.title }}</a> by {{ dep.package.author.display_name }}
{% elif dep.meta_package %}
<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 %}
<a href="{{ r.getURL() }}">{{ r.title }}</a>
by
<a href="{{ url_for('user_profile_page', username=r.author.username) }}">{{ r.author.display_name }}</a>
</li>
{% else %}
<li><i>No dependencies</i></li>
<li>No edit requests have been made.</li>
{% endfor %}
</ul>
<!-- </td>
<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> -->
{% endif %}
#}
{#
{% if current_user.is_authenticated or requests %}
<h3>Edit Requests</h3>
{% if alternatives %}
<h3>Related</h3>
{% from "macros/packagegridtile.html" import render_pkggrid %}
{{ render_pkggrid(alternatives) }}
{% endif %}
{% if similar_topics %}
<h3>Similar Forum Topics</h3>
<ul>
{% for r in requests %}
{% for t in similar_topics %}
<li>
<a href="{{ r.getURL() }}">{{ r.title }}</a>
by
<a href="{{ url_for('user_profile_page', username=r.author.username) }}">{{ r.author.display_name }}</a>
[{{ 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>
{% else %}
<li>No edit requests have been made.</li>
{% endfor %}
</ul>
{% endif %}
#}
{% 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.getType().value }}]
<a href="https://forum.minetest.net/viewtopic.php?t={{ t.topic_id }}">
{{ t.title }} by {{ t.author.display_name }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</main>
{% endblock %}

View File

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

View File

@@ -5,12 +5,26 @@ Threads
{% endblock %}
{% 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 %}&#x1f512; {% endif %}{{ thread.title }}</h1>
{% if thread.package %}
<p>
Package: <a href="{{ thread.package.getDetailsURL() }}">{{ thread.package.title }}</a>
</p>
{% if thread.package or current_user.is_authenticated %}
{% if thread.package %}
<p>Package: <a href="{{ thread.package.getDetailsURL() }}">{{ thread.package.title }}</a></p>
{% endif %}
{% endif %}
{% if thread.private %}

View File

@@ -5,53 +5,79 @@
{% endblock %}
{% block content %}
<h2>Awaiting Approval</h2>
<h2 class="mb-4">Approval Queue</h2>
{% if canApproveNew and packages %}
<h3>Packages</h3>
<ul>
{% for p in packages %}
<li><a href="{{ p.getDetailsURL() }}">
{{ p.title }} by {{ p.author.display_name }}
</a></li>
{% else %}
<li><i>No packages need reviewing.</i></ul>
{% endfor %}
</ul>
{% endif %}
<div class="row">
{% if canApproveNew and packages %}
<div class="col-sm-6">
<div class="card">
<h3 class="card-header">Packages</h3>
<div class="list-group list-group-flush">
{% for p in packages %}
<a href="{{ p.getDetailsURL() }}" class="list-group-item list-group-item-action">
{{ 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 %}
<h3>Screenshots</h3>
<ul>
{% for s in screenshots %}
<li>
<a href="{{ s.getEditURL() }}">{{ s.title }}</a>
on
<a href="{{ s.package.getDetailsURL() }}">
{{ s.package.title }} by {{ s.package.author.display_name }}
</a>
</li>
{% else %}
<li><i>No screenshots need reviewing.</i></ul>
{% endfor %}
</ul>
{% endif %}
{% if canApproveRel and releases %}
<h3>Releases</h3>
<ul>
{% for r in releases %}
<li>
<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><i>No releases need reviewing.</i></ul>
{% endfor %}
</ul>
<div class="card my-4">
<h3 class="card-header">Screenshots
<form class="float-right" method="post" action="{{ url_for('todo_page') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="action" value="screenshots_approve_all" />
<input class="btn btn-sm btn-primary" type="submit" value="Approve All" />
</form>
</h3>
<ul class="card-body d-flex p-0 flex-row flex-wrap justify-content-start align-content-start p-4">
{% for s in screenshots %}
<li class="packagetile flex-fill"><a href="{{ s.getEditURL() }}"
style="background-image: url({{ s.getThumbnailURL(3) or '/static/placeholder.png' }});">
<div class="packagegridscrub"></div>
<div class="packagegridinfo">
<h3>
{{ s.title }}
<br />
<small>{{ s.package.title }} by {{ s.package.author.display_name }}</small>
</h3>
<p></p>
</div>
</a></li>
{% else %}
<li><i>No screenshots need reviewing.</i></li>
{% endfor %}
{% for i in range(4) %}
<li class="packagetile flex-fill"></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if not (packages or screenshots or releases) %}
@@ -60,11 +86,19 @@
</p>
{% endif %}
<h2>Unadded Topic List</h2>
<h2 class="mt-4">Unadded Topic List</h2>
<p>
There are
<a href="{{ url_for('todo_topics_page') }}">{{ topics_to_add }} packages</a>
to be added to cdb, based on forum topics picked up by Krock's mod search.
{{ total_topics - topics_to_add }} / {{ total_topics }} packages have been been added to cdb,
based on cdb's forum parser. {{ topics_to_add }} remaining.
</p>
<div class="progress my-4">
{% set perc = 32 %}
<div class="progress-bar bg-success" role="progressbar"
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<a class="btn btn-primary" href="{{ url_for('todo_topics_page') }}">View Unadded Topic List</a>
{% endblock %}

View File

@@ -5,13 +5,94 @@ Topics to be Added
{% endblock %}
{% 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>
<p>
{{ total - (topics | count) }} / {{ total }} packages have been added.
{{ topics | count }} remaining.
{{ total - topic_count }} / {{ total }} topics have been added as packages to CDB.
{{ topic_count }} remaining.
</p>
<div class="progress">
{% set perc = 100 * (total - topic_count) / total %}
<div class="progress-bar bg-success" role="progressbar"
style="width: {{ perc }}%" aria-valuenow="{{ perc }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
{% from "macros/topictable.html" import render_topictable %}
{{ render_topictable(topics) }}
<form method="GET" action="{{ url_for('todo_topics_page') }}" class="my-4">
<input type="hidden" name="show_discarded" value={{ show_discarded and "True" or "False" }} />
<input type="hidden" name="n" value={{ n }} />
<input type="hidden" name="sort" value={{ sort_by or "date" }} />
<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 %}>&laquo;</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 %}>&raquo;</a>
</li>
</ul>
{% endblock %}
{% block scriptextra %}
<script>
var csrf_token = "{{ csrf_token() }}";
</script>
<script src="/static/topic_discard.js"></script>
{% endblock %}

View File

@@ -5,10 +5,10 @@ Creating an Account
{% endblock %}
{% block content %}
<div class="box box_grey">
<h2>{{ self.title() }}</h2>
<div class="card">
<h2 class="card-header">{{ self.title() }}</h2>
<div class="box-body">
<div class="card-body">
<p>
If you have a forum account, you'll need to prove that you own it
to get an account on ContentDB.
@@ -19,7 +19,7 @@ Creating an Account
Please log out to continue.
</p>
<p>
<a href="{{ url_for('user.logout', next=url_for('user_claim_page')) }}" class="button">Logout</a>
<a href="{{ url_for('user.logout', next=url_for('user_claim_page')) }}" class="btn">Logout</a>
</p>
{% else %}
<p>
@@ -28,7 +28,7 @@ Creating an Account
out of the Minetest community.
</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
</a>
{% endif %}
@@ -36,18 +36,23 @@ Creating an Account
</div>
{% if not current_user.is_authenticated %}
<div class="box box_grey">
<h2>Option 1 - Use GitHub field in forum profile</h2>
<div class="row mt-4">
<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') }}">
<input type="hidden" name="claim_type" value="github">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<form method="post" class="card-body" action="{{ url_for('user_claim_page') }}">
<input class="form-control" type="hidden" name="claim_type" value="github">
<input class="form-control" type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<p>
Enter your forum username here:
</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>
You'll need to have the GitHub field in your forum profile
@@ -56,14 +61,19 @@ Creating an Account
do that here</a>.
</p>
<input type="submit" value="Next: log in with GitHub">
<input class="btn btn-primary" type="submit" value="Next: log in with GitHub">
</form>
</div>
</div>
<div class="box box_grey">
<h2>Option 2 - Paste verification token into signature</h2>
<div class="col-sm-4">
<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="csrf_token" value="{{ csrf_token() }}" />
@@ -71,7 +81,7 @@ Creating an Account
Enter your forum username here:
</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>
Go to
@@ -79,11 +89,12 @@ Creating an Account
User Control Panel > Profile > Edit signature
</a>
</p>
<p>
Paste this into your signature:
</p>
<input type="text" value="{{ key }}" readonly size=32>
<input class="form-control my-3" type="text" value="{{ key }}" readonly size=32>
<p>
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.
</p>
<input type="submit" value="Next">
<input class="btn btn-primary" type="submit" value="Next">
</form>
</div>
</div>
<div class="box box_grey">
<h2>Option 3 - Email/password sign up</h2>
<div class="col-sm-4">
<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">
<p>
<div class="card-body">
<p class="alert alert-danger">
<b>Only do this if you don't have a forum account!</b>
</p>
<p>
@@ -108,8 +124,10 @@ Creating an Account
options.
</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>
{% endif %}
{% endblock %}

View File

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

View File

@@ -7,7 +7,7 @@
{% block content %}
{% 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.
<a class="alert_right button" href="{{ url_for('home_page') }}">Skip</a>

View File

@@ -7,72 +7,131 @@
{% block content %}
{% 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">
Is this you? Claim your account now!
<div class="alert alert-info alert alert-info">
<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>
{% endif %}
<div class="box box_grey">
<h2>{{ user.display_name }}</h2>
<table class="box-body">
<tr>
<td>Rank:</td>
<td>
{{ user.rank.getTitle() }}
</td>
</tr>
<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 %}
&#x1f30e;
{% 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>
<div class="row mb-3">
<div class="col-sm-6">
<div class="card">
<h2 class="card-header">{{ user.display_name }}</h2>
<div class="card-body row">
<div class="col-md-2">
{% if user.forums_username %}
<a href="https://forum.minetest.net/ucp.php?i=profile&mode=avatar">
{% elif user.email %}
<a href="https://en.gravatar.com/">
{% endif %}
</td>
</tr>
{% endif %}
</table>
</div>
<img class="img-responsive user-photo img-thumbnail img-thumbnail-1" src="{{ user.getProfilePicURL() }}">
{% if user.forums_username or user.email %}
</a>
{% endif %}
</div>
<div class="col">
<table class="table">
<tr>
<td>Rank:</td>
<td>
{{ user.rank.getTitle() }}
</td>
</tr>
<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 %}
&#x1f30e;
{% 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 %}
{% from "macros/forms.html" import render_field, render_submit_field %}
<div class="box box_grey">
<h2>Edit Details</h2>
<form action="" method="POST" class="form box-body" role="form">
<div class="row">
<div class="col-sm-6 col-md-5 col-lg-4">
<div class="col-sm-6">
<div class="card">
<div class="card-header">Edit Details</div>
<div class="card-body">
<form action="" method="POST" class="form box-body" role="form">
{{ form.hidden_tag() }}
{% if user.checkPerm(current_user, "CHANGE_DNAME") %}
@@ -89,31 +148,41 @@
{% endif %}
{{ render_submit_field(form.submit, tabindex=280) }}
</div>
</form>
</div>
</form>
</div>
</div>
{% endif %}
</div>
{% from "macros/packagegridtile.html" import render_pkggrid %}
{{ render_pkggrid(packages, show_author=False) }}
{% if topics_to_add %}
<div class="box box_grey">
<h2>Unadded Packages</h2>
{% if current_user == user or (current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.EDITOR)) %}
<div class="card mt-3">
<a name="unadded-topics"></a>
<h2 class="card-header">Unadded topics</h2>
<div class="box-body">
<p>
List of your topics without a matching package.
Powered by Krock's Mod Search.
{% if topics_to_add %}
<p class="card-body">
List of your forum topics which do not have a matching package.
Topics with a strikethrough have been marked as discarded.
</p>
{% from "macros/topictable.html" import render_topictable %}
{{ render_topictable(topics_to_add, show_author=False) }}
</div>
{% from "macros/topics.html" import render_topics_table %}
{{ render_topics_table(topics_to_add, show_author=False, show_discard=True, current_user=current_user) }}
{% else %}
<p class="card-body">Congrats! You don't have any topics which aren't on CDB.</p>
{% endif %}
</div>
{% endif %}
{% endblock %}
{% block scriptextra %}
<script>
var csrf_token = "{{ csrf_token() }}";
</script>
<script src="/static/topic_discard.js"></script>
{% endblock %}

View File

@@ -50,6 +50,28 @@ def doFileUpload(file, allowedExtensions, fileTypeName):
file.save(os.path.join("app/public/uploads", filename))
return "/uploads/" + filename
def make_flask_user_password(plaintext_str):
# http://passlib.readthedocs.io/en/stable/modular_crypt_format.html
# http://passlib.readthedocs.io/en/stable/lib/passlib.hash.bcrypt.html#format-algorithm
# Flask_User stores passwords in the Modular Crypt Format.
# https://github.com/lingthio/Flask-User/blob/master/flask_user/user_manager__settings.py#L166
# Note that Flask_User allows customizing password algorithms.
# USER_PASSLIB_CRYPTCONTEXT_SCHEMES defaults to bcrypt but if
# default changes or is customized, the code below needs adapting.
# Individual password values will look like:
# $2b$12$.az4S999Ztvy/wa3UdQvMOpcki1Qn6VYPXmEFMIdWQyYs7ULnH.JW
# $XX$RR$SSSSSSSSSSSSSSSSSSSSSSHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
# $XX : Selects algorithm (2b is bcrypt).
# $RR : Selects bcrypt key expansion rounds (12 is 2**12 rounds).
# $SSS... : 22 chars of (random, per-password) salt
# HHH... : 31 remaining chars of password hash (note no dollar sign)
import bcrypt
plaintext = plaintext_str.encode("UTF-8")
password = bcrypt.hashpw(plaintext, bcrypt.gensalt())
if isinstance(password, str):
return password
else:
return password.decode("UTF-8")
def _do_login_user(user, remember_me=False):
def _call_or_get(v):
@@ -151,3 +173,13 @@ def clearNotifications(url):
if current_user.is_authenticated:
Notification.query.filter_by(user=current_user, url=url).delete()
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)

View File

@@ -18,13 +18,11 @@
from app import app, pages
from flask import *
from flask_user import *
from flask_login import login_user, logout_user
from app.models import *
import flask_menu as menu
from flask.ext import markdown
from sqlalchemy import func
from werkzeug.contrib.cache import SimpleCache
from urllib.parse import urlparse
from sqlalchemy.sql.expression import func
cache = SimpleCache()
@app.template_filter()
@@ -35,6 +33,10 @@ def throw(err):
def domain(url):
return urlparse(url).netloc
@app.template_filter()
def date(value):
return value.strftime("%Y-%m-%d")
@app.template_filter()
def datetime(value):
return value.strftime("%Y-%m-%d %H:%M") + " UTC"
@@ -46,13 +48,18 @@ def send_upload(path):
@app.route("/")
@menu.register_menu(app, ".", "Home")
def home_page():
query = Package.query.filter_by(approved=True, soft_deleted=False)
count = query.count()
packages = query.order_by(db.desc(Package.created_at)).limit(15).all()
return render_template("index.html", packages=packages, count=count)
query = Package.query.filter_by(approved=True, soft_deleted=False)
count = query.count()
new = query.order_by(db.desc(Package.created_at)).limit(8).all()
pop_mod = query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score)).limit(8).all()
pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(4).all()
pop_txp = query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score)).limit(4).all()
downloads = db.session.query(func.sum(PackageRelease.downloads)).first()[0]
return render_template("index.html", count=count, downloads=downloads, \
new=new, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam)
from . import users, githublogin, packages, meta, threads, api
from . import sass, tasks, admin, notifications, tagseditor, thumbnails
from . import users, packages, meta, threads, api
from . import sass, thumbnails, tasks, admin
@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
@app.route('/<path:path>/')

View File

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

View File

@@ -17,26 +17,28 @@
from flask import *
from flask_user import *
from flask.ext import menu
import flask_menu as menu
from app import app
from app.models import *
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies
from app.tasks.forumtasks import importUsersFromModList, importKrocksModList
from celery import uuid
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies, makeVCSRelease
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
from flask_wtf import FlaskForm
from wtforms import *
from app.utils import loginUser, rank_required
from app.utils import loginUser, rank_required, triggerNotif
import datetime
@app.route("/admin/", methods=["GET", "POST"])
@rank_required(UserRank.ADMIN)
def admin_page():
if request.method == "POST":
action = request.form["action"]
if action == "importusers":
task = importUsersFromModList.delay()
return redirect(url_for("check_task", id=task.id, r=url_for("user_list_page")))
elif action == "importmodlist":
task = importKrocksModList.delay()
if action == "importmodlist":
task = importTopicList.delay()
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":
packages = Package.query \
.filter_by(soft_deleted=False) \
@@ -67,6 +69,31 @@ def admin_page():
db.session.commit()
return redirect(url_for("admin_page"))
elif action == "recalcscores":
for p in Package.query.all():
p.recalcScore()
db.session.commit()
return redirect(url_for("admin_page"))
elif action == "vcsrelease":
for package in Package.query.filter(Package.repo.isnot(None)).all():
if package.releases.count() != 0:
continue
rel = PackageRelease()
rel.package = package
rel.title = datetime.date.today().isoformat()
rel.url = ""
rel.task_id = uuid()
rel.approved = True
db.session.add(rel)
db.session.commit()
makeVCSRelease.apply_async((rel.id, "master"), task_id=rel.task_id)
msg = "{}: Release {} created".format(package.title, rel.title)
triggerNotif(package.author, current_user, msg, rel.getEditURL())
db.session.commit()
else:
flash("Unknown action: " + action, "error")

View File

@@ -0,0 +1,62 @@
# 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("/licenses/")
@rank_required(UserRank.MODERATOR)
def license_list_page():
return render_template("admin/licenses/list.html", licenses=License.query.order_by(db.asc(License.name)).all())
class LicenseForm(FlaskForm):
name = StringField("Name", [InputRequired(), Length(3,100)])
is_foss = BooleanField("Is FOSS")
submit = SubmitField("Save")
@app.route("/licenses/new/", methods=["GET", "POST"])
@app.route("/licenses/<name>/edit/", methods=["GET", "POST"])
@rank_required(UserRank.MODERATOR)
def createedit_license_page(name=None):
license = None
if name is not None:
license = License.query.filter_by(name=name).first()
if license is None:
abort(404)
form = LicenseForm(formdata=request.form, obj=license)
if request.method == "GET" and license is None:
form.is_foss.data = True
elif request.method == "POST" and form.validate():
if license is None:
license = License(form.name.data)
db.session.add(license)
flash("Created license " + form.name.data, "success")
else:
flash("Updated license " + form.name.data, "success")
form.populate_obj(license)
db.session.commit()
return redirect(url_for("license_list_page"))
return render_template("admin/licenses/edit.html", license=license, form=form)

View File

@@ -27,7 +27,7 @@ from app.utils import rank_required
@app.route("/tags/")
@rank_required(UserRank.MODERATOR)
def tag_list_page():
return render_template("tags/list.html", tags=Tag.query.order_by(db.asc(Tag.title)).all())
return render_template("admin/tags/list.html", tags=Tag.query.order_by(db.asc(Tag.title)).all())
class TagForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(3,100)])
@@ -54,4 +54,4 @@ def createedit_tag_page(name=None):
db.session.commit()
return redirect(url_for("createedit_tag_page", name=tag.name))
return render_template("tags/edit.html", tag=tag, form=form)
return render_template("admin/tags/edit.html", tag=tag, form=form)

101
app/views/admin/todo.py Normal file
View 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)

View File

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

View File

@@ -20,16 +20,50 @@ from flask_user import *
from app import app
from app.models import *
from app.utils import is_package_page
from .packages import build_packages_query
from app.querybuilder import QueryBuilder
@app.route("/api/packages/")
def api_packages_page():
query, _ = build_packages_query()
pkgs = [package.getAsDictionaryShort(app.config["BASE_URL"]) \
for package in query.all() if package.getDownloadRelease() is not None]
qb = QueryBuilder(request.args)
query = qb.buildPackageQuery()
ver = qb.getMinetestVersion()
pkgs = [package.getAsDictionaryShort(app.config["BASE_URL"], version=ver) \
for package in query.all()]
return jsonify(pkgs)
@app.route("/api/packages/<author>/<name>/")
@is_package_page
def api_package_page(package):
return jsonify(package.getAsDictionary(app.config["BASE_URL"]))
@app.route("/api/topics/")
def api_topics_page():
qb = QueryBuilder(request.args)
query = qb.buildTopicQuery(show_added=True)
return jsonify([t.getAsDictionary() for t in query.all()])
@app.route("/api/topic_discard/", methods=["POST"])
@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])

View File

@@ -15,294 +15,4 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import *
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
def build_packages_query():
type_name = request.args.get("type")
type = None
if type_name is not None:
type = PackageType[type_name.upper()]
title = "Packages"
query = Package.query.filter_by(soft_deleted=False)
if type is not None:
title = type.value + "s"
query = query.filter_by(type=type, approved=True)
search = request.args.get("q")
if search is not None and search.strip() != "":
query = query.filter(Package.title.ilike('%' + search + '%'))
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 \
KrockForumTopic.query \
.filter_by(name=package.name) \
.filter(KrockForumTopic.topic_id != package.forums) \
.filter(~ db.exists().where(Package.forums==KrockForumTopic.topic_id)) \
.order_by(db.asc(KrockForumTopic.name), db.asc(KrockForumTopic.title)) \
.all()
releases = getReleases(package)
requests = [r for r in package.requests if r.status == 0]
review_thread = Thread.query.filter_by(package_id=package.id, private=True).first()
if review_thread is not None and not review_thread.checkPerm(current_user, Permission.SEE_THREAD):
review_thread = None
return render_template("packages/view.html", \
package=package, releases=releases, requests=requests, \
alternatives=alternatives, similar_topics=similar_topics, \
review_thread=review_thread)
@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, get_pk=lambda a: a.id, get_label=lambda a: a.name)
media_license = QuerySelectField("Media License", [InputRequired()], query_factory=lambda: License.query, 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
from . import packages, screenshots, releases

View File

@@ -17,7 +17,6 @@
from flask import *
from flask_user import *
from flask.ext import menu
from app import app
from app.models import *

View File

@@ -0,0 +1,360 @@
# Content DB
# Copyright (C) 2018 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import render_template, abort, request, redirect, url_for, flash
from flask_user import current_user
import flask_menu as menu
from app import app
from app.models import *
from app.querybuilder import QueryBuilder
from app.tasks.importtasks import importRepoScreenshot
from app.utils import *
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
from sqlalchemy import or_
@menu.register_menu(app, ".mods", "Mods", order=11, endpoint_arguments_constructor=lambda: { 'type': 'mod' })
@menu.register_menu(app, ".games", "Games", order=12, endpoint_arguments_constructor=lambda: { 'type': 'game' })
@menu.register_menu(app, ".txp", "Texture Packs", order=13, endpoint_arguments_constructor=lambda: { 'type': 'txp' })
@menu.register_menu(app, ".random", "Random", order=14, endpoint_arguments_constructor=lambda: { 'random': '1' })
@app.route("/packages/")
def packages_page():
qb = QueryBuilder(request.args)
query = qb.buildPackageQuery()
title = qb.title
if qb.lucky:
package = query.first()
if package:
return redirect(package.getDetailsURL())
topic = qb.buildTopicQuery().first()
if qb.search and topic:
return redirect("https://forum.minetest.net/viewtopic.php?t=" + str(topic.topic_id))
page = int(request.args.get("page") or 1)
num = min(40, int(request.args.get("n") or 100))
query = query.paginate(page, num, True)
search = request.args.get("q")
type_name = request.args.get("type")
next_url = url_for("packages_page", type=type_name, q=search, page=query.next_num) \
if query.has_next else None
prev_url = url_for("packages_page", type=type_name, q=search, page=query.prev_num) \
if query.has_prev else None
topics = None
if qb.search and not query.has_next:
topics = qb.buildTopicQuery().all()
tags = Tag.query.all()
return render_template("packages/list.html", \
title=title, packages=query.items, topics=topics, \
query=search, tags=tags, type=type_name, \
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, packages_count=query.total)
def getReleases(package):
if package.checkPerm(current_user, Permission.MAKE_RELEASE):
return package.releases.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
else:
triggerNotif(package.author, current_user,
"{} edited".format(package.title), package.getDetailsURL())
form.populate_obj(package) # copy to row
mpackage_cache = {}
package.provides.clear()
mpackages = MetaPackage.SpecToList(form.provides_str.data, mpackage_cache)
for m in mpackages:
package.provides.append(m)
Dependency.query.filter_by(depender=package).delete()
deps = Dependency.SpecToList(package, form.harddep_str.data, mpackage_cache)
for dep in deps:
dep.optional = False
db.session.add(dep)
deps = Dependency.SpecToList(package, form.softdep_str.data, mpackage_cache)
for dep in deps:
dep.optional = True
db.session.add(dep)
if wasNew and package.type == PackageType.MOD and not package.name in mpackage_cache:
m = MetaPackage.GetOrCreate(package.name, mpackage_cache)
package.provides.append(m)
package.tags.clear()
for tag in form.tags.raw_data:
package.tags.append(Tag.query.get(tag))
db.session.commit() # save
next_url = package.getDetailsURL()
if wasNew and package.repo is not None:
task = importRepoScreenshot.delay(package.id)
next_url = url_for("check_task", id=task.id, r=next_url)
if wasNew and ("WTFPL" in package.license.name or "WTFPL" in package.media_license.name):
next_url = url_for("flatpage", path="help/wtfpl", r=next_url)
return redirect(next_url)
package_query = Package.query.filter_by(approved=True, soft_deleted=False)
if package is not None:
package_query = package_query.filter(Package.id != package.id)
enableWizard = name is None and request.method != "POST"
return render_template("packages/create_edit.html", package=package, \
form=form, author=author, enable_wizard=enableWizard, \
packages=package_query.all(), \
mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all())
@app.route("/packages/<author>/<name>/approve/", methods=["POST"])
@login_required
@is_package_page
def approve_package_page(package):
if not package.checkPerm(current_user, Permission.APPROVE_NEW):
flash("You don't have permission to do that.", "error")
elif package.approved:
flash("Package has already been approved", "error")
else:
package.approved = True
screenshots = PackageScreenshot.query.filter_by(package=package, approved=False).all()
for s in screenshots:
s.approved = True
triggerNotif(package.author, current_user,
"{} approved".format(package.title), package.getDetailsURL())
db.session.commit()
return redirect(package.getDetailsURL())
@app.route("/packages/<author>/<name>/remove/", methods=["GET", "POST"])
@login_required
@is_package_page
def remove_package_page(package):
if request.method == "GET":
return render_template("packages/remove.html", package=package)
if "delete" in request.form:
if not package.checkPerm(current_user, Permission.DELETE_PACKAGE):
flash("You don't have permission to do that.", "error")
return redirect(package.getDetailsURL())
package.soft_deleted = True
url = url_for("user_profile_page", username=package.author.username)
triggerNotif(package.author, current_user,
"{} deleted".format(package.title), url)
db.session.commit()
flash("Deleted package", "success")
return redirect(url)
elif "unapprove" in request.form:
if not package.checkPerm(current_user, Permission.UNAPPROVE_PACKAGE):
flash("You don't have permission to do that.", "error")
return redirect(package.getDetailsURL())
package.approved = False
triggerNotif(package.author, current_user,
"{} deleted".format(package.title), package.getDetailsURL())
db.session.commit()
flash("Unapproved package", "success")
return redirect(package.getDetailsURL())
else:
abort(400)

View File

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

View File

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

View File

@@ -1,64 +0,0 @@
# Content DB
# Copyright (C) 2018 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import *
from flask_user import *
from flask.ext import menu
from app import app
from app.models import *
@app.route("/todo/")
@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()
topics_to_add = KrockForumTopic.query \
.filter(~ db.exists().where(Package.forums==KrockForumTopic.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)
@app.route("/todo/topics/")
@login_required
def todo_topics_page():
total = KrockForumTopic.query.count()
topics = KrockForumTopic.query \
.filter(~ db.exists().where(Package.forums==KrockForumTopic.topic_id)) \
.order_by(db.asc(KrockForumTopic.name), db.asc(KrockForumTopic.title)) \
.all()
return render_template("todo/topics.html", topics=topics, total=total)

View File

@@ -17,7 +17,7 @@
from flask import *
from flask_user import *
from flask.ext import menu
import flask_menu as menu
from app import app, csrf
from app.models import *
from app.tasks import celery, TaskError

View File

@@ -21,14 +21,53 @@ from app import app
from app.models import *
from app.utils import triggerNotif, clearNotifications
import datetime
from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
@app.route("/threads/")
def threads_page():
threads = Thread.query.filter_by(private=False).all()
return render_template("threads/list.html", threads=threads)
query = Thread.query
if not Permission.SEE_THREAD.check(current_user):
query = query.filter_by(private=False)
return render_template("threads/list.html", threads=query.all())
@app.route("/threads/<int:id>/subscribe/", methods=["POST"])
@login_required
def thread_subscribe_page(id):
thread = Thread.query.get(id)
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
abort(404)
if current_user in thread.watchers:
flash("Already subscribed!", "success")
else:
flash("Subscribed to thread", "success")
thread.watchers.append(current_user)
db.session.commit()
return redirect(url_for("thread_page", id=id))
@app.route("/threads/<int:id>/unsubscribe/", methods=["POST"])
@login_required
def thread_unsubscribe_page(id):
thread = Thread.query.get(id)
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
abort(404)
if current_user in thread.watchers:
flash("Unsubscribed!", "success")
thread.watchers.remove(current_user)
db.session.commit()
else:
flash("Not subscribed to thread", "success")
return redirect(url_for("thread_page", id=id))
@app.route("/threads/<int:id>/", methods=["GET", "POST"])
def thread_page(id):
@@ -41,6 +80,13 @@ def thread_page(id):
if current_user.is_authenticated and request.method == "POST":
comment = request.form["comment"]
if not current_user.canCommentRL():
flash("Please wait before commenting again", "danger")
if package:
return redirect(package.getDetailsURL())
else:
return redirect(url_for("home_page"))
if len(comment) <= 500 and len(comment) > 3:
reply = ThreadReply()
reply.author = current_user
@@ -89,27 +135,33 @@ def new_thread_page():
if package is None:
flash("Unable to find that package!", "error")
# Don't allow making threads on approved packages for now
if package is None or package.approved:
# Don't allow making orphan threads on approved packages for now
if package is None:
abort(403)
def_is_private = request.args.get("private") or False
if not package.approved:
if package is None or not package.approved:
def_is_private = True
allow_change = package.approved
is_review_thread = package is not None and not package.approved
allow_change = package and package.approved
is_review_thread = package and not package.approved
# Check that user can make the thread
if is_review_thread and not (package.author == current_user or \
package.checkPerm(current_user, Permission.APPROVE_NEW)):
if not package.checkPerm(current_user, Permission.CREATE_THREAD):
flash("Unable to create thread!", "error")
return redirect(url_for("home_page"))
# Only allow creating one thread when not approved
elif is_review_thread and package.review_thread is not None:
flash("A review thread already exists!", "error")
if request.method == "GET":
return redirect(url_for("thread_page", id=package.review_thread.id))
return redirect(url_for("thread_page", id=package.review_thread.id))
elif not current_user.canOpenThreadRL():
flash("Please wait before opening another thread", "danger")
if package:
return redirect(package.getDetailsURL())
else:
return redirect(url_for("home_page"))
# Set default values
elif request.method == "GET":
@@ -142,13 +194,19 @@ def new_thread_page():
if is_review_thread:
package.review_thread = thread
notif_msg = None
if package is not None:
triggerNotif(package.author, current_user,
"New thread '{}' on package {}".format(thread.title, package.title), url_for("thread_page", id=thread.id))
notif_msg = "New thread '{}' on package {}".format(thread.title, package.title)
triggerNotif(package.author, current_user, notif_msg, url_for("thread_page", id=thread.id))
else:
notif_msg = "New thread '{}'".format(thread.title)
for user in User.query.filter(User.rank >= UserRank.EDITOR).all():
triggerNotif(user, current_user, notif_msg, url_for("thread_page", id=thread.id))
db.session.commit()
return redirect(url_for("thread_page", id=thread.id))
return render_template("threads/new.html", form=form, allow_private_change=allow_change)
return render_template("threads/new.html", form=form, allow_private_change=allow_change, package=package)

View File

@@ -18,10 +18,10 @@
from flask import *
from app import app
import glob, os
import os
from PIL import Image
ALLOWED_RESOLUTIONS=[(350,233)]
ALLOWED_RESOLUTIONS=[(100,67), (270,180), (350,233)]
def mkdir(path):
if not os.path.isdir(path):
@@ -29,18 +29,45 @@ def mkdir(path):
mkdir("app/public/thumbnails/")
@app.route("/thumbnails/<img>")
@app.route("/thumbnails/<int:w>x<int:h>/<img>")
def make_thumbnail(img, w=350, h=233):
if not (w, h) in ALLOWED_RESOLUTIONS:
def resize_and_crop(img_path, modified_path, size):
img = Image.open(img_path)
# Get current and desired ratio for the images
img_ratio = img.size[0] / float(img.size[1])
ratio = size[0] / float(size[1])
# Is more portrait than target, scale and crop
if ratio > img_ratio:
img = img.resize((int(size[0]), int(size[0] * img.size[1] / img.size[0])),
Image.BICUBIC)
box = (0, (img.size[1] - size[1]) / 2, img.size[0], (img.size[1] + size[1]) / 2)
img = img.crop(box)
# Is more landscape than target, scale and crop
elif ratio < img_ratio:
img = img.resize((int(size[1] * img.size[0] / img.size[1]), int(size[1])),
Image.BICUBIC)
box = ((img.size[0] - size[0]) / 2, 0, (img.size[0] + size[0]) / 2, img.size[1])
img = img.crop(box)
# Is exactly the same ratio as target
else:
img = img.resize(size, Image.BICUBIC)
img.save(modified_path)
@app.route("/thumbnails/<int:level>/<img>")
def make_thumbnail(img, level):
if level > len(ALLOWED_RESOLUTIONS) or level <= 0:
abort(403)
mkdir("app/public/thumbnails/{}x{}/".format(w, h))
w, h = ALLOWED_RESOLUTIONS[level - 1]
cache_filepath = "public/thumbnails/{}x{}/{}".format(w, h, img)
mkdir("app/public/thumbnails/{:d}/".format(level))
cache_filepath = "public/thumbnails/{:d}/{}".format(level, img)
source_filepath = "public/uploads/" + img
im = Image.open("app/" + source_filepath)
im.thumbnail((w, h), Image.ANTIALIAS)
im.save("app/" + cache_filepath, optimize=True)
resize_and_crop("app/" + source_filepath, "app/" + cache_filepath, (w, h))
return send_file(cache_filepath)

View File

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

View File

@@ -51,9 +51,9 @@ def github_authorized(oauth_token):
if current_user and current_user.is_authenticated:
if userByGithub is None:
current_user.github_username = username
db.session.add(auth)
db.session.commit()
return redirect(url_for("gitAccount", id=auth.id))
flash("Linked github to account", "success")
return redirect(url_for("home_page"))
else:
flash("Github account is already associated with another user", "danger")
return redirect(url_for("home_page"))

View File

@@ -18,16 +18,14 @@
from flask import *
from flask_user import *
from flask_login import login_user, logout_user
from flask.ext import menu
from app import app
from app import app, markdown
from app.models import *
from flask_wtf import FlaskForm
from flask_user.forms import RegisterForm
from wtforms import *
from wtforms.validators import *
from app.utils import rank_required, randomString, loginUser
from app.utils import randomString, loginUser, rank_required
from app.tasks.forumtasks import checkForumAccount
from app.tasks.emails import sendVerifyEmail
from app.tasks.emails import sendVerifyEmail, sendEmailRaw
from app.tasks.phpbbparser import getProfile
# Define the User profile form
@@ -98,16 +96,66 @@ def user_profile_page(username):
topics_to_add = None
if current_user == user or user.checkPerm(current_user, Permission.CHANGE_AUTHOR):
topics_to_add = KrockForumTopic.query \
topics_to_add = ForumTopic.query \
.filter_by(author_id=user.id) \
.filter(~ db.exists().where(Package.forums==KrockForumTopic.topic_id)) \
.order_by(db.asc(KrockForumTopic.name), db.asc(KrockForumTopic.title)) \
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
.all()
# Process GET or invalid POST
return render_template("users/user_profile_page.html",
user=user, form=form, packages=packages, topics_to_add=topics_to_add)
@app.route("/users/<username>/check/", methods=["POST"])
@login_required
def user_check(username):
user = User.query.filter_by(username=username).first()
if user is None:
abort(404)
if current_user != user and not current_user.rank.atLeast(UserRank.MODERATOR):
abort(403)
if user.forums_username is None:
abort(404)
task = checkForumAccount.delay(user.forums_username)
next_url = url_for("user_profile_page", username=username)
return redirect(url_for("check_task", id=task.id, r=next_url))
class SendEmailForm(FlaskForm):
subject = StringField("Subject", [InputRequired(), Length(1, 300)])
text = TextAreaField("Message", [InputRequired()])
submit = SubmitField("Send")
@app.route("/users/<username>/email/", methods=["GET", "POST"])
@rank_required(UserRank.MODERATOR)
def send_email_page(username):
user = User.query.filter_by(username=username).first()
if user is None:
abort(404)
next_url = url_for("user_profile_page", username=user.username)
if user.email is None:
flash("User has no email address!", "error")
return redirect(next_url)
form = SendEmailForm(request.form)
if form.validate_on_submit():
text = form.text.data
html = markdown(text)
task = sendEmailRaw.delay([user.email], form.subject.data, text, html)
return redirect(url_for("check_task", id=task.id, r=next_url))
return render_template("users/send_email.html", form=form)
class SetPasswordForm(FlaskForm):
email = StringField("Email", [Optional(), Email()])
password = PasswordField("New password", [InputRequired(), Length(2, 20)])

View File

@@ -22,3 +22,4 @@ MAIL_DEFAULT_SENDER=""
MAIL_SERVER=""
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_UTILS_ERROR_SEND_TO=[""]

37
docker-compose.yml Normal file
View File

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

View File

@@ -0,0 +1,28 @@
"""empty message
Revision ID: 11b6ef362f98
Revises: 9fc23495713b
Create Date: 2018-07-04 01:01:45.440662
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '11b6ef362f98'
down_revision = '9fc23495713b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('package', sa.Column('score', sa.Float(), nullable=False, server_default="0.0"))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('package', 'score')
# ### end Alembic commands ###

Some files were not shown because too many files have changed in this diff Show More