Placeholder user backend (#8935)

* Add missing copyright note to AnonymousUser [ci skip]

* Addin PlaceholderUser model and its associations

* Ensure PlaceholderUser do not "leak out" uncontrolled

* Extract Associations for User and PlaceholderUser.

- remove obsolete #assigned_issues
- remove obsoete #:responsible_for_issues
- add specs and factory for PlaceholderUser
Adding specs to PlaceholderModel

* Migration: enforce uniqueness of lastname for Group and PlaceholderUser

* Remove obsolete callback `after_add` on groups association.

The association of principals and groups still had a callback
`after_add` that called a method `user_added` on the group.
That method was not defined anymore as it was removed in
commit d93ff52a27.

* Move validation for #groupname to the right spec.

* Cleanup placeholder and group specs

* Remove Setting `workpackage_group_assignment`

* Refactoring: Extract assignable scope from Project to Member

* Refactor: Add Member scope not_locked.

* deprecate hacky scope

* remove wp_group_assignment specific test case

Co-authored-by: ulferts <jens.ulferts@googlemail.com>
pull/8949/head
Wieland Lindenthal 4 years ago committed by GitHub
parent 3157dd0d0b
commit efbd3e9b41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      app/models/anonymous_user.rb
  2. 37
      app/models/associations/groupable.rb
  3. 6
      app/models/custom_actions/actions/assigned_to.rb
  4. 4
      app/models/member.rb
  5. 43
      app/models/members/scopes/assignable.rb
  6. 41
      app/models/members/scopes/not_locked.rb
  7. 42
      app/models/placeholder_user.rb
  8. 12
      app/models/principal.rb
  9. 51
      app/models/project.rb
  10. 7
      app/models/queries/work_packages/filter/assignee_or_group_filter.rb
  11. 7
      app/models/queries/work_packages/filter/principal_base_filter.rb
  12. 3
      app/models/queries/work_packages/filter/principal_loader.rb
  13. 21
      app/models/user.rb
  14. 1
      app/views/work_packages/settings/work_package_tracking.html.erb
  15. 1
      config/locales/en.yml
  16. 2
      config/settings.yml
  17. 17
      db/migrate/20210126112238_add_uniqueness_constrain_on_lastname_for_groups_and_placeholder_users.rb
  18. 8
      lib/api/v3/queries/schemas/all_principals_filter_dependency_representer.rb
  19. 4
      lib/api/v3/work_packages/principal_setter.rb
  20. 18
      lib/open_project/deprecation.rb
  21. 33
      spec/factories/placeholder_user_factory.rb
  22. 65
      spec/lib/api/v3/queries/schemas/all_principals_filter_dependency_representer_spec.rb
  23. 1
      spec/lib/api/v3/queries/schemas/group_filter_dependency_representer_spec.rb
  24. 39
      spec/models/custom_actions/actions/assigned_to_spec.rb
  25. 29
      spec/models/custom_actions/actions/responsible_spec.rb
  26. 9
      spec/models/group_spec.rb
  27. 55
      spec/models/placeholder_user_spec.rb
  28. 18
      spec/models/project_spec.rb
  29. 31
      spec/models/queries/work_packages/filter/assigned_to_filter_spec.rb
  30. 23
      spec/models/queries/work_packages/filter/responsible_filter_spec.rb
  31. 27
      spec/models/user_spec.rb
  32. 8
      spec/models/work_package_spec.rb
  33. 24
      spec/requests/api/v3/work_package_resource_spec.rb
  34. 46
      spec/requests/api/v3/work_packages/form/work_package_form_resource_spec.rb
  35. 19
      spec/support/api/v3/shared_available_principals_examples.rb
  36. 17
      spec_legacy/unit/mail_handler_spec.rb

@ -1,3 +1,33 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 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-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 docs/COPYRIGHT.rdoc for more details.
#++
class AnonymousUser < User
validate :validate_unique_anonymous_user, on: :create

@ -0,0 +1,37 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
module Associations::Groupable
def self.included(base)
base.has_and_belongs_to_many :groups,
join_table: "#{base.table_name_prefix}group_users#{base.table_name_suffix}",
after_remove: ->(user, group) { group.user_removed(user) }
end
end

@ -97,10 +97,6 @@ class CustomActions::Actions::AssignedTo < CustomActions::Actions::Base
end
def principal_class
if Setting.work_package_group_assignment?
Principal
else
User
end
Principal
end
end

@ -52,7 +52,9 @@ class Member < ApplicationRecord
scope_classes Members::Scopes::Global,
Members::Scopes::Visible,
Members::Scopes::Of
Members::Scopes::Of,
Members::Scopes::Assignable,
Members::Scopes::NotLocked
def name
principal.name

@ -0,0 +1,43 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
# Find all members that are whose principals are not locked and have an
# assignable role.
module Members::Scopes
class Assignable
def self.fetch
Member
.not_locked
.includes(:roles)
.references(:roles)
.where(roles: { assignable: true })
end
end
end

@ -0,0 +1,41 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
# Find all members that are whose principals are not locked.
module Members::Scopes
class NotLocked
def self.fetch
Member
.includes(:principal)
.references(:principals)
.merge(Principal.not_builtin_but_with_placeholder_users.active_or_registered)
end
end
end

@ -0,0 +1,42 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 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-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 docs/COPYRIGHT.rdoc for more details.
#++
class PlaceholderUser < Principal
alias_attribute(:name, :lastname)
validates_presence_of(:name)
validates_uniqueness_of(:name)
include ::Associations::Groupable
def to_s
lastname
end
end

@ -73,8 +73,20 @@ class Principal < ApplicationRecord
}
scope :not_builtin, -> {
# TODO: Remove PlaceholderUser from this list. This is a temporary hack that ensures that Placeholders don't
# suddenly show up where they are are not supposed to show up. In case you want them to show up use the temporary
# scope :not_builtin_but_with_placeholder_users
where.not(type: [SystemUser.name, AnonymousUser.name, DeletedUser.name, PlaceholderUser.name])
}
scope :not_builtin_but_with_placeholder_users, -> {
# TODO: This is temporary precaution scope to circumvent the hack in the :not_builtin scope. Needs to be
# removed before we release this code.
where.not(type: [SystemUser.name, AnonymousUser.name, DeletedUser.name])
}
OpenProject::Deprecation.deprecate_class_method self,
:not_builtin_but_with_placeholder_users,
:not_builtin
scope :like, ->(q) {
firstnamelastname = "((firstname || ' ') || lastname)"

@ -42,6 +42,7 @@ class Project < ApplicationRecord
# reserved identifiers
RESERVED_IDENTIFIERS = %w(new).freeze
# TODO: Is this association ever used? Groups are missing (and PlaceholderUsers).
has_many :members, -> {
includes(:principal, :roles)
.where(
@ -53,17 +54,16 @@ class Project < ApplicationRecord
.references(:principal, :roles)
}
has_many :possible_assignee_members, -> {
includes(:principal, :roles)
.where(Project.possible_principles_condition)
.references(:principals, :roles)
}, class_name: 'Member'
has_many :possible_assignee_members,
-> { assignable },
class_name: 'Member'
# Read only
has_many :possible_assignees,
->(object) {
# Have to reference members and roles again although
# possible_assignee_members does already specify it to be able to use the
# Project.possible_principles_condition there
# assignable_principals there
#
# The .where(members_users: { project_id: object.id })
# part is an optimization preventing to have all the members joined
@ -74,17 +74,17 @@ class Project < ApplicationRecord
},
through: :possible_assignee_members,
source: :principal
has_many :possible_responsible_members, -> {
includes(:principal, :roles)
.where(Project.possible_principles_condition)
.references(:principals, :roles)
}, class_name: 'Member'
has_many :possible_responsible_members,
-> { assignable },
class_name: 'Member'
# Read only
has_many :possible_responsibles,
->(object) {
# Have to reference members and roles again although
# possible_responsible_members does already specify it to be able to use
# the Project.possible_principles_condition there
# assignable_principals there
#
# The .where(members_users: { project_id: object.id })
# part is an optimization preventing to have all the members joined
@ -97,15 +97,7 @@ class Project < ApplicationRecord
source: :principal
has_many :memberships, class_name: 'Member'
has_many :member_principals,
-> {
includes(:principal)
.references(:principals)
.where("#{Principal.table_name}.type='Group' OR " +
"(#{Principal.table_name}.type='User' AND " +
"(#{Principal.table_name}.status=#{Principal::STATUSES[:active]} OR " +
"#{Principal.table_name}.status=#{Principal::STATUSES[:registered]} OR " +
"#{Principal.table_name}.status=#{Principal::STATUSES[:invited]}))")
},
-> { not_locked },
class_name: 'Member'
has_many :users, through: :members, source: :principal
has_many :principals, through: :member_principals, source: :principal
@ -230,7 +222,7 @@ class Project < ApplicationRecord
# Returns all projects the user is allowed to see.
#
# Employs the :view_project permission to perform the
# authorization check as the permissino is public, meaning it is granted
# authorization check as the permission is public, meaning it is granted
# to everybody having at least one role in a project regardless of the
# role's permissions.
def self.visible_by(user = User.current)
@ -520,21 +512,6 @@ class Project < ApplicationRecord
.flatten
end
def self.possible_principles_condition
condition = if Setting.work_package_group_assignment?
["(#{Principal.table_name}.type=? OR #{Principal.table_name}.type=?)", 'User', 'Group']
else
["(#{Principal.table_name}.type=?)", 'User']
end
condition[0] += " AND (#{User.table_name}.status=? OR #{User.table_name}.status=?) AND roles.assignable = ?"
condition << Principal::STATUSES[:active]
condition << Principal::STATUSES[:invited]
condition << true
sanitize_sql_array condition
end
protected
def shared_versions_on_persisted

@ -32,12 +32,7 @@ class Queries::WorkPackages::Filter::AssigneeOrGroupFilter <
Queries::WorkPackages::Filter::PrincipalBaseFilter
def allowed_values
@allowed_values ||= begin
values = principal_loader.user_values
if Setting.work_package_group_assignment?
values += principal_loader.group_values
end
values = principal_loader.user_values + principal_loader.group_values
me_allowed_value + values.sort
end
end

@ -34,12 +34,7 @@ class Queries::WorkPackages::Filter::PrincipalBaseFilter <
def allowed_values
@allowed_values ||= begin
values = principal_loader.user_values
if Setting.work_package_group_assignment?
values += principal_loader.group_values
end
values = principal_loader.user_values + principal_loader.group_values
me_allowed_value + values.sort
end
end

@ -55,8 +55,7 @@ class Queries::WorkPackages::Filter::PrincipalLoader
if project
project.principals.sort
else
user_or_principal = Setting.work_package_group_assignment? ? Principal : User
user_or_principal.active_or_registered.in_visible_project.sort
Principal.active_or_registered.in_visible_project.sort
end
end

@ -34,11 +34,11 @@ class User < Principal
include ::Scopes::Scoped
USER_FORMATS_STRUCTURE = {
firstname_lastname: [:firstname, :lastname],
firstname: [:firstname],
lastname_firstname: [:lastname, :firstname],
lastname_coma_firstname: [:lastname, :firstname],
username: [:login]
firstname_lastname: [:firstname, :lastname],
firstname: [:firstname],
lastname_firstname: [:lastname, :firstname],
lastname_coma_firstname: [:lastname, :firstname],
username: [:login]
}.freeze
USER_MAIL_OPTION_ALL = ['all', :label_user_mail_option_all].freeze
@ -57,19 +57,10 @@ class User < Principal
USER_MAIL_OPTION_NON
].freeze
has_and_belongs_to_many :groups,
join_table: "#{table_name_prefix}group_users#{table_name_suffix}",
after_add: ->(user, group) { group.user_added(user) },
after_remove: ->(user, group) { group.user_removed(user) }
include ::Associations::Groupable
has_many :categories, foreign_key: 'assigned_to_id',
dependent: :nullify
has_many :assigned_issues, foreign_key: 'assigned_to_id',
class_name: 'WorkPackage',
dependent: :nullify
has_many :responsible_for_issues, foreign_key: 'responsible_id',
class_name: 'WorkPackage',
dependent: :nullify
has_many :watches, class_name: 'Watcher',
dependent: :delete_all
has_many :changesets, dependent: :nullify

@ -34,7 +34,6 @@ See docs/COPYRIGHT.rdoc for more details.
<%= styled_form_tag({action: 'edit'}) do %>
<section class="form--section">
<div class="form--field"><%= setting_check_box :cross_project_work_package_relations %></div>
<div class="form--field"><%= setting_check_box :work_package_group_assignment %></div>
<div class="form--field"><%= setting_check_box :display_subprojects_work_packages %></div>
<div class="form--field"><%= setting_check_box :work_package_startdate_is_adddate %></div>
<div class="form--field"><%= setting_select :work_package_done_ratio, WorkPackage::DONE_RATIO_OPTIONS.collect {|i| [t("setting_work_package_done_ratio_#{i}"), i]}, container_class: '-middle' %></div>

@ -2301,7 +2301,6 @@ en:
setting_welcome_text: "Welcome block text"
setting_welcome_title: "Welcome block title"
setting_welcome_on_homescreen: "Display welcome block on homescreen"
setting_work_package_group_assignment: "Allow assignment to groups"
setting_work_package_list_default_highlighting_mode: "Default highlighting mode"
setting_work_package_list_default_highlighted_attributes: "Default inline highlighted attributes"

@ -230,8 +230,6 @@ user_format:
cross_project_work_package_relations:
default: 1
format: boolean
work_package_group_assignment:
default: 1
notified_events:
serialized: true
default:

@ -0,0 +1,17 @@
class AddUniquenessConstrainOnLastnameForGroupsAndPlaceholderUsers < ActiveRecord::Migration[6.1]
def up
# Partial unique index
execute <<-SQL
CREATE UNIQUE INDEX unique_lastname_for_groups_and_placeholder_users ON
users (lastname, type)
WHERE (type = 'Group' OR type = 'PlaceholderUser');
SQL
end
def down
# Remove unique index
execute <<-SQL
DROP INDEX IF EXISTS unique_lastname_for_groups_and_placeholder_users;
SQL
end
end

@ -37,9 +37,9 @@ module API
def json_cache_key
if filter.project
super + [Setting.work_package_group_assignment?, filter.project.id]
super + [filter.project.id]
else
super + [Setting.work_package_group_assignment?]
super
end
end
@ -49,10 +49,6 @@ module API
params = [{ status: { operator: '!',
values: [Principal::STATUSES[:locked].to_s] } }]
unless Setting.work_package_group_assignment?
params << { type: { operator: '=', values: ['User'] } }
end
params << if filter.project
{ member: { operator: '=', values: [filter.project.id.to_s] } }
else

@ -34,12 +34,10 @@ module API
class PrincipalSetter
def self.lambda(name, property_name = name)
->(args) {
expected_namespaces = Setting.work_package_group_assignment? ? %i(groups users) : %i(users)
lambda = ::API::V3::Principals::AssociatedSubclassLambda
.setter(name,
property_name: property_name,
namespaces: expected_namespaces)
namespaces: %i(groups users))
instance_exec(**args, &lambda)
}

@ -34,8 +34,22 @@ module OpenProject::Deprecation
##
# Deprecate the given method with a notice regarding future removal
def deprecate_method(mod, method)
deprecator.deprecate_methods(mod, method)
#
# @mod [Class] The module on which the method is to be replaced.
# @method [:symbol] The method to replace.
# @replacement [nil, :symbol, String] The replacement method.
def deprecate_method(mod, method, replacement = nil)
deprecator.deprecate_methods(mod, method => replacement)
end
##
# Deprecate the given class method with a notice regarding future removal
#
# @mod [Class] The module on which the method is to be replaced.
# @method [:symbol] The method to replace.
# @replacement [nil, :symbol, String] The replacement method.
def deprecate_class_method(mod, method, replacement = nil)
deprecate_method(mod.singleton_class, method, replacement)
end
def replaced(old_method, new_method, called_from)

@ -0,0 +1,33 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
FactoryBot.define do
factory :placeholder_user, parent: :principal, class: PlaceholderUser do
sequence(:name) { |n| "UX Designer #{n}" }
end
end

@ -35,7 +35,6 @@ describe ::API::V3::Queries::Schemas::AllPrincipalsFilterDependencyRepresenter,
let(:query) { FactoryBot.build_stubbed(:query, project: project) }
let(:filter) { Queries::WorkPackages::Filter::AssignedToFilter.create!(context: query) }
let(:form_embedded) { false }
let(:group_assignment_enabled) { false }
let(:instance) do
described_class.new(filter,
@ -43,12 +42,6 @@ describe ::API::V3::Queries::Schemas::AllPrincipalsFilterDependencyRepresenter,
form_embedded: form_embedded)
end
before do
allow(Setting)
.to receive(:work_package_group_assignment?)
.and_return(group_assignment_enabled)
end
subject(:generated) { instance.to_json }
context 'generation' do
@ -72,53 +65,11 @@ describe ::API::V3::Queries::Schemas::AllPrincipalsFilterDependencyRepresenter,
it_behaves_like 'filter dependency empty'
end
context 'within a project with group assignment' do
context 'within a project' do
let(:filter_query) do
[{ status: { operator: '!', values: ['3'] } },
{ member: { operator: '=', values: [project.id.to_s] } }]
end
let(:group_assignment_enabled) { true }
context "for operator 'Queries::Operators::Equals'" do
let(:operator) { Queries::Operators::Equals }
it_behaves_like 'filter dependency with allowed link'
end
context "for operator 'Queries::Operators::NotEquals'" do
let(:operator) { Queries::Operators::NotEquals }
it_behaves_like 'filter dependency with allowed link'
end
end
context 'within a project without group assignment' do
let(:filter_query) do
[{ status: { operator: '!', values: ['3'] } },
{ type: { operator: '=', values: ['User'] } },
{ member: { operator: '=', values: [project.id.to_s] } }]
end
context "for operator 'Queries::Operators::Equals'" do
let(:operator) { Queries::Operators::Equals }
it_behaves_like 'filter dependency with allowed link'
end
context "for operator 'Queries::Operators::NotEquals'" do
let(:operator) { Queries::Operators::NotEquals }
it_behaves_like 'filter dependency with allowed link'
end
end
context 'global with no group assignments' do
let(:project) { nil }
let(:filter_query) do
[{ status: { operator: '!', values: ['3'] } },
{ type: { operator: '=', values: ['User'] } },
{ member: { operator: '*', values: [] } }]
end
context "for operator 'Queries::Operators::Equals'" do
let(:operator) { Queries::Operators::Equals }
@ -133,13 +84,12 @@ describe ::API::V3::Queries::Schemas::AllPrincipalsFilterDependencyRepresenter,
end
end
context 'global with group assignments' do
context 'global' do
let(:project) { nil }
let(:filter_query) do
[{ status: { operator: '!', values: ['3'] } },
{ member: { operator: '*', values: [] } }]
end
let(:group_assignment_enabled) { true }
context "for operator 'Queries::Operators::Equals'" do
let(:operator) { Queries::Operators::Equals }
@ -190,17 +140,6 @@ describe ::API::V3::Queries::Schemas::AllPrincipalsFilterDependencyRepresenter,
instance.to_json
end
it 'busts the cache on a different work_package_group_assignment setting' do
allow(Setting)
.to receive(:work_package_group_assignment?)
.and_return(!Setting.work_package_group_assignment?)
expect(instance)
.to receive(:to_hash)
instance.to_json
end
it 'busts the cache on changes to the locale' do
expect(instance)
.to receive(:to_hash)

@ -70,7 +70,6 @@ describe ::API::V3::Queries::Schemas::GroupFilterDependencyRepresenter, clear_ca
[{ type: { operator: '=', values: ['Group'] } },
{ member: { operator: '=', values: [project.id.to_s] } }]
end
let(:group_assignment_enabled) { true }
context "for operator 'Queries::Operators::Equals'" do
let(:operator) { Queries::Operators::Equals }

@ -32,21 +32,11 @@ describe CustomActions::Actions::AssignedTo, type: :model do
let(:key) { :assigned_to }
let(:type) { :associated_property }
let(:allowed_values) do
users = []
if !Setting.work_package_group_assignment?
users = [FactoryBot.build_stubbed(:user),
FactoryBot.build_stubbed(:user)]
allow(User)
.to receive_message_chain(:active_or_registered, :select, :order_by_name)
.and_return(users)
else
users = [FactoryBot.build_stubbed(:user),
FactoryBot.build_stubbed(:group)]
allow(Principal)
.to receive_message_chain(:active_or_registered, :select, :order_by_name)
.and_return(users)
end
users = [FactoryBot.build_stubbed(:user),
FactoryBot.build_stubbed(:group)]
allow(Principal)
.to receive_message_chain(:active_or_registered, :select, :order_by_name)
.and_return(users)
[{ value: nil, label: '-' },
{ value: 'current_user', label: '(Assign to executing user)' },
@ -56,22 +46,11 @@ describe CustomActions::Actions::AssignedTo, type: :model do
it_behaves_like 'base custom action'
it_behaves_like 'associated custom action' do
describe '#allowed_values' do
context 'group assignment disabled', with_settings: { work_package_group_assignment?: false } do
it 'is the list of all users' do
allowed_values
expect(instance.allowed_values)
.to eql(allowed_values)
end
end
context 'group assignment enabled', with_settings: { work_package_group_assignment?: true } do
it 'is the list of all users' do
allowed_values
it 'is the list of all users' do
allowed_values
expect(instance.allowed_values)
.to eql(allowed_values)
end
expect(instance.allowed_values)
.to eql(allowed_values)
end
end
end

@ -32,37 +32,26 @@ describe CustomActions::Actions::Responsible, type: :model do
let(:key) { :responsible }
let(:type) { :associated_property }
let(:allowed_values) do
users = [FactoryBot.build_stubbed(:user),
FactoryBot.build_stubbed(:user)]
principals = [FactoryBot.build_stubbed(:user),
FactoryBot.build_stubbed(:group)]
allow(User)
.to receive_message_chain(:active_or_registered, :select, :order_by_name)
.and_return(users)
.and_return(principals)
[{ value: nil, label: '-' },
{ value: users.first.id, label: users.first.name },
{ value: users.last.id, label: users.last.name }]
{ value: principals.first.id, label: principals.first.name },
{ value: principals.last.id, label: principals.last.name }]
end
it_behaves_like 'base custom action'
it_behaves_like 'associated custom action' do
describe '#allowed_values' do
context 'group assignment disabled', with_settings: { work_package_group_assignment?: false } do
it 'is the list of all users' do
allowed_values
it 'is the list of all users and groups' do
allowed_values
expect(instance.allowed_values)
.to eql(allowed_values)
end
end
context 'group assignment enabled', with_settings: { work_package_group_assignment?: true } do
it 'is the list of all users' do
allowed_values
expect(instance.allowed_values)
.to eql(allowed_values)
end
expect(instance.allowed_values)
.to eql(allowed_values)
end
end
end

@ -37,12 +37,12 @@ describe Group, type: :model do
let(:watcher) { FactoryBot.create :user }
let(:project) { FactoryBot.create(:project_with_types) }
let(:status) { FactoryBot.create(:status) }
let(:package) {
let(:package) do
FactoryBot.build(:work_package, type: project.types.first,
author: user,
project: project,
status: status)
}
end
it 'should create' do
g = Group.new(lastname: 'New group')
@ -167,4 +167,9 @@ describe Group, type: :model do
end
end
end
describe '#groupname' do
it { expect(group).to validate_presence_of :groupname }
it { expect(group).to validate_uniqueness_of :groupname }
end
end

@ -0,0 +1,55 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe PlaceholderUser, type: :model do
let(:placeholder_user) { FactoryBot.build(:placeholder_user) }
subject { placeholder_user }
describe '#name' do
it 'updates the name' do
subject.name = "Foo"
expect(subject.name).to eq("Foo")
end
it 'updates the lastname attribute' do
subject.name = "Foo"
expect(subject.lastname).to eq("Foo")
end
it { is_expected.to validate_presence_of :name }
it { is_expected.to validate_uniqueness_of :name }
end
describe "#to_s" do
it 'returns the lastname' do
expect(subject.to_s).to eq(subject.lastname)
end
end
end

@ -135,30 +135,20 @@ describe Project, type: :model do
roles: [role])
end
shared_examples_for 'respecting group assignment settings' do
context 'with group assignment' do
before { allow(Setting).to receive(:work_package_group_assignment?).and_return(true) }
it { is_expected.to match_array([user, group]) }
end
context 'w/o group assignment' do
before { allow(Setting).to receive(:work_package_group_assignment?).and_return(false) }
it { is_expected.to match_array([user]) }
end
shared_examples_for 'returning groups and users' do
it { is_expected.to match_array([user, group]) }
end
describe 'assignees' do
subject { project.possible_assignees }
it_behaves_like 'respecting group assignment settings'
it_behaves_like 'returning groups and users'
end
describe 'responsibles' do
subject { project.possible_responsibles }
it_behaves_like 'respecting group assignment settings'
it_behaves_like 'returning groups and users'
end
end

