Compare commits

..

14 Commits

Author SHA1 Message Date
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
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
31 changed files with 324 additions and 144 deletions

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

View File

@@ -37,7 +37,7 @@ make_searchable(db.metadata)
class ArticleQuery(BaseQuery, SearchQueryMixin): class ArticleQuery(BaseQuery, SearchQueryMixin):
pass pass
class UserRank(enum.Enum): class UserRank(enum.Enum):
@@ -129,6 +129,10 @@ class User(db.Model, UserMixin):
active = db.Column("is_active", db.Boolean, nullable=False, server_default="0") active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
display_name = db.Column(db.String(100), nullable=False, server_default="") display_name = db.Column(db.String(100), nullable=False, server_default="")
# Links
website_url = db.Column(db.String(255), nullable=True, default=None)
donate_url = db.Column(db.String(255), nullable=True, default=None)
# Content # Content
notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id") notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id")
@@ -417,21 +421,39 @@ class Package(db.Model):
for e in PackagePropertyKey: for e in PackagePropertyKey:
setattr(self, e.name, getattr(package, e.name)) setattr(self, e.name, getattr(package, e.name))
def getAsDictionaryShort(self, base_url, protonum=None): 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) tnurl = self.getThumbnailURL(1)
release = self.getDownloadRelease(version=version, protonum=protonum)
return { return {
"name": self.name, "name": self.name,
"title": self.title, "title": self.title,
"author": self.author.display_name, "author": self.author.display_name,
"short_description": self.short_desc, "short_description": self.short_desc,
"type": self.type.toName(), "type": self.type.toName(),
"release": self.getDownloadRelease(protonum).id if self.getDownloadRelease(protonum) is not None else None, "release": release and release.id,
"thumbnail": (base_url + tnurl) if tnurl is not None else None, "thumbnail": (base_url + tnurl) if tnurl is not None else None,
"score": round(self.score * 10) / 10 "score": round(self.score * 10) / 10
} }
def getAsDictionary(self, base_url, protonum=None): def getAsDictionary(self, base_url, version=None, protonum=None):
tnurl = self.getThumbnailURL(1) tnurl = self.getThumbnailURL(1)
release = self.getDownloadRelease(version=version, protonum=protonum)
return { return {
"author": self.author.display_name, "author": self.author.display_name,
"name": self.name, "name": self.name,
@@ -454,7 +476,7 @@ class Package(db.Model):
"screenshots": [base_url + ss.url for ss in self.screenshots], "screenshots": [base_url + ss.url for ss in self.screenshots],
"url": base_url + self.getDownloadURL(), "url": base_url + self.getDownloadURL(),
"release": self.getDownloadRelease(protonum).id if self.getDownloadRelease(protonum) is not None else None, "release": release and release.id,
"score": round(self.score * 10) / 10 "score": round(self.score * 10) / 10
} }
@@ -503,9 +525,8 @@ class Package(db.Model):
return url_for("package_download_page", return url_for("package_download_page",
author=self.author.username, name=self.name) author=self.author.username, name=self.name)
def getDownloadRelease(self, protonum=None): def getDownloadRelease(self, version=None, protonum=None):
version = None if version is None and protonum is not None:
if protonum is not None:
version = MinetestRelease.query.filter(MinetestRelease.protocol >= int(protonum)).first() version = MinetestRelease.query.filter(MinetestRelease.protocol >= int(protonum)).first()
if version is not None: if version is not None:
version = version.id version = version.id
@@ -514,7 +535,7 @@ class Package(db.Model):
for rel in self.releases: for rel in self.releases:
if rel.approved and (protonum is None or if rel.approved and (version is None or
((rel.min_rel is None or rel.min_rel_id <= version) and \ ((rel.min_rel is None or rel.min_rel_id <= version) and \
(rel.max_rel is None or rel.max_rel_id >= version))): (rel.max_rel is None or rel.max_rel_id >= version))):
return rel return rel
@@ -695,6 +716,16 @@ class PackageRelease(db.Model):
def __init__(self): def __init__(self):
self.releaseDate = datetime.datetime.now() self.releaseDate = datetime.datetime.now()
def approve(self, user):
if self.package.approved and \
not self.package.checkPerm(user, Permission.APPROVE_RELEASE):
return False
assert(self.task_id is None and self.url is not None and self.url != "")
self.approved = True
return True
class PackageReview(db.Model): class PackageReview(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)

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

