mirror of
https://github.com/minetest/contentdb.git
synced 2024-12-23 14:32:25 +01:00
203 lines
7.8 KiB
Python
203 lines
7.8 KiB
Python
|
# ContentDB
|
||
|
# Copyright (C) rubenwardy
|
||
|
#
|
||
|
# This program is free software: you can redistribute it and/or modify
|
||
|
# it under the terms of the GNU Affero General Public License as published by
|
||
|
# the Free Software Foundation, either version 3 of the License, or
|
||
|
# (at your option) any later version.
|
||
|
#
|
||
|
# This program is distributed in the hope that it will be useful,
|
||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
# GNU Affero General Public License for more details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU Affero General Public License
|
||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
|
||
|
from typing import List, Tuple, Union, Optional
|
||
|
|
||
|
from flask_babel import lazy_gettext, LazyString
|
||
|
from sqlalchemy import and_, or_
|
||
|
|
||
|
from app.models import Package, PackageType, PackageState, PackageRelease, db, MetaPackage, ForumTopic, User, \
|
||
|
Permission, UserRank
|
||
|
|
||
|
|
||
|
class PackageValidationNote:
|
||
|
# level is danger, warning, or info
|
||
|
level: str
|
||
|
message: LazyString
|
||
|
buttons: List[Tuple[str, LazyString]]
|
||
|
|
||
|
# False to prevent "Approve"
|
||
|
allow_approval: bool
|
||
|
|
||
|
# False to prevent "Submit for Approval"
|
||
|
allow_submit: bool
|
||
|
|
||
|
def __init__(self, level: str, message: LazyString, allow_approval: bool, allow_submit: bool):
|
||
|
self.level = level
|
||
|
self.message = message
|
||
|
self.buttons = []
|
||
|
self.allow_approval = allow_approval
|
||
|
self.allow_submit = allow_submit
|
||
|
|
||
|
def add_button(self, url: str, label: LazyString) -> "PackageValidationNote":
|
||
|
self.buttons.append((url, label))
|
||
|
return self
|
||
|
|
||
|
|
||
|
def is_package_name_taken(normalised_name: str) -> bool:
|
||
|
return Package.query.filter(
|
||
|
and_(Package.state == PackageState.APPROVED,
|
||
|
or_(Package.name == normalised_name,
|
||
|
Package.name == normalised_name + "_game"))).count() > 0
|
||
|
|
||
|
|
||
|
def get_conflicting_mod_names(package: Package) -> set[str]:
|
||
|
conflicting_modnames = (db.session.query(MetaPackage.name)
|
||
|
.filter(MetaPackage.id.in_([mp.id for mp in package.provides]))
|
||
|
.filter(MetaPackage.packages.any(and_(Package.id != package.id, Package.state == PackageState.APPROVED)))
|
||
|
.all())
|
||
|
conflicting_modnames += (db.session.query(ForumTopic.name)
|
||
|
.filter(ForumTopic.name.in_([mp.name for mp in package.provides]))
|
||
|
.filter(ForumTopic.topic_id != package.forums)
|
||
|
.filter(~ db.exists().where(Package.forums == ForumTopic.topic_id))
|
||
|
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title))
|
||
|
.all())
|
||
|
return set([x[0] for x in conflicting_modnames])
|
||
|
|
||
|
|
||
|
def count_packages_with_forum_topic(topic_id: int) -> int:
|
||
|
return Package.query.filter(Package.forums == topic_id, Package.state != PackageState.DELETED).count() > 1
|
||
|
|
||
|
|
||
|
def get_forum_topic(topic_id: int) -> Optional[ForumTopic]:
|
||
|
return ForumTopic.query.get(topic_id)
|
||
|
|
||
|
|
||
|
def validate_package_for_approval(package: Package) -> List[PackageValidationNote]:
|
||
|
retval: List[PackageValidationNote] = []
|
||
|
|
||
|
def template(level: str, allow_approval: bool, allow_submit: bool):
|
||
|
def inner(msg: LazyString):
|
||
|
note = PackageValidationNote(level, msg, allow_approval, allow_submit)
|
||
|
retval.append(note)
|
||
|
return note
|
||
|
|
||
|
return inner
|
||
|
|
||
|
danger = template("danger", allow_approval=False, allow_submit=False)
|
||
|
warning = template("warning", allow_approval=True, allow_submit=True)
|
||
|
info = template("info", allow_approval=False, allow_submit=True)
|
||
|
|
||
|
if package.type != PackageType.MOD and is_package_name_taken(package.normalised_name):
|
||
|
danger(lazy_gettext("A package already exists with this name. Please see Policy and Guidance 3"))
|
||
|
|
||
|
if package.releases.filter(PackageRelease.task_id.is_(None)).count() == 0:
|
||
|
if package.releases.count() == 0:
|
||
|
message = lazy_gettext("You need to create a release before this package can be approved.")
|
||
|
else:
|
||
|
message = lazy_gettext("Release is still importing, or has an error.")
|
||
|
|
||
|
danger(message) \
|
||
|
.add_button(package.get_url("packages.create_release"), lazy_gettext("Create release")) \
|
||
|
.add_button(package.get_url("packages.setup_releases"), lazy_gettext("Set up releases"))
|
||
|
|
||
|
# Don't bother validating any more until we have a release
|
||
|
return retval
|
||
|
|
||
|
if (package.type == PackageType.GAME or package.type == PackageType.TXP) and \
|
||
|
package.screenshots.count() == 0:
|
||
|
danger(lazy_gettext("You need to add at least one screenshot."))
|
||
|
|
||
|
missing_deps = package.get_missing_hard_dependencies_query().all()
|
||
|
if len(missing_deps) > 0:
|
||
|
missing_deps = ", ".join([ x.name for x in missing_deps])
|
||
|
danger(lazy_gettext(
|
||
|
"The following hard dependencies need to be added to ContentDB first: %(deps)s", deps=missing_deps))
|
||
|
|
||
|
if package.type != PackageType.GAME and not package.supports_all_games and package.supported_games.count() == 0:
|
||
|
danger(lazy_gettext(
|
||
|
"What games does your package support? Please specify on the supported games page", deps=missing_deps)) \
|
||
|
.add_button(package.get_url("packages.game_support"), lazy_gettext("Supported Games"))
|
||
|
|
||
|
if "Other" in package.license.name or "Other" in package.media_license.name:
|
||
|
info(lazy_gettext("Please wait for the license to be added to CDB."))
|
||
|
|
||
|
# Check similar mod name
|
||
|
conflicting_modnames = set()
|
||
|
if package.type != PackageType.TXP:
|
||
|
conflicting_modnames = get_conflicting_mod_names(package)
|
||
|
|
||
|
if len(conflicting_modnames) > 4:
|
||
|
warning(lazy_gettext("Please make sure that this package has the right to the names it uses."))
|
||
|
elif len(conflicting_modnames) > 0:
|
||
|
names_list = list(conflicting_modnames)
|
||
|
names_list.sort()
|
||
|
warning(lazy_gettext("Please make sure that this package has the right to the names %(names)s",
|
||
|
names=", ".join(names_list))) \
|
||
|
.add_button(package.get_url('packages.similar'), lazy_gettext("See more"))
|
||
|
|
||
|
# Check forum topic
|
||
|
if package.state != PackageState.APPROVED and package.forums is not None:
|
||
|
if count_packages_with_forum_topic(package.forums) > 1:
|
||
|
danger("<b>" + lazy_gettext("Error: Another package already uses this forum topic!") + "</b>")
|
||
|
|
||
|
topic = get_forum_topic(package.forums)
|
||
|
if topic is not None:
|
||
|
if topic.author != package.author:
|
||
|
danger("<b>" + lazy_gettext("Error: Forum topic author doesn't match package author.") + "</b>")
|
||
|
elif package.type != PackageType.TXP:
|
||
|
warning(lazy_gettext("Warning: Forum topic not found. The topic may have been created since the last forum crawl."))
|
||
|
|
||
|
return retval
|
||
|
|
||
|
|
||
|
PACKAGE_STATE_FLOW = {
|
||
|
PackageState.WIP: {PackageState.READY_FOR_REVIEW},
|
||
|
PackageState.CHANGES_NEEDED: {PackageState.READY_FOR_REVIEW},
|
||
|
PackageState.READY_FOR_REVIEW: {PackageState.WIP, PackageState.CHANGES_NEEDED, PackageState.APPROVED},
|
||
|
PackageState.APPROVED: {PackageState.CHANGES_NEEDED},
|
||
|
PackageState.DELETED: {PackageState.READY_FOR_REVIEW},
|
||
|
}
|
||
|
|
||
|
|
||
|
def can_move_to_state(package: Package, user: User, new_state: Union[str, PackageState]) -> bool:
|
||
|
if not user.is_authenticated:
|
||
|
return False
|
||
|
|
||
|
if type(new_state) == str:
|
||
|
new_state = PackageState[new_state]
|
||
|
elif type(new_state) != PackageState:
|
||
|
raise Exception("Unknown state given to can_move_to_state()")
|
||
|
|
||
|
if new_state not in PACKAGE_STATE_FLOW[package.state]:
|
||
|
return False
|
||
|
|
||
|
if new_state == PackageState.READY_FOR_REVIEW or new_state == PackageState.APPROVED:
|
||
|
# Can the user approve?
|
||
|
if new_state == PackageState.APPROVED and not package.check_perm(user, Permission.APPROVE_NEW):
|
||
|
return False
|
||
|
|
||
|
# Must be able to edit or approve package to change its state
|
||
|
if not (package.check_perm(user, Permission.APPROVE_NEW) or package.check_perm(user, Permission.EDIT_PACKAGE)):
|
||
|
return False
|
||
|
|
||
|
# Are there any validation warnings?
|
||
|
validation_notes = validate_package_for_approval(package)
|
||
|
for note in validation_notes:
|
||
|
if not note.allow_submit or (new_state == PackageState.APPROVED and not note.allow_approval):
|
||
|
return False
|
||
|
|
||
|
return True
|
||
|
|
||
|
elif new_state == PackageState.CHANGES_NEEDED:
|
||
|
return package.check_perm(user, Permission.APPROVE_NEW)
|
||
|
|
||
|
elif new_state == PackageState.WIP:
|
||
|
return package.check_perm(user, Permission.EDIT_PACKAGE) and \
|
||
|
(user in package.maintainers or user.rank.at_least(UserRank.ADMIN))
|
||
|
|
||
|
return True
|