diff --git a/app/services/base_services/set_attributes.rb b/app/services/base_services/set_attributes.rb index f1564b606979..2f99ec17453d 100644 --- a/app/services/base_services/set_attributes.rb +++ b/app/services/base_services/set_attributes.rb @@ -69,14 +69,14 @@ def set_default_attributes(_params) def set_custom_values_to_validate(params) return model.deactivate_custom_field_validations! if contract_options[:skip_custom_field_validation] - custom_field_ids = custom_field_ids_from(params) + custom_field_ids = custom_field_ids_to_validate(params) - # Only update custom_values_to_validate when custom field params are provided. + # Only update custom_values_to_validate when the custom field params are provided. # Otherwise keep them intact, so other services can still set them. - return if custom_field_ids.empty? + return unless custom_field_ids.any? - # Validate only the custom values being updated via the params. - model.custom_values_to_validate = model.custom_values.filter do |cv| + # Validate the custom values updated via the params only. + model.custom_values_to_validate = model.custom_field_values.filter do |cv| custom_field_ids.include?(cv.custom_field_id) end end @@ -98,6 +98,13 @@ def prepare_model(model) model end + def custom_field_ids_to_validate(params) + # Leave custom_field_ids_to_validate empty when the model is not persisted, + # allowing the default behaviour to set the id's to be validated in the + # model.custom_values_to_validate method. + model.persisted? ? custom_field_ids_from(params) : [] + end + def custom_field_ids_from(params) # 1. Retrieve custom fields set via the accessor `wp.custom_field_1 = 1` custom_field_ids = params.keys.filter_map { |k| k[/^custom_field_(\d+)$/, 1]&.to_i } diff --git a/app/services/projects/set_attributes_service.rb b/app/services/projects/set_attributes_service.rb index 938ec6b95369..f24600b7c2e1 100644 --- a/app/services/projects/set_attributes_service.rb +++ b/app/services/projects/set_attributes_service.rb @@ -30,7 +30,6 @@ module Projects class SetAttributesService < ::BaseServices::SetAttributes - private def set_attributes(params) @@ -111,5 +110,15 @@ def faulty_code?(status_code) def first_not_set_code (Project.status_codes.keys - [model.status_code]).first end + + def custom_field_ids_to_validate(params) + # In case of new records, validate custom fields that are enabled for all projects + # and also required. + if model.new_record? + model.available_custom_fields.for_all.required.pluck(:id) + else + custom_field_ids_from(params) + end + end end end diff --git a/spec/requests/api/v3/projects/create_resource_spec.rb b/spec/requests/api/v3/projects/create_resource_spec.rb index 0b8d30085923..aafa45c8fe30 100644 --- a/spec/requests/api/v3/projects/create_resource_spec.rb +++ b/spec/requests/api/v3/projects/create_resource_spec.rb @@ -302,6 +302,58 @@ it_behaves_like "creates a project with a custom value", "Engineering" end + + context "with another custom field present" do + shared_let(:other_custom_field) do + create(:text_project_custom_field, + name: "Other CF") + end + + context "when a value for the other cf is provided but the required one is missing (regression #70107)" do + let(:body) do + { + identifier: "new_project_identifier", + name: "Project name", + other_custom_field.attribute_name(:camel_case) => { + raw: "Other value" + } + }.to_json + end + + it "responds with 422 and explains the custom field error" do + expect(last_response).to have_http_status(:unprocessable_entity) + + expect(last_response.body) + .to be_json_eql("Department can't be blank.".to_json) + .at_path("message") + end + end + end + + context "with another custom field present that is required but not for_all" do + shared_let(:required_not_for_all_custom_field) do + create(:text_project_custom_field, + name: "Not for all CF", + is_required: true, + is_for_all: false) + end + + context "when a value for the required field is provided, but no value for the required not for_all custom_field" do + let(:body) do + { + identifier: "new_project_identifier", + name: "Project name", + shared_custom_field.attribute_name(:camel_case) => { + raw: "Engineering" + } + }.to_json + end + + it "responds with 201 and does not validate the required not for_all custom_field" do + expect(last_response).to have_http_status(:created) + end + end + end end context "with a visible custom field" do