@@ -1,5 +1,5 @@
from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease
from .utils import isNo from .utils import isNo, isYes
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
from flask import abort from flask import abort
from sqlalchemy import or_ from sqlalchemy import or_
@@ -28,13 +28,33 @@ class QueryBuilder:
self.lucky = self.random or "lucky" in args self.lucky = self.random or "lucky" in args
self.hide_nonfree = "nonfree" in hide_flags self.hide_nonfree = "nonfree" in hide_flags
self.limit = 1 if self.lucky else None self.limit = 1 if self.lucky else None
self.order_by = args.get("sort") or "score" self.order_by = args.get("sort")
self.order_dir = args.get("order") or "desc" self.order_dir = args.get("order") or "desc"
self.protocol_version = args.get("protocol_version") 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() == "": if self.search is not None and self.search.strip() == "":
self.search = None 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): def buildPackageQuery(self):
query = Package.query.filter_by(soft_deleted=False, approved=True) query = Package.query.filter_by(soft_deleted=False, approved=True)
@@ -48,7 +68,7 @@ class QueryBuilder:
query = query.order_by(func.random()) query = query.order_by(func.random())
else: else:
to_order = None to_order = None
if self.order_by == "score": if self.order_by is None or self.order_by == "score":
to_order = Package.score to_order = Package.score
elif self.order_by == "created_at": elif self.order_by == "created_at":
to_order = Package.created_at to_order = Package.created_at
@@ -69,14 +89,9 @@ class QueryBuilder:
query = query.filter(Package.media_license.has(License.is_foss == True)) query = query.filter(Package.media_license.has(License.is_foss == True))
if self.protocol_version: if self.protocol_version:
self.protocol_version = int(self.protocol_version) version = self.getMinetestVersion()
version = MinetestRelease.query.filter(MinetestRelease.protocol>=self.protocol_version).first()
if version is not None:
version = version.id
else:
version = 10000000
query = query.join(Package.releases) \ query = query.join(Package.releases) \
.filter(PackageRelease.approved==True) \
.filter(or_(PackageRelease.min_rel_id==None, PackageRelease.min_rel_id<=version)) \ .filter(or_(PackageRelease.min_rel_id==None, PackageRelease.min_rel_id<=version)) \
.filter(or_(PackageRelease.max_rel_id==None, PackageRelease.max_rel_id>=version)) .filter(or_(PackageRelease.max_rel_id==None, PackageRelease.max_rel_id>=version))
@@ -85,18 +100,31 @@ class QueryBuilder:
return query return query
def buildTopicQuery(self): def buildTopicQuery(self, show_added=False):
topics = ForumTopic.query \ query = ForumTopic.query
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title)) if 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: if self.search:
topics = topics.filter(ForumTopic.title.ilike('%' + self.search + '%')) query = query.filter(ForumTopic.title.ilike('%' + self.search + '%'))
if len(self.types) > 0: if len(self.types) > 0:
topics = topics.filter(ForumTopic.type.in_(self.types)) query = query.filter(ForumTopic.type.in_(self.types))
if self.limit: if self.limit:
topics = topics.limit(self.limit) query = query.limit(self.limit)
return topics return query

View File

@@ -348,6 +348,7 @@ def makeVCSReleaseFromGithub(id, branch, release, url):
release.url = urlmaker.getCommitDownload(commits[0]["sha"]) release.url = urlmaker.getCommitDownload(commits[0]["sha"])
release.task_id = None release.task_id = None
release.commit_hash = commits[0]["sha"] release.commit_hash = commits[0]["sha"]
release.approve(release.package.author)
print(release.url) print(release.url)
db.session.commit() db.session.commit()
@@ -379,6 +380,7 @@ def makeVCSRelease(id, branch):
release.url = "/uploads/" + filename release.url = "/uploads/" + filename
release.task_id = None release.task_id = None
release.commit_hash = repo.head.object.hexsha release.commit_hash = repo.head.object.hexsha
release.approve(release.package.author)
print(release.url) print(release.url)
db.session.commit() db.session.commit()

View File

@@ -9,6 +9,9 @@
<link rel="stylesheet" type="text/css" href="/static/bootstrap.css"> <link rel="stylesheet" type="text/css" href="/static/bootstrap.css">
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=6"> <link rel="stylesheet" type="text/css" href="/static/custom.css?v=6">
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" /> <link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
<link rel="shortcut icon" href="/favicon-16.png" sizes="16x16">
<link rel="icon" href="/favicon-128.png" sizes="128x128">
<link rel="icon" href="/favicon-32.png" sizes="32x32">
{% block headextra %}{% endblock %} {% block headextra %}{% endblock %}
</head> </head>
@@ -126,10 +129,12 @@
{% endblock %} {% endblock %}
<footer class="container footer-copyright my-5 page-footer font-small text-center"> <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="https://github.com/minetest/contentdb">GitHub</a> |
<a href="{{ url_for('flatpage', path='help') }}">Help</a> | <a href="{{ url_for('flatpage', path='help') }}">Help</a> |
<a href="{{ url_for('flatpage', path='help/reporting') }}">Report / DMCA</a> <a href="{{ url_for('flatpage', path='policy_and_guidance') }}">Policy and Guidance</a> |
<a href="{{ url_for('flatpage', path='help/reporting') }}">Report / DMCA</a> |
<a href="{{ url_for('user_list_page') }}">User List</a>
</footer> </footer>
<script src="/static/jquery.min.js"></script> <script src="/static/jquery.min.js"></script>

