diff options
28 files changed, 571 insertions, 343 deletions
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 45ed4c4e1..d67f0382b 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -23,6 +23,7 @@ class GroupsController < ApplicationController accept_api_auth :index, :show, :create, :update, :destroy, :add_users, :remove_user helper :custom_fields + helper :principal_memberships def index respond_to do |format| @@ -119,23 +120,6 @@ class GroupsController < ApplicationController end end - def edit_membership - @membership = Member.edit_membership(params[:membership_id], params[:membership], @group) - @membership.save if request.post? - respond_to do |format| - format.html { redirect_to edit_group_path(@group, :tab => 'memberships') } - format.js - end - end - - def destroy_membership - Member.find(params[:membership_id]).destroy if request.post? - respond_to do |format| - format.html { redirect_to edit_group_path(@group, :tab => 'memberships') } - format.js - end - end - private def find_group diff --git a/app/controllers/principal_memberships_controller.rb b/app/controllers/principal_memberships_controller.rb new file mode 100644 index 000000000..5af897b6e --- /dev/null +++ b/app/controllers/principal_memberships_controller.rb @@ -0,0 +1,80 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class PrincipalMembershipsController < ApplicationController + layout 'admin' + + before_filter :require_admin + before_filter :find_principal, :only => [:new, :create] + before_filter :find_membership, :only => [:update, :destroy] + + def new + @projects = Project.active.all + @roles = Role.find_all_givable + respond_to do |format| + format.html + format.js + end + end + + def create + @members = Member.create_principal_memberships(@principal, params[:membership]) + respond_to do |format| + format.html { redirect_to_principal @principal } + format.js + end + end + + def update + @membership.attributes = params[:membership] + @membership.save + respond_to do |format| + format.html { redirect_to_principal @principal } + format.js + end + end + + def destroy + if @membership.deletable? + @membership.destroy + end + respond_to do |format| + format.html { redirect_to_principal @principal } + format.js + end + end + + private + + def find_principal + principal_id = params[:user_id] || params[:group_id] + @principal = Principal.find(principal_id) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_membership + @membership = Member.find(params[:id]) + @principal = @membership.principal + rescue ActiveRecord::RecordNotFound + render_404 + end + + def redirect_to_principal(principal) + redirect_to edit_polymorphic_path(principal, :tab => 'memberships') + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index bb56fb285..d14914af4 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -19,13 +19,14 @@ class UsersController < ApplicationController layout 'admin' before_filter :require_admin, :except => :show - before_filter :find_user, :only => [:show, :edit, :update, :destroy, :edit_membership, :destroy_membership] + before_filter :find_user, :only => [:show, :edit, :update, :destroy] accept_api_auth :index, :show, :create, :update, :destroy helper :sort include SortHelper helper :custom_fields include CustomFieldsHelper + helper :principal_memberships def index sort_init 'login', 'asc' @@ -173,26 +174,6 @@ class UsersController < ApplicationController end end - def edit_membership - @membership = Member.edit_membership(params[:membership_id], params[:membership], @user) - @membership.save - respond_to do |format| - format.html { redirect_to edit_user_path(@user, :tab => 'memberships') } - format.js - end - end - - def destroy_membership - @membership = Member.find(params[:membership_id]) - if @membership.deletable? - @membership.destroy - end - respond_to do |format| - format.html { redirect_to edit_user_path(@user, :tab => 'memberships') } - format.js - end - end - private def find_user diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7e9cdce90..923dff581 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -252,7 +252,7 @@ module ApplicationHelper # Renders a tree of projects as a nested set of unordered lists # The given collection may be a subset of the whole project tree # (eg. some intermediate nodes are private and can not be seen) - def render_project_nested_lists(projects) + def render_project_nested_lists(projects, &block) s = '' if projects.any? ancestors = [] @@ -272,7 +272,7 @@ module ApplicationHelper end classes = (ancestors.empty? ? 'root' : 'child') s << "<li class='#{classes}'><div class='#{classes}'>" - s << h(block_given? ? yield(project) : project.name) + s << h(block_given? ? capture(project, &block) : project.name) s << "</div>\n" ancestors << project end diff --git a/app/helpers/principal_memberships_helper.rb b/app/helpers/principal_memberships_helper.rb new file mode 100644 index 000000000..e734f42c9 --- /dev/null +++ b/app/helpers/principal_memberships_helper.rb @@ -0,0 +1,56 @@ +# encoding: utf-8 +# +# Redmine - project management software +# Copyright (C) 2006-2014 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +module PrincipalMembershipsHelper + def render_principal_memberships(principal) + render :partial => 'principal_memberships/index', :locals => {:principal => principal} + end + + def call_table_header_hook(principal) + if principal.is_a?(Group) + call_hook :view_groups_memberships_table_header, :group => principal + else + call_hook :view_users_memberships_table_header, :user => principal + end + end + + def call_table_row_hook(principal, membership) + if principal.is_a?(Group) + call_hook :view_groups_memberships_table_row, :group => principal, :membership => membership + else + call_hook :view_users_memberships_table_row, :user => principal, :membership => membership + end + end + + def new_principal_membership_path(principal, *args) + if principal.is_a?(Group) + new_group_membership_path(principal, *args) + else + new_user_membership_path(principal, *args) + end + end + + def principal_membership_path(principal, membership, *args) + if principal.is_a?(Group) + group_membership_path(principal, membership, *args) + else + user_membership_path(principal, membership, *args) + end + end +end diff --git a/app/models/member.rb b/app/models/member.rb index 8256d2e68..257178866 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -47,7 +47,7 @@ class Member < ActiveRecord::Base new_role_ids = ids - role_ids # Add new roles - new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id) } + new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id, :member => self) } # Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy) member_roles_to_destroy = member_roles.select {|mr| !ids.include?(mr.role_id)} if member_roles_to_destroy.any? @@ -102,11 +102,23 @@ class Member < ActiveRecord::Base end end - # Find or initialize a Member with an id, attributes, and for a Principal - def self.edit_membership(id, new_attributes, principal=nil) - @membership = id.present? ? Member.find(id) : Member.new(:principal => principal) - @membership.attributes = new_attributes - @membership + # Creates memberships for principal with the attributes + # * project_ids : one or more project ids + # * role_ids : ids of the roles to give to each membership + # + # Example: + # Member.create_principal_memberships(user, :project_ids => [2, 5], :role_ids => [1, 3] + def self.create_principal_memberships(principal, attributes) + members = [] + if attributes + project_ids = Array.wrap(attributes[:project_ids] || attributes[:project_id]) + role_ids = attributes[:role_ids] + project_ids.each do |project_id| + members << Member.new(:principal => principal, :role_ids => role_ids, :project_id => project_id) + end + principal.members << members + end + members end # Finds or initilizes a Member for the given project and principal diff --git a/app/models/principal.rb b/app/models/principal.rb index d10241b3f..e6e6ea78e 100644 --- a/app/models/principal.rb +++ b/app/models/principal.rb @@ -84,6 +84,11 @@ class Principal < ActiveRecord::Base to_s end + # Return true if the principal is a member of project + def member_of?(project) + projects.to_a.include?(project) + end + def <=>(principal) if principal.nil? -1 diff --git a/app/models/user.rb b/app/models/user.rb index d86627e85..3ac98620b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -498,11 +498,6 @@ class User < Principal end end - # Return true if the user is a member of project - def member_of?(project) - projects.to_a.include?(project) - end - # Returns a hash of user's projects grouped by roles def projects_by_role return @projects_by_role if @projects_by_role diff --git a/app/views/groups/_memberships.html.erb b/app/views/groups/_memberships.html.erb index ec275c594..1242bf6e8 100644 --- a/app/views/groups/_memberships.html.erb +++ b/app/views/groups/_memberships.html.erb @@ -1,65 +1 @@ -<% roles = Role.find_all_givable %> -<% projects = Project.active.to_a %> - -<div class="splitcontentleft"> -<% if @group.memberships.any? %> -<table class="list memberships"> - <thead><tr> - <th><%= l(:label_project) %></th> - <th><%= l(:label_role_plural) %></th> - <th style="width:15%"></th> - </tr></thead> - <tbody> - <% @group.memberships.each do |membership| %> - <% next if membership.new_record? %> - <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class"> - <td class="project"><%= link_to_project membership.project %></td> - <td class="roles"> - <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span> - <%= form_for(:membership, :remote => true, - :url => { :action => 'edit_membership', :id => @group, :membership_id => membership }, - :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %> - <p><% roles.each do |role| %> - <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role), :id => nil %> <%=h role %></label><br /> - <% end %></p> - <p><%= submit_tag l(:button_change) %> - <%= link_to_function( - l(:button_cancel), - "$('#member-#{membership.id}-roles').show(); $('#member-#{membership.id}-roles-form').hide(); return false;" - ) %></p> - <% end %> - </td> - <td class="buttons"> - <%= link_to_function( - l(:button_edit), - "$('#member-#{membership.id}-roles').hide(); $('#member-#{membership.id}-roles-form').show(); return false;", - :class => 'icon icon-edit' - ) %> - <%= delete_link({:controller => 'groups', :action => 'destroy_membership', :id => @group, :membership_id => membership}, - :remote => true, - :method => :post) %> - </td> - </tr> -<% end; reset_cycle %> - </tbody> -</table> -<% else %> -<p class="nodata"><%= l(:label_no_data) %></p> -<% end %> -</div> - -<div class="splitcontentright"> -<% if projects.any? %> -<fieldset><legend><%=l(:label_project_new)%></legend> -<%= form_for(:membership, :remote => true, :url => { :action => 'edit_membership', :id => @group }) do %> -<%= label_tag "membership_project_id", l(:description_choose_project), :class => "hidden-for-sighted" %> -<%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %> -<p><%= l(:label_role_plural) %>: -<% roles.each do |role| %> - <label><%= check_box_tag 'membership[role_ids][]', role.id, false, :id => nil %> <%=h role %></label> -<% end %></p> -<p><%= submit_tag l(:button_add) %></p> -<% end %> -</fieldset> -<% end %> -</div> +<%= render_principal_memberships @group %> diff --git a/app/views/principal_memberships/_index.html.erb b/app/views/principal_memberships/_index.html.erb new file mode 100644 index 000000000..8203999ad --- /dev/null +++ b/app/views/principal_memberships/_index.html.erb @@ -0,0 +1,52 @@ +<% roles = Role.find_all_givable %> + +<p><%= link_to l(:label_add_projects), new_principal_membership_path(principal), :remote => true, :class => "icon icon-add" %></p> + +<% if principal.memberships.any? %> +<table class="list memberships"> + <thead><tr> + <th><%= l(:label_project) %></th> + <th><%= l(:label_role_plural) %></th> + <th style="width:15%"></th> + <%= call_table_header_hook principal %> + </tr></thead> + <tbody> + <% principal.memberships.preload(:member_roles => :role).each do |membership| %> + <% next if membership.new_record? %> + <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class"> + <td class="project name"> + <%= link_to_project membership.project %> + </td> + <td class="roles"> + <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span> + <%= form_for(:membership, :remote => true, + :url => principal_membership_path(principal, membership), :method => :put, + :html => {:id => "member-#{membership.id}-roles-form", + :style => 'display:none;'}) do %> + <p><% roles.each do |role| %> + <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role), + :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?}, + :id => nil %> <%=h role %></label><br /> + <% end %></p> + <%= hidden_field_tag 'membership[role_ids][]', '' %> + <p><%= submit_tag l(:button_change) %> + <%= link_to_function l(:button_cancel), + "$('#member-#{membership.id}-roles').show(); $('#member-#{membership.id}-roles-form').hide(); return false;" + %></p> + <% end %> + </td> + <td class="buttons"> + <%= link_to_function l(:button_edit), + "$('#member-#{membership.id}-roles').hide(); $('#member-#{membership.id}-roles-form').show(); return false;", + :class => 'icon icon-edit' + %> + <%= delete_link principal_membership_path(principal, membership), :remote => true if membership.deletable? %> + </td> + <%= call_table_row_hook principal, membership %> + </tr> + <% end; reset_cycle %> + </tbody> +</table> +<% else %> +<p class="nodata"><%= l(:label_no_data) %></p> +<% end %> diff --git a/app/views/principal_memberships/_new_form.html.erb b/app/views/principal_memberships/_new_form.html.erb new file mode 100644 index 000000000..bdd7df64c --- /dev/null +++ b/app/views/principal_memberships/_new_form.html.erb @@ -0,0 +1,22 @@ +<fieldset class="box"> + <legend><%= l(:label_project_plural) %></legend> + <div style="max-height:300px; overflow:auto;"> + <div class="projects-selection"> + <%= render_project_nested_lists(@projects) do |p| %> + <label> + <%= check_box_tag('membership[project_ids][]', p.id, false, :id => nil, :disabled => @principal.member_of?(p)) %> <%= p %> + </label> + <% end %> + </div> + </div> +</fieldset> + +<fieldset class="box"> + <legend><%= l(:label_role_plural) %></legend> + <% @roles.each do |role| %> + <label class="inline"> + <%= check_box_tag 'membership[role_ids][]', role.id, false, :id => nil %> + <%=h role %> + </label> + <% end %> +</fieldset> diff --git a/app/views/principal_memberships/_new_modal.html.erb b/app/views/principal_memberships/_new_modal.html.erb new file mode 100644 index 000000000..175e09330 --- /dev/null +++ b/app/views/principal_memberships/_new_modal.html.erb @@ -0,0 +1,9 @@ +<h3 class="title"><%= l(:label_add_projects) %></h3> + +<%= form_for :membership, :remote => true, :url => user_memberships_path(@principal), :method => :post do |f| %> + <%= render :partial => 'new_form' %> + <p class="buttons"> + <%= submit_tag l(:button_add), :name => nil %> + <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %> + </p> +<% end %> diff --git a/app/views/principal_memberships/create.js.erb b/app/views/principal_memberships/create.js.erb new file mode 100644 index 000000000..17d7ee14d --- /dev/null +++ b/app/views/principal_memberships/create.js.erb @@ -0,0 +1,12 @@ +$('#tab-content-memberships').html('<%= escape_javascript(render :partial => 'principal_memberships/index', :locals => {:principal => @principal}) %>'); +hideOnLoad(); + +<% if @members.present? && @members.all? {|m| m.persisted? } %> + hideModal(); + <% @members.each do |member| %> + $("#member-<%= member.id %>").effect("highlight"); + <% end %> +<% elsif @members.present? %> + <% errors = @members.collect {|m| m.errors.full_messages}.flatten.uniq.join(', ') %> + alert('<%= raw(escape_javascript(l(:notice_failed_to_save_members, :errors => errors))) %>'); +<% end %> diff --git a/app/views/principal_memberships/destroy.js.erb b/app/views/principal_memberships/destroy.js.erb new file mode 100644 index 000000000..c8564f459 --- /dev/null +++ b/app/views/principal_memberships/destroy.js.erb @@ -0,0 +1 @@ +$('#tab-content-memberships').html('<%= escape_javascript(render :partial => 'principal_memberships/index', :locals => {:principal => @principal}) %>'); diff --git a/app/views/principal_memberships/new.html.erb b/app/views/principal_memberships/new.html.erb new file mode 100644 index 000000000..64d2ebe21 --- /dev/null +++ b/app/views/principal_memberships/new.html.erb @@ -0,0 +1,6 @@ +<h2><%= l(:label_add_projects) %></h2> + +<%= form_for :membership, :url => user_memberships_path(@principal), :method => :post do |f| %> + <%= render :partial => 'new_form' %> + <p><%= submit_tag l(:button_add), :name => nil %></p> +<% end %> diff --git a/app/views/principal_memberships/new.js.erb b/app/views/principal_memberships/new.js.erb new file mode 100644 index 000000000..625eeaf68 --- /dev/null +++ b/app/views/principal_memberships/new.js.erb @@ -0,0 +1,13 @@ +$('#ajax-modal').html('<%= escape_javascript(render :partial => 'principal_memberships/new_modal') %>'); +showModal('ajax-modal', '700px'); + +$('.projects-selection').on('click', 'input[type=checkbox]', function(e){ + if (!$(this).is(':checked')) { + if ($(this).closest('li').find('ul input[type=checkbox]:not(:checked)').length > 0) { + $(this).closest('li').find('ul input[type=checkbox]:not(:checked)').attr('checked', 'checked'); + e.preventDefault(); + } else { + $(this).closest('li').find('ul input[type=checkbox]:checked').removeAttr('checked'); + } + } +}); diff --git a/app/views/principal_memberships/update.js.erb b/app/views/principal_memberships/update.js.erb new file mode 100644 index 000000000..2986c4e55 --- /dev/null +++ b/app/views/principal_memberships/update.js.erb @@ -0,0 +1,6 @@ +<% if @membership.valid? %> + $('#tab-content-memberships').html('<%= escape_javascript(render :partial => 'principal_memberships/index', :locals => {:principal => @principal}) %>'); + $("#member-<%= @membership.id %>").effect("highlight"); +<% else %> + alert('<%= raw(escape_javascript(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))) %>'); +<% end %> diff --git a/app/views/users/_memberships.html.erb b/app/views/users/_memberships.html.erb index cd4201727..75871d63e 100644 --- a/app/views/users/_memberships.html.erb +++ b/app/views/users/_memberships.html.erb @@ -1,68 +1 @@ -<% roles = Role.find_all_givable %> -<% projects = Project.active.to_a %> - -<div class="splitcontentleft"> -<% if @user.memberships.any? %> -<table class="list memberships"> - <thead><tr> - <th><%= l(:label_project) %></th> - <th><%= l(:label_role_plural) %></th> - <th style="width:15%"></th> - <%= call_hook(:view_users_memberships_table_header, :user => @user )%> - </tr></thead> - <tbody> - <% @user.memberships.each do |membership| %> - <% next if membership.new_record? %> - <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class"> - <td class="project"> - <%= link_to_project membership.project %> - </td> - <td class="roles"> - <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span> - <%= form_for(:membership, :remote => true, - :url => user_membership_path(@user, membership), :method => :put, - :html => {:id => "member-#{membership.id}-roles-form", - :style => 'display:none;'}) do %> - <p><% roles.each do |role| %> - <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role), - :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?}, - :id => nil %> <%=h role %></label><br /> - <% end %></p> - <%= hidden_field_tag 'membership[role_ids][]', '' %> - <p><%= submit_tag l(:button_change) %> - <%= link_to_function l(:button_cancel), - "$('#member-#{membership.id}-roles').show(); $('#member-#{membership.id}-roles-form').hide(); return false;" - %></p> - <% end %> - </td> - <td class="buttons"> - <%= link_to_function l(:button_edit), - "$('#member-#{membership.id}-roles').hide(); $('#member-#{membership.id}-roles-form').show(); return false;", - :class => 'icon icon-edit' - %> - <%= delete_link user_membership_path(@user, membership), :remote => true if membership.deletable? %> - </td> - <%= call_hook(:view_users_memberships_table_row, :user => @user, :membership => membership, :roles => roles, :projects => projects )%> - </tr> - <% end; reset_cycle %> - </tbody> -</table> -<% else %> -<p class="nodata"><%= l(:label_no_data) %></p> -<% end %> -</div> - -<div class="splitcontentright"> -<% if projects.any? %> -<fieldset><legend><%=l(:label_project_new)%></legend> -<%= form_for(:membership, :remote => true, :url => user_memberships_path(@user)) do %> -<%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, projects) %> -<p><%= l(:label_role_plural) %>: -<% roles.each do |role| %> - <label><%= check_box_tag 'membership[role_ids][]', role.id, false, :id => nil %> <%=h role %></label> -<% end %></p> -<p><%= submit_tag l(:button_add) %></p> -<% end %> -</fieldset> -<% end %> -</div> +<%= render_principal_memberships @user %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 8f858162c..3799db2bf 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -918,6 +918,7 @@ en: label_check_for_updates: Check for updates label_latest_compatible_version: Latest compatible version label_unknown_plugin: Unknown plugin + label_add_projects: Add projects button_login: Login button_submit: Submit diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 0f2e0d0e6..1b106ffe7 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -938,6 +938,7 @@ fr: label_check_for_updates: Vérifier les mises à jour label_latest_compatible_version: Dernière version compatible label_unknown_plugin: Plugin inconnu + label_add_projects: Ajouter des projets button_login: Connexion button_submit: Soumettre diff --git a/config/routes.rb b/config/routes.rb index a988eb311..b7f1f7984 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -73,10 +73,9 @@ Rails.application.routes.draw do match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post - resources :users - match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => [:put, :patch], :as => 'user_membership' - match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete - match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships' + resources :users do + resources :memberships, :controller => 'principal_memberships' + end post 'watchers/watch', :to => 'watchers#watch', :as => 'watch' delete 'watchers/watch', :to => 'watchers#unwatch' @@ -270,6 +269,7 @@ Rails.application.routes.draw do resources :attachments, :only => [:show, :destroy] resources :groups do + resources :memberships, :controller => 'principal_memberships' member do get 'autocomplete_for_user' end @@ -277,8 +277,6 @@ Rails.application.routes.draw do match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users' match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user' - match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post - match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post resources :trackers, :except => :show do collection do diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 19507fced..e60cf8d6d 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -626,6 +626,19 @@ input.autocomplete.ajax-loading { .role-visibility {padding-left:2em;} +.projects-selection { + column-count: auto; + column-width: 200px; + -webkit-column-count: auto; + -webkit-column-width: 200px; + -webkit-column-gap : 0.5rem; + -webkit-column-rule: 1px solid #ccc; + -moz-column-count: auto; + -moz-column-width: 200px; + -moz-column-gap : 0.5rem; + -moz-column-rule: 1px solid #ccc; +} + /***** Flash & error messages ****/ #errorExplanation, div.flash, .nodata, .warning, .conflict { padding: 4px 4px 4px 30px; diff --git a/test/functional/groups_controller_test.rb b/test/functional/groups_controller_test.rb index d1a2b6729..778b3f3cd 100644 --- a/test/functional/groups_controller_test.rb +++ b/test/functional/groups_controller_test.rb @@ -144,62 +144,6 @@ class GroupsControllerTest < ActionController::TestCase end end - def test_new_membership - assert_difference 'Group.find(10).members.count' do - post :edit_membership, :id => 10, :membership => { :project_id => 2, :role_ids => ['1', '2']} - end - end - - def test_xhr_new_membership - assert_difference 'Group.find(10).members.count' do - xhr :post, :edit_membership, :id => 10, :membership => { :project_id => 2, :role_ids => ['1', '2']} - assert_response :success - assert_template 'edit_membership' - assert_equal 'text/javascript', response.content_type - end - assert_match /OnlineStore/, response.body - end - - def test_xhr_new_membership_with_failure - assert_no_difference 'Group.find(10).members.count' do - xhr :post, :edit_membership, :id => 10, :membership => { :project_id => 999, :role_ids => ['1', '2']} - assert_response :success - assert_template 'edit_membership' - assert_equal 'text/javascript', response.content_type - end - assert_match /alert/, response.body, "Alert message not sent" - end - - def test_edit_membership - assert_no_difference 'Group.find(10).members.count' do - post :edit_membership, :id => 10, :membership_id => 6, :membership => { :role_ids => ['1', '3']} - end - end - - def test_xhr_edit_membership - assert_no_difference 'Group.find(10).members.count' do - xhr :post, :edit_membership, :id => 10, :membership_id => 6, :membership => { :role_ids => ['1', '3']} - assert_response :success - assert_template 'edit_membership' - assert_equal 'text/javascript', response.content_type - end - end - - def test_destroy_membership - assert_difference 'Group.find(10).members.count', -1 do - post :destroy_membership, :id => 10, :membership_id => 6 - end - end - - def test_xhr_destroy_membership - assert_difference 'Group.find(10).members.count', -1 do - xhr :post, :destroy_membership, :id => 10, :membership_id => 6 - assert_response :success - assert_template 'destroy_membership' - assert_equal 'text/javascript', response.content_type - end - end - def test_autocomplete_for_user xhr :get, :autocomplete_for_user, :id => 10, :q => 'smi', :format => 'js' assert_response :success diff --git a/test/functional/principal_memberships_controller_test.rb b/test/functional/principal_memberships_controller_test.rb new file mode 100644 index 000000000..fe89c1153 --- /dev/null +++ b/test/functional/principal_memberships_controller_test.rb @@ -0,0 +1,209 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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.expand_path('../../test_helper', __FILE__) + +class PrincipalMembershipsControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :groups_users + + def setup + @request.session[:user_id] = 1 + end + + def test_new_user_membership + get :new, :user_id => 7 + assert_response :success + assert_select 'label', :text => 'eCookbook' do + assert_select 'input[name=?][value=1]:not([disabled])', 'membership[project_ids][]' + end + end + + def test_new_user_membership_should_disable_user_projects + Member.create!(:user_id => 7, :project_id => 1, :role_ids => [1]) + + get :new, :user_id => 7 + assert_response :success + assert_select 'label', :text => 'eCookbook' do + assert_select 'input[name=?][value=1][disabled=disabled]', 'membership[project_ids][]' + end + end + + def test_xhr_new_user_membership + xhr :get, :new, :user_id => 7 + assert_response :success + assert_equal 'text/javascript', response.content_type + end + + def test_create_user_membership + assert_difference 'Member.count' do + post :create, :user_id => 7, :membership => {:project_ids => [3], :role_ids => [2]} + end + assert_redirected_to '/users/7/edit?tab=memberships' + member = Member.order('id DESC').first + assert_equal User.find(7), member.principal + assert_equal [2], member.role_ids + assert_equal 3, member.project_id + end + + def test_create_user_membership_with_multiple_roles + assert_difference 'Member.count' do + post :create, :user_id => 7, :membership => {:project_ids => [3], :role_ids => [2, 3]} + end + member = Member.order('id DESC').first + assert_equal User.find(7), member.principal + assert_equal [2, 3], member.role_ids.sort + assert_equal 3, member.project_id + end + + def test_create_user_membership_with_multiple_projects_and_roles + assert_difference 'Member.count', 2 do + post :create, :user_id => 7, :membership => {:project_ids => [1, 3], :role_ids => [2, 3]} + end + members = Member.order('id DESC').limit(2).sort_by(&:project_id) + assert_equal 1, members[0].project_id + assert_equal 3, members[1].project_id + members.each do |member| + assert_equal User.find(7), member.principal + assert_equal [2, 3], member.role_ids.sort + end + end + + def test_xhr_create_user_membership + assert_difference 'Member.count' do + xhr :post, :create, :user_id => 7, :membership => {:project_ids => [3], :role_ids => [2]}, :format => 'js' + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + member = Member.order('id DESC').first + assert_equal User.find(7), member.principal + assert_equal [2], member.role_ids + assert_equal 3, member.project_id + assert_include 'tab-content-memberships', response.body + end + + def test_xhr_create_user_membership_with_failure + assert_no_difference 'Member.count' do + xhr :post, :create, :user_id => 7, :membership => {:project_ids => [3]}, :format => 'js' + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + assert_include 'alert', response.body, "Alert message not sent" + assert_include 'Role can\\\'t be empty', response.body, "Error message not sent" + end + + def test_update_user_membership + assert_no_difference 'Member.count' do + put :update, :user_id => 2, :id => 1, :membership => {:role_ids => [2]} + assert_redirected_to '/users/2/edit?tab=memberships' + end + assert_equal [2], Member.find(1).role_ids + end + + def test_xhr_update_user_membership + assert_no_difference 'Member.count' do + xhr :put, :update, :user_id => 2, :id => 1, :membership => {:role_ids => [2]}, :format => 'js' + assert_response :success + assert_template 'update' + assert_equal 'text/javascript', response.content_type + end + assert_equal [2], Member.find(1).role_ids + assert_include 'tab-content-memberships', response.body + end + + def test_destroy_user_membership + assert_difference 'Member.count', -1 do + delete :destroy, :user_id => 2, :id => 1 + end + assert_redirected_to '/users/2/edit?tab=memberships' + assert_nil Member.find_by_id(1) + end + + def test_xhr_destroy_user_membership_js_format + assert_difference 'Member.count', -1 do + xhr :delete, :destroy, :user_id => 2, :id => 1 + assert_response :success + assert_template 'destroy' + assert_equal 'text/javascript', response.content_type + end + assert_nil Member.find_by_id(1) + assert_include 'tab-content-memberships', response.body + end + + def test_xhr_new_group_membership + xhr :get, :new, :group_id => 10 + assert_response :success + assert_equal 'text/javascript', response.content_type + end + + def test_create_group_membership + assert_difference 'Group.find(10).members.count' do + post :create, :group_id => 10, :membership => {:project_ids => [2], :role_ids => ['1', '2']} + end + end + + def test_xhr_create_group_membership + assert_difference 'Group.find(10).members.count' do + xhr :post, :create, :group_id => 10, :membership => {:project_ids => [2], :role_ids => ['1', '2']} + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + assert_match /OnlineStore/, response.body + end + + def test_xhr_create_group_membership_with_failure + assert_no_difference 'Group.find(10).members.count' do + xhr :post, :create, :group_id => 10, :membership => {:project_ids => [999], :role_ids => ['1', '2']} + assert_response :success + assert_template 'create' + assert_equal 'text/javascript', response.content_type + end + assert_match /alert/, response.body, "Alert message not sent" + end + + def test_update_group_membership + assert_no_difference 'Group.find(10).members.count' do + put :update, :group_id => 10, :id => 6, :membership => {:role_ids => ['1', '3']} + end + end + + def test_xhr_update_group_membership + assert_no_difference 'Group.find(10).members.count' do + xhr :post, :update, :group_id => 10, :id => 6, :membership => {:role_ids => ['1', '3']} + assert_response :success + assert_template 'update' + assert_equal 'text/javascript', response.content_type + end + end + + def test_destroy_group_membership + assert_difference 'Group.find(10).members.count', -1 do + delete :destroy, :group_id => 10, :id => 6 + end + end + + def test_xhr_destroy_group_membership + assert_difference 'Group.find(10).members.count', -1 do + xhr :delete, :destroy, :group_id => 10, :id => 6 + assert_response :success + assert_template 'destroy' + assert_equal 'text/javascript', response.content_type + end + end +end diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb index bfc82c0f2..78b6689fc 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -424,78 +424,4 @@ class UsersControllerTest < ActionController::TestCase end assert_redirected_to '/users?name=foo' end - - def test_create_membership - assert_difference 'Member.count' do - post :edit_membership, :id => 7, :membership => { :project_id => 3, :role_ids => [2]} - end - assert_redirected_to :action => 'edit', :id => '7', :tab => 'memberships' - member = Member.order('id DESC').first - assert_equal User.find(7), member.principal - assert_equal [2], member.role_ids - assert_equal 3, member.project_id - end - - def test_create_membership_js_format - assert_difference 'Member.count' do - post :edit_membership, :id => 7, :membership => {:project_id => 3, :role_ids => [2]}, :format => 'js' - assert_response :success - assert_template 'edit_membership' - assert_equal 'text/javascript', response.content_type - end - member = Member.order('id DESC').first - assert_equal User.find(7), member.principal - assert_equal [2], member.role_ids - assert_equal 3, member.project_id - assert_include 'tab-content-memberships', response.body - end - - def test_create_membership_js_format_with_failure - assert_no_difference 'Member.count' do - post :edit_membership, :id => 7, :membership => {:project_id => 3}, :format => 'js' - assert_response :success - assert_template 'edit_membership' - assert_equal 'text/javascript', response.content_type - end - assert_include 'alert', response.body, "Alert message not sent" - assert_include 'Role can\\\'t be empty', response.body, "Error message not sent" - end - - def test_update_membership - assert_no_difference 'Member.count' do - put :edit_membership, :id => 2, :membership_id => 1, :membership => { :role_ids => [2]} - assert_redirected_to :action => 'edit', :id => '2', :tab => 'memberships' - end - assert_equal [2], Member.find(1).role_ids - end - - def test_update_membership_js_format - assert_no_difference 'Member.count' do - put :edit_membership, :id => 2, :membership_id => 1, :membership => {:role_ids => [2]}, :format => 'js' - assert_response :success - assert_template 'edit_membership' - assert_equal 'text/javascript', response.content_type - end - assert_equal [2], Member.find(1).role_ids - assert_include 'tab-content-memberships', response.body - end - - def test_destroy_membership - assert_difference 'Member.count', -1 do - delete :destroy_membership, :id => 2, :membership_id => 1 - end - assert_redirected_to :action => 'edit', :id => '2', :tab => 'memberships' - assert_nil Member.find_by_id(1) - end - - def test_destroy_membership_js_format - assert_difference 'Member.count', -1 do - delete :destroy_membership, :id => 2, :membership_id => 1, :format => 'js' - assert_response :success - assert_template 'destroy_membership' - assert_equal 'text/javascript', response.content_type - end - assert_nil Member.find_by_id(1) - assert_include 'tab-content-memberships', response.body - end end diff --git a/test/integration/routing/groups_test.rb b/test/integration/routing/groups_test.rb index c89d9c9ae..ea6e73703 100644 --- a/test/integration/routing/groups_test.rb +++ b/test/integration/routing/groups_test.rb @@ -94,13 +94,5 @@ class RoutingGroupsTest < ActionDispatch::IntegrationTest { :method => 'delete', :path => "/groups/567/users/12.xml" }, { :controller => 'groups', :action => 'remove_user', :id => '567', :user_id => '12', :format => 'xml' } ) - assert_routing( - { :method => 'post', :path => "/groups/destroy_membership/567" }, - { :controller => 'groups', :action => 'destroy_membership', :id => '567' } - ) - assert_routing( - { :method => 'post', :path => "/groups/edit_membership/567" }, - { :controller => 'groups', :action => 'edit_membership', :id => '567' } - ) end end diff --git a/test/integration/routing/principal_memberships_test.rb b/test/integration/routing/principal_memberships_test.rb new file mode 100644 index 000000000..017ee1de6 --- /dev/null +++ b/test/integration/routing/principal_memberships_test.rb @@ -0,0 +1,56 @@ +# Redmine - project management software +# Copyright (C) 2006-2014 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.expand_path('../../../test_helper', __FILE__) + +class RoutingPrincipalMembershipsTest < ActionDispatch::IntegrationTest + def test_user_memberships + assert_routing( + { :method => 'post', :path => "/users/123/memberships" }, + { :controller => 'principal_memberships', :action => 'create', + :user_id => '123' } + ) + assert_routing( + { :method => 'put', :path => "/users/123/memberships/55" }, + { :controller => 'principal_memberships', :action => 'update', + :user_id => '123', :id => '55' } + ) + assert_routing( + { :method => 'delete', :path => "/users/123/memberships/55" }, + { :controller => 'principal_memberships', :action => 'destroy', + :user_id => '123', :id => '55' } + ) + end + + def test_group_memberships + assert_routing( + { :method => 'post', :path => "/groups/123/memberships" }, + { :controller => 'principal_memberships', :action => 'create', + :group_id => '123' } + ) + assert_routing( + { :method => 'put', :path => "/groups/123/memberships/55" }, + { :controller => 'principal_memberships', :action => 'update', + :group_id => '123', :id => '55' } + ) + assert_routing( + { :method => 'delete', :path => "/groups/123/memberships/55" }, + { :controller => 'principal_memberships', :action => 'destroy', + :group_id => '123', :id => '55' } + ) + end +end diff --git a/test/integration/routing/users_test.rb b/test/integration/routing/users_test.rb index dbdcbcc65..911c1de99 100644 --- a/test/integration/routing/users_test.rb +++ b/test/integration/routing/users_test.rb @@ -79,20 +79,5 @@ class RoutingUsersTest < ActionDispatch::IntegrationTest { :controller => 'users', :action => 'destroy', :id => '44', :format => 'xml' } ) - assert_routing( - { :method => 'post', :path => "/users/123/memberships" }, - { :controller => 'users', :action => 'edit_membership', - :id => '123' } - ) - assert_routing( - { :method => 'put', :path => "/users/123/memberships/55" }, - { :controller => 'users', :action => 'edit_membership', - :id => '123', :membership_id => '55' } - ) - assert_routing( - { :method => 'delete', :path => "/users/123/memberships/55" }, - { :controller => 'users', :action => 'destroy_membership', - :id => '123', :membership_id => '55' } - ) end end |