Add Maintenance State field

Fixes #160
This commit is contained in:
rubenwardy 2021-12-20 21:07:12 +00:00
parent a800685947
commit 5d32d7922f
12 changed files with 156 additions and 10 deletions

View File

@ -385,7 +385,7 @@ def list_all_reviews():
query = query.options(joinedload(PackageReview.author), joinedload(PackageReview.package))
if request.args.get("author"):
query = query.join(User).filter(User.username == request.args.get("author"))
query = query.filter(PackageReview.author.has(User.username == request.args.get("author")))
if request.args.get("is_positive"):
query = query.filter(PackageReview.recommends == isYes(request.args.get("is_positive")))

View File

@ -228,12 +228,20 @@ def makeLabel(obj):
else:
return obj.title
def NotNullOption(_form, field):
if field.data is None or field.data.name == "__None":
raise ValidationError("This field is required")
class PackageForm(FlaskForm):
type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
title = StringField("Title (Human-readable)", [InputRequired(), Length(1, 100)])
name = StringField("Name (Technical)", [InputRequired(), Length(1, 100), Regexp("^[a-z0-9_]+$", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
dev_state = SelectField("Maintenance State", [InputRequired(), NotNullOption], choices=PackageDevState.choices(with_none=True), coerce=PackageDevState.coerce)
tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=makeLabel)
content_warnings = QuerySelectMultipleField('Content Warnings', query_factory=lambda: ContentWarning.query.order_by(db.asc(ContentWarning.name)), get_pk=lambda a: a.id, get_label=makeLabel)
license = QuerySelectField("License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
@ -318,6 +326,7 @@ def create_edit(author=None, name=None):
"title": form.title.data,
"name": form.name.data,
"short_desc": form.short_desc.data,
"dev_state": form.dev_state.data,
"tags": form.tags.raw_data,
"content_warnings": form.content_warnings.raw_data,
"license": form.license.data,

View File

@ -61,6 +61,8 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
* `title`: Human-readable title.
* `name`: Technical name (needs permission if already approved).
* `short_description`
* `dev_state`: One of `WIP`, `BETA`, `ACTIVELY_DEVELOPED`, `MAINTENANCE_ONLY`, `AS_IS`, `DEPRECATED`,
`LOOKING_FOR_MAINTAINER`.
* `tags`: List of [tag](#tags) names.
* `content_warnings`: List of [content warning](#content-warnings) names.
* `license`: A [license](#licenses) name.

View File

@ -16,10 +16,15 @@ contentdb_flag_blacklist = nonfree, bad_language, drugs
A flag can be:
* `nonfree` - can be used to hide packages which do not qualify as
'free software', as defined by the Free Software Foundation.
'free software', as defined by the Free Software Foundation.
* `wip` - packages marked as Work in Progress
* `deprecated` - packages marked as Deprecated
* A content warning, given below.
* `android_default` - meta-flag that filters out any content with a content warning.
* `desktop_default` - meta-flag that doesn't filter anything out for now.
* `android_default` - meta-flag that filters out any content with a content warning and WIP packages
* `desktop_default` - meta-flag that filters out WIP packages.
The `_default` flags are designed so that we can change how different platforms filter the package list
based on
## Content Warnings

View File

@ -50,6 +50,8 @@ It should be a JSON dictionary with one or more of the following optional keys:
* `title`: Human-readable title.
* `name`: Technical name (needs permission if already approved).
* `short_description`
* `dev_state`: One of `WIP`, `BETA`, `ACTIVELY_DEVELOPED`, `MAINTENANCE_ONLY`, `AS_IS`, `DEPRECATED`,
`LOOKING_FOR_MAINTAINER`.
* `tags`: List of tag names, see [/api/tags/](/api/tags/).
* `content_warnings`: List of content warning names, see [/api/content_warnings/](/api/content_warnings/).
* `license`: A license name, see [/api/licenses/](/api/licenses/).

View File

@ -46,6 +46,9 @@ but still has value. Note that this doesn't mean that you should add a thing
you started working on yesterday, it's worth adding all the basic stuff to
make your package useful.
You should make sure to mark Work in Progress stuff as such in the "maintenance status" column,
as this will help advise players.
Adding non-player facing mods, such as libraries and server tools, is perfectly fine
and encouraged. ContentDB isn't just for player-facing things, and adding
libraries allows them to be installed when a mod depends on it.

View File

@ -19,7 +19,8 @@ import re
import validators
from app.logic.LogicError import LogicError
from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, AuditSeverity, License, UserRank
from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, AuditSeverity, \
License, UserRank, PackageDevState
from app.utils import addAuditLog
@ -47,6 +48,7 @@ ALLOWED_FIELDS = {
"name": str,
"short_description": str,
"short_desc": str,
"dev_state": AnyType,
"tags": list,
"content_warnings": list,
"license": AnyType,
@ -116,13 +118,18 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
if "type" in data:
data["type"] = PackageType.coerce(data["type"])
if "dev_state" in data:
data["dev_state"] = PackageDevState.coerce(data["dev_state"])
if data["dev_state"] is None:
raise LogicError(400, "dev_state cannot be null")
if "license" in data:
data["license"] = get_license(data["license"])
if "media_license" in data:
data["media_license"] = get_license(data["media_license"])
for key in ["name", "title", "short_desc", "desc", "type", "license", "media_license",
for key in ["name", "title", "short_desc", "desc", "type", "dev_state", "license", "media_license",
"repo", "website", "issueTracker", "forums"]:
if key in data:
setattr(package, key, data[key])

View File

@ -73,6 +73,65 @@ class PackageType(enum.Enum):
return item if type(item) == PackageType else PackageType[item.upper()]
class PackageDevState(enum.Enum):
WIP = "Work in Progress"
BETA = "Beta"
ACTIVELY_DEVELOPED = "Actively Developed"
MAINTENANCE_ONLY = "Maintenance Only"
AS_IS = "As-Is"
DEPRECATED = "Deprecated"
LOOKING_FOR_MAINTAINER = "Looking for Maintainer"
def toName(self):
return self.name.lower()
def __str__(self):
return self.name
def get_desc(self):
if self == PackageDevState.WIP:
return "Under active development, and may break worlds/things without warning"
elif self == PackageDevState.BETA:
return "Fully playable, but with some breakages/changes expected"
elif self == PackageDevState.MAINTENANCE_ONLY:
return "Finished, with bug fixes being made as needed"
elif self == PackageDevState.AS_IS:
return "Finished, the maintainer doesn't intend to continue working on it or provide support"
elif self == PackageDevState.DEPRECATED:
return "The maintainer doesn't recommend this package. See the description for more info"
else:
return None
@classmethod
def get(cls, name):
try:
return PackageDevState[name.upper()]
except KeyError:
return None
@classmethod
def choices(cls, with_none):
def build_label(choice):
desc = choice.get_desc()
if desc is None:
return choice.value
else:
return f"{choice.value}: {desc}"
ret = [(choice, build_label(choice)) for choice in cls]
if with_none:
ret.insert(0, ("__None", ""))
return ret
@classmethod
def coerce(cls, item):
if item is None or (isinstance(item, str) and item.upper() == "__NONE"):
return None
return item if type(item) == PackageDevState else PackageDevState[item.upper()]
class PackageState(enum.Enum):
WIP = "Draft"
CHANGES_NEEDED = "Changes Needed"
@ -292,7 +351,8 @@ 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])
state = db.Column(db.Enum(PackageState), nullable=False, default=PackageState.WIP)
state = db.Column(db.Enum(PackageState), nullable=False, default=PackageState.WIP)
dev_state = db.Column(db.Enum(PackageDevState), nullable=True, default=None)
@property
def approved(self):

View File

@ -3,7 +3,8 @@ from sqlalchemy import or_
from sqlalchemy.orm import subqueryload
from sqlalchemy.sql.expression import func
from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease, User, Tag, ContentWarning, PackageState
from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease, User, Tag, \
ContentWarning, PackageState, PackageDevState
from .utils import isYes, get_int_or_abort
@ -30,7 +31,6 @@ class QueryBuilder:
# Hide
hide_flags = args.getlist("hide")
self.title = title
self.types = types
self.tags = tags
@ -41,9 +41,16 @@ class QueryBuilder:
self.order_by = args.get("sort")
self.order_dir = args.get("order") or "desc"
use_platform_defaults = "android_default" in hide_flags or "desktop_default" in hide_flags
self.hide_nonfree = "nonfree" in hide_flags
self.hide_wip = "wip" in hide_flags or use_platform_defaults
self.hide_deprecated = "deprecated" in hide_flags or use_platform_defaults
self.hide_flags = set(hide_flags)
self.hide_flags.discard("nonfree")
self.hide_flags.discard("wip")
self.hide_flags.discard("deprecated")
# Filters
self.search = args.get("q")
@ -136,6 +143,11 @@ class QueryBuilder:
query = query.filter(Package.license.has(License.is_foss == True))
query = query.filter(Package.media_license.has(License.is_foss == True))
if self.hide_wip:
query = query.filter(or_(Package.dev_state == None, Package.dev_state != PackageDevState.WIP))
if self.hide_deprecated:
query = query.filter(or_(Package.dev_state == None, Package.dev_state != PackageDevState.DEPRECATED))
if self.version:
query = query.join(Package.releases) \
.filter(PackageRelease.approved == True) \

View File

@ -77,6 +77,7 @@
{% endif %}
</div>
{{ render_field(form.short_desc, class_="pkg_meta") }}
{{ render_field(form.dev_state, class_="pkg_meta", hint=_("Please choose 'Work in Progress' if your package is unstable, and shouldn't be recommended to all players")) }}
{{ render_multiselect_field(form.tags, class_="pkg_meta") }}
{{ render_multiselect_field(form.content_warnings, class_="pkg_meta") }}
<div class="pkg_meta row">

View File

@ -71,6 +71,12 @@
</p>
<p>
{% if package.dev_state.name == "LOOKING_FOR_MAINTAINER" or package.dev_state.name == "DEPRECATED" %}
<span class="badge badge-warning" title="{{ package.dev_state.get_desc() }}">
<i class="fas fa-exclamation-circle" style="margin-right: 0.3em;"></i>
{{ package.dev_state.value }}
</span>
{% endif %}
{% if package_warning %}
<a class="badge badge-danger" href="/help/non_free/">
<i class="fas fa-exclamation-circle" style="margin-right: 0.3em;"></i>
@ -84,6 +90,12 @@
{{ warning.title }}
</a>
{% endfor %}
{% if package.dev_state.name == "WIP" %}
<span class="badge badge-info" title="{{ package.dev_state.get_desc() }}">
<i class="fas fa-tools" style="margin-right: 0.3em;"></i>
{{ _("Work in Progress") }}
</span>
{% endif %}
{% for t in package.tags %}
<a class="badge badge-primary" rel="nofollow"
title="{{ t.description or '' }}"
@ -379,7 +391,13 @@
{{ render_license(package.media_license) }} for media.
{% endif %}
</dd>
<dt>Added</dt>
<dt>{{ _("Maintenance State") }}</dt>
{% if package.dev_state %}
<dd title="{{ package.dev_state.get_desc() }}">{{ package.dev_state.value }}</dd>
{% else %}
<dd><i>Unknown</i></dd>
{% endif %}
<dt>{{ _("Added") }}</dt>
<dd>{{ package.created_at | datetime }}</dd>
<dt>Maintainers</dt>
<dd>

View File

@ -0,0 +1,27 @@
"""empty message
Revision ID: 17b303f33f68
Revises: 96a01fe23389
Create Date: 2021-12-20 19:48:58.571336
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '17b303f33f68'
down_revision = '96a01fe23389'
branch_labels = None
depends_on = None
def upgrade():
status = postgresql.ENUM('WIP', 'BETA', 'ACTIVELY_DEVELOPED', 'MAINTENANCE_ONLY', 'AS_IS', 'DEPRECATED', 'LOOKING_FOR_MAINTAINER', name='packagedevstate')
status.create(op.get_bind())
op.add_column('package', sa.Column('dev_state', sa.Enum('WIP', 'BETA', 'ACTIVELY_DEVELOPED', 'MAINTENANCE_ONLY', 'AS_IS', 'DEPRECATED', 'LOOKING_FOR_MAINTAINER', name='packagedevstate'), nullable=True))
def downgrade():
op.drop_column('package', 'dev_state')