User groups feature initial commit.

git-svn-id: http://redmine.rubyforge.org/svn/branches/work@1373 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang
2008-04-28 15:10:04 +00:00
parent 4783d3d7c5
commit ced3cab7bb
41 changed files with 812 additions and 65 deletions

View File

@@ -1,5 +1,5 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
# Copyright (C) 2006-2008 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
@@ -32,16 +32,18 @@ class CustomFieldsController < ApplicationController
def new
case params[:type]
when "IssueCustomField"
@custom_field = IssueCustomField.new(params[:custom_field])
@custom_field.trackers = Tracker.find(params[:tracker_ids]) if params[:tracker_ids]
when "UserCustomField"
@custom_field = UserCustomField.new(params[:custom_field])
when "ProjectCustomField"
@custom_field = ProjectCustomField.new(params[:custom_field])
else
redirect_to :action => 'list'
return
when "IssueCustomField"
@custom_field = IssueCustomField.new(params[:custom_field])
@custom_field.trackers = Tracker.find(params[:tracker_ids]) if params[:tracker_ids]
when "UserCustomField"
@custom_field = UserCustomField.new(params[:custom_field])
when "ProjectCustomField"
@custom_field = ProjectCustomField.new(params[:custom_field])
when "GroupCustomField"
@custom_field = GroupCustomField.new(params[:custom_field])
else
render_404
return
end
if request.post? and @custom_field.save
flash[:notice] = l(:notice_successful_create)

View File

@@ -0,0 +1,114 @@
# redMine - project management software
# Copyright (C) 2008 FreeCode
#
# 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 => 'name')
@group = Group.new
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
@custom_values = GroupCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @group) }
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])
@custom_values = GroupCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @group.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
end
# POST /groups
# POST /groups.xml
def create
@group = Group.new(params[:group])
@custom_values = GroupCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @group, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
@group.custom_values = @custom_values
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])
@custom_values = GroupCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @group, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
@group.custom_values = @custom_values
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
end

View File

@@ -20,11 +20,17 @@ class MembersController < ApplicationController
before_filter :find_member, :except => :new
before_filter :find_project, :only => :new
before_filter :authorize
helper :projects
def new
@project.members << Member.new(params[:member]) if request.post?
member = Member.new(params[:member])
if params[:principal].to_s =~ %r{^(group|user)_(\d+)$}
member.principal_type, member.principal_id = $1.camelize, $2.to_i
end
@project.members << member if request.post?
respond_to do |format|
format.html { redirect_to :action => 'settings', :tab => 'members', :id => @project }
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
@@ -54,7 +60,7 @@ private
end
def find_member
@member = Member.find(params[:id])
@member = Member.find(params[:id], :conditions => 'inherited_from IS NULL')
@project = @member.project
rescue ActiveRecord::RecordNotFound
render_404

View File

@@ -42,6 +42,7 @@ class UsersController < ApplicationController
per_page_option,
params['page']
@users = User.find :all,:order => sort_clause,
:include => :group,
:conditions => conditions,
:limit => @user_pages.items_per_page,
:offset => @user_pages.current.offset
@@ -58,6 +59,7 @@ class UsersController < ApplicationController
@user.admin = params[:user][:admin] || false
@user.login = params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
@user.group_id = params[:user][:group_id]
@custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
@user.custom_values = @custom_values
if @user.save
@@ -67,6 +69,7 @@ class UsersController < ApplicationController
end
end
@auth_sources = AuthSource.find(:all)
@groups = Group.find(:all, :order => 'name')
end
def edit
@@ -77,6 +80,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_id = params[:user][:group_id]
if params[:custom_fields]
@custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) }
@user.custom_values = @custom_values
@@ -88,6 +92,7 @@ class UsersController < ApplicationController
end
end
@auth_sources = AuthSource.find(:all)
@groups = Group.find(:all, :order => 'name')
@roles = Role.find_all_givable
@projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects
@membership ||= Member.new
@@ -95,7 +100,7 @@ class UsersController < ApplicationController
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], :conditions => 'inherited_from IS NULL') : Member.new(:principal => @user)
@membership.attributes = params[:membership]
if request.post? and @membership.save
flash[:notice] = l(:notice_successful_update)
@@ -105,7 +110,7 @@ class UsersController < ApplicationController
def destroy_membership
@user = User.find(params[:id])
if request.post? and Member.find(params[:membership_id]).destroy
if request.post? and Member.find(params[:membership_id], :conditions => 'inherited_from IS NULL').destroy
flash[:notice] = l(:notice_successful_update)
end
redirect_to :action => 'edit', :id => @user and return

