Compare commits

..

25 Commits

Author SHA1 Message Date
rubenwardy
cc564af44e Fix broken reference based git import
Fixes #130
2019-08-31 22:09:19 +01:00
rubenwardy
655ed2255a Fix crash by truncating notification titles 2019-08-31 18:38:15 +01:00
rubenwardy
96b22744ec Fix crash on null release task_id 2019-08-31 18:32:59 +01:00
rubenwardy
130d0bc7a0 Fix wtfforms setting fields to empty string instead of None 2019-08-30 21:11:38 +01:00
rubenwardy
1469e37c38 Fix accidental limit on password length 2019-08-12 14:10:28 +01:00
rubenwardy
6ce495fcd3 Fix crash on reading mod.conf from Github 2019-08-09 11:27:54 +01:00
rubenwardy
776a3eff2a Fail gracefully when given a bad git reference 2019-08-09 11:25:19 +01:00
rubenwardy
04e8ae5bdd Fix unexpected crash on bad Github URL 2019-08-09 11:17:39 +01:00
rubenwardy
18b9fb3876 Fix typo in zip uploading 2019-08-09 11:10:45 +01:00
rubenwardy
1da86f27a7 Fix topic ID parse error in import topics task 2019-07-29 23:31:42 +01:00
rubenwardy
85340a2fe9 Add note about media license
Fixes #150
2019-07-29 22:48:05 +01:00
rubenwardy
c4a4d9c116 Fix broken link on create thread
Fixes #147
2019-07-29 22:39:56 +01:00
rubenwardy
87a184595c Add file extension filters to file upload dialogs
Thanks to @b3u
2019-07-29 22:34:39 +01:00
rubenwardy
b3b1e421f2 Check that uploaded images are valid images 2019-07-29 22:21:56 +01:00
rubenwardy
60483ef542 Add translation support 2019-07-29 21:44:39 +01:00
rubenwardy
3c8a8b8988 Fix name field always being readonly 2019-07-29 21:03:04 +01:00
rubenwardy
2f8bdd8f0f Increase CSS version 2019-07-29 20:41:48 +01:00
rubenwardy
e87db8b87f Prevent users from changing the name of approved packages 2019-07-29 20:29:55 +01:00
rubenwardy
b36273a848 Add website and donation support 2019-07-02 00:45:16 +01:00
Hugo Locurcio
7b087158d7 Optimize images losslessly using oxipng -o6 --zopfli --strip 2019-06-12 00:10:56 +01:00
rubenwardy
2fbc44bd54 Make user list public 2019-06-10 00:11:57 +01:00
rubenwardy
950512c2a7 Add favicon 2019-06-07 16:54:33 +01:00
rubenwardy
f4010d498f Update policy and guidance 2019-04-23 01:30:17 +01:00
rubenwardy
f04d4ff3cd Allow release auto-approval on unapproved packages 2019-03-30 15:42:31 +00:00
rubenwardy
f8b290fc45 Add badges next to packages awaiting approval list 2019-03-30 15:41:38 +00:00
41 changed files with 283 additions and 99 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
config.cfg
*.env
*.sqlite
.vscode
custom.css
tmp
log.txt

View File

