]> source.dussan.org Git - redmine.git/commitdiff
Adds a single controller for users and groups memberships and support for adding...
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Thu, 23 Oct 2014 21:46:40 +0000 (21:46 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Thu, 23 Oct 2014 21:46:40 +0000 (21:46 +0000)
git-svn-id: http://svn.redmine.org/redmine/trunk@13498 e93f8b46-1217-0410-a6f0-8f06a7374b81

28 files changed:
app/controllers/groups_controller.rb
app/controllers/principal_memberships_controller.rb [new file with mode: 0644]
app/controllers/users_controller.rb
app/helpers/application_helper.rb
app/helpers/principal_memberships_helper.rb [new file with mode: 0644]
app/models/member.rb
app/models/principal.rb
app/models/user.rb
app/views/groups/_memberships.html.erb
app/views/principal_memberships/_index.html.erb [new file with mode: 0644]
app/views/principal_memberships/_new_form.html.erb [new file with mode: 0644]
app/views/principal_memberships/_new_modal.html.erb [new file with mode: 0644]
app/views/principal_memberships/create.js.erb [new file with mode: 0644]
app/views/principal_memberships/destroy.js.erb [new file with mode: 0644]
app/views/principal_memberships/new.html.erb [new file with mode: 0644]
app/views/principal_memberships/new.js.erb [new file with mode: 0644]
app/views/principal_memberships/update.js.erb [new file with mode: 0644]
app/views/users/_memberships.html.erb
config/locales/en.yml
config/locales/fr.yml
config/routes.rb
public/stylesheets/application.css
test/functional/groups_controller_test.rb
test/functional/principal_memberships_controller_test.rb [new file with mode: 0644]
test/functional/users_controller_test.rb
test/integration/routing/groups_test.rb
test/integration/routing/principal_memberships_test.rb [new file with mode: 0644]
test/integration/routing/users_test.rb

index 45ed4c4e1aa5a326d7c58f782c186931b33d2ccc..d67f0382b5ad981218e26128c4daa888072cd730 100644 (file)
@@ -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 (file)
index 0000000..5af897b
--- /dev/null
@@ -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
index bb56fb285399ef8ab58cabccab9b58a6d4af0efd..d14914af4efa92bec1380ee94a778a1b3d8de919 100644 (file)
@@ -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
index 7e9cdce9027fffd148bebfb5fd144eab391776f6..923dff581dfe49b73315e4b53e26f12f8efe1787 100644 (file)
@@ -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 (file)
index 0000000..e734f42
--- /dev/null
@@ -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
index 8256d2e6895addc7ec3a17d61845e0eb99538398..257178866559e691c3ff26e72cd990d32fa89e0e 100644 (file)
@@ -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
index d10241b3f00aab3879724f7016e68f1cba907070..e6e6ea78e911f08b6c2ec80eff6af0a23a4da4bc 100644 (file)
@@ -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
index d86627e85109dd5fd208e88c09245d240ef08ffc..3ac98620bd5423b336fcc8ddd448661ade772a33 100644 (file)
@@ -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
index ec275c5949410a4f27f453a787127bc993b136f3..1242bf6e8ef4a298535deb314088b388b5a74c55 100644 (file)
@@ -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 (file)
index 0000000..8203999
--- /dev/null
@@ -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 (file)
index 0000000..bdd7df6
--- /dev/null
@@ -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 (file)
index 0000000..175e093
--- /dev/null
@@ -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 (file)
index 0000000..17d7ee1
--- /dev/null
@@ -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 (file)
index 0000000..c8564f4
--- /dev/null
@@ -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 (file)
index 0000000..64d2ebe
--- /dev/null
@@ -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 (file)
index 0000000..625eeaf
--- /dev/null
@@ -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 (file)
index 0000000..2986c4e
--- /dev/null
@@ -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 %>
index cd4201727a4a65ec12ec35ac556a06bc775aafdc..75871d63e42fc911cc2ace25b6a6afba1d3018d1 100644 (file)
@@ -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 %>
index 8f858162c5df7f88fce997c85490f8f99bf4b29b..3799db2bf002dbffd8fa11b56b0c12886beb4b67 100644 (file)
@@ -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
index 0f2e0d0e637448a279ff63384dcac1df709d3c68..1b106ffe7f7095a9da16e75594794c185a2f6b7d 100644 (file)
@@ -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
index a988eb31143e7934ff03cd27304cfb40bd26702f..b7f1f79840d4e6c12ec1ecd71f445076c8996f03 100644 (file)
@@ -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
index 19507fcedbd929d64bca48dd7a66c923765df230..e60cf8d6dd14b7d9f4def8a5609c1e4b6d327dc6 100644 (file)
@@ -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;
index d1a2b6729aafa34ac8be2ee65445110fd0dc6cf4..778b3f3cd5b6d3f4215df7e24d9201a28a7f27eb 100644 (file)
@@ -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 (file)
index 0000000..fe89c11
--- /dev/null
@@ -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
index bfc82c0f27432cf652b66f400f0a36a00972c9c5..78b6689fc91522a10e8c2f16797a007beb23e568 100644 (file)
@@ -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
index c89d9c9ae5cf071830b88adeb48dbe129d975721..ea6e737038125595d364de65b9106d6f71843448 100644 (file)
@@ -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 (file)
index 0000000..017ee1d
--- /dev/null
@@ -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
index dbdcbcc65359622777f904dffdf56c132a6abb46..911c1de99ceb1b7adfd71bfb6f6cb73fd6c9168b 100644 (file)
@@ -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