Index: test/functional/projects_controller_test.rb =================================================================== --- test/functional/projects_controller_test.rb (revision 1482) +++ test/functional/projects_controller_test.rb (working copy) @@ -23,7 +23,7 @@ class ProjectsControllerTest < Test::Unit::TestCase fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details, - :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages + :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages, :membership_activities def setup @controller = ProjectsController.new @@ -41,13 +41,13 @@ assert assigns(:project_tree).has_key?(Project.find(1)) # Subproject in corresponding value assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3)) - end - - def test_show_by_id - get :show, :id => 1 + end + + def test_show_by_id + get :show, :id => 1 assert_response :success - assert_template 'show' - assert_not_nil assigns(:project) + assert_template 'show' + assert_not_nil assigns(:project) end def test_show_by_identifier @@ -95,7 +95,7 @@ assert_response :success assert_template 'destroy' assert_not_nil Project.find_by_id(1) - end + end def test_post_destroy @request.session[:user_id] = 1 # admin @@ -103,20 +103,20 @@ assert_redirected_to 'admin/projects' assert_nil Project.find_by_id(1) end - - def test_list_files - get :list_files, :id => 1 - assert_response :success - assert_template 'list_files' - assert_not_nil assigns(:versions) - end - - def test_changelog - get :changelog, :id => 1 - assert_response :success - assert_template 'changelog' - assert_not_nil assigns(:versions) + + def test_list_files + get :list_files, :id => 1 + assert_response :success + assert_template 'list_files' + assert_not_nil assigns(:versions) end + + def test_changelog + get :changelog, :id => 1 + assert_response :success + assert_template 'changelog' + assert_not_nil assigns(:versions) + end def test_roadmap get :roadmap, :id => 1 @@ -178,6 +178,20 @@ } end + def test_membership_activity + @request.session[:user_id] = 1 # admin + get :activity, :id => 1, :from => '2006-08-09' + assert_not_nil assigns(:events_by_day) + + ['Dave Lopper', 'Dave2 Lopper2', 'John Smith', 'John Smith'].each_with_index do |name, i| + assert_select "dt:nth-of-type(#{i + 8}) a", name + end + + assert_select "dt.member-destroy a", "Dave2 Lopper2" + assert_select "dt.member-edit a", "Dave Lopper" + + end + def test_activity_with_subprojects get :activity, :id => 1, :with_subprojects => 1 assert_response :success Index: test/functional/users_controller_test.rb =================================================================== --- test/functional/users_controller_test.rb (revision 1482) +++ test/functional/users_controller_test.rb (working copy) @@ -22,7 +22,7 @@ class UsersController; def rescue_action(e) raise e end; end class UsersControllerTest < Test::Unit::TestCase - fixtures :users, :projects, :members + fixtures :users, :projects, :members, :membership_activities def setup @controller = UsersController.new @@ -52,11 +52,13 @@ :membership => { :role_id => 2} assert_redirected_to 'users/edit/2' assert_equal 2, Member.find(1).role_id + assert_equal 'edit', MembershipActivity.find(:first, :order => 'created_on DESC').action end def test_destroy_membership post :destroy_membership, :id => 2, :membership_id => 1 assert_redirected_to 'users/edit/2' assert_nil Member.find_by_id(1) + assert_equal 'destroy', MembershipActivity.find(:first, :order => 'created_on DESC').action end end Index: test/fixtures/membership_activities.yml =================================================================== --- test/fixtures/membership_activities.yml (revision 0) +++ test/fixtures/membership_activities.yml (revision 0) @@ -0,0 +1,65 @@ +--- +membership_activities_001: + id: 1 + project_id: 1 + action: "new" + user_id: 2 + creator_id: 1 + role_id: 1 + created_on: 2006-07-19 19:35:33 +02:00 + +membership_activities_002: + id: 2 + project_id: 1 + action: "new" + user_id: 3 + creator_id: 1 + role_id: 3 + created_on: 2006-07-19 19:35:36 +02:00 + +membership_activities_003: + id: 3 + project_id: 2 + action: "new" + user_id: 2 + creator_id: 1 + role_id: 2 + created_on: 2006-07-19 19:35:36 +02:00 + +membership_activities_004: + id: 4 + project_id: 1 + action: "new" + user_id: 5 + creator_id: 1 + role_id: 2 + created_on: 2006-07-19 19:35:36 +02:00 + +membership_activities_005: + id: 5 + project_id: 5 + action: "new" + user_id: 2 + creator_id: 1 + role_id: 1 + created_on: 2006-07-19 19:35:33 +02:00 + +membership_activities_006: + id: 6 + project_id: 1 + action: "destroy" + user_id: 5 + creator_id: 1 + role_id: 2 + created_on: 2006-07-19 20:35:36 +02:00 + +membership_activities_007: + id: 7 + project_id: 1 + action: "edit" + user_id: 3 + creator_id: 1 + role_id: 2 + created_on: 2006-07-19 20:35:33 +02:00 + + \ No newline at end of file Index: app/models/project.rb =================================================================== --- app/models/project.rb (revision 1482) +++ app/models/project.rb (working copy) @@ -37,6 +37,7 @@ has_one :repository, :dependent => :destroy has_many :changesets, :through => :repository has_one :wiki, :dependent => :destroy + has_many :membership_activities, :dependent => :destroy # Custom field for the project issues has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', @@ -180,6 +181,16 @@ # Deletes all project's members def delete_all_members + members.each do |m| + MembershipActivity.create( + :project_id => id, + :action => 'destroy', + :user_id => m.user_id, + :creator_id => User.current.id, + :role_id => m.role_id + ) + end + Member.delete_all(['project_id = ?', id]) end Index: app/models/membership_activity.rb =================================================================== --- app/models/membership_activity.rb (revision 0) +++ app/models/membership_activity.rb (revision 0) @@ -0,0 +1,29 @@ +# redMine - project management software +# Copyright (C) 2006 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 MembershipActivity < ActiveRecord::Base + belongs_to :project + belongs_to :user + belongs_to :creator, :class_name => 'User', :foreign_key => 'creator_id' + belongs_to :role + + acts_as_event :title => Proc.new {|o| "#{o.user.name}" }, + :description => Proc.new { |o| l(('text_' + o.action + '_member').to_sym, o.role.name) }, + :author => :creator, + :type => Proc.new {|o| "member-#{o.action}" }, + :url => Proc.new {|o| {:controller => 'account', :action => 'show', :id => o.user.id} } +end Index: app/controllers/members_controller.rb =================================================================== --- app/controllers/members_controller.rb (revision 1482) +++ app/controllers/members_controller.rb (working copy) @@ -20,18 +20,23 @@ before_filter :find_member, :except => :new before_filter :find_project, :only => :new before_filter :authorize + after_filter :log_activity def new - @project.members << Member.new(params[:member]) if request.post? + if request.post? + @member = Member.new(params[:member]) + @project.members << @member + end + respond_to do |format| - format.html { redirect_to :action => 'settings', :tab => 'members', :id => @project } + format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project } format.js { render(:update) {|page| page.replace_html "tab-content-members", :partial => 'projects/settings/members'} } end end def edit if request.post? and @member.update_attributes(params[:member]) - respond_to do |format| + 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'} } end @@ -40,7 +45,7 @@ def destroy @member.destroy - respond_to do |format| + 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'} } end @@ -59,4 +64,15 @@ rescue ActiveRecord::RecordNotFound render_404 end -end + + def log_activity + MembershipActivity.create( + :project_id => @member.project_id, + :action => action_name, + :user_id => @member.user_id, + :creator_id => User.current.id, + :role_id => @member.role_id + ) + end + +end \ No newline at end of file Index: app/controllers/users_controller.rb =================================================================== --- app/controllers/users_controller.rb (revision 1482) +++ app/controllers/users_controller.rb (working copy) @@ -18,7 +18,8 @@ class UsersController < ApplicationController layout 'base' before_filter :require_admin - + after_filter :log_activity, :only => [:edit_membership, :destroy_membership] + helper :sort include SortHelper helper :custom_fields @@ -104,7 +105,24 @@ def destroy_membership @user = User.find(params[:id]) - Member.find(params[:membership_id]).destroy if request.post? + if request.post? + @membership = Member.find(params[:membership_id]) + @membership.destroy + end redirect_to :action => 'edit', :id => @user, :tab => 'memberships' end + +private + def log_activity + action = action_name == 'destroy_membership' ? 'destroy' : params[:membership_id] ? 'edit' : 'new' + + MembershipActivity.create( + :project_id => @membership.project_id, + :action => action, + :user_id => @membership.user_id, + :creator_id => User.current.id, + :role_id => @membership.role_id + ) + end + end Index: app/controllers/projects_controller.rb =================================================================== --- app/controllers/projects_controller.rb (revision 1482) +++ app/controllers/projects_controller.rb (working copy) @@ -237,7 +237,7 @@ @date_to ||= Date.today + 1 @date_from = @date_to - @days - @event_types = %w(issues news files documents changesets wiki_pages messages) + @event_types = %w(issues news files documents changesets wiki_pages messages membership_activity) if @project @event_types.delete('wiki_pages') unless @project.wiki @event_types.delete('changesets') unless @project.repository @@ -316,6 +316,12 @@ @events += Message.find(:all, :include => [{:board => :project}, :author], :conditions => cond.conditions) end + if @scope.include?('membership_activity') + cond = ARCondition.new(Project.allowed_to_condition(User.current, :manage_members, :project => @project, :with_subprojects => @with_subprojects)) + cond.add(["#{MembershipActivity.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to]) + @events += MembershipActivity.find(:all, :include => [:project, :user, :creator], :conditions => cond.conditions) + end + @events_by_day = @events.group_by(&:event_date) respond_to do |format| Index: lang/en.yml =================================================================== --- lang/en.yml (revision 1482) +++ lang/en.yml (working copy) @@ -513,6 +513,7 @@ label_chronological_order: In chronological order label_reverse_chronological_order: In reverse chronological order label_planning: Planning +label_membership_activity_plural: Membership activity button_login: Login button_submit: Submit @@ -596,6 +597,9 @@ text_destroy_time_entries: Delete reported hours text_assign_time_entries_to_project: Assign reported hours to the project text_reassign_time_entries: 'Reassign reported hours to this issue:' +text_new_member: "Was added as %s" +text_edit_member: "Changed roles to %s" +text_destroy_member: "Was removed from the project" text_user_wrote: '%s wrote:' default_role_manager: Manager Index: db/migrate/095_create_membership_activities.rb =================================================================== --- db/migrate/095_create_membership_activities.rb (revision 0) +++ db/migrate/095_create_membership_activities.rb (revision 0) @@ -0,0 +1,16 @@ +class CreateMembershipActivities < ActiveRecord::Migration + def self.up + create_table :membership_activities do |t| + t.column :project_id, :integer + t.column :action, :string + t.column :user_id, :integer + t.column :creator_id, :integer + t.column :role_id, :integer + t.column :created_on, :datetime + end + end + + def self.down + drop_table :membership_activities + end +end \ No newline at end of file Index: vendor/plugins/acts_as_event/lib/acts_as_event.rb =================================================================== --- vendor/plugins/acts_as_event/lib/acts_as_event.rb (revision 1482) +++ vendor/plugins/acts_as_event/lib/acts_as_event.rb (working copy) @@ -64,7 +64,13 @@ def event_url(options = {}) option = event_options[:url] - (option.is_a?(Proc) ? option.call(self) : send(option)).merge(options) + if option.is_a?(Proc) + option.call(self) + elsif option.is_a?(Symbol) + send(option) + else + option + end.merge(options) end module ClassMethods Index: lib/redmine.rb =================================================================== --- lib/redmine.rb (revision 1482) +++ lib/redmine.rb (working copy) @@ -21,6 +21,7 @@ map.permission :select_project_modules, {:projects => :modules}, :require => :member map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member + map.permission :view_membership_activity, {} map.project_module :issue_tracking do |map| # Issue categories Index: public/stylesheets/application.css =================================================================== --- public/stylesheets/application.css (revision 1482) +++ public/stylesheets/application.css (working copy) @@ -194,7 +194,11 @@ dt.attachment { background-image: url(../images/attachment.png); } dt.document { background-image: url(../images/document.png); } dt.project { background-image: url(../images/projects.png); } +dt.member-new { background-image: url(../images/user_new.png); } +dt.member-edit { background-image: url(../images/user.png); } +dt.member-destroy { background-image: url(../images/false.png); } + div#roadmap fieldset.related-issues { margin-bottom: 1em; } div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; } div#roadmap .wiki h1:first-child { display: none; }