OSDN Git Service

Allows multiple roles on the same project (#706). Prerequisite for user groups feature.
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 10 May 2009 10:54:31 +0000 (10:54 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 10 May 2009 10:54:31 +0000 (10:54 +0000)
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2726 e93f8b46-1217-0410-a6f0-8f06a7374b81

65 files changed:
app/controllers/application.rb
app/controllers/issues_controller.rb
app/controllers/members_controller.rb
app/controllers/projects_controller.rb
app/controllers/queries_controller.rb
app/controllers/roles_controller.rb
app/controllers/users_controller.rb
app/helpers/application_helper.rb
app/models/issue.rb
app/models/issue_status.rb
app/models/member.rb
app/models/member_role.rb [new file with mode: 0644]
app/models/project.rb
app/models/role.rb
app/models/user.rb
app/views/account/show.rhtml
app/views/projects/settings/_members.rhtml
app/views/queries/_form.rhtml
app/views/users/_memberships.rhtml
app/views/workflows/edit.rhtml
db/migrate/20090503121501_create_member_roles.rb [new file with mode: 0644]
db/migrate/20090503121505_populate_member_roles.rb [new file with mode: 0644]
db/migrate/20090503121510_drop_members_role_id.rb [new file with mode: 0644]
public/stylesheets/application.css
test/fixtures/member_roles.yml [new file with mode: 0644]
test/fixtures/members.yml
test/functional/attachments_controller_test.rb
test/functional/boards_controller_test.rb
test/functional/documents_controller_test.rb
test/functional/issue_categories_controller_test.rb
test/functional/issue_relations_controller_test.rb
test/functional/issues_controller_test.rb
test/functional/journals_controller_test.rb
test/functional/mail_handler_controller_test.rb
test/functional/members_controller_test.rb
test/functional/messages_controller_test.rb
test/functional/news_controller_test.rb
test/functional/projects_controller_test.rb
test/functional/queries_controller_test.rb
test/functional/repositories_bazaar_controller_test.rb
test/functional/repositories_controller_test.rb
test/functional/repositories_darcs_controller_test.rb
test/functional/repositories_git_controller_test.rb
test/functional/repositories_mercurial_controller_test.rb
test/functional/repositories_subversion_controller_test.rb
test/functional/roles_controller_test.rb
test/functional/search_controller_test.rb
test/functional/timelog_controller_test.rb
test/functional/users_controller_test.rb
test/functional/versions_controller_test.rb
test/functional/watchers_controller_test.rb
test/functional/wiki_controller_test.rb
test/functional/wikis_controller_test.rb
test/unit/activity_test.rb
test/unit/changeset_test.rb
test/unit/issue_test.rb
test/unit/mail_handler_test.rb
test/unit/mailer_test.rb
test/unit/member_test.rb
test/unit/message_test.rb
test/unit/news_test.rb
test/unit/project_test.rb
test/unit/query_test.rb
test/unit/search_test.rb
test/unit/user_test.rb

index f864873..9123cfc 100644 (file)
@@ -38,10 +38,6 @@ class ApplicationController < ActionController::Base
     require_dependency "repository/#{scm.underscore}"
   end
   
-  def current_role
-    @current_role ||= User.current.role_for_project(@project)
-  end
-  
   def user_setup
     # Check the settings cache for each request
     Setting.check_cache
index 784d620..24c2040 100644 (file)
@@ -146,7 +146,7 @@ class IssuesController < ApplicationController
       return
     end    
     @issue.status = default_status
-    @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
+    @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.roles_for_project(@project), @issue.tracker)).uniq
     
     if request.get? || request.xhr?
       @issue.start_date ||= Date.today
index e2bc925..eb59895 100644 (file)
@@ -48,7 +48,12 @@ class MembersController < ApplicationController
     if request.post? and @member.update_attributes(params[:member])
         respond_to do |format|
         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'} }
+        format.js { 
+          render(:update) {|page| 
+            page.replace_html "tab-content-members", :partial => 'projects/settings/members'
+            page.visual_effect(:highlight, "member-#{@member.id}")
+          }
+        }
       end
     end
   end
index b663291..3f29e4e 100644 (file)
@@ -112,7 +112,7 @@ class ProjectsController < ApplicationController
       redirect_to_project_menu_item(@project, params[:jump]) && return
     end
     
-    @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
+    @members_by_role = @project.members.find(:all, :include => [:user, :roles], :order => 'position').group_by {|m| m.roles.first}
     @subprojects = @project.children.visible
     @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
     @trackers = @project.rolled_up_trackers
index b688d2c..16755a1 100644 (file)
@@ -24,7 +24,7 @@ class QueriesController < ApplicationController
     @query = Query.new(params[:query])
     @query.project = params[:query_is_for_all] ? nil : @project
     @query.user = User.current
-    @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
+    @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
     @query.column_names = nil if params[:default_columns]
     
     params[:fields].each do |field|
@@ -48,7 +48,7 @@ class QueriesController < ApplicationController
       end if params[:fields]
       @query.attributes = params[:query]
       @query.project = nil if params[:query_is_for_all]
-      @query.is_public = false unless (@query.project && current_role.allowed_to?(:manage_public_queries)) || User.current.admin?
+      @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
       @query.column_names = nil if params[:default_columns]
       
       if @query.save
index 6185d70..8074b18 100644 (file)
@@ -40,7 +40,7 @@ class RolesController < ApplicationController
         @role.workflows.copy(copy_from)
       end
       flash[:notice] = l(:notice_successful_create)
-      redirect_to :action => 'list'
+      redirect_to :action => 'index'
     end
     @permissions = @role.setable_permissions
     @roles = Role.find :all, :order => 'builtin, position'
@@ -50,7 +50,7 @@ class RolesController < ApplicationController
     @role = Role.find(params[:id])
     if request.post? and @role.update_attributes(params[:role])
       flash[:notice] = l(:notice_successful_update)
-      redirect_to :action => 'list'
+      redirect_to :action => 'index'
     end
     @permissions = @role.setable_permissions
   end
@@ -58,7 +58,7 @@ class RolesController < ApplicationController
   def destroy
     @role = Role.find(params[:id])
     @role.destroy
-    redirect_to :action => 'list'
+    redirect_to :action => 'index'
   rescue
     flash[:error] = 'This role is in use and can not be deleted.'
     redirect_to :action => 'index'
@@ -73,7 +73,7 @@ class RolesController < ApplicationController
         role.save
       end
       flash[:notice] = l(:notice_successful_update)
-      redirect_to :action => 'list'
+      redirect_to :action => 'index'
     end
   end
 end
