mirror of
https://github.com/minetest/contentdb.git
synced 2025-01-03 19:57:29 +01:00
Implement package states for easier reviews
This commit is contained in:
parent
e81eb9c8d5
commit
92fb54556a
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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]
|
||||
|
@ -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)) \
|
||||
|
@ -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])
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"]
|
||||
|
131
app/models.py
131
app/models.py
@ -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,
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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
|
||||
|
101
app/templates/macros/package_approval.html
Normal file
101
app/templates/macros/package_approval.html
Normal 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 %}
|
@ -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>
|
||||
|
@ -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 %}🔒{% 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 %}🔒{% 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") %}
|
||||
|
@ -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 %}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
37
migrations/versions/b3c7ff6655af_.py
Normal file
37
migrations/versions/b3c7ff6655af_.py
Normal 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 ###
|
Loading…
Reference in New Issue
Block a user