Handle add_users with a separate service with CTE

pull/8324/head
Oliver Günther 5 years ago
parent c2e2640d92
commit d93ff52a27
No known key found for this signature in database
GPG Key ID: A3A8BDAD7C0C552C
  1. 44
      app/contracts/groups/base_contract.rb
  2. 17
      app/controllers/groups_controller.rb
  3. 32
      app/models/group.rb
  4. 32
      app/models/group_user.rb
  5. 117
      app/services/groups/add_users_service.rb
  6. 9
      db/migrate/20200428105404_unique_member_role.rb

@ -0,0 +1,44 @@
#-- 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.
#++
module Groups
class BaseContract < ::ModelContract
def validate
user_allowed_to_manage
super
end
def user_allowed_to_manage
unless user.admin?
errors.add :base, :error_unauthorized
end
end
end
end

@ -34,9 +34,8 @@ class GroupsController < ApplicationController
helper_method :gon
before_action :require_admin
before_action :find_group, only: [:destroy,
:show, :create_memberships, :destroy_membership,
:edit_membership]
before_action :find_group, only: %i[destroy show create_memberships destroy_membership
edit_membership add_users]
# GET /groups
# GET /groups.xml
@ -123,11 +122,15 @@ class GroupsController < ApplicationController
end
def add_users
@group = Group.includes(:users).find(params[:id])
@users = User.includes(:memberships).where(id: params[:user_ids])
@group.users << @users
call = @group
.add_members!(User.where(id: params[:user_ids]).pluck(:id))
if call.success?
flash[:notice] = I18n.t(:notice_successful_update)
else
call.apply_flash_message!(flash)
end
I18n.t :notice_successful_update
redirect_to controller: '/groups', action: 'edit', id: @group, tab: 'users'
end

@ -30,7 +30,6 @@
class Group < Principal
has_and_belongs_to_many :users,
join_table: "#{table_name_prefix}group_users#{table_name_suffix}",
after_add: :user_added,
after_remove: :user_removed
acts_as_customizable
@ -62,31 +61,6 @@ class Group < Principal
alias :name :to_s
def user_added(user)
members.each do |member|
next if member.project.nil?
user_member = Member.find_by(project_id: member.project_id, user_id: user.id)
if user_member.nil?
user_member = Member.new.tap do |m|
m.project_id = member.project_id
m.user_id = user.id
end
member.member_roles.each do |member_role|
user_member.add_role(member_role.role, member_role.id)
end
user_member.save!
else
member.member_roles.each do |member_role|
user_member.add_and_save_role(member_role.role, member_role.id)
end
end
end
end
def user_removed(user)
member_roles = MemberRole
.includes(member: :member_roles)
@ -105,8 +79,10 @@ class Group < Principal
# adds group members
# meaning users that are members of the group
def add_member!(users)
self.users << users
def add_members!(user_ids)
::Groups::AddUsersService
.new(self, current_user: User.current)
.call(user_ids)
end
private

@ -0,0 +1,32 @@
#-- 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 GroupUser < ApplicationRecord
self.table_name = "#{table_name_prefix}group_users#{table_name_suffix}"
end

@ -0,0 +1,117 @@
#-- 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.
#++
module Groups
class AddUsersService < ::BaseServices::BaseContracted
attr_reader :group
def initialize(group, current_user:)
@group = group
super user: current_user,
contract_class: BaseContract
end
def after_validate(user_ids, _call)
::Group.transaction do
add_to_user_and_projects(user_ids)
end
end
def model
group
end
private
##
# Add users as the same members to the projects
# the group is a member of
def add_to_user_and_projects(user_ids)
exec_query!(user_ids)
ServiceResult.new success: true, result: group
rescue StandardError => e
Rails.logger.error { "Failed to add users to group #{group.id}: #{e} #{e.message}" }
ServiceResult.new(success: false, message: I18n.t(:notice_internal_server_error, app_title: Setting.app_title))
end
def exec_query!(user_ids)
sql_query = ::OpenProject::SqlSanitization
.sanitize add_to_user_and_projects_cte, group_id: group.id, user_ids: user_ids
::Group.connection.exec_query(sql_query)
end
def add_to_user_and_projects_cte
<<~SQL
-- select existing users from given IDs
WITH found_users AS (
SELECT id as user_id FROM #{User.table_name} WHERE id IN (:user_ids)
),
-- select existing memberships of the group
group_memberships AS (
SELECT project_id, user_id FROM #{Member.table_name} WHERE user_id = :group_id
),
-- select existing member_roles of the group
group_roles AS (
SELECT members.project_id AS project_id,
members.user_id AS user_id,
members.id AS member_id,
member_roles.role_id AS role_id
FROM #{MemberRole.table_name} member_roles
JOIN #{Member.table_name} members
ON members.id = member_roles.member_id AND members.user_id = :group_id
),
-- insert into group_users association
new_group_users AS (
INSERT INTO group_users (group_id, user_id)
SELECT :group_id as group_id, user_id FROM found_users
ON CONFLICT DO NOTHING
),
-- insert the group user into members
new_members AS (
INSERT INTO #{Member.table_name} (project_id, user_id)
SELECT group_memberships.project_id, found_users.user_id
FROM found_users, group_memberships
-- We need to return all members for the given group memberships
-- even if they already exist as members (e.g., added individually) to ensure we add all roles
-- to mark that we reset the created_at date since replacing the member
ON CONFLICT(project_id, user_id) DO UPDATE SET created_on = CURRENT_TIMESTAMP
RETURNING id, user_id
)
-- copy the member roles of the group
INSERT INTO #{MemberRole.table_name} (member_id, role_id, inherited_from)
SELECT new_members.id, group_roles.role_id, group_roles.member_id
FROM group_roles, new_members
-- Ignore if the role was already inserted by us
ON CONFLICT DO NOTHING
SQL
end
end
end

@ -0,0 +1,9 @@
class UniqueMemberRole < ActiveRecord::Migration[6.0]
def change
change_table :member_roles do |t|
t.index %i[member_id role_id inherited_from],
name: 'unique_inherited_role',
unique: true
end
end
end
Loading…
Cancel
Save