index e0b508c..72a4b6c 100644 (file)
@@ -86,10 +86,7 @@ class UsersController < ApplicationController
       end
     end
     @auth_sources = AuthSource.find(:all)
-    @roles = Role.find_all_givable
-    @projects = Project.active.find(:all, :order => 'lft')
     @membership ||= Member.new
-    @memberships = @user.memberships
   end
   
   def edit_membership
@@ -97,12 +94,23 @@ class UsersController < ApplicationController
     @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user)
     @membership.attributes = params[:membership]
     @membership.save if request.post?
-    redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
+    respond_to do |format|
+       format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
+       format.js { 
+         render(:update) {|page| 
+           page.replace_html "tab-content-memberships", :partial => 'users/memberships'
+           page.visual_effect(:highlight, "member-#{@membership.id}")
+         }
+       }
+     end
   end
   
   def destroy_membership
     @user = User.find(params[:id])
     Member.find(params[:membership_id]).destroy if request.post?
-    redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
+    respond_to do |format|
+      format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
+      format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
+    end
   end
 end
index b7865ec..2382983 100644 (file)
@@ -28,10 +28,6 @@ module ApplicationHelper
   extend Forwardable
   def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
 
-  def current_role
-    @current_role ||= User.current.role_for_project(@project)
-  end
-
   # Return true if user is authorized for controller/action, otherwise false
   def authorize_for(controller, action)
     User.current.allowed_to?({:controller => controller, :action => action}, @project)
index 23035b9..da49577 100644 (file)
@@ -205,7 +205,7 @@ class Issue < ActiveRecord::Base
   
   # Returns an array of status that user is able to apply
   def new_statuses_allowed_to(user)
-    statuses = status.find_new_statuses_allowed_to(user.role_for_project(project), tracker)
+    statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
     statuses << status unless statuses.empty?
     statuses.uniq.sort
   end
index 16c7bce..ca33d37 100644 (file)
@@ -36,24 +36,34 @@ class IssueStatus < ActiveRecord::Base
 
   # Returns an array of all statuses the given role can switch to
   # Uses association cache when called more than one time
-  def new_statuses_allowed_to(role, tracker)
-    new_statuses = workflows.select {|w| w.role_id == role.id && w.tracker_id == tracker.id}.collect{|w| w.new_status} if role && tracker
-    new_statuses ? new_statuses.compact.sort{|x, y| x.position <=> y.position } : []
+  def new_statuses_allowed_to(roles, tracker)
+    if roles && tracker
+      role_ids = roles.collect(&:id)
+      new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort
+    else
+      []
+    end
   end
   
   # Same thing as above but uses a database query
   # More efficient than the previous method if called just once
-  def find_new_statuses_allowed_to(role, tracker)  
-    new_statuses = workflows.find(:all, 
-                                   :include => :new_status,
-                                   :conditions => ["role_id=? and tracker_id=?", role.id, tracker.id]).collect{ |w| w.new_status }.compact  if role && tracker
-    new_statuses ? new_statuses.sort{|x, y| x.position <=> y.position } : []
+  def find_new_statuses_allowed_to(roles, tracker)
+    if roles && tracker
+      workflows.find(:all,
+                     :include => :new_status,
+                     :conditions => { :role_id => roles.collect(&:id), 
+                                      :tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort
+    else
+      []
+    end
   end
   
-  def new_status_allowed_to?(status, role, tracker)
-    status && role && tracker ?
-      !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => role.id, :tracker_id => tracker.id}).nil? :
+  def new_status_allowed_to?(status, roles, tracker)
+    if status && roles && tracker
+      !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil?
+    else
       false
+    end
   end
 
   def <=>(status)
index 5228d1f..2dc91cb 100644 (file)
@@ -1,5 +1,5 @@
-# redMine - project management software
-# Copyright (C) 2006  Jean-Philippe Lang
+# Redmine - project management software
+# Copyright (C) 2006-2009  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
 
 class Member < ActiveRecord::Base
   belongs_to :user
-  belongs_to :role
+  has_many :member_roles, :dependent => :delete_all
+  has_many :roles, :through => :member_roles
   belongs_to :project
 
-  validates_presence_of :role, :user, :project
+  validates_presence_of :user, :project
   validates_uniqueness_of :user_id, :scope => :project_id
-
-  def validate
-    errors.add :role_id, :invalid if role && !role.member?
-  end
   
   def name
     self.user.name
@@ -42,11 +39,18 @@ class Member < ActiveRecord::Base
   end
   
   def <=>(member)
-    role == member.role ? (user <=> member.user) : (role <=> member.role)
+    a, b = roles.sort.first, member.roles.sort.first
+    a == b ? (user <=> member.user) : (a <=> b)
   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]
   end
+  
+  protected
+  
+  def validate
+    errors.add_to_base "Role can't be blank" if roles.empty?
+  end
 end
diff --git a/app/models/member_role.rb b/app/models/member_role.rb
new file mode 100644 (file)
index 0000000..46777cd
--- /dev/null
@@ -0,0 +1,27 @@
+# Redmine - project management software
+# Copyright (C) 2006-2009  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 MemberRole < ActiveRecord::Base
+  belongs_to :member
+  belongs_to :role
+  
+  validates_presence_of :role
+  
+  def validate
+    errors.add :role_id, :invalid if role && !role.member?
+  end
+end
index 261844e..922f522 100644 (file)
@@ -135,7 +135,7 @@ class Project < ActiveRecord::Base
       statements << "1=0"
       if user.logged?
         statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
-        allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
+        allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
         statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
       elsif Role.anonymous.allowed_to?(permission)
         # anonymous user allowed on public project
@@ -247,12 +247,14 @@ class Project < ActiveRecord::Base
   
   # Deletes all project's members
   def delete_all_members
+    me, mr = Member.table_name, MemberRole.table_name
+    connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
     Member.delete_all(['project_id = ?', id])
   end
   
   # Users issues can be assigned to
   def assignable_users
-    members.select {|m| m.role.assignable?}.collect {|m| m.user}.sort
+    members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
   end
   
   # Returns the mail adresses of users that should be always notified on project events
index b07e7a0..a93c4ea 100644 (file)
@@ -38,7 +38,8 @@ class Role < ActiveRecord::Base
     end
   end
   
-  has_many :members
+  has_many :member_roles, :dependent => :destroy
+  has_many :members, :through => :member_roles
   acts_as_list
   
   serialize :permissions, Array