View File

@@ -20,7 +20,8 @@ module CustomFieldsHelper
def custom_fields_tabs
tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural},
{: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

View File

@@ -0,0 +1,19 @@
# redMine - project management software
# Copyright (C) 2008 FreeCode
#
# 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
end

View File

@@ -42,6 +42,19 @@ module ProjectsHelper
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
end
def principal_select_tag(groups, users)
options = ''
options << "<optgroup label=\"#{l(:label_group_plural)}\" class=\"groups\">" +
options_for_select(groups.collect {|g| [g.name, "group_#{g.id}"]}) +
"</optgroup>" unless groups.empty?
options << "<optgroup label=\"#{l(:label_user_plural)}\" class=\"users\">" +
options_for_select(users.collect {|u| [u.name, "user_#{u.id}"]}) +
"</optgroup>" unless users.empty?
select_tag 'principal', options
end
# Generates a gantt image
# Only defined if RMagick is avalaible
def gantt_image(events, date_from, months, zoom)

View File

@@ -0,0 +1,39 @@
# redMine - project management software
# Copyright (C) 2008 FreeCode
#
# 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 < ActiveRecord::Base
has_many :users, :dependent => :nullify
has_many :memberships, :class_name => 'Member', :as => :principal, :dependent => :destroy
has_many :members, :as => :principal,
:include => [ :project, :role ],
:conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}",
:order => "#{Project.table_name}.name"
has_many :custom_values, :dependent => :delete_all, :as => :customized
validates_presence_of :name
validates_uniqueness_of :name
validates_length_of :name, :maximum => 30
def <=>(group)
name <=> group.name
end
def to_s
name
end
end

View File

@@ -0,0 +1,22 @@
# redMine - project management software
# Copyright (C) 2008 FreeCode
#
# 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

View File

@@ -16,27 +16,52 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Member < ActiveRecord::Base
belongs_to :user
belongs_to :role
belongs_to :project
belongs_to :role
belongs_to :principal, :polymorphic => true
belongs_to :user, :foreign_key => 'principal_id'
validates_presence_of :role, :user, :project
validates_uniqueness_of :user_id, :scope => :project_id
attr_protected :inherited_from
validates_presence_of :project, :role, :principal
validates_uniqueness_of :principal_id, :scope => [:project_id, :principal_type, :inherited_from]
def validate
errors.add :role_id, :activerecord_error_invalid if role && !role.member?
end
def name
self.user.name
principal.name
end
# Groups sorted by role then users sorted by role
def <=>(member)
role == member.role ? (user <=> member.user) : (role <=> member.role)
principal_type == member.principal_type ?
(role == member.role ? principal <=> member.principal : role <=> member.role) :
(principal_type <=> member.principal_type)
end
def to_s
principal.to_s
end
def after_save
# Update memberships based on group inheritance
if principal.is_a? Group
Member.delete_all "inherited_from = #{id}"
principal.users.each do |user|
Member.create! :project => project, :role => role, :principal => user, :inherited_from => id
end
end
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]
# Remove inherited memberships
Member.delete_all "inherited_from = #{id}"
# Remove category based auto assignments for this member
if principal.is_a? User
IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project_id, principal_id]
end
end
end

View File

@@ -20,8 +20,9 @@ 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 :users, :through => :members
has_many :members, :include => :user, :conditions => "#{Member.table_name}.principal_type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
has_many :memberships, :class_name => 'Member'
has_many :users, :through => :members, :uniq => true
has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :enabled_modules, :dependent => :delete_all
has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"

View File

