From 9c51ea8e57e81c2dff1d855d1a6b39da4dbca12f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 15 Oct 2025 10:40:21 +0200 Subject: [PATCH] Disable placeholder on enterprise --- .../banner_component.html.erb | 2 +- .../my/enterprise_banners_controller.rb | 6 +- .../users/invitation/project_step/form.rb | 40 ++- app/models/users/Invitation/form_model.rb | 12 +- config/locales/en.yml | 4 +- .../users/invitation/form_model_spec.rb | 250 ++++++++++++++++++ 6 files changed, 293 insertions(+), 21 deletions(-) create mode 100644 spec/models/users/invitation/form_model_spec.rb diff --git a/app/components/enterprise_edition/banner_component.html.erb b/app/components/enterprise_edition/banner_component.html.erb index fbaeaaa2c48..b634dee2a86 100644 --- a/app/components/enterprise_edition/banner_component.html.erb +++ b/app/components/enterprise_edition/banner_component.html.erb @@ -93,7 +93,7 @@ classes: class_names("op-enterprise-banner--close-icon", classes), scheme: :invisible, tag: :a, - href: dismiss_enterprise_banner_path(feature_key: @dismiss_key), + href: dismiss_enterprise_banner_path(feature_key: @feature_key, dismiss_key: @dismiss_key), data: { turbo_stream: true, turbo_method: :post }, icon: :x, aria: { label: t("ee.upsell.hide_banner") } diff --git a/app/controllers/my/enterprise_banners_controller.rb b/app/controllers/my/enterprise_banners_controller.rb index c9cca555a87..d025c907492 100644 --- a/app/controllers/my/enterprise_banners_controller.rb +++ b/app/controllers/my/enterprise_banners_controller.rb @@ -63,10 +63,8 @@ class My::EnterpriseBannersController < ApplicationController private def get_feature_key - raw_key = params[:feature_key] - - @dismiss_key = raw_key - @feature_key = raw_key.gsub(/_trial$/, "").to_sym + @feature_key = params[:feature_key].to_sym + @dismiss_key = params[:dismiss_key].presence&.to_sym || @feature_key render_400 unless OpenProject::Token.lowest_plan_for(@feature_key) end diff --git a/app/forms/users/invitation/project_step/form.rb b/app/forms/users/invitation/project_step/form.rb index ea8496849dc..12437cea14d 100644 --- a/app/forms/users/invitation/project_step/form.rb +++ b/app/forms/users/invitation/project_step/form.rb @@ -57,18 +57,34 @@ module Users::Invitation::ProjectStep name: :principal_type, visually_hide_label: true ) do |radio_group| - radio_group.radio_button(value: "User", - checked: model.principal_type.nil? || model.principal_type == "User", - label: User.model_name.human, - caption: I18n.t("users.invite_user_modal.type.user.description")) - radio_group.radio_button(value: "Group", - checked: model.principal_type == "Group", - label: Group.model_name.human, - caption: I18n.t("users.invite_user_modal.type.group.description")) - radio_group.radio_button(value: "PlaceholderUser", - checked: model.principal_type == "PlaceholderUser", - label: PlaceholderUser.model_name.human, - caption: I18n.t("users.invite_user_modal.type.placeholder_user.description")) + radio_group.radio_button( + value: "User", + checked: model.principal_type.nil? || model.principal_type == "User", + label: User.model_name.human, + caption: I18n.t("users.invite_user_modal.type.user.description") + ) + radio_group.radio_button( + value: "Group", + checked: model.principal_type == "Group", + label: Group.model_name.human, + caption: I18n.t("users.invite_user_modal.type.group.description") + ) + + radio_group.radio_button( + value: "PlaceholderUser", + disabled: !EnterpriseToken.allows_to?(:placeholder_users), + checked: model.principal_type == "PlaceholderUser", + label: PlaceholderUser.model_name.human, + caption: I18n.t("users.invite_user_modal.type.placeholder_user.description") + ) + end + + unless EnterpriseToken.allows_to?(:placeholder_users) + f.html_content do + render(EnterpriseEdition::BannerComponent.new(:placeholder_users, + dismissable: true, + dismiss_key: "invitation_placeholder_users")) + end end end end diff --git a/app/models/users/Invitation/form_model.rb b/app/models/users/Invitation/form_model.rb index a4b548f9475..d6d4cf3e9df 100644 --- a/app/models/users/Invitation/form_model.rb +++ b/app/models/users/Invitation/form_model.rb @@ -40,11 +40,21 @@ module Users::Invitation attribute :message, :text, default: nil validates :project_id, presence: true, on: :project_step - validates :principal_type, inclusion: { in: %w[User PlaceholderUser Group] }, on: :project_step + validates :principal_type, + inclusion: { in: ->(*) { available_principal_types } }, + on: :project_step validates :id_or_email, presence: true, on: :principal_step validates :role_id, presence: true, on: :principal_step + def self.available_principal_types + if EnterpriseToken.allows_to?(:placeholder_users) + %w[User PlaceholderUser Group] + else + %w[User Group] + end + end + def project_name project&.name || project_id end diff --git a/config/locales/en.yml b/config/locales/en.yml index 050f8bf007e..74540d55d65 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -379,7 +379,7 @@ en: danger_dialog: confirmation_live_message_checked: "The button to proceed is now active." - confirmation_live_message_unchecked: "The button to proceed is now inactive. You need to tick the checkbox to continue." + confirmation_live_message_unchecked: "The button to proceed is now inactive. You need to tick the checkbox to continue." op_dry_validation: or: "or" @@ -733,8 +733,6 @@ en: title: "Add placeholder user to %{project_name}" title_no_ee: "Placeholder user (Enterprise edition only add-on)" description: "Has no access to the project and no emails are sent out." - description_no_ee: 'Has no access to the project and no emails are sent out. -
Check out the Enterprise edition' already_member_message: "Already a member of %{project}" principal: diff --git a/spec/models/users/invitation/form_model_spec.rb b/spec/models/users/invitation/form_model_spec.rb new file mode 100644 index 00000000000..ffeac270a1a --- /dev/null +++ b/spec/models/users/invitation/form_model_spec.rb @@ -0,0 +1,250 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" + +RSpec.describe Users::Invitation::FormModel do + subject(:form_model) { described_class.new } + + let(:project) { build_stubbed(:project) } + + describe "validations" do + describe "principal_type" do + context "with enterprise token allowing placeholder users" do + before do + allow(EnterpriseToken) + .to receive(:allows_to?) + .with(:placeholder_users) + .and_return(true) + end + + context "when validating on project_step" do + it "accepts User as principal_type" do + form_model.project = project + form_model.principal_type = "User" + + expect(form_model).to be_valid(:project_step) + end + + it "accepts PlaceholderUser as principal_type" do + form_model.project = project + form_model.principal_type = "PlaceholderUser" + + expect(form_model).to be_valid(:project_step) + end + + it "accepts Group as principal_type" do + form_model.project = project + form_model.principal_type = "Group" + + expect(form_model).to be_valid(:project_step) + end + + it "rejects invalid principal_type" do + form_model.project = project + form_model.principal_type = "InvalidType" + + expect(form_model).not_to be_valid(:project_step) + expect(form_model.errors[:principal_type]).to be_present + end + + it "rejects nil principal_type" do + form_model.project = project + form_model.principal_type = nil + + expect(form_model).not_to be_valid(:project_step) + expect(form_model.errors[:principal_type]).to be_present + end + end + end + + context "without enterprise token allowing placeholder users" do + before do + allow(EnterpriseToken) + .to receive(:allows_to?) + .with(:placeholder_users) + .and_return(false) + end + + context "when validating on project_step" do + it "accepts User as principal_type" do + form_model.project = project + form_model.principal_type = "User" + + expect(form_model).to be_valid(:project_step) + end + + it "accepts Group as principal_type" do + form_model.project = project + form_model.principal_type = "Group" + + expect(form_model).to be_valid(:project_step) + end + + it "rejects PlaceholderUser as principal_type" do + form_model.project = project + form_model.principal_type = "PlaceholderUser" + + expect(form_model).not_to be_valid(:project_step) + expect(form_model.errors[:principal_type]).to be_present + end + + it "rejects invalid principal_type" do + form_model.project = project + form_model.principal_type = "InvalidType" + + expect(form_model).not_to be_valid(:project_step) + expect(form_model.errors[:principal_type]).to be_present + end + + it "rejects nil principal_type" do + form_model.project = project + form_model.principal_type = nil + + expect(form_model).not_to be_valid(:project_step) + expect(form_model.errors[:principal_type]).to be_present + end + end + end + end + + describe "project_id" do + context "when validating on project_step" do + it "requires project_id to be present" do + form_model.principal_type = "User" + + expect(form_model).not_to be_valid(:project_step) + expect(form_model.errors[:project_id]).to be_present + end + + it "is valid when project_id is present" do + form_model.project = project + form_model.principal_type = "User" + + expect(form_model).to be_valid(:project_step) + end + end + end + + describe "id_or_email" do + context "when validating on principal_step" do + it "requires id_or_email to be present" do + form_model.role_id = 1 + + expect(form_model).not_to be_valid(:principal_step) + expect(form_model.errors[:id_or_email]).to be_present + end + + it "is valid when id_or_email is present" do + form_model.id_or_email = "user@example.com" + form_model.role_id = 1 + + expect(form_model).to be_valid(:principal_step) + end + end + end + + describe "role_id" do + context "when validating on principal_step" do + it "requires role_id to be present" do + form_model.id_or_email = "user@example.com" + + expect(form_model).not_to be_valid(:principal_step) + expect(form_model.errors[:role_id]).to be_present + end + + it "is valid when role_id is present" do + form_model.id_or_email = "user@example.com" + form_model.role_id = 1 + + expect(form_model).to be_valid(:principal_step) + end + end + end + end + + describe ".available_principal_types" do + context "with enterprise token allowing placeholder users" do + before do + allow(EnterpriseToken) + .to receive(:allows_to?) + .with(:placeholder_users) + .and_return(true) + end + + it "returns User, PlaceholderUser, and Group" do + expect(described_class.available_principal_types).to eq(%w[User PlaceholderUser Group]) + end + end + + context "without enterprise token allowing placeholder users" do + before do + allow(EnterpriseToken) + .to receive(:allows_to?) + .with(:placeholder_users) + .and_return(false) + end + + it "returns User and Group" do + expect(described_class.available_principal_types).to eq(%w[User Group]) + end + end + end + + describe "#project_name" do + it "returns the project name when project is present" do + form_model.project = project + expect(form_model.project_name).to eq(project.name) + end + + it "returns the project_id when project is not present" do + form_model.project_id = 123 + expect(form_model.project_name).to eq(123) + end + end + + describe "#to_h" do + it "returns a hash with all attributes" do + form_model.project_id = 1 + form_model.role_id = 2 + form_model.principal_type = "User" + form_model.id_or_email = "user@example.com" + form_model.message = "Welcome!" + + expect(form_model.to_h).to eq({ + project_id: 1, + role_id: 2, + principal_type: "User", + id_or_email: "user@example.com", + message: "Welcome!" + }) + end + end +end