@@ -82,7 +83,11 @@ class Role < ActiveRecord::Base
   end
   
   def <=>(role)
-    position <=> role.position
+    role ? position <=> role.position : -1
+  end
+  
+  def to_s
+    name
   end
   
   # Return true if the role is a builtin role
index 3c9a1c7..3c6f723 100644 (file)
@@ -33,7 +33,7 @@ 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"
+  has_many :memberships, :class_name => 'Member', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
   has_many :members, :dependent => :delete_all
   has_many :projects, :through => :memberships
   has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
@@ -229,26 +229,30 @@ class User < ActiveRecord::Base
     !logged?
   end
   
-  # Return user's role for project
-  def role_for_project(project)
+  # Return user's roles for project
+  def roles_for_project(project)
+    roles = []
     # No role on archived projects
-    return nil unless project && project.active?
+    return roles unless project && project.active?
     if logged?
       # Find project membership
       membership = memberships.detect {|m| m.project_id == project.id}
       if membership
-        membership.role
+        roles = membership.roles
       else
         @role_non_member ||= Role.non_member
+        roles << @role_non_member
       end
     else
       @role_anonymous ||= Role.anonymous
+      roles << @role_anonymous
     end
+    roles
   end
   
   # Return true if the user is a member of project
   def member_of?(project)
-    role_for_project(project).member?
+    !roles_for_project(project).detect {|role| role.member?}.nil?
   end
   
   # Return true if the user is allowed to do the specified action on project
@@ -264,13 +268,13 @@ class User < ActiveRecord::Base
       # Admin users are authorized for anything else
       return true if admin?
       
-      role = role_for_project(project)
-      return false unless role
-      role.allowed_to?(action) && (project.is_public? || role.member?)
+      roles = roles_for_project(project)
+      return false unless roles
+      roles.detect {|role| (project.is_public? || role.member?) && role.allowed_to?(action)}
       
     elsif options[:global]
       # authorize if user has at least one role that has this permission
-      roles = memberships.collect {|m| m.role}.uniq
+      roles = memberships.collect {|m| m.roles}.flatten.uniq
       roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
     else
       false
index 6b70c5f..c97da7f 100644 (file)
@@ -25,7 +25,7 @@
 <ul>
 <% for membership in @memberships %>
        <li><%= link_to(h(membership.project.name), :controller => 'projects', :action => 'show', :id => membership.project) %>
-    (<%=h membership.role.name %>, <%= format_date(membership.created_on) %>)</li>
+    (<%=h membership.roles.collect(&:to_s).join(', ') %>, <%= format_date(membership.created_on) %>)</li>
 <% end %>
 </ul>
 <% end %>
index 3124039..a6bdd95 100644 (file)
@@ -1,36 +1,43 @@
 <%= error_messages_for 'member' %>
 <% roles = Role.find_all_givable
-   members = @project.members.find(:all, :include => [:role, :user]).sort %>
+   members = @project.members.find(:all, :include => [:roles, :user]).sort %>
 
 <div class="splitcontentleft">
 <% if members.any? %>
-<table class="list">
+<table class="list members">
        <thead>
          <th><%= l(:label_user) %></th>
-         <th><%= l(:label_role) %></th>
+         <th><%= l(:label_role_plural) %></th>
          <th style="width:15%"></th>
           <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
        </thead>
        <tbody>
        <% members.each do |member| %>
        <% next if member.new_record? %>
-       <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %>">
-       <td><%=h member.name %></td>
-    <td align="center">
+       <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %> member">
+       <td class="user"><%= link_to_user member.user %></td>
+  <td class="roles">
+    <span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
     <% 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" %>
-        <%= submit_tag l(:button_change), :class => "small" %>
+      <% remote_form_for(:member, member, :url => {:controller => 'members', :action => 'edit', :id => member},
+                                                                                                                                               :method => :post,
+                                                                                                                                               :html => { :id => "member-#{member.id}-roles-form", :style => 'display:none;' }) do |f| %>
+       <p><% roles.each do |role| %>
+        <label><%= check_box_tag 'member[role_ids][]', role.id, member.roles.include?(role) %> <%=h role %></label><br />
+        <% end %></p>
+        <p><%= submit_tag l(:button_change), :class => "small" %>
+        <%= link_to_function l(:button_cancel), "$('member-#{member.id}-roles').show(); $('member-#{member.id}-roles-form').hide(); return false;" %></p>
       <% end %>
     <% end %>
-    </td>
-    <td align="center">
+  </td>
+  <td class="buttons">
+      <%= link_to_function l(:button_edit), "$('member-#{member.id}-roles').hide(); $('member-#{member.id}-roles-form').show(); return false;", :class => 'icon icon-edit' %>
       <%= link_to_remote l(:button_delete), { :url => {:controller => 'members', :action => 'destroy', :id => member},                                              
                                               :method => :post
                                             }, :title => l(:button_delete),
                                                :class => 'icon icon-del' %>
-    </td>
-    <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
+  </td>
+  <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
        </tr>
        </tbody>
 <% end; reset_cycle %>
                        <% end -%>
                        </div>
                <% end %>
-    <p><%= l(:label_role) %>: <%= f.select :role_id, roles.collect{|role| [role.name, role.id]}, :selected => nil %>
-    <%= submit_tag l(:button_add) %></p>
+    <p><%= l(:label_role_plural) %>:
+    <% roles.each do |role| %>
+       <label><%= check_box_tag 'member[role_ids][]', role.id %> <%=h role %></label>
+       <% end %></p>
+    <p><%= submit_tag l(:button_add) %></p>
                </fieldset>
   <% end %>
 <% end %>
index 28faba1..577699d 100644 (file)
@@ -6,7 +6,7 @@
 <p><label for="query_name"><%=l(:field_name)%></label>
 <%= text_field 'query', 'name', :size => 80 %></p>
 
-<% if User.current.admin? || (@project && current_role.allowed_to?(:manage_public_queries)) %>
+<% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @project) %>
 <p><label for="query_is_public"><%=l(:field_is_public)%></label>
 <%= check_box 'query', 'is_public',
       :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("query_is_for_all").checked = false; $("query_is_for_all").disabled = true;} else {$("query_is_for_all").disabled = false;}') %></p>
index d1657fb..8b458e0 100644 (file)
@@ -1,24 +1,36 @@
-<% if @memberships.any? %>
+<% roles = Role.find_all_givable %>
+<% projects = Project.active.find(:all, :order => 'lft') %>
+
+<div class="splitcontentleft">
+<% if @user.memberships.any? %>
 <table class="list memberships">
        <thead>
          <th><%= l(:label_project) %></th>
