Implement package states for easier reviews

This commit is contained in:
rubenwardy 2020-09-16 17:51:03 +01:00
parent e81eb9c8d5
commit 92fb54556a
20 changed files with 363 additions and 172 deletions

View File

@ -10,6 +10,8 @@ Docker is the recommended way to develop and deploy ContentDB.
1. Install `docker` and `docker-compose`.
Debian/Ubuntu:
sudo apt install docker-ce docker-compose
2. Copy `config.example.cfg` to `config.cfg`.
@ -40,7 +42,7 @@ Docker is the recommended way to develop and deploy ContentDB.
8. Create initial data
1. `./utils/bash.sh`
2. Either `python setup.py -t` or `python setup.py -o`:
2. Either `python utils/setup.py -t` or `python utils/setup.py -o`:
1. `-o` creates just the admin, and static data like tags, and licenses.
2. `-t` will do `-o` and also create test packages. (Recommended)

View File

@ -57,7 +57,7 @@ def admin_page():
elif action == "reimportpackages":
tasks = []
for package in Package.query.filter_by(soft_deleted=False).all():
for package in Package.query.filter(Package.state!=PackageState.DELETED).all():
release = package.releases.first()
if release:
zippath = release.url.replace("/uploads/", app.config["UPLOAD_DIR"])
@ -96,7 +96,7 @@ def admin_page():
elif action == "importscreenshots":
packages = Package.query \
.filter_by(soft_deleted=False) \
.filter(Package.state!=PackageState.DELETED) \
.outerjoin(PackageScreenshot, Package.id==PackageScreenshot.package_id) \
.filter(PackageScreenshot.id==None) \
.all()
@ -110,7 +110,7 @@ def admin_page():
if package is None:
flash("Unknown package", "danger")
else:
package.soft_deleted = False
package.state = PackageState.READY_FOR_REVIEW
db.session.commit()
return redirect(url_for("admin.admin_page"))
@ -163,7 +163,7 @@ def admin_page():
else:
flash("Unknown action: " + action, "danger")
deleted_packages = Package.query.filter_by(soft_deleted=True).all()
deleted_packages = Package.query.filter(Package.state==PackageState.DELETED).all()
return render_template("admin/list.html", deleted_packages=deleted_packages)
class SwitchUserForm(FlaskForm):

View File

@ -15,7 +15,7 @@ def home():
joinedload(Package.license), \
joinedload(Package.media_license))
query = Package.query.filter_by(approved=True, soft_deleted=False)
query = Package.query.filter_by(state=PackageState.APPROVED)
count = query.count()
new = join(query.order_by(db.desc(Package.approved_at))).limit(8).all()
@ -24,7 +24,7 @@ def home():
pop_txp = join(query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score))).limit(4).all()
updated = db.session.query(Package).select_from(PackageRelease).join(Package) \
.filter_by(soft_deleted=False, approved=True) \
.filter_by(state=PackageState.APPROVED) \
.order_by(db.desc(PackageRelease.releaseDate)) \
.limit(20).all()
updated = updated[:8]

View File

@ -41,7 +41,7 @@ def view(name):
.filter(MetaPackage.name==name) \
.join(MetaPackage.dependencies) \
.join(Dependency.depender) \
.filter(Dependency.optional==False, Package.approved==True, Package.soft_deleted==False) \
.filter(Dependency.optional==False, Package.state==PackageState.APPROVED) \
.all()
optional_dependers = db.session.query(Package) \
@ -49,11 +49,11 @@ def view(name):
.filter(MetaPackage.name==name) \
.join(MetaPackage.dependencies) \
.join(Dependency.depender) \
.filter(Dependency.optional==True, Package.approved==True, Package.soft_deleted==False) \
.filter(Dependency.optional==True, Package.state==PackageState.APPROVED) \
.all()
similar_topics = None
if mpackage.packages.filter_by(approved=True, soft_deleted=False).count() == 0:
if mpackage.packages.filter_by(state=PackageState.APPROVED).count() == 0:
similar_topics = ForumTopic.query \
.filter_by(name=name) \
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \

View File