@@ -24,6 +24,7 @@ from flaskext.markdown import Markdown
from flask_github import GitHub
from flask_wtf.csrf import CsrfProtect
from flask_flatpages import FlatPages
from flask_babel import Babel
import os
app = Flask(__name__, static_folder="public/static")
@@ -37,6 +38,7 @@ github = GitHub(app)
csrf = CsrfProtect(app)
mail = Mail(app)
pages = FlatPages(app)
babel = Babel(app)
gravatar = Gravatar(app,
size=58,
rating='g',
@@ -50,5 +52,11 @@ if not app.debug:
from .maillogger import register_mail_error_handler
register_mail_error_handler(app, mail)
@babel.localeselector
def get_locale():
return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
from . import models, tasks
from .views import *

View File

@@ -10,36 +10,45 @@ ContentDB is for the community, and as such listings should be useful to the
community. To help with this, there are a few rules to improve the quality of
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.
* No inappropriate content. <sup>2.1</sup>
* Content must be playable/useful, but not necessarily finished. <sup>2.2</sup>
* Don't use the name of another mod unless your mod is a fork or reimplementation. <sup>3</sup>
* Licenses must allow derivatives, redistribution, and must not discriminate. <sup>4</sup>
* Don't put promotions or advertisements in package listings, except for
donation and personal website links which are permitted in the
long description. <sup>5</sup>
* The ContentDB admin reserves the right to remove packages for any reason,
including ones not covered by this document, and to ban users who abuse
this service. <sup>1</sup>
## 1. General
It is not permitted to submit abusive, obscene, vulgar, slanderous, hateful,
threatening, sexually-orientated or any material that may violate any laws be
it of your country, the country where "Content DB” is hosted or International Law.
The ContentDB admin reserves the right to remove packages for any reason,
including ones not covered by this document, and to ban users who abuse this service.
Also see the [help page on tags](/help/package_tags/).
## 2. Accepted Content and State of Completion
## 2. Accepted Content
### 2.1. Acceptable Content
Sexually-orientated content is not permitted.
Mature content, including that relating to drugs, excessive gore, violence, or
horror, is not currently permitted - but will be in the future.
The submission of malware is strictly prohibited. This includes software which
does not do as it advertises, for example if it posts telemetry without stating
clearly that it does in the package meta.
### 2.2. State of Completion
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
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.
@@ -116,15 +125,15 @@ Public domain is not a valid license in many countries, please use CC0 or MIT in
## 5. Promotions and Advertisements (inc. asking for donations)
Any information other than the long description - including screenshots - must
not contain any promotions or advertisements. This includes asking for donations,
promoting online shops, or linking to personal websites and social media.
You may note place any promotions or advertisements in any meta data including
screensthos. This includes asking for donations, promoting online shops,
or linking to personal websites and social media. Please instead use the
fields provided on your user profile page to place links to websites and
donation pages.
ContentDB is for the community. We may remove any promotions if we feel that
they're inappropriate.
Paid promotions are not allowed at all, anywhere.
## 6. Reporting Violations

View File

@@ -76,6 +76,7 @@ class Permission(enum.Enum):
APPROVE_CHANGES = "APPROVE_CHANGES"
DELETE_PACKAGE = "DELETE_PACKAGE"
CHANGE_AUTHOR = "CHANGE_AUTHOR"
CHANGE_NAME = "CHANGE_NAME"
MAKE_RELEASE = "MAKE_RELEASE"
ADD_SCREENSHOTS = "ADD_SCREENSHOTS"
APPROVE_SCREENSHOT = "APPROVE_SCREENSHOT"
@@ -129,6 +130,10 @@ class User(db.Model, UserMixin):
active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
display_name = db.Column(db.String(100), nullable=False, server_default="")
# Links
website_url = db.Column(db.String(255), nullable=True, default=None)
donate_url = db.Column(db.String(255), nullable=True, default=None)
# Content
notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id")
@@ -208,6 +213,9 @@ class Notification(db.Model):
url = db.Column(db.String(200), nullable=True)
def __init__(self, us, cau, titl, ur):
if len(titl) > 100:
title = title[:99] + ""
self.user = us
self.causer = cau
self.title = titl
@@ -417,6 +425,22 @@ class Package(db.Model):
for e in PackagePropertyKey:
setattr(self, e.name, getattr(package, e.name))
def getState(self):
if self.approved:
return "approved"
elif self.review_thread_id:
return "thread"
elif (self.type == PackageType.GAME or \
self.type == PackageType.TXP) and \
self.screenshots.count() == 0:
return "wip"
elif not self.getDownloadRelease():
return "wip"
elif "Other" in self.license.name or "Other" in self.media_license.name:
return "license"
else:
return "ready"
def getAsDictionaryShort(self, base_url, version=None, protonum=None):
tnurl = self.getThumbnailURL(1)
release = self.getDownloadRelease(version=version, protonum=protonum)
@@ -552,6 +576,10 @@ class Package(db.Model):
else:
return user.rank.atLeast(UserRank.EDITOR)
# Anyone can change the package name when not approved, but only editors when approved
elif perm == Permission.CHANGE_NAME:
return not self.approved or user.rank.atLeast(UserRank.EDITOR)
# Editors can change authors and approve new packages
elif perm == Permission.APPROVE_NEW or perm == Permission.CHANGE_AUTHOR:
return user.rank.atLeast(UserRank.EDITOR)
@@ -697,7 +725,8 @@ class PackageRelease(db.Model):
self.releaseDate = datetime.datetime.now()
def approve(self, user):
if not self.package.checkPerm(user, Permission.APPROVE_RELEASE):
if self.package.approved and \
not self.package.checkPerm(user, Permission.APPROVE_RELEASE):
return False
assert(self.task_id is None and self.url is not None and self.url != "")

BIN
app/public/favicon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
app/public/favicon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

BIN
app/public/favicon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 B

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 B

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 B

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 B

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 B

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -99,11 +99,19 @@ def parseTitle(title):
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
try:
contents = urllib.request.urlopen("https://krock-works.uk.to/minetest/modList.php").read().decode("utf-8")
for x in json.loads(contents):
try:
link = x.get("link")
if link is not None:
links[int(x["topicId"])] = link
except ValueError:
pass
except urllib.error.URLError:
print("Unable to open krocks mod search!")
return links
return links

View File

@@ -29,6 +29,10 @@ from app.utils import randomString
class GithubURLMaker:
def __init__(self, url):
self.baseUrl = None
self.user = None
self.repo = None
# Rewrite path
import re
m = re.search("^\/([^\/]+)\/([^\/]+)\/?$", url.path)
@@ -51,6 +55,9 @@ class GithubURLMaker:
def getScreenshotURL(self):
return self.baseUrl + "/screenshot.png"
def getModConfURL(self):
return self.baseUrl + "/mod.conf"
def getCommitsURL(self, branch):
return "https://api.github.com/repos/{}/{}/commits?sha={}" \
.format(self.user, self.repo, urllib.parse.quote_plus(branch))
@@ -292,15 +299,17 @@ def cloneRepo(urlstr, ref=None, recursive=False):
gitUrl = generateGitURL(urlstr)
print("Cloning from " + gitUrl)
repo = git.Repo.clone_from(gitUrl, gitDir, \
progress=None, env=None, depth=1, recursive=recursive, kill_after_timeout=15)
progress=None, env=None, depth=1, recursive=recursive, kill_after_timeout=15, b=ref)
if ref is not None:
repo.create_head("myhead", ref).checkout()
return gitDir, repo
except GitCommandError as e:
# This is needed to stop the backtrace being weird
err = e.stderr
except gitdb.exc.BadName as e:
err = "Unable to find the reference " + (ref or "?") + "\n" + e.stderr
raise TaskError(err.replace("stderr: ", "") \
.replace("Cloning into '" + gitDir + "'...", "") \
.strip())
@@ -339,8 +348,11 @@ def makeVCSReleaseFromGithub(id, branch, release, url):
raise TaskError("Invalid github repo URL")
commitsURL = urlmaker.getCommitsURL(branch)
contents = urllib.request.urlopen(commitsURL).read().decode("utf-8")
commits = json.loads(contents)
try:
contents = urllib.request.urlopen(commitsURL).read().decode("utf-8")
commits = json.loads(contents)
except HTTPError:
raise TaskError("Unable to get commits for Github repository. Either the repository or reference doesn't exist.")
if len(commits) == 0 or not "sha" in commits[0]:
raise TaskError("No commits found")
@@ -349,7 +361,6 @@ def makeVCSReleaseFromGithub(id, branch, release, url):
release.task_id = None
release.commit_hash = commits[0]["sha"]
release.approve(release.package.author)
print(release.url)
db.session.commit()
return release.url