-         <th><%= l(:label_role) %></th>
+         <th><%= l(:label_role_plural) %></th>
          <th style="width:15%"></th>
        </thead>
        <tbody>
-       <% @memberships.each do |membership| %>
+       <% @user.memberships.each do |membership| %>
        <% next if membership.new_record? %>
-       <tr class="<%= cycle 'odd', 'even' %>">
-       <td><%=h membership.project %></td>
-    <td align="center">
-    <% form_tag({ :action => 'edit_membership', :id => @user, :membership_id => membership }) do %>
-        <%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name", membership.role_id) %>
-        <%= submit_tag l(:button_change), :class => "small" %>
+       <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
+       <td class="project"><%=h membership.project %></td>
+  <td class="roles">
+    <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
+    <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @user, :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) %> <%=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 align="center">
-      <%= link_to l(:button_delete), {:action => 'destroy_membership', :id => @user, :membership_id => membership }, :method => :post, :class => 'icon icon-del' %>
-    </td>
+  </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' %>
+      <%= link_to_remote l(:button_delete), { :url => { :controller => 'users', :action => 'destroy_membership', :id => @user, :membership_id => membership },
+                                                                                                                                                               :method => :post },
+                                                                                                                                                 :class => 'icon icon-del' %>
+  </td>
        </tr>
        </tbody>
 <% end; reset_cycle %>
 <% else %>
 <p class="nodata"><%= l(:label_no_data) %></p>
 <% end %>
+</div>
 
-<% if @projects.any? %>
-<p>
-<label><%=l(:label_project_new)%></label><br/>
-<% form_tag({ :action => 'edit_membership', :id => @user }) do %>
-<%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, @projects) %>
-<%= l(:label_role) %>:
-<%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name") %>
-<%= submit_tag l(:button_add) %>
+<div class="splitcontentright">
+<% if projects.any? %>
+<fieldset><legend><%=l(:label_project_new)%></legend>
+<% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @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 %> <%=h role %></label>
+<% end %></p>
+<p><%= submit_tag l(:button_add) %></p>
 <% end %>
-</p>
+</fieldset>
 <% end %>
+</div>
index 1aef8eb..134dfb3 100644 (file)
@@ -43,7 +43,7 @@
        <% for old_status in @statuses %>
                <tr class="<%= cycle("odd", "even") %>">
                <td><%= old_status.name %></td>
-               <% new_status_ids_allowed = old_status.find_new_statuses_allowed_to(@role, @tracker).collect(&:id) -%>
+               <% new_status_ids_allowed = old_status.find_new_statuses_allowed_to([@role], @tracker).collect(&:id) -%>
                <% for new_status in @statuses -%>
                        <td align="center">
       <input type="checkbox"
diff --git a/db/migrate/20090503121501_create_member_roles.rb b/db/migrate/20090503121501_create_member_roles.rb
new file mode 100644 (file)
index 0000000..38519ea
--- /dev/null
@@ -0,0 +1,12 @@
+class CreateMemberRoles < ActiveRecord::Migration
+  def self.up
+    create_table :member_roles do |t|
+      t.column :member_id, :integer, :null => false
+      t.column :role_id, :integer, :null => false
+    end
+  end
+
+  def self.down
+    drop_table :member_roles
+  end
+end
diff --git a/db/migrate/20090503121505_populate_member_roles.rb b/db/migrate/20090503121505_populate_member_roles.rb
new file mode 100644 (file)
index 0000000..8b7c987
--- /dev/null
@@ -0,0 +1,11 @@
+class PopulateMemberRoles < ActiveRecord::Migration
+  def self.up
+    Member.find(:all).each do |member|
+      MemberRole.create!(:member_id => member.id, :role_id => member.role_id)
+    end
+  end
+
+  def self.down
+    MemberRole.delete_all
+  end
+end
diff --git a/db/migrate/20090503121510_drop_members_role_id.rb b/db/migrate/20090503121510_drop_members_role_id.rb
new file mode 100644 (file)
index 0000000..c281199
--- /dev/null
@@ -0,0 +1,9 @@
+class DropMembersRoleId < ActiveRecord::Migration
+  def self.up
+    remove_column :members, :role_id
+  end
+
+  def self.down
+    raise IrreversibleMigration
+  end
+end
index b3db226..813c8f1 100644 (file)
@@ -87,6 +87,8 @@ table.list th {  background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
 table.list td { vertical-align: top; }
 table.list td.id { width: 2%; text-align: center;}
 table.list td.checkbox { width: 15px; padding: 0px;}
+table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
+table.list td.buttons a { padding-right: 0.6em; }
  
 tr.project td.name a { padding-left: 16px; white-space:nowrap; }
 tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; }
@@ -114,6 +116,8 @@ table.files tr.file td { text-align: center; }
 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
 table.files tr.file td.digest { font-size: 80%; }
 
+table.members td.roles, table.memberships td.roles { width: 45%; }
+
 tr.message { height: 2.6em; }
 tr.message td.last_message { font-size: 80%; }
 tr.message.locked td.subject a { background-image: url(../images/locked.png); }