View File

@@ -217,6 +217,13 @@
</table> </table>
</div> </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 %} {% if package.type == package.type.MOD %}
<div class="card my-4"> <div class="card my-4">
<div class="card-header">Dependencies</div> <div class="card-header">Dependencies</div>

View File

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

View File

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

View File

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

View File

@@ -20,8 +20,9 @@ from flask_user import *
import flask_menu as menu import flask_menu as menu
from app import app from app import app
from app.models import * from app.models import *
from app.querybuilder import QueryBuilder
@app.route("/todo/") @app.route("/todo/", methods=["GET", "POST"])
@login_required @login_required
def todo_page(): def todo_page():
canApproveNew = Permission.APPROVE_NEW.check(current_user) canApproveNew = Permission.APPROVE_NEW.check(current_user)
@@ -40,60 +41,61 @@ def todo_page():
if canApproveScn: if canApproveScn:
screenshots = PackageScreenshot.query.filter_by(approved=False).all() screenshots = PackageScreenshot.query.filter_by(approved=False).all()
if not canApproveNew and not canApproveRel and not canApproveScn:
abort(403)
topics_to_add = ForumTopic.query \ 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)) \ .filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
.filter_by(discarded=False) \
.count() .count()
return render_template("todo/list.html", title="Reports and Work Queue", return render_template("todo/list.html", title="Reports and Work Queue",
packages=packages, releases=releases, screenshots=screenshots, packages=packages, releases=releases, screenshots=screenshots,
canApproveNew=canApproveNew, canApproveRel=canApproveRel, canApproveScn=canApproveScn, canApproveNew=canApproveNew, canApproveRel=canApproveRel, canApproveScn=canApproveScn,
topics_to_add=topics_to_add) topics_to_add=topics_to_add, total_topics=total_topics)
@app.route("/todo/topics/") @app.route("/todo/topics/")
@login_required @login_required
def todo_topics_page(): def todo_topics_page():
query = ForumTopic.query qb = QueryBuilder(request.args)
qb.setSortIfNone("date")
show_discarded = request.args.get("show_discarded") == "True" query = qb.buildTopicQuery()
if not show_discarded:
query = query.filter_by(discarded=False)
total = query.count()
query = query.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
sort_by = request.args.get("sort")
if sort_by == "name":
query = query.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title))
elif sort_by == "views":
query = query.order_by(db.desc(ForumTopic.views))
elif sort_by is None or sort_by == "date":
query = query.order_by(db.asc(ForumTopic.created_at))
sort_by = "date"
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() topic_count = query.count()
search = request.args.get("q")
if search is not None and search.strip() != "":
query = query.filter(ForumTopic.title.ilike('%' + search + '%'))
page = int(request.args.get("page") or 1) page = int(request.args.get("page") or 1)
num = int(request.args.get("n") or 100) num = int(request.args.get("n") or 100)
if num > 100 and not current_user.rank.atLeast(UserRank.EDITOR): if num > 100 and not current_user.rank.atLeast(UserRank.EDITOR):
num = 100 num = 100
query = query.paginate(page, num, True) query = query.paginate(page, num, True)
next_url = url_for("todo_topics_page", page=query.next_num, query=search, \ next_url = url_for("todo_topics_page", page=query.next_num, query=qb.search, \
show_discarded=show_discarded, n=num, sort=sort_by) \ show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
if query.has_next else None if query.has_next else None
prev_url = url_for("todo_topics_page", page=query.prev_num, query=search, \ prev_url = url_for("todo_topics_page", page=query.prev_num, query=qb.search, \
show_discarded=show_discarded, n=num, sort=sort_by) \ show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
if query.has_prev else None if query.has_prev else None
return render_template("todo/topics.html", topics=query.items, total=total, \ return render_template("todo/topics.html", topics=query.items, total=total, \
topic_count=topic_count, query=search, show_discarded=show_discarded, \ 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, \ next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, \
n=num, sort_by=sort_by) n=num, sort_by=qb.order_by)

View File

@@ -26,9 +26,10 @@ from app.querybuilder import QueryBuilder
def api_packages_page(): def api_packages_page():
qb = QueryBuilder(request.args) qb = QueryBuilder(request.args)
query = qb.buildPackageQuery() query = qb.buildPackageQuery()
ver = qb.getMinetestVersion()
pkgs = [package.getAsDictionaryShort(app.config["BASE_URL"], request.args.get("protocol_version")) \ pkgs = [package.getAsDictionaryShort(app.config["BASE_URL"], version=ver) \
for package in query.all() if package.getDownloadRelease() is not None] for package in query.all()]
return jsonify(pkgs) return jsonify(pkgs)
@app.route("/api/packages/<author>/<name>/") @app.route("/api/packages/<author>/<name>/")
@@ -39,10 +40,9 @@ def api_package_page(package):
@app.route("/api/topics/") @app.route("/api/topics/")
def api_topics_page(): def api_topics_page():
query = ForumTopic.query \ qb = QueryBuilder(request.args)
.order_by(db.asc(ForumTopic.wip), db.asc(ForumTopic.name), db.asc(ForumTopic.title)) query = qb.buildTopicQuery(show_added=True)
pkgs = [t.getAsDictionary() for t in query.all()] return jsonify([t.getAsDictionary() for t in query.all()])
return jsonify(pkgs)
@app.route("/api/topic_discard/", methods=["POST"]) @app.route("/api/topic_discard/", methods=["POST"])

View File

@@ -74,9 +74,9 @@ def packages_page():
def getReleases(package): def getReleases(package):
if package.checkPerm(current_user, Permission.MAKE_RELEASE): if package.checkPerm(current_user, Permission.MAKE_RELEASE):
return package.releases return package.releases.limit(5)
else: else:
return [rel for rel in package.releases if rel.approved] return package.releases.filter_by(approved=True).limit(5)
@app.route("/packages/<author>/<name>/") @app.route("/packages/<author>/<name>/")

View File

@@ -85,7 +85,6 @@ def create_release_page(package):
rel.task_id = uuid() rel.task_id = uuid()
rel.min_rel = form["min_rel"].data.getActual() rel.min_rel = form["min_rel"].data.getActual()
rel.max_rel = form["max_rel"].data.getActual() rel.max_rel = form["max_rel"].data.getActual()
rel.approved = package.checkPerm(current_user, Permission.APPROVE_RELEASE)
db.session.add(rel) db.session.add(rel)
db.session.commit() db.session.commit()
@@ -105,7 +104,7 @@ def create_release_page(package):
rel.url = uploadedPath rel.url = uploadedPath
rel.min_rel = form["min_rel"].data.getActual() rel.min_rel = form["min_rel"].data.getActual()
rel.max_rel = form["max_rel"].data.getActual() rel.max_rel = form["max_rel"].data.getActual()
rel.approved = package.checkPerm(current_user, Permission.APPROVE_RELEASE) rel.approve(current_user)
db.session.add(rel) db.session.add(rel)
db.session.commit() db.session.commit()

View File

@@ -32,11 +32,13 @@ from app.tasks.phpbbparser import getProfile
class UserProfileForm(FlaskForm): class UserProfileForm(FlaskForm):
display_name = StringField("Display name", [Optional(), Length(2, 20)]) display_name = StringField("Display name", [Optional(), Length(2, 20)])
email = StringField("Email", [Optional(), Email()]) email = StringField("Email", [Optional(), Email()])
website_url = StringField("Website URL", [Optional(), URL()])
donate_url = StringField("Donation URL", [Optional(), URL()])
rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER) rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER)
submit = SubmitField("Save") submit = SubmitField("Save")
@app.route("/users/", methods=["GET"]) @app.route("/users/", methods=["GET"])
@login_required
def user_list_page(): def user_list_page():
users = User.query.order_by(db.desc(User.rank), db.asc(User.display_name)).all() users = User.query.order_by(db.desc(User.rank), db.asc(User.display_name)).all()
return render_template("users/list.html", users=users) return render_template("users/list.html", users=users)
@@ -60,6 +62,8 @@ def user_profile_page(username):
# Copy form fields to user_profile fields # Copy form fields to user_profile fields
if user.checkPerm(current_user, Permission.CHANGE_DNAME): if user.checkPerm(current_user, Permission.CHANGE_DNAME):
user.display_name = form["display_name"].data 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): if user.checkPerm(current_user, Permission.CHANGE_RANK):
newRank = form["rank"].data newRank = form["rank"].data
@@ -74,7 +78,7 @@ def user_profile_page(username):
token = randomString(32) token = randomString(32)
ver = UserEmailVerification() ver = UserEmailVerification()
ver.user = user ver.user = user
ver.token = token ver.token = token
ver.email = newEmail ver.email = newEmail
db.session.add(ver) db.session.add(ver)

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