View File

@@ -7,8 +7,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
<link rel="stylesheet" type="text/css" href="/static/bootstrap.css">
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=6">
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=7">
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
<link rel="shortcut icon" href="/favicon-16.png" sizes="16x16">
<link rel="icon" href="/favicon-128.png" sizes="128x128">
<link rel="icon" href="/favicon-32.png" sizes="32x32">
{% block headextra %}{% endblock %}
</head>
@@ -76,24 +79,24 @@
<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>
<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>
<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>
<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>
<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>
<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>
<li><a class="nav-link" href="{{ url_for('user.login') }}">{{ _("Sign in") }}</a></li>
{% endif %}
</ul>
</div>
@@ -126,10 +129,12 @@
{% endblock %}
<footer class="container footer-copyright my-5 page-footer font-small text-center">
ContentDB &copy; 2018 to <a href="https://rubenwardy.com/">rubenwardy</a> |
ContentDB &copy; 2018-9 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>
<a href="{{ url_for('flatpage', path='help') }}">{{ _("Help") }}</a> |
<a href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("Policy and Guidance") }}</a> |
<a href="{{ url_for('flatpage', path='help/reporting') }}">{{ _("Report / DMCA") }}</a> |
<a href="{{ url_for('user_list_page') }}">{{ _("User List") }}</a>
</footer>
<script src="/static/jquery.min.js"></script>

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block title %}
Welcome
{{ _("Welcome") }}
{% endblock %}
{% block scriptextra %}
@@ -38,35 +38,35 @@ Welcome
<a href="{{ url_for('packages_page', sort='created_at', order='desc') }}" class="btn btn-secondary float-right">
See more
{{ _("See more") }}
</a>
<h2 class="my-3">Recently Added</h2>
<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
{{ _("See more") }}
</a>
<h2 class="my-3">Top Mods</h2>
<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
{{ _("See more") }}
</a>
<h2 class="my-3">Top Games</h2>
<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
{{ _("See more") }}
</a>
<h2 class="my-3">Top Texture Packs</h2>
<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.
{{ _("CDB has %(count)d packages, with a total of %(downloads)d downloads.", count=count, downloads=downloads) }}
</small>
</div>
<!-- </main> -->