@@ -333,11 +337,11 @@ p.other-formats { text-align: right; font-size:0.9em; color: #666; }
 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
 
 /* Project members tab */
-div#tab-content-members .splitcontentleft { width: 64% }
-div#tab-content-members .splitcontentright { width: 34% }
-div#tab-content-members fieldset { padding:1em; margin-bottom: 1em; }
-div#tab-content-members fieldset legend { font-weight: bold; }
-div#tab-content-members fieldset label { display: block; }
+div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft { width: 64% }
+div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright { width: 34% }
+div#tab-content-members fieldset, div#tab-content-memberships fieldset { padding:1em; margin-bottom: 1em; }
+div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend { font-weight: bold; }
+div#tab-content-members fieldset label, div#tab-content-memberships fieldset label { display: block; }
 div#tab-content-members fieldset div { max-height: 400px; overflow:auto; }
 
 * html div#tab-content-members fieldset div { height: 450px; }
diff --git a/test/fixtures/member_roles.yml b/test/fixtures/member_roles.yml
new file mode 100644 (file)
index 0000000..6c91f15
--- /dev/null
@@ -0,0 +1,23 @@
+--- 
+member_roles_001: 
+  id: 1
+  role_id: 1
+  member_id: 1
+member_roles_002: 
+  id: 2
+  role_id: 2
+  member_id: 2
+member_roles_003: 
+  id: 3
+  role_id: 2
+  member_id: 3
+member_roles_004: 
+  id: 4
+  role_id: 2
+  member_id: 4
+member_roles_005: 
+  id: 5
+  role_id: 1
+  member_id: 5
+  
+  
\ No newline at end of file
index 4156bb8..b1b19c3 100644 (file)
@@ -2,21 +2,18 @@
 members_001: \r
   created_on: 2006-07-19 19:35:33 +02:00\r
   project_id: 1\r
-  role_id: 1\r
   id: 1\r
   user_id: 2\r
   mail_notification: true\r
 members_002: \r
   created_on: 2006-07-19 19:35:36 +02:00\r
   project_id: 1\r
-  role_id: 2\r
   id: 2\r
   user_id: 3\r
   mail_notification: true\r
 members_003: \r
   created_on: 2006-07-19 19:35:36 +02:00\r
   project_id: 2\r
-  role_id: 2\r
   id: 3\r
   user_id: 2\r
   mail_notification: true\r
@@ -24,7 +21,6 @@ members_004:
   id: 4\r
   created_on: 2006-07-19 19:35:36 +02:00\r
   project_id: 1\r
-  role_id: 2\r
   # Locked user\r
   user_id: 5\r
   mail_notification: true\r
@@ -32,7 +28,6 @@ members_005:
   id: 5\r
   created_on: 2006-07-19 19:35:33 +02:00\r
   project_id: 5\r
-  role_id: 1\r
   user_id: 2\r
   mail_notification: true\r
   
\ No newline at end of file
index a49caa8..3a4b897 100644 (file)
@@ -23,7 +23,7 @@ class AttachmentsController; def rescue_action(e) raise e end; end
 
 
 class AttachmentsControllerTest < Test::Unit::TestCase
-  fixtures :users, :projects, :roles, :members, :enabled_modules, :issues, :trackers, :attachments,
+  fixtures :users, :projects, :roles, :members, :member_roles, :enabled_modules, :issues, :trackers, :attachments,
            :versions, :wiki_pages, :wikis, :documents
   
   def setup
index 01db0f9..eb9a50e 100644 (file)
@@ -22,7 +22,7 @@ require 'boards_controller'
 class BoardsController; def rescue_action(e) raise e end; end
 
 class BoardsControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :members, :roles, :boards, :messages, :enabled_modules
+  fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules
   
   def setup
     @controller = BoardsController.new
index b5788c7..c0fe095 100644 (file)
@@ -22,7 +22,7 @@ require 'documents_controller'
 class DocumentsController; def rescue_action(e) raise e end; end
 
 class DocumentsControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :roles, :members, :enabled_modules, :documents, :enumerations
+  fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :documents, :enumerations
   
   def setup
     @controller = DocumentsController.new
index 5bd8263..ffb8733 100644 (file)
@@ -22,7 +22,7 @@ require 'issue_categories_controller'
 class IssueCategoriesController; def rescue_action(e) raise e end; end
 
 class IssueCategoriesControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :members, :roles, :enabled_modules, :issue_categories
+  fixtures :projects, :users, :members, :member_roles, :roles, :enabled_modules, :issue_categories
   
   def setup
     @controller = IssueCategoriesController.new
index dc64a00..a23f64a 100644 (file)
@@ -10,6 +10,7 @@ class IssueRelationsControllerTest < Test::Unit::TestCase
            :users,
            :roles,
            :members,
+           :member_roles,
            :issues,
            :issue_statuses,
            :enabled_modules,
index dfea328..2bb95f8 100644 (file)
@@ -26,6 +26,7 @@ class IssuesControllerTest < Test::Unit::TestCase
            :users,
            :roles,
            :members,
+           :member_roles,
            :issues,
            :issue_statuses,
            :versions,
index 327c7e7..ae112ab 100644 (file)
@@ -22,7 +22,7 @@ require 'journals_controller'
 class JournalsController; def rescue_action(e) raise e end; end
 
 class JournalsControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :members, :roles, :issues, :journals, :journal_details, :enabled_modules
+  fixtures :projects, :users, :members, :member_roles, :roles, :issues, :journals, :journal_details, :enabled_modules
   
   def setup
     @controller = JournalsController.new
index 6c5af23..e99f99a 100644 (file)
@@ -22,7 +22,7 @@ require 'mail_handler_controller'
 class MailHandlerController; def rescue_action(e) raise e end; end
 
 class MailHandlerControllerTest < Test::Unit::TestCase
-  fixtures :users, :projects, :enabled_modules, :roles, :members, :issues, :issue_statuses, :trackers, :enumerations
+  fixtures :users, :projects, :enabled_modules, :roles, :members, :member_roles, :issues, :issue_statuses, :trackers, :enumerations
   
   FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
   
index efd28a4..2680c39 100644 (file)
@@ -23,7 +23,7 @@ class MembersController; def rescue_action(e) raise e end; end
 
 
 class MembersControllerTest < Test::Unit::TestCase
-  fixtures :projects, :members, :roles, :users
+  fixtures :projects, :members, :member_roles, :roles, :users
   
   def setup
     @controller = MembersController.new
@@ -42,7 +42,7 @@ class MembersControllerTest < Test::Unit::TestCase
   
   def test_create
     assert_difference 'Member.count' do
-      post :new, :id => 1, :member => {:role_id => 1, :user_id => 7}
+      post :new, :id => 1, :member => {:role_ids => [1], :user_id => 7}
     end
     assert_redirected_to '/projects/ecookbook/settings/members'
     assert User.find(7).member_of?(Project.find(1))
@@ -50,7 +50,7 @@ class MembersControllerTest < Test::Unit::TestCase
   
   def test_create_by_user_login
     assert_difference 'Member.count' do
-      post :new, :id => 1, :member => {:role_id => 1, :user_login => 'someone'}
+      post :new, :id => 1, :member => {:role_ids => [1], :user_login => 'someone'}
     end
     assert_redirected_to '/projects/ecookbook/settings/members'
     assert User.find(7).member_of?(Project.find(1))
@@ -58,7 +58,7 @@ class MembersControllerTest < Test::Unit::TestCase
   
   def test_create_multiple
     assert_difference 'Member.count', 3 do
-      post :new, :id => 1, :member => {:role_id => 1, :user_ids => [7, 8, 9]}
+      post :new, :id => 1, :member => {:role_ids => [1], :user_ids => [7, 8, 9]}
     end
     assert_redirected_to '/projects/ecookbook/settings/members'
     assert User.find(7).member_of?(Project.find(1))
@@ -66,7 +66,7 @@ class MembersControllerTest < Test::Unit::TestCase
   
   def test_edit
     assert_no_difference 'Member.count' do
-      post :edit, :id => 2, :member => {:role_id => 1, :user_id => 3}
+      post :edit, :id => 2, :member => {:role_ids => [1], :user_id => 3}
     end
     assert_redirected_to '/projects/ecookbook/settings/members'
   end
index 94062b4..70061ac 100644 (file)
@@ -22,7 +22,7 @@ require 'messages_controller'
 class MessagesController; def rescue_action(e) raise e end; end
 
 class MessagesControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :members, :roles, :boards, :messages, :enabled_modules
+  fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules
   
   def setup
     @controller = MessagesController.new
index 22ad2d2..a313624 100644 (file)
@@ -22,7 +22,7 @@ require 'news_controller'
 class NewsController; def rescue_action(e) raise e end; end
 
 class NewsControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :roles, :members, :enabled_modules, :news, :comments
+  fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news, :comments
   
   def setup
     @controller = NewsController.new
index 4393ac0..2fba106 100644 (file)
@@ -22,7 +22,7 @@ require 'projects_controller'
 class ProjectsController; def rescue_action(e) raise e end; end
 
 class ProjectsControllerTest < Test::Unit::TestCase
-  fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
+  fixtures :projects, :versions, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
            :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages,
            :attachments
 
index 1951aff..8edde44 100644 (file)
@@ -22,7 +22,7 @@ require 'queries_controller'
 class QueriesController; def rescue_action(e) raise e end; end
 
 class QueriesControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries
+  fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries
   
   def setup
     @controller = QueriesController.new
index acb6c1d..b1787a5 100644 (file)
@@ -22,7 +22,7 @@ require 'repositories_controller'
 class RepositoriesController; def rescue_action(e) raise e end; end
 
 class RepositoriesBazaarControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
+  fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules
 
   # No '..' in the repository path
   REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/bazaar_repository'
index ceb7341..d51a429 100644 (file)
@@ -22,7 +22,7 @@ require 'repositories_controller'
 class RepositoriesController; def rescue_action(e) raise e end; end
 
 class RepositoriesControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :roles, :members, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
+  fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
   
   def setup
     @controller = RepositoriesController.new
index 43c7159..8f1c7df 100644 (file)
@@ -22,7 +22,7 @@ require 'repositories_controller'
 class RepositoriesController; def rescue_action(e) raise e end; end
 
 class RepositoriesDarcsControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
+  fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules
 
   # No '..' in the repository path
   REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/darcs_repository'
index 201a506..7f63ea3 100644 (file)
@@ -22,7 +22,7 @@ require 'repositories_controller'
 class RepositoriesController; def rescue_action(e) raise e end; end
 
 class RepositoriesGitControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
+  fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules
 
   # No '..' in the repository path
   REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
index cb870aa..53cbedd 100644 (file)
@@ -22,7 +22,7 @@ require 'repositories_controller'
 class RepositoriesController; def rescue_action(e) raise e end; end
 
 class RepositoriesMercurialControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
+  fixtures :projects, :users, :roles, :members, :member_roles, :repositories, :enabled_modules
 
   # No '..' in the repository path
   REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository'
index aef8c2e..e31094e 100644 (file)
@@ -22,7 +22,7 @@ require 'repositories_controller'
 class RepositoriesController; def rescue_action(e) raise e end; end
 
 class RepositoriesSubversionControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :roles, :members, :enabled_modules,
+  fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
            :repositories, :issues, :issue_statuses, :changesets, :changes,
            :issue_categories, :enumerations, :custom_fields, :custom_values, :trackers
 
index 5c47be1..61e9bd6 100644 (file)
@@ -22,7 +22,7 @@ require 'roles_controller'
 class RolesController; def rescue_action(e) raise e end; end
 
 class RolesControllerTest < Test::Unit::TestCase
-  fixtures :roles, :users, :members, :workflows
+  fixtures :roles, :users, :members, :member_roles, :workflows
   
   def setup
     @controller = RolesController.new
@@ -65,7 +65,7 @@ class RolesControllerTest < Test::Unit::TestCase
                          :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
                          :assignable => '0'}
     
