diff --git a/tests/unit/admin/views/test_classifiers.py b/tests/unit/admin/views/test_classifiers.py index c705251ddc01..81d18bbaaedc 100644 --- a/tests/unit/admin/views/test_classifiers.py +++ b/tests/unit/admin/views/test_classifiers.py @@ -13,6 +13,8 @@ import pretend import pytest +from webob.multidict import MultiDict + from warehouse.admin.views import classifiers as views from warehouse.classifiers.models import Classifier @@ -82,10 +84,28 @@ def test_add_parent_classifier(self, db_request): class TestDeprecateClassifier: - def test_deprecate_classifier(self, db_request): - classifier = ClassifierFactory(deprecated=False) + @pytest.mark.parametrize("has_alternative_classifiers", [(True,), (False,)]) + def test_deprecate_classifier(self, db_request, has_alternative_classifiers): + classifier = ClassifierFactory( + classifier="Classifier :: For Testing", deprecated=False + ) + + db_request.params = MultiDict({"classifier_id": classifier.id}) + + if has_alternative_classifiers: + classifier.alternatives.extend( + [ + ClassifierFactory(classifier="AA :: Alternative", deprecated=True), + ClassifierFactory(classifier="BB :: Alternative", deprecated=False), + ] + ) + db_request.params.extend( + [ + ("deprecated_by", alternative.id) + for alternative in classifier.alternatives + ] + ) - db_request.params = {"classifier_id": classifier.id} db_request.session.flash = pretend.call_recorder(lambda *a, **kw: None) db_request.route_path = lambda *a: "/the/path" @@ -93,3 +113,8 @@ def test_deprecate_classifier(self, db_request): db_request.db.flush() assert classifier.deprecated + assert db_request.session.flash.calls == [ + pretend.call( + "Deprecated classifier 'Classifier :: For Testing'", queue="success" + ) + ] diff --git a/tests/unit/forklift/test_legacy.py b/tests/unit/forklift/test_legacy.py index cc6e6eefa542..d46861642d22 100644 --- a/tests/unit/forklift/test_legacy.py +++ b/tests/unit/forklift/test_legacy.py @@ -1570,7 +1570,31 @@ def test_upload_fails_with_invalid_classifier(self, pyramid_config, db_request): "for this field" ) - def test_upload_fails_with_deprecated_classifier(self, pyramid_config, db_request): + @pytest.mark.parametrize( + "has_alternative_classifiers, expected_message", + [ + ( + False, + ( + "400 Invalid value for classifiers. " + "Error: Classifier 'AA :: BB' has been deprecated, see /url " + "for a list of valid classifiers." + ), + ), + ( + True, + ( + "400 Invalid value for classifiers. " + "Error: Classifier 'AA :: BB' has been deprecated " + "in favor of the following classifier(s): " + "'AA :: Alternative', 'BB :: Alternative'" + ), + ), + ], + ) + def test_upload_fails_with_deprecated_classifier( + self, pyramid_config, db_request, has_alternative_classifiers, expected_message + ): pyramid_config.testing_securitypolicy(userid=1) user = UserFactory.create() @@ -1581,6 +1605,14 @@ def test_upload_fails_with_deprecated_classifier(self, pyramid_config, db_reques RoleFactory.create(user=user, project=project) classifier = ClassifierFactory(classifier="AA :: BB", deprecated=True) + if has_alternative_classifiers: + classifier.alternatives.extend( + [ + ClassifierFactory(classifier="AA :: Alternative", deprecated=True), + ClassifierFactory(classifier="BB :: Alternative", deprecated=False), + ] + ) + filename = "{}-{}.tar.gz".format(project.name, release.version) db_request.POST = MultiDict( @@ -1606,11 +1638,7 @@ def test_upload_fails_with_deprecated_classifier(self, pyramid_config, db_reques resp = excinfo.value assert resp.status_code == 400 - assert resp.status == ( - "400 Invalid value for classifiers. " - "Error: Classifier 'AA :: BB' has been deprecated, see /url " - "for a list of valid classifiers." - ) + assert resp.status == expected_message @pytest.mark.parametrize( "digests", diff --git a/warehouse/admin/static/js/controllers/deprecate_classifier_controller.js b/warehouse/admin/static/js/controllers/deprecate_classifier_controller.js new file mode 100644 index 000000000000..82698086c6cc --- /dev/null +++ b/warehouse/admin/static/js/controllers/deprecate_classifier_controller.js @@ -0,0 +1,40 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import { Controller } from "stimulus"; + +export default class extends Controller { + static targets = [ + "deprecatedClassifier", "alternativeClassifier" + ] + + update() { + const deprecatedClassifierId = this.deprecatedClassifierTarget.options[this.deprecatedClassifierTarget.selectedIndex].value; + this.alternativeClassifierTargets.forEach(target => { + const selectedAlternativeId = target.options[target.selectedIndex].value; + + // Reset the value to prevent self-selection. + if (deprecatedClassifierId === selectedAlternativeId) { + target.selectedIndex = 0; + } + + // Disable deprecated classifier selection. + for (let optionIndex = 0; optionIndex < target.options.length; ++optionIndex) { + const option = target.options[optionIndex]; + option.disabled = option.value === deprecatedClassifierId || option.dataset.deprecated; + } + }); + } +} diff --git a/warehouse/admin/templates/admin/classifiers/index.html b/warehouse/admin/templates/admin/classifiers/index.html index 08b30e100357..9508e67a4757 100644 --- a/warehouse/admin/templates/admin/classifiers/index.html +++ b/warehouse/admin/templates/admin/classifiers/index.html @@ -78,7 +78,7 @@