@@ -35,18 +35,25 @@ class User < ActiveRecord::Base
:username => '#{login}'
}
has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name", :dependent => :delete_all
has_many :memberships, :class_name => 'Member',
:as => :principal,
:include => [ :project, :role ],
:conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}",
:order => "#{Project.table_name}.name, inherited_from ASC",
:dependent => :delete_all
has_many :projects, :through => :memberships
has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
belongs_to :auth_source
belongs_to :group
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_id
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? }
@@ -71,6 +78,23 @@ class User < ActiveRecord::Base
# update hashed_password if password was set
self.hashed_password = User.hash_password(self.password) if self.password
end
def after_save
if @group_changed
# Update inherited memberships
Member.delete_all "principal_type = 'User' AND principal_id = #{id} AND inherited_from IS NOT NULL"
unless group.nil?
group.memberships.each do |m|
Member.create! :project => m.project, :role => m.role, :principal => self, :inherited_from => m.id
end
end
end
end
def group_id=(gid)
@group_changed = true unless gid == group_id
write_attribute(:group_id, gid)
end
def self.active
with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do
@@ -167,8 +191,8 @@ class User < ActiveRecord::Base
end
def notified_project_ids=(ids)
Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
Member.update_all("mail_notification = #{connection.quoted_false}", ["principal_type = 'User' AND principal_id = ?", id])
Member.update_all("mail_notification = #{connection.quoted_true}", ["principal_type = 'User' AND principal_id = ? AND project_id IN (?)", id, ids]) if ids && !ids.empty?
@notified_projects_ids = nil
notified_projects_ids
end

View File

@@ -8,6 +8,11 @@
</p>
<p class="icon22 icon22-users">
<%= link_to l(:label_group_plural), :controller => 'groups' %> |
<%= link_to l(:label_new), :controller => 'groups', :action => 'new' %>
</p>
<p class="icon22 icon22-user">
<%= link_to l(:label_user_plural), :controller => 'users' %> |
<%= link_to l(:label_new), :controller => 'users', :action => 'add' %>
</p>

View File

@@ -99,12 +99,9 @@ when "IssueCustomField" %>
<p><%= f.check_box :is_filter %></p>
<p><%= f.check_box :searchable %></p>
<% when "UserCustomField" %>
<% else %>
<p><%= f.check_box :is_required %></p>
<% when "ProjectCustomField" %>
<p><%= f.check_box :is_required %></p>
<% end %>
</div>
<%= javascript_tag "toggle_custom_field_format();" %>

View File

@@ -0,0 +1,10 @@
<div class="box tabular">
<p><%= f.text_field :name %></p>
<% for @custom_value in @custom_values %>
<p><%= custom_field_tag_with_label @custom_value %></p>
<% end %>
<% unless @group.users.empty? %>
<p><label><%= l(:label_user_plural) %> (<%= @group.users.size %>)</label><%= @group.users.collect {|u| link_to_user u }.join(', ') %></p>
<% end %>
</div>

View File

@@ -0,0 +1,8 @@
<h2><%= l(:label_group) %></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_save) %></p>
<% end %>

View File

@@ -0,0 +1,25 @@
<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="group <%= cycle 'odd', 'even' %>">
<td><%= link_to h(group.name), :action => 'edit', :id => group %></td>
<td align="center"><%= group.users.size %></td>
<td align="right"><%= 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 %>
<% form_for(@group) do |f| %>
<p><label><%= l(:label_group_new) %>: <%= f.text_field :name %> <%= f.submit l(:button_create) %></p>
<% end %>

View File

@@ -0,0 +1,8 @@
<h2><%= 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 %>

View File

@@ -0,0 +1,7 @@
<h2><%=h @group %></h2>
<ul>
<% @group.users.each do |user| %>
<li><%=h user %></li>
<% end %>
</ul>

View File

@@ -1,22 +1,23 @@
<%= error_messages_for 'member' %>
<% roles = Role.find_all_givable %>
<% users = User.find_active(:all).sort - @project.users %>
<% users = User.find_active(:all).sort - @project.users.find(:all, :conditions => 'inherited_from IS NULL') %>
<% groups = Group.find(:all, :order => 'name') - @project.memberships.collect(&:principal) %>
<% # members sorted by role position
members = @project.members.find(:all, :include => [:role, :user]).sort %>
members = @project.memberships.select {|m| m.inherited_from.nil? }.sort #members.find(:all, :include => [:role, :user]).sort %>
<% if members.any? %>
<table class="list">
<table class="list members">
<thead>
<th><%= l(:label_user) %></th>
<th><%= l(:label_member) %></th>
<th><%= l(:label_role) %></th>
<th style="width:15%"></th>
</thead>
<tbody>
<% members.each do |member| %>
<% next if member.new_record? %>
<tr class="<%= cycle 'odd', 'even' %>">
<td><%= member.name %></td>
<td align="center">
<tr class="<%= cycle 'odd', 'even' %> <%= member.principal.class.name.downcase %>">
<td class="name"><%=h member %></td>
<td class="role">
<% if authorize_for('members', 'edit') %>
<% remote_form_for(:member, member, :url => {:controller => 'members', :action => 'edit', :id => member}, :method => :post) do |f| %>
<%= f.select :role_id, roles.collect{|role| [role.name, role.id]}, {}, :class => "small" %>
@@ -38,10 +39,10 @@
<p class="nodata"><%= l(:label_no_data) %></p>
<% end %>
<% if authorize_for('members', 'new') && !users.empty? %>
<% if authorize_for('members', 'new') && !(users.empty? && groups.empty?) %>
<p><%=l(:label_member_new)%></p>
<% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post) do |f| %>
<p><label for="member_user_id"><%=l(:label_member_new)%></label><br />
<%= f.select :user_id, users.collect{|user| [user.name, user.id]} %>
<p><%= principal_select_tag(groups, users) %>
<%= l(:label_role) %>: <%= f.select :role_id, roles.collect{|role| [role.name, role.id]}, :selected => nil %>
<%= submit_tag l(:button_add) %></p>
<% end %>