-    assert_redirected_to 'roles/list'
+    assert_redirected_to 'roles'
     role = Role.find_by_name('RoleWithoutWorkflowCopy')
     assert_not_nil role
     assert_equal [:add_issues, :edit_issues, :log_time], role.permissions
@@ -78,7 +78,7 @@ class RolesControllerTest < Test::Unit::TestCase
                          :assignable => '0'},
                :copy_workflow_from => '1'
     
-    assert_redirected_to 'roles/list'
+    assert_redirected_to 'roles'
     role = Role.find_by_name('RoleWithWorkflowCopy')
     assert_not_nil role
     assert_equal Role.find(1).workflows.size, role.workflows.size
@@ -97,7 +97,7 @@ class RolesControllerTest < Test::Unit::TestCase
                           :permissions => ['edit_project', ''],
                           :assignable => '0'}
     
-    assert_redirected_to 'roles/list'
+    assert_redirected_to 'roles'
     role = Role.find(1)
     assert_equal [:edit_project], role.permissions
   end
@@ -107,7 +107,7 @@ class RolesControllerTest < Test::Unit::TestCase
     assert r.save
     
     post :destroy, :id => r
-    assert_redirected_to 'roles/list'
+    assert_redirected_to 'roles'
     assert_nil Role.find_by_id(r.id)
   end
   
@@ -139,7 +139,7 @@ class RolesControllerTest < Test::Unit::TestCase
   
   def test_post_report
     post :report, :permissions => { '0' => '', '1' => ['edit_issues'], '3' => ['add_issues', 'delete_issues']}
-    assert_redirected_to 'roles/list'
+    assert_redirected_to 'roles'
     
     assert_equal [:edit_issues], Role.find(1).permissions
     assert_equal [:add_issues, :delete_issues], Role.find(3).permissions
@@ -148,33 +148,33 @@ class RolesControllerTest < Test::Unit::TestCase
   
   def test_clear_all_permissions
     post :report, :permissions => { '0' => '' }
-    assert_redirected_to 'roles/list'
+    assert_redirected_to 'roles'
     assert Role.find(1).permissions.empty?
   end
   
   def test_move_highest
     post :edit, :id => 3, :role => {:move_to => 'highest'}
-    assert_redirected_to 'roles/list'
+    assert_redirected_to 'roles'
     assert_equal 1, Role.find(3).position
   end
 
   def test_move_higher
     position = Role.find(3).position
     post :edit, :id => 3, :role => {:move_to => 'higher'}
