summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/groups_controller.rb162
-rw-r--r--app/controllers/members_controller.rb16
-rw-r--r--app/controllers/users_controller.rb15
-rw-r--r--app/helpers/application_helper.rb14
-rw-r--r--app/helpers/custom_fields_helper.rb3
-rw-r--r--app/helpers/groups_helper.rb34
-rw-r--r--app/helpers/users_helper.rb1
-rw-r--r--app/models/group.rb48
-rw-r--r--app/models/group_custom_field.rb22
-rw-r--r--app/models/member.rb38
-rw-r--r--app/models/member_role.rb27
-rw-r--r--app/models/principal.rb38
-rw-r--r--app/models/project.rb7
-rw-r--r--app/models/user.rb15
-rw-r--r--app/views/admin/index.rhtml5
-rw-r--r--app/views/groups/_form.html.erb8
-rw-r--r--app/views/groups/_general.html.erb4
-rw-r--r--app/views/groups/_memberships.html.erb56
-rw-r--r--app/views/groups/_users.html.erb49
-rw-r--r--app/views/groups/autocomplete_for_user.html.erb1
-rw-r--r--app/views/groups/edit.html.erb23
-rw-r--r--app/views/groups/index.html.erb25
-rw-r--r--app/views/groups/new.html.erb8
-rw-r--r--app/views/groups/show.html.erb7
-rw-r--r--app/views/members/autocomplete_for_member.rhtml1
-rw-r--r--app/views/members/autocomplete_for_member_login.rhtml5
-rw-r--r--app/views/projects/settings/_members.rhtml43
-rw-r--r--app/views/users/_groups.rhtml9
-rw-r--r--app/views/users/_memberships.rhtml8
29 files changed, 627 insertions, 65 deletions
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
new file mode 100644
index 000000000..54b5d1b97
--- /dev/null
+++ b/app/controllers/groups_controller.rb
@@ -0,0 +1,162 @@
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# 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.
+
+class GroupsController < ApplicationController
+ layout 'base'
+ before_filter :require_admin
+
+ helper :custom_fields
+
+ # GET /groups
+ # GET /groups.xml
+ def index
+ @groups = Group.find(:all, :order => 'lastname')
+
+ respond_to do |format|
+ format.html # index.html.erb
+ format.xml { render :xml => @groups }
+ end
+ end
+
+ # GET /groups/1
+ # GET /groups/1.xml
+ def show
+ @group = Group.find(params[:id])
+
+ respond_to do |format|
+ format.html # show.html.erb
+ format.xml { render :xml => @group }
+ end
+ end
+
+ # GET /groups/new
+ # GET /groups/new.xml
+ def new
+ @group = Group.new
+
+ respond_to do |format|
+ format.html # new.html.erb
+ format.xml { render :xml => @group }
+ end
+ end
+
+ # GET /groups/1/edit
+ def edit
+ @group = Group.find(params[:id])
+ end
+
+ # POST /groups
+ # POST /groups.xml
+ def create
+ @group = Group.new(params[:group])
+
+ respond_to do |format|
+ if @group.save
+ flash[:notice] = l(:notice_successful_create)
+ format.html { redirect_to(groups_path) }
+ format.xml { render :xml => @group, :status => :created, :location => @group }
+ else
+ format.html { render :action => "new" }
+ format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ # PUT /groups/1
+ # PUT /groups/1.xml
+ def update
+ @group = Group.find(params[:id])
+
+ respond_to do |format|
+ if @group.update_attributes(params[:group])
+ flash[:notice] = l(:notice_successful_update)
+ format.html { redirect_to(groups_path) }
+ format.xml { head :ok }
+ else
+ format.html { render :action => "edit" }
+ format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ # DELETE /groups/1
+ # DELETE /groups/1.xml
+ def destroy
+ @group = Group.find(params[:id])
+ @group.destroy
+
+ respond_to do |format|
+ format.html { redirect_to(groups_url) }
+ format.xml { head :ok }
+ end
+ end
+
+ def add_users
+ @group = Group.find(params[:id])
+ users = User.find_all_by_id(params[:user_ids])
+ @group.users << users if request.post?
+ respond_to do |format|
+ format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
+ format.js {
+ render(:update) {|page|
+ page.replace_html "tab-content-users", :partial => 'groups/users'
+ users.each {|user| page.visual_effect(:highlight, "user-#{user.id}") }
+ }
+ }
+ end
+ end
+
+ def remove_user
+ @group = Group.find(params[:id])
+ @group.users.delete(User.find(params[:user_id])) if request.post?
+ respond_to do |format|
+ format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
+ format.js { render(:update) {|page| page.replace_html "tab-content-users", :partial => 'groups/users'} }
+ end
+ end
+
+ def autocomplete_for_user
+ @group = Group.find(params[:id])
+ @users = User.active.like(params[:q]).find(:all, :limit => 100) - @group.users
+ render :layout => false
+ end
+
+ def edit_membership
+ @group = Group.find(params[:id])
+ @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:principal => @group)
+ @membership.attributes = params[:membership]
+ @membership.save if request.post?
+ respond_to do |format|
+ format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
+ format.js {
+ render(:update) {|page|
+ page.replace_html "tab-content-memberships", :partial => 'groups/memberships'
+ page.visual_effect(:highlight, "member-#{@membership.id}")
+ }
+ }
+ end
+ end
+
+ def destroy_membership
+ @group = Group.find(params[:id])
+ Member.find(params[:membership_id]).destroy if request.post?
+ respond_to do |format|
+ format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
+ format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'groups/memberships'} }
+ end
+ end
+end
diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb
index eb5989525..3bfa606e1 100644
--- a/app/controllers/members_controller.rb
+++ b/app/controllers/members_controller.rb
@@ -16,8 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MembersController < ApplicationController
- before_filter :find_member, :except => [:new, :autocomplete_for_member_login]
- before_filter :find_project, :only => [:new, :autocomplete_for_member_login]
+ before_filter :find_member, :except => [:new, :autocomplete_for_member]
+ before_filter :find_project, :only => [:new, :autocomplete_for_member]
before_filter :authorize
def new
@@ -59,17 +59,17 @@ class MembersController < ApplicationController
end
def destroy
- @member.destroy
- respond_to do |format|
+ if request.post? && @member.deletable?
+ @member.destroy
+ end
+ respond_to do |format|
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
end
end
- def autocomplete_for_member_login
- @users = User.active.find(:all, :conditions => ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", "#{params[:user]}%", "#{params[:user]}%", "#{params[:user]}%"],
- :limit => 10,
- :order => 'login ASC') - @project.users
+ def autocomplete_for_member
+ @principals = Principal.active.like(params[:q]).find(:all, :limit => 100) - @project.principals
render :layout => false
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index e9fdfaf79..0abda0cd0 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -63,7 +63,7 @@ class UsersController < ApplicationController
if @user.save
Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
flash[:notice] = l(:notice_successful_create)
- redirect_to :action => 'list'
+ redirect_to :controller => 'users', :action => 'edit', :id => @user
end
end
@auth_sources = AuthSource.find(:all)
@@ -75,6 +75,7 @@ class UsersController < ApplicationController
@user.admin = params[:user][:admin] if params[:user][:admin]
@user.login = params[:user][:login] if params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
+ @user.group_ids = params[:user][:group_ids] if params[:user][:group_ids]
@user.attributes = params[:user]
# Was the account actived ? (do it before User#save clears the change)
was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
@@ -85,17 +86,18 @@ class UsersController < ApplicationController
Mailer.deliver_account_information(@user, params[:password])
end
flash[:notice] = l(:notice_successful_update)
- # Give a string to redirect_to otherwise it would use status param as the response code
- redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
+ redirect_to :back
end
end
@auth_sources = AuthSource.find(:all)
@membership ||= Member.new
+ rescue ::ActionController::RedirectBackError
+ redirect_to :controller => 'users', :action => 'edit', :id => @user
end
def edit_membership
@user = User.find(params[:id])
- @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user)
+ @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:principal => @user)
@membership.attributes = params[:membership]
@membership.save if request.post?
respond_to do |format|
@@ -111,7 +113,10 @@ class UsersController < ApplicationController
def destroy_membership
@user = User.find(params[:id])
- Member.find(params[:membership_id]).destroy if request.post?
+ @membership = Member.find(params[:membership_id])
+ if request.post? && @membership.deletable?
+ @membership.destroy
+ end
respond_to do |format|
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index f1b088c4a..581ea2fe8 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -46,7 +46,11 @@ module ApplicationHelper
# Display a link to user's account page
def link_to_user(user, options={})
- (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
+ if user.is_a?(User)
+ !user.anonymous? ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
+ else
+ user.to_s
+ end
end
def link_to_issue(issue, options={})
@@ -190,6 +194,14 @@ module ApplicationHelper
end
s
end
+
+ def principals_check_box_tags(name, principals)
+ s = ''
+ principals.each do |principal|
+ s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
+ end
+ s
+ end
# Truncates and returns the string as a single line
def truncate_single_line(string, *args)
diff --git a/app/helpers/custom_fields_helper.rb b/app/helpers/custom_fields_helper.rb
index a8778a6cf..eb39b4c0a 100644
--- a/app/helpers/custom_fields_helper.rb
+++ b/app/helpers/custom_fields_helper.rb
@@ -21,7 +21,8 @@ module CustomFieldsHelper
tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural},
{:name => 'TimeEntryCustomField', :label => :label_spent_time},
{:name => 'ProjectCustomField', :label => :label_project_plural},
- {:name => 'UserCustomField', :label => :label_user_plural}
+ {:name => 'UserCustomField', :label => :label_user_plural},
+ {:name => 'GroupCustomField', :label => :label_group_plural}
]
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
new file mode 100644
index 000000000..f139a7260
--- /dev/null
+++ b/app/helpers/groups_helper.rb
@@ -0,0 +1,34 @@
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# 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.
+
+module GroupsHelper
+ # Options for the new membership projects combo-box
+ def options_for_membership_project_select(user, projects)
+ options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
+ options << project_tree_options_for_select(projects) do |p|
+ {:disabled => (user.projects.include?(p))}
+ end
+ options
+ end
+
+ def group_settings_tabs
+ tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
+ {:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
+ {:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
+ ]
+ end
+end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 5b8ecaf8c..9581bbdc1 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -47,6 +47,7 @@ module UsersHelper
def user_settings_tabs
tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
+ {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural},
{:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
]
end
diff --git a/app/models/group.rb b/app/models/group.rb
new file mode 100644
index 000000000..80e096be8
--- /dev/null
+++ b/app/models/group.rb
@@ -0,0 +1,48 @@
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# 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.
+
+class Group < Principal
+ has_and_belongs_to_many :users, :after_add => :user_added,
+ :after_remove => :user_removed
+
+ acts_as_customizable
+
+ validates_presence_of :lastname
+ validates_uniqueness_of :lastname, :case_sensitive => false
+ validates_length_of :lastname, :maximum => 30
+
+ def to_s
+ lastname.to_s
+ end
+
+ def user_added(user)
+ members.each do |member|
+ user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
+ member.member_roles.each do |member_role|
+ user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
+ end
+ user_member.save!
+ end
+ end
+
+ def user_removed(user)
+ members.each do |member|
+ MemberRole.find(:all, :include => :member,
+ :conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
+ end
+ end
+end
diff --git a/app/models/group_custom_field.rb b/app/models/group_custom_field.rb
new file mode 100644
index 000000000..b7c199cbf
--- /dev/null
+++ b/app/models/group_custom_field.rb
@@ -0,0 +1,22 @@
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# 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.
+
+class GroupCustomField < CustomField
+ def type_name
+ :label_group_plural
+ end
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index 2dc91cba7..6fffb2161 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -17,40 +17,50 @@
class Member < ActiveRecord::Base
belongs_to :user
- has_many :member_roles, :dependent => :delete_all
+ belongs_to :principal, :foreign_key => 'user_id'
+ has_many :member_roles, :dependent => :destroy
has_many :roles, :through => :member_roles
belongs_to :project
- validates_presence_of :user, :project
+ validates_presence_of :principal, :project
validates_uniqueness_of :user_id, :scope => :project_id
def name
self.user.name
end
- # Sets user by login
- def user_login=(login)
- login = login.to_s
- unless login.blank?
- if (u = User.find_by_login(login))
- self.user = u
- end
- end
+ alias :base_role_ids= :role_ids=
+ def role_ids=(arg)
+ ids = (arg || []).collect(&:to_i) - [0]
+ # Keep inherited roles
+ ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)
+
+ new_role_ids = ids - role_ids
+ # Add new roles
+ new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id) }
+ # Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
+ member_roles.select {|mr| !ids.include?(mr.role_id)}.each(&:destroy)
end
def <=>(member)
a, b = roles.sort.first, member.roles.sort.first
- a == b ? (user <=> member.user) : (a <=> b)
+ a == b ? (principal <=> member.principal) : (a <=> b)
+ end
+
+ def deletable?
+ member_roles.detect {|mr| mr.inherited_from}.nil?
end
def before_destroy
- # remove category based auto assignments for this member
- IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
+ if user
+ # remove category based auto assignments for this member
+ IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
+ end
end
protected
def validate
- errors.add_to_base "Role can't be blank" if roles.empty?
+ errors.add_to_base "Role can't be blank" if member_roles.empty? && roles.empty?
end
end
diff --git a/app/models/member_role.rb b/app/models/member_role.rb
index 46777cd1e..5a31c17c5 100644
--- a/app/models/member_role.rb
+++ b/app/models/member_role.rb
@@ -19,9 +19,36 @@ class MemberRole < ActiveRecord::Base
belongs_to :member
belongs_to :role
+ after_destroy :remove_member_if_empty
+
+ after_create :add_role_to_group_users
+ after_destroy :remove_role_from_group_users
+
validates_presence_of :role
def validate
errors.add :role_id, :invalid if role && !role.member?
end
+
+ private
+
+ def remove_member_if_empty
+ if member.roles.empty?
+ member.destroy
+ end
+ end
+
+ def add_role_to_group_users
+ if member.principal.is_a?(Group)
+ member.principal.users.each do |user|
+ user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
+ user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
+ user_member.save!
+ end
+ end
+ end
+
+ def remove_role_from_group_users
+ MemberRole.find(:all, :conditions => { :inherited_from => id }).each(&:destroy)
+ end
end
diff --git a/app/models/principal.rb b/app/models/principal.rb
new file mode 100644
index 000000000..a4a946da5
--- /dev/null
+++ b/app/models/principal.rb
@@ -0,0 +1,38 @@
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
+#
+# 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.
+
+class Principal < ActiveRecord::Base
+ set_table_name 'users'
+
+ has_many :members, :foreign_key => 'user_id', :dependent => :destroy
+ has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
+ has_many :projects, :through => :memberships
+
+ # Groups and active users
+ named_scope :active, :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status = 1)"
+
+ named_scope :like, lambda {|q|
+ s = "%#{q.to_s.strip.downcase}%"
+ {:conditions => ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", s, s, s],
+ :order => 'type, login, lastname, firstname'
+ }
+ }
+
+ def <=>(principal)
+ self.to_s.downcase <=> principal.to_s.downcase
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 0b0c47a24..a6b1ee482 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -20,8 +20,13 @@ class Project < ActiveRecord::Base
STATUS_ACTIVE = 1
STATUS_ARCHIVED = 9
- has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
+ has_many :members, :include => :user, :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
+ has_many :member_principals, :class_name => 'Member',
+ :include => :principal,
+ :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
has_many :users, :through => :members
+ has_many :principals, :through => :member_principals, :source => :principal
+
has_many :enabled_modules, :dependent => :delete_all
has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
diff --git a/app/models/user.rb b/app/models/user.rb
index 0caaf34f6..6922cb51a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,5 +1,5 @@
-# redMine - project management software
-# Copyright (C) 2006-2007 Jean-Philippe Lang
+# Redmine - project management software
+# Copyright (C) 2006-2009 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -17,7 +17,7 @@
require "digest/sha1"
-class User < ActiveRecord::Base
+class User < Principal
# Account statuses
STATUS_ANONYMOUS = 0
@@ -33,9 +33,8 @@ class User < ActiveRecord::Base
:username => '#{login}'
}
- has_many :memberships, :class_name => 'Member', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
- has_many :members, :dependent => :delete_all
- has_many :projects, :through => :memberships
+ has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
+ :after_remove => Proc.new {|user, group| group.user_removed(user)}
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
has_many :changesets, :dependent => :nullify
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
@@ -50,7 +49,7 @@ class User < ActiveRecord::Base
attr_accessor :password, :password_confirmation
attr_accessor :last_before_login_on
# Prevents unauthorized assignments
- attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
+ attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
@@ -317,7 +316,7 @@ class User < ActiveRecord::Base
end
private
-
+
# Return password digest
def self.hash_password(clear_password)
Digest::SHA1.hexdigest(clear_password || "")
diff --git a/app/views/admin/index.rhtml b/app/views/admin/index.rhtml
index 323641744..1a0684a13 100644
--- a/app/views/admin/index.rhtml
+++ b/app/views/admin/index.rhtml
@@ -12,6 +12,11 @@
<%= link_to l(:label_new), :controller => 'users', :action => 'add' %>
</p>
+<p class="icon22 icon22-groups">
+<%= link_to l(:label_group_plural), :controller => 'groups' %> |
+<%= link_to l(:label_new), :controller => 'groups', :action => 'new' %>
+</p>
+
<p class="icon22 icon22-role">
<%= link_to l(:label_role_and_permissions), :controller => 'roles' %>
</p>
diff --git a/app/views/groups/_form.html.erb b/app/views/groups/_form.html.erb
new file mode 100644
index 000000000..433abdab4
--- /dev/null
+++ b/app/views/groups/_form.html.erb
@@ -0,0 +1,8 @@
+<%= error_messages_for :group %>
+
+<div class="box tabular">
+ <p><%= f.text_field :lastname, :label => :field_name %></p>
+ <% @group.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :group, value %></p>
+ <% end %>
+</div>
diff --git a/app/views/groups/_general.html.erb b/app/views/groups/_general.html.erb
new file mode 100644
index 000000000..19c6f8880
--- /dev/null
+++ b/app/views/groups/_general.html.erb
@@ -0,0 +1,4 @@
+<% labelled_tabular_form_for :group, @group, :url => { :controller => 'group', :action => 'update', :tab => nil } do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_save) %>
+<% end %>
diff --git a/app/views/groups/_memberships.html.erb b/app/views/groups/_memberships.html.erb
new file mode 100644
index 000000000..613f05e2d
--- /dev/null
+++ b/app/views/groups/_memberships.html.erb
@@ -0,0 +1,56 @@
+<% roles = Role.find_all_givable %>
+<% projects = Project.active.find(:all, :order => 'lft') %>
+
+<div class="splitcontentleft">
+<% if @group.memberships.any? %>
+<table class="list memberships">
+ <thead>
+ <th><%= l(:label_project) %></th>
+ <th><%= l(:label_role_plural) %></th>
+ <th style="width:15%"></th>
+ </thead>
+ <tbody>
+ <% @group.memberships.each do |membership| %>
+ <% next if membership.new_record? %>
+ <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
+ <td class="project"><%=h membership.project %></td>
+ <td class="roles">
+ <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
+ <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @group, :membership_id => membership },
+ :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
+ <p><% roles.each do |role| %>
+ <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
+ <% end %></p>
+ <p><%= submit_tag l(:button_change) %>
+ <%= link_to_function l(:button_cancel), "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;" %></p>
+ <% end %>
+ </td>
+ <td class="buttons">
+ <%= link_to_function l(:button_edit), "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
+ <%= link_to_remote l(:button_delete), { :url => { :controller => 'groups', :action => 'destroy_membership', :id => @group, :membership_id => membership },
+ :method => :post },
+ :class => 'icon icon-del' %>
+ </td>
+ </tr>
+ </tbody>
+<% end; reset_cycle %>
+</table>
+<% else %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+</div>
+
+<div class="splitcontentright">
+<% if projects.any? %>
+<fieldset><legend><%=l(:label_project_new)%></legend>
+<% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @group }) do %>
+<%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %>
+<p><%= l(:label_role_plural) %>:
+<% roles.each do |role| %>
+ <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
+<% end %></p>
+<p><%= submit_tag l(:button_add) %></p>
+<% end %>
+</fieldset>
+<% end %>
+</div>
diff --git a/app/views/groups/_users.html.erb b/app/views/groups/_users.html.erb
new file mode 100644
index 000000000..e471f52c7
--- /dev/null
+++ b/app/views/groups/_users.html.erb
@@ -0,0 +1,49 @@
+<div class="splitcontentleft">
+<% if @group.users.any? %>
+ <table class="list users">
+ <thead>
+ <th><%= l(:label_user) %></th>
+ <th style="width:15%"></th>
+ </thead>
+ <tbody>
+ <% @group.users.sort.each do |user| %>
+ <tr id="user-<%= user.id %>" class="<%= cycle 'odd', 'even' %>">
+ <td class="user"><%= link_to_user user %></td>
+ <td class="buttons">
+ <%= link_to_remote l(:button_delete), { :url => { :controller => 'groups', :action => 'remove_user', :id => @group, :user_id => user },
+ :method => :post },
+ :class => 'icon icon-del' %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+ </table>
+<% else %>
+ <p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+</div>
+
+<div class="splitcontentright">
+<% users = User.active.find(:all, :limit => 100) - @group.users %>
+<% if users.any? %>
+ <% remote_form_for(:group, @group, :url => {:controller => 'groups', :action => 'add_users', :id => @group}, :method => :post) do |f| %>
+ <fieldset><legend><%=l(:label_user_new)%></legend>
+
+ <p><%= text_field_tag 'user_search', nil, :size => "40" %></p>
+ <%= observe_field(:user_search,
+ :frequency => 0.5,
+ :update => :users,
+ :url => { :controller => 'groups', :action => 'autocomplete_for_user', :id => @group },
+ :with => 'q')
+ %>
+
+ <div id="users">
+ <%= principals_check_box_tags 'user_ids[]', users %>
+ </div>
+
+ <p><%= submit_tag l(:button_add) %></p>
+ </fieldset>
+ <% end %>
+<% end %>
+
+</div>
diff --git a/app/views/groups/autocomplete_for_user.html.erb b/app/views/groups/autocomplete_for_user.html.erb
new file mode 100644
index 000000000..de1b0073a
--- /dev/null
+++ b/app/views/groups/autocomplete_for_user.html.erb
@@ -0,0 +1 @@
+<%= principals_check_box_tags 'user_ids[]', @users %>
diff --git a/app/views/groups/edit.html.erb b/app/views/groups/edit.html.erb
new file mode 100644
index 000000000..93e890887
--- /dev/null
+++ b/app/views/groups/edit.html.erb
@@ -0,0 +1,23 @@
+<h2><%= link_to l(:label_group_plural), groups_path %> &#187; <%= h(@group) %></h2>
+
+<% selected_tab = params[:tab] ? params[:tab].to_s : group_settings_tabs.first[:name] %>
+
+<div class="tabs">
+<ul>
+<% group_settings_tabs.each do |tab| -%>
+ <li><%= link_to l(tab[:label]), { :tab => tab[:name] },
+ :id => "tab-#{tab[:name]}",
+ :class => (tab[:name] != selected_tab ? nil : 'selected'),
+ :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li>
+<% end -%>
+</ul>
+</div>
+
+<% group_settings_tabs.each do |tab| -%>
+<%= content_tag('div', render(:partial => tab[:partial]),
+ :id => "tab-content-#{tab[:name]}",
+ :style => (tab[:name] != selected_tab ? 'display:none' : nil),
+ :class => 'tab-content') %>
+<% end -%>
+
+<% html_title(l(:label_group), @group, l(:label_administration)) -%>
diff --git a/app/views/groups/index.html.erb b/app/views/groups/index.html.erb
new file mode 100644
index 000000000..48b9ab42d
--- /dev/null
+++ b/app/views/groups/index.html.erb
@@ -0,0 +1,25 @@
+<div class="contextual">
+<%= link_to l(:label_group_new), new_group_path, :class => 'icon icon-add' %>
+</div>
+
+<h2><%= l(:label_group_plural) %></h2>
+
+<% if @groups.any? %>
+<table class="list groups">
+ <thead><tr>
+ <th><%=l(:label_group)%></th>
+ <th><%=l(:label_user_plural)%></th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+<% @groups.each do |group| %>
+ <tr class="<%= cycle 'odd', 'even' %>">
+ <td><%= link_to h(group), :action => 'edit', :id => group %></td>
+ <td align="center"><%= group.users.size %></td>
+ <td class="buttons"><%= link_to l(:button_delete), group, :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %></td>
+ </tr>
+<% end %>
+</table>
+<% else %>
+<p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
diff --git a/app/views/groups/new.html.erb b/app/views/groups/new.html.erb
new file mode 100644
index 000000000..3c8d2ea30
--- /dev/null
+++ b/app/views/groups/new.html.erb
@@ -0,0 +1,8 @@
+<h2><%= link_to l(:label_group_plural), groups_path %> &#187; <%= l(:label_group_new) %></h2>
+
+<%= error_messages_for :group %>
+
+<% form_for(@group, :builder => TabularFormBuilder, :lang => current_language) do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<p><%= f.submit l(:button_create) %></p>
+<% end %>
diff --git a/app/views/groups/show.html.erb b/app/views/groups/show.html.erb
new file mode 100644
index 000000000..02927ebd8
--- /dev/null
+++ b/app/views/groups/show.html.erb
@@ -0,0 +1,7 @@
+<h2><%= link_to l(:label_group_plural), groups_path %> &#187; <%=h @group %></h2>
+
+<ul>
+<% @group.users.each do |user| %>
+ <li><%=h user %></li>
+<% end %>
+</ul>
diff --git a/app/views/members/autocomplete_for_member.rhtml b/app/views/members/autocomplete_for_member.rhtml
new file mode 100644
index 000000000..96b4c973b
--- /dev/null
+++ b/app/views/members/autocomplete_for_member.rhtml
@@ -0,0 +1 @@
+<%= principals_check_box_tags 'member[user_ids][]', @principals %> \ No newline at end of file
diff --git a/app/views/members/autocomplete_for_member_login.rhtml b/app/views/members/autocomplete_for_member_login.rhtml
deleted file mode 100644
index 09a08bf95..000000000
--- a/app/views/members/autocomplete_for_member_login.rhtml
+++ /dev/null
@@ -1,5 +0,0 @@
-<ul>
-<% @users.each do |user| -%>
- <li><%= h user.login %><span class="informal"> (<%= h(user.name(:lastname_coma_firstname)) %>)</span></li>
-<% end -%>
-</ul>
diff --git a/app/views/projects/settings/_members.rhtml b/app/views/projects/settings/_members.rhtml
index a6bdd9504..bfea86753 100644
--- a/app/views/projects/settings/_members.rhtml
+++ b/app/views/projects/settings/_members.rhtml
@@ -1,12 +1,12 @@
<%= error_messages_for 'member' %>
<% roles = Role.find_all_givable
- members = @project.members.find(:all, :include => [:roles, :user]).sort %>
+ members = @project.member_principals.find(:all, :include => [:roles, :principal]).sort %>
<div class="splitcontentleft">
<% if members.any? %>
<table class="list members">
<thead>
- <th><%= l(:label_user) %></th>
+ <th><%= l(:label_user) %> / <%= l(:label_group) %></th>
<th><%= l(:label_role_plural) %></th>
<th style="width:15%"></th>
<%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
@@ -15,7 +15,7 @@
<% members.each do |member| %>
<% next if member.new_record? %>
<tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %> member">
- <td class="user"><%= link_to_user member.user %></td>
+ <td class="<%= member.principal.class.name.downcase %>"><%= link_to_user member.principal %></td>
<td class="roles">
<span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
<% if authorize_for('members', 'edit') %>
@@ -23,8 +23,10 @@
:method => :post,
:html => { :id => "member-#{member.id}-roles-form", :style => 'display:none;' }) do |f| %>
<p><% roles.each do |role| %>
- <label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role) %> <%=h role %></label><br />
+ <label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role),
+ :disabled => member.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
<% end %></p>
+ <%= hidden_field_tag 'member[role_ids][]', '' %>
<p><%= submit_tag l(:button_change), :class => "small" %>
<%= link_to_function l(:button_cancel), "$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;" %></p>
<% end %>
@@ -32,10 +34,10 @@
</td>
<td class="buttons">
<%= link_to_function l(:button_edit), "$('member-#{member.id}-roles').hide(); $('member-#{member.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
- <%= link_to_remote l(:button_delete), { :url => {:controller => 'members', :action => 'destroy', :id => member},
+ <%= link_to_remote(l(:button_delete), { :url => {:controller => 'members', :action => 'destroy', :id => member},
:method => :post
}, :title => l(:button_delete),
- :class => 'icon icon-del' %>
+ :class => 'icon icon-del') if member.deletable? %>
</td>
<%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
</tr>
@@ -48,27 +50,30 @@
</div>
-<% users_count = User.active.count - @project.users.count
- users = (users_count < 300) ? User.active.find(:all, :limit => 200).sort - @project.users : [] %>
+<% principals = Principal.active.find(:all, :limit => 100, :order => 'type, login, lastname ASC') - @project.principals %>
<div class="splitcontentright">
-<% if roles.any? && users_count > 0 %>
+<% if roles.any? && principals.any? %>
<% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post) do |f| %>
<fieldset><legend><%=l(:label_member_new)%></legend>
- <p><%= text_field_tag 'member[user_login]', nil, :size => "40" %></p>
- <div id="member_user_login_choices" class="autocomplete">sqd</div>
- <%= javascript_tag "new Ajax.Autocompleter('member_user_login', 'member_user_login_choices', '#{ url_for(:controller => 'members', :action => 'autocomplete_for_member_login', :id => @project) }', { minChars: 1, frequency: 0.5, paramName: 'user' });" %>
- <% unless users.empty? %>
- <div>
- <% users.each do |user| -%>
- <label><%= check_box_tag 'member[user_ids][]', user.id, false %> <%= user %></label>
- <% end -%>
- </div>
- <% end %>
+
+ <p><%= text_field_tag 'principal_search', nil, :size => "40" %></p>
+ <%= observe_field(:principal_search,
+ :frequency => 0.5,
+ :update => :principals,
+ :url => { :controller => 'members', :action => 'autocomplete_for_member', :id => @project },
+ :with => 'q')
+ %>
+
+ <div id="principals">
+ <%= principals_check_box_tags 'member[user_ids][]', principals %>
+ </div>
+
<p><%= l(:label_role_plural) %>:
<% roles.each do |role| %>
<label><%= check_box_tag 'member[role_ids][]', role.id %> <%=h role %></label>
<% end %></p>
+
<p><%= submit_tag l(:button_add) %></p>
</fieldset>
<% end %>
diff --git a/app/views/users/_groups.rhtml b/app/views/users/_groups.rhtml
new file mode 100644
index 000000000..6d0c2b3fb
--- /dev/null
+++ b/app/views/users/_groups.rhtml
@@ -0,0 +1,9 @@
+<% form_for(:user, :url => { :action => 'edit' }) do %>
+<div class="box">
+<% Group.all.each do |group| %>
+<label><%= check_box_tag 'user[group_ids][]', group.id, @user.groups.include?(group) %> <%=h group %></label><br />
+<% end %>
+<%= hidden_field_tag 'user[group_ids][]', '' %>
+</div>
+<%= submit_tag l(:button_save) %>
+<% end %>
diff --git a/app/views/users/_memberships.rhtml b/app/views/users/_memberships.rhtml
index cca982a3e..7659f1cc0 100644
--- a/app/views/users/_memberships.rhtml
+++ b/app/views/users/_memberships.rhtml
@@ -20,17 +20,19 @@
<% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user, :membership_id => membership },
:html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
<p><% roles.each do |role| %>
- <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
+ <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role),
+ :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
<% end %></p>
+ <%= hidden_field_tag 'membership[role_ids][]', '' %>
<p><%= submit_tag l(:button_change) %>
<%= link_to_function l(:button_cancel), "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;" %></p>
<% end %>
</td>
<td class="buttons">
<%= link_to_function l(:button_edit), "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
- <%= link_to_remote l(:button_delete), { :url => { :controller => 'users', :action => 'destroy_membership', :id => @user, :membership_id => membership },
+ <%= link_to_remote(l(:button_delete), { :url => { :controller => 'users', :action => 'destroy_membership', :id => @user, :membership_id => membership },
:method => :post },
- :class => 'icon icon-del' %>
+ :class => 'icon icon-del') if membership.deletable? %>
</td>
<%= call_hook(:view_users_memberships_table_row, :user => @user, :membership => membership, :roles => roles, :projects => projects )%>
</tr>