##// END OF EJS Templates
User groups branch merged....
Jean-Philippe Lang -
r2755:770745714544
parent child
Show More
@@ -0,0 +1,162
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class GroupsController < ApplicationController
19 layout 'base'
20 before_filter :require_admin
21
22 helper :custom_fields
23
24 # GET /groups
25 # GET /groups.xml
26 def index
27 @groups = Group.find(:all, :order => 'lastname')
28
29 respond_to do |format|
30 format.html # index.html.erb
31 format.xml { render :xml => @groups }
32 end
33 end
34
35 # GET /groups/1
36 # GET /groups/1.xml
37 def show
38 @group = Group.find(params[:id])
39
40 respond_to do |format|
41 format.html # show.html.erb
42 format.xml { render :xml => @group }
43 end
44 end
45
46 # GET /groups/new
47 # GET /groups/new.xml
48 def new
49 @group = Group.new
50
51 respond_to do |format|
52 format.html # new.html.erb
53 format.xml { render :xml => @group }
54 end
55 end
56
57 # GET /groups/1/edit
58 def edit
59 @group = Group.find(params[:id])
60 end
61
62 # POST /groups
63 # POST /groups.xml
64 def create
65 @group = Group.new(params[:group])
66
67 respond_to do |format|
68 if @group.save
69 flash[:notice] = l(:notice_successful_create)
70 format.html { redirect_to(groups_path) }
71 format.xml { render :xml => @group, :status => :created, :location => @group }
72 else
73 format.html { render :action => "new" }
74 format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
75 end
76 end
77 end
78
79 # PUT /groups/1
80 # PUT /groups/1.xml
81 def update
82 @group = Group.find(params[:id])
83
84 respond_to do |format|
85 if @group.update_attributes(params[:group])
86 flash[:notice] = l(:notice_successful_update)
87 format.html { redirect_to(groups_path) }
88 format.xml { head :ok }
89 else
90 format.html { render :action => "edit" }
91 format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
92 end
93 end
94 end
95
96 # DELETE /groups/1
97 # DELETE /groups/1.xml
98 def destroy
99 @group = Group.find(params[:id])
100 @group.destroy
101
102 respond_to do |format|
103 format.html { redirect_to(groups_url) }
104 format.xml { head :ok }
105 end
106 end
107
108 def add_users
109 @group = Group.find(params[:id])
110 users = User.find_all_by_id(params[:user_ids])
111 @group.users << users if request.post?
112 respond_to do |format|
113 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
114 format.js {
115 render(:update) {|page|
116 page.replace_html "tab-content-users", :partial => 'groups/users'
117 users.each {|user| page.visual_effect(:highlight, "user-#{user.id}") }
118 }
119 }
120 end
121 end
122
123 def remove_user
124 @group = Group.find(params[:id])
125 @group.users.delete(User.find(params[:user_id])) if request.post?
126 respond_to do |format|
127 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
128 format.js { render(:update) {|page| page.replace_html "tab-content-users", :partial => 'groups/users'} }
129 end
130 end
131
132 def autocomplete_for_user
133 @group = Group.find(params[:id])
134 @users = User.active.like(params[:q]).find(:all, :limit => 100) - @group.users
135 render :layout => false
136 end
137
138 def edit_membership
139 @group = Group.find(params[:id])
140 @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:principal => @group)
141 @membership.attributes = params[:membership]
142 @membership.save if request.post?
143 respond_to do |format|
144 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
145 format.js {
146 render(:update) {|page|
147 page.replace_html "tab-content-memberships", :partial => 'groups/memberships'
148 page.visual_effect(:highlight, "member-#{@membership.id}")
149 }
150 }
151 end
152 end
153
154 def destroy_membership
155 @group = Group.find(params[:id])
156 Member.find(params[:membership_id]).destroy if request.post?
157 respond_to do |format|
158 format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
159 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'groups/memberships'} }
160 end
161 end
162 end
@@ -0,0 +1,34
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module GroupsHelper
19 # Options for the new membership projects combo-box
20 def options_for_membership_project_select(user, projects)
21 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
22 options << project_tree_options_for_select(projects) do |p|
23 {:disabled => (user.projects.include?(p))}
24 end
25 options
26 end
27
28 def group_settings_tabs
29 tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
30 {:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
31 {:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
32 ]
33 end
34 end
@@ -0,0 +1,48
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class Group < Principal
19 has_and_belongs_to_many :users, :after_add => :user_added,
20 :after_remove => :user_removed
21
22 acts_as_customizable
23
24 validates_presence_of :lastname
25 validates_uniqueness_of :lastname, :case_sensitive => false
26 validates_length_of :lastname, :maximum => 30
27
28 def to_s
29 lastname.to_s
30 end
31
32 def user_added(user)
33 members.each do |member|
34 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)
35 member.member_roles.each do |member_role|
36 user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
37 end
38 user_member.save!
39 end
40 end
41
42 def user_removed(user)
43 members.each do |member|
44 MemberRole.find(:all, :include => :member,
45 :conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
46 end
47 end
48 end
@@ -0,0 +1,22
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class GroupCustomField < CustomField
19 def type_name
20 :label_group_plural
21 end
22 end
@@ -0,0 +1,38
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 class Principal < ActiveRecord::Base
19 set_table_name 'users'
20
21 has_many :members, :foreign_key => 'user_id', :dependent => :destroy
22 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"
23 has_many :projects, :through => :memberships
24
25 # Groups and active users
26 named_scope :active, :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status = 1)"
27
28 named_scope :like, lambda {|q|
29 s = "%#{q.to_s.strip.downcase}%"
30 {:conditions => ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", s, s, s],
31 :order => 'type, login, lastname, firstname'
32 }
33 }
34
35 def <=>(principal)
36 self.to_s.downcase <=> principal.to_s.downcase
37 end
38 end
@@ -0,0 +1,8
1 <%= error_messages_for :group %>
2
3 <div class="box tabular">
4 <p><%= f.text_field :lastname, :label => :field_name %></p>
5 <% @group.custom_field_values.each do |value| %>
6 <p><%= custom_field_tag_with_label :group, value %></p>
7 <% end %>
8 </div>
@@ -0,0 +1,4
1 <% labelled_tabular_form_for :group, @group, :url => { :controller => 'group', :action => 'update', :tab => nil } do |f| %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
3 <%= submit_tag l(:button_save) %>
4 <% end %>
@@ -0,0 +1,56
1 <% roles = Role.find_all_givable %>
2 <% projects = Project.active.find(:all, :order => 'lft') %>
3
4 <div class="splitcontentleft">
5 <% if @group.memberships.any? %>
6 <table class="list memberships">
7 <thead>
8 <th><%= l(:label_project) %></th>
9 <th><%= l(:label_role_plural) %></th>
10 <th style="width:15%"></th>
11 </thead>
12 <tbody>
13 <% @group.memberships.each do |membership| %>
14 <% next if membership.new_record? %>
15 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
16 <td class="project"><%=h membership.project %></td>
17 <td class="roles">
18 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
19 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @group, :membership_id => membership },
20 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
21 <p><% roles.each do |role| %>
22 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
23 <% end %></p>
24 <p><%= submit_tag l(:button_change) %>
25 <%= link_to_function l(:button_cancel), "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;" %></p>
26 <% end %>
27 </td>
28 <td class="buttons">
29 <%= link_to_function l(:button_edit), "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
30 <%= link_to_remote l(:button_delete), { :url => { :controller => 'groups', :action => 'destroy_membership', :id => @group, :membership_id => membership },
31 :method => :post },
32 :class => 'icon icon-del' %>
33 </td>
34 </tr>
35 </tbody>
36 <% end; reset_cycle %>
37 </table>
38 <% else %>
39 <p class="nodata"><%= l(:label_no_data) %></p>
40 <% end %>
41 </div>
42
43 <div class="splitcontentright">
44 <% if projects.any? %>
45 <fieldset><legend><%=l(:label_project_new)%></legend>
46 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @group }) do %>
47 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %>
48 <p><%= l(:label_role_plural) %>:
49 <% roles.each do |role| %>
50 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
51 <% end %></p>
52 <p><%= submit_tag l(:button_add) %></p>
53 <% end %>
54 </fieldset>
55 <% end %>
56 </div>
@@ -0,0 +1,49
1 <div class="splitcontentleft">
2 <% if @group.users.any? %>
3 <table class="list users">
4 <thead>
5 <th><%= l(:label_user) %></th>
6 <th style="width:15%"></th>
7 </thead>
8 <tbody>
9 <% @group.users.sort.each do |user| %>
10 <tr id="user-<%= user.id %>" class="<%= cycle 'odd', 'even' %>">
11 <td class="user"><%= link_to_user user %></td>
12 <td class="buttons">
13 <%= link_to_remote l(:button_delete), { :url => { :controller => 'groups', :action => 'remove_user', :id => @group, :user_id => user },
14 :method => :post },
15 :class => 'icon icon-del' %>
16 </td>
17 </tr>
18 <% end %>
19 </tbody>
20 </table>
21 <% else %>
22 <p class="nodata"><%= l(:label_no_data) %></p>
23 <% end %>
24 </div>
25
26 <div class="splitcontentright">
27 <% users = User.active.find(:all, :limit => 100) - @group.users %>
28 <% if users.any? %>
29 <% remote_form_for(:group, @group, :url => {:controller => 'groups', :action => 'add_users', :id => @group}, :method => :post) do |f| %>
30 <fieldset><legend><%=l(:label_user_new)%></legend>
31
32 <p><%= text_field_tag 'user_search', nil, :size => "40" %></p>
33 <%= observe_field(:user_search,
34 :frequency => 0.5,
35 :update => :users,
36 :url => { :controller => 'groups', :action => 'autocomplete_for_user', :id => @group },
37 :with => 'q')
38 %>
39
40 <div id="users">
41 <%= principals_check_box_tags 'user_ids[]', users %>
42 </div>
43
44 <p><%= submit_tag l(:button_add) %></p>
45 </fieldset>
46 <% end %>
47 <% end %>
48
49 </div>
@@ -0,0 +1,1
1 <%= principals_check_box_tags 'user_ids[]', @users %>
@@ -0,0 +1,23
1 <h2><%= link_to l(:label_group_plural), groups_path %> &#187; <%= h(@group) %></h2>
2
3 <% selected_tab = params[:tab] ? params[:tab].to_s : group_settings_tabs.first[:name] %>
4
5 <div class="tabs">
6 <ul>
7 <% group_settings_tabs.each do |tab| -%>
8 <li><%= link_to l(tab[:label]), { :tab => tab[:name] },
9 :id => "tab-#{tab[:name]}",
10 :class => (tab[:name] != selected_tab ? nil : 'selected'),
11 :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li>
12 <% end -%>
13 </ul>
14 </div>
15
16 <% group_settings_tabs.each do |tab| -%>
17 <%= content_tag('div', render(:partial => tab[:partial]),
18 :id => "tab-content-#{tab[:name]}",
19 :style => (tab[:name] != selected_tab ? 'display:none' : nil),
20 :class => 'tab-content') %>
21 <% end -%>
22
23 <% html_title(l(:label_group), @group, l(:label_administration)) -%>
@@ -0,0 +1,25
1 <div class="contextual">
2 <%= link_to l(:label_group_new), new_group_path, :class => 'icon icon-add' %>
3 </div>
4
5 <h2><%= l(:label_group_plural) %></h2>
6
7 <% if @groups.any? %>
8 <table class="list groups">
9 <thead><tr>
10 <th><%=l(:label_group)%></th>
11 <th><%=l(:label_user_plural)%></th>
12 <th></th>
13 </tr></thead>
14 <tbody>
15 <% @groups.each do |group| %>
16 <tr class="<%= cycle 'odd', 'even' %>">
17 <td><%= link_to h(group), :action => 'edit', :id => group %></td>
18 <td align="center"><%= group.users.size %></td>
19 <td class="buttons"><%= link_to l(:button_delete), group, :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %></td>
20 </tr>
21 <% end %>
22 </table>
23 <% else %>
24 <p class="nodata"><%= l(:label_no_data) %></p>
25 <% end %>
@@ -0,0 +1,8
1 <h2><%= link_to l(:label_group_plural), groups_path %> &#187; <%= l(:label_group_new) %></h2>
2
3 <%= error_messages_for :group %>
4
5 <% form_for(@group, :builder => TabularFormBuilder, :lang => current_language) do |f| %>
6 <%= render :partial => 'form', :locals => { :f => f } %>
7 <p><%= f.submit l(:button_create) %></p>
8 <% end %>
@@ -0,0 +1,7
1 <h2><%= link_to l(:label_group_plural), groups_path %> &#187; <%=h @group %></h2>
2
3 <ul>
4 <% @group.users.each do |user| %>
5 <li><%=h user %></li>
6 <% end %>
7 </ul>
@@ -0,0 +1,1
1 <%= principals_check_box_tags 'member[user_ids][]', @principals %> No newline at end of file
@@ -0,0 +1,9
1 <% form_for(:user, :url => { :action => 'edit' }) do %>
2 <div class="box">
3 <% Group.all.each do |group| %>
4 <label><%= check_box_tag 'user[group_ids][]', group.id, @user.groups.include?(group) %> <%=h group %></label><br />
5 <% end %>
6 <%= hidden_field_tag 'user[group_ids][]', '' %>
7 </div>
8 <%= submit_tag l(:button_save) %>
9 <% end %>
@@ -0,0 +1,8
1 class PopulateUsersType < ActiveRecord::Migration
2 def self.up
3 Principal.update_all("type = 'User'", "type IS NULL")
4 end
5
6 def self.down
7 end
8 end
@@ -0,0 +1,13
1 class CreateGroupsUsers < ActiveRecord::Migration
2 def self.up
3 create_table :groups_users, :id => false do |t|
4 t.column :group_id, :integer, :null => false
5 t.column :user_id, :integer, :null => false
6 end
7 add_index :groups_users, [:group_id, :user_id], :unique => true, :name => :groups_users_ids
8 end
9
10 def self.down
11 drop_table :groups_users
12 end
13 end
@@ -0,0 +1,9
1 class AddMemberRolesInheritedFrom < ActiveRecord::Migration
2 def self.up
3 add_column :member_roles, :inherited_from, :integer
4 end
5
6 def self.down
7 remove_column :member_roles, :inherited_from
8 end
9 end
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
@@ -0,0 +1,5
1 ---
2 groups_users_001:
3 group_id: 10
4 user_id: 8
5 No newline at end of file
@@ -0,0 +1,107
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'groups_controller'
20
21 # Re-raise errors caught by the controller.
22 class GroupsController; def rescue_action(e) raise e end; end
23
24 class GroupsControllerTest < Test::Unit::TestCase
25 fixtures :projects, :users, :members, :member_roles
26
27 def setup
28 @controller = GroupsController.new
29 @request = ActionController::TestRequest.new
30 @response = ActionController::TestResponse.new
31 User.current = nil
32 @request.session[:user_id] = 1
33 end
34
35 def test_index
36 get :index
37 assert_response :success
38 assert_template 'index'
39 end
40
41 def test_show
42 get :show, :id => 10
43 assert_response :success
44 assert_template 'show'
45 end
46
47 def test_new
48 get :new
49 assert_response :success
50 assert_template 'new'
51 end
52
53 def test_create
54 assert_difference 'Group.count' do
55 post :create, :group => {:lastname => 'New group'}
56 end
57 assert_redirected_to 'groups'
58 end
59
60 def test_edit
61 get :edit, :id => 10
62 assert_response :success
63 assert_template 'edit'
64 end
65
66 def test_update
67 post :update, :id => 10
68 assert_redirected_to 'groups'
69 end
70
71 def test_destroy
72 assert_difference 'Group.count', -1 do
73 post :destroy, :id => 10
74 end
75 assert_redirected_to 'groups'
76 end
77
78 def test_add_users
79 assert_difference 'Group.find(10).users.count', 2 do
80 post :add_users, :id => 10, :user_ids => ['2', '3']
81 end
82 end
83
84 def test_remove_user
85 assert_difference 'Group.find(10).users.count', -1 do
86 post :remove_user, :id => 10, :user_id => '8'
87 end
88 end
89
90 def test_new_membership
91 assert_difference 'Group.find(10).members.count' do
92 post :edit_membership, :id => 10, :membership => { :project_id => 2, :role_ids => ['1', '2']}
93 end
94 end
95
96 def test_edit_membership
97 assert_no_difference 'Group.find(10).members.count' do
98 post :edit_membership, :id => 10, :membership_id => 6, :membership => { :role_ids => ['1', '3']}
99 end
100 end
101
102 def test_destroy_membership
103 assert_difference 'Group.find(10).members.count', -1 do
104 post :destroy_membership, :id => 10, :membership_id => 6
105 end
106 end
107 end
@@ -0,0 +1,77
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../test_helper'
19
20 class GroupTest < Test::Unit::TestCase
21 fixtures :all
22
23 def test_create
24 g = Group.new(:lastname => 'New group')
25 assert g.save
26 end
27
28 def test_roles_given_to_new_user
29 group = Group.find(11)
30 user = User.find(9)
31 project = Project.first
32
33 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
34 group.users << user
35 assert user.member_of?(project)
36 end
37
38 def test_roles_given_to_existing_user
39 group = Group.find(11)
40 user = User.find(9)
41 project = Project.first
42
43 group.users << user
44 m = Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
45 assert user.member_of?(project)
46 end
47
48 def test_roles_updated
49 group = Group.find(11)
50 user = User.find(9)
51 project = Project.first
52 group.users << user
53 m = Member.create!(:principal => group, :project => project, :role_ids => [1])
54 assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort
55
56 m.role_ids = [1, 2]
57 assert_equal [1, 2], user.reload.roles_for_project(project).collect(&:id).sort
58
59 m.role_ids = [2]
60 assert_equal [2], user.reload.roles_for_project(project).collect(&:id).sort
61
62 m.role_ids = [1]
63 assert_equal [1], user.reload.roles_for_project(project).collect(&:id).sort
64 end
65
66 def test_roles_removed_when_removing_group_membership
67 assert User.find(8).member_of?(Project.find(5))
68 Member.find_by_project_id_and_user_id(5, 10).destroy
69 assert !User.find(8).member_of?(Project.find(5))
70 end
71
72 def test_roles_removed_when_removing_user_from_group
73 assert User.find(8).member_of?(Project.find(5))
74 User.find(8).groups.clear
75 assert !User.find(8).member_of?(Project.find(5))
76 end
77 end
@@ -16,8 +16,8
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MembersController < ApplicationController
18 class MembersController < ApplicationController
19 before_filter :find_member, :except => [:new, :autocomplete_for_member_login]
19 before_filter :find_member, :except => [:new, :autocomplete_for_member]
20 before_filter :find_project, :only => [:new, :autocomplete_for_member_login]
20 before_filter :find_project, :only => [:new, :autocomplete_for_member]
21 before_filter :authorize
21 before_filter :authorize
22
22
23 def new
23 def new
@@ -59,17 +59,17 class MembersController < ApplicationController
59 end
59 end
60
60
61 def destroy
61 def destroy
62 @member.destroy
62 if request.post? && @member.deletable?
63 respond_to do |format|
63 @member.destroy
64 end
65 respond_to do |format|
64 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
66 format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
65 format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
67 format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} }
66 end
68 end
67 end
69 end
68
70
69 def autocomplete_for_member_login
71 def autocomplete_for_member
70 @users = User.active.find(:all, :conditions => ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", "#{params[:user]}%", "#{params[:user]}%", "#{params[:user]}%"],
72 @principals = Principal.active.like(params[:q]).find(:all, :limit => 100) - @project.principals
71 :limit => 10,
72 :order => 'login ASC') - @project.users
73 render :layout => false
73 render :layout => false
74 end
74 end
75
75
@@ -63,7 +63,7 class UsersController < ApplicationController
63 if @user.save
63 if @user.save
64 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
64 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
65 flash[:notice] = l(:notice_successful_create)
65 flash[:notice] = l(:notice_successful_create)
66 redirect_to :action => 'list'
66 redirect_to :controller => 'users', :action => 'edit', :id => @user
67 end
67 end
68 end
68 end
69 @auth_sources = AuthSource.find(:all)
69 @auth_sources = AuthSource.find(:all)
@@ -75,6 +75,7 class UsersController < ApplicationController
75 @user.admin = params[:user][:admin] if params[:user][:admin]
75 @user.admin = params[:user][:admin] if params[:user][:admin]
76 @user.login = params[:user][:login] if params[:user][:login]
76 @user.login = params[:user][:login] if params[:user][:login]
77 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
77 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
78 @user.group_ids = params[:user][:group_ids] if params[:user][:group_ids]
78 @user.attributes = params[:user]
79 @user.attributes = params[:user]
79 # Was the account actived ? (do it before User#save clears the change)
80 # Was the account actived ? (do it before User#save clears the change)
80 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
81 was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
@@ -85,17 +86,18 class UsersController < ApplicationController
85 Mailer.deliver_account_information(@user, params[:password])
86 Mailer.deliver_account_information(@user, params[:password])
86 end
87 end
87 flash[:notice] = l(:notice_successful_update)
88 flash[:notice] = l(:notice_successful_update)
88 # Give a string to redirect_to otherwise it would use status param as the response code
89 redirect_to :back
89 redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
90 end
90 end
91 end
91 end
92 @auth_sources = AuthSource.find(:all)
92 @auth_sources = AuthSource.find(:all)
93 @membership ||= Member.new
93 @membership ||= Member.new
94 rescue ::ActionController::RedirectBackError
95 redirect_to :controller => 'users', :action => 'edit', :id => @user
94 end
96 end
95
97
96 def edit_membership
98 def edit_membership
97 @user = User.find(params[:id])
99 @user = User.find(params[:id])
98 @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user)
100 @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:principal => @user)
99 @membership.attributes = params[:membership]
101 @membership.attributes = params[:membership]
100 @membership.save if request.post?
102 @membership.save if request.post?
101 respond_to do |format|
103 respond_to do |format|
@@ -111,7 +113,10 class UsersController < ApplicationController
111
113
112 def destroy_membership
114 def destroy_membership
113 @user = User.find(params[:id])
115 @user = User.find(params[:id])
114 Member.find(params[:membership_id]).destroy if request.post?
116 @membership = Member.find(params[:membership_id])
117 if request.post? && @membership.deletable?
118 @membership.destroy
119 end
115 respond_to do |format|
120 respond_to do |format|
116 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
121 format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
117 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
122 format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
@@ -46,7 +46,11 module ApplicationHelper
46
46
47 # Display a link to user's account page
47 # Display a link to user's account page
48 def link_to_user(user, options={})
48 def link_to_user(user, options={})
49 (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
49 if user.is_a?(User)
50 !user.anonymous? ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
51 else
52 user.to_s
53 end
50 end
54 end
51
55
52 def link_to_issue(issue, options={})
56 def link_to_issue(issue, options={})
@@ -190,6 +194,14 module ApplicationHelper
190 end
194 end
191 s
195 s
192 end
196 end
197
198 def principals_check_box_tags(name, principals)
199 s = ''
200 principals.each do |principal|
201 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
202 end
203 s
204 end
193
205
194 # Truncates and returns the string as a single line
206 # Truncates and returns the string as a single line
195 def truncate_single_line(string, *args)
207 def truncate_single_line(string, *args)
@@ -21,7 +21,8 module CustomFieldsHelper
21 tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural},
21 tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural},
22 {:name => 'TimeEntryCustomField', :label => :label_spent_time},
22 {:name => 'TimeEntryCustomField', :label => :label_spent_time},
23 {:name => 'ProjectCustomField', :label => :label_project_plural},
23 {:name => 'ProjectCustomField', :label => :label_project_plural},
24 {:name => 'UserCustomField', :label => :label_user_plural}
24 {:name => 'UserCustomField', :label => :label_user_plural},
25 {:name => 'GroupCustomField', :label => :label_group_plural}
25 ]
26 ]
26 end
27 end
27
28
@@ -47,6 +47,7 module UsersHelper
47
47
48 def user_settings_tabs
48 def user_settings_tabs
49 tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
49 tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
50 {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural},
50 {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
51 {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
51 ]
52 ]
52 end
53 end
@@ -17,40 +17,50
17
17
18 class Member < ActiveRecord::Base
18 class Member < ActiveRecord::Base
19 belongs_to :user
19 belongs_to :user
20 has_many :member_roles, :dependent => :delete_all
20 belongs_to :principal, :foreign_key => 'user_id'
21 has_many :member_roles, :dependent => :destroy
21 has_many :roles, :through => :member_roles
22 has_many :roles, :through => :member_roles
22 belongs_to :project
23 belongs_to :project
23
24
24 validates_presence_of :user, :project
25 validates_presence_of :principal, :project
25 validates_uniqueness_of :user_id, :scope => :project_id
26 validates_uniqueness_of :user_id, :scope => :project_id
26
27
27 def name
28 def name
28 self.user.name
29 self.user.name
29 end
30 end
30
31
31 # Sets user by login
32 alias :base_role_ids= :role_ids=
32 def user_login=(login)
33 def role_ids=(arg)
33 login = login.to_s
34 ids = (arg || []).collect(&:to_i) - [0]
34 unless login.blank?
35 # Keep inherited roles
35 if (u = User.find_by_login(login))
36 ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)
36 self.user = u
37
37 end
38 new_role_ids = ids - role_ids
38 end
39 # Add new roles
40 new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id) }
41 # Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
42 member_roles.select {|mr| !ids.include?(mr.role_id)}.each(&:destroy)
39 end
43 end
40
44
41 def <=>(member)
45 def <=>(member)
42 a, b = roles.sort.first, member.roles.sort.first
46 a, b = roles.sort.first, member.roles.sort.first
43 a == b ? (user <=> member.user) : (a <=> b)
47 a == b ? (principal <=> member.principal) : (a <=> b)
48 end
49
50 def deletable?
51 member_roles.detect {|mr| mr.inherited_from}.nil?
44 end
52 end
45
53
46 def before_destroy
54 def before_destroy
47 # remove category based auto assignments for this member
55 if user
48 IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
56 # remove category based auto assignments for this member
57 IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
58 end
49 end
59 end
50
60
51 protected
61 protected
52
62
53 def validate
63 def validate
54 errors.add_to_base "Role can't be blank" if roles.empty?
64 errors.add_to_base "Role can't be blank" if member_roles.empty? && roles.empty?
55 end
65 end
56 end
66 end
@@ -19,9 +19,36 class MemberRole < ActiveRecord::Base
19 belongs_to :member
19 belongs_to :member
20 belongs_to :role
20 belongs_to :role
21
21
22 after_destroy :remove_member_if_empty
23
24 after_create :add_role_to_group_users
25 after_destroy :remove_role_from_group_users
26
22 validates_presence_of :role
27 validates_presence_of :role
23
28
24 def validate
29 def validate
25 errors.add :role_id, :invalid if role && !role.member?
30 errors.add :role_id, :invalid if role && !role.member?
26 end
31 end
32
33 private
34
35 def remove_member_if_empty
36 if member.roles.empty?
37 member.destroy
38 end
39 end
40
41 def add_role_to_group_users
42 if member.principal.is_a?(Group)
43 member.principal.users.each do |user|
44 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)
45 user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
46 user_member.save!
47 end
48 end
49 end
50
51 def remove_role_from_group_users
52 MemberRole.find(:all, :conditions => { :inherited_from => id }).each(&:destroy)
53 end
27 end
54 end
@@ -20,8 +20,13 class Project < ActiveRecord::Base
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}"
23 has_many :members, :include => :user, :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
24 has_many :member_principals, :class_name => 'Member',
25 :include => :principal,
26 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
24 has_many :users, :through => :members
27 has_many :users, :through => :members
28 has_many :principals, :through => :member_principals, :source => :principal
29
25 has_many :enabled_modules, :dependent => :delete_all
30 has_many :enabled_modules, :dependent => :delete_all
26 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
31 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
27 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
32 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
@@ -1,5 +1,5
1 # redMine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
@@ -17,7 +17,7
17
17
18 require "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < ActiveRecord::Base
20 class User < Principal
21
21
22 # Account statuses
22 # Account statuses
23 STATUS_ANONYMOUS = 0
23 STATUS_ANONYMOUS = 0
@@ -33,9 +33,8 class User < ActiveRecord::Base
33 :username => '#{login}'
33 :username => '#{login}'
34 }
34 }
35
35
36 has_many :memberships, :class_name => 'Member', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
36 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
37 has_many :members, :dependent => :delete_all
37 :after_remove => Proc.new {|user, group| group.user_removed(user)}
38 has_many :projects, :through => :memberships
39 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
38 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
40 has_many :changesets, :dependent => :nullify
39 has_many :changesets, :dependent => :nullify
41 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
40 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
@@ -50,7 +49,7 class User < ActiveRecord::Base
50 attr_accessor :password, :password_confirmation
49 attr_accessor :password, :password_confirmation
51 attr_accessor :last_before_login_on
50 attr_accessor :last_before_login_on
52 # Prevents unauthorized assignments
51 # Prevents unauthorized assignments
53 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
52 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
54
53
55 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
54 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
56 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
55 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }
@@ -317,7 +316,7 class User < ActiveRecord::Base
317 end
316 end
318
317
319 private
318 private
320
319
321 # Return password digest
320 # Return password digest
322 def self.hash_password(clear_password)
321 def self.hash_password(clear_password)
323 Digest::SHA1.hexdigest(clear_password || "")
322 Digest::SHA1.hexdigest(clear_password || "")
@@ -12,6 +12,11
12 <%= link_to l(:label_new), :controller => 'users', :action => 'add' %>
12 <%= link_to l(:label_new), :controller => 'users', :action => 'add' %>
13 </p>
13 </p>
14
14
15 <p class="icon22 icon22-groups">
16 <%= link_to l(:label_group_plural), :controller => 'groups' %> |
17 <%= link_to l(:label_new), :controller => 'groups', :action => 'new' %>
18 </p>
19
15 <p class="icon22 icon22-role">
20 <p class="icon22 icon22-role">
16 <%= link_to l(:label_role_and_permissions), :controller => 'roles' %>
21 <%= link_to l(:label_role_and_permissions), :controller => 'roles' %>
17 </p>
22 </p>
@@ -1,12 +1,12
1 <%= error_messages_for 'member' %>
1 <%= error_messages_for 'member' %>
2 <% roles = Role.find_all_givable
2 <% roles = Role.find_all_givable
3 members = @project.members.find(:all, :include => [:roles, :user]).sort %>
3 members = @project.member_principals.find(:all, :include => [:roles, :principal]).sort %>
4
4
5 <div class="splitcontentleft">
5 <div class="splitcontentleft">
6 <% if members.any? %>
6 <% if members.any? %>
7 <table class="list members">
7 <table class="list members">
8 <thead>
8 <thead>
9 <th><%= l(:label_user) %></th>
9 <th><%= l(:label_user) %> / <%= l(:label_group) %></th>
10 <th><%= l(:label_role_plural) %></th>
10 <th><%= l(:label_role_plural) %></th>
11 <th style="width:15%"></th>
11 <th style="width:15%"></th>
12 <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
12 <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
@@ -15,7 +15,7
15 <% members.each do |member| %>
15 <% members.each do |member| %>
16 <% next if member.new_record? %>
16 <% next if member.new_record? %>
17 <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %> member">
17 <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %> member">
18 <td class="user"><%= link_to_user member.user %></td>
18 <td class="<%= member.principal.class.name.downcase %>"><%= link_to_user member.principal %></td>
19 <td class="roles">
19 <td class="roles">
20 <span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
20 <span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
21 <% if authorize_for('members', 'edit') %>
21 <% if authorize_for('members', 'edit') %>
@@ -23,8 +23,10
23 :method => :post,
23 :method => :post,
24 :html => { :id => "member-#{member.id}-roles-form", :style => 'display:none;' }) do |f| %>
24 :html => { :id => "member-#{member.id}-roles-form", :style => 'display:none;' }) do |f| %>
25 <p><% roles.each do |role| %>
25 <p><% roles.each do |role| %>
26 <label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role) %> <%=h role %></label><br />
26 <label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role),
27 :disabled => member.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
27 <% end %></p>
28 <% end %></p>
29 <%= hidden_field_tag 'member[role_ids][]', '' %>
28 <p><%= submit_tag l(:button_change), :class => "small" %>
30 <p><%= submit_tag l(:button_change), :class => "small" %>
29 <%= link_to_function l(:button_cancel), "$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;" %></p>
31 <%= link_to_function l(:button_cancel), "$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;" %></p>
30 <% end %>
32 <% end %>
@@ -32,10 +34,10
32 </td>
34 </td>
33 <td class="buttons">
35 <td class="buttons">
34 <%= link_to_function l(:button_edit), "$('member-#{member.id}-roles').hide(); $('member-#{member.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
36 <%= link_to_function l(:button_edit), "$('member-#{member.id}-roles').hide(); $('member-#{member.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
35 <%= link_to_remote l(:button_delete), { :url => {:controller => 'members', :action => 'destroy', :id => member},
37 <%= link_to_remote(l(:button_delete), { :url => {:controller => 'members', :action => 'destroy', :id => member},
36 :method => :post
38 :method => :post
37 }, :title => l(:button_delete),
39 }, :title => l(:button_delete),
38 :class => 'icon icon-del' %>
40 :class => 'icon icon-del') if member.deletable? %>
39 </td>
41 </td>
40 <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
42 <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
41 </tr>
43 </tr>
@@ -48,27 +50,30
48 </div>
50 </div>
49
51
50
52
51 <% users_count = User.active.count - @project.users.count
53 <% principals = Principal.active.find(:all, :limit => 100, :order => 'type, login, lastname ASC') - @project.principals %>
52 users = (users_count < 300) ? User.active.find(:all, :limit => 200).sort - @project.users : [] %>
53
54
54 <div class="splitcontentright">
55 <div class="splitcontentright">
55 <% if roles.any? && users_count > 0 %>
56 <% if roles.any? && principals.any? %>
56 <% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post) do |f| %>
57 <% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post) do |f| %>
57 <fieldset><legend><%=l(:label_member_new)%></legend>
58 <fieldset><legend><%=l(:label_member_new)%></legend>
58 <p><%= text_field_tag 'member[user_login]', nil, :size => "40" %></p>
59
59 <div id="member_user_login_choices" class="autocomplete">sqd</div>
60 <p><%= text_field_tag 'principal_search', nil, :size => "40" %></p>
60 <%= 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' });" %>
61 <%= observe_field(:principal_search,
61 <% unless users.empty? %>
62 :frequency => 0.5,
62 <div>
63 :update => :principals,
63 <% users.each do |user| -%>
64 :url => { :controller => 'members', :action => 'autocomplete_for_member', :id => @project },
64 <label><%= check_box_tag 'member[user_ids][]', user.id, false %> <%= user %></label>
65 :with => 'q')
65 <% end -%>
66 %>
66 </div>
67
67 <% end %>
68 <div id="principals">
69 <%= principals_check_box_tags 'member[user_ids][]', principals %>
70 </div>
71
68 <p><%= l(:label_role_plural) %>:
72 <p><%= l(:label_role_plural) %>:
69 <% roles.each do |role| %>
73 <% roles.each do |role| %>
70 <label><%= check_box_tag 'member[role_ids][]', role.id %> <%=h role %></label>
74 <label><%= check_box_tag 'member[role_ids][]', role.id %> <%=h role %></label>
71 <% end %></p>
75 <% end %></p>
76
72 <p><%= submit_tag l(:button_add) %></p>
77 <p><%= submit_tag l(:button_add) %></p>
73 </fieldset>
78 </fieldset>
74 <% end %>
79 <% end %>
@@ -20,17 +20,19
20 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user, :membership_id => membership },
20 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user, :membership_id => membership },
21 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
21 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
22 <p><% roles.each do |role| %>
22 <p><% roles.each do |role| %>
23 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
23 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role),
24 :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
24 <% end %></p>
25 <% end %></p>
26 <%= hidden_field_tag 'membership[role_ids][]', '' %>
25 <p><%= submit_tag l(:button_change) %>
27 <p><%= submit_tag l(:button_change) %>
26 <%= link_to_function l(:button_cancel), "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;" %></p>
28 <%= link_to_function l(:button_cancel), "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;" %></p>
27 <% end %>
29 <% end %>
28 </td>
30 </td>
29 <td class="buttons">
31 <td class="buttons">
30 <%= link_to_function l(:button_edit), "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
32 <%= link_to_function l(:button_edit), "$('member-#{membership.id}-roles').hide(); $('member-#{membership.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
31 <%= link_to_remote l(:button_delete), { :url => { :controller => 'users', :action => 'destroy_membership', :id => @user, :membership_id => membership },
33 <%= link_to_remote(l(:button_delete), { :url => { :controller => 'users', :action => 'destroy_membership', :id => @user, :membership_id => membership },
32 :method => :post },
34 :method => :post },
33 :class => 'icon icon-del' %>
35 :class => 'icon icon-del') if membership.deletable? %>
34 </td>
36 </td>
35 <%= call_hook(:view_users_memberships_table_row, :user => @user, :membership => membership, :roles => roles, :projects => projects )%>
37 <%= call_hook(:view_users_memberships_table_row, :user => @user, :membership => membership, :roles => roles, :projects => projects )%>
36 </tr>
38 </tr>
@@ -683,6 +683,9 en:
683 label_date_from_to: From {{start}} to {{end}}
683 label_date_from_to: From {{start}} to {{end}}
684 label_wiki_content_added: Wiki page added
684 label_wiki_content_added: Wiki page added
685 label_wiki_content_updated: Wiki page updated
685 label_wiki_content_updated: Wiki page updated
686 label_group: Group
687 label_group_plural: Groups
688 label_group_new: New group
686
689
687 button_login: Login
690 button_login: Login
688 button_submit: Submit
691 button_submit: Submit
@@ -229,7 +229,8 ActionController::Routing::Routes.draw do |map|
229 map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/
229 map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/
230 map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/
230 map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/
231
231
232
232 map.resources :groups
233
233 #left old routes at the bottom for backwards compat
234 #left old routes at the bottom for backwards compat
234 map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
235 map.connect 'projects/:project_id/issues/:action', :controller => 'issues'
235 map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
236 map.connect 'projects/:project_id/documents/:action', :controller => 'documents'
@@ -23,7 +23,7 Redmine::AccessControl.map do |map|
23 map.permission :add_project, {:projects => :add}, :require => :loggedin
23 map.permission :add_project, {:projects => :add}, :require => :loggedin
24 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
24 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
25 map.permission :select_project_modules, {:projects => :modules}, :require => :member
25 map.permission :select_project_modules, {:projects => :modules}, :require => :member
26 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member_login]}, :require => :member
26 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member
27 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
27 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
28
28
29 map.project_module :issue_tracking do |map|
29 map.project_module :issue_tracking do |map|
1 NO CONTENT: modified file, binary diff hidden
NO CONTENT: modified file, binary diff hidden
@@ -342,12 +342,14 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
342 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
342 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
343
343
344 /* Project members tab */
344 /* Project members tab */
345 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft { width: 64% }
345 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
346 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright { width: 34% }
346 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
347 div#tab-content-members fieldset, div#tab-content-memberships fieldset { padding:1em; margin-bottom: 1em; }
347 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
348 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend { font-weight: bold; }
348 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
349 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label { display: block; }
349 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
350 div#tab-content-members fieldset div { max-height: 400px; overflow:auto; }
350 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
351
352 table.members td.group { padding-left: 20px; background: url(../images/users.png) no-repeat 0% 0%; }
351
353
352 * html div#tab-content-members fieldset div { height: 450px; }
354 * html div#tab-content-members fieldset div { height: 450px; }
353
355
@@ -725,6 +727,7 vertical-align: middle;
725
727
726 .icon22-projects { background-image: url(../images/22x22/projects.png); }
728 .icon22-projects { background-image: url(../images/22x22/projects.png); }
727 .icon22-users { background-image: url(../images/22x22/users.png); }
729 .icon22-users { background-image: url(../images/22x22/users.png); }
730 .icon22-groups { background-image: url(../images/22x22/groups.png); }
728 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
731 .icon22-tracker { background-image: url(../images/22x22/tracker.png); }
729 .icon22-role { background-image: url(../images/22x22/role.png); }
732 .icon22-role { background-image: url(../images/22x22/role.png); }
730 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
733 .icon22-workflow { background-image: url(../images/22x22/workflow.png); }
@@ -19,5 +19,21 member_roles_005:
19 id: 5
19 id: 5
20 role_id: 1
20 role_id: 1
21 member_id: 5
21 member_id: 5
22
22 member_roles_006:
23 No newline at end of file
23 id: 6
24 role_id: 1
25 member_id: 6
26 member_roles_007:
27 id: 7
28 role_id: 2
29 member_id: 6
30 member_roles_008:
31 id: 8
32 role_id: 1
33 member_id: 7
34 inherited_from: 6
35 member_roles_009:
36 id: 9
37 role_id: 2
38 member_id: 7
39 inherited_from: 7
@@ -30,4 +30,16 members_005:
30 project_id: 5
30 project_id: 5
31 user_id: 2
31 user_id: 2
32 mail_notification: true
32 mail_notification: true
33 members_006:
34 id: 6
35 created_on: 2006-07-19 19:35:33 +02:00
36 project_id: 5
37 user_id: 10
38 mail_notification: false
39 members_007:
40 id: 7
41 created_on: 2006-07-19 19:35:33 +02:00
42 project_id: 5
43 user_id: 8
44 mail_notification: false
33 No newline at end of file
45
@@ -144,5 +144,13 users_009:
144 mail_notification: false
144 mail_notification: false
145 login: miscuser9
145 login: miscuser9
146 type: User
146 type: User
147 groups_010:
148 id: 10
149 lastname: A Team
150 type: Group
151 groups_011:
152 id: 11
153 lastname: B Team
154 type: Group
147
155
148 No newline at end of file
156
@@ -48,14 +48,6 class MembersControllerTest < Test::Unit::TestCase
48 assert User.find(7).member_of?(Project.find(1))
48 assert User.find(7).member_of?(Project.find(1))
49 end
49 end
50
50
51 def test_create_by_user_login
52 assert_difference 'Member.count' do
53 post :new, :id => 1, :member => {:role_ids => [1], :user_login => 'someone'}
54 end
55 assert_redirected_to '/projects/ecookbook/settings/members'
56 assert User.find(7).member_of?(Project.find(1))
57 end
58
59 def test_create_multiple
51 def test_create_multiple
60 assert_difference 'Member.count', 3 do
52 assert_difference 'Member.count', 3 do
61 post :new, :id => 1, :member => {:role_ids => [1], :user_ids => [7, 8, 9]}
53 post :new, :id => 1, :member => {:role_ids => [1], :user_ids => [7, 8, 9]}
@@ -79,11 +71,12 class MembersControllerTest < Test::Unit::TestCase
79 assert !User.find(3).member_of?(Project.find(1))
71 assert !User.find(3).member_of?(Project.find(1))
80 end
72 end
81
73
82 def test_autocomplete_for_member_login
74 def test_autocomplete_for_member
83 get :autocomplete_for_member_login, :id => 1, :user => 'mis'
75 get :autocomplete_for_member, :id => 1, :q => 'mis'
84 assert_response :success
76 assert_response :success
85 assert_template 'autocomplete_for_member_login'
77 assert_template 'autocomplete_for_member'
86
78
87 assert_tag :ul, :child => {:tag => 'li', :content => /miscuser8/}
79 assert_tag :label, :content => /User Misc/,
80 :child => { :tag => 'input', :attributes => { :name => 'member[user_ids][]', :value => '8' } }
88 end
81 end
89 end
82 end
@@ -18,7 +18,7
18 require "#{File.dirname(__FILE__)}/../test_helper"
18 require "#{File.dirname(__FILE__)}/../test_helper"
19
19
20 class AdminTest < ActionController::IntegrationTest
20 class AdminTest < ActionController::IntegrationTest
21 fixtures :users
21 fixtures :all
22
22
23 def test_add_user
23 def test_add_user
24 log_user("admin", "admin")
24 log_user("admin", "admin")
@@ -26,16 +26,17 class AdminTest < ActionController::IntegrationTest
26 assert_response :success
26 assert_response :success
27 assert_template "users/add"
27 assert_template "users/add"
28 post "/users/add", :user => { :login => "psmith", :firstname => "Paul", :lastname => "Smith", :mail => "psmith@somenet.foo", :language => "en" }, :password => "psmith09", :password_confirmation => "psmith09"
28 post "/users/add", :user => { :login => "psmith", :firstname => "Paul", :lastname => "Smith", :mail => "psmith@somenet.foo", :language => "en" }, :password => "psmith09", :password_confirmation => "psmith09"
29 assert_redirected_to "/users"
30
29
31 user = User.find_by_login("psmith")
30 user = User.find_by_login("psmith")
32 assert_kind_of User, user
31 assert_kind_of User, user
32 assert_redirected_to "/users/#{ user.id }/edit"
33
33 logged_user = User.try_to_login("psmith", "psmith09")
34 logged_user = User.try_to_login("psmith", "psmith09")
34 assert_kind_of User, logged_user
35 assert_kind_of User, logged_user
35 assert_equal "Paul", logged_user.firstname
36 assert_equal "Paul", logged_user.firstname
36
37
37 post "users/edit", :id => user.id, :user => { :status => User::STATUS_LOCKED }
38 post "users/edit", :id => user.id, :user => { :status => User::STATUS_LOCKED }
38 assert_redirected_to "/users"
39 assert_redirected_to "/users/#{ user.id }/edit"
39 locked_user = User.try_to_login("psmith", "psmith09")
40 locked_user = User.try_to_login("psmith", "psmith09")
40 assert_equal nil, locked_user
41 assert_equal nil, locked_user
41 end
42 end
@@ -63,6 +63,18 class ProjectTest < Test::Unit::TestCase
63 end
63 end
64 end
64 end
65
65
66 def test_members_should_be_active_users
67 Project.all.each do |project|
68 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
69 end
70 end
71
72 def test_users_should_be_active_users
73 Project.all.each do |project|
74 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
75 end
76 end
77
66 def test_archive
78 def test_archive
67 user = @ecookbook.members.first.user
79 user = @ecookbook.members.first.user
68 @ecookbook.archive
80 @ecookbook.archive
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now