-    assert_redirected_to 'roles/list'
+    assert_redirected_to 'roles'
     assert_equal position - 1, Role.find(3).position
   end
 
   def test_move_lower
     position = Role.find(2).position
     post :edit, :id => 2, :role => {:move_to => 'lower'}
-    assert_redirected_to 'roles/list'
+    assert_redirected_to 'roles'
     assert_equal position + 1, Role.find(2).position
   end
 
   def test_move_lowest
     post :edit, :id => 2, :role => {:move_to => 'lowest'}
-    assert_redirected_to 'roles/list'
+    assert_redirected_to 'roles'
     assert_equal Role.count, Role.find(2).position
   end
 end
index ec81e41..0f74c53 100644 (file)
@@ -5,7 +5,7 @@ require 'search_controller'
 class SearchController; def rescue_action(e) raise e end; end
 
 class SearchControllerTest < Test::Unit::TestCase
-  fixtures :projects, :enabled_modules, :roles, :users,
+  fixtures :projects, :enabled_modules, :roles, :users, :members, :member_roles,
            :issues, :trackers, :issue_statuses,
            :custom_fields, :custom_values,
            :repositories, :changesets
index a69a70e..df80f5d 100644 (file)
@@ -22,7 +22,7 @@ require 'timelog_controller'
 class TimelogController; def rescue_action(e) raise e end; end
 
 class TimelogControllerTest < Test::Unit::TestCase
-  fixtures :projects, :enabled_modules, :roles, :members, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses, :custom_fields, :custom_values
+  fixtures :projects, :enabled_modules, :roles, :members, :member_roles, :issues, :time_entries, :users, :trackers, :enumerations, :issue_statuses, :custom_fields, :custom_values
 
   def setup
     @controller = TimelogController.new
index d41f882..e9aad1a 100644 (file)
@@ -24,7 +24,7 @@ class UsersController; def rescue_action(e) raise e end; end
 class UsersControllerTest < Test::Unit::TestCase
   include Redmine::I18n
   
-  fixtures :users, :projects, :members
+  fixtures :users, :projects, :members, :member_roles, :roles
   
   def setup
     @controller = UsersController.new
@@ -123,9 +123,9 @@ class UsersControllerTest < Test::Unit::TestCase
   
   def test_edit_membership
     post :edit_membership, :id => 2, :membership_id => 1,
-                           :membership => { :role_id => 2}
+                           :membership => { :role_ids => [2]}
     assert_redirected_to :action => 'edit', :id => '2', :tab => 'memberships'
-    assert_equal 2, Member.find(1).role_id
+    assert_equal [2], Member.find(1).role_ids
   end
   
   def test_edit_with_activation_should_send_a_notification
index 2ddf3a9..3d212de 100644 (file)
@@ -22,7 +22,7 @@ require 'versions_controller'
 class VersionsController; def rescue_action(e) raise e end; end
 
 class VersionsControllerTest < Test::Unit::TestCase
-  fixtures :projects, :versions, :issues, :users, :roles, :members, :enabled_modules
+  fixtures :projects, :versions, :issues, :users, :roles, :members, :member_roles, :enabled_modules
   
   def setup
     @controller = VersionsController.new
index cd65394..275599f 100644 (file)
@@ -22,7 +22,7 @@ require 'watchers_controller'
 class WatchersController; def rescue_action(e) raise e end; end
 
 class WatchersControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :roles, :members, :enabled_modules,
+  fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules,
            :issues, :trackers, :projects_trackers, :issue_statuses, :enumerations, :watchers
   
   def setup
index 22d816e..9d104c5 100644 (file)
@@ -22,7 +22,7 @@ require 'wiki_controller'
 class WikiController; def rescue_action(e) raise e end; end
 
 class WikiControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments
+  fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments
   
   def setup
     @controller = WikiController.new
index c558db0..7a5634f 100644 (file)
@@ -22,7 +22,7 @@ require 'wikis_controller'
 class WikisController; def rescue_action(e) raise e end; end
 
 class WikisControllerTest < Test::Unit::TestCase
-  fixtures :projects, :users, :roles, :members, :enabled_modules, :wikis
+  fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis
   
   def setup
     @controller = WikisController.new
index d47694e..0b1773d 100644 (file)
@@ -18,7 +18,7 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class ActivityTest < Test::Unit::TestCase
-  fixtures :projects, :versions, :attachments, :users, :roles, :members, :issues, :journals, :journal_details,
+  fixtures :projects, :versions, :attachments, :users, :roles, :members, :member_roles, :issues, :journals, :journal_details,
            :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages
 
   def setup
index 6a0df2c..78e2339 100644 (file)
@@ -18,7 +18,7 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class ChangesetTest < Test::Unit::TestCase
-  fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :users, :members, :trackers
+  fixtures :projects, :repositories, :issues, :issue_statuses, :changesets, :changes, :issue_categories, :enumerations, :custom_fields, :custom_values, :users, :members, :member_roles, :trackers
 
   def setup
   end
index 5052f7a..d836f2b 100644 (file)
@@ -18,7 +18,7 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class IssueTest < Test::Unit::TestCase
-  fixtures :projects, :users, :members,
+  fixtures :projects, :users, :members, :member_roles,
            :trackers, :projects_trackers,
            :issue_statuses, :issue_categories,
            :enumerations,
@@ -242,6 +242,10 @@ class IssueTest < Test::Unit::TestCase
     assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
   end
   
+  def test_assignable_users
+    assert_kind_of User, Issue.find(1).assignable_users.first
+  end
+  
   def test_create_should_send_email_notification
     ActionMailer::Base.deliveries.clear
     issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.priorities.first, :subject => 'test_create', :estimated_hours => '1:30')
index 704e12c..295b8ae 100644 (file)
@@ -22,6 +22,7 @@ class MailHandlerTest < Test::Unit::TestCase
                    :enabled_modules,
                    :roles,
                    :members,
+                   :member_roles,
                    :issues,
                    :issue_statuses,
                    :workflows,
index d15af6e..b1c39bb 100644 (file)
@@ -19,7 +19,7 @@ require File.dirname(__FILE__) + '/../test_helper'
 
 class MailerTest < Test::Unit::TestCase
   include Redmine::I18n
