1. Install `docker` and `docker-compose`.
+ Debian/Ubuntu:
+
sudo apt install docker-ce docker-compose
2. Copy `config.example.cfg` to `config.cfg`.
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)
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"])
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()
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"))
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):
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()
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]
.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) \
.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)) \
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 = ""
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])
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()
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"
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")
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))
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)
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()
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())
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)
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)
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)
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:
.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, \
@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()
# 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
mod = Package()
- mod.approved = True
+ mod.state = PackageState.APPROVED
mod.name = "alpha"
mod.title = "Alpha Test"
mod.license = licenses["MIT"]
session.add(rel)
mod1 = Package()
- mod1.approved = True
+ mod1.state = PackageState.APPROVED
mod1.name = "awards"
mod1.title = "Awards"
mod1.license = licenses["LGPLv2.1"]
session.add(rel)
mod2 = Package()
- mod2.approved = True
+ mod2.state = PackageState.APPROVED
mod2.name = "mesecons"
mod2.title = "Mesecons"
mod2.tags.append(tags["tools"])
session.add(mod2)
mod = Package()
- mod.approved = True
+ mod.state = PackageState.APPROVED
mod.name = "handholds"
mod.title = "Handholds"
mod.license = licenses["MIT"]
session.add(rel)
mod = Package()
- mod.approved = True
+ mod.state = PackageState.APPROVED
mod.name = "other_worlds"
mod.title = "Other Worlds"
mod.license = licenses["MIT"]
session.add(mod)
mod = Package()
- mod.approved = True
+ mod.state = PackageState.APPROVED
mod.name = "food"
mod.title = "Food"
mod.license = licenses["LGPLv2.1"]
session.add(mod)
mod = Package()
- mod.approved = True
+ mod.state = PackageState.APPROVED
mod.name = "food_sweet"
mod.title = "Sweet Foods"
mod.license = licenses["CC0"]
session.add(mod)
game1 = Package()
- game1.approved = True
+ game1.state = PackageState.APPROVED
game1.name = "capturetheflag"
game1.title = "Capture The Flag"
game1.type = PackageType.GAME
mod = Package()
- mod.approved = True
+ mod.state = PackageState.APPROVED
mod.name = "pixelbox"
mod.title = "PixelBOX Reloaded"
mod.license = licenses["CC0"]
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"
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)
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)
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,
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",
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,
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))
@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
--- /dev/null
+{% 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 %}
<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>
</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 %}
-
- {% 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).
+ {% 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 "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>
- {% 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>
+ {% from "macros/threads.html" import render_thread %}
+ {{ render_thread(review_thread, current_user) }}
{% endif %}
- {% endif %}
+ </aside>
+ {% endif %}
+ <main class="container mt-4">
<aside class="float-right ml-4" style="width: 18rem;">
<div class="card mb-4">
<div class="card-header">
{% 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") %}
<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 }}
{% 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 %}
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)
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
--- /dev/null
+"""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 ###