From 49eea966d8409cee144561c1d38177271812b671 Mon Sep 17 00:00:00 2001 From: ulferts Date: Tue, 24 Feb 2026 11:08:05 +0100 Subject: [PATCH] add visible scope for user custom fields --- app/models/custom_fields/scopes/visible.rb | 17 +++++- app/models/user_custom_field.rb | 4 ++ .../user_custom_fields/scopes/visible.rb | 45 ++++++++++++++ .../custom_fields/scopes/visible_spec.rb | 48 +++++++++------ .../user_custom_fields/scopes/visible_spec.rb | 61 +++++++++++++++++++ 5 files changed, 153 insertions(+), 22 deletions(-) create mode 100644 app/models/user_custom_fields/scopes/visible.rb create mode 100644 spec/models/user_custom_fields/scopes/visible_spec.rb diff --git a/app/models/custom_fields/scopes/visible.rb b/app/models/custom_fields/scopes/visible.rb index 68b0c0f5fd8..948b5c731c7 100644 --- a/app/models/custom_fields/scopes/visible.rb +++ b/app/models/custom_fields/scopes/visible.rb @@ -34,8 +34,21 @@ module CustomFields::Scopes class_methods do def visible(user = User.current) - where(type: ProjectCustomField.name).and(ProjectCustomField.visible(user)) - .or(where(type: WorkPackageCustomField.name).and(WorkPackageCustomField.visible(user))) + known_subclasses.inject(none) do |scope, klass| + scope.or(where(type: klass.name).and(klass.visible(user))) + end + end + + def known_subclasses + # In dev/test without eager loading, subclasses might not be loaded. + if Rails.env.local? + CustomField + .group(:type) + .pluck(:type) + .map(&:safe_constantize) + end + + CustomField.subclasses end end end diff --git a/app/models/user_custom_field.rb b/app/models/user_custom_field.rb index 267e8d765f3..398e7f9a0dc 100644 --- a/app/models/user_custom_field.rb +++ b/app/models/user_custom_field.rb @@ -29,6 +29,10 @@ #++ class UserCustomField < CustomField + includes Scopes::Scoped + + scopes :visible + def type_name :label_user_plural end diff --git a/app/models/user_custom_fields/scopes/visible.rb b/app/models/user_custom_fields/scopes/visible.rb new file mode 100644 index 00000000000..ae7bfab44ba --- /dev/null +++ b/app/models/user_custom_fields/scopes/visible.rb @@ -0,0 +1,45 @@ +# 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. +# ++ + +module UserCustomFields::Scopes + module Visible + extend ActiveSupport::Concern + + class_methods do + def visible(user = User.current) + if user.admin? + all + else + where(admin_only: false) + end + end + end + end +end diff --git a/spec/models/custom_fields/scopes/visible_spec.rb b/spec/models/custom_fields/scopes/visible_spec.rb index 108dcc4da09..7aef46d6ae8 100644 --- a/spec/models/custom_fields/scopes/visible_spec.rb +++ b/spec/models/custom_fields/scopes/visible_spec.rb @@ -33,9 +33,11 @@ require "spec_helper" RSpec.describe CustomFields::Scopes::Visible do shared_let(:project_cf) { create(:string_project_custom_field) } shared_let(:work_package_cf) { create(:string_wp_custom_field) } + shared_let(:user_cf) { create(:user_custom_field) } let(:project_cf_visible) { false } let(:work_package_cf_visible) { false } + let(:user_cf_visible) { false } # Since there would be very many tests here, we break the rule of testing # the scope as a black box. Knowing that the scope relies on the individual visible scopes of each @@ -46,14 +48,14 @@ RSpec.describe CustomFields::Scopes::Visible do current_user { build_stubbed(:user) } before do - allow(ProjectCustomField) - .to receive(:visible) - .with(current_user) - .and_return(project_cf_visible ? ProjectCustomField.all : ProjectCustomField.none) - allow(WorkPackageCustomField) - .to receive(:visible) - .with(current_user) - .and_return(work_package_cf_visible ? WorkPackageCustomField.all : WorkPackageCustomField.none) + { ProjectCustomField => project_cf_visible, + WorkPackageCustomField => work_package_cf_visible, + UserCustomField => user_cf_visible }.each do |klass, visible| + allow(klass) + .to receive(:visible) + .with(current_user) + .and_return(visible ? klass.all : klass.none) + end end context "for a project custom field" do @@ -63,12 +65,6 @@ RSpec.describe CustomFields::Scopes::Visible do it "returns the project custom field" do expect(subject).to contain_exactly(project_cf) end - - it "calls the visible scope of the project custom field" do - subject - - expect(ProjectCustomField).to have_received(:visible).with(current_user) - end end context "if the fields are invisible" do @@ -87,12 +83,6 @@ RSpec.describe CustomFields::Scopes::Visible do it "returns the work package custom field" do expect(subject).to contain_exactly(work_package_cf) end - - it "calls the visible scope of the work_package custom field" do - subject - - expect(WorkPackageCustomField).to have_received(:visible).with(current_user) - end end context "if the fields are invisible" do @@ -103,5 +93,23 @@ RSpec.describe CustomFields::Scopes::Visible do end end end + + context "for a user custom field" do + context "if the fields are visible" do + let(:user_cf_visible) { true } + + it "returns the user custom field" do + expect(subject).to contain_exactly(user_cf) + end + end + + context "if the fields are invisible" do + let(:user_cf_visible) { false } + + it "does not return the user custom field" do + expect(subject).to be_empty + end + end + end end end diff --git a/spec/models/user_custom_fields/scopes/visible_spec.rb b/spec/models/user_custom_fields/scopes/visible_spec.rb new file mode 100644 index 00000000000..19ca7058b17 --- /dev/null +++ b/spec/models/user_custom_fields/scopes/visible_spec.rb @@ -0,0 +1,61 @@ +# 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 UserCustomFields::Scopes::Visible do + shared_let(:admin_only_user_cf) { create(:user_custom_field, admin_only: true) } + shared_let(:user_cf) { create(:user_custom_field, admin_only: false) } + + shared_let(:admin) { create(:admin) } + shared_let(:user) { create(:user) } + + describe ".visible" do + subject { UserCustomField.visible(current_user) } + + current_user { build_stubbed(:user) } + + context "for an admin" do + current_user { admin } + + it "returns all custom fields" do + expect(subject).to contain_exactly(admin_only_user_cf, user_cf) + end + end + + context "for a non admin" do + current_user { user } + + it "returns only the non admin custom fields" do + expect(subject).to contain_exactly(user_cf) + end + end + end +end