-  fixtures :projects, :issues, :users, :members, :documents, :attachments, :news, :tokens, :journals, :journal_details, :changesets, :trackers, :issue_statuses, :enumerations, :messages, :boards, :repositories
+  fixtures :projects, :issues, :users, :members, :member_roles, :documents, :attachments, :news, :tokens, :journals, :journal_details, :changesets, :trackers, :issue_statuses, :enumerations, :messages, :boards, :repositories
   
   def test_generated_links_in_emails
     ActionMailer::Base.deliveries.clear
index 0797823..66f1283 100644 (file)
 require File.dirname(__FILE__) + '/../test_helper'
 
 class MemberTest < Test::Unit::TestCase
-  fixtures :users, :projects, :roles, :members
+  fixtures :users, :projects, :roles, :members, :member_roles
 
   def setup
     @jsmith = Member.find(1)
   end
   
   def test_create\r
-    member = Member.new(:project_id => 1, :user_id => 4, :role_id => 1)\r
-    assert member.save\r
+    member = Member.new(:project_id => 1, :user_id => 4, :role_ids => [1, 2])\r
+    assert member.save
+    member.reload
+    
+    assert_equal 2, member.roles.size
+    assert_equal Role.find(1), member.roles.sort.first\r
   end\r
 
   def test_update    
     assert_equal "eCookbook", @jsmith.project.name\r
-    assert_equal "Manager", @jsmith.role.name\r
+    assert_equal "Manager", @jsmith.roles.first.name\r
     assert_equal "jsmith", @jsmith.user.login\r
     \r
-    @jsmith.role = Role.find(2)\r
+    @jsmith.mail_notification = !@jsmith.mail_notification\r
     assert @jsmith.save
   end
+
+  def test_update_roles
+    assert_equal 1, @jsmith.roles.size
+    @jsmith.role_ids = [1, 2]
+    assert @jsmith.save
+    assert_equal 2, @jsmith.reload.roles.size
+  end
   \r
   def test_validate\r
-    member = Member.new(:project_id => 1, :user_id => 2, :role_id =>2)\r
-    # same use can't have more than one role for a project\r
+    member = Member.new(:project_id => 1, :user_id => 2, :role_ids => [2])\r
+    # same use can't have more than one membership for a project\r
+    assert !member.save
+    
+    member = Member.new(:project_id => 1, :user_id => 2, :role_ids => [])
+    # must have one role at least
     assert !member.save\r
   end\r
   \r
-  def test_destroy\r
-    @jsmith.destroy\r
+  def test_destroy
+    assert_difference 'Member.count', -1 do
+      assert_difference 'MemberRole.count', -1 do\r
+        @jsmith.destroy
+      end
+    end
+    \r
     assert_raise(ActiveRecord::RecordNotFound) { Member.find(@jsmith.id) }\r
   end
 end
index bc9bd5f..d88e98b 100644 (file)
@@ -18,7 +18,7 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class MessageTest < Test::Unit::TestCase
-  fixtures :projects, :roles, :members, :boards, :messages, :users, :watchers
+  fixtures :projects, :roles, :members, :member_roles, :boards, :messages, :users, :watchers
 
   def setup
     @board = Board.find(1)
@@ -116,7 +116,7 @@ class MessageTest < Test::Unit::TestCase
     author = message.author
     assert message.editable_by?(author)
     
-    author.role_for_project(message.project).remove_permission!(:edit_own_messages)
+    author.roles_for_project(message.project).first.remove_permission!(:edit_own_messages)
     assert !message.reload.editable_by?(author.reload)
   end
   
@@ -125,7 +125,7 @@ class MessageTest < Test::Unit::TestCase
     author = message.author
     assert message.destroyable_by?(author)
     
-    author.role_for_project(message.project).remove_permission!(:delete_own_messages)
+    author.roles_for_project(message.project).first.remove_permission!(:delete_own_messages)
     assert !message.reload.destroyable_by?(author.reload)
   end
 end
index 3a908dc..a4fc89e 100644 (file)
@@ -18,7 +18,7 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class NewsTest < Test::Unit::TestCase
-  fixtures :projects, :users, :roles, :members, :enabled_modules, :news
+  fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :news
 
   def valid_news
     { :title => 'Test news', :description => 'Lorem ipsum etc', :author => User.find(:first) }
index f9a17e2..4ecd9d4 100644 (file)
@@ -20,8 +20,8 @@ require File.dirname(__FILE__) + '/../test_helper'
 class ProjectTest < Test::Unit::TestCase\r
   fixtures :projects, :enabled_modules, \r
            :issues, :issue_statuses, :journals, :journal_details,\r
-           :users, :members, :roles, :projects_trackers, :trackers, :boards,\r
-           :queries
+           :users, :members, :member_roles, :roles, :projects_trackers, :trackers, :boards,\r
+           :queries\r
 \r
   def setup\r
     @ecookbook = Project.find(1)\r
index 80112b7..e12bac8 100644 (file)
@@ -18,7 +18,7 @@
 require File.dirname(__FILE__) + '/../test_helper'
 
 class QueryTest < Test::Unit::TestCase
-  fixtures :projects, :enabled_modules, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
+  fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
 
   def test_custom_fields_for_all_projects_should_be_available_in_global_queries
     query = Query.new(:project => nil, :name => '_')
index 1b32df7..46d7694 100644 (file)
@@ -19,7 +19,8 @@ require File.dirname(__FILE__) + '/../test_helper'
 
 class SearchTest < Test::Unit::TestCase
   fixtures :users,
-           :members,
+           :members, 
+           :member_roles,
            :projects,
            :roles,
            :enabled_modules,
index f6d2704..7080c0b 100644 (file)
@@ -18,7 +18,7 @@
 require File.dirname(__FILE__) + '/../test_helper'\r
 \r
 class UserTest < Test::Unit::TestCase\r
-  fixtures :users, :members, :projects\r
+  fixtures :users, :members, :projects, :roles, :member_roles\r
 \r
   def setup\r
     @admin = User.find(1)\r
@@ -130,14 +130,14 @@ class UserTest < Test::Unit::TestCase
     assert_equal key, @jsmith.rss_key\r
   end\r
   \r
-  def test_role_for_project\r
+  def test_roles_for_project\r
     # user with a role\r
-    role = @jsmith.role_for_project(Project.find(1))\r
-    assert_kind_of Role, role\r
-    assert_equal "Manager", role.name\r
+    roles = @jsmith.roles_for_project(Project.find(1))\r
+    assert_kind_of Role, roles.first\r
+    assert_equal "Manager", roles.first.name\r
     \r
     # user with no role\r
-    assert !@dlopper.role_for_project(Project.find(2)).member?\r
+    assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}\r
   end\r
   \r
   def test_mail_notification_all\r