@ -275,18 +275,6 @@ describe Queries::WorkPackages::Filter::AssignedToFilter, type: :model do
expect(instance).to be_available
end
it 'is false if there is another group selectable but the setting is not favourable' do
allow(Setting)
.to receive(:work_package_group_assignment?)
.and_return(false)
allow(principal_loader)
.to receive(:group_values)
.and_return([[group_1.name, group_1.id.to_s]])
expect(instance).to_not be_available
end
end
end
@ -314,16 +302,6 @@ describe Queries::WorkPackages::Filter::AssignedToFilter, type: :model do
[user_1.name, user_1.id.to_s],
[group_1.name, group_1.id.to_s]])
end
it 'returns the me value and only the available users if no group assignmit is allowed' do
allow(Setting)
.to receive(:work_package_group_assignment?)
.and_return(false)
expect(instance.allowed_values)
.to match_array([[I18n.t(:label_me), 'me'],
[user_1.name, user_1.id.to_s]])
end
end
context 'when not being logged in' do
@ -334,15 +312,6 @@ describe Queries::WorkPackages::Filter::AssignedToFilter, type: :model do
.to match_array([[user_1.name, user_1.id.to_s],
[group_1.name, group_1.id.to_s]])
end
it 'returns the available users if no group assignmit is allowed' do
allow(Setting)
.to receive(:work_package_group_assignment?)
.and_return(false)
expect(instance.allowed_values)
.to match_array([[user_1.name, user_1.id.to_s]])
end
end
end
end

