-
-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor package approval validation to unify implementation
- Loading branch information
1 parent
0b76982
commit dde47cf
Showing
24 changed files
with
442 additions
and
241 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
# 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 |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.