View File

@@ -7,6 +7,9 @@
<p><%= f.text_field :firstname, :required => true %></p>
<p><%= f.text_field :lastname, :required => true %></p>
<p><%= f.text_field :mail, :required => true %></p>
<% unless @groups.empty? -%>
<p><%= f.select :group_id, @groups.collect {|g| [g.name, g.id]}, { :include_blank => true } %></p>
<% end -%>
<p><%= f.select :language, lang_options_for_select %></p>
<% for @custom_value in @custom_values %>

View File

@@ -1,7 +1,7 @@
<div class="box" style="margin-top: 16px;">
<h3><%= l(:label_project_plural) %></h3>
<% @user.memberships.each do |membership| %>
<% @user.memberships.select {|m| m.inherited_from.nil? }.each do |membership| %>
<% form_tag({ :action => 'edit_membership', :id => @user, :membership_id => membership }, :class => "tabular") do %>
<p style="margin:0;padding-top:0;">
<label><%= membership.project.name %></label>
@@ -13,6 +13,8 @@
</p>
<% end %>
<% end %>
<% unless @projects.empty? || @roles.empty? %>
<hr />
<p>
<label><%=l(:label_project_new)%></label><br/>
@@ -20,10 +22,12 @@
<select name="membership[project_id]">
<%= options_from_collection_for_select @projects, "id", "name", @membership.project_id %>
</select>
<%= l(:label_role) %>:
<select name="membership[role_id]">
<%= options_from_collection_for_select @roles, "id", "name", @membership.role_id %>
</select>
<%= submit_tag l(:button_add) %>
<% end %>
</p>
<% end %>
</div>

View File

@@ -18,6 +18,7 @@
<%= sort_header_tag('firstname', :caption => l(:field_firstname)) %>
<%= sort_header_tag('lastname', :caption => l(:field_lastname)) %>
<%= sort_header_tag('mail', :caption => l(:field_mail)) %>
<%= sort_header_tag("#{Group.table_name}.name", :caption => l(:field_group)) %>
<%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %>
<%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
<%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %>
@@ -27,9 +28,10 @@
<% for user in @users -%>
<tr class="user <%= cycle("odd", "even") %> <%= %w(anon active registered locked)[user.status] %>">
<td class="username"><%= link_to user.login, :action => 'edit', :id => user %></td>
<td class="firstname"><%= user.firstname %></td>
<td class="lastname"><%= user.lastname %></td>
<td class="email"><%= user.mail %></td>
<td class="firstname"><%=h user.firstname %></td>
<td class="lastname"><%=h user.lastname %></td>
<td class="email"><%=h user.mail %></td>
<td class="group"><%=h user.group %></td>
<td align="center"><%= image_tag('true.png') if user.admin? %></td>
<td class="created_on" align="center"><%= format_time(user.created_on) %></td>
<td class="last_login_on" align="center"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td>

View File

@@ -1,4 +1,6 @@
ActionController::Routing::Routes.draw do |map|
map.resources :groups
# Add your own custom routes here.
# The priority is based upon order of creation: first created -> highest priority.

View File

@@ -0,0 +1,11 @@
class CreateGroups < ActiveRecord::Migration
def self.up
create_table :groups do |t|
t.column :name, :string, :null => false
end
end
def self.down
drop_table :groups
end
end

View File

