Feature/limit attribute help text visibility (#5749)

* code prettification

* avoid help text for newly added cf requiring restart

When using the class method statically, the values are determined upon reading the class. By that, a restart is required before newly added custom fields are added to the list and removed custom fields will still be listed

* limit visibility of attribute help texts

[ci skip]
pull/5751/head
ulferts 7 years ago committed by Oliver Günther
parent bf669db8c8
commit 4b7e3ff26d
  1. 2
      app/controllers/api/v2/custom_fields_controller.rb
  2. 49
      app/models/attribute_help_text.rb
  3. 38
      app/models/attribute_help_text/work_package.rb
  4. 32
      app/models/work_package_custom_field.rb
  5. 5
      lib/api/v3/help_texts/help_texts_api.rb
  6. 130
      spec/controllers/api/v2/custom_fields_controller_spec.rb
  7. 123
      spec/models/attribute_help_text/work_package_spec.rb
  8. 29
      spec/requests/api/v3/help_texts/help_texts_resource_spec.rb

@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@ -39,7 +40,6 @@ module Api
.visible_by_user(User.current)
.includes(:projects, :types)
.order(:id)
.uniq
other_fields = CustomField.where("type != 'WorkPackageCustomField'")
.order(:type, :id)

@ -1,3 +1,30 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# 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-2017 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 doc/COPYRIGHT.rdoc for more details.
#++
class AttributeHelpText < ActiveRecord::Base
def self.available_types
@ -15,6 +42,16 @@ class AttributeHelpText < ActiveRecord::Base
all.group_by(&:attribute_scope)
end
def self.visible(user)
scope = AttributeHelpText.subclasses[0].visible_condition(user)
AttributeHelpText.subclasses[1..-1].each do |subclass|
scope = scope.or(subclass.visible_condition(user))
end
scope
end
validates_presence_of :help_text
validates_uniqueness_of :attribute_name, scope: :type
@ -23,11 +60,19 @@ class AttributeHelpText < ActiveRecord::Base
end
def attribute_scope
raise 'not implemented'
raise NotImplementedError
end
def type_caption
raise 'not implemented'
raise NotImplementedError
end
def self.visible_condition
raise NotImplementedError
end
def self.available_attributes
raise NotImplementedError
end
end

@ -1,3 +1,31 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# 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-2017 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 doc/COPYRIGHT.rdoc for more details.
#++
class AttributeHelpText::WorkPackage < AttributeHelpText
def self.available_attributes
attributes = ::Type.translated_work_package_form_attributes
@ -18,4 +46,14 @@ class AttributeHelpText::WorkPackage < AttributeHelpText
def type_caption
I18n.t(:label_work_package)
end
def self.visible_condition(user)
visible_cf_names = WorkPackageCustomField
.visible_by_user(user)
.pluck(:id)
.map { |id| "custom_field_#{id}" }
where(attribute_name: visible_cf_names)
.or(where.not("attribute_name LIKE 'custom_field_%'"))
end
end

@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
@ -28,27 +29,32 @@
#++
class WorkPackageCustomField < CustomField
has_and_belongs_to_many :projects, join_table: "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", foreign_key: 'custom_field_id'
has_and_belongs_to_many :types, join_table: "#{table_name_prefix}custom_fields_types#{table_name_suffix}", foreign_key: 'custom_field_id'
has_many :work_packages, through: :work_package_custom_values
has_and_belongs_to_many :projects,
join_table: "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
foreign_key: 'custom_field_id'
has_and_belongs_to_many :types,
join_table: "#{table_name_prefix}custom_fields_types#{table_name_suffix}",
foreign_key: 'custom_field_id'
has_many :work_packages,
through: :work_package_custom_values
scope :visible_by_user, -> (user) {
unless user.admin?
joins('LEFT OUTER JOIN custom_fields_projects AS cfp ON (custom_fields.id = cfp.custom_field_id) ' \
'LEFT OUTER JOIN projects AS p ON (cfp.project_id = p.id) ' \
'LEFT OUTER JOIN members AS m ON (p.id = m.project_id)')
.where("p.is_public = #{ActiveRecord::Base.connection.quoted_true} " \
"OR custom_fields.is_for_all = #{ActiveRecord::Base.connection.quoted_true} " \
'OR m.user_id = ?', user.id)
scope :visible_by_user, ->(user) {
if user.allowed_to_globally?(:edit_projects)
all
else
where(projects: { id: Project.visible(user) })
.where(types: { id: Type.enabled_in(Project.visible(user)) })
.or(where(is_for_all: true).references(:projects, :types))
.includes(:projects, :types)
end
}
def self.summable
ids = Setting.work_package_list_summable_columns.map { |column_name|
ids = Setting.work_package_list_summable_columns.map do |column_name|
if match = /cf_(\d+)/.match(column_name)
match[1]
end
}.compact
end.compact
where(id: ids)
end

@ -35,7 +35,7 @@ module API
class HelpTextsAPI < ::API::OpenProjectAPI
resources :help_texts do
get do
@entries = AttributeHelpText.all
@entries = AttributeHelpText.visible(current_user)
HelpTextCollectionRepresenter.new(@entries, api_v3_paths.help_texts, current_user: current_user)
end
@ -44,8 +44,7 @@ module API
end
route_param :id do
before do
@help_text = AttributeHelpText.find(params[:id])
raise API::Errors::NotFound unless @help_text
@help_text = AttributeHelpText.visible(current_user).find(params[:id])
end
get do

@ -30,50 +30,97 @@ require 'spec_helper'
describe Api::V2::CustomFieldsController, type: :controller do
describe '#index' do
let!(:custom_field) { FactoryGirl.create(:custom_field) }
let!(:wp_custom_field_1) { FactoryGirl.create(:work_package_custom_field) }
let!(:wp_custom_field_2) { FactoryGirl.create(:work_package_custom_field) }
let!(:wp_custom_field_3) { FactoryGirl.create(:work_package_custom_field) }
let!(:wp_custom_field_for_all) { FactoryGirl.create(:work_package_custom_field, is_for_all: true) }
let!(:wp_custom_field_public) { FactoryGirl.create(:work_package_custom_field) }
let!(:type) { FactoryGirl.create(:type) }
let!(:custom_field) do
FactoryGirl.create(:custom_field)
end
let!(:wp_custom_field_1) do
cf = FactoryGirl.create(:work_package_custom_field)
type.custom_fields << cf
cf
end
let!(:wp_custom_field_2) do
cf = FactoryGirl.create(:work_package_custom_field)
type.custom_fields << cf
cf
end
let!(:wp_custom_field_3) do
cf = FactoryGirl.create(:work_package_custom_field)
type.custom_fields << cf
cf
end
let!(:wp_custom_field_for_all) do
cf = FactoryGirl.create(:work_package_custom_field, is_for_all: true)
type.custom_fields << cf
cf
end
let!(:wp_custom_field_public) do
cf = FactoryGirl.create(:work_package_custom_field)
type.custom_fields << cf
cf
end
let(:wp_custom_fields) { [wp_custom_field_1, wp_custom_field_2] }
let(:project) {
FactoryGirl.create(:project,
is_public: false,
work_package_custom_fields: wp_custom_fields)
}
let(:project_2) {
FactoryGirl.create(:project,
is_public: false,
work_package_custom_fields: wp_custom_fields)
}
let!(:public_project) {
FactoryGirl.create(:public_project,
work_package_custom_fields: [wp_custom_field_public])
}
let(:project) do
project = FactoryGirl.create(:project,
is_public: false,
work_package_custom_fields: wp_custom_fields)
project.types << type
project
end
let(:project_2) do
project = FactoryGirl.create(:project,
is_public: false,
work_package_custom_fields: wp_custom_fields)
project.types << type
project
end
let!(:public_project) do
project = FactoryGirl.create(:public_project,
work_package_custom_fields: [wp_custom_field_public])
project.types << type
project
end
before do
Role.non_member
Role.anonymous
end
shared_examples_for 'valid workflow index request' do
it { expect(response).to render_template('api/v2/custom_fields/index') }
end
shared_examples_for 'a user w/o a project' do
before do get :index, format: :xml end
before do
get :index, format: :xml
end
it_behaves_like 'valid workflow index request'
subject { assigns(:custom_fields) }
it { expect(subject.count).to eq(3) }
it { expect(subject).to include(custom_field) }
it { expect(subject).to include(wp_custom_field_for_all) }
it { expect(subject).to include(wp_custom_field_public) }
it { expect(subject).to match_array([custom_field, wp_custom_field_for_all, wp_custom_field_public]) }
end
describe 'unauthorized access' do
before do allow(Setting).to receive(:login_required).and_return false end
before do
allow(Setting).to receive(:login_required).and_return false
end
it_behaves_like 'a user w/o a project'
end
@ -82,7 +129,9 @@ describe Api::V2::CustomFieldsController, type: :controller do
context 'w/o project' do
let(:current_user) { FactoryGirl.create(:user) }
before do allow(User).to receive(:current).and_return current_user end
before do
allow(User).to receive(:current).and_return current_user
end
it_behaves_like 'a user w/o a project'
end
@ -100,19 +149,14 @@ describe Api::V2::CustomFieldsController, type: :controller do
subject { assigns(:custom_fields) }
it { expect(subject.count).to eq(5) }
it { expect(subject).to include(wp_custom_field_1) }
it { expect(subject).to include(wp_custom_field_2) }
it { expect(subject).to include(wp_custom_field_for_all) }
it { expect(subject).to include(wp_custom_field_public) }
it { expect(subject).to include(custom_field) }
it { expect(subject).not_to include(wp_custom_field_3) }
it do
is_expected
.to match_array([custom_field,
wp_custom_field_1,
wp_custom_field_2,
wp_custom_field_for_all,
wp_custom_field_public])
end
end
context 'as admin with project' do

@ -1,3 +1,31 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2017 the OpenProject Foundation (OPF)
#
# 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-2017 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 doc/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe AttributeHelpText::WorkPackage, type: :model do
@ -17,6 +45,101 @@ describe AttributeHelpText::WorkPackage, type: :model do
end
end
describe '.visible' do
let(:project) { FactoryGirl.create(:project) }
let(:role) { FactoryGirl.create(:role, permissions: permissions) }
let(:user) do
FactoryGirl.create(:user,
member_in_project: project,
member_through_role: role)
end
let(:permission) { [] }
let(:static_instance) { FactoryGirl.create :work_package_help_text, attribute_name: 'project' }
let(:cf_instance) do
custom_field = FactoryGirl.create :text_wp_custom_field
FactoryGirl.create :work_package_help_text, attribute_name: "custom_field_#{custom_field.id}"
end
subject { FactoryGirl.build :work_package_help_text }
before do
# need to clear the cache to free the memoized
# Type.translated_work_package_form_attributes
Rails.cache.clear
static_instance
cf_instance
end
subject { described_class.visible(user) }
context 'user having no permission' do
let(:user) do
FactoryGirl.create(:user)
end
it 'returns the help text for the static attribute but not the one for the custom field' do
is_expected
.to match_array([static_instance])
end
end
context 'user having the `edit_projects` permission' do
let(:permissions) { [:edit_projects] }
it 'returns the help text for the static and cf attribute' do
is_expected
.to match_array([static_instance, cf_instance])
end
end
context 'user being member in a project with activated custom fields' do
let(:permissions) { [] }
let(:type) do
type = FactoryGirl.create(:type)
project.types << type
type
end
let(:cf_instance_active) do
custom_field = FactoryGirl.create(:text_wp_custom_field)
project.work_package_custom_fields << custom_field
type.custom_fields << custom_field
FactoryGirl.create :work_package_help_text, attribute_name: "custom_field_#{custom_field.id}"
end
let(:cf_instance_inactive) do
cf_instance
end
let(:cf_instance_inactive_no_type) do
custom_field = FactoryGirl.create(:text_wp_custom_field)
project.work_package_custom_fields << custom_field
FactoryGirl.create :work_package_help_text, attribute_name: "custom_field_#{custom_field.id}"
end
let(:cf_instance_inactive_not_in_project) do
custom_field = FactoryGirl.create(:text_wp_custom_field)
type.custom_fields << custom_field
FactoryGirl.create :work_package_help_text, attribute_name: "custom_field_#{custom_field.id}"
end
let(:cf_instance_for_all) do
custom_field = FactoryGirl.create(:text_wp_custom_field, is_for_all: true)
FactoryGirl.create :work_package_help_text, attribute_name: "custom_field_#{custom_field.id}"
end
before do
cf_instance_active
cf_instance_inactive
cf_instance_inactive_no_type
cf_instance_inactive_not_in_project
cf_instance_for_all
end
it 'returns the help text for the static and active cf attributes' do
is_expected
.to match_array([static_instance, cf_instance_active, cf_instance_for_all])
end
end
end
describe 'validations' do
before do
allow(described_class).to receive(:available_attributes).and_return(status: 'Status')

@ -33,14 +33,25 @@ describe 'API v3 Help texts resource' do
include Rack::Test::Methods
include API::V3::Utilities::PathHelper
let(:project) { FactoryGirl.create(:project) }
let(:role) { FactoryGirl.create(:role, permissions: [:view_work_packages]) }
let(:current_user) do
FactoryGirl.create(:admin)
FactoryGirl.create(:user,
member_in_project: project,
member_through_role: role)
end
let!(:help_texts) do
# need to clear the cache to free the memoized
# Type.translated_work_package_form_attributes
Rails.cache.clear
custom_field = FactoryGirl.create :text_wp_custom_field
[
FactoryGirl.create(:work_package_help_text, attribute_name: 'assignee'),
FactoryGirl.create(:work_package_help_text, attribute_name: 'status')
FactoryGirl.create(:work_package_help_text, attribute_name: 'status'),
FactoryGirl.create(:work_package_help_text, attribute_name: "custom_field_#{custom_field.id}")
]
end
@ -51,7 +62,7 @@ describe 'API v3 Help texts resource' do
context 'logged in user' do
before do
allow(User).to receive(:current).and_return current_user
login_as(current_user)
get get_path
end
@ -70,7 +81,7 @@ describe 'API v3 Help texts resource' do
context 'logged in user' do
before do
allow(User).to receive(:current).and_return(current_user)
login_as(current_user)
get get_path
end
@ -87,6 +98,16 @@ describe 'API v3 Help texts resource' do
let(:type) { 'HelpText' }
end
end
context 'invisible type id' do
# cf not visible to the user
let(:help_text) { help_texts.last }
it_behaves_like 'not found' do
let(:id) { help_text.id }
let(:type) { 'HelpText' }
end
end
end
end
end

Loading…
Cancel
Save