@ -250,6 +250,7 @@ describe Queries::WorkPackages::Filter::ResponsibleFilter, type: :model do
describe '#allowed_values' do
let(:logged_in) { true }
let(:group) { FactoryBot.build_stubbed(:group) }
before do
allow(User)
@ -259,23 +260,18 @@ describe Queries::WorkPackages::Filter::ResponsibleFilter, type: :model do
allow(principal_loader)
.to receive(:user_values)
.and_return([[user_1.name, user_1.id.to_s]])
allow(principal_loader)
.to receive(:group_values)
.and_return([[group.name, group.id.to_s]])
end
context 'when being logged in' do
it 'returns the me value and the available users' do
expect(instance.allowed_values)
.to match_array([[I18n.t(:label_me), 'me'],
[user_1.name, user_1.id.to_s]])
end
it 'returns the me value and only the available users if no group assignmit is allowed' do
allow(Setting)
.to receive(:work_package_group_assignment?)
.and_return(false)
it 'returns the me value, the available users, and groups' do
expect(instance.allowed_values)
.to match_array([[I18n.t(:label_me), 'me'],
[user_1.name, user_1.id.to_s]])
[user_1.name, user_1.id.to_s],
[group.name, group.id.to_s]])
end
end
@ -284,7 +280,8 @@ describe Queries::WorkPackages::Filter::ResponsibleFilter, type: :model do
it 'returns the available users' do
expect(instance.allowed_values)
.to match_array([[user_1.name, user_1.id.to_s]])
.to match_array([[user_1.name, user_1.id.to_s],
[group.name, group.id.to_s]])
end
end
end