@@ -0,0 +1,9 @@
class AddUsersGroupId < ActiveRecord::Migration
def self.up
add_column :users, :group_id, :integer
end
def self.down
remove_column :users, :group_id
end
end

View File

@@ -0,0 +1,20 @@
class ChangeMembersUsersAssociationToPolymorphic < ActiveRecord::Migration
def self.up
add_column :members, :principal_type, :string
add_column :members, :principal_id, :integer
add_column :members, :inherited_from, :integer
Member.update_all "principal_type = 'User', principal_id = user_id"
remove_column :members, :user_id
add_index :members, [:principal_type, :principal_id], :name => :members_principal
end
def self.down
# Remove inherited memberships
Member.delete_all "inherited_from IS NOT NULL"
add_column :members, :user_id, :integer, :default => 0, :null => false
Member.update_all "user_id = principal_id"
remove_column :members, :principal_type, :string
remove_column :members, :principal_id, :integer
remove_column :members, :inherited_from, :integer
end
end

View File

@@ -179,6 +179,7 @@ field_time_zone: Time zone
field_searchable: Searchable
field_default_value: Default value
field_comments_sorting: Display comments
field_group: Group
setting_app_title: Application title
setting_app_subtitle: Application subtitle
@@ -510,6 +511,9 @@ label_preferences: Preferences
label_chronological_order: In chronological order
label_reverse_chronological_order: In reverse chronological order
label_planning: Planning
label_group: Group
label_group_plural: Groups
label_group_new: New group
button_login: Login
button_submit: Submit

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -105,12 +105,19 @@ tr.message td.last_message { font-size: 80%; }
tr.message.locked td.subject a { background-image: url(../images/locked.png); }
tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; }
table.members td { vertical-align: middle; }
table.members td.name { padding-left: 20px; }
table.members tr.group td.name { background: url(../images/users.png) 0 50% no-repeat; }
table.members tr.user td.name { background: url(../images/user.png) 0 50% no-repeat; }
tr.user td { width:13%; }
tr.user td.email { width:18%; }
tr.user td { white-space: nowrap; }
tr.user.locked, tr.user.registered { color: #aaa; }
tr.user.locked a, tr.user.registered a { color: #aaa; }
optgroup { font-style: normal; }
tr.time-entry { text-align: center; white-space: nowrap; }
tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
@@ -574,6 +581,7 @@ vertical-align: middle;
.icon22-projects { background-image: url(../images/22x22/projects.png); }
.icon22-users { background-image: url(../images/22x22/users.png); }
.icon22-user { background-image: url(../images/22x22/user.png); }
.icon22-tracker { background-image: url(../images/22x22/tracker.png); }
.icon22-role { background-image: url(../images/22x22/role.png); }
.icon22-workflow { background-image: url(../images/22x22/workflow.png); }

7
groups/test/fixtures/groups.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
managers:
id: 1
name: Managers
clients:
id: 2
name: Clients

View File

@@ -4,24 +4,43 @@ members_001:
project_id: 1
role_id: 1
id: 1
user_id: 2
principal_type: User
principal_id: 2
members_002:
created_on: 2006-07-19 19:35:36 +02:00
project_id: 1
role_id: 2
id: 2
user_id: 3
principal_type: User
principal_id: 3
members_003:
created_on: 2006-07-19 19:35:36 +02:00
project_id: 2
role_id: 2
id: 3
user_id: 2
principal_type: User
principal_id: 2
members_004:
id: 4
created_on: 2006-07-19 19:35:36 +02:00
project_id: 1
role_id: 2
# Locked user
user_id: 5
principal_type: User
principal_id: 5
members_005:
id: 5
created_on: 2008-01-19 19:35:36 +02:00
project_id: 1
role_id: 3
principal_type: Group
principal_id: 2 # Clients
members_006:
id: 6
created_on: 2008-01-19 19:35:36 +02:00
project_id: 1
role_id: 3
principal_type: User
principal_id: 7
inherited_from: 5

View File

@@ -1,5 +1,5 @@
---
roles_001:
manager:
name: Manager
id: 1
builtin: 0
@@ -41,7 +41,7 @@ roles_001:
- :view_changesets
position: 1
roles_002:
developer:
name: Developer
id: 2
builtin: 0
@@ -78,7 +78,7 @@ roles_002:
- :view_changesets
position: 2
roles_003:
reporter:
name: Reporter
id: 3
builtin: 0
@@ -113,7 +113,7 @@ roles_003:
- :view_changesets
position: 3
roles_004:
non_member:
name: Non member
id: 4
builtin: 1
@@ -141,7 +141,7 @@ roles_004:
- :view_changesets
position: 4
roles_005:
anonymous:
name: Anonymous
id: 5
builtin: 2

View File

@@ -96,5 +96,38 @@ users_006:
mail_notification: false
login: ''
type: AnonymousUser
client:
id: 7
created_on: 2006-07-19 19:33:19 +02:00
status: 1
last_login_on:
language: ''
hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415
updated_on: 2006-07-19 19:33:19 +02:00
admin: false
mail: 'client@foo.bar'
lastname: User
firstname: Client
auth_source_id:
mail_notification: false
login: client
type: User
group_id: 2 # Clients
new_client:
id: 8
created_on: 2006-07-19 19:33:19 +02:00
status: 1
last_login_on:
language: ''
hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415
updated_on: 2006-07-19 19:33:19 +02:00
admin: false
mail: 'newclient@foo.bar'
lastname: New
firstname: Client
auth_source_id:
mail_notification: false
login: newclient
type: User

View File

@@ -0,0 +1,76 @@
# redMine - project management software
# Copyright (C) 2008 FreeCode
#
# 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.
require File.dirname(__FILE__) + '/../test_helper'
require 'groups_controller'
# Re-raise errors caught by the controller.
class GroupsController; def rescue_action(e) raise e end; end
class GroupsControllerTest < Test::Unit::TestCase
fixtures :groups, :users
def setup
@controller = GroupsController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
User.current = nil
@request.session[:user_id] = 1 # admin
end
def test_should_get_index
get :index
assert_response :success
assert_not_nil assigns(:groups)
end
def test_should_get_new
get :new
assert_response :success
end
def test_should_create_group
assert_difference('Group.count') do
post :create, :group => { :name => 'New group' }
end
assert_redirected_to groups_path
assert_not_nil Group.find_by_name('New group')
end
def test_should_show_group
get :show, :id => 1
assert_response :success
end
def test_should_get_edit
get :edit, :id => 1
assert_response :success
end
def test_should_update_group
put :update, :id => 1, :group => { :name => 'Renamed' }
assert_redirected_to groups_path
assert_equal 'Renamed', Group.find(1).name
end
def test_should_destroy_group
assert_difference('Group.count', -1) do
delete :destroy, :id => 1
end
assert_redirected_to groups_path
end
end

View File

@@ -0,0 +1,80 @@
# Redmine - project management software
# Copyright (C) 2008 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.
require File.dirname(__FILE__) + '/../test_helper'
require 'members_controller'
# Re-raise errors caught by the controller.
class MembersController; def rescue_action(e) raise e end; end
class MembersControllerTest < Test::Unit::TestCase
fixtures :projects, :roles, :users, :groups, :members
def setup
@controller = MembersController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
User.current = nil
@request.session[:user_id] = 1 # admin
end
def test_should_create_user_member
p = Project.find(1)
u = users(:new_client)
assert_difference('Member.count') do
post :new, :id => p.id, :member => { :role_id => roles(:reporter).id }, :principal => "user_#{u.id}"
end
assert_redirected_to :controller => 'projects', :action => 'settings', :id => p, :tab => 'members'
assert u.reload.member_of?(p)
assert_equal roles(:reporter), u.role_for_project(p)
end
def test_should_create_group_member
p = Project.find(2)
assert_difference('Member.count') do
post :new, :id => p.id, :member => { :role_id => roles(:reporter) }, :principal => 'group_1'
end
assert_redirected_to :controller => 'projects', :action => 'settings', :id => p, :tab => 'members'
end
def test_should_update_user_member
u = User.find(3)
p = Project.find(1)
assert_equal roles(:developer), u.role_for_project(p)
assert_difference('Member.count', 0) do
post :edit, :id => 2, :member => { :role_id => roles(:manager).id }
end
assert_redirected_to :controller => 'projects', :action => 'settings', :id => p, :tab => 'members'
assert_equal roles(:manager), u.reload.role_for_project(p)
end
def test_should_destroy
p = Project.find(1)
assert_difference('Member.count', -1) do
post :destroy, :id => 2
end
assert_redirected_to :controller => 'projects', :action => 'settings', :id => p, :tab => 'members'
end
def test_should_not_destroy_inherited_membership
p = Project.find(1)
assert_difference('Member.count', 0) do
post :destroy, :id => 6
end
assert_response 404
end
end

View File

@@ -47,6 +47,14 @@ class UsersControllerTest < Test::Unit::TestCase
assert_nil assigns(:users).detect {|u| !u.active?}
end
def test_should_add_membership
assert_difference('User.find(2).memberships.count') do
post :edit_membership, :id => 2, :membership => { :role_id => 1, :project_id => 3 }
assert_redirected_to 'users/edit/2'
assert User.find(2).member_of?(Project.find(3))
end
end
def test_edit_membership
post :edit_membership, :id => 2, :membership_id => 1,
:membership => { :role_id => 2}

View File

@@ -0,0 +1,129 @@
# redMine - project management software
# Copyright (C) 2008 FreeCode
#
# 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.
require File.dirname(__FILE__) + '/../test_helper'
class GroupTest < ActiveSupport::TestCase
fixtures :groups, :users, :projects, :roles, :members
def test_should_validate_presence_of_name
g = Group.new(:name => '')
assert !g.save
assert_equal 1, g.errors.size
end
def test_should_validate_uniqueness_of_name
g = Group.new(:name => groups(:clients).name)
assert !g.save
assert_equal 1, g.errors.size
end
def test_should_create
g = Group.new(:name => 'New group')
assert g.save
assert g.users.empty?
end
def test_should_destroy
g = groups(:clients)
p = Project.find(1)
u = users(:client)
assert u.member_of?(p)
assert_difference('Member.count', -2) do
g.destroy
end
u.reload
assert_nil u.group
assert !u.member_of?(p)
end
def test_should_add_user_to_group
g = groups(:clients)
p = Project.find(1)
u = users(:new_client)
r = roles(:reporter)
assert !u.member_of?(p)
assert_difference('Member.count') do
assert_difference('g.reload.users.size') do
u.group_id = g.id
assert u.save
end
end
u.reload
assert u.group = g
assert u.member_of?(p)
assert_equal r, u.role_for_project(p)
end
def test_should_add_group_to_project
g = groups(:clients)
p = Project.find(2)
u = users(:client)
r = roles(:reporter)
assert !u.member_of?(p)
assert_difference('Member.count', 2) do
assert_difference('p.reload.users.size') do
m = Member.new(:project => p, :principal => g, :role => r)
assert m.save
end
end
u.reload
assert u.member_of?(p)
assert_equal r, u.role_for_project(p)
end
def test_should_remove_user_from_group
g = groups(:clients)
p = Project.find(1)
u = users(:client)
assert u.member_of?(p)
assert_difference('Member.count', -1) do
assert_difference('g.reload.users.size', -1) do
u.group_id = nil
assert u.save
end
end
end
def test_should_override_group_role
g = groups(:clients)
p = Project.find(1)
u = users(:client)
assert u.member_of?(p)
assert_equal roles(:reporter), u.role_for_project(p)
assert_difference('Member.count', 1) do
assert_difference('p.reload.users.size', 0) do
m = Member.new(:project => p, :principal => u, :role => roles(:manager))
assert m.save
end
end
assert_equal roles(:manager), u.reload.role_for_project(p)
# Remove the group, user should still be a member
assert_difference('Member.count', -2) do
assert_difference('p.reload.users.size', 0) do
assert g.destroy
end
end
assert_equal roles(:manager), u.reload.role_for_project(p)
end
end

View File

@@ -25,7 +25,7 @@ class MemberTest < Test::Unit::TestCase
end
def test_create
member = Member.new(:project_id => 1, :user_id => 4, :role_id => 1)
member = Member.new(:project_id => 1, :principal_type => 'User', :principal_id => 4, :role_id => 1)
assert member.save
end
@@ -39,7 +39,7 @@ class MemberTest < Test::Unit::TestCase
end
def test_validate
member = Member.new(:project_id => 1, :user_id => 2, :role_id =>2)
member = Member.new(:project_id => 1, :principal_type => 'User', :principal_id => 2, :role_id =>2)
# same use can't have more than one role for a project
assert !member.save
end

View File

@@ -80,10 +80,10 @@ class ProjectTest < Test::Unit::TestCase
end
def test_destroy
# 2 active members
assert_equal 2, @ecookbook.members.size
# 3 active members
assert_equal 3, @ecookbook.members.size
# and 1 is locked
assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
assert_equal 5, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
# some boards
assert @ecookbook.boards.any?