@@ -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 %> » <%= 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 %> » <%= 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 %> » <%=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 |
@@ -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 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class MembersController < ApplicationController |
|
19 |
before_filter :find_member, :except => [:new, :autocomplete_for_member |
|
|
20 |
before_filter :find_project, :only => [:new, :autocomplete_for_member |
|
|
19 | before_filter :find_member, :except => [:new, :autocomplete_for_member] | |
|
20 | before_filter :find_project, :only => [:new, :autocomplete_for_member] | |
|
21 | 21 | before_filter :authorize |
|
22 | 22 | |
|
23 | 23 | def new |
@@ -59,17 +59,17 class MembersController < ApplicationController | |||
|
59 | 59 | end |
|
60 | 60 | |
|
61 | 61 | def destroy |
|
62 | if request.post? && @member.deletable? | |
|
62 | 63 | @member.destroy |
|
64 | end | |
|
63 | 65 |
|
|
64 | 66 | format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } |
|
65 | 67 | format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} } |
|
66 | 68 | end |
|
67 | 69 | end |
|
68 | 70 | |
|
69 |
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]}%"], | |
|
71 | :limit => 10, | |
|
72 | :order => 'login ASC') - @project.users | |
|
71 | def autocomplete_for_member | |
|
72 | @principals = Principal.active.like(params[:q]).find(:all, :limit => 100) - @project.principals | |
|
73 | 73 | render :layout => false |
|
74 | 74 | end |
|
75 | 75 |
@@ -63,7 +63,7 class UsersController < ApplicationController | |||
|
63 | 63 | if @user.save |
|
64 | 64 | Mailer.deliver_account_information(@user, params[:password]) if params[:send_information] |
|
65 | 65 | flash[:notice] = l(:notice_successful_create) |
|
66 |
redirect_to :action => ' |
|
|
66 | redirect_to :controller => 'users', :action => 'edit', :id => @user | |
|
67 | 67 | end |
|
68 | 68 | end |
|
69 | 69 | @auth_sources = AuthSource.find(:all) |
@@ -75,6 +75,7 class UsersController < ApplicationController | |||
|
75 | 75 | @user.admin = params[:user][:admin] if params[:user][:admin] |
|
76 | 76 | @user.login = params[:user][:login] if params[:user][:login] |
|
77 | 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 | 79 | @user.attributes = params[:user] |
|
79 | 80 | # Was the account actived ? (do it before User#save clears the change) |
|
80 | 81 | was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE]) |
@@ -85,17 +86,18 class UsersController < ApplicationController | |||
|
85 | 86 | Mailer.deliver_account_information(@user, params[:password]) |
|
86 | 87 | end |
|
87 | 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(url_for(:action => 'list', :status => params[:status], :page => params[:page])) | |
|
89 | redirect_to :back | |
|
90 | 90 | end |
|
91 | 91 | end |
|
92 | 92 | @auth_sources = AuthSource.find(:all) |
|
93 | 93 | @membership ||= Member.new |
|
94 | rescue ::ActionController::RedirectBackError | |
|
95 | redirect_to :controller => 'users', :action => 'edit', :id => @user | |
|
94 | 96 | end |
|
95 | 97 | |
|
96 | 98 | def edit_membership |
|
97 | 99 | @user = User.find(params[:id]) |
|
98 |
@membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(: |
|
|
100 | @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:principal => @user) | |
|
99 | 101 | @membership.attributes = params[:membership] |
|
100 | 102 | @membership.save if request.post? |
|
101 | 103 | respond_to do |format| |
@@ -111,7 +113,10 class UsersController < ApplicationController | |||
|
111 | 113 | |
|
112 | 114 | def destroy_membership |
|
113 | 115 | @user = User.find(params[:id]) |
|
114 |
Member.find(params[:membership_id]) |
|
|
116 | @membership = Member.find(params[:membership_id]) | |
|
117 | if request.post? && @membership.deletable? | |
|
118 | @membership.destroy | |
|
119 | end | |
|
115 | 120 | respond_to do |format| |
|
116 | 121 | format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' } |
|
117 | 122 | format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} } |
@@ -46,7 +46,11 module ApplicationHelper | |||
|
46 | 46 | |
|
47 | 47 | # Display a link to user's account page |
|
48 | 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 | 54 | end |
|
51 | 55 | |
|
52 | 56 | def link_to_issue(issue, options={}) |
@@ -191,6 +195,14 module ApplicationHelper | |||
|
191 | 195 | s |
|
192 | 196 | end |
|
193 | 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 | |
|
205 | ||
|
194 | 206 | # Truncates and returns the string as a single line |
|
195 | 207 | def truncate_single_line(string, *args) |
|
196 | 208 | truncate(string, *args).gsub(%r{[\r\n]+}m, ' ') |
@@ -21,7 +21,8 module CustomFieldsHelper | |||
|
21 | 21 | tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural}, |
|
22 | 22 | {:name => 'TimeEntryCustomField', :label => :label_spent_time}, |
|
23 | 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 | 27 | end |
|
27 | 28 |
@@ -47,6 +47,7 module UsersHelper | |||
|
47 | 47 | |
|
48 | 48 | def user_settings_tabs |
|
49 | 49 | tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general}, |
|
50 | {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural}, | |
|
50 | 51 | {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural} |
|
51 | 52 | ] |
|
52 | 53 | end |
@@ -17,40 +17,50 | |||
|
17 | 17 | |
|
18 | 18 | class Member < ActiveRecord::Base |
|
19 | 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 | 22 | has_many :roles, :through => :member_roles |
|
22 | 23 | belongs_to :project |
|
23 | 24 | |
|
24 |
validates_presence_of : |
|
|
25 | validates_presence_of :principal, :project | |
|
25 | 26 | validates_uniqueness_of :user_id, :scope => :project_id |
|
26 | 27 | |
|
27 | 28 | def name |
|
28 | 29 | self.user.name |
|
29 | 30 | end |
|
30 | 31 | |
|
31 | # Sets user by login | |
|
32 | def user_login=(login) | |
|
33 | login = login.to_s | |
|
34 | unless login.blank? | |
|
35 | if (u = User.find_by_login(login)) | |
|
36 | self.user = u | |
|
37 | end | |
|
38 | end | |
|
32 | alias :base_role_ids= :role_ids= | |
|
33 | def role_ids=(arg) | |
|
34 | ids = (arg || []).collect(&:to_i) - [0] | |
|
35 | # Keep inherited roles | |
|
36 | ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id) | |
|
37 | ||
|
38 | new_role_ids = ids - role_ids | |
|
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 | 43 | end |
|
40 | 44 | |
|
41 | 45 | def <=>(member) |
|
42 | 46 | a, b = roles.sort.first, member.roles.sort.first |
|
43 |
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 | 52 | end |
|
45 | 53 | |
|
46 | 54 | def before_destroy |
|
55 | if user | |
|
47 | 56 | # remove category based auto assignments for this member |
|
48 | 57 | IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id] |
|
49 | 58 | end |
|
59 | end | |
|
50 | 60 | |
|
51 | 61 | protected |
|
52 | 62 | |
|
53 | 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 | 65 | end |
|
56 | 66 | end |
@@ -19,9 +19,36 class MemberRole < ActiveRecord::Base | |||
|
19 | 19 | belongs_to :member |
|
20 | 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 | 27 | validates_presence_of :role |
|
23 | 28 | |
|
24 | 29 | def validate |
|
25 | 30 | errors.add :role_id, :invalid if role && !role.member? |
|
26 | 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 | 54 | end |
@@ -20,8 +20,13 class Project < ActiveRecord::Base | |||
|
20 | 20 | STATUS_ACTIVE = 1 |
|
21 | 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 | 27 | has_many :users, :through => :members |
|
28 | has_many :principals, :through => :member_principals, :source => :principal | |
|
29 | ||
|
25 | 30 | has_many :enabled_modules, :dependent => :delete_all |
|
26 | 31 | has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" |
|
27 | 32 | has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker] |
@@ -1,5 +1,5 | |||
|
1 |
# |
|
|
2 |
# Copyright (C) 2006-200 |
|
|
1 | # Redmine - project management software | |
|
2 | # Copyright (C) 2006-2009 Jean-Philippe Lang | |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
@@ -17,7 +17,7 | |||
|
17 | 17 | |
|
18 | 18 | require "digest/sha1" |
|
19 | 19 | |
|
20 |
class User < |
|
|
20 | class User < Principal | |
|
21 | 21 | |
|
22 | 22 | # Account statuses |
|
23 | 23 | STATUS_ANONYMOUS = 0 |
@@ -33,9 +33,8 class User < ActiveRecord::Base | |||
|
33 | 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" | |
|
37 | has_many :members, :dependent => :delete_all | |
|
38 | has_many :projects, :through => :memberships | |
|
36 | has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)}, | |
|
37 | :after_remove => Proc.new {|user, group| group.user_removed(user)} | |
|
39 | 38 | has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify |
|
40 | 39 | has_many :changesets, :dependent => :nullify |
|
41 | 40 | has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' |
@@ -50,7 +49,7 class User < ActiveRecord::Base | |||
|
50 | 49 | attr_accessor :password, :password_confirmation |
|
51 | 50 | attr_accessor :last_before_login_on |
|
52 | 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 | 54 | validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } |
|
56 | 55 | validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? } |
@@ -12,6 +12,11 | |||
|
12 | 12 | <%= link_to l(:label_new), :controller => 'users', :action => 'add' %> |
|
13 | 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 | 20 | <p class="icon22 icon22-role"> |
|
16 | 21 | <%= link_to l(:label_role_and_permissions), :controller => 'roles' %> |
|
17 | 22 | </p> |
@@ -1,12 +1,12 | |||
|
1 | 1 | <%= error_messages_for 'member' %> |
|
2 | 2 | <% roles = Role.find_all_givable |
|
3 |
members = @project.members.find(:all, :include => [:roles, : |
|
|
3 | members = @project.member_principals.find(:all, :include => [:roles, :principal]).sort %> | |
|
4 | 4 | |
|
5 | 5 | <div class="splitcontentleft"> |
|
6 | 6 | <% if members.any? %> |
|
7 | 7 | <table class="list members"> |
|
8 | 8 | <thead> |
|
9 | <th><%= l(:label_user) %></th> | |
|
9 | <th><%= l(:label_user) %> / <%= l(:label_group) %></th> | |
|
10 | 10 | <th><%= l(:label_role_plural) %></th> |
|
11 | 11 | <th style="width:15%"></th> |
|
12 | 12 | <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %> |
@@ -15,7 +15,7 | |||
|
15 | 15 | <% members.each do |member| %> |
|
16 | 16 | <% next if member.new_record? %> |
|
17 | 17 | <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %> member"> |
|
18 |
<td class=" |
|
|
18 | <td class="<%= member.principal.class.name.downcase %>"><%= link_to_user member.principal %></td> | |
|
19 | 19 | <td class="roles"> |
|
20 | 20 | <span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span> |
|
21 | 21 | <% if authorize_for('members', 'edit') %> |
@@ -23,8 +23,10 | |||
|
23 | 23 | :method => :post, |
|
24 | 24 | :html => { :id => "member-#{member.id}-roles-form", :style => 'display:none;' }) do |f| %> |
|
25 | 25 | <p><% roles.each do |role| %> |
|
26 |
<label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role) |
|
|
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 | 28 | <% end %></p> |
|
29 | <%= hidden_field_tag 'member[role_ids][]', '' %> | |
|
28 | 30 | <p><%= submit_tag l(:button_change), :class => "small" %> |
|
29 | 31 | <%= link_to_function l(:button_cancel), "$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;" %></p> |
|
30 | 32 | <% end %> |
@@ -32,10 +34,10 | |||
|
32 | 34 | </td> |
|
33 | 35 | <td class="buttons"> |
|
34 | 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 |
|
|
37 | <%= link_to_remote(l(:button_delete), { :url => {:controller => 'members', :action => 'destroy', :id => member}, | |
|
36 | 38 | :method => :post |
|
37 | 39 | }, :title => l(:button_delete), |
|
38 | :class => 'icon icon-del' %> | |
|
40 | :class => 'icon icon-del') if member.deletable? %> | |
|
39 | 41 | </td> |
|
40 | 42 | <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %> |
|
41 | 43 | </tr> |
@@ -48,27 +50,30 | |||
|
48 | 50 | </div> |
|
49 | 51 | |
|
50 | 52 | |
|
51 | <% users_count = User.active.count - @project.users.count | |
|
52 | users = (users_count < 300) ? User.active.find(:all, :limit => 200).sort - @project.users : [] %> | |
|
53 | <% principals = Principal.active.find(:all, :limit => 100, :order => 'type, login, lastname ASC') - @project.principals %> | |
|
53 | 54 | |
|
54 | 55 | <div class="splitcontentright"> |
|
55 |
<% if roles.any? && |
|
|
56 | <% if roles.any? && principals.any? %> | |
|
56 | 57 | <% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post) do |f| %> |
|
57 | 58 | <fieldset><legend><%=l(:label_member_new)%></legend> |
|
58 | <p><%= text_field_tag 'member[user_login]', nil, :size => "40" %></p> | |
|
59 | <div id="member_user_login_choices" class="autocomplete">sqd</div> | |
|
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 | <% unless users.empty? %> | |
|
62 | <div> | |
|
63 | <% users.each do |user| -%> | |
|
64 | <label><%= check_box_tag 'member[user_ids][]', user.id, false %> <%= user %></label> | |
|
65 | <% end -%> | |
|
59 | ||
|
60 | <p><%= text_field_tag 'principal_search', nil, :size => "40" %></p> | |
|
61 | <%= observe_field(:principal_search, | |
|
62 | :frequency => 0.5, | |
|
63 | :update => :principals, | |
|
64 | :url => { :controller => 'members', :action => 'autocomplete_for_member', :id => @project }, | |
|
65 | :with => 'q') | |
|
66 | %> | |
|
67 | ||
|
68 | <div id="principals"> | |
|
69 | <%= principals_check_box_tags 'member[user_ids][]', principals %> | |
|
66 | 70 |
|
|
67 | <% end %> | |
|
71 | ||
|
68 | 72 | <p><%= l(:label_role_plural) %>: |
|
69 | 73 | <% roles.each do |role| %> |
|
70 | 74 | <label><%= check_box_tag 'member[role_ids][]', role.id %> <%=h role %></label> |
|
71 | 75 | <% end %></p> |
|
76 | ||
|
72 | 77 | <p><%= submit_tag l(:button_add) %></p> |
|
73 | 78 | </fieldset> |
|
74 | 79 | <% end %> |
@@ -20,17 +20,19 | |||
|
20 | 20 | <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user, :membership_id => membership }, |
|
21 | 21 | :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %> |
|
22 | 22 | <p><% roles.each do |role| %> |
|
23 |
<label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) |
|
|
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 | 25 | <% end %></p> |
|
26 | <%= hidden_field_tag 'membership[role_ids][]', '' %> | |
|
25 | 27 | <p><%= submit_tag l(:button_change) %> |
|
26 | 28 | <%= link_to_function l(:button_cancel), "$('member-#{membership.id}-roles').show(); $('member-#{membership.id}-roles-form').hide(); return false;" %></p> |
|
27 | 29 | <% end %> |
|
28 | 30 | </td> |
|
29 | 31 | <td class="buttons"> |
|
30 | 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 |
|
|
33 | <%= link_to_remote(l(:button_delete), { :url => { :controller => 'users', :action => 'destroy_membership', :id => @user, :membership_id => membership }, | |
|
32 | 34 | :method => :post }, |
|
33 | :class => 'icon icon-del' %> | |
|
35 | :class => 'icon icon-del') if membership.deletable? %> | |
|
34 | 36 | </td> |
|
35 | 37 | <%= call_hook(:view_users_memberships_table_row, :user => @user, :membership => membership, :roles => roles, :projects => projects )%> |
|
36 | 38 | </tr> |
@@ -683,6 +683,9 en: | |||
|
683 | 683 | label_date_from_to: From {{start}} to {{end}} |
|
684 | 684 | label_wiki_content_added: Wiki page added |
|
685 | 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 | 690 | button_login: Login |
|
688 | 691 | button_submit: Submit |
@@ -229,6 +229,7 ActionController::Routing::Routes.draw do |map| | |||
|
229 | 229 | map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/ |
|
230 | 230 | map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/ |
|
231 | 231 | |
|
232 | map.resources :groups | |
|
232 | 233 | |
|
233 | 234 | #left old routes at the bottom for backwards compat |
|
234 | 235 | map.connect 'projects/:project_id/issues/:action', :controller => 'issues' |
@@ -23,7 +23,7 Redmine::AccessControl.map do |map| | |||
|
23 | 23 | map.permission :add_project, {:projects => :add}, :require => :loggedin |
|
24 | 24 | map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member |
|
25 | 25 | map.permission :select_project_modules, {:projects => :modules}, :require => :member |
|
26 |
map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member |
|
|
26 | map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member | |
|
27 | 27 | map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member |
|
28 | 28 | |
|
29 | 29 | map.project_module :issue_tracking do |map| |
|
1 | NO CONTENT: modified file, binary diff hidden |
@@ -342,12 +342,14 p.other-formats { text-align: right; font-size:0.9em; color: #666; } | |||
|
342 | 342 | a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; } |
|
343 | 343 | |
|
344 | 344 | /* Project members tab */ |
|
345 | div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft { width: 64% } | |
|
346 | div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright { width: 34% } | |
|
347 | div#tab-content-members fieldset, div#tab-content-memberships fieldset { padding:1em; margin-bottom: 1em; } | |
|
348 | div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend { font-weight: bold; } | |
|
349 | div#tab-content-members fieldset label, div#tab-content-memberships fieldset label { display: block; } | |
|
350 | div#tab-content-members fieldset div { max-height: 400px; overflow:auto; } | |
|
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, div#tab-content-users .splitcontentright { width: 34% } | |
|
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, div#tab-content-users fieldset legend { font-weight: bold; } | |
|
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, 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 | 354 | * html div#tab-content-members fieldset div { height: 450px; } |
|
353 | 355 | |
@@ -725,6 +727,7 vertical-align: middle; | |||
|
725 | 727 | |
|
726 | 728 | .icon22-projects { background-image: url(../images/22x22/projects.png); } |
|
727 | 729 | .icon22-users { background-image: url(../images/22x22/users.png); } |
|
730 | .icon22-groups { background-image: url(../images/22x22/groups.png); } | |
|
728 | 731 | .icon22-tracker { background-image: url(../images/22x22/tracker.png); } |
|
729 | 732 | .icon22-role { background-image: url(../images/22x22/role.png); } |
|
730 | 733 | .icon22-workflow { background-image: url(../images/22x22/workflow.png); } |
@@ -19,5 +19,21 member_roles_005: | |||
|
19 | 19 | id: 5 |
|
20 | 20 | role_id: 1 |
|
21 | 21 | member_id: 5 |
|
22 | ||
|
23 | No newline at end of file | |
|
22 | member_roles_006: | |
|
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 | 30 | project_id: 5 |
|
31 | 31 | user_id: 2 |
|
32 | 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 | 45 | No newline at end of file |
@@ -144,5 +144,13 users_009: | |||
|
144 | 144 | mail_notification: false |
|
145 | 145 | login: miscuser9 |
|
146 | 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 | 156 | No newline at end of file |
@@ -48,14 +48,6 class MembersControllerTest < Test::Unit::TestCase | |||
|
48 | 48 | assert User.find(7).member_of?(Project.find(1)) |
|
49 | 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 | 51 | def test_create_multiple |
|
60 | 52 | assert_difference 'Member.count', 3 do |
|
61 | 53 | post :new, :id => 1, :member => {:role_ids => [1], :user_ids => [7, 8, 9]} |
@@ -79,11 +71,12 class MembersControllerTest < Test::Unit::TestCase | |||
|
79 | 71 | assert !User.find(3).member_of?(Project.find(1)) |
|
80 | 72 | end |
|
81 | 73 | |
|
82 |
def test_autocomplete_for_member |
|
|
83 |
get :autocomplete_for_member |
|
|
74 | def test_autocomplete_for_member | |
|
75 | get :autocomplete_for_member, :id => 1, :q => 'mis' | |
|
84 | 76 | assert_response :success |
|
85 |
assert_template 'autocomplete_for_member |
|
|
77 | assert_template 'autocomplete_for_member' | |
|
86 | 78 | |
|
87 |
assert_tag : |
|
|
79 | assert_tag :label, :content => /User Misc/, | |
|
80 | :child => { :tag => 'input', :attributes => { :name => 'member[user_ids][]', :value => '8' } } | |
|
88 | 81 | end |
|
89 | 82 | end |
@@ -18,7 +18,7 | |||
|
18 | 18 | require "#{File.dirname(__FILE__)}/../test_helper" |
|
19 | 19 | |
|
20 | 20 | class AdminTest < ActionController::IntegrationTest |
|
21 |
fixtures : |
|
|
21 | fixtures :all | |
|
22 | 22 | |
|
23 | 23 | def test_add_user |
|
24 | 24 | log_user("admin", "admin") |
@@ -26,16 +26,17 class AdminTest < ActionController::IntegrationTest | |||
|
26 | 26 | assert_response :success |
|
27 | 27 | assert_template "users/add" |
|
28 | 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 | 30 | user = User.find_by_login("psmith") |
|
32 | 31 | assert_kind_of User, user |
|
32 | assert_redirected_to "/users/#{ user.id }/edit" | |
|
33 | ||
|
33 | 34 | logged_user = User.try_to_login("psmith", "psmith09") |
|
34 | 35 | assert_kind_of User, logged_user |
|
35 | 36 | assert_equal "Paul", logged_user.firstname |
|
36 | 37 | |
|
37 | 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 | 40 | locked_user = User.try_to_login("psmith", "psmith09") |
|
40 | 41 | assert_equal nil, locked_user |
|
41 | 42 | end |
@@ -63,6 +63,18 class ProjectTest < Test::Unit::TestCase | |||
|
63 | 63 | end |
|
64 | 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 | 78 | def test_archive |
|
67 | 79 | user = @ecookbook.members.first.user |
|
68 | 80 | @ecookbook.archive |
|
1 | NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now