@ -184,33 +184,6 @@ describe User, type: :model do
end
end
describe '#assigned_issues' do
before do
user.save!
end
describe 'WHEN the user has an issue assigned' do
before do
member.save!
issue.assigned_to = user
issue.save!
end
it { expect(user.assigned_issues).to eq([issue]) }
end
describe 'WHEN the user has no issue assigned' do
before do
member.save!
issue.save!
end
it { expect(user.assigned_issues).to eq([]) }
end
end
describe '#authentication_provider' do
before do
user.identity_url = 'test_provider:veryuniqueid'

@ -128,10 +128,6 @@ describe WorkPackage, type: :model do
context 'group_assignment' do
let(:group) { FactoryBot.create(:group) }
before do
allow(Setting).to receive(:work_package_group_assignment).and_return(true)
end
subject do
FactoryBot.create(:work_package,
assigned_to: group).assigned_to
@ -199,9 +195,7 @@ describe WorkPackage, type: :model do
subject { work_package.valid? }
context 'with assignable groups' do
before { allow(Setting).to receive(:work_package_group_assignment?).and_return(true) }
context 'with group assigned' do
include_context 'assign group as responsible'
it { is_expected.to be_truthy }

@ -761,21 +761,15 @@ describe 'API v3 Work package resource',
before { allow(User).to receive(:current).and_return current_user }
shared_context 'setup group membership' do |group_assignment|
shared_context 'setup group membership' do
let(:group) { FactoryBot.create(:group) }
let(:group_role) { FactoryBot.create(:role) }
let(:group_member) do
let!(:group_member) do
FactoryBot.create(:member,
principal: group,
project: project,
roles: [group_role])
end
before do
allow(Setting).to receive(:work_package_group_assignment?).and_return(group_assignment)
group_member
end
end
shared_examples_for 'handling people' do |property|
@ -871,20 +865,6 @@ describe 'API v3 Work package resource',
end
end
end
context 'group assignment disabled' do
let(:user_href) { api_v3_paths.user group.id }
include_context 'setup group membership', false
include_context 'patch request'
it_behaves_like 'constraint violation' do
let(:message) do
I18n.t('api_v3.errors.validation.invalid_user_assigned_to_work_package',
property: WorkPackage.human_attribute_name(property))
end
end
end
end
end