View File

@@ -20,19 +20,19 @@
{% endblock %}
{% block content %}
<h1>Create Package</h1>
<h1>{{ _("Create Package") }}</h1>
<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="float-right btn btn-sm btn-default" href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("View") }}</a>
Have you read the Package Inclusion Policy and Guidance yet?
{{ _("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.
{{ _("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>
@@ -42,12 +42,16 @@
{{ form.hidden_tag() }}
<fieldset>
<legend>Package</legend>
<legend>{{ _("Package") }}</legend>
<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") }}
{% if package and package.approved and not package.checkPerm(current_user, "CHANGE_NAME") %}
{{ render_field(form.name, class_="pkg_meta col-sm-3", readonly=True) }}
{% else %}
{{ render_field(form.name, class_="pkg_meta col-sm-3") }}
{% endif %}
</div>
{{ render_field(form.short_desc, class_="pkg_meta") }}
{{ render_multiselect_field(form.tags, class_="pkg_meta") }}
@@ -55,11 +59,15 @@
{{ render_field(form.license, class_="not_txp col-sm-6") }}
{{ render_field(form.media_license, class_="col-sm-6") }}
</div>
<div class="pkg_meta row">
<div class="not_txp col-sm-6"></div>
<div class="not_txp col-sm-6">{{ _("If there is no media, set the Media License to the same as the License.") }}</div>
</div>
{{ render_field(form.desc, class_="pkg_meta", fieldclass="form-control markdown") }}
</fieldset>
<fieldset class="pkg_meta">
<legend class="not_txp">Dependencies</legend>
<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") }}
@@ -67,30 +75,29 @@
</fieldset>
<fieldset>
<legend class="pkg_meta">Repository and Links</legend>
<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>
<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. Click skip if the import fails.</p>
<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") }}
<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>
<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)
{{ _("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") }}
{{ render_field(form.forums, class_="pkg_meta", placeholder=_("Tip: paste in a forum topic URL")) }}
</fieldset>
<div class="pkg_meta">{{ render_submit_field(form.submit) }}</div>

View File

@@ -17,7 +17,7 @@
{{ render_field(form.vcsLabel, class_="mt-3") }}
{% endif %}
{{ render_field(form.fileUpload, fieldclass="form-control-file", class_="mt-3") }}
{{ render_field(form.fileUpload, fieldclass="form-control-file", class_="mt-3", accept=".zip") }}
<div class="row">
{{ render_field(form.min_rel, class_="col-sm-6") }}

View File

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

View File

@@ -217,6 +217,13 @@
</table>
</div>
{% if package.author.donate_url %}
<div class="alert alert-secondary">
Like {{ package.author.display_name }}'s work?
<a href="{{ package.author.donate_url }}" rel="nofollow">Donate now!</a>
</div>
{% endif %}
{% if package.type == package.type.MOD %}
<div class="card my-4">
<div class="card-header">Dependencies</div>

View File

@@ -6,13 +6,11 @@
{% block content %}
{% if package and current_user != package.author and not current_user.rank.atLeast(current_user.rank.EDITOR) %}
{% if package.issueTracker %}
<div class="alert alert-warning">
Found a bug? Post on the <a href="{{ package.issue_tracker }}">issue tracker</a> instead.<br />
{% if package and current_user != package.author and not current_user.rank.atLeast(current_user.rank.EDITOR) and package.issueTracker %}
<div class="alert alert-warning">
Found a bug? Post on the <a href="{{ package.issueTracker }}">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 %}
</div>
{% endif %}
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}

View File

@@ -15,6 +15,16 @@
<div class="list-group list-group-flush">
{% for p in packages %}
<a href="{{ p.getDetailsURL() }}" class="list-group-item list-group-item-action">
{% if p.getState() == "thread" %}
<span class="mr-2 badge badge-danger">Thread</span>
{% elif p.getState() == "ready" %}
<span class="mr-2 badge badge-success">Ready</span>
{% elif p.getState() == "wip" %}
<span class="mr-2 badge badge-warning">WIP</span>
{% elif p.getState() == "license" %}
<span class="mr-2 badge badge-info">WIP</span>
{% endif %}
{{ p.title }} by {{ p.author.display_name }}
</a>
{% else %}

View File

@@ -11,8 +11,10 @@
<a href="{{ url_for('user_profile_page', username=user.username) }}">
{{ user.display_name }}
</a> -
{{ user.rank.getTitle() }} -
{{ user.packages.count() }} packages.
{{ user.rank.getTitle() }}
{% if current_user.is_authenticated %}
- {{ user.packages.count() }} packages.
{% endif %}
{% endfor %}
</ul>
{% endblock %}

View File

@@ -7,7 +7,7 @@
{% block content %}
{% if not current_user.is_authenticated and user.rank == user.rank.NOT_JOINED and user.forums_username %}
<div class="alert alert-info alert alert-info">
<div class="alert alert-info">
<a class="float-right btn btn-default btn-sm"
href="{{ url_for('user_claim_page', username=user.forums_username) }}">Claim</a>
@@ -40,7 +40,7 @@
</td>
</tr>
<tr>
<td>Accounts:</td>
<td>Links:</td>
<td>
{% if user.forums_username %}
<a href="https://forum.minetest.net/memberlist.php?mode=viewprofile&un={{ user.forums_username }}">
@@ -50,7 +50,7 @@
No forum account
{% endif %}
{% if (user.forums_username and user.github_username) or user == current_user %}
{% if user.github_username or user == current_user %}
|
{% endif %}
@@ -60,8 +60,16 @@
<a href="{{ url_for('github_signin_page') }}">Link Github</a>
{% endif %}
{% if user.website_url %}
| <a href="{{ user.website_url }}" rel="nofollow">Website</a>
{% endif %}
{% if user == current_user %}
&#x1f30e;
<br>
<small class="text-muted">
<span style="padding-right: 5px;">&#x1f30e;</span>
Visible to everyone
</small>
{% endif %}
</td>
</tr>
@@ -136,6 +144,8 @@
{% if user.checkPerm(current_user, "CHANGE_DNAME") %}
{{ render_field(form.display_name, tabindex=230) }}
{{ render_field(form.website_url, tabindex=232) }}
{{ render_field(form.donate_url, tabindex=233) }}
{% endif %}
{% if user.checkPerm(current_user, "CHANGE_EMAIL") %}
@@ -158,6 +168,13 @@
{% from "macros/packagegridtile.html" import render_pkggrid %}
{{ render_pkggrid(packages, show_author=False) }}
{% if user.donate_url %}
<div class="alert alert-secondary">
Like {{ user.display_name }}'s work?
<a href="{{ user.donate_url }}" rel="nofollow">Donate now!</a>
</div>
{% endif %}
{% 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>

View File

@@ -20,7 +20,7 @@ from flask_user import *
from flask_login import login_user, logout_user
from app.models import *
from app import app
import random, string, os
import random, string, os, imghdr
def getExtension(filename):
return filename.rsplit(".", 1)[1].lower() if "." in filename else None
@@ -28,6 +28,10 @@ def getExtension(filename):
def isFilenameAllowed(filename, exts):
return getExtension(filename) in exts
ALLOWED_IMAGES = set(["jpeg", "png"])
def isAllowedImage(data):
return imghdr.what(None, data) in ALLOWED_IMAGES
def shouldReturnJson():
return "application/json" in request.accept_mimetypes and \
not "text/html" in request.accept_mimetypes
@@ -36,16 +40,32 @@ def randomString(n):
return ''.join(random.choice(string.ascii_lowercase + \
string.ascii_uppercase + string.digits) for _ in range(n))
def doFileUpload(file, allowedExtensions, fileTypeName):
def doFileUpload(file, fileType, fileTypeDesc):
if not file or file is None or file.filename == "":
flash("No selected file", "error")
return None
allowedExtensions = []
isImage = False
if fileType == "image":
allowedExtensions = ["jpg", "jpeg", "png"]
isImage = True
elif fileType == "zip":
allowedExtensions = ["zip"]
else:
raise Exception("Invalid fileType")
ext = getExtension(file.filename)
if ext is None or not ext in allowedExtensions:
flash("Please upload load " + fileTypeName, "error")
flash("Please upload load " + fileTypeDesc, "danger")
return None
if isImage and not isAllowedImage(file.stream.read()):
flash("Uploaded image isn't actually an image", "danger")
return None
file.stream.seek(0)
filename = randomString(10) + "." + ext
file.save(os.path.join("app/public/uploads", filename))
return "/uploads/" + filename

View File

@@ -180,9 +180,9 @@ class PackageForm(FlaskForm):
tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title)
harddep_str = StringField("Hard Dependencies", [Optional()])
softdep_str = StringField("Soft Dependencies", [Optional()])
repo = StringField("VCS Repository URL", [Optional(), URL()])
website = StringField("Website URL", [Optional(), URL()])
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()])
repo = StringField("VCS Repository URL", [Optional(), URL()], filters = [lambda x: x or None])
website = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None])
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()], filters = [lambda x: x or None])
forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
submit = SubmitField("Save")
@@ -243,12 +243,21 @@ def create_edit_package_page(author=None, name=None):
package = Package()
package.author = author
wasNew = True
elif package.approved and package.name != form.name.data and \
not package.checkPerm(current_user, Permission.CHANGE_NAME):
flash("Unable to change package name", "danger")
return redirect(url_for("create_edit_package_page", author=author, name=name))
else:
triggerNotif(package.author, current_user,
"{} edited".format(package.title), package.getDetailsURL())
form.populate_obj(package) # copy to row
if package.type== PackageType.TXP:
package.license = package.media_license
mpackage_cache = {}
package.provides.clear()
mpackages = MetaPackage.SpecToList(form.provides_str.data, mpackage_cache)