@ -45,7 +45,7 @@ def generate_metrics(full=False):
downloads_result = db.session.query(func.sum(Package.downloads)).one_or_none()
downloads = 0 if not downloads_result or not downloads_result[0] else downloads_result[0]
packages = Package.query.filter_by(approved=True, soft_deleted=False).count()
packages = Package.query.filter_by(state=PackageState.APPROVED).count()
users = User.query.filter(User.rank != UserRank.NOT_JOINED).count()
ret = ""
@ -55,7 +55,7 @@ def generate_metrics(full=False):
if full:
scores = Package.query.join(User).with_entities(User.username, Package.name, Package.score) \
.filter(Package.approved==True, Package.soft_deleted==False).all()
.filter(Package.state==PackageState.APPROVED).all()
ret += write_array_stat("contentdb_package_score", "Package score", "gauge", \
[({ "author": score[0], "name": score[1] }, score[2]) for score in scores])

View File

@ -122,8 +122,8 @@ def view(package):
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) \
.filter_by(name=package.name, type=PackageType.MOD) \
.filter(Package.id != package.id, Package.state!=PackageState.DELETED) \
.order_by(db.desc(Package.score)) \
.all()
@ -148,9 +148,9 @@ def view(package):
topic_error = None
topic_error_lvl = "warning"
if not package.approved and package.forums is not None:
if package.state != PackageState.APPROVED and package.forums is not None:
errors = []
if Package.query.filter_by(forums=package.forums, soft_deleted=False).count() > 1:
if Package.query.filter(Package.forums==package.forums, Package.state!=PackageState.DELETED).count() > 1:
errors.append("<b>Error: Another package already uses this forum topic!</b>")
topic_error_lvl = "danger"
@ -294,7 +294,7 @@ def create_edit(author=None, name=None):
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:
if package.state == PackageState.READY_FOR_REVIEW:
Package.query.filter_by(name=form["name"].data, author_id=author.id).delete()
else:
flash("Package already exists!", "danger")
@ -305,8 +305,7 @@ def create_edit(author=None, name=None):
package.maintainers.append(author)
wasNew = True
elif package.approved and package.name != form.name.data and \
not package.checkPerm(current_user, Permission.CHANGE_NAME):
elif 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("packages.create_edit", author=author, name=name))
@ -359,7 +358,7 @@ def create_edit(author=None, name=None):
return redirect(next_url)
package_query = Package.query.filter_by(approved=True, soft_deleted=False)
package_query = Package.query.filter_by(state=PackageState.APPROVED)
if package is not None:
package_query = package_query.filter(Package.id != package.id)
@ -369,18 +368,23 @@ def create_edit(author=None, name=None):
packages=package_query.all(), \
mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all())
@bp.route("/packages/<author>/<name>/approve/", methods=["POST"])
@bp.route("/packages/<author>/<name>/state/", methods=["POST"])
@login_required
@is_package_page
def approve(package):
if not package.checkPerm(current_user, Permission.APPROVE_NEW):
flash("You don't have permission to do that.", "danger")
def move_to_state(package):
state = PackageState.get(request.args.get("state"))
if state is None:
abort(400)
elif package.approved:
flash("Package has already been approved", "danger")
if not package.canMoveToState(current_user, state):
flash("You don't have permission to do that", "danger")
return redirect(package.getDetailsURL())
else:
package.approved = True
package.state = state
msg = "Marked {} as {}".format(package.title, state.value)
if state == PackageState.APPROVED:
if not package.approved_at:
package.approved_at = datetime.datetime.now()
@ -389,10 +393,19 @@ def approve(package):
s.approved = True
msg = "Approved {}".format(package.title)
addNotification(package.maintainers, current_user, msg, package.getDetailsURL(), package)
severity = AuditSeverity.NORMAL if current_user == package.author else AuditSeverity.EDITOR
addAuditLog(severity, current_user, msg, package.getDetailsURL(), package)
db.session.commit()
addNotification(package.maintainers, current_user, msg, package.getDetailsURL(), package)
severity = AuditSeverity.NORMAL if current_user in package.maintainers else AuditSeverity.EDITOR
addAuditLog(severity, current_user, msg, package.getDetailsURL(), package)
db.session.commit()
if package.state == PackageState.CHANGES_NEEDED:
flash("Please comment what changes are needed in the review thread", "warning")
if package.review_thread:
return redirect(package.review_thread.getViewURL())
else:
return redirect(url_for('threads.new', pid=package.id, title='Package approval comments'))
return redirect(package.getDetailsURL())
@ -409,7 +422,7 @@ def remove(package):
flash("You don't have permission to do that.", "danger")
return redirect(package.getDetailsURL())
package.soft_deleted = True
package.state = PackageState.DELETED
url = url_for("users.profile", username=package.author.username)
msg = "Deleted {}".format(package.title)
@ -425,7 +438,7 @@ def remove(package):
flash("You don't have permission to do that.", "danger")
return redirect(package.getDetailsURL())
package.approved = False
package.state = PackageState.READY_FOR_REVIEW
msg = "Unapproved {}".format(package.title)
addNotification(package.maintainers, current_user, msg, package.getDetailsURL(), package)