@ -399,24 +399,6 @@ describe 'API v3 Work package form resource', type: :request, with_mail: false d
end
describe 'assignee and responsible' do
shared_context 'setup group membership' do |group_assignment|
let(:group) { FactoryBot.create(:group) }
let(:role) { FactoryBot.create(:role) }
let(:group_member) do
FactoryBot.create(:member,
principal: group,
project: project,
roles: [role])
end
before do
allow(Setting).to receive(:work_package_group_assignment?)
.and_return(group_assignment)
group_member.save!
end
end
shared_examples_for 'handling people' do |property|
let(:path) { "_embedded/payload/_links/#{property}/href" }
let(:visible_user) do
@ -457,8 +439,18 @@ describe 'API v3 Work package form resource', type: :request, with_mail: false d
context 'existing group' do
let(:user_link) { api_v3_paths.group group.id }
let(:group) { FactoryBot.create(:group) }
let(:role) { FactoryBot.create(:role) }
let(:group_member) do
FactoryBot.create(:member,
principal: group,
project: project,
roles: [role])
end
include_context 'setup group membership', true
before do
group_member.save!
end
it_behaves_like 'valid user assignment'
end
@ -491,22 +483,6 @@ describe 'API v3 Work package form resource', type: :request, with_mail: false d
end
end
end
context 'group assignement disabled' do
let(:user_link) { api_v3_paths.group group.id }
include_context 'setup group membership', false
include_context 'post request'
it_behaves_like 'invalid resource link' do
let(:message) do
I18n.t('api_v3.errors.invalid_resource',
property: property,
expected: "/api/v3/users/:id",
actual: user_link)
end
end
end
end
end

