diff --git a/groups/app/controllers/custom_fields_controller.rb b/groups/app/controllers/custom_fields_controller.rb index 1e1c988d9..57f700dc6 100644 --- a/groups/app/controllers/custom_fields_controller.rb +++ b/groups/app/controllers/custom_fields_controller.rb @@ -1,5 +1,5 @@ # redMine - project management software -# Copyright (C) 2006 Jean-Philippe Lang +# Copyright (C) 2006-2008 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -32,16 +32,18 @@ class CustomFieldsController < ApplicationController def new case params[:type] - when "IssueCustomField" - @custom_field = IssueCustomField.new(params[:custom_field]) - @custom_field.trackers = Tracker.find(params[:tracker_ids]) if params[:tracker_ids] - when "UserCustomField" - @custom_field = UserCustomField.new(params[:custom_field]) - when "ProjectCustomField" - @custom_field = ProjectCustomField.new(params[:custom_field]) - else - redirect_to :action => 'list' - return + when "IssueCustomField" + @custom_field = IssueCustomField.new(params[:custom_field]) + @custom_field.trackers = Tracker.find(params[:tracker_ids]) if params[:tracker_ids] + when "UserCustomField" + @custom_field = UserCustomField.new(params[:custom_field]) + when "ProjectCustomField" + @custom_field = ProjectCustomField.new(params[:custom_field]) + when "GroupCustomField" + @custom_field = GroupCustomField.new(params[:custom_field]) + else + render_404 + return end if request.post? and @custom_field.save flash[:notice] = l(:notice_successful_create) diff --git a/groups/app/controllers/groups_controller.rb b/groups/app/controllers/groups_controller.rb new file mode 100644 index 000000000..b259fc320 --- /dev/null +++ b/groups/app/controllers/groups_controller.rb @@ -0,0 +1,114 @@ +# redMine - project management software +# Copyright (C) 2008 FreeCode +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class GroupsController < ApplicationController + layout 'base' + before_filter :require_admin + + helper :custom_fields + + # GET /groups + # GET /groups.xml + def index + @groups = Group.find(:all, :order => 'name') + @group = Group.new + + respond_to do |format| + format.html # index.html.erb + format.xml { render :xml => @groups } + end + end + + # GET /groups/1 + # GET /groups/1.xml + def show + @group = Group.find(params[:id]) + + respond_to do |format| + format.html # show.html.erb + format.xml { render :xml => @group } + end + end + + # GET /groups/new + # GET /groups/new.xml + def new + @group = Group.new + @custom_values = GroupCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @group) } + + respond_to do |format| + format.html # new.html.erb + format.xml { render :xml => @group } + end + end + + # GET /groups/1/edit + def edit + @group = Group.find(params[:id]) + @custom_values = GroupCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @group.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) } + end + + # POST /groups + # POST /groups.xml + def create + @group = Group.new(params[:group]) + @custom_values = GroupCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @group, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) } + @group.custom_values = @custom_values + + respond_to do |format| + if @group.save + flash[:notice] = l(:notice_successful_create) + format.html { redirect_to(groups_path) } + format.xml { render :xml => @group, :status => :created, :location => @group } + else + format.html { render :action => "new" } + format.xml { render :xml => @group.errors, :status => :unprocessable_entity } + end + end + end + + # PUT /groups/1 + # PUT /groups/1.xml + def update + @group = Group.find(params[:id]) + @custom_values = GroupCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @group, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) } + @group.custom_values = @custom_values + + respond_to do |format| + if @group.update_attributes(params[:group]) + flash[:notice] = l(:notice_successful_update) + format.html { redirect_to(groups_path) } + format.xml { head :ok } + else + format.html { render :action => "edit" } + format.xml { render :xml => @group.errors, :status => :unprocessable_entity } + end + end + end + + # DELETE /groups/1 + # DELETE /groups/1.xml + def destroy + @group = Group.find(params[:id]) + @group.destroy + + respond_to do |format| + format.html { redirect_to(groups_url) } + format.xml { head :ok } + end + end +end diff --git a/groups/app/controllers/members_controller.rb b/groups/app/controllers/members_controller.rb index a1706e601..6a42ed05e 100644 --- a/groups/app/controllers/members_controller.rb +++ b/groups/app/controllers/members_controller.rb @@ -20,11 +20,17 @@ class MembersController < ApplicationController before_filter :find_member, :except => :new before_filter :find_project, :only => :new before_filter :authorize + + helper :projects def new - @project.members << Member.new(params[:member]) if request.post? + member = Member.new(params[:member]) + if params[:principal].to_s =~ %r{^(group|user)_(\d+)$} + member.principal_type, member.principal_id = $1.camelize, $2.to_i + end + @project.members << member if request.post? respond_to do |format| - format.html { redirect_to :action => 'settings', :tab => 'members', :id => @project } + format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} } end end @@ -54,7 +60,7 @@ private end def find_member - @member = Member.find(params[:id]) + @member = Member.find(params[:id], :conditions => 'inherited_from IS NULL') @project = @member.project rescue ActiveRecord::RecordNotFound render_404 diff --git a/groups/app/controllers/users_controller.rb b/groups/app/controllers/users_controller.rb index 48fc6fade..9fb11be00 100644 --- a/groups/app/controllers/users_controller.rb +++ b/groups/app/controllers/users_controller.rb @@ -42,6 +42,7 @@ class UsersController < ApplicationController per_page_option, params['page'] @users = User.find :all,:order => sort_clause, + :include => :group, :conditions => conditions, :limit => @user_pages.items_per_page, :offset => @user_pages.current.offset @@ -58,6 +59,7 @@ class UsersController < ApplicationController @user.admin = params[:user][:admin] || false @user.login = params[:user][:login] @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id + @user.group_id = params[:user][:group_id] @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) } @user.custom_values = @custom_values if @user.save @@ -67,6 +69,7 @@ class UsersController < ApplicationController end end @auth_sources = AuthSource.find(:all) + @groups = Group.find(:all, :order => 'name') end def edit @@ -77,6 +80,7 @@ class UsersController < ApplicationController @user.admin = params[:user][:admin] if params[:user][:admin] @user.login = params[:user][:login] if params[:user][:login] @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id + @user.group_id = params[:user][:group_id] if params[:custom_fields] @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) } @user.custom_values = @custom_values @@ -88,6 +92,7 @@ class UsersController < ApplicationController end end @auth_sources = AuthSource.find(:all) + @groups = Group.find(:all, :order => 'name') @roles = Role.find_all_givable @projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects @membership ||= Member.new @@ -95,7 +100,7 @@ class UsersController < ApplicationController def edit_membership @user = User.find(params[:id]) - @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user) + @membership = params[:membership_id] ? Member.find(params[:membership_id], :conditions => 'inherited_from IS NULL') : Member.new(:principal => @user) @membership.attributes = params[:membership] if request.post? and @membership.save flash[:notice] = l(:notice_successful_update) @@ -105,7 +110,7 @@ class UsersController < ApplicationController def destroy_membership @user = User.find(params[:id]) - if request.post? and Member.find(params[:membership_id]).destroy + if request.post? and Member.find(params[:membership_id], :conditions => 'inherited_from IS NULL').destroy flash[:notice] = l(:notice_successful_update) end redirect_to :action => 'edit', :id => @user and return diff --git a/groups/app/helpers/custom_fields_helper.rb b/groups/app/helpers/custom_fields_helper.rb index 61c8d6b36..aae105b72 100644 --- a/groups/app/helpers/custom_fields_helper.rb +++ b/groups/app/helpers/custom_fields_helper.rb @@ -20,7 +20,8 @@ module CustomFieldsHelper def custom_fields_tabs tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural}, {:name => 'ProjectCustomField', :label => :label_project_plural}, - {:name => 'UserCustomField', :label => :label_user_plural} + {:name => 'UserCustomField', :label => :label_user_plural}, + {:name => 'GroupCustomField', :label => :label_group_plural} ] end diff --git a/groups/app/helpers/groups_helper.rb b/groups/app/helpers/groups_helper.rb new file mode 100644 index 000000000..859c7873b --- /dev/null +++ b/groups/app/helpers/groups_helper.rb @@ -0,0 +1,19 @@ +# redMine - project management software +# Copyright (C) 2008 FreeCode +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module GroupsHelper +end diff --git a/groups/app/helpers/projects_helper.rb b/groups/app/helpers/projects_helper.rb index ffbf25e83..0696eefbf 100644 --- a/groups/app/helpers/projects_helper.rb +++ b/groups/app/helpers/projects_helper.rb @@ -42,6 +42,19 @@ module ProjectsHelper tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)} end + def principal_select_tag(groups, users) + options = '' + options << "" unless groups.empty? + + options << "" unless users.empty? + + select_tag 'principal', options + end + # Generates a gantt image # Only defined if RMagick is avalaible def gantt_image(events, date_from, months, zoom) diff --git a/groups/app/models/group.rb b/groups/app/models/group.rb new file mode 100644 index 000000000..23c9d8d20 --- /dev/null +++ b/groups/app/models/group.rb @@ -0,0 +1,39 @@ +# redMine - project management software +# Copyright (C) 2008 FreeCode +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class Group < ActiveRecord::Base + + has_many :users, :dependent => :nullify + has_many :memberships, :class_name => 'Member', :as => :principal, :dependent => :destroy + has_many :members, :as => :principal, + :include => [ :project, :role ], + :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", + :order => "#{Project.table_name}.name" + has_many :custom_values, :dependent => :delete_all, :as => :customized + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + + def <=>(group) + name <=> group.name + end + + def to_s + name + end +end diff --git a/groups/app/models/group_custom_field.rb b/groups/app/models/group_custom_field.rb new file mode 100644 index 000000000..8941a4e88 --- /dev/null +++ b/groups/app/models/group_custom_field.rb @@ -0,0 +1,22 @@ +# redMine - project management software +# Copyright (C) 2008 FreeCode +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class GroupCustomField < CustomField + def type_name + :label_group_plural + end +end diff --git a/groups/app/models/member.rb b/groups/app/models/member.rb index b4617c229..b16f6a8ea 100644 --- a/groups/app/models/member.rb +++ b/groups/app/models/member.rb @@ -16,27 +16,52 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class Member < ActiveRecord::Base - belongs_to :user - belongs_to :role belongs_to :project + belongs_to :role + belongs_to :principal, :polymorphic => true + belongs_to :user, :foreign_key => 'principal_id' - validates_presence_of :role, :user, :project - validates_uniqueness_of :user_id, :scope => :project_id - + attr_protected :inherited_from + + validates_presence_of :project, :role, :principal + validates_uniqueness_of :principal_id, :scope => [:project_id, :principal_type, :inherited_from] + def validate errors.add :role_id, :activerecord_error_invalid if role && !role.member? end def name - self.user.name + principal.name end + # Groups sorted by role then users sorted by role def <=>(member) - role == member.role ? (user <=> member.user) : (role <=> member.role) + principal_type == member.principal_type ? + (role == member.role ? principal <=> member.principal : role <=> member.role) : + (principal_type <=> member.principal_type) + end + + def to_s + principal.to_s + end + + def after_save + # Update memberships based on group inheritance + if principal.is_a? Group + Member.delete_all "inherited_from = #{id}" + principal.users.each do |user| + Member.create! :project => project, :role => role, :principal => user, :inherited_from => id + end + end end def before_destroy - # remove category based auto assignments for this member - IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id] + # Remove inherited memberships + Member.delete_all "inherited_from = #{id}" + + # Remove category based auto assignments for this member + if principal.is_a? User + IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project_id, principal_id] + end end end diff --git a/groups/app/models/project.rb b/groups/app/models/project.rb index 964469649..3deac3231 100644 --- a/groups/app/models/project.rb +++ b/groups/app/models/project.rb @@ -20,8 +20,9 @@ class Project < ActiveRecord::Base STATUS_ACTIVE = 1 STATUS_ARCHIVED = 9 - has_many :members, :include => :user, :conditions => "#{User.table_name}.status=#{User::STATUS_ACTIVE}" - has_many :users, :through => :members + has_many :members, :include => :user, :conditions => "#{Member.table_name}.principal_type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}" + has_many :memberships, :class_name => 'Member' + has_many :users, :through => :members, :uniq => true has_many :custom_values, :dependent => :delete_all, :as => :customized has_many :enabled_modules, :dependent => :delete_all has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" diff --git a/groups/app/models/user.rb b/groups/app/models/user.rb index a67a08567..ecc4b569c 100644 --- a/groups/app/models/user.rb +++ b/groups/app/models/user.rb @@ -35,18 +35,25 @@ class User < ActiveRecord::Base :username => '#{login}' } - has_many :memberships, :class_name => 'Member', :include => [ :project, :role ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name", :dependent => :delete_all + has_many :memberships, :class_name => 'Member', + :as => :principal, + :include => [ :project, :role ], + :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", + :order => "#{Project.table_name}.name, inherited_from ASC", + :dependent => :delete_all + has_many :projects, :through => :memberships has_many :custom_values, :dependent => :delete_all, :as => :customized has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify has_one :preference, :dependent => :destroy, :class_name => 'UserPreference' has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'" belongs_to :auth_source + belongs_to :group attr_accessor :password, :password_confirmation attr_accessor :last_before_login_on # Prevents unauthorized assignments - attr_protected :login, :admin, :password, :password_confirmation, :hashed_password + attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_id validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) } validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? } @@ -71,6 +78,23 @@ class User < ActiveRecord::Base # update hashed_password if password was set self.hashed_password = User.hash_password(self.password) if self.password end + + def after_save + if @group_changed + # Update inherited memberships + Member.delete_all "principal_type = 'User' AND principal_id = #{id} AND inherited_from IS NOT NULL" + unless group.nil? + group.memberships.each do |m| + Member.create! :project => m.project, :role => m.role, :principal => self, :inherited_from => m.id + end + end + end + end + + def group_id=(gid) + @group_changed = true unless gid == group_id + write_attribute(:group_id, gid) + end def self.active with_scope :find => { :conditions => [ "status = ?", STATUS_ACTIVE ] } do @@ -167,8 +191,8 @@ class User < ActiveRecord::Base end def notified_project_ids=(ids) - Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id]) - Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty? + Member.update_all("mail_notification = #{connection.quoted_false}", ["principal_type = 'User' AND principal_id = ?", id]) + Member.update_all("mail_notification = #{connection.quoted_true}", ["principal_type = 'User' AND principal_id = ? AND project_id IN (?)", id, ids]) if ids && !ids.empty? @notified_projects_ids = nil notified_projects_ids end diff --git a/groups/app/views/admin/index.rhtml b/groups/app/views/admin/index.rhtml index 18bee34cb..d65b15a9c 100644 --- a/groups/app/views/admin/index.rhtml +++ b/groups/app/views/admin/index.rhtml @@ -8,6 +8,11 @@
+<%= link_to l(:label_group_plural), :controller => 'groups' %> | +<%= link_to l(:label_new), :controller => 'groups', :action => 'new' %> +
+ +<%= link_to l(:label_user_plural), :controller => 'users' %> | <%= link_to l(:label_new), :controller => 'users', :action => 'add' %>
diff --git a/groups/app/views/custom_fields/_form.rhtml b/groups/app/views/custom_fields/_form.rhtml index 5e4eadf21..b3731fac7 100644 --- a/groups/app/views/custom_fields/_form.rhtml +++ b/groups/app/views/custom_fields/_form.rhtml @@ -99,12 +99,9 @@ when "IssueCustomField" %><%= f.check_box :is_filter %>
<%= f.check_box :searchable %>
-<% when "UserCustomField" %> +<% else %><%= f.check_box :is_required %>
- -<% when "ProjectCustomField" %> -<%= f.check_box :is_required %>
- + <% end %> <%= javascript_tag "toggle_custom_field_format();" %> diff --git a/groups/app/views/groups/_form.html.erb b/groups/app/views/groups/_form.html.erb new file mode 100644 index 000000000..18e6f4bc9 --- /dev/null +++ b/groups/app/views/groups/_form.html.erb @@ -0,0 +1,10 @@ +<%= f.text_field :name %>
+ <% for @custom_value in @custom_values %> +<%= custom_field_tag_with_label @custom_value %>
+ <% end %> + + <% unless @group.users.empty? %> +<%= @group.users.collect {|u| link_to_user u }.join(', ') %>
+ <% end %> +<%= f.submit l(:button_save) %>
+<% end %> diff --git a/groups/app/views/groups/index.html.erb b/groups/app/views/groups/index.html.erb new file mode 100644 index 000000000..65f43c970 --- /dev/null +++ b/groups/app/views/groups/index.html.erb @@ -0,0 +1,25 @@ +| <%=l(:label_group)%> | +<%=l(:label_user_plural)%> | ++ |
|---|---|---|
| <%= link_to h(group.name), :action => 'edit', :id => group %> | +<%= group.users.size %> | +<%= link_to l(:button_delete), group, :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %> | +
<%= l(:label_no_data) %>
+<% end %> + +<% form_for(@group) do |f| %> + +<% end %> diff --git a/groups/app/views/groups/new.html.erb b/groups/app/views/groups/new.html.erb new file mode 100644 index 000000000..632156f29 --- /dev/null +++ b/groups/app/views/groups/new.html.erb @@ -0,0 +1,8 @@ +<%= f.submit l(:button_create) %>
+<% end %> diff --git a/groups/app/views/groups/show.html.erb b/groups/app/views/groups/show.html.erb new file mode 100644 index 000000000..e4b74334a --- /dev/null +++ b/groups/app/views/groups/show.html.erb @@ -0,0 +1,7 @@ +| <%= l(:label_user) %> | +<%= l(:label_member) %> | <%= l(:label_role) %> | <% members.each do |member| %> <% next if member.new_record? %> - | |||||||
|---|---|---|---|---|---|---|---|---|---|---|
| <%= member.name %> | -+ | |||||||||
| <%=h member %> | +
<% if authorize_for('members', 'edit') %>
<% remote_form_for(:member, member, :url => {:controller => 'members', :action => 'edit', :id => member}, :method => :post) do |f| %>
<%= f.select :role_id, roles.collect{|role| [role.name, role.id]}, {}, :class => "small" %>
@@ -38,10 +39,10 @@
<%= l(:label_no_data) %> <% end %> -<% if authorize_for('members', 'new') && !users.empty? %> +<% if authorize_for('members', 'new') && !(users.empty? && groups.empty?) %> +<%=l(:label_member_new)%> <% remote_form_for(:member, @member, :url => {:controller => 'members', :action => 'new', :id => @project}, :method => :post) do |f| %> -
<%= principal_select_tag(groups, users) %> <%= l(:label_role) %>: <%= f.select :role_id, roles.collect{|role| [role.name, role.id]}, :selected => nil %> <%= submit_tag l(:button_add) %> <% end %> diff --git a/groups/app/views/users/_form.rhtml b/groups/app/views/users/_form.rhtml index ff4278c1f..f2b330828 100644 --- a/groups/app/views/users/_form.rhtml +++ b/groups/app/views/users/_form.rhtml @@ -7,6 +7,9 @@<%= f.text_field :firstname, :required => true %> <%= f.text_field :lastname, :required => true %> <%= f.text_field :mail, :required => true %> +<% unless @groups.empty? -%> +<%= f.select :group_id, @groups.collect {|g| [g.name, g.id]}, { :include_blank => true } %> +<% end -%><%= f.select :language, lang_options_for_select %> <% for @custom_value in @custom_values %> diff --git a/groups/app/views/users/_memberships.rhtml b/groups/app/views/users/_memberships.rhtml index 2499ba387..06d3f6029 100644 --- a/groups/app/views/users/_memberships.rhtml +++ b/groups/app/views/users/_memberships.rhtml @@ -1,7 +1,7 @@<%= l(:label_project_plural) %>-<% @user.memberships.each do |membership| %> +<% @user.memberships.select {|m| m.inherited_from.nil? }.each do |membership| %> <% form_tag({ :action => 'edit_membership', :id => @user, :membership_id => membership }, :class => "tabular") do %>@@ -13,6 +13,8 @@ <% end %> <% end %> + +<% unless @projects.empty? || @roles.empty? %>
| |||||||||
| <%= link_to user.login, :action => 'edit', :id => user %> | -<%= user.firstname %> | -<%= user.lastname %> | -<%= user.mail %> | +<%=h user.firstname %> | +<%=h user.lastname %> | +<%=h user.mail %> | +<%=h user.group %> | <%= image_tag('true.png') if user.admin? %> | <%= format_time(user.created_on) %> | <%= format_time(user.last_login_on) unless user.last_login_on.nil? %> | diff --git a/groups/config/routes.rb b/groups/config/routes.rb index 0edb71a06..bc4247837 100644 --- a/groups/config/routes.rb +++ b/groups/config/routes.rb @@ -1,4 +1,6 @@ ActionController::Routing::Routes.draw do |map| + map.resources :groups + # Add your own custom routes here. # The priority is based upon order of creation: first created -> highest priority. diff --git a/groups/db/migrate/093_create_groups.rb b/groups/db/migrate/093_create_groups.rb new file mode 100644 index 000000000..ca21ba118 --- /dev/null +++ b/groups/db/migrate/093_create_groups.rb @@ -0,0 +1,11 @@ +class CreateGroups < ActiveRecord::Migration + def self.up + create_table :groups do |t| + t.column :name, :string, :null => false + end + end + + def self.down + drop_table :groups + end +end diff --git a/groups/db/migrate/094_add_users_group_id.rb b/groups/db/migrate/094_add_users_group_id.rb new file mode 100644 index 000000000..12578c683 --- /dev/null +++ b/groups/db/migrate/094_add_users_group_id.rb @@ -0,0 +1,9 @@ +class AddUsersGroupId < ActiveRecord::Migration + def self.up + add_column :users, :group_id, :integer + end + + def self.down + remove_column :users, :group_id + end +end diff --git a/groups/db/migrate/095_change_members_users_association_to_polymorphic.rb b/groups/db/migrate/095_change_members_users_association_to_polymorphic.rb new file mode 100644 index 000000000..a16a19c12 --- /dev/null +++ b/groups/db/migrate/095_change_members_users_association_to_polymorphic.rb @@ -0,0 +1,20 @@ +class ChangeMembersUsersAssociationToPolymorphic < ActiveRecord::Migration + def self.up + add_column :members, :principal_type, :string + add_column :members, :principal_id, :integer + add_column :members, :inherited_from, :integer + Member.update_all "principal_type = 'User', principal_id = user_id" + remove_column :members, :user_id + add_index :members, [:principal_type, :principal_id], :name => :members_principal + end + + def self.down + # Remove inherited memberships + Member.delete_all "inherited_from IS NOT NULL" + add_column :members, :user_id, :integer, :default => 0, :null => false + Member.update_all "user_id = principal_id" + remove_column :members, :principal_type, :string + remove_column :members, :principal_id, :integer + remove_column :members, :inherited_from, :integer + end +end diff --git a/groups/lang/en.yml b/groups/lang/en.yml index e39aec301..320501c2b 100644 --- a/groups/lang/en.yml +++ b/groups/lang/en.yml @@ -179,6 +179,7 @@ field_time_zone: Time zone field_searchable: Searchable field_default_value: Default value field_comments_sorting: Display comments +field_group: Group setting_app_title: Application title setting_app_subtitle: Application subtitle @@ -510,6 +511,9 @@ label_preferences: Preferences label_chronological_order: In chronological order label_reverse_chronological_order: In reverse chronological order label_planning: Planning +label_group: Group +label_group_plural: Groups +label_group_new: New group button_login: Login button_submit: Submit diff --git a/groups/public/images/22x22/user.png b/groups/public/images/22x22/user.png new file mode 100644 index 000000000..77f0f9c19 Binary files /dev/null and b/groups/public/images/22x22/user.png differ diff --git a/groups/public/images/22x22/users.png b/groups/public/images/22x22/users.png index 92f396207..e7b80580c 100644 Binary files a/groups/public/images/22x22/users.png and b/groups/public/images/22x22/users.png differ diff --git a/groups/public/stylesheets/application.css b/groups/public/stylesheets/application.css index 26f66f0b8..8169beb49 100644 --- a/groups/public/stylesheets/application.css +++ b/groups/public/stylesheets/application.css @@ -105,12 +105,19 @@ tr.message td.last_message { font-size: 80%; } tr.message.locked td.subject a { background-image: url(../images/locked.png); } tr.message.sticky td.subject a { background-image: url(../images/sticky.png); font-weight: bold; } +table.members td { vertical-align: middle; } +table.members td.name { padding-left: 20px; } +table.members tr.group td.name { background: url(../images/users.png) 0 50% no-repeat; } +table.members tr.user td.name { background: url(../images/user.png) 0 50% no-repeat; } + tr.user td { width:13%; } tr.user td.email { width:18%; } tr.user td { white-space: nowrap; } tr.user.locked, tr.user.registered { color: #aaa; } tr.user.locked a, tr.user.registered a { color: #aaa; } +optgroup { font-style: normal; } + tr.time-entry { text-align: center; white-space: nowrap; } tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; } td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; } @@ -574,6 +581,7 @@ vertical-align: middle; .icon22-projects { background-image: url(../images/22x22/projects.png); } .icon22-users { background-image: url(../images/22x22/users.png); } +.icon22-user { background-image: url(../images/22x22/user.png); } .icon22-tracker { background-image: url(../images/22x22/tracker.png); } .icon22-role { background-image: url(../images/22x22/role.png); } .icon22-workflow { background-image: url(../images/22x22/workflow.png); } diff --git a/groups/test/fixtures/groups.yml b/groups/test/fixtures/groups.yml new file mode 100644 index 000000000..792f69755 --- /dev/null +++ b/groups/test/fixtures/groups.yml @@ -0,0 +1,7 @@ +managers: + id: 1 + name: Managers +clients: + id: 2 + name: Clients + \ No newline at end of file diff --git a/groups/test/fixtures/members.yml b/groups/test/fixtures/members.yml index 2c9209131..4e0a5a739 100644 --- a/groups/test/fixtures/members.yml +++ b/groups/test/fixtures/members.yml @@ -4,24 +4,43 @@ members_001: project_id: 1 role_id: 1 id: 1 - user_id: 2 + principal_type: User + principal_id: 2 members_002: created_on: 2006-07-19 19:35:36 +02:00 project_id: 1 role_id: 2 id: 2 - user_id: 3 + principal_type: User + principal_id: 3 members_003: created_on: 2006-07-19 19:35:36 +02:00 project_id: 2 role_id: 2 id: 3 - user_id: 2 + principal_type: User + principal_id: 2 members_004: id: 4 created_on: 2006-07-19 19:35:36 +02:00 project_id: 1 role_id: 2 # Locked user - user_id: 5 + principal_type: User + principal_id: 5 +members_005: + id: 5 + created_on: 2008-01-19 19:35:36 +02:00 + project_id: 1 + role_id: 3 + principal_type: Group + principal_id: 2 # Clients +members_006: + id: 6 + created_on: 2008-01-19 19:35:36 +02:00 + project_id: 1 + role_id: 3 + principal_type: User + principal_id: 7 + inherited_from: 5 \ No newline at end of file diff --git a/groups/test/fixtures/roles.yml b/groups/test/fixtures/roles.yml index 1ede6fca9..95240e846 100644 --- a/groups/test/fixtures/roles.yml +++ b/groups/test/fixtures/roles.yml @@ -1,5 +1,5 @@ --- -roles_001: +manager: name: Manager id: 1 builtin: 0 @@ -41,7 +41,7 @@ roles_001: - :view_changesets position: 1 -roles_002: +developer: name: Developer id: 2 builtin: 0 @@ -78,7 +78,7 @@ roles_002: - :view_changesets position: 2 -roles_003: +reporter: name: Reporter id: 3 builtin: 0 @@ -113,7 +113,7 @@ roles_003: - :view_changesets position: 3 -roles_004: +non_member: name: Non member id: 4 builtin: 1 @@ -141,7 +141,7 @@ roles_004: - :view_changesets position: 4 -roles_005: +anonymous: name: Anonymous id: 5 builtin: 2 diff --git a/groups/test/fixtures/users.yml b/groups/test/fixtures/users.yml index de3553173..17f91dbfe 100644 --- a/groups/test/fixtures/users.yml +++ b/groups/test/fixtures/users.yml @@ -96,5 +96,38 @@ users_006: mail_notification: false login: '' type: AnonymousUser +client: + id: 7 + created_on: 2006-07-19 19:33:19 +02:00 + status: 1 + last_login_on: + language: '' + hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415 + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: 'client@foo.bar' + lastname: User + firstname: Client + auth_source_id: + mail_notification: false + login: client + type: User + group_id: 2 # Clients +new_client: + id: 8 + created_on: 2006-07-19 19:33:19 +02:00 + status: 1 + last_login_on: + language: '' + hashed_password: 7feb7657aa7a7bf5aef3414a5084875f27192415 + updated_on: 2006-07-19 19:33:19 +02:00 + admin: false + mail: 'newclient@foo.bar' + lastname: New + firstname: Client + auth_source_id: + mail_notification: false + login: newclient + type: User \ No newline at end of file diff --git a/groups/test/functional/groups_controller_test.rb b/groups/test/functional/groups_controller_test.rb new file mode 100644 index 000000000..da3a4b940 --- /dev/null +++ b/groups/test/functional/groups_controller_test.rb @@ -0,0 +1,76 @@ +# redMine - project management software +# Copyright (C) 2008 FreeCode +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'groups_controller' + +# Re-raise errors caught by the controller. +class GroupsController; def rescue_action(e) raise e end; end + +class GroupsControllerTest < Test::Unit::TestCase + fixtures :groups, :users + + def setup + @controller = GroupsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_should_get_index + get :index + assert_response :success + assert_not_nil assigns(:groups) + end + + def test_should_get_new + get :new + assert_response :success + end + + def test_should_create_group + assert_difference('Group.count') do + post :create, :group => { :name => 'New group' } + end + assert_redirected_to groups_path + assert_not_nil Group.find_by_name('New group') + end + + def test_should_show_group + get :show, :id => 1 + assert_response :success + end + + def test_should_get_edit + get :edit, :id => 1 + assert_response :success + end + + def test_should_update_group + put :update, :id => 1, :group => { :name => 'Renamed' } + assert_redirected_to groups_path + assert_equal 'Renamed', Group.find(1).name + end + + def test_should_destroy_group + assert_difference('Group.count', -1) do + delete :destroy, :id => 1 + end + assert_redirected_to groups_path + end +end diff --git a/groups/test/functional/members_controller_test.rb b/groups/test/functional/members_controller_test.rb new file mode 100644 index 000000000..851bda299 --- /dev/null +++ b/groups/test/functional/members_controller_test.rb @@ -0,0 +1,80 @@ +# Redmine - project management software +# Copyright (C) 2008 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' +require 'members_controller' + +# Re-raise errors caught by the controller. +class MembersController; def rescue_action(e) raise e end; end + +class MembersControllerTest < Test::Unit::TestCase + fixtures :projects, :roles, :users, :groups, :members + + def setup + @controller = MembersController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + @request.session[:user_id] = 1 # admin + end + + def test_should_create_user_member + p = Project.find(1) + u = users(:new_client) + assert_difference('Member.count') do + post :new, :id => p.id, :member => { :role_id => roles(:reporter).id }, :principal => "user_#{u.id}" + end + assert_redirected_to :controller => 'projects', :action => 'settings', :id => p, :tab => 'members' + assert u.reload.member_of?(p) + assert_equal roles(:reporter), u.role_for_project(p) + end + + def test_should_create_group_member + p = Project.find(2) + assert_difference('Member.count') do + post :new, :id => p.id, :member => { :role_id => roles(:reporter) }, :principal => 'group_1' + end + assert_redirected_to :controller => 'projects', :action => 'settings', :id => p, :tab => 'members' + end + + def test_should_update_user_member + u = User.find(3) + p = Project.find(1) + assert_equal roles(:developer), u.role_for_project(p) + assert_difference('Member.count', 0) do + post :edit, :id => 2, :member => { :role_id => roles(:manager).id } + end + assert_redirected_to :controller => 'projects', :action => 'settings', :id => p, :tab => 'members' + assert_equal roles(:manager), u.reload.role_for_project(p) + end + + def test_should_destroy + p = Project.find(1) + assert_difference('Member.count', -1) do + post :destroy, :id => 2 + end + assert_redirected_to :controller => 'projects', :action => 'settings', :id => p, :tab => 'members' + end + + def test_should_not_destroy_inherited_membership + p = Project.find(1) + assert_difference('Member.count', 0) do + post :destroy, :id => 6 + end + assert_response 404 + end +end diff --git a/groups/test/functional/users_controller_test.rb b/groups/test/functional/users_controller_test.rb index 8629a7131..84c10eb7d 100644 --- a/groups/test/functional/users_controller_test.rb +++ b/groups/test/functional/users_controller_test.rb @@ -47,6 +47,14 @@ class UsersControllerTest < Test::Unit::TestCase assert_nil assigns(:users).detect {|u| !u.active?} end + def test_should_add_membership + assert_difference('User.find(2).memberships.count') do + post :edit_membership, :id => 2, :membership => { :role_id => 1, :project_id => 3 } + assert_redirected_to 'users/edit/2' + assert User.find(2).member_of?(Project.find(3)) + end + end + def test_edit_membership post :edit_membership, :id => 2, :membership_id => 1, :membership => { :role_id => 2} diff --git a/groups/test/unit/group_test.rb b/groups/test/unit/group_test.rb new file mode 100644 index 000000000..d93d2b61d --- /dev/null +++ b/groups/test/unit/group_test.rb @@ -0,0 +1,129 @@ +# redMine - project management software +# Copyright (C) 2008 FreeCode +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.dirname(__FILE__) + '/../test_helper' + +class GroupTest < ActiveSupport::TestCase + fixtures :groups, :users, :projects, :roles, :members + + def test_should_validate_presence_of_name + g = Group.new(:name => '') + assert !g.save + assert_equal 1, g.errors.size + end + + def test_should_validate_uniqueness_of_name + g = Group.new(:name => groups(:clients).name) + assert !g.save + assert_equal 1, g.errors.size + end + + def test_should_create + g = Group.new(:name => 'New group') + assert g.save + assert g.users.empty? + end + + def test_should_destroy + g = groups(:clients) + p = Project.find(1) + u = users(:client) + assert u.member_of?(p) + + assert_difference('Member.count', -2) do + g.destroy + end + u.reload + assert_nil u.group + assert !u.member_of?(p) + end + + def test_should_add_user_to_group + g = groups(:clients) + p = Project.find(1) + u = users(:new_client) + r = roles(:reporter) + assert !u.member_of?(p) + + assert_difference('Member.count') do + assert_difference('g.reload.users.size') do + u.group_id = g.id + assert u.save + end + end + u.reload + assert u.group = g + assert u.member_of?(p) + assert_equal r, u.role_for_project(p) + end + + def test_should_add_group_to_project + g = groups(:clients) + p = Project.find(2) + u = users(:client) + r = roles(:reporter) + assert !u.member_of?(p) + + assert_difference('Member.count', 2) do + assert_difference('p.reload.users.size') do + m = Member.new(:project => p, :principal => g, :role => r) + assert m.save + end + end + u.reload + assert u.member_of?(p) + assert_equal r, u.role_for_project(p) + end + + def test_should_remove_user_from_group + g = groups(:clients) + p = Project.find(1) + u = users(:client) + assert u.member_of?(p) + + assert_difference('Member.count', -1) do + assert_difference('g.reload.users.size', -1) do + u.group_id = nil + assert u.save + end + end + end + + def test_should_override_group_role + g = groups(:clients) + p = Project.find(1) + u = users(:client) + assert u.member_of?(p) + assert_equal roles(:reporter), u.role_for_project(p) + + assert_difference('Member.count', 1) do + assert_difference('p.reload.users.size', 0) do + m = Member.new(:project => p, :principal => u, :role => roles(:manager)) + assert m.save + end + end + assert_equal roles(:manager), u.reload.role_for_project(p) + + # Remove the group, user should still be a member + assert_difference('Member.count', -2) do + assert_difference('p.reload.users.size', 0) do + assert g.destroy + end + end + assert_equal roles(:manager), u.reload.role_for_project(p) + end +end diff --git a/groups/test/unit/member_test.rb b/groups/test/unit/member_test.rb index 079782306..62b1776ec 100644 --- a/groups/test/unit/member_test.rb +++ b/groups/test/unit/member_test.rb @@ -25,7 +25,7 @@ class MemberTest < Test::Unit::TestCase end def test_create - member = Member.new(:project_id => 1, :user_id => 4, :role_id => 1) + member = Member.new(:project_id => 1, :principal_type => 'User', :principal_id => 4, :role_id => 1) assert member.save end @@ -39,7 +39,7 @@ class MemberTest < Test::Unit::TestCase end def test_validate - member = Member.new(:project_id => 1, :user_id => 2, :role_id =>2) + member = Member.new(:project_id => 1, :principal_type => 'User', :principal_id => 2, :role_id =>2) # same use can't have more than one role for a project assert !member.save end diff --git a/groups/test/unit/project_test.rb b/groups/test/unit/project_test.rb index 9af68c231..60106bc07 100644 --- a/groups/test/unit/project_test.rb +++ b/groups/test/unit/project_test.rb @@ -80,10 +80,10 @@ class ProjectTest < Test::Unit::TestCase end def test_destroy - # 2 active members - assert_equal 2, @ecookbook.members.size + # 3 active members + assert_equal 3, @ecookbook.members.size # and 1 is locked - assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size + assert_equal 5, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size # some boards assert @ecookbook.boards.any?