# 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 . 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("" + lazy_gettext("Error: Another package already uses this forum topic!") + "") topic = get_forum_topic(package.forums) if topic is not None: if topic.author != package.author: danger("" + lazy_gettext("Error: Forum topic author doesn't match package author.") + "") 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