View File

@ -298,6 +298,10 @@ def new():
if is_review_thread:
package.review_thread = thread
if package.state == PackageState.READY_FOR_REVIEW and current_user not in package.maintainers:
package.state = PackageState.CHANGES_NEEDED
notif_msg = "New thread '{}'".format(thread.title)
if package is not None:
addNotification(package.maintainers, current_user, notif_msg, thread.getViewURL(), package)

View File

@ -31,8 +31,12 @@ def view():
canApproveScn = Permission.APPROVE_SCREENSHOT.check(current_user)
packages = None
wip_packages = None
if canApproveNew:
packages = Package.query.filter_by(approved=False, soft_deleted=False).order_by(db.desc(Package.created_at)).all()
packages = Package.query.filter_by(state=PackageState.READY_FOR_REVIEW) \
.order_by(db.desc(Package.created_at)).all()
wip_packages = Package.query.filter(Package.state<PackageState.READY_FOR_REVIEW) \
.order_by(db.desc(Package.created_at)).all()
releases = None
if canApproveRel:
@ -64,16 +68,16 @@ def view():
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
.count()
total_packages = Package.query.filter_by(approved=True, soft_deleted=False).count()
total_to_tag = Package.query.filter_by(approved=True, soft_deleted=False, tags=None).count()
total_packages = Package.query.filter_by(state=PackageState.APPROVED).count()
total_to_tag = Package.query.filter_by(state=PackageState.APPROVED, tags=None).count()
unfulfilled_meta_packages = MetaPackage.query \
.filter(~ MetaPackage.packages.any(approved=True, soft_deleted=False)) \
.filter(~ MetaPackage.packages.any(state=PackageState.APPROVED)) \
.filter(MetaPackage.dependencies.any(optional=False)) \
.order_by(db.asc(MetaPackage.name)).count()
return render_template("todo/list.html", title="Reports and Work Queue",
packages=packages, releases=releases, screenshots=screenshots,
packages=packages, wip_packages=wip_packages, releases=releases, screenshots=screenshots,
canApproveNew=canApproveNew, canApproveRel=canApproveRel, canApproveScn=canApproveScn,
topics_to_add=topics_to_add, total_topics=total_topics, \
total_packages=total_packages, total_to_tag=total_to_tag, \
@ -128,7 +132,7 @@ def tags():
@login_required
def metapackages():
mpackages = MetaPackage.query \
.filter(~ MetaPackage.packages.any(approved=True, soft_deleted=False)) \
.filter(~ MetaPackage.packages.any(state=PackageState.APPROVED)) \
.filter(MetaPackage.dependencies.any(optional=False)) \
.order_by(db.asc(MetaPackage.name)).all()

View File

@ -115,9 +115,9 @@ def profile(username):
# Redirect to home page
return redirect(url_for("users.profile", username=username))
packages = user.packages.filter_by(soft_deleted=False)
packages = user.packages.filter(Package.state!=PackageState.DELETED)
if not current_user.is_authenticated or (user != current_user and not current_user.canAccessTodoList()):
packages = packages.filter_by(approved=True)
packages = packages.filter_by(state=PackageState.APPROVED)
packages = packages.order_by(db.asc(Package.title))
topics_to_add = None

View File

@ -63,7 +63,7 @@ def populate_test_data(session):
mod = Package()
mod.approved = True
mod.state = PackageState.APPROVED
mod.name = "alpha"
mod.title = "Alpha Test"
mod.license = licenses["MIT"]
@ -87,7 +87,7 @@ def populate_test_data(session):
session.add(rel)
mod1 = Package()
mod1.approved = True
mod1.state = PackageState.APPROVED
mod1.name = "awards"
mod1.title = "Awards"
mod1.license = licenses["LGPLv2.1"]
@ -124,7 +124,7 @@ awards.register_achievement("award_mesefind",{
session.add(rel)
mod2 = Package()
mod2.approved = True
mod2.state = PackageState.APPROVED
mod2.name = "mesecons"
mod2.title = "Mesecons"
mod2.tags.append(tags["tools"])
@ -213,7 +213,7 @@ No warranty is provided, express or implied, for any part of the project.
session.add(mod2)
mod = Package()
mod.approved = True
mod.state = PackageState.APPROVED
mod.name = "handholds"
mod.title = "Handholds"
mod.license = licenses["MIT"]
@ -237,7 +237,7 @@ No warranty is provided, express or implied, for any part of the project.
session.add(rel)
mod = Package()
mod.approved = True
mod.state = PackageState.APPROVED
mod.name = "other_worlds"
mod.title = "Other Worlds"
mod.license = licenses["MIT"]
@ -254,7 +254,7 @@ No warranty is provided, express or implied, for any part of the project.
session.add(mod)
mod = Package()
mod.approved = True
mod.state = PackageState.APPROVED
mod.name = "food"
mod.title = "Food"
mod.license = licenses["LGPLv2.1"]
@ -270,7 +270,7 @@ No warranty is provided, express or implied, for any part of the project.
session.add(mod)
mod = Package()
mod.approved = True
mod.state = PackageState.APPROVED
mod.name = "food_sweet"
mod.title = "Sweet Foods"
mod.license = licenses["CC0"]
@ -287,7 +287,7 @@ No warranty is provided, express or implied, for any part of the project.
session.add(mod)
game1 = Package()
game1.approved = True
game1.state = PackageState.APPROVED
game1.name = "capturetheflag"
game1.title = "Capture The Flag"
game1.type = PackageType.GAME
@ -350,7 +350,7 @@ Uses the CTF PvP Engine.
mod = Package()
mod.approved = True
mod.state = PackageState.APPROVED
mod.name = "pixelbox"
mod.title = "PixelBOX Reloaded"
mod.license = licenses["CC0"]

View File

@ -336,6 +336,54 @@ class PackageType(enum.Enum):
return item if type(item) == PackageType else PackageType[item]
class PackageState(enum.Enum):
WIP = "Work in Progress"
CHANGES_NEEDED = "Changes Needed"
READY_FOR_REVIEW = "Ready for Review"
APPROVED = "Approved"
DELETED = "Deleted"
def toName(self):
return self.name.lower()
def verb(self):
if self == self.READY_FOR_REVIEW:
return "Submit for Review"
elif self == self.APPROVED:
return "Approve"
elif self == self.DELETED:
return "Delete"
else:
return self.value
def __str__(self):
return self.name
@classmethod
def get(cls, name):
try:
return PackageState[name.upper()]
except KeyError:
return None
@classmethod
def choices(cls):
return [(choice, choice.value) for choice in cls]
@classmethod
def coerce(cls, item):
return item if type(item) == PackageState else PackageState[item]
PACKAGE_STATE_FLOW = {
PackageState.WIP: set([ PackageState.READY_FOR_REVIEW ]),
PackageState.CHANGES_NEEDED: set([ PackageState.READY_FOR_REVIEW ]),
PackageState.READY_FOR_REVIEW: set([ PackageState.WIP, PackageState.CHANGES_NEEDED, PackageState.APPROVED ]),
PackageState.APPROVED: set([ PackageState.CHANGES_NEEDED ]),
PackageState.DELETED: set([ PackageState.READY_FOR_REVIEW ]),
}
class PackagePropertyKey(enum.Enum):
name = "Name"
title = "Title"
@ -480,8 +528,11 @@ class Package(db.Model):
media_license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
media_license = db.relationship("License", foreign_keys=[media_license_id])
approved = db.Column(db.Boolean, nullable=False, default=False)
soft_deleted = db.Column(db.Boolean, nullable=False, default=False)
state = db.Column(db.Enum(PackageState), default=PackageState.WIP)
@property
def approved(self):
return self.state == PackageState.APPROVED
score = db.Column(db.Float, nullable=False, default=0)
score_downloads = db.Column(db.Float, nullable=False, default=0)
@ -525,7 +576,7 @@ class Package(db.Model):
self.author_id = package.author_id
self.created_at = package.created_at
self.approved = package.approved
self.state = package.state
self.maintainers.append(self.author)
@ -578,22 +629,6 @@ class Package(db.Model):
def getSortedOptionalDependencies(self):
return self.getSortedDependencies(False)
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 getAsDictionaryKey(self):
return {
"name": self.name,
@ -682,9 +717,14 @@ class Package(db.Model):
return url_for("packages.create_edit",
author=self.author.username, name=self.name)
def getApproveURL(self):
return url_for("packages.approve",
author=self.author.username, name=self.name)
def getSetStateURL(self, state):
if type(state) == str:
state = PackageState[perm]
elif type(state) != PackageState:
raise Exception("Unknown state given to Package.canMoveToState()")
return url_for("packages.move_to_state",
author=self.author.username, name=self.name, state=state.name.lower())
def getRemoveURL(self):
return url_for("packages.remove",
@ -784,6 +824,53 @@ class Package(db.Model):
else:
raise Exception("Permission {} is not related to packages".format(perm.name))
def canMoveToState(self, user, state):
if not user.is_authenticated:
return False
if type(state) == str:
state = PackageState[perm]
elif type(state) != PackageState:
raise Exception("Unknown state given to Package.canMoveToState()")
if state not in PACKAGE_STATE_FLOW[self.state]:
return False
if state == PackageState.READY_FOR_REVIEW or state == PackageState.APPROVED:
requiredPerm = Permission.APPROVE_NEW if state == PackageState.APPROVED else Permission.EDIT_PACKAGE
if not self.checkPerm(user, requiredPerm):
return False
if state == PackageState.APPROVED and \
("Other" in self.license.name or "Other" in self.media_license.name):
return False
needsScreenshot = \
(self.type == self.type.GAME or self.type == self.type.TXP) and \
self.screenshots.count() == 0
return self.releases.count() > 0 and not needsScreenshot
elif state == PackageState.CHANGES_NEEDED:
return self.checkPerm(user, Permission.APPROVE_NEW)
elif state == PackageState.WIP:
return self.checkPerm(user, Permission.EDIT_PACKAGE) and user in self.maintainers
return True
def getNextStates(self, user):
states = []
for state in PackageState:
if self.canMoveToState(user, state):
states.append(state)
return states
def getScoreDict(self):
return {
"author": self.author.username,

View File

@ -72,9 +72,9 @@ class QueryBuilder:
query = None
if self.order_by == "last_release":
query = db.session.query(Package).select_from(PackageRelease).join(Package) \
.filter_by(soft_deleted=False, approved=True)
.filter_by(state=PackageState.APPROVED)
else:
query = Package.query.filter_by(soft_deleted=False, approved=True)
query = Package.query.filter_by(state=PackageState.APPROVED)
return self.filterPackageQuery(self.orderPackageQuery(query))

View File

@ -321,7 +321,7 @@ def makeVCSRelease(self, id, branch):
@celery.task()
def importRepoScreenshot(id):
package = Package.query.get(id)
if package is None or package.soft_deleted:
if package is None or package.state == PackageState.DELETED:
raise Exception("Unexpected none package")
# Get URL Maker

View File

@ -0,0 +1,101 @@
{% macro render_banners(package, current_user, topic_error, topic_error_lvl, similar_topics) -%}
<div class="row mb-4">
<span class="col">
State: <strong>{{ package.state.value }}</strong>
</span>
{% for state in package.getNextStates(current_user) %}
<form class="col-auto" method="post" action="{{ package.getSetStateURL(state) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input class="btn btn-sm btn-secondary" type="submit" value="{{ state.verb() }}" />
</form>
{% endfor %}
</div>
{% set level = "warning" %}
{% if package.releases.count() == 0 %}
{% set message %}
<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 %}
{% endset %}
{% elif (package.type == package.type.GAME or package.type == package.type.TXP) and package.screenshots.count() == 0 %}
{% set message = "You need to add at least one screenshot." %}
{% elif topic_error_lvl == "danger" %}
{% elif package.state == package.state.READY_FOR_REVIEW and ("Other" in package.license.name or "Other" in package.media_license.name) %}
{% set message = "Please wait for the license to be added to CDB." %}
{% else %}
{% set level = "info" %}
{% set message %}
{% if package.screenshots.count() == 0 %}
<b>You should add at least one screenshot, but this isn't required.</b><br />
{% endif %}
{% if package.state == package.state.READY_FOR_REVIEW %}
{% 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.
{% else %}
Please wait for the package to be approved.
{% endif %}
{% else %}
{% if package.checkPerm(current_user, "EDIT_PACKAGE") %}
You can now submit this package for approval if you're ready.
{% else %}
This package can be submitted for approval when ready.
{% endif %}
{% endif %}
{% endset %}
{% endif %}
{% if message %}
<div class="alert alert-{{ level }}">
<span class="icon_message"></span>
{{ message | safe }}
<div style="clear: both;"></div>
</div>
{% endif %}
{% 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 package.review_thread and (package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW")) %}
<div class="alert alert-secondary">
<a class="float-right btn btn-sm btn-secondary" href="{{ url_for('threads.new', pid=package.id, title='Package approval comments') }}">Open Thread</a>
Privately ask a question or give feedback
<div style="clear:both;"></div>
</div>
{% endif %}
{% endmacro %}

View File

@ -10,7 +10,7 @@
<h2>Provided By</h2>
{% from "macros/packagegridtile.html" import render_pkggrid %}
{{ render_pkggrid(mpackage.packages.filter_by(approved=True, soft_deleted=False).all()) }}
{{ render_pkggrid(mpackage.packages.filter_by(state="APPROVED").all()) }}
{% if similar_topics %}
<p>Unforuntately, this isn't on ContentDB yet! Here's some forum topics:</p>

View File

@ -134,81 +134,27 @@
</div>
</header>
<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 %}
{% if not package.approved %}
<aside class="container mt-4">
{% from "macros/package_approval.html" import render_banners %}
{{ render_banners(package, current_user, topic_error, topic_error_lvl, similar_topics) }}
{% 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 %}
{% if review_thread and review_thread.checkPerm(current_user, "SEE_THREAD") %}
<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 %}
<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>
{% from "macros/threads.html" import render_thread %}
{{ render_thread(review_thread, current_user) }}
{% endif %}
</aside>
{% 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('threads.new', 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 %}
<main class="container mt-4">
<aside class="float-right ml-4" style="width: 18rem;">
<div class="card mb-4">
<div class="card-header">
@ -431,21 +377,6 @@
{% endif %}
</aside>
{% if not package.approved and (package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW")) %}
{% if review_thread %}
<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) }}
{% endif %}
{% endif %}
<ul class="screenshot_list mb-4">
{% for ss in package.screenshots %}
{% if ss.approved or package.checkPerm(current_user, "ADD_SCREENSHOTS") %}

View File

@ -8,21 +8,17 @@
<h2 class="mb-4">Approval Queue</h2>
<div class="row">
{% if canApproveNew and packages %}
{% if canApproveNew and (packages or wip_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">
{% if p.getState() == "thread" %}
<span class="mr-2 badge badge-danger">Thread</span>
{% elif p.getState() == "ready" %}
{% if "Other" in p.license.name or "Other" in p.media_license.name %}
<span class="mr-2 badge badge-info">License</span>
{% else %}
<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 }}
@ -32,6 +28,21 @@
{% endfor %}
</div>
</div>
<div class="card mt-5">
<h3 class="card-header">WIP Packages</h3>
<div class="list-group list-group-flush">
{% for p in wip_packages %}
<a href="{{ p.getDetailsURL() }}" class="list-group-item list-group-item-action">
<span class="mr-2 badge badge-warning">{{ p.state.value }}</span>
{{ 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 %}

View File

@ -46,7 +46,7 @@ def test_packages_with_contents(client):
packages = parse_json(rv.data)
assert len(packages) > 0
assert len(packages) == Package.query.filter_by(approved=True).count()
assert len(packages) == Package.query.filter_by(state=PackageState.APPROVED).count()
validate_package_list(packages)

View File

@ -200,7 +200,8 @@ def getPackageByInfo(author, name):
if user is None:
return None
package = Package.query.filter_by(name=name, author_id=user.id, soft_deleted=False).first()
package = Package.query.filter_by(name=name, author_id=user.id) \
.filter(Package.state!=PackageState.DELETED).first()
if package is None:
return None

View File

@ -0,0 +1,37 @@
"""empty message
Revision ID: b3c7ff6655af
Revises: dff4b87e4a76
Create Date: 2020-09-16 14:35:43.805422
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'b3c7ff6655af'
down_revision = 'dff4b87e4a76'
branch_labels = None
depends_on = None
def upgrade():
status = postgresql.ENUM('WIP', 'READY_FOR_REVIEW', 'APPROVED', 'DELETED', name='packagestate')
status.create(op.get_bind())
op.add_column('package', sa.Column('state', sa.Enum('WIP', 'CHANGES_NEEDED', 'READY_FOR_REVIEW', 'APPROVED', 'DELETED', name='packagestate'), nullable=True))
op.execute("UPDATE package SET state='APPROVED' WHERE approved=true")
op.execute("UPDATE package SET state='DELETED' WHERE soft_deleted=true")
op.drop_column('package', 'approved')
op.drop_column('package', 'updated_at')
op.drop_column('package', 'soft_deleted')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('package', sa.Column('soft_deleted', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False))
op.add_column('package', sa.Column('approved', sa.BOOLEAN(), autoincrement=False, nullable=False))
op.drop_column('package', 'state')
# ### end Alembic commands ###