View File

@@ -54,7 +54,7 @@ class CreatePackageReleaseForm(FlaskForm):
class EditPackageReleaseForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(1, 30)])
url = StringField("URL", [URL])
task_id = StringField("Task ID")
task_id = StringField("Task ID", filters = [lambda x: x or None])
approved = BooleanField("Is Approved")
min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
@@ -96,7 +96,7 @@ def create_release_page(package):
return redirect(url_for("check_task", id=rel.task_id, r=rel.getEditURL()))
else:
uploadedPath = doFileUpload(form.fileUpload.data, ["zip"], "a zip file")
uploadedPath = doFileUpload(form.fileUpload.data, "zip", "a zip file")
if uploadedPath is not None:
rel = PackageRelease()
rel.package = package
@@ -164,7 +164,7 @@ def edit_release_page(package, id):
if package.checkPerm(current_user, Permission.CHANGE_RELEASE_URL):
release.url = form["url"].data
release.task_id = form["task_id"].data
if release.task_id.strip() == "":
if release.task_id is not None:
release.task_id = None
if canApprove:

View File

@@ -49,7 +49,7 @@ def create_screenshot_page(package, id=None):
# Initial form class from post data and default data
form = CreateScreenshotForm()
if request.method == "POST" and form.validate():
uploadedPath = doFileUpload(form.fileUpload.data, ["png", "jpg", "jpeg"],
uploadedPath = doFileUpload(form.fileUpload.data, "image",
"a PNG or JPG image file")
if uploadedPath is not None:
ss = PackageScreenshot()

View File

@@ -31,12 +31,14 @@ from app.tasks.phpbbparser import getProfile
# Define the User profile form
class UserProfileForm(FlaskForm):
display_name = StringField("Display name", [Optional(), Length(2, 20)])
email = StringField("Email", [Optional(), Email()])
email = StringField("Email", [Optional(), Email()], filters = [lambda x: x or None])
website_url = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None])
donate_url = StringField("Donation URL", [Optional(), URL()], filters = [lambda x: x or None])
rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER)
submit = SubmitField("Save")
@app.route("/users/", methods=["GET"])
@login_required
def user_list_page():
users = User.query.order_by(db.desc(User.rank), db.asc(User.display_name)).all()
return render_template("users/list.html", users=users)
@@ -60,6 +62,8 @@ def user_profile_page(username):
# Copy form fields to user_profile fields
if user.checkPerm(current_user, Permission.CHANGE_DNAME):
user.display_name = form["display_name"].data
user.website_url = form["website_url"].data
user.donate_url = form["donate_url"].data
if user.checkPerm(current_user, Permission.CHANGE_RANK):
newRank = form["rank"].data
@@ -74,7 +78,7 @@ def user_profile_page(username):
token = randomString(32)
ver = UserEmailVerification()
ver.user = user
ver.user = user
ver.token = token
ver.email = newEmail
db.session.add(ver)
@@ -158,8 +162,8 @@ def send_email_page(username):
class SetPasswordForm(FlaskForm):
email = StringField("Email", [Optional(), Email()])
password = PasswordField("New password", [InputRequired(), Length(2, 20)])
password2 = PasswordField("Verify password", [InputRequired(), Length(2, 20)])
password = PasswordField("New password", [InputRequired(), Length(2, 100)])
password2 = PasswordField("Verify password", [InputRequired(), Length(2, 100)])
submit = SubmitField("Save")
@app.route("/user/set-password/", methods=["GET", "POST"])

3
babel.cfg Normal file
View File

@@ -0,0 +1,3 @@
[python: app/**.py]
[jinja2: app/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

View File

@@ -23,3 +23,7 @@ MAIL_SERVER=""
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_UTILS_ERROR_SEND_TO=[""]
LANGUAGES = {
'en': 'English',
}

View File

@@ -0,0 +1,30 @@
"""empty message
Revision ID: d6ae9682c45f
Revises: 7ff57806ffd5
Create Date: 2019-07-01 23:27:42.666877
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'd6ae9682c45f'
down_revision = '7ff57806ffd5'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('donate_url', sa.String(length=255), nullable=True))
op.add_column('user', sa.Column('website_url', sa.String(length=255), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'website_url')
op.drop_column('user', 'donate_url')
# ### end Alembic commands ###

View File

@@ -7,11 +7,13 @@ Flask-Menu~=0.7
Flask-Migrate~=2.3
Flask-SQLAlchemy~=2.3
Flask-User~=0.6
Flask-Babel
GitHub-Flask~=3.2
SQLAlchemy-Searchable==1.0.3
beautifulsoup4~=4.6
celery~=4.2
celery==4.1.1
kombu==4.2.0
GitPython~=2.1
lxml~=4.2
pillow~=5.3