@ -81,23 +81,8 @@ shared_examples_for 'available principals' do |principals|
describe 'groups' do
let!(:users) { [group] }
context 'with work_package_group_assignment' do
before do
allow(Setting).to receive(:work_package_group_assignment?).and_return(true)
end
# current user and group
it_behaves_like "returns available #{principals}", 2, 2, 'Group'
end
context 'without work_package_group_assignment' do
before do
allow(Setting).to receive(:work_package_group_assignment?).and_return(false)
end
# Only the current user
it_behaves_like "returns available #{principals}", 1, 1, 'User'
end
# current user and group
it_behaves_like "returns available #{principals}", 2, 2, 'Group'
end
end

@ -52,17 +52,14 @@ describe MailHandler, type: :model do
assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
end
context 'with group assignment set',
with_settings: { work_package_group_assignment: 1 } do
it 'should add work package with group assignment' do
work_package = submit_email('ticket_on_given_project.eml') do |email|
email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
end
assert work_package.is_a?(WorkPackage)
assert !work_package.new_record?
work_package.reload
assert_equal Group.find(11), work_package.assigned_to
it 'should add work package with group assignment' do
work_package = submit_email('ticket_on_given_project.eml') do |email|
email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
end
assert work_package.is_a?(WorkPackage)
assert !work_package.new_record?
work_package.reload
assert_equal Group.find(11), work_package.assigned_to
end
it 'should add work package with partial attributes override' do

Loading…
Cancel
Save